콘텐츠로 이동

SRT 파일로 자막 만들기

이 레시피를 마치면 .srt나 .vtt 파일을 자막(caption) 트랙으로 가져와 스타일을 입히고, 말하는 단어를 하나씩 강조할 수 있습니다. 파일은 엔진이 파싱하므로 브라우저, Node.js, Python이 같은 파일에서 같은 큐를 읽습니다.

다음과 같은 자막 파일이 있다고 합시다.

talk.srt
1
00:00:00,300 --> 00:00:02,000
Every frame is rendered on the GPU.
2
00:00:02,200 --> 00:00:04,000
The 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는 인라인 타임스탬프로 단어 시간을 담습니다.

talk.vtt
WEBVTT
00:00:00.300 --> 00:00:02.000
Every <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" }로 핵심 단어를 띄울 수 있습니다.

터미널 창
alpha2-host export --project captions.json \
--asset forest=forest.jpg \
--font HankenGrotesk.ttf \
--out captions.mp4

기본 범위는 타임라인에서 마지막으로 보이는 것이 끝나는 지점까지이며, 자막 큐도 포함합니다.