@lincyaw/kernel 1.0.0-rc.0 → 1.0.0-rc.10
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 +7 -7
- package/package.json +6 -2
- package/src/access.ts +18 -0
- package/src/anchors.ts +5 -1
- package/src/authoring.ts +56 -0
- package/src/blocks.ts +0 -8
- package/src/component-code.ts +6 -0
- package/src/contracts/agent.ts +5 -82
- 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 +4 -5
- package/src/contracts/locale.ts +3 -0
- package/src/contracts/offline.ts +2 -0
- package/src/contracts/plugin.ts +183 -0
- package/src/contracts/presenter-panels.ts +17 -0
- package/src/contracts/preview.ts +305 -310
- 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 +1 -51
- package/src/contracts/speech-settings.ts +16 -0
- package/src/contracts/speech.ts +82 -70
- 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 +28 -278
- 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 +8 -0
- package/src/execution.ts +5 -1
- package/src/fingerprint.ts +1 -1
- package/src/follow.ts +26 -30
- package/src/git.ts +1 -1
- package/src/index.ts +66 -13
- package/src/ir.ts +54 -5
- package/src/jupyter-kernel.ts +9 -0
- package/src/kernel.ts +38 -25
- package/src/keyboard.ts +43 -0
- package/src/locale.ts +13 -0
- package/src/lock.ts +4 -8
- package/src/logging.ts +1 -1
- package/src/{markdown.ts → lower-myst.ts} +2 -1
- 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 -133
- 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/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 +1234 -0
- package/src/props.ts +17 -0
- package/src/push-stream.ts +67 -0
- package/src/reading.ts +12 -7
- package/src/recording.ts +38 -0
- package/src/renderer-components.ts +30 -0
- package/src/responses.ts +49 -0
- package/src/run-outputs.ts +15 -0
- package/src/runner.py +3 -3
- package/src/services.ts +6 -6
- 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,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
|
package/src/contracts/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Logger } from '
|
|
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 }
|
package/src/contracts/speech.ts
CHANGED
|
@@ -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,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'] }
|