@hypit/hypit 0.1.13 → 0.2.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/README.md +37 -3
- package/bin/hypit.mjs +0 -2
- package/dist/public/browser-capture.d.ts +9 -3
- package/dist/public/caption.d.ts +2 -0
- package/dist/public/narrative.d.ts +2 -0
- package/dist/public/runtime-kit.d.ts +2 -2
- package/dist/public/speech.d.ts +2 -0
- package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
- package/package.json +2 -1
- package/packages/browser-capture/README.md +9 -2
- package/packages/browser-capture/package.json +5 -0
- package/packages/browser-capture/src/browser.ts +22 -8
- package/packages/browser-capture/src/index.ts +8 -2
- package/packages/caption/README.md +1 -1
- package/packages/caption/src/display.ts +1 -0
- package/packages/caption-fine/README.md +19 -5
- package/packages/caption-fine/package.json +1 -0
- package/packages/caption-fine/src/joined-box.ts +114 -0
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +99 -92
- package/packages/caption-fine/src/spacing.ts +8 -61
- package/packages/caption-fine-studio/src/index.ts +2 -4
- package/packages/cli/README.md +3 -2
- package/packages/cli/package.json +1 -0
- package/packages/cli/src/arguments.ts +2 -2
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +21 -9
- package/packages/cli/src/machine-view.ts +1 -1
- package/packages/cli/src/main.ts +2 -1
- package/packages/cli/src/oauth.ts +50 -7
- package/packages/cli/src/output.ts +1 -0
- package/packages/credential-store-file/README.md +60 -0
- package/packages/credential-store-file/package.json +21 -0
- package/packages/credential-store-file/src/activation.ts +29 -0
- package/packages/credential-store-file/src/index.ts +1 -0
- package/packages/credential-store-file/src/store.ts +89 -0
- package/packages/credential-store-os/src/store.ts +5 -1
- package/packages/fonts-open/src/surface.ts +8 -2
- package/packages/hyperframes/README.md +16 -2
- package/packages/hyperframes/src/browser-program.ts +6 -1
- package/packages/hyperframes/src/document.ts +31 -21
- package/packages/hyperframes/src/project.ts +11 -20
- package/packages/media-execution/README.md +7 -0
- package/packages/media-execution/src/execute.ts +5 -5
- package/packages/media-execution/src/index.ts +1 -1
- package/packages/media-execution/src/process-env.ts +20 -0
- package/packages/media-execution/src/surface.ts +83 -76
- package/packages/narrative/README.md +3 -1
- package/packages/narrative/src/schema.ts +1 -0
- package/packages/narrative/src/types.ts +2 -0
- package/packages/package-loader-node/README.md +10 -0
- package/packages/package-loader-node/src/index.ts +1 -0
- package/packages/package-loader-node/src/loader.ts +25 -5
- package/packages/package-loader-node/src/location.ts +11 -2
- package/packages/provider-hyperframes-local/README.md +81 -8
- package/packages/provider-hyperframes-local/package.json +13 -3
- package/packages/provider-hyperframes-local/src/activation.ts +20 -6
- package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
- package/packages/provider-hyperframes-local/src/browser.ts +118 -0
- package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
- package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
- package/packages/provider-hyperframes-local/src/capture-process.ts +29 -52
- package/packages/provider-hyperframes-local/src/capture-worker.ts +2 -0
- package/packages/provider-hyperframes-local/src/capture.ts +12 -5
- package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
- package/packages/provider-hyperframes-local/src/options.ts +2 -2
- package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
- package/packages/provider-hyperframes-local/src/process.ts +22 -0
- package/packages/provider-hyperframes-local/src/program.ts +25 -33
- package/packages/provider-hyperframes-local/src/provider.ts +4 -7
- package/packages/provider-hyperframes-local/src/render.ts +9 -4
- package/packages/provider-whisperx-local/README.md +26 -10
- package/packages/provider-whisperx-local/src/activation.ts +10 -1
- package/packages/provider-whisperx-local/src/program.ts +8 -4
- package/packages/runtime-host-node/README.md +5 -0
- package/packages/runtime-host-node/src/index.ts +8 -2
- package/packages/runtime-host-node/src/packages.ts +13 -7
- package/packages/runtime-kit/README.md +6 -2
- package/packages/runtime-kit/src/index.ts +2 -2
- package/packages/runtime-local/README.md +16 -1
- package/packages/runtime-local/src/config.ts +1 -1
- package/packages/runtime-local/src/credentials.ts +10 -5
- package/packages/runtime-local/src/host.ts +2 -0
- package/packages/runtime-local/src/index.ts +1 -0
- package/packages/runtime-local/src/programs.ts +53 -17
- package/packages/script/README.md +81 -34
- package/packages/script/bin/migrate-0.2.mjs +90 -0
- package/packages/script/src/edit.ts +28 -60
- package/packages/script/src/lexical.ts +59 -130
- package/packages/script/src/manifest.ts +11 -11
- package/packages/script/src/narrative.ts +4 -2
- package/packages/script/src/parser.ts +140 -193
- package/packages/script/src/types.ts +2 -3
- package/packages/studio/src/preview/runtime-shim.ts +17 -3
- package/packages/studio/src/ui/code.ts +1 -1
- package/packages/studio/src/ui/main.ts +2 -2
- package/packages/studio/src/ui/markers.ts +3 -3
- package/packages/studio/src/ui/selection.ts +1 -1
- package/packages/studio/src/ui/syntax.ts +42 -21
- package/packages/temporal-markup/README.md +1 -1
- package/packages/video-cli/README.md +14 -6
- package/packages/video-cli/package.json +1 -0
- package/packages/video-cli/src/capture.ts +17 -2
- package/packages/video-cli/src/creation.ts +4 -6
- package/packages/video-cli/src/distribution.ts +3 -10
- package/packages/video-cli/src/media.ts +10 -3
- package/packages/video-cli/src/version.ts +1 -1
- package/packages/whisperx/README.md +56 -3
- package/packages/whisperx/src/component.ts +2 -5
- package/packages/whisperx/src/evidence.ts +4 -3
- package/packages/whisperx/src/index.ts +1 -0
- package/packages/whisperx/src/manifest.ts +2 -3
- package/packages/whisperx/src/surface.ts +3 -6
- package/packages/whisperx/src/types.ts +9 -1
- package/packages/yt-dlp/README.md +10 -3
- package/packages/yt-dlp/package.json +5 -0
- package/packages/yt-dlp/src/download.ts +11 -29
- package/packages/yt-dlp/src/environment.ts +36 -0
- package/packages/yt-dlp/src/index.ts +1 -0
- package/services/whisperx/README.md +45 -12
- package/services/whisperx/src/hypit_whisperx_service/application.py +3 -0
- package/services/whisperx/src/hypit_whisperx_service/check.py +7 -0
- package/services/whisperx/src/hypit_whisperx_service/config.py +7 -0
- package/services/whisperx/src/hypit_whisperx_service/engine.py +21 -5
- package/services/whisperx/src/hypit_whisperx_service/models.py +131 -0
- package/services/whisperx/src/hypit_whisperx_service/prepare.py +21 -3
- package/services/whisperx/src/hypit_whisperx_service/resources.py +19 -3
- package/services/yt-dlp/README.md +14 -15
- package/services/yt-dlp/pyproject.toml +1 -1
- package/services/yt-dlp/uv.lock +513 -2
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
import { ScriptSyntaxError } from "./error.js";
|
|
3
3
|
import {
|
|
4
4
|
cleanProjection,
|
|
5
|
-
displayWordSurfaces,
|
|
6
5
|
joinProjection,
|
|
7
6
|
lexicalUnits,
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
analyzeProse,
|
|
8
|
+
splitDisplayPrefix,
|
|
10
9
|
} from "./lexical.js";
|
|
11
10
|
import type {
|
|
12
11
|
CaptionWordAttribute,
|
|
@@ -37,8 +36,6 @@ type MutableSegment = {
|
|
|
37
36
|
readonly tokenStart: number;
|
|
38
37
|
readonly sourceStart: number;
|
|
39
38
|
readonly contentStart: number;
|
|
40
|
-
lexicalRun: string;
|
|
41
|
-
readonly lexicalMarkers: Array<{ readonly position: number; readonly offset: number }>;
|
|
42
39
|
};
|
|
43
40
|
|
|
44
41
|
const SEGMENT_ID = /^[a-z][a-z0-9_-]{0,63}$/u;
|
|
@@ -49,7 +46,7 @@ const ATTRIBUTE_NAME = /^[a-z][a-z0-9_-]{0,63}$/u;
|
|
|
49
46
|
const ATTRIBUTE_VALUE = /^[A-Za-z0-9_.:-]+$/u;
|
|
50
47
|
|
|
51
48
|
/** A marker's structural position before its affinity picks one anchor. */
|
|
52
|
-
type RawMarkerBoundary = Omit<MarkerBoundary, "anchorId"
|
|
49
|
+
type RawMarkerBoundary = { -readonly [K in keyof Omit<MarkerBoundary, "anchorId">]: Omit<MarkerBoundary, "anchorId">[K] };
|
|
53
50
|
type RawEdge<T> = Omit<T, "boundary"> & { readonly boundary: RawMarkerBoundary };
|
|
54
51
|
type RawSelection = Omit<ParsedSelection, "startAnchorId" | "endAnchorId" | "open" | "close"> & {
|
|
55
52
|
readonly open: RawEdge<ParsedSelection["open"]>;
|
|
@@ -65,33 +62,15 @@ function normalizeWord(value: string): string {
|
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
function parseMarker(source: string, offset: number): Marker | undefined {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
65
|
+
const match = /^@\{(~)?(\/)?([a-z][a-z0-9_-]{0,63})([!~])?\}/u.exec(source.slice(offset));
|
|
66
|
+
if (!match) return undefined;
|
|
67
|
+
const [, left, close, id, suffix] = match;
|
|
68
|
+
if (close) {
|
|
69
|
+
if (left || suffix === "!") return undefined;
|
|
70
|
+
return { id: id!, kind: "close", affinity: suffix === "~" ? "right" : "left", length: match[0].length };
|
|
73
71
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return { id: closeLeft[1]!, kind: "close", affinity: "left", length: closeLeft[0].length };
|
|
77
|
-
}
|
|
78
|
-
const moment = new RegExp(String.raw`^(~)?@(${markerName})!`, "u").exec(rest);
|
|
79
|
-
if (moment) {
|
|
80
|
-
return {
|
|
81
|
-
id: moment[2]!,
|
|
82
|
-
kind: "moment",
|
|
83
|
-
affinity: moment[1] === "~" ? "left" : "right",
|
|
84
|
-
length: moment[0].length,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
const open = new RegExp(String.raw`^(~)?@(${markerName})(?![A-Za-z0-9_-])`, "u").exec(rest);
|
|
88
|
-
if (!open) return undefined;
|
|
89
|
-
return {
|
|
90
|
-
id: open[2]!,
|
|
91
|
-
kind: "open",
|
|
92
|
-
affinity: open[1] === "~" ? "left" : "right",
|
|
93
|
-
length: open[0].length,
|
|
94
|
-
};
|
|
72
|
+
if (suffix === "~") return undefined;
|
|
73
|
+
return { id: id!, kind: suffix === "!" ? "moment" : "open", affinity: left ? "left" : "right", length: match[0].length };
|
|
95
74
|
}
|
|
96
75
|
|
|
97
76
|
function segmentAnchorId(segmentId: string, edge: "start" | "end"): string {
|
|
@@ -142,8 +121,9 @@ export function parseScript(
|
|
|
142
121
|
|
|
143
122
|
const segments: ParsedSegment[] = [];
|
|
144
123
|
const tokens: ParsedToken[] = [];
|
|
145
|
-
const proseRanges: Array<{ start: number; end: number }> = [];
|
|
146
124
|
const captionRegions: ParsedCaptionRegion[] = [];
|
|
125
|
+
let precedingCaptionRegion: number | undefined;
|
|
126
|
+
let pendingCaptionText = "";
|
|
147
127
|
const selections = new Map<string, RawSelection>();
|
|
148
128
|
const moments = new Map<string, RawMoment>();
|
|
149
129
|
const captionBreaks: Array<{ readonly tokenIndex: number; readonly range: { readonly start: number; readonly end: number } }> = [];
|
|
@@ -157,33 +137,64 @@ export function parseScript(
|
|
|
157
137
|
let offset = 0;
|
|
158
138
|
let structuralPosition = 0;
|
|
159
139
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
140
|
+
// One prose run crosses annotations and comments. Only an explicit correspondence,
|
|
141
|
+
// speaker change or Segment edge ends it. Source positions stay local to parsing.
|
|
142
|
+
let runText = "";
|
|
143
|
+
let runCaption = "";
|
|
144
|
+
let runCapture = true;
|
|
145
|
+
let runStarts: number[] = [];
|
|
146
|
+
let runEnds: number[] = [];
|
|
147
|
+
let runBoundaries: Array<{ position: number; offset: number; kind: "marker" | "cue"; value: { tokenIndex: number } }> = [];
|
|
148
|
+
let runAttributes: Array<{ position: number; offset: number; end: number; attributes: readonly CaptionWordAttribute[] }> = [];
|
|
149
|
+
|
|
150
|
+
const boundary = (start: number): RawMarkerBoundary => {
|
|
151
|
+
const value = { tokenIndex: tokens.length, structuralPosition,
|
|
152
|
+
...(current ? { segmentId: current.id } : {}) };
|
|
153
|
+
if (current) runBoundaries.push({ position: runText.length, offset: start, kind: "marker", value });
|
|
154
|
+
return value;
|
|
155
|
+
};
|
|
165
156
|
|
|
166
|
-
const finishLexicalRun = ():
|
|
167
|
-
if (!current) return;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
157
|
+
const finishLexicalRun = (): ParsedCaptionRegion["marks"] => {
|
|
158
|
+
if (!current) return [];
|
|
159
|
+
const { units, editRanges: ranges } = analyzeProse(runText, runCapture && precedingCaptionRegion !== undefined);
|
|
160
|
+
const tokenStart = tokens.length;
|
|
161
|
+
for (const event of runBoundaries) {
|
|
162
|
+
if (ranges.some(range => range.start < event.position && event.position < range.end)) {
|
|
163
|
+
fail(event.kind === "marker" ? "SCRIPT_MARKER_TOKEN_BOUNDARY" : "SCRIPT_CAPTION_BREAK_TOKEN_BOUNDARY",
|
|
164
|
+
"A semantic marker or Cue break cannot split a word and its attached punctuation.", event.offset);
|
|
165
|
+
}
|
|
166
|
+
event.value.tokenIndex = tokenStart + units.filter(unit => unit.index + unit.text.length <= event.position).length;
|
|
167
|
+
if (event.kind === "cue" && event.value.tokenIndex === current.tokenStart) {
|
|
168
|
+
fail("SCRIPT_CAPTION_BREAK_EMPTY", "Caption Cue break must follow visible Script text.", event.offset);
|
|
178
169
|
}
|
|
179
170
|
}
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
if (units.length === 0 && runAttributes.length > 0) fail("SCRIPT_ATTRIBUTE_TARGET", "A spoken display attribute needs a complete word.", runAttributes[0]!.offset);
|
|
172
|
+
const marks = bindDisplayAttributes(runText, runAttributes, units);
|
|
173
|
+
for (const [ordinal, unit] of units.entries()) {
|
|
174
|
+
const index = tokens.length;
|
|
175
|
+
const segmentTokenIndex = index - current.tokenStart;
|
|
176
|
+
const range = ranges[ordinal]!;
|
|
177
|
+
const attachedAttributes = runAttributes.filter(attribute =>
|
|
178
|
+
attribute.position >= unit.index + unit.text.length && attribute.position <= range.end);
|
|
179
|
+
tokens.push({ id: tokenId(current.id, segmentTokenIndex), index, segmentId: current.id, segmentTokenIndex,
|
|
180
|
+
startAnchorId: tokenAnchorId(current.id, segmentTokenIndex, "start"),
|
|
181
|
+
endAnchorId: tokenAnchorId(current.id, segmentTokenIndex, "end"),
|
|
182
|
+
text: unit.text, normalized: normalizeWord(unit.text),
|
|
183
|
+
range: { start: runStarts[unit.index]!, end: runEnds[unit.index + unit.text.length - 1]! },
|
|
184
|
+
editRange: { start: runStarts[range.start]!, end: Math.max(runEnds[range.end - 1]!, ...attachedAttributes.map(attribute => sourceOffset + attribute.end)) },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (runText || runCaption) {
|
|
188
|
+
const range = { start: runStarts[0]!, end: runEnds.at(-1)! };
|
|
189
|
+
current.atoms.push({ kind: "text", speech: runText, caption: runCaption, tokenStart, tokenEndExclusive: tokens.length, range });
|
|
190
|
+
if (runCapture) addCaptionRegion(runCaption, current.id, tokenStart, tokens.length, "identity", range, marks);
|
|
191
|
+
}
|
|
192
|
+
runText = ""; runCaption = ""; runStarts = []; runEnds = []; runBoundaries = []; runAttributes = [];
|
|
193
|
+
return marks;
|
|
182
194
|
};
|
|
183
195
|
|
|
184
196
|
const addMarker = (marker: Marker, start: number): void => {
|
|
185
197
|
if (!TEMPORAL_ID.test(marker.id)) fail("SCRIPT_TEMPORAL_ID", `Invalid temporal id "${marker.id}".`, start);
|
|
186
|
-
if (current) current.lexicalMarkers.push({ position: current.lexicalRun.length, offset: start });
|
|
187
198
|
if (marker.kind === "moment") {
|
|
188
199
|
if (openSelections.has(marker.id) || selections.has(marker.id)) {
|
|
189
200
|
fail("SCRIPT_TEMPORAL_TYPE", `Temporal name "${marker.id}" cannot be both Selection and Moment.`, start);
|
|
@@ -194,7 +205,7 @@ export function parseScript(
|
|
|
194
205
|
moments.set(marker.id, {
|
|
195
206
|
id: marker.id,
|
|
196
207
|
affinity: marker.affinity,
|
|
197
|
-
boundary: boundary(),
|
|
208
|
+
boundary: boundary(start),
|
|
198
209
|
range: { start: sourceOffset + start, end: sourceOffset + start + marker.length },
|
|
199
210
|
});
|
|
200
211
|
return;
|
|
@@ -211,7 +222,7 @@ export function parseScript(
|
|
|
211
222
|
}
|
|
212
223
|
openSelections.set(marker.id, {
|
|
213
224
|
affinity: marker.affinity,
|
|
214
|
-
boundary: boundary(),
|
|
225
|
+
boundary: boundary(start),
|
|
215
226
|
start: sourceOffset + start,
|
|
216
227
|
end: sourceOffset + start + marker.length,
|
|
217
228
|
});
|
|
@@ -229,7 +240,7 @@ export function parseScript(
|
|
|
229
240
|
},
|
|
230
241
|
close: {
|
|
231
242
|
affinity: marker.affinity,
|
|
232
|
-
boundary: boundary(),
|
|
243
|
+
boundary: boundary(start),
|
|
233
244
|
range: { start: sourceOffset + start, end: sourceOffset + start + marker.length },
|
|
234
245
|
},
|
|
235
246
|
});
|
|
@@ -261,6 +272,7 @@ export function parseScript(
|
|
|
261
272
|
positions.push(absoluteStart + index);
|
|
262
273
|
continue;
|
|
263
274
|
}
|
|
275
|
+
if (dual && raw[index] === "<") fail("SCRIPT_DUAL_NESTED", "Dual Text cannot nest; escape a literal < as \\<.", absoluteStart + index);
|
|
264
276
|
buffer += raw[index];
|
|
265
277
|
index += 1;
|
|
266
278
|
positions.push(absoluteStart + index);
|
|
@@ -301,13 +313,32 @@ export function parseScript(
|
|
|
301
313
|
return { attributes, length: close + 1 };
|
|
302
314
|
};
|
|
303
315
|
|
|
316
|
+
const bindDisplayAttributes = (
|
|
317
|
+
display: string,
|
|
318
|
+
annotations: readonly { position: number; offset: number; attributes: readonly CaptionWordAttribute[] }[],
|
|
319
|
+
units = lexicalUnits(display),
|
|
320
|
+
): ParsedCaptionRegion["marks"] => {
|
|
321
|
+
const marks: Array<ParsedCaptionRegion["marks"][number]> = [];
|
|
322
|
+
for (const annotation of annotations) {
|
|
323
|
+
const displayIndex = units.length === 0
|
|
324
|
+
? (display.trim() && annotation.position === display.trimEnd().length ? 0 : -1)
|
|
325
|
+
: units.findLastIndex(unit => unit.index + unit.text.length <= annotation.position);
|
|
326
|
+
if (displayIndex < 0 || units.some(unit => unit.index < annotation.position && annotation.position < unit.index + unit.text.length)) {
|
|
327
|
+
fail("SCRIPT_ATTRIBUTE_TARGET", "A display attribute must follow a complete word.", annotation.offset);
|
|
328
|
+
}
|
|
329
|
+
if (marks.some(mark => mark.displayIndex === displayIndex)) fail("SCRIPT_ATTRIBUTE_OVERLAP", "A display word cannot receive two separate attribute blocks.", annotation.offset);
|
|
330
|
+
marks.push({ displayIndex, attributes: annotation.attributes });
|
|
331
|
+
}
|
|
332
|
+
return marks;
|
|
333
|
+
};
|
|
334
|
+
|
|
304
335
|
const parseMarkedDisplay = (
|
|
305
336
|
raw: string,
|
|
306
337
|
absoluteStart: number,
|
|
307
338
|
): { readonly display: string; readonly marks: readonly ParsedCaptionRegion["marks"][number][] } => {
|
|
308
339
|
const parts: string[] = [];
|
|
309
|
-
const marks: ParsedCaptionRegion["marks"][number][] = [];
|
|
310
340
|
let cursor = 0;
|
|
341
|
+
const attributePositions: Array<{ position: number; offset: number; attributes: readonly CaptionWordAttribute[] }> = [];
|
|
311
342
|
for (let index = 0; index < raw.length; index += 1) {
|
|
312
343
|
if (raw[index] === "\\") {
|
|
313
344
|
index += 1;
|
|
@@ -319,21 +350,15 @@ export function parseScript(
|
|
|
319
350
|
if (!before || /\s$/u.test(before)) {
|
|
320
351
|
fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must immediately follow a display token.", absoluteStart + index);
|
|
321
352
|
}
|
|
322
|
-
const cleanBefore = literalString(
|
|
323
|
-
const displayIndex = displayWordSurfaces(cleanBefore).length - 1;
|
|
324
|
-
if (displayIndex < 0) {
|
|
325
|
-
fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must follow a visible display token.", absoluteStart + index);
|
|
326
|
-
}
|
|
327
|
-
if (marks.some((mark) => mark.displayIndex === displayIndex)) {
|
|
328
|
-
fail("SCRIPT_ATTRIBUTE_OVERLAP", "A display token cannot receive two separate attribute blocks.", absoluteStart + index);
|
|
329
|
-
}
|
|
353
|
+
const cleanBefore = parts.join("") + literalString(raw.slice(cursor, index), absoluteStart + cursor, true);
|
|
330
354
|
parts.push(literalString(raw.slice(cursor, index), absoluteStart + cursor, true));
|
|
331
|
-
|
|
355
|
+
attributePositions.push({ position: cleanBefore.length, offset: absoluteStart + index, attributes: block.attributes });
|
|
332
356
|
cursor = index + block.length;
|
|
333
357
|
index += block.length - 1;
|
|
334
358
|
}
|
|
335
359
|
parts.push(literalString(raw.slice(cursor), absoluteStart + cursor, true));
|
|
336
|
-
|
|
360
|
+
const display = parts.join("");
|
|
361
|
+
return { display, marks: bindDisplayAttributes(display, attributePositions) };
|
|
337
362
|
};
|
|
338
363
|
|
|
339
364
|
const assertDualDisplayLiteral = (raw: string, absoluteStart: number): void => {
|
|
@@ -365,6 +390,7 @@ export function parseScript(
|
|
|
365
390
|
captionRegions.push({
|
|
366
391
|
id: `caption-region:${captionRegions.length + 1}`,
|
|
367
392
|
display: "",
|
|
393
|
+
separatorBefore: "",
|
|
368
394
|
segmentId,
|
|
369
395
|
startToken: tokenStart,
|
|
370
396
|
endTokenExclusive: tokenEndExclusive,
|
|
@@ -375,21 +401,30 @@ export function parseScript(
|
|
|
375
401
|
}
|
|
376
402
|
return;
|
|
377
403
|
}
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
404
|
+
// Dual Text edge whitespace is syntax padding; ordinary prose whitespace is content.
|
|
405
|
+
const raw = kind === "alias" ? cleanProjection(displayValue) : displayValue;
|
|
406
|
+
const value = (pendingCaptionText + raw).replace(/\s+/gu, " ");
|
|
407
|
+
const split = splitDisplayPrefix(value, precedingCaptionRegion !== undefined);
|
|
408
|
+
const previous = precedingCaptionRegion === undefined ? undefined : captionRegions[precedingCaptionRegion];
|
|
409
|
+
if (previous && split.previous.trim()) {
|
|
410
|
+
captionRegions[precedingCaptionRegion!] = {
|
|
383
411
|
...previous,
|
|
384
|
-
display:
|
|
385
|
-
range: { start: previous.range.start, end: range.end },
|
|
412
|
+
display: previous.display + split.previous.trimEnd(),
|
|
386
413
|
};
|
|
387
414
|
}
|
|
388
|
-
|
|
389
|
-
|
|
415
|
+
pendingCaptionText = /\s$/u.test(value) ? " " : "";
|
|
416
|
+
const display = (previous ? split.current : split.previous + split.current).trim();
|
|
417
|
+
if (tokenEndExclusive <= tokenStart) {
|
|
418
|
+
// A literal opener outside an explicit correspondence awaits that display,
|
|
419
|
+
// while closing punctuation has already been attached to the preceding one.
|
|
420
|
+
pendingCaptionText = previous ? split.current || pendingCaptionText : value;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (!display) return;
|
|
390
424
|
captionRegions.push({
|
|
391
425
|
id: `caption-region:${captionRegions.length + 1}`,
|
|
392
426
|
display,
|
|
427
|
+
separatorBefore: previous && /\s$/u.test(split.previous) ? " " : "",
|
|
393
428
|
segmentId,
|
|
394
429
|
startToken: tokenStart,
|
|
395
430
|
endTokenExclusive: tokenEndExclusive,
|
|
@@ -397,102 +432,27 @@ export function parseScript(
|
|
|
397
432
|
marks,
|
|
398
433
|
range,
|
|
399
434
|
});
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const markLastCaptionSurface = (
|
|
403
|
-
attributes: readonly CaptionWordAttribute[],
|
|
404
|
-
offset: number,
|
|
405
|
-
): void => {
|
|
406
|
-
const previous = captionRegions.at(-1);
|
|
407
|
-
if (previous === undefined || previous.kind === "hidden") {
|
|
408
|
-
fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must follow a visible display token.", offset);
|
|
409
|
-
}
|
|
410
|
-
const target = previous!;
|
|
411
|
-
if (target.kind === "alias") {
|
|
412
|
-
fail("SCRIPT_ATTRIBUTE_DUAL", "Annotate a Dual display word before the pipe; a mark cannot wrap a Dual Text.", offset);
|
|
413
|
-
}
|
|
414
|
-
const displayIndex = displayWordSurfaces(target.display).length - 1;
|
|
415
|
-
if (displayIndex < 0) fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must follow a visible display token.", offset);
|
|
416
|
-
const existing = target.marks.find((mark) => mark.displayIndex === displayIndex);
|
|
417
|
-
if (existing !== undefined) {
|
|
418
|
-
fail("SCRIPT_ATTRIBUTE_OVERLAP", "A display token cannot receive two separate attribute blocks.", offset);
|
|
419
|
-
}
|
|
420
|
-
captionRegions[captionRegions.length - 1] = {
|
|
421
|
-
...target,
|
|
422
|
-
marks: [...target.marks, { displayIndex, attributes }],
|
|
423
|
-
};
|
|
435
|
+
precedingCaptionRegion = captionRegions.length - 1;
|
|
424
436
|
};
|
|
425
437
|
|
|
426
438
|
const addText = (
|
|
427
439
|
speech: string,
|
|
428
440
|
caption: string,
|
|
429
441
|
start: number,
|
|
430
|
-
|
|
442
|
+
_end: number,
|
|
431
443
|
captureCaptionRegion = true,
|
|
432
444
|
sourcePositions?: readonly number[],
|
|
433
445
|
): void => {
|
|
434
446
|
if (!current) {
|
|
435
|
-
if (speech.trim() || caption.trim())
|
|
436
|
-
fail("SCRIPT_TEXT_OUTSIDE_SEGMENT", "Natural-language text is only allowed inside a named Segment.", start);
|
|
437
|
-
}
|
|
447
|
+
if (speech.trim() || caption.trim()) fail("SCRIPT_TEXT_OUTSIDE_SEGMENT", "Natural-language text is only allowed inside a named Segment.", start);
|
|
438
448
|
return;
|
|
439
449
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
// A punctuation-only piece can follow a display attribute or escaped source piece.
|
|
447
|
-
const preceding = tokens.at(-1);
|
|
448
|
-
const closing = splitLeadingClosingPunctuation(speech).previous;
|
|
449
|
-
if (preceding?.segmentId === current.id && preceding.editRange.end === sourceOffset + start && closing && speech.startsWith(closing)) {
|
|
450
|
-
tokens[tokens.length - 1] = { ...preceding, editRange: {
|
|
451
|
-
start: preceding.editRange.start, end: position(closing.length),
|
|
452
|
-
} };
|
|
453
|
-
}
|
|
454
|
-
for (const [unitIndex, match] of units.entries()) {
|
|
455
|
-
const normalized = normalizeWord(match.text);
|
|
456
|
-
if (!normalized) continue;
|
|
457
|
-
const index = tokens.length;
|
|
458
|
-
const segmentTokenIndex = index - current.tokenStart;
|
|
459
|
-
const id = tokenId(current.id, segmentTokenIndex);
|
|
460
|
-
tokens.push({
|
|
461
|
-
id,
|
|
462
|
-
index,
|
|
463
|
-
segmentId: current.id,
|
|
464
|
-
segmentTokenIndex,
|
|
465
|
-
startAnchorId: tokenAnchorId(current.id, segmentTokenIndex, "start"),
|
|
466
|
-
endAnchorId: tokenAnchorId(current.id, segmentTokenIndex, "end"),
|
|
467
|
-
text: match.text,
|
|
468
|
-
normalized,
|
|
469
|
-
range: {
|
|
470
|
-
start: position(match.index),
|
|
471
|
-
end: position(match.index + match.text.length),
|
|
472
|
-
},
|
|
473
|
-
editRange: {
|
|
474
|
-
start: position(editRanges[unitIndex]!.start),
|
|
475
|
-
end: position(editRanges[unitIndex]!.end),
|
|
476
|
-
},
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
current.atoms.push({
|
|
480
|
-
kind: "text",
|
|
481
|
-
speech,
|
|
482
|
-
caption,
|
|
483
|
-
tokenStart,
|
|
484
|
-
tokenEndExclusive: tokens.length,
|
|
485
|
-
range: { start: sourceOffset + start, end: sourceOffset + end },
|
|
486
|
-
});
|
|
487
|
-
if (captureCaptionRegion && cleanProjection(caption)) {
|
|
488
|
-
addCaptionRegion(
|
|
489
|
-
caption,
|
|
490
|
-
current.id,
|
|
491
|
-
tokenStart,
|
|
492
|
-
tokens.length,
|
|
493
|
-
"identity",
|
|
494
|
-
{ start: sourceOffset + start, end: sourceOffset + end },
|
|
495
|
-
);
|
|
450
|
+
runCapture = captureCaptionRegion;
|
|
451
|
+
runText += speech;
|
|
452
|
+
runCaption += caption;
|
|
453
|
+
for (let index = 0; index < speech.length; index++) {
|
|
454
|
+
runStarts.push(sourceOffset + (sourcePositions?.[index] ?? start + index));
|
|
455
|
+
runEnds.push(sourceOffset + (sourcePositions?.[index + 1] ?? start + index + 1));
|
|
496
456
|
}
|
|
497
457
|
};
|
|
498
458
|
|
|
@@ -502,12 +462,15 @@ export function parseScript(
|
|
|
502
462
|
caption: string | undefined,
|
|
503
463
|
marks: ParsedCaptionRegion["marks"] = [],
|
|
504
464
|
): void => {
|
|
465
|
+
const leading = /^\s*/u.exec(raw)![0].length;
|
|
466
|
+
raw = raw.trim();
|
|
467
|
+
absoluteStart += leading;
|
|
468
|
+
if (caption !== undefined) caption = caption.trim();
|
|
505
469
|
const startToken = tokens.length;
|
|
506
470
|
// An omitted speech side shares the written display source. Keep its original offsets:
|
|
507
471
|
// marker edits target these words, not a synthetic copy beyond the pipe.
|
|
508
472
|
const shared = caption === undefined;
|
|
509
473
|
const spokenParts: string[] = [];
|
|
510
|
-
const sharedMarks: Array<ParsedCaptionRegion["marks"][number]> = [];
|
|
511
474
|
let partStart = 0;
|
|
512
475
|
let index = 0;
|
|
513
476
|
let emittedCaption = false;
|
|
@@ -526,6 +489,7 @@ export function parseScript(
|
|
|
526
489
|
if (raw.startsWith("||", index)) {
|
|
527
490
|
fail("SCRIPT_CAPTION_BREAK_DUAL", "Caption Cue break cannot occur inside Dual Text; split the Dual Text into separate units.", absoluteStart + index);
|
|
528
491
|
}
|
|
492
|
+
if (!shared && raw[index] === "{") fail("SCRIPT_DUAL_SPEECH_ATTRIBUTE", "Display attributes belong to the display side; escape literal braces in speech.", absoluteStart + index);
|
|
529
493
|
if (shared && raw[index] === "{") {
|
|
530
494
|
const before = raw.slice(partStart, index);
|
|
531
495
|
if (!before || /\s$/u.test(before)) {
|
|
@@ -533,25 +497,15 @@ export function parseScript(
|
|
|
533
497
|
}
|
|
534
498
|
addLiteral(before, absoluteStart + partStart);
|
|
535
499
|
const block = parseAttributeBlock(raw.slice(index), absoluteStart + index);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must follow a visible display token.", absoluteStart + index);
|
|
539
|
-
}
|
|
540
|
-
if (sharedMarks.some(mark => mark.displayIndex === displayIndex)) {
|
|
541
|
-
fail("SCRIPT_ATTRIBUTE_OVERLAP", "A display token cannot receive two separate attribute blocks.", absoluteStart + index);
|
|
542
|
-
}
|
|
543
|
-
sharedMarks.push({ displayIndex, attributes: block.attributes });
|
|
544
|
-
const token = tokens.at(-1)!;
|
|
545
|
-
tokens[tokens.length - 1] = { ...token, editRange: {
|
|
546
|
-
start: token.editRange.start, end: sourceOffset + absoluteStart + index + block.length,
|
|
547
|
-
} };
|
|
500
|
+
runAttributes.push({ position: runText.length, offset: absoluteStart + index,
|
|
501
|
+
end: absoluteStart + index + block.length, attributes: block.attributes });
|
|
548
502
|
index += block.length;
|
|
549
503
|
partStart = index;
|
|
550
504
|
continue;
|
|
551
505
|
}
|
|
552
506
|
const marker = parseMarker(raw, index);
|
|
553
507
|
if (!marker) {
|
|
554
|
-
if (raw[index] === "@") fail("SCRIPT_MARKER", "Unescaped @ must begin a
|
|
508
|
+
if (raw[index] === "@") fail("SCRIPT_MARKER", "Unescaped @ must begin a delimited @{...} marker; migrate bare markers from 0.1 explicitly.", absoluteStart + index);
|
|
555
509
|
index += 1;
|
|
556
510
|
continue;
|
|
557
511
|
}
|
|
@@ -562,6 +516,7 @@ export function parseScript(
|
|
|
562
516
|
}
|
|
563
517
|
addLiteral(raw.slice(partStart), absoluteStart + partStart);
|
|
564
518
|
const display = caption ?? spokenParts.join("");
|
|
519
|
+
const sharedMarks = finishLexicalRun();
|
|
565
520
|
if (shared && tokens.length === startToken) {
|
|
566
521
|
fail("SCRIPT_DUAL_EMPTY", "Dual Text with omitted speech must contain spoken text on its display side.", absoluteStart);
|
|
567
522
|
}
|
|
@@ -600,7 +555,6 @@ export function parseScript(
|
|
|
600
555
|
|
|
601
556
|
while (offset < source.length) {
|
|
602
557
|
if (source.startsWith("<!--", offset)) {
|
|
603
|
-
finishLexicalRun();
|
|
604
558
|
const end = source.indexOf("-->", offset + 4);
|
|
605
559
|
if (end < 0) fail("SCRIPT_COMMENT", "Unclosed Script comment.", offset);
|
|
606
560
|
offset = end + 3;
|
|
@@ -619,6 +573,8 @@ export function parseScript(
|
|
|
619
573
|
fail("SCRIPT_SEGMENT_DUPLICATE", `Duplicate Segment id "${id}".`, offset);
|
|
620
574
|
}
|
|
621
575
|
const contentStart = offset + open[0].length;
|
|
576
|
+
precedingCaptionRegion = undefined;
|
|
577
|
+
pendingCaptionText = "";
|
|
622
578
|
current = {
|
|
623
579
|
id,
|
|
624
580
|
index: segments.length,
|
|
@@ -626,8 +582,6 @@ export function parseScript(
|
|
|
626
582
|
tokenStart: tokens.length,
|
|
627
583
|
sourceStart: sourceOffset + offset,
|
|
628
584
|
contentStart: sourceOffset + contentStart,
|
|
629
|
-
lexicalRun: "",
|
|
630
|
-
lexicalMarkers: [],
|
|
631
585
|
};
|
|
632
586
|
offset = contentStart;
|
|
633
587
|
if (self) closeCurrent(offset, true);
|
|
@@ -660,7 +614,7 @@ export function parseScript(
|
|
|
660
614
|
offset += marker.length;
|
|
661
615
|
continue;
|
|
662
616
|
}
|
|
663
|
-
if (source[offset] === "@") fail("SCRIPT_MARKER", "Unescaped @ must begin a
|
|
617
|
+
if (source[offset] === "@") fail("SCRIPT_MARKER", "Unescaped @ must begin a delimited @{...} marker; migrate bare markers from 0.1 explicitly.", offset);
|
|
664
618
|
|
|
665
619
|
if (source[offset] === "<" && current) {
|
|
666
620
|
const end = findUnescaped(source, ">", offset + 1);
|
|
@@ -679,7 +633,7 @@ export function parseScript(
|
|
|
679
633
|
assertDualDisplayLiteral(inside.slice(0, pipe), offset + 1);
|
|
680
634
|
const markedDisplay = parseMarkedDisplay(inside.slice(0, pipe), offset + 1);
|
|
681
635
|
const display = markedDisplay.display;
|
|
682
|
-
if (!speech.replace(
|
|
636
|
+
if (!speech.replace(/@\{[^{}]*\}/gu, "").trim()) {
|
|
683
637
|
fail("SCRIPT_DUAL_EMPTY", "Dual Text speech side must not be empty.", offset);
|
|
684
638
|
}
|
|
685
639
|
consumeSpeechSide(speech, offset + pipe + 2, display, markedDisplay.marks);
|
|
@@ -689,6 +643,8 @@ export function parseScript(
|
|
|
689
643
|
}
|
|
690
644
|
if (!ROLE_LABEL.test(inside)) fail("SCRIPT_ANGLE", `Unknown Script construct <${inside}>.`, offset);
|
|
691
645
|
finishLexicalRun();
|
|
646
|
+
precedingCaptionRegion = undefined;
|
|
647
|
+
pendingCaptionText = "";
|
|
692
648
|
current.atoms.push({
|
|
693
649
|
kind: "role",
|
|
694
650
|
label: inside,
|
|
@@ -715,29 +671,21 @@ export function parseScript(
|
|
|
715
671
|
offset += 1;
|
|
716
672
|
}
|
|
717
673
|
const raw = source.slice(textStart, offset);
|
|
718
|
-
if (raw) proseRanges.push({ start: sourceOffset + textStart, end: sourceOffset + offset });
|
|
719
674
|
for (const piece of literalPieces(raw, textStart)) addText(piece.value, piece.value, piece.start, piece.end, true, piece.positions);
|
|
720
675
|
if (source[offset] === "{") {
|
|
721
676
|
const block = parseAttributeBlock(source.slice(offset), offset);
|
|
722
677
|
if (!raw || /\s$/u.test(raw)) {
|
|
723
678
|
fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must immediately follow a display token.", offset);
|
|
724
679
|
}
|
|
725
|
-
|
|
726
|
-
const token = tokens.at(-1)!;
|
|
727
|
-
tokens[tokens.length - 1] = { ...token, editRange: {
|
|
728
|
-
start: token.editRange.start, end: sourceOffset + offset + block.length,
|
|
729
|
-
} };
|
|
680
|
+
runAttributes.push({ position: runText.length, offset, end: offset + block.length, attributes: block.attributes });
|
|
730
681
|
offset += block.length;
|
|
731
682
|
continue;
|
|
732
683
|
}
|
|
733
684
|
if (source.startsWith("||", offset)) {
|
|
734
|
-
if (!current
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
tokenIndex: tokens.length,
|
|
739
|
-
range: { start: sourceOffset + offset, end: sourceOffset + offset + 2 },
|
|
740
|
-
});
|
|
685
|
+
if (!current) fail("SCRIPT_CAPTION_BREAK_EMPTY", "Caption Cue break belongs inside a Segment.", offset);
|
|
686
|
+
const value = { tokenIndex: tokens.length, range: { start: sourceOffset + offset, end: sourceOffset + offset + 2 } };
|
|
687
|
+
captionBreaks.push(value);
|
|
688
|
+
runBoundaries.push({ position: runText.length, offset, kind: "cue", value });
|
|
741
689
|
offset += 2;
|
|
742
690
|
}
|
|
743
691
|
}
|
|
@@ -881,7 +829,6 @@ export function parseScript(
|
|
|
881
829
|
return {
|
|
882
830
|
|
|
883
831
|
sourceRange: { start: sourceOffset, end: sourceOffset + source.length },
|
|
884
|
-
proseRanges,
|
|
885
832
|
segments,
|
|
886
833
|
tokens,
|
|
887
834
|
turns,
|
|
@@ -61,13 +61,14 @@ export type ParsedMoment = Omit<NarrativeMoment, "narrativeId"> & {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
export type ParsedCaptionRegion = {
|
|
64
|
+
readonly separatorBefore: "" | " ";
|
|
64
65
|
readonly id: string;
|
|
65
66
|
readonly display: string;
|
|
66
67
|
readonly segmentId: string;
|
|
67
68
|
readonly startToken: number;
|
|
68
69
|
readonly endTokenExclusive: number;
|
|
69
70
|
readonly kind: "identity" | "alias" | "hidden";
|
|
70
|
-
/** Flat word attributes authored on the display side; indices address
|
|
71
|
+
/** Flat word attributes authored on the display side; indices address displaySurfaces(display). */
|
|
71
72
|
readonly marks: readonly {
|
|
72
73
|
readonly displayIndex: number;
|
|
73
74
|
readonly attributes: readonly CaptionWordAttribute[];
|
|
@@ -81,8 +82,6 @@ export type ParsedNarrative = Omit<
|
|
|
81
82
|
> & {
|
|
82
83
|
/** Exact Script body range, used only for source-preserving Program-boundary edits. */
|
|
83
84
|
readonly sourceRange: SourceRange;
|
|
84
|
-
/** Ordinary source prose/whitespace; excludes tags, Dual Text, attributes and comments. */
|
|
85
|
-
readonly proseRanges: readonly SourceRange[];
|
|
86
85
|
readonly segments: readonly ParsedSegment[];
|
|
87
86
|
readonly tokens: readonly ParsedToken[];
|
|
88
87
|
readonly turns: readonly ParsedTurn[];
|
|
@@ -47,7 +47,12 @@ function shim(): string {
|
|
|
47
47
|
start: parseFloat(ownStart || (present && present.getAttribute('data-start')) || '0') || 0,
|
|
48
48
|
duration: parseFloat(ownDuration || (present && present.getAttribute('data-duration')) || '0') || 0,
|
|
49
49
|
mediaStart: parseFloat(element.getAttribute('data-media-start') || '0') || 0,
|
|
50
|
-
rate: parseFloat(element.getAttribute('data-playback-rate') || '1') || 1
|
|
50
|
+
rate: parseFloat(element.getAttribute('data-playback-rate') || '1') || 1,
|
|
51
|
+
startFrame: Number(element.getAttribute('data-hypit-start-frame')),
|
|
52
|
+
endFrame: Number(element.getAttribute('data-hypit-end-frame')),
|
|
53
|
+
sourceFrame: element.getAttribute('data-hypit-source-frame').split('/').map(BigInt),
|
|
54
|
+
sourceRate: element.getAttribute('data-hypit-source-rate').split('/').map(BigInt),
|
|
55
|
+
sourceFps: element.getAttribute('data-hypit-source-fps').split('/').map(Number)
|
|
51
56
|
});
|
|
52
57
|
}
|
|
53
58
|
var audioContext;
|
|
@@ -149,7 +154,8 @@ function shim(): string {
|
|
|
149
154
|
}
|
|
150
155
|
for (var record of media) {
|
|
151
156
|
var local = currentSeconds - record.start;
|
|
152
|
-
var
|
|
157
|
+
var programFrame = Math.round(currentSeconds * fps);
|
|
158
|
+
var inside = programFrame >= record.startFrame && programFrame < record.endFrame;
|
|
153
159
|
var element = record.element;
|
|
154
160
|
element.style.visibility = inside ? 'visible' : 'hidden';
|
|
155
161
|
// Normalized picture Artifacts are deliberately silent. Programme sound
|
|
@@ -157,10 +163,18 @@ function shim(): string {
|
|
|
157
163
|
element.muted = true;
|
|
158
164
|
if (!inside) { element.pause(); continue; }
|
|
159
165
|
var target = record.mediaStart + (local + frameSeconds / 2) * record.rate;
|
|
166
|
+
var discrete = scrubbing || programFrame === record.endFrame - 1;
|
|
167
|
+
if (discrete) {
|
|
168
|
+
// The renderer floors exact source-frame coordinates. The midpoint of a
|
|
169
|
+
// programme frame can cross that source boundary at fractional speeds.
|
|
170
|
+
var a = record.sourceFrame, r = record.sourceRate;
|
|
171
|
+
var sourceFrame = Number((a[0] * r[1] + BigInt(programFrame - record.startFrame) * r[0] * a[1]) / (a[1] * r[1]));
|
|
172
|
+
target = (sourceFrame + 0.5) * record.sourceFps[1] / record.sourceFps[0];
|
|
173
|
+
}
|
|
160
174
|
if (Number.isFinite(element.duration) && element.duration > 0) {
|
|
161
175
|
target = Math.min(target, Math.max(0, element.duration - 0.001));
|
|
162
176
|
}
|
|
163
|
-
if (
|
|
177
|
+
if (discrete) {
|
|
164
178
|
element.pause();
|
|
165
179
|
waits.push(seekDecoded(element, target));
|
|
166
180
|
} else {
|
|
@@ -354,7 +354,7 @@ export function createCodePane(): CodePane {
|
|
|
354
354
|
else {
|
|
355
355
|
const span = document.createElement("span");
|
|
356
356
|
// A marker pair shares a tone with the clip it binds, so the two
|
|
357
|
-
// ends of `@claim …
|
|
357
|
+
// ends of `@{claim} … @{/claim}` read as one thing.
|
|
358
358
|
const tone = token.id === undefined ? undefined : tones.get(token.id);
|
|
359
359
|
span.className = `tok tok-${token.kind}${tone === undefined ? "" : ` tone-${tone}`}`;
|
|
360
360
|
if (token.id !== undefined) span.dataset.marker = token.id;
|