hyper-animator-codex 0.5.0 → 0.7.0

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.
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ const TRAILING_PUNCTUATION = /[,。!?;:,.!?;:、]+$/u;
6
+
7
+ function requireValue(args, index, flag) {
8
+ const value = args[index + 1];
9
+ if (!value || value.startsWith("--")) {
10
+ throw new Error(`${flag} requires a value`);
11
+ }
12
+ return value;
13
+ }
14
+
15
+ function parseArgs(args) {
16
+ const parsed = {
17
+ narration: undefined,
18
+ output_dir: join(process.cwd(), "hyper-animator-output", "subtitles"),
19
+ style: "default",
20
+ };
21
+
22
+ for (let index = 0; index < args.length; index += 1) {
23
+ const arg = args[index];
24
+ if (arg === "--narration") {
25
+ parsed.narration = requireValue(args, index, arg);
26
+ index += 1;
27
+ } else if (arg === "--output-dir") {
28
+ parsed.output_dir = requireValue(args, index, arg);
29
+ index += 1;
30
+ } else if (arg === "--style") {
31
+ parsed.style = requireValue(args, index, arg);
32
+ index += 1;
33
+ } else if (arg === "--help" || arg === "-h") {
34
+ parsed.help = true;
35
+ } else {
36
+ throw new Error(`Unknown option: ${arg}`);
37
+ }
38
+ }
39
+
40
+ return parsed;
41
+ }
42
+
43
+ function validateNarration(narration) {
44
+ if (!narration || typeof narration !== "object" || !Array.isArray(narration.scenes)) {
45
+ throw new Error("narration JSON must include scenes[]");
46
+ }
47
+
48
+ narration.scenes.forEach((scene, index) => {
49
+ if (typeof scene?.narration !== "string" || !scene.narration.trim()) {
50
+ throw new Error(`scenes[${index}].narration is required`);
51
+ }
52
+
53
+ if (scene.duration_estimate !== undefined && (!Number.isFinite(Number(scene.duration_estimate)) || Number(scene.duration_estimate) <= 0)) {
54
+ throw new Error(`scenes[${index}].duration_estimate must be a positive number`);
55
+ }
56
+ });
57
+ }
58
+
59
+ function cleanCueText(text) {
60
+ return text.trim().replace(TRAILING_PUNCTUATION, "").trim();
61
+ }
62
+
63
+ function splitNarration(text) {
64
+ return text
65
+ .split(/[\r\n]+|(?<=[,。!?;:,.!?;:、])/u)
66
+ .map(cleanCueText)
67
+ .filter(Boolean);
68
+ }
69
+
70
+ function roundTime(value) {
71
+ return Number(value.toFixed(3));
72
+ }
73
+
74
+ function buildCues(narration) {
75
+ const cues = [];
76
+ let cursor = 0;
77
+
78
+ narration.scenes.forEach((scene, sceneIndex) => {
79
+ const parts = splitNarration(scene.narration);
80
+ const duration = Number(scene.duration_estimate) || Math.max(parts.length * 2, 2);
81
+ const cueDuration = duration / Math.max(parts.length, 1);
82
+
83
+ parts.forEach((text) => {
84
+ const start = cursor;
85
+ const end = cursor + cueDuration;
86
+ cues.push({
87
+ index: cues.length + 1,
88
+ scene: scene.scene ?? sceneIndex + 1,
89
+ start: roundTime(start),
90
+ end: roundTime(end),
91
+ text,
92
+ });
93
+ cursor = end;
94
+ });
95
+ });
96
+
97
+ return cues;
98
+ }
99
+
100
+ function formatSrtTime(seconds) {
101
+ const totalMs = Math.round(seconds * 1000);
102
+ const ms = totalMs % 1000;
103
+ const totalSeconds = Math.floor(totalMs / 1000);
104
+ const sec = totalSeconds % 60;
105
+ const totalMinutes = Math.floor(totalSeconds / 60);
106
+ const min = totalMinutes % 60;
107
+ const hour = Math.floor(totalMinutes / 60);
108
+
109
+ return `${String(hour).padStart(2, "0")}:${String(min).padStart(2, "0")}:${String(sec).padStart(2, "0")},${String(ms).padStart(3, "0")}`;
110
+ }
111
+
112
+ function renderSrt(cues) {
113
+ return `${cues.map((cue) => [
114
+ cue.index,
115
+ `${formatSrtTime(cue.start)} --> ${formatSrtTime(cue.end)}`,
116
+ cue.text,
117
+ ].join("\n")).join("\n\n")}\n`;
118
+ }
119
+
120
+ async function generateSubtitles(options) {
121
+ if (!options.narration) {
122
+ throw new Error("--narration is required");
123
+ }
124
+
125
+ const narration = JSON.parse(await readFile(options.narration, "utf8"));
126
+ validateNarration(narration);
127
+
128
+ const cues = buildCues(narration);
129
+ await mkdir(options.output_dir, { recursive: true });
130
+
131
+ const jsonPath = join(options.output_dir, "subtitles.json");
132
+ const srtPath = join(options.output_dir, "subtitles.srt");
133
+ const payload = {
134
+ ok: true,
135
+ style: options.style,
136
+ source: options.narration,
137
+ cues,
138
+ };
139
+
140
+ await writeFile(jsonPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
141
+ await writeFile(srtPath, renderSrt(cues), "utf8");
142
+
143
+ return {
144
+ ...payload,
145
+ json_path: jsonPath,
146
+ srt_path: srtPath,
147
+ };
148
+ }
149
+
150
+ function printHelp() {
151
+ console.log(`Usage:
152
+ node scripts/generate_subtitles.mjs --narration <file> [options]
153
+
154
+ Options:
155
+ --narration <file> Narration JSON file
156
+ --output-dir <dir> Output directory, default hyper-animator-output/subtitles
157
+ --style <name> Subtitle style label for downstream HTML/render steps
158
+ `);
159
+ }
160
+
161
+ async function main() {
162
+ try {
163
+ const options = parseArgs(process.argv.slice(2));
164
+ if (options.help) {
165
+ printHelp();
166
+ return;
167
+ }
168
+
169
+ console.log(JSON.stringify(await generateSubtitles(options), null, 2));
170
+ } catch (error) {
171
+ console.log(JSON.stringify({
172
+ ok: false,
173
+ error: {
174
+ message: error.message,
175
+ },
176
+ }, null, 2));
177
+ process.exitCode = 1;
178
+ }
179
+ }
180
+
181
+ main();