SRT 파일로 자막 만들기
이 레시피를 마치면 .srt나 .vtt 파일을 자막(caption) 트랙으로 가져와 스타일을 입히고, 말하는 단어를 하나씩 강조할 수 있습니다. 파일은 엔진이 파싱하므로 브라우저, Node.js, Python이 같은 파일에서 같은 큐를 읽습니다.
파일 가져오기
섹션 제목: “파일 가져오기”다음과 같은 자막 파일이 있다고 합시다.
100:00:00,300 --> 00:00:02,000Every frame is rendered on the GPU.
200:00:02,200 --> 00:00:04,000The same project, in every target.alpha2/captions의 parseCaptions는 WebAssembly로 된 엔진의 파서로 이 파일을 읽고, captionTrack은 큐를 트랙으로 감쌉니다.
import init, * as wasm from "alpha2/wasm";import { captionTrack, parseCaptions } from "alpha2/captions";
await init();const text = await (await fetch("/talk.srt")).text();const { rows, skipped, notes } = parseCaptions(wasm, text, "srt"); // .vtt 파일이면 "webvtt"
const style = { font_family: "Hanken Grotesk", font_size: 0.055, background: { color: "#000000b3", padding: 0.3 },};const tracks = rows.map((cues, i) => captionTrack(`subs-${i}`, "Subtitles", cues, style));project.timeline.tracks.push(...tracks);rows의 한 행은 트랙 하나에 들어갈 큐입니다. 한 행의 큐는 서로 겹치지 않으므로, 큐가 겹치는 파일은 행이 여러 개 나옵니다. 행마다 자막 트랙을 하나씩 추가하십시오.skipped는 파서가 읽지 못한 블록 수이고,notes는 읽었지만 보존하지 못한 마크업 수입니다.- 타임스탬프는 정확하게 변환됩니다. 1ms는 120틱이므로
00:00:00,300은36000이 됩니다.
이렇게 만든 트랙을 사진 위에 얹은 프로젝트입니다.
{ "schema_version": 1, "id": "captions-srt", "name": "Captions from SRT", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "forest", "locator": "asset://forest", "kind": "image" } ], "timeline": { "tracks": [ { "id": "picture", "name": "Picture", "kind": "video", "clips": [ { "id": "photo", "source_id": "forest", "timeline_range": { "start": 0, "end": 540000 }, "source_range": { "start": 0, "end": 540000 } } ], "transitions": [] }, { "id": "subs", "name": "Subtitles", "kind": "caption", "clips": [], "captions": { "style": { "font_family": "Hanken Grotesk", "font_size": 0.055, "background": { "color": "#000000b3", "padding": 0.3 } }, "cues": [ { "timeline_range": { "start": 36000, "end": 240000 }, "text": "Every frame is rendered on the GPU." }, { "timeline_range": { "start": 264000, "end": 480000 }, "text": "The same project, in every target." } ] } } ] }}자막 트랙은 kind: "caption"이고, 클립 없이 style과 cues를 담은 captions 객체를 가집니다. 생략한 스타일 필드는 기본값을 따릅니다. 가운데 정렬, position 0.5, 0.92에 아래쪽 가운데(anchor 0.5, 1)를 맞추고, 흰색, 그림 짧은 변의 0.05인 font_size, max_width 0.9에서 줄바꿈합니다. 여기서는 폰트와 조금 더 큰 크기, 그리고 background를 더했습니다. background는 줄마다 뒤에 깔리는 상자이며, padding은 폰트 크기에 대한 비율입니다.
지금 읽는 단어 강조하기
섹션 제목: “지금 읽는 단어 강조하기”SRT는 큐마다 시간이 하나뿐입니다. 단어 단위로 강조하려면 단어마다 시간이 있어야 합니다. WebVTT는 인라인 타임스탬프로 단어 시간을 담습니다.
WEBVTT
00:00:00.300 --> 00:00:02.000Every <00:00:00.700>frame <00:00:01.100>is <00:00:01.300>rendered <00:00:01.700>live.parseCaptions(wasm, text, "webvtt")는 이런 큐를 런(run)으로 나누고, 각 런에 큐 시작 기준의 time을 붙입니다. 그러면 스타일의 animator가 timing 셀렉터로 런의 상태에 따라 글자를 바꿀 수 있습니다. 상태는 upcoming(아직 읽지 않음), active(읽는 중), spoken(읽음)입니다.
{ "schema_version": 1, "id": "captions-karaoke", "name": "Karaoke captions", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "wall", "locator": "asset://wall", "kind": "image" } ], "timeline": { "tracks": [ { "id": "picture", "name": "Picture", "kind": "video", "clips": [ { "id": "photo", "source_id": "wall", "timeline_range": { "start": 0, "end": 300000 }, "source_range": { "start": 0, "end": 300000 } } ], "transitions": [] }, { "id": "subs", "name": "Subtitles", "kind": "caption", "clips": [], "captions": { "style": { "font_family": "Hanken Grotesk", "font_size": 0.08, "font_weight": 700, "position": { "x": 0.5, "y": 0.85 }, "strokes": [ { "color": "#000000", "width": 0.08 } ], "shadows": [ { "color": "#00000099", "offset": { "x": 0, "y": 0.06 }, "blur": 0.15 } ], "animators": [ { "selector": { "type": "timing", "state": "upcoming" }, "properties": { "opacity": { "mode": "constant", "value": 0.55 } } }, { "selector": { "type": "timing", "state": "active", "ramp": 3000 }, "properties": { "fill": { "mode": "constant", "value": "#ffd040" }, "scale": { "mode": "constant", "value": { "x": 1.12, "y": 1.12 } } } } ] }, "cues": [ { "timeline_range": { "start": 36000, "end": 240000 }, "text": [ { "text": "Every ", "time": { "start": 0, "end": 48000 } }, { "text": "frame ", "time": { "start": 48000, "end": 96000 } }, { "text": "is ", "time": { "start": 96000, "end": 120000 } }, { "text": "rendered ", "time": { "start": 120000, "end": 168000 } }, { "text": "live.", "time": { "start": 168000, "end": 204000 } } ] } ] } } ] }}두 애니메이터는 다음과 같습니다.
- 첫째는
upcoming런을 골라opacity를 0.55로 낮춥니다. 아직 읽지 않은 단어가 흐려집니다. - 둘째는
active런을 골라fill을 호박색으로,scale을 1.12로 바꿉니다.ramp: 3000(25ms)은 변화를 딱 끊지 않고 서서히 넣고 뺍니다.
텍스트에는 strokes 외곽선과 부드러운 shadows가 있으며, 둘 다 폰트 크기에 대한 비율입니다. 덕분에 어떤 그림 위에서도 읽기 쉽습니다. active 셀렉터에 "sweep": true를 더하면 강조가 단어 안에서 글자 단위로 움직입니다.
런에는 class도 붙일 수 있습니다(WebVTT의 <c.name>, <b>, <i>, <u>가 클래스가 됩니다). class 셀렉터로 이런 런을 꾸밀 수 있습니다. 예를 들어 { "type": "class", "class": "key" }로 핵심 단어를 띄울 수 있습니다.
export하기
섹션 제목: “export하기”alpha2-host export --project captions.json \ --asset forest=forest.jpg \ --font HankenGrotesk.ttf \ --out captions.mp4기본 범위는 타임라인에서 마지막으로 보이는 것이 끝나는 지점까지이며, 자막 큐도 포함합니다.
import { readFile } from "node:fs/promises";import { exportProject } from "@alpha2-sh/node";
const project = JSON.parse(await readFile("captions.json", "utf8"));await exportProject({ project, assets: { forest: "forest.jpg" }, fonts: ["HankenGrotesk.ttf"], startTicks: 0, endTicks: 540_000, // 4.5초 frameRate: { num: 30, den: 1 }, output: "captions.mp4",});alpha2.compose는 노드 하나로 자막 파일을 읽어 영상 위에 얹습니다. CaptionedVideo는 영상을 캔버스에 맞추고(기본값 cover) 그 위에 자막을 놓습니다. Captions는 .srt나 .vtt 파일 경로와 style을 받습니다. style은 outline(기본값), boxed, bar-bottom, bold-center, bold, karaoke, pop 중 하나이며, 마지막 둘은 단어를 강조하므로 단어 시간이 필요합니다.
import alpha2from alpha2 import compose as c
video = c.Video(c.CaptionedVideo("talk.mp4", c.Captions("talk.srt", style="boxed")))compiled = video.compile(fonts=["HankenGrotesk.ttf"])alpha2.export(compiled, "talk-captioned.mp4")컴파일할 때 영상의 크기와 길이는 호스트의 conform에 묻습니다(GPU는 필요 없습니다). 음성 인식 도구가 준 단어 시간으로 노래방 자막을 만들려면 파일 대신 단어 시간을 넘기십시오.
words = [ {"start": 0.0, "end": 0.5, "text": "Sing"}, {"start": 0.5, "end": 1.0, "text": "along"},]captions = c.Captions(words=words, style="karaoke", highlight="#ffd040")함께 보기
섹션 제목: “함께 보기”- 자막: 큐, 런, 스타일, 화자.
- 자막 스키마: 자막의 모든 필드.
- Python:
alpha2.compose와 다른 노드. - 애니메이션 로어 서드: 레이아웃 상자 안의 텍스트.
