reborn-caption-kit 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/install.mjs +1 -1
- package/package.json +38 -38
- package/payload/skills/caption.mjs +75 -3
package/install.mjs
CHANGED
|
@@ -189,7 +189,7 @@ function 안내스킬심기(문) {
|
|
|
189
189
|
/* ── 안내 스킬 심기 끝 ─────────────────────────────────────────────────── */
|
|
190
190
|
|
|
191
191
|
/* ── 본 실행 ─────────────────────────────────────────────────────────── */
|
|
192
|
-
console.log(`${c.b}자막킷${c[0]} ${c.d}—
|
|
192
|
+
console.log(`${c.b}자막킷${c[0]} ${c.d}— 목소리를 듣고 낱말 단위 자막을 영상에 굽습니다. 전사가 로컬에서 돌아 비용이 0원입니다${c[0]}`);
|
|
193
193
|
|
|
194
194
|
const 대상 = 여기바로 ? process.cwd() : resolve(process.cwd(), "claude-caption-kit");
|
|
195
195
|
|
package/package.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "reborn-caption-kit",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "목소리를 듣고 낱말 단위로 튀어나오는 자막을 영상에 굽습니다 — 로컬에서 돌아 API 비용이 0원입니다",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"claude",
|
|
7
|
-
"claude-code",
|
|
8
|
-
"subtitle",
|
|
9
|
-
"caption",
|
|
10
|
-
"whisper",
|
|
11
|
-
"ffmpeg",
|
|
12
|
-
"reels",
|
|
13
|
-
"shorts",
|
|
14
|
-
"karaoke"
|
|
15
|
-
],
|
|
16
|
-
"homepage": "https://rebornlabs.kr/claudekit",
|
|
17
|
-
"bugs": {
|
|
18
|
-
"url": "https://mobility.rebornlabs.kr/cs"
|
|
19
|
-
},
|
|
20
|
-
"license": "SEE LICENSE IN CLAUDE.md",
|
|
21
|
-
"author": "REBORN LABS",
|
|
22
|
-
"type": "module",
|
|
23
|
-
"bin": {
|
|
24
|
-
"reborn-caption-kit": "install.mjs"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"install.mjs",
|
|
28
|
-
"README.md",
|
|
29
|
-
"안내스킬.md",
|
|
30
|
-
"payload"
|
|
31
|
-
],
|
|
32
|
-
"engines": {
|
|
33
|
-
"node": ">=18"
|
|
34
|
-
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"test": "node install.mjs --self-test"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "reborn-caption-kit",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "목소리를 듣고 낱말 단위로 튀어나오는 자막을 영상에 굽습니다 — 로컬에서 돌아 API 비용이 0원입니다",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"subtitle",
|
|
9
|
+
"caption",
|
|
10
|
+
"whisper",
|
|
11
|
+
"ffmpeg",
|
|
12
|
+
"reels",
|
|
13
|
+
"shorts",
|
|
14
|
+
"karaoke"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://rebornlabs.kr/claudekit",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://mobility.rebornlabs.kr/cs"
|
|
19
|
+
},
|
|
20
|
+
"license": "SEE LICENSE IN CLAUDE.md",
|
|
21
|
+
"author": "REBORN LABS",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"reborn-caption-kit": "install.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"install.mjs",
|
|
28
|
+
"README.md",
|
|
29
|
+
"안내스킬.md",
|
|
30
|
+
"payload"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node install.mjs --self-test"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -63,7 +63,33 @@ export function ass탈출(글) {
|
|
|
63
63
|
* 낱말 목록을 **묶음(구절)** 으로 자른다.
|
|
64
64
|
* ★한 낱말씩 홀로 띄우면 화면이 빈다. 낱말을 2~4개씩 쌓아 두고 **지금 말하는 낱말만** 강조한다.
|
|
65
65
|
*/
|
|
66
|
+
/**
|
|
67
|
+
* 전사기는 구두점을 **다음 낱말 앞에** 붙여 줄 때가 있다(「,목소리에」).
|
|
68
|
+
* 그대로 두면 자막 낱말이 쉼표로 시작해 눈에 걸린다 — 앞 낱말 꼬리로 옮긴다.
|
|
69
|
+
* ★구두점만으로 이뤄진 낱말도 앞에 합친다. 홀로 뜨면 화면에 점 하나가 깜빡인다.
|
|
70
|
+
* ★전각·특수 부호까지 잡으려고 「글자도 숫자도 아닌 접두」로 본다. 여는 괄호·따옴표는 남긴다.
|
|
71
|
+
*/
|
|
72
|
+
export function 구두점정리(낱말들) {
|
|
73
|
+
const 밖 = [];
|
|
74
|
+
for (const w of 낱말들) {
|
|
75
|
+
const 글 = String(w.글 == null ? "" : w.글).trim();
|
|
76
|
+
if (!글) continue;
|
|
77
|
+
const m = 글.match(/^([^\p{L}\p{N}\p{Ps}\p{Pi}"'(\[{]+)\s*(.*)$/u);
|
|
78
|
+
const 앞 = 밖[밖.length - 1];
|
|
79
|
+
if (m && 앞) {
|
|
80
|
+
앞.글 += m[1];
|
|
81
|
+
앞.끝 = Math.max(앞.끝, w.시작); // 꼬리를 받았으니 앞 낱말이 그때까지 머문다
|
|
82
|
+
if (!m[2]) continue; // 구두점뿐이면 낱말을 새로 만들지 않는다
|
|
83
|
+
밖.push({ ...w, 글: m[2] });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
밖.push({ ...w, 글: m ? (m[2] || m[1]) : 글 });
|
|
87
|
+
}
|
|
88
|
+
return 밖;
|
|
89
|
+
}
|
|
90
|
+
|
|
66
91
|
export function 묶기(낱말들, 옵션 = {}) {
|
|
92
|
+
낱말들 = 구두점정리(낱말들);
|
|
67
93
|
const 최대낱말 = 옵션.최대낱말 || 4;
|
|
68
94
|
const 최대초 = 옵션.최대초 || 2.4;
|
|
69
95
|
const 쉼 = 옵션.쉼 == null ? 0.45 : 옵션.쉼;
|
|
@@ -90,10 +116,19 @@ export function 묶기(낱말들, 옵션 = {}) {
|
|
|
90
116
|
*/
|
|
91
117
|
export function 대사줄(묶음들, 꼴) {
|
|
92
118
|
const 줄 = [];
|
|
93
|
-
for (
|
|
119
|
+
for (let m = 0; m < 묶음들.length; m++) {
|
|
120
|
+
const 묶음 = 묶음들[m];
|
|
121
|
+
/**
|
|
122
|
+
* ★★묶음의 마지막 줄은 **다음 묶음이 시작하기 전에** 끝나야 한다.
|
|
123
|
+
* 전사기가 주는 낱말 끝은 다음 낱말 시작보다 늦을 때가 있다(겹친다).
|
|
124
|
+
* 그대로 두면 앞 묶음 마지막 줄과 새 묶음 첫 줄이 **같은 자리에 겹쳐 그려져**
|
|
125
|
+
* 글자가 서로 뭉개진다 — 화면에 대놓고 보이는 사고다.
|
|
126
|
+
*/
|
|
127
|
+
const 다음시작 = m + 1 < 묶음들.length ? 묶음들[m + 1][0].시작 : Infinity;
|
|
94
128
|
for (let i = 0; i < 묶음.length; i++) {
|
|
95
129
|
const 시작 = 묶음[i].시작;
|
|
96
|
-
|
|
130
|
+
let 끝 = i + 1 < 묶음.length ? 묶음[i + 1].시작 : 묶음[i].끝;
|
|
131
|
+
if (끝 > 다음시작) 끝 = 다음시작;
|
|
97
132
|
if (!(끝 > 시작)) continue;
|
|
98
133
|
const 조각 = [];
|
|
99
134
|
for (let j = 0; j <= i; j++) {
|
|
@@ -101,7 +136,13 @@ export function 대사줄(묶음들, 꼴) {
|
|
|
101
136
|
if (j === i) 조각.push("{\\c&H" + 꼴.강조 + "&\\fscx112\\fscy112}" + t + "{\\c&H" + 꼴.글자 + "&\\fscx100\\fscy100}");
|
|
102
137
|
else 조각.push(t);
|
|
103
138
|
}
|
|
104
|
-
|
|
139
|
+
/**
|
|
140
|
+
* ★★필드는 정확히 9개다 — Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Text.
|
|
141
|
+
* 쉼표를 하나라도 더 찍으면 넘친 쉼표가 **Text 의 첫 글자가 되어 화면에 그려진다.**
|
|
142
|
+
* 2026-09-17 에 실제로 모든 자막이 「,영상」처럼 쉼표로 시작했고,
|
|
143
|
+
* .ass 파일을 눈으로 봐서는 안 보였다(파일에는 없고 화면에만 있었다).
|
|
144
|
+
*/
|
|
145
|
+
줄.push("Dialogue: 0," + ass시각(시작) + "," + ass시각(끝) + ",말,,0,0,0," + 조각.join(" "));
|
|
105
146
|
}
|
|
106
147
|
}
|
|
107
148
|
return 줄;
|
|
@@ -158,6 +199,37 @@ if (직접실행 && 깃발("--self-test")) {
|
|
|
158
199
|
T("★거꾸로 된 시각은 버린다", 대사줄([[w("x", 1, .5)]], 꼴).length === 0);
|
|
159
200
|
// ★역시험 — 검사기가 늘 초록이면 검사기가 고장난 것이다
|
|
160
201
|
T("★강조 없는 줄은 안 만든다", !대사줄([[]], 꼴).length);
|
|
202
|
+
/* ★★묶음 경계 겹침 — 실제로 화면에서 글자가 뭉개진 사고(2026-09-17).
|
|
203
|
+
전사기가 주는 낱말 끝이 다음 낱말 시작보다 늦을 때가 있다. */
|
|
204
|
+
const 겹 = 대사줄([[w("가", 0, .9)], [w("나", .5, 1.2)]], 꼴);
|
|
205
|
+
const 끝값 = (줄) => (줄.match(/,(\d:\d\d:\d\d\.\d\d),말/g) || [])[0];
|
|
206
|
+
T("★앞 묶음 마지막 줄이 다음 묶음 시작을 넘지 않는다", 겹[0].includes("0:00:00.50"));
|
|
207
|
+
T("★겹침을 잘라도 줄이 사라지진 않는다", 겹.length === 2);
|
|
208
|
+
// ★완전히 삼켜지는 줄(시작 ≥ 다음 묶음 시작)은 아예 만들지 않는다
|
|
209
|
+
T("★삼켜진 줄은 버린다", 대사줄([[w("가", 1.0, 1.9)], [w("나", .8, 1.5)]], 꼴).length === 1);
|
|
210
|
+
/* ★★ASS Dialogue 필드는 9개다. 하나라도 넘치면 넘친 쉼표가 **글자로 그려진다**
|
|
211
|
+
— 2026-09-17 모든 자막이 「,영상」처럼 쉼표로 시작한 사고. */
|
|
212
|
+
T("★Dialogue 씩 앞 8필드가 정확히 9개로 나뉘다", (() => {
|
|
213
|
+
const l = 대사줄([[w("가", 0, .4)]], 꼴)[0];
|
|
214
|
+
const 머리 = l.slice("Dialogue: ".length);
|
|
215
|
+
const 칸 = 머리.split(",");
|
|
216
|
+
return 칸.length >= 9 && 칸[3] === "말" && 칸[4] === "" && 칸[5] === "0" && 칸[6] === "0" && 칸[7] === "0" && 칸[8].startsWith("{");
|
|
217
|
+
})());
|
|
218
|
+
T("★Text 가 쉼표로 시작하지 않는다", (() => {
|
|
219
|
+
const l = 대사줄([[w("가", 0, .4)]], 꼴)[0];
|
|
220
|
+
return !l.slice("Dialogue: ".length).split(",").slice(8).join(",").startsWith(",");
|
|
221
|
+
})());
|
|
222
|
+
/* ★구두점 정리 — 낱말이 쉼표로 시작하면 자막에서 눈에 걸린다 */
|
|
223
|
+
T("★앞에 붙은 쉼표를 앞 낱말 꼬리로 옮긴다", (() => {
|
|
224
|
+
const r = 구두점정리([w("자막이", 0, .4), w(",목소리에", .4, .9)]);
|
|
225
|
+
return r.length === 2 && r[0].글 === "자막이," && r[1].글 === "목소리에";
|
|
226
|
+
})());
|
|
227
|
+
T("★구두점뿐인 낱말은 새로 만들지 않는다", (() => {
|
|
228
|
+
const r = 구두점정리([w("가", 0, .4), w(".", .4, .5), w("나", .5, .9)]);
|
|
229
|
+
return r.length === 2 && r[0].글 === "가.";
|
|
230
|
+
})());
|
|
231
|
+
T("★맨 앞이 구두점이면 그것만 남기지 버리지 않는다", 구두점정리([w(",", 0, .2)]).length === 1);
|
|
232
|
+
T("★빈 낱말은 걸러낸다", 구두점정리([w(" ", 0, .2), w("가", .2, .4)]).length === 1);
|
|
161
233
|
for (const [n, ok] of 결과) console.log(" " + (ok ? c.g + "✓" : c.r + "✗") + c[0] + " " + n);
|
|
162
234
|
const 실패 = 결과.filter(([, ok]) => !ok).length;
|
|
163
235
|
console.log(실패 ? "\n" + c.r + "✗ 자기시험 " + 실패 + "건 실패" + c[0] : "\n" + c.g + "✓ 자기시험 " + 결과.length + "건 통과" + c[0]);
|