@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
|
@@ -9,159 +9,88 @@ const WORD_CHARACTER = String.raw`(?:(?!${CHARACTER_UNIT})[\p{L}\p{M}\p{N}])`;
|
|
|
9
9
|
const LEXICAL_UNIT = new RegExp([
|
|
10
10
|
String.raw`(?:\p{N}{1,3}(?:[,,]\p{N}{3})+|\p{N}+)(?:[..]\p{N}+)?(?:-\p{N}+(?:[..]\p{N}+)?)*(?!\p{N}|-[\p{L}\p{M}])`,
|
|
11
11
|
String.raw`${CHARACTER_UNIT}\p{M}*`,
|
|
12
|
-
String.raw
|
|
12
|
+
String.raw`(?:(?!${CHARACTER_UNIT})[\p{L}\p{N}])${WORD_CHARACTER}*(?:['’.-]${WORD_CHARACTER}+)*`,
|
|
13
13
|
].join("|"), "gu");
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* ASCII quotation marks have no Unicode opening/closing category. In a gap between
|
|
25
|
-
* lexical units, whitespace before the mark is the useful authoring signal:
|
|
26
|
-
* `said "hello` opens a quote, while `hello" world` closes one. A quote before the
|
|
27
|
-
* first unit is opening by definition; contractions stay inside one lexical unit.
|
|
28
|
-
*/
|
|
29
|
-
function isOpeningPunctuation(
|
|
30
|
-
character: string,
|
|
31
|
-
gap: string,
|
|
32
|
-
position: number,
|
|
33
|
-
hasPreviousSurface: boolean,
|
|
34
|
-
): boolean {
|
|
35
|
-
if (OPENING_PUNCTUATION.has(character) || UNICODE_OPENING_PUNCTUATION.test(character)) return true;
|
|
36
|
-
if (!QUOTE_PUNCTUATION.has(character)) return false;
|
|
37
|
-
if (!hasPreviousSurface) return true;
|
|
38
|
-
const before = [...gap.slice(0, position)].at(-1);
|
|
39
|
-
return before !== undefined && /\s/u.test(before);
|
|
15
|
+
// Unicode owns bracket/quote categories. ASCII symmetric quotes need local context;
|
|
16
|
+
// they have no opening/closing category. This is prose analysis, not marker syntax.
|
|
17
|
+
function openingAt(gap: string, hasPrevious: boolean, beforeWord = true): number {
|
|
18
|
+
for (const match of gap.matchAll(/./gu)) {
|
|
19
|
+
const character = match[0];
|
|
20
|
+
if (/[\p{Ps}\p{Pi}\p{Sc}]/u.test(character)
|
|
21
|
+
|| ((/["'`]/u.test(character) || (beforeWord && /\p{Pf}/u.test(character))) && (!hasPrevious || /\s$/u.test(gap.slice(0, match.index))))) return match.index;
|
|
22
|
+
}
|
|
23
|
+
return gap.length;
|
|
40
24
|
}
|
|
41
25
|
|
|
42
26
|
export function lexicalUnits(value: string): readonly LexicalUnit[] {
|
|
43
|
-
return [...value.matchAll(LEXICAL_UNIT)].map(
|
|
44
|
-
text: match[0],
|
|
45
|
-
index: match.index,
|
|
46
|
-
}));
|
|
27
|
+
return [...value.matchAll(LEXICAL_UNIT)].map(match => ({ text: match[0], index: match.index }));
|
|
47
28
|
}
|
|
48
29
|
|
|
49
30
|
export function lexicalCount(value: string): number {
|
|
50
31
|
return lexicalUnits(value).length;
|
|
51
32
|
}
|
|
52
33
|
|
|
53
|
-
/**
|
|
54
|
-
export function lexicalEditRanges(value: string): readonly { start: number; end: number }[] {
|
|
55
|
-
const units = lexicalUnits(value);
|
|
56
|
-
return units.map((unit, index) => {
|
|
57
|
-
const previousEnd = index === 0 ? 0 : units[index - 1]!.index + units[index - 1]!.text.length;
|
|
58
|
-
const nextStart = units[index + 1]?.index ?? value.length;
|
|
59
|
-
const leading = value.slice(previousEnd, unit.index);
|
|
60
|
-
let start = unit.index;
|
|
61
|
-
for (const [position, character] of [...leading.matchAll(/./gu)].map((match) => [match.index, match[0]] as const)) {
|
|
62
|
-
if (isOpeningPunctuation(character, leading, position, index > 0)) { start = previousEnd + position; break; }
|
|
63
|
-
}
|
|
64
|
-
let end = unit.index + unit.text.length;
|
|
65
|
-
const trailing = value.slice(end, nextStart);
|
|
66
|
-
for (const match of trailing.matchAll(/./gu)) {
|
|
67
|
-
if (/\s/u.test(match[0]) || isOpeningPunctuation(match[0], trailing, match.index, true)) break;
|
|
68
|
-
end += match[0].length;
|
|
69
|
-
}
|
|
70
|
-
return { start, end };
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Canonical prose spacing; punctuation remains display/speech information, never a timing token. */
|
|
75
|
-
function attachProseSpacing(value: string): string {
|
|
76
|
-
return value
|
|
77
|
-
.replace(/ +([,.;:!?%…,。!?;:、%‰)】》」』〕〉}\]])/gu, "$1")
|
|
78
|
-
.replace(/([([{(【《「『〔〈“‘]) +/gu, "$1")
|
|
79
|
-
// Do not erase a cross-script space: `here 你好` must remain two semantic regions.
|
|
80
|
-
// Only collapse explicit spaces inside one CJK run; the lexical tokenizer already keeps
|
|
81
|
-
// adjacent Latin and CJK runs separate when no space was authored.
|
|
82
|
-
.replace(/([\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]) +(?=[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}])/gu, "$1");
|
|
83
|
-
}
|
|
84
|
-
|
|
34
|
+
/** Normalize source formatting without inventing language-specific separators. */
|
|
85
35
|
export function cleanProjection(value: string): string {
|
|
86
|
-
return
|
|
36
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
87
37
|
}
|
|
88
38
|
|
|
89
|
-
/** Source formatting keeps line breaks and lexical boundaries, including spaced decimal-like prose. */
|
|
90
|
-
export function cleanHorizontalProse(value: string): string {
|
|
91
|
-
const collapsed = value.replace(/[ \t]+/gu, " ");
|
|
92
|
-
const attached = attachProseSpacing(collapsed);
|
|
93
|
-
const words = (text: string) => lexicalUnits(text).map(unit => unit.text);
|
|
94
|
-
// A separator in `3 .14` cannot be erased into the different token `3.14`.
|
|
95
|
-
const before = words(collapsed);
|
|
96
|
-
const after = words(attached);
|
|
97
|
-
return before.length === after.length && before.every((word, index) => word === after[index]) ? attached : collapsed;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Structural markers split atoms but must not invent prose whitespace when those atoms rejoin. */
|
|
101
39
|
export function joinProjection(parts: readonly string[]): string {
|
|
102
40
|
return cleanProjection(parts.join(""));
|
|
103
41
|
}
|
|
104
42
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* for rendering: opening punctuation belongs to the next unit; all other inter-token punctuation
|
|
108
|
-
* belongs to the previous unit. Whitespace is layout, not a display word of its own.
|
|
109
|
-
*/
|
|
110
|
-
export function displayWordSurfaces(value: string): readonly string[] {
|
|
111
|
-
const prose = cleanProjection(value);
|
|
112
|
-
const units = lexicalUnits(prose);
|
|
113
|
-
if (units.length === 0) return [];
|
|
43
|
+
export type DisplaySurface = { readonly text: string; readonly separatorBefore: "" | " " };
|
|
44
|
+
type ProseWord = { start: number; end: number; separatorBefore: "" | " " };
|
|
114
45
|
|
|
115
|
-
|
|
46
|
+
/** Analyze complete prose once. Speech cores and their written surfaces are distinct:
|
|
47
|
+
* punctuation has spelling and source extent, but does not acquire speech timing.
|
|
48
|
+
* A display-only literal (for example an emoji in Dual Text) needs no speech core.
|
|
49
|
+
*/
|
|
50
|
+
export function analyzeProse(value: string, followsDisplay = false): {
|
|
51
|
+
readonly units: readonly LexicalUnit[];
|
|
52
|
+
readonly surfaces: readonly DisplaySurface[];
|
|
53
|
+
readonly editRanges: readonly { start: number; end: number }[];
|
|
54
|
+
} {
|
|
55
|
+
const units = lexicalUnits(value);
|
|
56
|
+
const words: ProseWord[] = [];
|
|
116
57
|
let cursor = 0;
|
|
117
|
-
let prefix = "";
|
|
118
58
|
for (const unit of units) {
|
|
119
|
-
const gap =
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.filter(({ character, position }) => !isOpeningPunctuation(character, gap, position, true))
|
|
128
|
-
.map(({ character }) => character)
|
|
129
|
-
.join("");
|
|
130
|
-
const opening = punctuation
|
|
131
|
-
.filter(({ character, position }) => isOpeningPunctuation(character, gap, position, true))
|
|
132
|
-
.map(({ character }) => character)
|
|
133
|
-
.join("");
|
|
134
|
-
if (suffix) surfaces[surfaces.length - 1] += suffix;
|
|
135
|
-
prefix += opening;
|
|
136
|
-
}
|
|
137
|
-
surfaces.push(`${prefix}${unit.text}`);
|
|
138
|
-
prefix = "";
|
|
59
|
+
const gap = value.slice(cursor, unit.index);
|
|
60
|
+
const opening = openingAt(gap, words.length > 0 || followsDisplay);
|
|
61
|
+
const closing = gap.slice(0, opening);
|
|
62
|
+
const previous = words.at(-1);
|
|
63
|
+
if (previous) previous.end = cursor + closing.trimEnd().length;
|
|
64
|
+
const start = previous || followsDisplay ? cursor + opening : value.slice(0, unit.index).search(/\S/u);
|
|
65
|
+
words.push({ start: start < 0 ? unit.index : start, end: unit.index + unit.text.length,
|
|
66
|
+
separatorBefore: previous && /\s$/u.test(closing) ? " " : "" });
|
|
139
67
|
cursor = unit.index + unit.text.length;
|
|
140
68
|
}
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
|
|
69
|
+
const last = words.at(-1);
|
|
70
|
+
if (last) last.end = value.trimEnd().length;
|
|
71
|
+
const surfaces = words.map(word => ({ text: value.slice(word.start, word.end).replace(/\s+/gu, " "), separatorBefore: word.separatorBefore }));
|
|
72
|
+
const editRanges = words.map((word, index) => {
|
|
73
|
+
const unit = units[index]!;
|
|
74
|
+
let end = unit.index + unit.text.length;
|
|
75
|
+
// Whitespace is a legal insertion boundary. Only adjacent trailing punctuation
|
|
76
|
+
// belongs to the indivisible edit surface ("word," versus "word ,").
|
|
77
|
+
const trailing = value.slice(end, word.end);
|
|
78
|
+
const whitespace = trailing.search(/\s/u);
|
|
79
|
+
end += whitespace < 0 ? trailing.length : whitespace;
|
|
80
|
+
return { start: word.start, end };
|
|
81
|
+
});
|
|
82
|
+
if (surfaces.length === 0 && value.trim()) surfaces.push({ text: cleanProjection(value), separatorBefore: "" });
|
|
83
|
+
return { units, surfaces, editRanges };
|
|
144
84
|
}
|
|
145
85
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const first = lexicalUnits(
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
.map((character, position) => ({ character, position }))
|
|
157
|
-
.filter(({ character }) => !/\s/u.test(character));
|
|
158
|
-
const previous = punctuation
|
|
159
|
-
.filter(({ character, position }) => !isOpeningPunctuation(character, leading, position, false))
|
|
160
|
-
.map(({ character }) => character)
|
|
161
|
-
.join("");
|
|
162
|
-
const opening = punctuation
|
|
163
|
-
.filter(({ character, position }) => isOpeningPunctuation(character, leading, position, false))
|
|
164
|
-
.map(({ character }) => character)
|
|
165
|
-
.join("");
|
|
166
|
-
return { previous, current: `${opening}${prose.slice(first.index)}` };
|
|
86
|
+
export function displaySurfaces(value: string): readonly DisplaySurface[] {
|
|
87
|
+
return analyzeProse(value).surfaces;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Correspondence boundaries retain punctuation on either side of an explicit Dual. */
|
|
91
|
+
export function splitDisplayPrefix(value: string, hasPrevious: boolean): { previous: string; current: string } {
|
|
92
|
+
const first = lexicalUnits(value)[0];
|
|
93
|
+
const end = first?.index ?? value.length;
|
|
94
|
+
const split = openingAt(value.slice(0, end), hasPrevious, first !== undefined);
|
|
95
|
+
return { previous: value.slice(0, split), current: value.slice(split) };
|
|
167
96
|
}
|
|
@@ -48,15 +48,15 @@ export const scriptMarkupSurfaces = [
|
|
|
48
48
|
],
|
|
49
49
|
example: [
|
|
50
50
|
'<script id="story">',
|
|
51
|
-
" @whole",
|
|
51
|
+
" @{whole}",
|
|
52
52
|
" <hook>",
|
|
53
|
-
" <HOST> @problem Never let anyone take credit for your work.
|
|
53
|
+
" <HOST> @{problem} Never let anyone take credit for your work. @{/problem}",
|
|
54
54
|
" </hook>",
|
|
55
55
|
"",
|
|
56
56
|
" <meeting>",
|
|
57
|
-
" <HOST> I started sending <BCC | B C C> recaps. @ranking! Everything changed.",
|
|
57
|
+
" <HOST> I started sending <BCC | B C C> recaps. @{ranking!} Everything changed.",
|
|
58
58
|
" </meeting>",
|
|
59
|
-
"
|
|
59
|
+
" @{/whole~}",
|
|
60
60
|
"</script>",
|
|
61
61
|
].join("\n"),
|
|
62
62
|
notes: [
|
|
@@ -67,16 +67,16 @@ export const scriptMarkupSurfaces = [
|
|
|
67
67
|
"Dual Text is written `<display | speech>`: the left side reaches Caption and the right side reaches dialogue and speech. `<display|>` inherits speech from the displayed prose and forms the same complete alignment unit; its word times remain individual. `<|speech>` speaks without displaying. Both sides empty is invalid.",
|
|
68
68
|
"Inside Dual Text, semantic markers belong to the source of spoken text: the explicit right side, or the shared left side when speech is omitted. Display attributes remain visual metadata and never enter spoken text.",
|
|
69
69
|
"A flat token attribute follows a complete display token as `{name}` or `{name=value}`; multiple attributes use one comma-separated block. Attributes do not nest, do not carry timing, and never split a Dual Alignment Unit.",
|
|
70
|
-
"Selection and Moment markers are zero-width, share one name namespace, and may not split a speech token:",
|
|
70
|
+
"Selection and Moment markers are fully enclosed in `@{...}` with all sigils inside. They are zero-width, share one name namespace, and may not split a speech token. Surrounding prose spaces remain content; do not add spaces to delimit a name:",
|
|
71
71
|
[
|
|
72
72
|
"| Marker | Meaning |",
|
|
73
73
|
"|---|---|",
|
|
74
|
-
"| `@id` | Opens a Selection at the next word's start |",
|
|
75
|
-
"|
|
|
76
|
-
"|
|
|
77
|
-
"|
|
|
78
|
-
"| `@id
|
|
79
|
-
"|
|
|
74
|
+
"| `@{id}` | Opens a Selection at the next word's start |",
|
|
75
|
+
"| `@{~id}` | Opens a Selection at the previous word's end |",
|
|
76
|
+
"| `@{/id}` | Closes a Selection at the previous word's end |",
|
|
77
|
+
"| `@{/id~}` | Closes a Selection at the next word's start |",
|
|
78
|
+
"| `@{id!}` | A Moment at the next word's start |",
|
|
79
|
+
"| `@{~id!}` | A Moment at the previous word's end |",
|
|
80
80
|
].join("\n"),
|
|
81
81
|
"Each Selection name has one opening and one closing marker; use distinct names for distinct semantic ranges.",
|
|
82
82
|
"`<!-- -->` comments never enter any projection, and `\\@`, `\\<`, `\\\\`, `\\{` and `\\}` write those characters literally; inside Dual Text `\\|` and `\\>` do the same.",
|
|
@@ -4,7 +4,7 @@ import type { CaptionAlignmentUnit, CaptionDocument, CaptionDisplayWord } from "
|
|
|
4
4
|
import { sealText } from "@hypit/text";
|
|
5
5
|
|
|
6
6
|
import type { ParsedCaptionRegion, ParsedNarrative } from "./types.js";
|
|
7
|
-
import { cleanProjection,
|
|
7
|
+
import { cleanProjection, displaySurfaces, joinProjection, lexicalCount } from "./lexical.js";
|
|
8
8
|
|
|
9
9
|
function turnForRegion(parsed: ParsedNarrative, region: ParsedCaptionRegion): ParsedNarrative["turns"][number] {
|
|
10
10
|
const turn = parsed.turns.find((candidate) =>
|
|
@@ -21,7 +21,8 @@ function projectCaption(parsed: ParsedNarrative, id: string, narrativeId: string
|
|
|
21
21
|
for (const region of parsed.captionProjection.regions) {
|
|
22
22
|
if (region.kind === "hidden") continue;
|
|
23
23
|
const turn = turnForRegion(parsed, region);
|
|
24
|
-
const
|
|
24
|
+
const display = displaySurfaces(region.display);
|
|
25
|
+
const surfaces = display.map(word => word.text);
|
|
25
26
|
if (surfaces.length === 0) throw new Error(`Caption region ${region.id} contains no visible display surface`);
|
|
26
27
|
// A Dual Text alias is one indivisible N:M correspondence unit. Ordinary prose gives one
|
|
27
28
|
// unit per display surface so the author can place cue/style boundaries between words.
|
|
@@ -46,6 +47,7 @@ function projectCaption(parsed: ParsedNarrative, id: string, narrativeId: string
|
|
|
46
47
|
turnId: turn.id,
|
|
47
48
|
...(turn.role === undefined ? {} : { role: turn.role }),
|
|
48
49
|
text: surface,
|
|
50
|
+
separatorBefore: group.indices[groupIndex] === 0 ? region.separatorBefore : display[group.indices[groupIndex]!]!.separatorBefore,
|
|
49
51
|
attributes,
|
|
50
52
|
});
|
|
51
53
|
return wordId;
|