@hypit/hypit 0.1.13 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +37 -3
  2. package/bin/hypit.mjs +0 -2
  3. package/dist/public/browser-capture.d.ts +9 -3
  4. package/dist/public/caption.d.ts +2 -0
  5. package/dist/public/narrative.d.ts +2 -0
  6. package/dist/public/runtime-kit.d.ts +2 -2
  7. package/dist/public/speech.d.ts +2 -0
  8. package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
  9. package/package.json +2 -1
  10. package/packages/browser-capture/README.md +9 -2
  11. package/packages/browser-capture/package.json +5 -0
  12. package/packages/browser-capture/src/browser.ts +22 -8
  13. package/packages/browser-capture/src/index.ts +8 -2
  14. package/packages/caption/README.md +1 -1
  15. package/packages/caption/src/display.ts +1 -0
  16. package/packages/caption-fine/README.md +19 -5
  17. package/packages/caption-fine/package.json +1 -0
  18. package/packages/caption-fine/src/joined-box.ts +114 -0
  19. package/packages/caption-fine/src/manifest.ts +1 -1
  20. package/packages/caption-fine/src/render.ts +99 -92
  21. package/packages/caption-fine/src/spacing.ts +8 -61
  22. package/packages/caption-fine-studio/src/index.ts +2 -4
  23. package/packages/cli/README.md +3 -2
  24. package/packages/cli/package.json +1 -0
  25. package/packages/cli/src/arguments.ts +2 -2
  26. package/packages/cli/src/command.ts +1 -1
  27. package/packages/cli/src/commands/environment.ts +21 -9
  28. package/packages/cli/src/machine-view.ts +1 -1
  29. package/packages/cli/src/main.ts +2 -1
  30. package/packages/cli/src/oauth.ts +50 -7
  31. package/packages/cli/src/output.ts +1 -0
  32. package/packages/credential-store-file/README.md +60 -0
  33. package/packages/credential-store-file/package.json +21 -0
  34. package/packages/credential-store-file/src/activation.ts +29 -0
  35. package/packages/credential-store-file/src/index.ts +1 -0
  36. package/packages/credential-store-file/src/store.ts +89 -0
  37. package/packages/credential-store-os/src/store.ts +5 -1
  38. package/packages/fonts-open/src/surface.ts +8 -2
  39. package/packages/hyperframes/README.md +16 -2
  40. package/packages/hyperframes/src/browser-program.ts +6 -1
  41. package/packages/hyperframes/src/document.ts +31 -21
  42. package/packages/hyperframes/src/project.ts +11 -20
  43. package/packages/media-execution/README.md +7 -0
  44. package/packages/media-execution/src/execute.ts +5 -5
  45. package/packages/media-execution/src/index.ts +1 -1
  46. package/packages/media-execution/src/process-env.ts +20 -0
  47. package/packages/media-execution/src/surface.ts +83 -76
  48. package/packages/narrative/README.md +3 -1
  49. package/packages/narrative/src/schema.ts +1 -0
  50. package/packages/narrative/src/types.ts +2 -0
  51. package/packages/package-loader-node/README.md +10 -0
  52. package/packages/package-loader-node/src/index.ts +1 -0
  53. package/packages/package-loader-node/src/loader.ts +25 -5
  54. package/packages/package-loader-node/src/location.ts +11 -2
  55. package/packages/provider-hyperframes-local/README.md +81 -8
  56. package/packages/provider-hyperframes-local/package.json +13 -3
  57. package/packages/provider-hyperframes-local/src/activation.ts +20 -6
  58. package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
  59. package/packages/provider-hyperframes-local/src/browser.ts +118 -0
  60. package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
  61. package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
  62. package/packages/provider-hyperframes-local/src/capture-process.ts +29 -52
  63. package/packages/provider-hyperframes-local/src/capture-worker.ts +2 -0
  64. package/packages/provider-hyperframes-local/src/capture.ts +12 -5
  65. package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
  66. package/packages/provider-hyperframes-local/src/options.ts +2 -2
  67. package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
  68. package/packages/provider-hyperframes-local/src/process.ts +22 -0
  69. package/packages/provider-hyperframes-local/src/program.ts +25 -33
  70. package/packages/provider-hyperframes-local/src/provider.ts +4 -7
  71. package/packages/provider-hyperframes-local/src/render.ts +9 -4
  72. package/packages/provider-whisperx-local/README.md +26 -10
  73. package/packages/provider-whisperx-local/src/activation.ts +10 -1
  74. package/packages/provider-whisperx-local/src/program.ts +8 -4
  75. package/packages/runtime-host-node/README.md +5 -0
  76. package/packages/runtime-host-node/src/index.ts +8 -2
  77. package/packages/runtime-host-node/src/packages.ts +13 -7
  78. package/packages/runtime-kit/README.md +6 -2
  79. package/packages/runtime-kit/src/index.ts +2 -2
  80. package/packages/runtime-local/README.md +16 -1
  81. package/packages/runtime-local/src/config.ts +1 -1
  82. package/packages/runtime-local/src/credentials.ts +10 -5
  83. package/packages/runtime-local/src/host.ts +2 -0
  84. package/packages/runtime-local/src/index.ts +1 -0
  85. package/packages/runtime-local/src/programs.ts +53 -17
  86. package/packages/script/README.md +81 -34
  87. package/packages/script/bin/migrate-0.2.mjs +90 -0
  88. package/packages/script/src/edit.ts +28 -60
  89. package/packages/script/src/lexical.ts +59 -130
  90. package/packages/script/src/manifest.ts +11 -11
  91. package/packages/script/src/narrative.ts +4 -2
  92. package/packages/script/src/parser.ts +140 -193
  93. package/packages/script/src/types.ts +2 -3
  94. package/packages/studio/src/preview/runtime-shim.ts +17 -3
  95. package/packages/studio/src/ui/code.ts +1 -1
  96. package/packages/studio/src/ui/main.ts +2 -2
  97. package/packages/studio/src/ui/markers.ts +3 -3
  98. package/packages/studio/src/ui/selection.ts +1 -1
  99. package/packages/studio/src/ui/syntax.ts +42 -21
  100. package/packages/temporal-markup/README.md +1 -1
  101. package/packages/video-cli/README.md +14 -6
  102. package/packages/video-cli/package.json +1 -0
  103. package/packages/video-cli/src/capture.ts +17 -2
  104. package/packages/video-cli/src/creation.ts +4 -6
  105. package/packages/video-cli/src/distribution.ts +3 -10
  106. package/packages/video-cli/src/media.ts +10 -3
  107. package/packages/video-cli/src/version.ts +1 -1
  108. package/packages/whisperx/README.md +56 -3
  109. package/packages/whisperx/src/component.ts +2 -5
  110. package/packages/whisperx/src/evidence.ts +4 -3
  111. package/packages/whisperx/src/index.ts +1 -0
  112. package/packages/whisperx/src/manifest.ts +2 -3
  113. package/packages/whisperx/src/surface.ts +3 -6
  114. package/packages/whisperx/src/types.ts +9 -1
  115. package/packages/yt-dlp/README.md +10 -3
  116. package/packages/yt-dlp/package.json +5 -0
  117. package/packages/yt-dlp/src/download.ts +11 -29
  118. package/packages/yt-dlp/src/environment.ts +36 -0
  119. package/packages/yt-dlp/src/index.ts +1 -0
  120. package/services/whisperx/README.md +45 -12
  121. package/services/whisperx/src/hypit_whisperx_service/application.py +3 -0
  122. package/services/whisperx/src/hypit_whisperx_service/check.py +7 -0
  123. package/services/whisperx/src/hypit_whisperx_service/config.py +7 -0
  124. package/services/whisperx/src/hypit_whisperx_service/engine.py +21 -5
  125. package/services/whisperx/src/hypit_whisperx_service/models.py +131 -0
  126. package/services/whisperx/src/hypit_whisperx_service/prepare.py +21 -3
  127. package/services/whisperx/src/hypit_whisperx_service/resources.py +19 -3
  128. package/services/yt-dlp/README.md +14 -15
  129. package/services/yt-dlp/pyproject.toml +1 -1
  130. package/services/yt-dlp/uv.lock +513 -2
