@hypit/hypit 0.2.5 → 0.2.7

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 (79) hide show
  1. package/README.md +6 -1
  2. package/dist/public/hyperframes.d.ts +19 -2
  3. package/package.json +3 -1
  4. package/packages/caption/src/temporalize.ts +11 -1
  5. package/packages/caption-fine/README.md +4 -4
  6. package/packages/credential-store-file/README.md +4 -0
  7. package/packages/credential-store-file/src/store.ts +2 -2
  8. package/packages/credential-store-os/runtime/windows-credential.ps1 +3 -0
  9. package/packages/credential-store-os/src/windows.ts +14 -0
  10. package/packages/credential-store-platform/README.md +2 -0
  11. package/packages/hyperframes/README.md +7 -0
  12. package/packages/hyperframes/src/html-project.ts +99 -0
  13. package/packages/hyperframes/src/index.ts +2 -0
  14. package/packages/hyperframes/src/project.ts +28 -0
  15. package/packages/media-execution/src/execute.ts +21 -3
  16. package/packages/pixverse/README.md +35 -0
  17. package/packages/pixverse/package.json +21 -0
  18. package/packages/pixverse/src/activation.ts +11 -0
  19. package/packages/pixverse/src/index.ts +122 -0
  20. package/packages/pixverse/src/surface.ts +146 -0
  21. package/packages/provider-hiapi/README.md +9 -1
  22. package/packages/provider-hyperframes-local/README.md +19 -7
  23. package/packages/provider-hyperframes-local/src/capture.ts +18 -8
  24. package/packages/provider-hyperframes-local/src/index.ts +1 -1
  25. package/packages/provider-hyperframes-local/src/output.ts +1 -1
  26. package/packages/provider-hyperframes-local/src/process-tree.ts +8 -2
  27. package/packages/provider-hyperframes-local/src/provider.ts +29 -2
  28. package/packages/provider-hyperframes-local/src/render.ts +45 -20
  29. package/packages/provider-hyperframes-local/src/sampling.ts +3 -2
  30. package/packages/provider-hypihub/README.md +10 -2
  31. package/packages/provider-monid/README.md +29 -9
  32. package/packages/provider-monid/package.json +2 -1
  33. package/packages/provider-monid/src/mapping.ts +24 -0
  34. package/packages/provider-monid/src/provider.ts +24 -11
  35. package/packages/provider-monid/src/routes.ts +25 -3
  36. package/packages/provider-pollo/README.md +9 -1
  37. package/packages/provider-tokendance/README.md +9 -1
  38. package/packages/provider-whisperx-local/README.md +8 -5
  39. package/packages/render-hyperframes/README.md +9 -0
  40. package/packages/render-hyperframes/src/index.ts +3 -0
  41. package/packages/render-hyperframes/src/manifest.ts +7 -2
  42. package/packages/render-hyperframes/src/product.ts +41 -2
  43. package/packages/runtime-local/src/process-control.ts +4 -1
  44. package/packages/runtime-local/src/programs.ts +15 -3
  45. package/packages/runtime-local/src/supervisor.ts +8 -1
  46. package/packages/script/README.md +11 -4
  47. package/packages/script/src/format.ts +11 -24
  48. package/packages/script/src/manifest.ts +2 -2
  49. package/packages/script/src/parser.ts +16 -21
  50. package/packages/script/src/surface.ts +5 -3
  51. package/packages/script/src/types.ts +1 -1
  52. package/packages/source/README.md +1 -1
  53. package/packages/source/src/header.ts +2 -1
  54. package/packages/studio/README.md +13 -0
  55. package/packages/studio/src/feedback-server.ts +7 -0
  56. package/packages/studio/src/mutation-origin.ts +20 -0
  57. package/packages/studio/src/parameters.ts +3 -7
  58. package/packages/studio/src/preview/render.ts +7 -1
  59. package/packages/studio/src/server.ts +39 -0
  60. package/packages/studio/src/session.ts +7 -3
  61. package/packages/studio/src/ui/syntax.ts +11 -10
  62. package/packages/temporal-markup/EDITING.md +1 -1
  63. package/packages/video-cli/README.md +47 -4
  64. package/packages/video-cli/package.json +3 -0
  65. package/packages/video-cli/src/cli.ts +9 -9
  66. package/packages/video-cli/src/creation.ts +3 -3
  67. package/packages/video-cli/src/frame-grid.ts +34 -0
  68. package/packages/video-cli/src/index.ts +4 -0
  69. package/packages/video-cli/src/media-frames.ts +25 -0
  70. package/packages/video-cli/src/media.ts +198 -46
  71. package/packages/video-cli/src/process.ts +13 -3
  72. package/packages/video-cli/src/secret-input.ts +14 -0
  73. package/packages/video-cli/src/snapshot.ts +186 -0
  74. package/packages/wan/README.md +31 -0
  75. package/packages/wan/package.json +21 -0
  76. package/packages/wan/src/activation.ts +11 -0
  77. package/packages/wan/src/index.ts +160 -0
  78. package/packages/wan/src/surface.ts +147 -0
  79. package/packages/yt-dlp/src/download.ts +32 -18
