@lincyaw/kernel 1.0.0-rc.0 → 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.
Files changed (119) hide show
  1. package/README.md +7 -7
  2. package/package.json +6 -2
  3. package/src/access.ts +18 -0
  4. package/src/anchors.ts +5 -1
  5. package/src/authoring.ts +56 -0
  6. package/src/blocks.ts +0 -8
  7. package/src/component-code.ts +6 -0
  8. package/src/contracts/agent.ts +5 -82
  9. package/src/contracts/anchors.ts +46 -0
  10. package/src/contracts/authoring-actions.ts +60 -0
  11. package/src/contracts/authoring-clients.ts +213 -0
  12. package/src/contracts/authoring.ts +180 -0
  13. package/src/contracts/checks.ts +29 -0
  14. package/src/contracts/classroom.ts +49 -0
  15. package/src/contracts/diagnostics.ts +37 -0
  16. package/src/contracts/document-audio.ts +150 -0
  17. package/src/contracts/document-comments.ts +31 -0
  18. package/src/contracts/document-history.ts +63 -0
  19. package/src/contracts/document-image.ts +32 -0
  20. package/src/contracts/document-reads.ts +57 -0
  21. package/src/contracts/document-voice.ts +44 -0
  22. package/src/contracts/document.ts +240 -0
  23. package/src/contracts/execution.ts +4 -5
  24. package/src/contracts/locale.ts +3 -0
  25. package/src/contracts/offline.ts +2 -0
  26. package/src/contracts/plugin.ts +183 -0
  27. package/src/contracts/presenter-panels.ts +17 -0
  28. package/src/contracts/preview.ts +305 -310
  29. package/src/contracts/production-assets.ts +61 -0
  30. package/src/contracts/production.ts +109 -0
  31. package/src/contracts/projects.ts +59 -0
  32. package/src/contracts/server.ts +1 -51
  33. package/src/contracts/speech-settings.ts +16 -0
  34. package/src/contracts/speech.ts +82 -70
  35. package/src/contracts/steps.ts +15 -0
  36. package/src/contracts/transcription-adoption.ts +79 -0
  37. package/src/contracts/transcription.ts +112 -0
  38. package/src/contracts/voice.ts +97 -0
  39. package/src/contracts/voiceover-execution.ts +68 -0
  40. package/src/contracts/workspace.ts +458 -0
  41. package/src/contracts.ts +28 -278
  42. package/src/diagnostics.ts +7 -0
  43. package/src/document-authoring-actions.ts +47 -0
  44. package/src/document-caption-timings.ts +64 -0
  45. package/src/document-colors.ts +25 -0
  46. package/src/document-comments.ts +118 -0
  47. package/src/document-components.ts +251 -0
  48. package/src/document-operations.ts +132 -0
  49. package/src/document-pages.ts +215 -0
  50. package/src/document-reads.ts +81 -0
  51. package/src/document-schema.ts +85 -0
  52. package/src/document-voices.ts +153 -0
  53. package/src/document.ts +252 -0
  54. package/src/durations.ts +7 -0
  55. package/src/errors.ts +8 -0
  56. package/src/execution.ts +5 -1
  57. package/src/fingerprint.ts +1 -1
  58. package/src/follow.ts +26 -30
  59. package/src/git.ts +1 -1
  60. package/src/index.ts +66 -13
  61. package/src/ir.ts +54 -5
  62. package/src/jupyter-kernel.ts +9 -0
  63. package/src/kernel.ts +38 -25
  64. package/src/keyboard.ts +43 -0
  65. package/src/locale.ts +13 -0
  66. package/src/lock.ts +4 -8
  67. package/src/logging.ts +1 -1
  68. package/src/{markdown.ts → lower-myst.ts} +2 -1
  69. package/src/node/assets.ts +23 -0
  70. package/src/node/authoring-actions.ts +50 -0
  71. package/src/node/authoring-clients.ts +14 -0
  72. package/src/node/document-audio.ts +22 -0
  73. package/src/node/document-history.ts +11 -0
  74. package/src/node/document-image.ts +8 -0
  75. package/src/node/document-reads.ts +10 -0
  76. package/src/node/document-voice.ts +7 -0
  77. package/src/node/ffmpeg.ts +29 -0
  78. package/src/node/file-version.ts +6 -0
  79. package/src/node/fonts.ts +22 -0
  80. package/src/node/index.ts +29 -133
  81. package/src/node/installed-packages.ts +11 -0
  82. package/src/node/lines.ts +24 -0
  83. package/src/node/locale.ts +7 -0
  84. package/src/node/network.ts +11 -0
  85. package/src/node/operator-speech.ts +91 -0
  86. package/src/node/packages.ts +74 -0
  87. package/src/node/paths.ts +7 -0
  88. package/src/node/projects.ts +7 -0
  89. package/src/node/resources.ts +86 -0
  90. package/src/node/revision.ts +6 -0
  91. package/src/node/run-directory.ts +70 -0
  92. package/src/node/transcription-adoption.ts +9 -0
  93. package/src/node/transcription.ts +10 -0
  94. package/src/node/voiceover-routes.ts +73 -0
  95. package/src/node/voiceover.ts +199 -0
  96. package/src/page-source.ts +359 -0
  97. package/src/parameters.ts +81 -0
  98. package/src/preview-edit.ts +18 -0
  99. package/src/preview-events.ts +57 -0
  100. package/src/preview-frame.ts +1220 -0
  101. package/src/props.ts +17 -0
  102. package/src/push-stream.ts +67 -0
  103. package/src/reading.ts +12 -7
  104. package/src/recording.ts +38 -0
  105. package/src/renderer-components.ts +30 -0
  106. package/src/responses.ts +49 -0
  107. package/src/run-outputs.ts +15 -0
  108. package/src/runner.py +3 -3
  109. package/src/services.ts +6 -6
  110. package/src/slides.ts +36 -0
  111. package/src/speech-words.ts +76 -0
  112. package/src/speech.ts +13 -0
  113. package/src/step-keys.ts +38 -0
  114. package/src/steps.ts +7 -0
  115. package/src/transcription-adoption.ts +105 -0
  116. package/src/uri-decoding.ts +8 -0
  117. package/src/urls.ts +13 -0
  118. package/src/views.ts +47 -0
  119. package/src/voice.ts +86 -0
