@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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +32 -2
  3. package/package.json +41 -3
  4. package/src/access.ts +18 -0
  5. package/src/anchors.ts +58 -0
  6. package/src/authoring.ts +56 -0
  7. package/src/blocks.ts +19 -0
  8. package/src/checks.ts +35 -0
  9. package/src/component-code.ts +6 -0
  10. package/src/contracts/agent.ts +76 -0
  11. package/src/contracts/anchors.ts +46 -0
  12. package/src/contracts/authoring-actions.ts +60 -0
  13. package/src/contracts/authoring-clients.ts +213 -0
  14. package/src/contracts/authoring.ts +180 -0
  15. package/src/contracts/checks.ts +29 -0
  16. package/src/contracts/classroom.ts +49 -0
  17. package/src/contracts/diagnostics.ts +37 -0
  18. package/src/contracts/document-audio.ts +150 -0
  19. package/src/contracts/document-comments.ts +31 -0
  20. package/src/contracts/document-history.ts +63 -0
  21. package/src/contracts/document-image.ts +32 -0
  22. package/src/contracts/document-reads.ts +57 -0
  23. package/src/contracts/document-voice.ts +44 -0
  24. package/src/contracts/document.ts +240 -0
  25. package/src/contracts/execution.ts +118 -0
  26. package/src/contracts/locale.ts +3 -0
  27. package/src/contracts/offline.ts +2 -0
  28. package/src/contracts/operations.ts +8 -0
  29. package/src/contracts/plugin.ts +183 -0
  30. package/src/contracts/presenter-panels.ts +17 -0
  31. package/src/contracts/preview.ts +386 -0
  32. package/src/contracts/production-assets.ts +61 -0
  33. package/src/contracts/production.ts +109 -0
  34. package/src/contracts/projects.ts +59 -0
  35. package/src/contracts/server.ts +108 -0
  36. package/src/contracts/service.ts +8 -0
  37. package/src/contracts/speech-settings.ts +16 -0
  38. package/src/contracts/speech.ts +149 -0
  39. package/src/contracts/steps.ts +15 -0
  40. package/src/contracts/transcription-adoption.ts +79 -0
  41. package/src/contracts/transcription.ts +112 -0
  42. package/src/contracts/voice.ts +97 -0
  43. package/src/contracts/voiceover-execution.ts +68 -0
  44. package/src/contracts/workspace.ts +458 -0
  45. package/src/contracts.ts +35 -0
  46. package/src/diagnostics.ts +7 -0
  47. package/src/document-authoring-actions.ts +47 -0
  48. package/src/document-caption-timings.ts +64 -0
  49. package/src/document-colors.ts +25 -0
  50. package/src/document-comments.ts +118 -0
  51. package/src/document-components.ts +251 -0
  52. package/src/document-operations.ts +132 -0
  53. package/src/document-pages.ts +215 -0
  54. package/src/document-reads.ts +81 -0
  55. package/src/document-schema.ts +85 -0
  56. package/src/document-voices.ts +153 -0
  57. package/src/document.ts +252 -0
  58. package/src/durations.ts +7 -0
  59. package/src/errors.ts +15 -0
  60. package/src/events.ts +32 -0
  61. package/src/execution.ts +25 -0
  62. package/src/fingerprint.ts +31 -0
  63. package/src/follow.ts +33 -0
  64. package/src/git.ts +12 -0
  65. package/src/html.ts +41 -0
  66. package/src/index.ts +82 -0
  67. package/src/ir.ts +367 -0
  68. package/src/jupyter-kernel.ts +9 -0
  69. package/src/kernel.ts +190 -0
  70. package/src/keyboard.ts +43 -0
  71. package/src/locale.ts +13 -0
  72. package/src/lock.ts +81 -0
  73. package/src/logging.ts +11 -0
  74. package/src/lower-myst.ts +312 -0
  75. package/src/node/assets.ts +23 -0
  76. package/src/node/authoring-actions.ts +50 -0
  77. package/src/node/authoring-clients.ts +14 -0
  78. package/src/node/document-audio.ts +22 -0
  79. package/src/node/document-history.ts +11 -0
  80. package/src/node/document-image.ts +8 -0
  81. package/src/node/document-reads.ts +10 -0
  82. package/src/node/document-voice.ts +7 -0
  83. package/src/node/ffmpeg.ts +29 -0
  84. package/src/node/file-version.ts +6 -0
  85. package/src/node/fonts.ts +22 -0
  86. package/src/node/index.ts +29 -0
  87. package/src/node/installed-packages.ts +11 -0
  88. package/src/node/lines.ts +24 -0
  89. package/src/node/locale.ts +7 -0
  90. package/src/node/network.ts +11 -0
  91. package/src/node/operator-speech.ts +91 -0
  92. package/src/node/packages.ts +74 -0
  93. package/src/node/paths.ts +7 -0
  94. package/src/node/projects.ts +7 -0
  95. package/src/node/resources.ts +86 -0
  96. package/src/node/revision.ts +6 -0
  97. package/src/node/run-directory.ts +70 -0
  98. package/src/node/strip-types.mjs +15 -0
  99. package/src/node/transcription-adoption.ts +9 -0
  100. package/src/node/transcription.ts +10 -0
  101. package/src/node/voiceover-routes.ts +73 -0
  102. package/src/node/voiceover.ts +199 -0
  103. package/src/page-source.ts +359 -0
  104. package/src/parameters.ts +81 -0
  105. package/src/preview-edit.ts +18 -0
  106. package/src/preview-events.ts +57 -0
  107. package/src/preview-frame.ts +1220 -0
  108. package/src/props.ts +17 -0
  109. package/src/published.ts +11 -0
  110. package/src/push-stream.ts +67 -0
  111. package/src/reading.ts +96 -0
  112. package/src/recording.ts +38 -0
  113. package/src/renderer-components.ts +30 -0
  114. package/src/requests.ts +28 -0
  115. package/src/responses.ts +49 -0
  116. package/src/run-outputs.ts +15 -0
  117. package/src/runner.py +454 -0
  118. package/src/services.ts +146 -0
  119. package/src/slides.ts +36 -0
  120. package/src/speech-words.ts +76 -0
  121. package/src/speech.ts +13 -0
  122. package/src/step-keys.ts +38 -0
  123. package/src/steps.ts +7 -0
  124. package/src/transcription-adoption.ts +105 -0
  125. package/src/uri-decoding.ts +8 -0
  126. package/src/urls.ts +13 -0
  127. package/src/views.ts +47 -0
  128. package/src/voice.ts +86 -0
