@hypit/hypit 0.1.12 → 0.1.14

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 (74) hide show
  1. package/README.md +36 -2
  2. package/bin/hypit.mjs +0 -2
  3. package/dist/public/model-kit.d.ts +16 -0
  4. package/package.json +3 -1
  5. package/packages/cli/README.md +3 -0
  6. package/packages/cli/package.json +1 -0
  7. package/packages/cli/src/commands/environment.ts +12 -7
  8. package/packages/cli/src/commands/results.ts +19 -3
  9. package/packages/cli/src/main.ts +2 -1
  10. package/packages/cli/src/oauth.ts +50 -7
  11. package/packages/cli/src/output.ts +2 -2
  12. package/packages/cli/src/source-discovery.ts +2 -2
  13. package/packages/credential-store-file/README.md +60 -0
  14. package/packages/credential-store-file/package.json +21 -0
  15. package/packages/credential-store-file/src/activation.ts +29 -0
  16. package/packages/credential-store-file/src/index.ts +1 -0
  17. package/packages/credential-store-file/src/store.ts +89 -0
  18. package/packages/fonts-open/src/surface.ts +8 -2
  19. package/packages/hyperframes/README.md +16 -2
  20. package/packages/hyperframes/src/browser-program.ts +6 -1
  21. package/packages/hyperframes/src/document.ts +31 -21
  22. package/packages/hyperframes/src/project.ts +11 -20
  23. package/packages/media-execution/README.md +7 -0
  24. package/packages/media-execution/src/execute.ts +5 -5
  25. package/packages/media-execution/src/index.ts +1 -1
  26. package/packages/media-execution/src/process-env.ts +20 -0
  27. package/packages/media-execution/src/surface.ts +83 -76
  28. package/packages/model-kit/README.md +28 -0
  29. package/packages/model-kit/src/index.ts +58 -19
  30. package/packages/package-loader-node/README.md +10 -0
  31. package/packages/package-loader-node/src/index.ts +1 -0
  32. package/packages/package-loader-node/src/loader.ts +25 -5
  33. package/packages/package-loader-node/src/location.ts +11 -2
  34. package/packages/project-context-node/README.md +2 -0
  35. package/packages/project-context-node/src/project-context.ts +5 -3
  36. package/packages/provider-hyperframes-local/README.md +95 -13
  37. package/packages/provider-hyperframes-local/package.json +13 -3
  38. package/packages/provider-hyperframes-local/src/activation.ts +20 -6
  39. package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
  40. package/packages/provider-hyperframes-local/src/browser.ts +118 -0
  41. package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
  42. package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
  43. package/packages/provider-hyperframes-local/src/capture-process.ts +54 -59
  44. package/packages/provider-hyperframes-local/src/capture-worker.ts +28 -7
  45. package/packages/provider-hyperframes-local/src/capture.ts +12 -5
  46. package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
  47. package/packages/provider-hyperframes-local/src/options.ts +2 -2
  48. package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
  49. package/packages/provider-hyperframes-local/src/process.ts +22 -0
  50. package/packages/provider-hyperframes-local/src/program.ts +25 -33
  51. package/packages/provider-hyperframes-local/src/provider.ts +4 -7
  52. package/packages/provider-hyperframes-local/src/render.ts +9 -4
  53. package/packages/provider-hypihub/README.md +3 -0
  54. package/packages/runtime-host-node/README.md +5 -0
  55. package/packages/runtime-host-node/src/index.ts +7 -2
  56. package/packages/runtime-host-node/src/packages.ts +13 -7
  57. package/packages/runtime-local/README.md +10 -0
  58. package/packages/runtime-local/src/config.ts +1 -1
  59. package/packages/runtime-local/src/credentials.ts +10 -5
  60. package/packages/runtime-local/src/runtime.ts +1 -1
  61. package/packages/seedance/README.md +42 -0
  62. package/packages/seedance/src/index.ts +5 -22
  63. package/packages/seedance/src/surface.ts +2 -3
  64. package/packages/seedance/src/validation.ts +16 -0
  65. package/packages/studio/src/preview/runtime-shim.ts +17 -3
  66. package/packages/studio/src/server.ts +2 -2
  67. package/packages/video-cli/README.md +5 -1
  68. package/packages/video-cli/package.json +1 -0
  69. package/packages/video-cli/src/distribution.ts +3 -10
  70. package/packages/video-cli/src/version.ts +1 -1
  71. package/packages/workspace-fs-node/src/workspace.ts +2 -2
  72. package/packages/yt-dlp/README.md +3 -2
  73. package/packages/yt-dlp/package.json +4 -0
  74. package/packages/yt-dlp/src/download.ts +10 -14
@@ -1,4 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { access, stat } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import { delimiter, isAbsolute, resolve } from "node:path";
2
5
 
