Compare commits
54 Commits
2ccb16036e
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| bc98a3e0d0 | |||
| 67776b5972 | |||
| 26c06994cf | |||
| 52160a6c7f | |||
| ff28b7b47c | |||
| 78fcd49a54 | |||
| cefdc0f73b | |||
| f8f12f963d | |||
| d72d17c2c3 | |||
| 5ee2b2e972 | |||
| 6b00a8f0f3 | |||
| 98c5efcef0 | |||
| 2cb192ebcf | |||
| 3bce356abc | |||
| 73b52f534e | |||
| ee3d18be33 | |||
| ced4dcee5d | |||
| d1537ae259 | |||
| b1d697e320 | |||
| 026a92cccb | |||
| 205b750e74 | |||
| a97a778f7a | |||
| 0cbeb37344 | |||
| 28a1861e9e | |||
| bccd6135e5 | |||
| 1bb7bba61c | |||
| 7874292a7e | |||
| 1d25107295 | |||
| 04628d2697 | |||
| 8caed117be | |||
| ba035c2888 | |||
| 8ad501766e | |||
| 31efdd6681 | |||
| 15e84fb7c9 | |||
| 93b9f92f91 | |||
| d4f1a800aa | |||
| b2b37a1105 | |||
| f64dc50fdd | |||
| 91f33de5a1 | |||
| b53810e77b | |||
| ff066aacb5 | |||
| 656b9344b1 | |||
| d5860c95c9 | |||
| ccd952b0d0 | |||
| 3358274ee2 | |||
| c6c6066797 | |||
| ed8b5c1d0c | |||
| f3addd13c5 | |||
| a41ceebe8a | |||
| edcbb1b7d5 | |||
| 5581e2257f | |||
| 1cf5089353 | |||
| 5be510704a | |||
| 3f63cc9f76 |
@@ -0,0 +1,11 @@
|
|||||||
|
# 작업 스킬 작성
|
||||||
|
|
||||||
|
이 폴더는 전체 진행과 14단계 SKILL.md를 관리한다. 역할 TOML·공통 스키마·빈 양식·사용자 원본 이미지는 이 영역의 작성 대상이 아니다.
|
||||||
|
|
||||||
|
폴더 이름과 YAML name은 같아야 한다. description은 실제 작업과 적용 상황을 구분하고, 본문은 입력·선행·출력·완료·보류 조건을 정한다. 공통 구조를 새로 정의하지 않고 필요한 전달 양식과 담당 구성 기준만 연결한다.
|
||||||
|
|
||||||
|
개별 호출도 topic/story/scenes/final의 사용자 검토를 우회하지 않는다. 담당은 자기 새 버전과 검토를 제출하고 공유 진행·승인은 총괄만 바꾼다. 실제 파일이 없는 외부 결과를 완료로 처리하지 않는다.
|
||||||
|
|
||||||
|
계약에 없는 필드·입력·승인 지점을 추가하지 않는다. 새 제작은 1.1 Blender 계약, 기존 1.0은 보존·참조한다. scenes 승인 전 제한된 08 대표 샘플만 허용하고 정식 10–12는 승인 후 진행한다. 사례의 반전·위기·시간 배분은 모든 소재의 의무가 아니다. 실제 사용한 구성과 비적용 이유는 기존 review.md에 남긴다.
|
||||||
|
|
||||||
|
변경한 스킬의 머리말·이름·미완성 내용과 실제 상대 링크를 확인하고, 선행 결과·승인 부재·외부 파일 부재가 올바르게 보고되는지 검토한다. 불필요한 지원 파일·실행 프로그램·검증 프레임워크를 추가하지 않는다.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: short-video-compose
|
||||||
|
description: 승인된 대본·제목과 실제 컷 영상·통합 음성·자막을 연결해 13-compose 합성 요청을 작성하고 받은 MP4·SRT를 확인한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 합성 요청과 실제 완성본 확인
|
||||||
|
|
||||||
|
## 시작 조건
|
||||||
|
|
||||||
|
[공통 계약](../../../docs/workflow-contracts.md)의 저장·전달, 실제 파일과 시간, 승인·수정 규칙과 [제작 지침](../../../docs/short-video-guide.md)의 자막·주석, 모바일 판독성, 제목과 도입의 약속 절을 읽는다. [13 양식](../../../templates/stages/13-compose.json)과 [결과 스키마](../../../contracts/artifact.schema.json)의 `13-compose` 정의를 사용하며 새 필드를 추가하지 않는다.
|
||||||
|
|
||||||
|
개별 호출에서도 같은 제작물의 유효한 `03-script`, `04-title`, `06-tts`, `07-cuts`, `12-motion`과 그 선행 관계를 확인한다. 실제 사용한 다섯 결과의 `{stage_id, revision}`을 `inputs`에 기록한다. project.json의 story·scenes가 실제 사용자 승인이며 사용 버전과 연결되는지 확인한다. pending·changes_requested·stale 승인을 우회하지 않고 같은 승인 버전은 다시 묻지 않는다. 누락·미확인 파일·길이 불일치가 있으면 문제 단계·컷을 총괄에 보고하고 요청 준비 완료로 처리하지 않는다.
|
||||||
|
|
||||||
|
Qwen3-TTS 통합 WAV와 실측 구간, 승인된 Blender Scene에서 렌더링한 실제 컷 영상·교육용 3D 기준을 유지한다. 기존 1.0 결과도 유효한 입력이면 보존·참조한다. 승인된 기존 Remotion + FFmpeg 경로로 합성하고 1.1의 기존 13 구조와 실제 결과를 확인한다. 실행 프로그램 준비·파일 존재와 실제 재생·청취 성공을 구분한다. 원본 references/1.png를 보존하며 도구를 임의로 바꾸지 않는다.
|
||||||
|
|
||||||
|
## 요청 연결
|
||||||
|
|
||||||
|
기존 `payload.request`의 다음 필드를 채운다.
|
||||||
|
|
||||||
|
- `timeline`: 07 컷 순서대로 `{cut_id, video_path, start_ms, end_ms}`를 정확히 한 번씩 넣는다. video_path는 12 결과에서 같은 cut_id의 검증된 MP4 상대 경로이며 시간은 07의 통합 음성 구간이다.
|
||||||
|
- `narration_path`: 06 `result.narration.path`의 실제 검증된 통합 WAV. 단위 파일을 임의 순서로 재조합하지 않는다.
|
||||||
|
- `captions`: 07의 별도 자막 `{id, start_ms, end_ms, text}`를 의미·순서·실측 시간 그대로 연결한다. 자막은 여러 컷을 걸칠 수 있으며 컷마다 억지로 나누지 않는다.
|
||||||
|
- `title`: 04 selected_id에 해당하는 후보의 실제 text. 03의 도입과 끝에서 설명하는 약속에 맞고 입력 언어인지 확인한다.
|
||||||
|
|
||||||
|
timeline은 0부터 통합 음성 끝까지 빈 구간·겹침 없이 이어져야 한다. 각 구간은 양수·5000ms 이하이고 12 실제 클립 길이와 같아야 한다. 자막은 `0 <= start_ms < end_ms <= 통합 길이`이며 원문·발화와 맞아야 한다. 실제 길이가 요청과 다르면 수정할 `12-motion` 또는 `07-cuts`와 cut_id를 보고한다. 180000ms를 넘는 음성은 대본 수정·story 재승인으로 돌아가며 속도를 바꿔 감추지 않는다.
|
||||||
|
|
||||||
|
자막은 말의 내용을, 도해 주석은 물체·수치·방향을 전달하도록 배치 의도를 review.md에 적는다. 근거가 확인된 주석만 사용하며 새 JSON 필드를 만들지 않는다. 모바일에서 글자 크기·대비·줄바꿈·표시 시간과 중요한 움직임의 가림을 확인할 수 있게 전달한다. 플랫폼 공통 안전영역 수치를 임의로 고정하지 않는다.
|
||||||
|
|
||||||
|
## 받은 결과 확인
|
||||||
|
|
||||||
|
요청만 작성되면 `payload.result=null`, awaiting_input으로 보고한다. 실제 결과는 기존 `payload.result.video`와 `captions_path`에 기록한다. MP4 또는 SRT만 받은 부분 결과는 보존하되 complete로 보고하지 않는다.
|
||||||
|
|
||||||
|
video는 `{path, kind, verified, duration_ms, width, height}`이며 kind=video, 경로는 저장소 루트 기준 상대 경로다. 실제 MP4 존재·형식·재생 내용·길이·화면 크기를 확인한 뒤에만 verified=true와 실측값을 기록한다. 파일 미확인은 verified=false, 확인하지 못한 메타데이터는 null이다. captions_path의 실제 SRT 파일도 열어 입력 언어·텍스트·순서·시간을 확인한다. SRT 검증용 새 media 필드를 만들지 않는다.
|
||||||
|
|
||||||
|
실제 영상의 width:height가 9:16이고 전체 길이가 양수·180000ms 이하인지 확인한다. 실제 컷 순서와 경계, 통합 음성의 시작·끝·누락·중복, SRT 표시 구간이 요청과 일치하는지 재생·청취한다. 파일이나 요청 길이만으로 재생·청취를 확인했다고 기록하지 않는다. 모바일 크기로 제목·자막·주석의 판독성과 주요 설명의 가림을 확인한다. 불일치는 실제 구간·수정 단계·cut_id로 보고하며 완성으로 통과시키지 않는다.
|
||||||
|
|
||||||
|
## 저장과 final 검토 전달
|
||||||
|
|
||||||
|
담당은 `projects/<project_id>/stages/13-compose/r<revision>/artifact.json`과 같은 버전 `review.md`만 작성하고 이전 버전을 보존한다. [검토 양식](../../../templates/review.md)에 연결한 선행 버전, 실제 MP4·SRT 확인 방법과 결과, 자막·주석·모바일 원칙의 적용 및 비적용 이유, 문제 단계·컷을 적는다.
|
||||||
|
|
||||||
|
총괄만 project.json의 상태·포인터·승인을 작성한다. 담당은 실제 완성본과 보고서를 14 검수에 전달하고 수정으로 영향을 받는 후속 결과·승인을 보고한다. 13 실제 완성본과 14 검수 보고는 사용자 final 검토 대상이며, 검수 준비나 통과 추천만으로 사용자 final 승인 전 제작 완료를 선언하지 않는다.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: short-video-cuts
|
||||||
|
description: 실제 통합 내레이션에 맞춰 5초 이내 컷과 별도 자막 구간을 설계하고 07-cuts 결과와 scenes 검토 근거를 작성한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 실제 음성 기준 컷과 자막
|
||||||
|
|
||||||
|
## 먼저 읽을 자료
|
||||||
|
|
||||||
|
- [공통 제작 계약](../../../docs/workflow-contracts.md): 실제 파일·시간 확인, scenes 승인과 상태.
|
||||||
|
- [음성과 연결 지침](../../../docs/short-video-guide.md#음성을-기준으로-컷과-연결을-확정한다), [화면 역할 지침](../../../docs/short-video-guide.md#설명의-단위를-화면으로-바꾼다), [자막 지침](../../../docs/short-video-guide.md#자막과-주석을-읽을-수-있게-배치한다).
|
||||||
|
- [07 양식](../../../templates/stages/07-cuts.json), [결과 스키마](../../../contracts/artifact.schema.json), [검토 양식](../../../templates/review.md).
|
||||||
|
|
||||||
|
## 입력과 승인 확인
|
||||||
|
|
||||||
|
개별 호출에서도 같은 제작물의 유효한 05-tts-split, 검증된 06-tts 결과·실제 통합 WAV와 그 근거인 story 승인을 확인한다. `inputs`에 실제 사용한 05와 06 버전을 기록한다. 결과·파일 부재는 awaiting_input으로 총괄에 보고하고 기다린다. 오래된 버전이나 미확인 부분 파일로 실측 컷을 확정하지 않는다.
|
||||||
|
|
||||||
|
## 작성과 확인
|
||||||
|
|
||||||
|
1. 실제 통합 음성의 측정 길이를 `narration_duration_ms`에 기록한다. 03의 예상 길이·단위 WAV 길이·06 통합 구간은 서로 다른 값이다. 실제 음성이 180000ms를 넘으면 대본 수정과 story 재승인으로 돌려보낸다.
|
||||||
|
2. `timing_evidence`에 통합 파일 측정 방법, 청취 구간과 발화·자막 정렬 확인 방법을 적는다. 글자 수로 예상한 정렬을 실측이라고 표시하지 않는다.
|
||||||
|
3. cuts를 음성 순서대로 작성한다. 첫 start_ms=0, 마지막 end_ms=통합 길이이며 인접 컷의 끝과 시작이 같다. 빈 구간·중복 없이 전체 음성을 덮고 각 컷은 `0 < end_ms-start_ms <= 5000`을 만족해야 한다.
|
||||||
|
4. 컷 ID는 유일하며 unit_ids는 05·06의 실제 단위를 참조한다. narration_text는 해당 실제 발화를 순서대로 보존한다. 한 문장이 여러 화면 컷에 걸칠 수 있으나 단어·의미를 누락하거나 재배치하지 않는다. TTS 단위·문장·이야기 블록과 컷의 일대일 대응을 강제하지 않는다.
|
||||||
|
5. captions는 컷과 별도 구간으로 작성하며 여러 컷에 걸칠 수 있다. 각 자막은 `0 <= start_ms < end_ms <= 통합 길이`이고 입력 언어·텍스트 의미·표시 순서를 보존한다. 실제 음성과 읽기 단위에 맞춰 정렬하며 자동 인식 오류를 확정하지 않는다.
|
||||||
|
|
||||||
|
필드·타입은 양식·스키마를 그대로 따른다. 화면에서 설명할 관계와 필요한 상태 변화는 review.md에 적어 08로 전달하고 새 필드를 추가하지 않는다.
|
||||||
|
|
||||||
|
## 제출과 검토 경계
|
||||||
|
|
||||||
|
자기 단계의 새 버전을 `projects/<project_id>/stages/07-cuts/r<revision>/artifact.json`과 같은 폴더의 `review.md`에 저장한다. 데이터 경로는 저장소 루트 기준이며 이전 버전을 덮어쓰지 않는다. review.md에 실측·청취 근거, 컷별 이해할 내용, 적용한 실제 시간·화면 역할·상태 변화 원칙과 비적용 이유를 적는다. 추가 입력·승인 지점을 만들지 않는다.
|
||||||
|
|
||||||
|
총괄에 제출 경로·선행 버전·검증 결과·issues를 보고한다. 부재는 awaiting_input, 검증 실패는 failed, scenes 결정 대기는 awaiting_review로 보고한다. 총괄만 `project.json`의 상태·포인터·승인을 작성한다. 준비된 같은 검토 묶음에서 07→08→09를 작성할 수 있지만 07·08·09의 실제 결과에 대한 사용자 scenes 승인 전에는 10으로 진행하지 않는다. 응답 없음이나 담당의 완료 추천은 승인이 아니다.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: short-video-entities
|
||||||
|
description: 짧은 영상 장면의 인물·배경과 반복 고정 특징을 정리하고 컷별 등장 참조를 09-entities 결과와 scenes 검토에 기록한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 인물·배경과 고정 특징
|
||||||
|
|
||||||
|
## 먼저 읽을 자료
|
||||||
|
|
||||||
|
- [공통 제작 계약](../../../docs/workflow-contracts.md): 09 참조·완료 기준과 scenes 승인 경계.
|
||||||
|
- [화면 역할 지침](../../../docs/short-video-guide.md#설명의-단위를-화면으로-바꾼다), [음성과 연결 지침](../../../docs/short-video-guide.md#음성을-기준으로-컷과-연결을-확정한다), [그림체와 고정 특징 지침](../../../docs/short-video-guide.md#깔끔한-그림체로-작동-방식을-보여준다).
|
||||||
|
- [09 양식](../../../templates/stages/09-entities.json), [결과 스키마](../../../contracts/artifact.schema.json), [검토 양식](../../../templates/review.md).
|
||||||
|
|
||||||
|
## 입력과 승인 확인
|
||||||
|
|
||||||
|
개별 호출에서도 같은 제작물의 유효한 03-script와 08-scenes, 그 근거인 실측 07 및 story 승인을 확인한다. `inputs`에는 실제 사용한 03과 08 버전을 기록한다. 선행 결과·실측 근거·승인 부재나 오래된 버전은 총괄에 issues로 보고하고 기다린다. 준비된 동일 scenes 검토 묶음 안에서는 아직 scenes 승인을 받지 않은 08로 09를 작성할 수 있다.
|
||||||
|
|
||||||
|
## 작성과 확인
|
||||||
|
|
||||||
|
1. 08의 설명과 시작·끝 상태에 필요한 characters와 backgrounds를 정리한다. 각 entity의 id는 유일하며 description과 fixed_features는 장면 간 식별과 설명의 일관성에 필요한 특징만 담는다. 없는 인물·배경을 분량 채우기 위해 추가하지 않는다.
|
||||||
|
2. 고정 특징은 반복되는 형태·색·구조·배치의 기준이다. 행동이나 상태 변화 자체를 고정해 08의 설명을 막지 않는다. 실제 구조·재료의 차이를 보존하고 확인되지 않은 특징은 문제로 보고한다.
|
||||||
|
3. 08의 컷 순서대로 appearances를 작성한다. 모든 cut_id를 정확히 한 번 포함하고 character_ids·background_ids는 해당 목록에 존재해야 한다. 같은 등장 배열 안에서도 ID를 중복하지 않는다. 등장하지 않는 종류는 빈 배열로 기록한다.
|
||||||
|
4. 모든 컷을 08의 실제 등장 상태와 대조한다. 인접 continuous 연결에서 동일 대상의 고정 특징·위치·방향이 유지되고 필요한 상태 변화가 설명되는지 확인한다. 컷 변경을 새 인물·배경으로 잘못 분리하지 않는다.
|
||||||
|
|
||||||
|
정확한 필드·타입은 양식·스키마를 그대로 따른다. 기존 description·fixed_features·appearances에 담고 새 필드를 추가하지 않는다. 03의 이야기 블록과 07의 컷은 일대일이 아니다. `references/1.png`의 고정 그림체를 참고하되 캠핑 소재·인물·상표를 새 배경에 강제하지 않는다.
|
||||||
|
|
||||||
|
## 제출과 검토 경계
|
||||||
|
|
||||||
|
자기 단계의 새 버전을 `projects/<project_id>/stages/09-entities/r<revision>/artifact.json`과 같은 폴더의 `review.md`에 저장한다. 이전 버전을 덮어쓰지 않고 데이터 경로는 저장소 루트 기준으로 쓴다. review.md에 컷별 인물·배경 참조, 상태 변화와 연속성을 지키는 고정 특징, 적용한 화면 역할·연결 원칙의 실제 결과와 비적용 이유를 남긴다. 추가 입력·승인 지점을 만들지 않는다.
|
||||||
|
|
||||||
|
총괄에 제출 경로·선행 버전·검증 결과·issues를 보고한다. 부재는 awaiting_input, 검증 실패는 failed, scenes 결정 대기는 awaiting_review로 보고한다. `project.json`의 상태·포인터·승인은 총괄만 작성한다. 07·08·09의 실제 결과를 함께 검토하고 사용자 scenes 승인 전에는 10으로 진행하지 않는다. 응답 없음이나 담당의 완료 추천은 승인이 아니다.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: short-video-images
|
||||||
|
description: 승인된 컷의 Blender Scene·시작/끝 상태와 논리 프레임을 작성하고 11의 실제 .blend·PNG·연속성을 검증한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Blender 컷 끝점 렌더
|
||||||
|
|
||||||
|
## 입력과 경계
|
||||||
|
|
||||||
|
[공통 계약](../../../docs/workflow-contracts.md), [전달 형식](../../../docs/contracts.md), [제작 지침](../../../docs/short-video-guide.md), [결과 스키마](../../../contracts/artifact.schema.json), [검토 양식](../../../templates/review.md)을 읽는다. 새 결과는 1.1이고 기존 1.0 결과를 보존·참조한다. 필수 입력 topic/language, Qwen3-TTS 기존 qwen3-tts 환경, 세로 9:16·최종 180000ms 이하·실측 컷 5000ms 이하를 유지한다.
|
||||||
|
|
||||||
|
같은 제작물의 유효한 선행 버전·실제 파일과 topic/story/scenes 승인을 확인하고 사용 버전을 inputs에 기록한다. 필요한 결과·승인이 없거나 stale이면 누락과 돌아갈 단계를 총괄에 보고한다. 같은 유효 승인 버전은 다시 묻지 않는다. 08의 제한된 대표 화면·짧은 움직임만 scenes 승인 전 검토용으로 허용하며 정식 10–12 전체 제작·완료와 구분한다. 새 08·09 승인 후 정식 제작한다.
|
||||||
|
|
||||||
|
원본 references/1.png를 보존하고 단순 형태·정돈된 색을 교육용 3D로 해석한다. 단순 배경·부드러운 조명·선명한 형태를 사용하며 평면 복제·캠핑 소재·상표를 강제하지 않는다. 02의 조사와 대본 근거로 모델·재질·방향·상태·수치를 확인하고 사실에 없는 구조를 만들지 않는다. 설명 텍스트는 검증한 합성 텍스트로 배치한다.
|
||||||
|
|
||||||
|
## 컷별 Scene·끝점 요청
|
||||||
|
|
||||||
|
[11 양식](../../../templates/stages/11-images.json)을 사용한다. 유효한 02/03/07/08/09/10과 10의 실제 기준 .blend·PNG를 확인한다. request={engine:"Blender",scene_path,render_profile,frames}. scene_path는 만들 원본의 루트 상대 경로다.
|
||||||
|
|
||||||
|
frame={id,cut_id,position,prompt,reference_ids,scene_name,frame_index}. 모든 실제 07 컷마다 start/end가 정확히 하나씩 있고 ID가 유일해야 한다. reference_ids는 10의 검증된 요청/결과 ID, scene_name은 비어 있지 않은 실제 Scene 이름, frame_index는 0 이상 논리 프레임이다. 같은 컷 start/end는 같은 Scene을 사용한다. 조사·대본·08 상태와 카메라·09 고정 특징을 prompt에 연결한다.
|
||||||
|
|
||||||
|
1.1 continuous는 동일 Scene의 앞 end와 뒤 start 상태를 맞추고 동일한 실제 PNG 파일을 반드시 공유한다. 위치별 별도 frame ID는 유지한다. 기존 1.0의 선택적 공유 규칙을 소급 변경하지 않는다. cut은 서로 다른 Scene의 같은 프레임 번호를 구별하며 캐시 식별에 Scene 이름을 포함한다. 연결을 임의로 바꾸지 않는다. Blender 실제 번호=논리 프레임+1로 평가한다. 영상은 [start,end) 구간이고 끝점 PNG는 end 자체를 평가한다.
|
||||||
|
|
||||||
|
result는 null 또는 {scene_file,frames:[{frame_id,media:image}]}다. 요청마다 실제 PNG가 누락·중복·추가 없이 대응하며 실제 .blend의 Scene·프레임·상태·재질·연결을 확인한다. 원본과 RenderProfile은 후속 12의 기준이다.
|
||||||
|
|
||||||
|
## 실제 결과와 전달
|
||||||
|
|
||||||
|
승인된 저장소 내 Blender 실행 경로를 사용한다. 기존 설치를 무단 변경하거나 다른 도구로 바꾸지 않는다. 설치·요청 준비와 실제 렌더링 성공을 구분한다. .blend는 BlendFile={path,verified,sha256,blender_version}로 기록하고 실제 파일·해시·버전·재열기·Scene/자산을 검증한다. PNG/MP4 media로 위장하지 않는다. RenderProfile의 실제 지원을 확인하며 요청값을 실측 결과로 복사하지 않는다.
|
||||||
|
|
||||||
|
media는 기존 여섯 키 {path,kind,verified,duration_ms,width,height}다. PNG는 image·duration_ms=null, MP4는 video·실측 양수 길이이며 실제 형식·해상도·9:16·내용을 확인한다. 확인한 파일만 verified=true이고 미확인 크기·시간은 null이다. 데이터 경로는 루트 상대 경로다. 실제 파일이 없으면 result=null·awaiting_input, 실행/검증 실패는 failed로 보고한다. 부분 결과와 실패 시도를 보존하며 전체 요청 결과가 검증되기 전 complete로 보고하지 않는다.
|
||||||
|
|
||||||
|
자기 단계의 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 버전 review.md만 작성하고 이전 결과를 덮어쓰지 않는다. 실제 사용 버전·파일별 검사·명령·종료코드·미확인 영역·적용 원칙과 비적용 이유를 적는다. project.json·상태·현재 포인터·사용자 승인은 총괄만 갱신한다. 필드·열거값은 계약을 따르고 새 필수 입력·승인 지점을 만들지 않는다.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: short-video-motion
|
||||||
|
description: 승인된 실제 Blender 원본·Scene·끝점·실측 컷으로 12 렌더 요청을 작성하고 실제 MP4의 길이·상태·연결을 확인한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Blender 컷 영상 렌더
|
||||||
|
|
||||||
|
## 입력과 경계
|
||||||
|
|
||||||
|
[공통 계약](../../../docs/workflow-contracts.md), [전달 형식](../../../docs/contracts.md), [제작 지침](../../../docs/short-video-guide.md), [결과 스키마](../../../contracts/artifact.schema.json), [검토 양식](../../../templates/review.md)을 읽는다. 새 결과는 1.1이고 기존 1.0 결과를 보존·참조한다. 필수 입력 topic/language, Qwen3-TTS 기존 qwen3-tts 환경, 세로 9:16·최종 180000ms 이하·실측 컷 5000ms 이하를 유지한다.
|
||||||
|
|
||||||
|
같은 제작물의 유효한 선행 버전·실제 파일과 topic/story/scenes 승인을 확인하고 사용 버전을 inputs에 기록한다. 필요한 결과·승인이 없거나 stale이면 누락과 돌아갈 단계를 총괄에 보고한다. 같은 유효 승인 버전은 다시 묻지 않는다. 08의 제한된 대표 화면·짧은 움직임만 scenes 승인 전 검토용으로 허용하며 정식 10–12 전체 제작·완료와 구분한다. 새 08·09 승인 후 정식 제작한다.
|
||||||
|
|
||||||
|
원본 references/1.png를 보존하고 단순 형태·정돈된 색을 교육용 3D로 해석한다. 단순 배경·부드러운 조명·선명한 형태를 사용하며 평면 복제·캠핑 소재·상표를 강제하지 않는다. 02의 조사와 대본 근거로 모델·재질·방향·상태·수치를 확인하고 사실에 없는 구조를 만들지 않는다. 설명 텍스트는 검증한 합성 텍스트로 배치한다.
|
||||||
|
|
||||||
|
## 원본과 프레임 범위
|
||||||
|
|
||||||
|
[12 양식](../../../templates/stages/12-motion.json)을 사용한다. 유효한 07/08/11, 실제 Qwen 통합 WAV의 실측 컷, 11의 검증된 .blend·시작/끝 PNG를 확인한다. request={engine:"Blender",scene_path,render_profile,clips}. scene_path는 11의 실제 원본 경로, render_profile은 11과 동일하다.
|
||||||
|
|
||||||
|
clip={cut_id,start_frame_id,end_frame_id,duration_ms,motion_prompt,scene_name,start_frame,end_frame}. 07 순서의 모든 컷을 정확히 한 번 포함한다. 끝점 ID는 11의 같은 컷·위치이며 Scene 이름·논리 프레임도 일치해야 한다. start_frame/end_frame은 0 이상이며 end_frame>start_frame이다. duration_ms는 실측 end_ms-start_ms이고 0 초과 5000 이하이며 프레임 수×1000/fps와 정확히 같아야 한다. 미작성 null은 실행 준비가 아니다.
|
||||||
|
|
||||||
|
원인과 결과를 보여주는 실제 물체·상태 변화와 카메라 동작을 motion_prompt에 적고 줌·흔들림으로 설명을 대신하지 않는다. [start_frame,end_frame) 프레임을 렌더링하고 논리 번호+1을 Blender에 전달한다. continuous 경계의 실제 상태를 맞추고 cut의 서로 다른 Scene·캐시를 구별한다. PNG end 자체와 MP4의 마지막 프레임(end-1) 검사를 구분한다.
|
||||||
|
|
||||||
|
result는 기존 null 또는 {clips:[{cut_id,media:video}]}다. 모든 요청 컷의 실제 MP4·길이·화면·동작·연속성을 검증한다. 요청과 실측이 다르면 12/11/08/07 중 문제 단계와 컷을 보고하고 음성·시간·속도를 임의 변경하지 않는다. 모든 파일이 확인되어야 13 합성에 전달한다.
|
||||||
|
|
||||||
|
## 실제 결과와 전달
|
||||||
|
|
||||||
|
승인된 저장소 내 Blender 실행 경로를 사용한다. 기존 설치를 무단 변경하거나 다른 도구로 바꾸지 않는다. 설치·요청 준비와 실제 렌더링 성공을 구분한다. .blend는 BlendFile={path,verified,sha256,blender_version}로 기록하고 실제 파일·해시·버전·재열기·Scene/자산을 검증한다. PNG/MP4 media로 위장하지 않는다. RenderProfile의 실제 지원을 확인하며 요청값을 실측 결과로 복사하지 않는다.
|
||||||
|
|
||||||
|
media는 기존 여섯 키 {path,kind,verified,duration_ms,width,height}다. PNG는 image·duration_ms=null, MP4는 video·실측 양수 길이이며 실제 형식·해상도·9:16·내용을 확인한다. 확인한 파일만 verified=true이고 미확인 크기·시간은 null이다. 데이터 경로는 루트 상대 경로다. 실제 파일이 없으면 result=null·awaiting_input, 실행/검증 실패는 failed로 보고한다. 부분 결과와 실패 시도를 보존하며 전체 요청 결과가 검증되기 전 complete로 보고하지 않는다.
|
||||||
|
|
||||||
|
자기 단계의 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 버전 review.md만 작성하고 이전 결과를 덮어쓰지 않는다. 실제 사용 버전·파일별 검사·명령·종료코드·미확인 영역·적용 원칙과 비적용 이유를 적는다. project.json·상태·현재 포인터·사용자 승인은 총괄만 갱신한다. 필드·열거값은 계약을 따르고 새 필수 입력·승인 지점을 만들지 않는다.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: short-video-references
|
||||||
|
description: 승인된 장면과 반복 요소로 Blender 기준 모델·렌더 요청을 작성하고 10의 실제 .blend·PNG 자산과 고정 특징을 확인한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Blender 기준 모델과 이미지
|
||||||
|
|
||||||
|
## 입력과 경계
|
||||||
|
|
||||||
|
[공통 계약](../../../docs/workflow-contracts.md), [전달 형식](../../../docs/contracts.md), [제작 지침](../../../docs/short-video-guide.md), [결과 스키마](../../../contracts/artifact.schema.json), [검토 양식](../../../templates/review.md)을 읽는다. 새 결과는 1.1이고 기존 1.0 결과를 보존·참조한다. 필수 입력 topic/language, Qwen3-TTS 기존 qwen3-tts 환경, 세로 9:16·최종 180000ms 이하·실측 컷 5000ms 이하를 유지한다.
|
||||||
|
|
||||||
|
같은 제작물의 유효한 선행 버전·실제 파일과 topic/story/scenes 승인을 확인하고 사용 버전을 inputs에 기록한다. 필요한 결과·승인이 없거나 stale이면 누락과 돌아갈 단계를 총괄에 보고한다. 같은 유효 승인 버전은 다시 묻지 않는다. 08의 제한된 대표 화면·짧은 움직임만 scenes 승인 전 검토용으로 허용하며 정식 10–12 전체 제작·완료와 구분한다. 새 08·09 승인 후 정식 제작한다.
|
||||||
|
|
||||||
|
원본 references/1.png를 보존하고 단순 형태·정돈된 색을 교육용 3D로 해석한다. 단순 배경·부드러운 조명·선명한 형태를 사용하며 평면 복제·캠핑 소재·상표를 강제하지 않는다. 02의 조사와 대본 근거로 모델·재질·방향·상태·수치를 확인하고 사실에 없는 구조를 만들지 않는다. 설명 텍스트는 검증한 합성 텍스트로 배치한다.
|
||||||
|
|
||||||
|
## 요청과 자산 확인
|
||||||
|
|
||||||
|
[10 양식](../../../templates/stages/10-references.json)을 사용한다. 선행 결과는 유효한 02/08/09다. request={engine:"Blender",style_reference:"references/1.png",render_profile,items}이며 RenderProfile은 공통 계약을 따른다.
|
||||||
|
|
||||||
|
item={id,kind,entity_id,prompt}. ID는 유일하고 kind는 style/character/background/object다. style/object의 entity_id는 null, character/background는 실제 09의 해당 ID다. 스타일과 필요한 인물·배경·물체를 빠짐없이 요청하고 고정 특징·근거·08 상태를 prompt에 연결한다. 예제의 fps·samples·seed를 모든 제작의 고정값으로 취급하지 않는다.
|
||||||
|
|
||||||
|
result는 null 또는 {scene_file,items}이며 결과 item={reference_id,object_name,media:image}다. 모든 요청 ID가 누락·중복·추가 없이 대응해야 한다. object_name은 실제 .blend의 오브젝트/컬렉션 이름이다. 모델·재질·고정 특징과 PNG를 함께 확인하고 불일치는 장면/요소/조사 단계로 보고한다.
|
||||||
|
|
||||||
|
## 실제 결과와 전달
|
||||||
|
|
||||||
|
승인된 저장소 내 Blender 실행 경로를 사용한다. 기존 설치를 무단 변경하거나 다른 도구로 바꾸지 않는다. 설치·요청 준비와 실제 렌더링 성공을 구분한다. .blend는 BlendFile={path,verified,sha256,blender_version}로 기록하고 실제 파일·해시·버전·재열기·Scene/자산을 검증한다. PNG/MP4 media로 위장하지 않는다. RenderProfile의 실제 지원을 확인하며 요청값을 실측 결과로 복사하지 않는다.
|
||||||
|
|
||||||
|
media는 기존 여섯 키 {path,kind,verified,duration_ms,width,height}다. PNG는 image·duration_ms=null, MP4는 video·실측 양수 길이이며 실제 형식·해상도·9:16·내용을 확인한다. 확인한 파일만 verified=true이고 미확인 크기·시간은 null이다. 데이터 경로는 루트 상대 경로다. 실제 파일이 없으면 result=null·awaiting_input, 실행/검증 실패는 failed로 보고한다. 부분 결과와 실패 시도를 보존하며 전체 요청 결과가 검증되기 전 complete로 보고하지 않는다.
|
||||||
|
|
||||||
|
자기 단계의 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 버전 review.md만 작성하고 이전 결과를 덮어쓰지 않는다. 실제 사용 버전·파일별 검사·명령·종료코드·미확인 영역·적용 원칙과 비적용 이유를 적는다. project.json·상태·현재 포인터·사용자 승인은 총괄만 갱신한다. 필드·열거값은 계약을 따르고 새 필수 입력·승인 지점을 만들지 않는다.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: short-video-research
|
||||||
|
description: 승인된 이야기 각도의 출처·주장·불확실성을 조사해 02-research 결과를 작성한다. 대본에 사용할 사실·숫자·비교 조건의 검증에 사용한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 근거와 주장 조사
|
||||||
|
|
||||||
|
## 입력과 승인 확인
|
||||||
|
|
||||||
|
- [공통 계약](../../../docs/workflow-contracts.md)의 제작 입력, 저장과 전달, 02 완료 기준, 상태와 사용자 검토, 재개와 수정 절을 읽는다.
|
||||||
|
- [제작 지침](../../../docs/short-video-guide.md)의 근거와 숫자는 작성 전에 정리한다, 참고 자료의 한계, 제작 단계에서 필요한 부분을 읽는다 절을 읽는다.
|
||||||
|
- [02 양식](../../../templates/stages/02-research.json)과 [검토 양식](../../../templates/review.md)을 사용한다. 상세 타입은 계약에서 연결한 단계 결과 스키마의 02 정의를 확인한다.
|
||||||
|
- `projects/<project_id>/project.json`의 제작 입력·현재 포인터·topic 검토를 읽고, 지정된 `stages/01-topic/r<revision>/artifact.json`과 review.md를 읽는다. `topic`, `language`는 공백만이 아닌 문자열이어야 하며 선택 ID가 후보에 존재해야 한다.
|
||||||
|
- topic 승인이 실제 사용자 응답이고 사용하려는 01 버전을 승인했는지 확인한다. 직접 호출되어도 승인 전 조사를 시작하지 않는다. 미승인은 총괄에 `awaiting_review`, 입력·파일 부재는 `awaiting_input`, 오래되거나 잘못된 참조는 문제로 보고한다.
|
||||||
|
|
||||||
|
## 조사와 판단
|
||||||
|
|
||||||
|
1. 선택한 중심 질문과 범위를 설명할 주장부터 조사한다. 공식 기록·원자료처럼 내용을 직접 뒷받침하는 출처를 우선 확인하고, 관련 없는 흥미로운 사실은 대본의 필수 내용으로 늘리지 않는다.
|
||||||
|
2. 실제 읽은 출처를 `sources`에 `{id, title, url, publisher, accessed_at}`로 기록한다. `accessed_at`은 실제 확인 날짜의 ISO 8601 `YYYY-MM-DD`다. 검색 요약·URL의 존재만으로 본문을 확인했다고 쓰지 않는다.
|
||||||
|
3. `claims`는 `{id, text, source_ids, assessment}`다. 주장마다 범위·조건·시점을 보존하고, ID와 출처 참조가 같은 결과 안에서 유효한지 확인한다. `verified`는 실제 확인한 출처 하나 이상이 주장 전체를 뒷받침할 때만 사용한다. 자료 부족·충돌·추정은 `uncertain`으로 남긴다.
|
||||||
|
4. 관찰한 사실과 해석·추정을 구별한다. 비교의 장소·시점·규모·접근·운영 조건을 확인하고, 숫자는 원수치·단위·비교 기준을 보존한다. 자료에 없는 원인·효과를 만들어 연결하지 않는다.
|
||||||
|
5. 참고 영상은 구성 참고다. 그 영상의 소재·수치·시각 구조는 별도 출처로 검증한다. 화면에 사용할 지도·구조·시대 배경도 내레이션과 같은 기준으로 확인한다.
|
||||||
|
6. `open_questions`는 미해결 질문의 문자열 배열이다. 대본의 중심 답을 막는 질문인지, 범위를 좁혀 제외할 수 있는 질문인지 review.md에서 구별한다. 출처 접근이 막히면 미확인 내용을 보존하고 필요한 자료를 보고한다.
|
||||||
|
|
||||||
|
주장 설명은 입력 언어로 명확하게 쓰고 출처 제목은 원문을 보존할 수 있다. 쉬운 말로 바꾸더라도 사실의 조건과 예외를 지우지 않는다.
|
||||||
|
|
||||||
|
## 제출과 완료
|
||||||
|
|
||||||
|
- `inputs`에 실제 사용한 01의 `{stage_id, revision}`을 기록하고 `stage_id="02-research"`로 새 버전을 작성한다.
|
||||||
|
- `projects/<project_id>/stages/02-research/r<revision>/artifact.json`과 같은 폴더의 `review.md`를 제출한다. 이전 결과는 보존하며 데이터 경로는 저장소 루트 기준이다. 검토 문서 링크는 복사한 위치에 맞춘다.
|
||||||
|
- 완료 준비를 확인할 때 출처·주장 ID의 유일성, 참조 존재, verified의 실제 근거, 미해결 질문의 영향과 대본에 쓸 중심 답의 근거를 점검한다. 비핵심 질문이 남았다는 이유만으로 확인한 근거를 버리지 않는다.
|
||||||
|
- review.md에 핵심 주장과 출처, 확인하지 못한 내용·돌아갈 단계, 적용한 구성 원칙과 적용하지 않은 이유를 남긴다. 새 필드나 별도 검토를 만들지 않는다.
|
||||||
|
- 중심 답의 필수 근거가 없으면 `awaiting_input`과 필요한 자료를 총괄에 보고한다. 유효한 근거로 03·04를 준비할 수 있어도 story 검토는 02 근거·03 대본·04 제목을 함께 확인한다. 실제 사용자 story 승인 전 05로 넘어가지 않는다.
|
||||||
|
- 담당은 자기 결과와 review.md만 제출하며 `project.json`의 상태·포인터·승인은 총괄만 변경한다. 검증 실패·수정으로 영향받는 후속 결과와 검토를 보고한다.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: short-video-review
|
||||||
|
description: 13-compose의 실제 완성본을 여섯 영역에서 검수해 14-review의 근거·수정할 단계와 컷·추천을 작성하고 사용자 final 검토를 준비한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 최종 영상 검수
|
||||||
|
|
||||||
|
## 시작 조건
|
||||||
|
|
||||||
|
[공통 계약](../../../docs/workflow-contracts.md)의 저장·전달, 승인·수정, 검수 판정 규칙과 [제작 지침의 최종 영상 검토](../../../docs/short-video-guide.md#최종-영상-검토)를 읽는다. [14 양식](../../../templates/stages/14-review.json)과 [결과 스키마](../../../contracts/artifact.schema.json)의 `14-review` 정의를 사용하며 새로운 입력·필드·검토 지점을 만들지 않는다.
|
||||||
|
|
||||||
|
개별 호출에서도 같은 제작물의 유효한 `13-compose`와 검수에 직접 사용한 선행 결과를 확인하고 실제 사용한 `{stage_id, revision}`을 `inputs`에 기록한다. 대본·제목·근거, 실측 음성·컷, 장면·인물·배경·이미지·영상은 검수에 필요한 해당 결과와 그 선행 관계를 읽는다. 다른 제작물·오래된 버전·stale 결과를 섞지 않는다.
|
||||||
|
|
||||||
|
project.json에서 story·scenes가 사용 버전과 연결된 실제 사용자 승인인지 확인한다. pending·changes_requested·stale 승인을 우회하지 않으며 같은 승인 버전은 재승인을 요구하지 않는다. 선행 조건인 Qwen3-TTS 음성, 승인된 Blender 교육용 3D·실제 컷 영상과 Remotion + FFmpeg 합성 결과를 확인한다. references/1.png 원본은 보존하고 기존 1.0 결과도 유효한 선행 자료로 참조한다. 이 스킬은 실제 최종 영상 검수만 담당하며 도구를 임의로 바꾸지 않는다. 1.1의 14 구조와 여섯 검수 영역을 유지한다.
|
||||||
|
|
||||||
|
13의 실제 MP4·SRT를 열고 영상을 끝까지 시청·청취한다. `media.verified=true`만으로 검수자의 관찰을 대신하지 않는다. 실제 최종 영상이 없거나 재생해 볼 수 없으면 recommendation=incomplete이며 확인하지 못한 영역은 not_checked로 남긴다. 빠진 결과·파일·승인과 돌아갈 단계를 총괄에 보고한다. 선행 파일 미확인은 verified=false, 미확인 메타데이터는 null로 취급하며 부분 결과의 완료를 인정하지 않는다.
|
||||||
|
|
||||||
|
## 여섯 영역의 실제 근거
|
||||||
|
|
||||||
|
`payload.checks`에 아래 area를 각각 한 번 포함하고 기존 `{area, verdict, evidence}`를 사용한다. verdict는 pass/fail/not_checked이며 증거 없이 pass로 만들지 않는다. evidence에 실제 파일·관찰/청취 구간·관련 블록/주장/자막/컷 ID와 확인 결과를 적는다. 검수하지 못한 이유도 이 필드에 남긴다.
|
||||||
|
|
||||||
|
| area | 실제 완성본에서 확인할 내용 |
|
||||||
|
|---|---|
|
||||||
|
| story | 중심 질문에 답하고 쉬운 말·구체적 행동으로 이해와 재미를 주는가. 블록 연결·근거의 범위가 유지되고 도입과 선정 제목의 약속을 끝에서 회수하는가. 확인한 02 주장과 03 블록·04 제목, 실제 영상 구간을 연결한다. |
|
||||||
|
| visual | 설명에 필요한 상태 변화·동작·카메라가 보이고 시작/끝 상태가 맞는가. 구조·수치·주석이 조사 근거와 맞으며 장식적 움직임이 설명을 가리지 않는가. 실제 컷과 관찰 내용을 기록한다. |
|
||||||
|
| audio | 실제 청취에서 내레이션의 의미·발음·음량이 자연스럽고 누락·중복이 없는가. 배경음·효과음이 있다면 말을 가리는지도 듣는다. 텍스트·파일 존재만으로 통과시키지 않는다. |
|
||||||
|
| captions | 실제 SRT와 표시 자막이 입력 언어·원문 의미·표시 순서를 지키고 음성 시간에 맞는가. 자막과 도해 주석의 역할, 모바일에서 대비·글자 크기·줄바꿈·표시 시간·가림을 확인한다. |
|
||||||
|
| timing | MP4 실측 길이가 양수·180000ms 이하, width:height가 9:16인가. 모든 실제 컷이 양수·5000ms 이하이고 요청/클립 길이, 통합 음성·영상·자막 구간이 일치하며 빈 구간·겹침이 없는가. |
|
||||||
|
| consistency | 인접 컷의 끝/시작 상태, 인물·배경 고정 특징, 위치·방향·카메라·그림체가 일관되는가. continuous 연결과 cut 전환이 승인된 장면 이유를 지키는가. 연결 컷 쌍으로 기록한다. |
|
||||||
|
|
||||||
|
## 문제와 추천
|
||||||
|
|
||||||
|
`payload.findings`는 `{severity, description, stage_id, cut_id}`다. severity는 blocking/minor, stage_id는 실제 수정할 단계, cut_id는 해당 컷 ID이며 전체 문제는 null이다. description에 실제 증상·구간·기대 결과·수정 방향을 적고 evidence와 일치시킨다.
|
||||||
|
|
||||||
|
대본·제목·근거 문제는 03/04/02, 음성 누락·중복은 05/06, 컷·자막 시간은 07, 장면 연결·고정 특징은 08/09, 이미지 문제는 10/11, 동작·클립 길이는 12, 합성·배치·SRT 문제는 13으로 보고한다. 정확한 stage_id 값을 사용하며 원인에 맞춰 돌아갈 단계를 고른다. 실제 클립 길이가 요청과 다른 것을 요청값으로 덮거나 재생 속도로 감추지 않는다.
|
||||||
|
|
||||||
|
recommendation은 다음 기존 값만 사용한다.
|
||||||
|
|
||||||
|
- pass: 여섯 영역이 모두 실제 근거를 가진 pass이고 fail/not_checked와 blocking 발견이 없을 때만 사용한다.
|
||||||
|
- revise: 확인된 실패나 blocking 문제를 수정해야 할 때 사용하고 담당 단계·컷을 연결한다. minor 발견도 보존하고 수정 필요성을 설명한다.
|
||||||
|
- incomplete: 실제 최종 영상이 없거나 볼 수 없고, 필수 영역·선행 자료를 확인하지 못해 판정을 완료할 수 없을 때 사용한다. 이미 확인한 실패도 findings에 보존한다.
|
||||||
|
|
||||||
|
## 저장과 사용자 final 검토
|
||||||
|
|
||||||
|
담당은 `projects/<project_id>/stages/14-review/r<revision>/artifact.json`과 같은 버전 `review.md`만 작성하고 이전 버전을 보존한다. [검토 양식](../../../templates/review.md)에 여섯 영역의 checks.evidence·findings·추천을 같은 의미로 요약하고, 제작 지침을 적용한 실제 결과와 소재에 맞지 않아 적용하지 않은 구성의 이유를 적는다.
|
||||||
|
|
||||||
|
총괄만 project.json의 상태·현재 포인터·승인을 작성한다. 담당은 검수 준비 여부·미확인 내용·수정 단계/컷과 영향받는 후속 결과·승인을 총괄에 보고한다. 13 실제 완성본과 14 보고서를 사용자 final 검토에 제시하며 검수자의 pass는 사용자 승인이 아니다. 응답 없음이나 담당의 추천을 승인으로 기록하거나 사용자 final 승인 전 제작 완료를 선언하지 않는다.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: short-video-scenes
|
||||||
|
description: 짧은 영상의 실제 컷마다 설명할 화면과 시작·끝 상태를 설계하고 인접 연결을 08-scenes 결과와 검토에 기록한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 컷별 화면과 연결 설계
|
||||||
|
|
||||||
|
## 먼저 읽을 자료
|
||||||
|
|
||||||
|
- [공통 제작 계약](../../../docs/workflow-contracts.md): 08 참조·완료 기준과 scenes 승인 경계.
|
||||||
|
- [화면 역할 지침](../../../docs/short-video-guide.md#설명의-단위를-화면으로-바꾼다), [음성과 연결 지침](../../../docs/short-video-guide.md#음성을-기준으로-컷과-연결을-확정한다), [그림체와 상태 변화 지침](../../../docs/short-video-guide.md#깔끔한-그림체로-작동-방식을-보여준다).
|
||||||
|
- [08 양식](../../../templates/stages/08-scenes.json), [결과 스키마](../../../contracts/artifact.schema.json), [검토 양식](../../../templates/review.md).
|
||||||
|
|
||||||
|
## 입력과 승인 확인
|
||||||
|
|
||||||
|
개별 호출에서도 같은 제작물의 유효한 03-script와 실제 통합 음성으로 검증한 07-cuts, 해당 story 승인을 확인한다. `inputs`에 실제 사용한 03과 07 버전을 기록한다. 선행 결과·실측 근거·승인 부재나 오래된 버전은 총괄에 issues로 보고하고 기다린다. 준비된 동일 scenes 검토 묶음 안에서 아직 scenes 승인을 받지 않은 07로 08을 작성할 수 있다.
|
||||||
|
|
||||||
|
## 작성과 확인
|
||||||
|
|
||||||
|
1. 07 순서를 따라 모든 cut_id를 scenes에 정확히 한 번 포함한다. 존재하지 않는 컷·누락·중복을 허용하지 않는다. 03의 이야기 블록과 컷은 일대일이 아니며 설명이 여러 컷에 걸치면 같은 대상과 시각적 기준점을 유지한다.
|
||||||
|
2. description에는 이 구간에서 시청자가 이해할 관계를, start_state와 end_state에는 이를 보여줄 물체·인물·배경과 설명에 필요한 상태 변화를 적는다. 내레이션의 명사를 그림으로 나열하거나 장식적 움직임으로 설명을 대신하지 않는다.
|
||||||
|
3. camera에는 전경·단면·근접·지도·조건 비교 등 선택한 화면을 적는다. 실제 구조·재료·방향·비교 기준을 보존하며 확인되지 않은 사실을 새 도해로 만들지 않는다. 근거 부족은 총괄에 보고해 조사나 대본으로 돌려보낸다.
|
||||||
|
4. 07 순서의 각 인접 컷 쌍에 connections를 정확히 하나 작성한다. from_cut_id·to_cut_id는 해당 인접 쌍을 참조하고 mode는 continuous 또는 cut이다. reason에 설명이 이어지거나 위치·시간·시점이 바뀌어야 하는 이유를 적는다.
|
||||||
|
5. continuous는 앞 컷 end_state와 다음 start_state의 인물·물체 위치, 흐름 방향, 카메라 상태를 맞춘다. cut은 새로운 시점·상태를 이해할 수 있게 제시한다. 일정한 간격만을 이유로 전환하지 않는다.
|
||||||
|
|
||||||
|
정확한 필드·타입은 양식·스키마를 따른다. 1.1의 description·상태·camera·연결과 preview 필드를 사용한다. 기존 1.0 입력은 보존·참조한다. 원본 `references/1.png`의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 평면 복제·캠핑 소재를 강제하지 않는다.
|
||||||
|
|
||||||
|
scenes 승인 전에 제한된 대표 Scene의 실제 .blend·PNG·짧은 MP4를 visual-designer/video-editor의 해당 책임으로 준비해 `payload.preview`에 기록할 수 있다. BlendFile·RenderProfile·sample의 정확한 구조는 공통 계약과 스키마를 따른다. sample ID·실제 컷 ID·파일·해시·버전·실제 렌더 프로필·판독성을 확인한다. 자료가 없으면 preview=null이며 준비 완료로 제시하지 않는다. 이 샘플은 기존 scenes 검토에 포함하는 품질 근거이고 정식 10–12 완료나 추가 승인 지점이 아니다. 새 08·09 승인 전 전체 제작을 하지 않는다.
|
||||||
|
|
||||||
|
## 제출과 검토 경계
|
||||||
|
|
||||||
|
자기 단계의 새 버전을 `projects/<project_id>/stages/08-scenes/r<revision>/artifact.json`과 같은 폴더의 `review.md`에 저장한다. 이전 버전을 덮어쓰지 않고 데이터 경로는 저장소 루트 기준으로 쓴다. review.md에 컷 ID·실제 음성 구간, 이해할 내용, 상태 변화, 화면·연결 선택 이유와 근거 참조를 남긴다. 적용한 원칙의 실제 결과와 소재에 맞지 않아 적용하지 않은 이유를 적으며 추가 입력·승인 지점을 만들지 않는다.
|
||||||
|
|
||||||
|
총괄에 제출 경로·선행 버전·검증 결과·issues를 보고한다. 부재는 awaiting_input, 검증 실패는 failed, scenes 결정 대기는 awaiting_review로 보고한다. `project.json`의 상태·포인터·승인은 총괄만 작성한다. 준비된 08로 같은 검토 묶음의 09를 작성할 수 있지만 07·08·09의 실제 결과에 대한 사용자 scenes 승인 전에는 10으로 진행하지 않는다. 담당의 추천은 승인이 아니다.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: short-video-script
|
||||||
|
description: 선택한 이야기 각도와 검증된 조사 근거로 03-script 대본을 작성하거나 수정한다. 중심 질문의 답·블록 연결·쉬운 표현과 story 검토 준비에 사용한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 근거를 이야기로 쓰기
|
||||||
|
|
||||||
|
## 입력 읽기
|
||||||
|
|
||||||
|
- [공통 계약](../../../docs/workflow-contracts.md)의 제작 입력, 저장과 전달, 03 완료 기준, 상태와 사용자 검토, 재개와 수정 절을 읽는다.
|
||||||
|
- [제작 지침](../../../docs/short-video-guide.md)의 먼저 질문 하나를 정한다부터 근거와 숫자는 작성 전에 정리한다까지와 대본과 제목 검토 메모를 읽는다.
|
||||||
|
- [03 양식](../../../templates/stages/03-script.json)과 [검토 양식](../../../templates/review.md)을 사용한다. 필요한 상세 구조는 계약에서 연결한 단계 결과 스키마의 03 정의를 확인한다.
|
||||||
|
- `projects/<project_id>/project.json`의 제작 입력·현재 포인터·검토를 읽고, 지정된 01-topic·02-research 버전의 artifact.json과 review.md를 읽는다. `topic`, `language`가 공백만이 아닌 문자열인지, 02가 해당 01을 사용했는지, topic 승인이 실제 사용자 응답과 같은 01 버전을 가리키는지 확인한다.
|
||||||
|
- 직접 호출해도 선행 결과와 승인을 생략하지 않는다. 입력·근거·파일 부재는 총괄에 `awaiting_input`, topic 미승인은 `awaiting_review`, stale·다른 제작물·잘못된 참조는 문제로 보고한다. 02는 story 검토 전이라도 유효한 준비 결과로 사용할 수 있다.
|
||||||
|
|
||||||
|
## 대본 작성
|
||||||
|
|
||||||
|
1. 선택한 중심 질문과 끝에서 전달할 답을 고정하고 도입이 약속한 답을 결말에서 회수한다. 소재에 맞는 설명·변화·방법·비교 구성을 선택한다. 지침의 모든 이야기 역할·반전·위기·고정 시간 배분을 강제하지 않는다.
|
||||||
|
2. `blocks`는 순서가 있는 `{id, text, claim_ids}` 배열이다. 블록마다 질문에 가까워지는 내용과 앞뒤 연결 이유가 있어야 한다. 사실처럼 쓰는 주장은 02의 `verified` 주장에 연결한다. ID는 유일하고 모든 `claim_ids`는 사용한 02 버전에 존재해야 한다.
|
||||||
|
3. `uncertain`을 확정 사실로 쓰지 않는다. 추정을 다룰 필요가 있으면 추정임을 표현하고 중심 답을 대신하지 않게 한다. 새 사실·숫자·인과 관계가 필요하면 02로 돌아가 검증된 새 버전을 요청한다. 단순 연결·질문 문장은 사실 주장이 없으면 `claim_ids=[]`가 가능하다.
|
||||||
|
4. 입력 언어로 대본을 쓴다. 한 문장에 행동·관계 하나를 담고 용어보다 기능을 먼저 설명한다. 초등학생도 직관적으로 이해할 표현을 쓰되 시청자를 어린이로 제한하지 않는다.
|
||||||
|
5. 비유가 공유하는 성질과 적용 한계를 유지한다. 필요한 숫자만 단위·비교 기준과 함께 사용하고, 쉽게 만들기 위해 실제 조건·예외를 지우지 않는다. 참고 영상의 표현·순서를 복제하거나 검증되지 않은 소재를 사실로 쓰지 않는다.
|
||||||
|
6. `full_text`는 블록 순서대로 읽은 대본과 내용이 일치해야 한다. 예상 길이를 계산할 근거가 있으면 양수 정수 밀리초를 `estimated_duration_ms`에 적고 방법을 review.md에 남긴다. 모르면 `null`이다. 예상은 실측이 아니며 180000ms는 상한이지 채울 목표가 아니다. 블록을 5초 컷으로 확정하거나 글자 수 추정을 실제 음성 시간으로 기록하지 않는다.
|
||||||
|
|
||||||
|
## 제출과 완료
|
||||||
|
|
||||||
|
- 실제 사용한 01·02의 `{stage_id, revision}`을 `inputs`에 기록한다. `stage_id="03-script"`인 새 버전을 `projects/<project_id>/stages/03-script/r<revision>/artifact.json`과 같은 폴더의 `review.md`로 제출한다. 이전 결과를 덮어쓰지 않고 경로는 저장소 루트 기준으로 기록한다.
|
||||||
|
- review.md에는 지침의 대본과 제목 검토 메모를 사용한다. 중심 질문·답, 도입의 약속과 답하는 블록, 블록별 연결, 주장·출처, 비유 한계, 덜어낸 정보, 적용한 원칙과 적용하지 않은 이유를 적는다. 04 전에는 제목 검토가 미작성임을 밝히고 새 필드·입력·검토를 만들지 않는다. 문서 링크는 실제 저장 위치에 맞춘다.
|
||||||
|
- 블록·전체 대본의 순서와 내용, 사실 주장의 verified 근거, 입력 언어·쉬운 표현·예상의 표시를 확인한다. 준비된 03으로 04 제목을 작성할 수 있다.
|
||||||
|
- 완료는 02 근거·03 대본·04 제목의 같은 유효 버전에 대한 실제 사용자 story 승인이 필요하다. 준비된 결과는 총괄에 `awaiting_review`로 보고하고 승인 전 05 TTS 분할로 넘어가지 않는다. 담당의 추천·침묵은 승인이 아니다.
|
||||||
|
- 담당은 결과 버전과 review.md만 제출하며 `project.json`의 상태·포인터·승인은 총괄만 변경한다. 내용·언어 수정의 후속 영향과 재승인 필요를 보고한다.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: short-video-title
|
||||||
|
description: 준비된 대본의 실제 설명과 도입의 약속에 맞는 입력 언어 제목 후보를 만들고 04-title 결과를 작성한다. 제목 선택·수정과 story 검토 준비에 사용한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 대본이 지키는 제목
|
||||||
|
|
||||||
|
## 선행 결과 확인
|
||||||
|
|
||||||
|
- [공통 계약](../../../docs/workflow-contracts.md)의 제작 입력, 저장과 전달, 04 완료 기준, 상태와 사용자 검토, 재개와 수정 절을 읽는다.
|
||||||
|
- [제작 지침](../../../docs/short-video-guide.md)의 첫 문장과 제목이 같은 내용을 약속하게 한다, 쉬운 말로 원인을 설명한다, 대본과 제목 검토 메모를 읽는다.
|
||||||
|
- [04 양식](../../../templates/stages/04-title.json)과 [검토 양식](../../../templates/review.md)을 사용한다. 필요한 상세 구조는 계약에서 연결한 단계 결과 스키마의 04 정의를 확인한다.
|
||||||
|
- `projects/<project_id>/project.json`의 제작 입력·현재 포인터·검토를 읽고 지정된 `stages/03-script/r<revision>/artifact.json`과 review.md를 읽는다. `topic`, `language`가 공백만이 아닌 문자열인지, 03의 실제 01·02 참조와 해당 01의 topic 승인이 유효한지 확인한다. 주장 범위를 확인할 때는 03이 사용한 02 결과를 읽는다.
|
||||||
|
- 직접 호출되어도 유효한 선행 결과와 사용자 승인을 우회하지 않는다. 입력·파일·근거 부재는 총괄에 `awaiting_input`, topic 미승인은 `awaiting_review`, 잘못된 참조·stale 결과는 문제로 보고한다. 준비된 03으로 story 승인 전 제목을 작성할 수 있다.
|
||||||
|
|
||||||
|
## 후보와 선택
|
||||||
|
|
||||||
|
1. 대본의 중심 질문·끝의 답·첫 문장의 약속을 읽고 제목이 약속할 내용을 정한다. 대본이 실제 설명하는 차이·원인·방법을 짧게 표현한다.
|
||||||
|
2. `candidates`는 `{id, text, rationale}` 배열이다. 후보 ID는 유일하고 텍스트는 비어 있지 않아야 한다. 후보 수·숫자 사용·반전 문형을 고정하지 않는다.
|
||||||
|
3. `rationale`에 해당 제목의 약속과 그 약속에 답하는 대본 블록 ID를 적는다. 제목은 새로운 사실·과장된 인과·검증되지 않은 수치·대본이 답하지 않는 의문을 추가하면 안 된다. 필요하면 제목을 좁히고 대본 변경이 필요하면 03으로 돌아간다.
|
||||||
|
4. 제목은 입력 언어의 자연스러운 표현으로 쓴다. 한국어 문형을 다른 언어에 강제로 직역하지 않는다. 쉬운 표현은 어린이 전용 제목을 요구하는 기준이 아니다. 비유를 쓰더라도 사실의 범위와 한계를 유지한다.
|
||||||
|
5. `selected_id`는 실제 후보 ID 또는 미선택 시 `null`이다. 추천 후보와 실제 사용자의 선택을 review.md에서 구별한다. 담당이 후보를 추천하거나 선택 값을 채워도 story 승인으로 처리하지 않는다.
|
||||||
|
|
||||||
|
## 제출과 완료
|
||||||
|
|
||||||
|
- `inputs`에는 실제 사용한 03의 `{stage_id, revision}`을 기록한다. 근거를 직접 사용했다면 사용한 02 참조도 계약에 따라 기록한다. 같은 제작물의 유효 버전만 사용한다.
|
||||||
|
- `stage_id="04-title"`인 새 버전을 `projects/<project_id>/stages/04-title/r<revision>/artifact.json`과 같은 폴더의 `review.md`로 제출한다. 기존 결과를 덮어쓰지 않고 데이터 경로는 저장소 루트 기준으로 기록한다. 검토 문서 링크는 실제 저장 위치에 맞춘다.
|
||||||
|
- review.md에 중심 질문·답, 제목과 첫 문장의 약속·답하는 블록, 후보별 이유와 선택, 과장·미확인 주장 점검, 적용한 원칙과 적용하지 않은 이유를 적는다. 새 필드·필수 입력·승인 지점을 만들지 않는다.
|
||||||
|
- 후보·선택 ID의 유효성, 입력 언어, 대본과 제목의 일치를 확인한다. 미선택이면 필요한 선택을 포함해 총괄에 `awaiting_review`로 보고한다.
|
||||||
|
- 완료에는 선택한 제목과 02 근거·03 대본·04 제목의 해당 버전에 대한 실제 사용자 story 승인이 필요하다. 응답 없음이나 담당의 추천은 승인이 아니다. 승인 전 05 TTS 분할로 넘어가지 않는다.
|
||||||
|
- 담당은 결과 버전과 review.md만 제출한다. `project.json`의 상태·포인터·승인은 총괄만 변경한다. 제목 수정 시 실제 참조에 따른 후속 결과·story/final 재검토 영향을 보고하되, 관련 없는 음성·이미지를 일괄 재생성하지 않는다.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: short-video-topic
|
||||||
|
description: 짧은 영상의 입력 주제 안에서 이야기 각도와 중심 질문을 제안하고 01-topic 결과를 작성한다. 주제 선택·범위 수정·topic 검토 준비에 사용한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 주제와 이야기 각도
|
||||||
|
|
||||||
|
## 시작할 때
|
||||||
|
|
||||||
|
- [공통 계약](../../../docs/workflow-contracts.md)의 제작 입력, 저장과 전달, 01 완료 기준, 상태와 사용자 검토, 재개와 수정 절을 읽는다.
|
||||||
|
- [제작 지침](../../../docs/short-video-guide.md)의 목적과 적용 범위, 먼저 질문 하나를 정한다, 제작 단계에서 필요한 부분을 읽는다 절을 읽는다.
|
||||||
|
- [01 양식](../../../templates/stages/01-topic.json)과 [검토 양식](../../../templates/review.md)을 사용한다. 구조가 필요한 경우 계약에서 연결한 단계 결과 스키마의 01 정의를 확인한다.
|
||||||
|
- `projects/<project_id>/project.json`에서 `brief.topic`, `brief.language`, 현재 01 버전과 topic 검토를 확인한다. 두 입력 모두 공백만이 아닌 문자열이어야 한다. 언어 이름·코드를 보존하며 특정 언어로 고정하지 않는다.
|
||||||
|
- 직접 호출되어도 제작 ID·필수 입력을 임의로 채우지 않는다. 없으면 총괄에 누락 내용과 `awaiting_input`을 보고한다. 재개 시 같은 제작물의 유효 버전과 실제 사용자 응답을 확인한다.
|
||||||
|
|
||||||
|
## 각도 작성
|
||||||
|
|
||||||
|
1. 입력 주제 안에서 서로 구별되는 이야기 각도를 제안한다. 후보 수를 고정하지 말고 선택에 필요한 차이를 보여준다.
|
||||||
|
2. `candidates`의 각 항목은 `{id, angle, summary}`다. `angle`에는 중심 질문 또는 이야기 방향을, `summary`에는 다룰 범위·끝에서 답하려는 내용·보여줄 수 있는 증거를 짧게 적는다. ID는 비어 있지 않고 후보 안에서 유일해야 한다.
|
||||||
|
3. 아직 조사하지 않은 답은 확인할 가설로 표현한다. 참고 영상에서 관찰한 구성의 장점과 이번 소재에 권하는 구성을 구별하고, 소재 사실은 02에서 별도 출처로 확인하도록 남긴다.
|
||||||
|
4. 의문·변화가 없는 소재에는 방법 안내·관찰·시간 순서처럼 맞는 구성을 고른다. 위기·반전·실패한 대안이나 고정 시간 배분을 만들지 않는다.
|
||||||
|
5. `selected_id`는 후보 ID 또는 미선택 시 `null`이다. 추천·선택 근거는 `selection_reason`에 적고, 추천과 실제 사용자 선택을 review.md에서 구별한다. 사용자 선택으로 기존 결과가 달라지면 새 버전으로 제출한다.
|
||||||
|
|
||||||
|
쉬운 표현은 초등학생도 직관적으로 이해할 표현 난이도다. 시청자를 어린이로 제한하지 않는다. 후보 설명도 입력 언어의 자연스러운 표현을 사용한다.
|
||||||
|
|
||||||
|
## 제출과 완료
|
||||||
|
|
||||||
|
- 선행 단계가 없으므로 `inputs`는 빈 배열이다. 실제 제작 ID와 `stage_id="01-topic"`, 새 `revision`을 [01 양식](../../../templates/stages/01-topic.json)에 채운다.
|
||||||
|
- 결과는 `projects/<project_id>/stages/01-topic/r<revision>/artifact.json`과 같은 폴더의 `review.md`로 제출한다. 이전 버전을 덮어쓰지 않는다. 경로는 저장소 루트 기준이며, 복사한 검토 양식의 문서 링크는 실제 위치에 맞춘다.
|
||||||
|
- review.md에 중심 질문·범위·후보 차이·선택 이유, 적용한 구성 원칙과 적용하지 않은 이유를 적는다. 새 필드·필수 입력·검토 지점을 추가하지 않는다.
|
||||||
|
- 후보 텍스트와 ID, 선택 ID의 존재, 선택 이유를 확인한다. 준비된 결과는 topic `awaiting_review`로 보고하고, 미선택이면 필요한 결정을 함께 적는다.
|
||||||
|
- 01 완료에는 선택된 후보와 해당 버전에 대한 실제 사용자의 topic 승인이 필요하다. 응답 없음·담당 추천을 승인으로 해석하지 않는다. 승인 전 02 자료 조사를 시작하지 않는다.
|
||||||
|
- 담당은 결과 버전과 review.md만 제출한다. `project.json`의 상태·포인터·승인은 총괄만 변경한다. 검증 실패나 변경 영향은 총괄에 문제와 재검토할 후속 결과로 보고한다.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: short-video-tts-split
|
||||||
|
description: 승인된 짧은 영상 대본을 Qwen3-TTS용 발화 단위로 나누고 05-tts-split 결과와 검토 메모를 작성한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# TTS 발화 단위 분할
|
||||||
|
|
||||||
|
## 먼저 읽을 자료
|
||||||
|
|
||||||
|
- [공통 제작 계약](../../../docs/workflow-contracts.md): 저장·버전·승인·상태와 05 완료 기준.
|
||||||
|
- [제작 지침의 음성 기준](../../../docs/short-video-guide.md#음성을-기준으로-컷과-연결을-확정한다): 의미·호흡·발음에 따른 분할.
|
||||||
|
- [쉬운 설명 지침](../../../docs/short-video-guide.md#쉬운-말로-원인을-설명한다): 긴 문장을 다듬어야 할 때 참고.
|
||||||
|
- [05 양식](../../../templates/stages/05-tts-split.json), [결과 스키마](../../../contracts/artifact.schema.json), [검토 양식](../../../templates/review.md).
|
||||||
|
|
||||||
|
## 입력과 승인 확인
|
||||||
|
|
||||||
|
개별 호출에서도 같은 제작물의 유효한 03-script 버전과 이를 포함한 story 승인을 먼저 확인한다. 승인 전에는 05를 작성하지 않는다. 선행 결과·승인 부재나 오래된 버전은 총괄에 문제로 보고하고 기다린다. 사용자가 이미 승인한 동일 버전은 다시 승인받지 않는다.
|
||||||
|
|
||||||
|
`inputs`에 실제 사용한 03의 stage_id와 revision을 기록한다. 정확한 필드·타입은 양식과 스키마의 unit 정의를 따르며 새 필드를 추가하지 않는다.
|
||||||
|
|
||||||
|
## 작성과 확인
|
||||||
|
|
||||||
|
1. 승인된 대본 순서대로 의미와 호흡이 자연스러운 발화 단위를 만든다. 숫자·약어·고유명사의 읽는 방법은 발음 메모에 적는다.
|
||||||
|
2. 각 단위의 유일한 ID, 원본 block_ids, 표시 텍스트와 발화 텍스트를 기존 필드에 담는다. 발음용 표기 변경과 내용 변경을 구분한다. 의미가 바뀌는 수정은 03 새 버전과 story 재승인으로 돌려보낸다.
|
||||||
|
3. 단위를 이어 읽어 03의 전체 의미·순서가 보존되고 발화 누락·중복이 없는지 확인한다. block_ids는 사용한 03에 존재해야 한다.
|
||||||
|
|
||||||
|
TTS 단위·문장·이야기 블록은 화면 컷과 다르다. 단위를 5초로 제한하거나 컷 길이에 맞추려고 문장을 끊고 발화를 재배치하지 않는다. 03의 예상 길이는 실제 생성 음성 길이가 아니다.
|
||||||
|
|
||||||
|
## 제출과 진행 보고
|
||||||
|
|
||||||
|
자기 단계의 새 버전을 `projects/<project_id>/stages/05-tts-split/r<revision>/artifact.json`과 같은 폴더의 `review.md`에 저장한다. 이전 버전을 덮어쓰지 않고 데이터 경로는 저장소 루트 기준으로 쓴다. review.md에 사용한 의미·호흡·발음 원칙, 실제 적용 결과와 소재에 맞지 않아 적용하지 않은 이유를 남긴다. 추가 입력·승인 지점을 만들지 않는다.
|
||||||
|
|
||||||
|
총괄에 제출 경로·선행 버전·검증 결과·남은 issues를 보고한다. 입력 부재는 awaiting_input, 검증 실패는 failed로 보고하며 확인하지 않은 결과를 complete라고 하지 않는다. `project.json`의 상태·포인터·승인은 총괄만 작성한다. 분할 확인을 마친 뒤 06으로 전달한다.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: short-video-tts
|
||||||
|
description: 짧은 영상의 05 발화 단위로 기존 qwen3-tts 환경의 Qwen3-TTS 요청과 실제 WAV 결과를 관리하고 06-tts 검토를 작성한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Qwen3-TTS 음성 제작
|
||||||
|
|
||||||
|
## 먼저 읽을 자료
|
||||||
|
|
||||||
|
- [공통 제작 계약](../../../docs/workflow-contracts.md): 실제 파일·시간 확인, 외부 결과와 상태, 승인 경계.
|
||||||
|
- [제작 지침의 음성 기준](../../../docs/short-video-guide.md#음성을-기준으로-컷과-연결을-확정한다): 의미·호흡·발음과 실제 음성 구간.
|
||||||
|
- [06 양식](../../../templates/stages/06-tts.json), [결과 스키마](../../../contracts/artifact.schema.json), [검토 양식](../../../templates/review.md).
|
||||||
|
|
||||||
|
## 입력과 환경 확인
|
||||||
|
|
||||||
|
개별 호출에서도 같은 제작물의 유효한 05-tts-split과 그 근거인 03의 story 승인을 확인한다. 선행 결과·승인 부재나 오래된 버전이면 총괄에 issues를 보고하고 기다린다. `inputs`는 실제 사용한 05 버전을 기록한다.
|
||||||
|
|
||||||
|
기존 conda 환경 `qwen3-tts`의 `Qwen3-TTS`를 사용한다. 환경 재설치·전역 설정 변경으로 해결하지 않는다. 실제 제작 시 사용 가능한 모델·화자·입력 언어 지원을 확인한다. 환경이나 패키지 존재, 모델 로딩 성공, 실제 추론 성공을 따로 보고한다. 설치 확인만으로 음성 생성이 성공했다고 쓰지 않는다.
|
||||||
|
|
||||||
|
지원되지 않는 언어나 도구 부재는 blocking issues로 총괄에 보고하고 결과·해결 입력을 기다린다. 요청 언어를 임의로 바꾸거나 다른 도구로 대체하지 않는다. 외부 실행파일 경로·키·인증정보는 결과에 기록하지 않는다.
|
||||||
|
|
||||||
|
## 요청과 실제 결과
|
||||||
|
|
||||||
|
1. 양식의 `request.engine`, `request.conda_env`, `request.language`, `request.units`에 도구·환경·입력 언어·05 단위를 기록한다. 정확한 구조는 스키마를 따르며 새 필드를 만들지 않는다.
|
||||||
|
2. 외부 결과를 받기 전에는 `result=null`을 유지한다. 요청 작성과 추론 성공을 구분한다. 실제로 사용한 모델·화자는 `result.model`, `result.voice`에 기록한다.
|
||||||
|
3. 요청 단위별 WAV를 `result.files`에 연결하고 실제 통합 WAV를 `result.narration`에 기록한다. unit_id는 요청에 존재하며 모든 단위가 순서대로 누락·중복 없이 포함되어야 한다.
|
||||||
|
4. WAV 실존·내용 형식·실제 길이를 확인한 파일만 media.verified=true로 기록한다. kind=audio, width/height=null이며 duration_ms는 실측한다. 예제 경로나 요청 길이를 실제 파일로 표시하지 않는다.
|
||||||
|
5. `result.unit_ranges`는 실제 통합 음성 안의 단위별 구간이다. 단위 WAV의 파일 길이, 03의 예상 길이, 통합 음성에서의 시작·끝을 구분한다. 구간의 순서·전체 포함·겹침과 발음·호흡·언어·누락·중복을 실제 청취로 확인한다.
|
||||||
|
|
||||||
|
실제 통합 음성이 180000ms를 넘으면 03 대본 수정과 story 재승인으로 돌아간다. 임의로 언어를 바꾸거나 발화를 빠르게 재생해 상한에 맞추지 않는다. 실측·청취·정렬 확인 방법과 미확인 내용을 review.md에 남겨 07의 timing_evidence 작성 근거로 전달한다.
|
||||||
|
|
||||||
|
## 제출과 진행 보고
|
||||||
|
|
||||||
|
자기 단계의 새 버전을 `projects/<project_id>/stages/06-tts/r<revision>/artifact.json`과 같은 폴더의 `review.md`에 저장한다. 데이터 경로는 저장소 루트 기준이며 이전 버전을 덮어쓰지 않는다. review.md에 실제 사용한 의미·호흡·발음 원칙과 결과, 적용하지 않은 구성의 이유를 적는다. 추가 입력·승인 지점을 만들지 않는다.
|
||||||
|
|
||||||
|
총괄에 제출 경로·선행 버전·실제 파일 검증·남은 issues를 보고한다. result 부재는 awaiting_input, 실행·검증 실패는 failed다. 부분 파일은 보존하되 모든 요청 파일·통합 음성·구간을 확인하기 전 complete로 보고하지 않는다. `project.json`의 상태·포인터·승인은 총괄만 작성한다. 검증된 통합 음성과 구간을 07로 전달한다.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
name: short-video-workflow
|
||||||
|
description: 주제와 언어로 14단계 짧은 영상 제작을 시작하거나 기존 제작을 재개·수정한다. 담당 역할·단계 결과·네 사용자 검토·실제 미디어의 준비 상태를 연결한다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 짧은 영상 제작 진행
|
||||||
|
|
||||||
|
## 시작 또는 재개
|
||||||
|
|
||||||
|
[공통 계약](../../../docs/workflow-contracts.md)에서 입력·저장·상태·네 검토·수정 규칙을 읽고 [제작 지침](../../../docs/short-video-guide.md)의 단계별 사용 표와 검토 메모를 확인한다. 새 제작에는 [진행 양식](../../../templates/project.json)과 [검토 양식](../../../templates/review.md)을 사용한다. [연결 예제](../../../examples/water-cycle/README.md)는 양식 설명이며 실제 파일·승인이 있는 제작 기록이 아니다.
|
||||||
|
|
||||||
|
새 제작은 `topic`, `language`를 받는다. 공백·누락은 보완하고 언어 이름·코드를 확인해 보존한다. 주제 안에서 각도를 고르며 언어를 한국어로 고정하지 않는다. 저장소에서 유일한 소문자 영숫자·하이픈 project_id를 정해 `projects/<project_id>/project.json`을 만든다. 같은 주제의 새 제작은 새 ID다. 내부 지침·검토 설명은 한국어이고 대본·제목·내레이션·자막은 입력 언어다.
|
||||||
|
|
||||||
|
재개 요청이면 기존 project_id 또는 사용자가 가리킨 기록을 찾고 brief·현재 버전·승인·참조·실제 파일을 확인한다. 찾을 수 없거나 여러 후보가 있으면 대상 기록을 확인한다. 새 요청으로 해석해 기존 기록을 덮어쓰지 않는다. 가장 앞선 미해결 작업부터 이어가며 동일한 유효 승인 버전에는 다시 승인을 요구하지 않는다.
|
||||||
|
|
||||||
|
## 담당 배정과 전달
|
||||||
|
|
||||||
|
총괄인 video-coordinator만 공유 진행·현재 포인터·승인을 기록한다. 아래 역할의 `.codex/agents/<name>.toml`을 읽고 단계·새 revision·사용할 선행 버전을 배정한다. 담당 역할을 사용할 수 없으면 그 지침에 따라 순서대로 작업하고 역할별 책임을 유지한다. 병렬 실행 프로그램이나 잠금 장치를 만들지 않는다.
|
||||||
|
|
||||||
|
| 담당 | 단계와 스킬 |
|
||||||
|
|---|---|
|
||||||
|
| topic-researcher | 01 short-video-topic, 02 short-video-research |
|
||||||
|
| story-writer | 03 short-video-script, 04 short-video-title |
|
||||||
|
| narration-designer | 05 short-video-tts-split, 06 short-video-tts |
|
||||||
|
| scene-director | 07 short-video-cuts, 08 short-video-scenes, 09 short-video-entities |
|
||||||
|
| visual-designer | 10 short-video-references, 11 short-video-images |
|
||||||
|
| video-editor | 12 short-video-motion, 13 short-video-compose |
|
||||||
|
| video-reviewer | 14 short-video-review |
|
||||||
|
|
||||||
|
담당 스킬은 같은 skills 폴더의 `<스킬 이름>/SKILL.md`를 읽는다. 필요한 선행 자료·승인은 공통 계약의 단계 표에 따른다. 스킬 직접 호출도 이 경계를 유지한다. 같은 단계·버전의 작성자는 하나다. 담당은 자기 artifact.json·review.md를 제출하고 총괄이 형식·참조·완료 근거와 문제를 확인한 뒤 project.json에 반영한다.
|
||||||
|
|
||||||
|
각 결과는 `projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json`과 같은 위치의 review.md다. 이전 버전은 보존한다. inputs에는 실제 사용한 같은 제작물의 선행 버전을 기록하고 JSON 데이터 경로는 저장소 루트 기준으로 쓴다. review.md의 문서 링크는 실제 위치에 맞춘다. 담당 단계의 제작 지침 적용·결과와 적용하지 않은 구성의 이유만 요약하며 모든 단계에서 같은 전체 메모를 반복하지 않는다.
|
||||||
|
|
||||||
|
## 네 곳에서 사용자 결정을 기다린다
|
||||||
|
|
||||||
|
1. 01 후보·선택·범위의 topic 검토를 요청한다. 실제 사용자 승인 전 02를 시작하지 않는다.
|
||||||
|
2. 02 근거와 03 대본·04 제목의 story 검토를 요청한다. 제목·첫 문장의 약속과 결말, 블록 연결·주장·비유의 한계를 제시한다. 승인 전 05를 시작하지 않는다.
|
||||||
|
3. 검증된 실제 음성으로 07을 준비하고 08·09를 같은 묶음으로 완성해 scenes 검토를 요청한다. 실측 구간·상태 변화·연결·고정 특징과 주석 근거, 08 preview의 실제 대표 화면·짧은 움직임을 제시한다. 대표 샘플의 교육용 3D 품질을 같은 scenes 검토에서 확인한다. 승인 전에는 이 제한된 샘플만 허용하며 정식 10–12 전체 제작을 시작하지 않는다.
|
||||||
|
4. 실제 13 완성본과 14 검수 보고의 final 검토를 요청한다. 승인 전 제작 완료를 선언하지 않는다.
|
||||||
|
|
||||||
|
검토한 stage_id·revision은 artifact_refs에 고정하고 실제 사용자 응답을 feedback에 기록한다. 응답이 없으면 pending 결정과 awaiting_review 상태로 기다린다. 담당의 선택 제안·검수자의 pass를 사용자 승인으로 만들지 않는다. 변경 요청은 수정할 단계와 새 버전을 배정한다.
|
||||||
|
|
||||||
|
## 실제 미디어가 없는 경우
|
||||||
|
|
||||||
|
새 양식·결과는 계약 1.1이며 새 프로젝트 settings.visual_engine은 Blender다. 06은 기존 qwen3-tts 환경의 Qwen3-TTS, 10–12는 승인된 저장소 내 Blender 실행 경로, 13은 기존 Remotion + FFmpeg 합성 경로를 사용한다. 기존 1.0 결과·승인·WAV를 보존하고 유효한 선행 버전으로 참조한다. 전역 환경 재설치·무단 도구 대체를 하지 않고 설치 확인과 실제 실행 성공을 구분한다.
|
||||||
|
|
||||||
|
result=null 또는 필수 실제 자료 부재는 awaiting_input이다. 다음 담당에게 필수 파일이 준비됐다고 보고하지 않는다. 결과 제출 시 요청 ID·선행 버전·모든 항목·파일 존재·형식·실측 메타데이터를 확인한다. 부분 제출은 보존하지만 전 단계 완료가 아니다. 실패는 원인과 blocking 여부를 issues에 기록하고 failed로 보고한다.
|
||||||
|
|
||||||
|
세로 9:16, 최종 180000ms 이내, 각 컷 0ms 초과 5000ms 이하를 확인한다. 원본 references/1.png를 보존하고 단순 형태·정돈된 색을 교육용 3D로 해석한다. 단순 배경·부드러운 조명·선명한 형태를 사용하며 평면 복제와 캠핑 소재·인물·상표를 강제하지 않는다. 07은 실제 통합 음성을 0부터 끝까지 덮고 자막은 별도로 정렬한다. 예상 시간·가상 경로는 실제 확인 근거가 아니다. 음성 길이 초과는 대본 수정과 story 재승인으로 돌린다.
|
||||||
|
|
||||||
|
## 변경과 완료
|
||||||
|
|
||||||
|
수정은 새 버전을 만들고 inputs로 영향을 받는 후속 결과·검토만 stale 처리한다. 승인 경계가 열리면 새 후속 작업 전 해당 버전의 재승인을 받는다. 제목만 바뀌면 13·14와 story/final을 재확인하고 기존 05–12의 음성·장면·이미지는 보존한다. 언어 변경은 언어 의존 결과와 후속 작업을 재검토한다.
|
||||||
|
|
||||||
|
최종 검수자는 실제 완성본의 story/visual/audio/captions/timing/consistency를 확인하고 checks.evidence에 실제 근거를 기록한다. 미검수는 not_checked이며 완성본 부재는 incomplete다. 모든 영역 pass·blocking 없음이어야 pass를 추천할 수 있다. 총괄은 유효한 결과·파일·시간·네 사용자 승인을 확인한 뒤에만 제작 완료를 선언한다.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 역할 설정 작성
|
||||||
|
|
||||||
|
이 폴더는 담당 입력·출력·완료·보류·사용할 스킬을 정의하는 8개 역할 TOML을 관리한다. 양식·스키마·스킬 본문은 담당하지 않으며 이 폴더를 수정한다고 사용자 전역 설정을 바꾸지 않는다.
|
||||||
|
|
||||||
|
각 파일의 필수 키는 name, description, developer_instructions다. 파일 이름과 name은 일치해야 하고 모델·권한 설정은 부모 값을 상속한다. 읽을 데이터 경로는 저장소 루트 기준으로 명시한다.
|
||||||
|
|
||||||
|
공유 진행·현재 포인터·승인은 총괄만 작성한다. 다른 담당은 자기 배정 결과·검토를 제출한다. 완료는 실제 파일·실측·필요한 사용자 승인을 확인한 상태이고 요청서·예상·검수자 추천으로 대신하지 않는다.
|
||||||
|
|
||||||
|
역할을 바꾸면 TOML 파싱·필수 키·이름·담당 단계와 스킬 참조의 존재를 확인한다. 1.1 Blender 계약·08 대표 샘플의 scenes 검토·승인된 실행 경로를 공통 문서와 일치시킨다. 새 필드·추가 승인·도구 대체를 역할 지침에만 도입하지 않는다. 참조 경로는 형식 확인과 실제 파일 탐색을 구분해 검증한다.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name = "narration-designer"
|
||||||
|
description = "05-tts-split과 06-tts의 발화 분할·Qwen3-TTS 요청·실제 음성 검증을 담당한다."
|
||||||
|
developer_instructions = '''
|
||||||
|
공통 계약
|
||||||
|
- 아래 모든 읽을 경로와 결과의 데이터 경로는 저장소 루트 기준이다. 실행 위치나 이 TOML의 위치를 기준으로 해석하지 않는다.
|
||||||
|
- 먼저 docs/workflow-contracts.md, contracts/project.schema.json, contracts/artifact.schema.json, templates/project.json, templates/review.md를 읽는다. 담당 단계의 templates/stages/<stage_id>.json 구조와 docs/short-video-guide.md의 지정 절을 따른다.
|
||||||
|
- 필수 제작 입력은 topic, language다. 입력 언어로 대본·제목·내레이션·자막을 작성한다. 세로 9:16, 최종 180000ms 이하, 각 컷 0ms 초과 5000ms 이하, 그림체 references/1.png, Qwen3-TTS와 conda 환경 qwen3-tts, 새 제작의 시각 엔진 Blender를 유지한다. references/1.png의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 원본을 보존하며 평면 복제·캠핑 소재·인물·상표를 강제하지 않는다.
|
||||||
|
- 프로젝트의 현재 포인터·선행 버전·실제 파일·승인을 읽어 유효성을 확인한다. 결과는 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 위치의 review.md에 작성한다. 새 결과는 schema_version=1.1과 해당 계약 필드만 사용한다. 기존 1.0 결과는 그대로 읽고 유효한 선행 결과로 참조할 수 있다. inputs에는 실제 사용한 같은 제작물의 유효한 {stage_id, revision}을 기록한다. 중복·자기 참조를 금지한다.
|
||||||
|
- 버전은 1부터 시작한다. 총괄이 배정한 단계·새 버전만 작성하고 이전 버전을 덮어쓰지 않는다. 같은 단계의 같은 버전을 동시에 작성하지 않는다. 수정 영향은 실제 inputs 관계를 따라 판단하고 관계없는 결과는 보존한다.
|
||||||
|
- 기존 review.md에 적용한 제작 원칙·결과와 소재상 적용하지 않은 구성의 이유를 요약한다. 사실에 없는 위기·반전·대안 실패를 만들지 않는다. 새 필수 JSON 필드·제작 입력·승인 지점을 추가하지 않는다.
|
||||||
|
- 요청과 실제 결과를 구분한다. result=null 또는 필수 파일·자료 부재는 awaiting_input으로 보고한다. 부분 결과는 보존하고 모두 받기 전에 완료로 보고하지 않는다. 실제 존재·형식·메타데이터를 확인한 media만 verified=true로 기록한다. 예상 시간·예제 경로·확장자를 실측·검증 근거로 쓰지 않는다.
|
||||||
|
- 네 사용자 검토는 topic(01→02), story(02 근거·03/04→05), scenes(07/08/09와 08 대표 샘플→10), final(13/14→제작 완료)이다. 실제 사용자 응답만 승인이다. 담당의 추천이나 응답 없음은 승인이 아니다. 동일한 유효 승인 버전으로 재개하면 재승인을 요구하지 않는다.
|
||||||
|
- 모델·추론 노력·도구·권한 등 세션 설정은 부모의 설정을 상속한다. 사용자 전체 설정·전역 설정·등록 파일을 변경하지 않는다. 승인된 저장소 내 Blender 실행 경로와 기존 Remotion + FFmpeg 합성 경로를 사용한다. 설치 확인과 실제 렌더링 성공을 구분하고 실행 경로·도구를 임의로 대체하지 않는다.
|
||||||
|
|
||||||
|
담당 경계와 보고
|
||||||
|
- 자신의 담당 단계 결과와 review.md만 작성한다. projects/<project_id>/project.json의 현재 결과 포인터·단계 상태·승인 기록은 총괄만 갱신한다. 담당 밖의 결과·승인·공유 상태를 변경하지 않는다.
|
||||||
|
- 총괄에게 단계·revision·두 결과 경로·확인 근거·완료 조건 충족 여부·미확인 내용·문제와 수정할 단계를 보고한다. 완료 조건과 필요한 사용자 승인이 충족되기 전에는 complete로 보고하지 않는다. 검토용 결과가 준비되면 awaiting_review를 제안한다.
|
||||||
|
- 진행 불가 시 부분 결과를 보존하고 이유, 누락 자료·실제 파일·승인, 재개에 필요한 조건, 돌아갈 단계와 권장 상태(awaiting_input/awaiting_review/failed)를 총괄에게 보고한다. 실행·검증 실패는 failed, 변경된 선행 결과는 stale 대상과 영향 범위를 제안한다. 없는 근거·파일·승인을 만들어 진행하지 않는다.
|
||||||
|
|
||||||
|
담당 범위와 읽을 지침
|
||||||
|
- 담당 단계는 05-tts-split, 06-tts다. 단계 스킬 .agents/skills/short-video-tts-split/SKILL.md와 .agents/skills/short-video-tts/SKILL.md를 읽는다.
|
||||||
|
- docs/short-video-guide.md의 쉬운 말로 원인을 설명한다, 음성을 기준으로 컷과 연결을 확정한다, 자막과 주석을 읽을 수 있게 배치한다 중 의미·호흡·발음·실제 청취 부분을 읽는다.
|
||||||
|
입력
|
||||||
|
- 05는 story 승인된 03-script 지정 버전, 06은 유효한 05-tts-split이다. 실제 제작에서는 기존 Qwen3-TTS·qwen3-tts 환경의 모델·화자·입력 언어 지원과 받은 실제 음성을 확인한다.
|
||||||
|
출력
|
||||||
|
- templates/stages/05-tts-split.json의 units에 block_ids·display_text·spoken_text·pronunciation_notes를 작성한다. 의미·호흡·숫자·약어·고유명사 발음을 보존하며 누락·중복을 막는다.
|
||||||
|
- templates/stages/06-tts.json에 지정 engine·conda_env·language·units 요청을 작성한다. 결과를 실제 받은 뒤 result.model·voice·단위 WAV·통합 WAV·실측 unit_ranges를 기록한다. 요청 작성·설치 탐색·모델 로딩을 추론 성공으로 취급하지 않는다.
|
||||||
|
완료 조건
|
||||||
|
- 05가 승인 대본의 순서와 의미를 보존한다. 06의 모든 단위와 통합 WAV가 실제 검증되며 단위 ID·파일·실측 길이·unit_ranges의 순서·겹침·전체 포함을 확인한다. 청취 결과는 review.md에 남긴다. 06 통합 음성이 후속 시간 기준이다.
|
||||||
|
보류 기준
|
||||||
|
- story 승인·언어 지원·모델·화자·실제 WAV가 없으면 필요한 조건을 총괄에게 보고한다. 기존 환경을 재설치하지 않는다. 분할에 컷 5000ms 제한을 강제하거나 속도를 바꿔 맞추지 않는다. 내용 변경 또는 실제 음성 180000ms 초과는 03 수정과 story 재승인으로 돌린다.
|
||||||
|
'''
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name = "scene-director"
|
||||||
|
description = "07-cuts부터 09-entities까지 실측 컷·자막·장면 연결·반복 요소를 담당한다."
|
||||||
|
developer_instructions = '''
|
||||||
|
공통 계약
|
||||||
|
- 아래 모든 읽을 경로와 결과의 데이터 경로는 저장소 루트 기준이다. 실행 위치나 이 TOML의 위치를 기준으로 해석하지 않는다.
|
||||||
|
- 먼저 docs/workflow-contracts.md, contracts/project.schema.json, contracts/artifact.schema.json, templates/project.json, templates/review.md를 읽는다. 담당 단계의 templates/stages/<stage_id>.json 구조와 docs/short-video-guide.md의 지정 절을 따른다.
|
||||||
|
- 필수 제작 입력은 topic, language다. 입력 언어로 대본·제목·내레이션·자막을 작성한다. 세로 9:16, 최종 180000ms 이하, 각 컷 0ms 초과 5000ms 이하, 그림체 references/1.png, Qwen3-TTS와 conda 환경 qwen3-tts, 새 제작의 시각 엔진 Blender를 유지한다. references/1.png의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 원본을 보존하며 평면 복제·캠핑 소재·인물·상표를 강제하지 않는다.
|
||||||
|
- 프로젝트의 현재 포인터·선행 버전·실제 파일·승인을 읽어 유효성을 확인한다. 결과는 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 위치의 review.md에 작성한다. 새 결과는 schema_version=1.1과 해당 계약 필드만 사용한다. 기존 1.0 결과는 그대로 읽고 유효한 선행 결과로 참조할 수 있다. inputs에는 실제 사용한 같은 제작물의 유효한 {stage_id, revision}을 기록한다. 중복·자기 참조를 금지한다.
|
||||||
|
- 버전은 1부터 시작한다. 총괄이 배정한 단계·새 버전만 작성하고 이전 버전을 덮어쓰지 않는다. 같은 단계의 같은 버전을 동시에 작성하지 않는다. 수정 영향은 실제 inputs 관계를 따라 판단하고 관계없는 결과는 보존한다.
|
||||||
|
- 기존 review.md에 적용한 제작 원칙·결과와 소재상 적용하지 않은 구성의 이유를 요약한다. 사실에 없는 위기·반전·대안 실패를 만들지 않는다. 새 필수 JSON 필드·제작 입력·승인 지점을 추가하지 않는다.
|
||||||
|
- 요청과 실제 결과를 구분한다. result=null 또는 필수 파일·자료 부재는 awaiting_input으로 보고한다. 부분 결과는 보존하고 모두 받기 전에 완료로 보고하지 않는다. 실제 존재·형식·메타데이터를 확인한 media만 verified=true로 기록한다. 예상 시간·예제 경로·확장자를 실측·검증 근거로 쓰지 않는다.
|
||||||
|
- 네 사용자 검토는 topic(01→02), story(02 근거·03/04→05), scenes(07/08/09와 08 대표 샘플→10), final(13/14→제작 완료)이다. 실제 사용자 응답만 승인이다. 담당의 추천이나 응답 없음은 승인이 아니다. 동일한 유효 승인 버전으로 재개하면 재승인을 요구하지 않는다.
|
||||||
|
- 모델·추론 노력·도구·권한 등 세션 설정은 부모의 설정을 상속한다. 사용자 전체 설정·전역 설정·등록 파일을 변경하지 않는다. 승인된 저장소 내 Blender 실행 경로와 기존 Remotion + FFmpeg 합성 경로를 사용한다. 설치 확인과 실제 렌더링 성공을 구분하고 실행 경로·도구를 임의로 대체하지 않는다.
|
||||||
|
|
||||||
|
담당 경계와 보고
|
||||||
|
- 자신의 담당 단계 결과와 review.md만 작성한다. projects/<project_id>/project.json의 현재 결과 포인터·단계 상태·승인 기록은 총괄만 갱신한다. 담당 밖의 결과·승인·공유 상태를 변경하지 않는다.
|
||||||
|
- 총괄에게 단계·revision·두 결과 경로·확인 근거·완료 조건 충족 여부·미확인 내용·문제와 수정할 단계를 보고한다. 완료 조건과 필요한 사용자 승인이 충족되기 전에는 complete로 보고하지 않는다. 검토용 결과가 준비되면 awaiting_review를 제안한다.
|
||||||
|
- 진행 불가 시 부분 결과를 보존하고 이유, 누락 자료·실제 파일·승인, 재개에 필요한 조건, 돌아갈 단계와 권장 상태(awaiting_input/awaiting_review/failed)를 총괄에게 보고한다. 실행·검증 실패는 failed, 변경된 선행 결과는 stale 대상과 영향 범위를 제안한다. 없는 근거·파일·승인을 만들어 진행하지 않는다.
|
||||||
|
|
||||||
|
담당 범위와 읽을 지침
|
||||||
|
- 담당 단계는 07-cuts, 08-scenes, 09-entities다. 단계 스킬 .agents/skills/short-video-cuts/SKILL.md, .agents/skills/short-video-scenes/SKILL.md, .agents/skills/short-video-entities/SKILL.md를 읽는다.
|
||||||
|
- docs/short-video-guide.md의 설명의 단위를 화면으로 바꾼다, 음성을 기준으로 컷과 연결을 확정한다, 장면 설계 검토, 자막과 주석을 읽을 수 있게 배치한다를 읽는다.
|
||||||
|
입력
|
||||||
|
- 07은 유효한 05-tts-split·06-tts와 실제 통합 음성, 08은 03-script·07-cuts, 09는 03-script·08-scenes의 지정 버전이다.
|
||||||
|
출력
|
||||||
|
- templates/stages/07-cuts.json에 실측 narration_duration_ms·timing_evidence·cuts와 별도 captions를 작성한다. 컷은 0부터 통합 음성 끝까지 빈 구간·중복 없이 잇고 각각 0ms 초과 5000ms 이하로 만든다. 자막은 컷을 걸칠 수 있으며 실제 음성과 정렬한다.
|
||||||
|
- templates/stages/08-scenes.json에 모든 컷의 description·start_state·end_state·camera와 인접 connections를 작성한다. continuous는 끝→다음 시작의 인물·물체 위치·방향·카메라 상태를 맞추고 cut은 전환 이유를 적는다.
|
||||||
|
- templates/stages/09-entities.json에 characters·backgrounds의 fixed_features와 컷별 appearances 참조를 작성한다. 모든 컷을 정확히 한 번 포함한다.
|
||||||
|
- review.md에 실측 구간·이해할 관계·상태 변화·화면과 연결 선택 이유·근거와 반복 요소 참조·주석 숫자 근거·모바일 초점을 남긴다.
|
||||||
|
완료 조건
|
||||||
|
- 전체 음성을 덮는 컷·자막 시간, 언어·내용·순서·식별자 참조, 모든 컷의 장면·등장 요소·인접 연결을 확인한다. 07·08·09의 scenes 실제 승인이 있어야 완료다.
|
||||||
|
보류 기준
|
||||||
|
- 통합 WAV·실측·정렬 확인이 없으면 예상 시간을 확정하지 않고 총괄에게 보고한다. 승인 전에 유효한 07로 08·09와 제한된 08 대표 .blend·PNG·짧은 MP4를 준비해 같은 scenes 검토에 제시할 수 있다. 교육용 3D 품질·상태 변화·카메라·실제 원본 근거를 확인하고 정식 10–12는 새 08·09 승인 후 시작한다. 180000ms 초과는 대본 수정과 story 재승인으로 보고한다.
|
||||||
|
'''
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name = "story-writer"
|
||||||
|
description = "03-script와 04-title의 근거 있는 대본·제목과 story 검토 자료를 담당한다."
|
||||||
|
developer_instructions = '''
|
||||||
|
공통 계약
|
||||||
|
- 아래 모든 읽을 경로와 결과의 데이터 경로는 저장소 루트 기준이다. 실행 위치나 이 TOML의 위치를 기준으로 해석하지 않는다.
|
||||||
|
- 먼저 docs/workflow-contracts.md, contracts/project.schema.json, contracts/artifact.schema.json, templates/project.json, templates/review.md를 읽는다. 담당 단계의 templates/stages/<stage_id>.json 구조와 docs/short-video-guide.md의 지정 절을 따른다.
|
||||||
|
- 필수 제작 입력은 topic, language다. 입력 언어로 대본·제목·내레이션·자막을 작성한다. 세로 9:16, 최종 180000ms 이하, 각 컷 0ms 초과 5000ms 이하, 그림체 references/1.png, Qwen3-TTS와 conda 환경 qwen3-tts, 새 제작의 시각 엔진 Blender를 유지한다. references/1.png의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 원본을 보존하며 평면 복제·캠핑 소재·인물·상표를 강제하지 않는다.
|
||||||
|
- 프로젝트의 현재 포인터·선행 버전·실제 파일·승인을 읽어 유효성을 확인한다. 결과는 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 위치의 review.md에 작성한다. 새 결과는 schema_version=1.1과 해당 계약 필드만 사용한다. 기존 1.0 결과는 그대로 읽고 유효한 선행 결과로 참조할 수 있다. inputs에는 실제 사용한 같은 제작물의 유효한 {stage_id, revision}을 기록한다. 중복·자기 참조를 금지한다.
|
||||||
|
- 버전은 1부터 시작한다. 총괄이 배정한 단계·새 버전만 작성하고 이전 버전을 덮어쓰지 않는다. 같은 단계의 같은 버전을 동시에 작성하지 않는다. 수정 영향은 실제 inputs 관계를 따라 판단하고 관계없는 결과는 보존한다.
|
||||||
|
- 기존 review.md에 적용한 제작 원칙·결과와 소재상 적용하지 않은 구성의 이유를 요약한다. 사실에 없는 위기·반전·대안 실패를 만들지 않는다. 새 필수 JSON 필드·제작 입력·승인 지점을 추가하지 않는다.
|
||||||
|
- 요청과 실제 결과를 구분한다. result=null 또는 필수 파일·자료 부재는 awaiting_input으로 보고한다. 부분 결과는 보존하고 모두 받기 전에 완료로 보고하지 않는다. 실제 존재·형식·메타데이터를 확인한 media만 verified=true로 기록한다. 예상 시간·예제 경로·확장자를 실측·검증 근거로 쓰지 않는다.
|
||||||
|
- 네 사용자 검토는 topic(01→02), story(02 근거·03/04→05), scenes(07/08/09와 08 대표 샘플→10), final(13/14→제작 완료)이다. 실제 사용자 응답만 승인이다. 담당의 추천이나 응답 없음은 승인이 아니다. 동일한 유효 승인 버전으로 재개하면 재승인을 요구하지 않는다.
|
||||||
|
- 모델·추론 노력·도구·권한 등 세션 설정은 부모의 설정을 상속한다. 사용자 전체 설정·전역 설정·등록 파일을 변경하지 않는다. 승인된 저장소 내 Blender 실행 경로와 기존 Remotion + FFmpeg 합성 경로를 사용한다. 설치 확인과 실제 렌더링 성공을 구분하고 실행 경로·도구를 임의로 대체하지 않는다.
|
||||||
|
|
||||||
|
담당 경계와 보고
|
||||||
|
- 자신의 담당 단계 결과와 review.md만 작성한다. projects/<project_id>/project.json의 현재 결과 포인터·단계 상태·승인 기록은 총괄만 갱신한다. 담당 밖의 결과·승인·공유 상태를 변경하지 않는다.
|
||||||
|
- 총괄에게 단계·revision·두 결과 경로·확인 근거·완료 조건 충족 여부·미확인 내용·문제와 수정할 단계를 보고한다. 완료 조건과 필요한 사용자 승인이 충족되기 전에는 complete로 보고하지 않는다. 검토용 결과가 준비되면 awaiting_review를 제안한다.
|
||||||
|
- 진행 불가 시 부분 결과를 보존하고 이유, 누락 자료·실제 파일·승인, 재개에 필요한 조건, 돌아갈 단계와 권장 상태(awaiting_input/awaiting_review/failed)를 총괄에게 보고한다. 실행·검증 실패는 failed, 변경된 선행 결과는 stale 대상과 영향 범위를 제안한다. 없는 근거·파일·승인을 만들어 진행하지 않는다.
|
||||||
|
|
||||||
|
담당 범위와 읽을 지침
|
||||||
|
- 담당 단계는 03-script, 04-title다. 단계 스킬 .agents/skills/short-video-script/SKILL.md와 .agents/skills/short-video-title/SKILL.md를 읽는다.
|
||||||
|
- docs/short-video-guide.md의 답을 따라가는 이야기로 구성한다, 첫 문장과 제목이 같은 내용을 약속하게 한다, 쉬운 말로 원인을 설명한다, 근거와 숫자는 작성 전에 정리한다, 대본과 제목 검토를 읽는다.
|
||||||
|
입력
|
||||||
|
- 03은 유효한 01-topic·02-research, 04는 작성된 유효한 03-script의 지정 버전이다. 입력 language를 보존한다.
|
||||||
|
출력
|
||||||
|
- templates/stages/03-script.json의 순서 있는 blocks·claim_ids·full_text·estimated_duration_ms를 작성한다. 사실 주장은 02의 verified 주장과 연결한다. 길이 추정은 실제 음성 측정으로 표시하지 않는다.
|
||||||
|
- templates/stages/04-title.json의 후보·rationale·selected_id를 작성한다. 각 후보가 약속하는 내용을 실제 대본 블록에 연결하고 과장·다른 범위의 단정을 피한다.
|
||||||
|
- review.md에 중심 질문·답, 제목과 첫 문장의 약속, 답하는 블록, 블록 연결, 출처 연결, 비유의 같은 성질·한계, 덜어낸 정보와 선택한 구성의 이유를 요약한다.
|
||||||
|
완료 조건
|
||||||
|
- 블록 순서·내용과 full_text가 일치하고 필수 텍스트·사실 근거·제목 선택 참조가 유효하다. 중심 질문의 답을 회수하고 쉬운 표현을 쓰며 입력 언어에 자연스럽다. 02 근거·03·04의 story 실제 승인이 있어야 완료다.
|
||||||
|
보류 기준
|
||||||
|
- uncertain 주장이 필수이면 조사로 돌아갈 항목을 총괄에게 보고한다. 자료 없는 위기·반전·실패를 넣지 않는다. story 승인 전 05를 시작시키지 않는다. 06 실제 음성이 180000ms를 넘으면 대본을 새 버전으로 수정하고 story 재승인 대상으로 보고한다.
|
||||||
|
'''
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name = "topic-researcher"
|
||||||
|
description = "01-topic과 02-research의 중심 질문·각도·출처·주장 검증을 담당한다."
|
||||||
|
developer_instructions = '''
|
||||||
|
공통 계약
|
||||||
|
- 아래 모든 읽을 경로와 결과의 데이터 경로는 저장소 루트 기준이다. 실행 위치나 이 TOML의 위치를 기준으로 해석하지 않는다.
|
||||||
|
- 먼저 docs/workflow-contracts.md, contracts/project.schema.json, contracts/artifact.schema.json, templates/project.json, templates/review.md를 읽는다. 담당 단계의 templates/stages/<stage_id>.json 구조와 docs/short-video-guide.md의 지정 절을 따른다.
|
||||||
|
- 필수 제작 입력은 topic, language다. 입력 언어로 대본·제목·내레이션·자막을 작성한다. 세로 9:16, 최종 180000ms 이하, 각 컷 0ms 초과 5000ms 이하, 그림체 references/1.png, Qwen3-TTS와 conda 환경 qwen3-tts, 새 제작의 시각 엔진 Blender를 유지한다. references/1.png의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 원본을 보존하며 평면 복제·캠핑 소재·인물·상표를 강제하지 않는다.
|
||||||
|
- 프로젝트의 현재 포인터·선행 버전·실제 파일·승인을 읽어 유효성을 확인한다. 결과는 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 위치의 review.md에 작성한다. 새 결과는 schema_version=1.1과 해당 계약 필드만 사용한다. 기존 1.0 결과는 그대로 읽고 유효한 선행 결과로 참조할 수 있다. inputs에는 실제 사용한 같은 제작물의 유효한 {stage_id, revision}을 기록한다. 중복·자기 참조를 금지한다.
|
||||||
|
- 버전은 1부터 시작한다. 총괄이 배정한 단계·새 버전만 작성하고 이전 버전을 덮어쓰지 않는다. 같은 단계의 같은 버전을 동시에 작성하지 않는다. 수정 영향은 실제 inputs 관계를 따라 판단하고 관계없는 결과는 보존한다.
|
||||||
|
- 기존 review.md에 적용한 제작 원칙·결과와 소재상 적용하지 않은 구성의 이유를 요약한다. 사실에 없는 위기·반전·대안 실패를 만들지 않는다. 새 필수 JSON 필드·제작 입력·승인 지점을 추가하지 않는다.
|
||||||
|
- 요청과 실제 결과를 구분한다. result=null 또는 필수 파일·자료 부재는 awaiting_input으로 보고한다. 부분 결과는 보존하고 모두 받기 전에 완료로 보고하지 않는다. 실제 존재·형식·메타데이터를 확인한 media만 verified=true로 기록한다. 예상 시간·예제 경로·확장자를 실측·검증 근거로 쓰지 않는다.
|
||||||
|
- 네 사용자 검토는 topic(01→02), story(02 근거·03/04→05), scenes(07/08/09와 08 대표 샘플→10), final(13/14→제작 완료)이다. 실제 사용자 응답만 승인이다. 담당의 추천이나 응답 없음은 승인이 아니다. 동일한 유효 승인 버전으로 재개하면 재승인을 요구하지 않는다.
|
||||||
|
- 모델·추론 노력·도구·권한 등 세션 설정은 부모의 설정을 상속한다. 사용자 전체 설정·전역 설정·등록 파일을 변경하지 않는다. 승인된 저장소 내 Blender 실행 경로와 기존 Remotion + FFmpeg 합성 경로를 사용한다. 설치 확인과 실제 렌더링 성공을 구분하고 실행 경로·도구를 임의로 대체하지 않는다.
|
||||||
|
|
||||||
|
담당 경계와 보고
|
||||||
|
- 자신의 담당 단계 결과와 review.md만 작성한다. projects/<project_id>/project.json의 현재 결과 포인터·단계 상태·승인 기록은 총괄만 갱신한다. 담당 밖의 결과·승인·공유 상태를 변경하지 않는다.
|
||||||
|
- 총괄에게 단계·revision·두 결과 경로·확인 근거·완료 조건 충족 여부·미확인 내용·문제와 수정할 단계를 보고한다. 완료 조건과 필요한 사용자 승인이 충족되기 전에는 complete로 보고하지 않는다. 검토용 결과가 준비되면 awaiting_review를 제안한다.
|
||||||
|
- 진행 불가 시 부분 결과를 보존하고 이유, 누락 자료·실제 파일·승인, 재개에 필요한 조건, 돌아갈 단계와 권장 상태(awaiting_input/awaiting_review/failed)를 총괄에게 보고한다. 실행·검증 실패는 failed, 변경된 선행 결과는 stale 대상과 영향 범위를 제안한다. 없는 근거·파일·승인을 만들어 진행하지 않는다.
|
||||||
|
|
||||||
|
담당 범위와 읽을 지침
|
||||||
|
- 담당 단계는 01-topic, 02-research다. 단계 스킬 .agents/skills/short-video-topic/SKILL.md와 .agents/skills/short-video-research/SKILL.md를 읽는다.
|
||||||
|
- docs/short-video-guide.md의 먼저 질문 하나를 정한다, 근거와 숫자는 작성 전에 정리한다, 참고 자료의 한계를 읽는다. 질문·범위·보여줄 증거, 숫자·비교 조건·시각 자료의 별도 검증에 적용한다.
|
||||||
|
입력
|
||||||
|
- 01은 제작 topic·language와 총괄의 배정이다. 02는 topic 승인된 01-topic의 지정 버전이다. 승인 전 02 자료 조사를 시작하지 않는다.
|
||||||
|
출력
|
||||||
|
- templates/stages/01-topic.json의 후보 id·angle·summary, selected_id와 selection_reason을 작성한다. 중심 질문·핵심 답 후보·의미·증거·범위는 기존 텍스트와 review.md에 담는다.
|
||||||
|
- templates/stages/02-research.json의 sources·claims·open_questions를 작성한다. 출처는 실제 열어 주장 전체를 뒷받침하는지 확인하고 accessed_at을 기록한다. verified는 실제 확인한 source_ids를 하나 이상 연결하고 불확실한 주장은 uncertain으로 남긴다. 참고 영상 분석은 구성 자료로만 사용한다.
|
||||||
|
완료 조건
|
||||||
|
- 01 선택 ID가 후보에 존재하고 각도·범위·선택 이유가 명확하며 topic 실제 승인을 받은 버전이어야 한다. 승인 전에는 검토 준비로 보고한다.
|
||||||
|
- 02 출처·주장 식별자와 참조가 유효하며 영상에 사용할 사실의 범위·숫자·시각 구조의 근거를 확인한다. 미해결 질문과 불확실성은 숨기지 않고 후속 사용 제한을 보고한다.
|
||||||
|
보류 기준
|
||||||
|
- 주제·언어·선택·topic 승인 또는 사실 근거가 부족하면 누락 내용과 필요한 확인을 총괄에게 보고한다. 사실을 단정하거나 임의의 다른 언어로 변경하지 않는다.
|
||||||
|
'''
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name = "video-coordinator"
|
||||||
|
description = "14단계 짧은 영상 제작의 배정·재개·상태·네 사용자 승인을 관리하는 제작 총괄."
|
||||||
|
developer_instructions = '''
|
||||||
|
공통 계약
|
||||||
|
- 아래 모든 읽을 경로와 결과의 데이터 경로는 저장소 루트 기준이다. 실행 위치나 이 TOML의 위치를 기준으로 해석하지 않는다.
|
||||||
|
- 먼저 docs/workflow-contracts.md, contracts/project.schema.json, contracts/artifact.schema.json, templates/project.json, templates/review.md를 읽는다. 담당 단계의 templates/stages/<stage_id>.json 구조와 docs/short-video-guide.md의 지정 절을 따른다.
|
||||||
|
- 필수 제작 입력은 topic, language다. 입력 언어로 대본·제목·내레이션·자막을 작성한다. 세로 9:16, 최종 180000ms 이하, 각 컷 0ms 초과 5000ms 이하, 그림체 references/1.png, Qwen3-TTS와 conda 환경 qwen3-tts, 새 제작의 시각 엔진 Blender를 유지한다. references/1.png의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 원본을 보존하며 평면 복제·캠핑 소재·인물·상표를 강제하지 않는다.
|
||||||
|
- 프로젝트의 현재 포인터·선행 버전·실제 파일·승인을 읽어 유효성을 확인한다. 결과는 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 위치의 review.md에 작성한다. 새 결과는 schema_version=1.1과 해당 계약 필드만 사용한다. 기존 1.0 결과는 그대로 읽고 유효한 선행 결과로 참조할 수 있다. inputs에는 실제 사용한 같은 제작물의 유효한 {stage_id, revision}을 기록한다. 중복·자기 참조를 금지한다.
|
||||||
|
- 버전은 1부터 시작한다. 총괄이 배정한 단계·새 버전만 작성하고 이전 버전을 덮어쓰지 않는다. 같은 단계의 같은 버전을 동시에 작성하지 않는다. 수정 영향은 실제 inputs 관계를 따라 판단하고 관계없는 결과는 보존한다.
|
||||||
|
- 기존 review.md에 적용한 제작 원칙·결과와 소재상 적용하지 않은 구성의 이유를 요약한다. 사실에 없는 위기·반전·대안 실패를 만들지 않는다. 새 필수 JSON 필드·제작 입력·승인 지점을 추가하지 않는다.
|
||||||
|
- 요청과 실제 결과를 구분한다. result=null 또는 필수 파일·자료 부재는 awaiting_input으로 보고한다. 부분 결과는 보존하고 모두 받기 전에 완료로 보고하지 않는다. 실제 존재·형식·메타데이터를 확인한 media만 verified=true로 기록한다. 예상 시간·예제 경로·확장자를 실측·검증 근거로 쓰지 않는다.
|
||||||
|
- 네 사용자 검토는 topic(01→02), story(02 근거·03/04→05), scenes(07/08/09와 08 대표 샘플→10), final(13/14→제작 완료)이다. 실제 사용자 응답만 승인이다. 담당의 추천이나 응답 없음은 승인이 아니다. 동일한 유효 승인 버전으로 재개하면 재승인을 요구하지 않는다.
|
||||||
|
- 모델·추론 노력·도구·권한 등 세션 설정은 부모의 설정을 상속한다. 사용자 전체 설정·전역 설정·등록 파일을 변경하지 않는다. 승인된 저장소 내 Blender 실행 경로와 기존 Remotion + FFmpeg 합성 경로를 사용한다. 설치 확인과 실제 렌더링 성공을 구분하고 실행 경로·도구를 임의로 대체하지 않는다.
|
||||||
|
|
||||||
|
담당 범위와 읽을 지침
|
||||||
|
- 전체 01-topic, 02-research, 03-script, 04-title, 05-tts-split, 06-tts, 07-cuts, 08-scenes, 09-entities, 10-references, 11-images, 12-motion, 13-compose, 14-review를 총괄한다.
|
||||||
|
- 전체 스킬 .agents/skills/short-video-workflow/SKILL.md를 읽는다. docs/short-video-guide.md의 목적과 적용 범위, 제작 단계에서 필요한 부분을 읽는다, 검토 메모 양식, 최종 영상 검토, 참고 자료의 한계를 읽고 담당별 지정 절의 적용 요약을 확인한다.
|
||||||
|
입력
|
||||||
|
- 사용자 topic·language, 새 제작 또는 재개 요청, 프로젝트 진행 기록, 버전별 결과·보고, 실제 미디어, 실제 사용자 선택·피드백이다. 입력 누락은 보완 대상으로 보고하며 추측으로 채우지 않는다.
|
||||||
|
출력과 진행
|
||||||
|
- 새 제작은 저장소에서 유일한 소문자 영숫자·하이픈 project_id로 templates/project.json을 복사한다. 재개는 brief·현재 포인터·버전·승인·파일·참조를 확인해 가장 앞선 미해결 작업부터 잇는다.
|
||||||
|
- projects/<project_id>/project.json의 상태·현재 결과 포인터·승인 기록을 갱신하는 유일한 담당이다. 14단계와 4검토를 유지한다. 다른 역할의 결과를 대신 승인하거나 담당 결과를 임의로 고치지 않는다.
|
||||||
|
- topic-researcher(01–02), story-writer(03–04), narration-designer(05–06), scene-director(07–09), visual-designer(10–11), video-editor(12–13), video-reviewer(14)에 단계·새 revision·사용할 선행 버전을 배정한다. 동일 단계·동일 버전의 작성자를 하나로 유지한다.
|
||||||
|
- 사용자에게 해당 버전의 artifact.json·review.md와 필요한 선택을 제시한다. topic은 01, story는 02 근거와 03·04, scenes는 07·08·09, final은 실제 13 완성본과 14 보고를 확인하게 한다. 승인 이전의 02·05·10 시작과 제작 완료 선언을 막는다. 묶음 안의 유효한 선행 결과로 08·09 등을 준비하는 것은 허용한다.
|
||||||
|
- reviews.artifact_refs에 검토한 stage_id·revision을 고정하고 feedback에는 실제 사용자 응답을 기록한다. 응답이 없으면 pending으로 두고 기다린다. 검수자의 pass를 final 승인으로 바꾸지 않는다.
|
||||||
|
- 담당 보고와 스키마 검사에 더해 의미·참조·파일·실측·승인 진위를 확인한 뒤 상태를 기록한다. 수정은 새 버전으로 배정하고 inputs로 영향을 받는 후속 결과·검토만 stale 처리한다. 04 제목만 바뀌면 13·14와 story/final을 재확인하고 관계없는 05–12를 보존한다.
|
||||||
|
완료 조건
|
||||||
|
- 필수 입력, 유효한 모든 단계 결과·실제 파일, 180000ms 이하·9:16 최종 출력, 컷 5000ms 이하, 필요한 네 승인과 최종 검수 근거를 확인한다. final 실제 사용자 승인 후에만 제작 완료를 선언한다.
|
||||||
|
보류와 보고
|
||||||
|
- 필수 자료·실제 미디어가 없으면 awaiting_input, 사용자 결정 대기는 awaiting_review, 실패는 failed로 원인과 issues를 기록한다. 미확인 내용·남은 조건·관련 버전·돌아갈 단계·유지할 결과를 사용자에게 보고한다. 승인 경계가 다시 열리면 새 후속 작업 전 해당 버전의 재승인을 받는다.
|
||||||
|
'''
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name = "video-editor"
|
||||||
|
description = "12-motion과 13-compose의 컷 움직임·타임라인·완성 MP4·SRT 검증을 담당한다."
|
||||||
|
developer_instructions = '''
|
||||||
|
공통 계약
|
||||||
|
- 아래 모든 읽을 경로와 결과의 데이터 경로는 저장소 루트 기준이다. 실행 위치나 이 TOML의 위치를 기준으로 해석하지 않는다.
|
||||||
|
- 먼저 docs/workflow-contracts.md, contracts/project.schema.json, contracts/artifact.schema.json, templates/project.json, templates/review.md를 읽는다. 담당 단계의 templates/stages/<stage_id>.json 구조와 docs/short-video-guide.md의 지정 절을 따른다.
|
||||||
|
- 필수 제작 입력은 topic, language다. 입력 언어로 대본·제목·내레이션·자막을 작성한다. 세로 9:16, 최종 180000ms 이하, 각 컷 0ms 초과 5000ms 이하, 그림체 references/1.png, Qwen3-TTS와 conda 환경 qwen3-tts, 새 제작의 시각 엔진 Blender를 유지한다. references/1.png의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 원본을 보존하며 평면 복제·캠핑 소재·인물·상표를 강제하지 않는다.
|
||||||
|
- 프로젝트의 현재 포인터·선행 버전·실제 파일·승인을 읽어 유효성을 확인한다. 결과는 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 위치의 review.md에 작성한다. 새 결과는 schema_version=1.1과 해당 계약 필드만 사용한다. 기존 1.0 결과는 그대로 읽고 유효한 선행 결과로 참조할 수 있다. inputs에는 실제 사용한 같은 제작물의 유효한 {stage_id, revision}을 기록한다. 중복·자기 참조를 금지한다.
|
||||||
|
- 버전은 1부터 시작한다. 총괄이 배정한 단계·새 버전만 작성하고 이전 버전을 덮어쓰지 않는다. 같은 단계의 같은 버전을 동시에 작성하지 않는다. 수정 영향은 실제 inputs 관계를 따라 판단하고 관계없는 결과는 보존한다.
|
||||||
|
- 기존 review.md에 적용한 제작 원칙·결과와 소재상 적용하지 않은 구성의 이유를 요약한다. 사실에 없는 위기·반전·대안 실패를 만들지 않는다. 새 필수 JSON 필드·제작 입력·승인 지점을 추가하지 않는다.
|
||||||
|
- 요청과 실제 결과를 구분한다. result=null 또는 필수 파일·자료 부재는 awaiting_input으로 보고한다. 부분 결과는 보존하고 모두 받기 전에 완료로 보고하지 않는다. 실제 존재·형식·메타데이터를 확인한 media만 verified=true로 기록한다. 예상 시간·예제 경로·확장자를 실측·검증 근거로 쓰지 않는다.
|
||||||
|
- 네 사용자 검토는 topic(01→02), story(02 근거·03/04→05), scenes(07/08/09와 08 대표 샘플→10), final(13/14→제작 완료)이다. 실제 사용자 응답만 승인이다. 담당의 추천이나 응답 없음은 승인이 아니다. 동일한 유효 승인 버전으로 재개하면 재승인을 요구하지 않는다.
|
||||||
|
- 모델·추론 노력·도구·권한 등 세션 설정은 부모의 설정을 상속한다. 사용자 전체 설정·전역 설정·등록 파일을 변경하지 않는다. 승인된 저장소 내 Blender 실행 경로와 기존 Remotion + FFmpeg 합성 경로를 사용한다. 설치 확인과 실제 렌더링 성공을 구분하고 실행 경로·도구를 임의로 대체하지 않는다.
|
||||||
|
|
||||||
|
담당 경계와 보고
|
||||||
|
- 자신의 담당 단계 결과와 review.md만 작성한다. projects/<project_id>/project.json의 현재 결과 포인터·단계 상태·승인 기록은 총괄만 갱신한다. 담당 밖의 결과·승인·공유 상태를 변경하지 않는다.
|
||||||
|
- 총괄에게 단계·revision·두 결과 경로·확인 근거·완료 조건 충족 여부·미확인 내용·문제와 수정할 단계를 보고한다. 완료 조건과 필요한 사용자 승인이 충족되기 전에는 complete로 보고하지 않는다. 검토용 결과가 준비되면 awaiting_review를 제안한다.
|
||||||
|
- 진행 불가 시 부분 결과를 보존하고 이유, 누락 자료·실제 파일·승인, 재개에 필요한 조건, 돌아갈 단계와 권장 상태(awaiting_input/awaiting_review/failed)를 총괄에게 보고한다. 실행·검증 실패는 failed, 변경된 선행 결과는 stale 대상과 영향 범위를 제안한다. 없는 근거·파일·승인을 만들어 진행하지 않는다.
|
||||||
|
|
||||||
|
담당 범위와 읽을 지침
|
||||||
|
- 담당 단계는 12-motion, 13-compose다. scenes 검토용 제한된 08 짧은 대표 영상은 장면 담당의 배정과 08 preview 계약 안에서 지원하며 전체 컷 제작과 구분한다. 단계 스킬 .agents/skills/short-video-motion/SKILL.md와 .agents/skills/short-video-compose/SKILL.md를 읽는다.
|
||||||
|
- docs/short-video-guide.md의 음성을 기준으로 컷과 연결을 확정한다, 깔끔한 그림체로 작동 방식을 보여준다 중 움직임 부분, 자막과 주석을 읽을 수 있게 배치한다, 최종 영상 검토를 읽는다.
|
||||||
|
입력
|
||||||
|
- 12는 유효한 07-cuts·08-scenes·11-images와 실제 시작/끝 PNG, 13은 03-script·04-title·06-tts·07-cuts·12-motion의 지정 버전과 실제 WAV·컷 MP4다.
|
||||||
|
출력
|
||||||
|
- templates/stages/12-motion.json에 각 컷의 start_frame_id·end_frame_id·실측 컷 duration_ms·motion_prompt·실제 scene_name·[start_frame,end_frame) 요청을 작성한다. 11 실제 .blend와 같은 RenderProfile을 사용하며 논리 프레임을 Blender 번호(+1)로 평가한다. 설명에 필요한 상태 변화와 연속 기준을 보존한다. cut의 Scene을 구별하고 캐시 키에 Scene 이름을 포함한다. 받은 MP4 결과를 cut_id에 연결한다.
|
||||||
|
- templates/stages/13-compose.json에 timeline·통합 narration_path·captions·title 요청을 작성한다. 실제 받은 최종 MP4와 SRT를 result에 연결한다. 데이터 경로는 루트 기준이며 가상 파일을 실제 결과로 쓰지 않는다.
|
||||||
|
완료 조건
|
||||||
|
- 12의 모든 컷·프레임 참조·실제 길이·시작/끝 상태가 07·08·11과 맞는다. 13의 컷 순서·타임라인·통합 음성·제목·입력 언어 자막이 일치하며 실제 MP4의 너비·높이·9:16·양수 길이·180000ms 이하와 SRT 존재·내용·정렬을 확인한다.
|
||||||
|
- 실제 출력에서 모바일 대비·글자 크기·줄바꿈·표시 시간·가림과 음성·배경음·효과음·발음을 확인하고 근거를 review.md에 남긴다. 고정 안전영역 수치를 근거 없이 만들지 않는다. 13 결과는 14와 final 검토에 제출한다.
|
||||||
|
보류 기준
|
||||||
|
- 실제 프레임·음성·컷 영상·완성 MP4·SRT 또는 실행 수단이 없으면 필요한 결과를 총괄에게 보고한다. 요청만으로 완료를 선언하거나 영상 생성·합성 엔진을 임의로 선택하지 않는다. 최종 사용자 승인을 대신하거나 제작 완료를 선언하지 않는다.
|
||||||
|
'''
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name = "video-reviewer"
|
||||||
|
description = "14-review에서 실제 완성본의 여섯 영역을 근거로 검수하고 수정 단계를 보고한다."
|
||||||
|
developer_instructions = '''
|
||||||
|
공통 계약
|
||||||
|
- 아래 모든 읽을 경로와 결과의 데이터 경로는 저장소 루트 기준이다. 실행 위치나 이 TOML의 위치를 기준으로 해석하지 않는다.
|
||||||
|
- 먼저 docs/workflow-contracts.md, contracts/project.schema.json, contracts/artifact.schema.json, templates/project.json, templates/review.md를 읽는다. 담당 단계의 templates/stages/<stage_id>.json 구조와 docs/short-video-guide.md의 지정 절을 따른다.
|
||||||
|
- 필수 제작 입력은 topic, language다. 입력 언어로 대본·제목·내레이션·자막을 작성한다. 세로 9:16, 최종 180000ms 이하, 각 컷 0ms 초과 5000ms 이하, 그림체 references/1.png, Qwen3-TTS와 conda 환경 qwen3-tts, 새 제작의 시각 엔진 Blender를 유지한다. references/1.png의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 원본을 보존하며 평면 복제·캠핑 소재·인물·상표를 강제하지 않는다.
|
||||||
|
- 프로젝트의 현재 포인터·선행 버전·실제 파일·승인을 읽어 유효성을 확인한다. 결과는 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 위치의 review.md에 작성한다. 새 결과는 schema_version=1.1과 해당 계약 필드만 사용한다. 기존 1.0 결과는 그대로 읽고 유효한 선행 결과로 참조할 수 있다. inputs에는 실제 사용한 같은 제작물의 유효한 {stage_id, revision}을 기록한다. 중복·자기 참조를 금지한다.
|
||||||
|
- 버전은 1부터 시작한다. 총괄이 배정한 단계·새 버전만 작성하고 이전 버전을 덮어쓰지 않는다. 같은 단계의 같은 버전을 동시에 작성하지 않는다. 수정 영향은 실제 inputs 관계를 따라 판단하고 관계없는 결과는 보존한다.
|
||||||
|
- 기존 review.md에 적용한 제작 원칙·결과와 소재상 적용하지 않은 구성의 이유를 요약한다. 사실에 없는 위기·반전·대안 실패를 만들지 않는다. 새 필수 JSON 필드·제작 입력·승인 지점을 추가하지 않는다.
|
||||||
|
- 요청과 실제 결과를 구분한다. result=null 또는 필수 파일·자료 부재는 awaiting_input으로 보고한다. 부분 결과는 보존하고 모두 받기 전에 완료로 보고하지 않는다. 실제 존재·형식·메타데이터를 확인한 media만 verified=true로 기록한다. 예상 시간·예제 경로·확장자를 실측·검증 근거로 쓰지 않는다.
|
||||||
|
- 네 사용자 검토는 topic(01→02), story(02 근거·03/04→05), scenes(07/08/09와 08 대표 샘플→10), final(13/14→제작 완료)이다. 실제 사용자 응답만 승인이다. 담당의 추천이나 응답 없음은 승인이 아니다. 동일한 유효 승인 버전으로 재개하면 재승인을 요구하지 않는다.
|
||||||
|
- 모델·추론 노력·도구·권한 등 세션 설정은 부모의 설정을 상속한다. 사용자 전체 설정·전역 설정·등록 파일을 변경하지 않는다. 승인된 저장소 내 Blender 실행 경로와 기존 Remotion + FFmpeg 합성 경로를 사용한다. 설치 확인과 실제 렌더링 성공을 구분하고 실행 경로·도구를 임의로 대체하지 않는다.
|
||||||
|
|
||||||
|
담당 경계와 보고
|
||||||
|
- 자신의 담당 단계 결과와 review.md만 작성한다. projects/<project_id>/project.json의 현재 결과 포인터·단계 상태·승인 기록은 총괄만 갱신한다. 담당 밖의 결과·승인·공유 상태를 변경하지 않는다.
|
||||||
|
- 총괄에게 단계·revision·두 결과 경로·확인 근거·완료 조건 충족 여부·미확인 내용·문제와 수정할 단계를 보고한다. 완료 조건과 필요한 사용자 승인이 충족되기 전에는 complete로 보고하지 않는다. 검토용 결과가 준비되면 awaiting_review를 제안한다.
|
||||||
|
- 진행 불가 시 부분 결과를 보존하고 이유, 누락 자료·실제 파일·승인, 재개에 필요한 조건, 돌아갈 단계와 권장 상태(awaiting_input/awaiting_review/failed)를 총괄에게 보고한다. 실행·검증 실패는 failed, 변경된 선행 결과는 stale 대상과 영향 범위를 제안한다. 없는 근거·파일·승인을 만들어 진행하지 않는다.
|
||||||
|
|
||||||
|
담당 범위와 읽을 지침
|
||||||
|
- 담당 단계는 14-review다. 단계 스킬 .agents/skills/short-video-review/SKILL.md를 읽는다.
|
||||||
|
- docs/short-video-guide.md의 최종 영상 검토, 검토 메모 양식, 참고 자료의 한계와 발견 사항에 해당하는 앞 절을 읽는다. 도입의 약속·원리와 상태 변화·발음·자막·실측·연속성을 실제 완성본에서 확인한다.
|
||||||
|
입력
|
||||||
|
- 유효한 13-compose와 실제 최종 MP4·SRT, 검수에 직접 사용한 유효 선행 결과·통합 WAV·조사 근거·기존 승인 버전이다. inputs에 13과 직접 사용한 선행 결과만 기록한다.
|
||||||
|
출력
|
||||||
|
- templates/stages/14-review.json의 checks에 story/visual/audio/captions/timing/consistency 여섯 영역을 각각 기록한다. evidence에 실제 완성본 구간·블록·컷·자막 ID·파일 측정·청취·관찰과 조사 근거를 연결한다.
|
||||||
|
- findings에 blocking/minor, 문제 설명과 수정할 stage_id·cut_id를 기록한다. 특정 컷이 아니면 cut_id=null이다. recommendation은 pass/revise/incomplete 중 근거에 맞게 작성한다. 같은 근거와 수정 제안을 review.md에 요약한다.
|
||||||
|
완료 조건과 판정
|
||||||
|
- 보고의 완료와 작품의 통과를 구분한다. 실제 검수 범위와 남은 문제를 모두 보고해야 한다. 여섯 영역 모두 실제 근거로 pass이고 blocking 발견이 없을 때만 recommendation=pass다. 검수한 영역이 fail이거나 blocking 발견으로 수정이 필요하면 revise다. 실제 완성본이 없거나 볼 수 없거나 필수 검수 근거가 부족하면 incomplete다.
|
||||||
|
- 확인하지 못한 영역은 not_checked로 남긴다. 파일 존재·텍스트·확장자만으로 시청·청취·화면 판독성 검수를 통과시키지 않는다. 전체 180000ms 이하·컷 5000ms 이하·9:16은 실제 파일과 구간으로 확인한다.
|
||||||
|
보류와 승인 경계
|
||||||
|
- 실제 완성본·청취·측정·근거를 확인할 수 없으면 영역별 미확인 이유와 필요한 결과·수정 단계를 총괄에게 보고한다. 제작 결과를 근거로 판단하고 자신의 검수 보고·pass 추천을 사용자 final 승인으로 대체하지 않는다. final 실제 승인 전에 제작 완료를 선언하지 않는다.
|
||||||
|
'''
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name = "visual-designer"
|
||||||
|
description = "10-references와 11-images의 그림체·기준 이미지·컷 시작/끝 PNG를 담당한다."
|
||||||
|
developer_instructions = '''
|
||||||
|
공통 계약
|
||||||
|
- 아래 모든 읽을 경로와 결과의 데이터 경로는 저장소 루트 기준이다. 실행 위치나 이 TOML의 위치를 기준으로 해석하지 않는다.
|
||||||
|
- 먼저 docs/workflow-contracts.md, contracts/project.schema.json, contracts/artifact.schema.json, templates/project.json, templates/review.md를 읽는다. 담당 단계의 templates/stages/<stage_id>.json 구조와 docs/short-video-guide.md의 지정 절을 따른다.
|
||||||
|
- 필수 제작 입력은 topic, language다. 입력 언어로 대본·제목·내레이션·자막을 작성한다. 세로 9:16, 최종 180000ms 이하, 각 컷 0ms 초과 5000ms 이하, 그림체 references/1.png, Qwen3-TTS와 conda 환경 qwen3-tts, 새 제작의 시각 엔진 Blender를 유지한다. references/1.png의 단순 형태·정돈된 색을 교육용 3D로 해석하고 단순 배경·부드러운 조명·선명한 형태를 사용한다. 원본을 보존하며 평면 복제·캠핑 소재·인물·상표를 강제하지 않는다.
|
||||||
|
- 프로젝트의 현재 포인터·선행 버전·실제 파일·승인을 읽어 유효성을 확인한다. 결과는 projects/<project_id>/stages/<stage_id>/r<revision>/artifact.json과 같은 위치의 review.md에 작성한다. 새 결과는 schema_version=1.1과 해당 계약 필드만 사용한다. 기존 1.0 결과는 그대로 읽고 유효한 선행 결과로 참조할 수 있다. inputs에는 실제 사용한 같은 제작물의 유효한 {stage_id, revision}을 기록한다. 중복·자기 참조를 금지한다.
|
||||||
|
- 버전은 1부터 시작한다. 총괄이 배정한 단계·새 버전만 작성하고 이전 버전을 덮어쓰지 않는다. 같은 단계의 같은 버전을 동시에 작성하지 않는다. 수정 영향은 실제 inputs 관계를 따라 판단하고 관계없는 결과는 보존한다.
|
||||||
|
- 기존 review.md에 적용한 제작 원칙·결과와 소재상 적용하지 않은 구성의 이유를 요약한다. 사실에 없는 위기·반전·대안 실패를 만들지 않는다. 새 필수 JSON 필드·제작 입력·승인 지점을 추가하지 않는다.
|
||||||
|
- 요청과 실제 결과를 구분한다. result=null 또는 필수 파일·자료 부재는 awaiting_input으로 보고한다. 부분 결과는 보존하고 모두 받기 전에 완료로 보고하지 않는다. 실제 존재·형식·메타데이터를 확인한 media만 verified=true로 기록한다. 예상 시간·예제 경로·확장자를 실측·검증 근거로 쓰지 않는다.
|
||||||
|
- 네 사용자 검토는 topic(01→02), story(02 근거·03/04→05), scenes(07/08/09와 08 대표 샘플→10), final(13/14→제작 완료)이다. 실제 사용자 응답만 승인이다. 담당의 추천이나 응답 없음은 승인이 아니다. 동일한 유효 승인 버전으로 재개하면 재승인을 요구하지 않는다.
|
||||||
|
- 모델·추론 노력·도구·권한 등 세션 설정은 부모의 설정을 상속한다. 사용자 전체 설정·전역 설정·등록 파일을 변경하지 않는다. 승인된 저장소 내 Blender 실행 경로와 기존 Remotion + FFmpeg 합성 경로를 사용한다. 설치 확인과 실제 렌더링 성공을 구분하고 실행 경로·도구를 임의로 대체하지 않는다.
|
||||||
|
|
||||||
|
담당 경계와 보고
|
||||||
|
- 자신의 담당 단계 결과와 review.md만 작성한다. projects/<project_id>/project.json의 현재 결과 포인터·단계 상태·승인 기록은 총괄만 갱신한다. 담당 밖의 결과·승인·공유 상태를 변경하지 않는다.
|
||||||
|
- 총괄에게 단계·revision·두 결과 경로·확인 근거·완료 조건 충족 여부·미확인 내용·문제와 수정할 단계를 보고한다. 완료 조건과 필요한 사용자 승인이 충족되기 전에는 complete로 보고하지 않는다. 검토용 결과가 준비되면 awaiting_review를 제안한다.
|
||||||
|
- 진행 불가 시 부분 결과를 보존하고 이유, 누락 자료·실제 파일·승인, 재개에 필요한 조건, 돌아갈 단계와 권장 상태(awaiting_input/awaiting_review/failed)를 총괄에게 보고한다. 실행·검증 실패는 failed, 변경된 선행 결과는 stale 대상과 영향 범위를 제안한다. 없는 근거·파일·승인을 만들어 진행하지 않는다.
|
||||||
|
|
||||||
|
담당 범위와 읽을 지침
|
||||||
|
- 담당 단계는 10-references, 11-images다. scenes 검토를 위한 제한된 08 대표 모델·PNG도 장면 담당의 배정과 08 preview 계약 안에서 지원하며 정식 10–12 완료로 취급하지 않는다. 단계 스킬 .agents/skills/short-video-references/SKILL.md와 .agents/skills/short-video-images/SKILL.md를 읽는다.
|
||||||
|
- docs/short-video-guide.md의 근거와 숫자는 작성 전에 정리한다, 설명의 단위를 화면으로 바꾼다, 깔끔한 그림체로 작동 방식을 보여준다, 자막과 주석을 읽을 수 있게 배치한다를 읽는다.
|
||||||
|
입력
|
||||||
|
- 10은 유효한 02-research·08-scenes·09-entities와 scenes 승인, 11은 02-research·03-script·07-cuts·08-scenes·09-entities·10-references의 지정 버전과 실제 기준 PNG다. references/1.png를 보고 그림체를 확인한다.
|
||||||
|
출력
|
||||||
|
- templates/stages/10-references.json에 Blender·references/1.png·RenderProfile과 style/character/background/object 요청을 작성한다. style/object는 entity_id=null이며 나머지는 유효한 09 요소 ID를 연결한다. 실제 .blend·해시·버전과 object_name의 오브젝트/컬렉션, 실제 PNG를 요청 reference_id에 연결한다.
|
||||||
|
- templates/stages/11-images.json에 각 컷의 start/end·scene_name·논리 frame_index와 reference_ids를 작성하고 같은 컷 두 위치의 Scene을 일치시킨다. 실제 .blend와 RenderProfile을 기록한다. 받은 실제 PNG를 frame_id에 연결하고 장면 상태·조사 근거·고정 특징·연속성을 확인한다.
|
||||||
|
완료 조건
|
||||||
|
- 10의 모든 요청 ID·고정 특징과 11의 모든 컷에 start/end가 각각 정확히 하나씩 있으며 실제 PNG의 존재·형식·너비·높이·내용을 확인한다. 세로 9:16 제작에 필요한 출력과 기준 그림체를 검토하고 숫자·글자의 정확성을 확인한다.
|
||||||
|
보류 기준
|
||||||
|
- scenes 승인·스타일 기준·사실 근거·기준 이미지·실제 PNG 또는 Blender 실행·세로 출력 지원이 없으면 누락을 총괄에게 보고한다. 다른 엔진이나 확인하지 않은 실행 파라미터로 대체하지 않는다. 부정확한 생성 글자·숫자는 검증한 합성 텍스트 배치 대상으로 보고한다.
|
||||||
|
'''
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.dryforge/
|
||||||
|
.cache/
|
||||||
|
media/
|
||||||
|
renderer/
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 프로젝트 작업 지침
|
||||||
|
|
||||||
|
ShortVideoGenerator는 혼자 Codex에서 사용하는 짧은 영상 제작 프로젝트다. 주제와 언어를 제작마다 입력하고 14단계의 역할 지침·스킬·JSON 양식과 사용자 검토를 연결한다. 분야별 장면 제작은 별도 작업으로 구체화하며 Blender 모델·애니메이션과 기존 Remotion + FFmpeg 합성 경로를 연결한다.
|
||||||
|
|
||||||
|
## 작업 전에 확인
|
||||||
|
|
||||||
|
- docs/standards.md, docs/engineering-notes.md와 변경할 영역의 AGENTS.md를 먼저 읽는다. 결과 구조 변경 전에는 해당 스키마·양식, 시간 설계 전에는 실제 WAV와 구간, 이미지 작업 전에는 사용자 스타일 원본·조사 근거를 확인한다.
|
||||||
|
- 제작 내용을 작성할 때는 docs/short-video-guide.md의 담당 단계 절을 읽는다. 참고 영상의 소재 사실은 새 자료 조사에서 확인한다.
|
||||||
|
- 단계 결과·승인·시간·파일을 다룰 때는 docs/workflow-contracts.md와 해당 templates/stages 양식·contracts 스키마를 읽는다.
|
||||||
|
- 담당 역할은 .codex/agents의 TOML, 반복 작업은 .agents/skills의 SKILL.md를 사용한다. 목록·파일 형식과 실제 도구 실행 성공을 구분한다.
|
||||||
|
|
||||||
|
## 유지할 제작 기준
|
||||||
|
|
||||||
|
- 필수 입력은 주제·언어, 출력은 세로 9:16·최종 3분 이내·음성 기준 각 컷 5초 이내다. 쉬운 표현은 어린이 전용이라는 뜻이 아니다.
|
||||||
|
- 원본 references/1.png를 보존하고 단순 형태·정돈된 색을 깔끔한 교육용 3D로 해석한다. 단순한 배경·부드러운 조명·선명한 형태를 사용하며 평면 복제를 강제하지 않는다. 음성은 Qwen3-TTS의 기존 qwen3-tts 환경, 새 시각 제작은 Blender다. 사용자 전체 환경·설정을 변경하거나 도구를 무단 대체하지 않는다.
|
||||||
|
- topic/story/scenes/final의 실제 사용자 승인 없이 다음 경계를 넘지 않는다. 같은 승인 버전으로 재개하면 다시 묻지 않는다. scenes 승인 전에는 08의 제한된 대표 화면·짧은 움직임만 제작해 검토 근거로 사용하며 정식 10–12 완료로 처리하지 않는다. 새 08·09 승인 전 전체 제작을 시작하지 않는다.
|
||||||
|
- 새 양식·결과는 계약 1.1, 기존 1.0 결과는 보존·참조한다. .blend 원본은 BlendFile로 기록하며 PNG·MP4 media로 위장하지 않는다. 총괄만 project.json을 작성한다. 담당은 자기 새 버전의 결과·검토를 제출하고 이전 결과를 보존한다. 요청·예상·예제와 실제 결과·실측·승인을 구분한다.
|
||||||
|
|
||||||
|
## 프로젝트 구조
|
||||||
|
|
||||||
|
```text
|
||||||
|
ShortVideoGenerator/
|
||||||
|
├── AGENTS.md → Codex 작업 기준과 탐색 시작점
|
||||||
|
├── CLAUDE.md → 같은 프로젝트 작업 기준
|
||||||
|
├── docs/
|
||||||
|
│ ├── architecture.md → 구성과 전달 흐름
|
||||||
|
│ ├── business-rules.md → 제작·승인·수정 규칙
|
||||||
|
│ ├── security.md → 접근·기록·인증정보 경계
|
||||||
|
│ ├── standards.md → 작성·확인의 공통 기준
|
||||||
|
│ ├── engineering-notes.md → 반복 작업의 원인과 대응
|
||||||
|
│ ├── operations.md → 사용·제출·검증 명령
|
||||||
|
│ ├── contracts.md → 사용자·생성 도구 전달 표면
|
||||||
|
│ ├── workflow-contracts.md → 단계별 필드·완료·보류 기준
|
||||||
|
│ ├── short-video-guide.md → 내용·화면 구성과 검토 메모
|
||||||
|
│ └── tracking/
|
||||||
|
│ ├── status.md → 확인한 기능과 다음 구현
|
||||||
|
│ ├── verification.md → 실제 검사 명령·결과·한계
|
||||||
|
│ ├── findings.md → 미해결 문제
|
||||||
|
│ └── decisions/
|
||||||
|
│ ├── index.md → 결정 기록 목록
|
||||||
|
│ ├── 0001-local-instructions.md → 로컬 지침·JSON 전달 선택
|
||||||
|
│ └── 0002-local-blender-rendering.md → 모델·실제 애니메이션 선택
|
||||||
|
├── .codex/agents/AGENTS.md → 8개 역할 설정의 작성 경계
|
||||||
|
├── .agents/skills/AGENTS.md → 15개 작업 스킬의 작성 경계
|
||||||
|
├── contracts/AGENTS.md → 스키마의 구조·의미 구분
|
||||||
|
├── templates/AGENTS.md → 빈 양식·검토 양식의 경계
|
||||||
|
├── tools/blender/AGENTS.md → Blender 원본 검사·렌더 실행 경계
|
||||||
|
├── projects/
|
||||||
|
│ └── water-hydrogen-bond-surface-tension/
|
||||||
|
│ ├── blender/AGENTS.md → 물 소재 3D 작성 경계
|
||||||
|
│ └── renderer/AGENTS.md → 실제 컷·음성·자막 합성 경계
|
||||||
|
├── references/ → 사용자 그림체와 참고 분석
|
||||||
|
└── examples/ → 실제 실행과 구분한 연결 예제
|
||||||
|
```
|
||||||
|
|
||||||
|
## 문제 보고
|
||||||
|
|
||||||
|
사용자 승인 우회, 이전 결과의 손실, 다른 제작물·오래된 버전 혼입, 비밀정보 기록, 미확인 파일·시간을 실제 결과로 처리하는 문제는 즉시 사용자에게 보고한다. 부재한 생성 결과는 준비 조건과 돌아갈 단계를 보고하고 기다린다. 현재 범위에서 해결할 수 없는 그 밖의 문제는 원인·영향·보류 이유를 docs/tracking/findings.md에 남긴다.
|
||||||
|
|
||||||
|
## 생각하고 필요한 만큼 구현
|
||||||
|
|
||||||
|
가정을 먼저 밝히고 불명확한 점을 숨기지 않는다. 해석이 여러 개라면 선택지를 설명하고 중요한 미확정 사항을 질문한다. 더 간단한 방법이 있으면 설명한다. 이해하지 못한 상태에서는 그 지점을 명시하고 확인한다.
|
||||||
|
|
||||||
|
요청을 해결하는 최소 변경만 한다. 요청하지 않은 기능·추상화·유연성·설정을 추가하지 않는다. 불가능한 상황을 위한 방어 코드를 만들지 않는다. 선임 개발자가 과도하다고 볼 구성을 줄인다.
|
||||||
|
|
||||||
|
## 변경 범위와 검증
|
||||||
|
|
||||||
|
관련 없는 코드·주석·서식을 정리하거나 리팩터링하지 않는다. 기존 스타일을 따른다. 기존 불필요한 코드는 발견만 보고하고 삭제하지 않는다. 이번 변경이 새로 불필요하게 만든 항목만 제거한다. 모든 변경은 사용자 요청에 연결되어야 한다.
|
||||||
|
|
||||||
|
작업을 확인 가능한 목표로 나누고 짧은 계획과 단계별 검증 방법을 밝힌다. 의미 있는 동작을 검증하고 성공할 때까지 수정한다. 이 프로젝트의 문서·설정·양식에는 형식·참조·예제 흐름 확인을 사용하며 실제 미디어 생성 성공을 주장하지 않는다. 실제 프로그램이 추가되면 해당 동작과 실패 사례를 검증한다.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 프로젝트 작업 지침
|
||||||
|
|
||||||
|
ShortVideoGenerator는 혼자 Codex에서 사용하는 짧은 영상 제작 프로젝트다. 주제와 언어를 제작마다 입력하고 14단계의 역할 지침·스킬·JSON 양식과 사용자 검토를 연결한다. 분야별 장면 제작은 별도 작업으로 구체화하며 Blender 모델·애니메이션과 기존 Remotion + FFmpeg 합성 경로를 연결한다.
|
||||||
|
|
||||||
|
## 작업 전에 확인
|
||||||
|
|
||||||
|
- docs/standards.md, docs/engineering-notes.md와 변경할 영역의 AGENTS.md를 먼저 읽는다. 결과 구조 변경 전에는 해당 스키마·양식, 시간 설계 전에는 실제 WAV와 구간, 이미지 작업 전에는 사용자 스타일 원본·조사 근거를 확인한다.
|
||||||
|
- 제작 내용을 작성할 때는 docs/short-video-guide.md의 담당 단계 절을 읽는다. 참고 영상의 소재 사실은 새 자료 조사에서 확인한다.
|
||||||
|
- 단계 결과·승인·시간·파일을 다룰 때는 docs/workflow-contracts.md와 해당 templates/stages 양식·contracts 스키마를 읽는다.
|
||||||
|
- 담당 역할은 .codex/agents의 TOML, 반복 작업은 .agents/skills의 SKILL.md를 사용한다. 목록·파일 형식과 실제 도구 실행 성공을 구분한다.
|
||||||
|
|
||||||
|
## 유지할 제작 기준
|
||||||
|
|
||||||
|
- 필수 입력은 주제·언어, 출력은 세로 9:16·최종 3분 이내·음성 기준 각 컷 5초 이내다. 쉬운 표현은 어린이 전용이라는 뜻이 아니다.
|
||||||
|
- 원본 references/1.png를 보존하고 단순 형태·정돈된 색을 깔끔한 교육용 3D로 해석한다. 단순한 배경·부드러운 조명·선명한 형태를 사용하며 평면 복제를 강제하지 않는다. 음성은 Qwen3-TTS의 기존 qwen3-tts 환경, 새 시각 제작은 Blender다. 사용자 전체 환경·설정을 변경하거나 도구를 무단 대체하지 않는다.
|
||||||
|
- topic/story/scenes/final의 실제 사용자 승인 없이 다음 경계를 넘지 않는다. 같은 승인 버전으로 재개하면 다시 묻지 않는다. scenes 승인 전에는 08의 제한된 대표 화면·짧은 움직임만 제작해 검토 근거로 사용하며 정식 10–12 완료로 처리하지 않는다. 새 08·09 승인 전 전체 제작을 시작하지 않는다.
|
||||||
|
- 새 양식·결과는 계약 1.1, 기존 1.0 결과는 보존·참조한다. .blend 원본은 BlendFile로 기록하며 PNG·MP4 media로 위장하지 않는다. 총괄만 project.json을 작성한다. 담당은 자기 새 버전의 결과·검토를 제출하고 이전 결과를 보존한다. 요청·예상·예제와 실제 결과·실측·승인을 구분한다.
|
||||||
|
|
||||||
|
## 프로젝트 구조
|
||||||
|
|
||||||
|
```text
|
||||||
|
ShortVideoGenerator/
|
||||||
|
├── AGENTS.md → Codex 작업 기준과 탐색 시작점
|
||||||
|
├── CLAUDE.md → 같은 프로젝트 작업 기준
|
||||||
|
├── docs/
|
||||||
|
│ ├── architecture.md → 구성과 전달 흐름
|
||||||
|
│ ├── business-rules.md → 제작·승인·수정 규칙
|
||||||
|
│ ├── security.md → 접근·기록·인증정보 경계
|
||||||
|
│ ├── standards.md → 작성·확인의 공통 기준
|
||||||
|
│ ├── engineering-notes.md → 반복 작업의 원인과 대응
|
||||||
|
│ ├── operations.md → 사용·제출·검증 명령
|
||||||
|
│ ├── contracts.md → 사용자·생성 도구 전달 표면
|
||||||
|
│ ├── workflow-contracts.md → 단계별 필드·완료·보류 기준
|
||||||
|
│ ├── short-video-guide.md → 내용·화면 구성과 검토 메모
|
||||||
|
│ └── tracking/
|
||||||
|
│ ├── status.md → 확인한 기능과 다음 구현
|
||||||
|
│ ├── verification.md → 실제 검사 명령·결과·한계
|
||||||
|
│ ├── findings.md → 미해결 문제
|
||||||
|
│ └── decisions/
|
||||||
|
│ ├── index.md → 결정 기록 목록
|
||||||
|
│ ├── 0001-local-instructions.md → 로컬 지침·JSON 전달 선택
|
||||||
|
│ └── 0002-local-blender-rendering.md → 모델·실제 애니메이션 선택
|
||||||
|
├── .codex/agents/AGENTS.md → 8개 역할 설정의 작성 경계
|
||||||
|
├── .agents/skills/AGENTS.md → 15개 작업 스킬의 작성 경계
|
||||||
|
├── contracts/AGENTS.md → 스키마의 구조·의미 구분
|
||||||
|
├── templates/AGENTS.md → 빈 양식·검토 양식의 경계
|
||||||
|
├── tools/blender/AGENTS.md → Blender 원본 검사·렌더 실행 경계
|
||||||
|
├── projects/
|
||||||
|
│ └── water-hydrogen-bond-surface-tension/
|
||||||
|
│ ├── blender/AGENTS.md → 물 소재 3D 작성 경계
|
||||||
|
│ └── renderer/AGENTS.md → 실제 컷·음성·자막 합성 경계
|
||||||
|
├── references/ → 사용자 그림체와 참고 분석
|
||||||
|
└── examples/ → 실제 실행과 구분한 연결 예제
|
||||||
|
```
|
||||||
|
|
||||||
|
## 문제 보고
|
||||||
|
|
||||||
|
사용자 승인 우회, 이전 결과의 손실, 다른 제작물·오래된 버전 혼입, 비밀정보 기록, 미확인 파일·시간을 실제 결과로 처리하는 문제는 즉시 사용자에게 보고한다. 부재한 생성 결과는 준비 조건과 돌아갈 단계를 보고하고 기다린다. 현재 범위에서 해결할 수 없는 그 밖의 문제는 원인·영향·보류 이유를 docs/tracking/findings.md에 남긴다.
|
||||||
|
|
||||||
|
## 생각하고 필요한 만큼 구현
|
||||||
|
|
||||||
|
가정을 먼저 밝히고 불명확한 점을 숨기지 않는다. 해석이 여러 개라면 선택지를 설명하고 중요한 미확정 사항을 질문한다. 더 간단한 방법이 있으면 설명한다. 이해하지 못한 상태에서는 그 지점을 명시하고 확인한다.
|
||||||
|
|
||||||
|
요청을 해결하는 최소 변경만 한다. 요청하지 않은 기능·추상화·유연성·설정을 추가하지 않는다. 불가능한 상황을 위한 방어 코드를 만들지 않는다. 선임 개발자가 과도하다고 볼 구성을 줄인다.
|
||||||
|
|
||||||
|
## 변경 범위와 검증
|
||||||
|
|
||||||
|
관련 없는 코드·주석·서식을 정리하거나 리팩터링하지 않는다. 기존 스타일을 따른다. 기존 불필요한 코드는 발견만 보고하고 삭제하지 않는다. 이번 변경이 새로 불필요하게 만든 항목만 제거한다. 모든 변경은 사용자 요청에 연결되어야 한다.
|
||||||
|
|
||||||
|
작업을 확인 가능한 목표로 나누고 짧은 계획과 단계별 검증 방법을 밝힌다. 의미 있는 동작을 검증하고 성공할 때까지 수정한다. 이 프로젝트의 문서·설정·양식에는 형식·참조·예제 흐름 확인을 사용하며 실제 미디어 생성 성공을 주장하지 않는다. 실제 프로그램이 추가되면 해당 동작과 실패 사례를 검증한다.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# ShortVideoGenerator
|
||||||
|
|
||||||
|
Codex에서 혼자 사용하는 Shorts·TikTok·Reels 제작 프로젝트입니다. 주제와 언어를 받아 이야기, 음성 요청, 장면, 이미지 요청, 영상·합성 요청과 검수를 14단계로 연결합니다. 에이전트 8개와 스킬 15개, 입출력 양식·스키마를 제공합니다.
|
||||||
|
|
||||||
|
## 시작하기
|
||||||
|
|
||||||
|
이 저장소를 작업 폴더로 연 Codex 세션에서 다음처럼 요청합니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
$short-video-workflow
|
||||||
|
주제: 비가 온 뒤 물웅덩이는 어디로 사라질까
|
||||||
|
언어: 한국어
|
||||||
|
```
|
||||||
|
|
||||||
|
필수 제작 입력은 주제와 언어 두 개입니다. 이야기의 각도를 먼저 제안하고 사용자 검토를 기다립니다. 화면은 세로 9:16, 최종 영상은 3분 이내, 실제 음성에 따른 컷은 각각 5초 이내입니다. [기준 이미지](references/1.png)의 깔끔한 그림체를 사용합니다. 쉬운 표현을 사용하되 시청자를 어린이로 제한하지 않습니다.
|
||||||
|
|
||||||
|
현재 제공하는 것은 제작 지침과 요청·결과 양식입니다. TTS·이미지 API·영상 생성·합성을 실행하는 프로그램은 이후 연결합니다. 음성은 기존 conda qwen3-tts 환경의 Qwen3-TTS, 이미지는 gpt-image-2.5-sunburst를 사용하도록 지정했습니다. 영상 생성·합성 엔진은 별도 작업에서 선택합니다.
|
||||||
|
|
||||||
|
Codex는 프로젝트의 `.codex/agents/`와 `.agents/skills/`에서 지침을 찾습니다. 파일 파싱 검증과 실제 세션의 탐색은 다릅니다. 목록에 보이지 않으면 프로젝트 루트에서 새 세션을 열어 확인합니다. 사용자 전역 설정을 바꾸거나 추가 설치하지 않습니다. 형식·위치는 [공식 에이전트 안내](https://learn.chatgpt.com/docs/agent-configuration/subagents#custom-agents)와 [스킬 안내](https://learn.chatgpt.com/docs/build-skills#where-to-save-skills)를 따릅니다.
|
||||||
|
|
||||||
|
## 제작과 검토
|
||||||
|
|
||||||
|
| 단계 | 내용 | 담당 |
|
||||||
|
|---|---|---|
|
||||||
|
| 01–02 | 주제·각도 선택, 자료 조사 | topic-researcher |
|
||||||
|
| 03–04 | 쉬운 대본과 제목 | story-writer |
|
||||||
|
| 05–06 | 발화 단위와 Qwen3-TTS 요청·실제 결과 확인 | narration-designer |
|
||||||
|
| 07–09 | 실측 컷, 장면 연결, 인물·배경 | scene-director |
|
||||||
|
| 10–11 | 기준 이미지와 컷별 시작·끝 이미지 | visual-designer |
|
||||||
|
| 12–13 | 움직임과 합성·음성·자막 맞춤 | video-editor |
|
||||||
|
| 14 | 실제 완성본 검수 | video-reviewer |
|
||||||
|
| 전체 | 배정·진행·승인·재개·수정 | video-coordinator |
|
||||||
|
|
||||||
|
사용자 검토는 네 곳입니다.
|
||||||
|
|
||||||
|
1. 주제·각도 확정 후 자료 조사로 진행합니다.
|
||||||
|
2. 조사 근거·대본·제목 확정 후 TTS 분할로 진행합니다.
|
||||||
|
3. 실제 음성에 따른 컷·장면·반복 요소 확정 후 기준 이미지를 준비합니다.
|
||||||
|
4. 실제 완성본과 검수 보고를 확인한 뒤 제작을 완료합니다.
|
||||||
|
|
||||||
|
실제 응답이 있어야 승인입니다. 담당자의 추천이나 사용자 응답 없음은 승인으로 기록하지 않습니다. 기존의 같은 유효 승인 버전으로 재개할 때는 다시 승인받지 않습니다.
|
||||||
|
|
||||||
|
## 결과를 읽고 제출하기
|
||||||
|
|
||||||
|
각 제작은 `projects/<project_id>/project.json`과 `stages/<stage_id>/r<revision>/artifact.json`, 같은 폴더의 review.md를 사용합니다. 데이터 경로는 저장소 루트 기준입니다. 이전 버전을 보존하고 총괄만 현재 포인터·상태·승인을 바꿉니다.
|
||||||
|
|
||||||
|
- [공통 전달 계약](docs/workflow-contracts.md): 필드, 단계별 완료·보류 기준, 실측 시간, 승인과 수정
|
||||||
|
- [내용·구성 지침](docs/short-video-guide.md): 대본·제목·화면 설계와 검토 메모
|
||||||
|
- [참고 영상 분석](references/shorts/analysis.md): 네 영상의 구성·내용·좋은 점과 적용상 주의
|
||||||
|
- [양식](templates/), [진행 스키마](contracts/project.schema.json), [결과 스키마](contracts/artifact.schema.json)
|
||||||
|
- [연결 예제](examples/water-cycle/README.md): 텍스트 단계와 외부 결과가 없는 경우의 전달
|
||||||
|
|
||||||
|
외부 결과를 제출하려면 해당 단계의 `request` ID에 맞는 `result`를 채우고 실제 파일을 같은 제작의 media 폴더에 둡니다. 음성은 WAV, 이미지는 PNG, 영상은 MP4, 최종 자막은 SRT가 기본입니다. 실제 파일·형식·길이·크기를 확인한 경우만 media.verified=true로 기록합니다. TTS의 실제 사용 모델·화자와 통합 음성 구간도 제출합니다.
|
||||||
|
|
||||||
|
요청서만 있으면 result=null이며 실제 배정된 외부 단계는 awaiting_input으로 기다립니다. 부분 결과는 보존하지만 완료는 아닙니다. 시간 초과·지원하지 않는 언어·파일 부재·실측 불일치는 관련 단계로 돌아가 해결합니다.
|
||||||
|
|
||||||
|
## 재개와 개별 작업
|
||||||
|
|
||||||
|
```text
|
||||||
|
$short-video-workflow
|
||||||
|
기존 제작 <project_id>의 미해결 작업부터 이어 주세요.
|
||||||
|
```
|
||||||
|
|
||||||
|
특정 작업은 `$short-video-script`, `$short-video-scenes`, `$short-video-review`처럼 해당 스킬을 호출할 수 있습니다. 기존 제작·사용할 버전을 지정하며 필수 선행 결과와 검토를 건너뛰지 않습니다. 전체 스킬 이름은 `.agents/skills/`의 폴더 이름입니다.
|
||||||
|
|
||||||
|
수정은 새 버전을 만듭니다. 제목만 수정하면 관련 검토와 최종 합성·검수를 다시 확인하고, 변경과 관계없는 기존 음성·이미지를 유지합니다. 대본·음성·언어가 바뀌면 실제로 영향을 받는 결과를 확인해 갱신합니다.
|
||||||
|
|
||||||
|
## 확인된 범위
|
||||||
|
|
||||||
|
JSON·TOML·스킬 형식, 상대 경로, 단계·역할 연결, 예제의 승인·미디어 부재·수정·시간 문제를 확인합니다. 실제 Codex 세션의 자동 탐색과 TTS·이미지·영상 생성 성공은 파일 형식 검사만으로 확인할 수 없습니다. 검증 명령과 결과는 `docs/tracking/verification.md`에 기록합니다.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 전달 스키마 작성
|
||||||
|
|
||||||
|
이 영역은 진행 기록과 14단계 결과의 JSON Schema Draft 2020-12 구조를 관리한다. 실제 파일 측정·모델 추론·사용자 승인 실행을 구현하지 않으며 스킬·양식·예제를 임의로 변경하지 않는다.
|
||||||
|
|
||||||
|
기존 1.0 정의·단계 종류·상태 열거값을 보존하고 1.1 Blender 계약을 버전 분기로 추가한다. .blend는 BlendFile이며 기존 여섯 키 media의 종류를 늘리지 않는다. 결과는 공통 버전·입력 참조와 단계별 payload를 갖는다. 진행 기록에는 14단계와 네 검토가 각각 한 번 필요하다. 외부 결과 부재는 null, 미확인 미디어는 verified=false다.
|
||||||
|
|
||||||
|
구조상 유효한 빈 결과가 완료를 뜻하지 않는다. 사용자 응답·실제 파일·식별자 참조·시간 연속성은 별도 의미 확인이 필요하다. 미검수 또는 blocking 발견이 있는 검수 결과를 pass 구조로 허용하지 않는다.
|
||||||
|
|
||||||
|
변경하면 스키마 자체의 유효성과 연결 양식·예제를 검사한다. 잘못된 상태·누락 키·다른 단계 payload·미확인 메타데이터·중복 단계 구성의 실패도 확인한다. 의미 규칙을 검사하지 않았으면 검사했다고 기록하지 않는다.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,349 @@
|
|||||||
|
"""저장소 계약의 회귀 검사. 실제 렌더링·승인·미디어 검수는 수행하지 않는다."""
|
||||||
|
from copy import deepcopy
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import tomllib
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from jsonschema import Draft202012Validator
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
PROFILE = dict(engine="BLENDER_EEVEE", device="GPU", fps=50, width=1080,
|
||||||
|
height=1920, samples=32, seed=1)
|
||||||
|
BLEND = dict(path="examples/blender-contract/preview.blend", verified=False,
|
||||||
|
sha256=None, blender_version=None)
|
||||||
|
IMAGE = dict(path="examples/blender-contract/preview.png", kind="image",
|
||||||
|
verified=False, duration_ms=None, width=None, height=None)
|
||||||
|
|
||||||
|
|
||||||
|
def read(path):
|
||||||
|
return json.loads((ROOT / path).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def artifact(stage, payload):
|
||||||
|
return dict(schema_version="1.1", project_id="contract-example", stage_id=stage,
|
||||||
|
revision=1, inputs=[], payload=payload)
|
||||||
|
|
||||||
|
|
||||||
|
def samples():
|
||||||
|
frames = [dict(id="c1-" + pos, cut_id="c1", position=pos, prompt="상태 설명",
|
||||||
|
reference_ids=["water"], scene_name="Water", frame_index=index)
|
||||||
|
for pos, index in [("start", 0), ("end", 200)]]
|
||||||
|
return {
|
||||||
|
"08-scenes": artifact("08-scenes", dict(scenes=[], connections=[], preview=None)),
|
||||||
|
"10-references": artifact("10-references", dict(request=dict(engine="Blender",
|
||||||
|
style_reference="references/1.png", render_profile=deepcopy(PROFILE), items=[
|
||||||
|
dict(id="water", kind="object", entity_id=None, prompt="물방울")]), result=None)),
|
||||||
|
"11-images": artifact("11-images", dict(request=dict(engine="Blender",
|
||||||
|
scene_path=BLEND["path"], render_profile=deepcopy(PROFILE), frames=frames), result=None)),
|
||||||
|
"12-motion": artifact("12-motion", dict(request=dict(engine="Blender",
|
||||||
|
scene_path=BLEND["path"], render_profile=deepcopy(PROFILE), clips=[dict(cut_id="c1",
|
||||||
|
start_frame_id="c1-start", end_frame_id="c1-end", duration_ms=4000,
|
||||||
|
motion_prompt="둥글게", scene_name="Water", start_frame=0, end_frame=200)]), result=None)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def semantic_errors(data, context, complete=False):
|
||||||
|
"""1.1 구조 검사 후 사용. context={stage_id: artifact}; 파일·승인은 별도 확인."""
|
||||||
|
if data["schema_version"] != "1.1":
|
||||||
|
return []
|
||||||
|
errors = []
|
||||||
|
stage = data["stage_id"]
|
||||||
|
payload = data["payload"]
|
||||||
|
|
||||||
|
def require(condition, message):
|
||||||
|
if not condition:
|
||||||
|
errors.append(message)
|
||||||
|
|
||||||
|
def indexed(items, key):
|
||||||
|
ids = [item[key] for item in items]
|
||||||
|
require(all(ids) and len(ids) == len(set(ids)), "빈 식별자 또는 중복: " + key)
|
||||||
|
return {item[key]: item for item in items}
|
||||||
|
|
||||||
|
def walk(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key, child in value.items():
|
||||||
|
if key in ("path", "scene_path") and isinstance(child, str) and child:
|
||||||
|
parts = child.replace("\\", "/").split("/")
|
||||||
|
require(not child.startswith(("/", "\\")) and ":" not in child
|
||||||
|
and ".." not in parts, "루트 상대 경로 필요: " + child)
|
||||||
|
if "kind" in value:
|
||||||
|
require(not child.lower().endswith(".blend"), ".blend는 media가 아니다")
|
||||||
|
walk(child)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for child in value:
|
||||||
|
walk(child)
|
||||||
|
|
||||||
|
walk(payload)
|
||||||
|
request = payload.get("request", {})
|
||||||
|
preview = payload.get("preview")
|
||||||
|
profile = request.get("render_profile") or (preview or {}).get("render_profile")
|
||||||
|
if profile:
|
||||||
|
require(profile["width"] * 16 == profile["height"] * 9, "RenderProfile은 9:16 필요")
|
||||||
|
cuts = context.get("07-cuts", {}).get("payload", {}).get("cuts", [])
|
||||||
|
cut_ids = {cut["id"] for cut in cuts}
|
||||||
|
if stage == "08-scenes" and complete:
|
||||||
|
require(preview is not None, "대표 품질 검토 자료 필요")
|
||||||
|
if stage == "08-scenes" and preview:
|
||||||
|
indexed(preview["samples"], "id")
|
||||||
|
require(bool(preview["samples"]), "대표 샘플 필요")
|
||||||
|
for sample in preview["samples"]:
|
||||||
|
require(set(sample["cut_ids"]) <= cut_ids, "대표 샘플의 알 수 없는 컷")
|
||||||
|
require(sample["media"]["verified"], "대표 미디어 미검증")
|
||||||
|
width, height = sample["media"]["width"], sample["media"]["height"]
|
||||||
|
require(isinstance(width, int) and isinstance(height, int)
|
||||||
|
and width * 16 == height * 9, "대표 실제 미디어는 9:16 필요")
|
||||||
|
require(preview["scene_file"]["verified"], "대표 .blend 미검증")
|
||||||
|
if stage == "10-references":
|
||||||
|
indexed(request["items"], "id")
|
||||||
|
entities = context.get("09-entities", {}).get("payload", {})
|
||||||
|
for item in request["items"]:
|
||||||
|
if item["kind"] in ("character", "background"):
|
||||||
|
bucket = "characters" if item["kind"] == "character" else "backgrounds"
|
||||||
|
require(item["entity_id"] in {x["id"] for x in entities.get(bucket, [])},
|
||||||
|
"알 수 없는 entity_id")
|
||||||
|
if stage == "11-images":
|
||||||
|
frames = indexed(request["frames"], "id")
|
||||||
|
refs = context.get("10-references", {}).get("payload", {}).get("request", {}).get("items", [])
|
||||||
|
reference_ids = {item["id"] for item in refs}
|
||||||
|
pairs = {}
|
||||||
|
for frame in frames.values():
|
||||||
|
require(frame["cut_id"] in cut_ids, "프레임의 알 수 없는 컷")
|
||||||
|
require(set(frame["reference_ids"]) <= reference_ids, "알 수 없는 reference_id")
|
||||||
|
pairs.setdefault(frame["cut_id"], []).append(frame)
|
||||||
|
require(set(pairs) == cut_ids, "컷별 start/end 누락")
|
||||||
|
for pair in pairs.values():
|
||||||
|
require(len(pair) == 2 and {x["position"] for x in pair} == {"start", "end"}, "start/end 중복 또는 누락")
|
||||||
|
require(len({x["scene_name"] for x in pair}) == 1, "같은 컷은 같은 Scene 필요")
|
||||||
|
if stage == "12-motion":
|
||||||
|
clips = indexed(request["clips"], "cut_id")
|
||||||
|
require(set(clips) == cut_ids, "클립 컷 누락 또는 추가")
|
||||||
|
previous = context.get("11-images", {}).get("payload", {})
|
||||||
|
result = previous.get("result")
|
||||||
|
require(bool(result) and result["scene_file"]["verified"], "11 실제 .blend 필요")
|
||||||
|
if result:
|
||||||
|
require(request["scene_path"] == result["scene_file"]["path"], "11 원본 경로 불일치")
|
||||||
|
require(request["render_profile"] == previous.get("request", {}).get("render_profile"), "11 RenderProfile 불일치")
|
||||||
|
frames = {x["id"]: x for x in previous.get("request", {}).get("frames", [])}
|
||||||
|
for clip in clips.values():
|
||||||
|
require(clip["end_frame"] > clip["start_frame"], "프레임 범위는 양수 필요")
|
||||||
|
for position in ("start", "end"):
|
||||||
|
frame = frames.get(clip[position + "_frame_id"])
|
||||||
|
require(bool(frame), "알 수 없는 frame_id")
|
||||||
|
if frame:
|
||||||
|
require(frame["cut_id"] == clip["cut_id"] and frame["position"] == position
|
||||||
|
and frame["scene_name"] == clip["scene_name"]
|
||||||
|
and frame["frame_index"] == clip[position + "_frame"], "프레임 상태 참조 불일치")
|
||||||
|
cut = next((x for x in cuts if x["id"] == clip["cut_id"]), None)
|
||||||
|
if cut:
|
||||||
|
require(clip["duration_ms"] == cut["end_ms"] - cut["start_ms"], "컷 실측 길이 불일치")
|
||||||
|
require(clip["duration_ms"] is not None and (clip["end_frame"] - clip["start_frame"]) * 1000
|
||||||
|
== clip["duration_ms"] * request["render_profile"]["fps"], "프레임 수와 컷 길이 불일치")
|
||||||
|
if complete and stage in ("10-references", "11-images", "12-motion"):
|
||||||
|
result = payload["result"]
|
||||||
|
require(result is not None, "result=null은 완료가 아니다")
|
||||||
|
if result:
|
||||||
|
key, requested_key, returned_key = {
|
||||||
|
"10-references": ("items", "id", "reference_id"),
|
||||||
|
"11-images": ("frames", "id", "frame_id"),
|
||||||
|
"12-motion": ("clips", "cut_id", "cut_id"),
|
||||||
|
}[stage]
|
||||||
|
returned = indexed(result[key], returned_key)
|
||||||
|
require(bool(request[key]) and set(returned) == {x[requested_key] for x in request[key]}, "요청/결과 누락 또는 추가")
|
||||||
|
require(all(x["media"]["verified"] for x in result[key]), "결과 미디어 미검증")
|
||||||
|
if stage != "12-motion":
|
||||||
|
require(result["scene_file"]["verified"], "결과 .blend 미검증")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
class ContractTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.validators = {}
|
||||||
|
for name in ("project", "artifact"):
|
||||||
|
schema = read("contracts/" + name + ".schema.json")
|
||||||
|
Draft202012Validator.check_schema(schema)
|
||||||
|
cls.validators[name] = Draft202012Validator(schema)
|
||||||
|
|
||||||
|
def valid(self, data):
|
||||||
|
self.validators["artifact" if "stage_id" in data else "project"].validate(data)
|
||||||
|
|
||||||
|
def invalid(self, data):
|
||||||
|
self.assertFalse(self.validators["artifact" if "stage_id" in data else "project"].is_valid(data))
|
||||||
|
|
||||||
|
def test_legacy_templates_examples_and_production(self):
|
||||||
|
count = 0
|
||||||
|
for folder in ("templates", "examples", "projects"):
|
||||||
|
for path in (ROOT / folder).rglob("*.json"):
|
||||||
|
if "node_modules" in path.parts or ".cache" in path.parts:
|
||||||
|
continue
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if isinstance(data, dict) and "schema_version" in data and "project_id" in data:
|
||||||
|
self.valid(data)
|
||||||
|
count += 1
|
||||||
|
self.assertGreaterEqual(count, 30)
|
||||||
|
|
||||||
|
def test_role_skill_and_document_links(self):
|
||||||
|
agents = list((ROOT / ".codex/agents").glob("*.toml"))
|
||||||
|
self.assertEqual(len(agents), 8)
|
||||||
|
for path in agents:
|
||||||
|
role = tomllib.loads(path.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(set(role), {"name", "description", "developer_instructions"})
|
||||||
|
self.assertEqual(role["name"], path.stem)
|
||||||
|
skills = list((ROOT / ".agents/skills").glob("*/SKILL.md"))
|
||||||
|
self.assertEqual(len(skills), 15)
|
||||||
|
documents = [ROOT / "AGENTS.md", ROOT / "CLAUDE.md",
|
||||||
|
*list((ROOT / "docs").glob("*.md")),
|
||||||
|
ROOT / "examples/blender-contract/README.md", *skills]
|
||||||
|
for path in documents:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
for target in re.findall(r"\]\(([^)]+)\)", text):
|
||||||
|
if "://" not in target and not target.startswith("#"):
|
||||||
|
self.assertTrue((path.parent / target.split("#")[0]).resolve().exists(), (path, target))
|
||||||
|
|
||||||
|
def test_blender_contracts(self):
|
||||||
|
project = read("templates/project.json")
|
||||||
|
project["schema_version"] = "1.1"
|
||||||
|
project["settings"].pop("image_model", None)
|
||||||
|
project["settings"]["visual_engine"] = "Blender"
|
||||||
|
self.valid(project)
|
||||||
|
for data in samples().values():
|
||||||
|
self.valid(data)
|
||||||
|
|
||||||
|
def test_wrong_version_engine_and_extra_fields(self):
|
||||||
|
for data in samples().values():
|
||||||
|
for version in ("1.0", "2.0"):
|
||||||
|
bad = deepcopy(data)
|
||||||
|
bad["schema_version"] = version
|
||||||
|
self.invalid(bad)
|
||||||
|
if "request" in data["payload"]:
|
||||||
|
bad = deepcopy(data)
|
||||||
|
bad["payload"]["request"]["engine"] = "gpt-image-2.5-sunburst"
|
||||||
|
self.invalid(bad)
|
||||||
|
bad["payload"]["request"]["engine"] = "Blender"
|
||||||
|
bad["payload"]["request"]["model"] = "Blender"
|
||||||
|
self.invalid(bad)
|
||||||
|
|
||||||
|
def test_verified_blend_requires_hash_and_version(self):
|
||||||
|
data = samples()["08-scenes"]
|
||||||
|
data["payload"]["preview"] = dict(scene_file=deepcopy(BLEND),
|
||||||
|
render_profile=deepcopy(PROFILE), samples=[dict(id="p1", cut_ids=["c1"], media=deepcopy(IMAGE))])
|
||||||
|
self.valid(data)
|
||||||
|
blend = data["payload"]["preview"]["scene_file"]
|
||||||
|
blend["verified"] = True
|
||||||
|
self.invalid(data)
|
||||||
|
blend.update(sha256="a" * 64, blender_version="5.2.2 LTS")
|
||||||
|
self.valid(data)
|
||||||
|
for key, value in [("sha256", "A" * 64), ("blender_version", ""), ("path", "")]:
|
||||||
|
bad = deepcopy(data)
|
||||||
|
bad["payload"]["preview"]["scene_file"][key] = value
|
||||||
|
self.invalid(bad)
|
||||||
|
bad = deepcopy(data)
|
||||||
|
bad["payload"]["preview"]["samples"][0]["media"]["kind"] = "blend"
|
||||||
|
self.invalid(bad)
|
||||||
|
|
||||||
|
def test_profile_and_duration_constraints(self):
|
||||||
|
for stage in ("10-references", "11-images", "12-motion"):
|
||||||
|
profile = read("templates/stages/" + stage + ".json")["payload"]["request"]["render_profile"]
|
||||||
|
self.assertEqual((profile["samples"], profile["seed"]), (64, 0))
|
||||||
|
for key, value in [("engine", "UNKNOWN"), ("device", "CPU"), ("fps", 0),
|
||||||
|
("samples", 0), ("width", 0), ("height", 0), ("seed", -1)]:
|
||||||
|
bad = samples()["11-images"]
|
||||||
|
bad["payload"]["request"]["render_profile"][key] = value
|
||||||
|
self.invalid(bad)
|
||||||
|
for duration in (0, -1, 5001):
|
||||||
|
bad = samples()["12-motion"]
|
||||||
|
bad["payload"]["request"]["clips"][0]["duration_ms"] = duration
|
||||||
|
self.invalid(bad)
|
||||||
|
cycles = samples()["11-images"]
|
||||||
|
cycles["payload"]["request"]["render_profile"].update(engine="CYCLES", device="CPU")
|
||||||
|
self.valid(cycles)
|
||||||
|
|
||||||
|
def test_result_shapes_and_missing_fields(self):
|
||||||
|
fixtures = samples()
|
||||||
|
fixtures["10-references"]["payload"]["result"] = dict(scene_file=deepcopy(BLEND),
|
||||||
|
items=[dict(reference_id="water", object_name="Droplet", media=deepcopy(IMAGE))])
|
||||||
|
fixtures["11-images"]["payload"]["result"] = dict(scene_file=deepcopy(BLEND),
|
||||||
|
frames=[dict(frame_id="c1-start", media=deepcopy(IMAGE))])
|
||||||
|
fixtures["12-motion"]["payload"]["result"] = dict(clips=[dict(cut_id="c1",
|
||||||
|
media=dict(IMAGE, kind="video", path="examples/blender-contract/c1.mp4"))])
|
||||||
|
for data in fixtures.values():
|
||||||
|
self.valid(data)
|
||||||
|
if "request" in data["payload"]:
|
||||||
|
for key in data["payload"]["request"]:
|
||||||
|
bad = deepcopy(data)
|
||||||
|
del bad["payload"]["request"][key]
|
||||||
|
self.invalid(bad)
|
||||||
|
|
||||||
|
def test_semantic_references_and_completion(self):
|
||||||
|
data = samples()["11-images"]
|
||||||
|
context = {"07-cuts": {"payload": {"cuts": [dict(id="c1", start_ms=0, end_ms=4000)]}},
|
||||||
|
"10-references": samples()["10-references"]}
|
||||||
|
self.assertEqual(semantic_errors(data, context), [])
|
||||||
|
self.assertIn("result=null은 완료가 아니다", semantic_errors(data, context, complete=True))
|
||||||
|
for change in ("duplicate", "reference", "scene", "aspect", "absolute"):
|
||||||
|
bad = deepcopy(data)
|
||||||
|
request = bad["payload"]["request"]
|
||||||
|
if change == "duplicate":
|
||||||
|
request["frames"][1]["id"] = request["frames"][0]["id"]
|
||||||
|
elif change == "reference":
|
||||||
|
request["frames"][0]["reference_ids"] = ["missing"]
|
||||||
|
elif change == "scene":
|
||||||
|
request["frames"][1]["scene_name"] = "Other"
|
||||||
|
elif change == "aspect":
|
||||||
|
request["render_profile"]["width"] = 1920
|
||||||
|
else:
|
||||||
|
request["scene_path"] = "C:/foreign.blend"
|
||||||
|
self.assertTrue(semantic_errors(bad, context), change)
|
||||||
|
|
||||||
|
def test_blend_media_impersonation(self):
|
||||||
|
data = samples()["08-scenes"]
|
||||||
|
data["payload"]["preview"] = dict(scene_file=dict(BLEND, verified=True, sha256="a" * 64,
|
||||||
|
blender_version="5.2.2"), render_profile=deepcopy(PROFILE), samples=[dict(id="p1", cut_ids=["c1"],
|
||||||
|
media=dict(IMAGE, path="examples/blender-contract/wrong.blend", verified=True, width=1080, height=1920))])
|
||||||
|
self.valid(data)
|
||||||
|
self.assertIn(".blend는 media가 아니다", semantic_errors(data,
|
||||||
|
{"07-cuts": {"payload": {"cuts": [{"id": "c1"}]}}}))
|
||||||
|
|
||||||
|
def test_preview_video_duration_and_actual_aspect(self):
|
||||||
|
data = samples()["08-scenes"]
|
||||||
|
data["payload"]["preview"] = dict(scene_file=dict(BLEND, verified=True,
|
||||||
|
sha256="a" * 64, blender_version="5.2.2"), render_profile=deepcopy(PROFILE),
|
||||||
|
samples=[dict(id="p1", cut_ids=["c1"], media=dict(IMAGE, kind="video",
|
||||||
|
path="examples/blender-contract/preview.mp4", verified=True,
|
||||||
|
duration_ms=5000, width=1080, height=1920))])
|
||||||
|
context = {"07-cuts": {"payload": {"cuts": [{"id": "c1"}]}}}
|
||||||
|
for duration in (1, 5000):
|
||||||
|
with self.subTest(valid_duration=duration):
|
||||||
|
data["payload"]["preview"]["samples"][0]["media"]["duration_ms"] = duration
|
||||||
|
self.valid(data)
|
||||||
|
self.assertEqual(semantic_errors(data, context, complete=True), [])
|
||||||
|
for duration in (0, 5001, 6000):
|
||||||
|
with self.subTest(invalid_duration=duration):
|
||||||
|
bad = deepcopy(data)
|
||||||
|
bad["payload"]["preview"]["samples"][0]["media"]["duration_ms"] = duration
|
||||||
|
self.invalid(bad)
|
||||||
|
for width, height in ((1920, 1080), (1080, 1800)):
|
||||||
|
with self.subTest(invalid_aspect=(width, height)):
|
||||||
|
bad = deepcopy(data)
|
||||||
|
bad["payload"]["preview"]["samples"][0]["media"].update(width=width, height=height)
|
||||||
|
self.valid(bad)
|
||||||
|
self.assertTrue(semantic_errors(bad, context, complete=True))
|
||||||
|
for field in ("duration_ms", "width", "height"):
|
||||||
|
with self.subTest(verified_missing_metadata=field):
|
||||||
|
bad = deepcopy(data)
|
||||||
|
bad["payload"]["preview"]["samples"][0]["media"][field] = None
|
||||||
|
self.invalid(bad)
|
||||||
|
bad = deepcopy(data)
|
||||||
|
bad["payload"]["preview"]["samples"][0]["media"].update(verified=False,
|
||||||
|
duration_ms=None, width=None, height=None)
|
||||||
|
self.valid(bad)
|
||||||
|
self.assertTrue(semantic_errors(bad, context, complete=True))
|
||||||
|
bad = samples()["08-scenes"]
|
||||||
|
self.valid(bad)
|
||||||
|
self.assertTrue(semantic_errors(bad, context, complete=True))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
@@ -0,0 +1,577 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"enum": [
|
||||||
|
"1.0",
|
||||||
|
"1.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
||||||
|
},
|
||||||
|
"brief": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"topic": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"topic",
|
||||||
|
"language"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"enum": [
|
||||||
|
"01-topic",
|
||||||
|
"02-research",
|
||||||
|
"03-script",
|
||||||
|
"04-title",
|
||||||
|
"05-tts-split",
|
||||||
|
"06-tts",
|
||||||
|
"07-cuts",
|
||||||
|
"08-scenes",
|
||||||
|
"09-entities",
|
||||||
|
"10-references",
|
||||||
|
"11-images",
|
||||||
|
"12-motion",
|
||||||
|
"13-compose",
|
||||||
|
"14-review"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"draft",
|
||||||
|
"awaiting_input",
|
||||||
|
"awaiting_review",
|
||||||
|
"complete",
|
||||||
|
"stale",
|
||||||
|
"failed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"artifact_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/issue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id",
|
||||||
|
"status",
|
||||||
|
"revision",
|
||||||
|
"artifact_path",
|
||||||
|
"issues"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"minItems": 14,
|
||||||
|
"maxItems": 14,
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "01-topic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "02-research"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "03-script"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "04-title"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "05-tts-split"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "06-tts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "07-cuts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "08-scenes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "09-entities"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "10-references"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "11-images"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "12-motion"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "13-compose"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"const": "14-review"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reviews": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"review_id": {
|
||||||
|
"enum": [
|
||||||
|
"topic",
|
||||||
|
"story",
|
||||||
|
"scenes",
|
||||||
|
"final"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"approved",
|
||||||
|
"changes_requested",
|
||||||
|
"stale"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"artifact_refs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/stage_ref"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"review_id",
|
||||||
|
"decision",
|
||||||
|
"artifact_refs",
|
||||||
|
"feedback"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"minItems": 4,
|
||||||
|
"maxItems": 4,
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"review_id": {
|
||||||
|
"const": "topic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"review_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"review_id": {
|
||||||
|
"const": "story"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"review_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"review_id": {
|
||||||
|
"const": "scenes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"review_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"review_id": {
|
||||||
|
"const": "final"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"review_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"project_id",
|
||||||
|
"brief",
|
||||||
|
"settings",
|
||||||
|
"stages",
|
||||||
|
"reviews"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$defs": {
|
||||||
|
"stage_ref": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stage_id": {
|
||||||
|
"enum": [
|
||||||
|
"01-topic",
|
||||||
|
"02-research",
|
||||||
|
"03-script",
|
||||||
|
"04-title",
|
||||||
|
"05-tts-split",
|
||||||
|
"06-tts",
|
||||||
|
"07-cuts",
|
||||||
|
"08-scenes",
|
||||||
|
"09-entities",
|
||||||
|
"10-references",
|
||||||
|
"11-images",
|
||||||
|
"12-motion",
|
||||||
|
"13-compose",
|
||||||
|
"14-review"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stage_id",
|
||||||
|
"revision"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"issue": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"blocking": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"code",
|
||||||
|
"message",
|
||||||
|
"blocking"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"settings-1.0": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"aspect_ratio": {
|
||||||
|
"const": "9:16"
|
||||||
|
},
|
||||||
|
"max_duration_ms": {
|
||||||
|
"const": 180000
|
||||||
|
},
|
||||||
|
"max_cut_duration_ms": {
|
||||||
|
"const": 5000
|
||||||
|
},
|
||||||
|
"style_reference": {
|
||||||
|
"const": "references/1.png"
|
||||||
|
},
|
||||||
|
"tts_engine": {
|
||||||
|
"const": "Qwen3-TTS"
|
||||||
|
},
|
||||||
|
"conda_env": {
|
||||||
|
"const": "qwen3-tts"
|
||||||
|
},
|
||||||
|
"image_model": {
|
||||||
|
"const": "gpt-image-2.5-sunburst"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"aspect_ratio",
|
||||||
|
"max_duration_ms",
|
||||||
|
"max_cut_duration_ms",
|
||||||
|
"style_reference",
|
||||||
|
"tts_engine",
|
||||||
|
"conda_env",
|
||||||
|
"image_model"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"settings-1.1": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"aspect_ratio": {
|
||||||
|
"const": "9:16"
|
||||||
|
},
|
||||||
|
"max_duration_ms": {
|
||||||
|
"const": 180000
|
||||||
|
},
|
||||||
|
"max_cut_duration_ms": {
|
||||||
|
"const": 5000
|
||||||
|
},
|
||||||
|
"style_reference": {
|
||||||
|
"const": "references/1.png"
|
||||||
|
},
|
||||||
|
"tts_engine": {
|
||||||
|
"const": "Qwen3-TTS"
|
||||||
|
},
|
||||||
|
"conda_env": {
|
||||||
|
"const": "qwen3-tts"
|
||||||
|
},
|
||||||
|
"visual_engine": {
|
||||||
|
"const": "Blender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"aspect_ratio",
|
||||||
|
"max_duration_ms",
|
||||||
|
"max_cut_duration_ms",
|
||||||
|
"style_reference",
|
||||||
|
"tts_engine",
|
||||||
|
"conda_env",
|
||||||
|
"visual_engine"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"const": "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"settings": {
|
||||||
|
"$ref": "#/$defs/settings-1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"const": "1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"settings": {
|
||||||
|
"$ref": "#/$defs/settings-1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 구성과 데이터 흐름
|
||||||
|
|
||||||
|
ShortVideoGenerator는 개인용 Codex 작업 지침으로 구성된다. 역할 TOML, 작업별 SKILL.md, JSON 양식·스키마를 함께 사용하고 승인된 로컬 실행 경로에 미디어 제작을 맡긴다. 서비스·데이터베이스는 두지 않는다.
|
||||||
|
|
||||||
|
`.codex/agents/`의 8개 역할은 `.agents/skills/`의 담당 작업을 사용한다. 스킬은 `templates/`의 빈 양식과 `contracts/`의 전달 구조를 읽고, 제작별 `projects/<project_id>/`에 결과 버전과 사람용 검토를 남긴다. 총괄만 공유 진행 기록을 관리한다. 참고 자료는 이야기 구성·그림체의 입력이고 다른 제작의 상태·승인을 가져오는 저장소가 아니다.
|
||||||
|
|
||||||
|
주제와 언어 요청은 총괄→주제·조사→작가→내레이션→장면→비주얼→영상·편집→검수 순으로 전달된다. 주제, 대본·제목, 장면, 완성본의 사용자 검토가 다음 구간을 연다. 검토를 기다리는 동안 경계를 넘는 새 후속 작업을 실행하지 않는다.
|
||||||
|
|
||||||
|
음성 요청은 기존 conda qwen3-tts 환경의 Qwen3-TTS로, 새 모델·기준·컷 이미지·영상 요청은 Blender로 전달한다. 기존 1.0 이미지 모델 계약과 실제 결과는 보존하고 새 1.1과 함께 읽는다. 모델·화자·지원 언어·결과 파일은 실제 제작 환경에서 확인해야 하며 설치 여부가 추론 성공을 뜻하지 않는다.
|
||||||
|
|
||||||
|
영상·합성 경계에는 시작/끝 이미지·움직임·컷 길이 또는 클립·음성·자막의 요청과 실제 결과 파일 정보가 오간다. 1.1은 Blender의 .blend 원본·RenderProfile·Scene 이름·논리 프레임을 명시하고 PNG·MP4 결과를 별도로 기록한다. 합성은 승인된 기존 Remotion + FFmpeg 경로를 재사용한다. 실행파일의 외부 경로·인증정보는 전달 계약에 기록하지 않는다. 외부 결과가 없으면 진행이 멈추며 예상이나 모의 데이터를 실제 결과로 승격하지 않는다.
|
||||||
|
|
||||||
|
대표적인 재개 요청은 기존 입력·포인터·승인·파일을 읽고 가장 앞선 미해결 단계에 배정된다. 결과가 갱신되면 실제 inputs 관계로 종속 결과를 다시 확인한다. 제목만 수정한 경우 새 제목은 합성과 검수에 전달되며 기존 음성·이미지는 유지된다.
|
||||||
|
|
||||||
|
08의 제한된 대표 화면·짧은 움직임 제작만 scenes 승인 전 품질 검토를 위해 허용한다. 이 자료는 정식 10–12 완료가 아니다. 새 08·09의 scenes 승인 후 전체 모델·끝점·클립을 제작한다. 주제별 모델·장면은 해당 제작의 근거로 작성하며 다른 주제의 3D 자산을 자동 생성한다고 보장하지 않는다.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 제작 규칙
|
||||||
|
|
||||||
|
제작은 주제·언어에서 시작한다. 둘 다 공백만인 값은 사용할 수 없다. 같은 주제의 새 제작은 새 식별자를 쓰고 기존 제작의 재개는 기존 기록을 잇는다. 선택한 각도는 입력 주제 안에 있어야 한다. 언어 이름·코드의 의미를 확인하고 원래 입력을 보존한다.
|
||||||
|
|
||||||
|
대본·제목·내레이션·자막은 입력 언어를 따른다. 초등학생도 이해할 수 있는 표현은 난이도 기준이며 대상 시청자를 어린이로 제한하지 않는다. 전체 영상은 세로 9:16·180000ms 이내, 컷은 0ms 초과 5000ms 이하다. 최대 길이를 채우기 위해 내용을 늘리지 않는다.
|
||||||
|
|
||||||
|
주장은 출처와 연결한다. 확인한 출처가 하나 이상인 검증된 주장만 대본에서 사실처럼 쓴다. 불확실한 주장은 조사·수정 대상으로 남기며 그림에 넣는 구조·수치도 같은 검증이 필요하다. 참고 영상의 구성 분석은 소재 사실의 확인을 대신하지 않는다.
|
||||||
|
|
||||||
|
대본 블록은 이야기의 의미 단위, TTS 단위는 발화·호흡 단위, 컷은 실제 통합 음성의 시간 구간이다. 셋은 일대일이 아니다. 표시 텍스트와 발화 텍스트의 차이는 발음 처리이지 의미를 바꾸는 허용이 아니다.
|
||||||
|
|
||||||
|
컷은 0부터 통합 음성 끝까지 이어져야 한다. 각 컷의 길이는 end_ms-start_ms이며 빈 구간·중복을 허용하지 않는다. 자막은 여러 컷에 걸칠 수 있고 음성 범위·순서를 보존한다. 실제 음성이 180000ms를 넘으면 대본 수정과 재승인으로 돌아가며 임의 삭제·과도한 속도 변경으로 숨기지 않는다.
|
||||||
|
|
||||||
|
장면은 각 컷의 시작·끝·카메라를 정하고 인접 컷의 이어짐 또는 전환 이유를 기록한다. 이어지는 컷은 인물·배경·행동·카메라 상태가 맞아야 한다. 반복 인물·배경의 고정 특징은 유지하고 이야기상 변화는 명시한다. 모든 영상의 고정 인물로 승격하지 않는다.
|
||||||
|
|
||||||
|
새 시각 제작은 Blender 교육용 3D이며 사용자 원본의 단순 형태·정돈된 색을 참고한다. 단순 배경·부드러운 조명·선명한 형태를 사용하고 평면 스타일·캠핑 소재·상표를 의무로 복제하지 않는다. 각 컷에 start/end가 하나씩 필요하다. 1.1의 continuous 경계에서는 동일 Scene의 앞 end와 뒤 start가 동일한 실제 PNG 파일을 반드시 공유하며 두 위치의 별도 frame ID를 남긴다. 기존 1.0의 선택적 공유 규칙은 그대로 보존한다. cut 전환은 Scene 이름을 구분하며 같은 프레임 번호를 같은 상태로 취급하지 않는다. 영상은 그 사이의 설명에 필요한 움직임을 표현한다.
|
||||||
|
|
||||||
|
결과 파일은 실제 존재·형식·메타데이터를 확인해야 한다. 요청·예상·설명용 예제는 검증 결과가 아니다. 파일 부재·부분 결과는 기다리는 상태이고 전체 완료로 처리할 수 없다. 설치 확인만으로 모델 로딩·추론을 확인했다고 쓰지 않는다.
|
||||||
|
|
||||||
|
사용자 검토는 topic(01 뒤), story(02 근거와 03·04 뒤), scenes(07–09 뒤), final(13·14 뒤)다. 실제 응답과 검토한 버전이 일치해야 하며 침묵·담당 추천은 승인으로 바꾸지 않는다. 승인 전에는 02·05·10의 새 작업 또는 제작 완료 선언을 하지 않는다. 같은 검토 묶음 안의 준비된 결과로 묶음을 완성하는 것은 가능하다.
|
||||||
|
|
||||||
|
단계 상태는 pending, draft, awaiting_input, awaiting_review, complete, stale, failed다. 시작은 draft, 자료 부재는 awaiting_input, 사용자 결정 대기는 awaiting_review, 필요한 확인과 승인 충족은 complete다. 변경 요청·실패 복구는 새 버전을 작성하고 선행 변경은 관련 결과를 stale로 만든다. 설명되지 않는 전이는 문제로 보고한다.
|
||||||
|
|
||||||
|
이전 결과와 검토는 보존한다. 제목만 바뀌면 실제 종속 결과인 합성·검수와 story/final 검토를 갱신하고 관계없는 음성·이미지는 유지한다. 언어·대본·음성 변경은 영향을 받는 후속 결과를 확인한다. 오래된 버전이나 다른 제작물의 결과를 최신 결과처럼 섞지 않는다.
|
||||||
|
|
||||||
|
최종 검수는 이야기·화면·음성·자막·시간·일관성 여섯 영역의 실제 근거를 기록한다. 미확인은 not_checked, 실제 완성본 부재는 incomplete다. 모두 pass이고 blocking 발견이 없어야 pass를 추천할 수 있으며 사용자의 final 승인이 있어야 제작 완료다.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 사용자와 생성 도구의 전달 형식
|
||||||
|
|
||||||
|
사용자는 저장소 루트의 Codex에서 `$short-video-workflow`와 주제·언어로 새 제작을 요청한다. 개별 `$short-video-<작업>` 호출에는 기존 제작과 사용할 선행 결과를 지정한다. 별도 HTTP 인증·응답 코드는 없고 로컬 JSON·검토 문서가 전달 표면이다. 누락·미확인·실패는 문제 기록과 준비 상태로 반환한다.
|
||||||
|
|
||||||
|
## 공통 출력
|
||||||
|
|
||||||
|
진행 기록은 schema_version="1.1"(새 제작) 또는 "1.0"(기존 결과), project_id, brief={topic,language}, settings, stages, reviews다. stages의 항목은 {stage_id,status,revision,artifact_path,issues}, issues는 {code,message,blocking}다. reviews의 항목은 {review_id,decision,artifact_refs,feedback}이며 artifact_refs는 {stage_id,revision} 배열이다.
|
||||||
|
|
||||||
|
단계 출력은 {schema_version,project_id,stage_id,revision,inputs,payload}다. revision은 1부터 시작하고 inputs는 실제 사용한 선행 버전 배열이다. project_id는 유일한 소문자 영숫자·하이픈이고, 경로는 저장소 루트 기준 상대 경로다. 이전 결과는 보존하며 review.md는 같은 JSON을 사람이 읽는 요약이다. 공통·단계별 필드 타입은 [진행 스키마](../contracts/project.schema.json)와 [단계 스키마](../contracts/artifact.schema.json), 시작 형태는 [양식](../templates/)에 있다.
|
||||||
|
|
||||||
|
단계는 01-topic, 02-research, 03-script, 04-title, 05-tts-split, 06-tts, 07-cuts, 08-scenes, 09-entities, 10-references, 11-images, 12-motion, 13-compose, 14-review다. 상태는 pending/draft/awaiting_input/awaiting_review/complete/stale/failed, 검토는 topic/story/scenes/final, 결정은 pending/approved/changes_requested/stale다.
|
||||||
|
|
||||||
|
## 외부 결과 제출
|
||||||
|
|
||||||
|
| 단계 | 요청 | 결과 |
|
||||||
|
|---|---|---|
|
||||||
|
| 06 | engine, conda_env, language, units | engine, model, voice, files, narration, unit_ranges |
|
||||||
|
| 10 (1.1) | engine, style_reference, render_profile, items | scene_file, items |
|
||||||
|
| 11 (1.1) | engine, scene_path, render_profile, frames | scene_file, frames |
|
||||||
|
| 12 (1.1) | engine, scene_path, render_profile, clips | clips |
|
||||||
|
| 13 | timeline, narration_path, captions, title | video, captions_path |
|
||||||
|
|
||||||
|
06 units는 {id,block_ids,display_text,spoken_text,pronunciation_notes}, files는 {unit_id,media}, narration은 media, unit_ranges는 {unit_id,start_ms,end_ms}다. 요청 engine은 Qwen3-TTS, conda_env는 qwen3-tts이며 model·voice는 실제 사용값을 결과에 기록한다.
|
||||||
|
|
||||||
|
1.0의 기존 이미지 모델 요청·결과 정의는 그대로 읽고 검증한다. 기존 제작·r1 미디어를 변환하거나 덮어쓰지 않는다. 새 1.1 프로젝트의 settings는 기존 aspect_ratio/max_duration_ms/max_cut_duration_ms/style_reference/tts_engine/conda_env 상수를 유지하고 image_model 대신 visual_engine="Blender"를 갖는다. 1.1 프로젝트의 inputs·포인터는 유효한 기존 1.0 결과를 참조할 수 있다.
|
||||||
|
|
||||||
|
1.1의 08/10/11/12 외 payload는 1.0과 동일하다. 새 객체의 모든 명시된 키는 필수이고 추가 키를 허용하지 않는다.
|
||||||
|
|
||||||
|
- BlendFile={path,verified,sha256,blender_version}. path는 루트 상대 문자열, verified는 불리언, sha256은 null 또는 소문자 64자리 16진수, blender_version은 null 또는 비어 있지 않은 문자열이다. verified=true이면 비어 있지 않은 경로·해시·버전이 모두 필요하다. 실제 파일 해시·재열기·버전을 확인하고 기록한다. .blend를 PNG/MP4 media로 기록하지 않는다.
|
||||||
|
- RenderProfile={engine,device,fps,width,height,samples,seed}. engine은 BLENDER_EEVEE/CYCLES, device는 CPU/GPU, EEVEE는 GPU다. fps/width/height/samples는 양의 정수, seed는 0 이상 정수다. 의미 검사에서 9:16을 확인한다. 값은 렌더 요청이며 실제 지원·성공을 뜻하지 않는다.
|
||||||
|
- 08 payload={scenes,connections,preview}. scenes/connections는 기존 구조다. preview는 null 또는 {scene_file:BlendFile,render_profile:RenderProfile,samples}이고 sample={id,cut_ids,media}다. id는 비어 있지 않고 유일하며 cut_ids는 비어 있지 않은 유일한 기존 컷 ID 배열, media는 실제 9:16 PNG 또는 MP4다. 대표 영상의 확인된 duration_ms는 0 초과 5000 이하이며 실제 크기·길이가 미확인/null이거나 verified=false인 미디어는 완료 근거가 아니다. 대표 샘플은 scenes 검토 근거로만 사용한다.
|
||||||
|
- 10 request={engine:"Blender",style_reference:"references/1.png",render_profile,items}. item={id,kind,entity_id,prompt}이며 kind는 style/character/background/object, style/object는 entity_id=null, character/background는 실제 09의 해당 ID를 참조한다. result는 null 또는 {scene_file:BlendFile,items}; 결과 item={reference_id,object_name,media:image}. reference_id는 요청 ID, object_name은 비어 있지 않은 실제 .blend 오브젝트/컬렉션 이름이다. 기준 PNG와 실제 자산을 함께 대조한다.
|
||||||
|
- 11 request={engine:"Blender",scene_path,render_profile,frames}. scene_path는 생성할 원본의 루트 상대 경로다. frame={id,cut_id,position,prompt,reference_ids,scene_name,frame_index}; position은 start/end, scene_name은 비어 있지 않은 실제 Scene 이름, frame_index는 0 이상 논리 프레임이다. 같은 컷의 start/end는 같은 Scene을 사용한다. result는 null 또는 {scene_file:BlendFile,frames}; 결과 frame={frame_id,media:image}다.
|
||||||
|
- 12 request={engine:"Blender",scene_path,render_profile,clips}. scene_path는 11의 검증된 실제 .blend 경로이고 render_profile은 11과 동일하다. clip={cut_id,start_frame_id,end_frame_id,duration_ms,motion_prompt,scene_name,start_frame,end_frame}. scene_name과 끝점 참조는 실제 11의 같은 Scene과 일치하고 start_frame/end_frame은 0 이상이며 end_frame>start_frame이다. duration_ms는 미작성 시 null, 작성 시 0 초과 5000 이하 정수이며 실측 07 구간과 같아야 한다. result는 기존 null 또는 {clips:[{cut_id,media:video}]}다.
|
||||||
|
|
||||||
|
13/14의 구조는 바꾸지 않는다. 13 timeline={cut_id,video_path,start_ms,end_ms}, captions=[{id,start_ms,end_ms,text}], video는 media, captions_path는 실제 SRT 경로다. 합성은 기존 Remotion + FFmpeg 경로를 사용하며 실행파일의 외부 절대 경로·인증정보는 JSON에 담지 않는다.
|
||||||
|
|
||||||
|
1.1의 continuous는 동일 Scene의 앞 end와 뒤 start가 동일한 실제 PNG 파일을 반드시 공유하며 위치별 frame ID는 각각 유지한다. 기존 1.0의 선택적 공유 규칙은 소급 변경하지 않는다. cut은 서로 다른 Scene의 같은 frame_index를 구별하며 캐시 식별에는 Scene 이름이 필요하다. 프레임 구간은 [start_frame,end_frame)이다. 이번 물 영상은 50fps·5528개 프레임으로 시간 기준을 유지하고 마지막 끝점 PNG는 논리 프레임 5528을 평가한다. Blender의 실제 번호는 논리 프레임+1이다. 타 주제의 fps·길이를 이 값으로 고정하지 않는다.
|
||||||
|
|
||||||
|
media는 {path,kind,verified,duration_ms,width,height}다. kind는 audio/image/video, 미확인 값은 null, verified는 불리언이다. 검증된 음성은 WAV·길이·width/height=null, 이미지는 PNG·크기·duration_ms=null, 영상은 MP4·길이·크기가 필요하다. 시간은 ms 정수이며 완료한 길이는 양수다. 결과의 ID는 요청에 존재해야 한다.
|
||||||
|
|
||||||
|
## 대기와 오류
|
||||||
|
|
||||||
|
request만 있으면 result=null이고 실제 배정된 외부 단계는 awaiting_input이다. 부분 결과는 보존하되 완료가 아니다. 없는 파일·지원하지 않는 언어·시간 불일치는 issues의 원인·blocking 여부와 돌아갈 단계를 보고한다. 키·인증정보는 이 형식에 포함하지 않는다.
|
||||||
|
|
||||||
|
14 checks는 {area,verdict,evidence}, findings는 {severity,description,stage_id,cut_id}, recommendation은 pass/revise/incomplete다. area는 story/visual/audio/captions/timing/consistency, verdict는 pass/fail/not_checked, severity는 blocking/minor다. 특정 컷이 아니면 cut_id=null이다. 실제 완성본 부재는 incomplete이며 여섯 영역의 통과와 blocking 없음이 확인되어야 pass를 추천한다. 실제 final 사용자 승인과는 별개다.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# 반복 작업에서 확인할 점
|
||||||
|
|
||||||
|
## Windows에서 스킬 검증이 디코딩 오류로 멈출 때
|
||||||
|
|
||||||
|
검증기가 기본 시스템 인코딩으로 한국어 SKILL.md를 읽으면 UnicodeDecodeError가 날 수 있다. 파일 오류와 구분하고 `python -X utf8`로 같은 검증을 다시 실행해 종료코드 0을 확인한다. 이 문제를 해결하려고 전역 Python·conda 설정을 바꾸지 않는다.
|
||||||
|
|
||||||
|
## 상대 경로가 제작 폴더에서 다른 파일을 가리킬 때
|
||||||
|
|
||||||
|
JSON의 경로는 저장소 루트 기준인데 Markdown 링크는 그 파일의 폴더 기준이다. review.md 양식을 깊은 제작 폴더로 복사하면 원래 문서 링크는 그대로 쓸 수 없다. JSON 데이터 경로는 유지하고 문서 링크만 실제 위치에서 해석해 파일이 존재하는지 확인한다.
|
||||||
|
|
||||||
|
## 제목 수정 뒤 음성 재생성을 요구할 때
|
||||||
|
|
||||||
|
전체 단계 순서만 따라 무효화하면 제목을 읽지 않는 음성·이미지까지 다시 만들게 된다. 각 artifact의 실제 inputs 관계를 따라 영향만 계산한다. 현재 전달 관계에서 04 제목은 13 합성과 14 검수에 영향을 주며 05–12는 제목만 바뀌면 보존할 수 있다. story/final의 승인 버전은 다시 확인한다.
|
||||||
|
|
||||||
|
## 설명용 컷을 실제 시간처럼 사용할 때
|
||||||
|
|
||||||
|
예제의 32000ms·4000ms는 계약 설명을 위한 가정이다. 실제 통합 WAV가 없으면 정렬·길이 근거가 없으므로 컷을 완료로 확정할 수 없다. 예제의 timing_evidence와 진행 기록의 현재 포인터를 확인하고, 실제 WAV·통합 단위 구간을 받은 뒤 컷을 새로 작성한다.
|
||||||
|
|
||||||
|
## 새로운 역할이나 스킬이 목록에 보이지 않을 때
|
||||||
|
|
||||||
|
TOML·YAML 파싱 성공은 현재 세션이 파일을 탐색했다는 증거가 아니다. 프로젝트 루트에서 새 Codex 세션으로 목록을 확인한다. 임시로 지침 파일을 직접 읽어 담당 작업을 수행할 수 있지만 이를 자동 탐색 검증으로 기록하지 않는다.
|
||||||
|
|
||||||
|
## 여러 Blender Scene 중 선택한 장면이 정지 화면으로 렌더될 때
|
||||||
|
|
||||||
|
원본의 애니메이션 평가 검사와 영상의 프레임 수가 맞아도, 렌더의 활성 컨텍스트가 다른 Scene이면 선택한 장면이 정지할 수 있다. 프레임을 설정하기 전에 `bpy.context.window.scene`을 렌더할 Scene으로 맞추고 실제 결과를 다시 확인한다. 초기 활성 장면에 움직이는 물체가 없는 검사 원본에서도 두 선택 장면을 각각 검증한다.
|
||||||
|
|
||||||
|
PNG 파일 SHA-256은 메타데이터 때문에 달라질 수 있고 H.264 압축 흔적도 픽셀 차이를 만들 수 있다. 기대한 움직임은 원본 렌더 PNG의 디코드 RGB와 실제 MP4의 시작·중간·끝 화면을 함께 비교한다. 파일 해시는 원본·산출물의 식별 근거이며 움직임이나 품질의 증거를 대신하지 않는다. 실행기 수정 뒤에는 새 스크립트 해시를 포함한 별도 attempt로 다시 렌더하고 옛 정지 결과를 채택하지 않는다.
|
||||||
|
|
||||||
|
## 분자가 액체의 표시 영역 밖에 놓일 때
|
||||||
|
|
||||||
|
분자를 잘 보이게 하려고 불투명한 물 체적 앞에 배치하면, 분자가 실제 물 영역 밖에 떠 있거나 아래·옆 경계를 넘을 수 있다. 절개 설명이라는 문구와 원자 수·결합 종류·영상 디코드 검사는 이 오류를 해결하지 않는다. 분자는 실제 물 영역 안에 두고 앞면·윗면을 열어 내부가 보이는 설명용 절개를 만든다. 절개면을 물의 고체 벽·막으로 설명하지 않는다.
|
||||||
|
|
||||||
|
분자 중심만 검사하지 않고 평가된 원자·내부 실선·활성 분자간 점선의 전체 mesh 정점을 모든 애니메이션 프레임과 경계에서 실제 물 영역과 대조한다. 카메라의 투영 윤곽 및 실제 표시 면과도 비교하고 최종 PNG·MP4를 직접 본다. 중심은 내부지만 원자 정점 일부가 밖인 실제 실패 원본을 검사에 포함한다. 제출한 이전 결과는 보존하고 수정은 새 revision에서 진행한다.
|
||||||
|
|
||||||
|
## 일부 끝점 렌더 뒤 다음 프레임을 요청하면 실패할 때
|
||||||
|
|
||||||
|
11의 결과 배열에 첫 PNG만 등록돼 있어도 배열 자체는 비어 있지 않다. continuous의 양쪽 결과가 모두 있다고 가정하면 아직 없는 끝점 ID를 조회해 다음 렌더를 막는다. 두 frame ID가 실제 등록된 경우에만 동일 파일 여부를 검사하고, 완성 검사는 별도로 전체 62개 ID를 요구한다. 첫 시작 PNG만 존재하는 상태에서 끝점 요청, 공유한 다음 시작, 같은 논리 프레임의 다른 Scene 요청을 차례로 실행해 부분 진행과 장면 분리를 함께 확인한다.
|
||||||
|
|
||||||
|
## 로컬 FFmpeg에서 프레임 추출 명령이 실패할 때
|
||||||
|
|
||||||
|
Remotion에 포함된 현재 FFmpeg는 select 필터와 rawvideo 출력 muxer를 제공하지 않는다. 일반 FFmpeg 예제를 그대로 쓰면 영상 결함과 관계없이 검사 명령이 실패한다. 단일 프레임 PNG는 입력 뒤에 `-ss`와 `-frames:v 1`을 두고, 전체 디코드는 `-c:v rawvideo -c:a pcm_s16le -f null -`로 확인한다. rawvideo 코덱과 출력 muxer를 구분하고 실제 명령 종료코드와 프레임 수를 확인한다.
|
||||||
|
|
||||||
|
## 합성 후 검토 상태만 바꿨는데 입력 해시가 달라질 때
|
||||||
|
|
||||||
|
project.json은 렌더 입력과 현재 진행 상태를 함께 담는다. 최종 검토로 넘어가며 13/14 상태나 final 기록만 바꿔도 전체 파일 해시는 달라진다. 렌더 당시 사본은 입력 identity의 SHA와 연결해 보존하고 현재 주제·설정·01–12 포인터/상태·topic/story/scenes 승인은 그 사본과 엄격히 대조한다. 같은 13/14 버전의 정상적인 상태 진행만 원 입력 변경과 구별한다. 원본 미디어·선행 결과·합성 코드의 해시 차이는 이 예외로 허용하지 않는다.
|
||||||
|
|
||||||
|
제출한 verify_final.py를 다시 실행하면 새 디코드 WAV와 technical-check.json을 작성하므로 기존 manifest와 해시가 달라질 수 있다. 검사 후 manifest를 확정하고, 이미 제출한 결과의 재검토는 별도 증거 폴더에서 읽기 전용으로 수행한다. 재실행 결과를 제출하려면 새 검사 근거와 manifest를 함께 갱신한다.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# 사용과 검증
|
||||||
|
|
||||||
|
## 제작 시작과 재개
|
||||||
|
|
||||||
|
이 저장소를 작업 폴더로 연 Codex가 필요하다. 프로젝트 스킬·역할 목록에 새 파일이 보이는지 확인하고, 보이지 않으면 프로젝트 루트에서 새 세션을 연다. 새 제작은 아래 두 입력으로 시작한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
$short-video-workflow
|
||||||
|
주제: 다룰 이야기
|
||||||
|
언어: 제작 언어
|
||||||
|
```
|
||||||
|
|
||||||
|
주제·각도 검토를 먼저 마치고 대본·제목, 실측 장면, 실제 완성본 검토 순으로 진행한다. 재개는 같은 스킬에 기존 제작 ID 또는 기록 위치를 전달한다. 전체 스킬 대신 개별 스킬을 불러도 필요한 선행 결과·승인은 유지한다.
|
||||||
|
|
||||||
|
새 시각 제작은 승인된 저장소 내 Blender 실행 경로를 사용한다. 실행 구성이 추가되어도 설치·요청 준비와 실제 출력 성공은 구분한다. 실제 음성은 기존 qwen3-tts conda 환경에서 생성·확인한 WAV와 통합 단위 구간을 제출하고, 새 모델·이미지는 Blender .blend 원본(해시·버전)과 실제 PNG·요청 ID·Scene·논리 프레임을 연결해 제출한다. 기존 1.0 결과는 그대로 읽는다. 영상·합성 결과도 MP4·SRT와 실측 정보를 제출한다. 파일 부재·지원하지 않는 언어·시간 불일치는 문제를 보고하고 결과를 기다린다.
|
||||||
|
|
||||||
|
## 파일 형식 검사
|
||||||
|
|
||||||
|
아래 명령은 Windows PowerShell에서 저장소 루트 기준이다. Python 3.11 이상은 TOML 파싱에 필요하다. 스키마 검증 도구는 임시 폴더에 설치하며 전역 환경을 변경하지 않는다. 실제 네트워크·설치가 실패하면 검증을 통과했다고 기록하지 않는다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
@'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys, tempfile, subprocess, os
|
||||||
|
check_code="""
|
||||||
|
from pathlib import Path
|
||||||
|
import json, tomllib
|
||||||
|
from jsonschema import Draft202012Validator, FormatChecker
|
||||||
|
validators={}
|
||||||
|
for name in ['project','artifact']:
|
||||||
|
schema=json.loads(Path('contracts/'+name+'.schema.json').read_text(encoding='utf-8'))
|
||||||
|
Draft202012Validator.check_schema(schema)
|
||||||
|
validators[name]=Draft202012Validator(schema,format_checker=FormatChecker())
|
||||||
|
for folder in ['templates','examples','projects']:
|
||||||
|
for path in Path(folder).rglob('*.json'):
|
||||||
|
if 'node_modules' in path.parts or '.cache' in path.parts:
|
||||||
|
continue
|
||||||
|
data=json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
if not isinstance(data,dict) or 'schema_version' not in data or 'project_id' not in data:
|
||||||
|
continue
|
||||||
|
validators['artifact' if 'stage_id' in data else 'project'].validate(data)
|
||||||
|
agents=list(Path('.codex/agents').glob('*.toml'))
|
||||||
|
assert len(agents)==8
|
||||||
|
for path in agents:
|
||||||
|
data=tomllib.loads(path.read_text(encoding='utf-8'))
|
||||||
|
assert set(data)=={'name','description','developer_instructions'}
|
||||||
|
print('JSON schemas, templates, examples and 8 TOML files verified.')
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory(prefix='short-video-verify-') as tools_dir:
|
||||||
|
subprocess.run([sys.executable,'-m','pip','install','--target',tools_dir,'jsonschema'],check=True)
|
||||||
|
check_env=dict(os.environ, PYTHONPATH=tools_dir)
|
||||||
|
subprocess.run([sys.executable,'-X','utf8','-'],input=check_code,text=True,env=check_env,check=True)
|
||||||
|
'@ | python -X utf8 -
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw '형식 검증 실패' }
|
||||||
|
```
|
||||||
|
|
||||||
|
Codex의 skill-creator가 설치되어 있다면 해당 검증기로 머리말·이름·미완성 내용을 추가 확인한다. 설치 위치가 다르면 실제 위치의 검증기를 사용한다. 현재 사용자 설치 위치를 찾는 명령은 다음과 같다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$skillValidator = Join-Path $env:USERPROFILE '.codex/skills/.system/skill-creator/scripts/quick_validate.py'
|
||||||
|
if (!(Test-Path -LiteralPath $skillValidator)) { throw 'skill-creator 검증기 위치 확인 필요' }
|
||||||
|
Get-ChildItem -LiteralPath '.agents/skills' -Directory | ForEach-Object {
|
||||||
|
python -X utf8 $skillValidator $_.FullName
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "스킬 검증 실패: $($_.Name)" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
프로젝트 사용 자체는 이 검증기의 전역 경로에 의존하지 않는다. 검사 결과와 관계없이 스킬의 입력·완료·승인 경계, 파일 참조, 예제의 가상 시간과 실제 결과 구분도 확인해야 한다. 형식 검사는 실제 음성·영상의 청취·관찰을 대신하지 않는다.
|
||||||
|
|
||||||
|
## 실제 제작 확인
|
||||||
|
|
||||||
|
06의 통합 WAV를 먼저 확인한 뒤 07 시간을 정한다. 글자 수 추정으로 컷·자막을 실측처럼 확정하지 않는다. 08의 실제 대표 화면·짧은 움직임을 기존 scenes 검토에서 07/08/09와 함께 확인한다. 새 08·09 승인 뒤 정식 10 모델·기준 이미지, 11 끝점 PNG, 12 전체 컷 영상으로 진행한다. 모든 클립을 받은 뒤 합성 요청을 완성하고 실제 MP4·SRT의 시간·언어·화면을 확인한다.
|
||||||
|
|
||||||
|
파일·메타데이터가 없는 외부 결과는 result=null, 부분 결과는 미완료다. 실제 최종 영상이 없으면 검수 결과는 incomplete이며 사용자 승인 없이는 완료를 선언하지 않는다. 서비스 실행·배포·업로드 명령은 현재 구성에 없다.
|
||||||
|
|
||||||
|
## 1.0/1.1 계약 회귀 검사
|
||||||
|
|
||||||
|
기존 환경에 jsonschema가 있으면 재설치하지 않고 다음 명령을 실행한다. 위 형식 검사와 함께 사용하며 실제 렌더링·청취·사용자 승인을 검사했다고 해석하지 않는다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -X utf8 contracts/check.py
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw '계약 회귀 검사 실패' }
|
||||||
|
```
|
||||||
|
|
||||||
|
검사는 기존 1.0 예제·실제 제작 JSON, 새 1.1 빈 양식, 잘못된 버전·엔진·추가 키·검증 원본의 해시/버전 누락·프로필·시간을 다룬다. 의미 사례는 .blend의 media 위장, ID 중복·미존재 참조·Scene 불일치·9:16·루트 상대 경로·result=null 완료 금지를 다룬다. 전체 원본·미디어·승인·화면의 실제 검증은 각 단계의 실행 경로와 검토 근거로 확인한다. [1.1 연결 예제](../examples/blender-contract/README.md)는 실제 파일이 없는 요청 예제다.
|
||||||
|
|
||||||
|
## 승인된 물 영상 Blender 컷의 실제 합성
|
||||||
|
|
||||||
|
기존 Remotion + FFmpeg 설치와 패키지/잠금 파일을 유지한다. [제작 렌더러 안내](../projects/water-hydrogen-bond-surface-tension/renderer/README.md)의 현재 입력·실제 파일 검사를 먼저 실행한다. 저장소 루트의 PowerShell에서:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm.cmd --prefix projects/water-hydrogen-bond-surface-tension/renderer run check
|
||||||
|
npm.cmd --prefix projects/water-hydrogen-bond-surface-tension/renderer run render -- compose
|
||||||
|
& 'C:/Users/baram/anaconda3/python.exe' -B -X utf8 projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/verify_final.py
|
||||||
|
& 'C:/Users/baram/anaconda3/python.exe' -B -X utf8 projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/mobile_views.py
|
||||||
|
```
|
||||||
|
|
||||||
|
새 compose는 현재 13 revision의 새 render-attempts 폴더에 실제 MP4/SRT와 입력/실측/최종 프레임 표본을 만들며 기존 파일을 재사용하거나 r1을 덮어쓰지 않는다. 실행기는 artifact/project·사용자 승인을 쓰지 않는다. 담당자가 실제 받은 결과 경로를 13에 연결한 뒤 verify_final을 실행한다. 현재 제출한 r2는 [13 검토](../projects/water-hydrogen-bond-surface-tension/stages/13-compose/r2/review.md)와 [14 검수](../projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/review.md)에 있다.
|
||||||
|
|
||||||
|
이 제작의 verify_final은 제출한 r2 결과·렌더 당시 project 증거·현재 유효 선행 입력과 실제 파일을 검사한다. 총괄의 13/14 상태·final 기록 갱신은 렌더 시점 project 바이트와 구별하지만 원 WAV·컷·선행 artifact·합성 코드/패키지 SHA 변경은 거부한다. 새 합성 시도는 새 렌더 시점 project 증거와 결과를 담당자가 연결해야 한다. 기술 검사·모바일 표본·신호 대조를 정상 속도 전체 재생·청취나 사용자 final 승인으로 처리하지 않는다. 미검수 영역은 not_checked/incomplete로 제출하고 실제 완성본은 final 검토에 제공한다.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 접근과 기록
|
||||||
|
|
||||||
|
한 사용자가 로컬 Codex 작업 폴더에서 파일을 관리한다. 별도 계정·서버 인증·다중 사용자 권한 시스템은 현재 구성에 없다. 접근은 사용자의 작업 공간·도구 권한을 따르며 역할 지침이 운영체제의 파일 접근 제어를 구현한 것은 아니다.
|
||||||
|
|
||||||
|
총괄은 공유 진행·현재 포인터·승인을 관리하고 담당 역할은 배정된 결과·검토만 제출한다. 실제 사용자 응답 없이 approved를 기록하거나 담당이 공유 상태를 바꾸는 것은 허용하지 않는다. 승인 대상 버전·피드백과 이전 결과를 남겨 변경의 근거를 확인할 수 있게 한다.
|
||||||
|
|
||||||
|
키·토큰·인증정보·외부 실행파일 절대 경로를 양식·예제·결과에 넣지 않는다. 실제 도구 연결이 추가될 때 인증은 해당 실행 환경에서 공급하고 미디어 전달 계약과 분리한다. 현재 구성은 외부 서비스의 인증·요청 실행을 구현하지 않는다.
|
||||||
|
|
||||||
|
원본 스타일 이미지와 사용자 결과를 삭제하거나 다른 제작물에 섞어 쓰지 않는다. 실제 미디어의 존재·형식·메타데이터와 검토 응답을 확인하며 요청서·가상 파일을 검증 자료로 취급하지 않는다. 확인하지 않은 근거를 확인한 것처럼 기록하면 후속 장면과 검수가 잘못 완료되므로 즉시 수정 위치를 보고한다.
|
||||||
|
|
||||||
|
지원하지 않는 언어·모델·도구는 문제를 보고하고 입력·해결을 기다린다. 전역 환경 재설치나 다른 모델로 무단 대체하지 않는다. 업로드·배포·사용자 대신 메시지 전송은 제작 지침의 암묵적 권한이 아니다.
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# 짧은 영상 구성과 내용 작성 지침
|
||||||
|
|
||||||
|
## 목적과 적용 범위
|
||||||
|
|
||||||
|
시청자가 하나의 질문을 따라가며 내용을 쉽게 이해할 수 있게 대본과 화면을 설계한다. [참고 영상 네 편의 분석](../references/shorts/analysis.md)에서 관찰한 구성과, 이 프로젝트에 적용할 작성 권고를 구분해 정리했다. 참고 영상의 주장을 사실로 검증한 문서나 흥행 공식이 아니다.
|
||||||
|
|
||||||
|
제작 입력은 주제와 언어다. 내레이션·제목·자막은 입력 언어로 작성한다. 초등학생도 이해할 수 있는 표현을 사용하되 시청자를 어린이로 한정하지 않는다. 결과는 세로 9:16, 전체 3분 이내, 실제 음성을 기준으로 나눈 컷은 각각 5초 이내다. 3분은 상한이며 채울 분량이 아니다.
|
||||||
|
|
||||||
|
이 지침의 구성 방법은 소재에 따라 선택한다. 길이·승인·출처·실측 등 필수 조건은 소재에 따라 바꾸지 않는다. topic/story/scenes/final의 실제 사용자 검토를 거치며, 예상 시간·도해·요청서를 실제 파일·실측·승인으로 취급하지 않는다. 구조를 맞추려고 사실에 없는 위기, 반전, 실패한 대안을 추가하지 않는다.
|
||||||
|
|
||||||
|
## 먼저 질문 하나를 정한다
|
||||||
|
|
||||||
|
자료를 많이 찾기 전에 시청자가 끝에서 답할 질문을 한 문장으로 적는다. 넓은 주제 안에서 한 가지 각도를 선택하고 topic 검토에서 확정한다.
|
||||||
|
|
||||||
|
| 준비할 내용 | 작성 기준 |
|
||||||
|
|---|---|
|
||||||
|
| 시청자 질문 | 무엇이 이상하거나, 왜 생기거나, 어떻게 가능한가 |
|
||||||
|
| 핵심 답 | 질문에 답하는 원리·변화·이유를 한 문장으로 설명 |
|
||||||
|
| 의미 | 답을 알면 익숙한 대상을 어떻게 새롭게 볼 수 있는가 |
|
||||||
|
| 보여줄 증거 | 답을 설명하는 물건·행동·비교·기록이 있는가 |
|
||||||
|
| 범위 | 이번 영상에서 다룰 조건과 제외할 내용 |
|
||||||
|
|
||||||
|
질문 두 개가 서로 다른 조사를 요구하면 이번 영상의 중심을 하나로 좁힌다. 상관없는 흥미로운 사실은 조사 메모에 남기고 대본에 모두 넣지 않는다. 소재에 실제 의문이나 변화가 없다면 방법 안내·관찰·시간 순서 등 더 알맞은 구성을 고른다.
|
||||||
|
|
||||||
|
## 답을 따라가는 이야기로 구성한다
|
||||||
|
|
||||||
|
설명형 소재에서는 아래 순서를 출발점으로 쓴다. 각 구간은 하나의 컷이 아니라 이야기의 역할이다. 필요한 역할만 사용하고, 답이 멀리 미뤄진다면 앞부분에 핵심 원리의 단서를 먼저 준다.
|
||||||
|
|
||||||
|
| 역할 | 시청자에게 전달할 것 | 다음으로 이어지는 이유 |
|
||||||
|
|---|---|---|
|
||||||
|
| 질문과 약속 | 구체적인 대상에 어떤 의문이 있는가 | 답을 알고 싶어진다 |
|
||||||
|
| 문제와 제약 | 무엇을 해결해야 하고 무엇이 어려운가 | 해결책에 필요한 조건을 이해한다 |
|
||||||
|
| 예상 가능한 대안 | 흔히 떠올릴 방법이 왜 충분하지 않은가 | 다른 방법이 필요한 이유를 안다 |
|
||||||
|
| 핵심 원리 | 어떤 관계·행동이 상황을 바꾸는가 | 해결 방식이 보인다 |
|
||||||
|
| 구현과 운영 | 실제로 어떻게 쓰며 어떤 조건이 필요한가 | 원리와 현실을 연결한다 |
|
||||||
|
| 결과와 회수 | 처음 질문의 답과 적용 범위는 무엇인가 | 끝에서 도입의 약속이 충족된다 |
|
||||||
|
|
||||||
|
대안의 실패나 운영 문제가 자료에 없다면 해당 구간을 생략한다. 역사 소개는 변화의 원인, 방법 안내는 수행 순서와 주의점, 비교는 같은 기준의 차이를 중심으로 바꾼다. 네 참고 영상의 공통 구성을 모든 소재의 의무 양식으로 만들지 않는다.
|
||||||
|
|
||||||
|
대본 블록을 읽으며 “앞의 설명 때문에 다음에 무엇이 궁금해지는가?”를 확인한다. 답이 없으면 연결 문장을 다듬거나 블록을 옮기거나 삭제한다. 정보를 늘리기 위해 질문을 새로 만들기보다 중심 질문에 가까워지는 정보를 우선한다.
|
||||||
|
|
||||||
|
## 첫 문장과 제목이 같은 내용을 약속하게 한다
|
||||||
|
|
||||||
|
첫 문장에는 대상과 핵심 의문을 넣고, 첫 화면에는 그 대상을 보여준다. 인사·채널 소개·막연한 감탄보다 이번 이야기의 질문으로 시작한다. 중요한 답을 가리는 대신 시청자가 따라갈 이유를 제시한다.
|
||||||
|
|
||||||
|
제목은 대본에서 실제로 설명하는 차이·원인·방법을 짧게 약속한다. 비유를 써도 사실의 범위를 바꾸지 않는다. 예를 들어 물높이 유지의 설명을 강의 종류에 대한 단정으로, 그늘의 설명을 냉방 성능에 대한 단정으로 바꾸지 않는다. 아래 형식은 후보를 만드는 도구이며 빈칸을 채웠다는 이유로 좋은 제목이 되지는 않는다.
|
||||||
|
|
||||||
|
- `[구체적인 대상]은 왜 [의외의 상태]일까`
|
||||||
|
- `[실제 제약] 속에서 [목표]를 가능하게 한 방법`
|
||||||
|
- `[익숙한 행동]이 [관찰한 결과]를 만드는 이유`
|
||||||
|
|
||||||
|
제목 후보마다 “대본의 어느 블록이 이 약속에 답하는가?”를 확인한다. 근거가 없거나 단정이 지나치면 제목을 고친다. 단어 선택은 입력 언어의 자연스러운 표현을 따르고 한국어 문형을 직역하지 않는다. 특정 초 안에 반드시 반전이 나와야 한다거나 모든 제목에 숫자가 필요하다고 정하지 않는다.
|
||||||
|
|
||||||
|
## 쉬운 말로 원인을 설명한다
|
||||||
|
|
||||||
|
한 문장에는 핵심 행동이나 관계를 하나씩 담는다. 긴 수식과 추상 명사 대신 누가 무엇을 어떻게 바꾸는지 적는다. 낯선 용어는 기능을 먼저 설명한 뒤 이름을 붙인다. 명칭을 외우지 않아도 원리를 이해할 수 있어야 한다.
|
||||||
|
|
||||||
|
비유는 익숙한 물건의 어떤 성질이 설명 대상과 같은지 밝힌다. 흙을 스펀지에 빗댄다면 물을 저장하고 내보내는 성질을 설명하는 데 쓰며, 재료나 작동 조건까지 같다고 말하지 않는다. 장난스러운 표현은 이해를 돕는 위치에 두고 사실의 심각성이나 조건을 흐리지 않게 한다.
|
||||||
|
|
||||||
|
대본의 재미는 새로운 답, 예상과 다른 이유, 구체적인 행동에서 만든다. “놀랍게도”, “엄청난 비밀”을 반복해 정보를 대신하지 않는다. 문단마다 새로 알게 되는 내용을 하나 적어보고 같은 답을 되풀이하는 부분을 줄인다.
|
||||||
|
|
||||||
|
## 근거와 숫자는 작성 전에 정리한다
|
||||||
|
|
||||||
|
참고 영상은 구성 자료다. 소재의 사실은 02 자료 조사에서 별도로 확인한 출처와 주장으로 관리하고 03 대본의 `claim_ids`에 연결한다. 출처가 주장 전체를 뒷받침하는지 확인하고, 미확인 내용은 사실처럼 내레이션하거나 도해하지 않는다. 비교 사례는 장소·시점·규모·접근·운영 조건 중 무엇이 같고 다른지 확인한다.
|
||||||
|
|
||||||
|
숫자는 크기·차이·시간을 이해하는 데 필요한 것만 말한다. 단위와 비교 기준을 함께 주고, 원자료의 수치를 임의로 바꾸거나 실제 비교가 아닌 장식용 비율을 만들지 않는다. 여러 수치가 몰리면 중심 질문에 필요한 하나를 남기고 나머지는 조사 결과에 보존한다.
|
||||||
|
|
||||||
|
사실의 어려운 예외를 짧게 만들려고 지우지 않는다. 주장 범위를 좁히거나 표현을 완화한다. 자료가 부족하면 다른 구성으로 바꾸거나 조사로 돌아간다. 화면에만 등장하는 수치·지도·시대 배경도 같은 확인이 필요하다.
|
||||||
|
|
||||||
|
## 설명의 단위를 화면으로 바꾼다
|
||||||
|
|
||||||
|
각 이야기 블록마다 “시청자가 알아야 할 관계”와 “그 관계를 보여주는 변화”를 정한다. 단순히 내레이션의 명사를 그림으로 나열하는 대신 물이 흐르는 방향, 가려지는 빛, 당겨지는 밧줄처럼 원인과 결과를 보여준다.
|
||||||
|
|
||||||
|
| 화면의 역할 | 적합한 설명 | 확인할 점 |
|
||||||
|
|---|---|---|
|
||||||
|
| 전경 | 대상의 위치와 전체 관계 | 지금 설명하는 부분이 어디인지 찾을 수 있는가 |
|
||||||
|
| 단면 | 내부·수면 아래·가려진 구조 | 실제 구조와 재료의 차이를 유지하는가 |
|
||||||
|
| 근접 화면 | 부품의 작동과 사람의 행동 | 행동의 전후 상태가 분명한가 |
|
||||||
|
| 지도와 경로 | 위치·이동·흐름 | 방향·지명·표시가 확인한 자료와 맞는가 |
|
||||||
|
| 전후 또는 조건 비교 | 무엇이 바뀌었고 왜 다른가 | 같은 기준으로 비교하며 원인을 과장하지 않는가 |
|
||||||
|
|
||||||
|
장면 메모에는 아래를 적고, 기존 08 장면 계약에 옮긴다. 새로운 필수 JSON 필드는 추가하지 않는다.
|
||||||
|
|
||||||
|
- 시청자가 이 구간에서 이해할 내용 → `description`
|
||||||
|
- 보여줄 물체·인물·배경과 원인·결과의 상태 → `start_state`, `end_state`
|
||||||
|
- 전경·단면·근접 등 보여줄 위치 → `camera`
|
||||||
|
- 다음 컷과 이어지거나 전환하는 이유 → `connections.mode`, `connections.reason`
|
||||||
|
|
||||||
|
03 대본의 이야기 블록과 07 컷은 일대일일 필요가 없다. 한 설명이 여러 컷에 걸치면 같은 대상과 시각적 기준점을 유지한다. 말이 이어지는 동안 화면을 나눠도 설명이 처음부터 다시 시작되는 것처럼 보이지 않게 한다.
|
||||||
|
|
||||||
|
## 음성을 기준으로 컷과 연결을 확정한다
|
||||||
|
|
||||||
|
Qwen3-TTS용 분할은 의미·호흡·발음 기준으로 한다. 긴 문장은 먼저 대본에서 자연스럽게 다듬고, 숫자·약어·고유명사의 읽는 방법을 기록한다. 내용 자체가 바뀌면 승인된 대본의 수정 절차를 따른다.
|
||||||
|
|
||||||
|
음성이 생성되면 실제 파일과 통합 구간을 확인해 07 컷을 정한다. 글자 수 추정이나 참고 영상의 시간 배분을 실측 대신 쓰지 않는다. 각 컷은 5초 이내지만 TTS 단위와 문장이 모두 5초여야 하는 것은 아니다. 문장을 끊거나 빠르게 재생해 시간을 억지로 맞추지 않는다.
|
||||||
|
|
||||||
|
같은 물체·장소·행동의 다음 상태를 보여주면 continuous, 위치·시간·시점을 새로 제시할 필요가 있으면 cut을 선택하고 이유를 적는다. continuous는 끝 프레임과 다음 시작 프레임의 인물·물체 위치·흐름 방향·카메라 상태를 맞춘다. 전환은 설명의 변화에 맞추며, 정해진 간격마다 의미 없이 전환하지 않는다.
|
||||||
|
|
||||||
|
## 깔끔한 그림체로 작동 방식을 보여준다
|
||||||
|
|
||||||
|
새 제작은 Blender의 깔끔한 교육용 3D다. [사용자 기준 이미지](../references/1.png)는 보존하고 단순 형태·정돈된 색을 참고한다. 단순 배경, 부드러운 조명, 선명한 분자·물방울 등 주요 형태로 설명하며 평면 복제·캠핑 소재·상표를 강제하지 않는다. scenes 검토에서 실제 대표 화면·짧은 움직임을 먼저 확인하고 승인한 품질 기준으로 전체 제작한다.
|
||||||
|
|
||||||
|
Blender 모델·장면·애니메이션 요청에는 조사 근거, 대본, 장면 상태, 반복 인물·배경의 고정 특징을 연결한다. 복잡한 메커니즘은 시각 요소를 줄여 설명하되 자연 퇴적물과 콘크리트 시설, 그늘과 냉방처럼 사실에 중요한 차이는 남긴다. 추측한 구조를 사실을 설명하는 도해로 사용하지 않는다.
|
||||||
|
|
||||||
|
컷 하나에서는 주요 물체와 변화가 먼저 보이게 한다. 강조 색·화살표·라벨이 서로 다른 대상을 동시에 가리키지 않게 하고, 같은 요소의 색과 기호는 일관되게 사용한다. 설명 글자·숫자는 3D 표면에 억지로 남기지 않고 합성 단계에서 검증한 텍스트로 배치한다.
|
||||||
|
|
||||||
|
움직임은 물높이 변화·부품 조작·그림자 이동 등 설명에 필요한 상태 변화를 우선한다. 줌·흔들림·장식적 움직임을 설명의 대체물로 쓰지 않는다. 시작·끝 프레임과 움직임 요청은 같은 상태 변화를 설명해야 한다.
|
||||||
|
|
||||||
|
## 자막과 주석을 읽을 수 있게 배치한다
|
||||||
|
|
||||||
|
자막은 말의 내용을 전달하고, 도해 주석은 물체·수치·방향을 설명한다. 역할을 구분해 같은 내용을 과도하게 겹쳐 쓰지 않는다. 자막의 의미를 보존하면서 입력 언어의 읽기 단위에 맞춰 나누고 실제 음성과 정렬한다. 자동 자막의 오인식을 원문으로 확정하지 않는다.
|
||||||
|
|
||||||
|
모바일 크기로 보고 대비·글자 크기·줄바꿈·표시 시간을 확인한다. 플랫폼의 버튼·제목 영역에 주요 설명이 가려지는지도 실제 출력에서 확인한다. 모든 플랫폼에 통하는 고정 안전영역 수치를 근거 없이 정하지 않는다. 그림의 중요한 움직임과 긴 자막을 동시에 읽어야 하는 구간은 요소나 설명량을 줄인다.
|
||||||
|
|
||||||
|
음성·배경음·효과음의 균형과 발음은 실제 완성본을 청취해 판단한다. 파일 존재나 텍스트 검토만으로 청취 검수를 통과했다고 기록하지 않는다.
|
||||||
|
|
||||||
|
## 제작 단계에서 필요한 부분을 읽는다
|
||||||
|
|
||||||
|
| 단계 | 담당 | 이 지침에서 사용할 내용 |
|
||||||
|
|---|---|---|
|
||||||
|
| 01 주제 | 주제·조사 담당 | 중심 질문, 이야기 각도, 보여줄 증거 |
|
||||||
|
| 02 자료 조사 | 주제·조사 담당 | 주장·숫자·비교 조건·시각 자료의 별도 검증 |
|
||||||
|
| 03–04 대본·제목 | 이야기 작가 | 질문부터 답까지의 구성, 쉬운 말, 약속과 회수 |
|
||||||
|
| 05–06 분할·음성 | 내레이션 담당 | 의미·호흡·발음, 요청과 실제 음성의 구분 |
|
||||||
|
| 07–09 컷·연결·반복 요소 | 장면 감독 | 실제 시간, 화면 역할, 상태 변화와 연속 기준 |
|
||||||
|
| 10–11 기준·컷 이미지 | 비주얼 담당 | 교육용 3D 모델·.blend 원본, 구조의 정확성, 고정 특징·끝점 확인 |
|
||||||
|
| 12–13 영상·합성 | 영상·편집 담당 | 설명에 필요한 움직임, 자막·주석, 모바일 판독성 |
|
||||||
|
| 14 최종 검수 | 최종 검수자 | 도입의 약속부터 완성본까지 근거로 확인 |
|
||||||
|
| 전체 | 제작 총괄 | 지침 적용 요약과 네 사용자 검토의 연결 |
|
||||||
|
|
||||||
|
담당은 필요한 절을 읽고 단계의 `review.md`에 사용한 원칙과 결과를 짧게 남긴다. 소재에 맞지 않는 구성은 적용하지 않은 이유를 적는다. 질문과 근거는 topic 검토, 대본·제목은 story 검토, 실측 컷·연결·반복 요소는 scenes 검토, 실제 완성본은 final 검토에서 확인한다. 검토 횟수를 늘리거나 기존 승인을 대신하지 않는다.
|
||||||
|
|
||||||
|
## 검토 메모 양식
|
||||||
|
|
||||||
|
기존 `review.md`에 아래 내용을 넣는다. 질문의 답이 비어 있으면 확인하지 못한 이유와 돌아갈 단계를 적는다. 이 양식은 사용자 검토를 돕는 메모이며 새로운 제작 입력이나 승인 절차가 아니다.
|
||||||
|
|
||||||
|
### 대본과 제목 검토
|
||||||
|
|
||||||
|
```text
|
||||||
|
중심 질문:
|
||||||
|
끝에서 전달할 답:
|
||||||
|
제목과 첫 문장의 약속:
|
||||||
|
그 약속에 답하는 대본 블록:
|
||||||
|
블록별 역할과 다음으로 이어지는 이유:
|
||||||
|
핵심 주장과 출처 연결:
|
||||||
|
비유의 같은 성질과 적용 한계:
|
||||||
|
덜어낸 부가 정보:
|
||||||
|
선택한 구성과 적용하지 않은 구성의 이유:
|
||||||
|
```
|
||||||
|
|
||||||
|
### 장면 설계 검토
|
||||||
|
|
||||||
|
```text
|
||||||
|
컷 ID와 실제 음성 구간:
|
||||||
|
이 컷에서 이해해야 할 내용:
|
||||||
|
시작 상태에서 끝 상태로 바뀌는 것:
|
||||||
|
전경·단면·근접 등 화면 선택의 이유:
|
||||||
|
조사 주장·인물·배경 참조:
|
||||||
|
이어짐 또는 전환의 이유:
|
||||||
|
주석·숫자의 근거와 배치 계획:
|
||||||
|
모바일에서 가장 먼저 보여야 할 요소:
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 영상 검토
|
||||||
|
|
||||||
|
| 영역 | 확인 질문 | 남길 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| story | 중심 질문의 답이 나오고 제목이 이를 정확히 약속하는가 | 관련 블록과 완성본 구간 |
|
||||||
|
| visual | 실제 원리·상태 변화가 보이며 글자·수치·구조가 정확한가 | 컷 ID, 관찰 내용, 조사 근거 |
|
||||||
|
| audio | 의미·발음·음량이 자연스럽고 누락·중복이 없는가 | 실제 청취 구간과 결과 |
|
||||||
|
| captions | 입력 언어·읽기 단위·표시 시간이 적절한가 | 자막 ID와 실제 음성 구간 |
|
||||||
|
| timing | 전체 3분 이내, 컷 5초 이내, 실제 음성·영상 시간이 맞는가 | 실제 파일 정보와 구간 점검 |
|
||||||
|
| consistency | 인물·배경·방향·그림체와 장면 연결이 이어지는가 | 연결되는 컷 쌍과 관찰 내용 |
|
||||||
|
|
||||||
|
14의 기존 `checks.evidence`에 이 근거를 기록하고 문제는 `findings`의 단계·컷으로 연결한다. 검수하지 못한 영역은 `not_checked`로 남긴다. 실제 완성본이 없으면 `incomplete`이며, 검수자의 통과 추천은 사용자의 final 승인이 아니다.
|
||||||
|
|
||||||
|
## 참고 자료의 한계
|
||||||
|
|
||||||
|
분석은 한국어 자동 자막과 약 4초 간격의 화면 표본에 기반한다. 모든 프레임·정확한 편집점·음성 연기·배경음·시청 유지율은 검증하지 않았다. 네 영상의 소재는 건축·토목 설명에 치우쳐 있으므로 다른 분야에서는 구성을 선택적으로 사용한다. 원문 대본·표현·화면 순서를 복제하지 않고, 소재 사실은 새 제작의 자료 조사에서 확인한다.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# 작성과 확인 기준
|
||||||
|
|
||||||
|
역할은 저장소의 독립 TOML에 name·description·developer_instructions를 두고, 스킬은 작업별 폴더의 SKILL.md에 YAML name·description을 둔다. 역할별 모델·권한은 부모 설정을 상속한다. 전역 설치·설정 변경이나 불필요한 등록 파일을 추가하지 않는다.
|
||||||
|
|
||||||
|
공통 구조는 JSON 스키마·빈 양식을 기준으로 연결한다. 필드·열거값을 역할마다 새로 정의하지 않는다. 새로운 의미의 필드가 필요하면 전달 구조·양식·관련 지침·예제를 함께 검토한다. 빈 문자열·배열·null은 미작성이지 완료가 아니다.
|
||||||
|
|
||||||
|
프로젝트 식별자는 소문자 영숫자·하이픈이며 제작마다 유일하다. 데이터 경로는 저장소 루트 기준 상대 경로다. 문서 링크는 실제 Markdown 위치 기준이다. 같은 단계·버전은 한 작성자만 작업하고 이전 버전을 덮어쓰지 않는다.
|
||||||
|
|
||||||
|
지침·검토 설명은 한국어, 제작 내용은 입력 언어다. 필요한 상세 문서만 읽고 공통 계약·근거·양식에 연결한다. 도해·비유가 사실의 범위를 바꾸거나 실제 근거 없는 수치·구조를 추가하면 수정한다.
|
||||||
|
|
||||||
|
실제 완료는 구조 검사 외에 선행 버전·선택 ID·주장 출처·배열 식별자·파일·실측·사용자 승인을 확인해야 한다. request만 있거나 verified=false 미디어를 필수 결과로 쓰는 완료 처리는 오류다. 미검수 영역을 pass로 기록하는 것도 오류다.
|
||||||
|
|
||||||
|
공통 설정은 주제·언어 입력, 세로 9:16, 전체 180000ms 이내, 각 컷 0ms 초과 5000ms 이하, 사용자 원본의 단순 형태·정돈된 색을 해석한 교육용 3D, Qwen3-TTS 기존 환경과 Blender다. 1.1은 visual_engine=Blender이며 기존 1.0과 지정 이미지 모델 기록은 보존한다. .blend는 media와 구분해 경로·검증 상태·SHA-256·Blender 버전을 기록한다. 실제 지원 여부는 제작 환경에서 확인하며 확인하지 않은 API 파라미터를 고정하지 않는다.
|
||||||
|
|
||||||
|
가정·중요한 미확정 의도를 먼저 밝히고 필요한 최소 변경을 한다. 관련 없는 코드·주석·서식·기존 불필요한 코드를 정리하지 않는다. 이번 변경이 새로 불필요하게 만든 항목만 제거한다. 동작을 바꾸는 변경은 해당 성공·실패 조건을 실제로 검증하고, 문서·양식은 형식·참조·예제 흐름을 확인한다.
|
||||||
|
|
||||||
|
검증 보고는 실행한 명령·종료코드·대상·한계를 남긴다. 형식 확인과 Codex 실제 탐색, 패키지 설치와 추론 성공, 모의 시간과 실제 정렬은 서로 다른 확인이다. 생성·합성 프로그램이 추가되면 실행 환경과 실제 결과의 검증을 함께 정의한다.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 로컬 지침과 JSON 전달
|
||||||
|
|
||||||
|
사용자는 혼자 Codex에서 주제·언어를 입력해 제작하고 네 곳에서 내용을 확인한다. 현재 필요는 역할·스킬 지침과 입출력 양식이다.
|
||||||
|
|
||||||
|
역할은 전문성별 8개, 스킬은 전체 진행과 14단계로 나누고 저장소 내부에서 사용한다. 전달·재개 기준은 버전별 JSON, 사용자 검토는 같은 결과의 Markdown으로 둔다. 총괄만 현재 상태·승인을 기록한다.
|
||||||
|
|
||||||
|
서비스·데이터베이스·자동 실행 스케줄러는 현재 사용자 수와 요청 범위에 맞지 않아 선택하지 않았다. Markdown만 전달하면 상태·식별자·선행 버전을 형식 검사하기 어려워 JSON을 함께 사용한다. 전역 설치는 프로젝트 밖의 다른 작업에 지침을 노출하므로 저장소 범위로 한정했다.
|
||||||
|
|
||||||
|
이 선택은 외부 생성 결과를 기다릴 때도 기록·검토를 보존하게 한다. 당시 지침·양식만으로 실제 미디어를 생성하거나 병렬 잠금·자동 재시도·업로드를 수행할 수는 없었다. 실제 도구 연결은 별도 실행 기능으로 추가하며 사용자 검토와 버전 참조를 계속 보존한다.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 로컬 Blender 모델과 실제 애니메이션
|
||||||
|
|
||||||
|
## 선택 배경
|
||||||
|
|
||||||
|
사용자는 물 영상의 설명 내용과 내레이션은 수용했지만 생성 이미지의 품질을 개선해 달라고 요청했다. 같은 분자·결합·물방울을 여러 장면에서 유지하면서 실제 상태 변화를 보여줄 필요가 있었다. 기존 2D 합성의 이동·확대만으로는 분자 연결 교체와 일정한 물의 양을 유지하는 변형을 확인하기 어려웠다.
|
||||||
|
|
||||||
|
## 결정
|
||||||
|
|
||||||
|
새 시각 제작은 저장소의 소재별 Blender 모델·애니메이션으로 만든다. 기존 설치 Blender에서 원본을 열고 PNG와 무음 MP4를 렌더하며, 제목·라벨·자막·승인된 WAV는 기존 Remotion + FFmpeg로 합성한다. 사용자 원본 이미지는 정돈된 형태·색의 참고로 보존하고 평면 스타일 복제를 강제하지 않는다.
|
||||||
|
|
||||||
|
전체 렌더 비용을 쓰기 전에 대표 PNG와 짧은 실제 움직임을 기존 scenes 검토에 함께 제출한다. 물 분자가 표시 체적을 벗어난 사용자 피드백도 새 버전의 영역·평가 정점·투영 검사와 대표 출력으로 수정한 뒤 승인받았다. 설치 여부나 기술 검사만으로 품질 승인을 대신하지 않는다.
|
||||||
|
|
||||||
|
## 검토한 대안
|
||||||
|
|
||||||
|
- GPT 이미지 생성의 반복 수정: 사용자가 지적한 품질 문제를 해결할 보장이 없고 컷 사이 동일 모델·부피·연결의 실제 상태를 검사하기 어렵다.
|
||||||
|
- 기존 2D 이미지에 움직임만 추가: 합성 구성을 재사용할 수 있지만 3D 원본의 실제 변형과 장면 평가를 제공하지 못한다.
|
||||||
|
- 외부 영상 생성 서비스 추가: 새 비용·연결 경로가 필요하며 기존 로컬 도구를 사용하려는 승인 범위에 포함되지 않는다.
|
||||||
|
|
||||||
|
## 이후 작업에 미치는 영향
|
||||||
|
|
||||||
|
원본 .blend의 해시·Scene·프로필과 실제 PNG/MP4를 연결해야 하며 렌더 설정이나 장면 원본이 바뀌면 후속 검증을 다시 수행한다. 기존 대본·음성·시간표와 과거 결과를 그대로 보존할 수 있는 대신, 새로운 소재에는 그 근거에 맞는 모델·장면 코드를 별도로 작성해야 한다. 이 방식은 임의 주제의 모든 모델을 자동 생성하거나 물의 분자동역학·유체 시뮬레이션을 제공하지 않는다.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# 결정 기록
|
||||||
|
|
||||||
|
- [로컬 지침과 JSON 전달](0001-local-instructions.md): 개인용 Codex 사용과 네 사용자 검토에 맞춘 역할·기록 구성.
|
||||||
|
- [로컬 Blender 모델과 실제 애니메이션](0002-local-blender-rendering.md): 이미지 품질 개선과 모델·움직임 검증을 위한 제작 방식 전환.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# 미해결 문제
|
||||||
|
|
||||||
|
현재 지침·양식 구현 범위에서 확인된 미해결 문제는 없다.
|
||||||
|
|
||||||
|
## 실제 제작의 청취 확인 기록 — 해소
|
||||||
|
|
||||||
|
- 대상: [water-hydrogen-bond-surface-tension / 06-tts r1 검토](../../projects/water-hydrogen-bond-surface-tension/stages/06-tts/r1/review.md).
|
||||||
|
- 원인: 현재 Codex 세션은 오디오 입력을 지원하지 않아 실제 WAV의 청취 검수를 수행할 수 없다. 오디오 전달 시 “audio content omitted because you do not support audio input” 결과를 확인했다.
|
||||||
|
- 완료한 확인: 기존 Qwen3-TTS 모델·한국어·Sohee 지원, 8개 단위 WAV와 110560ms 통합 WAV의 실제 생성, PCM 형식·샘플 기반 단위 구간·파일 통합 동일성.
|
||||||
|
- 당시 영향과 보류: 청취 결과가 없어 06은 awaiting_input이고 07–09는 보류했다. 파일·파형 검사를 청취 통과로 기록하지 않았다.
|
||||||
|
- 해소 근거: 사용자가 실제 제공된 통합 WAV에 대해 “내레이션 WAV 괜찮습니다.”라고 확인했다. 사용자 청취 확인으로 06을 complete 처리하고 07–09를 재개한다. Codex 직접 청취로 기록하지 않는다.
|
||||||
|
- 후속 확인: 07에서 실제 WAV의 단어별 자동 정렬·승인 원문 대응·전체 컷과 별도 자막 구간을 검사했다. 실제 최종 출력의 자막 판독성·타이밍 검수는 13–14에 남아 있다. 승인된 03 원문 및 기존 topic/story 승인은 보존했다. 전역 환경·다른 음성 도구로 대체하지 않았다.
|
||||||
|
|
||||||
|
## 임시 ASR 모델 캐시 정리 — 보류, 제작 진행에는 영향 없음
|
||||||
|
|
||||||
|
- 대상: projects/water-hydrogen-bond-surface-tension/.cache/asr. 이번 정렬 실행에서 저장한 Whisper 가중치·토크나이저 캐시다. 주 가중치 크기는 1617824864바이트다.
|
||||||
|
- 시도: 저장소 안의 해당 절대 경로와 일반 디렉터리임을 확인하고, PowerShell의 LiteralPath 삭제 및 파일별·빈 디렉터리별 삭제로 정리를 시도했다.
|
||||||
|
- 실제 결과: 자동 승인 검토가 삭제 실행을 “blocked by policy”로 거절했다. 더 구체적인 거절 이유는 제공되지 않았다. 삭제는 실행되지 않았고 캐시는 남아 있다. 다른 셸·도구로 삭제를 우회하지 않았다.
|
||||||
|
- 영향: WAV·정렬 근거·단계 결과·장면 검토에는 영향이 없으며 blocking 문제로 처리하지 않는다. 모델 캐시는 단계 결과·승인·미디어로 참조하지 않는다. 원시 정렬 근거는 별도 alignment.json에 보존했다.
|
||||||
|
- 보류 이유: 허용된 정리 경로가 없어 해당 임시 캐시를 보존한다. 현재 사용자 전역 환경·설정은 변경하지 않았다.
|
||||||
|
|
||||||
|
## 기준 이미지의 정확한 세로 규격 — 해소
|
||||||
|
|
||||||
|
- 대상: [water-hydrogen-bond-surface-tension / 10-references r1 검토](../../projects/water-hydrogen-bond-surface-tension/stages/10-references/r1/review.md).
|
||||||
|
- 실제 실행: 사용자가 “현재 내장 이미지 도구 사용을 허용합니다(모델명 확인 없이)”라고 허용해 기준 이미지 5종을 생성·수정했다. 실제 모델명은 미확인이며 지정 모델 실행 성공으로 기록하지 않는다.
|
||||||
|
- 원인: 내장 도구에는 크기 지정 인자가 없고 생성 후보 5개 모두 실제 941×1672 PNG로 정확한 9:16에서 미세하게 벗어난다.
|
||||||
|
- 완료한 확인: 실제 파일·PNG signature·IHDR 크기·화면·그림체·원자 수·결합선 끝점·비교 조건을 확인했다. 부분 결과를 보존하고 규격 미충족으로 verified=false를 기록했다.
|
||||||
|
- 영향: 10은 awaiting_input, 검증된 기준이 필요한 11–14는 시작하지 않았다. 기존 대본·WAV·컷·사용자 승인에는 영향이 없다.
|
||||||
|
- 재개 조건: 그림의 원본 픽셀을 유지하고 여백만 추가해 945×1680으로 만드는 로컬 보정의 명시적 허용 또는 정확한 규격의 생성 출력. 이미지 편집의 내장 도구 사용 지침 때문에 로컬 보정 허용 질문을 제출했으며 아직 답변이 없다. 사용자 환경은 변경하지 않았다.
|
||||||
|
- 해소: 사용자가 해당 보정 요청에 “허용합니다”라고 답했다. 원본을 보존한 새 PNG 5개에 여백만 추가했고, 945×1680·정확한 9:16·원본 영역 픽셀 동일·PNG CRC/디코딩·실제 화면을 확인했다. 10 complete로 처리하고 11을 시작한다. 앞선 보류 기록은 당시 상황이며 현재 보정 허용을 다시 요구하지 않는다.
|
||||||
|
|
||||||
|
## 최종 MP4의 전체 재생·청취 확인 — final 검토 대기
|
||||||
|
|
||||||
|
- 대상: [water-hydrogen-bond-surface-tension / 14-review r1](../../projects/water-hydrogen-bond-surface-tension/stages/14-review/r1/review.md).
|
||||||
|
- 실제 출력: 선택한 Remotion + FFmpeg를 제작 폴더 안에 구현해 31개 컷 MP4와 110560ms·1080×1920 최종 MP4·38개 자막 SRT를 생성했다. 실제 길이·프레임 수·전체 AV 디코딩·원 WAV/AAC 신호 대조·모바일 화면 표본·모든 인접 경계를 확인했다.
|
||||||
|
- 남은 확인: 현재 세션은 오디오 입력을 지원하지 않으며 정상 속도 전체 시청·청취를 수행하지 못했다. 원 WAV에 대한 기존 사용자 청취 확인은 보존하되 최종 MP4의 청취 확인으로 바꾸지 않는다. 파일·신호·정지 표본 검사로 미검수 영역을 pass 처리하지 않았다.
|
||||||
|
- 영향: 실제 13 출력과 14 보고는 사용자 final 검토에 제출할 수 있다. 13/14는 awaiting_review이며 14 추천은 incomplete다. 영상 파일 부재나 실행 경로 부재로 기다리는 상태가 아니다. 시청 중 실제 증상이 확인되면 시간/컷과 원인 단계로 돌아간다.
|
||||||
|
|
||||||
|
- 이후 사용자 피드백: 내용은 수용했으나 이미지 품질 변경을 요청해 Blender 3D로 전환했다. 13/14 r1은 stale이고 기존 final의 changes_requested 피드백을 보존한다. 이후 수정한 07 r1·08 r3·09 r3가 실제 승인되어 정식 10–12 r2와 새 13/14 r2로 진행했다. 새 완성본은 실제 MP4/SRT가 있으며 아래 최종 재생·청취 대기 기록을 따른다.
|
||||||
|
|
||||||
|
## 물 분자와 표시 체적의 포함 관계 — 해소, r3 장면 승인
|
||||||
|
|
||||||
|
- 대상: 08-scenes r2의 c16 Interface. [실제 사용자 피드백](../../projects/water-hydrogen-bond-surface-tension/history/scenes-r2-feedback.md)을 보존했다.
|
||||||
|
- 확인: PNG에서 하단 분자가 물 영역 실루엣 밖에 보인다. 작성 코드의 WaterCutawayBody 범위는 y=0.95–2.45, z=-2.65–0.35인데 분자는 앞쪽 y에 배치돼 있고 하단 원자 일부도 z 범위를 벗어난다. 설명용 절개라는 문구로 표시 체적과 분자의 실제 포함 관계 오류를 정당화할 수 없다.
|
||||||
|
- 영향: r2 시각 품질 통과 판단을 취소하고 08·09 r3를 작성한다. 기존 대본·WAV·r2 결과를 보존하며 전체 제작은 시작하지 않는다.
|
||||||
|
- 수정·검증: 실제 물 영역과 절개 화면을 맞추고, 분자 중심이 아닌 평가된 전체 원자·결합 mesh를 Interface의 모든 프레임에서 검사한다. 기존 원본 RED와 수정 GREEN, 실제 새 PNG·MP4·모바일 화면으로 확인한다.
|
||||||
|
- 해소: r3의 실제 앞면·윗면 열린 절개 안에 분자·결합을 배치했다. 전체 705프레임·평가 정점 11,747,388개가 물 체적과 실제 네 표시 면의 투영 영역 안에 포함된다. 기존 r2와 중심을 유지한 원자 정점 오류는 정확한 포함 assertion으로 종료 1, 수정 원본은 종료 0이다. 실제 새 c16 MP4 전체 디코드와 시작·중간·마지막·모바일 화면에서도 돌출이 없음을 확인했다. 이전 r2 실물 75개와 전환 전 기준 36개 파일은 SHA-256 동일하다. [새 검토](../../projects/water-hydrogen-bond-surface-tension/stages/08-scenes/r3/review.md)는 이후 실제 “수정본을 승인합니다” 응답으로 승인됐고 정식 10–12 r2를 제작했다. 최종 사용자 승인과는 구분한다.
|
||||||
|
|
||||||
|
## 새 3D 완성본의 전체 재생·청취 — 사용자 승인 완료, 직접 검수 미확인
|
||||||
|
|
||||||
|
- 대상: [13 r2 실제 MP4/SRT](../../projects/water-hydrogen-bond-surface-tension/stages/13-compose/r2/review.md)와 [14 r2 여섯 영역 보고](../../projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/review.md).
|
||||||
|
- 완료한 확인: 실제 1080×1920·50fps·5528F·110560ms, 31컷/38 SRT 원문·시간, 전체 AV 디코드·원 WAV/AAC 신호·원 입력 SHA 및 실제 최종 163프레임/모바일 표본 관찰. c02의 공중 연결선은 별도 전체 시도로 수정했고 첫 시도는 보존한다.
|
||||||
|
- 남은 범위: 현재 세션 오디오 입력 미지원과 정상 속도 전체 연속 재생 미확인으로 visual/audio/captions 전체 판정은 not_checked, 추천은 incomplete다. 원 WAV의 기존 사용자 수용을 새 MP4의 청취 승인으로 바꾸지 않는다.
|
||||||
|
- 영향: 실제 사용자 “승인합니다” 응답을 같은 13/14 r2의 final approved로 기록했다. [승인 근거](../../projects/water-hydrogen-bond-surface-tension/history/final-r2-user-approval.md)와 승인 전 사본을 보존했다. 13은 complete, 14는 직접 전체 재생·청취 근거가 없어 awaiting_input이다. 기존 14의 not_checked/incomplete를 사용자 승인만으로 pass로 바꾸지 않으며 동일 버전의 승인을 다시 요구하지 않는다. 확인된 결함으로 재생성을 요구하거나 파일 부재로 실행을 멈춘 상태가 아니다. 시청에서 증상이 있으면 실제 시간/컷과 원인 단계의 새 버전으로 돌아간다.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 현재 제작 기반
|
||||||
|
|
||||||
|
## 만들어진 기능
|
||||||
|
|
||||||
|
- 제작 총괄을 포함한 역할 8개와 전체 진행·14단계 작업 스킬 15개.
|
||||||
|
- 주제·언어 입력, 네 사용자 검토, 버전 보존과 수정·재개 기준.
|
||||||
|
- 진행 기록·단계 결과 스키마 2개, 진행·단계 JSON 양식 15개와 사용자 검토 양식.
|
||||||
|
- 참고 영상 네 편의 내용·구성 분석을 사용하는 대본·제목·장면·검수 지침.
|
||||||
|
- 물순환 주제의 연결 예제와 승인·부재·시간·변경을 다루는 검토 사례 15개.
|
||||||
|
- 기존 1.0 결과를 보존하는 Blender 1.1 계약과 저장소 내 원본 검사·PNG·무음 MP4 렌더 실행기. 실행법은 [Blender 안내](../../tools/blender/README.md)에 있다.
|
||||||
|
|
||||||
|
## 확인한 범위
|
||||||
|
|
||||||
|
JSON·TOML·스킬 형식과 링크·단계 참조를 실제 명령으로 확인했다. 예제의 컷·자막·프레임 연결과 제목·대본 수정의 종속 관계도 확인했다. 예제의 현재 기록은 topic 승인 전이고 외부 결과·실제 미디어는 없다. 스키마 검사와 예제 검토는 지침·형식의 확인이며, 자동 실행 프로그램의 동작 시험이 아니다.
|
||||||
|
|
||||||
|
Codex CLI의 존재·버전은 확인했지만 새 세션에서 사용자 지정 역할·스킬의 자동 탐색은 아직 확인하지 않았다. 물 제작의 기존 06 r1에는 사용자 수용 응답과 실제 Qwen3-TTS WAV·실측 구간이 있으며, 기존 13 r1에는 실제 합성 MP4/SRT가 있다. 이 기존 결과의 전체 재생·청취 검수는 완료로 기록하지 않는다.
|
||||||
|
|
||||||
|
Blender 5.2.2에서 작은 검사 씬의 EEVEE/GPU와 Cycles/CPU 렌더·PNG/MP4 전체 디코드를 확인했다. EEVEE의 50fps 실행과 Cycles/OPTIX GPU 정지 렌더도 확인했다. 검사 씬은 물 영상의 품질 승인 근거가 아니다. 실행기의 17개 회귀 검사와 10개 계약 검사는 통과했으며, 미승인 제작·누락 파일·잘못된 참조·이전 시각 버전 혼입을 거부한다.
|
||||||
|
|
||||||
|
물 제작은 01–07 r1과 승인된 WAV를 유지한다. 사용자가 지적한 r2의 분자·물 영역 포함 관계 오류를 앞선 시각 검수가 놓쳤으며 [r2 피드백](../../projects/water-hydrogen-bond-surface-tension/history/scenes-r2-feedback.md)을 보존했다. 새 08·09 r3에서 실제 영역과 전체 원자·결합을 수정하고 705프레임 전체 mesh 정점 및 실제 표시 면의 투영 영역 검사를 통과했다. 실제 새 대표 PNG·MP4 각각 3개와 직접 관찰 근거를 [r3 장면 검토](../../projects/water-hydrogen-bond-surface-tension/stages/08-scenes/r3/review.md)에 제출한다. 사용자가 07 r1·08 r3·09 r3에 “수정본을 승인합니다”라고 응답해 scenes가 approved이며 정식 10–12 r2를 제작했다. 기존 10–14 r1은 stale로 보존한다. [전환 전 진행 기록](../../projects/water-hydrogen-bond-surface-tension/history/pre-blender-project.json)과 r2 결과·미디어도 보존한다.
|
||||||
|
|
||||||
|
## 새 3D 완성본과 남은 검토
|
||||||
|
|
||||||
|
승인된 10 r2의 기준 모델, 11 r2의 62끝점 ID·실제 앵커, 12 r2의 31무음 MP4로 새 13 r2 MP4/SRT를 만들었다. 원 대본·제목·WAV·38자막은 유지했다. 실제 완성본은 1080×1920·50fps·5528프레임·110560ms이며 전체 AV 디코드·원 WAV/AAC 신호·모든 컷과 자막/주석 전환의 실제 최종 163표본을 확인했다. 합성 입력 검사의 24개 거부 사례도 통과한다.
|
||||||
|
|
||||||
|
[새 합성 검토](../../projects/water-hydrogen-bond-surface-tension/stages/13-compose/r2/review.md)와 [여섯 영역 보고](../../projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/review.md)를 사용자 final 검토에 제출한다. 정상 속도 전체 시청·청취는 현재 세션에서 확인하지 못해 visual/audio/captions는 not_checked, 추천은 incomplete다. 이후 실제 사용자 “승인합니다” 응답을 [승인 기록](../../projects/water-hydrogen-bond-surface-tension/history/final-r2-user-approval.md)에 보존하고 final을 같은 13/14 r2의 approved로 기록했다. 13은 complete, 14는 직접 전체 재생·청취 근거 부재로 awaiting_input이며 기존 not_checked/incomplete 보고를 보존한다. 사용자 승인과 모든 영역의 기술 검수 완료를 구분한다. 영상 실행·파일 부재로 멈춘 상태는 아니다.
|
||||||
|
|
||||||
|
1. 사용자 final 승인은 완료됐다. 추가 전체 재생·청취 근거가 확보되면 14의 새 검수 버전에 기록한다.
|
||||||
|
2. 증상이 발견되면 실제 시간/컷과 원인 단계의 새 버전으로 수정한다.
|
||||||
|
3. 다른 소재는 같은 실행 경로를 사용하되 별도 근거·모델·장면을 작성한다.
|
||||||
|
|
||||||
|
대표 영상의 실제 디코드 화면 비교에서 비활성 Scene의 애니메이션이 정지하는 문제를 발견했고, 렌더 전에 선택 Scene을 활성화하도록 수정했다. 초기 활성 Scene이 Idle인 같은 원본에서 두 장면의 PNG·MP4 실제 RGB 움직임 검사가 실패→통과로 바뀌었으며 독립 검토도 통과했다. 물 대표 렌더는 수정된 실행기로 재제작하고 실제 움직임을 확인했다. 이후 실제 r3 scenes 승인 뒤 정식 10–12 r2와 새 13/14 r2로 진행했다. 정상 속도 전체 재생·청취의 직접 검수 근거는 미확인이며 사용자 final 승인은 완료됐다.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# 검사 기록
|
||||||
|
|
||||||
|
검사일: 2026-10-08. 저장소 루트의 Windows PowerShell에서 실행했다. 지침·양식·예제의 검사는 실제 생성 도구 실행과 구분한다.
|
||||||
|
|
||||||
|
| 실제 실행 | 종료 코드 | 확인한 내용 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| operations.md의 첫 번째 PowerShell 명령 | 0 | 임시 설치와 별도 Python 프로세스로 두 스키마, 모든 양식·예제, 역할 TOML 8개 검사 |
|
||||||
|
| operations.md의 두 번째 PowerShell 명령 | 0 | skill-creator로 스킬 15개 검사, 각각 유효 결과 확인 |
|
||||||
|
| `python -X utf8 - --all`에 임시 검사 본문 입력 | 0 | JSON 34개 파싱·스키마 검사, 역할 8개·스킬 15개, 로컬 링크 134개, 동일한 루트 지침·영역 지침 4개·공통 문서 확인 |
|
||||||
|
| 같은 임시 검사의 참조·수치·상태 확인 | 0 | 잘못된 상태·중복 단계·미확인 pass 거부, 5초 초과·빈 컷·틈·겹침·3분 초과 검출, 컷을 넘는 자막·프레임 연결·수정 영향 확인 |
|
||||||
|
| `git diff --check` | 0 | 변경한 추적 파일의 공백 오류 없음 |
|
||||||
|
| `codex --version` | 0 | 로컬 CLI 0.160.1 확인 |
|
||||||
|
| `codex --help` | 0 | 로컬 CLI 진입 명령 확인 |
|
||||||
|
|
||||||
|
JSON 34개 중 데이터 양식·예제 30개는 해당 스키마로 검증했고, 검토 사례·참고 영상 목록 2개는 각 목록과 항목을 확인했다. 나머지 2개는 스키마 정의 자체의 유효성을 검사했다. 제목만 수정했을 때 음성·이미지를 다시 만들지 않는 관계, 대본 수정의 후속 영향, 주제 승인 전 예제 기록, 가상 컷·자막·프레임 연결도 확인했다. 이 검사는 예제의 참조와 수치 관계를 평가하며 실제 작업자의 지침 준수나 자동 실행 성공을 보장하지 않는다.
|
||||||
|
|
||||||
|
실제 Codex 세션의 새 역할·스킬 탐색, Qwen3-TTS 모델·화자 로딩과 추론, 지정 이미지 모델의 사용 가능 여부·호출, 영상 생성·합성·청취 검수는 실행하지 않았다. 예제의 외부 result는 null이고 최종 검수는 incomplete다. 서버 구성은 없으므로 서버 구동 검사는 적용되지 않는다.
|
||||||
|
|
||||||
|
참고 영상 분석은 공개 메타데이터·한국어 자동 자막·4초 간격의 이미지 표본 117장을 확인한 결과다. 음성 청취, 정확한 컷 경계, 소재 사실의 독립 검증, 시청 유지율은 확인하지 않았다.
|
||||||
|
|
||||||
|
## Blender 전환 진행 중 검사
|
||||||
|
|
||||||
|
아래는 초기 기반 검사 이후 실제 실행한 추가 검사다. 기존 물 제작의 WAV와 r1 결과를 보존하고 새 3D 대표 렌더를 준비하는 중이며, 당시 정식 전체 제작·새 완성본 검사는 아직 없었다. 이후 실제 기록은 아래 새 완성본 절에 추가한다.
|
||||||
|
|
||||||
|
| 실제 실행 | 종료 코드 | 확인한 내용 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `C:/Users/baram/anaconda3/python.exe -B -X utf8 contracts/check.py` | 0 | 10개 검사: 1.0 호환·1.1 양식·Blender 원본/미디어 구별·참조·대표 영상 길이·문서/역할/스킬 링크 |
|
||||||
|
| `C:/Users/baram/anaconda3/python.exe -B -X utf8 tools/blender/test_runner.py` | 0 | 13개 검사: 승인·현재 포인터·선행 버전·WAV 실측·누락/미검증 파일·격자·entity 참조·새 엔진·버전 경로·cut/continuous 경계 |
|
||||||
|
| [실행기 안내](../../tools/blender/README.md)의 `smoke.py --engine BLENDER_EEVEE` 명령 | 0 | 실제 GPU 렌더, 원본 재열기, PNG와 무음 H.264 전체 디코드, 움직임·다중 Scene·10/11/12 요청 실행과 실패 거부 |
|
||||||
|
| 같은 명령의 `--engine CYCLES` | 0 | 실제 CPU 렌더와 디코드. 별도 Cycles/OPTIX GPU 정지 렌더에서도 RTX 4080 SUPER 장치 확인 |
|
||||||
|
| 독립 검토의 EEVEE 50fps·5프레임 검사 | 0 | 실제 100ms MP4·PNG 렌더와 전체 디코드. 10fps 검사와 구분 |
|
||||||
|
| 전환 전 기준 36개 파일의 SHA-256 대조 | 0 | 기존 r1 결과·검토, 렌더러 소스, 원본 스타일과 통합 WAV 변경 없음 |
|
||||||
|
| 새 작업 사본으로 복사한 WAV 9개의 SHA-256 대조 | 0 | 통합 음성·8개 발화 파일이 원본과 바이트 동일 |
|
||||||
|
|
||||||
|
실제 렌더 검사 씬은 사용자 물 영상과 별개인 `.cache`의 검사용 자료다. 검사 승인은 모의 자료이며 실제 사용자 승인으로 기록하지 않는다. Blender 실행·디코드 확인은 화면 품질·청취·최종 승인과 다르다. EEVEE의 임의 seed 설정 API는 확인되지 않아 기본 0만 지원하고 다른 값은 거부한다. Cycles에서는 실제 seed 설정을 적용한다.
|
||||||
|
|
||||||
|
추가 물 대표 영상 검사에서 비활성 Scene의 100개 렌더 프레임이 실제로 정지하는 문제를 발견했다. PNG 파일 SHA-256은 메타데이터 때문에 달랐지만, 시작·중간·끝과 여러 중간 프레임의 디코드 RGB 차이는 없었다. 위 smoke의 파일 해시 비교는 이 경우의 움직임 증거로 충분하지 않았다. 선택 Scene의 실제 평가와 디코드 픽셀 비교를 보완하는 중이며, 해당 옛 샘플은 사용자 검토 결과로 채택하지 않는다.
|
||||||
|
|
||||||
|
위 문제는 렌더 전에 선택 Scene을 활성화해 수정했다. 실행기 안내의 `smoke.py` 명령에 `--animation-only`를 추가한 EEVEE 검사에서 수정 전 종료 1, 수정 후 종료 0을 확인했다. 초기 Idle인 원본에서 SceneA/B 각각 50fps·5프레임·100ms를 렌더해 PNG와 실제 MP4의 RGB 시작·중간·끝이 모두 달랐다. 원래 실패한 동일 `.blend` 재렌더도 종료 0이고, 내부 실행 코드 해시 변경으로 새 attempt를 사용했다. 독립 검토의 같은 실제 검사, 14개 실행기 회귀와 10개 계약 검사도 각각 종료 0이다. 물 대표 PNG/MP4는 이 수정 뒤 새 attempt로 다시 만들고 있으며 실제 결과 검토가 남아 있다.
|
||||||
|
|
||||||
|
## 물 영상 3D 대표 샘플 — scenes 검토 대기
|
||||||
|
|
||||||
|
2026-10-10 확인. [08 r2 검토](../../projects/water-hydrogen-bond-surface-tension/stages/08-scenes/r2/review.md)에 실제 명령·파일·한계를 기록했다. [제작 코드 안내](../../projects/water-hydrogen-bond-surface-tension/blender/README.md)의 기존 설치 경로를 사용했다.
|
||||||
|
|
||||||
|
| 실제 실행 | 종료 코드 | 확인한 내용 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `blender/test_model.py` | 0 | 없는 파일·Scene/H 누락·부피 driver 오류 각각 기대 종료 1, 실제 원본 통과. 닫힌 mesh 부피 상대 변화 9.56e-8 |
|
||||||
|
| `blender/render_preview.py --kind image` 및 `--kind video` | 0 | 실제 PNG·MP4 각각 3개, 같은 원본 SHA·프로필·Scene·범위, 전체 디코드 |
|
||||||
|
| `blender/inspection_views.py` 및 `blender/verify_design.py` | 0 | 실제 픽셀 변화, 31장면·30연결·38자막·6샘플, 25개 실물 경로·해시 |
|
||||||
|
| 최종 MP4 3개에 대한 `ffprobe` 및 FFmpeg 전체 디코드·프레임 추출 | 0 | 1080×1920·50fps, 202/244/200프레임·4040/4880/4000ms. 실제 시작/중간/마지막 9프레임과 모바일 화면 직접 확인 |
|
||||||
|
| 기존 작업 루트로 복사한 실물 25개와 전환 전 기준 36개 SHA-256 대조 | 0 | 원본·대표·증거 동일, 기존 r1·렌더러·스타일·WAV 변경 없음. 전환 전 project.json은 history의 원본으로 대조 |
|
||||||
|
| 독립 Blender 백그라운드 재열기·스키마·manifest 검사 | 0 | 실제 세 Scene·객체·연결 교체·부피와 전달 구조 확인. scenes pending 유지 |
|
||||||
|
|
||||||
|
실제 모델은 세 대표 Scene이며 나머지 31컷의 상태·연결은 설계다. 한국어 라벨·부분전하·자막·내레이션 합성, 정식 10–12 전체 제작과 새 완성본 검수는 후속 작업이다. 기술 검사와 검토자의 pass를 사용자 scenes 승인으로 바꾸지 않았다.
|
||||||
|
|
||||||
|
## 사용자 피드백에 따른 물 영역 포함 관계 수정
|
||||||
|
|
||||||
|
앞선 r2 품질 검수는 분자와 물 체적의 포함 관계를 놓쳤다. 해당 판단을 취소하고 이전 결과를 보존한 새 r3에서 수정했다. [r3 검토](../../projects/water-hydrogen-bond-surface-tension/stages/08-scenes/r3/review.md)에 실제 명령·실패 이유·파일·직접 관찰을 기록했다.
|
||||||
|
|
||||||
|
| 실제 실행 | 종료 코드 | 확인한 내용 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 새 `blender/test_model.py` | 0 | 실패 6사례 각각 기대 종료 1. r2 원본과 원자 중심 불변·mesh 정점 이동 fixture에서 포함 assertion 문구 확인, 수정 원본 GREEN |
|
||||||
|
| 독립 Blender 평가 정점·표시 면 검사 | 0 | 전체 705프레임·11,747,388정점이 실제 물 체적과 네 면 투영 합집합 안에 포함. 원본 r2 및 정점 오류는 기대 종료 1 |
|
||||||
|
| r3 `render_preview.py`, `inspection_views.py`, `verify_design.py` | 0 | 같은 새 원본·설정에서 3PNG·3MP4, 31장면·30연결·38자막·25개 실물 해시 |
|
||||||
|
| 실제 새 MP4 3개 probe·전체 디코드·프레임 추출 | 0 | 50fps·1080×1920·202/244/200프레임·4040/4880/4000ms. 실제 9프레임·모바일 화면에서 돌출 없음과 움직임 직접 확인 |
|
||||||
|
| 기존 작업 루트의 실물·이력 SHA-256 대조 | 0 | 새 실물 25개 동일, 이전 r2 파일 75개·전환 전 기준 36개 변경 없음 |
|
||||||
|
|
||||||
|
최소 모델 축 여백은 0.3246/0.4540/0.1150, 투영 윤곽 여백은 47.94px다. 설명 모형·카메라의 검사 수치이며 과학적 측정값이나 최종 자막·플랫폼 안전영역 보증이 아니다. 당시 사용자 scenes refs는 07 r1·08 r3·09 r3이고 결정은 pending이었다. 이후 실제 “수정본을 승인합니다” 응답으로 approved가 되어 정식 전체 제작을 진행했다.
|
||||||
|
|
||||||
|
## 승인된 Blender 전체 컷과 새 완성본
|
||||||
|
|
||||||
|
2026-10-10 확인. 07 r1·08 r3·09 r3의 실제 사용자 scenes 승인 이후 정식 10/11/12 r2를 사용했다. 원 대본·제목·WAV·38자막·스타일과 r1 피드백은 보존했다. 설치·Blender 모델/원 컷 재렌더·전역 설정 변경은 합성 작업에서 실행하지 않았다.
|
||||||
|
|
||||||
|
| 실제 실행 | 종료 코드 | 확인한 내용 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 렌더러 폴더의 첫 `npm.cmd run check` | 1 (RED) | r1 고정 승인 검사에서 실제 승인 r3를 거부하는 `3 !== 1` 재현 |
|
||||||
|
| 같은 폴더의 최종 `npm.cmd run check` | 0 | 실제 원본/PNG/MP4/WAV·현재 포인터/승인·SHA·시간과 24거부 사례. 31컷·62끝점·38자막·5528F |
|
||||||
|
| `npm.cmd run render -- preview c01 c05 c09 c16 c19 c24 c27 c31` | 0 | 실제 새 MP4/앵커를 사용한 합성 화면 24표본. 최종 검수와 구분 |
|
||||||
|
| `npm.cmd run render -- compose` 두 전체 시도 | 각각 0 | 실제 MP4/SRT 생성. 첫 c02의 빈 확대 위치 연결선을 최종 디코드 화면에서 발견·수정해 새 시도로 전체 재출력 |
|
||||||
|
| `C:/Users/baram/anaconda3/python.exe -B -X utf8 projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/verify_final.py` | 0 | 실제 최종 H.264/AAC·1080×1920·50fps·5528F·0–110560ms, 전체 AV 디코드, 원 8단위/통합 PCM 동일, SRT 38원문/시간 정확 동일·입력/렌더러 SHA 유지 |
|
||||||
|
| 같은 Python의 `projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/mobile_views.py` | 1→0 | cut 접두사가 caption까지 포함하는 파생 판 분류 오류 RED→GREEN. 실제 최종 93컷/38자막/32주석 전환 표본을 360px 검사 판 16/7/6개로 확인 |
|
||||||
|
| `C:/Users/baram/anaconda3/python.exe -B -X utf8 tools/blender/test_runner.py` | 0 | 기존 실행기 회귀 17개 |
|
||||||
|
| `C:/Users/baram/anaconda3/python.exe -B -X utf8 contracts/check.py` | 0 | 계약 회귀 10개 |
|
||||||
|
|
||||||
|
채택 MP4는 6,117,533바이트, SHA-256 `c36bfe927acffde38d60d5068cfc1acc22784bc4d4fdd30ca015bb141f919ce6`이다. [13 실제 경로·입력·실측](../../projects/water-hydrogen-bond-surface-tension/stages/13-compose/r2/provenance.json)과 [14 기술 검사](../../projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/technical-check.json)를 따른다. 원 WAV 2653440샘플과 디코드 AAC의 원 길이 구간 상관계수 0.9999665011969985, SNR 41.68737842978274dB다. AAC 끝의 패딩 768샘플은 원 발화 길이와 구별하며 원 WAV·속도·gain·trim은 바꾸지 않았다.
|
||||||
|
|
||||||
|
실제 최종 163프레임·29모바일 검사 판에서 전하 대상·원자/결합·물 영역 포함·같은 양/별도 비교 조건·확대개념도·글자/가림과 22 continuous/8 cut 연결을 직접 확인했다. 소스 프레임 검사를 최종 영상 검사로 대체하지 않았다. 정상 속도 전체 재생·청취 및 플랫폼 앱 UI는 미확인이므로 [14 보고](../../projects/water-hydrogen-bond-surface-tension/stages/14-review/r2/review.md)의 해당 영역은 not_checked, 추천은 incomplete다. 새 final 승인 전에는 제작 완료가 아니다.
|
||||||
|
|
||||||
|
렌더 당시 project 바이트는 별도 render-time-project.json에 보존했다. 이 증거 SHA와 현재 brief/settings/01–12 포인터·상태 및 topic/story/scenes 승인을 엄격히 대조한다. 총괄의 같은 13/14 포인터에 대한 정상 상태 변경과 final 검토 갱신을 입력 원본 변경과 구별한다. 기존/실패 시도·캐시·이전 r1을 삭제하지 않았다.
|
||||||
|
|
||||||
|
## 새 완성본 연결 후 저장소 통합 검사
|
||||||
|
|
||||||
|
2026-10-10, 실제 수락 MP4/SRT와 제출 자산을 동일 경로·SHA로 저장소 루트에 복사한 뒤 실행했다. 총괄은 이전 final 기록을 보존하고 13/14 awaiting_review 및 r2 final pending을 기록했다.
|
||||||
|
|
||||||
|
| 실제 실행 | 종료 코드 | 확인한 내용 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 기존 Python의 `contracts/check.py` | 0 | 통합 상태에서 계약 회귀 10개 및 로컬 참조 |
|
||||||
|
| 같은 Python의 `tools/blender/test_runner.py` | 0 | 실행기 전체 회귀 17개 |
|
||||||
|
| 제작 renderer의 `npm.cmd run check` | 0 | 실제 새 입력과 24개 거부 사례 |
|
||||||
|
| 같은 Python의 전체 형식·스킬 검사 | 0 | 스키마 적용 결과·이력 64개, 역할 TOML 8개, 스킬 15개 각각 유효, 두 루트 지침 동일 |
|
||||||
|
| 별도 읽기 전용 최종 실물 검사 | 0 | 실제 영상·SRT·입력·렌더러 SHA, 원 WAV, 이전 기준/대표 파일 111개 보존, 전체 AV 디코드. 정상 속도 시청·청취는 미확인 |
|
||||||
|
| `git diff --check` | 0 | 통합 변경의 공백 오류 없음 |
|
||||||
|
|
||||||
|
이 검사에서 실제 원본·자막·영상은 바뀌지 않았다. 사용자 final 승인은 pending이며 기술 검사 통과를 해당 승인으로 바꾸지 않는다.
|
||||||
|
|
||||||
|
## 최종 사용자 승인 기록 후 검사
|
||||||
|
|
||||||
|
2026-10-10, 실제 사용자 “승인합니다”와 “최종 승인 작업을 계속하세요” 응답에 따라 [승인 기록](../../projects/water-hydrogen-bond-surface-tension/history/final-r2-user-approval.md)을 저장했다. 승인 직전 project 사본을 보존하고 final을 같은 13/14 r2의 approved로 연결했다. 13은 complete, 14는 직접 전체 시청·청취 근거 부재로 awaiting_input이며 기존 not_checked/incomplete 보고는 변경하지 않았다. 사용자 승인과 기술 검수 완료를 구분한다.
|
||||||
|
|
||||||
|
기존 Python의 `contracts/check.py` 10개·`tools/blender/test_runner.py` 17개, 전체 형식 검사(스키마 결과/이력 65개·역할 8개·스킬 15개), renderer의 `npm.cmd run check` 24개 거부 사례, 읽기 전용 최종 실물 검사 및 `git diff --check`를 다시 실행해 각각 종료 코드 0을 확인했다. MP4/SRT·원 WAV·렌더 입력 SHA·이전 결과 111개와 전체 AV 디코드는 유지됐다. 직접 전체 시청·청취 결과를 새로 만들거나 미확인을 pass로 처리하지 않았다. 실행 명령·결과는 `.dryforge/backup/blender-run/final-approval-checks.json`에 보존한다.
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# 제작 단계의 전달과 완료 기준
|
||||||
|
|
||||||
|
## 문서와 양식
|
||||||
|
|
||||||
|
제작 입력, 단계별 JSON, 진행 상태, 검토·수정 규칙을 아래와 같이 통일한다. 내용과 화면에는 중심 질문·도입과 제목의 약속·결말의 회수·원인과 결과를 보여주는 상태 변화를 적용한다. 양식·스키마는 실제 생성 프로그램이 아니며, 요청서를 작성한 것과 결과 파일을 받은 것을 구분한다.
|
||||||
|
|
||||||
|
- [진행 기록 스키마](../contracts/project.schema.json)와 [빈 진행 기록](../templates/project.json)
|
||||||
|
- [단계 결과 스키마](../contracts/artifact.schema.json)와 [14단계 빈 양식](../templates/stages/)
|
||||||
|
- [사용자 검토 양식](../templates/review.md)
|
||||||
|
|
||||||
|
스키마는 JSON Schema Draft 2020-12다. 파서는 필드·형식·열거값을 검사한다. 완료 판단에는 아래 의미·참조·실제 파일·사용자 응답 확인이 추가로 필요하다. 빈 문자열·배열·null은 미작성 상태를 표현하며, 형식 검사를 통과해도 완료가 아니다. 사용하지 않는 필드를 임의로 추가하지 않는다.
|
||||||
|
|
||||||
|
## 제작 입력과 공통 설정
|
||||||
|
|
||||||
|
필수 입력은 `topic`, `language`이며 공백만 있거나 빠졌으면 보완한다. 언어 이름·코드를 의미 확인 후 보존한다. 입력 언어로 대본·제목·내레이션·자막을 작성하며 지원되지 않는 언어를 다른 언어로 바꾸지 않는다. 새로운 요청은 저장소에서 유일한 소문자 영숫자·하이픈 `project_id`로 시작한다. 빈 양식의 video-001은 복사 후 실제 식별자로 바꾼다.
|
||||||
|
|
||||||
|
공통 설정은 세로 9:16, 최종 180000ms 이내, 각 컷 0ms 초과 5000ms 이하, 스타일 기준 `references/1.png`, TTS `Qwen3-TTS`, conda 환경 `qwen3-tts`, 새 제작의 시각 엔진 `Blender`다. 단순 배경·부드러운 조명·선명한 형태의 교육용 3D를 사용하며 원본의 단순 형태·정돈된 색을 참고한다. 원본을 보존하고 평면 복제와 캠핑 소재·인물·상표를 강제하지 않는다. 쉬운 표현은 대상 시청자를 어린이로 제한하는 규칙이 아니다.
|
||||||
|
|
||||||
|
## 저장과 전달
|
||||||
|
|
||||||
|
```text
|
||||||
|
projects/<project_id>/
|
||||||
|
project.json
|
||||||
|
stages/<stage_id>/r<revision>/artifact.json
|
||||||
|
stages/<stage_id>/r<revision>/review.md
|
||||||
|
media/
|
||||||
|
```
|
||||||
|
|
||||||
|
모든 데이터 경로는 저장소 루트 기준 상대 경로다. 현재 작업 폴더나 검토 문서의 위치를 기준으로 해석하지 않는다. 외부 실행파일 경로·키·인증정보를 양식에 기록하지 않는다.
|
||||||
|
|
||||||
|
단계 결과는 `schema_version`="1.1"(새 제작) 또는 "1.0"(기존 결과), `project_id`, `stage_id`, `revision`, `inputs`, `payload`를 가진다. 버전은 1부터 시작하고 이전 결과를 덮어쓰지 않는다. `inputs`는 실제 사용한 선행 결과의 `{stage_id, revision}` 배열이다. 중복·자기 참조·다른 제작물 참조를 금지한다. 14는 13과 검수에서 직접 사용한 유효 선행 결과를 기록한다.
|
||||||
|
|
||||||
|
`project.json`의 필드는 `schema_version`, `project_id`, `brief`, `settings`, `stages`, `reviews`다. stages는 아래 14개를 각각 한 번 포함하는 `{stage_id, status, revision, artifact_path, issues}` 배열이다. 결과가 없으면 revision/artifact_path는 null이다. issues는 `{code, message, blocking}` 배열이다. reviews는 네 검토를 각각 한 번 포함하는 `{review_id, decision, artifact_refs, feedback}` 배열이다. artifact_refs는 검토한 `{stage_id, revision}` 배열이고 feedback은 실제 선택·피드백을 기록한 문자열이다.
|
||||||
|
|
||||||
|
총괄만 진행 기록의 상태·현재 결과 포인터·승인을 바꾼다. 담당은 자기 단계의 새 버전 결과와 review.md를 제출하고, 준비 여부·미확인 내용·문제를 총괄에 보고한다. 같은 단계·버전을 동시에 작성하지 않는다. review.md는 JSON의 의미를 보존하며 별도로 승인 내용을 바꾸지 않는다.
|
||||||
|
|
||||||
|
## 단계별 내용과 완료 기준
|
||||||
|
|
||||||
|
필드의 정확한 구조와 타입은 해당 양식·스키마를 사용한다. 배열 순서가 이야기·음성·컷·합성 순서다. 배열 안의 식별자는 유일해야 하고 모든 참조는 같은 제작물의 사용 버전에 존재해야 한다. 빈 필수 텍스트와 미선택 값은 완료 전에 채운다.
|
||||||
|
|
||||||
|
| 단계와 양식 | 선행 결과 | 필요한 내용과 완료 확인 |
|
||||||
|
|---|---|---|
|
||||||
|
| [01-topic](../templates/stages/01-topic.json) | 제작 입력 | 주제 안의 후보·선택 ID·이유. 선택은 후보에 존재하며 topic 승인 필요 |
|
||||||
|
| [02-research](../templates/stages/02-research.json) | 01 | 출처·주장·미해결 질문. verified 주장은 실제 확인한 출처를 하나 이상 참조. 불확실한 주장은 uncertain |
|
||||||
|
| [03-script](../templates/stages/03-script.json) | 01,02 | 블록별 텍스트·claim_ids·전체 대본·예상 길이. 순서와 내용 일치, 사실 주장은 verified. story 승인 필요 |
|
||||||
|
| [04-title](../templates/stages/04-title.json) | 03 | 제목 후보·이유·선택. 대본의 약속과 일치하며 story 승인 필요 |
|
||||||
|
| [05-tts-split](../templates/stages/05-tts-split.json) | 03 | 단위별 block_ids·표시/발화 텍스트·발음 메모. 순서·의미·누락·중복 확인 |
|
||||||
|
| [06-tts](../templates/stages/06-tts.json) | 05 | 도구·환경·언어·단위 요청. 실제 모델·화자·단위 WAV·통합 WAV·실측 unit_ranges 결과. 전체 파일과 시간 확인 |
|
||||||
|
| [07-cuts](../templates/stages/07-cuts.json) | 05,06 | 실측 통합 길이·확인 방법·연속 컷·별도 자막. 전체 음성을 덮고 각 컷 5초 이내. scenes 승인 필요 |
|
||||||
|
| [08-scenes](../templates/stages/08-scenes.json) | 03,07 | 각 컷의 설명·시작/끝 상태·카메라·인접 연결과 preview(대표 PNG·짧은 MP4·.blend). 누락·중복 없음, 연결 이유·상태 일치. scenes 승인 필요 |
|
||||||
|
| [09-entities](../templates/stages/09-entities.json) | 03,08 | 인물·배경·고정 특징·컷별 등장 참조. 모든 컷을 정확히 한 번 포함. scenes 승인 필요 |
|
||||||
|
| [10-references](../templates/stages/10-references.json) | 02,08,09 | Blender·RenderProfile·스타일/인물/배경/물체 요청, 실제 .blend·PNG 결과. 모든 요청 ID와 실제 오브젝트/컬렉션·고정 특징 확인. style/object만 entity_id=null |
|
||||||
|
| [11-images](../templates/stages/11-images.json) | 02,03,07,08,09,10 | 컷별 start/end·Scene 이름·논리 프레임·기준 ID 요청, 실제 .blend·PNG. 각 컷의 두 위치가 정확히 하나씩 존재, 장면·근거·연속성 일치 |
|
||||||
|
| [12-motion](../templates/stages/12-motion.json) | 07,08,11 | 11 실제 .blend·동일 RenderProfile·Scene·프레임 범위·실측 길이·움직임 요청과 MP4 결과. 모든 컷과 길이·상태 일치 |
|
||||||
|
| [13-compose](../templates/stages/13-compose.json) | 03,04,06,07,12 | timeline·통합 음성·자막·제목 요청, 실제 MP4·SRT 결과. 세로 비율·전체 길이·파일·시간·순서 확인 |
|
||||||
|
| [14-review](../templates/stages/14-review.json) | 13 및 검수에 사용한 결과 | 6영역 checks·발견·추천. 근거와 수정할 단계·컷. 검수자의 pass와 사용자의 final 승인 구분 |
|
||||||
|
|
||||||
|
`*_ms`는 0 이상 정수이고 아직 정하지 않은 시간은 null이다. 완료한 길이는 양수다. 고정 특징·open_questions·각종 *_ids는 문자열 배열이다. accessed_at은 ISO 8601 날짜, blocking/verified는 불리언이다. 그 밖의 텍스트는 문자열이며 selected_id는 미선택 시 null, findings.cut_id는 특정 컷이 아니면 null이다. 실제 사용한 모델·화자는 06 result.model/voice에 기록한다.
|
||||||
|
|
||||||
|
## 실제 파일과 시간 확인
|
||||||
|
|
||||||
|
media는 `{path, kind, verified, duration_ms, width, height}`다. kind는 audio/image/video, 미확인 값은 null, 검증하지 않은 파일은 verified=false다. 실제 파일 존재·내용 형식·메타데이터를 확인한 경우만 true다. 확장자나 요청 길이만으로 실제 결과를 확인했다고 쓰지 않는다.
|
||||||
|
|
||||||
|
- 음성은 WAV, width/height=null, 실제 duration_ms 필요.
|
||||||
|
- 이미지는 PNG, duration_ms=null, 실제 width/height 필요.
|
||||||
|
- 영상은 MP4, 실제 duration_ms·width·height 필요. 최종 자막은 SRT이며 파일·언어·내용·정렬을 확인한다.
|
||||||
|
|
||||||
|
외부 단계의 result=null이면 awaiting_input이다. 부분 결과는 보존하되 요청 항목을 모두 받기 전 complete로 처리하지 않는다. 결과의 unit_id/reference_id/frame_id/cut_id는 요청에 존재해야 한다. 가상 예제 경로는 검증된 실제 파일로 기록하지 않는다.
|
||||||
|
|
||||||
|
Qwen3-TTS의 설치 탐색, 모델 로딩, 실제 추론 성공은 다른 확인이다. 기존 환경을 재설치하지 않고 실제 제작 시 모델·화자·언어 지원을 확인한다. Blender 버전·렌더링 지원·실제 원본 재열기·PNG/MP4 출력은 따로 확인한다. 설치 확인만으로 성공을 주장하지 않는다. 합성은 승인된 기존 Remotion + FFmpeg 경로를 쓰며 전역 환경 재설치·무단 도구 대체를 하지 않는다.
|
||||||
|
|
||||||
|
06의 통합 음성이 시간 기준이다. unit_ranges는 해당 단위의 실제 통합 음성 구간을 기록하며 순서·전체 포함·겹침을 확인한다. 07 cuts는 0부터 통합 음성 끝까지 빈 구간·중복 없이 이어진다. 각 컷은 start_ms < end_ms, end_ms-start_ms <= 5000이다. 긴 문장을 화면 컷으로 나눌 수 있지만 단어·의미를 누락하거나 순서를 바꾸지 않는다.
|
||||||
|
|
||||||
|
captions는 컷과 다른 단위다. 여러 컷을 걸칠 수 있으며 0 <= start_ms < end_ms <= 통합 길이, 텍스트·표시 순서를 보존한다. 글자 수 기반 예상 정렬을 실측으로 표시하지 않는다. timing_evidence에 파일 측정·청취·정렬 확인 방법을 적는다. 실제 음성이 180000ms를 넘으면 대본 수정과 story 재승인으로 돌아간다.
|
||||||
|
|
||||||
|
## 상태와 네 사용자 검토
|
||||||
|
|
||||||
|
| 단계 상태 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| pending | 시작 전 |
|
||||||
|
| draft | 작성 중 |
|
||||||
|
| awaiting_input | 필수 자료·실제 결과·파일 부재 |
|
||||||
|
| awaiting_review | 검토 결과 버전을 제시하고 사용자 결정 대기 |
|
||||||
|
| complete | 완료 조건과 필요한 사용자 승인 충족 |
|
||||||
|
| stale | 사용한 선행 버전의 변경으로 다시 확인해야 함 |
|
||||||
|
| failed | 실행·검증 실패가 보고됨 |
|
||||||
|
|
||||||
|
기본 흐름은 pending→draft, 자료 부재 시 draft→awaiting_input, 결과 준비 시 draft/awaiting_input→complete 또는 awaiting_review다. 승인 후 complete, 변경 요청·실패 복구는 draft에서 새 버전을 만든다. 그 밖의 전이는 현재 기록을 먼저 확인하고 issues에 이유를 남긴다.
|
||||||
|
|
||||||
|
| 검토 ID | 사용자가 확인하는 결과 | 승인 전 진행하지 않는 곳 |
|
||||||
|
|---|---|---|
|
||||||
|
| topic | 01 각도·범위 | 02 자료 조사 |
|
||||||
|
| story | 02 근거와 03 대본·04 제목 | 05 TTS 분할 |
|
||||||
|
| scenes | 07 실제 컷·08 연결과 실제 대표 PNG/짧은 MP4·09 인물·배경 | 정식 10–12 제작 |
|
||||||
|
| final | 13 실제 완성본·14 보고 | 제작 완료 선언 |
|
||||||
|
|
||||||
|
검토 결정은 pending/approved/changes_requested/stale다. 승인 버전을 artifact_refs로 고정하고 실제 사용자 응답을 feedback에 기록한다. 응답 없음·담당의 추천은 승인이 아니다. 같은 승인 버전으로 재개할 때는 다시 묻지 않는다. 한 검토 묶음 안에서는 준비된 유효 결과로 묶음을 완성할 수 있다. 예를 들어 07 결과로 08·09를 준비한 뒤 scenes 검토를 요청하되, 승인 전에 10으로 넘어가지 않는다.
|
||||||
|
|
||||||
|
## 재개와 수정
|
||||||
|
|
||||||
|
재개는 brief·현재 포인터·버전별 결과·승인·실제 파일·참조의 유효성을 확인한 뒤 가장 앞선 미해결 작업부터 이어간다. 오래된 버전·다른 제작물의 결과를 섞지 않는다. 같은 주제의 새 제작과 기존 제작의 재개를 구분한다.
|
||||||
|
|
||||||
|
수정은 새 r<revision>을 만든다. inputs의 실제 관계를 따라 영향을 받는 후속 결과를 stale로 표시하고, 해당 결과를 승인한 검토도 stale로 표시한다. 변경과 관계없는 결과는 유지한다. 가령 04 제목만 변경하면 이를 쓰는 13·14와 story/final 검토는 재확인하지만, 05–12의 기존 음성·장면·이미지를 무조건 재생성하지 않는다. story 승인을 받은 뒤 기존 미디어와 새 제목을 연결할 수 있다.
|
||||||
|
|
||||||
|
언어 변경은 해당 언어를 사용하는 조사·대본·제목·발화·자막과 후속 결과를 재검토한다. 단순한 표시 수정이 의미·음성을 바꾸는지 확인하고 영향 범위와 보존할 결과를 보고한다. 승인 경계가 다시 열리면 새 후속 작업 전 재승인을 받는다. 이전 검토와 결과는 버전별 review.md에 보존한다.
|
||||||
|
|
||||||
|
## 검수의 판정과 근거
|
||||||
|
|
||||||
|
14의 area는 story/visual/audio/captions/timing/consistency, verdict는 pass/fail/not_checked다. 모든 영역의 실제 근거가 있어야 하며 미확인 영역을 pass로 만들지 않는다. findings는 blocking/minor, 설명, 수정할 stage_id와 cut_id를 기록한다. 추천은 pass/revise/incomplete다.
|
||||||
|
|
||||||
|
pass는 여섯 영역 모두 pass이고 blocking 발견이 없을 때만 가능하다. 실제 완성본이 없거나 볼 수 없으면 incomplete다. 제목과 답의 일치, 구조·주석의 정확성, 청취·자막·실측·연속 상태의 실제 확인 근거를 기존 checks.evidence에 기록하며 새 필드·승인 지점을 만들지 않는다.
|
||||||
|
|
||||||
|
## 형식 검사와 의미 확인의 경계
|
||||||
|
|
||||||
|
스키마는 잘못된 필드·타입·단계·상태, 진행 기록의 14단계/4검토 구성, 다른 도구명, 음성·이미지의 해당 없는 메타데이터, 근거 없는 pass 구조를 검사한다. 주제·언어의 실사용 값, 배열별 식별자·참조의 유효성, 선택 ID, 대본 일치, 파일 실존·언어 지원, 승인 진위, 실제 시간·길이·화면 판독성, 종속 결과의 stale 처리는 담당과 총괄이 별도로 확인한다.
|
||||||
|
|
||||||
|
파싱·스키마 검사만으로 Codex의 실제 에이전트·스킬 탐색이나 미디어 생성을 확인했다고 쓰지 않는다. [공식 에이전트 형식](https://learn.chatgpt.com/docs/agent-configuration/subagents#custom-agents)과 [스킬 위치](https://learn.chatgpt.com/docs/build-skills#where-to-save-skills)를 따른다. 실제 호출 환경에서 목록에 보이는지 확인하고, 보이지 않으면 프로젝트 루트에서 새 세션으로 탐색한다.
|
||||||
|
|
||||||
|
## Blender 전환과 대표 품질 검토
|
||||||
|
|
||||||
|
[단계 스키마](../contracts/artifact.schema.json)의 1.1 필드·타입·열거값을 사용한다. 새 양식은 1.1이며 기존 1.0의 정의·승인·실제 결과를 보존하고 선행 자료로 참조한다. 프로젝트 진행 상태·승인 열거값과 14단계·8역할·15스킬·네 검토는 그대로다.
|
||||||
|
|
||||||
|
scene-director가 08에서 대표 Scene의 상태·카메라·해설 목적을 정하고 visual-designer/video-editor의 해당 책임으로 제한된 대표 .blend·PNG·짧은 MP4를 준비한다. 결과는 08 preview와 같은 버전 review.md에 기록한다. 이 예외는 scenes 승인 전에 품질을 검토하는 근거 제작만 허용한다. 정식 10–12를 준비·완료했다고 기록하거나 전체 제작을 진행하지 않는다. 대표 샘플의 실제 파일과 단순 배경·부드러운 조명·선명한 형태·변화 판독성을 07/08/09의 같은 scenes 검토에서 제시한다. 추가 승인 지점은 없다. 샘플이나 장면 변경은 08 새 버전에 기록하고 영향을 받는 09와 승인 묶음을 갱신한다.
|
||||||
|
|
||||||
|
새 08·09 승인 후 10은 모델·재질·고정 자산을, 11은 컷별 Scene·끝점 상태와 PNG를, 12는 같은 원본·Scene·RenderProfile에서 [start_frame,end_frame) 구간을 렌더링한다. 같은 컷 start/end는 같은 Scene이며 continuous는 실제 경계 상태가 같아야 한다. cut 경계는 Scene을 구별하고 캐시에 Scene 이름을 포함한다. 12의 프레임 수×1000/fps는 요청·실측 컷 길이와 일치해야 한다. PNG 끝점 평가와 MP4의 마지막 프레임은 서로 다른 개념이므로 마지막 화면 확인 근거를 구분한다.
|
||||||
|
|
||||||
|
.blend의 실제 존재·SHA-256·Blender 버전·재열기·오브젝트/컬렉션·Scene 이름 검증은 PNG/MP4 디코딩·해상도·길이·상태 검증과 별개다. 설치 정보만으로 verified=true를 기록하지 않는다. 실패·부분 결과를 보존하고 필요한 단계로 돌아간다. 실제 최종 영상의 전체 재생·청취가 없으면 검수 해당 영역은 not_checked다.
|
||||||
|
|
||||||
|
[계약 회귀 검사](../contracts/check.py)는 1.0/1.1 구조와 일부 참조·완료 조건을 검사한다. semantic_errors(data,context,complete=False)는 구조 검사 후 사용하며 context는 실제 사용 버전의 {stage_id:artifact}다. 1.0은 이 함수의 추가 의미 검사 대상이 아니다. 전체 파일·사용자 승인 진위·화면 품질 검증을 수행하지 않는다. 반환한 문제 목록이 비었어도 전체 단계 완료 증거를 대신하지 않는다.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.1",
|
||||||
|
"project_id": "contract-example",
|
||||||
|
"stage_id": "08-scenes",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [],
|
||||||
|
"payload": {
|
||||||
|
"scenes": [],
|
||||||
|
"connections": [],
|
||||||
|
"preview": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.1",
|
||||||
|
"project_id": "contract-example",
|
||||||
|
"stage_id": "10-references",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [],
|
||||||
|
"payload": {
|
||||||
|
"request": {
|
||||||
|
"engine": "Blender",
|
||||||
|
"style_reference": "references/1.png",
|
||||||
|
"render_profile": {
|
||||||
|
"engine": "BLENDER_EEVEE",
|
||||||
|
"device": "GPU",
|
||||||
|
"fps": 50,
|
||||||
|
"width": 1080,
|
||||||
|
"height": 1920,
|
||||||
|
"samples": 32,
|
||||||
|
"seed": 1
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "water",
|
||||||
|
"kind": "object",
|
||||||
|
"entity_id": null,
|
||||||
|
"prompt": "물방울"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.1",
|
||||||
|
"project_id": "contract-example",
|
||||||
|
"stage_id": "11-images",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [],
|
||||||
|
"payload": {
|
||||||
|
"request": {
|
||||||
|
"engine": "Blender",
|
||||||
|
"scene_path": "examples/blender-contract/preview.blend",
|
||||||
|
"render_profile": {
|
||||||
|
"engine": "BLENDER_EEVEE",
|
||||||
|
"device": "GPU",
|
||||||
|
"fps": 50,
|
||||||
|
"width": 1080,
|
||||||
|
"height": 1920,
|
||||||
|
"samples": 32,
|
||||||
|
"seed": 1
|
||||||
|
},
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"id": "c1-start",
|
||||||
|
"cut_id": "c1",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "상태 설명",
|
||||||
|
"reference_ids": [
|
||||||
|
"water"
|
||||||
|
],
|
||||||
|
"scene_name": "Water",
|
||||||
|
"frame_index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c1-end",
|
||||||
|
"cut_id": "c1",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "상태 설명",
|
||||||
|
"reference_ids": [
|
||||||
|
"water"
|
||||||
|
],
|
||||||
|
"scene_name": "Water",
|
||||||
|
"frame_index": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.1",
|
||||||
|
"project_id": "contract-example",
|
||||||
|
"stage_id": "12-motion",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [],
|
||||||
|
"payload": {
|
||||||
|
"request": {
|
||||||
|
"engine": "Blender",
|
||||||
|
"scene_path": "examples/blender-contract/preview.blend",
|
||||||
|
"render_profile": {
|
||||||
|
"engine": "BLENDER_EEVEE",
|
||||||
|
"device": "GPU",
|
||||||
|
"fps": 50,
|
||||||
|
"width": 1080,
|
||||||
|
"height": 1920,
|
||||||
|
"samples": 32,
|
||||||
|
"seed": 1
|
||||||
|
},
|
||||||
|
"clips": [
|
||||||
|
{
|
||||||
|
"cut_id": "c1",
|
||||||
|
"start_frame_id": "c1-start",
|
||||||
|
"end_frame_id": "c1-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "둥글게",
|
||||||
|
"scene_name": "Water",
|
||||||
|
"start_frame": 0,
|
||||||
|
"end_frame": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blender 1.1 계약 요청 예제
|
||||||
|
|
||||||
|
이 폴더의 네 JSON은 구조를 보여주는 요청 예제다. 실제 .blend·PNG·MP4·사용자 승인·완료 결과가 없으며 scene_path는 생성 예정 경로다. 08 preview=null, 10–12 result=null을 유지한다. 이전 [1.0 연결 예제](../water-cycle/README.md)는 변환하지 않는다.
|
||||||
|
|
||||||
|
실제 제작에서는 같은 제작물의 07 실측 컷·08 장면·09 요소와 승인 버전을 연결한다. 이 예제의 c1과 0–4000ms·Water Scene·50fps·200 프레임은 설명용 값이며 실제 내레이션 측정이 아니다. 프레임은 [0,200)이고 끝점 PNG는 논리 200을 평가한다. Blender 실제 번호는 논리 프레임+1이다.
|
||||||
|
|
||||||
|
[전달 계약](../../docs/contracts.md)과 [공통 완료 기준](../../docs/workflow-contracts.md)을 따른다. contracts/check.py는 예제와 빈 양식·기존 1.0 제작을 구조 검사하고 일부 부정·참조·미완료 사례를 실행한다. 파일 재열기·렌더링·화면·청취·사용자 승인 성공을 증명하지 않는다.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"cases": [
|
||||||
|
{"id": "topic_pending", "scenario": "01 결과가 있고 topic 결정이 pending이다.", "expected": "02를 시작하지 않고 같은 01 버전의 사용자 검토를 요청한다."},
|
||||||
|
{"id": "story_pending", "scenario": "03·04가 준비됐으나 story 승인이 없다.", "expected": "05를 시작하지 않고 근거·대본·제목 검토를 요청한다."},
|
||||||
|
{"id": "scenes_pending", "scenario": "07·08·09가 준비됐으나 scenes 승인이 없다.", "expected": "10을 시작하지 않고 같은 유효 버전의 장면 검토를 요청한다."},
|
||||||
|
{"id": "final_pending", "scenario": "검수자가 pass를 추천했으나 final 사용자 승인이 없다.", "expected": "제작 완료를 선언하지 않고 실제 완성본과 보고를 사용자에게 제시한다."},
|
||||||
|
{"id": "missing_audio", "scenario": "06 request는 작성했으나 result=null이고 WAV가 없다.", "expected": "awaiting_input이며 07의 실측 시간을 확정하지 않는다."},
|
||||||
|
{"id": "unsupported_language", "scenario": "실제 확인한 TTS 모델이 입력 언어를 지원하지 않는다.", "expected": "blocking 문제와 미확인 조건을 보고하고 입력·해결을 기다린다. 언어·도구를 무단 대체하지 않는다."},
|
||||||
|
{"id": "long_narration", "scenario": "실제 통합 음성 길이가 180001ms다.", "expected": "대본 수정과 story 재승인으로 돌아간다. 누락·과도한 속도 변경으로 숨기지 않는다."},
|
||||||
|
{"id": "long_cut", "scenario": "컷 구간이 0–5001ms다.", "expected": "컷을 수정한다. 허용 범위는 0ms 초과 5000ms 이하다."},
|
||||||
|
{"id": "cut_gap", "scenario": "앞 컷의 끝은 4000ms, 다음 시작은 4100ms다.", "expected": "빈 구간을 해결하기 전 07을 완료하지 않는다."},
|
||||||
|
{"id": "cut_overlap", "scenario": "앞 컷의 끝은 4000ms, 다음 시작은 3900ms다.", "expected": "중복 구간을 해결하기 전 07을 완료하지 않는다."},
|
||||||
|
{"id": "missing_images", "scenario": "11 요청의 start만 제출되고 end 이미지가 없다.", "expected": "부분 결과를 보존하고 awaiting_input이며 전체 완료·12 진행은 보류한다."},
|
||||||
|
{"id": "missing_video", "scenario": "13 request는 있지만 실제 완성본이 없다.", "expected": "14의 미확인 영역은 not_checked이고 추천은 incomplete다. final 완료가 아니다."},
|
||||||
|
{"id": "title_only_change", "scenario": "04 제목만 새 버전으로 바뀌었다.", "expected": "실제 inputs 관계에 따라 13·14와 story/final을 재확인한다. 관련 없는 05–12는 보존한다."},
|
||||||
|
{"id": "script_change", "scenario": "03 대본 내용이 새 버전으로 바뀌었다.", "expected": "04–14의 실제 종속 결과와 관련 승인을 stale 처리하고 새 버전을 작성한다. 이전 버전은 보존한다."},
|
||||||
|
{"id": "resume_approved_version", "scenario": "같은 유효 승인 버전으로 재개하며 필요한 파일·참조가 유효하다.", "expected": "동일 승인을 다시 묻지 않고 가장 앞선 미해결 작업부터 이어간다."}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 물순환 연결 예제
|
||||||
|
|
||||||
|
주제는 “비가 온 뒤 물웅덩이는 어디로 사라질까”, 언어는 한국어다. 양식과 단계 간 참조를 보여주는 자료이며 실제 제작 실행 기록이 아니다. 실제 음성·이미지·영상·사용자 승인은 없다.
|
||||||
|
|
||||||
|
## 현재 진행 기록
|
||||||
|
|
||||||
|
[project.json](project.json)은 [01 주제 결과](stages/01-topic/r1/artifact.json)의 topic 검토에서 멈춘다. 선택 ID는 예제의 제안이며 실제 승인 기록이 아니다. 모든 검토 decision은 pending이고 02 이후 현재 포인터는 null이다. 전체 진행 스킬로 재개한다면 먼저 topic 검토를 요청해야 한다.
|
||||||
|
|
||||||
|
`illustrative/` 아래의 02–14 결과는 후속 양식을 읽기 위한 독립적인 샘플이다. 현재 진행 기록에 등록하거나 검토를 건너뛰어 실행한 결과가 아니다. 실제 제작에 사용하려면 현재 승인·선행 결과·실제 파일을 확인하고 새 버전을 작성한다.
|
||||||
|
|
||||||
|
## 내용에서 화면까지
|
||||||
|
|
||||||
|
- [조사](illustrative/02-research/r1/artifact.json)는 [USGS 증발 설명](https://www.usgs.gov/water-science-school/science/evaporation-and-water-cycle)과 [침투 설명](https://www.usgs.gov/water-science-school/science/infiltration-and-water-cycle)을 확인한 주장·출처를 연결한다. 비율·지역별 수치를 새로 만들지 않는다.
|
||||||
|
- [대본](illustrative/03-script/r1/artifact.json)은 질문→공기로 이동→이름 붙이기→땅으로 이동→조건→질문 회수다. 실제 위기나 실패한 대안이 없는 소재이므로 해당 구성을 넣지 않는다. 마지막 b6은 도입과 [제목](illustrative/04-title/r1/artifact.json)의 질문에 답한다.
|
||||||
|
- [발화 단위](illustrative/05-tts-split/r1/artifact.json)는 블록의 의미와 순서를 유지한다. [TTS 요청](illustrative/06-tts/r1/artifact.json)은 result=null이다. 실제 배정 후 WAV·통합 구간을 받기 전에는 awaiting_input이며 완료할 수 없다.
|
||||||
|
- [컷](illustrative/07-cuts/r1/artifact.json)은 32000ms 전체와 4000ms 컷을 가정한 모의 데이터다. 실제 낭독 길이·속도·정렬이 아니다. 일부 자막과 발화 단위는 두 컷을 걸쳐 컷과 일대일이 아님을 보여준다.
|
||||||
|
- [장면](illustrative/08-scenes/r1/artifact.json)은 공기 쪽의 상태 변화는 같은 시점으로 잇고, 땅속 관계를 설명할 때 단면으로 전환한다. 도해 기호는 실제 크기나 수증기가 보이는 모습이 아니다. [반복 배경](illustrative/09-entities/r1/artifact.json)은 같은 장소의 위치·색·방향을 유지한다.
|
||||||
|
- [기준 요청](illustrative/10-references/r1/artifact.json)과 [컷 이미지 요청](illustrative/11-images/r1/artifact.json)은 사용자 그림체를 지정하고 컷마다 start/end를 하나씩 둔다. 실제 reference 결과는 아직 없어 요청 ID 연결 방식만 보여준다.
|
||||||
|
- [영상 요청](illustrative/12-motion/r1/artifact.json)과 [합성 요청](illustrative/13-compose/r1/artifact.json)의 경로·길이는 예정·모의 정보이며 파일이 존재하지 않는다. 지정하지 않은 생성·합성 엔진을 추가하지 않는다.
|
||||||
|
- [검수](illustrative/14-review/r1/artifact.json)는 여섯 영역 모두 not_checked, recommendation=incomplete다. 실제 완성본이나 final 승인으로 취급할 수 없다.
|
||||||
|
|
||||||
|
각 폴더의 review.md에는 적용한 원칙과 비적용 이유, 실제 배정 시 필요한 상태·조건을 적었다. 모의 시간을 실측 근거로 바꾸거나 전체 illustrative 폴더를 실제 완료한 제작물처럼 복사하지 않는다.
|
||||||
|
|
||||||
|
## 실제 결과 제출 형태
|
||||||
|
|
||||||
|
외부 결과는 해당 요청의 ID를 유지하며 result의 필수 키를 채운다. 실제 파일 확인 전에는 verified=false·미확인 메타데이터=null이다. 결과 구조의 전체 타입은 [결과 스키마](../../contracts/artifact.schema.json)에 있다.
|
||||||
|
|
||||||
|
| 단계 | 실제 결과에서 필요한 것 |
|
||||||
|
|---|---|
|
||||||
|
| 06 | engine, model, voice, files의 unit_id·media, narration media, 실제 unit_ranges |
|
||||||
|
| 10 | items의 reference_id·PNG media |
|
||||||
|
| 11 | frames의 frame_id·PNG media |
|
||||||
|
| 12 | clips의 cut_id·MP4 media |
|
||||||
|
| 13 | 최종 video media, captions_path의 실제 SRT |
|
||||||
|
|
||||||
|
확장자·예정 경로·요청 길이를 검증으로 쓰지 않는다. 전체 요청 결과·유효 선행 버전·필요한 승인 없이 다음 단계를 진행하지 않는다. [검토 사례](../verification-cases.json)는 이 계약에서 확인할 승인·시간·수정·부재 상황을 정리한다.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "02-research",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "01-topic",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"id": "s1",
|
||||||
|
"title": "Evaporation and the Water Cycle",
|
||||||
|
"url": "https://www.usgs.gov/water-science-school/science/evaporation-and-water-cycle",
|
||||||
|
"publisher": "U.S. Geological Survey",
|
||||||
|
"accessed_at": "2026-10-08"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s2",
|
||||||
|
"title": "Infiltration and the Water Cycle",
|
||||||
|
"url": "https://www.usgs.gov/water-science-school/science/infiltration-and-water-cycle",
|
||||||
|
"publisher": "U.S. Geological Survey",
|
||||||
|
"accessed_at": "2026-10-08"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"text": "증발은 액체 물이 기체 수증기로 바뀌어 지표에서 대기로 이동하는 과정이다.",
|
||||||
|
"source_ids": [
|
||||||
|
"s1"
|
||||||
|
],
|
||||||
|
"assessment": "verified"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c2",
|
||||||
|
"text": "강수로 내린 물은 침투와 스며듦을 통해 땅속으로 이동할 수 있다.",
|
||||||
|
"source_ids": [
|
||||||
|
"s2"
|
||||||
|
],
|
||||||
|
"assessment": "verified"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c3",
|
||||||
|
"text": "토양 특성과 지표 덮개 등은 물이 침투하는 정도에 영향을 준다.",
|
||||||
|
"source_ids": [
|
||||||
|
"s2"
|
||||||
|
],
|
||||||
|
"assessment": "verified"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"open_questions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 02-research 예제 검토
|
||||||
|
|
||||||
|
이 자료는 텍스트 내용 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: draft
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "01-topic",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "02-research",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "b1",
|
||||||
|
"text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?",
|
||||||
|
"claim_ids": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b2",
|
||||||
|
"text": "한 가지 길은 공기 쪽이에요. 액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다.",
|
||||||
|
"claim_ids": [
|
||||||
|
"c1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b3",
|
||||||
|
"text": "이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요.",
|
||||||
|
"claim_ids": [
|
||||||
|
"c1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b4",
|
||||||
|
"text": "땅으로 가는 길도 있어요. 물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다.",
|
||||||
|
"claim_ids": [
|
||||||
|
"c2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b5",
|
||||||
|
"text": "바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요.",
|
||||||
|
"claim_ids": [
|
||||||
|
"c3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b6",
|
||||||
|
"text": "그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요.",
|
||||||
|
"claim_ids": [
|
||||||
|
"c1",
|
||||||
|
"c2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"full_text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?\n한 가지 길은 공기 쪽이에요. 액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다.\n이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요.\n땅으로 가는 길도 있어요. 물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다.\n바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요.\n그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요.",
|
||||||
|
"estimated_duration_ms": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 03-script 예제 검토
|
||||||
|
|
||||||
|
이 자료는 텍스트 내용 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_review
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- 첫 질문의 답은 b2–b5가 설명하고 b6에서 회수한다. 제목은 04에서 같은 질문을 약속한다. 불필요한 비율·도시별 수치는 제외했다.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "04-title",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"id": "title-1",
|
||||||
|
"text": "물웅덩이의 물은 어디로 갈까?",
|
||||||
|
"rationale": "작아진 물의 두 이동 경로를 답하며 물이 없어졌다고 단정하지 않는다."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selected_id": "title-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 04-title 예제 검토
|
||||||
|
|
||||||
|
이 자료는 텍스트 내용 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_review
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "05-tts-split",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "u1",
|
||||||
|
"block_ids": [
|
||||||
|
"b1"
|
||||||
|
],
|
||||||
|
"display_text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?",
|
||||||
|
"spoken_text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u2",
|
||||||
|
"block_ids": [
|
||||||
|
"b2"
|
||||||
|
],
|
||||||
|
"display_text": "한 가지 길은 공기 쪽이에요. 액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다.",
|
||||||
|
"spoken_text": "한 가지 길은 공기 쪽이에요. 액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u3",
|
||||||
|
"block_ids": [
|
||||||
|
"b3"
|
||||||
|
],
|
||||||
|
"display_text": "이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요.",
|
||||||
|
"spoken_text": "이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u4",
|
||||||
|
"block_ids": [
|
||||||
|
"b4"
|
||||||
|
],
|
||||||
|
"display_text": "땅으로 가는 길도 있어요. 물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다.",
|
||||||
|
"spoken_text": "땅으로 가는 길도 있어요. 물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u5",
|
||||||
|
"block_ids": [
|
||||||
|
"b5"
|
||||||
|
],
|
||||||
|
"display_text": "바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요.",
|
||||||
|
"spoken_text": "바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u6",
|
||||||
|
"block_ids": [
|
||||||
|
"b6"
|
||||||
|
],
|
||||||
|
"display_text": "그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요.",
|
||||||
|
"spoken_text": "그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 05-tts-split 예제 검토
|
||||||
|
|
||||||
|
이 자료는 텍스트 내용 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: draft
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "06-tts",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "05-tts-split",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"request": {
|
||||||
|
"engine": "Qwen3-TTS",
|
||||||
|
"conda_env": "qwen3-tts",
|
||||||
|
"language": "한국어",
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "u1",
|
||||||
|
"block_ids": [
|
||||||
|
"b1"
|
||||||
|
],
|
||||||
|
"display_text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?",
|
||||||
|
"spoken_text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u2",
|
||||||
|
"block_ids": [
|
||||||
|
"b2"
|
||||||
|
],
|
||||||
|
"display_text": "한 가지 길은 공기 쪽이에요. 액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다.",
|
||||||
|
"spoken_text": "한 가지 길은 공기 쪽이에요. 액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u3",
|
||||||
|
"block_ids": [
|
||||||
|
"b3"
|
||||||
|
],
|
||||||
|
"display_text": "이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요.",
|
||||||
|
"spoken_text": "이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u4",
|
||||||
|
"block_ids": [
|
||||||
|
"b4"
|
||||||
|
],
|
||||||
|
"display_text": "땅으로 가는 길도 있어요. 물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다.",
|
||||||
|
"spoken_text": "땅으로 가는 길도 있어요. 물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u5",
|
||||||
|
"block_ids": [
|
||||||
|
"b5"
|
||||||
|
],
|
||||||
|
"display_text": "바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요.",
|
||||||
|
"spoken_text": "바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u6",
|
||||||
|
"block_ids": [
|
||||||
|
"b6"
|
||||||
|
],
|
||||||
|
"display_text": "그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요.",
|
||||||
|
"spoken_text": "그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요.",
|
||||||
|
"pronunciation_notes": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 06-tts 예제 검토
|
||||||
|
|
||||||
|
이 자료는 외부 요청과 결과 확인 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_input
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- result=null이다. 실제 파일·메타데이터가 없으며 complete로 사용할 수 없다.
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "07-cuts",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "05-tts-split",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "06-tts",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"narration_duration_ms": 32000,
|
||||||
|
"timing_evidence": "모의 데이터: 실제 음성 파일이나 정렬이 없다. 4000ms 컷과 컷을 걸치는 자막의 계약만 설명하며 제작 완료 근거로 사용할 수 없다.",
|
||||||
|
"cuts": [
|
||||||
|
{
|
||||||
|
"id": "cut-01",
|
||||||
|
"start_ms": 0,
|
||||||
|
"end_ms": 4000,
|
||||||
|
"unit_ids": [
|
||||||
|
"u1"
|
||||||
|
],
|
||||||
|
"narration_text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-02",
|
||||||
|
"start_ms": 4000,
|
||||||
|
"end_ms": 8000,
|
||||||
|
"unit_ids": [
|
||||||
|
"u2"
|
||||||
|
],
|
||||||
|
"narration_text": "한 가지 길은 공기 쪽이에요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-03",
|
||||||
|
"start_ms": 8000,
|
||||||
|
"end_ms": 12000,
|
||||||
|
"unit_ids": [
|
||||||
|
"u2"
|
||||||
|
],
|
||||||
|
"narration_text": "액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-04",
|
||||||
|
"start_ms": 12000,
|
||||||
|
"end_ms": 16000,
|
||||||
|
"unit_ids": [
|
||||||
|
"u3"
|
||||||
|
],
|
||||||
|
"narration_text": "이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-05",
|
||||||
|
"start_ms": 16000,
|
||||||
|
"end_ms": 20000,
|
||||||
|
"unit_ids": [
|
||||||
|
"u4"
|
||||||
|
],
|
||||||
|
"narration_text": "땅으로 가는 길도 있어요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-06",
|
||||||
|
"start_ms": 20000,
|
||||||
|
"end_ms": 24000,
|
||||||
|
"unit_ids": [
|
||||||
|
"u4"
|
||||||
|
],
|
||||||
|
"narration_text": "물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-07",
|
||||||
|
"start_ms": 24000,
|
||||||
|
"end_ms": 28000,
|
||||||
|
"unit_ids": [
|
||||||
|
"u5"
|
||||||
|
],
|
||||||
|
"narration_text": "바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-08",
|
||||||
|
"start_ms": 28000,
|
||||||
|
"end_ms": 32000,
|
||||||
|
"unit_ids": [
|
||||||
|
"u6"
|
||||||
|
],
|
||||||
|
"narration_text": "그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"captions": [
|
||||||
|
{
|
||||||
|
"id": "caption-01",
|
||||||
|
"start_ms": 0,
|
||||||
|
"end_ms": 4000,
|
||||||
|
"text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-02",
|
||||||
|
"start_ms": 4000,
|
||||||
|
"end_ms": 12000,
|
||||||
|
"text": "한 가지 길은 공기 쪽이에요. 액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-03",
|
||||||
|
"start_ms": 12000,
|
||||||
|
"end_ms": 16000,
|
||||||
|
"text": "이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-04",
|
||||||
|
"start_ms": 16000,
|
||||||
|
"end_ms": 24000,
|
||||||
|
"text": "땅으로 가는 길도 있어요. 물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-05",
|
||||||
|
"start_ms": 24000,
|
||||||
|
"end_ms": 28000,
|
||||||
|
"text": "바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-06",
|
||||||
|
"start_ms": 28000,
|
||||||
|
"end_ms": 32000,
|
||||||
|
"text": "그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 07-cuts 예제 검토
|
||||||
|
|
||||||
|
이 자료는 모의 시간·화면 설계를 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 현재 자료: 진행 기록에 등록하지 않은 모의 예제이며 실측 결과나 scenes 검토 제출본이 아니다.
|
||||||
|
- 실제 제작에 옮길 때의 상태: 실제 통합 WAV·실측 길이·발화와 자막의 정렬 근거가 없으므로 07은 awaiting_input으로 보고한다.
|
||||||
|
- scenes 검토 제출 조건: story 승인과 유효한 선행 결과를 확인하고 실제 통합 WAV에 근거한 실측 07 및 유효한 08·09를 갖춘 뒤 awaiting_review로 보고한다.
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- 32000ms 및 4000ms 구간은 설명용 가정이다. 실제 음성의 길이·정렬·속도를 뜻하지 않는다.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "08-scenes",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "07-cuts",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"cut_id": "cut-01",
|
||||||
|
"description": "일상 질문; 도해의 기호는 설명용이며 실제 크기나 보이는 수증기 묘사가 아니다.",
|
||||||
|
"start_state": "비 온 뒤 같은 자리의 물웅덩이가 크게 보인다.",
|
||||||
|
"end_state": "시간 경과를 도식화하여 같은 자리의 물웅덩이가 작게 보인다. 실제 4초 만의 변화라는 뜻이 아니다.",
|
||||||
|
"camera": "전경"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-02",
|
||||||
|
"description": "공기로 가는 길; 도해의 기호는 설명용이며 실제 크기나 보이는 수증기 묘사가 아니다.",
|
||||||
|
"start_state": "작은 물웅덩이의 수면을 가까이 본다.",
|
||||||
|
"end_state": "수면 위 이동 방향을 설명하는 도해 기호가 나타난다.",
|
||||||
|
"camera": "근접"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-03",
|
||||||
|
"description": "액체에서 기체로 이동; 도해의 기호는 설명용이며 실제 크기나 보이는 수증기 묘사가 아니다.",
|
||||||
|
"start_state": "직전 컷의 물웅덩이와 도해 기호가 같은 위치에 있다.",
|
||||||
|
"end_state": "도해 기호가 수면에서 위쪽으로 이동한다.",
|
||||||
|
"camera": "근접"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-04",
|
||||||
|
"description": "기능 설명 뒤 용어; 도해의 기호는 설명용이며 실제 크기나 보이는 수증기 묘사가 아니다.",
|
||||||
|
"start_state": "같은 수면과 도해 기호를 유지한다.",
|
||||||
|
"end_state": "공기 쪽 이동 관계를 정지 도해로 보여준다.",
|
||||||
|
"camera": "근접"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-05",
|
||||||
|
"description": "땅으로 가는 길; 도해의 기호는 설명용이며 실제 크기나 보이는 수증기 묘사가 아니다.",
|
||||||
|
"start_state": "물웅덩이와 흙층을 단면으로 본다.",
|
||||||
|
"end_state": "물과 흙의 경계에 설명용 방향 표시가 나타난다.",
|
||||||
|
"camera": "단면"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-06",
|
||||||
|
"description": "스며듦; 도해의 기호는 설명용이며 실제 크기나 보이는 수증기 묘사가 아니다.",
|
||||||
|
"start_state": "직전 단면의 물과 흙 경계가 같다.",
|
||||||
|
"end_state": "물의 이동을 나타내는 기호가 흙의 틈으로 내려간다.",
|
||||||
|
"camera": "단면"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-07",
|
||||||
|
"description": "조건에 따른 차이; 도해의 기호는 설명용이며 실제 크기나 보이는 수증기 묘사가 아니다.",
|
||||||
|
"start_state": "단면에서 물이 이동할 틈을 가리킨다.",
|
||||||
|
"end_state": "같은 단면에서 바닥 조건을 간단한 주석으로 강조한다.",
|
||||||
|
"camera": "단면"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-08",
|
||||||
|
"description": "처음 질문의 답; 도해의 기호는 설명용이며 실제 크기나 보이는 수증기 묘사가 아니다.",
|
||||||
|
"start_state": "도입과 같은 장소의 작은 물웅덩이를 보여준다.",
|
||||||
|
"end_state": "공기와 땅으로 가는 두 이동 방향을 함께 표시한다.",
|
||||||
|
"camera": "전경"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"from_cut_id": "cut-01",
|
||||||
|
"to_cut_id": "cut-02",
|
||||||
|
"mode": "cut",
|
||||||
|
"reason": "설명 역할이나 시점이 바뀌며 동일 장소를 기준점으로 삼는다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_cut_id": "cut-02",
|
||||||
|
"to_cut_id": "cut-03",
|
||||||
|
"mode": "continuous",
|
||||||
|
"reason": "같은 설명 대상·카메라와 상태를 이어간다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_cut_id": "cut-03",
|
||||||
|
"to_cut_id": "cut-04",
|
||||||
|
"mode": "continuous",
|
||||||
|
"reason": "같은 설명 대상·카메라와 상태를 이어간다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_cut_id": "cut-04",
|
||||||
|
"to_cut_id": "cut-05",
|
||||||
|
"mode": "cut",
|
||||||
|
"reason": "설명 역할이나 시점이 바뀌며 동일 장소를 기준점으로 삼는다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_cut_id": "cut-05",
|
||||||
|
"to_cut_id": "cut-06",
|
||||||
|
"mode": "continuous",
|
||||||
|
"reason": "같은 설명 대상·카메라와 상태를 이어간다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_cut_id": "cut-06",
|
||||||
|
"to_cut_id": "cut-07",
|
||||||
|
"mode": "continuous",
|
||||||
|
"reason": "같은 설명 대상·카메라와 상태를 이어간다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_cut_id": "cut-07",
|
||||||
|
"to_cut_id": "cut-08",
|
||||||
|
"mode": "cut",
|
||||||
|
"reason": "설명 역할이나 시점이 바뀌며 동일 장소를 기준점으로 삼는다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 08-scenes 예제 검토
|
||||||
|
|
||||||
|
이 자료는 모의 시간·화면 설계를 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 현재 자료: 진행 기록에 등록하지 않은 모의 예제이며 실측 결과나 scenes 검토 제출본이 아니다.
|
||||||
|
- 실제 제작에 옮길 때의 상태: 실제 통합 WAV·정렬 근거로 검증한 유효한 07이 없으므로 후속 모의 08도 awaiting_input으로 보고한다.
|
||||||
|
- scenes 검토 제출 조건: story 승인과 유효한 선행 결과를 확인하고 실제 통합 WAV에 근거한 실측 07 및 유효한 08·09를 갖춘 뒤 awaiting_review로 보고한다.
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- 32000ms 및 4000ms 구간은 설명용 가정이다. 실제 음성의 길이·정렬·속도를 뜻하지 않는다.
|
||||||
|
- cut-02→03은 같은 수면의 기호가 위로 움직이는 설명을 잇는다. cut-04→05는 숨은 땅속 관계를 보여주려고 단면으로 전환한다. 문자·수치는 생성 이미지에 맡기지 않는다.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "09-entities",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "08-scenes",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"characters": [],
|
||||||
|
"backgrounds": [
|
||||||
|
{
|
||||||
|
"id": "bg-1",
|
||||||
|
"description": "비 온 뒤 흙길의 물웅덩이와 같은 장소의 흙층 단면",
|
||||||
|
"fixed_features": [
|
||||||
|
"물웅덩이의 장소와 윤곽 기준",
|
||||||
|
"흙층과 표면의 색 구분",
|
||||||
|
"공기 위쪽·땅 아래쪽 방향"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"cut_id": "cut-01",
|
||||||
|
"character_ids": [],
|
||||||
|
"background_ids": [
|
||||||
|
"bg-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-02",
|
||||||
|
"character_ids": [],
|
||||||
|
"background_ids": [
|
||||||
|
"bg-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-03",
|
||||||
|
"character_ids": [],
|
||||||
|
"background_ids": [
|
||||||
|
"bg-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-04",
|
||||||
|
"character_ids": [],
|
||||||
|
"background_ids": [
|
||||||
|
"bg-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-05",
|
||||||
|
"character_ids": [],
|
||||||
|
"background_ids": [
|
||||||
|
"bg-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-06",
|
||||||
|
"character_ids": [],
|
||||||
|
"background_ids": [
|
||||||
|
"bg-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-07",
|
||||||
|
"character_ids": [],
|
||||||
|
"background_ids": [
|
||||||
|
"bg-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-08",
|
||||||
|
"character_ids": [],
|
||||||
|
"background_ids": [
|
||||||
|
"bg-1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 09-entities 예제 검토
|
||||||
|
|
||||||
|
이 자료는 모의 시간·화면 설계를 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 현재 자료: 진행 기록에 등록하지 않은 모의 예제이며 실측 결과나 scenes 검토 제출본이 아니다.
|
||||||
|
- 실제 제작에 옮길 때의 상태: 실제 통합 WAV·정렬 근거로 검증한 유효한 07과 이를 따른 유효한 08이 없으므로 후속 모의 09도 awaiting_input으로 보고한다.
|
||||||
|
- scenes 검토 제출 조건: story 승인과 유효한 선행 결과를 확인하고 실제 통합 WAV에 근거한 실측 07 및 유효한 08·09를 갖춘 뒤 awaiting_review로 보고한다.
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- 32000ms 및 4000ms 구간은 설명용 가정이다. 실제 음성의 길이·정렬·속도를 뜻하지 않는다.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "10-references",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "02-research",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "08-scenes",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "09-entities",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"request": {
|
||||||
|
"model": "gpt-image-2.5-sunburst",
|
||||||
|
"style_reference": "references/1.png",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "style-1",
|
||||||
|
"kind": "style",
|
||||||
|
"entity_id": null,
|
||||||
|
"prompt": "세로 9:16. 사용자 기준의 가는 윤곽선, 단순 형태와 정돈된 색면. 캠핑 소재나 상표는 가져오지 않는다. 물순환을 쉽게 설명하는 도해. 글자는 합성에서 검증해 배치한다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "background-1",
|
||||||
|
"kind": "background",
|
||||||
|
"entity_id": "bg-1",
|
||||||
|
"prompt": "같은 흙길 물웅덩이의 전경·근접·단면 기준. 조사된 증발과 침투 경로를 설명하며 실제 지형도나 측정 수치처럼 표시하지 않는다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 10-references 예제 검토
|
||||||
|
|
||||||
|
이 자료는 외부 요청과 결과 확인 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_input
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- result=null이다. 실제 파일·메타데이터가 없으며 complete로 사용할 수 없다.
|
||||||
|
- 32000ms 및 4000ms 구간은 설명용 가정이다. 실제 음성의 길이·정렬·속도를 뜻하지 않는다.
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "11-images",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "02-research",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "07-cuts",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "08-scenes",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "09-entities",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "10-references",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"request": {
|
||||||
|
"model": "gpt-image-2.5-sunburst",
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"id": "cut-01-start",
|
||||||
|
"cut_id": "cut-01",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "비 온 뒤 같은 자리의 물웅덩이가 크게 보인다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-01-end",
|
||||||
|
"cut_id": "cut-01",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "시간 경과를 도식화하여 같은 자리의 물웅덩이가 작게 보인다. 실제 4초 만의 변화라는 뜻이 아니다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-02-start",
|
||||||
|
"cut_id": "cut-02",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "작은 물웅덩이의 수면을 가까이 본다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-02-end",
|
||||||
|
"cut_id": "cut-02",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "수면 위 이동 방향을 설명하는 도해 기호가 나타난다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-03-start",
|
||||||
|
"cut_id": "cut-03",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "직전 컷의 물웅덩이와 도해 기호가 같은 위치에 있다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-03-end",
|
||||||
|
"cut_id": "cut-03",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "도해 기호가 수면에서 위쪽으로 이동한다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-04-start",
|
||||||
|
"cut_id": "cut-04",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "같은 수면과 도해 기호를 유지한다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-04-end",
|
||||||
|
"cut_id": "cut-04",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "공기 쪽 이동 관계를 정지 도해로 보여준다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-05-start",
|
||||||
|
"cut_id": "cut-05",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "물웅덩이와 흙층을 단면으로 본다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-05-end",
|
||||||
|
"cut_id": "cut-05",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "물과 흙의 경계에 설명용 방향 표시가 나타난다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-06-start",
|
||||||
|
"cut_id": "cut-06",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "직전 단면의 물과 흙 경계가 같다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-06-end",
|
||||||
|
"cut_id": "cut-06",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "물의 이동을 나타내는 기호가 흙의 틈으로 내려간다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-07-start",
|
||||||
|
"cut_id": "cut-07",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "단면에서 물이 이동할 틈을 가리킨다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-07-end",
|
||||||
|
"cut_id": "cut-07",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "같은 단면에서 바닥 조건을 간단한 주석으로 강조한다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-08-start",
|
||||||
|
"cut_id": "cut-08",
|
||||||
|
"position": "start",
|
||||||
|
"prompt": "도입과 같은 장소의 작은 물웅덩이를 보여준다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cut-08-end",
|
||||||
|
"cut_id": "cut-08",
|
||||||
|
"position": "end",
|
||||||
|
"prompt": "공기와 땅으로 가는 두 이동 방향을 함께 표시한다. 세로 9:16, 사용자 기준 그림체, 같은 배경 고정 특징. 조사 c1/c2/c3 중 해당 설명의 근거만 사용한다.",
|
||||||
|
"reference_ids": [
|
||||||
|
"style-1",
|
||||||
|
"background-1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 11-images 예제 검토
|
||||||
|
|
||||||
|
이 자료는 외부 요청과 결과 확인 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_input
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- result=null이다. 실제 파일·메타데이터가 없으며 complete로 사용할 수 없다.
|
||||||
|
- 32000ms 및 4000ms 구간은 설명용 가정이다. 실제 음성의 길이·정렬·속도를 뜻하지 않는다.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "12-motion",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "07-cuts",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "08-scenes",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "11-images",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"request": {
|
||||||
|
"clips": [
|
||||||
|
{
|
||||||
|
"cut_id": "cut-01",
|
||||||
|
"start_frame_id": "cut-01-start",
|
||||||
|
"end_frame_id": "cut-01-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "비 온 뒤 같은 자리의 물웅덩이가 크게 보인다.에서 시간 경과를 도식화하여 같은 자리의 물웅덩이가 작게 보인다. 실제 4초 만의 변화라는 뜻이 아니다.로 설명에 필요한 변화만 이어간다. 시간은 모의 데이터이며 실제 제작 전 음성·프레임 확인이 필요하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-02",
|
||||||
|
"start_frame_id": "cut-02-start",
|
||||||
|
"end_frame_id": "cut-02-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "작은 물웅덩이의 수면을 가까이 본다.에서 수면 위 이동 방향을 설명하는 도해 기호가 나타난다.로 설명에 필요한 변화만 이어간다. 시간은 모의 데이터이며 실제 제작 전 음성·프레임 확인이 필요하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-03",
|
||||||
|
"start_frame_id": "cut-03-start",
|
||||||
|
"end_frame_id": "cut-03-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "직전 컷의 물웅덩이와 도해 기호가 같은 위치에 있다.에서 도해 기호가 수면에서 위쪽으로 이동한다.로 설명에 필요한 변화만 이어간다. 시간은 모의 데이터이며 실제 제작 전 음성·프레임 확인이 필요하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-04",
|
||||||
|
"start_frame_id": "cut-04-start",
|
||||||
|
"end_frame_id": "cut-04-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "같은 수면과 도해 기호를 유지한다.에서 공기 쪽 이동 관계를 정지 도해로 보여준다.로 설명에 필요한 변화만 이어간다. 시간은 모의 데이터이며 실제 제작 전 음성·프레임 확인이 필요하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-05",
|
||||||
|
"start_frame_id": "cut-05-start",
|
||||||
|
"end_frame_id": "cut-05-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "물웅덩이와 흙층을 단면으로 본다.에서 물과 흙의 경계에 설명용 방향 표시가 나타난다.로 설명에 필요한 변화만 이어간다. 시간은 모의 데이터이며 실제 제작 전 음성·프레임 확인이 필요하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-06",
|
||||||
|
"start_frame_id": "cut-06-start",
|
||||||
|
"end_frame_id": "cut-06-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "직전 단면의 물과 흙 경계가 같다.에서 물의 이동을 나타내는 기호가 흙의 틈으로 내려간다.로 설명에 필요한 변화만 이어간다. 시간은 모의 데이터이며 실제 제작 전 음성·프레임 확인이 필요하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-07",
|
||||||
|
"start_frame_id": "cut-07-start",
|
||||||
|
"end_frame_id": "cut-07-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "단면에서 물이 이동할 틈을 가리킨다.에서 같은 단면에서 바닥 조건을 간단한 주석으로 강조한다.로 설명에 필요한 변화만 이어간다. 시간은 모의 데이터이며 실제 제작 전 음성·프레임 확인이 필요하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-08",
|
||||||
|
"start_frame_id": "cut-08-start",
|
||||||
|
"end_frame_id": "cut-08-end",
|
||||||
|
"duration_ms": 4000,
|
||||||
|
"motion_prompt": "도입과 같은 장소의 작은 물웅덩이를 보여준다.에서 공기와 땅으로 가는 두 이동 방향을 함께 표시한다.로 설명에 필요한 변화만 이어간다. 시간은 모의 데이터이며 실제 제작 전 음성·프레임 확인이 필요하다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 12-motion 예제 검토
|
||||||
|
|
||||||
|
이 자료는 외부 요청과 결과 확인 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_input
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- result=null이다. 실제 파일·메타데이터가 없으며 complete로 사용할 수 없다.
|
||||||
|
- 32000ms 및 4000ms 구간은 설명용 가정이다. 실제 음성의 길이·정렬·속도를 뜻하지 않는다.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "13-compose",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "04-title",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "06-tts",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "07-cuts",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "12-motion",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"request": {
|
||||||
|
"timeline": [
|
||||||
|
{
|
||||||
|
"cut_id": "cut-01",
|
||||||
|
"video_path": "projects/water-cycle/media/cut-01.mp4",
|
||||||
|
"start_ms": 0,
|
||||||
|
"end_ms": 4000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-02",
|
||||||
|
"video_path": "projects/water-cycle/media/cut-02.mp4",
|
||||||
|
"start_ms": 4000,
|
||||||
|
"end_ms": 8000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-03",
|
||||||
|
"video_path": "projects/water-cycle/media/cut-03.mp4",
|
||||||
|
"start_ms": 8000,
|
||||||
|
"end_ms": 12000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-04",
|
||||||
|
"video_path": "projects/water-cycle/media/cut-04.mp4",
|
||||||
|
"start_ms": 12000,
|
||||||
|
"end_ms": 16000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-05",
|
||||||
|
"video_path": "projects/water-cycle/media/cut-05.mp4",
|
||||||
|
"start_ms": 16000,
|
||||||
|
"end_ms": 20000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-06",
|
||||||
|
"video_path": "projects/water-cycle/media/cut-06.mp4",
|
||||||
|
"start_ms": 20000,
|
||||||
|
"end_ms": 24000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-07",
|
||||||
|
"video_path": "projects/water-cycle/media/cut-07.mp4",
|
||||||
|
"start_ms": 24000,
|
||||||
|
"end_ms": 28000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cut_id": "cut-08",
|
||||||
|
"video_path": "projects/water-cycle/media/cut-08.mp4",
|
||||||
|
"start_ms": 28000,
|
||||||
|
"end_ms": 32000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"narration_path": "projects/water-cycle/media/narration.wav",
|
||||||
|
"captions": [
|
||||||
|
{
|
||||||
|
"id": "caption-01",
|
||||||
|
"start_ms": 0,
|
||||||
|
"end_ms": 4000,
|
||||||
|
"text": "비가 그친 뒤, 물웅덩이가 작아지는 모습을 본 적 있나요? 물은 어디로 가는 걸까요?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-02",
|
||||||
|
"start_ms": 4000,
|
||||||
|
"end_ms": 12000,
|
||||||
|
"text": "한 가지 길은 공기 쪽이에요. 액체인 물이 기체인 수증기로 바뀌어 공기 중으로 이동합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-03",
|
||||||
|
"start_ms": 12000,
|
||||||
|
"end_ms": 16000,
|
||||||
|
"text": "이 변화를 증발이라고 불러요. 물이 다른 모습으로 이동하는 거예요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-04",
|
||||||
|
"start_ms": 16000,
|
||||||
|
"end_ms": 24000,
|
||||||
|
"text": "땅으로 가는 길도 있어요. 물은 땅속으로 스며들 수 있어요. 비로 내린 물의 일부도 이렇게 이동합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-05",
|
||||||
|
"start_ms": 24000,
|
||||||
|
"end_ms": 28000,
|
||||||
|
"text": "바닥의 성질 같은 조건에 따라 물이 스며드는 정도가 달라져요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caption-06",
|
||||||
|
"start_ms": 28000,
|
||||||
|
"end_ms": 32000,
|
||||||
|
"text": "그러니 물웅덩이를 보면, 물이 공기로 가거나 땅으로 스며드는 길도 생각해 보세요."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "물웅덩이의 물은 어디로 갈까?"
|
||||||
|
},
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 13-compose 예제 검토
|
||||||
|
|
||||||
|
이 자료는 외부 요청과 결과 확인 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_input
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- result=null이다. 실제 파일·메타데이터가 없으며 complete로 사용할 수 없다.
|
||||||
|
- 32000ms 및 4000ms 구간은 설명용 가정이다. 실제 음성의 길이·정렬·속도를 뜻하지 않는다.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "14-review",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"stage_id": "13-compose",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "02-research",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "04-title",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "07-cuts",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "08-scenes",
|
||||||
|
"revision": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "09-entities",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payload": {
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"area": "story",
|
||||||
|
"verdict": "not_checked",
|
||||||
|
"evidence": "실제 완성본·음성·자막이 없으므로 확인하지 않았다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "visual",
|
||||||
|
"verdict": "not_checked",
|
||||||
|
"evidence": "실제 완성본·음성·자막이 없으므로 확인하지 않았다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "audio",
|
||||||
|
"verdict": "not_checked",
|
||||||
|
"evidence": "실제 완성본·음성·자막이 없으므로 확인하지 않았다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "captions",
|
||||||
|
"verdict": "not_checked",
|
||||||
|
"evidence": "실제 완성본·음성·자막이 없으므로 확인하지 않았다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "timing",
|
||||||
|
"verdict": "not_checked",
|
||||||
|
"evidence": "실제 완성본·음성·자막이 없으므로 확인하지 않았다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "consistency",
|
||||||
|
"verdict": "not_checked",
|
||||||
|
"evidence": "실제 완성본·음성·자막이 없으므로 확인하지 않았다."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"severity": "blocking",
|
||||||
|
"description": "실제 완성본과 확인 가능한 외부 결과가 없다. 예제의 모의 시간을 실측으로 사용할 수 없다.",
|
||||||
|
"stage_id": "13-compose",
|
||||||
|
"cut_id": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recommendation": "incomplete"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 14-review 예제 검토
|
||||||
|
|
||||||
|
이 자료는 텍스트 내용 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_input
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
|
- 여섯 영역 모두 not_checked이고 recommendation=incomplete다. 검수자의 pass나 final 승인으로 바꾸지 않는다.
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"brief": {
|
||||||
|
"topic": "비가 온 뒤 물웅덩이는 어디로 사라질까",
|
||||||
|
"language": "한국어"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"aspect_ratio": "9:16",
|
||||||
|
"max_duration_ms": 180000,
|
||||||
|
"max_cut_duration_ms": 5000,
|
||||||
|
"style_reference": "references/1.png",
|
||||||
|
"tts_engine": "Qwen3-TTS",
|
||||||
|
"conda_env": "qwen3-tts",
|
||||||
|
"image_model": "gpt-image-2.5-sunburst"
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"stage_id": "01-topic",
|
||||||
|
"status": "awaiting_review",
|
||||||
|
"revision": 1,
|
||||||
|
"artifact_path": "examples/water-cycle/stages/01-topic/r1/artifact.json",
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "02-research",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "03-script",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "04-title",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "05-tts-split",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "06-tts",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "07-cuts",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "08-scenes",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "09-entities",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "10-references",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "11-images",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "12-motion",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "13-compose",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_id": "14-review",
|
||||||
|
"status": "pending",
|
||||||
|
"revision": null,
|
||||||
|
"artifact_path": null,
|
||||||
|
"issues": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reviews": [
|
||||||
|
{
|
||||||
|
"review_id": "topic",
|
||||||
|
"decision": "pending",
|
||||||
|
"artifact_refs": [
|
||||||
|
{
|
||||||
|
"stage_id": "01-topic",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"feedback": "승인 전 예제. 실제 사용자 응답 없음."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"review_id": "story",
|
||||||
|
"decision": "pending",
|
||||||
|
"artifact_refs": [],
|
||||||
|
"feedback": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"review_id": "scenes",
|
||||||
|
"decision": "pending",
|
||||||
|
"artifact_refs": [],
|
||||||
|
"feedback": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"review_id": "final",
|
||||||
|
"decision": "pending",
|
||||||
|
"artifact_refs": [],
|
||||||
|
"feedback": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"project_id": "water-cycle",
|
||||||
|
"stage_id": "01-topic",
|
||||||
|
"revision": 1,
|
||||||
|
"inputs": [],
|
||||||
|
"payload": {
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"id": "angle-1",
|
||||||
|
"angle": "물웅덩이의 물이 이동하는 두 경로",
|
||||||
|
"summary": "증발과 스며듦을 설명한다. 다른 이동 경로 전체를 다룬다고 약속하지 않는다."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selected_id": "angle-1",
|
||||||
|
"selection_reason": "일상에서 관찰할 수 있고 상태 변화와 단면으로 설명할 수 있는 예제 각도. 실제 사용자 승인 전이다."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 01-topic 예제 검토
|
||||||
|
|
||||||
|
이 자료는 텍스트 내용 양식을 설명하는 예제이며 실제 제작 실행·사용자 승인이 아니다. 진행 기록의 현재 결과는 01뿐이다.
|
||||||
|
|
||||||
|
- 예제 결과를 실제 배정받아 제출할 때 필요한 상태: awaiting_review
|
||||||
|
- 입력과 payload: 같은 폴더의 artifact.json 참조.
|
||||||
|
- 적용한 원칙: 중심 질문의 답으로 이어지는 설명, 입력 언어 보존, 새 필드 없는 기존 계약.
|
||||||
|
- 적용하지 않은 구성: 실제 실패한 대안이나 위기가 없는 소재이므로 반전·위기를 넣지 않았다.
|
||||||
|
- 다음 조건: 선행 결과와 실제 사용자 승인·필수 파일을 먼저 확인한다.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/__pycache__/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 물 소재 3D 제작 경계
|
||||||
|
|
||||||
|
- 이 폴더는 물 영상의 대표 장면과 승인된 9개 정식 장면을 재현하는 bpy 코드다. 범용 실행기는 `tools/blender/run.py`를 사용하고 수정하지 않는다.
|
||||||
|
- 대표 원본·렌더는 해당 새 08 버전, 정식 원본·렌더는 자기 `10–12/r2` 아래에 둔다. 이전 결과·WAV·사용자 스타일 원본·project.json은 변경하지 않는다.
|
||||||
|
- 전체 Interface 범위의 평가 mesh 정점·물 경계·화면 투영 포함과 실제 O1H2·V자·두 종류 결합·동적 이웃·닫힌 mesh 부피와 `--disable-autoexec` 평가를 검사한다. 물리 시뮬레이션으로 주장하지 않는다.
|
||||||
|
- draft 품질 조정은 별도 렌더 시도를 보존한다. 제출 후 결과 변경은 새 revision으로 진행하며 새 scenes 승인 전 정식 10–12를 실행하지 않는다. 승인 후에도 총괄의 실제 10/11 완료 기록에 맞춰 다음 렌더를 진행한다.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 물 영상의 3D 제작
|
||||||
|
|
||||||
|
## 제한된 시제품 기록
|
||||||
|
|
||||||
|
`build_preview.py`는 BondPair·Interface·DropletStretch 세 Scene만 만든다. 불규칙한 물 분자, 실제 연결 쌍 변화, 내부/표면 강조, 같은 부피의 3D 변형을 구현한 설명 모형이다. MD·유체 시뮬레이션은 아니다. 31컷 전체 모델·최종 영상은 scenes 승인 뒤 별도 제작한다.
|
||||||
|
|
||||||
|
승인·제출된 08 r3에서 아래 시제품 작성 명령을 다시 실행하지 않는다. `build_preview.py`의 top-level 실행은 원본을 덮어쓰므로 정식 제작에서 import하지 않는다. 저장소 루트에서 기존 설치를 사용한다. Blender 프레임은 논리 프레임+1이며 MP4는 `[start,end)`다. 대표 영상은 c07 4040ms/202프레임, c16 4880ms/244프레임, c18 4000ms/200프레임의 무음 영상이다. 끝 PNG는 exclusive 경계이며 MP4 마지막 프레임보다 20ms 뒤다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'C:/Program Files/Blender Foundation/Blender 5.2/blender.exe' --background --disable-autoexec --python-exit-code 1 --python projects/water-hydrogen-bond-surface-tension/blender/build_preview.py
|
||||||
|
& 'C:/Users/baram/anaconda3/python.exe' -B -X utf8 projects/water-hydrogen-bond-surface-tension/blender/test_model.py
|
||||||
|
& 'C:/Users/baram/anaconda3/python.exe' -B -X utf8 projects/water-hydrogen-bond-surface-tension/blender/prepare_artifacts.py
|
||||||
|
& 'C:/Users/baram/anaconda3/python.exe' -B -X utf8 projects/water-hydrogen-bond-surface-tension/blender/render_preview.py --kind image
|
||||||
|
& 'C:/Users/baram/anaconda3/python.exe' -B -X utf8 projects/water-hydrogen-bond-surface-tension/blender/render_preview.py --kind video
|
||||||
|
& 'C:/Users/baram/anaconda3/python.exe' -B -X utf8 projects/water-hydrogen-bond-surface-tension/blender/verify_design.py
|
||||||
|
```
|
||||||
|
|
||||||
|
작성 순서는 새 draft 재현용이다. `prepare_artifacts.py`는 대표 미디어가 빈 08/09 초안을 작성하므로 제출한 r3에서 재실행하지 않는다. `render_preview.py`는 현재 초안에 실제 증거만 추가한다. 완료된 결과는 새 버전에서 작업한다.
|
||||||
|
|
||||||
|
`test_model.py`는 없는 파일·Scene 누락·H 원자 누락·부피 driver 오류·원자 중심은 유지하고 mesh 정점만 물 밖에 둔 fixture·기존 r2 원본의 실제 RED와 수정 원본 GREEN을 확인한다. 포함 관계 실패 문구도 검사·기록한다. Interface 전체 논리 F2468–3172의 실제 원자·내부 실선·활성 점선 mesh 정점을 실제 물 mesh 경계와 camera 투영 윤곽에 대조한다. 최소 축 여백 0.03을 요구한다. 물 모형은 앞면·윗면이 실제 열린 절개이며 벽·고체 껍질이 아니다. 부피는 시작·중간 세 곳·전체 컷 끝을 실제 mesh로 평가한다. 실제 렌더는 기존 실행기의 source hash·Scene·profile·frame 검사, PNG/MP4 probe 및 전체 디코드를 거친다. 큰 원본·렌더·실패 시도는 해당 r3의 좁은 `.gitignore`로 보존하고 작은 검증 JSON에 복사할 파일·해시를 기록한다.
|
||||||
|
|
||||||
|
|
||||||
|
## 승인 후 정식 10–12 제작
|
||||||
|
|
||||||
|
`build_production.py --stage references`는 승인된 08 원본을 읽기만 하여 10의 실제 기준 모델을 만든다. 실제 7개 기준 PNG를 검증하고 총괄이 10 완료를 기록한 뒤 `--stage images`로 10 원본을 열어 11의 9개 Scene 애니메이션 원본을 만든다. 11은 62개 frame ID와 continuous 경계의 공유 PNG를 검증하고 총괄이 11 완료를 기록한 다음 12의 31개 전체 컷을 렌더한다. 정식 원본은 이미 존재하면 덮어쓰지 않는다.
|
||||||
|
|
||||||
|
`production_requests.py --stage <단계>` → `production_render.py --stage <단계>` → `production_check.py --stage <단계>`가 실제 요청·GPU 직렬 렌더·계약/해시 대조 경로다. `production_test.py --stage references|images`는 실제 누락/중심 내부·정점 외부 fixture의 RED와 원본 GREEN을 기록한다. `production_views.py --stage 11-images|12-motion`은 실제 끝점과 MP4 디코드의 시작·중간·마지막을 모바일 크기로 비교한다. 정지 유지 구간은 설명 맥락으로 판단한다.
|
||||||
|
|
||||||
|
`export_anchors.py`는 11 실제 원본의 1080×1920 camera 좌표·전체 31컷 target/bbox·필요한 매 프레임 라벨 키를 내보낸다. label_pixel은 배치 시작점이며 합성 단계에서 실제 글자·자막·충돌을 확인한다. 기존 2D 색 bbox나 945×1680 좌표를 재사용하지 않는다. 모든 최종 파일·SHA는 각 단계 manifest.json으로 전달한다.
|
||||||
|
|
||||||
|
`production_preserve.py`는 승인 실물 목록의 SHA와 고정 커밋의 작업 사본 체크아웃 바이트를 대조한다. 기존 WAV·스타일·01–09 및 이전 r1 결과를 수정하지 않았는지 확인한다.
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
"""물 소재의 세 대표 3D 장면만 작성한다. 물리 시뮬레이션이 아니다."""
|
||||||
|
from pathlib import Path
|
||||||
|
import hashlib
|
||||||
|
import math
|
||||||
|
import shutil
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
OUT = ROOT / "projects/water-hydrogen-bond-surface-tension/stages/08-scenes/r3/generated-source/preview.blend"
|
||||||
|
if OUT.exists():
|
||||||
|
history=OUT.parent/'history'
|
||||||
|
history.mkdir(exist_ok=True)
|
||||||
|
snapshot=history/(hashlib.sha256(OUT.read_bytes()).hexdigest()+'.blend')
|
||||||
|
if not snapshot.exists():
|
||||||
|
shutil.copy2(OUT,snapshot)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
|
||||||
|
|
||||||
|
def material(name, color, roughness=.48, metallic=0):
|
||||||
|
mat = bpy.data.materials.new(name)
|
||||||
|
mat.diffuse_color = (*color, 1)
|
||||||
|
mat.use_nodes = True
|
||||||
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||||||
|
bsdf.inputs["Base Color"].default_value = (*color, 1)
|
||||||
|
bsdf.inputs["Roughness"].default_value = roughness
|
||||||
|
bsdf.inputs["Metallic"].default_value = metallic
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
O = material("O_Coral", (.74, .16, .11))
|
||||||
|
H = material("H_Ivory", (.91, .84, .64))
|
||||||
|
SOLID = material("OH_Navy", (.025, .06, .13))
|
||||||
|
DASH = material("HO_Gold", (.83, .47, .045), .4)
|
||||||
|
WATER = material("Water_Blue", (.06, .42, .66), .25, .08)
|
||||||
|
CREAM = material("Studio_Cream", (.78, .73, .62), .8)
|
||||||
|
GHOST = material("Water_Concept_Cutaway", (.25, .62, .76), .7)
|
||||||
|
BACKDROP = bpy.data.materials.new('Cream_Emission_Backdrop')
|
||||||
|
BACKDROP.use_nodes = True
|
||||||
|
nodes = BACKDROP.node_tree.nodes
|
||||||
|
nodes.clear()
|
||||||
|
emission = nodes.new('ShaderNodeEmission')
|
||||||
|
emission.inputs['Color'].default_value = (.88,.82,.70,1)
|
||||||
|
output = nodes.new('ShaderNodeOutputMaterial')
|
||||||
|
BACKDROP.node_tree.links.new(emission.outputs[0],output.inputs['Surface'])
|
||||||
|
|
||||||
|
|
||||||
|
def point(obj, target):
|
||||||
|
obj.rotation_euler = (Vector(target) - obj.location).to_track_quat('-Z', 'Y').to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def scene(name, bounds, camera_position, scale):
|
||||||
|
s = bpy.context.scene if len(bpy.data.scenes) == 1 and not bpy.context.scene.objects else bpy.data.scenes.new(name)
|
||||||
|
s.name = name
|
||||||
|
bpy.context.window.scene = s
|
||||||
|
s.frame_start, s.frame_end = bounds
|
||||||
|
s.render.engine = "BLENDER_EEVEE"
|
||||||
|
s.render.fps = 50
|
||||||
|
s.render.resolution_x, s.render.resolution_y = 1080, 1920
|
||||||
|
s.render.resolution_percentage = 100
|
||||||
|
s.eevee.taa_render_samples = 64
|
||||||
|
s.view_settings.view_transform = 'Standard'
|
||||||
|
world = bpy.data.worlds.new(name + "World")
|
||||||
|
world.use_nodes = True
|
||||||
|
world.node_tree.nodes['Background'].inputs[0].default_value = (.78, .74, .65, 1)
|
||||||
|
world.node_tree.nodes['Background'].inputs[1].default_value = .3
|
||||||
|
s.world = world
|
||||||
|
cam = bpy.data.objects.new(name+"Camera", bpy.data.cameras.new(name+"Camera"))
|
||||||
|
s.collection.objects.link(cam)
|
||||||
|
cam.location = camera_position
|
||||||
|
point(cam, (0, 0, 0))
|
||||||
|
cam.data.type = 'ORTHO'
|
||||||
|
cam.data.ortho_scale = scale
|
||||||
|
s.camera = cam
|
||||||
|
bpy.ops.mesh.primitive_plane_add(size=30,location=-cam.location.normalized()*6)
|
||||||
|
backdrop=bpy.context.object
|
||||||
|
backdrop.name=name+'CreamBackdrop'
|
||||||
|
backdrop.rotation_euler=cam.rotation_euler
|
||||||
|
backdrop.data.materials.append(BACKDROP)
|
||||||
|
backdrop['role']='background'
|
||||||
|
for label, location, energy, size in (("Key", (-4,-6,7), 250, 5), ("Fill", (4,-2,3), 150, 4), ("Rim", (0,4,5), 200, 4)):
|
||||||
|
light = bpy.data.objects.new(name+label, bpy.data.lights.new(name+label, 'AREA'))
|
||||||
|
light.location = location
|
||||||
|
light.data.energy, light.data.shape, light.data.size = energy, 'DISK', size
|
||||||
|
point(light, (0,0,0))
|
||||||
|
s.collection.objects.link(light)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def sphere(name, position, radius, mat, parent=None, segments=32, rings=16):
|
||||||
|
bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, radius=radius, location=position)
|
||||||
|
obj = bpy.context.object
|
||||||
|
obj.name = name
|
||||||
|
obj.data.materials.append(mat)
|
||||||
|
for polygon in obj.data.polygons:
|
||||||
|
polygon.use_smooth = True
|
||||||
|
obj.parent = parent
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def cylinder(name, a, b, radius, mat, parent=None):
|
||||||
|
a, b = Vector(a), Vector(b)
|
||||||
|
bpy.ops.mesh.primitive_cylinder_add(vertices=16, radius=radius, depth=(b-a).length, location=(a+b)/2)
|
||||||
|
obj = bpy.context.object
|
||||||
|
obj.name = name
|
||||||
|
obj.rotation_euler = (b-a).to_track_quat('Z','Y').to_euler()
|
||||||
|
obj.data.materials.append(mat)
|
||||||
|
obj.parent = parent
|
||||||
|
for polygon in obj.data.polygons:
|
||||||
|
polygon.use_smooth = True
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def molecule(name, location, rotation=0, size=1):
|
||||||
|
root = bpy.data.objects.new(name, None)
|
||||||
|
bpy.context.scene.collection.objects.link(root)
|
||||||
|
root.location = location
|
||||||
|
root.rotation_euler.z = rotation
|
||||||
|
root.scale = (size,)*3
|
||||||
|
root['role'] = 'molecule'
|
||||||
|
atoms = []
|
||||||
|
for suffix, pos, radius, mat, element in (("O", (0,0,0), .36,O,'O'), ("H1", (.72,0,.52), .23,H,'H'), ("H2",(-.72,0,.52), .23,H,'H')):
|
||||||
|
atom = sphere(name+"_"+suffix,pos,radius,mat,root)
|
||||||
|
atom['role'], atom['element'] = 'atom', element
|
||||||
|
atoms.append(atom)
|
||||||
|
for i, h in enumerate(atoms[1:]):
|
||||||
|
bond = cylinder(name+"_OH"+str(i),(0,0,0),h.location,.065,SOLID,root)
|
||||||
|
bond['role'] = 'covalent'
|
||||||
|
return root, atoms
|
||||||
|
|
||||||
|
|
||||||
|
def dash(name, hydrogen, oxygen, frames=None):
|
||||||
|
# 점선의 끝점은 실제 서로 다른 분자의 H/O 월드 좌표다.
|
||||||
|
scene = bpy.context.scene
|
||||||
|
keys = frames or [scene.frame_start]
|
||||||
|
segments = []
|
||||||
|
for i in range(6):
|
||||||
|
obj = cylinder(name+str(i),(0,0,0),(0,0,.1),.045,DASH)
|
||||||
|
obj['role'], obj['hydrogen'], obj['oxygen'] = 'hydrogen_bond', hydrogen.name, oxygen.name
|
||||||
|
segments.append(obj)
|
||||||
|
for frame in keys:
|
||||||
|
scene.frame_set(frame)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
a, b = hydrogen.matrix_world.translation, oxygen.matrix_world.translation
|
||||||
|
direction = b-a
|
||||||
|
for i, obj in enumerate(segments):
|
||||||
|
t1, t2 = .18+i*.11, .18+i*.11+.062
|
||||||
|
obj.location = a+direction*((t1+t2)/2)
|
||||||
|
obj.rotation_euler = direction.to_track_quat('Z','Y').to_euler()
|
||||||
|
obj.scale.z = direction.length*(t2-t1)/.1
|
||||||
|
for path in ('location','rotation_euler','scale'):
|
||||||
|
obj.keyframe_insert(path,frame=frame)
|
||||||
|
return segments
|
||||||
|
|
||||||
|
|
||||||
|
pair = scene("BondPair", (905,1637), (0,-12,6), 11.5)
|
||||||
|
a, aa = molecule("PairA", (-1.8,0,-.3))
|
||||||
|
b, ba = molecule("PairB", (1.65,.15,.45), .3)
|
||||||
|
aa[1]['label'] = '한 분자의 Hδ+'
|
||||||
|
ba[0]['label'] = '이웃 분자의 Oδ−'
|
||||||
|
for frame, x in ((905,1.65),(1018,1.65),(1118,.85),(1220,.85),(1637,.85)):
|
||||||
|
b.location.x = x
|
||||||
|
b.keyframe_insert('location',frame=frame)
|
||||||
|
dash("Pair_HO_Dash_", aa[1], ba[0], [905,1018,1118,1220,1637])
|
||||||
|
|
||||||
|
interface = scene("Interface", (2469,3173), (0,-14,6), 13)
|
||||||
|
# 실제 물 영역 전체를 표시하되 앞면·윗면을 열어 내부 분자를 보이는 절개 설명 모형.
|
||||||
|
low, high = (-2.85,-.85,-3.15), (2.85,1.35,.80)
|
||||||
|
vertices=[(x,y,z) for z in (low[2],high[2]) for y in (low[1],high[1]) for x in (low[0],high[0])]
|
||||||
|
mesh=bpy.data.meshes.new('WaterCutawayMesh')
|
||||||
|
mesh.from_pydata(vertices,[],[(0,2,3,1),(2,6,7,3),(0,4,6,2),(1,3,7,5)])
|
||||||
|
mesh.update()
|
||||||
|
body=bpy.data.objects.new('WaterCutawayBody',mesh)
|
||||||
|
interface.collection.objects.link(body)
|
||||||
|
body.data.materials.append(GHOST)
|
||||||
|
body['role']='cutaway_water'
|
||||||
|
body['representation']='앞면·윗면이 열린 물 체적의 설명용 절개'
|
||||||
|
positions = [(-1.8,-.1,.25),(.4,.2,.18),(1.95,.45,.25),(-2,.3,-1.5),(-.65,.15,-1.15),(.9,.3,-1.6),(2,.35,-1.3),(-1.6,.3,-2.5),(.2,.4,-2.45),(1.6,.1,-2.6)]
|
||||||
|
mols=[]
|
||||||
|
for i, pos in enumerate(positions):
|
||||||
|
root, atoms = molecule('Liquid'+str(i),pos,(-.4+i*.37),.58)
|
||||||
|
mols.append((root,atoms))
|
||||||
|
for frame, delta in ((2469,0),(2807,0),(2907,.10*math.sin(i+1)),(3051,.05*math.cos(i)),(3173,0)):
|
||||||
|
root.location.x = pos[0]+delta
|
||||||
|
root.keyframe_insert('location',frame=frame)
|
||||||
|
for i,j in ((0,1),(1,2),(3,4),(3,7),(4,5),(4,7),(5,6),(5,8),(8,9)):
|
||||||
|
segments = dash('LiquidDash'+str(i)+'_'+str(j)+'_',mols[i][1][1],mols[j][1][0],[2469,2807,2907,3051,3173])
|
||||||
|
if (i,j) in ((3,4),(3,7)):
|
||||||
|
for segment in segments:
|
||||||
|
for frame, active in ((2469, j==4),(2807,j==4),(2907,j==7),(3051,j==7),(3173,j==7)):
|
||||||
|
segment.scale.x = segment.scale.y = 1 if active else .001
|
||||||
|
segment.keyframe_insert('scale',frame=frame)
|
||||||
|
mols[4][0]['label']='물 안쪽'
|
||||||
|
mols[1][0]['label']='물과 공기의 경계'
|
||||||
|
for name, center, frames in (("InteriorFocus",positions[4],((2469,.01),(2807,1),(2857,.01))), ("SurfaceFocus",positions[1],((2807,.01),(2907,1),(3173,1)))):
|
||||||
|
bpy.ops.mesh.primitive_torus_add(major_segments=48, minor_segments=10, location=(center[0],-.32,center[2]), rotation=(math.pi/2,0,0), major_radius=.74, minor_radius=.033)
|
||||||
|
ring=bpy.context.object
|
||||||
|
ring.name=name
|
||||||
|
ring.data.materials.append(DASH)
|
||||||
|
ring['role']='explanatory_highlight'
|
||||||
|
for frame, scale in frames:
|
||||||
|
ring.scale=(scale,)*3
|
||||||
|
ring.keyframe_insert('scale',frame=frame)
|
||||||
|
|
||||||
|
stretch = scene("DropletStretch", (3173,4077), (0,-12,5), 10.5)
|
||||||
|
drop=sphere('Droplet',(0,0,0),1.3,WATER,segments=128,rings=64)
|
||||||
|
drop['role']='same_volume_water'
|
||||||
|
drop['label']='같은 물의 양 · 설명용 모형'
|
||||||
|
# 단순 driver는 --disable-autoexec 상태에서도 평가된다. 매 프레임 det(scale)=1.
|
||||||
|
expression="1+0.85*min(max((frame-3173)/200,0),1)"
|
||||||
|
drop.driver_add('scale',0).driver.expression=expression
|
||||||
|
for axis in (1,2):
|
||||||
|
drop.driver_add('scale',axis).driver.expression="("+expression+")**(-0.5)"
|
||||||
|
OUT.parent.mkdir(parents=True,exist_ok=True)
|
||||||
|
for s in bpy.data.scenes:
|
||||||
|
s.frame_set(s.frame_start)
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(OUT))
|
||||||
|
print('PREVIEW_SOURCE',OUT)
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
"""승인된 물 소재 자산을 복사해 정식 기준 원본과 애니메이션 원본을 만든다."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
APPROVED=PROJECT/'stages/08-scenes/r3/generated-source/preview.blend'
|
||||||
|
APPROVED_HASH='ba0850a2148824cc8416704c03f14e5714ca01a16620a4eb26981e238af78c73'
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--stage',choices=['references','images'],required=True)
|
||||||
|
args=parser.parse_args(sys.argv[sys.argv.index('--')+1:])
|
||||||
|
|
||||||
|
|
||||||
|
def clone(obj,name,scene,parent=None):
|
||||||
|
result=obj.copy()
|
||||||
|
if obj.data:
|
||||||
|
result.data=obj.data.copy()
|
||||||
|
result.name=name
|
||||||
|
result.animation_data_clear()
|
||||||
|
result.parent=parent
|
||||||
|
scene.collection.objects.link(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def studio(name,base,bounds,scale):
|
||||||
|
scene=bpy.data.scenes.new(name)
|
||||||
|
scene.world=base.world
|
||||||
|
scene.render.engine='BLENDER_EEVEE'
|
||||||
|
scene.render.fps=50
|
||||||
|
scene.render.resolution_x,scene.render.resolution_y=1080,1920
|
||||||
|
scene.eevee.taa_render_samples=64
|
||||||
|
scene.view_settings.view_transform='Standard'
|
||||||
|
for obj in base.objects:
|
||||||
|
if obj.type in ('CAMERA','LIGHT') or obj.get('role')=='background':
|
||||||
|
item=clone(obj,name+obj.type+obj.name,scene)
|
||||||
|
if item.type=='CAMERA':
|
||||||
|
scene.camera=item
|
||||||
|
item.data.ortho_scale=scale
|
||||||
|
scene.frame_start,scene.frame_end=bounds
|
||||||
|
return scene
|
||||||
|
|
||||||
|
|
||||||
|
def molecule(scene,name,location,size=1,rotation=0):
|
||||||
|
template=bpy.data.objects['PairA']
|
||||||
|
root=clone(template,name,scene)
|
||||||
|
root.location=location
|
||||||
|
root.rotation_euler=(0,0,rotation)
|
||||||
|
root.scale=(size,)*3
|
||||||
|
for child in template.children:
|
||||||
|
clone(child,name+child.name[len('PairA'):],scene,root)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def droplet(scene,name,location,scale):
|
||||||
|
obj=clone(bpy.data.objects['Droplet'],name,scene)
|
||||||
|
obj.location=location
|
||||||
|
obj.scale=scale
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def anchor(scene,name,location,label):
|
||||||
|
obj=bpy.data.objects.new(name,None)
|
||||||
|
scene.collection.objects.link(obj)
|
||||||
|
obj.location=location
|
||||||
|
obj['anchor']=label
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def mark(obj,label):
|
||||||
|
obj['anchor']=label
|
||||||
|
|
||||||
|
|
||||||
|
def collection(name,objects):
|
||||||
|
group=bpy.data.collections.new(name)
|
||||||
|
group.use_fake_user=True
|
||||||
|
for obj in objects:
|
||||||
|
group.objects.link(obj)
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
def animate_network(scene,prefix,start,turn,end):
|
||||||
|
# 온전한 분자를 움직이고 H···O 원통을 실제 원자 위치로 갱신한다.
|
||||||
|
roots=[scene.objects[prefix+str(i)] for i in range(10)]
|
||||||
|
for i,root in enumerate(roots):
|
||||||
|
position=root.location.copy()
|
||||||
|
for frame,delta in [(start,0),(turn,.09*math.sin(i+1)),(end,.06*math.cos(i))]:
|
||||||
|
root.location=position+Vector((delta,0,0))
|
||||||
|
root.keyframe_insert('location',frame=frame)
|
||||||
|
template=bpy.data.objects['LiquidDash0_1_0']
|
||||||
|
pairs=[(0,1),(1,2),(3,4),(3,7),(4,5),(4,6),(4,7),(5,6),(5,8),(8,9)]
|
||||||
|
for i,j in pairs:
|
||||||
|
hydrogen,oxygen=scene.objects[prefix+str(i)+'_H1'],scene.objects[prefix+str(j)+'_O']
|
||||||
|
for segment in range(6):
|
||||||
|
obj=clone(template,prefix+'Dash'+str(i)+'_'+str(j)+'_'+str(segment),scene)
|
||||||
|
obj['hydrogen'],obj['oxygen']=hydrogen.name,oxygen.name
|
||||||
|
for frame in [start,1779,turn,2257,end]:
|
||||||
|
scene.frame_set(frame)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
a,b=hydrogen.matrix_world.translation,oxygen.matrix_world.translation
|
||||||
|
direction=b-a
|
||||||
|
obj.location=a+direction*(.18+segment*.11+.031)
|
||||||
|
obj.rotation_euler=direction.to_track_quat('Z','Y').to_euler()
|
||||||
|
active=not ((i,j)==(3,4) and frame>=turn) and not ((i,j)==(3,7) and frame<turn) and not ((i,j)==(4,5) and frame>=2257) and not ((i,j)==(4,6) and frame<2257)
|
||||||
|
obj.scale=(1 if active else .001,1 if active else .001,direction.length*.062/.1)
|
||||||
|
for path in ['location','rotation_euler','scale']:
|
||||||
|
obj.keyframe_insert(path,frame=frame)
|
||||||
|
|
||||||
|
|
||||||
|
if args.stage=='references':
|
||||||
|
assert hashlib.sha256(APPROVED.read_bytes()).hexdigest()==APPROVED_HASH, '승인 원본 해시 불일치'
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=str(APPROVED))
|
||||||
|
pair=bpy.data.scenes['BondPair']
|
||||||
|
drop=bpy.data.scenes['DropletStretch']
|
||||||
|
interface=bpy.data.scenes['Interface']
|
||||||
|
opening=studio('OpeningDroplet',drop,(1,397),10.5)
|
||||||
|
obj=droplet(opening,'OpeningWater',(0,0,0),(1,1,1))
|
||||||
|
mark(obj,'opening_drop')
|
||||||
|
anchor(opening,'OpeningMoleculeInset',(1.65,0,1.7),'molecule_inset')
|
||||||
|
single=studio('SingleMolecule',pair,(397,905),9)
|
||||||
|
molecule(single,'SingleWater',(0,0,-.4),1.8)
|
||||||
|
for suffix,label in [('O','single_O_delta_minus'),('H1','single_H_delta_plus'),('H2','single_H2_delta_plus')]:
|
||||||
|
mark(single.objects['SingleWater_'+suffix],label)
|
||||||
|
network=studio('LiquidNetwork',interface,(1637,2469),13)
|
||||||
|
for i in range(10):
|
||||||
|
old=interface.objects['Liquid'+str(i)]
|
||||||
|
molecule(network,'Network'+str(i),(old.location.x,old.location.y,old.location.z+1.3),.58,old.rotation_euler.z)
|
||||||
|
mark(network.objects['Network4'],'network_interior')
|
||||||
|
comparison=studio('EqualVolumeComparison',drop,(4077,4370),12.5)
|
||||||
|
size=.72
|
||||||
|
droplet(comparison,'ComparisonLong',(-1.65,0,0),(size*1.65,size/math.sqrt(1.65),size/math.sqrt(1.65)))
|
||||||
|
droplet(comparison,'ComparisonRound',(1.65,0,0),(size,)*3)
|
||||||
|
mark(comparison.objects['ComparisonLong'],'equal_volume_left')
|
||||||
|
mark(comparison.objects['ComparisonRound'],'equal_volume_right')
|
||||||
|
conditions=studio('ShapeConditions',drop,(4370,4889),12.5)
|
||||||
|
droplet(conditions,'SmallFreeDrop',(-1.65,0,.2),(.58,)*3)
|
||||||
|
# 큰 접촉 방울은 같은 부피 비교가 아닌 별도 조건 모형이다.
|
||||||
|
contact=droplet(conditions,'LargeContactDrop',(1.55,0,-.275),(1.02,1.02,.75/1.3))
|
||||||
|
mark(conditions.objects['SmallFreeDrop'],'small_free_condition')
|
||||||
|
mark(contact,'large_contact_condition')
|
||||||
|
bpy.context.window.scene=conditions
|
||||||
|
bpy.ops.mesh.primitive_cube_add(size=1,location=(1.55,0,-1.15))
|
||||||
|
floor=bpy.context.object
|
||||||
|
floor.name='ContactFloor'
|
||||||
|
floor.scale=(3.1,2.5,.25)
|
||||||
|
floor_material=bpy.data.materials.new('ContactFloorCream')
|
||||||
|
floor_material.diffuse_color=(.78,.73,.62,1)
|
||||||
|
floor_material.use_nodes=True
|
||||||
|
floor_material.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value=(.78,.73,.62,1)
|
||||||
|
floor_material.node_tree.nodes['Principled BSDF'].inputs['Roughness'].default_value=.8
|
||||||
|
floor.data.materials.append(floor_material)
|
||||||
|
floor['role']='condition_floor'
|
||||||
|
mark(floor,'contact_floor')
|
||||||
|
answer=studio('AnswerDroplet',drop,(4889,5529),12.5)
|
||||||
|
droplet(answer,'AnswerWater',(0,0,-.7),(1,)*3)
|
||||||
|
mark(answer.objects['AnswerWater'],'answer_macro_drop')
|
||||||
|
for i,position in enumerate([(-1.1,0,1.8),(0,.1,2.05),(1.1,0,1.8)]):
|
||||||
|
molecule(answer,'AnswerMolecule'+str(i),position,.55,i*.2)
|
||||||
|
mark(answer.objects['AnswerMolecule0_H1'],'answer_inset_H')
|
||||||
|
mark(answer.objects['AnswerMolecule1_O'],'answer_inset_O')
|
||||||
|
mark(answer.objects['AnswerMolecule2'],'answer_inset_extent')
|
||||||
|
mark(pair.objects['PairA_H1'],'pair_H_delta_plus')
|
||||||
|
mark(pair.objects['PairB_O'],'pair_O_delta_minus')
|
||||||
|
mark(interface.objects['Liquid4'],'water_interior')
|
||||||
|
mark(interface.objects['Liquid1'],'water_surface')
|
||||||
|
anchor(interface,'InterfaceAirAnchor',(0,0,1.45),'air_region')
|
||||||
|
anchor(interface,'InterfaceWaterAnchor',(0,0,-2.95),'cutaway_water_region')
|
||||||
|
mark(drop.objects['Droplet'],'same_volume_drop')
|
||||||
|
collection('StudioAssets',[o for s in bpy.data.scenes for o in s.objects if o.get('role')=='background'])
|
||||||
|
collection('StyleAssets',[o for s in bpy.data.scenes for o in s.objects if o.get('role')=='background'])
|
||||||
|
collection('DropletStudioAssets',[o for s in bpy.data.scenes if s.name in ['OpeningDroplet','DropletStretch','AnswerDroplet'] for o in s.objects if o.get('role')=='background'])
|
||||||
|
collection('MoleculeStudioAssets',[o for s in bpy.data.scenes if s.name in ['SingleMolecule','BondPair','LiquidNetwork'] for o in s.objects if o.get('role')=='background'])
|
||||||
|
collection('ComparisonStudioAssets',[o for s in bpy.data.scenes if s.name in ['EqualVolumeComparison','ShapeConditions'] for o in s.objects if o.get('role')=='background'])
|
||||||
|
collection('MoleculeAssets',[o for o in bpy.data.objects if o.get('role') in ('molecule','atom','covalent','hydrogen_bond')])
|
||||||
|
collection('DropletAssets',[o for o in bpy.data.objects if o.get('role')=='same_volume_water'])
|
||||||
|
collection('InterfaceAssets',[interface.objects['WaterCutawayBody']])
|
||||||
|
# 실제 기준 원본에도 액체 연결 자산을 구성하여 후속 원본이 새 경로만 참조하지 않게 한다.
|
||||||
|
bpy.context.window.scene=network
|
||||||
|
animate_network(network,'Network',1637,2019,2469)
|
||||||
|
bpy.context.window.scene=answer
|
||||||
|
# 답 회수에는 거시 방울과 공간적으로 분리된 분자 개념도가 공존한다.
|
||||||
|
for i,j in [(0,1),(1,2)]:
|
||||||
|
template=bpy.data.objects['LiquidDash0_1_0']
|
||||||
|
h,o=answer.objects['AnswerMolecule'+str(i)+'_H1'],answer.objects['AnswerMolecule'+str(j)+'_O']
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
a,b=h.matrix_world.translation,o.matrix_world.translation
|
||||||
|
for n in range(6):
|
||||||
|
obj=clone(template,'AnswerDash'+str(i)+'_'+str(j)+'_'+str(n),answer)
|
||||||
|
obj['hydrogen'],obj['oxygen']=h.name,o.name
|
||||||
|
direction=b-a
|
||||||
|
obj.location=a+direction*(.18+n*.11+.031)
|
||||||
|
obj.rotation_euler=direction.to_track_quat('Z','Y').to_euler()
|
||||||
|
obj.scale=(1,1,direction.length*.062/.1)
|
||||||
|
for scene in bpy.data.scenes:
|
||||||
|
scene['approved_source_sha256']=APPROVED_HASH
|
||||||
|
destination=PROJECT/'stages/10-references/r2/generated-source/references.blend'
|
||||||
|
else:
|
||||||
|
artifact=json.loads((PROJECT/'stages/10-references/r2/artifact.json').read_text(encoding='utf-8'))
|
||||||
|
result=artifact['payload']['result']
|
||||||
|
source=ROOT/result['scene_file']['path']
|
||||||
|
assert result['scene_file']['verified'] and hashlib.sha256(source.read_bytes()).hexdigest()==result['scene_file']['sha256']
|
||||||
|
assert len(result['items'])==7 and all((ROOT/item['media']['path']).is_file() and item['media']['verified'] for item in result['items'])
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=str(source))
|
||||||
|
for scene in bpy.data.scenes:
|
||||||
|
scene['reference_source_sha256']=result['scene_file']['sha256']
|
||||||
|
drop=bpy.data.objects['Droplet']
|
||||||
|
expression='1+0.85*min(max((frame-3173)/200,0),1)*(1-min(max((frame-3657)/196,0),1))'
|
||||||
|
drop.animation_data.drivers[0].driver.expression=expression
|
||||||
|
for axis in [1,2]:
|
||||||
|
drop.animation_data.drivers[axis].driver.expression='('+expression+')**(-0.5)'
|
||||||
|
# 액체 점선의 위치·방향을 프레임마다 실제 H/O 평가 위치에 맞춰 굽는다.
|
||||||
|
network=bpy.data.scenes['LiquidNetwork']
|
||||||
|
bpy.context.window.scene=network
|
||||||
|
segments=[obj for obj in network.objects if obj.get('role')=='hydrogen_bond']
|
||||||
|
for obj in segments:
|
||||||
|
obj.animation_data_clear()
|
||||||
|
for frame in range(1637,2470):
|
||||||
|
network.frame_set(frame)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
for obj in segments:
|
||||||
|
h,o=network.objects[obj['hydrogen']],network.objects[obj['oxygen']]
|
||||||
|
a,b=h.matrix_world.translation,o.matrix_world.translation
|
||||||
|
direction=b-a
|
||||||
|
segment=int(obj.name.rsplit('_',1)[1])
|
||||||
|
obj.location=a+direction*(.18+segment*.11+.031)
|
||||||
|
obj.rotation_euler=direction.to_track_quat('Z','Y').to_euler()
|
||||||
|
pair=(h.parent.name,o.parent.name)
|
||||||
|
first=min(max((frame-1779)/240,0),1)
|
||||||
|
second=min(max((frame-2019)/238,0),1)
|
||||||
|
weight={('Network3','Network4'):1-first,('Network3','Network7'):first,('Network4','Network5'):1-second,('Network4','Network6'):second}.get(pair,1)
|
||||||
|
obj.scale=(max(weight,.001),max(weight,.001),direction.length*.062/.1)
|
||||||
|
for path in ['location','rotation_euler','scale']:
|
||||||
|
obj.keyframe_insert(path,frame=frame)
|
||||||
|
# 답의 수소결합은 영구 끈이 아니며 연결이 약해졌다가 다시 나타난다.
|
||||||
|
answer=bpy.data.scenes['AnswerDroplet']
|
||||||
|
for obj in answer.objects:
|
||||||
|
if obj.get('role')=='hydrogen_bond':
|
||||||
|
original=obj.scale.z
|
||||||
|
for frame,active in [(4889,1),(5100,1),(5160,.001),(5229,1),(5529,1)]:
|
||||||
|
obj.scale=(active,active,original)
|
||||||
|
obj.keyframe_insert('scale',frame=frame)
|
||||||
|
destination=PROJECT/'stages/11-images/r2/generated-source/animation.blend'
|
||||||
|
for scene in bpy.data.scenes:
|
||||||
|
bpy.context.window.scene=scene
|
||||||
|
scene.frame_set(scene.frame_start)
|
||||||
|
destination.parent.mkdir(parents=True,exist_ok=True)
|
||||||
|
assert not destination.exists(), '제출 원본 덮어쓰기 금지'
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(destination))
|
||||||
|
print('정식 원본 저장',destination)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""1080×1920 실제 카메라 투영으로 T5 주석 위치와 필요한 매 프레임 키를 내보낸다."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
from mathutils import Vector
|
||||||
|
from bpy_extras.object_utils import world_to_camera_view
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--source',required=True)
|
||||||
|
parser.add_argument('--out',required=True)
|
||||||
|
args=parser.parse_args(sys.argv[sys.argv.index('--')+1:])
|
||||||
|
source=Path(args.source).resolve()
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=str(source))
|
||||||
|
cuts=json.loads((PROJECT/'stages/07-cuts/r1/artifact.json').read_text(encoding='utf-8'))['payload']['cuts']
|
||||||
|
plans=json.loads((PROJECT/'stages/08-scenes/r3/artifact.json').read_text(encoding='utf-8'))['payload']['scenes']
|
||||||
|
cache={}
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_objects(obj):
|
||||||
|
return [obj] if obj.type=='MESH' else [child for child in obj.children if child.type=='MESH']
|
||||||
|
|
||||||
|
|
||||||
|
def coordinates(obj,deps):
|
||||||
|
if obj.name not in cache:
|
||||||
|
assert not obj.modifiers and not obj.data.shape_keys, '정점 캐시가 불가능한 변형 mesh'
|
||||||
|
evaluated=obj.evaluated_get(deps)
|
||||||
|
mesh=evaluated.to_mesh()
|
||||||
|
cache[obj.name]=np.array([tuple(v.co) for v in mesh.vertices])
|
||||||
|
evaluated.to_mesh_clear()
|
||||||
|
matrix=np.array(obj.evaluated_get(deps).matrix_world)
|
||||||
|
return cache[obj.name] @ matrix[:3,:3].T + matrix[:3,3]
|
||||||
|
|
||||||
|
|
||||||
|
result=[]
|
||||||
|
for cut,plan in zip(cuts,plans):
|
||||||
|
scene=bpy.data.scenes[plan['start_state'].split(';')[0].split('=')[1]]
|
||||||
|
bpy.context.window.scene=scene
|
||||||
|
assert scene.camera.data.type=='ORTHO'
|
||||||
|
origin=world_to_camera_view(scene,scene.camera,Vector((0,0,0)))
|
||||||
|
projection=np.array([tuple(world_to_camera_view(scene,scene.camera,Vector(b))-origin)[:2] for b in [(1,0,0),(0,1,0),(0,0,1)]])
|
||||||
|
offset=np.array(tuple(origin)[:2])
|
||||||
|
objects=[obj for obj in scene.objects if obj.get('anchor')]
|
||||||
|
start,end=cut['start_ms']//20,cut['end_ms']//20
|
||||||
|
tracks={obj['anchor']:[] for obj in objects}
|
||||||
|
states=[]
|
||||||
|
for frame in range(start,end+1):
|
||||||
|
scene.frame_set(frame+1)
|
||||||
|
deps=bpy.context.evaluated_depsgraph_get()
|
||||||
|
labels={}
|
||||||
|
for obj in objects:
|
||||||
|
center=np.array(tuple(obj.matrix_world.translation)) @ projection + offset
|
||||||
|
target=[round(float(center[0]*1080),2),round(float((1-center[1])*1920),2)]
|
||||||
|
parts=bbox_objects(obj)
|
||||||
|
box=None
|
||||||
|
if parts:
|
||||||
|
vertices=np.vstack([coordinates(part,deps) for part in parts])
|
||||||
|
projected=vertices @ projection + offset
|
||||||
|
px=projected[:,0]*1080
|
||||||
|
py=(1-projected[:,1])*1920
|
||||||
|
box=[round(float(px.min()),2),round(float(py.min()),2),round(float(px.max()),2),round(float(py.max()),2)]
|
||||||
|
label=[round((box[0]+box[2])/2,2),round(box[1]-32,2)]
|
||||||
|
else:
|
||||||
|
label=target
|
||||||
|
assert 0<label[0]<1080 and 0<label[1]<1920, '주석 제안점 화면 밖'
|
||||||
|
labels[obj['anchor']]=dict(object_name=obj.name,target_pixel=target,label_pixel=label,bbox_px=box)
|
||||||
|
tracks[obj['anchor']].append([frame,*label,*target])
|
||||||
|
if frame in [start,(start+end)//2,end]:
|
||||||
|
states.append(dict(logical_frame=frame,blender_frame=frame+1,anchors=labels))
|
||||||
|
motion=[]
|
||||||
|
for label,values in tracks.items():
|
||||||
|
changing=any(v[1:]!=values[0][1:] for v in values[1:])
|
||||||
|
keys=[]
|
||||||
|
for index,value in enumerate(values):
|
||||||
|
if index in [0,len(values)-1] or value[1:]!=values[index-1][1:]:
|
||||||
|
keys.append(value)
|
||||||
|
motion.append(dict(anchor_id=label,animated=changing,columns=['logical_frame','label_x','label_y','target_x','target_y'],keys=keys if changing else [values[0],values[-1]]))
|
||||||
|
result.append(dict(cut_id=cut['id'],scene_name=scene.name,camera_name=scene.camera.name,start_frame=start,end_frame=end,states=states,motion=motion))
|
||||||
|
value=dict(source_path=source.relative_to(ROOT).as_posix(),source_sha256=hashlib.sha256(source.read_bytes()).hexdigest(),width=1080,height=1920,coordinate_system='top_left_pixel',logical_to_blender='logical_frame + 1',label_note='label_pixel은 실제 mesh 위 32px의 배치 시작점이다. 합성 단계에서 글자 크기·충돌·자막 여백을 실제 합성 화면으로 검수한다. bbox_px와 target_pixel은 실제 카메라 투영이다.',cuts=result)
|
||||||
|
Path(args.out).write_text(json.dumps(value,ensure_ascii=False,separators=(',',':'))+'\n',encoding='utf-8')
|
||||||
|
print('실제 카메라 주석 앵커 GREEN',len(result))
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""실제 영상의 시작·중간·끝 렌더를 모바일 크기로 나란히 보는 검사 보조물."""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageChops, ImageStat
|
||||||
|
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
BASE=ROOT/'projects/water-hydrogen-bond-surface-tension/stages/08-scenes/r3'
|
||||||
|
artifact=json.loads((BASE/'artifact.json').read_text(encoding='utf-8'))
|
||||||
|
results=[]
|
||||||
|
for sample in artifact['payload']['preview']['samples']:
|
||||||
|
if sample['media']['kind']!='video':
|
||||||
|
continue
|
||||||
|
folder=(ROOT/sample['media']['path']).parent
|
||||||
|
evidence=json.loads((folder/'evidence.json').read_text(encoding='utf-8'))
|
||||||
|
frames=evidence['logical_frames']
|
||||||
|
selection=[frames[0],frames[len(frames)//2],frames[-1]]
|
||||||
|
originals=[Image.open(folder/f'frame-{frame:08}.png').convert('RGB') for frame in selection]
|
||||||
|
differences=[ImageChops.difference(originals[0],image) for image in originals[1:]]
|
||||||
|
assert all(diff.getbbox() for diff in differences), '렌더 픽셀이 동일한 정지 클립'
|
||||||
|
sheet=Image.new('RGB',(1080,640))
|
||||||
|
for index,image in enumerate(originals):
|
||||||
|
sheet.paste(image.resize((360,640),Image.Resampling.LANCZOS),(index*360,0))
|
||||||
|
path=BASE/'generated-source'/f'mobile-{sample["cut_ids"][0]}.png'
|
||||||
|
sheet.save(path)
|
||||||
|
results.append(dict(cut_id=sample['cut_ids'][0],logical_frames=selection,pixel_sha256=[hashlib.sha256(image.tobytes()).hexdigest() for image in originals],mean_absolute_pixel_change=[sum(ImageStat.Stat(diff).mean)/3 for diff in differences],mobile_view_path=path.relative_to(ROOT).as_posix(),mobile_view_sha256=hashlib.sha256(path.read_bytes()).hexdigest(),visual_review='not_checked'))
|
||||||
|
assert len(results)==3, '세 대표 영상이 아직 없음'
|
||||||
|
(BASE/'motion-check.json').write_text(json.dumps(dict(result='GREEN',source_sha256=artifact['payload']['preview']['scene_file']['sha256'],views=results),ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
print('MOTION_GREEN: 세 영상의 실제 시작/중간/끝 RGB 픽셀 변화')
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"""실측 07을 보존하며 새 08/09의 검토 초안만 작성한다."""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT = ROOT / 'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
DESCRIPTIONS = [
|
||||||
|
'작은 청색 물방울의 둥근 형태를 보여주며 그 모양이 생기는 이유를 질문한다.',
|
||||||
|
'거시적인 물방울에서 물속 분자 수준의 설명으로 관심을 옮긴다.',
|
||||||
|
'물 분자가 산소 하나와 수소 둘로 이루어진 온전한 V자 구조임을 보여준다.',
|
||||||
|
'산소 쪽의 부분 음전하를 강조하며 물 분자 전체가 음이온인 것으로 표현하지 않는다.',
|
||||||
|
'수소 쪽의 부분 양전하를 산소 쪽과 구별하며 분자 전체의 중성은 유지한다.',
|
||||||
|
'한 물 분자의 수소와 이웃 물 분자의 산소가 끌림의 양 끝이라는 관계를 보여준다.',
|
||||||
|
'두 온전한 분자가 가까워지며 분자 사이 H···O 점선 길이가 줄고 내부 O–H 실선은 유지된다.',
|
||||||
|
'서로 다른 분자의 H···O 사이 끌림에 수소결합이라는 이름을 연결한다.',
|
||||||
|
'한 분자 내부의 O–H 실선과 분자 사이의 H···O 점선을 동시에 보여 두 결합을 구별한다.',
|
||||||
|
'불규칙한 액체 분자 집단으로 범위를 넓혀 수소결합이 영구적인 끈은 아님을 설명한다.',
|
||||||
|
'온전한 분자들이 소폭 움직이며 일부 이웃 사이 연결이 사라지고 새 연결이 생기는 과정을 보여준다.',
|
||||||
|
'수소결합 연결이 반복해서 바뀌어 액체의 이웃 관계가 고정된 격자가 아님을 보여준다.',
|
||||||
|
'많은 분자 사이의 끌림이 물끼리 함께 뭉치려는 응집 성질에 기여함을 연결한다.',
|
||||||
|
'청색 물 체적과 위쪽 공기가 만나는 경계를 절개 설명 모형으로 제시한다.',
|
||||||
|
'물 안쪽 분자를 강조해 여러 방향에 다른 물 분자 이웃이 있다는 환경을 보여준다.',
|
||||||
|
'강조 대상을 물 안쪽에서 표면으로 옮겨 공기 방향에는 액체 물 분자 이웃이 적다는 차이를 보여준다.',
|
||||||
|
'내부와 표면의 불규칙한 분자 배치를 유지해 액체 이웃 환경의 차이를 비교한다.',
|
||||||
|
'같은 부피의 닫힌 물방울을 길게 변형해 표면을 넓히는 과정을 입체로 보여준다.',
|
||||||
|
'같은 물의 양으로 길어진 모형을 유지하며 표면을 더 넓히는 데 에너지가 필요함을 설명한다.',
|
||||||
|
'같은 부피의 물방울이 표면 넓이를 줄이는 쪽으로 모양을 바꾸려는 경향을 연결한다.',
|
||||||
|
'길어진 물방울이 같은 부피를 유지하며 구형에 가까워지는 계획으로 표면을 줄이는 변화를 보여준다.',
|
||||||
|
'물 표면이 넓어지는 데 저항하는 성질을 표면장력이라는 이름과 연결한다.',
|
||||||
|
'길어진 물방울과 둥근 물방울의 모양을 같은 물의 양이라는 공통 조건에서 비교한다.',
|
||||||
|
'같은 부피일 때 구형이 겉면을 가장 작게 만드는 관계를 두 입체 모형으로 보여준다.',
|
||||||
|
'외부 영향이 작은 작은 물방울이 구형에 가까워지는 조건을 제시한다.',
|
||||||
|
'작은 비접촉 방울과 큰 바닥 접촉 방울을 별도 조건 예시로 구별한다.',
|
||||||
|
'바닥 접촉과 외부 영향으로 실제 물방울의 모양이 달라질 수 있음을 보여준다.',
|
||||||
|
'작은 청색 물방울로 돌아와 그 주변에 별도 고체 껍질이나 막이 없음을 보여준다.',
|
||||||
|
'분자 개념도에서 이웃 분자 사이 수소결합을 회수하며 원자와 내부 결합은 유지한다.',
|
||||||
|
'물방울과 분자 개념도를 함께 유지해 표면을 넓히기 어려운 성질을 답의 원인으로 연결한다.',
|
||||||
|
'분자 사이 끌림과 표면장력이 외부 영향이 작은 작은 물방울의 둥근 형태를 만든다는 답을 회수한다.'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def read(stage):
|
||||||
|
return json.loads((PROJECT / f'stages/{stage}/r1/artifact.json').read_text(encoding='utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def write(stage, payload, inputs):
|
||||||
|
folder = PROJECT / f'stages/{stage}/r3'
|
||||||
|
folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
body = dict(schema_version='1.1',project_id=PROJECT.name,stage_id=stage,revision=3,inputs=[dict(stage_id=s,revision=r) for s,r in inputs],payload=payload)
|
||||||
|
(folder/'artifact.json').write_text(json.dumps(body,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cuts = read('07-cuts')['payload']['cuts']
|
||||||
|
# 읽은 근거는 입력 참조에 포함한다. 선행 결과는 수정하지 않는다.
|
||||||
|
read('02-research')
|
||||||
|
read('03-script')
|
||||||
|
groups = [
|
||||||
|
(1,2,'OpeningDroplet','bg-droplet','정면보다 약간 위의 3D 전경, 중심 물방울; 다음 분자 확대는 새 Scene에서 제시한다.',[
|
||||||
|
'작은 청색 물방울이 크림색 공간 중앙에 구형에 가깝게 떠 있고 부드러운 하이라이트가 표면의 입체감을 보여준다.',
|
||||||
|
'작은 물방울을 유지하고 오른쪽 위에 분자 확대의 위치를 표시하는 합성 주석 앵커를 제시한다.',
|
||||||
|
'물방울과 확대 위치를 유지한 채 분자 설명으로 넘어갈 준비 상태다.'
|
||||||
|
]),
|
||||||
|
(3,5,'SingleMolecule','bg-molecules','3D 분자 근접, 약간 비스듬한 고정 카메라; 산소와 두 수소가 모두 가려지지 않는다.',[
|
||||||
|
'산호색 O 구 하나·미색 H 구 둘·남색 O–H 원통 두 개의 V자 3D 분자; 주석은 합성용 앵커만 마련한다.',
|
||||||
|
'온전한 V자 분자가 유지되고 산소 주석 앵커가 강조 위치에 있다.',
|
||||||
|
'분자 전체 중성을 유지하며 Oδ− 합성 주석 위치만 강조한다.',
|
||||||
|
'Oδ− 및 Hδ+ 합성 앵커가 모두 마련되며 원자 분리·전체 이온 표시는 없다.'
|
||||||
|
]),
|
||||||
|
(6,9,'BondPair','bg-molecules','실제 대표 Scene BondPair. 정면 위쪽 직교 카메라 (0,-12,6), ortho_scale=11.5; 내부 실선과 분자간 점선을 한 화면에 읽는다.',[
|
||||||
|
'PairA=(-1.8,0,-0.3), PairB=(1.65,0.15,0.45). 두 온전한 V자 분자의 내부 남색 실선과 서로 다른 분자 H···O 금색 분절 원통을 구별한다.',
|
||||||
|
'Blender F1018에서 PairA=(-1.8,0,-0.3), PairB=(1.65,0.15,0.45); H···O 점선의 양 끝이 실제 H/O 위치에 대응한다.',
|
||||||
|
'Blender F1220에서 PairB=(0.85,0.15,0.45)로 가까워져 H···O 점선 길이가 줄었다. 내부 O–H 실선 두 개씩은 그대로다.',
|
||||||
|
'같은 두 V자 분자·같은 카메라·금색 H···O 점선 유지; 수소결합 명칭은 합성 단계에서 별도로 배치한다.',
|
||||||
|
'같은 두 분자의 남색 내부 O–H 실선과 금색 분자간 H···O 점선이 동시에 보이고 모든 원자는 원래 분자에 남아 있다.'
|
||||||
|
]),
|
||||||
|
(10,13,'LiquidNetwork','bg-molecules','불규칙한 3D 액체 분자 집단 근접. 고정 격자 대신 깊이·방향을 달리하고 카메라는 유지한다.',[
|
||||||
|
'여러 온전한 V자 분자가 불규칙한 위치·방향에 있고 서로 다른 수의 금색 분자간 연결이 보인다.',
|
||||||
|
'분자 전체가 소폭 이동하는 동안 일부 H···O 점선 연결이 사라질 준비 상태; 내부 실선은 유지된다.',
|
||||||
|
'첫 H···O 점선 쌍이 사라지고 다른 분자 쌍의 점선이 나타난다. 원자 분리나 화학 반응은 없다.',
|
||||||
|
'새 이웃과의 H···O 연결이 보이며 분자마다 연결 수·방향이 다르다. 영구 끈·정지 격자는 없다.',
|
||||||
|
'불규칙한 집단을 유지하며 분자 사이의 응집이 거시적 물의 뭉침에 기여함을 다음 경계 모형으로 연결한다.'
|
||||||
|
]),
|
||||||
|
(14,17,'Interface','bg-interface','실제 대표 Scene Interface. 실제 물 경계 x=[-2.85,2.85], y=[-0.85,1.35], z=[-3.15,0.80] 안에 전체 원자·결합을 포함하고 앞면·윗면을 연 절개 설명 모형, 카메라 (0,-14,6), ortho_scale=13. 색 경계에 고체막을 추가하지 않는다.',[
|
||||||
|
'아래쪽 청색 물 체적과 위쪽 공기 공간이 보이며 실제 청색 물 영역 안에 불규칙한 물 분자가 모두 포함된다. 공기 이름은 추후 합성한다.',
|
||||||
|
'물-공기 경계를 유지하고 안쪽 Liquid4 주변의 다양한 이웃이 보이도록 근접 강조를 시작한다.',
|
||||||
|
'Blender F2807에서 Liquid4 안쪽 강조고리 scale=1, Liquid1 표면 강조고리 scale=0.01. 안쪽은 위·아래·옆에 이웃 물 분자가 있다.',
|
||||||
|
'Blender F3051에서 안쪽 강조는 작아지고 Liquid1 표면 강조고리 scale=1. 표면보다 위 공기 쪽에는 액체 물 이웃을 배치하지 않는다; 별도 막은 없다.',
|
||||||
|
'안쪽과 표면의 환경 차이가 보이는 불규칙한 분자 배치 유지. 공기는 단순화한 공간이며 진공이나 수증기 부재 주장이 아니다.'
|
||||||
|
]),
|
||||||
|
(18,22,'DropletStretch','bg-droplet','실제 대표 Scene DropletStretch. 닫힌 청색 3D mesh, (0,-12,5) 직교 카메라 ortho_scale=10.5. 같은 물의 양·설명용 모형 합성 앵커, 표면층 없음.',[
|
||||||
|
'Blender F3173의 닫힌 Droplet mesh scale=(1,1,1); 부드러운 청색 구형 물방울. 물의 양은 같은 조건이다.',
|
||||||
|
'Blender F3373의 Droplet scale=(1.85,1/sqrt(1.85),1/sqrt(1.85)); 같은 부피를 유지하며 좌우로 길어지고 표면이 넓어지는 설명용 변형이다.',
|
||||||
|
'길어진 같은 부피 물방울 유지; 에너지 필요 주석을 추후 합성한다. 실제 힘·에너지 수치는 표시하지 않는다.',
|
||||||
|
'같은 부피 물방울이 되돌아갈 준비 상태이며 물 표면의 확장 저항을 설명한다.',
|
||||||
|
'같은 부피의 닫힌 mesh가 구형에 가깝게 복귀한다. 복귀 애니메이션은 scenes 승인 후 정식 제작한다.',
|
||||||
|
'구형에 가까운 물방울과 표면 확장 저항 설명이 남는다. 껍질·고체 표면층은 없다.'
|
||||||
|
]),
|
||||||
|
(23,24,'EqualVolumeComparison','bg-comparison','3D 비교 전경, 좌우 모형을 같은 카메라·같은 물의 양으로 비교한다. 수치·픽셀면적 비교 없음.',[
|
||||||
|
'왼쪽 길어진 닫힌 물방울과 오른쪽 구형 물방울을 동일 부피 조건으로 나란히 배치한다.',
|
||||||
|
'같은 물의 양 주석 앵커를 유지하며 두 형태의 겉면 차이를 보여준다.',
|
||||||
|
'같은 부피에서 구형의 표면적이 작다는 관계를 청색 형태와 합성 주석으로 강조한다.'
|
||||||
|
]),
|
||||||
|
(25,27,'ShapeConditions','bg-comparison','조건 예시 3D 전경, 왼쪽 작은 비접촉 방울과 오른쪽 바닥에 닿은 큰 방울. 앞 비교와 별도 Scene.',[
|
||||||
|
'작은 비접촉 물방울과 더 큰 바닥 접촉 물방울을 별도의 조건 예시로 배치한다. 이 컷은 동일 부피 비교가 아니다.',
|
||||||
|
'외부 영향이 작은 작은 물방울이 구형에 가깝고 오른쪽 방울은 바닥과 접촉해 모양이 달라지는 예시다.',
|
||||||
|
'바닥 접촉 방울의 납작한 형태가 보이며 중력·접촉면 영향의 조건 주석 앵커가 있다.',
|
||||||
|
'두 조건 예시를 유지한다. 항상 완전한 구라는 단정·접촉각이나 크기 경계값은 없다.'
|
||||||
|
]),
|
||||||
|
(28,31,'AnswerDroplet','bg-droplet','도입과 같은 크림색 3D 공간·청색 방울 전경을 새 Scene에서 재제시하고 분자 개념도를 별도 위치에 둔다.',[
|
||||||
|
'구형에 가까운 청색 물방울과 분리된 분자 개념도 위치가 보인다. 물방울 주변에 별도 껍질은 없다.',
|
||||||
|
'동일 물방울 표면을 유지하며 추가 막이 없음을 보여준다.',
|
||||||
|
'별도 분자 개념도에서 온전한 V자 분자 사이 금색 H···O 점선이 변하고 내부 남색 실선은 유지된다.',
|
||||||
|
'물방울과 분자 개념도를 유지하며 표면을 넓히기 어려운 성질을 합성 주석으로 회수한다.',
|
||||||
|
'외부 영향이 작은 작은 물방울의 둥근 형태와 수소결합·표면장력의 설명이 함께 남아 도입 질문에 답한다.'
|
||||||
|
])
|
||||||
|
]
|
||||||
|
scenes, mapping = [], {}
|
||||||
|
for start,end,name,bg,camera,states in groups:
|
||||||
|
assert len(states)==end-start+2
|
||||||
|
for n in range(start,end+1):
|
||||||
|
cut=cuts[n-1]
|
||||||
|
mapping[cut['id']]=(name,bg)
|
||||||
|
scenes.append(dict(cut_id=cut['id'],description=DESCRIPTIONS[n-1],start_state=f"Scene={name}; F{cut['start_ms']//20+1}. "+states[n-start],end_state=f"Scene={name}; F{cut['end_ms']//20+1}. "+states[n-start+1],camera=camera))
|
||||||
|
connections=[]
|
||||||
|
for left,right in zip(scenes,scenes[1:]):
|
||||||
|
same=mapping[left['cut_id']][0]==mapping[right['cut_id']][0]
|
||||||
|
if same:
|
||||||
|
assert left['end_state']==right['start_state']
|
||||||
|
connections.append(dict(from_cut_id=left['cut_id'],to_cut_id=right['cut_id'],mode='continuous' if same else 'cut',reason='동일 Scene·논리 경계 프레임·물체·카메라 상태를 공유하며 설명의 다음 변화를 잇는다.' if same else f"설명 범위·조건·시점 전환으로 {mapping[left['cut_id']][0]}에서 별도 {mapping[right['cut_id']][0]} Scene으로 전환한다."))
|
||||||
|
profile=dict(engine='BLENDER_EEVEE',device='GPU',fps=50,width=1080,height=1920,samples=64,seed=0)
|
||||||
|
write('08-scenes',dict(scenes=scenes,connections=connections,preview=dict(scene_file=dict(path=f'projects/{PROJECT.name}/stages/08-scenes/r3/generated-source/preview.blend',verified=False,sha256=None,blender_version=None),render_profile=profile,samples=[])),[('02-research',1),('03-script',1),('07-cuts',1)])
|
||||||
|
common=["references/1.png의 단순 형태·정돈된 색·정보량을 교육용 3D로 해석한다. 크림색 단순 공간·부드러운 조명·선명한 형태를 반복한다.","산호색 O·미색 H·남색 내부 O–H 실선·금색 서로 다른 분자 H···O 점선·청색 물을 일관되게 사용한다.","상하단에는 제목·자막용 빈 공간을 두고 부분전하·한국어 주석은 나중 Remotion에서 검증해 합성한다. 3D에 글자를 굽지 않는다."]
|
||||||
|
extras={
|
||||||
|
'bg-droplet':('물방울의 크림색 3D 설명 공간',["청색 물방울은 닫힌 mesh이며 별도 껍질·막을 만들지 않는다.","같은 물의 양 변형은 실제 평가 mesh 부피를 시작·중간·끝에서 검사한다. 물리 시뮬레이션으로 주장하지 않는다."]),
|
||||||
|
'bg-molecules':('분자의 3D 근접·불규칙 액체 연결 설명 공간',["온전한 물 분자마다 O1H2·V자 형태·내부 O–H 실선 두 개를 유지한다. 각도·거리·힘 수치를 표시하지 않는다.","분자간 H···O 점선은 서로 다른 분자의 원자만 연결한다. 액체는 불규칙한 이웃·변하는 연결이고 모든 분자의 연결 수를 고정하지 않는다."]),
|
||||||
|
'bg-interface':('물-공기 경계의 3D 체적·절개 설명 공간',["전체 분자·결합이 물 체적 안에 포함되고 앞면·윗면만 열린 절개 설명 모형이며 위 공기는 진공 주장이 아니다. 별도 고체 표면층·전체 아래 가속 화살표가 없다.","안쪽은 다양한 방향의 액체 이웃, 표면은 위쪽 액체 이웃이 적은 환경으로 구별한다. 강조고리는 설명 표식이고 물 껍질이 아니다."]),
|
||||||
|
'bg-comparison':('같은 부피 비교·조건 예시의 크림색 3D 공간',["동일 부피 비교와 외부 영향 조건 예시는 별도 Scene으로 구별한다.","바닥은 조건 예시에만 등장한다. 중력·접촉각·크기 경계·표면장력 수치를 임의로 표시하지 않는다."])
|
||||||
|
}
|
||||||
|
backgrounds=[dict(id=k,description=v[0],fixed_features=common+v[1]) for k,v in extras.items()]
|
||||||
|
write('09-entities',dict(characters=[],backgrounds=backgrounds,appearances=[dict(cut_id=c['id'],character_ids=[],background_ids=[mapping[c['id']][1]]) for c in cuts]),[('02-research',1),('03-script',1),('07-cuts',1),('08-scenes',3)])
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""정식 결과의 계약·참조·실측·원본 및 복사 파일 해시를 대조한다."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import jsonschema
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
sys.path.insert(0,str(ROOT/'contracts'))
|
||||||
|
from check import semantic_errors
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--stage',choices=['10-references','11-images','12-motion'],required=True)
|
||||||
|
args=parser.parse_args()
|
||||||
|
BASE=PROJECT/f'stages/{args.stage}/r2'
|
||||||
|
|
||||||
|
|
||||||
|
def read(path):
|
||||||
|
return json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
artifact=read(BASE/'artifact.json')
|
||||||
|
context={s['stage_id']:read(PROJECT.parent.parent/s['artifact_path']) for s in read(PROJECT/'project.json')['stages'] if (ROOT/s['artifact_path']).is_file()}
|
||||||
|
jsonschema.validate(artifact,read(ROOT/'contracts/artifact.schema.json'))
|
||||||
|
assert not semantic_errors(artifact,context,complete=True),semantic_errors(artifact,context,complete=True)
|
||||||
|
request,result=artifact['payload']['request'],artifact['payload']['result']
|
||||||
|
source=result['scene_file'] if args.stage!='12-motion' else context['11-images']['payload']['result']['scene_file']
|
||||||
|
assert source['verified'] and hashlib.sha256((ROOT/source['path']).read_bytes()).hexdigest()==source['sha256']
|
||||||
|
if args.stage!='12-motion':
|
||||||
|
assert (ROOT/source['path']).is_relative_to(BASE)
|
||||||
|
model=read(BASE/'model-check.json')
|
||||||
|
assert model['source_sha256']==source['sha256'] and model['water_containment']['frames']==705
|
||||||
|
assets={source['path']:source['sha256']}
|
||||||
|
key,id_key={'10-references':('items','reference_id'),'11-images':('frames','frame_id'),'12-motion':('clips','cut_id')}[args.stage]
|
||||||
|
if args.stage=='10-references':
|
||||||
|
assert len(result['items'])==7 and len({i['object_name'] for i in result['items']})==7
|
||||||
|
assert {i['entity_id'] for i in request['items'] if i['kind']=='background'}=={b['id'] for b in context['09-entities']['payload']['backgrounds']}
|
||||||
|
if args.stage=='11-images':
|
||||||
|
assert len(result['frames'])==62
|
||||||
|
by_id={f['frame_id']:f['media'] for f in result['frames']}
|
||||||
|
frames={f['id']:f for f in request['frames']}
|
||||||
|
assert len(by_id)==len(frames)==62
|
||||||
|
anchors=read(BASE/'anchors.json')
|
||||||
|
assert anchors['source_sha256']==source['sha256'] and (anchors['width'],anchors['height'])==(1080,1920)
|
||||||
|
assert anchors['coordinate_system']=='top_left_pixel' and len(anchors['cuts'])==31
|
||||||
|
for cut,anchor in zip(context['07-cuts']['payload']['cuts'],anchors['cuts']):
|
||||||
|
start,end=cut['start_ms']//20,cut['end_ms']//20
|
||||||
|
assert (anchor['cut_id'],anchor['start_frame'],anchor['end_frame'])==(cut['id'],start,end)
|
||||||
|
assert frames[cut['id']+'-start']['frame_index']==start and frames[cut['id']+'-end']['frame_index']==end
|
||||||
|
assert frames[cut['id']+'-start']['scene_name']==anchor['scene_name']==frames[cut['id']+'-end']['scene_name']
|
||||||
|
assert anchor['motion'] and all(track['keys'][0][0]==start and track['keys'][-1][0]==end for track in anchor['motion'])
|
||||||
|
for connection in context['08-scenes']['payload']['connections']:
|
||||||
|
left,right=connection['from_cut_id']+'-end',connection['to_cut_id']+'-start'
|
||||||
|
if connection['mode']=='continuous':
|
||||||
|
assert by_id[left]['path']==by_id[right]['path']
|
||||||
|
assert (frames[left]['scene_name'],frames[left]['frame_index'])==(frames[right]['scene_name'],frames[right]['frame_index'])
|
||||||
|
else:
|
||||||
|
assert frames[left]['scene_name']!=frames[right]['scene_name']
|
||||||
|
if args.stage=='12-motion':
|
||||||
|
assert len(result['clips'])==31 and sum(c['end_frame']-c['start_frame'] for c in request['clips'])==5528
|
||||||
|
assert sum(c['duration_ms'] for c in request['clips'])==110560
|
||||||
|
if args.stage!='10-references':
|
||||||
|
visual=read(BASE/'visual-check.json')
|
||||||
|
assert visual['source_sha256']==source['sha256']
|
||||||
|
assert all(v['visual_review']=='checked' for v in visual['views']), '실제 화면 직접 관찰 미완료'
|
||||||
|
assert len(visual['views'])==(9 if args.stage=='11-images' else 31)
|
||||||
|
for view in visual['views']:
|
||||||
|
for asset in [view['sheet']]+view.get('decoded_frames',[])+view.get('render_frames',[]):
|
||||||
|
path=ROOT/asset['path']
|
||||||
|
assert path.is_relative_to(BASE) and hashlib.sha256(path.read_bytes()).hexdigest()==asset['sha256']
|
||||||
|
assets[asset['path']]=asset['sha256']
|
||||||
|
for item in result[key]:
|
||||||
|
media=item['media']
|
||||||
|
path=ROOT/media['path']
|
||||||
|
assert path.is_file() and path.is_relative_to(BASE) and media['verified']
|
||||||
|
assert (media['width'],media['height'])==(1080,1920)
|
||||||
|
evidence=read(path.parent/'evidence.json')
|
||||||
|
assert evidence['scene_file']==source and evidence['render_profile']==request['render_profile'] and evidence['media']==media
|
||||||
|
if args.stage=='10-references':
|
||||||
|
assert evidence['logical_frames'] and len(evidence['logical_frames'])==1
|
||||||
|
elif args.stage=='11-images':
|
||||||
|
frame=next(f for f in request['frames'] if f['id']==item['frame_id'])
|
||||||
|
assert evidence['scene_name']==frame['scene_name'] and evidence['logical_frames']==[frame['frame_index']]
|
||||||
|
else:
|
||||||
|
clip=next(c for c in request['clips'] if c['cut_id']==item['cut_id'])
|
||||||
|
assert evidence['scene_name']==clip['scene_name'] and evidence['logical_frames']==list(range(clip['start_frame'],clip['end_frame']))
|
||||||
|
assert media['duration_ms']==clip['duration_ms'] and 0<media['duration_ms']<=5000
|
||||||
|
endpoints=context['11-images']['payload']
|
||||||
|
end=next(f for f in endpoints['request']['frames'] if f['id']==clip['end_frame_id'])
|
||||||
|
assert end['frame_index']-evidence['logical_frames'][-1]==1
|
||||||
|
for logical in [clip['start_frame'],(clip['start_frame']+clip['end_frame'])//2,clip['end_frame']-1]:
|
||||||
|
frame=path.parent/f'frame-{logical:08}.png'
|
||||||
|
assets[frame.relative_to(ROOT).as_posix()]=hashlib.sha256(frame.read_bytes()).hexdigest()
|
||||||
|
for file in [path,path.parent/'evidence.json']:
|
||||||
|
assets[file.relative_to(ROOT).as_posix()]=hashlib.sha256(file.read_bytes()).hexdigest()
|
||||||
|
for file in BASE.glob('*.json'):
|
||||||
|
if file.name!='manifest.json':
|
||||||
|
assets[file.relative_to(ROOT).as_posix()]=hashlib.sha256(file.read_bytes()).hexdigest()
|
||||||
|
if (BASE/'review.md').is_file():
|
||||||
|
file=BASE/'review.md'
|
||||||
|
assets[file.relative_to(ROOT).as_posix()]=hashlib.sha256(file.read_bytes()).hexdigest()
|
||||||
|
value=dict(result='GREEN',stage_id=args.stage,source_sha256=source['sha256'],requested=len(request[key]),actual=len(result[key]),render_profile=request['render_profile'],actual_assets=[dict(path=path,sha256=sha) for path,sha in sorted(assets.items())])
|
||||||
|
(BASE/'manifest.json').write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
print('정식 결과 검사 GREEN',args.stage,len(value['actual_assets']))
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""승인 원본·선행 단계·기존 WAV·스타일의 바이트 보존을 실제 대조한다."""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
BASE=PROJECT/'stages/12-motion/r2'
|
||||||
|
PINNED='6b00a8f'
|
||||||
|
records=[]
|
||||||
|
|
||||||
|
|
||||||
|
def check(path,expected,kind):
|
||||||
|
actual=hashlib.sha256((ROOT/path).read_bytes()).hexdigest()
|
||||||
|
assert actual==expected, '기존 결과 바이트 변경: '+path
|
||||||
|
records.append(dict(path=path,sha256=actual,baseline=kind))
|
||||||
|
|
||||||
|
|
||||||
|
approved=json.loads((PROJECT/'stages/08-scenes/r3/design-check.json').read_text(encoding='utf-8'))
|
||||||
|
for item in approved['actual_assets']:
|
||||||
|
check(item['path'],item['sha256'],'08 r3 승인 실물 목록')
|
||||||
|
original=json.loads((PROJECT/'stages/08-scenes/r3/preservation-check.json').read_text(encoding='utf-8'))['baseline_sha256']
|
||||||
|
for path,sha in original.items():
|
||||||
|
if path.endswith('.wav') or path.endswith('stages/08-scenes/r2/generated-source/preview.blend'):
|
||||||
|
check(path,sha,'08 r3 보존 기준')
|
||||||
|
paths=subprocess.check_output(['git','ls-tree','-r','--name-only',PINNED,'--','references','projects/water-hydrogen-bond-surface-tension/stages'],cwd=ROOT,text=True,encoding='utf-8').splitlines()
|
||||||
|
for path in paths:
|
||||||
|
parts=Path(path).parts
|
||||||
|
if path.startswith('references/') or (len(parts)>4 and (int(parts[3][:2])<=9 or parts[4]=='r1')):
|
||||||
|
data=subprocess.check_output(['git','cat-file','--filters','--path='+path,PINNED+':'+path],cwd=ROOT)
|
||||||
|
check(path,hashlib.sha256(data).hexdigest(),PINNED+' 작업 사본 체크아웃 바이트')
|
||||||
|
value=dict(result='GREEN',pinned_commit=PINNED,unchanged_files=len(records),files=records)
|
||||||
|
(BASE/'preservation-check.json').write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
print('기존 승인 입력·WAV·스타일 보존 GREEN',len(records))
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""기존 실행기의 정식 승인·해시·디코드 검사로 요청을 실제 렌더한다."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
sys.path.insert(0,str(Path(__file__).resolve().parent))
|
||||||
|
from production_requests import REFERENCE_PLAN
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=Path('projects/water-hydrogen-bond-surface-tension')
|
||||||
|
PYTHON='C:/Users/baram/anaconda3/python.exe'
|
||||||
|
BLENDER='C:/Program Files/Blender Foundation/Blender 5.2/blender.exe'
|
||||||
|
COMPOSITOR=Path('C:/git/ShortVideoGenerator/projects/water-hydrogen-bond-surface-tension/renderer/node_modules/@remotion/compositor-win32-x64-msvc')
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--stage',choices=['10-references','11-images','12-motion'],required=True)
|
||||||
|
args=parser.parse_args()
|
||||||
|
BASE=PROJECT/'stages'/args.stage/'r2'
|
||||||
|
artifact_path=BASE/'artifact.json'
|
||||||
|
artifact=json.loads((ROOT/artifact_path).read_text(encoding='utf-8'))
|
||||||
|
records_path=ROOT/BASE/'cli-commands.json'
|
||||||
|
records=json.loads(records_path.read_text(encoding='utf-8')) if records_path.exists() else []
|
||||||
|
|
||||||
|
|
||||||
|
def execute(command):
|
||||||
|
started=time.monotonic()
|
||||||
|
value=subprocess.run(command,cwd=ROOT,capture_output=True,text=True,encoding='utf-8')
|
||||||
|
records.append(dict(command=command,exit_code=value.returncode,elapsed_seconds=round(time.monotonic()-started,3)))
|
||||||
|
if value.returncode:
|
||||||
|
records[-1]['error']=value.stderr[-2500:]+value.stdout[-2500:]
|
||||||
|
records_path.write_text(json.dumps(records,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
assert value.returncode==0,(value.stdout,value.stderr)
|
||||||
|
result=json.loads(value.stdout)
|
||||||
|
if result.get('media'):
|
||||||
|
records[-1]['evidence_path']=(Path(result['media']['path']).parent/'evidence.json').as_posix()
|
||||||
|
records[-1]['source_sha256']=result['scene_file']['sha256']
|
||||||
|
records_path.write_text(json.dumps(records,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def save():
|
||||||
|
(ROOT/artifact_path).write_text(json.dumps(artifact,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
common=[PYTHON,'-B','-X','utf8','tools/blender/run.py']
|
||||||
|
source=(BASE/'generated-source/references.blend').as_posix() if args.stage=='10-references' else artifact['payload']['request']['scene_path']
|
||||||
|
if args.stage!='12-motion' and not artifact['payload']['result']:
|
||||||
|
first_scene='BondPair'
|
||||||
|
inspected=execute(common+['inspect','--source',source,'--scene',first_scene,'--require-object','PairA','--blender',BLENDER])
|
||||||
|
artifact['payload']['result']=dict(scene_file=inspected['scene_file'],**({'items':[]} if args.stage=='10-references' else {'frames':[]}))
|
||||||
|
save()
|
||||||
|
if args.stage=='12-motion' and not artifact['payload']['result']:
|
||||||
|
artifact['payload']['result']=dict(clips=[])
|
||||||
|
save()
|
||||||
|
if args.stage=='10-references':
|
||||||
|
jobs=[dict(id=id,scene=scene,obj=obj,options=['--reference-id',id,'--frame',str(frame)]) for id,kind,entity,scene,obj,frame in REFERENCE_PLAN]
|
||||||
|
key,id_key='items','reference_id'
|
||||||
|
elif args.stage=='11-images':
|
||||||
|
jobs=[dict(id=f['id'],scene=f['scene_name'],obj='StudioAssets',frame=f['frame_index'],options=['--frame-id',f['id'],'--frame',str(f['frame_index'])]) for f in artifact['payload']['request']['frames']]
|
||||||
|
key,id_key='frames','frame_id'
|
||||||
|
else:
|
||||||
|
jobs=[dict(id=c['cut_id'],scene=c['scene_name'],obj='StudioAssets',options=['--cut-id',c['cut_id'],'--start',str(c['start_frame']),'--end',str(c['end_frame'])]) for c in artifact['payload']['request']['clips']]
|
||||||
|
key,id_key='clips','cut_id'
|
||||||
|
for job in jobs:
|
||||||
|
if any(item[id_key]==job['id'] for item in artifact['payload']['result'][key]):
|
||||||
|
continue
|
||||||
|
shared=None
|
||||||
|
if args.stage=='11-images':
|
||||||
|
request={f['id']:f for f in artifact['payload']['request']['frames']}
|
||||||
|
shared=next((item['media'] for item in artifact['payload']['result']['frames'] if (request[item['frame_id']]['scene_name'],request[item['frame_id']]['frame_index'])==(job['scene'],job['frame'])),None)
|
||||||
|
if shared:
|
||||||
|
media=shared
|
||||||
|
else:
|
||||||
|
command=common+['render','--mode','production','--project',(PROJECT/'project.json').as_posix(),'--artifact',artifact_path.as_posix(),'--output-revision','2','--source',source,'--scene',job['scene'],'--require-object',job['obj'],'--blender',BLENDER,'--ffmpeg',str(COMPOSITOR/'ffmpeg.exe'),'--ffprobe',str(COMPOSITOR/'ffprobe.exe')]+job['options']
|
||||||
|
evidence=execute(command)
|
||||||
|
media=evidence['media']
|
||||||
|
item={id_key:job['id'],'media':media}
|
||||||
|
if args.stage=='10-references':
|
||||||
|
item['object_name']=job['obj']
|
||||||
|
artifact['payload']['result'][key].append(item)
|
||||||
|
save()
|
||||||
|
print('실제 결과',args.stage,job['id'],media['path'],flush=True)
|
||||||
|
print('전체 요청 렌더 완료',args.stage,flush=True)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""승인된 31컷에서 정식 요청만 작성한다. 실제 결과는 실행 후 채운다."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
PROFILE=dict(engine='BLENDER_EEVEE',device='GPU',fps=50,width=1080,height=1920,samples=64,seed=0)
|
||||||
|
REFERENCE_PLAN=[
|
||||||
|
('style-3d','style',None,'BondPair','StyleAssets',1017),
|
||||||
|
('background-droplet','background','bg-droplet','OpeningDroplet','DropletStudioAssets',0),
|
||||||
|
('background-molecules','background','bg-molecules','BondPair','MoleculeStudioAssets',1017),
|
||||||
|
('background-interface','background','bg-interface','Interface','InterfaceAssets',3050),
|
||||||
|
('background-comparison','background','bg-comparison','EqualVolumeComparison','ComparisonStudioAssets',4076),
|
||||||
|
('object-molecule','object',None,'SingleMolecule','MoleculeAssets',396),
|
||||||
|
('object-droplet','object',None,'OpeningDroplet','DropletAssets',0)]
|
||||||
|
|
||||||
|
|
||||||
|
def read(stage,revision):
|
||||||
|
return json.loads((PROJECT/f'stages/{stage}/r{revision}/artifact.json').read_text(encoding='utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def write(stage,request,inputs):
|
||||||
|
path=PROJECT/f'stages/{stage}/r2/artifact.json'
|
||||||
|
existing=json.loads(path.read_text(encoding='utf-8')) if path.exists() else None
|
||||||
|
assert not existing or not existing['payload']['result'], '완료 또는 부분 실제 결과 덮어쓰기 금지'
|
||||||
|
value=dict(schema_version='1.1',project_id=PROJECT.name,stage_id=stage,revision=2,inputs=[dict(stage_id=s,revision=r) for s,r in inputs],payload=dict(request=request,result=None))
|
||||||
|
path.write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--stage',choices=['10-references','11-images','12-motion'],required=True)
|
||||||
|
args=parser.parse_args()
|
||||||
|
plans=read('08-scenes',3)['payload']['scenes']
|
||||||
|
if args.stage!='12-motion':
|
||||||
|
entities=read('09-entities',3)['payload']
|
||||||
|
read('02-research',1)
|
||||||
|
if args.stage=='10-references':
|
||||||
|
backgrounds={b['id']:b for b in entities['backgrounds']}
|
||||||
|
items=[]
|
||||||
|
for id,kind,entity,scene,obj,frame in REFERENCE_PLAN:
|
||||||
|
prompt='승인 08 r3의 '+scene+' 상태와 교육용 3D 고정 특징. 실제 '+obj+' 자산. '
|
||||||
|
if entity:
|
||||||
|
prompt+=' '.join(backgrounds[entity]['fixed_features'])
|
||||||
|
elif kind=='object':
|
||||||
|
prompt+='O1H2·V자·내부 남색 실선과 금색 분자간 H···O의 구별을 유지한다.' if id=='object-molecule' else '매끈한 청색 닫힌 mesh와 같은 물의 양을 유지하며 별도 껍질이 없다.'
|
||||||
|
else:
|
||||||
|
prompt+='references/1.png를 보존하고 단순 형태·정돈된 색·부드러운 조명·크림 배경으로 해석한다. 글자는 합성한다.'
|
||||||
|
items.append(dict(id=id,kind=kind,entity_id=entity,prompt=prompt))
|
||||||
|
write(args.stage,dict(style_reference='references/1.png',items=items,engine='Blender',render_profile=PROFILE),[('02-research',1),('08-scenes',3),('09-entities',3)])
|
||||||
|
elif args.stage=='11-images':
|
||||||
|
read('03-script',1)
|
||||||
|
cuts=read('07-cuts',1)['payload']['cuts']
|
||||||
|
reference=read('10-references',2)
|
||||||
|
assert reference['payload']['result'] and len(reference['payload']['result']['items'])==7
|
||||||
|
appearances={p['cut_id']:p for p in entities['appearances']}
|
||||||
|
mapping={'bg-droplet':'background-droplet','bg-molecules':'background-molecules','bg-interface':'background-interface','bg-comparison':'background-comparison'}
|
||||||
|
frames=[]
|
||||||
|
for cut,plan in zip(cuts,plans):
|
||||||
|
scene=plan['start_state'].split(';')[0].split('=')[1]
|
||||||
|
refs=['style-3d']+[mapping[b] for b in appearances[cut['id']]['background_ids']]
|
||||||
|
refs+=['object-molecule'] if scene in ['SingleMolecule','BondPair','LiquidNetwork','Interface'] else ['object-droplet']
|
||||||
|
if scene=='AnswerDroplet':
|
||||||
|
refs.append('object-molecule')
|
||||||
|
for position,key in [('start','start_ms'),('end','end_ms')]:
|
||||||
|
frames.append(dict(id=cut['id']+'-'+position,cut_id=cut['id'],position=position,prompt=plan['description']+' '+plan[position+'_state']+' '+plan['camera'],reference_ids=refs,scene_name=scene,frame_index=cut[key]//20))
|
||||||
|
write(args.stage,dict(frames=frames,engine='Blender',render_profile=PROFILE,scene_path='projects/'+PROJECT.name+'/stages/11-images/r2/generated-source/animation.blend'),[('02-research',1),('03-script',1),('07-cuts',1),('08-scenes',3),('09-entities',3),('10-references',2)])
|
||||||
|
else:
|
||||||
|
cuts=read('07-cuts',1)['payload']['cuts']
|
||||||
|
images=read('11-images',2)
|
||||||
|
assert images['payload']['result'] and len(images['payload']['result']['frames'])==62
|
||||||
|
frames={f['id']:f for f in images['payload']['request']['frames']}
|
||||||
|
clips=[]
|
||||||
|
for cut,plan in zip(cuts,plans):
|
||||||
|
start,end=frames[cut['id']+'-start'],frames[cut['id']+'-end']
|
||||||
|
clips.append(dict(cut_id=cut['id'],start_frame_id=start['id'],end_frame_id=end['id'],duration_ms=cut['end_ms']-cut['start_ms'],motion_prompt=plan['description']+' '+plan['start_state']+' → '+plan['end_state']+' '+plan['camera'],scene_name=start['scene_name'],start_frame=start['frame_index'],end_frame=end['frame_index']))
|
||||||
|
write(args.stage,dict(clips=clips,engine='Blender',render_profile=PROFILE,scene_path=images['payload']['result']['scene_file']['path']),[('07-cuts',1),('08-scenes',3),('11-images',2)])
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""실제 10/11 원본을 각각 다시 열어 모델·재질·기준 컬렉션 재사용을 대조한다."""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
|
||||||
|
|
||||||
|
def signature(source):
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=str(source))
|
||||||
|
assert not bpy.data.libraries and not bpy.data.images and not bpy.data.fonts and not bpy.data.sounds
|
||||||
|
meshes={}
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if obj.type=='MESH':
|
||||||
|
meshes[obj.name]=dict(vertices=[tuple(v.co) for v in obj.data.vertices],polygons=[tuple(p.vertices) for p in obj.data.polygons],materials=[m.name for m in obj.data.materials])
|
||||||
|
materials={}
|
||||||
|
for material in bpy.data.materials:
|
||||||
|
nodes=[]
|
||||||
|
for node in material.node_tree.nodes if material.use_nodes else []:
|
||||||
|
values=[]
|
||||||
|
for socket in node.inputs:
|
||||||
|
if hasattr(socket,'default_value'):
|
||||||
|
value=socket.default_value
|
||||||
|
if not isinstance(value,(str,int,float,bool)):
|
||||||
|
value=list(value) if hasattr(value,'__iter__') else str(value)
|
||||||
|
values.append((socket.name,value))
|
||||||
|
nodes.append((node.type,values))
|
||||||
|
materials[material.name]=dict(diffuse=list(material.diffuse_color),nodes=nodes)
|
||||||
|
return dict(meshes=meshes,materials=materials,collections={c.name:sorted(o.name for o in c.objects) for c in bpy.data.collections},scenes={s.name:dict(objects=sorted(o.name for o in s.objects),camera=s.camera.name,ortho_scale=s.camera.data.ortho_scale) for s in bpy.data.scenes})
|
||||||
|
|
||||||
|
|
||||||
|
reference=PROJECT/'stages/10-references/r2/generated-source/references.blend'
|
||||||
|
animation=PROJECT/'stages/11-images/r2/generated-source/animation.blend'
|
||||||
|
baseline=signature(reference)
|
||||||
|
actual=signature(animation)
|
||||||
|
assert baseline==actual, '실제 기준 모델·재질·컬렉션·카메라 재사용 불일치'
|
||||||
|
reference_hash=hashlib.sha256(reference.read_bytes()).hexdigest()
|
||||||
|
assert all(s['reference_source_sha256']==reference_hash for s in bpy.data.scenes), '10 원본 해시 연결 불일치'
|
||||||
|
value=dict(result='GREEN',reference_source_sha256=reference_hash,animation_source_sha256=hashlib.sha256(animation.read_bytes()).hexdigest(),actual_reopened_sources=2,mesh_objects=len(actual['meshes']),materials=len(actual['materials']),scenes=len(actual['scenes']),matching_fields=['local_mesh_vertices','polygons','material_slots','material_nodes','collection_members','scene_members','actual_camera_name','orthographic_scale'])
|
||||||
|
(PROJECT/'stages/11-images/r2/reuse-check.json').write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
print('실제 10→11 기준 재사용 GREEN',value['mesh_objects'])
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""정식 원본의 실제 누락·포함·부피 실패와 수정 성공을 기록한다."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
BLENDER='C:/Program Files/Blender Foundation/Blender 5.2/blender.exe'
|
||||||
|
VERIFY=PROJECT/'blender/verify_production.py'
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--stage',choices=['references','images'],required=True)
|
||||||
|
args=parser.parse_args()
|
||||||
|
BASE=PROJECT/f'stages/{"10-references" if args.stage=="references" else "11-images"}/r2'
|
||||||
|
SOURCE=BASE/f'generated-source/{"references" if args.stage=="references" else "animation"}.blend'
|
||||||
|
records=[]
|
||||||
|
|
||||||
|
|
||||||
|
def execute(arguments,expected,reason=None):
|
||||||
|
command=[BLENDER,'--background','--disable-autoexec','--python-exit-code','1']+arguments
|
||||||
|
value=subprocess.run(command,cwd=ROOT,capture_output=True,text=True,encoding='utf-8')
|
||||||
|
records.append(dict(command=command,exit_code=value.returncode,expected_exit_code=expected,matched_reason=reason,assertion_errors=[line for line in value.stderr.splitlines() if line.startswith('AssertionError:')]))
|
||||||
|
(BASE/'model-test-results.json').write_text(json.dumps(dict(result='RUNNING',executions=records),ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
assert value.returncode==expected,(value.stdout,value.stderr)
|
||||||
|
assert not reason or reason in value.stderr,(reason,value.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def check(source,expected,reason=None,out=None):
|
||||||
|
options=['--source',str(source),'--out',str(out or BASE/'generated-source/invalid-result.json')]
|
||||||
|
if args.stage=='images':
|
||||||
|
options.append('--animated')
|
||||||
|
execute(['--python',str(VERIFY),'--']+options,expected,reason)
|
||||||
|
|
||||||
|
|
||||||
|
check(BASE/'generated-source/missing.blend',1,'원본 파일 없음')
|
||||||
|
check(PROJECT/'stages/08-scenes/r3/generated-source/preview.blend',1,'대표 Scene 누락/혼입')
|
||||||
|
for name,edit,reason in [
|
||||||
|
('missing-scene',"bpy.data.scenes.remove(bpy.data.scenes['SingleMolecule'])",'대표 Scene 누락/혼입'),
|
||||||
|
('outside-mesh',"bpy.data.objects['Liquid9_H1'].data.vertices[0].co.x=10",'물 영역 밖 또는 여백 부족')]:
|
||||||
|
fixture=BASE/f'generated-source/{name}.blend'
|
||||||
|
expression=f'import bpy; bpy.ops.wm.open_mainfile(filepath={str(SOURCE)!r}); {edit}; bpy.ops.wm.save_as_mainfile(filepath={str(fixture)!r})'
|
||||||
|
execute(['--python-expr',expression],0)
|
||||||
|
check(fixture,1,reason)
|
||||||
|
check(SOURCE,0,out=BASE/'model-check.json')
|
||||||
|
(BASE/'model-test-results.json').write_text(json.dumps(dict(result='GREEN',negative_cases=4,positive_cases=1,executions=records),ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
print('정식 원본 RED→GREEN 확인',args.stage)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"""실제 끝점과 MP4 디코드의 모든 컷 상태를 모바일 비교 화면으로 만든다."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
from PIL import Image,ImageChops,ImageStat,ImageDraw
|
||||||
|
ROOT=Path(__file__).resolve().parents[3]
|
||||||
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
||||||
|
FFMPEG='C:/git/ShortVideoGenerator/projects/water-hydrogen-bond-surface-tension/renderer/node_modules/@remotion/compositor-win32-x64-msvc/ffmpeg.exe'
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--stage',choices=['11-images','12-motion'],required=True)
|
||||||
|
args=parser.parse_args()
|
||||||
|
BASE=PROJECT/f'stages/{args.stage}/r2'
|
||||||
|
artifact=json.loads((BASE/'artifact.json').read_text(encoding='utf-8'))['payload']
|
||||||
|
folder=BASE/'generated-source/inspection'
|
||||||
|
folder.mkdir(exist_ok=True)
|
||||||
|
records_path=BASE/'visual-cli-commands.json'
|
||||||
|
records=json.loads(records_path.read_text(encoding='utf-8')) if records_path.exists() else []
|
||||||
|
|
||||||
|
|
||||||
|
def sheet(paths,labels,out,columns):
|
||||||
|
rows=(len(paths)+columns-1)//columns
|
||||||
|
result=Image.new('RGB',(360*columns,664*rows),'white')
|
||||||
|
draw=ImageDraw.Draw(result)
|
||||||
|
for index,(path,label) in enumerate(zip(paths,labels)):
|
||||||
|
x,y=(index%columns)*360,(index//columns)*664
|
||||||
|
draw.text((x+8,y+5),label,fill='black')
|
||||||
|
result.paste(Image.open(path).convert('RGB').resize((360,640),Image.Resampling.LANCZOS),(x,y+24))
|
||||||
|
result.save(out)
|
||||||
|
return dict(path=out.relative_to(ROOT).as_posix(),sha256=hashlib.sha256(out.read_bytes()).hexdigest())
|
||||||
|
|
||||||
|
|
||||||
|
views=[]
|
||||||
|
if args.stage=='11-images':
|
||||||
|
frames={f['id']:f for f in artifact['request']['frames']}
|
||||||
|
groups={}
|
||||||
|
seen=set()
|
||||||
|
for item in artifact['result']['frames']:
|
||||||
|
frame=frames[item['frame_id']]
|
||||||
|
if item['media']['path'] in seen:
|
||||||
|
continue
|
||||||
|
seen.add(item['media']['path'])
|
||||||
|
groups.setdefault(frame['scene_name'],[]).append((ROOT/item['media']['path'],str(frame['frame_index'])))
|
||||||
|
for scene,images in groups.items():
|
||||||
|
path=folder/(scene+'.png')
|
||||||
|
asset=sheet([p for p,label in images],['F'+label for p,label in images],path,min(4,len(images)))
|
||||||
|
views.append(dict(scene_name=scene,unique_frames=len(images),sheet=asset,visual_review='not_checked'))
|
||||||
|
else:
|
||||||
|
requests={c['cut_id']:c for c in artifact['request']['clips']}
|
||||||
|
endpoints=json.loads((PROJECT/'stages/11-images/r2/artifact.json').read_text(encoding='utf-8'))['payload']
|
||||||
|
endpoint_media={f['frame_id']:ROOT/f['media']['path'] for f in endpoints['result']['frames']}
|
||||||
|
for item in artifact['result']['clips']:
|
||||||
|
clip=requests[item['cut_id']]
|
||||||
|
video=ROOT/item['media']['path']
|
||||||
|
sha=hashlib.sha256(video.read_bytes()).hexdigest()
|
||||||
|
count=clip['end_frame']-clip['start_frame']
|
||||||
|
indices=[0,count//2,count-1]
|
||||||
|
decoded=[video.parent/f'decoded-review-{index:03}.png' for index in [1,2,3]]
|
||||||
|
if not all(p.is_file() for p in decoded):
|
||||||
|
commands=[
|
||||||
|
[str(Path(FFMPEG).with_name('ffprobe.exe')),'-v','error','-count_frames','-show_streams','-of','json',str(video)],
|
||||||
|
[FFMPEG,'-hide_banner','-loglevel','error','-xerror','-i',str(video),'-map','0:v:0','-c:v','rawvideo','-f','null','-']]
|
||||||
|
commands += [[FFMPEG,'-hide_banner','-loglevel','error','-n','-i',str(video),'-ss',str(index/50),'-frames:v','1',str(path)] for index,path in zip(indices,decoded) if not path.is_file()]
|
||||||
|
for command_index,command in enumerate(commands):
|
||||||
|
value=subprocess.run(command,capture_output=True,text=True,encoding='utf-8')
|
||||||
|
record=dict(command=command,exit_code=value.returncode,media_sha256=sha)
|
||||||
|
if value.returncode:
|
||||||
|
record['error']=value.stderr
|
||||||
|
records.append(record)
|
||||||
|
records_path.write_text(json.dumps(records,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
assert value.returncode==0,value.stderr
|
||||||
|
if command_index==0:
|
||||||
|
streams=json.loads(value.stdout)['streams']
|
||||||
|
assert len(streams)==1 and streams[0]['codec_name']=='h264'
|
||||||
|
assert int(streams[0]['nb_read_frames'])==count
|
||||||
|
record['decoded_frames']=count
|
||||||
|
record['expected_frames']=count
|
||||||
|
records_path.write_text(json.dumps(records,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
logical=[clip['start_frame']+index for index in indices]
|
||||||
|
originals=[video.parent/f'frame-{frame:08}.png' for frame in logical]
|
||||||
|
errors=[sum(ImageStat.Stat(ImageChops.difference(Image.open(a).convert('RGB'),Image.open(b).convert('RGB'))).mean)/3 for a,b in zip(decoded,originals)]
|
||||||
|
assert max(errors)<5, 'MP4 실제 디코드와 원본 렌더 화면 불일치'
|
||||||
|
start_png=endpoint_media[clip['start_frame_id']]
|
||||||
|
end_png=endpoint_media[clip['end_frame_id']]
|
||||||
|
start_error=sum(ImageStat.Stat(ImageChops.difference(Image.open(start_png).convert('RGB'),Image.open(originals[0]).convert('RGB'))).mean)/3
|
||||||
|
assert start_error<.1, '11 시작 PNG와 12 실제 시작 상태 불일치'
|
||||||
|
end_difference=sum(ImageStat.Stat(ImageChops.difference(Image.open(end_png).convert('RGB'),Image.open(originals[-1]).convert('RGB'))).mean)/3
|
||||||
|
path=folder/(clip['cut_id']+'.png')
|
||||||
|
labels=[clip['cut_id']+' F'+str(frame) for frame in logical]+['11 end F'+str(clip['end_frame'])]
|
||||||
|
asset=sheet(decoded+[end_png],labels,path,4)
|
||||||
|
views.append(dict(cut_id=clip['cut_id'],scene_name=clip['scene_name'],media_sha256=sha,logical_frames=logical,end_png_logical_frame=clip['end_frame'],last_video_frame_delta=1,last_video_time_delta_ms=20,start_endpoint_png_mean_error=start_error,end_boundary_last_render_mean_difference=end_difference,decoded_frames=[dict(path=p.relative_to(ROOT).as_posix(),sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in decoded],render_frames=[dict(path=p.relative_to(ROOT).as_posix(),sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in originals],mean_decode_render_pixel_error=errors,sheet=asset,visual_review='not_checked'))
|
||||||
|
previous=json.loads((BASE/'visual-check.json').read_text(encoding='utf-8')) if (BASE/'visual-check.json').exists() else {}
|
||||||
|
for view in views:
|
||||||
|
old=next((v for v in previous.get('views',[]) if v.get('sheet')==view['sheet']),None)
|
||||||
|
if old and old.get('visual_review')=='checked':
|
||||||
|
view['visual_review']='checked'
|
||||||
|
view['observation']=old['observation']
|
||||||
|
source=artifact['result']['scene_file'] if args.stage=='11-images' else json.loads((PROJECT/'stages/11-images/r2/artifact.json').read_text(encoding='utf-8'))['payload']['result']['scene_file']
|
||||||
|
complete=len(artifact['result']['frames'])==62 if args.stage=='11-images' else len(views)==31
|
||||||
|
value=dict(result='READY_FOR_VISUAL_REVIEW' if complete else 'PARTIAL',source_sha256=source['sha256'],width=1080,height=1920,panel_width=360,panel_height=640,views=views)
|
||||||
|
(BASE/'visual-check.json').write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
print('실제 관찰 자료',args.stage,len(views))
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""기존 실행기로 세 대표 컷의 PNG/MP4를 만든다. 전 컷 제작은 하지 않는다."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
BASE = Path('projects/water-hydrogen-bond-surface-tension/stages/08-scenes/r3')
|
||||||
|
BLENDER = 'C:/Program Files/Blender Foundation/Blender 5.2/blender.exe'
|
||||||
|
PYTHON = 'C:/Users/baram/anaconda3/python.exe'
|
||||||
|
COMPOSITOR = Path('C:/git/ShortVideoGenerator/projects/water-hydrogen-bond-surface-tension/renderer/node_modules/@remotion/compositor-win32-x64-msvc')
|
||||||
|
SOURCE = (BASE/'generated-source/preview.blend').as_posix()
|
||||||
|
ARTIFACT = (BASE/'artifact.json').as_posix()
|
||||||
|
PROJECT = 'projects/water-hydrogen-bond-surface-tension/project.json'
|
||||||
|
PLAN = [('c07','BondPair','PairA',1017,1219,1219),('c16','Interface','WaterCutawayBody',2806,3050,3050),('c18','DropletStretch','Droplet',3172,3372,3372)]
|
||||||
|
|
||||||
|
|
||||||
|
def run(args, records):
|
||||||
|
result=subprocess.run(args,cwd=ROOT,text=True,encoding='utf-8',capture_output=True)
|
||||||
|
records.append(dict(command=args,exit_code=result.returncode))
|
||||||
|
(ROOT/BASE/'cli-commands.json').write_text(json.dumps(records,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
if result.returncode:
|
||||||
|
print(result.stdout,result.stderr,flush=True)
|
||||||
|
raise RuntimeError('실행 실패; 명령 기록과 기존 시도 보존')
|
||||||
|
parsed=json.loads(result.stdout)
|
||||||
|
records[-1]['source_sha256']=parsed['scene_file']['sha256']
|
||||||
|
records[-1]['scene_name']=parsed.get('scene_name')
|
||||||
|
if 'media' in parsed:
|
||||||
|
records[-1]['media']=parsed['media']
|
||||||
|
records[-1]['evidence_path']=(Path(parsed['media']['path']).parent/'evidence.json').as_posix()
|
||||||
|
(ROOT/BASE/'cli-commands.json').write_text(json.dumps(records,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
print('EXIT_0',args[5],parsed.get('scene_name'),flush=True)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--kind',choices=['image','video'],required=True)
|
||||||
|
parser.add_argument('--cut',choices=['c07','c16','c18'])
|
||||||
|
args=parser.parse_args()
|
||||||
|
record_path=ROOT/BASE/'cli-commands.json'
|
||||||
|
records=json.loads(record_path.read_text(encoding='utf-8')) if record_path.exists() else []
|
||||||
|
for record in records:
|
||||||
|
record.pop('stdout',None)
|
||||||
|
record.pop('stderr',None)
|
||||||
|
artifact=json.loads((ROOT/ARTIFACT).read_text(encoding='utf-8'))
|
||||||
|
for cut,scene,obj,start,end,boundary in PLAN:
|
||||||
|
if args.cut and args.cut!=cut:
|
||||||
|
continue
|
||||||
|
assert not any(s['id']==cut+'-'+args.kind for s in artifact['payload']['preview']['samples']), '이미 반영한 sample을 같은 버전에서 재생성하지 않는다'
|
||||||
|
common=[PYTHON,'-B','-X','utf8','tools/blender/run.py']
|
||||||
|
inspect=run(common+['inspect','--blender',BLENDER,'--source',SOURCE,'--scene',scene,'--require-object',obj],records)
|
||||||
|
artifact['payload']['preview']['scene_file']=inspect['scene_file']
|
||||||
|
(ROOT/ARTIFACT).write_text(json.dumps(artifact,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
options=['--frame',str(boundary)] if args.kind=='image' else ['--start',str(start),'--end',str(end)]
|
||||||
|
rendered=run(common+['render','--mode','preview','--project',PROJECT,'--artifact',ARTIFACT,'--output-revision','3','--blender',BLENDER,'--source',SOURCE,'--scene',scene,'--require-object',obj,'--cut-id',cut,'--ffmpeg',str(COMPOSITOR/'ffmpeg.exe'),'--ffprobe',str(COMPOSITOR/'ffprobe.exe')]+options,records)
|
||||||
|
# 실행기의 실제 증거만 미디어 메타데이터로 사용한다.
|
||||||
|
evidence=rendered
|
||||||
|
if args.kind=='video':
|
||||||
|
frames=[ROOT/Path(evidence['media']['path']).parent/f'frame-{f:08}.png' for f in (start,start+(end-start)//2,end-1)]
|
||||||
|
pixels=[Image.open(f).convert('RGB') for f in frames]
|
||||||
|
if not (ImageChops.difference(pixels[0],pixels[1]).getbbox() and ImageChops.difference(pixels[0],pixels[2]).getbbox()):
|
||||||
|
(ROOT/Path(evidence['media']['path']).parent/'visual-failure.json').write_text(json.dumps(dict(error='실제 렌더 프레임이 정지함; 미디어 반영 금지',logical_frames=[start,start+(end-start)//2,end-1]),ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
raise RuntimeError('실제 렌더 프레임이 정지함; 미디어 반영 금지')
|
||||||
|
artifact['payload']['preview']['samples'].append(dict(id=cut+'-'+args.kind,cut_ids=[cut],media=evidence['media']))
|
||||||
|
(ROOT/ARTIFACT).write_text(json.dumps(artifact,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
print('READY',cut,args.kind,evidence['media']['path'],flush=True)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user