@bendyline/squisq 1.5.1 → 2.0.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 +63 -10
- package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
- package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
- package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
- package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
- package/dist/chunk-3E5F2XMR.js.map +1 -0
- package/dist/chunk-4X3JQXNM.js +35 -0
- package/dist/chunk-4X3JQXNM.js.map +1 -0
- package/dist/{chunk-2OIBZYKP.js → chunk-5DKBFXPY.js} +31 -121
- package/dist/chunk-5DKBFXPY.js.map +1 -0
- package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
- package/dist/chunk-5J4HKRJF.js.map +1 -0
- package/dist/chunk-5UYI3RJB.js +2503 -0
- package/dist/chunk-5UYI3RJB.js.map +1 -0
- package/dist/{chunk-TEEEILMP.js → chunk-66QO7XFB.js} +19 -292
- package/dist/chunk-66QO7XFB.js.map +1 -0
- package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
- package/dist/chunk-6PJRB2CF.js.map +1 -0
- package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
- package/dist/chunk-AMIR72JP.js.map +1 -0
- package/dist/chunk-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.js.map +1 -0
- package/dist/{chunk-LH7I6SH7.js → chunk-GF3BVNLU.js} +6360 -5723
- package/dist/chunk-GF3BVNLU.js.map +1 -0
- package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
- package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
- package/dist/chunk-JOQYPRDZ.js +127 -0
- package/dist/chunk-JOQYPRDZ.js.map +1 -0
- package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
- package/dist/chunk-LYNJEOAC.js.map +1 -0
- package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
- package/dist/chunk-PXLVXTLW.js.map +1 -0
- package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
- package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
- package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
- package/dist/chunk-QTSJOY54.js.map +1 -0
- package/dist/chunk-QVANWDPH.js +2358 -0
- package/dist/chunk-QVANWDPH.js.map +1 -0
- package/dist/{chunk-6ATE2PSM.js → chunk-R2GC6RLD.js} +7 -7
- package/dist/chunk-R2GC6RLD.js.map +1 -0
- package/dist/chunk-R2YZTCLO.js +292 -0
- package/dist/chunk-R2YZTCLO.js.map +1 -0
- package/dist/chunk-S7A5D7ZJ.js +174 -0
- package/dist/chunk-S7A5D7ZJ.js.map +1 -0
- package/dist/chunk-T3TI7GSZ.js +884 -0
- package/dist/chunk-T3TI7GSZ.js.map +1 -0
- package/dist/{chunk-RUDYOTA6.js → chunk-TUWMK62B.js} +110 -4
- package/dist/chunk-TUWMK62B.js.map +1 -0
- package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
- package/dist/chunk-VR7UPJ7C.js.map +1 -0
- package/dist/doc/index.d.ts +2334 -5
- package/dist/doc/index.js +134 -34
- package/dist/generate/index.d.ts +7 -2
- package/dist/generate/index.js +1 -1
- package/dist/imageEdit/index.d.ts +9 -6
- package/dist/imageEdit/index.js +3 -3
- package/dist/index.d.ts +12 -10
- package/dist/index.js +253 -81
- package/dist/jsonForm/index.d.ts +7 -3
- package/dist/jsonForm/index.js +9 -4
- package/dist/markdown/index.d.ts +6 -70
- package/dist/markdown/index.js +13 -9
- package/dist/narration/index.d.ts +618 -0
- package/dist/narration/index.js +82 -0
- package/dist/recommend/index.d.ts +15 -1
- package/dist/recommend/index.js +2 -2
- package/dist/schemas/index.d.ts +28 -6
- package/dist/schemas/index.js +15 -13
- package/dist/spatial/index.d.ts +0 -1
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
- package/dist/timing/index.js +5 -3
- package/dist/transform/index.d.ts +35 -24
- package/dist/transform/index.js +9 -10
- package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
- package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
- package/dist/versions/index.d.ts +2 -1
- package/dist/versions/index.js +2 -2
- package/package.json +6 -6
- package/src/__tests__/applyNarrationTiming.test.ts +190 -0
- package/src/__tests__/applyRenderStyle.test.ts +3 -3
- package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
- package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
- package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
- package/src/__tests__/asciiDiagramParse.test.ts +233 -0
- package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
- package/src/__tests__/asciiDiagramRender.test.ts +162 -0
- package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
- package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
- package/src/__tests__/asciiTimelineParse.test.ts +205 -0
- package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
- package/src/__tests__/asciiTimelineRender.test.ts +376 -0
- package/src/__tests__/attrTokens.test.ts +5 -0
- package/src/__tests__/contentContainer.test.ts +43 -0
- package/src/__tests__/coverBlock.test.ts +33 -2
- package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
- package/src/__tests__/dataTable.test.ts +25 -15
- package/src/__tests__/diagramBlock.test.ts +3 -3
- package/src/__tests__/diagramText.test.ts +26 -0
- package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
- package/src/__tests__/drawingBlock.test.ts +3 -3
- package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
- package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
- package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
- package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
- package/src/__tests__/getLayers.test.ts +32 -18
- package/src/__tests__/getLayersFallback.test.ts +8 -8
- package/src/__tests__/imageEditVersions.test.ts +2 -0
- package/src/__tests__/jsonForm.pointer.test.ts +47 -1
- package/src/__tests__/layoutBlock.test.ts +3 -3
- package/src/__tests__/majorApiTypes.test.ts +18 -0
- package/src/__tests__/markdown.test.ts +41 -0
- package/src/__tests__/markdownToDoc.test.ts +5 -2
- package/src/__tests__/matchFontFamily.test.ts +114 -0
- package/src/__tests__/materializeBlockLayers.test.ts +135 -0
- package/src/__tests__/materializeTestUtils.ts +17 -0
- package/src/__tests__/narrationAlign.test.ts +150 -0
- package/src/__tests__/narrationFeatures.test.ts +71 -0
- package/src/__tests__/narrationNuclei.test.ts +45 -0
- package/src/__tests__/narrationPacing.test.ts +189 -0
- package/src/__tests__/narrationScript.test.ts +149 -0
- package/src/__tests__/narrationSessionRepro.test.ts +62 -0
- package/src/__tests__/narrationSidecar.test.ts +88 -0
- package/src/__tests__/narrationSyllables.test.ts +65 -0
- package/src/__tests__/narrationTestSignals.ts +167 -0
- package/src/__tests__/narrationVad.test.ts +66 -0
- package/src/__tests__/resolveDocTheme.test.ts +16 -2
- package/src/__tests__/rootExports.test.ts +18 -0
- package/src/__tests__/scopedContentContainer.test.ts +20 -0
- package/src/__tests__/shapeGeometry.test.ts +49 -0
- package/src/__tests__/templateAnnotationParse.test.ts +10 -0
- package/src/__tests__/templateInputs.test.ts +69 -0
- package/src/__tests__/templateMetadata.test.ts +2 -1
- package/src/__tests__/templates.test.ts +143 -17
- package/src/__tests__/themeCompile.test.ts +49 -22
- package/src/__tests__/transformProvenance.test.ts +160 -0
- package/src/__tests__/transformV2.test.ts +40 -14
- package/src/__tests__/treeviewBattle.test.ts +76 -0
- package/src/__tests__/treeviewDetect.test.ts +168 -0
- package/src/__tests__/treeviewParse.test.ts +118 -0
- package/src/__tests__/treeviewPipeline.test.ts +183 -0
- package/src/__tests__/treeviewRender.test.ts +91 -0
- package/src/__tests__/versions.test.ts +44 -0
- package/src/doc/applyNarrationTiming.ts +244 -0
- package/src/doc/asciiDiagram/chars.ts +210 -0
- package/src/doc/asciiDiagram/detect.ts +175 -0
- package/src/doc/asciiDiagram/index.ts +38 -0
- package/src/doc/asciiDiagram/mapping.ts +225 -0
- package/src/doc/asciiDiagram/parse.ts +1095 -0
- package/src/doc/asciiDiagram/render.ts +963 -0
- package/src/doc/asciiDiagram/repair.ts +400 -0
- package/src/doc/asciiDiagram/types.ts +65 -0
- package/src/doc/asciiTimeline/detect.ts +138 -0
- package/src/doc/asciiTimeline/index.ts +26 -0
- package/src/doc/asciiTimeline/mapping.ts +43 -0
- package/src/doc/asciiTimeline/parse.ts +870 -0
- package/src/doc/asciiTimeline/render.ts +427 -0
- package/src/doc/asciiTimeline/types.ts +73 -0
- package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
- package/src/doc/audioMapping.ts +60 -46
- package/src/doc/customTemplatesFrontmatter.ts +0 -41
- package/src/doc/docToMarkdown.ts +126 -39
- package/src/doc/index.ts +76 -4
- package/src/doc/markdownToDoc.ts +135 -1
- package/src/doc/materializeBlockLayers.ts +304 -0
- package/src/doc/mediaAnnotations.ts +20 -0
- package/src/doc/resolveDocTheme.ts +8 -7
- package/src/doc/templateInputs.ts +185 -2
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
- package/src/doc/templates/coverBlock.ts +49 -13
- package/src/doc/templates/diagramBlock.ts +237 -38
- package/src/doc/templates/drawingBlock.ts +2 -2
- package/src/doc/templates/fallbackBlock.ts +1 -1
- package/src/doc/templates/index.ts +134 -279
- package/src/doc/templates/inputDescriptors.ts +20 -1
- package/src/doc/templates/metadata.ts +10 -0
- package/src/doc/templates/persistentLayers.ts +12 -126
- package/src/doc/templates/registry.ts +88 -0
- package/src/doc/templates/templateNames.ts +22 -0
- package/src/doc/templates/timelineBlock.ts +613 -0
- package/src/doc/templates/treeBlock.ts +131 -0
- package/src/doc/treeview/chars.ts +72 -0
- package/src/doc/treeview/detect.ts +145 -0
- package/src/doc/treeview/index.ts +30 -0
- package/src/doc/treeview/mapping.ts +95 -0
- package/src/doc/treeview/parse.ts +201 -0
- package/src/doc/treeview/render.ts +41 -0
- package/src/doc/treeview/types.ts +43 -0
- package/src/doc/utils/applyRenderStyle.ts +2 -9
- package/src/doc/utils/diagramText.ts +78 -0
- package/src/doc/utils/shapeGeometry.ts +157 -7
- package/src/doc/validate.ts +1 -1
- package/src/generate/templateMapper.ts +7 -0
- package/src/imageEdit/export.ts +6 -5
- package/src/imageEdit/versionPaths.ts +13 -26
- package/src/imageEdit/versions.ts +21 -100
- package/src/index.ts +1 -0
- package/src/internal/immutable.ts +58 -0
- package/src/jsonForm/index.ts +9 -1
- package/src/jsonForm/pointer.ts +68 -23
- package/src/markdown/attrTokens.ts +7 -5
- package/src/markdown/convert.ts +11 -1
- package/src/markdown/index.ts +1 -0
- package/src/markdown/stringify.ts +4 -9
- package/src/markdown/utils.ts +36 -14
- package/src/narration/align.ts +514 -0
- package/src/narration/features.ts +231 -0
- package/src/narration/index.ts +86 -0
- package/src/narration/nuclei.ts +118 -0
- package/src/narration/pacing.ts +211 -0
- package/src/narration/script.ts +239 -0
- package/src/narration/session.ts +100 -0
- package/src/narration/sidecar.ts +187 -0
- package/src/narration/syllables.ts +85 -0
- package/src/narration/trace.ts +50 -0
- package/src/narration/types.ts +299 -0
- package/src/narration/vad.ts +131 -0
- package/src/recommend/numberHighlight.ts +25 -0
- package/src/recommend/templates.ts +108 -12
- package/src/schemas/BlockTemplates.ts +156 -7
- package/src/schemas/Doc.ts +72 -9
- package/src/schemas/Media.ts +12 -2
- package/src/schemas/Theme.ts +45 -25
- package/src/schemas/Viewport.ts +2 -2
- package/src/schemas/fontStacks.ts +79 -0
- package/src/schemas/themeCompile.ts +15 -0
- package/src/schemas/themeConstants.ts +2 -0
- package/src/schemas/themeLibrary.ts +25 -19
- package/src/schemas/themeValidator.ts +1 -1
- package/src/spatial/Geohash.ts +0 -3
- package/src/storage/ContentContainer.ts +17 -9
- package/src/storage/ScopedContentContainer.ts +45 -15
- package/src/transform/applyTransform.ts +5 -5
- package/src/transform/index.ts +3 -1
- package/src/transform/registry.ts +169 -49
- package/src/transform/templateSelector.ts +21 -2
- package/src/transform/timingAllocator.ts +152 -21
- package/src/transform/types.ts +14 -1
- package/src/versions/gitignore.ts +34 -0
- package/src/versions/index.ts +2 -0
- package/src/versions/operations.ts +18 -104
- package/src/versions/paths.ts +13 -28
- package/src/versions/snapshotStore.ts +202 -0
- package/src/versions/types.ts +3 -1
- package/dist/chunk-2OIBZYKP.js.map +0 -1
- package/dist/chunk-6ATE2PSM.js.map +0 -1
- package/dist/chunk-A4O7GIWE.js.map +0 -1
- package/dist/chunk-DLXZMS5K.js.map +0 -1
- package/dist/chunk-HWVFJAAH.js.map +0 -1
- package/dist/chunk-IMSCRLLV.js.map +0 -1
- package/dist/chunk-JELIRVDP.js +0 -24
- package/dist/chunk-JELIRVDP.js.map +0 -1
- package/dist/chunk-KKNUBQ6Y.js.map +0 -1
- package/dist/chunk-LDQ2HJIX.js.map +0 -1
- package/dist/chunk-LH7I6SH7.js.map +0 -1
- package/dist/chunk-RUDYOTA6.js.map +0 -1
- package/dist/chunk-T5UK6YOB.js.map +0 -1
- package/dist/chunk-TEEEILMP.js.map +0 -1
- package/dist/chunk-UY7KGQ5R.js +0 -192
- package/dist/chunk-UY7KGQ5R.js.map +0 -1
- package/dist/chunk-VLJJHDAC.js +0 -218
- package/dist/chunk-VLJJHDAC.js.map +0 -1
- package/dist/chunk-VWUFZ6ZG.js.map +0 -1
- package/dist/story/index.d.ts +0 -1777
- package/dist/story/index.js +0 -273
- package/src/doc/getLayers.ts +0 -195
- /package/dist/{story → narration}/index.js.map +0 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative parser for two-or-more-lane flow art whose last edge wraps
|
|
3
|
+
* around the right margin. This dialect is common in AI-authored architecture
|
|
4
|
+
* notes and sequence sketches:
|
|
5
|
+
*
|
|
6
|
+
* CLIENT KERNEL
|
|
7
|
+
* ────── ──────
|
|
8
|
+
* input ──► command ────► validate
|
|
9
|
+
* ├─ invalid → rejected ─┐
|
|
10
|
+
* surfaced ◄─────────────────────────────────────┘
|
|
11
|
+
*
|
|
12
|
+
* It is intentionally strict. Every nonblank row must be a heading,
|
|
13
|
+
* underline, connector-only row, parenthetical continuation, or prose joined
|
|
14
|
+
* by an explicit arrow. Accepted art is converted to the ordinary timeline
|
|
15
|
+
* model, after which the canonical timeline renderer owns the authored form.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
AsciiTimeline,
|
|
20
|
+
AsciiTimelineEvent,
|
|
21
|
+
AsciiTimelineLink,
|
|
22
|
+
AsciiTimelineTrack,
|
|
23
|
+
} from './types.js';
|
|
24
|
+
|
|
25
|
+
interface TextSegment {
|
|
26
|
+
text: string;
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
trackIndex: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TrackBand {
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
row: number;
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface EventDraft {
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
trackIndex: number;
|
|
45
|
+
order: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface LinkDraft {
|
|
49
|
+
source: string;
|
|
50
|
+
target: string;
|
|
51
|
+
label?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface PendingDescription {
|
|
55
|
+
eventId: string;
|
|
56
|
+
text: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const UNDERLINE_RE = /[─━═-]{6,}/gu;
|
|
60
|
+
const FLOW_CHAR_RE = /[─━═│|►◄→←┌┐└┘├┤]/u;
|
|
61
|
+
const ARROW_RE = /[►◄→←]/u;
|
|
62
|
+
const LEFT_ARROW_RE = /[◄←]/u;
|
|
63
|
+
const RIGHT_ARROW_RE = /[►→]/u;
|
|
64
|
+
const CONNECTOR_ONLY_RE = /^[\s─━═│|►◄→←┌┐└┘├┤]+$/u;
|
|
65
|
+
const HEADER_RE = /^[A-Z][A-Z0-9 _/-]*(?:\s+\([^)]*\))?$/u;
|
|
66
|
+
|
|
67
|
+
/** Parse the wrapped-flow dialect, or return null without claiming the art. */
|
|
68
|
+
export function parseWrappedFlowTimeline(text: string): AsciiTimeline | null {
|
|
69
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
70
|
+
const header = findHeader(lines);
|
|
71
|
+
if (!header || header.tracks.length < 2) return null;
|
|
72
|
+
|
|
73
|
+
const drafts: EventDraft[] = [];
|
|
74
|
+
const links: LinkDraft[] = [];
|
|
75
|
+
const usedIds = new Set<string>();
|
|
76
|
+
const lastByTrack = new Map<number, string>();
|
|
77
|
+
let pendingDescription: PendingDescription | null = null;
|
|
78
|
+
let activeBranchRoot: string | null = null;
|
|
79
|
+
let wrapSource: string | null = null;
|
|
80
|
+
let arrowCount = 0;
|
|
81
|
+
let hasLeftArrow = false;
|
|
82
|
+
let hasRightArrow = false;
|
|
83
|
+
let hasWrapTop = false;
|
|
84
|
+
let hasWrapBottom = false;
|
|
85
|
+
|
|
86
|
+
const addEvent = (segment: TextSegment): EventDraft => {
|
|
87
|
+
const label = normalizeText(segment.text);
|
|
88
|
+
const id = uniqueId(label, usedIds);
|
|
89
|
+
const draft: EventDraft = {
|
|
90
|
+
id,
|
|
91
|
+
label,
|
|
92
|
+
trackIndex: segment.trackIndex,
|
|
93
|
+
order: drafts.length,
|
|
94
|
+
};
|
|
95
|
+
drafts.push(draft);
|
|
96
|
+
return draft;
|
|
97
|
+
};
|
|
98
|
+
const addLink = (source: string | undefined, target: string | undefined, label?: string) => {
|
|
99
|
+
if (!source || !target || source === target) return;
|
|
100
|
+
if (
|
|
101
|
+
links.some((link) => link.source === source && link.target === target && link.label === label)
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
links.push({ source, target, ...(label ? { label } : {}) });
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (let row = header.underlineRow + 1; row < lines.length; row++) {
|
|
109
|
+
const original = lines[row];
|
|
110
|
+
if (!original.trim()) continue;
|
|
111
|
+
if (CONNECTOR_ONLY_RE.test(original)) {
|
|
112
|
+
hasWrapTop ||= original.includes('┐');
|
|
113
|
+
hasWrapBottom ||= original.includes('┘');
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const trimmed = original.trim();
|
|
118
|
+
if (trimmed.startsWith('(') && !ARROW_RE.test(original)) {
|
|
119
|
+
const trackIndex = trackIndexAt(header.tracks, firstContentColumn(original));
|
|
120
|
+
const eventId = lastByTrack.get(trackIndex);
|
|
121
|
+
if (!eventId) return null;
|
|
122
|
+
pendingDescription = {
|
|
123
|
+
eventId,
|
|
124
|
+
text: normalizeText(original.replace(/[│|]+\s*$/u, '')),
|
|
125
|
+
};
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let line = original;
|
|
130
|
+
let continuationTarget: string | null = null;
|
|
131
|
+
if (pendingDescription) {
|
|
132
|
+
const firstArrow = firstArrowColumn(line);
|
|
133
|
+
if (firstArrow < 0) return null;
|
|
134
|
+
const continuation = normalizeText(Array.from(line).slice(0, firstArrow).join(''));
|
|
135
|
+
const event = drafts.find((candidate) => candidate.id === pendingDescription?.eventId);
|
|
136
|
+
if (!event) return null;
|
|
137
|
+
event.description = normalizeText(`${pendingDescription.text} ${continuation}`);
|
|
138
|
+
continuationTarget = event.id;
|
|
139
|
+
line = `${' '.repeat(firstArrow)}${Array.from(line).slice(firstArrow).join('')}`;
|
|
140
|
+
pendingDescription = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const segments = extractSegments(line, header.tracks);
|
|
144
|
+
if (segments.length === 0) return null;
|
|
145
|
+
if (segments.some((segment) => !/[\p{L}\p{N}]/u.test(segment.text))) return null;
|
|
146
|
+
|
|
147
|
+
const previousByTrack = new Map(lastByTrack);
|
|
148
|
+
const events = segments.map(addEvent);
|
|
149
|
+
const directions: Array<'left' | 'right' | null> = [];
|
|
150
|
+
for (let index = 0; index < segments.length - 1; index++) {
|
|
151
|
+
const between = Array.from(line)
|
|
152
|
+
.slice(segments[index].end + 1, segments[index + 1].start)
|
|
153
|
+
.join('');
|
|
154
|
+
const direction = LEFT_ARROW_RE.test(between)
|
|
155
|
+
? 'left'
|
|
156
|
+
: RIGHT_ARROW_RE.test(between)
|
|
157
|
+
? 'right'
|
|
158
|
+
: null;
|
|
159
|
+
directions.push(direction);
|
|
160
|
+
if (direction) arrowCount++;
|
|
161
|
+
hasLeftArrow ||= direction === 'left';
|
|
162
|
+
hasRightArrow ||= direction === 'right';
|
|
163
|
+
if (direction === 'left') addLink(events[index + 1].id, events[index].id);
|
|
164
|
+
if (direction === 'right') addLink(events[index].id, events[index + 1].id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const leadingBranch = /^[\s│|]*[├└]/u.exec(original)?.[0] ?? '';
|
|
168
|
+
if (leadingBranch) {
|
|
169
|
+
if (!activeBranchRoot) {
|
|
170
|
+
activeBranchRoot = previousByTrack.get(events[0].trackIndex) ?? null;
|
|
171
|
+
}
|
|
172
|
+
addLink(activeBranchRoot ?? undefined, events[0].id);
|
|
173
|
+
if (leadingBranch.includes('└')) activeBranchRoot = null;
|
|
174
|
+
} else {
|
|
175
|
+
activeBranchRoot = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const allLeft = directions.length > 0 && directions.every((direction) => direction === 'left');
|
|
179
|
+
const ordered = allLeft ? [...events].reverse() : events;
|
|
180
|
+
const first = ordered[0];
|
|
181
|
+
if (!leadingBranch && !original.includes('┘')) {
|
|
182
|
+
addLink(previousByTrack.get(first.trackIndex), first.id);
|
|
183
|
+
}
|
|
184
|
+
if (continuationTarget) addLink(ordered[ordered.length - 1]?.id, continuationTarget);
|
|
185
|
+
|
|
186
|
+
for (const event of ordered) lastByTrack.set(event.trackIndex, event.id);
|
|
187
|
+
|
|
188
|
+
if (original.includes('┐')) {
|
|
189
|
+
hasWrapTop = true;
|
|
190
|
+
wrapSource = events[events.length - 1]?.id ?? null;
|
|
191
|
+
}
|
|
192
|
+
if (original.includes('┘')) {
|
|
193
|
+
hasWrapBottom = true;
|
|
194
|
+
const target = allLeft ? events[0]?.id : events[events.length - 1]?.id;
|
|
195
|
+
addLink(wrapSource ?? undefined, target);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (pendingDescription || drafts.length < 6) return null;
|
|
200
|
+
if (arrowCount < 4 || !hasLeftArrow || !hasRightArrow || !hasWrapTop || !hasWrapBottom) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (!hasCrossTrackLink(drafts, links)) return null;
|
|
204
|
+
|
|
205
|
+
const columns = assignColumns(drafts, links);
|
|
206
|
+
const endColumn = Math.max(10, ...columns.values());
|
|
207
|
+
const tracks: AsciiTimelineTrack[] = header.tracks.map((band, trackIndex) => {
|
|
208
|
+
const trackEvents = drafts
|
|
209
|
+
.filter((event) => event.trackIndex === trackIndex)
|
|
210
|
+
.sort(
|
|
211
|
+
(left, right) =>
|
|
212
|
+
(columns.get(left.id) ?? 0) - (columns.get(right.id) ?? 0) || left.order - right.order,
|
|
213
|
+
);
|
|
214
|
+
let previousColumn = -1;
|
|
215
|
+
const events: AsciiTimelineEvent[] = trackEvents.map((event, index) => {
|
|
216
|
+
const ideal = columns.get(event.id) ?? index * 10;
|
|
217
|
+
const column = Math.max(previousColumn + 1, ideal);
|
|
218
|
+
previousColumn = column;
|
|
219
|
+
return {
|
|
220
|
+
id: event.id,
|
|
221
|
+
label: event.label,
|
|
222
|
+
...(event.description ? { description: event.description } : {}),
|
|
223
|
+
column,
|
|
224
|
+
side: index % 2 === 0 ? 'above' : 'below',
|
|
225
|
+
marker: 'filled',
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
id: band.id,
|
|
230
|
+
label: band.label,
|
|
231
|
+
row: band.row,
|
|
232
|
+
startColumn: 0,
|
|
233
|
+
endColumn,
|
|
234
|
+
events,
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
const width = Math.max(0, ...lines.map((line) => Array.from(line).length));
|
|
238
|
+
links.sort(
|
|
239
|
+
(left, right) =>
|
|
240
|
+
left.source.localeCompare(right.source) ||
|
|
241
|
+
left.target.localeCompare(right.target) ||
|
|
242
|
+
(left.label ?? '').localeCompare(right.label ?? ''),
|
|
243
|
+
);
|
|
244
|
+
return {
|
|
245
|
+
tracks,
|
|
246
|
+
links: links satisfies AsciiTimelineLink[],
|
|
247
|
+
width,
|
|
248
|
+
height: lines.length,
|
|
249
|
+
style: 'unicode',
|
|
250
|
+
warnings: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** High-confidence predicate used by the editor's lossless rewrite gate. */
|
|
255
|
+
export function isWrappedFlowTimelineSource(text: string): boolean {
|
|
256
|
+
return parseWrappedFlowTimeline(text) !== null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function findHeader(
|
|
260
|
+
lines: readonly string[],
|
|
261
|
+
): { tracks: TrackBand[]; underlineRow: number } | null {
|
|
262
|
+
for (let row = 1; row < Math.min(lines.length, 5); row++) {
|
|
263
|
+
const runs = Array.from(lines[row].matchAll(UNDERLINE_RE)).map((match) => ({
|
|
264
|
+
start: match.index,
|
|
265
|
+
end: (match.index ?? 0) + Array.from(match[0]).length - 1,
|
|
266
|
+
}));
|
|
267
|
+
if (runs.length < 2) continue;
|
|
268
|
+
const headings = splitHeader(lines[row - 1]);
|
|
269
|
+
if (
|
|
270
|
+
headings.length !== runs.length ||
|
|
271
|
+
headings.some((heading) => !HEADER_RE.test(heading.text))
|
|
272
|
+
) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const tracks = runs.map((run, index): TrackBand => {
|
|
276
|
+
const heading = headings[index];
|
|
277
|
+
const base = heading.text.replace(/\s+\([^)]*\)\s*$/u, '').trim();
|
|
278
|
+
return {
|
|
279
|
+
id: safeId(base),
|
|
280
|
+
label: heading.text,
|
|
281
|
+
row: row - 1,
|
|
282
|
+
start: run.start,
|
|
283
|
+
end: run.end,
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
return { tracks, underlineRow: row };
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function splitHeader(line: string): Array<{ text: string; start: number; end: number }> {
|
|
292
|
+
const chars = Array.from(line);
|
|
293
|
+
const segments: Array<{ text: string; start: number; end: number }> = [];
|
|
294
|
+
let start = -1;
|
|
295
|
+
let spaces = 0;
|
|
296
|
+
for (let index = 0; index <= chars.length; index++) {
|
|
297
|
+
const char = chars[index];
|
|
298
|
+
if (char === ' ') {
|
|
299
|
+
spaces++;
|
|
300
|
+
if (start >= 0 && spaces >= 3) {
|
|
301
|
+
const end = index - spaces;
|
|
302
|
+
const text = chars
|
|
303
|
+
.slice(start, end + 1)
|
|
304
|
+
.join('')
|
|
305
|
+
.trim();
|
|
306
|
+
if (text) segments.push({ text, start, end });
|
|
307
|
+
start = -1;
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
spaces = 0;
|
|
312
|
+
if (char !== undefined && start < 0) start = index;
|
|
313
|
+
if (char === undefined && start >= 0) {
|
|
314
|
+
const text = chars.slice(start).join('').trim();
|
|
315
|
+
if (text) segments.push({ text, start, end: chars.length - 1 });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return segments;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function extractSegments(line: string, tracks: readonly TrackBand[]): TextSegment[] {
|
|
322
|
+
const chars = Array.from(line);
|
|
323
|
+
const segments: TextSegment[] = [];
|
|
324
|
+
let start = -1;
|
|
325
|
+
for (let index = 0; index <= chars.length; index++) {
|
|
326
|
+
const structural = index === chars.length || FLOW_CHAR_RE.test(chars[index] ?? '');
|
|
327
|
+
if (!structural && start < 0) start = index;
|
|
328
|
+
if (!structural || start < 0) continue;
|
|
329
|
+
const raw = chars.slice(start, index).join('');
|
|
330
|
+
const leading = raw.length - raw.trimStart().length;
|
|
331
|
+
const text = normalizeText(raw);
|
|
332
|
+
if (text) {
|
|
333
|
+
const segmentStart = start + leading;
|
|
334
|
+
const segmentEnd = segmentStart + Array.from(text).length - 1;
|
|
335
|
+
segments.push({
|
|
336
|
+
text,
|
|
337
|
+
start: segmentStart,
|
|
338
|
+
end: segmentEnd,
|
|
339
|
+
trackIndex: trackIndexAt(tracks, segmentStart),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
start = -1;
|
|
343
|
+
}
|
|
344
|
+
return segments;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function trackIndexAt(tracks: readonly TrackBand[], column: number): number {
|
|
348
|
+
for (let index = 1; index < tracks.length; index++) {
|
|
349
|
+
if (column < tracks[index].start) return index - 1;
|
|
350
|
+
}
|
|
351
|
+
return tracks.length - 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function firstContentColumn(line: string): number {
|
|
355
|
+
const chars = Array.from(line);
|
|
356
|
+
const index = chars.findIndex((char) => /[\p{L}\p{N}(]/u.test(char));
|
|
357
|
+
return Math.max(0, index);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function firstArrowColumn(line: string): number {
|
|
361
|
+
return Array.from(line).findIndex((char) => ARROW_RE.test(char));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function normalizeText(value: string): string {
|
|
365
|
+
return value
|
|
366
|
+
.normalize('NFC')
|
|
367
|
+
.replace(/^[\s:]+|[\s:]+$/gu, '')
|
|
368
|
+
.replace(/\s+/gu, ' ')
|
|
369
|
+
.trim();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function safeId(value: string): string {
|
|
373
|
+
return (
|
|
374
|
+
value
|
|
375
|
+
.normalize('NFC')
|
|
376
|
+
.toLowerCase()
|
|
377
|
+
.replace(/[^a-z0-9_.~-]+/gu, '-')
|
|
378
|
+
.replace(/^-+|-+$/gu, '') || 'event'
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function uniqueId(label: string, used: Set<string>): string {
|
|
383
|
+
const base = safeId(label);
|
|
384
|
+
if (!used.has(base)) {
|
|
385
|
+
used.add(base);
|
|
386
|
+
return base;
|
|
387
|
+
}
|
|
388
|
+
let suffix = 2;
|
|
389
|
+
while (used.has(`${base}-${suffix}`)) suffix++;
|
|
390
|
+
const id = `${base}-${suffix}`;
|
|
391
|
+
used.add(id);
|
|
392
|
+
return id;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function hasCrossTrackLink(events: readonly EventDraft[], links: readonly LinkDraft[]): boolean {
|
|
396
|
+
const trackById = new Map(events.map((event) => [event.id, event.trackIndex]));
|
|
397
|
+
return links.some((link) => trackById.get(link.source) !== trackById.get(link.target));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function assignColumns(
|
|
401
|
+
events: readonly EventDraft[],
|
|
402
|
+
links: readonly LinkDraft[],
|
|
403
|
+
): Map<string, number> {
|
|
404
|
+
const outgoing = new Map<string, string[]>();
|
|
405
|
+
const indegree = new Map(events.map((event) => [event.id, 0]));
|
|
406
|
+
for (const link of links) {
|
|
407
|
+
outgoing.set(link.source, [...(outgoing.get(link.source) ?? []), link.target]);
|
|
408
|
+
indegree.set(link.target, (indegree.get(link.target) ?? 0) + 1);
|
|
409
|
+
}
|
|
410
|
+
const ranks = new Map(events.map((event) => [event.id, 0]));
|
|
411
|
+
const queue = events.filter((event) => indegree.get(event.id) === 0).map((event) => event.id);
|
|
412
|
+
let visited = 0;
|
|
413
|
+
while (queue.length > 0) {
|
|
414
|
+
const source = queue.shift() as string;
|
|
415
|
+
visited++;
|
|
416
|
+
for (const target of outgoing.get(source) ?? []) {
|
|
417
|
+
ranks.set(target, Math.max(ranks.get(target) ?? 0, (ranks.get(source) ?? 0) + 1));
|
|
418
|
+
const next = (indegree.get(target) ?? 1) - 1;
|
|
419
|
+
indegree.set(target, next);
|
|
420
|
+
if (next === 0) queue.push(target);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (visited < events.length) {
|
|
424
|
+
for (const event of events) ranks.set(event.id, event.order);
|
|
425
|
+
}
|
|
426
|
+
return new Map(Array.from(ranks, ([id, rank]) => [id, rank * 10]));
|
|
427
|
+
}
|
package/src/doc/audioMapping.ts
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Doc, Block, AudioSegment, AudioTimingData } from '../schemas/Doc.js';
|
|
17
|
+
import { resolveMediaSchedule } from '../schemas/Media.js';
|
|
17
18
|
import type { ContentContainer } from '../storage/ContentContainer.js';
|
|
18
19
|
import { extractPlainText } from '../markdown/utils.js';
|
|
20
|
+
import { applyNarrationTiming } from './applyNarrationTiming.js';
|
|
19
21
|
|
|
20
22
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
21
23
|
|
|
@@ -115,12 +117,9 @@ function flattenForAudio(blocks: Block[]): Block[] {
|
|
|
115
117
|
/**
|
|
116
118
|
* Info about a narration audio file found in the container.
|
|
117
119
|
*
|
|
118
|
-
*
|
|
119
|
-
* audio files that the browser recorder writes into `audio/`. The type
|
|
120
|
-
* name is retained for backwards compatibility with the surrounding
|
|
121
|
-
* matching logic; the data shape is identical regardless of codec.
|
|
120
|
+
* Covers MP3/WebM/MP4/Ogg/M4A audio files, including browser-recorder output.
|
|
122
121
|
*/
|
|
123
|
-
interface
|
|
122
|
+
interface NarrationAudioInfo {
|
|
124
123
|
/** Path in the container (e.g., "audio/narration-2026.webm"). */
|
|
125
124
|
path: string;
|
|
126
125
|
/** Filename without path (e.g., "narration-2026.webm"). */
|
|
@@ -189,10 +188,10 @@ function parseConsolidatedTiming(data: ArrayBuffer): Record<string, AudioTimingD
|
|
|
189
188
|
* Checks for a consolidated timing.json first, then falls back to
|
|
190
189
|
* per-file `.timing.json` sidecars.
|
|
191
190
|
*/
|
|
192
|
-
async function
|
|
191
|
+
async function discoverNarrationAudio(container: ContentContainer): Promise<NarrationAudioInfo[]> {
|
|
193
192
|
const files = await container.listFiles();
|
|
194
193
|
const audioFiles = files.filter((f) => isNarrationAudioPath(f.path));
|
|
195
|
-
const results:
|
|
194
|
+
const results: NarrationAudioInfo[] = [];
|
|
196
195
|
|
|
197
196
|
// Try to load consolidated timing.json
|
|
198
197
|
let consolidatedSections: Record<string, AudioTimingData> | null = null;
|
|
@@ -237,22 +236,22 @@ async function discoverMp3s(container: ContentContainer): Promise<Mp3Info[]> {
|
|
|
237
236
|
*/
|
|
238
237
|
function resolveFromAnnotations(
|
|
239
238
|
blocks: Block[],
|
|
240
|
-
|
|
241
|
-
): Array<{ block: Block;
|
|
242
|
-
const matches: Array<{ block: Block;
|
|
239
|
+
audioFiles: NarrationAudioInfo[],
|
|
240
|
+
): Array<{ block: Block; audio: NarrationAudioInfo }> {
|
|
241
|
+
const matches: Array<{ block: Block; audio: NarrationAudioInfo }> = [];
|
|
243
242
|
const flat = flattenForAudio(blocks);
|
|
244
243
|
|
|
245
244
|
for (const block of flat) {
|
|
246
245
|
const audioRef = block.templateOverrides?.audio;
|
|
247
246
|
if (!audioRef) continue;
|
|
248
247
|
|
|
249
|
-
// Find the
|
|
250
|
-
const
|
|
248
|
+
// Find the narration file by filename (exact or suffix match).
|
|
249
|
+
const audio = audioFiles.find(
|
|
251
250
|
(m) => m.filename === audioRef || m.path === audioRef || m.path.endsWith(`/${audioRef}`),
|
|
252
251
|
);
|
|
253
252
|
|
|
254
|
-
if (
|
|
255
|
-
matches.push({ block,
|
|
253
|
+
if (audio) {
|
|
254
|
+
matches.push({ block, audio });
|
|
256
255
|
}
|
|
257
256
|
}
|
|
258
257
|
|
|
@@ -266,27 +265,27 @@ const MIN_CONTENT_SIMILARITY = 0.35;
|
|
|
266
265
|
const MIN_FILENAME_SIMILARITY = 0.5;
|
|
267
266
|
|
|
268
267
|
/**
|
|
269
|
-
* Auto-match
|
|
268
|
+
* Auto-match narration files to blocks by content similarity.
|
|
270
269
|
* Uses timing.json sourceText when available, falls back to filename matching.
|
|
271
270
|
*/
|
|
272
271
|
function autoMatchBlocks(
|
|
273
272
|
blocks: Block[],
|
|
274
|
-
|
|
275
|
-
): Array<{ block: Block;
|
|
273
|
+
audioFiles: NarrationAudioInfo[],
|
|
274
|
+
): Array<{ block: Block; audio: NarrationAudioInfo; score: number }> {
|
|
276
275
|
const flat = flattenForAudio(blocks);
|
|
277
|
-
if (flat.length === 0 ||
|
|
276
|
+
if (flat.length === 0 || audioFiles.length === 0) return [];
|
|
278
277
|
|
|
279
|
-
// Score all (
|
|
278
|
+
// Score all (audio, block) pairs.
|
|
280
279
|
const scoredPairs: Array<{
|
|
281
280
|
block: Block;
|
|
282
281
|
blockIndex: number;
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
audio: NarrationAudioInfo;
|
|
283
|
+
audioIndex: number;
|
|
285
284
|
score: number;
|
|
286
285
|
}> = [];
|
|
287
286
|
|
|
288
|
-
for (let
|
|
289
|
-
const
|
|
287
|
+
for (let audioIndex = 0; audioIndex < audioFiles.length; audioIndex++) {
|
|
288
|
+
const audio = audioFiles[audioIndex];
|
|
290
289
|
|
|
291
290
|
for (let bi = 0; bi < flat.length; bi++) {
|
|
292
291
|
const block = flat[bi];
|
|
@@ -296,14 +295,14 @@ function autoMatchBlocks(
|
|
|
296
295
|
let score = 0;
|
|
297
296
|
|
|
298
297
|
// Primary: content similarity via timing.json sourceText
|
|
299
|
-
if (
|
|
300
|
-
score = scoreTextSimilarity(
|
|
298
|
+
if (audio.timing?.sourceText) {
|
|
299
|
+
score = scoreTextSimilarity(audio.timing.sourceText, blockText);
|
|
301
300
|
}
|
|
302
301
|
|
|
303
302
|
// Fallback: filename-based matching
|
|
304
303
|
if (score < MIN_CONTENT_SIMILARITY && block.title) {
|
|
305
304
|
const titleSlug = slugify(block.title);
|
|
306
|
-
const fileSlug = slugify(stripAudioExtension(
|
|
305
|
+
const fileSlug = slugify(stripAudioExtension(audio.filename));
|
|
307
306
|
// Check if the title slug appears in the filename slug
|
|
308
307
|
if (fileSlug.includes(titleSlug) && titleSlug.length >= 3) {
|
|
309
308
|
score = Math.max(score, MIN_FILENAME_SIMILARITY + 0.1);
|
|
@@ -311,22 +310,22 @@ function autoMatchBlocks(
|
|
|
311
310
|
}
|
|
312
311
|
|
|
313
312
|
if (score >= MIN_CONTENT_SIMILARITY) {
|
|
314
|
-
scoredPairs.push({ block, blockIndex: bi,
|
|
313
|
+
scoredPairs.push({ block, blockIndex: bi, audio, audioIndex, score });
|
|
315
314
|
}
|
|
316
315
|
}
|
|
317
316
|
}
|
|
318
317
|
|
|
319
|
-
// Greedy assignment: highest score first, each
|
|
318
|
+
// Greedy assignment: highest score first, each audio file and block at most once.
|
|
320
319
|
scoredPairs.sort((a, b) => b.score - a.score);
|
|
321
320
|
const usedBlocks = new Set<number>();
|
|
322
|
-
const
|
|
323
|
-
const matches: Array<{ block: Block;
|
|
321
|
+
const usedAudio = new Set<number>();
|
|
322
|
+
const matches: Array<{ block: Block; audio: NarrationAudioInfo; score: number }> = [];
|
|
324
323
|
|
|
325
324
|
for (const pair of scoredPairs) {
|
|
326
|
-
if (usedBlocks.has(pair.blockIndex) ||
|
|
325
|
+
if (usedBlocks.has(pair.blockIndex) || usedAudio.has(pair.audioIndex)) continue;
|
|
327
326
|
usedBlocks.add(pair.blockIndex);
|
|
328
|
-
|
|
329
|
-
matches.push({ block: pair.block,
|
|
327
|
+
usedAudio.add(pair.audioIndex);
|
|
328
|
+
matches.push({ block: pair.block, audio: pair.audio, score: pair.score });
|
|
330
329
|
}
|
|
331
330
|
|
|
332
331
|
return matches;
|
|
@@ -344,22 +343,35 @@ function autoMatchBlocks(
|
|
|
344
343
|
* assigned to the correct `audioSegment` indices.
|
|
345
344
|
*
|
|
346
345
|
* @param doc - The source Doc (not mutated).
|
|
347
|
-
* @param container - ContentContainer with
|
|
346
|
+
* @param container - ContentContainer with narration and timing files.
|
|
348
347
|
* @returns New Doc with audio segments resolved, or original doc if no matches.
|
|
349
348
|
*/
|
|
350
349
|
export async function resolveAudioMapping(doc: Doc, container: ContentContainer): Promise<Doc> {
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
350
|
+
// Step 0: a document-anchored narration take (preamble `{[audio src=…
|
|
351
|
+
// anchor=document]}` + v3 timing sidecar) re-times the block timeline
|
|
352
|
+
// to the recorded voice. See applyNarrationTiming for precedence.
|
|
353
|
+
const narration = await applyNarrationTiming(doc, container);
|
|
354
|
+
doc = narration.doc;
|
|
355
|
+
|
|
356
|
+
const audioFiles = await discoverNarrationAudio(container);
|
|
357
|
+
|
|
358
|
+
// Files already playing through the media schedule (documentMedia /
|
|
359
|
+
// block clips) must not ALSO be auto-mapped into audio.segments —
|
|
360
|
+
// that would play the narration twice.
|
|
361
|
+
const scheduledSrcs = new Set(resolveMediaSchedule(doc).map((clip) => clip.src));
|
|
362
|
+
const available = audioFiles.filter((f) => !scheduledSrcs.has(f.path));
|
|
363
|
+
if (available.length === 0) return doc;
|
|
354
364
|
|
|
355
365
|
// Try annotations first
|
|
356
|
-
let matches = resolveFromAnnotations(doc.blocks,
|
|
366
|
+
let matches = resolveFromAnnotations(doc.blocks, available);
|
|
357
367
|
|
|
358
|
-
// Fall back to auto-matching if no annotations found
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
// Fall back to auto-matching if no annotations found. When narration
|
|
369
|
+
// timing was applied, content auto-matching is skipped — the take owns
|
|
370
|
+
// the timeline, and fuzzy matches against other files would fight it.
|
|
371
|
+
if (matches.length === 0 && !narration.applied) {
|
|
372
|
+
matches = autoMatchBlocks(doc.blocks, available).map((m) => ({
|
|
361
373
|
block: m.block,
|
|
362
|
-
|
|
374
|
+
audio: m.audio,
|
|
363
375
|
}));
|
|
364
376
|
}
|
|
365
377
|
|
|
@@ -377,11 +389,11 @@ export async function resolveAudioMapping(doc: Doc, container: ContentContainer)
|
|
|
377
389
|
let startTime = 0;
|
|
378
390
|
|
|
379
391
|
for (let i = 0; i < matches.length; i++) {
|
|
380
|
-
const { block,
|
|
381
|
-
const duration =
|
|
392
|
+
const { block, audio } = matches[i];
|
|
393
|
+
const duration = audio.timing?.duration ?? 30; // Fallback duration if no timing
|
|
382
394
|
|
|
383
395
|
segments.push({
|
|
384
|
-
src:
|
|
396
|
+
src: audio.path,
|
|
385
397
|
name: block.title ?? block.id,
|
|
386
398
|
duration,
|
|
387
399
|
startTime,
|
|
@@ -397,7 +409,9 @@ export async function resolveAudioMapping(doc: Doc, container: ContentContainer)
|
|
|
397
409
|
return {
|
|
398
410
|
...doc,
|
|
399
411
|
blocks: newBlocks,
|
|
400
|
-
duration
|
|
412
|
+
// A narration take owns the overall duration; explicit per-block
|
|
413
|
+
// segments only ever extend it.
|
|
414
|
+
duration: narration.applied ? Math.max(doc.duration, startTime) : startTime,
|
|
401
415
|
audio: { segments },
|
|
402
416
|
};
|
|
403
417
|
}
|