@glissade/cli 0.42.0-pre.0 → 0.42.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.
package/dist/cli.js CHANGED
@@ -92,7 +92,7 @@ const USAGE = `usage:
92
92
  gs describe [--out <api.json>] [--examples] snapshot THIS engine's describe() API manifest (stdout, or --out to a file) — the input to gs migrate
93
93
  gs migrate <baseline-api.json> [--json] [--check] diff a saved API manifest against the current engine: moved imports / removed / added / changed, with a suggested fix per breaking item (advisory; --check exits non-zero on any breaking change for CI gating)
94
94
  gs repin <scene-module> --golden <dir> [--name <p>] [--frames a,b,..] [--fps <n>] [--since <ref>] [--write] [--only a,b] [--heatmap <dir>] [--floor <ssim>] [--force] narration-aware golden reviewer: render current vs committed goldens, report perceptual delta + the re-narration cause, re-pin only frames you allow (default dry-run; --floor refuses a bigger-than-expected drop)
95
- gs localize <scene-module> --to <locale> [--from <locale>] [--write] [--keep-voice] [--json] fork a narration into a new locale (clone segment/pause structure, PRESERVING beat ids so .start() anchors survive) + stub messages.<locale>.json from the scene's t() ids, running the render path's parity + localize checks BEFORE any TTS. Default dry-run (exits non-zero on drift); --write emits <base>.<locale>.narration.json + messages.<locale>.json
95
+ gs localize <scene-module> --to <locale> [--from <locale>] [--write] [--strict] [--keep-voice] [--json] fork a narration into a new locale (clone segment/pause structure, PRESERVING beat ids so .start() anchors survive) + stub messages.<locale>.json from the scene's t() ids, running the render path's parity + localize checks BEFORE any TTS. Default dry-run (exits non-zero on drift); --write emits <base>.<locale>.narration.json + messages.<locale>.json (re-localize CARRIES existing translations over — never clobbers); --strict refuses to write on a preflight failure
96
96
  gs --version print the engine version
97
97
 
98
98
  render options:
@@ -367,10 +367,11 @@ async function main() {
367
367
  to,
368
368
  ...lf.get("from") ? { from: lf.get("from") } : {},
369
369
  ...lf.has("write") ? { write: true } : {},
370
- ...lf.has("keep-voice") ? { keepVoice: true } : {}
370
+ ...lf.has("keep-voice") ? { keepVoice: true } : {},
371
+ ...lf.has("strict") ? { strict: true } : {}
371
372
  });
372
373
  process.stdout.write(lf.has("json") ? `${JSON.stringify(report, null, 2)}\n` : `${formatLocalizeReport(report)}\n`);
373
- if (!lf.has("write") && report.preflight.issues.length > 0) process.exit(1);
374
+ if (report.preflight.issues.length > 0 && (!lf.has("write") || lf.has("strict"))) process.exit(1);
374
375
  } catch (err) {
375
376
  fail(err instanceof Error ? err.message : String(err));
376
377
  }
package/dist/localize.js CHANGED
@@ -32,10 +32,17 @@ var LocalizeError = class extends Error {
32
32
  * `keepVoice` is set.
33
33
  */
34
34
  function forkNarrationScript(base, opts = {}) {
35
+ const existingText = /* @__PURE__ */ new Map();
36
+ for (const el of opts.existing?.segments ?? []) if (!isPause(el) && el.text !== "") existingText.set(el.id, el.text);
35
37
  const segments = base.segments.map((el) => {
36
38
  if (isPause(el)) return { ...el };
37
- if (opts.keepVoice) return { ...el };
38
- const { voice: _voice, ...rest } = el;
39
+ const carried = existingText.get(el.id);
40
+ const withText = carried !== void 0 && carried !== el.text ? {
41
+ ...el,
42
+ text: carried
43
+ } : { ...el };
44
+ if (opts.keepVoice) return withText;
45
+ const { voice: _voice, ...rest } = withText;
39
46
  return rest;
40
47
  });
41
48
  const forked = {
@@ -155,7 +162,11 @@ async function harvestMessageIds(modulePath) {
155
162
  const tIds = new Set(getConsumedMessageIds());
156
163
  const ids = new Set(tIds);
157
164
  const doc = mod.timeline;
158
- for (const tr of doc.tracks ?? []) if (tr.type === "string") ids.add(nodeIdOf(tr.target));
165
+ for (const tr of doc.tracks ?? []) {
166
+ if (tr.type !== "string") continue;
167
+ if (new Set((tr.keys ?? []).map((k) => k.value)).size > 1) continue;
168
+ ids.add(nodeIdOf(tr.target));
169
+ }
159
170
  return {
160
171
  ids: [...ids],
161
172
  tIds,
@@ -187,11 +198,15 @@ async function localizeCommand(modulePath, opts) {
187
198
  baseScript = scriptFromTiming(JSON.parse(readFileSync(baseTimingPath, "utf8")));
188
199
  narrationSource = "timing";
189
200
  }
190
- const forked = baseScript ? forkNarrationScript(baseScript, { keepVoice: opts.keepVoice ?? false }) : void 0;
191
- const beatIds = baseScript ? scriptBeatIds(baseScript) : [];
192
201
  const narrationPath = moduleStemOf(modulePath) + `.${to}.narration.json`;
193
- let localeBeatIds = beatIds;
194
- if (existsSync(narrationPath)) localeBeatIds = scriptBeatIds(JSON.parse(readFileSync(narrationPath, "utf8")));
202
+ const existingLocale = baseScript && existsSync(narrationPath) ? JSON.parse(readFileSync(narrationPath, "utf8")) : void 0;
203
+ const forked = baseScript ? forkNarrationScript(baseScript, {
204
+ keepVoice: opts.keepVoice ?? false,
205
+ ...existingLocale ? { existing: existingLocale } : {}
206
+ }) : void 0;
207
+ const beatIds = baseScript ? scriptBeatIds(baseScript) : [];
208
+ const localeBeatIds = existingLocale ? scriptBeatIds(existingLocale) : beatIds;
209
+ const carriedSegments = baseScript && forked ? forked.segments.filter((el, i) => !isPause(el) && el.text !== baseScript.segments[i].text).length : 0;
195
210
  const base = opts.from ? loadMessageTable(modulePath, opts.from) : void 0;
196
211
  const messagesPath = messagesFileFor(modulePath, to);
197
212
  const existingTable = existsSync(messagesPath) ? JSON.parse(readFileSync(messagesPath, "utf8")) : loadMessageTable(modulePath, to);
@@ -214,20 +229,25 @@ async function localizeCommand(modulePath, opts) {
214
229
  consumedIds: tIds
215
230
  });
216
231
  const wrote = [];
217
- if (opts.write) {
232
+ const refusedWrite = !!(opts.write && opts.strict && !preflight.ok);
233
+ if (opts.write && !refusedWrite) {
218
234
  if (forked) {
219
235
  writeFileSync(narrationPath, JSON.stringify(forked, null, 2) + "\n");
220
236
  wrote.push(narrationPath);
221
237
  }
222
- writeFileSync(messagesPath, JSON.stringify(stubTable, null, 2) + "\n");
223
- wrote.push(messagesPath);
238
+ if (messageIds.length > 0) {
239
+ writeFileSync(messagesPath, JSON.stringify(stubTable, null, 2) + "\n");
240
+ wrote.push(messagesPath);
241
+ }
224
242
  }
225
243
  return {
226
244
  locale: to,
227
245
  messageIds: messageIds.sort(),
228
246
  beatIds,
247
+ carriedSegments,
229
248
  preflight,
230
249
  wrote,
250
+ refusedWrite,
231
251
  narrationPath,
232
252
  messagesPath,
233
253
  narrationSource
@@ -237,15 +257,16 @@ async function localizeCommand(modulePath, opts) {
237
257
  function formatLocalizeReport(r) {
238
258
  const lines = [];
239
259
  lines.push(`gs localize → ${r.locale}`);
240
- lines.push(` narration: ${r.narrationSource === "none" ? "none found" : `${r.beatIds.length} beat(s) from the ${r.narrationSource} (ids preserved)`}`);
241
- lines.push(` messages: ${r.messageIds.length} id(s) harvested, ${r.preflight.untranslated} still untranslated`);
260
+ lines.push(` narration: ${r.narrationSource === "none" ? "none found" : `${r.beatIds.length} beat(s) from the ${r.narrationSource} (ids preserved)`}` + (r.carriedSegments > 0 ? `; ${r.carriedSegments} translation(s) carried over` : ""));
261
+ lines.push(r.messageIds.length === 0 ? " messages: none localizable (no t() ids / no single-cue string tracks) — narration only" : ` messages: ${r.messageIds.length} id(s) harvested, ${r.preflight.untranslated} still untranslated`);
242
262
  if (r.preflight.issues.length === 0) lines.push(" preflight: ✓ parity + localize clean (safe to translate + narrate)");
243
263
  else {
244
264
  lines.push(` preflight: ✗ ${r.preflight.issues.length} issue(s) — fix before narrating:`);
245
265
  for (const i of r.preflight.issues) lines.push(` [${i.kind}] ${i.message.replace(/\n/g, "\n ")}`);
246
266
  }
247
- if (r.wrote.length) lines.push(` wrote: ${r.wrote.join(", ")}`);
248
- else lines.push(` (dry run — re-run with --write to emit ${r.narrationPath} + ${r.messagesPath})`);
267
+ if (r.refusedWrite) lines.push(" --strict: refused to write (preflight failed); fix the drift above, then re-run");
268
+ else if (r.wrote.length) lines.push(` wrote: ${r.wrote.join(", ")}`);
269
+ else lines.push(` (dry run — re-run with --write to emit ${r.narrationPath}${r.messageIds.length ? ` + ${r.messagesPath}` : ""})`);
249
270
  return lines.join("\n");
250
271
  }
251
272
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/cli",
3
- "version": "0.42.0-pre.0",
3
+ "version": "0.42.0",
4
4
  "description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -28,15 +28,15 @@
28
28
  "@napi-rs/canvas": "^0.1.65",
29
29
  "esbuild": "0.28.0",
30
30
  "jiti": "^2.4.2",
31
- "@glissade/backend-skia": "0.42.0-pre.0",
32
- "@glissade/core": "0.42.0-pre.0",
33
- "@glissade/interact": "0.42.0-pre.0",
34
- "@glissade/lottie": "0.42.0-pre.0",
35
- "@glissade/narrate": "0.42.0-pre.0",
36
- "@glissade/player": "0.42.0-pre.0",
37
- "@glissade/scene": "0.42.0-pre.0",
38
- "@glissade/sfx": "0.42.0-pre.0",
39
- "@glissade/svg": "0.42.0-pre.0"
31
+ "@glissade/backend-skia": "0.42.0",
32
+ "@glissade/core": "0.42.0",
33
+ "@glissade/interact": "0.42.0",
34
+ "@glissade/lottie": "0.42.0",
35
+ "@glissade/narrate": "0.42.0",
36
+ "@glissade/player": "0.42.0",
37
+ "@glissade/scene": "0.42.0",
38
+ "@glissade/sfx": "0.42.0",
39
+ "@glissade/svg": "0.42.0"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",