@@ -0,0 +1,60 @@
1
+ # `@hypit/credential-store-file`
2
+
3
+ A writable CredentialStore for a deployment that explicitly chooses local file storage, including
4
+ Linux without an OS credential locker. It stores unencrypted credential values in an owner-private
5
+ directory outside the video project. On Windows the selected directory must have a private user ACL;
6
+ POSIX file modes do not set Windows ACLs. Choose `@hypit/credential-store-os` when using macOS Keychain
7
+ or Windows Credential Locker, or `@hypit/credential-store-env` for externally supplied, read-only values.
8
+
9
+ ## Select it before login
10
+
11
+ After `hypit runtime init`, edit the created Profile before running `auth` or starting its Runtime.
12
+ Replace its credential-store selection and the chosen Endpoint's credential reference together:
13
+
14
+ ```json
15
+ {
16
+ "credentials": {
17
+ "file": { "use": "@hypit/credential-store-file" }
18
+ },
19
+ "endpoints": {
20
+ "hypihub.default": {
21
+ "use": "@hypit/provider-hypihub",
22
+ "config": {
23
+ "baseUrl": "https://hypit.ai",
24
+ "apiKey": { "store": "file", "key": "hypihub.oauth" }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ This is a Profile fragment: preserve its format, dataRoot, other Endpoints and bindings. For an
32
+ existing Profile, edit only the selected references and use `hypit runtime use <profile>` to select it.
33
+ The Endpoint still owns its acquisition flow; the Store only persists its result:
34
+
35
+ ```bash
36
+ hypit auth login hypihub.default
37
+ hypit auth status hypihub.default
38
+ hypit auth logout hypihub.default
39
+ ```
40
+
41
+ No existing OS credential is migrated and no failed lookup tries another Store. A damaged document
42
+ can be replaced by login or deleted by logout without decoding the old value. Status and execution
43
+ report the read error rather than treating corruption as a missing credential.
44
+
45
+ ## Storage
46
+
47
+ The default directory is `credentials` under the Host state root printed by `hypit paths`.
48
+ `config.path` explicitly selects a directory, resolving relative paths against that Host root.
49
+ Opening the adapter and reading an absent key do not create files. Keep this directory outside Git.
50
+
51
+ Each key has its own JSON file containing only `secret` and optional `expiresAt`, as defined by
52
+ CredentialValue. Filenames use reversible lower-case hex encoding of the key's UTF-16 bytes, with a
53
+ `key-` prefix; they are not hashes or secret encryption. This preserves case-sensitive key identity on
54
+ case-insensitive filesystems and keeps slashes and reserved names out of filesystem path syntax.
55
+ Filesystem path-length limits still apply; an overlong key fails rather than selecting another name.
56
+
57
+ Writes create an owner-only temporary file and replace the single destination with rename. Different
58
+ keys do not overwrite each other's updates; concurrent writes to the same key keep the last completed
59
+ replacement. The Store does not enumerate credentials, maintain an index, or record write history.
60
+ Permission and I/O errors propagate. Existing directory permissions are not silently changed.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@hypit/credential-store-file",
3
+ "version": "0.0.0-dev",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "private": true,
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "hypit": {
15
+ "activation": "./src/activation.ts"
16
+ },
17
+ "dependencies": {
18
+ "@hypit/runtime": "workspace:*",
19
+ "@hypit/runtime-kit": "workspace:*"
20
+ }
21
+ }
@@ -0,0 +1,29 @@
1
+ import { join, resolve } from "node:path";
2
+ import {
3
+ createRuntimeCredentialStoreAdapterFacet,
4
+ runtimeConfigExact,
5
+ runtimeConfigObject,
6
+ runtimeConfigString,
7
+ } from "@hypit/runtime-kit";
8
+ import { FileCredentialStore } from "./store.js";
9
+
10
+ const fileCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet({
11
+ use: "@hypit/credential-store-file",
12
+ validate(context) {
13
+ const config = runtimeConfigObject(context.config, "file CredentialStore");
14
+ runtimeConfigExact(config, ["path"], "file CredentialStore");
15
+ runtimeConfigString(config.path, "file credential path");
16
+ },
17
+ open(context) {
18
+ const config = runtimeConfigObject(context.config, "file CredentialStore");
19
+ const path = runtimeConfigString(config.path, "file credential path");
20
+ return { value: new FileCredentialStore(path === undefined
21
+ ? join(context.hostStateRoot, "credentials") : resolve(context.hostStateRoot, path)) };
22
+ },
23
+ });
24
+
25
+ export const hypitPackage = {
26
+ format: "hypit.node-package@1" as const,
27
+ hostFacets: [fileCredentialStoreAdapter],
28
+ };
29
+ export default hypitPackage;
@@ -0,0 +1 @@
1
+ export { FileCredentialStore } from "./store.js";
@@ -0,0 +1,89 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, open, rename, rm, stat, unlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { verifyCredentialRef } from "@hypit/runtime";
5
+ import type { CredentialRef, CredentialValue, WritableCredentialStore } from "@hypit/runtime";
6
+
7
+ function missing(error: unknown): boolean {
8
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
9
+ }
10
+
11
+ function credentialValue(value: unknown): CredentialValue {
12
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid credential value");
13
+ const record = value as Record<string, unknown>;
14
+ if (typeof record.secret !== "string" || record.secret.length === 0
15
+ || (record.expiresAt !== undefined && (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)))) {
16
+ throw new Error("invalid credential value");
17
+ }
18
+ return { secret: record.secret, ...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt as number }) };
19
+ }
20
+
21
+ /** One file per Store-local key; no enumeration, index or cross-key read/modify/write. */
22
+ export class FileCredentialStore implements WritableCredentialStore {
23
+ constructor(readonly directory: string) {}
24
+
25
+ owns(ref: CredentialRef): boolean { return ref.store === "file"; }
26
+
27
+ #path(ref: CredentialRef): string {
28
+ verifyCredentialRef(ref);
29
+ if (!this.owns(ref)) throw new Error(`File CredentialStore does not own ${ref.store}`);
30
+ // Reversible UTF-16 encoding preserves the opaque JS key, even on case-insensitive filesystems.
31
+ // The prefix avoids reserved OS filenames; no key characters become path separators.
32
+ return join(this.directory, `key-${Buffer.from(ref.key, "utf16le").toString("hex")}.json`);
33
+ }
34
+
35
+ async #privateDirectory(create: boolean): Promise<void> {
36
+ if (create) await mkdir(this.directory, { recursive: true, mode: 0o700 });
37
+ const info = await stat(this.directory);
38
+ if (!info.isDirectory()) throw new Error(`Credential path is not a directory: ${this.directory}`);
39
+ if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
40
+ throw new Error(`Credential directory must be owner-private: ${this.directory}`);
41
+ }
42
+ }
43
+
44
+ async resolve(ref: CredentialRef): Promise<CredentialValue | undefined> {
45
+ if (!this.owns(ref)) return undefined;
46
+ const path = this.#path(ref);
47
+ try { await this.#privateDirectory(false); } catch (error) {
48
+ if (missing(error)) return undefined;
49
+ throw error;
50
+ }
51
+ let file;
52
+ try { file = await open(path, "r"); } catch (error) {
53
+ if (missing(error)) return undefined;
54
+ throw error;
55
+ }
56
+ try {
57
+ const info = await file.stat();
58
+ if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
59
+ throw new Error(`Credential file must be owner-private: ${path}`);
60
+ }
61
+ const text = await file.readFile("utf8");
62
+ // JSON parser messages may include secret bytes. Report the file, never its contents.
63
+ try { return credentialValue(JSON.parse(text)); } catch {
64
+ throw new Error(`Cannot decode credential file: ${path}; replace it with auth login or remove it with auth logout`);
65
+ }
66
+ } finally { await file.close(); }
67
+ }
68
+
69
+ async put(ref: CredentialRef, value: CredentialValue): Promise<void> {
70
+ const path = this.#path(ref);
71
+ const contents = JSON.stringify(credentialValue(value));
72
+ await this.#privateDirectory(true);
73
+ const temporary = join(this.directory, `.write-${randomUUID()}.tmp`);
74
+ const file = await open(temporary, "wx", 0o600);
75
+ try {
76
+ try { await file.writeFile(`${contents}\n`, "utf8"); } finally { await file.close(); }
77
+ await rename(temporary, path);
78
+ } finally { await rm(temporary, { force: true }); }
79
+ }
80
+
81
+ async delete(ref: CredentialRef): Promise<boolean> {
82
+ const path = this.#path(ref);
83
+ // Deletion never opens or decodes the old document.
84
+ try { await unlink(path); return true; } catch (error) {
85
+ if (missing(error)) return false;
86
+ throw error;
87
+ }
88
+ }
89
+ }
@@ -141,7 +141,11 @@ function platformBackend(service: string) {
141
141
  return { read: macosReader(service), write: macosWriter(service), remove: macosDeleter(service) };
142
142
  }
143
143
  if (process.platform === "win32") return windowsBackend();
144
- throw new Error("OS CredentialStore supports macOS and Windows only");
144
+ // A Profile that selects this Store cannot be repaired by anything the user does here, and the
145
+ // other two Stores are the answer, so name them where the failure is read.
146
+ throw new Error("OS CredentialStore supports macOS and Windows only; select "
147
+ + "@hypit/credential-store-file (owner-private local file) or @hypit/credential-store-env "
148
+ + "(externally supplied value) in this Profile's credentials instead");
145
149
  }
146
150
 
147
151
  /** One logical writable store backed by the current user's OS credential locker. */
@@ -9,7 +9,7 @@ import {
9
9
  } from "@hypit/media";
10
10
  import type { FontArtifactRef, FontStackRef } from "@hypit/media";
11
11
  import type { StructuredElement, StructuredSurfaceHandler } from "@hypit/markup";
12
- import { resolveNodePackageResource } from "@hypit/package-loader-node";
12
+ import { NodePackageNotFoundError, resolveNodePackageResource } from "@hypit/package-loader-node";
13
13
 
14
14
  import {
15
15
  openFontFamilies,
@@ -21,11 +21,17 @@ import type {
21
21
  OpenFontStyle,
22
22
  } from "./catalog.js";
23
23
 
24
+ const fontDependencies = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
25
+ readonly optionalDependencies: Readonly<Record<string, string>>;
26
+ };
27
+
24
28
  function resolveFontPackageFile(packageName: string, path: string): string {
25
29
  try {
26
30
  return resolveNodePackageResource(packageName, path, { from: import.meta.url });
27
31
  } catch (error) {
28
- const version = packageName === "@infolektuell/noto-color-emoji" ? "0.2.0" : "5.3.0";
32
+ if (!(error instanceof NodePackageNotFoundError)) throw error;
33
+ const version = fontDependencies.optionalDependencies[packageName];
34
+ if (version === undefined) throw error;
29
35
  throw new Error(
30
36
  `${packageName} is needed by this authored font. Install it once with: hypit packages install ${packageName}@${version}`,
31
37
  { cause: error },
@@ -12,6 +12,8 @@ This package understands only `VisualTrack`, `ProgramSpace` and canvas geometry.
12
12
  does not know Caption, Speech, B-roll, Seedance or any other author-domain component. It flattens
13
13
  every Track's Presents, orders them by their own absolute stacking keys and emits frame-bound local
14
14
  keyframes. An authoring Track never becomes an isolated render stacking surface.
15
+ Terminal text and SVG mask sources retain the same Present-relative frame animations as other
16
+ elements, including direct seeks into the middle of a Present.
15
17
 
16
18
  It deliberately ignores `AudioTrack`. HyperFrames produces a silent visual fact; the media pipeline
17
19
  compiles and renders program audio separately, then an explicit mux Provider joins the two. Changing
@@ -23,6 +25,12 @@ a Provider can verify and name staged bytes without guessing from the hash. A lo
23
25
  render Runtime calls `materializeHyperframesHtml()` with its own Artifact URL resolver before
24
26
  handing the HTML to HyperFrames. That environment-specific materialization is not a new compiled
25
27
  Record and does not change the compiled document.
28
+ Staging assigns short local filenames; opaque Resource IDs are never interpreted as filesystem
29
+ paths or filenames, so their length and punctuation do not restrict the host filesystem.
30
+ `stageHyperframesProject` streams each asset into its staged file. Its `validateSurface(surface, path,
31
+ signal)` callback borrows that completed file during inspection; it receives neither a whole-file
32
+ byte copy nor ownership of the file. The staging caller keeps the directory alive until all work
33
+ has settled, including cancellation.
26
34
 
27
35
  The document exposes its render domain directly rather than asking an Endpoint to scrape HTML:
28
36
  exact rational `frameRate`, integer `frameCount`, and canvas dimensions are explicit document
@@ -53,8 +61,12 @@ places a direct typed child. Every child is placed once, including sampled video
53
61
  This keeps actual video available to the renderer's exact source-frame preparation.
54
62
 
55
63
  CSS is scoped to the generated program root with `@scope`; `:scope` styles that root. The HTML can
56
- contain arbitrary local structure, SVG, internal stacking, masks and backdrop filters. The optional
57
- `setup` string is a JavaScript function body with `root` and `data` arguments. It returns a synchronous
64
+ contain arbitrary local structure, SVG, internal stacking, masks and backdrop filters.
65
+ CSS scope does not isolate HTML/SVG IDs. Use classes or data attributes for local queries; when an
66
+ SVG needs an ID for `url(#...)` or `href`, derive it from the unique `root.id` in `setup` and set both
67
+ the definition and its references there. Reusable instances must not repeat hard-coded SVG IDs.
68
+
69
+ The optional `setup` string is a JavaScript function body with `root` and `data` arguments. It returns a synchronous
58
70
  `render(localFrame)` function, evaluated on initial load and active `hf-seek` events. Outside the
59
71
  Present's lifetime, the dispatcher settles the boundary pose once instead of repeatedly updating
60
72
  an invisible program. Active seeks always redraw, including the same frame after resources become
@@ -63,6 +75,8 @@ objects in `setup`; express animation state as a function of frame and inputs so
63
75
  begin at any frame. Async work belongs to
64
76
  material preparation before rendering; browser resources belong in the program's declared artifacts.
65
77
  Use `hyperframesResourceUri(artifact.resource)` in resource-bearing markup or CSS.
78
+ Returning a Promise from `render` reports an authoring error; frame capture never waits for an
79
+ unbounded asynchronous drawing task or races it.
66
80
 
67
81
  Ordinary child sampling follows the Present clock. Reframing the parent leaves source playback
68
82
  unchanged. `projectTimelineMedia` from `@hypit/hypit/timeline` gives a component selected prepared
@@ -76,7 +76,12 @@ export function browserProgramScript(entries: readonly {
76
76
  // including repeated frame 0 after images/fonts finish loading.
77
77
  if (region !== 'active' && entry.region === region) continue;
78
78
  try {
79
- entry.render(Math.max(0, Math.min(entry.durationFrames, local)));
79
+ const result = entry.render(Math.max(0, Math.min(entry.durationFrames, local)));
80
+ if (result != null && typeof result.then === 'function') {
81
+ // Observe a later rejection, but never let asynchronous drawing race frame capture.
82
+ Promise.resolve(result).catch(fail);
83
+ throw new Error('Browser program render(localFrame) must be synchronous; prepare asynchronous resources before rendering.');
84
+ }
80
85
  entry.region = region;
81
86
  }
82
87
  catch (error) { fail(error); }
@@ -229,23 +229,22 @@ function exactFontStyle(element: VisualElement, stableId: StableDomId): string[]
229
229
  ];
230
230
  }
231
231
 
232
- function renderElement(
233
- element: VisualElement,
234
- children: ReadonlyMap<string, readonly VisualElement[]>,
235
- context: {
236
- readonly trackId: string;
237
- readonly presentId: string;
238
- readonly presentStart: string;
239
- readonly presentDuration: string;
240
- readonly presentDurationFrames: number;
241
- readonly presentStartFrame: number;
242
- readonly programNumerator: number;
243
- readonly programDenominator: number;
244
- readonly stackIndex: number;
245
- readonly emittedFilterIds: Set<string>;
246
- readonly stableId: StableDomId;
247
- },
248
- ): string {
232
+ type ElementContext = {
233
+ readonly trackId: string;
234
+ readonly presentId: string;
235
+ readonly presentStart: string;
236
+ readonly presentDuration: string;
237
+ readonly presentDurationFrames: number;
238
+ readonly presentStartFrame: number;
239
+ readonly programNumerator: number;
240
+ readonly programDenominator: number;
241
+ readonly stackIndex: number;
242
+ readonly emittedFilterIds: Set<string>;
243
+ readonly stableId: StableDomId;
244
+ };
245
+
246
+ /** Every emitted element, including SVG mask sources, keeps the same authored frame clock. */
247
+ function elementPresentation(element: VisualElement, context: ElementContext) {
249
248
  const id = context.stableId([context.trackId, context.presentId, element.id]);
250
249
  const animationName = element.animation === undefined
251
250
  ? undefined
@@ -277,6 +276,16 @@ function renderElement(
277
276
  ? ""
278
277
  : ` data-hypit-frame-animation data-hypit-animation-start-frame="${context.presentStartFrame}" data-hypit-animation-duration-frames="${animationDurationFrames}" data-hypit-animation-sample-frames="${context.presentDurationFrames}" data-hypit-animation-properties="${animationProperties.join(",")}"`;
279
278
  const common = `${commonAttributes}${animationAttributes} style="${escapeHtml(inlineStyle)}"`;
279
+ return { inlineStyle, commonAttributes: `${commonAttributes}${animationAttributes}`, common };
280
+ }
281
+
282
+ function renderElement(
283
+ element: VisualElement,
284
+ children: ReadonlyMap<string, readonly VisualElement[]>,
285
+ context: ElementContext,
286
+ ): string {
287
+ const { inlineStyle, commonAttributes, common } = elementPresentation(element, context);
288
+ const id = context.stableId([context.trackId, context.presentId, element.id]);
280
289
  if (element.kind === "mask") {
281
290
  const direct = children.get(element.id) ?? [];
282
291
  const maskRoot = direct.find((child) => child.id === element.maskElement);
@@ -295,6 +304,7 @@ function renderElement(
295
304
  throw new Error(`Local mask ${element.id} mask source must be one terminal owned element.`);
296
305
  }
297
306
  const maskSource = (() => {
307
+ const sourcePresentation = elementPresentation(maskRoot, context);
298
308
  if (maskRoot.kind === "text") {
299
309
  const alignment = maskRoot.style.find((declaration) => declaration.name === "text-align")?.value;
300
310
  const anchor = alignment === "right" || alignment === "end" ? "end" : alignment === "left" || alignment === "start" ? "start" : "middle";
@@ -310,17 +320,17 @@ function renderElement(
310
320
  ? blockAlignment === "flex-start" ? "0" : blockAlignment === "flex-end" ? "100%" : "50%"
311
321
  : String(blockAlignment === "flex-start" ? paddingTop : blockAlignment === "flex-end" ? maskHeight - paddingBottom : (paddingTop + maskHeight - paddingBottom) / 2);
312
322
  const baseline = blockAlignment === "flex-start" ? "text-before-edge" : blockAlignment === "flex-end" ? "text-after-edge" : "central";
313
- const textStyle = [css(maskRoot.style), ...exactFontStyle(maskRoot, context.stableId), "fill:currentColor"].filter(Boolean).join(";");
314
- return `<text${attributes(maskRoot.attributes)} x="${x}" y="${y}" text-anchor="${anchor}" dominant-baseline="${baseline}" style="${escapeHtml(textStyle)}">${escapeHtml(maskRoot.text)}</text>`;
323
+ const textStyle = `${sourcePresentation.inlineStyle};fill:currentColor`;
324
+ return `<text ${sourcePresentation.commonAttributes} x="${x}" y="${y}" text-anchor="${anchor}" dominant-baseline="${baseline}" style="${escapeHtml(textStyle)}">${escapeHtml(maskRoot.text)}</text>`;
315
325
  }
316
326
  if (maskRoot.kind === "text-flow" || maskRoot.kind === "path-text") {
317
327
  return `<foreignObject x="0" y="0" width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="position:relative;width:100%;height:100%">${renderOwned(maskRoot)}</div></foreignObject>`;
318
328
  }
319
329
  if (maskRoot.kind === "image") {
320
- return `<image x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" href="${escapeHtml(hyperframesResourceUri(maskRoot.artifact.resource))}" style="${escapeHtml(css(maskRoot.style))}"/>`;
330
+ return `<image ${sourcePresentation.common} x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" href="${escapeHtml(hyperframesResourceUri(maskRoot.artifact.resource))}"/>`;
321
331
  }
322
332
  if (maskRoot.kind === "surface" && maskRoot.surface.timing.kind === "still") {
323
- return `<image data-hypit-surface-resource="${maskRoot.surface.artifact.resource}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" href="${escapeHtml(hyperframesResourceUri(maskRoot.surface.artifact.resource))}" style="${escapeHtml(css(maskRoot.style))}"/>`;
333
+ return `<image ${sourcePresentation.common} data-hypit-surface-resource="${maskRoot.surface.artifact.resource}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" href="${escapeHtml(hyperframesResourceUri(maskRoot.surface.artifact.resource))}"/>`;
324
334
  }
325
335
  throw new Error(`Local mask ${element.id} requires a terminal owned text, image or still Surface mask source.`);
326
336
  })();
@@ -12,9 +12,10 @@ export type HyperframesArtifactReader = (
12
12
  artifact: BlobRef,
13
13
  signal?: AbortSignal,
14
14
  ) => Promise<Uint8Array | AsyncIterable<Uint8Array>>;
15
+ /** Inspect the completed staged file; its lifetime belongs to the staging caller. */
15
16
  export type HyperframesSurfaceValidator = (
16
17
  surface: CompositableSurfaceRef,
17
- bytes: Uint8Array,
18
+ path: string,
18
19
  signal?: AbortSignal,
19
20
  ) => Promise<void>;
20
21
 
@@ -39,9 +40,7 @@ function extension(mediaType: string): string {
39
40
  /**
40
41
  * Lay one document out as a directory HyperFrames can render.
41
42
  *
42
- * The same layout serves a local render and a distributed one: the local
43
- * Provider serves it to the capture engine, and the AWS Provider tars it into a site. If
44
- * the two laid it out differently, one Need would name two different projects.
43
+ * A local or hosted Provider can serve the same staged document to its capture engine.
45
44
  * Resource names are local to this staged project and carry no content claim.
46
45
  */
47
46
  export async function stageHyperframesProject(options: {
@@ -63,25 +62,25 @@ export async function stageHyperframesProject(options: {
63
62
  await mkdir(artifactDirectory, { recursive: true });
64
63
  const paths = new Map<string, string>();
65
64
  const surfaces = new Map(document.surfaces.map((surface) => [surface.artifact.resource, surface]));
66
- await Promise.allSettled(document.artifacts.map(async (artifact) => {
65
+ await Promise.allSettled(document.artifacts.map(async (artifact, index) => {
67
66
  try {
68
67
  signal.throwIfAborted();
69
- const name = `${artifact.resource}${extension(artifact.mediaType)}`;
68
+ // Resource identities are portable protocol values, not filesystem names.
69
+ const name = `asset-${index}${extension(artifact.mediaType)}`;
70
70
  const opened = await read(artifact, signal);
71
71
  signal.throwIfAborted();
72
72
  const chunks = opened instanceof Uint8Array
73
73
  ? (async function* () { yield opened; })()
74
74
  : opened;
75
75
  const surface = surfaces.get(artifact.resource);
76
- const retained: Uint8Array[] = [];
77
76
  let size = 0;
78
- const target = await open(join(artifactDirectory, name), "w");
77
+ const path = join(artifactDirectory, name);
78
+ const target = await open(path, "w");
79
79
  try {
80
80
  for await (const chunk of chunks) {
81
81
  signal.throwIfAborted();
82
- await target.write(chunk);
82
+ await target.writeFile(chunk);
83
83
  size += chunk.byteLength;
84
- if (surface !== undefined) retained.push(Uint8Array.from(chunk));
85
84
  }
86
85
  } finally {
87
86
  await target.close();
@@ -89,17 +88,9 @@ export async function stageHyperframesProject(options: {
89
88
  if (size !== artifact.size) {
90
89
  throw new Error(`HyperFrames Artifact ${artifact.resource} size differs`);
91
90
  }
92
- const bytes = surface === undefined ? undefined : (() => {
93
- const value = new Uint8Array(size);
94
- let offset = 0;
95
- for (const chunk of retained) {
96
- value.set(chunk, offset);
97
- offset += chunk.byteLength;
98
- }
99
- return value;
100
- })();
101
91
  if (surface !== undefined) {
102
- await options.validateSurface!(structuredClone(surface), bytes!.slice(), signal);
92
+ // The validator borrows the completed staged file for this call only.
93
+ await options.validateSurface!(structuredClone(surface), path, signal);
103
94
  }
104
95
  paths.set(artifact.resource, `./artifacts/${name}`);
105
96
  } catch (error) {
@@ -26,6 +26,13 @@ deployments can reuse the same functions to preserve stream selection, timing, c
26
26
  This is not an author package, Provider, queue or Core extension. It performs no endpoint selection,
27
27
  credential lookup or SVML parsing.
28
28
 
29
+ `verifyCompositableSurfaceFile({ surface, path, ... })` inspects a file already staged by an execution
30
+ owner. That owner must keep it unchanged until the call settles, including cancellation; inspection
31
+ neither copies nor removes it. `verifyCompositableSurfaceBytes({ surface, bytes, ... })` remains a
32
+ convenience wrapper for callers that have bytes, creates its own temporary file, and removes that
33
+ file only after probing has settled. Both use the same decoded Surface checks and return no extra
34
+ result facts. Filesystem paths belong to this Node execution boundary, not to Surface or Blob refs.
35
+
29
36
  Audio gain envelopes and audible subranges are evaluated per sample after tempo/looping and Clip
30
37
  fades, before the requested range is cropped. Original source and envelope progress therefore survive
31
38
  local silence and range rendering. This is generic AudioProgramPlan execution, independent of author
@@ -33,6 +33,7 @@ import {
33
33
  } from "@hypit/protocol";
34
34
  import type { BlobRef, CanonicalValue, StoredValue } from "@hypit/protocol";
35
35
 
36
+ import { mediaProcessEnv } from "./process-env.js";
36
37
  import { parseMediaInspection } from "./probe.js";
37
38
  import {
38
39
  compositeAnimatedWebpFrame,
@@ -98,10 +99,7 @@ async function runProcess(args: {
98
99
  shell: false,
99
100
  windowsHide: true,
100
101
  stdio: ["ignore", "pipe", "pipe"],
101
- env: {
102
- PATH: process.env.PATH ?? "",
103
- ...(args.sharedLibraryPath === undefined ? {} : { LD_LIBRARY_PATH: args.sharedLibraryPath }),
104
- },
102
+ env: mediaProcessEnv(args.sharedLibraryPath === undefined ? undefined : { LD_LIBRARY_PATH: args.sharedLibraryPath }),
105
103
  });
106
104
  const stdout: Buffer[] = [];
107
105
  let stdoutBytes = 0;
@@ -678,8 +676,10 @@ export async function executeNormalizeMedia(
678
676
  "scale=trunc(iw*max(sar\\,1)/2)*2:trunc(ih*max(1/sar\\,1)/2)*2",
679
677
  "setsar=1",
680
678
  ].join(",");
679
+ // FFmpeg enables autorotation by default. Its positive boolean option changed
680
+ // syntax across releases; no override is needed to materialize source rotation.
681
681
  const visualInput = animation === undefined
682
- ? [...encoding.inputArgs, "-autorotate", "-i", input, "-map", `0:${plan.video.index}`]
682
+ ? [...encoding.inputArgs, "-i", input, "-map", `0:${plan.video.index}`]
683
683
  : ["-f", "concat", "-safe", "0", "-i", await animatedWebpConcat(env, animation, work), "-map", "0:v:0"];
684
684
  // The execution format must retain the source's alpha while materializing
685
685
  // the program clock. Both encodings publish the same SynchronizedMedia type.
@@ -10,7 +10,7 @@ export {
10
10
  executeTransformMedia,
11
11
  } from "./execute.js";
12
12
  export { parseMediaInspection } from "./probe.js";
13
- export { verifyCompositableSurfaceBytes } from "./surface.js";
13
+ export { verifyCompositableSurfaceBytes, verifyCompositableSurfaceFile } from "./surface.js";
14
14
  export { probeMediaToolchain } from "./toolchain.js";
15
15
  export type { MediaToolchainState } from "./toolchain.js";
16
16
  export type * from "./execute.js";
@@ -0,0 +1,20 @@
1
+ const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec"] as const;
2
+
3
+ /**
4
+ * ffmpeg and ffprobe are started as PATH executables. The Host environment is
5
+ * not forwarded, so credential values stay out of those processes. Windows still
6
+ * needs the process-creation variables CreateProcess uses to resolve `.exe`.
7
+ */
8
+ export function mediaProcessEnv(
9
+ extra?: Readonly<Record<string, string>>,
10
+ ): NodeJS.ProcessEnv {
11
+ const env: NodeJS.ProcessEnv = { PATH: process.env.PATH ?? "" };
12
+ if (process.platform === "win32") {
13
+ for (const name of windowsProcessCreation) {
14
+ const value = process.env[name];
15
+ if (value !== undefined && value.length > 0) env[name] = value;
16
+ }
17
+ }
18
+ if (extra !== undefined) Object.assign(env, extra);
19
+ return env;
20
+ }