package/README.md CHANGED
@@ -15,6 +15,11 @@
15
15
  <a href="https://github.com/hypit-ai/hypit/blob/main/README.zh-CN.md"><strong>简体中文</strong></a>
16
16
  </p>
17
17
 
18
+ <p align="center">
19
+ <a href="https://trendshift.io/repositories/229030"><img alt="Trendshift #1 Repository of the Day" src="https://trendshift.io/api/badge/trendshift/repositories/229030/daily"></a>
20
+ <a href="https://trendshift.io/repositories/229030"><img alt="Trendshift #1 TypeScript Repository of the Day" src="https://trendshift.io/api/badge/trendshift/repositories/229030/daily?language=TypeScript"></a>
21
+ </p>
22
+
18
23
  <p align="center">
19
24
  <img alt="Stars" src="https://img.shields.io/github/stars/hypit-ai/hypit?style=flat-square&color=FFD700&logo=github&logoColor=white&label=Stars">
20
25
  <a href="https://github.com/hypit-ai/hypit/blob/main/package.json"><img alt="Node 22.15+" src="https://img.shields.io/badge/Node.js-22.15%2B-5FA04E?style=flat-square&logo=nodedotjs&logoColor=white"></a>
@@ -144,7 +149,7 @@ commands and the repository layout.
144
149
  </tr>
145
150
  </table>
146
151
 
147
- ## Launch Partner
152
+ ## Launch Partners
148
153
 
149
154
  <table>
150
155
  <tr>
@@ -788,5 +788,22 @@ type BrowserProgram = {
788
788
  };
789
789
  declare function browserProgram(value: BrowserProgram, artifacts?: readonly BlobRef[]): VisualProgramElement["program"];
790
790
 
791
- export { BROWSER_PROGRAM_FORMAT, assertHyperframesDocument, assertHyperframesFrameIndex, assertHyperframesFrameSpan, browserProgram, compileHyperframesDocument, hyperframesComponent, hyperframesDocumentFragment, hyperframesDocumentSchema, hyperframesManifest, hyperframesModuleRef, hyperframesProducers, hyperframesResourceUri, hyperframesTime, hyperframesTypes, materializeHyperframesHtml };
792
- export type { BrowserProgram, HyperframesCanvas, HyperframesDocument, HyperframesFrameDomain, HyperframesFrameSpan, ResourceUrlResolver };
791
+ /** An already materialized HTML programme and the local files its URLs address. */
792
+ type HyperframesHtmlProject = {
793
+ readonly html: string;
794
+ readonly assets: readonly {
795
+ readonly url: string;
796
+ readonly artifact: BlobRef;
797
+ }[];
798
+ };
799
+ /** Read the exact clock emitted by this package. Seconds never reconstruct frame identity. */
800
+ declare function hyperframesHtmlDomain(html: string): HyperframesFrameDomain & {
801
+ readonly canvas: HyperframesCanvas;
802
+ };
803
+ /** Resource URL positions in the compiler's self-contained HTML (inline JS and CSS). */
804
+ declare function mapHyperframesHtmlUrls(html: string, map: (url: string) => string): string;
805
+ declare function hyperframesHtmlAssetUrls(html: string): readonly string[];
806
+ declare function assertHyperframesHtmlProject(value: unknown): asserts value is HyperframesHtmlProject;
807
+
808
+ export { BROWSER_PROGRAM_FORMAT, assertHyperframesDocument, assertHyperframesFrameIndex, assertHyperframesFrameSpan, assertHyperframesHtmlProject, browserProgram, compileHyperframesDocument, hyperframesComponent, hyperframesDocumentFragment, hyperframesDocumentSchema, hyperframesHtmlAssetUrls, hyperframesHtmlDomain, hyperframesManifest, hyperframesModuleRef, hyperframesProducers, hyperframesResourceUri, hyperframesTime, hyperframesTypes, mapHyperframesHtmlUrls, materializeHyperframesHtml };
809
+ export type { BrowserProgram, HyperframesCanvas, HyperframesDocument, HyperframesFrameDomain, HyperframesFrameSpan, HyperframesHtmlProject, ResourceUrlResolver };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypit/hypit",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -232,6 +232,7 @@
232
232
  "@hypit/narrative": "workspace:*",
233
233
  "@hypit/performance": "workspace:*",
234
234
  "@hypit/performance-studio": "workspace:*",
235
+ "@hypit/pixverse": "workspace:*",
235
236
  "@hypit/program-space": "workspace:*",
236
237
  "@hypit/project-context-node": "workspace:*",
237
238
  "@hypit/protocol": "workspace:*",
@@ -279,6 +280,7 @@
279
280
  "@hypit/typography-track-studio": "workspace:*",
280
281
  "@hypit/visual-ir": "workspace:*",
281
282
  "@hypit/volcengine-matting": "workspace:*",
283
+ "@hypit/wan": "workspace:*",
282
284
  "@hypit/whisperx": "workspace:*",
283
285
  "@types/node": "24.10.1",
284
286
  "rollup": "4.62.4",
@@ -17,6 +17,7 @@ export function temporalizeCaptionDocument(
17
17
  }