@@ -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
@@ -1,4 +1,4 @@
1
- import type { Logger } from '../contracts.ts'
1
+ import type { Logger } from './plugin.ts'
2
2
  import { defineService } from './service.ts'
3
3
  import type { ServiceToken } from './service.ts'
4
4
 
@@ -27,14 +27,6 @@ export interface Route {
27
27
 
28
28
  export const MAX_BODY = 1 << 20
29
29
 
30
- export function json(data: unknown, status = 200, headers: Record<string, string> = {}): Response {
31
- return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', ...headers } })
32
- }
33
-
34
- export function problem(status: number, message: string): Response {
35
- return json({ error: message }, status)
36
- }
37
-
38
30
  export interface ProxySpec {
39
31
  path: string
40
32
  target: string
@@ -92,48 +84,6 @@ export interface Approvals {
92
84
 
93
85
  export const APPROVALS = defineService<Approvals>('presmium.approvals')
94
86
 
95
- export function eventStream(events: AsyncIterable<{ event?: string, data: unknown }>, signal?: AbortSignal): Response {
96
- const encoder = new TextEncoder()
97
- const iterator = events[Symbol.asyncIterator]()
98
- let closed = false
99
- const stop = (controller?: ReadableStreamDefaultController<Uint8Array>) => {
100
- if (closed) return
101
- closed = true
102
- void iterator.return?.()
103
- try {
104
- controller?.close()
105
- } catch {
106
- }
107
- }
108
- const stream = new ReadableStream<Uint8Array>({
109
- start(controller) {
110
- if (signal?.aborted) stop(controller)
111
- else signal?.addEventListener('abort', () => stop(controller), { once: true })
112
- },
113
- async pull(controller) {
114
- if (closed) return
115
- try {
116
- const next = await iterator.next()
117
- if (closed) return
118
- if (next.done) {
119
- stop(controller)
120
- return
121
- }
122
- const { event, data } = next.value
123
- const lines = `${event ? `event: ${event}\n` : ''}${JSON.stringify(data ?? null).split('\n').map(line => `data: ${line}`).join('\n')}\n\n`
124
- controller.enqueue(encoder.encode(lines))
125
- } catch (error) {
126
- closed = true
127
- controller.error(error)
128
- }
129
- },
130
- cancel() {
131
- stop()
132
- },
133
- })
134
- return new Response(stream, { headers: { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-store', 'X-Accel-Buffering': 'no' } })
135
- }
136
-
137
87
  export type AgentMode = 'author'
138
88
 
139
89
  export interface McpAccess {
@@ -0,0 +1,16 @@
1
+ /** Operator configuration is separate from document voice choices. Secret values are write-only. */
2
+ export interface SpeechSettingsAnswer {
3
+ provider: 'volcengine'
4
+ model: 'seed-tts-2.0'
5
+ revision: string
6
+ configured: boolean
7
+ appIdConfigured?: boolean
8
+ source: 'saved' | 'environment' | 'none'
9
+ stored: boolean
10
+ writable: boolean
11
+ verification: 'configuration-only'
12
+ }
13
+
14
+ export type SpeechSettingsChange =
15
+ | { action: 'save', revision: string, apiKey?: string, appId?: string }
16
+ | { action: 'clear', revision: string }
@@ -2,76 +2,6 @@ import { defineService } from './service.ts'
2
2
 
3
3
  export interface SpeechWord { text: string, start: number, end: number }
4
4
 
5
- const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u
6
- const LETTER = /[\p{L}\p{N}'’]/u
7
-
8
- export interface TextWord { text: string, start: number, end: number }
9
-
10
- export function splitWords(text: string): TextWord[] {
11
- const words: TextWord[] = []
12
- let run: TextWord | undefined
13
- let at = 0
14
- for (const char of text) {
15
- if (CJK.test(char)) {
16
- if (run) words.push(run)
17
- run = undefined
18
- words.push({ text: char, start: at, end: at + char.length })
19
- } else if (LETTER.test(char)) {
20
- run ??= { text: '', start: at, end: at }
21
- run.text += char
22
- run.end = at + char.length
23
- } else if (run) {
24
- words.push(run)
25
- run = undefined
26
- }
27
- at += char.length
28
- }
29
- if (run) words.push(run)
30
- return words
31
- }
32
-
33
- export interface WordSpan { from: number, to: number, start: number, end: number }
34
-
35
- export interface WrittenWord extends SpeechWord { offset?: number }
36
-
37
- export function remapWords(words: readonly SpeechWord[], written: string, spoken: string, spans: readonly WordSpan[]): WrittenWord[] {
38
- const places: { start: number, end: number }[] = []
39
- let at = 0
40
- for (const word of words) {
41
- const index = word.text ? spoken.indexOf(word.text, at) : -1
42
- if (index < 0) return words.map(w => ({ ...w }))
43
- places.push({ start: index, end: at = index + word.text.length })
44
- }
45
- const writtenAt = (offset: number) => {
46
- let shift = 0
47
- for (const span of spans) {
48
- if (offset < span.from) break
49
- if (offset < span.to) return span.start
50
- shift += (span.end - span.start) - (span.to - span.from)
51
- }
52
- return offset + shift
53
- }
54
- const result: WrittenWord[] = []
55
- let group: { span: WordSpan, word: WrittenWord } | undefined
56
- words.forEach((word, i) => {
57
- const { start, end } = places[i]!
58
- const span = spans.find(s => start < s.to && end > s.from)
59
- if (span && group?.span === span) {
60
- group.word.end = Math.max(group.word.end, word.end)
61
- return
62
- }
63
- group = undefined
64
- if (span) {
65
- const placed = { text: written.slice(span.start, span.end), start: word.start, end: word.end, offset: span.start }
66
- group = { span, word: placed }
67
- result.push(placed)
68
- } else {
69
- result.push({ ...word, offset: writtenAt(start) })
70
- }
71
- })
72
- return result
73
- }
74
-
75
5
  export interface Synthesis {
76
6
  audio: Blob | string
77
7
  mime: string
@@ -81,9 +11,20 @@ export interface Synthesis {
81
11
  engine: string
82
12
  model: string
83
13
  voice: string
14
+ provider: string
84
15
  synthetic: true
85
16
  }
86
17
 
18
+ export interface AigcLabel {
19
+ Label: '1'
20
+ ContentProducer: string
21
+ ProduceID: string
22
+ ReservedCode1: string
23
+ ContentPropagator: string
24
+ PropagateID: string
25
+ ReservedCode2: string
26
+ }
27
+
87
28
  export interface TranscriptSegment { start: number, end: number, text: string, words: SpeechWord[] }
88
29
 
89
30
  export interface Transcript {
@@ -100,6 +41,17 @@ export interface SpeechEngineInfo {
100
41
  tts: boolean
101
42
  wordTimes: boolean
102
43
  local: boolean
44
+ /** The engine can train a cloned voice from a sample (`SpeechService.cloneVoice`); absent means it cannot. */
45
+ clone?: boolean
46
+ /** Absent means the provider has not declared a supported authoring range. */
47
+ parameters?: { rate?: { min: number, max: number, step?: number }, styles?: string[] }
48
+ }
49
+
50
+ export interface SpeechVoice {
51
+ engine?: string
52
+ voice: string
53
+ rate?: number
54
+ style?: string
103
55
  }
104
56
 
105
57
  export interface VoiceInfo {
@@ -109,6 +61,11 @@ export interface VoiceInfo {
109
61
  cloned?: boolean
110
62
  }
111
63
 
64
+ /** A provider list may be empty. An unsupported catalogue must not be presented as an empty list. */
65
+ export type VoiceCatalog =
66
+ | { status: 'available', source: 'provider' | 'bundled', voices: VoiceInfo[] }
67
+ | { status: 'unavailable', reason: 'not-configured' | 'unsupported' | 'unknown', voices: VoiceInfo[] }
68
+
112
69
  export interface SynthesisRequest {
113
70
  engine?: string
114
71
  voice: string
@@ -119,6 +76,33 @@ export interface SynthesisRequest {
119
76
  lexicon?: Record<string, string>
120
77
  }
121
78
 
79
+ /**
80
+ * Training a cloned voice. The speaker's consent comes with the request and is registered before anything is sent:
81
+ * `statement` is their recording of the consent statement, kept by the operator, whose sha256 the record holds.
82
+ */
83
+ export interface VoiceCloneRequest {
84
+ engine?: string
85
+ /** The voice to train: a slot the operator owns at the provider (Volcengine's S_…) or a new name, per the engine. */
86
+ voice: string
87
+ /** One speaker, mono, about 10–30 s. */
88
+ sample: Blob
89
+ lang?: string
90
+ /** What the sample says, when known: providers that compare it reject a mismatch. */
91
+ text?: string
92
+ consent: { speaker: string, scope: string, statement: Blob }
93
+ }
94
+
95
+ export type VoiceCloneState = 'training' | 'ready' | 'failed' | 'unknown'
96
+
97
+ export interface VoiceCloneStatus {
98
+ engine: string
99
+ voice: string
100
+ state: VoiceCloneState
101
+ /** How many more trainings the provider allows for this voice. */
102
+ trainingsLeft?: number
103
+ message?: string
104
+ }
105
+
122
106
  export interface TranscriptionRequest {
123
107
  engine?: string
124
108
  audio: Blob
@@ -128,10 +112,38 @@ export interface TranscriptionRequest {
128
112
 
129
113
  export interface SpeechService {
130
114
  engines(): Promise<SpeechEngineInfo[]>
115
+ /** The actual default in this document's scoped service; absent means its identity is unknown. */
116
+ defaultSynthesisEngine?(): Promise<string | undefined>
131
117
  voices(engine: string, lang?: string): Promise<VoiceInfo[]>
118
+ /** Lists registered voices without synthesis. Absence means catalogue availability is unknown. */
119
+ voiceCatalog?(engine?: string, lang?: string, signal?: AbortSignal): Promise<VoiceCatalog>
132
120
  synthesize(request: SynthesisRequest, signal?: AbortSignal): Promise<Synthesis>
121
+ /** Exact request/configuration lookup with verified audio bytes. Offline reuse requires an unambiguous lock. A miss never calls synthesis or transcription providers. */
122
+ lookup?(request: SynthesisRequest, signal?: AbortSignal): Promise<Synthesis | undefined>
123
+ /** Verifies the current configuration profile, or an explicit offline lock, and the actual audio SHA. */
133
124
  cached?(key: string, signal?: AbortSignal): Promise<Synthesis | undefined>
125
+ alignWords?(key: string, options: { lang: string, transcribe?: boolean }, signal?: AbortSignal): Promise<SpeechWord[] | undefined>
126
+ pruneLock?(keep: ReadonlySet<string>): Promise<number>
134
127
  transcribe(request: TranscriptionRequest, signal?: AbortSignal): Promise<Transcript>
128
+ /** Registers the consent, then starts training; only for the document's owner. Absent: the service cannot clone. */
129
+ cloneVoice?(request: VoiceCloneRequest, signal?: AbortSignal): Promise<VoiceCloneStatus>
130
+ cloneStatus?(engine: string, voice: string, signal?: AbortSignal): Promise<VoiceCloneStatus>
135
131
  }
136
132
 
137
133
  export const SPEECH = defineService<SpeechService>('presmium.speech')
134
+
135
+ /** Stable author-facing reasons; provider payloads and credential values are never UI text. */
136
+ export function speechFailureCode(error: unknown): string | undefined {
137
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : ''
138
+ if (/^speech-(authentication|permission|quota|busy|network|empty-audio|invalid-request|provider|cache-failed)$/.test(message)) return message
139
+ if (/quota exceeded.*concurrency|too many requests|answered 429|is busy: more requests/i.test(message)) return 'speech-busy'
140
+ if (/quota exceeded|insufficient.*(balance|credit)|payment required|balance.*insufficient/i.test(message)) return 'speech-quota'
141
+ if (/answered 401|45000010|unauthorized|invalid (?:(?:x-)?api.?key|token)|authentication failed/i.test(message)) return 'speech-authentication'
142
+ if (/EACCES|ENOSPC|EROFS|speech.*cache.*(write|failed)/i.test(message)) return 'speech-cache-failed'
143
+ if (/answered 403|resource.*mismatch|permission denied|access denied|service.*not.*(enabled|activated)/i.test(message)) return 'speech-permission'
144
+ if (/fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|UND_ERR|network error/i.test(message)) return 'speech-network'
145
+ if (/gave no audio|no audio data/i.test(message)) return 'speech-empty-audio'
146
+ if (/answered 4\d\d|45000001|4500200[01]|invalid argument/i.test(message)) return 'speech-invalid-request'
147
+ if (/answered 5\d\d|55000000/i.test(message)) return 'speech-provider'
148
+ return undefined
149
+ }
@@ -0,0 +1,15 @@
1
+ export interface StepRange {
2
+ from: number
3
+ to?: number
4
+ }
5
+
6
+ export interface StepState {
7
+ active: boolean
8
+ reached: boolean
9
+ done: boolean
10
+ }
11
+
12
+ export interface Cursor {
13
+ slide: number
14
+ step: number
15
+ }
@@ -0,0 +1,79 @@
1
+ import type { CanonicalSource } from '../ir.ts'
2
+ import type { DocumentChangeAnswer } from './workspace.ts'
3
+ import type { NarrationNode, TeachingDocument } from './document.ts'
4
+ import type { Member } from './server.ts'
5
+
6
+ export type TranscriptionAdoptionPart = { type: 'text', text: string } | { type: 'preserve', key: string }
7
+ export interface TranscriptionAdoptionTarget {
8
+ sectionId: string
9
+ stepId: string
10
+ narrationId?: string
11
+ segmentIds: string[]
12
+ content?: TranscriptionAdoptionPart[]
13
+ discardAtoms?: string[]
14
+ shared?: 'edit-shared' | 'fork'
15
+ affectedStepIds?: string[]
16
+ }
17
+ export interface TranscriptionAdoptionDraft {
18
+ id: string
19
+ source: CanonicalSource
20
+ candidate: { id: string, revision: string }
21
+ draftRevision: number
22
+ targets: TranscriptionAdoptionTarget[]
23
+ /** Explicitly confirmed cleanup, only for cues actually removed by this adoption. */
24
+ removeCueTimings?: string[]
25
+ }
26
+ export interface TranscriptionAdoptionApply {
27
+ planId: string
28
+ source: CanonicalSource
29
+ candidate: { id: string, revision: string }
30
+ draftRevision: number
31
+ }
32
+ export interface TranscriptionAdoptionConflict { code: string, target?: number, key?: string, stepIds?: string[] }
33
+ export interface TranscriptionAdoptionPreview {
34
+ targets: { sectionId: string, stepId: string, narrationId?: string, outputNarrationId: string, segmentIds: string[], sourceText: string, before: NarrationNode[], proposed: NarrationNode[], atoms: { key: string, node: Exclude<NarrationNode, { type: 'text' }> }[] }[]
35
+ conflicts: TranscriptionAdoptionConflict[]
36
+ impact: { narrationIds: string[], stepIds: string[], sharedStepIds: string[], forks: { from: string, to: string, stepId: string, cues: { from: string, to: string }[] }[], removedAtoms: string[], removedCueTimings: string[] }
37
+ }
38
+ export interface TranscriptionAdoptionPlan extends TranscriptionAdoptionPreview {
39
+ document: string
40
+ changeId: string
41
+ source: CanonicalSource
42
+ candidate: { id: string, revision: string }
43
+ draftRevision: number
44
+ planId?: string
45
+ expiresAt?: string
46
+ content?: TeachingDocument
47
+ }
48
+ export interface TranscriptionAdoptionAnswer extends DocumentChangeAnswer {
49
+ adoption: { planId: string, candidate: { id: string, revision: string }, draftRevision: number }
50
+ }
51
+ /** Agent-safe surface. An editor HTTP adapter receives its trusted write path separately. */
52
+ export interface DocumentTranscriptionAdoption {
53
+ plan(document: string, draft: TranscriptionAdoptionDraft, member: Member): Promise<TranscriptionAdoptionPlan>
54
+ /** The adoption's checks without writing: plan, draft revision, candidate, current source; `apply` checks everything again. */
55
+ check(document: string, request: TranscriptionAdoptionApply, member: Member): Promise<void>
56
+ apply(document: string, request: TranscriptionAdoptionApply, member: Member): Promise<TranscriptionAdoptionAnswer>
57
+ }
58
+
59
+ const id = { type: 'string', minLength: 1, maxLength: 200, pattern: '^[^\u0000-\u001f]+$', not: { enum: ['__proto__', 'constructor', 'prototype'] } }
60
+ const digest = { type: 'string', pattern: '^[a-f0-9]{64}$' }
61
+ const key = { type: 'string', minLength: 1, maxLength: 512 }
62
+ const source = { type: 'object', properties: { documentId: id, version: { type: 'string', pattern: '^[a-f0-9]{20}$' }, fingerprint: { type: 'string', pattern: '^[a-f0-9]{16}$' } }, required: ['documentId', 'version', 'fingerprint'], additionalProperties: false }
63
+ const candidate = { type: 'object', properties: { id: digest, revision: digest }, required: ['id', 'revision'], additionalProperties: false }
64
+ const draftRevision = { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER }
65
+ const unique = (items: object) => ({ type: 'array', items, uniqueItems: true, maxItems: 1000 })
66
+ export const TRANSCRIPTION_ADOPTION_DRAFT_SCHEMA = {
67
+ type: 'object', additionalProperties: false, required: ['id', 'source', 'candidate', 'draftRevision', 'targets'],
68
+ properties: { id, source, candidate, draftRevision, removeCueTimings: unique(id), targets: { type: 'array', minItems: 1, maxItems: 100, items: {
69
+ type: 'object', additionalProperties: false, required: ['sectionId', 'stepId', 'segmentIds'], properties: {
70
+ sectionId: id, stepId: id, narrationId: id, segmentIds: { ...unique(id), minItems: 1 },
71
+ shared: { enum: ['edit-shared', 'fork'] }, affectedStepIds: unique(id), discardAtoms: unique(key),
72
+ content: { type: 'array', maxItems: 1000, items: { oneOf: [
73
+ { type: 'object', properties: { type: { const: 'text' }, text: { type: 'string', maxLength: 1_048_576 } }, required: ['type', 'text'], additionalProperties: false },
74
+ { type: 'object', properties: { type: { const: 'preserve' }, key }, required: ['type', 'key'], additionalProperties: false },
75
+ ] } },
76
+ },
77
+ } } },
78
+ }
79
+ export const TRANSCRIPTION_ADOPTION_APPLY_SCHEMA = { type: 'object', additionalProperties: false, properties: { planId: id, source, candidate, draftRevision }, required: ['planId', 'source', 'candidate', 'draftRevision'] }