@lincyaw/kernel 0.0.0-stage → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +32 -2
- package/package.json +41 -3
- package/src/access.ts +18 -0
- package/src/anchors.ts +58 -0
- package/src/authoring.ts +56 -0
- package/src/blocks.ts +19 -0
- package/src/checks.ts +35 -0
- package/src/component-code.ts +6 -0
- package/src/contracts/agent.ts +76 -0
- package/src/contracts/anchors.ts +46 -0
- package/src/contracts/authoring-actions.ts +60 -0
- package/src/contracts/authoring-clients.ts +213 -0
- package/src/contracts/authoring.ts +180 -0
- package/src/contracts/checks.ts +29 -0
- package/src/contracts/classroom.ts +49 -0
- package/src/contracts/diagnostics.ts +37 -0
- package/src/contracts/document-audio.ts +150 -0
- package/src/contracts/document-comments.ts +31 -0
- package/src/contracts/document-history.ts +63 -0
- package/src/contracts/document-image.ts +32 -0
- package/src/contracts/document-reads.ts +57 -0
- package/src/contracts/document-voice.ts +44 -0
- package/src/contracts/document.ts +240 -0
- package/src/contracts/execution.ts +118 -0
- package/src/contracts/locale.ts +3 -0
- package/src/contracts/offline.ts +2 -0
- package/src/contracts/operations.ts +8 -0
- package/src/contracts/plugin.ts +183 -0
- package/src/contracts/presenter-panels.ts +17 -0
- package/src/contracts/preview.ts +386 -0
- package/src/contracts/production-assets.ts +61 -0
- package/src/contracts/production.ts +109 -0
- package/src/contracts/projects.ts +59 -0
- package/src/contracts/server.ts +108 -0
- package/src/contracts/service.ts +8 -0
- package/src/contracts/speech-settings.ts +16 -0
- package/src/contracts/speech.ts +149 -0
- package/src/contracts/steps.ts +15 -0
- package/src/contracts/transcription-adoption.ts +79 -0
- package/src/contracts/transcription.ts +112 -0
- package/src/contracts/voice.ts +97 -0
- package/src/contracts/voiceover-execution.ts +68 -0
- package/src/contracts/workspace.ts +458 -0
- package/src/contracts.ts +35 -0
- package/src/diagnostics.ts +7 -0
- package/src/document-authoring-actions.ts +47 -0
- package/src/document-caption-timings.ts +64 -0
- package/src/document-colors.ts +25 -0
- package/src/document-comments.ts +118 -0
- package/src/document-components.ts +251 -0
- package/src/document-operations.ts +132 -0
- package/src/document-pages.ts +215 -0
- package/src/document-reads.ts +81 -0
- package/src/document-schema.ts +85 -0
- package/src/document-voices.ts +153 -0
- package/src/document.ts +252 -0
- package/src/durations.ts +7 -0
- package/src/errors.ts +15 -0
- package/src/events.ts +32 -0
- package/src/execution.ts +25 -0
- package/src/fingerprint.ts +31 -0
- package/src/follow.ts +33 -0
- package/src/git.ts +12 -0
- package/src/html.ts +41 -0
- package/src/index.ts +82 -0
- package/src/ir.ts +367 -0
- package/src/jupyter-kernel.ts +9 -0
- package/src/kernel.ts +190 -0
- package/src/keyboard.ts +43 -0
- package/src/locale.ts +13 -0
- package/src/lock.ts +81 -0
- package/src/logging.ts +11 -0
- package/src/lower-myst.ts +312 -0
- package/src/node/assets.ts +23 -0
- package/src/node/authoring-actions.ts +50 -0
- package/src/node/authoring-clients.ts +14 -0
- package/src/node/document-audio.ts +22 -0
- package/src/node/document-history.ts +11 -0
- package/src/node/document-image.ts +8 -0
- package/src/node/document-reads.ts +10 -0
- package/src/node/document-voice.ts +7 -0
- package/src/node/ffmpeg.ts +29 -0
- package/src/node/file-version.ts +6 -0
- package/src/node/fonts.ts +22 -0
- package/src/node/index.ts +29 -0
- package/src/node/installed-packages.ts +11 -0
- package/src/node/lines.ts +24 -0
- package/src/node/locale.ts +7 -0
- package/src/node/network.ts +11 -0
- package/src/node/operator-speech.ts +91 -0
- package/src/node/packages.ts +74 -0
- package/src/node/paths.ts +7 -0
- package/src/node/projects.ts +7 -0
- package/src/node/resources.ts +86 -0
- package/src/node/revision.ts +6 -0
- package/src/node/run-directory.ts +70 -0
- package/src/node/strip-types.mjs +15 -0
- package/src/node/transcription-adoption.ts +9 -0
- package/src/node/transcription.ts +10 -0
- package/src/node/voiceover-routes.ts +73 -0
- package/src/node/voiceover.ts +199 -0
- package/src/page-source.ts +359 -0
- package/src/parameters.ts +81 -0
- package/src/preview-edit.ts +18 -0
- package/src/preview-events.ts +57 -0
- package/src/preview-frame.ts +1220 -0
- package/src/props.ts +17 -0
- package/src/published.ts +11 -0
- package/src/push-stream.ts +67 -0
- package/src/reading.ts +96 -0
- package/src/recording.ts +38 -0
- package/src/renderer-components.ts +30 -0
- package/src/requests.ts +28 -0
- package/src/responses.ts +49 -0
- package/src/run-outputs.ts +15 -0
- package/src/runner.py +454 -0
- package/src/services.ts +146 -0
- package/src/slides.ts +36 -0
- package/src/speech-words.ts +76 -0
- package/src/speech.ts +13 -0
- package/src/step-keys.ts +38 -0
- package/src/steps.ts +7 -0
- package/src/transcription-adoption.ts +105 -0
- package/src/uri-decoding.ts +8 -0
- package/src/urls.ts +13 -0
- package/src/views.ts +47 -0
- package/src/voice.ts +86 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { isRecord } from '../errors.ts'
|
|
2
|
+
import type { ProductionBinding, ProductionPlaybackCommand, ProductionPlaybackState } from './production.ts'
|
|
3
|
+
|
|
4
|
+
export const PREVIEW_PROTOCOL = 1
|
|
5
|
+
|
|
6
|
+
export const PREVIEW_NAME = 'presmium-preview'
|
|
7
|
+
|
|
8
|
+
export const PREVIEW_PARAM = 'presmium-preview'
|
|
9
|
+
|
|
10
|
+
/** The workbench's appearance when it loads a preview (`dark` or `light`), so that the page's first paint matches it. */
|
|
11
|
+
export const PREVIEW_APPEARANCE_PARAM = 'presmium-appearance'
|
|
12
|
+
|
|
13
|
+
export const PREVIEW_REVISION = 'presmiumRevision'
|
|
14
|
+
|
|
15
|
+
export const AUTHOR_MARKS_CLASS = 'pm-author-marks'
|
|
16
|
+
|
|
17
|
+
/** Read-only, version-bound control of an individual document widget. */
|
|
18
|
+
export interface BlockPreviewCommand {
|
|
19
|
+
type: 'presmium:block-preview-command'
|
|
20
|
+
document: string
|
|
21
|
+
blockId: string
|
|
22
|
+
revision: string
|
|
23
|
+
sectionId: string
|
|
24
|
+
stepId: string
|
|
25
|
+
action: 'seek' | 'pause' | 'sync'
|
|
26
|
+
/** The parent's current navigation intent, including repeat visits to the same step. */
|
|
27
|
+
navigation?: string
|
|
28
|
+
/** Omitted and document initial/reload seeks show the completed step; start is an explicit playback boundary. */
|
|
29
|
+
at?: 'start' | 'end'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BlockPreviewPosition {
|
|
33
|
+
type: 'presmium:block-preview-position'
|
|
34
|
+
document: string
|
|
35
|
+
blockId: string
|
|
36
|
+
revision: string
|
|
37
|
+
sectionId: string
|
|
38
|
+
stepId: string
|
|
39
|
+
navigation: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BlockPreviewPlaybackIdentity {
|
|
43
|
+
document: string
|
|
44
|
+
blockId: string
|
|
45
|
+
revision: string
|
|
46
|
+
request: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BlockPreviewPlaybackRequest extends BlockPreviewPlaybackIdentity {
|
|
50
|
+
type: 'presmium:block-preview-playback-request'
|
|
51
|
+
sectionId: string
|
|
52
|
+
stepId: string
|
|
53
|
+
navigation: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BlockPreviewPlaybackGranted extends BlockPreviewPlaybackIdentity {
|
|
57
|
+
type: 'presmium:block-preview-playback-granted'
|
|
58
|
+
error?: string
|
|
59
|
+
navigation: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BlockPreviewPlaybackCancelled extends BlockPreviewPlaybackIdentity {
|
|
63
|
+
type: 'presmium:block-preview-playback-cancelled'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PreviewAnchor {
|
|
67
|
+
section: string
|
|
68
|
+
block?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A box in the preview page's viewport, in CSS pixels. */
|
|
72
|
+
export interface PreviewRect {
|
|
73
|
+
x: number
|
|
74
|
+
y: number
|
|
75
|
+
width: number
|
|
76
|
+
height: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A point in the preview page's viewport, in CSS pixels. */
|
|
80
|
+
export interface PreviewPoint {
|
|
81
|
+
x: number
|
|
82
|
+
y: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PreviewPin {
|
|
86
|
+
id: string
|
|
87
|
+
section: string
|
|
88
|
+
block: string
|
|
89
|
+
label: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface PreviewPosition {
|
|
93
|
+
section: string
|
|
94
|
+
step: number
|
|
95
|
+
trail: string[]
|
|
96
|
+
view?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Source-oriented edits from the block editor. Opaque blocks retain their source verbatim. */
|
|
100
|
+
export interface PreviewEditedBlock {
|
|
101
|
+
id: string
|
|
102
|
+
source?: string
|
|
103
|
+
at?: number
|
|
104
|
+
markdown?: string
|
|
105
|
+
scene?: string
|
|
106
|
+
durations?: number[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The source of one anchored block, read by the workbench from its current IR binding. */
|
|
110
|
+
export interface PreviewSource {
|
|
111
|
+
part: 'definition' | 'block'
|
|
112
|
+
path: string
|
|
113
|
+
language: string
|
|
114
|
+
content: string
|
|
115
|
+
version: string
|
|
116
|
+
shared: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Fixed live-viewer channels; no arbitrary upstream URL crosses the preview boundary. */
|
|
120
|
+
export const PREVIEW_PRESENTATION_PARAM = 'presmium-presentation'
|
|
121
|
+
/** `playback`: the workbench plays this page, so Space and the left and right arrows pressed in it go to the workbench. */
|
|
122
|
+
export const PREVIEW_KEYS_PARAM = 'presmium-keys'
|
|
123
|
+
export const PREVIEW_EVENTS_PARAM = 'presmium-events'
|
|
124
|
+
export type PreviewEventTarget =
|
|
125
|
+
| { topic: 'viewer-site', document: string, app: string, lang: string, form: 'web' | 'slides' }
|
|
126
|
+
| { topic: 'viewer-control', document: string, window: string }
|
|
127
|
+
export type PreviewEventMessage = { type: 'open' | 'error' } | { type: 'event', event: string, data: string }
|
|
128
|
+
export type PreviewEventSubscription = (target: PreviewEventTarget, received: (message: PreviewEventMessage) => void) => () => void
|
|
129
|
+
|
|
130
|
+
export function isPreviewEventTarget(value: unknown): value is PreviewEventTarget {
|
|
131
|
+
if (!isRecord(value) || !(typeof value.document === 'string' && value.document.length <= 1024)) return false
|
|
132
|
+
if (value.topic === 'viewer-site') return Object.keys(value).every(key => ['topic', 'document', 'app', 'lang', 'form'].includes(key))
|
|
133
|
+
&& bounded(value.app, 200) && (typeof value.lang === 'string' && value.lang.length <= 80) && (value.form === 'web' || value.form === 'slides')
|
|
134
|
+
return value.topic === 'viewer-control' && Object.keys(value).every(key => ['topic', 'document', 'window'].includes(key))
|
|
135
|
+
&& typeof value.window === 'string' && /^[a-zA-Z0-9-]{1,80}$/.test(value.window)
|
|
136
|
+
}
|
|
137
|
+
const eventMessage = (value: unknown): value is PreviewEventMessage => isRecord(value) && (value.type === 'open' || value.type === 'error'
|
|
138
|
+
|| value.type === 'event' && bounded(value.event, 40) && (typeof value.data === 'string' && value.data.length <= (2 << 20)))
|
|
139
|
+
|
|
140
|
+
export type PreviewCommand =
|
|
141
|
+
| { type: 'presentation-confirmed', revision: string }
|
|
142
|
+
| { type: 'events-ready', connection: string }
|
|
143
|
+
| { type: 'events-message', connection: string, id: string, message: PreviewEventMessage }
|
|
144
|
+
| { type: 'production-playback', request: number, control: ProductionPlaybackCommand }
|
|
145
|
+
| { type: 'document-history', action: 'undo' | 'redo', request: number }
|
|
146
|
+
| { type: 'document-history-reset' }
|
|
147
|
+
| { type: 'pause-playback', request: number }
|
|
148
|
+
| { type: 'playback-granted', request: number }
|
|
149
|
+
| { type: 'flush', request: number }
|
|
150
|
+
| { type: 'edit-result', target: PreviewAnchor, request: number, revision?: string, binding?: string, error?: string }
|
|
151
|
+
| { type: 'blocks-result', section: string, request: number, revision?: string, error?: string }
|
|
152
|
+
| { type: 'source-result', section: string, block: string, request: number, source?: PreviewSource, error?: string }
|
|
153
|
+
| { type: 'source-saved', section: string, block: string, request: number, sourceVersion?: string, error?: string }
|
|
154
|
+
| { type: 'source-created', section: string, request: number, block?: string, error?: string }
|
|
155
|
+
| { type: 'page-insert', section: string, id: string, request: number }
|
|
156
|
+
| { type: 'page-created', section: string, request: number, page?: string, error?: string }
|
|
157
|
+
| { type: 'go', section: string, step?: number, trail?: string[], view?: string }
|
|
158
|
+
| { type: 'highlight', target: PreviewAnchor | null }
|
|
159
|
+
| { type: 'pick', on: boolean }
|
|
160
|
+
| { type: 'pins', targets: PreviewPin[] }
|
|
161
|
+
| { type: 'marks', on: boolean }
|
|
162
|
+
| { type: 'appearance', dark: boolean }
|
|
163
|
+
| { type: 'activity', active: boolean, request: number }
|
|
164
|
+
/** Edits the text of a block (a section's heading when there is no block) in place, the caret at `at`; null ends editing and puts the text back. */
|
|
165
|
+
| { type: 'edit', target: PreviewAnchor | null, at?: PreviewPoint, revision?: string, binding?: string }
|
|
166
|
+
/** Makes the blocks (or sections) blink a few times, the first brought into view: what the AI just changed. */
|
|
167
|
+
| { type: 'flash', targets: PreviewAnchor[] }
|
|
168
|
+
|
|
169
|
+
export type PreviewCommandType = PreviewCommand['type']
|
|
170
|
+
|
|
171
|
+
/** A workbench command the page's keyboard asks for while it has the focus. */
|
|
172
|
+
export type PreviewShortcut = 'undo' | 'redo' | 'dismiss' | 'save' | 'play-pause' | 'previous' | 'next'
|
|
173
|
+
const PREVIEW_SHORTCUTS: readonly string[] = ['undo', 'redo', 'dismiss', 'save', 'play-pause', 'previous', 'next'] satisfies readonly PreviewShortcut[]
|
|
174
|
+
|
|
175
|
+
export type PreviewReport =
|
|
176
|
+
| { type: 'events-subscribe', connection: string, id: string, target: PreviewEventTarget }
|
|
177
|
+
| { type: 'events-unsubscribe', connection: string, id: string }
|
|
178
|
+
| { type: 'presentation-action', action: 'refresh' | 'exit' }
|
|
179
|
+
| { type: 'production-playback-state', state: ProductionPlaybackState }
|
|
180
|
+
| { type: 'production-playback-result', request: number, binding: ProductionBinding, state?: ProductionPlaybackState, error?: string }
|
|
181
|
+
| { type: 'document-history-state', section?: string, undo: boolean, redo: boolean }
|
|
182
|
+
| { type: 'document-history-result', request: number, handled: boolean, error?: string }
|
|
183
|
+
| { type: 'playback-paused', request: number, error?: string }
|
|
184
|
+
| { type: 'playback-request', request: number }
|
|
185
|
+
| { type: 'flushed', request: number, error?: string }
|
|
186
|
+
| { type: 'blocks-edited', section: string, request: number, revision: string, blocks: PreviewEditedBlock[] }
|
|
187
|
+
| { type: 'source-read', section: string, block: string, request: number, part?: 'definition' | 'block' }
|
|
188
|
+
| { type: 'source-save', section: string, block: string, request: number, path: string, base: string, content: string, part?: 'definition' | 'block' }
|
|
189
|
+
| { type: 'source-create', section: string, id: string, after?: string, request: number, revision: string }
|
|
190
|
+
| { type: 'source-draft', dirty: boolean }
|
|
191
|
+
/** Aggregate unsaved body blocks, independent of generated-source and narration drafts. */
|
|
192
|
+
| { type: 'body-draft', dirty: boolean }
|
|
193
|
+
/** Aggregate unacknowledged native title and complex-text drafts. */
|
|
194
|
+
| { type: 'native-draft', dirty: boolean }
|
|
195
|
+
| { type: 'page-create', section: string, id: string, request: number, revision: string }
|
|
196
|
+
| { type: 'page-create-error', request: number, error: string }
|
|
197
|
+
| { type: 'narrate', section: string, step: number, navigate?: boolean }
|
|
198
|
+
| { type: 'ready', renderer: 'web' | 'slides', document: string, revision?: string, capabilities: PreviewCommandType[], eventConnection?: string }
|
|
199
|
+
| ({ type: 'position' } & PreviewPosition)
|
|
200
|
+
| { type: 'picked', section: string, block?: string, step?: number, trail?: string[], rect?: PreviewRect, point?: PreviewPoint, clicks?: number }
|
|
201
|
+
| { type: 'selection', selection: (PreviewAnchor & { text: string, rect?: PreviewRect, obstacles?: PreviewRect[] }) | null }
|
|
202
|
+
/** Where the picked block or the selection is after the page scrolled or resized; null when it is out of sight. */
|
|
203
|
+
| { type: 'rect', rect: PreviewRect | null, obstacles?: PreviewRect[] }
|
|
204
|
+
/** Where the blocks the AI changed are in the preview's viewport while they blink, and again as the page scrolls (at most 16). */
|
|
205
|
+
| { type: 'flashed', rects: PreviewRect[] }
|
|
206
|
+
| { type: 'editing', target: PreviewAnchor, active: boolean }
|
|
207
|
+
/** The text of a block edited in place, before and after (its `textContent`); sent only when it changed. */
|
|
208
|
+
| { type: 'edited', target: PreviewAnchor, before: string, after: string, request?: number, revision?: string, binding?: string }
|
|
209
|
+
| { type: 'updated', revision?: string }
|
|
210
|
+
| { type: 'activity', active: boolean, request: number, revision?: string, error?: string }
|
|
211
|
+
| { type: 'report', level: 'error' | 'warning', plugin?: string, message: string, resolved?: boolean }
|
|
212
|
+
| { type: 'shortcut', command: PreviewShortcut }
|
|
213
|
+
|
|
214
|
+
export type PreviewEnvelope<M> = M & { protocol: typeof PREVIEW_NAME, version: number, nonce: string }
|
|
215
|
+
|
|
216
|
+
export function previewEnvelope<M extends PreviewCommand | PreviewReport>(nonce: string, message: M): PreviewEnvelope<M> {
|
|
217
|
+
return { ...message, protocol: PREVIEW_NAME, version: PREVIEW_PROTOCOL, nonce }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const text = (value: unknown): value is string => typeof value === 'string'
|
|
221
|
+
const optional = <T>(value: unknown, is: (v: unknown) => v is T) => value === undefined || is(value)
|
|
222
|
+
const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)
|
|
223
|
+
const positive = (value: unknown): value is number => finite(value) && value > 0
|
|
224
|
+
const nonnegative = (value: unknown): value is number => finite(value) && value >= 0
|
|
225
|
+
const productionId = (value: unknown): value is string => typeof value === 'string' && value.length > 0 && value.length <= 200
|
|
226
|
+
const productionBinding = (value: unknown): value is ProductionBinding => isRecord(value) && isRecord(value.source)
|
|
227
|
+
&& typeof value.source.documentId === 'string' && value.source.documentId.length > 0 && value.source.documentId.length <= 200
|
|
228
|
+
&& typeof value.source.version === 'string' && /^[\da-f]{20}$/.test(value.source.version)
|
|
229
|
+
&& typeof value.source.fingerprint === 'string' && /^[\da-f]{16}$/.test(value.source.fingerprint)
|
|
230
|
+
&& typeof value.irRevision === 'string' && value.irRevision.length > 0 && value.irRevision.length <= 200
|
|
231
|
+
&& typeof value.planId === 'string' && value.planId.length > 0 && value.planId.length <= 200
|
|
232
|
+
const productionState = (value: unknown): value is ProductionPlaybackState => {
|
|
233
|
+
if (!isRecord(value) || !productionBinding(value.binding) || !['loading', 'ready', 'estimated', 'unavailable', 'failed'].includes(String(value.status)) || !optional(value.error, text)) return false
|
|
234
|
+
if (value.position === undefined) return true
|
|
235
|
+
const position = value.position
|
|
236
|
+
return isRecord(position) && nonnegative(position.timeMs) && nonnegative(position.durationMs) && position.timeMs <= position.durationMs
|
|
237
|
+
&& optional(position.stepTimeMs, nonnegative) && optional(position.sectionId, text) && optional(position.stepId, text) && optional(position.waitingFor, text)
|
|
238
|
+
&& typeof position.playing === 'boolean' && positive(position.rate) && typeof position.trying === 'boolean'
|
|
239
|
+
}
|
|
240
|
+
const productionCommand = (value: unknown): value is ProductionPlaybackCommand => isRecord(value) && productionBinding(value.binding)
|
|
241
|
+
&& (value.action === 'load' || value.action === 'pause' || value.action === 'play' || value.action === 'seek' && nonnegative(value.timeMs)
|
|
242
|
+
|| value.action === 'step' && productionId(value.sectionId) && productionId(value.stepId) && optional(value.utteranceId, productionId) || value.action === 'rate' && positive(value.rate))
|
|
243
|
+
const step = (value: unknown): value is number => Number.isInteger(value) && (value as number) >= 0
|
|
244
|
+
const request = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0
|
|
245
|
+
const strings = (value: unknown): value is string[] => Array.isArray(value) && value.length <= 10_000 && value.every(text)
|
|
246
|
+
const anchor = (value: unknown): value is PreviewAnchor => isRecord(value) && text(value.section) && optional(value.block, text)
|
|
247
|
+
const length = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)
|
|
248
|
+
const rect = (value: unknown): value is PreviewRect => isRecord(value) && length(value.x) && length(value.y) && length(value.width) && length(value.height) && value.width >= 0 && value.height >= 0
|
|
249
|
+
const obstacles = (value: unknown): value is PreviewRect[] => Array.isArray(value) && value.length <= 16 && value.every(rect)
|
|
250
|
+
const point = (value: unknown): value is PreviewPoint => isRecord(value) && length(value.x) && length(value.y)
|
|
251
|
+
const clicks = (value: unknown): value is number => Number.isInteger(value) && (value as number) >= 1 && (value as number) <= 3
|
|
252
|
+
const toAnchor = (value: PreviewAnchor): PreviewAnchor => ({ section: value.section, ...value.block ? { block: value.block } : {} })
|
|
253
|
+
const toRect = (value: PreviewRect): PreviewRect => ({ x: value.x, y: value.y, width: value.width, height: value.height })
|
|
254
|
+
const toPoint = (value: PreviewPoint): PreviewPoint => ({ x: value.x, y: value.y })
|
|
255
|
+
const MAX_EDITED = 200_000
|
|
256
|
+
const bounded = (value: unknown, max: number): value is string => text(value) && value.length > 0 && value.length <= max
|
|
257
|
+
const editBaseline = (m: Record<string, unknown>) => m.revision === undefined && m.binding === undefined || bounded(m.revision, 200) && bounded(m.binding, 200)
|
|
258
|
+
const editedRequest = (m: Record<string, unknown>) => m.request === undefined && m.revision === undefined && m.binding === undefined || request(m.request) && bounded(m.revision, 200) && bounded(m.binding, 200)
|
|
259
|
+
const sourceTarget = (m: Record<string, unknown>) => bounded(m.section, 200) && bounded(m.block, 200) && request(m.request)
|
|
260
|
+
const sourcePart = (value: unknown): value is 'definition' | 'block' => value === 'definition' || value === 'block'
|
|
261
|
+
const pageId = (value: unknown): value is string => text(value) && /^page-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value)
|
|
262
|
+
const sourceValue = (value: unknown): value is PreviewSource => isRecord(value) && sourcePart(value.part) && bounded(value.path, 4096) && bounded(value.language, 100) && text(value.content) && value.content.length <= MAX_EDITED && bounded(value.version, 200) && request(value.shared) && value.shared > 0
|
|
263
|
+
|
|
264
|
+
export function toBlockPreviewCommand(value: unknown): BlockPreviewCommand | undefined {
|
|
265
|
+
if (!isRecord(value) || value.type !== 'presmium:block-preview-command' || !text(value.document)
|
|
266
|
+
|| !text(value.blockId) || !value.blockId || !text(value.revision) || !value.revision
|
|
267
|
+
|| !text(value.sectionId) || !value.sectionId || !text(value.stepId) || !value.stepId
|
|
268
|
+
|| value.action !== 'seek' && value.action !== 'pause' && value.action !== 'sync'
|
|
269
|
+
|| (value.action === 'pause' ? !optional(value.navigation, value => bounded(value, 200)) : !bounded(value.navigation, 200))
|
|
270
|
+
|| value.at !== undefined && value.at !== 'start' && value.at !== 'end') return undefined
|
|
271
|
+
return { type: value.type, document: value.document, blockId: value.blockId, revision: value.revision, sectionId: value.sectionId, stepId: value.stepId, action: value.action, ...value.navigation !== undefined ? { navigation: value.navigation as string } : {}, ...value.at !== undefined ? { at: value.at } : {} }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function toBlockPreviewPosition(value: unknown): BlockPreviewPosition | undefined {
|
|
275
|
+
if (!isRecord(value) || value.type !== 'presmium:block-preview-position' || !text(value.document)
|
|
276
|
+
|| !bounded(value.blockId, 4096) || !bounded(value.revision, 200) || !bounded(value.sectionId, 4096)
|
|
277
|
+
|| !bounded(value.stepId, 4096) || !bounded(value.navigation, 200)) return undefined
|
|
278
|
+
return { type: value.type, document: value.document, blockId: value.blockId, revision: value.revision, sectionId: value.sectionId, stepId: value.stepId, navigation: value.navigation }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const playbackIdentity = (value: unknown): value is Record<string, unknown> & BlockPreviewPlaybackIdentity => isRecord(value) && text(value.document) && bounded(value.blockId, 4096) && bounded(value.revision, 200) && request(value.request)
|
|
282
|
+
const toPlaybackIdentity = (value: BlockPreviewPlaybackIdentity): BlockPreviewPlaybackIdentity => ({ document: value.document, blockId: value.blockId, revision: value.revision, request: value.request })
|
|
283
|
+
|
|
284
|
+
export function toBlockPreviewPlaybackRequest(value: unknown): BlockPreviewPlaybackRequest | undefined {
|
|
285
|
+
return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-request' && bounded(value.sectionId, 4096) && bounded(value.stepId, 4096) && bounded(value.navigation, 200)
|
|
286
|
+
? { ...toPlaybackIdentity(value), type: value.type, sectionId: value.sectionId, stepId: value.stepId, navigation: value.navigation } : undefined
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function toBlockPreviewPlaybackGranted(value: unknown): BlockPreviewPlaybackGranted | undefined {
|
|
290
|
+
return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-granted' && optional(value.error, text) && bounded(value.navigation, 200)
|
|
291
|
+
? { ...toPlaybackIdentity(value), type: value.type, navigation: value.navigation, ...value.error !== undefined ? { error: value.error as string } : {} } : undefined
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function toBlockPreviewPlaybackCancelled(value: unknown): BlockPreviewPlaybackCancelled | undefined {
|
|
295
|
+
return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-cancelled' ? { ...toPlaybackIdentity(value), type: value.type } : undefined
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function isPreviewEnvelope(data: unknown, nonce: string): data is PreviewEnvelope<{ type: string }> {
|
|
299
|
+
return isRecord(data) && data.protocol === PREVIEW_NAME && data.nonce === nonce && typeof data.version === 'number' && text(data.type)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function toPreviewCommand(data: unknown, nonce: string): PreviewCommand | undefined {
|
|
303
|
+
if (!isPreviewEnvelope(data, nonce) || data.version !== PREVIEW_PROTOCOL) return undefined
|
|
304
|
+
const m = data as Record<string, unknown>
|
|
305
|
+
switch (m.type) {
|
|
306
|
+
case 'presentation-confirmed': return productionId(m.revision) ? { type: 'presentation-confirmed', revision: m.revision } : undefined
|
|
307
|
+
case 'events-ready': return productionId(m.connection) ? { type: 'events-ready', connection: m.connection } : undefined
|
|
308
|
+
case 'events-message': return productionId(m.connection) && productionId(m.id) && eventMessage(m.message) ? { type: 'events-message', connection: m.connection, id: m.id, message: m.message } : undefined
|
|
309
|
+
case 'production-playback': return request(m.request) && productionCommand(m.control) ? { type: 'production-playback', request: m.request, control: m.control } : undefined
|
|
310
|
+
case 'document-history': return request(m.request) && (m.action === 'undo' || m.action === 'redo') ? { type: 'document-history', request: m.request, action: m.action } : undefined
|
|
311
|
+
case 'document-history-reset': return { type: 'document-history-reset' }
|
|
312
|
+
case 'pause-playback':
|
|
313
|
+
case 'playback-granted': return request(m.request) ? { type: m.type, request: m.request } : undefined
|
|
314
|
+
case 'flush': return request(m.request) ? { type: 'flush', request: m.request } : undefined
|
|
315
|
+
case 'edit-result': return anchor(m.target) && request(m.request) && editBaseline(m) && optional(m.error, text) ? { type: 'edit-result', target: toAnchor(m.target), request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string, binding: m.binding as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
316
|
+
case 'blocks-result': return text(m.section) && request(m.request) && optional(m.revision, text) && optional(m.error, text) ? { type: 'blocks-result', section: m.section, request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
317
|
+
case 'source-result': return sourceTarget(m) && optional(m.source, sourceValue) && optional(m.error, text) ? { type: 'source-result', section: m.section as string, block: m.block as string, request: m.request as number, ...m.source === undefined ? {} : { source: m.source as PreviewSource }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
318
|
+
case 'source-saved': return sourceTarget(m) && optional(m.sourceVersion, (value): value is string => bounded(value, 200)) && optional(m.error, text) ? { type: 'source-saved', section: m.section as string, block: m.block as string, request: m.request as number, ...m.sourceVersion === undefined ? {} : { sourceVersion: m.sourceVersion as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
319
|
+
case 'source-created': return bounded(m.section, 200) && request(m.request) && optional(m.block, (value): value is string => bounded(value, 200)) && optional(m.error, text) ? { type: 'source-created', section: m.section, request: m.request, ...m.block === undefined ? {} : { block: m.block as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
320
|
+
case 'page-insert': return bounded(m.section, 200) && pageId(m.id) && request(m.request) ? { type: 'page-insert', section: m.section, id: m.id, request: m.request } : undefined
|
|
321
|
+
case 'page-created': return bounded(m.section, 200) && request(m.request) && optional(m.page, pageId) && optional(m.error, text) ? { type: 'page-created', section: m.section, request: m.request, ...m.page === undefined ? {} : { page: m.page as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
322
|
+
case 'go': return text(m.section) && optional(m.step, step) && optional(m.trail, strings) && optional(m.view, text) ? { type: 'go', section: m.section, ...m.step === undefined ? {} : { step: m.step as number }, ...m.trail ? { trail: m.trail as string[] } : {}, ...m.view ? { view: m.view as string } : {} } : undefined
|
|
323
|
+
case 'highlight': return m.target === null || anchor(m.target) ? { type: 'highlight', target: m.target === null ? null : { section: m.target.section, ...m.target.block ? { block: m.target.block } : {} } } : undefined
|
|
324
|
+
case 'pick':
|
|
325
|
+
case 'marks': return typeof m.on === 'boolean' ? { type: m.type, on: m.on } : undefined
|
|
326
|
+
case 'appearance': return typeof m.dark === 'boolean' ? { type: 'appearance', dark: m.dark } : undefined
|
|
327
|
+
case 'activity': return typeof m.active === 'boolean' && request(m.request) ? { type: 'activity', active: m.active, request: m.request } : undefined
|
|
328
|
+
case 'edit': return (m.target === null || anchor(m.target)) && optional(m.at, point) && editBaseline(m) ? { type: 'edit', target: m.target === null ? null : toAnchor(m.target), ...m.at ? { at: toPoint(m.at as PreviewPoint) } : {}, ...m.revision === undefined ? {} : { revision: m.revision as string, binding: m.binding as string } } : undefined
|
|
329
|
+
case 'pins': return Array.isArray(m.targets) && m.targets.length <= 1000 && m.targets.every(t => isRecord(t) && text(t.id) && text(t.section) && text(t.block) && text(t.label))
|
|
330
|
+
? { type: 'pins', targets: (m.targets as PreviewPin[]).map(({ id, section, block, label }) => ({ id, section, block, label })) }
|
|
331
|
+
: undefined
|
|
332
|
+
case 'flash': return Array.isArray(m.targets) && m.targets.length <= 1000 && m.targets.every(anchor) ? { type: 'flash', targets: (m.targets as PreviewAnchor[]).map(toAnchor) } : undefined
|
|
333
|
+
default: return undefined
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function toPreviewReport(data: unknown, nonce: string): PreviewReport | undefined {
|
|
338
|
+
if (!isPreviewEnvelope(data, nonce) || data.version !== PREVIEW_PROTOCOL) return undefined
|
|
339
|
+
const m = data as Record<string, unknown>
|
|
340
|
+
switch (m.type) {
|
|
341
|
+
case 'events-subscribe': return productionId(m.connection) && productionId(m.id) && isPreviewEventTarget(m.target) ? { type: 'events-subscribe', connection: m.connection, id: m.id, target: m.target } : undefined
|
|
342
|
+
case 'events-unsubscribe': return productionId(m.connection) && productionId(m.id) ? { type: 'events-unsubscribe', connection: m.connection, id: m.id } : undefined
|
|
343
|
+
case 'presentation-action': return m.action === 'refresh' || m.action === 'exit' ? { type: 'presentation-action', action: m.action } : undefined
|
|
344
|
+
case 'production-playback-state': return productionState(m.state) ? { type: 'production-playback-state', state: m.state } : undefined
|
|
345
|
+
case 'production-playback-result': return request(m.request) && productionBinding(m.binding) && optional(m.state, productionState) && optional(m.error, text)
|
|
346
|
+
? { type: 'production-playback-result', request: m.request, binding: m.binding, ...m.state ? { state: m.state as ProductionPlaybackState } : {}, ...m.error !== undefined ? { error: m.error as string } : {} } : undefined
|
|
347
|
+
case 'document-history-state': return optional(m.section, text) && typeof m.undo === 'boolean' && typeof m.redo === 'boolean' ? { type: 'document-history-state', ...m.section === undefined ? {} : { section: m.section as string }, undo: m.undo, redo: m.redo } : undefined
|
|
348
|
+
case 'document-history-result': return request(m.request) && typeof m.handled === 'boolean' && optional(m.error, text) ? { type: 'document-history-result', request: m.request, handled: m.handled, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
349
|
+
case 'playback-request': return request(m.request) ? { type: 'playback-request', request: m.request } : undefined
|
|
350
|
+
case 'playback-paused': return request(m.request) && optional(m.error, text) ? { type: 'playback-paused', request: m.request, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
351
|
+
case 'flushed': return request(m.request) && optional(m.error, text) ? { type: 'flushed', request: m.request, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
352
|
+
case 'blocks-edited': return text(m.section) && request(m.request) && text(m.revision) && Array.isArray(m.blocks) && m.blocks.length <= 2000 && m.blocks.every(b => isRecord(b) && text(b.id) && b.id.length <= 200 && optional(b.source, text) && optional(b.at, step) && optional(b.markdown, (v): v is string => text(v) && v.length <= MAX_EDITED) && optional(b.scene, text) && optional(b.durations, (v): v is number[] => Array.isArray(v) && v.length <= 1000 && v.every(n => typeof n === 'number' && Number.isFinite(n) && n >= 0))) ? { type: 'blocks-edited', section: m.section, request: m.request, revision: m.revision, blocks: m.blocks as unknown as PreviewEditedBlock[] } : undefined
|
|
353
|
+
case 'source-read': return sourceTarget(m) && optional(m.part, sourcePart) ? { type: 'source-read', section: m.section as string, block: m.block as string, request: m.request as number, ...m.part === undefined ? {} : { part: m.part as 'definition' | 'block' } } : undefined
|
|
354
|
+
case 'source-save': return sourceTarget(m) && optional(m.part, sourcePart) && bounded(m.path, 4096) && bounded(m.base, 200) && text(m.content) && m.content.length <= MAX_EDITED ? { type: 'source-save', section: m.section as string, block: m.block as string, request: m.request as number, path: m.path as string, base: m.base as string, content: m.content, ...m.part === undefined ? {} : { part: m.part as 'definition' | 'block' } } : undefined
|
|
355
|
+
case 'source-create': return bounded(m.section, 200) && text(m.id) && /^scene-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(m.id) && request(m.request) && bounded(m.revision, 200) && optional(m.after, (value): value is string => bounded(value, 200)) ? { type: 'source-create', section: m.section, id: m.id, request: m.request, revision: m.revision, ...m.after === undefined ? {} : { after: m.after as string } } : undefined
|
|
356
|
+
case 'source-draft': return typeof m.dirty === 'boolean' ? { type: 'source-draft', dirty: m.dirty } : undefined
|
|
357
|
+
case 'body-draft': return typeof m.dirty === 'boolean' ? { type: 'body-draft', dirty: m.dirty } : undefined
|
|
358
|
+
case 'native-draft': return typeof m.dirty === 'boolean' ? { type: 'native-draft', dirty: m.dirty } : undefined
|
|
359
|
+
case 'page-create': return bounded(m.section, 200) && pageId(m.id) && request(m.request) && bounded(m.revision, 200) ? { type: 'page-create', section: m.section, id: m.id, request: m.request, revision: m.revision } : undefined
|
|
360
|
+
case 'page-create-error': return request(m.request) && bounded(m.error, 4000) ? { type: 'page-create-error', request: m.request, error: m.error } : undefined
|
|
361
|
+
case 'narrate': return text(m.section) && step(m.step) && optional(m.navigate, (value): value is boolean => typeof value === 'boolean') ? { type: 'narrate', section: m.section, step: m.step, ...m.navigate === undefined ? {} : { navigate: m.navigate as boolean } } : undefined
|
|
362
|
+
case 'ready': return (m.renderer === 'web' || m.renderer === 'slides') && text(m.document) && optional(m.revision, text) && strings(m.capabilities) && optional(m.eventConnection, productionId) ? { type: 'ready', renderer: m.renderer, document: m.document, ...m.revision ? { revision: m.revision as string } : {}, capabilities: m.capabilities as PreviewCommandType[], ...m.eventConnection ? { eventConnection: m.eventConnection as string } : {} } : undefined
|
|
363
|
+
case 'position': return text(m.section) && step(m.step) && strings(m.trail) && optional(m.view, text) ? { type: 'position', section: m.section, step: m.step, trail: m.trail, ...m.view ? { view: m.view as string } : {} } : undefined
|
|
364
|
+
case 'picked': return text(m.section) && optional(m.block, text) && optional(m.step, step) && optional(m.trail, strings) && optional(m.rect, rect) && optional(m.point, point) && optional(m.clicks, clicks)
|
|
365
|
+
? { type: 'picked', section: m.section, ...m.block ? { block: m.block as string } : {}, ...m.step === undefined ? {} : { step: m.step as number }, ...m.trail ? { trail: m.trail as string[] } : {}, ...m.rect ? { rect: toRect(m.rect as PreviewRect) } : {}, ...m.point ? { point: toPoint(m.point as PreviewPoint) } : {}, ...m.clicks === undefined ? {} : { clicks: m.clicks as number } }
|
|
366
|
+
: undefined
|
|
367
|
+
case 'selection': {
|
|
368
|
+
if (m.selection === null) return { type: 'selection', selection: null }
|
|
369
|
+
const selected = m.selection
|
|
370
|
+
return isRecord(selected) && text(selected.text) && anchor(selected) && optional(selected.rect, rect) && optional(selected.obstacles, obstacles)
|
|
371
|
+
? { type: 'selection', selection: { ...toAnchor(selected), text: selected.text, ...selected.rect ? { rect: toRect(selected.rect as PreviewRect) } : {}, ...selected.obstacles !== undefined ? { obstacles: (selected.obstacles as PreviewRect[]).map(toRect) } : {} } }
|
|
372
|
+
: undefined
|
|
373
|
+
}
|
|
374
|
+
case 'flashed': return Array.isArray(m.rects) && m.rects.length <= 16 && m.rects.every(rect) ? { type: 'flashed', rects: (m.rects as PreviewRect[]).map(toRect) } : undefined
|
|
375
|
+
case 'rect': return (m.rect === null || rect(m.rect)) && optional(m.obstacles, obstacles) ? { type: 'rect', rect: m.rect === null ? null : toRect(m.rect), ...m.obstacles !== undefined ? { obstacles: (m.obstacles as PreviewRect[]).map(toRect) } : {} } : undefined
|
|
376
|
+
case 'editing': return anchor(m.target) && typeof m.active === 'boolean' ? { type: 'editing', target: toAnchor(m.target), active: m.active } : undefined
|
|
377
|
+
case 'edited': return anchor(m.target) && text(m.before) && text(m.after) && m.before.length <= MAX_EDITED && m.after.length <= MAX_EDITED && editedRequest(m) ? { type: 'edited', target: toAnchor(m.target), before: m.before, after: m.after, ...m.request === undefined ? {} : { request: m.request as number, revision: m.revision as string, binding: m.binding as string } } : undefined
|
|
378
|
+
case 'updated': return optional(m.revision, text) ? { type: 'updated', ...m.revision ? { revision: m.revision as string } : {} } : undefined
|
|
379
|
+
case 'activity': return typeof m.active === 'boolean' && request(m.request) && optional(m.revision, text) && optional(m.error, text)
|
|
380
|
+
? { type: 'activity', active: m.active, request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string }, ...m.error === undefined ? {} : { error: m.error as string } }
|
|
381
|
+
: undefined
|
|
382
|
+
case 'report': return (m.level === 'error' || m.level === 'warning') && text(m.message) && optional(m.plugin, text) && (m.resolved === undefined || typeof m.resolved === 'boolean') ? { type: 'report', level: m.level, message: m.message, ...m.plugin ? { plugin: m.plugin as string } : {}, ...m.resolved === undefined ? {} : { resolved: m.resolved as boolean } } : undefined
|
|
383
|
+
case 'shortcut': return typeof m.command === 'string' && PREVIEW_SHORTCUTS.includes(m.command) ? { type: 'shortcut', command: m.command as PreviewShortcut } : undefined
|
|
384
|
+
default: return undefined
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Asset, MediaClip, TimeRange } from './document.ts'
|
|
2
|
+
import type { ProductionBinding, ProductionPlan, ProductionPlanQuery } from './production.ts'
|
|
3
|
+
import type { CanonicalSource } from '../ir.ts'
|
|
4
|
+
import type { DocumentChangeAnswer } from './workspace.ts'
|
|
5
|
+
import { defineService } from './service.ts'
|
|
6
|
+
|
|
7
|
+
/** Actual local audio metadata, derived from a bounded, regular file rather than authored duration. */
|
|
8
|
+
export interface ProductionAssetInspection {
|
|
9
|
+
assetId: string
|
|
10
|
+
mediaId: string
|
|
11
|
+
kind: 'audio'
|
|
12
|
+
sha256: string
|
|
13
|
+
durationMs: number
|
|
14
|
+
byteLength: number
|
|
15
|
+
mediaType: string
|
|
16
|
+
codec: string
|
|
17
|
+
sampleRate: number
|
|
18
|
+
channels: number
|
|
19
|
+
}
|
|
20
|
+
export interface ProductionAssetQuery extends ProductionPlanQuery { assetId: string }
|
|
21
|
+
export interface ProductionAssetAnswer extends ProductionPlanQuery { asset: ProductionAssetInspection }
|
|
22
|
+
|
|
23
|
+
/** Multipart fields are change (this JSON) and file (the original bytes). Import does not attach a clip. */
|
|
24
|
+
export const DOCUMENT_AUDIO_MAX_BYTES = 16 << 20
|
|
25
|
+
export interface DocumentAudioImport { id: string, baseDocumentVersion: string }
|
|
26
|
+
export interface DocumentAudioImportAnswer extends DocumentChangeAnswer { assetId: string, inspection: ProductionAssetInspection }
|
|
27
|
+
/** Reads a declared asset directly from the saved document, including assets not yet used by a clip. */
|
|
28
|
+
export interface DocumentAudioInspectionAnswer { document: string, version: string, fingerprint: string, inspection: ProductionAssetInspection }
|
|
29
|
+
export type DocumentMediaClipChange =
|
|
30
|
+
| { id: string, baseDocumentVersion: string, binding: ProductionBinding, clip: MediaClip }
|
|
31
|
+
| { id: string, baseDocumentVersion: string, removeClipId: string }
|
|
32
|
+
|
|
33
|
+
export class ProductionAudioRefusal extends Error {
|
|
34
|
+
readonly status: number
|
|
35
|
+
readonly code: string
|
|
36
|
+
constructor(status: number, code: string) { super(code); this.status = status; this.code = code }
|
|
37
|
+
}
|
|
38
|
+
/** Node-only media capabilities. Workspace authoring owns the transaction; the plugin owns real media inspection. */
|
|
39
|
+
export interface ProductionAudioPlanQuery { source: CanonicalSource, irRevision?: string }
|
|
40
|
+
export interface ProductionAudioAuthoring {
|
|
41
|
+
inspectBytes(assetId: string, bytes: Uint8Array, signal?: AbortSignal): Promise<{ inspection: ProductionAssetInspection, extension: string }>
|
|
42
|
+
inspectAsset(asset: Asset, signal?: AbortSignal): Promise<ProductionAssetInspection>
|
|
43
|
+
/** Cache-only projection of an existing real IR; this never compiles or synthesizes. */
|
|
44
|
+
readPlan(query: ProductionAudioPlanQuery, signal?: AbortSignal): Promise<ProductionPlan>
|
|
45
|
+
validateClips(binding: ProductionBinding, clips: MediaClip[], assets: Record<string, Asset>, signal?: AbortSignal): Promise<void>
|
|
46
|
+
}
|
|
47
|
+
export const PRODUCTION_AUDIO_AUTHORING = defineService<ProductionAudioAuthoring>('presmium.productionAudioAuthoring')
|
|
48
|
+
|
|
49
|
+
/** One non-destructive use of an inspected file, placed inside an already confirmed step. */
|
|
50
|
+
export interface ProductionAudioClip {
|
|
51
|
+
id: string
|
|
52
|
+
assetId: string
|
|
53
|
+
mediaId: string
|
|
54
|
+
sectionId: string
|
|
55
|
+
stepId: string
|
|
56
|
+
startMs: number
|
|
57
|
+
endMs: number
|
|
58
|
+
sourceRange: TimeRange
|
|
59
|
+
volume: number
|
|
60
|
+
muted: boolean
|
|
61
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CanonicalSource } from '../ir.ts'
|
|
2
|
+
import type { TrackEvent } from './operations.ts'
|
|
3
|
+
import type { TimeRange } from './document.ts'
|
|
4
|
+
import type { ProductionAssetInspection } from './production-assets.ts'
|
|
5
|
+
|
|
6
|
+
/** The same supported rates are used by the author controls and the actual production driver. */
|
|
7
|
+
export const PRODUCTION_PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2] as const
|
|
8
|
+
|
|
9
|
+
/** One immutable derived plan; a cache change can change planId without changing the saved document. */
|
|
10
|
+
export interface ProductionBinding { source: CanonicalSource, irRevision: string, planId: string }
|
|
11
|
+
export function sameProductionBinding(a: ProductionBinding, b: ProductionBinding): boolean {
|
|
12
|
+
return a.planId === b.planId && a.irRevision === b.irRevision && a.source.documentId === b.source.documentId && a.source.version === b.source.version && a.source.fingerprint === b.source.fingerprint
|
|
13
|
+
}
|
|
14
|
+
export interface ProductionPlaybackPosition {
|
|
15
|
+
timeMs: number
|
|
16
|
+
stepTimeMs?: number
|
|
17
|
+
sectionId?: string
|
|
18
|
+
stepId?: string
|
|
19
|
+
durationMs: number
|
|
20
|
+
playing: boolean
|
|
21
|
+
rate: number
|
|
22
|
+
trying: boolean
|
|
23
|
+
/** Exact missing utterance that paused this playback intent before its estimated interval. */
|
|
24
|
+
waitingFor?: string
|
|
25
|
+
}
|
|
26
|
+
export interface ProductionPlaybackState {
|
|
27
|
+
binding: ProductionBinding
|
|
28
|
+
status: 'loading' | 'ready' | 'estimated' | 'unavailable' | 'failed'
|
|
29
|
+
position?: ProductionPlaybackPosition
|
|
30
|
+
error?: string
|
|
31
|
+
}
|
|
32
|
+
export type ProductionPlaybackCommand =
|
|
33
|
+
| { binding: ProductionBinding, action: 'load' | 'pause' | 'play' }
|
|
34
|
+
| { binding: ProductionBinding, action: 'seek', timeMs: number }
|
|
35
|
+
/** Selects an event boundary even when several stable steps share the same time. */
|
|
36
|
+
| { binding: ProductionBinding, action: 'step', sectionId: string, stepId: string, utteranceId?: string }
|
|
37
|
+
| { binding: ProductionBinding, action: 'rate', rate: number }
|
|
38
|
+
export interface ProductionPlanQuery { source: CanonicalSource, irRevision: string }
|
|
39
|
+
/** clipId is a plan resource ID: either an exact speech key or an inspected asset mediaId. */
|
|
40
|
+
export interface ProductionMediaQuery extends ProductionBinding { clipId: string }
|
|
41
|
+
export type ProductionTiming = 'confirmed' | 'estimated' | 'unknown'
|
|
42
|
+
|
|
43
|
+
/** All ranges use milliseconds in the single document-wide playback clock. Unknown boundaries are omitted. */
|
|
44
|
+
export interface ProductionRange { startMs?: number, endMs?: number, timing: ProductionTiming }
|
|
45
|
+
export interface ProductionAudio {
|
|
46
|
+
clipId: string
|
|
47
|
+
key: string
|
|
48
|
+
durationMs: number
|
|
49
|
+
/** Digest of the actual cached audio bytes, when available. Never inferred from a cache key. */
|
|
50
|
+
sha256?: string
|
|
51
|
+
}
|
|
52
|
+
export interface ProductionSegment extends ProductionRange {
|
|
53
|
+
id: string
|
|
54
|
+
sectionId: string
|
|
55
|
+
stepId: string
|
|
56
|
+
/** A cue can advance through several steps inside this one audio/caption segment. */
|
|
57
|
+
stepIds?: string[]
|
|
58
|
+
narrationIds?: string[]
|
|
59
|
+
sourceStepId?: string
|
|
60
|
+
blockId?: string
|
|
61
|
+
utteranceId?: string
|
|
62
|
+
label: string
|
|
63
|
+
status?: 'cached' | 'recorded' | 'silent' | 'missing' | 'blocked'
|
|
64
|
+
alignment?: 'words' | 'proportional' | 'none' | 'manual'
|
|
65
|
+
audio?: ProductionAudio
|
|
66
|
+
/** Imported media is distinct from a synthesized speech-cache entry. */
|
|
67
|
+
media?: ProductionAssetInspection
|
|
68
|
+
mediaClipId?: string
|
|
69
|
+
assetId?: string
|
|
70
|
+
sourceRange?: TimeRange
|
|
71
|
+
volume?: number
|
|
72
|
+
muted?: boolean
|
|
73
|
+
waveform?: number[]
|
|
74
|
+
waveformStatus?: 'available' | 'unavailable'
|
|
75
|
+
}
|
|
76
|
+
export interface ProductionTrack { kind: 'visual' | 'voice' | 'caption', segments: ProductionSegment[] }
|
|
77
|
+
export interface CaptionTimingSource {
|
|
78
|
+
available: boolean
|
|
79
|
+
reason?: string
|
|
80
|
+
sourceFingerprint: string
|
|
81
|
+
playbackFingerprint?: string
|
|
82
|
+
durationMs?: number
|
|
83
|
+
narrationIds: string[]
|
|
84
|
+
}
|
|
85
|
+
export interface ProductionStep extends ProductionRange { captionTiming?: CaptionTimingSource, stepId: string, index: number, holdMs?: number, animationMs?: number }
|
|
86
|
+
export interface ProductionPage extends ProductionRange { sectionId: string, title: string, steps: ProductionStep[] }
|
|
87
|
+
export interface ProductionDiagnostic {
|
|
88
|
+
code: string
|
|
89
|
+
severity: 'info' | 'warning' | 'error'
|
|
90
|
+
message: string
|
|
91
|
+
sectionId?: string
|
|
92
|
+
stepId?: string
|
|
93
|
+
blockId?: string
|
|
94
|
+
narrationId?: string
|
|
95
|
+
assetId?: string
|
|
96
|
+
mediaClipId?: string
|
|
97
|
+
}
|
|
98
|
+
export interface ProductionPlan {
|
|
99
|
+
binding: ProductionBinding
|
|
100
|
+
durationMs?: number
|
|
101
|
+
timing: ProductionTiming
|
|
102
|
+
pages: ProductionPage[]
|
|
103
|
+
tracks: ProductionTrack[]
|
|
104
|
+
events: TrackEvent[]
|
|
105
|
+
diagnostics: ProductionDiagnostic[]
|
|
106
|
+
/** Preview may buffer at missing speech; export still requires a confirmed complete plan. */
|
|
107
|
+
canPreview: boolean
|
|
108
|
+
canExport: boolean
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DocumentOperation, OperationAnswer } from './workspace.ts'
|
|
2
|
+
import type { CanonicalSource } from '../ir.ts'
|
|
3
|
+
import type { Member } from './server.ts'
|
|
4
|
+
|
|
5
|
+
export const PROJECT_MAX_REQUEST_CHARS = 4096
|
|
6
|
+
export const PROJECT_MAX_RESPONSE_CHARS = 7168
|
|
7
|
+
export const PROJECT_MAX_PLANS = 64
|
|
8
|
+
export const PROJECT_PLAN_TTL_MS = 300_000
|
|
9
|
+
export type ProjectOperation = DocumentOperation | { op: 'retitle', dir: string, title: string }
|
|
10
|
+
export interface ProjectCaller { member: Member, origin: 'editor' | 'agent', boundDocument?: string }
|
|
11
|
+
export interface ProjectObservation { snapshot?: string, configuration: string }
|
|
12
|
+
export interface ProjectCapabilities {
|
|
13
|
+
available: boolean
|
|
14
|
+
actions: Record<ProjectOperation['op'], { available: boolean, reasons: string[] }>
|
|
15
|
+
reasons: string[]
|
|
16
|
+
observation: ProjectObservation
|
|
17
|
+
verification: 'configuration-only'
|
|
18
|
+
rootDirectoryOperations: false
|
|
19
|
+
limits: { requestChars: number, responseChars: number, backupEntries: number, backupBytes: number, plans: number, ttlMs: number }
|
|
20
|
+
}
|
|
21
|
+
export interface ProjectPlanRequest { id: string, operation: ProjectOperation }
|
|
22
|
+
export interface ProjectPlan {
|
|
23
|
+
id: string
|
|
24
|
+
planId: string
|
|
25
|
+
digest: string
|
|
26
|
+
operation: ProjectOperation
|
|
27
|
+
observation: ProjectObservation
|
|
28
|
+
tree?: string
|
|
29
|
+
source?: CanonicalSource
|
|
30
|
+
documentId?: string
|
|
31
|
+
expiresAt: string
|
|
32
|
+
impact: { added: string[], removed: string[], files: number, bytes: number, referenceDocuments: string[], referencePolicy: 'preserve-or-refuse' }
|
|
33
|
+
}
|
|
34
|
+
export interface ProjectApply { planId: string, digest: string, confirmDeletion?: true }
|
|
35
|
+
export interface ProjectApplied extends OperationAnswer {
|
|
36
|
+
id: string
|
|
37
|
+
planId: string
|
|
38
|
+
snapshot?: string
|
|
39
|
+
tree?: string
|
|
40
|
+
source?: CanonicalSource
|
|
41
|
+
documentId?: string
|
|
42
|
+
revisionTruncated?: true
|
|
43
|
+
fileCount?: number
|
|
44
|
+
verification: { saved: true, readback: 'verified', compiled: 'not-requested' }
|
|
45
|
+
}
|
|
46
|
+
/** Directory management borrows the Web writer; metadata retitle borrows canonical document CAS. */
|
|
47
|
+
export interface ProjectAuthoring {
|
|
48
|
+
capabilities(caller: ProjectCaller): Promise<ProjectCapabilities>
|
|
49
|
+
plan(request: ProjectPlanRequest, caller: ProjectCaller): Promise<ProjectPlan>
|
|
50
|
+
readPlan(request: ProjectApply, caller: ProjectCaller): Promise<ProjectPlan>
|
|
51
|
+
apply(request: ProjectApply, caller: ProjectCaller, signal?: AbortSignal): Promise<ProjectApplied>
|
|
52
|
+
}
|
|
53
|
+
const text = { type: 'string', minLength: 1, maxLength: 200 } as const
|
|
54
|
+
const dir = { type: 'string', maxLength: 200 } as const
|
|
55
|
+
const operation = (op: string, properties: Record<string, unknown>, required: string[]) => ({ type: 'object', additionalProperties: false, properties: { op: { const: op }, dir, ...properties }, required: ['op', 'dir', ...required] })
|
|
56
|
+
export const PROJECT_PLAN_SCHEMA = { type: 'object', additionalProperties: false, required: ['id', 'operation'], properties: {
|
|
57
|
+
id: text, operation: { oneOf: [operation('create', { title: text, template: text }, []), operation('copy', { to: text, title: text }, ['to']), operation('rename', { to: text }, ['to']), operation('delete', {}, []), operation('retitle', { title: text }, ['title'])] },
|
|
58
|
+
} } as const
|
|
59
|
+
export const PROJECT_APPLY_SCHEMA = { type: 'object', additionalProperties: false, required: ['planId', 'digest'], properties: { planId: text, digest: { type: 'string', pattern: '^[a-f0-9]{64}$' }, confirmDeletion: { const: true } } } as const
|