18
18
  const breaks = new Set(document.cueBreaks.map((cueBreak) => cueBreak.afterUnitId));
19
19
  const timed: Array<{ unit: CaptionDocument["units"][number]; timing: TimedCaptionUnit }> = [];
20
+ const previousByRole = new Map<string | undefined, { timing: TimedCaptionUnit }>();
20
21
  for (const unit of document.units) {
21
22
  const window = tokenFrameSpan(semantic, unit.sourceTokenIds);
22
23
  if (window === undefined) continue;
@@ -25,7 +26,16 @@ export function temporalizeCaptionDocument(
25
26
  }
26
27
  const startFrame = window.startFrame;
27
28
  const endFrameExclusive = Math.max(startFrame + 1, window.endFrameExclusive);
28
- timed.push({ unit, timing: { unitId: unit.id, startFrame, endFrameExclusive } });
29
+ // Acoustic Word windows may overlap. The Timeline keeps those measurements; an authored unit
30
+ // gets one owner: as soon as the next unit of the same Role starts, this one stops. A Cue
31
+ // envelope ends at its last unit, so Cue boundaries are exclusive by the same rule.
32
+ const previous = previousByRole.get(unit.role);
33
+ if (previous !== undefined && startFrame > previous.timing.startFrame && startFrame < previous.timing.endFrameExclusive) {
34
+ previous.timing = { ...previous.timing, endFrameExclusive: startFrame };
35
+ }
36
+ const entry = { unit, timing: { unitId: unit.id, startFrame, endFrameExclusive } };
37
+ previousByRole.set(unit.role, entry);
38
+ timed.push(entry);
29
39
  }
30
40
  const cues: TimedCaptionCue[] = [];
31
41
  let current: { units: TimedCaptionUnit[]; segmentId: string; turnId: string } | undefined;
@@ -14,10 +14,10 @@ does not carry a private visual role.
14
14
  </caption-fine:Track>
15
15
  ```
16
16
 
17
- An optional Spatial Region Timeline replaces only the moving placement point. The timeline is authored
18
- numeric input, measured from the actual footage before the composition pass that consumes it;
19
- it is not a face-tracking request. A prior Build may produce that footage with ordinary fixed Caption,
20
- and a later Run can reuse the same media and alignment while changing placement:
17
+ An optional Spatial Region Timeline supplies a moving placement point when the user's reference
18
+ visibly uses head-following Caption or the user asks for that treatment. Ordinary Caption uses the
19
+ Style's placement without regions. The Region Timeline is authored numeric input mapped from available picture
20
+ evidence to the composition that consumes it; it is not a face-tracking request:
21
21
 
22
22
  ```xml
23
23
  <space:RegionTimeline id="heads" within={vertical} recipe={tracking.heads.default}/>
