motiontext-renderer
Version:
Web-based animated caption/subtitle renderer with plugin system
554 lines (440 loc) β’ 15.4 kB
Markdown
# MotionText Renderer
π¬ **μΉ κΈ°λ° μ λλ©μ΄μ
μλ§/μΊ‘μ
λ λλ¬ λΌμ΄λΈλ¬λ¦¬**
λμμ μ½ν
μΈ μ λμ μΈ μλ§κ³Ό μ λλ©μ΄μ
ν¨κ³Όλ₯Ό μ½κ² μΆκ°ν μ μλ TypeScript λΌμ΄λΈλ¬λ¦¬μ
λλ€. νλ¬κ·ΈμΈ μμ€ν
μ ν΅ν΄ νμ₯ κ°λ₯νλ©°, μΉ νμ€μ μ€μνλ μμ ν μλλ°μ€ νκ²½μμ λμν©λλ€.
## β¨ μ£Όμ κΈ°λ₯
- π― **μ κ·ν μ’νκ³**: μ€ν
μ΄μ§ κΈ°μ€ (0~1) μ’νλ‘ λͺ¨λ λλ°μ΄μ€ μ§μ
- β° **μ λ°ν λ―Έλμ΄ μ±ν¬**: requestVideoFrameCallback κΈ°λ° νλ μ λκΈ°ν
- π **λμ νλ¬κ·ΈμΈ μμ€ν
**: ES Dynamic Import + λ¬΄κ²°μ± κ²μ¦
- π‘οΈ **보μ μλλ°μ€**: νλ¬κ·ΈμΈ 격리 μ€ν νκ²½
- π **λ€μΈ΅ λ μ΄μ΄ μμ€ν
**: Track β Cue β Element κ³μΈ΅ ꡬ쑰
- π¦ **TypeScript μμ μ§μ**: νμ
μμ μ±κ³Ό IntelliSense
## π μ€μΉ
```bash
pnpm add motiontext-renderer
```
```bash
npm install motiontext-renderer
```
```bash
yarn add motiontext-renderer
```
> μ°Έκ³ : μ΄ λΌμ΄λΈλ¬λ¦¬λ GSAPμ νΌμ΄ μμ‘΄μ±μΌλ‘ μꡬν©λλ€. νΈμ€νΈ μ±μ GSAPμ μ€μΉνμΈμ.
>
> μ€μΉ: `pnpm add gsap` (λλ npm/yarn)
## π κΈ°λ³Έ μ¬μ©λ²
```typescript
import { MotionTextRenderer } from 'motiontext-renderer';
// 컨ν
μ΄λ μμμ λΉλμ€ μμ μ€λΉ
const container = document.getElementById('caption-container');
const video = document.getElementById('main-video');
// λ λλ¬ μ΄κΈ°ν
const renderer = new MotionTextRenderer(container);
// μ€μ λ‘λ
const config = {
version: '1.3',
timebase: { unit: 'seconds' },
stage: { baseAspect: '16:9' },
tracks: [
{
id: 'subtitle',
type: 'subtitle',
layer: 1
}
],
cues: [
{
id: 'cue1',
track: 'subtitle',
hintTime: 0,
root: {
id: 'group1',
type: 'group',
children: [
{
id: 'text1',
type: 'text',
absStart: 0,
absEnd: 3,
content: 'μλ
νμΈμ!',
layout: {
position: [0.5, 0.8]
}
}
]
}
}
]
};
await renderer.loadConfig(config);
// λΉλμ€μ μ°λ
renderer.attachMedia(video);
// μ¬μ μμ
renderer.play();
```
### π μΈλΆ(컀μ€ν
) νλ¬κ·ΈμΈ λ±λ‘/μμ μ€μ
νλ‘λμ
μ¬μ©μ²μμ 컀μ€ν
νλ¬κ·ΈμΈμ λ±λ‘νκ±°λ, νλ¬κ·ΈμΈ μμ (server/local/auto)μ μ€μ ν μ μλ κ³΅κ° APIλ₯Ό μ 곡ν©λλ€.
```ts
import {
configurePluginSource, // μμ μ€μ (server/local/auto)
registerExternalPlugin, // λ¨μΌ νλ¬κ·ΈμΈ λ±λ‘
registerExternalPluginsFromGlob // λ€κ±΄ λ±λ‘ (μ: import.meta.glob)
} from 'motiontext-renderer';
// 1) μμ μ€μ (μ ν)
configurePluginSource({
mode: 'auto', // 'server' | 'local' | 'auto'
serverBase: 'https://plugins.example.com',
localBase: '/plugins/' // λ²λ€/μ μ κ²½λ‘
});
// 2) νλ¬κ·ΈμΈ λ±λ‘ (λ¨μΌ)
// - module: { default: { name, version, animate... }, evalChannels? }
// - baseUrl: assets.getUrl()μ κΈ°μ€ URL
registerExternalPlugin({
name: 'myEffect',
version: '1.0.0',
module: await import('/plugins/myEffect@1.0.0/index.mjs'),
baseUrl: '/plugins/myEffect@1.0.0/'
});
// 3) νλ¬κ·ΈμΈ μΌκ΄ λ±λ‘ (Vite dev μμ)
const PLUGINS = import.meta.glob('/plugins/*/index.mjs');
await registerExternalPluginsFromGlob(PLUGINS);
```
### π¦ λ΄μ₯ CWI νλ¬κ·ΈμΈ μ리μ¦
Caption with Intention (CWI) νλ¬κ·ΈμΈλ€μ λ¨μ΄λ³ λ°ν κ°λμ λ°λ₯Έ λ€μν μ λλ©μ΄μ
μ μ 곡ν©λλ€:
- **cwi-color@1.0.0**: μμ λ³ν (ν°μ β νμλ³ μμ)
- **cwi-loud@1.0.0**: ν° μ리 ν¨κ³Ό (2.4λ°° νλ + μ§λ)
- **cwi-whisper@1.0.0**: μμμ ν¨κ³Ό (0.6λ°° μΆμ)
- **cwi-bouncing@1.0.0**: λ°μ΄μ± ν¨κ³Ό (1.15λ°° νλ + μν μμ§μ)
#### μ¬μ© μμ
```json
{
"definitions": {
"speakerPalette": {
"SPEAKER_01": "#4AA3FF",
"SPEAKER_02": "#FF4D4D",
"SPEAKER_03": "#FFD400"
}
},
"cues": [{
"root": {
"children": [{
"e_type": "text",
"text": "Hello",
"pluginChain": [
{
"name": "cwi-loud@1.0.0",
"params": {
"speaker": "SPEAKER_01",
"t0": 0.5,
"t1": 0.8
}
},
{
"name": "cwi-color@1.0.0",
"params": {
"speaker": "SPEAKER_01",
"t0": 0.5,
"t1": 0.8
}
}
]
}]
}
}]
}
```
#### Definitions μΉμ
μ ν΅ν μ΅μ ν
`definitions` μΉμ
μ μ¬μ©νλ©΄ κ³΅ν΅ λ°μ΄ν°λ₯Ό μ€μμμ κ΄λ¦¬ν μ μμ΅λλ€:
```json
{
"definitions": {
"speakerPalette": {
"SPEAKER_01": "#4AA3FF",
"SPEAKER_02": "#FF4D4D"
}
},
"cues": [{
"root": {
"children": [{
"pluginChain": [{
"name": "cwi-color@1.0.0",
"params": {
"speaker": "SPEAKER_01",
"palette": "definitions.speakerPalette"
}
}]
}]
}
}]
}
```
**μ£Όμ μ΄μ :**
- **μ€λ³΅ μ κ±°**: paletteλ₯Ό ν λ²λ§ μ μνκ³ μ°Έμ‘°λ‘ μ¬μ¬μ©
- **νμΌ ν¬κΈ° κ°μ**: κΈ°μ‘΄ λλΉ μ½ 75% ν¬κΈ° κ°μ (μ: 800KB β 206KB)
- **μ μ§λ³΄μ κ°μ **: palette μ€μ κ΄λ¦¬λ‘ μμ λ³κ²½ μ©μ΄
- **λ°νμ ν΄κ²°**: λ λλ¬κ° `"definitions.speakerPalette"` λ¬Έμμ΄μ μ€μ κ°μ²΄λ‘ μΉν
λͺ¨λ κ°μ
- server: `serverBase`μμ `plugins/<name@version>/manifest.json`μ λ°μ entry(index.mjs)λ₯Ό λ‘λν©λλ€. CDN/λ³λ νλ¬κ·ΈμΈ μλ²λ₯Ό μ°λ λ°°ν¬ νκ²½μ μ ν©ν©λλ€.
- local: λ²λ€ λλ μ μ κ²½λ‘μ ν¬ν¨λ νλ¬κ·ΈμΈμ μ§μ importν©λλ€. μλ² μμ΄λ λμνλ©°, μ±μ΄ μ 곡νλ μ μ μμ°μμ μ¦μ λ‘λ©ν λ μ ν©ν©λλ€.
- auto: μλ² μ°μ μλ ν μ€ν¨νλ©΄ λ‘μ»¬λ‘ ν΄λ°±ν©λλ€. κ°λ°/μμ° νκ²½μμ νΈλ¦¬ν©λλ€.
μΈμ μ΄λ€ λͺ¨λλ₯Ό μΈκΉ
- λ°°ν¬μ© CDN/μ μ© μλ²κ° μκ³ , νλ¬κ·ΈμΈ κ΅μ²΄Β·λ¬΄ν¨νΒ·λ²μ κ³ μ μ΄ νμ: server
- μ± λ²λ€μ νλ¬κ·ΈμΈμ ν¬ν¨νκ±°λ, νλ‘μ/μ€νλΌμΈ νκ²½: local
- κ°λ° μ€ μλ²κ° μμ λ/μμ λλ₯Ό λͺ¨λ κ³ λ €: auto
νλ¬κ·ΈμΈ λͺ¨λ κ·μ½(μμ½, v2.1)
```js
// index.mjs (μμ)
export default {
name: 'myEffect',
version: '1.0.0',
init(el, opts, ctx) {
// effectsRoot(el) νμλ§ μ‘°μ (μλλ°μ€)
},
animate(el, opts, ctx, duration) {
// 0..1 μ§νμ λ°λ seek ν¨μν λλ GSAP Timeline λ°ν
return (p) => {
el.style.opacity = String(Math.min(1, Math.max(0, p)));
};
},
cleanup(el) {}
};
```
μμ° URLκ³Ό baseUrl
- `registerExternalPlugin`μ `baseUrl`μ νλ¬κ·ΈμΈ λ΄λΆ `ctx.assets.getUrl('path')` ν΄μ κΈ°μ€μ΄ λ©λλ€.
- server λͺ¨λμμλ manifestμ entry/μμ° κ²½λ‘λ₯Ό κΈ°μ€μΌλ‘ μλ κ³μ°λ©λλ€.
- `registerExternalPluginsFromGlob`λ κΈ°λ³Έ νμλ‘ `.../<name>@<version>/index.mjs`λ₯Ό μΈμν΄ `baseUrl=.../<name>@<version>/`λ‘ μ€μ ν©λλ€. λ€λ₯Έ λλ ν°λ¦¬ ꡬ쑰λΌλ©΄ `parse` μ½λ°±μ μ λ¬ν΄ μ§μ μ§μ νμΈμ.
μλ² λͺ¨λμ© μ΅μ manifest μμ
```json
{
"name": "myEffect",
"version": "1.0.0",
"entry": "index.mjs"
}
```
μλ²λ `plugins/<name>@<version>/manifest.json`μ `index.mjs`(λ° νμ μμ°)λ₯Ό μ μ μΌλ‘ μλΉνλ©΄ λ©λλ€.
λ€κ±΄ λ±λ‘(λ²λ€λ¬λ³ ν)
- Vite: `import.meta.glob('/plugins/*/index.mjs')`λ₯Ό κΆμ₯ (λΉλκΈ° λ‘λ λ§΅ μμ±)
- Webpack/κΈ°ν: μ μ import ν `registerExternalPlugin`μ λ°λ³΅ νΈμΆνκ±°λ, λμ import κ°λ₯ν κ²½λ‘ κ·μΉμ μ¬μ©νμ¬ λ‘λ λ§΅μ ꡬμ±νμΈμ.
SSR/Next.js μ£Όμ
- ν΄λΌμ΄μΈνΈμμλ§ λ±λ‘νμΈμ. μ: `if (typeof window !== 'undefined') await registerExternalPluginsFromGlob(...)`.
νΈλ¬λΈμν
- βFailed to fetch dynamically imported moduleβ: κ²½λ‘/λλ©μΈ(μλ² λͺ¨λ), μ μ νμΌ μμΉ(local λͺ¨λ) νμΈ. μλ² λͺ¨λλΌλ©΄ CORS/κ²½λ‘(`plugins/<name@version>/...`)λ₯Ό μ κ²νμΈμ.
- βnot found @ versionβ: μλλ¦¬μ€ JSONμ `plugin.name`μ΄ `myEffect@1.0.0`μ²λΌ λ²μ κΉμ§ ν¬ν¨λμ΄μΌ ν©λλ€(νΉμ λμΌ name ν€λ‘ λ±λ‘).
- λ‘컬 κ²½λ‘ 404 (Vite dev): dev rootμ λ§λ κ²½λ‘μΈμ§ νμΈνκ³ , κ°λ₯νλ©΄ κΈλ‘(registrar)μ μ¬μ©νμΈμ.
## π§ κ°λ° κ°μ΄λ
### κ°λ° νκ²½ μ€μ
1. **μ μ₯μ ν΄λ‘ **
```bash
git clone https://github.com/teamKimtaerin/motiontext-renderer.git
cd motiontext-renderer
```
2. **μμ‘΄μ± μ€μΉ**
```bash
pnpm install
```
3. **AI νΈμ§κΈ° νκ²½ μ€μ (μ νμ¬ν)**
AI κΈ°λ° μλ§ νΈμ§ κΈ°λ₯μ μ¬μ©νλ €λ©΄ νκ²½λ³μλ₯Ό μ€μ νμΈμ:
```bash
# .env νμΌ μμ±
cp .env.example .env
# .env νμΌμ API ν€ μ€μ
ANTHROPIC_API_KEY=sk-ant-your-api-key-here
```
4. **κ°λ° μλ² μ€ν**
```bash
pnpm dev
# AI νΈμ§κΈ° μ¬μ© μ μΆκ°λ‘ νλ‘μ μλ² μ€ν
pnpm proxy:server
```
### κ°λ° λͺ
λ Ήμ΄
```bash
# κ°λ° λͺ¨λ (Vite κ°λ° μλ²)
pnpm dev
# λΉλ
pnpm build
# μ½λ νμ§ κ²μ¬
pnpm lint # ESLint μ€ν
pnpm lint:fix # ESLint μλ μμ
pnpm format # Prettier ν¬λ§·ν
pnpm format:check # ν¬λ§·ν
κ²μ¬
pnpm typecheck # TypeScript νμ
체ν¬
# μ 리
pnpm clean # dist ν΄λ μμ
```
### Dev νλ¬κ·ΈμΈ μμ μ€μ (M6.8)
λ°λͺ¨/κ°λ° νκ²½μμ νλ¬κ·ΈμΈ μμ€(μλ²/λ‘컬)λ₯Ό initμΌλ‘ μ€μ ν©λλ€.
- νκ²½λ³μλ‘ μ€μ (κΆμ₯):
```bash
# μλ² μ°μ , μ€ν¨ μ λ‘컬 ν΄λ°±(auto)
pnpm dev
# μλ²λ§ μ¬μ©
VITE_PLUGIN_MODE=server VITE_PLUGIN_ORIGIN=http://localhost:3300 pnpm dev
# λ‘컬 ν΄λλ§ μ¬μ©
VITE_PLUGIN_MODE=local VITE_PLUGIN_LOCAL_BASE=./demo/plugin-server/plugins/ pnpm dev
```
- μ½λμμ μ€μ (`demo/devPlugins.ts`):
```ts
import { configureDevPlugins } from '../src/loader/dev/DevPluginConfig';
configureDevPlugins({
mode: 'auto',
serverBase: 'http://localhost:3300',
localBase: './demo/plugin-server/plugins/',
});
```
## π¦ λ²μ κ΄λ¦¬ λ° λ°°ν¬ κ°μ΄λ
μ΄ νλ‘μ νΈλ **Changesets**λ₯Ό μ¬μ©νμ¬ Semantic Versioningμ μλνν©λλ€.
### π οΈ κΈ°λ₯ κ°λ° μ μν¬νλ‘μ°
1. **μ λΈλμΉ μμ± λ° μμ
**
```bash
git checkout -b feature/μκΈ°λ₯
# μ½λ μμ
...
```
2. **λ³κ²½μ¬ν κΈ°λ‘ (μ€μ!)**
```bash
pnpm changeset
```
μ€ννλ©΄ λνν ν둬ννΈκ° λνλ©λλ€:
- **ν¨μΉ(patch)**: λ²κ·Έ μμ (1.0.0 β 1.0.1)
- **λ§μ΄λ(minor)**: μ κΈ°λ₯ (1.0.0 β 1.1.0)
- **λ©μ΄μ (major)**: λΈλ μ΄νΉ 체μΈμ§ (1.0.0 β 2.0.0)
3. **μ»€λ° λ° PR μμ±**
```bash
git add .changeset/
git commit -m "feat: μλ‘μ΄ κΈ°λ₯ μΆκ°"
git push origin feature/μκΈ°λ₯
# GitHubμμ PR μμ±
```
4. **PR λ¨Έμ§**
- CI ν΅κ³Ό νμΈ
- μ½λ 리뷰 μλ£
- `main` λΈλμΉλ‘ λ¨Έμ§
### π€ μλ λ°°ν¬ νλ‘μΈμ€
#### 1λ¨κ³: μλ λ²μ PR μμ±
- `main` λΈλμΉμ pushλλ©΄ **Changesets Bot**μ΄ λμ
- "Version Packages" PRμ΄ μλ μμ±λ©λλ€
- μ΄ PRμλ λ€μμ΄ ν¬ν¨λ©λλ€:
- `package.json` λ²μ μλ μ¦κ°
- `CHANGELOG.md` μλ μ
λ°μ΄νΈ
- λμ λ λͺ¨λ λ³κ²½μ¬ν μ 리
#### 2λ¨κ³: NPM μλ λ°°ν¬
- **Version Packages** PRμ λ¨Έμ§νλ©΄:
- GitHub Actionsκ° μλ μ€ν
- νμ§ κ²μ¬ (lint, typecheck, build) μν
- NPM Registryμ μλ λ°°ν¬
- Git νκ·Έ μλ μμ± (μ: `v1.2.0`)
### π λ°°ν¬ μν νμΈ
```bash
# νμ¬ λ°°ν¬λ λ²μ νμΈ
npm info motiontext-renderer
# λ‘컬 λ²μ νμΈ
pnpm version
```
### π λ²μ νμ€ν 리 μμ
```
v0.1.0 β feat: μ΄κΈ° λ λλ¬ κ΅¬ν
v0.1.1 β fix: νμ
μ μ μ€λ₯ μμ
v0.2.0 β feat: νλ¬κ·ΈμΈ μμ€ν
μΆκ°
v0.2.1 β fix: λ©λͺ¨λ¦¬ λμ ν΄κ²°
v1.0.0 β feat!: API μ¬μ€κ³ (Breaking Change)
```
## ποΈ CI/CD νμ΄νλΌμΈ
### PR κ²μ¦ (.github/workflows/ci.yml)
λͺ¨λ Pull Requestμ λν΄ λ€μμ μλ κ²μ¬:
- β
ESLint κ·μΉ μ€μ
- β
Prettier ν¬λ§·ν
- β
TypeScript νμ
체ν¬
- β
λΉλ μ±κ³΅ μ¬λΆ
### μλ λ°°ν¬ (.github/workflows/release.yml)
`main` λΈλμΉ push μ μλ μ€ν:
1. νμ§ κ²μ¬ ν΅κ³Ό
2. νλ‘λμ
λΉλ μμ±
3. Changesetsλ‘ λ²μ κ΄λ¦¬
4. NPM λ°°ν¬ (NPM_TOKEN νμ)
5. GitHub Release μμ±
### π νμ GitHub Secrets
리ν¬μ§ν 리 Settings β Secretsμμ μ€μ :
```
NPM_TOKEN=npm_xxxxxxxxxxxxxxx
```
NPM ν ν° μμ± λ°©λ²:
1. [npmjs.com](https://npmjs.com) λ‘κ·ΈμΈ
2. Profile β Access Tokens
3. "Generate New Token" β "Automation" μ ν
4. μμ±λ ν ν°μ GitHub Secretsμ μΆκ°
## π― λ°°ν¬ μλλ¦¬μ€ μμ
### μλλ¦¬μ€ 1: λ²κ·Έ μμ
```bash
# 1. λΈλμΉ μμ± λ° μμ
git checkout -b fix/memory-leak
# μ½λ μμ ...
# 2. λ³κ²½μ¬ν κΈ°λ‘
pnpm changeset
# β patch μ ν
# β "λ©λͺ¨λ¦¬ λμ ν΄κ²°" μ€λͺ
μ
λ ₯
# 3. μ»€λ° λ° PR
git add .
git commit -m "fix: λ©λͺ¨λ¦¬ λμ ν΄κ²°"
git push
# 4. PR λ¨Έμ§ ν μλμΌλ‘ v1.0.1λ‘ λ°°ν¬
```
### μλλ¦¬μ€ 2: μ κΈ°λ₯ μΆκ°
```bash
# 1. κΈ°λ₯ κ°λ°
git checkout -b feature/plugin-system
# μ½λ μμ±...
# 2. λ³κ²½μ¬ν κΈ°λ‘
pnpm changeset
# β minor μ ν
# β "νλ¬κ·ΈμΈ μμ€ν
μΆκ°" μ€λͺ
# 3. PR λ¨Έμ§ ν μλμΌλ‘ v1.1.0μΌλ‘ λ°°ν¬
```
### μλλ¦¬μ€ 3: κΈ΄κΈ μμ
```bash
# hotfix λΈλμΉμμ μμ
git checkout -b hotfix/critical-bug
pnpm changeset # patch μ ν
# PR λ¨Έμ§ μ¦μ ν¨μΉ λ²μ λ°°ν¬
```
## π νλ‘μ νΈ κ΅¬μ‘°
```
motiontext-renderer/
βββ src/ # μμ€ μ½λ
β βββ index.ts # λ©μΈ μ§μ
μ
β βββ core/ # ν΅μ¬ λ λλ§ μμ§
β β βββ renderer.ts # λ λλ¬ ν΄λμ€
β βββ types/ # TypeScript νμ
μ μ
β βββ index.ts # κ³΅μ© νμ
λͺ¨μ
βββ dist/ # λΉλ κ²°κ³Όλ¬Ό (μλ μμ±)
βββ .changeset/ # λ²μ κ΄λ¦¬ μ€μ
βββ .github/workflows/ # CI/CD νμ΄νλΌμΈ
βββ package.json # νλ‘μ νΈ μ€μ
βββ tsconfig.json # TypeScript μ€μ
βββ vite.config.ts # Vite λΉλ μ€μ
βββ README.md # μ΄ νμΌ
```
## π€ κΈ°μ¬νκΈ°
1. Fork the Project
2. Create Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Make your changes
4. Record changeset (`pnpm changeset`)
5. Commit Changes (`git commit -m 'Add some AmazingFeature'`)
6. Push to Branch (`git push origin feature/AmazingFeature`)
7. Open a Pull Request
## π λΌμ΄μ μ€
MIT License - μμΈν λ΄μ©μ [LICENSE](LICENSE) νμΌμ μ°Έμ‘°νμΈμ.
## π λ§ν¬
- **GitHub**: https://github.com/teamKimtaerin/motiontext-renderer
- **NPM**: https://npmjs.com/package/motiontext-renderer
- **Issues**: https://github.com/teamKimtaerin/motiontext-renderer/issues
## π μ§μ
λ¬Έμμ¬νμ΄λ λ²κ·Έ 리ν¬νΈλ [GitHub Issues](https://github.com/teamKimtaerin/motiontext-renderer/issues)λ₯Ό μ΄μ©ν΄ μ£ΌμΈμ.
Made with β€οΈ by Team Kimtaerin