3
6
  type ProcessResult = {
4
7
  readonly stdout: Uint8Array;
@@ -13,6 +16,25 @@ export function positiveInteger(value: number, subject: string): number {
13
16
  return value;
14
17
  }
15
18
 
19
+ /** The engine's binary override requires a path; resolve our selected command without its fallback search. */
20
+ export async function mediaExecutablePath(value: string): Promise<string> {
21
+ const pathLike = isAbsolute(value) || value.includes("/") || value.includes("\\");
22
+ const bases = pathLike ? [resolve(value)]
23
+ : (process.env.PATH ?? "").split(delimiter).filter(Boolean).map(directory => resolve(directory, value));
24
+ const extensions = process.platform === "win32" && !/\.[^\\/]+$/u.test(value) ? [".exe", ".com", ""] : [""];
25
+ for (const base of bases) for (const extension of extensions) {
26
+ const candidate = `${base}${extension}`;
27
+ try {
28
+ if (!(await stat(candidate)).isFile()) continue;
29
+ await access(candidate, process.platform === "win32" ? constants.F_OK : constants.X_OK);
30
+ return candidate;
31
+ } catch (error) {
32
+ if (!["ENOENT", "ENOTDIR", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
33
+ }
34
+ }
35
+ throw new Error(`HyperFrames media executable ${value} is unavailable; correct the Provider's ffmpegPath or ffprobePath.`);
36
+ }
37
+
16
38
  function processEnvironment(): NodeJS.ProcessEnv {
17
39
  const names = ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL"] as const;
18
40
  return Object.fromEntries(names.flatMap((name) => process.env[name] === undefined
@@ -1,54 +1,46 @@
1
- import { execFile } from "node:child_process";
1
+ import { fileURLToPath } from "node:url";
2
+ import { join } from "node:path";
2
3
 
3
4
  import { probeMediaToolchain } from "@hypit/media-execution";
4
5
  import type { ManagedProgram, ManagedProgramState } from "@hypit/runtime-kit";
5
-
6
- function run(executable: string, args: readonly string[]): Promise<{ ok: boolean; output: string }> {
7
- return new Promise((resolve) => {
8
- execFile(executable, [...args], { timeout: 15_000, shell: false, windowsHide: true }, (error, stdout, stderr) => {
9
- resolve(error === null
10
- ? { ok: true, output: stdout.trim() }
11
- : { ok: false, output: (stderr.trim() || error.message).split("\n").at(-1) ?? "" });
12
- });
13
- });
14
- }
6
+ import { browserCacheDirectory, browserDownloadBaseUrl, browserDownloadUrl, browserExecutablePath, configuredBrowserPath, requireBrowserExecutable, selectedBrowserVersion } from "./browser.js";
7
+ import type { BrowserOptions } from "./browser.js";
15
8
 
16
9
  /**
17
- * HyperFrames owns the browser cache behind its CLI. It is deployment state,
18
- * not an author dependency, so the Endpoint lifecycle prepares it only
19
- * when this Provider is selected.
10
+ * This Provider owns browser selection and preparation. Probes never install;
11
+ * rendering receives the same selected executable instead of invoking engine discovery.
20
12
  */
21
13
  export function localHyperframesBrowserProgram(
22
- input: {
14
+ input: BrowserOptions & {
23
15
  readonly id: string;
24
16
  readonly nodePath: string;
25
- readonly hyperframesCliPath: string | (() => string);
26
17
  readonly ffprobePath: string;
27
18
  readonly ffmpegPath?: string;
28
19
  },
29
20
  ): ManagedProgram {
30
- const cliPath = () => typeof input.hyperframesCliPath === "string" ? input.hyperframesCliPath : input.hyperframesCliPath();
21
+ const version = selectedBrowserVersion(input);
22
+ const selectedPath = browserExecutablePath(input);
23
+ const baseUrl = browserDownloadBaseUrl(input);
31
24
  const probeBrowser = async (): Promise<ManagedProgramState> => {
32
- let executable: string;
33
- try { executable = cliPath(); } catch (error) { return { state: "down", detail: error instanceof Error ? error.message : String(error) }; }
34
- const located = await run(input.nodePath, [executable, "browser", "path"]);
35
- if (!located.ok) return { state: "down", detail: `HyperFrames browser is unavailable: ${located.output}` };
36
- const path = located.output.trim();
37
- if (path.length === 0 || path.includes("\n") || path.includes("\r")) {
38
- return { state: "mismatch", detail: "HyperFrames returned an invalid browser path" };
39
- }
40
- const version = await run(path, ["--version"]);
41
- if (!version.ok || version.output.length === 0) {
42
- return { state: "mismatch", detail: `HyperFrames browser cannot start: ${version.output}` };
43
- }
25
+ try { await requireBrowserExecutable(selectedPath, version); }
26
+ catch (error) { return { state: "down", detail: error instanceof Error ? error.message : String(error) }; }
44
27
  return { state: "ready" };
45
28
  };
46
29
  return {
47
30
  id: input.id,
48
- installation: {
49
- probe: probeBrowser,
50
- get commands() { return [{ command: input.nodePath, args: [cliPath(), "browser", "ensure"] }]; },
51
- },
31
+ ...(configuredBrowserPath(input) === undefined ? {
32
+ // Projects sharing this installation also share the existing Program lifecycle lock/logs.
33
+ stateRoot: join(browserCacheDirectory(input), ".hypit-render-program"),
34
+ installation: {
35
+ probe: probeBrowser,
36
+ commands: [{ label: `Install Chrome Headless Shell ${version} at ${selectedPath} from ${browserDownloadUrl(input)}`, command: input.nodePath, args: [
37
+ "--import", import.meta.resolve("tsx"),
38
+ "--import", new URL("./capture-bootstrap.ts", import.meta.url).href,
39
+ fileURLToPath(new URL("./browser-install.ts", import.meta.url)), browserCacheDirectory(input), version!,
40
+ ...(baseUrl === undefined ? [] : [baseUrl]),
41
+ ] }],
42
+ },
43
+ } : {}),
52
44
  async probe(): Promise<ManagedProgramState> {
53
45
  const browser = await probeBrowser();
54
46
  if (browser.state !== "ready") return browser;
@@ -2,7 +2,6 @@ import { defineEndpointPackage } from "@hypit/endpoint-kit";
2
2
  import { mediaTypes } from "@hypit/media";
3
3
  import { renderHyperframesCapabilities, verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
4
4
  import { canonicalize } from "@hypit/protocol";
5
- import { resolveNodePackageExecutable } from "@hypit/package-loader-node";
6
5
  import { renderHyperframesVisual, resolveExecutionOptions, renderWorkerLimit } from "./render.js";
7
6
  import type { HyperframesExecutionOptions } from "./options.js";
8
7
  import { renderProgressReporter } from "./progress.js";
@@ -14,17 +13,12 @@ export type CreateLocalHyperframesProviderOptions = HyperframesExecutionOptions
14
13
  readonly pool?: string;
15
14
  /** Used by managed browser installation, not frame capture. */
16
15
  readonly nodePath?: string;
17
- readonly hyperframesCliPath?: string;
18
16
  /** Whole render requests admitted concurrently; independent of frame workers. */
19
17
  readonly defaultConcurrency?: number;
20
18
  /** Shared Chrome slots across Need executions using this pool. */
21
19
  readonly browserCapacity?: number;
22
20
  };
23
21
 
24
- export function defaultHyperframesCliPath(): string {
25
- return resolveNodePackageExecutable("hyperframes", "hyperframes", { from: import.meta.url });
26
- }
27
-
28
22
  export function createLocalHyperframesProvider(config: CreateLocalHyperframesProviderOptions) {
29
23
  const execution = resolveExecutionOptions(config);
30
24
  const pool = config.pool ?? config.instance ?? "hyperframes.local";
@@ -62,7 +56,10 @@ export function createLocalHyperframesProvider(config: CreateLocalHyperframesPro
62
56
  : request.range.endFrameExclusive - request.range.startFrame;
63
57
  const progress = renderProgressReporter(context.reportProgress, frameCount);
64
58
  try {
65
- const visual = await renderHyperframesVisual(request, { ...config, workers: execution.workers, maxWorkers,
59
+ const visual = await renderHyperframesVisual(request, { ...config,
60
+ ...(execution.chromePath === undefined ? { browserVersion: execution.browserVersion! } : { chromePath: execution.chromePath }),
61
+ browserCacheDirectory: execution.browserCacheDirectory,
62
+ workers: execution.workers, maxWorkers,
66
63
  resources: context.resources, onProgress: progress.onProgress,
67
64
  ...(context.reportDiagnostic === undefined ? {} : { onDiagnostic: context.reportDiagnostic }) });
68
65
  return { value: { kind: "inline", value: canonicalize(visual) } };
@@ -6,7 +6,7 @@ import type { EndpointInvocationContext } from "@hypit/endpoint-kit";
6
6
  import { stageHyperframesProject } from "@hypit/hyperframes/project";
7
7
  import { sealRenderedVisual } from "@hypit/media";
8
8
  import type { MediaFrameRange, RenderedVisual } from "@hypit/media";
9
- import { verifyCompositableSurfaceBytes } from "@hypit/media-execution";
9
+ import { verifyCompositableSurfaceFile } from "@hypit/media-execution";
10
10
  import { verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
11
11
  import type { HyperframesVisualRequest } from "@hypit/render-hyperframes";
12
12
  import { isStreamingResourceStore } from "@hypit/runtime";
@@ -15,6 +15,7 @@ import { assert, positiveInteger } from "./process.js";
15
15
  import { runCaptureProcess } from "./capture-process.js";
16
16
  import { finished } from "node:stream/promises";
17
17
  import { autoWorkerLimit } from "./concurrency.js";
18
+ import { browserCacheDirectory, browserExecutablePath, configuredBrowserPath, requireBrowserExecutable, selectedBrowserVersion } from "./browser.js";
18
19
 
19
20
  export type HyperframesRenderProgress =
20
21
  | { readonly phase: "staging" | "encoding" | "storing"; readonly elapsedMs: number }
@@ -43,6 +44,9 @@ export function resolveExecutionOptions(options: HyperframesExecutionOptions) {
43
44
  const browserGpu = options.browserGpu ?? "hardware";
44
45
  assert(["auto", "software", "hardware"].includes(browserGpu), "HyperFrames browserGpu is invalid");
45
46
  return {
47
+ chromePath: configuredBrowserPath(options),
48
+ browserVersion: selectedBrowserVersion(options),
49
+ browserCacheDirectory: browserCacheDirectory(options),
46
50
  workers,
47
51
  maxWorkers: workers === "auto" ? positiveInteger(options.maxWorkers ?? autoWorkerLimit(), "maxWorkers") : workers,
48
52
  quality, browserGpu,
@@ -71,7 +75,7 @@ export async function renderHyperframesVisual(
71
75
  options: RenderHyperframesVisualOptions,
72
76
  ): Promise<RenderedVisual> {
73
77
  verifyHyperframesVisualRequest(request);
74
- const config = resolveExecutionOptions(options);
78
+ const config = { ...resolveExecutionOptions(options), chromePath: browserExecutablePath(options) };
75
79
  const { document } = request;
76
80
  const range = request.range ?? { startFrame: 0, endFrameExclusive: document.frameCount };
77
81
  const frameCount = range.endFrameExclusive - range.startFrame;
@@ -97,8 +101,9 @@ export async function renderHyperframesVisual(
97
101
  };
98
102
  try {
99
103
  signal.throwIfAborted();
104
+ await requireBrowserExecutable(config.chromePath, config.browserVersion);
100
105
  await options.onDiagnostic?.({ level: "info", message:
101
- `Render ${frameCount} frames; workers ${config.workers} (limit ${renderWorkerLimit(config, frameCount, document.frameRate.numerator / document.frameRate.denominator)}); opaque fast PNG; quality ${config.quality}; GPU ${config.browserGpu}; encoder ${config.ffmpegPath}` });
106
+ `Render ${frameCount} frames; workers ${config.workers} (limit ${renderWorkerLimit(config, frameCount, document.frameRate.numerator / document.frameRate.denominator)}); opaque fast PNG; quality ${config.quality}; GPU ${config.browserGpu}; browser ${config.chromePath}; encoder ${config.ffmpegPath}` });
102
107
  options.onProgress?.({ phase: "staging", elapsedMs: 0 });
103
108
  work = await mkdtemp(join(tmpdir(), "hypit-hyperframes-local-"));
104
109
  await stageHyperframesProject({ document, directory: work, signal,
@@ -109,7 +114,7 @@ export async function renderHyperframesVisual(
109
114
  assert(bytes !== undefined, `HyperFrames Artifact ${artifact.resource} is unavailable`);
110
115
  return bytes;
111
116
  },
112
- validateSurface: (surface, bytes, probeSignal) => verifyCompositableSurfaceBytes({ surface, bytes,
117
+ validateSurface: (surface, path, probeSignal) => verifyCompositableSurfaceFile({ surface, path,
113
118
  ffprobePath: config.ffprobePath, processTimeoutMs: config.processTimeoutMs,
114
119
  maxProbeOutputBytes: config.maxProcessOutputBytes, signal: probeSignal! }),
115
120
  });
@@ -118,6 +118,9 @@ Resource identity with the same declared person-reference classification is uplo
118
118
  cross-Build cache. Seedance visual references can carry `personReference` in their media fields;
119
119
  the mapping declares it as a resource-transport field and the upload session receives
120
120
  `is_person_reference`, preserving true, false and omission. It stays out of the generation body.
121
+ This covers reference images, reference videos, and first/last frames for every declared Seedance
122
+ variant. Omission remains absent on the wire; HypiHub's upload API currently defaults it to false,
123
+ so omission does not enable detection or person-reference preparation.
121
124
  HypiHub stores the authored classification and prepares the applicable upstream person reference;
122
125
  this Provider does not detect faces or select an upstream private-avatar group.
123
126
 
@@ -53,3 +53,8 @@ to start execution, then `const runtime = await host.createRuntime(); await runt
53
53
  to submit work. `RuntimeHostExecution` does not run a second embedded Worker loop. A local carrier
54
54
  may stop accepting new Builds and drain; its assigned Builds stay in place and all carriers continue
55
55
  using the same resource accounting. Process policy belongs to the local Runtime, not this Host ABI.
56
+
57
+ `prepareHostPackages` accepts exact specifier strings or `{ specifier, env }` installation inputs.
58
+ The optional environment is applied only to that npm child process, merged over its inherited
59
+ environment. It is neither persisted as installation status nor returned in package reports.
60
+ The shared installer does not interpret SDK-specific variables; their owning packages supply them.
@@ -70,17 +70,20 @@ export type BuildView = {
70
70
  }[];
71
71
  };
72
72
 
73
- export type RuntimeHostCredentialStatus = {
73
+ export type RuntimeHostCredentialDescription = {
74
74
  readonly endpoint: string;
75
75
  readonly slot: string;
76
76
  readonly label: string;
77
77
  readonly kind: "secret" | "json";
78
78
  readonly ref: CredentialRef;
79
79
  readonly acquisition?: CredentialAcquisition;
80
- readonly configured: boolean;
81
80
  readonly writable: boolean;
82
81
  };
83
82
 
83
+ export type RuntimeHostCredentialStatus = RuntimeHostCredentialDescription & {
84
+ readonly configured: boolean;
85
+ };
86
+
84
87
  export type RuntimeHostControl = {
85
88
  /** Build evidence while active. Finished evidence is read through its Result Repository. */
86
89
  logs?(build: string, lines: number): Promise<import("@hypit/runtime").ExecutionLogView | undefined>;
@@ -105,6 +108,8 @@ export type RuntimeHostResultControl = {
105
108
  };
106
109
 
107
110
  export type RuntimeHostCredentialControl = {
111
+ /** Endpoint declarations and Store write capability; never reads an existing secret. */
112
+ describeCredentials(endpoint?: string): Promise<readonly RuntimeHostCredentialDescription[]>;
108
113
  credentials(endpoint?: string): Promise<readonly RuntimeHostCredentialStatus[]>;
109
114
  putCredential(endpoint: string, slot: string, secret: string): Promise<RuntimeHostCredentialStatus>;
110
115
  deleteCredential(endpoint: string, slot: string): Promise<{
@@ -73,7 +73,7 @@ export async function inspectHostPackage(
73
73
  : undefined;
74
74
  }
75
75
 
76
- async function runNpm(root: string, specifiers: readonly string[], logPath: string): Promise<void> {
76
+ async function runNpm(root: string, specifiers: readonly string[], logPath: string, env?: Readonly<Record<string, string>>): Promise<void> {
77
77
  // npm's prefix and cwd must denote the same physical project (not /tmp vs /private/tmp).
78
78
  const cwd = await realpath(root);
79
79
  const npmArgs = [
@@ -96,6 +96,7 @@ async function runNpm(root: string, specifiers: readonly string[], logPath: stri
96
96
  cwd,
97
97
  shell: false,
98
98
  windowsHide: true,
99
+ env: { ...process.env, ...env },
99
100
  stdio: ["ignore", log.fd, log.fd],
100
101
  });
101
102
  child.on("error", (error) => reject(new Error(`Cannot start npm: ${error.message}. Log: ${logPath}`, { cause: error })));
@@ -114,21 +115,26 @@ async function runNpm(root: string, specifiers: readonly string[], logPath: stri
114
115
  * package.json, lockfile and dependencies; Hypit keeps no parallel inventory.
115
116
  */
116
117
  export async function prepareHostPackages(
117
- specifiers: readonly string[],
118
+ specifiers: readonly (string | { readonly specifier: string; readonly env?: Readonly<Record<string, string>> })[],
118
119
  options: {
119
120
  readonly root: string;
120
121
  readonly onProgress?: (event: HostPackageProgress) => void;
121
122
  },
122
123
  ): Promise<readonly HostPackageReport[]> {
123
124
  const root = resolve(options.root);
124
- const bySpecifier = new Map<string, RegistryPackageSpec>();
125
+ const bySpecifier = new Map<string, RegistryPackageSpec & { readonly env?: Readonly<Record<string, string>> }>();
125
126
  for (const specifier of specifiers) {
126
- const parsed = parseRegistryPackageSpec(specifier);
127
- bySpecifier.set(parsed.specifier, parsed);
127
+ const parsed = parseRegistryPackageSpec(typeof specifier === "string" ? specifier : specifier.specifier);
128
+ const env = { ...bySpecifier.get(parsed.specifier)?.env };
129
+ for (const [key, value] of Object.entries(typeof specifier === "string" ? {} : specifier.env ?? {})) {
130
+ if (env[key] !== undefined && env[key] !== value) throw new Error(`Conflicting installation environment ${key} for ${parsed.specifier}`);
131
+ env[key] = value;
132
+ }
133
+ bySpecifier.set(parsed.specifier, { ...parsed, ...(Object.keys(env).length === 0 ? {} : { env }) });
128
134
  }
129
135
  const required = [...bySpecifier.values()].sort((left, right) => left.specifier.localeCompare(right.specifier));
130
136
  const reports: HostPackageReport[] = [];
131
- for (const item of required) {
137
+ for (const { env, ...item } of required) {
132
138
  const installation = externalPackageInstallRoot(root, item.name, item.version);
133
139
  options.onProgress?.({ ...item, phase: "checking" });
134
140
  const missing = await installedVersion(installation, item.name) !== item.version;
@@ -142,7 +148,7 @@ export async function prepareHostPackages(
142
148
  dependencies: { [item.name]: item.version },
143
149
  }, null, 2)}\n`, "utf8");
144
150
  options.onProgress?.({ ...item, phase: "installing", logPath });
145
- await runNpm(installation, [item.specifier], logPath);
151
+ await runNpm(installation, [item.specifier], logPath, env);
146
152
  }
147
153
  const version = await installedVersion(installation, item.name);
148
154
  if (version !== item.version) {
@@ -258,3 +258,13 @@ capacity release and every completed Result. They use the production execution p
258
258
  This is a load experiment rather than part of every package regression. It reports fixture preparation,
259
259
  local progress, completion, executor RSS and cleanup separately; total test time also includes creating
260
260
  and removing the temporary Result repositories. Neither suite uses a completion-time performance target.
261
+
262
+
263
+ ### Credential management without reading the old value
264
+
265
+ `openCredentials(endpoint)` opens only the selected Endpoint's credential control.
266
+ `describeCredentials(endpoint)` returns its declared slots and each Store's write capability without
267
+ resolving secrets. `credentials(endpoint)` also reads current values to report status and propagates
268
+ read failures. Login and logout use the former: a damaged old credential cannot prevent replacement
269
+ or deletion. Successful writes and deletions report their operation's result without rereading the
270
+ secret. Execution still resolves credentials normally and reports Store errors.
@@ -462,7 +462,7 @@ export async function prepareRuntimeConfigPackages(
462
462
  ...document.endpoints.map((item) => item.use),
463
463
  ...credentials.credentials.map((item) => item.use),
464
464
  ], options.distributionPackageRoot);
465
- return await prepareHostPackages(requirements.map((item) => item.specifier), {
465
+ return await prepareHostPackages(requirements, {
466
466
  root: hypitHostPackageRoot(options.hostStateRoot),
467
467
  ...(options.onProgress === undefined ? {} : { onProgress: options.onProgress }),
468
468
  });
@@ -28,16 +28,21 @@ export function createLocalCredentialControl(
28
28
  : `Endpoint ${endpoint} repeats credential slot ${slot}`);
29
29
  return matches[0]!;
30
30
  };
31
- const status = async (item: typeof descriptions[number]) => ({
31
+ const describe = async (item: typeof descriptions[number]) => ({
32
32
  ...structuredClone(item),
33
- configured: await options.credentialStore.resolve(item.ref) !== undefined,
34
33
  writable: await writableCredentialStore(options.credentialStore, item.ref) !== undefined,
35
34
  });
36
35
 
37
36
  return {
37
+ async describeCredentials(endpoint) {
38
+ return await Promise.all(descriptions.filter((item) => endpoint === undefined || item.endpoint === endpoint).map(describe));
39
+ },
38
40
  async credentials(endpoint) {
39
41
  const selected = descriptions.filter((item) => endpoint === undefined || item.endpoint === endpoint);
40
- return await Promise.all(selected.map(status));
42
+ return await Promise.all(selected.map(async (item) => ({
43
+ ...await describe(item),
44
+ configured: await options.credentialStore.resolve(item.ref) !== undefined,
45
+ })));
41
46
  },
42
47
  async putCredential(endpoint, slot, secret) {
43
48
  assert(secret.length > 0, "credential secret is empty");
@@ -45,14 +50,14 @@ export function createLocalCredentialControl(
45
50
  const store = await writableCredentialStore(options.credentialStore, item.ref);
46
51
  assert(store !== undefined, `CredentialStore ${item.ref.store} is not writable`);
47
52
  await store.put(item.ref, { secret });
48
- return await status(item);
53
+ return { ...structuredClone(item), writable: true, configured: true };
49
54
  },
50
55
  async deleteCredential(endpoint, slot) {
51
56
  const item = credential(endpoint, slot);
52
57
  const store = await writableCredentialStore(options.credentialStore, item.ref);
53
58
  assert(store !== undefined, `CredentialStore ${item.ref.store} is not writable`);
54
59
  const deleted = await store.delete(item.ref);
55
- return { deleted, credential: await status(item) };
60
+ return { deleted, credential: { ...structuredClone(item), writable: true, configured: false } };
56
61
  },
57
62
  close() {
58
63
  return options.close?.();
@@ -41,7 +41,7 @@ function nonNegativeInteger(value: number, subject: string): number {
41
41
  function projectPath(root: string, path: string): string {
42
42
  const absolute = isAbsolute(path) ? resolve(path) : resolve(root, path);
43
43
  const relation = relative(resolve(root), absolute);
44
- assert(relation === "" || (!relation.startsWith("..") && !isAbsolute(relation)),
44
+ assert(relation === "" || (relation !== ".." && !relation.startsWith(`..${sep}`) && !isAbsolute(relation)),
45
45
  `Build Result source ${path} is outside project ${resolve(root)}`);
46
46
  return (relation || ".").split(sep).join("/");
47
47
  }
@@ -18,6 +18,20 @@ the author's literal, in whole seconds inside the model's declared range; measur
18
18
  with `hypit measure` and write the number here. Nothing in the graph computes it, so a Build plan is
19
19
  complete before it starts.
20
20
 
21
+ ## Reference audio
22
+
23
+ The Seedance package rejects reference audio declared as `audio/mp4` or `audio/x-m4a`. Convert the
24
+ audio to WAV or MP3 before using it; `media:ExtractAudio` produces WAV and can consume an upstream
25
+ component's media output. Renaming a file or changing its declared media type is not conversion.
26
+
27
+ Known imports are checked during Surface decoding. Future audio stays a graph input and is checked
28
+ when its Blob arrives. The same rule applies to drafts, complete requests, planning and direct
29
+ generation Producers. `sealSeedanceRequest` uses the endpoint's model-aware request builder;
30
+ `seedanceComponent` and `seedanceDefinition.component` expose the same implementation.
31
+
32
+ This checks the Blob's declared media type, not its bytes or codec. The selected Provider remains
33
+ responsible for any additional service-specific input limits.
34
+
21
35
  ## Visual reference metadata
22
36
 
23
37
  Declare whether each image or video contains a person/avatar reference, including an AI-generated
@@ -37,10 +51,38 @@ Inspect the actual reference when deciding the value. For `FrameVideo`, use
37
51
  `first-frame-person-reference` and `last-frame-person-reference` beside their respective frame
38
52
  inputs. A last-frame classification requires a last-frame input.
39
53
 
54
+ | Supplied visual input | Authored attribute | Request port |
55
+ | --- | --- | --- |
56
+ | Each `Reference image={...}` | `person-reference` | `referenceImage` |
57
+ | Each `Reference video={...}` | `person-reference` | `referenceVideo` |
58
+ | `FrameVideo` first frame | `first-frame-person-reference` | `firstFrame` |
59
+ | `FrameVideo` last frame | `last-frame-person-reference` | `lastFrame` |
60
+
61
+ These forms apply to `standard`, `fast`, `mini` and `2.5`. For example:
62
+
63
+ ```xml
64
+ <seedance:FrameVideo id="turn" model="fast" prompt={direction} duration="5"
65
+ first-frame={presenter.image} first-frame-person-reference="true"
66
+ last-frame={empty-room.image} last-frame-person-reference="false"/>
67
+ ```
68
+
69
+ Classify the material supplied to this request, not the intended result. A dance video with a person
70
+ still needs `true` when used only for motion, even if the prompt asks for a different performer.
71
+ An empty room stays `false` when the prompt asks to add a person. Inspect video across the selected
72
+ excerpt, not only its opening frame. This flag neither detects faces nor locks or names an identity.
73
+ Identity and action direction remain in the prompt and references.
74
+
75
+ The SVML author declares this parameter on each reference input. Admitted files, generated
76
+ images/videos and reused Results use the same attributes. For a future output, declare the intended
77
+ reference classification explicitly; if its contents are uncertain, generate and inspect that
78
+ material before using it downstream.
79
+
40
80
  The model's media ports carry this as `fields.personReference`. Providers interpret it through their
41
81
  service's media handling; it is not a prompt sentence or a Core-level identity. HypiHub sends it as
42
82
  `is_person_reference` when uploading the file, then uses the returned URL in the ordinary video
43
83
  request. A project Provider maps it according to its own API.
84
+ Omission does not request automatic face detection. HypiHub currently treats omitted upload flags
85
+ as unmarked (`false`); declare `true` explicitly for a person reference that needs its preparation.
44
86
 
45
87
  Video references can carry motion or camera behavior while image references carry the target
46
88
  appearance. Request duration and reference-clip duration are different limits. Check the selected
@@ -1,7 +1,4 @@
1
- import {
2
- sealGenerationPortRequest,
3
- sealGenerationPortTable,
4
- } from "@hypit/generation";
1
+ import { sealGenerationPortTable } from "@hypit/generation";
5
2
  import type {
6
3
  GenerationPortTable,
7
4
  GenerationPortValue,
@@ -12,6 +9,7 @@ import type { SurfaceAttributeVocabulary, SurfacePortVocabulary } from "@hypit/m
12
9
  import { defineExactModelModule } from "@hypit/model-kit";
13
10
  import { textTypes } from "@hypit/text";
14
11
  import type { ResourceId } from "@hypit/protocol";
12
+ import { validateSeedanceInputs } from "./validation.js";
15
13
 
16
14
  export const seedanceModuleRef = { name: "@hypit/seedance", version: "1" } as const;
17
15
  export const seedanceModels = ["seedance-2", "seedance-2-fast", "seedance-2-mini", "seedance-2.5"] as const;
@@ -93,7 +91,7 @@ export const seedancePorts: Readonly<Record<SeedanceModel, GenerationPortTable>>
93
91
  export type SeedancePortMap = Readonly<Record<string, readonly GenerationPortValue[]>>;
94
92
 
95
93
  export function sealSeedanceRequest(model: SeedanceModel, ports: SeedancePortMap): GenerationRequest {
96
- return sealGenerationPortRequest(seedancePorts[model], ports);
94
+ return seedanceEndpointsByModel[model].sealRequest(ports);
97
95
  }
98
96
 
99
97
  const seedanceBaseDefinition = defineExactModelModule({
@@ -110,6 +108,7 @@ const seedanceBaseDefinition = defineExactModelModule({
110
108
  : `${model.split("-").map((part) => part[0]!.toUpperCase() + part.slice(1)).join("")}Request`,
111
109
  producerName: `request-${model}`,
112
110
  ports: seedancePorts[model],
111
+ validateInputs: validateSeedanceInputs,
113
112
  })),
114
113
  });
115
114
 
@@ -327,23 +326,7 @@ export const seedanceMarkupSurfaces = [
327
326
  /** The duration is an author literal on every Seedance Surface, so the manifest is the exact-model module's own. */
328
327
  export const seedanceManifest = seedanceBaseDefinition.manifest;
329
328
 
330
- const referenceAudioProducers = new Set(Object.values(seedanceEndpoints)
331
- .map((endpoint) => endpoint.mediaBindings["referenceAudio"]!.producer.name));
332
-
333
- export const seedanceComponent = {
334
- ...seedanceBaseDefinition.component,
335
- producers: seedanceBaseDefinition.component.producers.map((facet) =>
336
- referenceAudioProducers.has(facet.producer.name) ? {
337
- ...facet,
338
- handler: (context: Parameters<typeof facet.handler>[0]) => {
339
- const artifact = context.inputs.artifact?.value;
340
- if (artifact?.kind === "blob" && ["audio/mp4", "audio/x-m4a"].includes(artifact.mediaType)) {
341
- throw new Error("Seedance reference audio does not accept m4a; convert to wav or mp3");
342
- }
343
- return facet.handler(context);
344
- },
345
- } : facet),
346
- };
329
+ export const seedanceComponent = seedanceBaseDefinition.component;
347
330
  export const seedanceDefinition = seedanceBaseDefinition;
348
331
 
349
332
  export {
@@ -23,6 +23,7 @@ import type {
23
23
  import { createSeedanceAssembledGenerationFragment } from "./fragment.js";
24
24
  import { seedanceEndpoints, seedancePorts } from "./index.js";
25
25
  import type { SeedanceModel, SeedancePortMap } from "./index.js";
26
+ import { validateSeedanceAudio } from "./validation.js";
26
27
 
27
28
  type MediaInput = {
28
29
  readonly port: "referenceImage" | "referenceVideo" | "referenceAudio" | "firstFrame" | "lastFrame";
@@ -109,9 +110,7 @@ function mediaReference(
109
110
  if (value !== undefined && (value.kind !== "blob" || !value.mediaType.startsWith(`${role}/`))) {
110
111
  throw new Error(`${subject} must reference ${role} media`);
111
112
  }
112
- if (value !== undefined && value.kind === "blob" && role === "audio" && ["audio/mp4", "audio/x-m4a"].includes(value.mediaType)) {
113
- throw new Error(`${subject} references m4a audio, which Seedance does not accept; convert to wav or mp3 and admit that file`);
114
- }
113
+ if (value?.kind === "blob" && role === "audio") validateSeedanceAudio(value, subject);
115
114
  return reference;
116
115
  }
117
116
 
@@ -0,0 +1,16 @@
1
+ import type { GenerationRequestDraft } from "@hypit/generation";
2
+ import type { BlobRef } from "@hypit/protocol";
3
+
4
+ /** Seedance's reference-audio rule, shared by known imports and request assembly. */
5
+ export function validateSeedanceAudio(artifact: BlobRef, subject = "Seedance reference audio"): void {
6
+ if (artifact.mediaType === "audio/mp4" || artifact.mediaType === "audio/x-m4a") {
7
+ throw new Error(`${subject} does not accept m4a; convert the reference audio to WAV or MP3 before using it (media:ExtractAudio produces WAV)`);
8
+ }
9
+ }
10
+
11
+ /** Missing references in a draft remain graph inputs; only supplied audio is checked. */
12
+ export function validateSeedanceInputs(request: GenerationRequestDraft): void {
13
+ for (const value of request.ports.referenceAudio ?? []) {
14
+ if (typeof value === "object" && value.role === "audio") validateSeedanceAudio(value.artifact);
15
+ }
16
+ }
@@ -47,7 +47,12 @@ function shim(): string {
47
47
  start: parseFloat(ownStart || (present && present.getAttribute('data-start')) || '0') || 0,
48
48
  duration: parseFloat(ownDuration || (present && present.getAttribute('data-duration')) || '0') || 0,
49
49
  mediaStart: parseFloat(element.getAttribute('data-media-start') || '0') || 0,
50
- rate: parseFloat(element.getAttribute('data-playback-rate') || '1') || 1
50
+ rate: parseFloat(element.getAttribute('data-playback-rate') || '1') || 1,
51
+ startFrame: Number(element.getAttribute('data-hypit-start-frame')),
52
+ endFrame: Number(element.getAttribute('data-hypit-end-frame')),
53
+ sourceFrame: element.getAttribute('data-hypit-source-frame').split('/').map(BigInt),
54
+ sourceRate: element.getAttribute('data-hypit-source-rate').split('/').map(BigInt),
55
+ sourceFps: element.getAttribute('data-hypit-source-fps').split('/').map(Number)
51
56
  });
52
57
  }
53
58
  var audioContext;
@@ -149,7 +154,8 @@ function shim(): string {
149
154
  }
150
155
  for (var record of media) {
151
156
  var local = currentSeconds - record.start;
152
- var inside = local >= 0 && local < record.duration;
157
+ var programFrame = Math.round(currentSeconds * fps);
158
+ var inside = programFrame >= record.startFrame && programFrame < record.endFrame;
153
159
  var element = record.element;
154
160
  element.style.visibility = inside ? 'visible' : 'hidden';
155
161
  // Normalized picture Artifacts are deliberately silent. Programme sound
@@ -157,10 +163,18 @@ function shim(): string {
157
163
  element.muted = true;
158
164
  if (!inside) { element.pause(); continue; }
159
165
  var target = record.mediaStart + (local + frameSeconds / 2) * record.rate;
166
+ var discrete = scrubbing || programFrame === record.endFrame - 1;
167
+ if (discrete) {
168
+ // The renderer floors exact source-frame coordinates. The midpoint of a
169
+ // programme frame can cross that source boundary at fractional speeds.
170
+ var a = record.sourceFrame, r = record.sourceRate;
171
+ var sourceFrame = Number((a[0] * r[1] + BigInt(programFrame - record.startFrame) * r[0] * a[1]) / (a[1] * r[1]));
172
+ target = (sourceFrame + 0.5) * record.sourceFps[1] / record.sourceFps[0];
173
+ }
160
174
  if (Number.isFinite(element.duration) && element.duration > 0) {
161
175
  target = Math.min(target, Math.max(0, element.duration - 0.001));
162
176
  }
163
- if (scrubbing || local >= record.duration - frameSeconds - 0.000001) {
177
+ if (discrete) {
164
178
  element.pause();
165
179
  waits.push(seekDecoded(element, target));
166
180
  } else {
@@ -2,7 +2,7 @@ import { watch } from "node:fs";
2
2
  import type { FSWatcher } from "node:fs";
3
3
  import { existsSync } from "node:fs";
4
4
  import { readFile } from "node:fs/promises";
5
- import { dirname, isAbsolute, relative, resolve } from "node:path";
5
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
6
6
  import { Readable } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
8
 
@@ -207,7 +207,7 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
207
207
  if (isAbsolute(patch.path)) throw new Error("Studio patches must use workspace-relative paths.");
208
208
  const absolute = resolve(options.workspaceRoot, patch.path);
209
209
  const rel = relative(options.workspaceRoot, absolute);
210
- if (rel.startsWith("..") || isAbsolute(rel) || !allowedSourceFiles.has(absolute)) {
210
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel) || !allowedSourceFiles.has(absolute)) {
211
211
  throw new Error(`Studio cannot write source file ${patch.path}.`);
212
212
  }
213
213
  if (!Number.isInteger(patch.range.start) || !Number.isInteger(patch.range.end)
@@ -34,7 +34,11 @@ an editable starter; its Endpoint entries describe available routes, not choices
34
34
  Keep an existing chosen service, or configure the chosen local or hosted Provider and its capability
35
35
  bindings. HypiHub is the recommended integrated hosted route in the official Distribution; other
36
36
  services use project Provider packages. If the user chooses HypiHub,
37
- `hypit auth login hypihub.default` connects that account.
37
+ `hypit auth login hypihub.default` connects that account after choosing its CredentialStore.
38
+ The starter selects the OS store for macOS Keychain / Windows Credential Locker. On Linux, or when
39
+ explicitly choosing file storage, edit the Profile's `credentials` and Endpoint reference as shown
40
+ in [file CredentialStore](../credential-store-file/README.md#select-it-before-login) before `auth` or
41
+ `runtime up`. This selection is configuration; execution never switches stores automatically.
38
42
  `hypit doctor --endpoint <name>` checks a selected Endpoint;
39
43
  `hypit runtime up --endpoint <name>` prepares that Endpoint and starts the Worker. Repeat the flag
40
44
  for several chosen Endpoints; omitting it prepares the whole Profile. `hypit programs up --endpoint