영상 편집기
abf44fd 기준입니다.
FilmCraft는 여러 영상·음성·자막을 타임라인에 배치하고, 편집·합성한 결과를 영상 파일로 내보내는 Rust 기반 비선형 편집기입니다.
핵심 로직은 crates/engine에 있고, 화면은 crates/ui-egui라는 별도 부품입니다.
같은 편집 명령을 사람이 화면에서 누르거나 자동화 도구가 호출해도, 공통 엔진이 처리하도록 설계했습니다.
영상 원본이 재료라면 클립은 “이 재료의 여기부터 3초만 쓰자”라는 지시입니다. 미리보기는 그 시점의 한 접시를 만들고, 내보내기는 모든 시점의 접시를 차례로 만들어 포장합니다.
화면·CLI·MCP는 주문을 받는 서로 다른 창구입니다. 창구마다 자르기 규칙을 다시 만들지 않고 같은 주방에 주문을 전달하므로, 편집 규칙과 사용자 인터페이스를 따로 공부할 수 있습니다.
Project 안에 미디어와 Sequence를 등록합니다.TrackItem의 시작 시각·원본 시작점·길이를 바꿉니다.crates/project/src/lib.rs의 MediaRef::File은 경로를 저장하고,
crates/edit/src/lib.rs의 split_track_at()은 클립 문서를 나눕니다.
이 함수를 실행한다고 원본 동영상의 바이트를 두 파일로 잘라 저장하는 것은 아닙니다.
Rust 문법을 모두 이해할 필요는 없습니다. 처음에는 명령 → 문서 변경 → 새 스냅샷 → 렌더링의 흐름만 잡으세요.
이 연결을 이해하면 그림판, 문서 편집기, CAD처럼 변경 이력이 중요한 다른 앱에도 같은 사고방식을 적용할 수 있습니다.
TrendShift Monthly #17 선정작이며, 2026-10-23 GitHub 기준 스타 15,809개와 포크 6,630개를 기록했습니다. 생성일은 2026-09-30입니다. 짧은 공개 기간에 관심이 모인 점은 확인할 수 있지만, 스타 증가의 원인이나 실제 사용자 수를 이 숫자만으로 알 수는 없습니다. 다음은 소스와 README에서 읽히는 관심 요인에 대한 해석입니다.
README는 주요 코덱을 공개 명세에서 직접 구현했다고 설명합니다.
실제로 crates/h264, crates/hevc, crates/av1, crates/aac와
MP4/MOV를 담당하는 crates/isobmff가 분리되어 있습니다.
입문자가 곧바로 H.264를 구현하기는 어렵지만, “파일을 읽는다”는 한 줄 아래에 어떤 계층이 있는지 보기에는 좋은 표본입니다.
crates/engine/src/commands.rs는 명령 식별자, 표시 이름, 매개변수 설명, 실행 가능 조건과 실행 함수를 함께 등록합니다.
crates/automation/src/server.rs의 FilmcraftMcp::run()은 창 없는 세션에서는
Session::execute()를 호출하고, 연결 모드에서는 실행 중인 앱으로 전달합니다.
“AI 기능”이라는 이름보다 외부 프로그램이 안정적인 동작 단위에 접근한다는 점이 학습상 중요합니다.
프로젝트가 비교 목표로 삼는 제품은 Adobe Premiere Pro입니다.
아래 표는 docs/target-app-parity.md와 docs/gaps.md가 제시한 목표를 바탕으로,
FilmCraft의 장점과 아직 남은 차이를 정리한 것입니다. 독립적인 기능 점수나 속도 순위는 아닙니다.
| 비교 축 | FilmCraft의 장점 | 경쟁 목표와의 차이 |
|---|---|---|
| 소스 학습 | 편집 규칙부터 GPU 셰이더까지 Rust 중심으로 연결해서 읽을 수 있음 | 제품 수준의 호환성과 안정성은 코드 공개 여부만으로 보장되지 않음 |
| 자동화 진입점 | GUI·CLI·MCP가 공유하는 명령 레지스트리 | Premiere의 자동화 체계와 같은 API라는 뜻은 아니며 기존 도구의 변환이 필요함 |
| 화면과 엔진 | 엔진 아래에 egui 의존성을 들이지 않는 계층 규칙 | 익숙한 화면을 목표로 해도 세부 조작감과 효과 결과는 별도 검증 대상 |
| 프로젝트 교환 | FCP7 XML·FCPXML·OTIO·AAF·OMF 등의 구현을 공부할 수 있음 | README 기준 Premiere의 .prproj 직접 열기는 미지원 |
| 확장 생태계 | 내장 효과와 코덱의 구현을 직접 수정할 수 있는 구조 | README 기준 VST3·Audio Units·OpenFX 호스팅은 미지원 |
| 제품 성숙도 | 미완성 영역을 문서로 공개하며 설계 의도를 추적하기 쉬움 | 현재 단계는 alpha이며 상용 제작 환경을 대체한다고 단정하기 어려움 |
이 저장소의 뚜렷한 학습상 장점은, 편집 화면의 동작을 파일 해석과 프레임 계산까지 거슬러 올라가 볼 수 있다는 점입니다.
README의 기능 범위 약 86%, 실제 작업 준비도 약 56%는 2026-10-10 재평가를 인용한 프로젝트 자체 지표입니다.
벤치마크 점수나 외부 인증으로 읽으면 안 됩니다. docs/gaps.md도 효과 이름의 존재와 결과의 정확성을 구별합니다.
이 앱에서 “백엔드”는 원격 HTTP 서버보다 창 없이 동작하는 로컬 엔진을 뜻합니다. 프론트엔드는 egui이고, 데스크톱 호스트가 운영체제의 창·오디오·파일 대화상자를 연결합니다. 일반 편집에 데이터베이스나 웹 서버 배포를 전제로 한 구조는 아닙니다.
| 부품 | 선언 / 잠금 버전 | 역할과 근거 파일 |
|---|---|---|
| Rust | edition = "2024"rust-version = "1.95" | 루트 Cargo.toml의 공통 패키지 설정. 문서도 stable ≥ 1.95를 요구 |
| serde | "1" / 1.0.229 | 루트 Cargo.toml. 문서·상태를 직렬화하며 derive, rc 기능 사용 |
| serde_json | "1" / 1.0.151 | 명령 매개변수, 프로젝트 형식, 제어 메시지의 JSON 표현 |
| thiserror | "2" / 2.0.21 | 공통 오류 정의. 잠금 파일에는 전이 의존성용 1.0.69도 존재 |
| rayon | "1" / 1.12.0 | CPU 병렬 처리. crates/export/src/job.rs의 프레임 묶음 렌더링 등에 사용 |
| image | "0.25" / 0.25.10 | crates/media/Cargo.toml. PNG·JPEG·GIF·WebP·TIFF·BMP 기능을 선택 |
| symphonia | "0.5.5" / 0.5.5 | crates/codecs/Cargo.toml. 일부 오디오 해석에 쓰는 외부 Rust 라이브러리 |
| harfrust | "0.12" / 0.12.0 | crates/text/Cargo.toml. OpenType 문자 모양 배치를 담당 |
표의 왼쪽 버전은 각 Cargo.toml의 허용 범위이고, 오른쪽은 Cargo.lock에 기록된 선택 버전입니다.
예를 들어 "0.25"와 0.25.10은 서로 모순되지 않습니다. 선언과 실제 해석 결과를 구별하는 연습이 됩니다.
0.12.0으로 선언하고, 하위 패키지가 version.workspace = true로 이어받습니다.| 부품 | 선언 / 잠금 버전 | 실제 연결 |
|---|---|---|
| egui / eframe | "0.36" / 각각 0.36.2 | crates/ui-egui의 패널과 apps/filmcraft의 앱 호스트. eframe에 wgpu·accesskit 기능 사용 |
| wgpu | "30" / 30.0.1 | crates/gpu/Cargo.toml. WGSL 셰이더로 합성·효과를 계산 |
| cpal | "0.16" / 0.16.0 | apps/filmcraft/Cargo.toml. 네이티브 오디오 출력 |
| rfd | "0.17" / 0.17.2 | 데스크톱의 파일 선택 대화상자 |
| craft-ui | 0.1.0 + Git 리비전 | 루트 매니페스트에서 5c874d51…에 고정한 공유 UI 부품 |
| wasm-bindgen | "=0.2.129" / 0.2.129 | apps/filmcraft-web/Cargo.toml. 브라우저와 Rust 사이 연결. CLI 도구도 같은 버전 필요 |
React나 Tauri가 중심인 앱이 아닙니다. 핵심 UI 의존성은 egui/eframe입니다.
웹 앱도 별도의 TypeScript 화면을 다시 만드는 대신 같은 엔진과 UI를 wasm32-unknown-unknown으로 컴파일합니다.
다만 브라우저의 파일·오디오·디코더에는 web-sys와 JavaScript 연결 코드가 필요합니다.
| 영역 | 설정 | 공부할 내용 |
|---|---|---|
| MCP | rmcp = "3.5" → 3.5.0tokio = "1" → 1.53.1 | crates/automation/Cargo.toml에서 서버·매크로·입출력 전송 기능 선택 |
| 개발 최적화 | 외부 패키지 opt-level = 2 / 기본 개발 프로필 1 | 루트 Cargo.toml. 컴파일 속도와 실행 속도의 절충 |
| 검사 실행기 | xtask/src/main.rs::ci() | 형식, release 모드 lint·테스트, 계층, 자산, WASM 검사 순서 |
| PR CI | ubuntu-24.04, 제한 120분 | .github/workflows/ci.yml. main push·PR·수동 실행에서 품질 검사 |
| CI 컴파일 | LTO 끄기 / 코드 생성 단위 16 | 배포용 최적화 설정을 테스트 바이너리의 링크 비용에 맞게 덮어씀 |
| 릴리스 | craft-common-actions 공유 워크플로 | .github/workflows/release.yml이 서명·배포 작업을 외부 공통 설정에 위임 |
빌드의 중심 파일은 Cargo.toml과 Cargo.lock입니다.
추적 파일 목록에는 package.json, pyproject.toml, go.mod, Dockerfile이 없습니다.
웹 대상이 있다고 해서 Node.js 설치나 컨테이너 서버가 필수인 것은 아닙니다.
의존성 목록을 읽을 때는 이름만 외우기보다, 어느 계층이 그 라이브러리를 가져오는지 함께 확인하세요.
특히 cpal은 데스크톱 호스트에, rmcp는 자동화에 두어 편집 규칙과 분리합니다.
자체 코덱이 많지만 symphonia와 image 등의 라이브러리도 사용합니다.
운영체제 미디어 API를 호출하는 crates/platform은 제한된 FFI 영역에서 unsafe를 허용합니다.
제품에 FFmpeg를 포함하지 않는다는 README 설명과, 테스트에서 FFmpeg를 외부 비교 도구로 쓴다는 설명은 함께 성립합니다.
아래는 주요 호출과 데이터 흐름을 단순화한 그림입니다. 위쪽은 사용자의 요청을 받는 부분, 가운데는 편집 상태를 바꾸는 부분, 아래쪽은 영상과 음성을 실제로 계산하는 부분입니다. 모든 의존성 선을 나열한 그래프는 아닙니다.
계층 표는 xtask/src/main.rs의 LAYERS에 있습니다.
layers()는 상위 계층으로 향하는 내부 의존성을 거부하고,
L5 아래에서 egui·eframe·cpal 같은 UI·OS 부품을 참조하지 못하게 검사합니다.
L1 이상에서 같은 계층 간 연결도 SAME_LAYER에 허용된 경우만 통과합니다.
crates/engine/src/commands.rs::CommandSpec에는 id, label, menu,
shortcut, params, enabled, run, journal이 있습니다.
표시할 정보와 실제 동작이 한 항목에 모여 있으므로 메뉴, 명령 검색, 자동화가 같은 목록을 참고할 수 있습니다.
// CommandSpec의 일부 필드: 원본 선언에서 발췌
pub id: &'static str,
pub params: &'static str,
pub enabled: Enabled,
pub run: Run,
pub journal: bool,
// 실제 타입 별칭
type Run = fn(&mut Session, &Value) -> Result<Value>;
type Enabled = fn(&Session) -> std::result::Result<(), String>;
params 자체는 설명 문자열이고, 호출 시 매개변수는 serde_json::Value입니다.
따라서 “등록만 하면 모든 명령의 입력 타입이 컴파일 시점에 검증된다”고 이해하면 안 됩니다.
각 핸들러의 파싱과 유효성 검사가 실제 경계입니다.
crates/ui-egui/src/menus.rs::invoke()가 엔진 명령을 Session::execute()로 전달합니다.execute()가 commands::find()로 명령을 찾고 enabled_for()로 실행 조건을 확인합니다.spec.run이 편집 함수를 호출합니다. 성공한 변경 명령은 저널에도 남습니다.FilmcraftMcp::run()도 같은 엔진 입구를 사용합니다. 창 없는 모드에서는 Mutex로 세션 접근을 조정합니다.예를 들어 timeline.razor는 트랙이나 클립을 고른 뒤 Session::edit_sequence() 안에서
edit::razor() 또는 edit::razor_items()를 호출합니다.
클립을 지정하고 연결 선택이 켜져 있으면 with_links()가 연결된 영상·음성 클립까지 처리 대상을 확장합니다.
// CLI 일괄 명령의 한 줄: 활성 시퀀스의 V1, 3초 지점
{"id": "timeline.razor", "params": {"track": "V1", "time": 762048000000}}
이 JSON 한 줄은 CLI의 run 하위 명령이 읽는 JSONL 항목입니다. TCP 제어 채널에서는 engine.execute라는 메서드와 인증 토큰을 담은 별도 요청 형식이 필요합니다.
전송 방식이 달라도 명령 식별자와 편집 규칙은 공유합니다.
crates/engine/src/lib.rs::Session::edit()는 먼저 프로젝트와 편집 상태를 복제하고,
복제본에 변경을 적용한 뒤 성공했을 때만 현재 프로젝트를 교체합니다.
시퀀스 중첩에 새 순환 참조가 생기지 않는지도 refuse_self_nesting()으로 확인합니다.
// Session::edit()에서 핵심 순서를 발췌
let mut p = (*self.project).clone();
let mut st = self.state.clone();
let r = f(&mut p, &mut st)?;
self.refuse_self_nesting(&p)?;
let old = std::mem::replace(&mut self.project, Arc::new(p));
self.history.undo.push((label.to_string(), old));
?는 오류가 나면 즉시 반환합니다. 위 코드에서 오류가 생기면 교체 단계에 도달하지 않으므로,
이 편집 경로의 현재 프로젝트와 상태가 유지됩니다. edit_sequence_as()는 여기에 Sequence::check() 검사도 더합니다.
여러 편집을 묶는 edit_group()에는 실패 시 이력과 문서를 되돌리는 처리도 있습니다.
원본 설계도에 바로 낙서하지 않고 복사본에서 수정한 뒤, 검수가 끝난 복사본만 새 정본으로 채택하는 방식입니다. 이전 정본을 버리지 않으면 실행 취소가 됩니다. 렌더러가 이전 정본을 읽는 동안에도 새 정본을 준비할 수 있습니다.
History의 두 스택은 Vec<(String, Arc<Project>)>입니다.
undo()는 이전 프로젝트를 꺼내 현재 것과 교환하고, 현재 것을 redo 스택에 넣습니다.
edit_merged()는 다른 편집 없이 같은 키·라벨로 이어지는 슬라이더 드래그를 한 번의 실행 취소 단계로 합칩니다.
비용도 구별해야 합니다. Arc::clone()으로 기존 스냅샷을 공유하는 일은 가볍지만,
위 Project::clone()은 프로젝트 데이터를 복제하는 경로입니다.
모든 편집이 상수 시간이라는 뜻도, 모든 하위 자료구조가 영속 자료구조라는 뜻도 아닙니다.
또 Session::new()는 History { limit: 200, ... }으로 초기화합니다. README의 “무제한 undo”보다 실제 기본 설정이 구체적입니다.
crates/time/src/lib.rs는 Tick(pub i64)와 초당 254_016_000_000 Tick을 정의합니다.
프레임률은 실수 하나가 아니라 FrameRate { num, den }으로 저장합니다.
흔히 29.97이라고 부르는 규격은 여기서 정확히 30000 / 1001입니다.
| 단위 | Tick 계산 | 의미 |
|---|---|---|
| 1초 | 254,016,000,000 | 시간 계산의 공통 눈금 |
| 24 fps의 한 프레임 | 10,584,000,000 | 초당 눈금을 24로 나눈 정수 |
| 30000/1001 fps의 한 프레임 | 8,475,667,200 | 초당 눈금 × 1001 ÷ 30000 |
| 48 kHz의 한 샘플 | 5,292,000 | 영상과 음성을 같은 눈금 위에 배치 |
// 학습용 계산 예시: crates/time의 공개 API
use filmcraft_time::{FrameRate, Tick};
let rate = FrameRate::FPS_29_97;
let first_frame = rate.frame_duration();
let sample = Tick::from_units(1, 48_000);
// first_frame.0 = 8_475_667_200
// sample.0 = 5_292_000
FrameRate::frame_duration()은 중간 계산에 i128을 사용하고,
sane()은 분자나 분모가 0 이하인 프레임률을 기본값으로 바꿉니다.
TimeRange는 [start, end)인 반열린 구간이므로,
맞닿은 두 클립의 경계 시각이 양쪽에 동시에 포함되지 않습니다.
정확성의 범위도 있습니다. 흔한 방송 프레임률은 정수로 표현되지만, 임의의 특이한 비율은 나눗셈에서 버림이 생길 수 있습니다.
Tick::from_seconds_f64()는 경계 입력을 반올림하고,
edit::split_track_at()의 배속 계산에는 f64가 사용됩니다.
따라서 “코드 전체에서 실수를 전혀 쓰지 않는다”보다 편집 시각의 기준을 정수로 통일한다가 정확합니다.
crates/render/src/lib.rs::render_sequence()는 시각·크기·소스를 받아 선형광 이미지로 렌더링합니다.
crates/render/src/plan.rs::plan_frame()은 GPU가 처리할 수 있는 레이어를 계획으로 표현합니다.
FramePlan은 Layers, Composite, Image 세 경우를 구별합니다.
FramePlan::Image로 전달합니다.crates/gpu/src/export_renderer.rs::ExportRenderer::render()에도 GPU 내보내기 경로가 있습니다.
다만 FramePlan::Composite는 None을 반환해 CPU 처리로 넘깁니다.
README의 “내보내기 렌더링은 CPU”라는 상태 설명만으로는 이 구현을 다 설명하지 못합니다.
crates/gpu/src/tests.rs::yuv_code_range_gpu_matches_cpu()는 8·10·12·16비트 입력을 CPU와 GPU로 처리해 비교합니다.
해당 테스트는 8비트 RGBA 출력에서 채널별 최대 오차를 1로 허용합니다.
그러므로 “같은 결과를 목표로 한다”와 “모든 경로의 모든 바이트가 항상 같다”도 구분해야 합니다.
빠른 구현을 만들 때는, 먼저 비교할 수 있는 기준 구현을 두고 지원하지 않는 경우 돌아갈 길까지 설계해야 합니다.
추적 중인 파일은 1,393개이며, crates/ 아래 패키지 50개, apps/ 아래 패키지 3개,
그리고 xtask가 있습니다. 아래 트리는 핵심 부분만 발췌한 것으로, 모든 폴더의 목록은 아닙니다.
| 궁금한 것 | 먼저 열 파일 | 다음에 확인할 것 |
|---|---|---|
| 색 문자열은 어떻게 검증할까? | crates/color/src/lib.rs | parse_hex()와 바로 아래 회귀 테스트 |
| 자르기는 무엇을 바꿀까? | crates/edit/src/lib.rs | split_track_at(), razor(), 길이와 원본 시작점 |
| 실행 취소는 어디서 만들까? | crates/engine/src/lib.rs | edit(), edit_merged(), undo() |
| 한 프레임은 어떻게 나오나? | crates/render/src/lib.rs | render_sequence()를 읽고 crates/render/src/plan.rs의 GPU 계획 경로와 비교 |
| 하드웨어가 다르면? | crates/codecs/src/lib.rs | 디코더 등록 순서와 software_video_decoder() |
| 앱 없이 조작할 수 있나? | crates/automation/src/server.rs | Backend, FilmcraftMcp::run() |
| 언어 / 영역 | 파일 수 | 물리적 줄 수 |
|---|---|---|
| Rust 전체 | 1,015 | 407,079 |
| WGSL | 5 | 1,422 |
| Python | 11 | 1,560 |
| 셸 / PowerShell | 10 / 2 | 808 / 231 |
| HTML / JavaScript | 1 / 1 | 95 / 59 |
커밋 abf44fd의 Git 추적 파일을 확장자별로 센 값입니다.
빈 줄·주석·테스트·소스에 들어 있는 표 데이터도 포함하며, 의존성 다운로드와 빌드 산출물은 포함하지 않습니다.
따라서 실행 로직만 센 순수 코드 줄 수와는 다릅니다.
Rust 가운데 crates/ui-egui는 75,495줄, crates/engine은 57,323줄,
crates/render는 23,281줄입니다. 시간 계산만 떼면 crates/time은 922줄입니다.
큰 제품 안에서도 학습 단위를 작게 고를 수 있습니다.
crates/edit/src/tests.rs, crates/engine/src/trim_tests.rs,
crates/gpu/src/tests.rs처럼 이름이 대응하는 테스트에서 먼저 입력과 기대 결과를 찾으세요.
함수 전체를 이해하기 전에도 “이 연산이 지켜야 하는 약속”을 알 수 있습니다.
커밋 abf44fd의 색상 입력 검증은 입문하기 좋은 예제입니다. crates/color/src/lib.rs::parse_hex()는
앞뒤 공백을 정리하고 선택적인 # 하나를 뺀 뒤, 모든 바이트가 ASCII 16진수 문자인지 검사합니다.
길이가 6이면 RGB, 8이면 RGBA로 처리하고 나머지는 None을 반환합니다.
// parse_hex()의 입력 검증 부분
let s = s.trim();
let s = s.strip_prefix('#').unwrap_or(s);
if !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
unwrap_or(s)는 값이 없을 때 원래 문자열을 쓰는 대체 처리입니다. 값이 없으면 중단되는 unwrap()과 다릅니다.
이어지는 s.get(i..i + 2)?는 잘못된 문자열 구간도 실패로 표현합니다.
문자열 길이뿐 아니라 허용된 문자도 검사해야 유효한 색상인지 판단할 수 있습니다.
실습 아이디어: parse_hex_rejects_signs()를 읽고 #+f+f+f,
##0f0f0f, #0f0f0f가 각각 어느 조건에서 갈리는지 표로 적어 보세요.
성공값만 확인하는 테스트보다 잘못된 입력의 종류를 분류하는 테스트를 배울 수 있습니다.
crates/project/src/lib.rs의 id_type! 매크로는 ItemId, TrackId,
ClipId 등을 각각 u64를 감싼 별도 타입으로 만듭니다.
같은 숫자 12라도 “원본 아이템 12”와 “타임라인 클립 12”는 다른 의미입니다.
struct ClipId(pub u64)처럼 기본 타입을 한 겹 감싸 의미를 부여하는 방법입니다.
함수가 ClipId를 받게 하면 TrackId를 실수로 넘기는 코드를 컴파일 단계에서 구별할 수 있습니다.
단, JSON 경계에서는 숫자를 올바른 타입과 실제 대상에 대응시키는 검사가 여전히 필요합니다.실습 아이디어: 서점의 BookId와 MemberId를 만들어 같은 숫자 타입을 직접 쓰는 경우와 비교하세요.
“메모리 표현이 같다”와 “업무상 같은 값이다”를 분리하는 연습입니다.
crates/media/src/lib.rs의 MediaSource는 프레임·오디오를 얻는 소스의 공통 약속입니다.
파일, 정지 이미지, 이미지 시퀀스, 합성 데모가 같은 인터페이스에 들어갑니다.
crates/render/src/lib.rs::SourceProvider는 아이템 ID를 소스에 연결하는 방법을 추상화합니다.
또 crates/codecs/src/video.rs::VideoDecoder에는 decode(), flush(),
reset() 등이 있습니다. register_video_decoder()는 새 디코더 팩토리를 목록 앞에 넣습니다.
운영체제 디코더를 먼저 시도하고, 지원 범위에 따라 소프트웨어 구현으로 돌아갈 수 있는 연결점입니다.
// VideoDecoder의 핵심 인터페이스 발췌
pub trait VideoDecoder: Send {
fn decode(&mut self, sample: &[u8], pts: i64)
-> Result<Vec<DecodedFrame>>;
fn flush(&mut self) -> Vec<DecodedFrame>;
fn reset(&mut self);
// 원본에는 이름·랜덤 접근·초안 모드 등의 메서드도 있음
}
실습 아이디어: 렌더러가 실제 파일 대신 단색 소스를 받는 과정을 그리세요. 테스트를 위해 큰 동영상을 준비하는 대신, 재현 가능한 가짜 입력을 넣을 수 있는 이유가 드러납니다.
Arc<Project>는 변경되지 않는 스냅샷을 여러 읽기 작업이 함께 소유하게 합니다.
반면 MCP의 창 없는 Backend::Headless는 Arc<Mutex<Session>>입니다.
전자는 문서를 읽는 쪽의 수명 문제, 후자는 명령이 세션 상태를 바꾸는 순서 문제를 풉니다.
실습 아이디어: “재생이 문서 A를 읽는 중에 편집으로 문서 B가 생겼다”는 상황을 시간순으로 그려 보세요. A를 읽는 작업이 끝나기 전에 메모리가 해제되지 않는 이유와, 공유 문서를 직접 수정하지 않는 이유를 설명해 보세요.
PlanLayer는 프레임과 좌표 변환 행렬, 불투명도, 혼합 모드를 함께 보관합니다.
crates/geom의 변환과 crates/color의 색 변환은 서로 다른 문제입니다.
위치가 맞는데 색이 틀리는 오류와, 색은 맞는데 위치가 어긋나는 오류를 분리해 볼 수 있습니다.
실습 아이디어: 단색 사각형 두 개로 시작해 위치 이동, 반투명 겹치기, 색 공간 변환을 한 번에 하나씩 추가하세요. 차이를 설명할 수 있는 작은 입력이 복잡한 실제 영상보다 디버깅에 유리합니다.
crates/media/src/cache.rs::FrameCache는 프레임 개수가 아니라 바이트 예산을 기준으로 캐시를 관리합니다.
예산을 넘으면 오래 사용하지 않은 항목부터 내보내 목표 사용량을 예산의 90%로 낮춥니다.
서로 크기가 다른 프레임을 모두 “한 장”으로 세면 메모리 사용을 통제하기 어렵기 때문입니다.
실습 아이디어: 같은 10장의 프레임이라도 360p와 4K의 메모리가 얼마나 다른지 계산하고, “최대 10장” 캐시와 “최대 64 MiB” 캐시의 차이를 비교하세요. 문서에 성능 수치가 있더라도 입력 크기·동시 작업 수·캐시 조건을 함께 읽어야 합니다.
xtask/src/main.rs::layers()는 아키텍처 규칙을 검사하고,
crates/format/src/atomic.rs::atomic_write()는 같은 디렉토리의 임시 파일에 먼저 쓰고 동기화한 뒤 이름을 바꿉니다.
마지막 디렉토리 동기화는 최선 노력 방식으로 처리합니다. “저장 버튼을 만들었다”보다 더 깊은 데이터 보존 문제입니다.
좋은 학습 단위는 기능 이름이 아니라, 그 기능이 실패해도 지켜야 하는 불변식입니다. 색상은 유효한 형식이어야 하고, 자른 두 조각의 길이는 원래 길이를 보존해야 하며, 실패한 편집은 현재 문서를 망가뜨리지 않아야 합니다.
커밋 기준 문서에는 모든 편집 상황을 포괄하는 CPU·RAM·VRAM 최소 사양표가 없습니다. 따라서 특정 메모리 용량을 공식 최소 사양으로 제시할 수 없습니다. 대신 실행 대상과 빌드 조건은 다음 파일에서 확인할 수 있습니다.
| 대상 | 확인되는 조건 | 읽을 근거 |
|---|---|---|
| 소스 빌드 | Rust stable ≥ 1.95, edition 2024. WASM 검사에는 해당 타깃 필요 | Cargo.toml, docs/contributing.md |
| macOS | 패키징 타깃은 11.0. Apple silicon·Intel 대상으로 구성 | packaging/macos/package.sh, packaging/macos/Info.plist.in |
| Windows | 소스 빌드에는 MSVC 도구. GPU 드라이버와 백엔드 선택이 실제 실행에 영향 | docs/contributing.md, apps/filmcraft/src/graphics.rs |
| Linux | C 도구 체인, 오디오·창 시스템 개발 라이브러리. CI는 Ubuntu 24.04 | .github/workflows/ci.yml의 패키지 목록 |
| 브라우저 | WASM과 그래픽·오디오 API. WebGPU에서 GPU 합성, WebGL2 경로에서는 CPU 합성 | docs/web.md, apps/filmcraft-web |
| 선택적 도구 | FFmpeg·ffprobe는 비교 테스트용이며 일반 실행의 필수 코덱 프로그램이 아님 | docs/contributing.md, AGENTS.md |
macOS의 11.0은 배포 설정값이며 모든 기기에서의 실행 보증과는 다릅니다.
또 기여 문서의 Linux 안내에는 GTK 3가 나오지만, 현재 CI는 ALSA·X11·Wayland·폰트·Vulkan 관련 패키지를 설치합니다.
환경 구성에서는 설명 문서의 날짜와 실제 워크플로를 함께 보아야 합니다.
RGBA의 네 채널을 각각 32비트 실수로 저장한다면 한 픽셀은 16바이트입니다. 압축 파일의 크기와 디코딩·합성 중 메모리 크기는 같지 않습니다. 아래는 프레임 한 장의 산술 계산입니다.
# 부가 자료구조를 제외한 RGBA f32 픽셀 버퍼
1920 × 1080 × 4 × 4 = 33,177,600 바이트 ≈ 31.6 MiB
3840 × 2160 × 4 × 4 = 132,710,400 바이트 ≈ 126.6 MiB
# 가로·세로를 각각 절반으로 줄이면 픽셀 수는 1/4
960 × 540 × 4 × 4 = 8,294,400 바이트 ≈ 7.9 MiB
crates/render/src/lib.rs의 Canvas는 실제 합성이 필요할 때까지 이미지 할당을 미룹니다.
하지만 효과의 중간 버퍼, 여러 레이어, 디코더 참조 프레임, GPU 텍스처가 추가될 수 있습니다.
위 값은 앱 전체 RAM 사용량도, GPU 경로의 텍스처 용량도 아닙니다.
crates/export/src/job.rs는 겹쳐 처리하는 프레임 묶음에 IN_FLIGHT_BUDGET = 1 << 29,
즉 512 MiB를 설정합니다. overlap_batch()는 그 안에 들어갈 묶음 크기를 계산하되 최소 한 프레임은 허용합니다.
이 예산은 해당 버퍼 묶음의 계산 기준이며 프로세스 전체 메모리의 강제 상한은 아닙니다.
학습을 시작할 때는 작은 해상도와 짧은 클립으로 계산을 이해한 뒤, 해상도·레이어·동시 작업을 하나씩 늘리세요.
FILMCRAFT_CPU_COMPOSITE=1은 데스크톱 미리보기의 GPU 합성을 끕니다. 내보내기의 GPU 사용은 별도 설정입니다.
창을 띄우는 그래픽 장치 초기화까지 없애는 설정은 아니므로 “GPU가 전혀 필요 없다”로 해석하면 안 됩니다.
창 없는 CLI와 창을 띄우는 데스크톱 앱도 요구 조건이 다릅니다.
docs/web.md 기준 웹 빌드는 단일 스레드입니다.
프레임 작업은 FrameServer::pump, 내보내기는 Session::pump_jobs를 통해 조금씩 진행하며,
WebAudio와 OPFS가 각각 소리와 복구 저장을 맡습니다. 데스크톱 스레드를 그대로 브라우저에서 실행하는 구조는 아닙니다.
docs/performance.md의 초기 측정 환경은 Apple M4 Pro, 14코어, 48 GB이며 높은 동시 부하도 명시합니다.
README의 디코딩 속도나 내보내기 시간은 프로젝트가 보고한 조건별 값입니다.
해상도·코덱·효과·캐시·빌드 프로필이 달라지면 결과도 달라집니다.
아래 과제는 읽기와 계산에서 시작해 작은 연습 모델로 확장합니다. 기존 코드를 통째로 실행하지 않아도 핵심 원리를 검증할 수 있습니다. 난이도보다 어떤 조건이 맞으면 성공인지를 먼저 정하고 진행하세요.
읽을 곳: crates/color/src/lib.rs::parse_hex(), parse_hex_rejects_signs().
할 일: 아래 입력의 결과와 실패 이유를 함수의 검사 순서대로 적습니다.
#를 모두 제거하는 구현과 하나만 제거하는 구현의 차이도 설명하세요.
| 입력 | 예상 결과 | 설명할 지점 |
|---|---|---|
#0f0f0f | 성공 | 6자리, 알파는 1.0 |
0f0f0f80 | 성공 | 8자리, 알파는 128/255 |
#+f+f+f | 실패 | 길이만 맞아도 부호는 16진수 문자가 아님 |
##0f0f0f | 실패 | 남은 두 번째 #이 문자 검사에서 걸림 |
#fff | 실패 | 이 함수는 3자리 축약형을 받지 않음 |
완료 기준: “잘못됐다” 대신 문자 집합·길이·접두사 중 어느 규칙인지 구분할 수 있습니다.
읽을 곳: crates/time/src/lib.rs::FrameRate::frame_duration()와 Tick::from_units().
할 일: 아래 독립적인 Python 계산을 읽고, 나머지가 0인 이유를 설명합니다. 1,000프레임 뒤 시각과 48 kHz 샘플 48,000개 뒤 시각도 계산해 보세요.
ticks_per_second = 254_016_000_000
numerator = ticks_per_second * 1001
assert numerator % 30000 == 0
frame_ticks = numerator // 30000
sample_ticks = ticks_per_second // 48000
assert sample_ticks * 48000 == ticks_per_second
print(frame_ticks, sample_ticks)
완료 기준: 한 프레임은 8,475,667,200 Tick,
1,000프레임은 8,475,667,200,000 Tick이라고 설명하고,
화면의 소수점 초 표시와 내부의 정수 시각을 구별합니다.
읽을 곳: crates/engine/src/commands.rs의 timeline.razor 등록부와
crates/edit/src/lib.rs::split_track_at().
조건: 클립은 타임라인 10초부터 6초 길이로 놓여 있고, 원본 시작점은 5초입니다. 배속은 1, 역재생·프레임 고정·전환·링크는 없다고 정합니다. 13초에서 자르면 어떻게 될까요?
완료 기준: 두 클립의 길이는 각각 3초이고 오른쪽 원본 시작점은 8초입니다. 원본 아이템 참조는 유지되지만 오른쪽 클립 ID는 새로 할당됩니다. 왼쪽·오른쪽 경계에서 자르면 왜 내부 분할이 없는지도 설명하세요.
확장: speed = 2일 때 오른쪽 source_in이 얼마나 이동하는지 비교하되,
타임라인 길이와 원본에서 소비하는 길이를 혼동하지 마세요.
읽을 곳: Session::edit(), edit_merged(), undo(), redo().
할 일: 연습용 Rust 프로그램에 제목과 숫자 목록만 가진 문서를 만듭니다. 복제본에서 숫자를 추가하고, 음수 입력이면 오류를 반환하며, 성공했을 때만 현재 문서와 교체합니다.
완료 기준: 실패한 요청 전후에 현재 문서와 이력 길이가 같고, 새 편집이 성공하면 redo가 비워집니다. 모든 값을 공유할 때와 실제 복제할 때의 비용도 구별합니다.
읽을 곳: crates/gpu/src/tests.rs::yuv_code_range_gpu_matches_cpu(),
crates/render/src/plan.rs::execute_cpu().
할 일: 단색, 회색 단계, 반투명 겹치기, 위치 이동을 각각 별도 입력으로 설계합니다. 같은 프레임 계획을 두 경로에 넣고 출력 크기, 최대 채널 오차, 차이가 생긴 좌표를 비교하는 기록 양식을 만드세요.
완료 기준: 해당 YUV 코드 범위 테스트의 허용 오차 1이 무엇을 뜻하는지 설명합니다. 다른 효과에도 같은 값을 무조건 적용하지 말고 비교 대상의 표현 형식에 맞춰 기준을 정합니다. GPU 장치가 없어 테스트를 건너뛴 경우는 비교 성공과 구별해 기록합니다.
실습의 성공 기준을 “화면이 나왔다”에서 “어떤 입력에 어떤 상태와 결과가 남는지 설명할 수 있다”로 바꿔 보세요.
주차는 진도 배분의 예시입니다. 하루에 읽을 파일 수를 정하기보다, 그 주의 질문에 답하는 산출물을 하나씩 남기세요. 프로그래밍을 막 시작했다면 1~2주차를 더 길게 잡고 코덱 내부 수학은 뒤로 미뤄도 됩니다.
| 주차 | 핵심 질문 / 기술 | 읽을 코드 | 남길 산출물 |
|---|---|---|---|
| 1주차 | 오류와 정상값을 어떻게 구별할까? Rust의 Option·Result·패턴 매칭 | crates/color/src/lib.rs의 parse_hex() | 유효·무효 입력 표와 새 타입을 쓴 작은 예제 |
| 2주차 | 영상의 시간은 어떻게 셀까? 정수·분수·반열린 구간 | crates/time/src/lib.rs | 24·30000/1001 fps와 48 kHz의 변환 표 |
| 3주차 | 편집은 어떤 자료구조를 바꿀까? 문서 모델과 불변식 | crates/project/src/lib.rs, crates/edit/src/lib.rs | 자르기 전후 클립 명세, 길이 보존 설명 |
| 4주차 | 실패와 실행 취소는 어디서 처리할까? 스냅샷·명령·트랜잭션 | crates/engine/src/lib.rs, crates/engine/src/commands.rs | 작은 문서의 undo·redo 모델과 실패 사례 |
| 5주차 | 파일에서 한 프레임을 어떻게 얻을까? 컨테이너·코덱·trait·캐시 | crates/media/src/lib.rs, crates/codecs/src/video.rs | 가져오기부터 프레임 요청까지의 호출 지도 |
| 6주차 | 빠른 구현은 어떻게 검증할까? 선형광·좌표 변환·GPU 비교 | crates/render/src/plan.rs, crates/gpu/src/tests.rs | 작은 합성 입력 세트와 오차 기록표 |
| 7주차 | 같은 엔진을 다른 환경에 연결하려면? MCP·WASM·OS 경계·CI | crates/automation/src/server.rs, apps/filmcraft-web, xtask | 입출력 경계도와 자동 검사할 규칙 세 가지 |
표에서 crates/color/부터 crates/automation/까지는 공통 기능을 나눈 crates/ 아래 라이브러리 패키지입니다.
매주 공식 문서에서 개념 하나를 읽고, 대응하는 함수와 테스트 하나를 찾아 연결하면 됩니다.
crates/ui-egui에서 메뉴 호출, 패널 상태, 단축키가 엔진 명령과 연결되는 방식을 읽습니다.crates/bitstream의 비트 입출력에서 시작해 crates/isobmff의 컨테이너 경계로 이동합니다.crates/geom의 변환, crates/render의 CPU 합성, crates/gpu의 WGSL 순서로 읽습니다.CommandSpec, FilmcraftMcp, 제어 채널의 인증·길이 제한 순서로 연결합니다.다음 단계로 넘어가는 기준은 파일을 다 읽었는지가 아니라, 작은 입력의 결과를 스스로 예측하고 근거 함수를 찾을 수 있는지입니다.
대형 저장소는 첫 장부터 끝까지 읽는 소설보다 도시 지도에 가깝습니다. 오늘의 목적지를 하나 고르고, 지나간 길에 함수 이름과 상태 변화를 적어 두세요. 같은 길을 UI·CLI·MCP에서 다시 만나면 구조가 머릿속에서 이어지기 시작합니다.
| 용어 | 쉬운 설명 |
|---|---|
| 비선형 편집 | 원본의 원하는 구간을 자유롭게 배치하는 편집 방식. 순서표와 원본을 따로 다룹니다. |
| Sequence | 영상 크기·프레임률과 여러 트랙을 묶은 하나의 편집 타임라인입니다. |
| TrackItem | 원본 아이템을 타임라인에 놓은 인스턴스. 시작점·길이·효과를 따로 가집니다. |
| source_in | 클립이 참조하는 원본 구간의 시작 시각입니다. 역재생은 이 구간의 끝쪽부터 보여 줍니다. |
| Tick | 영상과 오디오의 시각을 함께 세는 정수 눈금입니다. |
| 유리수 프레임률 | 30000/1001처럼 분자와 분모로 표현하는 초당 프레임 수입니다. |
| 반열린 구간 | 시작은 포함하고 끝은 제외하는 구간. 이웃한 클립의 경계를 겹치지 않게 표현합니다. |
| 컨테이너 | 영상·음성·시간 정보 등을 담는 파일 구조. MP4·MOV·Matroska 등이 해당합니다. |
| 코덱 | 영상·음성을 압축하거나 압축된 데이터를 복원하는 규칙과 구현입니다. |
| 디코딩 / 인코딩 | 압축 데이터를 프레임·샘플로 복원하는 일 / 프레임·샘플을 압축 데이터로 바꾸는 일입니다. |
| 멀티플렉싱(muxing) | 인코딩된 영상·음성 패킷과 시간 정보를 하나의 컨테이너에 묶는 작업입니다. |
| GOP | 서로 참조할 수 있는 압축 영상 프레임의 묶음. 원하는 한 장을 얻으려 앞 프레임부터 풀어야 할 수 있습니다. |
| PTS | 프레임을 보여 줄 시각. 디코더에 데이터를 넣는 순서와 화면에 보여 주는 순서는 다를 수 있습니다. |
| trait | 여러 구현이 지켜야 하는 기능 약속. 파일 소스나 디코더를 바꿔 끼울 때 사용합니다. |
| Arc | 여러 작업이 같은 값을 함께 소유하도록 돕는 참조 계수 포인터. 이것만으로 내부 수정이 안전해지지는 않습니다. |
| Mutex | 공유 상태에 한 번에 하나의 작업만 접근하도록 순서를 조정하는 잠금입니다. |
| 스냅샷 | 어느 시점의 문서 상태. 이후 편집이 생겨도 읽던 상태를 유지하는 데 사용합니다. |
| 트랜잭션 | 여러 변경을 성공 시 함께 반영하고 실패 시 반영하지 않는 처리 단위입니다. |
| 명령 레지스트리 | 명령 이름·입력 설명·실행 조건·함수를 모아 둔 목록입니다. |
| 불변식 | 어떤 기능을 실행해도 계속 참이어야 하는 규칙. 예를 들어 트랙의 클립 정렬이나 길이 조건입니다. |
| 선형광 | 빛의 양을 기준으로 계산하는 색 표현. 합성과 색 보정의 계산 공간을 이해할 때 중요합니다. |
| WGSL / wgpu | GPU가 실행할 계산을 적는 언어 / 여러 그래픽 백엔드 위에서 GPU 작업을 구성하는 Rust API입니다. |
| 기준 구현 | 다른 최적화 구현의 결과와 비교할 출발점. 기준 구현 자체의 오류 가능성도 별도로 검증해야 합니다. |
| LRU 캐시 | 최근 덜 사용한 항목부터 비우는 임시 저장소입니다. FilmCraft의 프레임 캐시는 바이트 예산도 고려합니다. |
| WASM / OPFS | 브라우저에서 실행하는 컴파일 코드 형식 / 웹 출처별 전용 파일 저장소입니다. |
| MCP / 연결 모드 | 도구 호출을 연결하는 프로토콜 / 호출을 실행 중인 데스크톱 앱에 전달하는 방식입니다. |
| FFI | 다른 언어·플랫폼 API와 연결하는 경계. 이 프로젝트에서는 OS 미디어 API를 별도 크레이트에 가둡니다. |
| 원자적 저장 | 완성된 새 파일을 준비한 후 교체해, 저장 도중의 불완전한 파일이 정본이 될 위험을 줄이는 방법입니다. |
| 주제 | 소스 / 확인할 함수 |
|---|---|
| 의존성 | Cargo.toml · Cargo.lock |
| 명령·상태 | crates/engine/src/lib.rs의 execute()·edit()·undo() / crates/engine/src/commands.rs의 CommandSpec |
| 문서·자르기 | crates/project/src/lib.rs / crates/edit/src/lib.rs의 split_track_at() |
| 시간·입력 | crates/time/src/lib.rs / crates/color/src/lib.rs의 parse_hex() |
| GPU 계획·비교 | crates/render/src/plan.rs / crates/gpu/src/tests.rs |
| 내보내기 | crates/gpu/src/export_renderer.rs / crates/export/src/job.rs |
| MCP·제어 채널 | crates/automation/src/server.rs / apps/filmcraft/src/control_server.rs |
| 저장·품질 검사 | crates/format/src/atomic.rs / xtask/src/main.rs / CI 설정 |
외부 기술 문서의 기본 페이지는 최신 버전을 보여 줄 수 있습니다.
이 글의 API와 의존성 버전을 맞춰 읽으려면 Cargo.lock의 버전을 기준으로 선택하세요.
| 항목 | 상태 | 기준 |
|---|---|---|
| 저장소 / 주 언어 | storytold/filmcraft / Rust | 2026-10-23 GitHub 기준 |
| 스타 / 포크 | 15,809 / 6,630 | 2026-10-23 GitHub 기준 |
| 생성일 | 2026-09-30 | GitHub 저장소 메타데이터 |
| 최근 push | 2026-10-23 | GitHub 저장소 메타데이터 |
| 최신 릴리스 | v0.12.0 · 2026-10-23 | GitHub 릴리스 메타데이터 |
| 분석 기준 커밋 | abf44fd · 2026-10-22Reject non-hex characters in colour hex parsing (#2600) | 커밋 원문 시각은 -0600. 한국 시각으로는 10월 23일 |
| 패키지 규모 | 라이브러리 등 50개 + 앱 3개 + xtask | 커밋 기준 매니페스트 개수 |
| 코드 규모 | Rust 407,079줄 / WGSL 1,422줄 | Git 추적 파일의 물리적 줄 수, 테스트·주석·빈 줄 포함 |
| GitHub 라이선스 표기 | Apache-2.0 | 2026-10-23 GitHub 기준 |
| 소스의 공통 라이선스 | MIT OR Apache-2.0 | Cargo.toml, LICENSE-MIT, LICENSE-APACHE |
| 현재 제품 단계 | alpha | 커밋 기준 README·ROADMAP. v0.12.0이라는 번호와 제품 안정 단계는 별개 |
이 글의 소스 기준은 abf44fd입니다. 최신 릴리스의 모든 파일이 이 커밋과 같다는 뜻은 아닙니다.
제품은 alpha이며 실제 제작 파일의 가져오기·재생·교환·내보내기 결과는 해당 환경에서 따로 확인해야 합니다.
README의 속도와 준비도 수치는 프로젝트의 보고값·자체 평가입니다.
문서에는 갱신 시차가 있습니다. README의 “무제한 undo”와 달리 기본 이력 한도는 200이며,
CPU 전용처럼 서술된 내보내기에도 GPU 구현이 있습니다.
“camera RAW 미지원”이라는 안내와 달리 crates/dng와 crates/media/src/still/dng.rs에는 DNG·CinemaDNG 연결이 있습니다.
이것을 모든 카메라 RAW 지원으로 확대해서 읽으면 안 됩니다.
docs/gaps.md의 “PR 테스트 CI 없음”이라는 서술과 달리 현재 .github/workflows/ci.yml은 PR에서 품질 검사를 호출합니다.
설정의 존재는 모든 검사 실행의 성공을 뜻하지 않습니다.
ROADMAP.md의 45개 크레이트·약 34.3만 줄도 현재 트리의 50개 크레이트·Rust 약 40.7만 줄과 기준 시점이 다릅니다.
README 기준 .prproj 직접 가져오기와 VST3·Audio Units·OpenFX 호스팅은 아직 지원하지 않습니다.
하드웨어 가속과 코덱 지원은 운영체제·장치·프로파일에 따라 달라집니다.
자동화 제어 채널은 루프백에 연결하고 64자리 16진수 토큰을 요구하며, 토큰이 없으면 리스너를 열지 않습니다.
GitHub의 Apache-2.0 표기와 달리 공통 패키지 선언은 MIT OR Apache-2.0입니다.
자산은 개별 출처·라이선스를 가지며, docs/brand/LICENSE-brand.txt의 브랜드 표시는 별도 조건입니다.
코드의 라이선스만 보고 모든 이미지·폰트·이름에 같은 조건을 적용하지 마세요.