@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
@@ -24,6 +24,10 @@ function findClose(input: ScriptSurfaceInput): { readonly start: number; readonl
24
24
  const close = `</${input.tag}>`;
25
25
  let cursor = input.contentStart;
26
26
  while (cursor < input.source.length) {
27
+ if (input.source[cursor] === "\\") {
28
+ cursor += 2;
29
+ continue;
30
+ }
27
31
  if (input.source.startsWith("<!--", cursor)) {
28
32
  const commentEnd = input.source.indexOf("-->", cursor + 4);
29
33
  if (commentEnd < 0) {
@@ -33,9 +37,7 @@ function findClose(input: ScriptSurfaceInput): { readonly start: number; readonl
33
37
  continue;
34
38
  }
35
39
  if (input.source.startsWith(close, cursor)) {
36
- let slashes = 0;
37
- for (let before = cursor - 1; before >= 0 && input.source[before] === "\\"; before -= 1) slashes += 1;
38
- if (slashes % 2 === 0) return { start: cursor, end: cursor + close.length };
40
+ return { start: cursor, end: cursor + close.length };
39
41
  }
40
42
  cursor += 1;
41
43
  }
@@ -35,7 +35,7 @@ export type ParsedSegment = NarrativeSegment & {
35
35
  readonly index: number;
36
36
  readonly atoms: readonly ParsedAtom[];
37
37
  readonly range: SourceRange;
38
- /** Exact body range between the Segment tags, used only for source-preserving marker edits. */
38
+ /** Exact body range between Segment tags for Script-owned source edits and formatting. */
39
39
  readonly contentRange: SourceRange;
40
40
  readonly selfClosing: boolean;
41
41
  };
@@ -9,5 +9,5 @@ It recognizes exactly one mandatory bounded Header:
9
9
  ```
10
10
 
11
11
  The Header selects an exact trusted Frontend. There is no suffix dispatch and no default parser.
12
- The package masks the Header while preserving character offsets, but does not recognize imports,
12
+ The package masks the Header while preserving UTF-16 source offsets and line breaks, but does not recognize imports,
13
13
  XML, Script, Recipes, Run syntax or domain Types. Those belong to the selected Frontend.
@@ -75,6 +75,7 @@ export function parseSourceHeader(sourceName: string, text: string): SourceHeade
75
75
 
76
76
  /** Preserve every original offset while making the Header ordinary whitespace to body Frontends. */
77
77
  export function maskSourceHeader(text: string, header: SourceHeader): string {
78
- const prefix = text.slice(0, header.end).replace(/[^\r\n]/gu, " ");
78
+ // Source ranges use UTF-16 offsets: replace each code unit, not each code point.
79
+ const prefix = text.slice(0, header.end).replace(/[^\r\n]/g, " ");
79
80
  return `${prefix}${text.slice(header.end)}`;
80
81
  }
@@ -1,5 +1,13 @@
1
1
  # Hypit Studio
2
2
 
3
+ For direct picture inspection, `hypit snapshot --studio <studio-url>` reads the current compiled
4
+ `HyperframesDocument` from `GET /__studio/document` and its existing `/__studio/material/<resource>`
5
+ resources. `GET /__studio/visual.html` exposes the same materialized picture without the interactive
6
+ Studio playback shim or audio. Both representations come from the same compilation as the displayed
7
+ preview. The snapshot invocation uses the selected Runtime Profile's frame Provider and creates no
8
+ Build. During a pending Source compilation the export route reports that state; a failed update
9
+ reports its error instead of returning the previous picture as current.
10
+
3
11
  The single official Web Studio for SVML. It opens an explicit Run Source,
4
12
  traces its Film or Render target back to the semantic and visual projections
5
13
  Studio can edit, runs deterministic Producers and explicitly permitted transient Needs, and composites the
@@ -159,6 +167,11 @@ the selected entity. A reference can resolve to a shared Frame or Recipe, so one
159
167
  may affect several consumers. Structured fields save together when editing ends and the value is complete; missing required values stay in the editor with a completion hint.
160
168
  Check save status; source conflicts reject stale edits rather than overwrite newer files.
161
169
 
170
+ Script source ranges come from its raw Surface and Companion. Marker edits use Script's parsed
171
+ anchors and preserve unrelated prose, whitespace and word attributes; the displayed timeline words
172
+ are not a replacement text source. Recipe parameter reads use the same Source Header preparation
173
+ as compilation, preserving UTF-16 offsets for the exact property being edited.
174
+
162
175
  The Timeline owns the editor's complete range; displayed objects do not extend it. Take placement
163
176
  and complete extent are reference information in Studio.
164
177
 
@@ -3,6 +3,7 @@ import { relative } from "node:path";
3
3
  import type { Plugin } from "vite";
4
4
  import { createFeedbackStore, FeedbackConflict } from "./feedback-store.js";
5
5
  import { readFeedbackMutation } from "./feedback.js";
6
+ import { allowsStudioMutation } from "./mutation-origin.js";
6
7
  import type { FeedbackDocument, FeedbackView } from "./feedback.js";
7
8
 
8
9
  /** Review storage is separate from compilation, Results and Agent delivery. */
@@ -23,6 +24,12 @@ export function studioFeedbackPlugin(workspaceRoot: string, runPath: string): Pl
23
24
  server.httpServer?.once("close", () => watcher.close());
24
25
  server.middlewares.use((request, response, next) => {
25
26
  if (new URL(request.url ?? "/", "http://studio.hypit.local").pathname !== "/__studio/feedback") return next();
27
+ if (request.method === "POST" && !allowsStudioMutation(request.headers)) {
28
+ response.statusCode = 403;
29
+ response.setHeader("content-type", "application/json; charset=utf-8");
30
+ response.end(JSON.stringify({ error: "Studio mutation must come from this local Studio session." }));
31
+ return;
32
+ }
26
33
  response.setHeader("content-type", "application/json; charset=utf-8");
27
34
  response.setHeader("cache-control", "no-store");
28
35
  void (async () => {
@@ -0,0 +1,20 @@
1
+ import type { IncomingHttpHeaders } from "node:http";
2
+
3
+ /** Studio writes belong to the local page serving this Studio session. */
4
+ export function allowsStudioMutation(headers: IncomingHttpHeaders): boolean {
5
+ const host = headers.host;
6
+ if (host === undefined) return false;
7
+ let target: URL;
8
+ try { target = new URL(`http://${host}`); } catch { return false; }
9
+ if (!["localhost", "127.0.0.1", "[::1]"].includes(target.hostname) || target.host !== host.toLowerCase()) return false;
10
+
11
+ const origin = headers.origin;
12
+ if (origin !== undefined) {
13
+ let source: URL;
14
+ try { source = new URL(origin); } catch { return false; }
15
+ if (source.origin !== origin || source.origin !== target.origin) return false;
16
+ }
17
+
18
+ const site = headers["sec-fetch-site"];
19
+ return site === undefined || site === "same-origin" || site === "none";
20
+ }
@@ -17,6 +17,7 @@ import type {
17
17
  } from "@hypit/studio-adapter";
18
18
  import { parseSvs } from "@hypit/svs";
19
19
  import { parseOpeningTag } from "@hypit/markup";
20
+ import { prepareAuthorSource } from "@hypit/elaborator";
20
21
  import { parameterControlForSchema, parameterRecordSchema } from "./parameter-values.js";
21
22
  import type { CanonicalValue } from "@hypit/protocol";
22
23
 
@@ -132,12 +133,6 @@ function sourceAbsolute(root: string, path: string, base?: string): string {
132
133
  return resolve(directory, path);
133
134
  }
134
135
 
135
- function svsText(source: string): string {
136
- const header = /^\s*<\?svml[\s\S]*?\?>/u.exec(source);
137
- if (header === null) return source;
138
- return `${header[0].replace(/[^\r\n]/gu, " ")}${source.slice(header[0].length)}`;
139
- }
140
-
141
136
  function recipeParameters(input: {
142
137
  readonly root: string;
143
138
  readonly files: readonly StudioSourceFile[];
@@ -176,7 +171,8 @@ function recipeParameters(input: {
176
171
  const source = sourceFor(input.root, imported.source, input.files, input.current.path);
177
172
  if (source === undefined || source.language !== "svs") return [];
178
173
  const recipePath = parts.join(".");
179
- const parsed = parseSvs(source.path, svsText(source.text));
174
+ const prepared = prepareAuthorSource({ id: source.path, name: source.path, text: source.text });
175
+ const parsed = parseSvs(source.path, prepared.text);
180
176
  const recipe = parsed.recipes.find((item) => item.value.path === recipePath);
181
177
  if (recipe === undefined) return [];
182
178
  return input.recipe.bindings.flatMap((declaration): readonly StudioSourceBinding[] => {
@@ -1,5 +1,6 @@
1
1
  import type { AudioTrack, Composition } from "@hypit/composition";
2
2
  import { compileHyperframesDocument, materializeHyperframesHtml } from "@hypit/hyperframes";
3
+ import type { HyperframesDocument } from "@hypit/hyperframes";
3
4
  import type { ProgramSpace } from "@hypit/program-space";
4
5
 
5
6
  import { injectRuntimeShim } from "./runtime-shim.js";
@@ -19,6 +20,11 @@ export type RenderInput = {
19
20
  * material all come from the projection selected by the Run.
20
21
  */
21
22
  export function renderPreview(input: RenderInput): string {
23
+ return renderStudioProgramme(input).preview;
24
+ }
25
+
26
+ /** The same compiled picture serves immediate frame capture and interactive playback. */
27
+ export function renderStudioProgramme(input: RenderInput): { readonly document: HyperframesDocument; readonly html: string; readonly preview: string } {
22
28
  const document = compileHyperframesDocument(input.composition, input.space);
23
29
  const html = materializeHyperframesHtml(document, (artifact) => {
24
30
  // The only Artifacts a preview can reference are files the author already
@@ -51,5 +57,5 @@ export function renderPreview(input: RenderInput): string {
51
57
  fadeInSamples: clip.fadeInSamples, fadeOutSamples: clip.fadeOutSamples,
52
58
  }))}"></audio>`;
53
59
  }).join("");
54
- return injectRuntimeShim(html, audio);
60
+ return { document, html, preview: injectRuntimeShim(html, audio) };
55
61
  }
@@ -15,6 +15,7 @@ import type { ServedFile } from "./compile.js";
15
15
  import type { StudioDomain } from "./domain.js";
16
16
  import type { StudioCompanionRegistry } from "./studio-registry.js";
17
17
  import { loadStudioRun } from "./run.js";
18
+ import { allowsStudioMutation } from "./mutation-origin.js";
18
19
  import { parameterAuthorValue, parameterOption, serializeParameterValue, serializeAttributeGroup, validateParameterValue } from "./parameter-values.js";
19
20
  import { readStudioSession } from "./session.js";
20
21
  import type { Range, StudioFailure, StudioLibraryRequest, StudioLibraryView, StudioMutation, StudioSnapshot } from "./shared.js";
@@ -80,6 +81,8 @@ class StudioMutationRejected extends Error {}
80
81
 
81
82
  export function studioPlugin(options: StudioPluginOptions): Plugin {
82
83
  let snapshot: StudioSnapshot | undefined;
84
+ let visualHtml: string | undefined;
85
+ let visualDocument: import("@hypit/hyperframes").HyperframesDocument | undefined;
83
86
  let failure: StudioFailure | undefined;
84
87
  let material: ReadonlyMap<string, ServedFile> = new Map();
85
88
  let revision = 0;
@@ -158,6 +161,8 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
158
161
  if (attempt !== requestedRevision) return;
159
162
  revision = attempt;
160
163
  snapshot = result.snapshot;
164
+ visualHtml = result.visualHtml;
165
+ visualDocument = result.document;
161
166
  material = result.material;
162
167
  failure = undefined;
163
168
  if (notify) server?.ws.send({ type: "custom", event: "studio:snapshot", data: snapshot });
@@ -473,6 +478,10 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
473
478
  value.middlewares.use((request, response, next) => {
474
479
  const url = new URL(request.url ?? "/", "http://studio.hypit.local");
475
480
  if (request.method === "PUT" && url.pathname === "/__studio/source") {
481
+ if (!allowsStudioMutation(request.headers)) {
482
+ json(response, 403, { error: "Cross-origin Studio mutations are prohibited." });
483
+ return;
484
+ }
476
485
  void (async () => {
477
486
  let acquired = false;
478
487
  try {
@@ -524,6 +533,10 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
524
533
  return;
525
534
  }
526
535
  if (request.method === "PUT" && url.pathname === "/__studio/artifact-name") {
536
+ if (!allowsStudioMutation(request.headers)) {
537
+ json(response, 403, { error: "Cross-origin Studio mutations are prohibited." });
538
+ return;
539
+ }
527
540
  void (async () => {
528
541
  try {
529
542
  const chunks: Buffer[] = [];
@@ -546,6 +559,10 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
546
559
  return;
547
560
  }
548
561
  if (request.method === "POST" && url.pathname === "/__studio/mutation") {
562
+ if (!allowsStudioMutation(request.headers)) {
563
+ json(response, 403, { error: "Cross-origin Studio mutations are prohibited." });
564
+ return;
565
+ }
549
566
  void (async () => {
550
567
  try {
551
568
  const chunks: Buffer[] = [];
@@ -580,6 +597,28 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
580
597
  next();
581
598
  return;
582
599
  }
600
+ if (url.pathname === "/__studio/visual.html" || url.pathname === "/__studio/document") {
601
+ void (async () => {
602
+ if (timer !== undefined || publishing > 0) {
603
+ json(response, 409, { error: "Studio is compiling a Source change; capture after the updated preview is ready." });
604
+ return;
605
+ }
606
+ if (snapshot === undefined && failure === undefined) await publish(++requestedRevision);
607
+ if (failure !== undefined || visualHtml === undefined) {
608
+ json(response, 500, failure ?? { error: "Studio has no compiled picture." });
609
+ return;
610
+ }
611
+ if (url.pathname === "/__studio/document") {
612
+ json(response, 200, visualDocument);
613
+ return;
614
+ }
615
+ response.statusCode = 200;
616
+ response.setHeader("content-type", "text/html; charset=utf-8");
617
+ response.setHeader("cache-control", "no-store");
618
+ response.end(request.method === "HEAD" ? undefined : visualHtml);
619
+ })();
620
+ return;
621
+ }
583
622
  if (url.pathname === "/__studio/session") {
584
623
  void (async () => {
585
624
  if (snapshot === undefined && failure === undefined) {
@@ -9,7 +9,7 @@ import type { ServedFile } from "./compile.js";
9
9
  import type { StudioDomain } from "./domain.js";
10
10
  import type { Observations } from "./observe.js";
11
11
  import { preview } from "./programme.js";
12
- import { renderPreview } from "./preview/render.js";
12
+ import { renderStudioProgramme } from "./preview/render.js";
13
13
  import type { RunPlan } from "./run.js";
14
14
  import type { StudioSnapshot } from "./shared.js";
15
15
  import type { StudioCompanionRegistry } from "./studio-registry.js";
@@ -47,6 +47,8 @@ function sourceFiles(run: RunPlan): readonly StudioSourceFile[] {
47
47
 
48
48
  export type StudioSession = {
49
49
  readonly snapshot: StudioSnapshot;
50
+ readonly document: import("@hypit/hyperframes").HyperframesDocument;
51
+ readonly visualHtml: string;
50
52
  readonly material: ReadonlyMap<string, ServedFile>;
51
53
  readonly observations: Observations;
52
54
  readonly projections: readonly StudioViewRequirement[];
@@ -77,7 +79,7 @@ export async function readStudioSession(input: {
77
79
  projections: inspection.projections,
78
80
  ...(input.transientExecution === undefined ? {} : { transientExecution: input.transientExecution }),
79
81
  });
80
- const rendered = renderPreview({
82
+ const rendered = renderStudioProgramme({
81
83
  composition: built.composition,
82
84
  space: built.space as never,
83
85
  served: new Set(built.served.keys()),
@@ -85,6 +87,8 @@ export async function readStudioSession(input: {
85
87
  const text = readFileSync(input.run.authorSource, "utf8");
86
88
  const files = sourceFiles(input.run);
87
89
  return {
90
+ document: rendered.document,
91
+ visualHtml: rendered.html,
88
92
  snapshot: snapshot(input.registry, built, {
89
93
  revision: input.revision,
90
94
  path: input.sourcePath ?? input.run.authorSource,
@@ -99,7 +103,7 @@ export async function readStudioSession(input: {
99
103
  },
100
104
  canvas: built.canvas,
101
105
  frameRate: built.frameRate,
102
- preview: { kind: "hyperframes", srcdoc: rendered },
106
+ preview: { kind: "hyperframes", srcdoc: rendered.preview },
103
107
  workspaceRoot: input.workspaceRoot,
104
108
  sourceFiles: files,
105
109
  surfaces: input.domain.surfaces,
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * A tokenizer for SVML's own surface syntax.
3
3
  *
4
- * SVML has a small, closed grammar tags, quoted strings, whole-value
5
- * references, and Script prose with markers — so it is tokenized directly rather
6
- * than through a general highlighter that has no grammar for it. The Script
4
+ * Recognize the common Markup shell and Script prose for highlighting, without
5
+ * validating package-owned Surfaces. The Script
7
6
  * markers are the point: `@{claim} … @{/claim}` is what a Media Item binds to, so it
8
7
  * has to read as a distinct thing from an ordinary attribute.
9
8
  *
@@ -105,8 +104,7 @@ export function tokenizeSvml(source: string): readonly Token[] {
105
104
  cursor = nameStart + name[0].length;
106
105
  cursor = tokenizeAttributes(source, cursor, push);
107
106
 
108
- // Script is the language's one Raw Surface: its body is prose with
109
- // markers, not markup, and must be scanned by different rules.
107
+ // Script's Raw Surface has its own prose grammar.
110
108
  if (!closing && localName(name[0]) === "script" && source[cursor - 2] !== "/") {
111
109
  cursor = tokenizeScriptBody(source, cursor, name[0], push);
112
110
  }
@@ -167,6 +165,7 @@ function tokenizeAttributes(source: string, from: number, push: Push): number {
167
165
  function tokenizeScriptBody(source: string, from: number, tag: string, push: Push): number {
168
166
  const close = `</${tag}>`;
169
167
  let cursor = from;
168
+ let inSegment = false;
170
169
  while (cursor < source.length) {
171
170
  if (source[cursor] === "\\") { cursor += 2; continue; }
172
171
  if (source.startsWith(close, cursor)) {
@@ -199,14 +198,16 @@ function tokenizeScriptBody(source: string, from: number, tag: string, push: Pus
199
198
  }
200
199
  const closing = source[cursor + 1] === "/";
201
200
  const nameStart = cursor + (closing ? 2 : 1);
202
- const name = NAME.exec(source.slice(nameStart));
203
- if (name === null) { cursor += 1; continue; }
204
201
  const end = source.indexOf(">", nameStart);
205
202
  const stop = end < 0 ? source.length : end + 1;
203
+ const selfClosing = end >= 0 && source[end - 1] === "/";
204
+ const nameEnd = end < 0 ? stop : end - (selfClosing ? 1 : 0);
206
205
  push(cursor, nameStart, "punct");
207
- // An upper-case tag inside Script is a Role Cue, not a Segment.
208
- push(nameStart, nameStart + name[0].length, /^[A-Z]/u.test(name[0]) ? "role" : "tag");
209
- push(nameStart + name[0].length, stop, "punct");
206
+ // Context distinguishes Segments from Role Cues, regardless of language or case.
207
+ push(nameStart, nameEnd, inSegment && !closing ? "role" : "tag");
208
+ push(nameEnd, stop, "punct");
209
+ if (closing) inSegment = false;
210
+ else if (!inSegment && !selfClosing) inSegment = true;
210
211
  cursor = stop;
211
212
  continue;
212
213
  }
@@ -164,7 +164,7 @@ write target. The `fixed` case means no temporal gesture writes that endpoint, n
164
164
  parameter editing is forbidden.
165
165
 
166
166
  - `@hypit/script` owns tokenization, anchor identities, legal marker sites, and structural writeback.
167
- It normalizes equivalent marker whitespace, retains punctuation and attributes, writes coincident
167
+ It relocates markers without rewriting unrelated prose whitespace, retains punctuation and attributes, writes coincident
168
168
  markers together, and preserves all unrelated relationships and caption information.
169
169
  - `@hypit/temporal-markup` owns the interpretation of each time form and declares its write targets.
170
170
  Direct semantic references expose semantic targets; quoted time expressions expose parameter
@@ -1,8 +1,8 @@
1
1
  # `@hypit/video-cli`
2
2
 
3
3
  Official video command application. It selects the Markup compiler Host and supplies one editable
4
- starter Runtime Profile. The CLI still imports no Provider or Store implementation; installed packages
5
- are activated only by explicit Source imports or Profile `use` entries.
4
+ starter Runtime Profile. Execution Endpoints are selected by Profile `use` entries; Source imports
5
+ activate author packages. Immediate tools own their temporary input/output resource storage.
6
6
 
7
7
  Every Frontend, Surface, deterministic Producer and Validator is activated from Source imports.
8
8
  Installing a new author package therefore does not require a video CLI or Core release. Source
@@ -67,9 +67,35 @@ to the chosen file. `measure` estimates a passage locally:
67
67
 
68
68
  ```bash
69
69
  hypit transcribe reference.mp4 --to notes/reference.transcript.json --language en
70
+ hypit transcribe assets/recorded-voice.wav --to notes/voice.transcript.json --language en
70
71
  hypit measure main.svml --segment hook --language en --pace normal --rounding round
71
72
  ```
72
73
 
74
+ `snapshot` follows the same immediate invocation model for picture inspection. Prefer it for
75
+ existing production states and motion sequences, keeping Studio for playback with sound and Builds
76
+ for encoded delivery:
77
+
78
+ ```bash
79
+ hypit snapshot --studio http://localhost:5191 --at-frame 240,255,269 --to evidence/states
80
+ hypit snapshot --studio http://localhost:5191 --start-frame 240 --end-frame-exclusive 270 \
81
+ --grid 4x3 --cell 480 --to evidence/motion
82
+ hypit snapshot ./picture/index.html --at-frame 240 --to evidence/detail
83
+ ```
84
+
85
+ `--studio` reads Studio's current compiled document and its declared resources; a path or HTML URL
86
+ reads materialized HTML with inline scripts/styles and directly addressed media/fonts. The Profile's
87
+ `@hypit/render-hyperframes@1#render-frames` Endpoint returns PNGs in selected-frame order.
88
+ `--runtime` and `--workspace` select the environment just as for `transcribe`. The call streams
89
+ resources through a temporary `FileResourceStore`, writes full-size PNGs and optional grid pages,
90
+ then releases that temporary storage. `--to` names a new directory. `--json` reports paths and original
91
+ frame positions. Grid labels remain outside the picture. No Build, Worker receipt or video encoding
92
+ is created. Provider-owned browser preparation is unchanged.
93
+
94
+ `media frames --every-frame` and `media tiles --every-frame` decode every source frame in a selected
95
+ half-open seconds interval once. Native timestamps, including variable frame rate, are read from
96
+ decoder PTS and time base. `tiles --ranges <json> --every-frame` decodes each listed interval and
97
+ paginates its images. `--transcript` adds word context. This native path uses no FPS resampling.
98
+
73
99
  For `transcribe`, set `--language` to an explicit lowercase two- or three-letter spoken language code,
74
100
  such as `en`, `zh` or `ko`. The selected service owns which languages it can align. Chinese speech uses `zh`, including
75
101
  Chinese speech containing English names. The request selects the recognition language and
@@ -106,6 +132,9 @@ prints what a Source may write:
106
132
  ```bash
107
133
  hypit media probe reference.mp4
108
134
  hypit media cut reference.mp4 --start 12 --end 19.5 --label-time --to notes/hook.mp4
135
+ hypit media cut assets/talk.mp4 --start 12 --end 19.5 --to assets/opening.mp4
136
+ hypit media cut assets/talk.mp4 --keep 12:15.5 --keep 16:19.5 --to assets/opening-edited.mp4
137
+ hypit media cut assets/narration.wav --keep 0.3:4.1 --keep 4.6:9.2 --to assets/narration-edited.wav
109
138
  hypit media frames reference.mp4 --at 12.4,13.1 --label-time --to notes/hook-frames
110
139
  hypit media tile reference.mp4 --start 12 --end 19.5 --to notes/hook-grid.jpg
111
140
  hypit media tile reference.mp4 --at 12.4,13.1,14.8 --columns 3 --to notes/exact-grid.jpg
@@ -119,7 +148,21 @@ hypit vocabulary @hypit/media-pipeline --tag StillVideo
119
148
  hypit vocabulary --visual text
120
149
  ```
121
150
 
122
- `cut` isolates the requested interval and can visibly overlay source time on the evidence copy.
151
+ `probe` accepts audio-only files as well as video. `cut` keeps one interval using `--start` and
152
+ `--end`, or joins explicitly retained, ordered, non-overlapping intervals using repeated
153
+ `--keep start:end` in source seconds. The latter is useful for removing gaps inside one recorded
154
+ performance; it does not decide where Script Segments belong. Video retains its available picture
155
+ and sound together (MP4 is a useful output container); audio-only outputs PCM WAV. A silent source
156
+ video stays silent. The
157
+ `--json` reports the source intervals, their nominal positions on the new local clock, and the
158
+ measured output duration; actual frame and sample boundaries can differ slightly from the nominal
159
+ positions. It writes a new file and refuses to
160
+ overwrite an existing one. `--label-time` retains its single-video-interval role: it visibly
161
+ overlays source time on an inspection copy, not on the clean production media.
162
+
163
+ `transcribe` also accepts audio or video. Its transcript refers to the *input file's* clock. A
164
+ cut or joined file has a new clock; use the final recorded performance and its Script in the
165
+ Build's semantic preparation rather than treating source transcript timestamps as final timing.
123
166
  `frames` writes one JPEG per requested time, selecting the first decoded frame at or after it.
124
167
  Visible frame labels use that frame's actual timestamp, as do the labels below each `tile` cell.
125
168
  Sampling is shared by `frames`, `tile` and `tiles`:
@@ -149,7 +192,7 @@ and its Endpoint remain separate. `boundaries` reports adjacent-frame
149
192
  change candidates and their measured scores; it does not suppress short changes or call them shots.
150
193
  `prepare-fetch` explicitly prepares the locked downloader environment; `fetch` requires it and
151
194
  turns a link into a file with the pinned yt-dlp; [the downloader package](../yt-dlp/README.md)
152
- owns its dependencies, download choices and file handling. Commands that create evidence write only
195
+ owns its dependencies, download choices and file handling. Commands that create files write only
153
196
  what `--to` names and refuse to overwrite. `vocabulary` reads the installed
154
197
  manifests: every package with its tags and models, or one package's Surfaces with their attributes,
155
198
  children and example, or the value shapes a drawing Producer must emit.
@@ -18,6 +18,7 @@
18
18
  "@hypit/driver-node": "workspace:*",
19
19
  "@hypit/estimate": "workspace:*",
20
20
  "@hypit/generation": "workspace:*",
21
+ "@hypit/hyperframes": "workspace:*",
21
22
  "@hypit/markup": "workspace:*",
22
23
  "@hypit/model-kit": "workspace:*",
23
24
  "@hypit/package-loader-node": "workspace:*",
@@ -28,6 +29,8 @@
28
29
  "@hypit/provider-monid": "workspace:*",
29
30
  "@hypit/provider-pollo": "workspace:*",
30
31
  "@hypit/provider-tokendance": "workspace:*",
32
+ "@hypit/render-hyperframes": "workspace:*",
33
+ "@hypit/resource-store-fs": "workspace:*",
31
34
  "@hypit/runtime": "workspace:*",
32
35
  "@hypit/runtime-kit": "workspace:*",
33
36
  "@hypit/runtime-host-node": "workspace:*",
@@ -4,8 +4,10 @@ import type { CliIo } from "@hypit/cli";
4
4
  import { creationCommands, isCreationCommand, writeCreationHelp } from "./creation.js";
5
5
  import { isMediaCommand, mediaCommands, writeMediaHelp } from "./media.js";
6
6
  import { writeVocabularyHelp } from "./vocabulary.js";
7
+ import { writeSnapshotHelp } from "./snapshot.js";
7
8
  import { writeCaptureHelp } from "./capture.js";
8
9
  import { runVersionCli, writeVersionHelp } from "./version.js";
10
+ import { acceptSecretBytes } from "./secret-input.js";
9
11
 
10
12
  const argv = process.argv.slice(2);
11
13
  const json = argv.includes("--json");
@@ -26,22 +28,19 @@ async function readSecret(prompt: string): Promise<string> {
26
28
  }
27
29
  process.stderr.write(prompt);
28
30
  return await new Promise<string>((resolve, reject) => {
29
- let value = "";
31
+ const raw: number[] = [];
30
32
  const finish = (error?: Error): void => {
31
33
  process.stdin.off("data", input);
32
34
  process.stdin.setRawMode(false);
33
35
  process.stdin.pause();
34
36
  process.stderr.write("\n");
35
- if (error === undefined) resolve(value);
37
+ if (error === undefined) resolve(Buffer.from(raw).toString("utf8"));
36
38
  else reject(error);
37
39
  };
38
40
  const input = (chunk: Buffer): void => {
39
- for (const byte of chunk) {
40
- if (byte === 3) { finish(new Error("credential input cancelled")); return; }
41
- if (byte === 10 || byte === 13) { finish(); return; }
42
- if (byte === 8 || byte === 127) { value = value.slice(0, -1); continue; }
43
- value += String.fromCharCode(byte);
44
- }
41
+ const result = acceptSecretBytes(raw, chunk);
42
+ if (result === "cancelled") finish(new Error("credential input cancelled"));
43
+ if (result === "done") finish();
45
44
  };
46
45
  process.stdin.setRawMode(true);
47
46
  process.stdin.resume();
@@ -68,6 +67,7 @@ async function main(): Promise<void> {
68
67
  const topic = argv[0] === "help" ? argv[1]
69
68
  : argv[0] === "--help" ? undefined
70
69
  : argv.includes("--help") ? argv[0] : undefined;
70
+ if (topic === "snapshot") { writeSnapshotHelp(io); return; }
71
71
  if (topic === "version") { writeVersionHelp(io); return; }
72
72
  if (isCreationCommand(topic)) {
73
73
  writeCreationHelp(io, topic);
@@ -89,7 +89,7 @@ async function main(): Promise<void> {
89
89
  writeCliHelp(io, topic);
90
90
  if (topic === undefined) {
91
91
  io.write(`\nInstallation\n version [--check] [--registry <url>] [--json]\n\nCreation tools (one request through the selected Runtime Profile, no Build)\n${
92
- creationCommands.map((item) => ` ${item}`).join("\n")}\n hypit help <tool> for each\n`
92
+ [...creationCommands, "snapshot"].map((item) => ` ${item}`).join("\n")}\n hypit help <tool> for each\n`
93
93
  + `\nStudio\n studio --run <build.svrun>\n hypit studio --help for session options\n\nPreparation (local tools and project files)\n media ${mediaCommands.join(" | ")}\n capture screenshot | run | install-browser\n vocabulary\n hypit help media, hypit help capture, hypit help vocabulary\n`);
94
94
  }
95
95
  return;
@@ -150,7 +150,7 @@ function capabilityName(capability: CapabilityRef): string {
150
150
  }
151
151
 
152
152
  /** Which Endpoint will serve this call and where its Provider publishes prices, before spending. */
153
- async function selectedProvider(host: CreationHost, need: Need, profile: string): Promise<RuntimeHostCapabilityProvider> {
153
+ export async function selectedProvider(host: CreationHost, need: Need, profile: string): Promise<RuntimeHostCapabilityProvider> {
154
154
  const [provider] = await host.providers([{
155
155
  request: need.id,
156
156
  capability: need.capability,
@@ -176,7 +176,7 @@ function priceLine(provider: RuntimeHostCapabilityProvider): string {
176
176
  : provider.pricing.kind === "local" ? "local, no Provider charge" : provider.pricing.url;
177
177
  }
178
178
 
179
- function providerView(provider: RuntimeHostCapabilityProvider) {
179
+ export function providerView(provider: RuntimeHostCapabilityProvider) {
180
180
  return {
181
181
  endpoint: provider.endpoint ?? null,
182
182
  use: provider.use ?? null,
@@ -184,7 +184,7 @@ function providerView(provider: RuntimeHostCapabilityProvider) {
184
184
  };
185
185
  }
186
186
 
187
- function providerLine(provider: RuntimeHostCapabilityProvider): string {
187
+ export function providerLine(provider: RuntimeHostCapabilityProvider): string {
188
188
  return `${provider.endpoint} (${provider.use}) · ${priceLine(provider)}`;
189
189
  }
190
190
 
@@ -0,0 +1,34 @@
1
+ import sharp from "sharp";
2
+
3
+ /** Arrange already captured evidence. Decoding and time selection belong to the caller. */
4
+ export async function writeFrameGrid(
5
+ frames: readonly { readonly path: string; readonly label: Buffer }[],
6
+ target: string, cellWidth: number, columns: number,
7
+ ): Promise<void> {
8
+ if (frames.length === 0) throw new Error("A frame grid needs at least one image");
9
+ const cells = [];
10
+ for (const frame of frames) {
11
+ const picture = await sharp(frame.path).resize({ width: cellWidth }).png().toBuffer({ resolveWithObject: true });
12
+ const labelHeight = (await sharp(frame.label).metadata()).height!;
13
+ cells.push({ picture: picture.data, height: picture.info.height, label: frame.label, labelHeight });
14
+ }
15
+ const pictureHeight = Math.max(...cells.map(cell => cell.height));
16
+ const rowHeight = pictureHeight + Math.max(...cells.map(cell => cell.labelHeight));
17
+ const rows = Math.ceil(cells.length / columns);
18
+ await sharp({ create: {
19
+ width: columns * (cellWidth + 8) + 8, height: rows * (rowHeight + 8) + 8,
20
+ channels: 3, background: "#0c0c0c",
21
+ } }).composite(cells.flatMap((cell, index) => {
22
+ const left = 8 + index % columns * (cellWidth + 8);
23
+ const top = 8 + Math.floor(index / columns) * (rowHeight + 8);
24
+ return [{ input: cell.picture, left, top }, { input: cell.label, left, top: top + pictureHeight }];
25
+ })).toFile(target);
26
+ }
27
+
28
+ export async function snapshotFrameLabel(frame: number, seconds: number, width: number): Promise<Buffer> {
29
+ const text = await sharp({ text: { text: `<span foreground="#f5f5f5">Frame ${frame} · ${seconds.toFixed(6)} s</span>`,
30
+ font: `sans ${Math.max(12, Math.round(width / 28))}`, width: width - 16, rgba: true,
31
+ } }).png().toBuffer({ resolveWithObject: true });
32
+ return sharp({ create: { width, height: text.info.height + 16, channels: 3, background: "#0c0c0c" } })
33
+ .composite([{ input: text.data, left: 8, top: 8 }]).png().toBuffer();
34
+ }
@@ -12,6 +12,7 @@ import { videoCliDistribution } from "./distribution.js";
12
12
  import { runMediaCli } from "./media.js";
13
13
  import { runVocabularyCli } from "./vocabulary.js";
14
14
  import { runCaptureCli } from "./capture.js";
15
+ import { runSnapshotCli } from "./snapshot.js";
15
16
  import { runVersionCli } from "./version.js";
16
17
 
17
18
  export {
@@ -42,6 +43,7 @@ export function runVideoCli(
42
43
  : [videoCliDistribution.packageRoot]);
43
44
  installExternalPackageResolution([hypitHostPackageRoot()]);
44
45
  if (isCreationCommand(argv[0])) return runCreationCli(argv, io);
46
+ if (argv[0] === "snapshot") return runSnapshotCli(argv, io);
45
47
  if (argv[0] === "media") return runMediaCli(argv, io);
46
48
  if (argv[0] === "capture") return runCaptureCli(argv, io);
47
49
  if (argv[0] === "vocabulary") return runVocabularyCli(argv, io);
@@ -50,3 +52,5 @@ export function runVideoCli(
50
52
  bootstrapPackages: packages,
51
53
  });
52
54
  }
55
+
56
+ export { runSnapshotCli, writeSnapshotHelp } from "./snapshot.js";