@glissade/narrate 0.34.0 → 0.35.0-pre.1

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/index.d.ts CHANGED
@@ -198,6 +198,17 @@ interface NarrationAnchors {
198
198
  duration(id: string): number;
199
199
  /** start + offset — a sub-beat inside a segment or pause window */
200
200
  at(id: string, offset?: number): number;
201
+ /**
202
+ * The absolute start (seconds) of a WORD inside a segment — the provider's
203
+ * per-word timestamp (`words[]` in the timing manifest), so a visual can land
204
+ * ON the spoken word instead of a whole segment early/late. `nth` (0-based)
205
+ * disambiguates a repeated word. Throws if the segment has no word timings
206
+ * (the provider didn't supply them) or the word/occurrence isn't found —
207
+ * fail-loud, so a stale ref can't silently drift to segment.start.
208
+ */
209
+ word(segId: string, word: string, nth?: number): number;
210
+ /** Like `word` but the word's END second (its `end` timestamp). */
211
+ wordEnd(segId: string, word: string, nth?: number): number;
201
212
  /**
202
213
  * Assert every id exists in the manifest — a build-time fast-fail that lists
203
214
  * ALL unknown ids at once (vs. discovering stale refs one render at a time
package/dist/index.js CHANGED
@@ -28,7 +28,8 @@ function narration(timing) {
28
28
  if (byId.has(s.id)) throw new NarrationError(`duplicate narration id '${s.id}'`);
29
29
  byId.set(s.id, {
30
30
  start: s.start,
31
- duration: s.duration
31
+ duration: s.duration,
32
+ ...s.words ? { words: s.words } : {}
32
33
  });
33
34
  }
34
35
  for (const p of timing.pauses ?? []) {
@@ -43,11 +44,26 @@ function narration(timing) {
43
44
  if (!b) throw new NarrationError(`no narration beat '${id}' (have: ${[...byId.keys()].join(", ")})`);
44
45
  return b;
45
46
  };
47
+ /** locate the nth (0-based) occurrence of `word` in a segment's word timings,
48
+ * matching on a punctuation/case-normalized token so 'busy' finds 'busy.' */
49
+ const findWord = (segId, word, nth) => {
50
+ const b = beat(segId);
51
+ if (!b.words || b.words.length === 0) throw new NarrationError(`narration beat '${segId}' has no word timings (the provider supplied none) — use .at('${segId}', offset) instead`);
52
+ const norm = (w) => w.toLowerCase().replace(/[^\p{L}\p{N}']/gu, "");
53
+ const target = norm(word);
54
+ const matches = b.words.filter((w) => norm(w.word) === target);
55
+ if (matches.length === 0) throw new NarrationError(`narration word '${word}' not found in beat '${segId}' (words: ${b.words.map((w) => w.word).join(" ")})`);
56
+ const hit = matches[nth];
57
+ if (hit === void 0) throw new NarrationError(`narration word '${word}' occurrence ${nth} not found in beat '${segId}' (only ${matches.length} occurrence${matches.length > 1 ? "s" : ""})`);
58
+ return hit;
59
+ };
46
60
  const anchors = {
47
61
  start: (id) => beat(id).start,
48
62
  end: (id) => beat(id).start + beat(id).duration,
49
63
  duration: (id) => beat(id).duration,
50
64
  at: (id, offset = 0) => beat(id).start + offset,
65
+ word: (segId, w, nth = 0) => findWord(segId, w, nth).start,
66
+ wordEnd: (segId, w, nth = 0) => findWord(segId, w, nth).end,
51
67
  require: (ids) => {
52
68
  const missing = ids.filter((id) => !byId.has(id));
53
69
  if (missing.length > 0) throw new NarrationError(`narration references unknown id${missing.length > 1 ? "s" : ""} ${missing.map((m) => `'${m}'`).join(", ")} — have: ${[...byId.keys()].join(", ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/narrate",
3
- "version": "0.34.0",
3
+ "version": "0.35.0-pre.1",
4
4
  "description": "glissade narration + captions: TTS at prepare time (gs narrate), deterministic caching, narration-anchored timeline beats, and captions as plain tracks. Render stays offline.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -22,8 +22,8 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "@glissade/core": "0.34.0",
26
- "@glissade/scene": "0.34.0"
25
+ "@glissade/core": "0.35.0-pre.1",
26
+ "@glissade/scene": "0.35.0-pre.1"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "kokoro-js": "^1.2.0"