Web-based animated caption/subtitle renderer with plugin system
npm install 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);
`
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/μ λ°μ 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') ν΄μ κΈ°μ€μ΄ λ©λλ€.registerExternalPluginsFromGlob
- server λͺ¨λμμλ manifestμ entry/μμ° κ²½λ‘λ₯Ό κΈ°μ€μΌλ‘ μλ κ³μ°λ©λλ€.
- λ κΈ°λ³Έ νμλ‘ .../λ₯Ό μΈμν΄ baseUrl=.../λ‘ μ€μ ν©λλ€. λ€λ₯Έ λλ ν°λ¦¬ ꡬ쑰λΌλ©΄ parse μ½λ°±μ μ λ¬ν΄ μ§μ μ§μ νμΈμ.
μλ² λͺ¨λμ© μ΅μ manifest μμ
`json`
{
"name": "myEffect",
"version": "1.0.0",
"entry": "index.mjs"
}plugins/
μλ²λ μ index.mjs(λ° νμ μμ°)λ₯Ό μ μ μΌλ‘ μλΉνλ©΄ λ©λλ€.
λ€κ±΄ λ±λ‘(λ²λ€λ¬λ³ ν)
- Vite: import.meta.glob('/plugins/*/index.mjs')λ₯Ό κΆμ₯ (λΉλκΈ° λ‘λ λ§΅ μμ±)registerExternalPlugin
- Webpack/κΈ°ν: μ μ import ν μ λ°λ³΅ νΈμΆνκ±°λ, λμ import κ°λ₯ν κ²½λ‘ κ·μΉμ μ¬μ©νμ¬ λ‘λ λ§΅μ ꡬμ±νμΈμ.
SSR/Next.js μ£Όμ
- ν΄λΌμ΄μΈνΈμμλ§ λ±λ‘νμΈμ. μ: if (typeof window !== 'undefined') await registerExternalPluginsFromGlob(...).
νΈλ¬λΈμν
- βFailed to fetch dynamically imported moduleβ: κ²½λ‘/λλ©μΈ(μλ² λͺ¨λ), μ μ νμΌ μμΉ(local λͺ¨λ) νμΈ. μλ² λͺ¨λλΌλ©΄ CORS/κ²½λ‘(plugins/)λ₯Ό μ κ²νμΈμ.plugin.name
- βnot found @ versionβ: μλλ¦¬μ€ JSONμ μ΄ 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
4. κ°λ° μλ² μ€ν
`bash
pnpm devAI νΈμ§κΈ° μ¬μ© μ μΆκ°λ‘ νλ‘μ μλ² μ€ν
pnpm proxy:server
`$3
`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 ν΄λ μμ
`$3
λ°λͺ¨/κ°λ° νκ²½μμ νλ¬κ·ΈμΈ μμ€(μλ²/λ‘컬)λ₯Ό 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μ μλνν©λλ€.
$3
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 λΈλμΉλ‘ λ¨Έμ§$3
#### 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)$3
`bash
νμ¬ λ°°ν¬λ λ²μ νμΈ
npm info motiontext-rendererλ‘컬 λ²μ νμΈ
pnpm version
`$3
`
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 νμ΄νλΌμΈ
$3
λͺ¨λ Pull Requestμ λν΄ λ€μμ μλ κ²μ¬:
- β
ESLint κ·μΉ μ€μ
- β
Prettier ν¬λ§·ν
- β
TypeScript νμ
체ν¬
- β
λΉλ μ±κ³΅ μ¬λΆ$3
main λΈλμΉ push μ μλ μ€ν:
1. νμ§ κ²μ¬ ν΅κ³Ό
2. νλ‘λμ
λΉλ μμ±
3. Changesetsλ‘ λ²μ κ΄λ¦¬
4. NPM λ°°ν¬ (NPM_TOKEN νμ)
5. GitHub Release μμ±$3
리ν¬μ§ν 리 Settings β Secretsμμ μ€μ :
`
NPM_TOKEN=npm_xxxxxxxxxxxxxxx
`NPM ν ν° μμ± λ°©λ²:
1. npmjs.com λ‘κ·ΈμΈ
2. Profile β Access Tokens
3. "Generate New Token" β "Automation" μ ν
4. μμ±λ ν ν°μ GitHub Secretsμ μΆκ°
---
π― λ°°ν¬ μλλ¦¬μ€ μμ
$3
`bash
1. λΈλμΉ μμ± λ° μμ
git checkout -b fix/memory-leak
μ½λ μμ ...
2. λ³κ²½μ¬ν κΈ°λ‘
pnpm changeset
β patch μ ν
β "λ©λͺ¨λ¦¬ λμ ν΄κ²°" μ€λͺ
μ
λ ₯
3. μ»€λ° λ° PR
git add .
git commit -m "fix: λ©λͺ¨λ¦¬ λμ ν΄κ²°"
git push4. PR λ¨Έμ§ ν μλμΌλ‘ v1.0.1λ‘ λ°°ν¬
`$3
`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`)---
MIT 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λ₯Ό μ΄μ©ν΄ μ£ΌμΈμ.
---
Made with β€οΈ by Team Kimtaerin