@@ -50,6 +50,10 @@ The default directory is `credentials` under the Host state root printed by `hyp
50
50
  `config.path` explicitly selects a directory, including an absolute directory outside that root;
51
51
  relative paths resolve against the Host root.
52
52
  Opening the adapter and reading an absent key do not create files. Keep this directory outside Git.
53
+ On WSL, the default under the Linux home uses normal Linux permissions. If the Host state root is
54
+ on a Windows-mounted drive, set this Store's `config.path` to a private directory in the WSL Linux
55
+ filesystem before login. A Windows mount's reported mode does not establish that its Windows ACL
56
+ protects the secret, so the Store does not waive its owner-private check for that path.
53
57
 
54
58
  Each key has its own JSON file containing only `secret` and optional `expiresAt`, as defined by
55
59
  CredentialValue. Filenames use reversible lower-case hex encoding of the key's UTF-16 bytes, with a
@@ -38,7 +38,7 @@ export class FileCredentialStore implements WritableCredentialStore {
38
38
  const info = await stat(this.directory);
39
39
  if (!info.isDirectory()) throw new Error(`Credential path is not a directory: ${this.directory}`);
40
40
  if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
41
- throw new Error(`Credential directory must be owner-private: ${this.directory}`);
41
+ throw new Error(`Credential directory must be owner-private: ${this.directory}. On WSL, keep credentials in the Linux filesystem rather than a Windows-mounted drive.`);
42
42
  }
43
43
  }
44
44
 
@@ -57,7 +57,7 @@ export class FileCredentialStore implements WritableCredentialStore {
57
57
  try {
58
58
  const info = await file.stat();
59
59
  if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
60
- throw new Error(`Credential file must be owner-private: ${path}`);
60
+ throw new Error(`Credential file must be owner-private: ${path}. On WSL, keep credentials in the Linux filesystem rather than a Windows-mounted drive.`);
61
61
  }
62
62
  const text = await file.readFile("utf8");
63
63
  // JSON parser messages may include secret bytes. Report the file, never its contents.
@@ -38,6 +38,9 @@ switch ($Operation) {
38
38
  # Add replaces the same resource/account; do not destroy the old value before it succeeds.
39
39
  $bytes = [Convert]::FromBase64String([string]$request.secret)
40
40
  $secret = [System.Text.Encoding]::UTF8.GetString($bytes)
41
+ if ($secret.Length -gt 512) {
42
+ throw "PasswordVault cannot store a $($secret.Length)-character secret (limit 512). Select @hypit/credential-store-file for OAuth tokens."
43
+ }
41
44
  $credential = New-Object Windows.Security.Credentials.PasswordCredential(
42
45
  [string]$request.service,
43
46
  [string]$request.account,
@@ -9,6 +9,19 @@ type WindowsCredentialResult = {
9
9
 
10
10
  const windowsScript = fileURLToPath(new URL("../runtime/windows-credential.ps1", import.meta.url));
11
11
 
12
+ /** PasswordVault password field; OAuth JSON routinely exceeds this. */
13
+ export const windowsLockerPasswordLimit = 512;
14
+
15
+ export function assertWindowsLockerSecret(secret: string, account: string): void {
16
+ if (secret.length > windowsLockerPasswordLimit) {
17
+ throw new Error(
18
+ `Windows Credential Locker cannot store the ${secret.length}-character secret for ${account}`
19
+ + ` (PasswordVault limit is ${windowsLockerPasswordLimit} characters).`
20
+ + " Select @hypit/credential-store-file for OAuth tokens.",
21
+ );
22
+ }
23
+ }
24
+
12
25
  /** The OS adapter owns this one child; Node owns its timeout, bounded output and termination. */
13
26
  export function windowsCredential(
14
27
  operation: "read" | "write" | "delete",
@@ -16,6 +29,7 @@ export function windowsCredential(
16
29
  account: string,
17
30
  secret?: string,
18
31
  ): Promise<WindowsCredentialResult> {
32
+ if (secret !== undefined) assertWindowsLockerSecret(secret, account);
19
33
  return new Promise((resolve, reject) => {
20
34
  const child = execFile("powershell.exe", [
21
35
  "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
@@ -47,6 +47,8 @@ credential stored in a locker stays in that locker; a credential stored in a fil
47
47
  - `path` selects the directory for Linux file storage, resolved against the Host state root printed by
48
48
  `hypit paths`. The default is the same `credentials` directory `@hypit/credential-store-file` uses,
49
49
  so on Linux, switching between the two Stores finds the credential already stored.
50
+ - WSL uses the Linux file Store. If the Host state root is on a Windows-mounted drive, select a
51
+ private directory in the WSL Linux filesystem with `path`; the Store keeps its permission check.
50
52
  - `service` selects the locker service name. The OS Store's own default applies when it is absent.
51
53
 
52
54
  ## Storage on each platform
@@ -25,6 +25,13 @@ a Provider can verify and name staged bytes without guessing from the hash. A lo
25
25
  render Runtime calls `materializeHyperframesHtml()` with its own Artifact URL resolver before
26
26
  handing the HTML to HyperFrames. That environment-specific materialization is not a new compiled
27
27
  Record and does not change the compiled document.
28
+
29
+ For observation of already materialized HTML, `HyperframesHtmlProject` carries the HTML and the
30
+ BlobRefs addressed by its media/font URLs. `hyperframesHtmlDomain` reads the compiler's exact root
31
+ frame clock and canvas; `stageHyperframesHtmlProject` stages those resources. This input does not
32
+ reconstruct a Composition or invent typed Surface facts from HTML. A caller with the original
33
+ `HyperframesDocument`, such as Studio, should pass that document to preserve all declared dependencies,
34
+ including resources embedded in browser-program data. The document remains the complete typed input.
28
35
  Staging assigns short local filenames; opaque Resource IDs are never interpreted as filesystem
29
36
  paths or filenames, so their length and punctuation do not restrict the host filesystem.
30
37
  `stageHyperframesProject` streams each asset into its staged file. Its `validateSurface(surface, path,
@@ -0,0 +1,99 @@
1
+ import type { BlobRef } from "@hypit/protocol";
2
+ import { isResourceId } from "@hypit/protocol";
3
+ import type { HyperframesCanvas, HyperframesFrameDomain } from "./types.js";
4
+
5
+ /** An already materialized HTML programme and the local files its URLs address. */
6
+ export type HyperframesHtmlProject = {
7
+ readonly html: string;
8
+ readonly assets: readonly { readonly url: string; readonly artifact: BlobRef }[];
9
+ };
10
+
11
+ /** Read the exact clock emitted by this package. Seconds never reconstruct frame identity. */
12
+ export function hyperframesHtmlDomain(html: string): HyperframesFrameDomain & { readonly canvas: HyperframesCanvas } {
13
+ const attributeValue = (tag: string, name: string): string | undefined => {
14
+ const marker = `${name}="`;
15
+ for (let at = tag.indexOf(marker); at !== -1; at = tag.indexOf(marker, at + marker.length)) {
16
+ const previous = tag[at - 1];
17
+ if (previous === undefined || !" \t\r\n\f".includes(previous)) continue;
18
+ const start = at + marker.length;
19
+ const end = tag.indexOf('"', start);
20
+ return end === -1 ? undefined : tag.slice(start, end);
21
+ }
22
+ return undefined;
23
+ };
24
+ let root: string | undefined;
25
+ for (let at = html.indexOf("<div"); at !== -1; at = html.indexOf("<div", at)) {
26
+ const end = html.indexOf(">", at + 4);
27
+ if (end === -1) break;
28
+ const tag = html.slice(at, end + 1);
29
+ at = end + 1;
30
+ if (attributeValue(tag, "data-composition-id") === undefined) continue;
31
+ if (root !== undefined) throw new Error("Snapshot HTML needs exactly one compiled HyperFrames composition root");
32
+ root = tag;
33
+ }
34
+ if (root === undefined) throw new Error("Snapshot HTML needs exactly one compiled HyperFrames composition root");
35
+ const attribute = (name: string) => {
36
+ const value = attributeValue(root, name);
37
+ if (value === undefined) throw new Error(`Snapshot HTML lacks ${name}; use the compiled HyperFrames HTML`);
38
+ return value;
39
+ };
40
+ const positive = (value: string, name: string) => {
41
+ const number = Number(value);
42
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(number) || number <= 0) throw new Error(`Invalid HyperFrames ${name}`);
43
+ return number;
44
+ };
45
+ const fps = attribute("data-fps").split("/");
46
+ if (fps.length > 2) throw new Error("HyperFrames data-fps must be an exact rational");
47
+ return {
48
+ frameRate: { numerator: positive(fps[0]!, "frame rate"), denominator: positive(fps[1] ?? "1", "frame rate denominator") },
49
+ frameCount: positive(attribute("data-hypit-frame-count"), "frame count"),
50
+ canvas: { width: positive(attribute("data-width"), "width"), height: positive(attribute("data-height"), "height") },
51
+ };
52
+ }
53
+
54
+ function unescape(value: string): string {
55
+ return value.replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
56
+ }
57
+
58
+ /** Resource URL positions in the compiler's self-contained HTML (inline JS and CSS). */
59
+ export function mapHyperframesHtmlUrls(html: string, map: (url: string) => string): string {
60
+ const escape = (value: string) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;");
61
+ const cssUrls = (css: string) => css.replace(/url\(\s*(["']?)([^)"']+)\1\s*\)/gu,
62
+ (_all, _quote: string, url: string) => `url("${map(url.trim()).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}")`);
63
+ return html.replace(/<style\b([^>]*)>([\s\S]*?)<\/style>/gu,
64
+ (_all, attributes: string, css: string) => `<style${attributes}>${cssUrls(css)}</style>`)
65
+ .replace(/<[a-z][^>]*>/gu, tag => {
66
+ const attributes = /^(?:<(?:img|video|source|image|use|script|link)\b)/u.test(tag)
67
+ ? tag.replace(/\s(src|poster|href)="([^"]*)"/gu,
68
+ (_all, key: string, url: string) => ` ${key}="${escape(map(unescape(url)))}"`) : tag;
69
+ return attributes.replace(/\sstyle="([^"]*)"/gu,
70
+ (_all, css: string) => ` style="${escape(cssUrls(unescape(css)))}"`);
71
+ });
72
+ }
73
+
74
+ export function hyperframesHtmlAssetUrls(html: string): readonly string[] {
75
+ const urls = new Set<string>();
76
+ mapHyperframesHtmlUrls(html, url => {
77
+ if (url !== "" && !url.startsWith("#") && !url.startsWith("data:")) urls.add(url);
78
+ return url;
79
+ });
80
+ return [...urls];
81
+ }
82
+
83
+ export function assertHyperframesHtmlProject(value: unknown): asserts value is HyperframesHtmlProject {
84
+ const project = value as HyperframesHtmlProject;
85
+ if (!project || typeof project.html !== "string" || !Array.isArray(project.assets)) throw new Error("HyperFrames HTML project needs html and assets");
86
+ hyperframesHtmlDomain(project.html);
87
+ if (/hypit-resource:\/\//u.test(project.html)) throw new Error("HTML still has unresolved resources; materialize its HyperframesDocument first");
88
+ const urls = new Set<string>();
89
+ for (const asset of project.assets) {
90
+ const blob = asset?.artifact;
91
+ if (typeof asset?.url !== "string" || urls.has(asset.url) || blob?.kind !== "blob" || !isResourceId(blob.resource)
92
+ || !Number.isSafeInteger(blob.size) || blob.size < 0 || typeof blob.mediaType !== "string" || !blob.mediaType) {
93
+ throw new Error("Invalid or repeated HyperFrames HTML asset");
94
+ }
95
+ urls.add(asset.url);
96
+ }
97
+ const referenced = hyperframesHtmlAssetUrls(project.html);
98
+ if (referenced.length !== urls.size || referenced.some(url => !urls.has(url))) throw new Error("HyperFrames HTML assets must address its referenced URLs");
99
+ }
@@ -11,3 +11,5 @@ export {
11
11
  export type * from "./types.js";
12
12
  export { browserProgram, BROWSER_PROGRAM_FORMAT } from "./browser-program.js";
13
13
  export type { BrowserProgram } from "./browser-program.js";
14
+ export { assertHyperframesHtmlProject, hyperframesHtmlDomain, hyperframesHtmlAssetUrls, mapHyperframesHtmlUrls } from "./html-project.js";
15
+ export type { HyperframesHtmlProject } from "./html-project.js";
@@ -6,6 +6,8 @@ import type { BlobRef } from "@hypit/protocol";
6
6
 
7
7
  import { assertHyperframesDocument, materializeHyperframesHtml } from "./document.js";
8
8
  import type { HyperframesDocument } from "./types.js";
9
+ import { assertHyperframesHtmlProject, mapHyperframesHtmlUrls } from "./html-project.js";
10
+ import type { HyperframesHtmlProject } from "./html-project.js";
9
11
 
10
12
  /** Reads one execution resource. Whose store it comes from is the caller's business. */
11
13
  export type HyperframesArtifactReader = (
@@ -37,6 +39,32 @@ function extension(mediaType: string): string {
37
39
  return found;
38
40
  }
39
41
 
42
+ /** Stage an existing HTML programme; no reverse compilation or inferred Surface declarations. */
43
+ export async function stageHyperframesHtmlProject(options: {
44
+ readonly project: HyperframesHtmlProject;
45
+ readonly directory: string;
46
+ readonly read: HyperframesArtifactReader;
47
+ readonly signal?: AbortSignal;
48
+ }): Promise<void> {
49
+ assertHyperframesHtmlProject(options.project);
50
+ await mkdir(join(options.directory, "artifacts"), { recursive: true });
51
+ const paths = new Map<string, string>();
52
+ for (const [index, asset] of options.project.assets.entries()) {
53
+ options.signal?.throwIfAborted();
54
+ const path = `artifacts/asset-${index}${extension(asset.artifact.mediaType)}`;
55
+ const bytes = await options.read(asset.artifact, options.signal);
56
+ const file = await open(join(options.directory, path), "w");
57
+ try {
58
+ if (bytes instanceof Uint8Array) await file.writeFile(bytes);
59
+ else for await (const chunk of bytes) { options.signal?.throwIfAborted(); await file.writeFile(chunk); }
60
+ } finally { await file.close(); }
61
+ paths.set(asset.url, `./${path}`);
62
+ }
63
+ await writeFile(join(options.directory, "index.html"), mapHyperframesHtmlUrls(options.project.html, url => paths.get(url) ?? url), {
64
+ encoding: "utf8", ...(options.signal === undefined ? {} : { signal: options.signal }),
65
+ });
66
+ }
67
+
40
68
  /**
41
69
  * Lay one document out as a directory HyperFrames can render.
42
70
  *
@@ -1307,9 +1307,27 @@ export async function executeMuxProgramMedia(
1307
1307
  && compareTimestamp(finalVideo[0]!.startPts, finalAudio[0]!.startPts) === 0,
1308
1308
  "Final mux audio and video do not share one presentation origin");
1309
1309
  const finalAudioSpan = duration(finalAudio[0]!);
1310
- assert(roundPositive(finalAudioSpan.numerator * 48_000n, finalAudioSpan.denominator)
1311
- === need.audio.sampleFrames,
1312
- "Final mux audio presentation span differs from TimelineAudio");
1310
+ /*
1311
+ * The staged WAV is asserted sample-exact above; this checks what survived AAC.
1312
+ *
1313
+ * AAC cannot carry an arbitrary sample count: the encoder emits 1024-sample frames and
1314
+ * reports a priming delay that the MP4 muxer compensates with an edit list, so the
1315
+ * presented span lands a few samples away from the input. Measured here with ffmpeg 6.1
1316
+ * on synthetic tone (i.e. independent of any project's material): a 4 249 600-sample input
1317
+ * presents as 4 249 584 (-16), and 4 236 800 presents as 4 236 768 (-32). Padding the
1318
+ * timeline to a whole number of AAC frames does not remove it — the delay is not a
1319
+ * frame-alignment artifact.
1320
+ *
1321
+ * Demanding exact equality therefore rejects every correct mux whose length is not a
1322
+ * fixed point of that round trip. The real invariant AAC can hold is that no whole frame
1323
+ * of audio went missing, so the tolerance is one AAC frame rather than an arbitrary epsilon;
1324
+ * anything larger still means genuine desync and still fails.
1325
+ */
1326
+ const AAC_FRAME_SAMPLES = 1_024;
1327
+ const finalAudioSamples = roundPositive(finalAudioSpan.numerator * 48_000n, finalAudioSpan.denominator);
1328
+ assert(Math.abs(finalAudioSamples - need.audio.sampleFrames) < AAC_FRAME_SAMPLES,
1329
+ `Final mux audio presentation span differs from TimelineAudio by ${
1330
+ String(finalAudioSamples - need.audio.sampleFrames)} samples`);
1313
1331
  const artifact = await env.artifacts.putFile(output, "video/mp4");
1314
1332
  const value: MuxedMedia = sealMuxedMedia({
1315
1333
  frameRate: need.visual.frameRate,
@@ -0,0 +1,35 @@
1
+ # `@hypit/pixverse`
2
+
3
+ Exact author/compute contracts and a package-owned author Surface for PixVerse V6.
4
+
5
+ The Surface projects the primary result to an ordinary video Artifact. The package contains no
6
+ Provider selection, API key or network execution. The selected Provider implements its exact
7
+ capability.
8
+
9
+ Connect prompt and frames as ordinary graph edges:
10
+
11
+ ```xml
12
+ <pix:Video
13
+ id="opening"
14
+ prompt={line}
15
+ duration="5"
16
+ quality="720p"
17
+ aspect-ratio="9:16"
18
+ generate-audio="true"
19
+ />
20
+
21
+ <pix:Video id="bridge" prompt={motion} duration="5" quality="720p"
22
+ first-frame={hero.image} last-frame={product.image}/>
23
+ ```
24
+
25
+ The Surface only lowers this syntax into the package's exact model request. It does not select a
26
+ Provider.
27
+
28
+ The model renders 1 to 15 seconds at `360p`, `540p`, `720p` or `1080p`. A prompt-only run states its
29
+ `aspect-ratio`; a run that starts from a `first-frame` takes that frame's shape instead. A
30
+ `last-frame` bridges from the first frame into one continuous shot, so it is not combined with
31
+ `multi-clip`, which renders the prompt as several cuts.
32
+
33
+ `generate-audio` renders an audio track alongside the picture, including speech the prompt asks a
34
+ character to say. The model exposes no separate voice, language or dialogue field, so a spoken line
35
+ belongs in the prompt itself.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@hypit/pixverse",
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
+ "hypit": {
11
+ "activation": "./src/activation.ts"
12
+ },
13
+ "dependencies": {
14
+ "@hypit/artifact": "workspace:*",
15
+ "@hypit/generation": "workspace:*",
16
+ "@hypit/markup": "workspace:*",
17
+ "@hypit/model-kit": "workspace:*",
18
+ "@hypit/protocol": "workspace:*",
19
+ "@hypit/text": "workspace:*"
20
+ }
21
+ }
@@ -0,0 +1,11 @@
1
+ import { createMarkupSurfaceHostFacet } from "@hypit/markup";
2
+ import {
3
+ pixverseComponent, pixverseDefinition, pixverseManifest, pixverseModuleRef, pixverseMarkupSurfaces,
4
+ } from "./index.js";
5
+ import { decodePixverseVideoSurface } from "./surface.js";
6
+ export const hypitPackage = { format: "hypit.node-package@1" as const, modules: [{ manifest: pixverseManifest }], components: [pixverseComponent], hostFacets: [
7
+ pixverseDefinition.hostFacet,
8
+ createMarkupSurfaceHostFacet({ module: pixverseModuleRef,
9
+ declaration: pixverseMarkupSurfaces.find((item) => item.name === "video")!, handler: decodePixverseVideoSurface }),
10
+ ] };
11
+ export default hypitPackage;
@@ -0,0 +1,122 @@
1
+ import { artifactTypes } from "@hypit/artifact";
2
+ import { sealGenerationPortRequest, sealGenerationPortTable } from "@hypit/generation";
3
+ import type { GenerationPortTable, GenerationPortValue, GenerationRequest } from "@hypit/generation";
4
+ import type { SurfaceAttributeVocabulary } from "@hypit/markup";
5
+ import { defineExactModelModule } from "@hypit/model-kit";
6
+ import { textTypes } from "@hypit/text";
7
+
8
+ export const pixverseModuleRef = { name: "@hypit/pixverse", version: "1" } as const;
9
+ export const pixverseModels = ["pixverse-v6"] as const;
10
+ export type PixverseModel = typeof pixverseModels[number];
11
+
12
+ const PIXVERSE_QUALITIES = ["360p", "540p", "720p", "1080p"] as const;
13
+ const PIXVERSE_ASPECT_RATIOS = ["16:9", "4:3", "1:1", "3:4", "9:16", "2:3", "3:2", "21:9"] as const;
14
+
15
+ export const pixverseV6Ports: GenerationPortTable = sealGenerationPortTable({
16
+ model: "pixverse-v6",
17
+ result: "video",
18
+ ports: [
19
+ { name: "prompt", value: { kind: "text", maxChars: 5_000 }, minItems: 1, maxItems: 1 },
20
+ { name: "firstFrame", value: { kind: "media", accepts: ["image"] }, minItems: 0, maxItems: 1 },
21
+ { name: "lastFrame", value: { kind: "media", accepts: ["image"] }, minItems: 0, maxItems: 1 },
22
+ { name: "duration", value: { kind: "number", integer: true, minimum: 1, maximum: 15 }, minItems: 1, maxItems: 1 },
23
+ { name: "quality", value: { kind: "enum", values: [...PIXVERSE_QUALITIES] }, minItems: 1, maxItems: 1 },
24
+ { name: "aspectRatio", value: { kind: "enum", values: [...PIXVERSE_ASPECT_RATIOS] }, minItems: 0, maxItems: 1 },
25
+ { name: "generateAudio", value: { kind: "boolean" }, minItems: 0, maxItems: 1 },
26
+ { name: "multiClip", value: { kind: "boolean" }, minItems: 0, maxItems: 1 },
27
+ { name: "seed", value: { kind: "number", integer: true, minimum: 0, maximum: 2_147_483_647 }, minItems: 0, maxItems: 1 },
28
+ ],
29
+ requires: [
30
+ // A last frame states where a run that already has a first frame ends.
31
+ { kind: "requiresPresent", port: "lastFrame", needs: ["firstFrame"] },
32
+ // A run that starts from a frame inherits that frame's shape, and one that
33
+ // bridges two frames renders a single continuous shot.
34
+ { kind: "atMostOneOf", ports: ["aspectRatio", "firstFrame"] },
35
+ { kind: "atMostOneOf", ports: ["multiClip", "lastFrame"] },
36
+ ],
37
+ });
38
+
39
+ export const pixversePorts: Readonly<Record<PixverseModel, GenerationPortTable>> = {
40
+ "pixverse-v6": pixverseV6Ports,
41
+ };
42
+
43
+ export function sealPixverseRequest(
44
+ ports: Readonly<Record<string, readonly GenerationPortValue[]>>,
45
+ ): GenerationRequest {
46
+ return sealGenerationPortRequest(pixverseV6Ports, ports);
47
+ }
48
+
49
+ const pixverseBaseDefinition = defineExactModelModule({
50
+ module: pixverseModuleRef,
51
+ endpoints: [{
52
+ key: "video",
53
+ requestTypeName: "PixverseV6Request",
54
+ producerName: "request-pixverse-v6",
55
+ ports: pixverseV6Ports,
56
+ }],
57
+ });
58
+
59
+ export const pixverseEndpoints = pixverseBaseDefinition.endpoints;
60
+ export const pixverseComponent = pixverseBaseDefinition.component;
61
+ const endpoint = pixverseEndpoints.video!;
62
+
63
+ const pixverseAttributes: readonly SurfaceAttributeVocabulary[] = [
64
+ { name: "id", kind: "identifier", required: true,
65
+ summary: "Names this generation so its video Artifact can be referenced elsewhere in the Source." },
66
+ { name: "prompt", kind: "reference", required: true, accepts: [textTypes.text],
67
+ summary: "The Text edge describing the shot, including any spoken line the model should voice." },
68
+ { name: "first-frame", kind: "reference", required: false, accepts: [artifactTypes.blob],
69
+ summary: "Starts the video from one image Artifact." },
70
+ { name: "last-frame", kind: "reference", required: false, accepts: [artifactTypes.blob],
71
+ summary: "Ends the video on one image Artifact, bridging from the first frame." },
72
+ { name: "duration", kind: "literal", required: true,
73
+ summary: "How many seconds of video to render, from 1 to 15." },
74
+ { name: "quality", kind: "literal", required: true, values: [...PIXVERSE_QUALITIES],
75
+ summary: "The size band the model renders at." },
76
+ { name: "aspect-ratio", kind: "literal", required: false, values: [...PIXVERSE_ASPECT_RATIOS],
77
+ summary: "The Frame shape of the generated video." },
78
+ { name: "generate-audio", kind: "literal", required: false, values: ["true", "false"],
79
+ summary: "Renders an audio track alongside the picture." },
80
+ { name: "multi-clip", kind: "literal", required: false, values: ["true", "false"],
81
+ summary: "Renders the prompt as several cuts instead of one continuous shot." },
82
+ { name: "seed", kind: "literal", required: false,
83
+ summary: "Seeds the model's sampling so a rerun stays close to this one." },
84
+ ];
85
+
86
+ export const pixverseMarkupSurfaces = [{
87
+ name: "video",
88
+ tag: "Video",
89
+ mode: "structured" as const,
90
+ outputs: [endpoint.draftType, endpoint.mediaBindings.firstFrame!.type, endpoint.mediaBindings.lastFrame!.type],
91
+ vocabulary: {
92
+ summary: "Generates one video with the exact PixVerse V6 model from a Text prompt, optionally starting from a frame or bridging two.",
93
+ attributes: pixverseAttributes,
94
+ ports: [{
95
+ name: "video",
96
+ type: artifactTypes.blob,
97
+ summary: "The generated video, addressed as `<id>.video`.",
98
+ }],
99
+ example: `<pix:Video
100
+ id="opening"
101
+ prompt={line}
102
+ duration="5"
103
+ quality="720p"
104
+ aspect-ratio="9:16"
105
+ generate-audio="true"
106
+ />`,
107
+ notes: [
108
+ "A run that starts from a frame takes its shape from that frame, so `aspect-ratio` states the shape only for a prompt-only run.",
109
+ "A `last-frame` bridges from the `first-frame` into one continuous shot, so it is not combined with `multi-clip`.",
110
+ "A spoken line belongs in the prompt; the model exposes no separate voice, language or dialogue field.",
111
+ "The Surface lowers the element into the package's exact model request and selects no Provider.",
112
+ ],
113
+ },
114
+ }] as const;
115
+
116
+ export const pixverseManifest = {
117
+ ...pixverseBaseDefinition.manifest,
118
+ };
119
+ export const pixverseDefinition = {
120
+ ...pixverseBaseDefinition,
121
+ manifest: pixverseManifest,
122
+ };