@@ -0,0 +1,29 @@
1
+ export { copyPublished, productionAssetFile } from './assets.ts'
2
+ export { fileVersion } from './file-version.ts'
3
+ export { DOCUMENT_CHANGE_JSON_SCHEMA, validateDocumentChange, validateDocumentPageChange, validateDocumentPageChangeApply, validateDocumentPageDuplication, validateDocumentPageDuplicationApply } from '../authoring.ts'
4
+ export { validateDocumentOutlineQuery, validateDocumentPartQuery } from './document-reads.ts'
5
+ export { DOCUMENT_AUTHORING_ACTION_SCHEMA, DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA, DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA, validateDocumentAuthoringAction, validateDocumentAuthoringActionReference, validateDocumentAuthoringActionImpact } from './authoring-actions.ts'
6
+ export { validateDocumentTranscriptionRequest } from './transcription.ts'
7
+ export { validateDocumentRecordingAdoption, validateDocumentAudioInspect, validateDocumentWorkspaceAudioImport, validateDocumentWorkspaceAudioInspect, validateDocumentMediaClipValidation, validateDocumentMediaClipEdit, validateDocumentProductionPlanQuery } from './document-audio.ts'
8
+ export { validateDocumentVoiceCatalogQuery } from './document-voice.ts'
9
+ export { createVoiceoverOwner, VoiceoverOperationError, validateVoiceoverStart, validateVoiceoverStatus, validateVoiceoverJobQuery, validateVoicePreviewCheck, validateVoicePreviewStart } from './voiceover.ts'
10
+ export { createVoiceoverRoutes } from './voiceover-routes.ts'
11
+ export { validateTranscriptionAdoptionDraft, validateTranscriptionAdoptionApply } from './transcription-adoption.ts'
12
+ export { FONT_LICENSE, fontLicense } from './fonts.ts'
13
+ export { findPackageBin, importSpecifier, literalGlob, packageOf, playwrightChromium, TYPESCRIPT_IMPORT } from './packages.ts'
14
+ export { ffmpegInstallCommand, findFfmpeg } from './ffmpeg.ts'
15
+ export type { Ffmpeg } from './ffmpeg.ts'
16
+ export { bracketIPv6, lanAddresses } from './network.ts'
17
+ export { forEachLine } from './lines.ts'
18
+ export { isInside } from './paths.ts'
19
+ export { backgroundPriority, machineState, NODE_RESOURCE_ARGS, nodeResourceArgs, resourceEnvironment, spawnBackgroundNode, spawnInteractiveNode, threadBudget } from './resources.ts'
20
+ export type { MachineState, ResourceClass } from './resources.ts'
21
+ export { irRevision } from './revision.ts'
22
+ export { findInstalledPackage } from './installed-packages.ts'
23
+ export { directSandbox, minimalRunEnv, RUN_REQUEST_FILE, RUN_RESULT_PREFIX, runResultFile } from './run-directory.ts'
24
+ export { validateDocumentHistoryQuery, validateDocumentHistoryRead, validateDocumentHistoryChange } from './document-history.ts'
25
+ export { validateAuthoringClientRequest, validateAuthoringClientQuery, validateAuthoringClientRequestQuery, validateAuthoringClientOpen, validateAuthoringClientReport, validateAuthoringClientAcknowledgement, validateAuthoringClientLease } from './authoring-clients.ts'
26
+ export { validateProjectPlan, validateProjectApply } from './projects.ts'
27
+ export { validateDocumentWorkspaceImageInspect, validateDocumentWorkspaceImageImport } from './document-image.ts'
28
+ export { systemLocale } from './locale.ts'
29
+ export { OperatorSpeechSettings, operatorSpeechSecrets, SpeechSettingsError } from './operator-speech.ts'
@@ -0,0 +1,11 @@
1
+ import { existsSync, realpathSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ /** The real directory of the package `name` installed in a `node_modules` at or above `from`, as Node resolves it. */
5
+ export function findInstalledPackage(name: string, from: string): string | undefined {
6
+ for (let dir = from; ; dir = dirname(dir)) {
7
+ const candidate = join(dir, 'node_modules', name)
8
+ if (existsSync(join(candidate, 'package.json'))) return realpathSync(candidate)
9
+ if (dirname(dir) === dir) return undefined
10
+ }
11
+ }
@@ -0,0 +1,24 @@
1
+ import type { Readable } from 'node:stream'
2
+
3
+ // Terminal colours and cursor moves, which a log shown on a page does not draw.
4
+ const ESCAPES = /\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*\x07/g
5
+
6
+ /** Calls `each` with every line `stream` writes (carriage returns end a line too), without terminal escapes. */
7
+ export function forEachLine(stream: Readable | null | undefined, each: (line: string) => void): void {
8
+ if (!stream) return
9
+ let rest = ''
10
+ stream.setEncoding('utf8')
11
+ stream.on('data', (chunk: string) => {
12
+ const parts = (rest + chunk).split(/\r\n|\r|\n/)
13
+ rest = parts.pop() ?? ''
14
+ for (const part of parts) {
15
+ const line = part.replace(ESCAPES, '')
16
+ if (line.trim()) each(line)
17
+ }
18
+ })
19
+ stream.on('end', () => {
20
+ const line = rest.replace(ESCAPES, '')
21
+ if (line.trim()) each(line)
22
+ rest = ''
23
+ })
24
+ }
@@ -0,0 +1,7 @@
1
+ import type { Locale } from '../contracts/locale.ts'
2
+
3
+ /** The language of command-line output: the system's (`LC_ALL`, `LC_MESSAGES`, `LANG`, else `Intl`'s); `zh*` is zh-CN. */
4
+ export function systemLocale(env: NodeJS.ProcessEnv = process.env): Locale {
5
+ const tag = env.LC_ALL || env.LC_MESSAGES || env.LANG || Intl.DateTimeFormat().resolvedOptions().locale
6
+ return /^zh/i.test(tag) ? 'zh-CN' : 'en'
7
+ }
@@ -0,0 +1,11 @@
1
+ import { networkInterfaces } from 'node:os'
2
+
3
+ /** This machine's IPv4 addresses on its local networks, for printing the addresses other devices can use. */
4
+ export function lanAddresses(): string[] {
5
+ return Object.values(networkInterfaces()).flatMap(entries => entries ?? []).filter(entry => entry.family === 'IPv4' && !entry.internal).map(entry => entry.address)
6
+ }
7
+
8
+ /** The host as a URL writes it: an IPv6 address in brackets. */
9
+ export function bracketIPv6(host: string): string {
10
+ return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host
11
+ }
@@ -0,0 +1,91 @@
1
+ import { createRequire } from 'node:module'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { dirname, join } from 'node:path'
5
+ import type { SpeechSettingsAnswer, SpeechSettingsChange } from '../contracts/speech-settings.ts'
6
+
7
+ const KEY = 'PRESMIUM_SPEECH_VOLCENGINE_KEY', APPID = 'PRESMIUM_SPEECH_VOLCENGINE_APPID'
8
+ const MAX_BYTES = 16384
9
+ const lockfile = createRequire(import.meta.url)('proper-lockfile') as { lock(file: string, options: { realpath: boolean, retries: number }): Promise<() => Promise<void>> }
10
+ interface Stored { version: 1, revision: string, key?: string, appId?: string }
11
+ const empty = (): Stored => ({ version: 1, revision: 'none' })
12
+
13
+ export class SpeechSettingsError extends Error {
14
+ readonly code: 'speech-settings-conflict' | 'speech-settings-invalid' | 'speech-settings-unavailable' | 'speech-settings-busy'
15
+ constructor(code: SpeechSettingsError['code']) { super(code); this.code = code }
16
+ }
17
+
18
+ /** One operator-owned file; no values leave this module except through the Node secret resolver. */
19
+ export class OperatorSpeechSettings {
20
+ readonly #file: string | undefined
21
+ readonly #fallback: (name: string) => string | undefined
22
+ constructor(directory: string | undefined, fallback: (name: string) => string | undefined) {
23
+ this.#file = directory ? join(directory, 'speech', 'credentials.json') : undefined
24
+ this.#fallback = fallback
25
+ }
26
+ #read(): Stored {
27
+ if (!this.#file) return empty()
28
+ let fd: number | undefined
29
+ try {
30
+ fd = openSync(this.#file, constants.O_RDONLY | constants.O_NOFOLLOW)
31
+ const stat = fstatSync(fd)
32
+ if (!stat.isFile() || stat.size > MAX_BYTES) throw new SpeechSettingsError('speech-settings-unavailable')
33
+ const value = JSON.parse(readFileSync(fd, 'utf8')) as Stored
34
+ if (value?.version !== 1 || typeof value.revision !== 'string' || value.key !== undefined && typeof value.key !== 'string' || value.appId !== undefined && (typeof value.appId !== 'string' || !/^\d{1,32}$/.test(value.appId))) throw new SpeechSettingsError('speech-settings-unavailable')
35
+ return value
36
+ } catch (error) {
37
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return empty()
38
+ throw new SpeechSettingsError('speech-settings-unavailable')
39
+ } finally { if (fd !== undefined) closeSync(fd) }
40
+ }
41
+ secret = (name: string): string | undefined => {
42
+ if (name !== KEY && name !== APPID) return this.#fallback(name)
43
+ const saved = this.#read()
44
+ // API-key authentication must not inherit a different environment credential's App ID.
45
+ if (name === APPID && saved.key) return saved.appId
46
+ return (name === KEY ? saved.key : undefined) ?? this.#fallback(name)
47
+ }
48
+ read(): SpeechSettingsAnswer {
49
+ const value = this.#read(), key = value.key ?? this.#fallback(KEY)?.trim()
50
+ return { provider: 'volcengine', model: 'seed-tts-2.0', revision: value.revision, configured: Boolean(key), appIdConfigured: Boolean(value.key ? value.appId : this.#fallback(APPID)), source: value.key ? 'saved' : key ? 'environment' : 'none', stored: Boolean(value.key), writable: Boolean(this.#file), verification: 'configuration-only' }
51
+ }
52
+ async change(change: SpeechSettingsChange): Promise<SpeechSettingsAnswer> {
53
+ if (!this.#file) throw new SpeechSettingsError('speech-settings-unavailable')
54
+ const directory = dirname(this.#file)
55
+ try {
56
+ mkdirSync(directory, { recursive: true, mode: 0o700 })
57
+ if (lstatSync(directory).isSymbolicLink()) throw new Error('unsafe-directory')
58
+ } catch { throw new SpeechSettingsError('speech-settings-unavailable') }
59
+ let release: () => Promise<void>
60
+ try { release = await lockfile.lock(this.#file, { realpath: false, retries: 0 }) }
61
+ catch { throw new SpeechSettingsError('speech-settings-busy') }
62
+ let temporary: string | undefined
63
+ try {
64
+ const previous = this.#read()
65
+ if (change.revision !== previous.revision) throw new SpeechSettingsError('speech-settings-conflict')
66
+ let next: Stored = { version: 1, revision: randomUUID() }
67
+ if (change.action === 'save') {
68
+ const key = change.apiKey?.trim() || previous.key || this.#fallback(KEY)?.trim()
69
+ if (!key || !/^[\x21-\x7e]{8,8192}$/.test(key)) throw new SpeechSettingsError('speech-settings-invalid')
70
+ const appId = change.appId?.trim() ?? (change.apiKey?.trim() ? undefined : previous.key ? previous.appId : this.#fallback(APPID)?.trim())
71
+ if (appId && !/^\d{1,32}$/.test(appId)) throw new SpeechSettingsError('speech-settings-invalid')
72
+ next = { ...next, key, ...appId ? { appId } : {} }
73
+ }
74
+ temporary = `${this.#file}.${randomUUID()}.tmp`
75
+ writeFileSync(temporary, `${JSON.stringify(next)}\n`, { mode: 0o600, flag: 'wx' })
76
+ renameSync(temporary, this.#file)
77
+ temporary = undefined
78
+ return this.read()
79
+ } catch (error) {
80
+ if (error instanceof SpeechSettingsError) throw error
81
+ throw new SpeechSettingsError('speech-settings-unavailable')
82
+ } finally {
83
+ if (temporary) rmSync(temporary, { force: true })
84
+ await release()
85
+ }
86
+ }
87
+ }
88
+
89
+ export function operatorSpeechSecrets(directory: string | undefined, fallback: (name: string) => string | undefined): (name: string) => string | undefined {
90
+ return new OperatorSpeechSettings(directory, fallback).secret
91
+ }
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { createRequire, findPackageJSON } from 'node:module'
3
+ import { dirname, isAbsolute, join, relative, sep } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ export const TYPESCRIPT_IMPORT: readonly string[] = ['--import', new URL('./strip-types.mjs', import.meta.url).href]
7
+
8
+ /**
9
+ * The import specifier of `file` in a module generated in `dir`: relative where it can be (Vite's dependency scan does
10
+ * not follow an import written as an absolute path), else absolute (another drive on Windows).
11
+ */
12
+ export function importSpecifier(dir: string, file: string): string {
13
+ const path = relative(dir, file)
14
+ if (isAbsolute(path)) return file
15
+ const posix = path.split(sep).join('/')
16
+ return posix.startsWith('.') ? posix : `./${posix}`
17
+ }
18
+
19
+ /** `file` as a glob that matches only it: Vite's `optimizeDeps.entries` are globs. */
20
+ export function literalGlob(file: string): string {
21
+ return file.split(sep).join('/').replace(/[\\*?[\]{}()!@+|]/g, '\\$&')
22
+ }
23
+
24
+ export function packageOf(file: string): string | undefined {
25
+ const manifest = findPackageJSON(pathToFileURL(file))
26
+ return manifest && dirname(manifest)
27
+ }
28
+
29
+ function manifestOf(name: string, from: string): string | undefined {
30
+ const require = createRequire(from)
31
+ try {
32
+ return require.resolve(`${name}/package.json`)
33
+ } catch {}
34
+ let dir: string
35
+ try {
36
+ dir = dirname(require.resolve(name))
37
+ } catch {
38
+ return undefined
39
+ }
40
+ for (; dirname(dir) !== dir; dir = dirname(dir)) {
41
+ const file = join(dir, 'package.json')
42
+ if (existsSync(file) && (JSON.parse(readFileSync(file, 'utf8')) as { name?: string }).name === name) return file
43
+ }
44
+ return undefined
45
+ }
46
+
47
+ /** The file of the package's command `command` (its `bin`), the package as Node resolves it from `from`; undefined when either is missing. */
48
+ export function findPackageBin(name: string, command: string, from: string): string | undefined {
49
+ const manifest = manifestOf(name, from)
50
+ if (!manifest) return undefined
51
+ const { bin } = JSON.parse(readFileSync(manifest, 'utf8')) as { bin?: string | Record<string, string> }
52
+ const file = typeof bin === 'string' ? bin : bin?.[command]
53
+ return file === undefined ? undefined : join(dirname(manifest), file)
54
+ }
55
+
56
+ /**
57
+ * Playwright's Chromium (`playwright-chromium`), as the deck installs it, else as `fallbackFrom` (a module's URL or a
58
+ * `package.json`) finds it.
59
+ */
60
+ export async function playwrightChromium<T = unknown>(deckDir: string, fallbackFrom?: string): Promise<T> {
61
+ for (const from of [join(deckDir, 'package.json'), ...fallbackFrom ? [fallbackFrom] : []]) {
62
+ let entry: string
63
+ try {
64
+ entry = createRequire(from).resolve('playwright-chromium')
65
+ } catch {
66
+ continue
67
+ }
68
+ const loaded = await import(pathToFileURL(entry).href) as { chromium?: T, default?: { chromium?: T } }
69
+ const chromium = loaded.chromium ?? loaded.default?.chromium
70
+ if (chromium) return chromium
71
+ throw new Error('playwright-chromium does not export its Chromium browser')
72
+ }
73
+ throw new Error('this needs playwright-chromium: install it in the deck (and its browser with pnpm exec playwright install chromium)')
74
+ }
@@ -0,0 +1,7 @@
1
+ import { isAbsolute, relative, sep } from 'node:path'
2
+
3
+ /** Whether path is root itself or lies under it; symbolic links are not resolved. */
4
+ export function isInside(path: string, root: string): boolean {
5
+ const rel = relative(root, path)
6
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { Schema } from '@cfworker/json-schema'
2
+ import { Validator } from '@cfworker/json-schema'
3
+ import { PROJECT_APPLY_SCHEMA, PROJECT_PLAN_SCHEMA } from '../contracts/projects.ts'
4
+ import type { ProjectApply, ProjectPlanRequest } from '../contracts/projects.ts'
5
+ const plan = new Validator(structuredClone(PROJECT_PLAN_SCHEMA) as unknown as Schema, '2020-12', false), apply = new Validator(structuredClone(PROJECT_APPLY_SCHEMA) as unknown as Schema, '2020-12', false)
6
+ export const validateProjectPlan = (value: unknown): value is ProjectPlanRequest => plan.validate(value).valid
7
+ export const validateProjectApply = (value: unknown): value is ProjectApply => apply.validate(value).valid
@@ -0,0 +1,86 @@
1
+ import { spawn } from 'node:child_process'
2
+ import type { ChildProcess, SpawnOptions } from 'node:child_process'
3
+ import { availableParallelism, constants, freemem, loadavg, setPriority } from 'node:os'
4
+ import { TYPESCRIPT_IMPORT } from './packages.ts'
5
+
6
+ const THREAD_VARIABLES = ['GOMAXPROCS', 'RAYON_NUM_THREADS', 'OMP_NUM_THREADS', 'OPENBLAS_NUM_THREADS', 'MKL_NUM_THREADS', 'BLIS_NUM_THREADS', 'VECLIB_MAXIMUM_THREADS', 'NUMEXPR_MAX_THREADS', 'NUMEXPR_NUM_THREADS', 'POLARS_MAX_THREADS']
7
+ export const NODE_RESOURCE_ARGS: readonly string[] = ['--v8-pool-size=1']
8
+
9
+ /**
10
+ * `interactive`: what the author waits for, the workspace service and the previews' dev servers; `background`: what
11
+ * runs while the author does something else, builds, exports and code runs.
12
+ */
13
+ export type ResourceClass = 'interactive' | 'background'
14
+
15
+ /** What the machine has now: its processors, how many of them are busy (the one-minute load), and its free memory. */
16
+ export interface MachineState {
17
+ cpus: number
18
+ load: number
19
+ free: number
20
+ }
21
+
22
+ export function machineState(): MachineState {
23
+ return { cpus: availableParallelism(), load: loadavg()[0] ?? 0, free: freemem() }
24
+ }
25
+
26
+ // Bundlers gain little from more threads than this, and each thread holds memory.
27
+ const INTERACTIVE_MAX = 8
28
+ // Below this much free memory an interactive child takes two threads at most.
29
+ const LOW_MEMORY = 2 * 1024 ** 3
30
+
31
+ /**
32
+ * The native threads a child may use. A background child keeps to the operator's `PRESMIUM_MAX_THREADS`, one by default
33
+ * and at most half the processors. An interactive child takes three quarters of the processors that are idle now (all
34
+ * of them where the load is not known, as on Windows), keeping one for the rest of the machine, at most eight, and two
35
+ * when memory is short; `PRESMIUM_MAX_THREADS` caps it.
36
+ */
37
+ export function threadBudget(kind: ResourceClass, source: NodeJS.ProcessEnv = process.env, machine: MachineState = machineState()): number {
38
+ const requested = Number(source.PRESMIUM_MAX_THREADS)
39
+ const operator = Number.isInteger(requested) && requested > 0 ? requested : undefined
40
+ if (kind === 'background') return Math.min(operator ?? 1, Math.max(1, Math.floor(machine.cpus / 2)))
41
+ const idle = Math.max(1, machine.cpus - Math.min(machine.load, machine.cpus))
42
+ let threads = Math.max(1, Math.min(Math.floor(idle * 0.75), machine.cpus - 1, INTERACTIVE_MAX))
43
+ if (machine.free < LOW_MEMORY) threads = Math.min(threads, 2)
44
+ return operator === undefined ? threads : Math.min(threads, operator)
45
+ }
46
+
47
+ /** Bound native thread pools as well as JS jobs; leave CPU capacity for other apps. */
48
+ export function resourceEnvironment(source: NodeJS.ProcessEnv = process.env, kind: ResourceClass = 'background', machine?: MachineState): Record<string, string> {
49
+ const env = Object.fromEntries(Object.entries(source).filter((entry): entry is [string, string] => entry[1] !== undefined))
50
+ const threads = threadBudget(kind, source, machine)
51
+ if (source.PRESMIUM_MAX_THREADS !== undefined && kind === 'background') env.PRESMIUM_MAX_THREADS = String(threads)
52
+ for (const name of THREAD_VARIABLES) {
53
+ const current = Number(source[name])
54
+ // An interactive child takes its own budget: a smaller value it inherited came from a background parent's limit.
55
+ env[name] = String(kind === 'background' && Number.isInteger(current) && current > 0 ? Math.min(current, threads) : threads)
56
+ }
57
+ const ioThreads = Number(source.UV_THREADPOOL_SIZE)
58
+ env.UV_THREADPOOL_SIZE = kind === 'interactive'
59
+ ? String(Math.max(4, threads))
60
+ : String(Number.isInteger(ioThreads) && ioThreads > 0 ? Math.min(ioThreads, 2) : 2)
61
+ return env
62
+ }
63
+
64
+ /** Node's own arguments for a child: V8's background pool as large as its thread budget. */
65
+ export function nodeResourceArgs(kind: ResourceClass, source: NodeJS.ProcessEnv = process.env, machine?: MachineState): string[] {
66
+ return kind === 'background' ? [...NODE_RESOURCE_ARGS] : [`--v8-pool-size=${threadBudget(kind, source, machine)}`]
67
+ }
68
+
69
+ /** Child priorities are best effort on platforms that restrict changing niceness. */
70
+ export function backgroundPriority(pid: number | undefined): void {
71
+ if (pid === undefined) return
72
+ try { setPriority(pid, constants.priority.PRIORITY_BELOW_NORMAL) } catch {}
73
+ }
74
+
75
+ /** Starts Node on `script` as a background child: one V8 worker, bounded thread pools, TypeScript imports and a lower priority. */
76
+ export function spawnBackgroundNode(script: string, args: readonly string[], options: SpawnOptions = {}): ChildProcess {
77
+ const child = spawn(process.execPath, [...NODE_RESOURCE_ARGS, ...TYPESCRIPT_IMPORT, script, ...args], { env: resourceEnvironment(), ...options })
78
+ backgroundPriority(child.pid)
79
+ return child
80
+ }
81
+
82
+ /** Starts Node on `script` as a child the author waits for: the interactive thread budget at normal priority, with TypeScript imports. */
83
+ export function spawnInteractiveNode(script: string, args: readonly string[], options: SpawnOptions = {}): ChildProcess {
84
+ const machine = machineState()
85
+ return spawn(process.execPath, [...nodeResourceArgs('interactive', process.env, machine), ...TYPESCRIPT_IMPORT, script, ...args], { env: resourceEnvironment(process.env, 'interactive', machine), ...options })
86
+ }
@@ -0,0 +1,6 @@
1
+ import { createHash } from 'node:crypto'
2
+ import type { IRDocument } from '../ir.ts'
3
+
4
+ export function irRevision(doc: IRDocument): string {
5
+ return createHash('sha256').update(JSON.stringify(doc)).digest('hex').slice(0, 16)
6
+ }
@@ -0,0 +1,70 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+ import type { ProcessSandbox, SandboxResult, SandboxRun } from '../contracts/execution.ts'
6
+ import { backgroundPriority, resourceEnvironment } from './resources.ts'
7
+
8
+ /** The runner reads its requests from this file of the run directory (`runner.py`, the script and WASI runners). */
9
+ export const RUN_REQUEST_FILE = '.presmium/request.json'
10
+
11
+ export const RUN_RESULT_PREFIX = '.presmium/result-'
12
+
13
+ /** The runner writes the result of the request at this index here, once it is complete. */
14
+ export function runResultFile(index: number): string {
15
+ return `${RUN_RESULT_PREFIX}${index}.json`
16
+ }
17
+
18
+ /** The environment of a run that is given none: `PATH`, `LANG` and a temporary `HOME`, no credentials. */
19
+ export function minimalRunEnv(): Record<string, string> {
20
+ const home = join(tmpdir(), 'presmium-home')
21
+ mkdirSync(home, { recursive: true })
22
+ return { PATH: process.env.PATH ?? '', LANG: process.env.LANG ?? 'C.UTF-8', HOME: home }
23
+ }
24
+
25
+ /** The process sandbox when none is configured: runs the command directly, in a new temporary run directory. */
26
+ export const directSandbox: ProcessSandbox = {
27
+ kind: 'none',
28
+ run(request: SandboxRun): Promise<SandboxResult> {
29
+ const dir = mkdtempSync(join(tmpdir(), 'presmium-run-'))
30
+ for (const [path, content] of Object.entries(request.files)) {
31
+ mkdirSync(dirname(join(dir, path)), { recursive: true })
32
+ writeFileSync(join(dir, path), content)
33
+ }
34
+ return new Promise((resolve) => {
35
+ const [command, ...args] = request.argv
36
+ const child = spawn(command!, args, { cwd: dir, env: resourceEnvironment(request.env), stdio: ['ignore', 'pipe', 'pipe'] })
37
+ backgroundPriority(child.pid)
38
+ const out: Buffer[] = []
39
+ const err: Buffer[] = []
40
+ let stopped = false
41
+ const stop = () => {
42
+ stopped = true
43
+ child.kill('SIGKILL')
44
+ }
45
+ const timer = setTimeout(stop, request.timeoutMs)
46
+ if (request.signal?.aborted) stop()
47
+ request.signal?.addEventListener('abort', stop, { once: true })
48
+ child.stdout.on('data', (chunk: Buffer) => out.push(chunk))
49
+ child.stderr.on('data', (chunk: Buffer) => err.push(chunk))
50
+ let settled = false
51
+ const finish = (exitCode: number | null, error?: Error) => {
52
+ if (settled) return
53
+ settled = true
54
+ clearTimeout(timer)
55
+ request.signal?.removeEventListener('abort', stop)
56
+ const files: Record<string, Uint8Array> = {}
57
+ for (const path of request.collect) {
58
+ try {
59
+ files[path] = readFileSync(join(dir, path))
60
+ } catch {}
61
+ }
62
+ rmSync(dir, { recursive: true, force: true })
63
+ const stderr = Buffer.concat(err).toString('utf8').replaceAll(dir, '.')
64
+ resolve({ exitCode: stopped ? null : exitCode, stdout: Buffer.concat(out).toString('utf8'), stderr: error ? `${stderr}${error.message}` : stderr, files })
65
+ }
66
+ child.on('error', error => finish(null, error))
67
+ child.on('close', code => finish(code))
68
+ })
69
+ },
70
+ }
@@ -0,0 +1,15 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { registerHooks, stripTypeScriptTypes } from 'node:module'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const emitWarning = process.emitWarning
6
+ process.emitWarning = (warning, ...args) => {
7
+ if (!String(warning).includes('stripTypeScriptTypes')) emitWarning.call(process, warning, ...args)
8
+ }
9
+
10
+ registerHooks({
11
+ load(url, context, nextLoad) {
12
+ if (!url.startsWith('file:') || !/\.m?ts$/.test(url) || !url.includes('/node_modules/')) return nextLoad(url, context)
13
+ return { format: 'module', source: stripTypeScriptTypes(readFileSync(fileURLToPath(url), 'utf8')), shortCircuit: true }
14
+ },
15
+ })
@@ -0,0 +1,9 @@
1
+ import { Validator } from '@cfworker/json-schema'
2
+ import type { Schema } from '@cfworker/json-schema'
3
+ import { TRANSCRIPTION_ADOPTION_APPLY_SCHEMA, TRANSCRIPTION_ADOPTION_DRAFT_SCHEMA } from '../contracts/transcription-adoption.ts'
4
+ import type { TranscriptionAdoptionApply, TranscriptionAdoptionDraft } from '../contracts/transcription-adoption.ts'
5
+
6
+ const draft = new Validator(structuredClone(TRANSCRIPTION_ADOPTION_DRAFT_SCHEMA) as unknown as Schema, '2020-12', false)
7
+ const apply = new Validator(structuredClone(TRANSCRIPTION_ADOPTION_APPLY_SCHEMA) as unknown as Schema, '2020-12', false)
8
+ export function validateTranscriptionAdoptionDraft(value: unknown): value is TranscriptionAdoptionDraft { return draft.validate(value).valid }
9
+ export function validateTranscriptionAdoptionApply(value: unknown): value is TranscriptionAdoptionApply { return apply.validate(value).valid }
@@ -0,0 +1,10 @@
1
+ import { Validator } from '@cfworker/json-schema'
2
+ import type { Schema } from '@cfworker/json-schema'
3
+ import { DOCUMENT_TRANSCRIPTION_REQUEST_SCHEMA } from '../contracts/transcription.ts'
4
+ import type { DocumentTranscriptionRequest } from '../contracts/transcription.ts'
5
+
6
+ const requestValidator = new Validator(structuredClone(DOCUMENT_TRANSCRIPTION_REQUEST_SCHEMA) as unknown as Schema, '2020-12', false)
7
+
8
+ export function validateDocumentTranscriptionRequest(value: unknown): value is DocumentTranscriptionRequest {
9
+ return requestValidator.validate(value).valid
10
+ }
@@ -0,0 +1,73 @@
1
+ import { errorMessage, hasRole, isRecord, jsonResponse } from '../index.ts'
2
+ import type { DocumentVoiceTasks, VoicePreviewCheck, VoicePreviewStart, VoiceoverStart } from '../contracts/voiceover-execution.ts'
3
+ import type { Route } from '../contracts/server.ts'
4
+ import { validateVoiceoverJobQuery, validateVoiceoverStart, validateVoicePreviewCheck, validateVoicePreviewStart, VoiceoverOperationError } from './voiceover.ts'
5
+
6
+ /** The legacy Web wire shapes adapt to the same author service used by MCP. */
7
+ export function createVoiceoverRoutes(service: DocumentVoiceTasks): Record<string, Route> {
8
+ const route = (handle: Route['handle']): Route => ({ role: 'editor', maxBody: 64 << 10, async handle(request, ctx) {
9
+ try {
10
+ if (!hasRole(ctx.member, 'editor')) throw new VoiceoverOperationError(403, 'voice-authoring-permission-denied')
11
+ return await handle(request, ctx)
12
+ } catch (error) {
13
+ const status = isRecord(error) && typeof error.status === 'number' ? error.status : 502
14
+ return jsonResponse({ error: errorMessage(error) }, status)
15
+ }
16
+ } })
17
+ const body = async (request: Request) => await request.json().catch(() => undefined) as unknown
18
+ const confirmed = (value: unknown) => {
19
+ if (!isRecord(value)) throw new VoiceoverOperationError(400, 'voice-generation-invalid')
20
+ if (value.confirmed !== true) throw new VoiceoverOperationError(409, 'voice-confirmation-required')
21
+ const { confirmed: _confirmed, ...request } = value
22
+ return request
23
+ }
24
+ return {
25
+ voiceover: route(async (request, ctx) => {
26
+ if (request.method === 'GET') return jsonResponse(await service.snapshot(ctx.document, {}, ctx.member, request.signal))
27
+ if (request.method !== 'POST') throw new VoiceoverOperationError(405, 'GET or POST')
28
+ const value = confirmed(await body(request))
29
+ if (!validateVoiceoverStart(value)) throw new VoiceoverOperationError(400, 'voice-generation-invalid')
30
+ return jsonResponse(await service.start(ctx.document, value as VoiceoverStart, ctx.member, request.signal), 202)
31
+ }),
32
+ 'voiceover/jobs': route(async (request, ctx) => {
33
+ if (request.method !== 'GET') throw new VoiceoverOperationError(405, 'GET')
34
+ return jsonResponse(await service.listJobs(ctx.document, ctx.member))
35
+ }),
36
+ 'voiceover/job': route(async (request, ctx) => {
37
+ if (request.method !== 'GET') throw new VoiceoverOperationError(405, 'GET')
38
+ const query = Object.fromEntries([...new URL(request.url).searchParams].filter(([key]) => key !== 'document'))
39
+ if (!validateVoiceoverJobQuery(query)) throw new VoiceoverOperationError(400, 'voice-job-query-invalid')
40
+ return jsonResponse(await service.getJob(ctx.document, query, ctx.member))
41
+ }),
42
+ 'voiceover/cancel': route(async (request, ctx) => {
43
+ if (request.method !== 'POST') throw new VoiceoverOperationError(405, 'POST')
44
+ const value = await body(request)
45
+ if (!validateVoiceoverJobQuery(value)) throw new VoiceoverOperationError(400, 'voice-cancel-invalid')
46
+ const answer = await service.cancel(ctx.document, value, ctx.member)
47
+ return jsonResponse('id' in value ? answer.job : answer)
48
+ }),
49
+ 'voice-preview': route(async (request, ctx) => {
50
+ if (request.method === 'GET') {
51
+ const params = new URL(request.url).searchParams
52
+ let voice: unknown, target: unknown, source: unknown
53
+ try { voice = JSON.parse(params.get('voice') ?? 'null'); target = JSON.parse(params.get('target') ?? 'null'); source = JSON.parse(params.get('source') ?? 'null') }
54
+ catch { throw new VoiceoverOperationError(400, 'voice-preview-invalid') }
55
+ const query = { revision: params.get('revision'), voice, mode: params.get('mode'), ...target !== null ? { target } : {}, ...source !== null ? { source } : {} }
56
+ if (!validateVoicePreviewCheck(query)) throw new VoiceoverOperationError(400, 'voice-preview-invalid')
57
+ return jsonResponse(await service.quotePreview(ctx.document, query as VoicePreviewCheck, ctx.member, request.signal))
58
+ }
59
+ if (request.method !== 'POST') throw new VoiceoverOperationError(405, 'GET or POST')
60
+ const value = confirmed(await body(request))
61
+ if (!validateVoicePreviewStart(value)) throw new VoiceoverOperationError(400, 'voice-preview-invalid')
62
+ const result = await service.preview(ctx.document, value as VoicePreviewStart, ctx.member, request.signal)
63
+ const { resource, bytes } = await service.readPreview(ctx.document, result.id, ctx.member)
64
+ const { id: _id, byteLength: _length, sha256: _sha, mediaType, ...answer } = resource
65
+ return jsonResponse({ ...answer, audio: `data:${mediaType};base64,${Buffer.from(bytes).toString('base64')}` })
66
+ }),
67
+ 'voice-preview/media': route(async (request, ctx) => {
68
+ if (request.method !== 'GET') throw new VoiceoverOperationError(405, 'GET')
69
+ const id = new URL(request.url).searchParams.get('id') ?? '', result = await service.readPreview(ctx.document, id, ctx.member)
70
+ return new Response(new Uint8Array(result.bytes), { headers: { 'Content-Type': result.resource.mediaType, 'Content-Length': String(result.bytes.byteLength), 'Cache-Control': 'no-store' } })
71
+ }),
72
+ }
73
+ }