@hypit/hypit 0.1.3 → 0.1.4

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 (92) hide show
  1. package/README.md +1 -1
  2. package/dist/public/studio-adapter.d.ts +31 -2
  3. package/package.json +4 -1
  4. package/packages/audio-track-studio/src/index.ts +7 -4
  5. package/packages/build-result/README.md +58 -0
  6. package/packages/build-result/package.json +2 -1
  7. package/packages/build-result/src/decode.ts +10 -8
  8. package/packages/build-result/src/file-reference.ts +42 -0
  9. package/packages/build-result/src/index.ts +3 -0
  10. package/packages/build-result/src/replace-file-windows.ts +58 -0
  11. package/packages/build-result/src/replace-file.ts +11 -0
  12. package/packages/build-result/src/store.ts +44 -13
  13. package/packages/build-result/src/types.ts +38 -8
  14. package/packages/build-result/src/writer.ts +12 -1
  15. package/packages/build-result-fs/README.md +23 -1
  16. package/packages/build-result-s3/README.md +5 -0
  17. package/packages/build-result-s3/src/repository.ts +19 -5
  18. package/packages/caption-fine/README.md +17 -2
  19. package/packages/caption-fine/src/manifest.ts +1 -1
  20. package/packages/caption-fine/src/render.ts +48 -36
  21. package/packages/caption-fine-studio/package.json +8 -3
  22. package/packages/caption-fine-studio/src/index.ts +10 -1
  23. package/packages/cli/README.md +25 -0
  24. package/packages/cli/src/arguments.ts +16 -11
  25. package/packages/cli/src/build-planning.ts +6 -1
  26. package/packages/cli/src/command.ts +1 -1
  27. package/packages/cli/src/commands/environment.ts +42 -13
  28. package/packages/cli/src/machine-view.ts +12 -3
  29. package/packages/cli/src/main.ts +4 -0
  30. package/packages/cli/src/output.ts +28 -12
  31. package/packages/cli/src/result-export.ts +22 -13
  32. package/packages/cli/src/run-file.ts +19 -8
  33. package/packages/comment-sticker-studio/src/index.ts +2 -1
  34. package/packages/compiler-node/src/compiler.ts +1 -0
  35. package/packages/estimate/README.md +56 -3
  36. package/packages/estimate/src/program.ts +7 -8
  37. package/packages/fonts-open/README.md +5 -0
  38. package/packages/fonts-open/package.json +5 -1
  39. package/packages/fonts-open/src/studio.ts +21 -0
  40. package/packages/media-track-studio/src/index.ts +7 -2
  41. package/packages/provider-whisperx-local/README.md +57 -0
  42. package/packages/ranking/src/render.ts +11 -9
  43. package/packages/ranking-studio/README.md +11 -0
  44. package/packages/ranking-studio/package.json +8 -3
  45. package/packages/ranking-studio/src/index.ts +11 -4
  46. package/packages/runtime/src/catalog.ts +2 -0
  47. package/packages/runtime-host-node/src/index.ts +1 -0
  48. package/packages/runtime-local/README.md +19 -0
  49. package/packages/runtime-local/src/programs.ts +39 -24
  50. package/packages/runtime-local/src/runtime.ts +2 -0
  51. package/packages/script/README.md +22 -0
  52. package/packages/script/src/edit.ts +123 -73
  53. package/packages/script/src/lexical.ts +40 -6
  54. package/packages/script/src/parser.ts +36 -8
  55. package/packages/script/src/types.ts +4 -0
  56. package/packages/seedance-kits/README.md +3 -1
  57. package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
  58. package/packages/speech-alignment/src/align.ts +45 -21
  59. package/packages/studio/INSPECTOR.md +104 -0
  60. package/packages/studio/README.md +65 -4
  61. package/packages/studio/src/build-library.ts +99 -84
  62. package/packages/studio/src/library-media.ts +19 -0
  63. package/packages/studio/src/parameter-values.ts +41 -1
  64. package/packages/studio/src/parameters.ts +20 -21
  65. package/packages/studio/src/server.ts +58 -30
  66. package/packages/studio/src/shared.ts +20 -6
  67. package/packages/studio/src/studio-registry.ts +22 -0
  68. package/packages/studio/src/style.css +135 -144
  69. package/packages/studio/src/temporal-edit.ts +131 -0
  70. package/packages/studio/src/ui/artifact-name.ts +80 -0
  71. package/packages/studio/src/ui/artifact-preview.ts +99 -0
  72. package/packages/studio/src/ui/icons.ts +5 -1
  73. package/packages/studio/src/ui/library.ts +373 -192
  74. package/packages/studio/src/ui/main.ts +153 -31
  75. package/packages/studio/src/ui/material-preview.ts +1 -1
  76. package/packages/studio/src/ui/overlay.ts +21 -20
  77. package/packages/studio/src/ui/sidebar-panel.ts +31 -0
  78. package/packages/studio/src/ui/stage.ts +124 -8
  79. package/packages/studio/src/ui/timeline.ts +16 -179
  80. package/packages/studio/start.ts +15 -3
  81. package/packages/studio-adapter/README.md +21 -0
  82. package/packages/studio-adapter/src/index.ts +26 -1
  83. package/packages/temporal-markup/EDITING.md +183 -0
  84. package/packages/temporal-markup/README.md +4 -0
  85. package/packages/typography-track-studio/package.json +8 -3
  86. package/packages/typography-track-studio/src/index.ts +5 -1
  87. package/packages/video-cli/README.md +12 -3
  88. package/packages/video-cli/src/creation.ts +11 -5
  89. package/packages/whisperx/README.md +7 -0
  90. package/packages/workspace/src/index.ts +2 -0
  91. package/packages/workspace-fs-node/README.md +4 -1
  92. package/packages/workspace-fs-node/src/workspace.ts +3 -1
@@ -1,3 +1,4 @@
1
+ import { assertOrderedBuildId } from "@hypit/protocol";
1
2
  import type {
2
3
  BlobRef,
3
4
  BuildState,
@@ -9,12 +10,19 @@ import type {
9
10
  export type BuildResultOutcome = "complete" | "failed" | "cancelled";
10
11
 
11
12
  export type BuildResultFileRef = {
13
+ readonly size: number;
14
+ readonly mediaType: string;
15
+ } & ({
12
16
  readonly kind: "build-file";
17
+ /** Absent for a file owned by the containing Result; explicit when reused elsewhere. */
18
+ readonly build?: string;
13
19
  /** Forward-only path relative to the owning Build's result directory. */
14
20
  readonly path: string;
15
- readonly size: number;
16
- readonly mediaType: string;
17
- };
21
+ } | {
22
+ readonly kind: "external-file";
23
+ /** Explicit address supplied by the Workspace; remains a live external dependency. */
24
+ readonly uri: string;
25
+ });
18
26
 
19
27
  /** A normalized half-open byte range within one Result file. */
20
28
  export type BuildResultFileRange = {
@@ -38,7 +46,7 @@ export type BuildResultResourceBinding = {
38
46
 
39
47
  /**
40
48
  * Durable encoding of one Composite value. Domain data stays ordinary canonical data while
41
- * Result-local Resource addresses live beside it, so reserved-looking domain objects cannot be
49
+ * Resource references live beside it, so reserved-looking domain objects cannot be
42
50
  * mistaken for persistence metadata.
43
51
  */
44
52
  export type BuildResultValueDocument = {
@@ -77,12 +85,18 @@ export function assertBuildResultFileRef(value: unknown, subject: string): asser
77
85
  throw new Error(`${subject} is not a Build file reference`);
78
86
  }
79
87
  const item = value as Readonly<Record<string, unknown>>;
80
- if (item.kind !== "build-file"
88
+ if ((item.kind !== "build-file" && item.kind !== "external-file")
81
89
  || typeof item.size !== "number" || !Number.isSafeInteger(item.size) || item.size < 0
82
90
  || typeof item.mediaType !== "string" || item.mediaType.length === 0) {
83
91
  throw new Error(`${subject} is not a valid Build file reference`);
84
92
  }
85
- assertBuildResultPath(item.path, `${subject}.path`);
93
+ if (item.kind === "external-file") {
94
+ if (typeof item.uri !== "string" || item.uri.length === 0) throw new Error(`${subject}.uri is missing`);
95
+ new URL(item.uri);
96
+ } else {
97
+ assertBuildResultPath(item.path, `${subject}.path`);
98
+ if (item.build !== undefined) assertOrderedBuildId(item.build as string);
99
+ }
86
100
  }
87
101
 
88
102
  function valueAtPath(value: CanonicalValue, path: BuildResultValuePath, subject: string): CanonicalValue {
@@ -150,6 +164,8 @@ export type BuildResultOutputValue =
150
164
  | { readonly kind: "value"; readonly path: string };
151
165
 
152
166
  export type BuildResultOutput = {
167
+ /** Author-provided display name; the Output identifier remains unchanged. */
168
+ readonly displayName?: string;
153
169
  readonly type: TypeRef;
154
170
  readonly value: BuildResultOutputValue;
155
171
  };
@@ -198,6 +214,8 @@ export type FinishedBuildResultManifest = BuildResultManifest & {
198
214
  };
199
215
 
200
216
  export type BuildResultPublishedOutput = {
217
+ /** Author-provided display name; the Output identifier remains unchanged. */
218
+ readonly displayName?: string;
201
219
  readonly name: string;
202
220
  readonly output: string;
203
221
  };
@@ -217,6 +235,8 @@ export type BuildResultSeed = {
217
235
  readonly targets: readonly string[];
218
236
  readonly publishedOutputs: readonly BuildResultPublishedOutput[];
219
237
  readonly forwards?: readonly BuildResultForward[];
238
+ /** Known file locations for admitted Resource identities, including nested uses. */
239
+ readonly resourceReferences?: Readonly<Record<string, BuildResultFileRef>>;
220
240
  };
221
241
 
222
242
  /** Validate the one-name-per-Output public Result surface before storage is touched. */
@@ -224,6 +244,11 @@ export function assertBuildResultSeed(seed: BuildResultSeed): void {
224
244
  if (seed.title !== undefined && seed.title.trim().length === 0) {
225
245
  throw new Error("Build Result title must not be empty");
226
246
  }
247
+ for (const [resource, file] of Object.entries(seed.resourceReferences ?? {})) {
248
+ if (resource.length === 0) throw new Error("Resource reference has no identity");
249
+ assertBuildResultFileRef(file, `Resource ${resource}`);
250
+ if (file.kind === "build-file" && file.build === undefined) throw new Error(`Resource ${resource} needs its owning Build`);
251
+ }
227
252
  const names = new Set<string>();
228
253
  const outputs = new Set<string>();
229
254
  for (const published of seed.publishedOutputs) {
@@ -275,6 +300,8 @@ export type BuildResultFinish = {
275
300
  };
276
301
 
277
302
  export type BuildResultPresentationUpdate = {
303
+ /** Exact public Output names; null removes an override. */
304
+ readonly outputDisplayNames?: Readonly<Record<string, string | null>>;
278
305
  /** `null` removes the current title; omission leaves it unchanged. */
279
306
  readonly title?: string | null;
280
307
  /** `null` removes the current note; omission leaves it unchanged. */
@@ -325,13 +352,14 @@ export type RepositoryBuildResultOutputDescription = {
325
352
 
326
353
  export type BuildResultWriter = {
327
354
  read(): Promise<BuildResultManifest>;
355
+ /** Publish newly accepted public Outputs; no storage writes when none are new. */
328
356
  sync(input: BuildResultSync): Promise<BuildResultManifest>;
329
357
  finish(input: BuildResultFinish): Promise<BuildResultManifest>;
330
358
  };
331
359
 
332
360
  /**
333
- * Project result history addressed only by Build id, Output name and Build-relative file path.
334
- * Filesystem paths, bucket keys and service URLs remain implementation details.
361
+ * Project result history addressed by Build id and Output name. File references carry their
362
+ * ownership or explicit external address; each repository supplies the corresponding byte access.
335
363
  */
336
364
  export type BuildResultRepository = {
337
365
  create(seed: BuildResultSeed): Promise<BuildResultWriter>;
@@ -346,6 +374,8 @@ export type BuildResultRepository = {
346
374
  /** Follow explicit Output forwarding and describe its terminal value without opening content. */
347
375
  describeOutput(build: string, output: string): Promise<RepositoryBuildResultOutputDescription | undefined>;
348
376
  resolve(build: string, output: string): Promise<RepositoryBuildResultOutput | undefined>;
377
+ /** Read current metadata for a file reference, including external dependencies. */
378
+ describeFile(build: string, file: BuildResultFileRef): Promise<BuildResultFileRef>;
349
379
  openFile(
350
380
  build: string,
351
381
  file: BuildResultFileRef,
@@ -13,8 +13,9 @@ import type {
13
13
 
14
14
  export type BuildResultWriterState = {
15
15
  readonly resources: Readonly<Record<string, string>>;
16
+ readonly resourceReferences?: Readonly<Record<string, BuildResultFileRef>>;
16
17
  readonly values: Readonly<Record<string, string>>;
17
- readonly publishedOutputs: readonly { readonly name: string; readonly output: string }[];
18
+ readonly publishedOutputs: readonly { readonly name: string; readonly output: string; readonly displayName?: string }[];
18
19
  readonly forwards: readonly { readonly output: string; readonly build: string; readonly sourceOutput: string }[];
19
20
  };
20
21
 
@@ -28,6 +29,8 @@ export type BuildResultWriteTarget = {
28
29
  };
29
30
 
30
31
  export type BuildResultSyncResult = {
32
+ /** Whether accepted public Outputs were added to the manifest. */
33
+ readonly changed: boolean;
31
34
  readonly manifest: BuildResultManifest;
32
35
  readonly writer: BuildResultWriterState;
33
36
  };
@@ -97,6 +100,8 @@ export async function syncBuildResultOutputs(input: {
97
100
  const outputs: Record<string, BuildResultOutput> = { ...input.manifest.outputs };
98
101
 
99
102
  const materialize = async (artifact: BlobRef): Promise<BuildResultFileRef> => {
103
+ const reference = input.writer.resourceReferences?.[artifact.resource];
104
+ if (reference !== undefined) return { ...reference, mediaType: artifact.mediaType };
100
105
  const identity = artifact.resource;
101
106
  assert(identity.length > 0, "Build resource has no instance identity");
102
107
  let path = resources.get(identity);
@@ -166,14 +171,20 @@ export async function syncBuildResultOutputs(input: {
166
171
  }).sort((left, right) => outputRank(left.record, left.forward) - outputRank(right.record, right.forward)
167
172
  || left.published.name.localeCompare(right.published.name));
168
173
 
174
+ if (ready.length === 0) {
175
+ return { changed: false, manifest: input.manifest, writer: input.writer };
176
+ }
177
+
169
178
  for (const { published, record, forward } of ready) {
170
179
  outputs[published.name] = {
180
+ ...(published.displayName === undefined ? {} : { displayName: published.displayName }),
171
181
  type: record.type,
172
182
  value: forward ?? await outputValue(record),
173
183
  };
174
184
  }
175
185
 
176
186
  return {
187
+ changed: true,
177
188
  manifest: { ...input.manifest, outputs },
178
189
  writer: {
179
190
  ...input.writer,
@@ -15,7 +15,7 @@ An explicit selection can move the same repository elsewhere:
15
15
  ```
16
16
 
17
17
  Relative paths are resolved from the project root. Results are grouped by the UTC date already encoded
18
- in their ordered Build ids, and each Build then owns one intact directory with its `result.json`, public
18
+ in their ordered Build ids. Each Build has a directory containing its `result.json`, new public
19
19
  Resource files and Composite Value Documents:
20
20
 
21
21
  ```text
@@ -27,3 +27,25 @@ project-wide history database. Browsing sorts the shallow date buckets and enume
27
27
  only inside the dates reached while filling the requested cursor page; only those Result manifests are
28
28
  opened. File reads support byte ranges. Active diagnosis checks the selected directory (or its nearest
29
29
  existing parent) for read/write access without creating a probe file.
30
+
31
+ Existing resources keep their addresses, including inside nested Composite values. External Workspace
32
+ files remain live references; reused Result files retain their original owning Build. See the shared
33
+ [Result value and reference model](../build-result/README.md) for ownership and export behavior.
34
+
35
+ ## Publication
36
+
37
+ Runtime execution state stays in the Runtime's store. Result synchronization publishes newly accepted
38
+ public Outputs: it saves new Resource files and Value Documents, then replaces `result.json`.
39
+ Internal graph progress and already published Outputs cause no Result writes. A private `.writer.json`
40
+ holds the file assignments needed while publishing and is removed at completion. The Runtime saves
41
+ all accepted public Outputs and the final outcome before removing active execution state, including
42
+ when the Build fails or is cancelled.
43
+
44
+ Files are written completely beside their destination before publication. POSIX systems use `rename`;
45
+ Windows uses `SetFileInformationByHandle(FileRenameInfoEx)` with replacement and POSIX semantics through
46
+ Koffi. Existing readers keep their old file, while subsequent opens see the new one. This platform code
47
+ belongs to the filesystem implementation; the Result model and other repository adapters do not
48
+ depend on it. A filesystem that rejects this operation reports the Windows error without timed retries.
49
+
50
+ The Windows behavior is defined by Microsoft's
51
+ [FileRenameInformationEx documentation](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4217551b-d2c0-42cb-9dc1-69a716cf6d0c).
@@ -33,6 +33,11 @@ The repository exposes the same logical shape as the filesystem adapter:
33
33
  Filesystem and S3 use the same Record-to-Result encoder. The adapter only writes the selected
34
34
  relative document or byte path; it does not interpret domain values. A Value Document keeps canonical
35
35
  domain data separate from the paths of nested Resources.
36
+ Nested reuse retains each Resource's original Build and path. Workspace files remain explicit external
37
+ references rather than being uploaded merely because this repository uses S3. The shared
38
+ [Result value and reference model](../build-result/README.md) describes ownership and external access.
39
+ Synchronization uploads new public Outputs and then their updated manifest. Internal graph progress
40
+ and repeated synchronization of already published Outputs produce no object writes.
36
41
 
37
42
  Physical object prefixes use a reversible descending-time form derived from the ordered Build id, so
38
43
  S3 can return a newest-first delimiter page directly. This is only adapter key layout: callers still
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  BuildResultFileRef,
3
+ ExternalFileAccess,
3
4
  BuildResultFileRange,
4
5
  BuildResultFinish,
5
6
  BuildResultManifest,
@@ -22,6 +23,8 @@ import {
22
23
  encodeBuildResultManifest,
23
24
  normalizeBuildResultForwards,
24
25
  syncBuildResultOutputs,
26
+ currentFileReference,
27
+ localExternalFiles,
25
28
  } from "@hypit/build-result";
26
29
  import { assertOrderedBuildId, buildIdCreatedAt } from "@hypit/protocol";
27
30
 
@@ -31,6 +34,7 @@ import type { AwsBuildResultS3ClientOptions, BuildResultS3Client } from "./clien
31
34
  export type S3BuildResultRepositoryOptions = AwsBuildResultS3ClientOptions & {
32
35
  readonly prefix?: string;
33
36
  readonly client?: BuildResultS3Client;
37
+ readonly externalFiles?: ExternalFileAccess;
34
38
  };
35
39
 
36
40
  function assert(condition: unknown, message: string): asserts condition {
@@ -102,6 +106,7 @@ class S3BuildResultWriter implements BuildResultWriter {
102
106
  },
103
107
  },
104
108
  });
109
+ if (!updated.changed) return manifest;
105
110
  await this.#repository.writeWriter(this.#build, updated.writer);
106
111
  await this.#repository.writeManifest(this.#build, updated.manifest);
107
112
  return updated.manifest;
@@ -132,10 +137,12 @@ class S3BuildResultWriter implements BuildResultWriter {
132
137
  export class S3BuildResultRepository implements BuildResultRepository {
133
138
  readonly #client: BuildResultS3Client;
134
139
  readonly #prefix: string;
140
+ readonly #externalFiles: ExternalFileAccess;
135
141
 
136
142
  constructor(options: S3BuildResultRepositoryOptions) {
137
143
  assert(options.bucket.trim().length > 0, "S3 Build Result bucket must not be empty");
138
144
  this.#prefix = normalizePrefix(options.prefix);
145
+ this.#externalFiles = options.externalFiles ?? localExternalFiles;
139
146
  this.#client = options.client ?? new AwsBuildResultS3Client(options);
140
147
  }
141
148
 
@@ -194,6 +201,7 @@ export class S3BuildResultRepository implements BuildResultRepository {
194
201
  resources: {},
195
202
  values: {},
196
203
  publishedOutputs: seed.publishedOutputs,
204
+ ...(seed.resourceReferences === undefined ? {} : { resourceReferences: seed.resourceReferences }),
197
205
  forwards,
198
206
  });
199
207
  await this.writeManifest(seed.id, manifest);
@@ -285,8 +293,9 @@ export class S3BuildResultRepository implements BuildResultRepository {
285
293
  currentOutput = entry.value.output;
286
294
  continue;
287
295
  }
288
- return entry.value.kind === "build-file"
289
- ? { type: entry.type, kind: "resource", size: entry.value.size, mediaType: entry.value.mediaType }
296
+ const file = entry.value.kind === "external-file" ? await currentFileReference(entry.value, this.#externalFiles) : entry.value;
297
+ return file.kind === "build-file" || file.kind === "external-file"
298
+ ? { type: entry.type, kind: "resource", size: file.size, mediaType: file.mediaType }
290
299
  : entry.value.kind === "value"
291
300
  ? { type: entry.type, kind: "composite" }
292
301
  : { type: entry.type, kind: "scalar" };
@@ -324,29 +333,34 @@ export class S3BuildResultRepository implements BuildResultRepository {
324
333
  };
325
334
  }
326
335
  const terminalKind = (entry.value as { readonly kind?: unknown }).kind;
327
- assert(terminalKind === "build-file" || terminalKind === "inline",
336
+ assert(terminalKind === "build-file" || terminalKind === "external-file" || terminalKind === "inline",
328
337
  `Build ${currentBuild} Output ${currentOutput} has unsupported Result value kind ${String(terminalKind)}`);
329
338
  return {
330
339
  build: currentBuild,
331
340
  output: currentOutput,
332
341
  type: entry.type,
333
- value: entry.value,
342
+ value: entry.value.kind === "external-file" ? await currentFileReference(entry.value, this.#externalFiles) : entry.value,
334
343
  };
335
344
  }
336
345
  }
337
346
 
347
+ async describeFile(_build: string, file: BuildResultFileRef): Promise<BuildResultFileRef> {
348
+ return await currentFileReference(file, this.#externalFiles);
349
+ }
350
+
338
351
  async openFile(
339
352
  build: string,
340
353
  file: BuildResultFileRef,
341
354
  range?: BuildResultFileRange,
342
355
  ): Promise<AsyncIterable<Uint8Array> | undefined> {
356
+ if (file.kind === "external-file") return await this.#externalFiles.open(file.uri, range);
343
357
  if (range !== undefined) {
344
358
  assert(Number.isSafeInteger(range.start) && range.start >= 0, "Build Result file range start is invalid");
345
359
  assert(Number.isSafeInteger(range.endExclusive) && range.endExclusive > range.start,
346
360
  "Build Result file range end is invalid");
347
361
  assert(range.endExclusive <= file.size, "Build Result file range exceeds the declared file size");
348
362
  }
349
- return await this.#client.open(this.#key(build, file.path), range);
363
+ return await this.#client.open(this.#key(file.build ?? build, file.path), range);
350
364
  }
351
365
 
352
366
  /** Verify that the configured bucket/prefix can be listed without loading Result history. */
@@ -97,8 +97,23 @@ font fallbacks supply the required glyphs; the layout does not select a font by
97
97
  Both count limits are omitted above so the example flows by available width. For a compact
98
98
  single-line treatment, author coherent Cues and choose a font, size and width that fit those Cues.
99
99
  `karaoke: off` keeps a complete Cue steady; `current` and `trail` follow its timed units, which are
100
- normally individual characters in Chinese. The Hypit Skill's Caption craft page owns grouping and
101
- visual direction.
100
+ normally individual characters in Chinese.
101
+
102
+ | Effect | Recipe | Visible behavior |
103
+ | --- | --- | --- |
104
+ | Whole-character emphasis | `karaoke: current; karaoke-transition: step` | The complete current unit takes the active Paint at its start, returning to base Paint at its end. |
105
+ | Accumulating emphasis | `karaoke: trail; karaoke-transition: step` | Each complete unit takes the active Paint at its start and keeps it for the rest of the Cue. |
106
+ | Sweeping fill | `karaoke: trail; karaoke-transition: wipe` | Paint sweeps inside each unit's glyphs during that unit's own time window. |
107
+ | Spoken-unit appearance | `atom-reveal: on-start` | Each complete unit appears at its start. |
108
+ | Typing appearance | `atom-reveal: typewriter` | Whole graphemes appear in sequence within each unit, starting with its first grapheme at the unit's start. A normal Han unit appears as a complete character. |
109
+
110
+ `step` is the default Karaoke transition. Choose it for ordinary Chinese character highlighting;
111
+ `wipe` deliberately produces partial glyph fills. Cue width and duration do not supply a substitute
112
+ clock for either mode. `atom-reveal` controls text appearing, independently of Karaoke recoloring
113
+ already visible text. A Dual Text span shares one timing unit, so its display words activate together
114
+ under `step`; keep pronunciation markup scoped to the name or expression that needs it.
115
+
116
+ The Hypit Skill's Caption craft page owns grouping and visual direction.
102
117
 
103
118
  `font` accepts an exact face or ordered stack. A local file declared through `media:Font` can be the
104
119
  primary face or a `<caption-fine:Fallback font={...}/>` child, just like a bundled face. See
@@ -206,7 +206,7 @@ export const captionFineMarkupSurfaces = [
206
206
  { name: "karaoke", required: false, values: ["off", "current", "trail"], fallback: "off",
207
207
  summary: "Decides whether the active Paint marks only the Word being spoken or every Word up to it." },
208
208
  { name: "karaoke-transition", required: false, values: ["step", "wipe"], fallback: "step",
209
- summary: "Decides whether the karaoke Paint snaps at the Word boundary or sweeps across the glyphs." },
209
+ summary: "step activates each whole timed unit at its start (normally one Han character in Chinese); wipe sweeps inside that unit's glyphs during its own interval." },
210
210
  { name: "active-box", required: false, values: ["off", "current", "trail"], fallback: "off",
211
211
  summary: "Decides whether a highlight box sits behind the Word being spoken or behind every Word up to it." },
212
212
  { name: "active-box-continuity", required: false, values: ["isolated", "joined"], fallback: "isolated",
@@ -480,23 +480,6 @@ function karaokeWipeAnimation(
480
480
  ] };
481
481
  }
482
482
 
483
- function typewriterAnimation(
484
- text: string,
485
- parameters: FineCaptionParameters,
486
- startFrame: number,
487
- endFrame: number,
488
- durationFrames: number,
489
- ): VisualAnimation | undefined {
490
- if (parameters.motion.atomReveal !== "typewriter") return undefined;
491
- const graphemes = Math.max(1, [...new Intl.Segmenter("und", { granularity: "grapheme" }).segment(text)].length);
492
- const span = Math.max(1, endFrame - startFrame);
493
- const offsets = Array.from({ length: graphemes + 1 }, (_, index) => startFrame + Math.round(span * index / graphemes));
494
- return animationFrom(durationFrames, [...stepOffsets(startFrame), ...offsets], (frame) => {
495
- const progress = frame < startFrame ? 0 : Math.floor(clamp((frame - startFrame) / span, 0, 1) * graphemes) / graphemes;
496
- return [{ name: "clip-path", value: wipeClip(progress, parameters.layout.direction) }];
497
- });
498
- }
499
-
500
483
  function activeResponseAnimation(
501
484
  parameters: FineCaptionParameters,
502
485
  startFrame: number,
@@ -867,9 +850,7 @@ function cueElements(
867
850
  // the karaoke wipe keeps sitting on the letterforms it reveals.
868
851
  const gaps = wordGaps(surfaces);
869
852
  const wordGap = uniformGap(gaps);
870
- const atomText = joinSurfaces(surfaces, " ");
871
853
  const entryId = `${atomId}-entry`;
872
- const typewriterId = `${atomId}-typewriter`;
873
854
  const loopId = `${atomId}-loop`;
874
855
  const responseId = `${atomId}-response`;
875
856
  if (parameters.layout.maxWordsPerLine !== undefined
@@ -884,7 +865,6 @@ function cueElements(
884
865
  wordsOnLine = 0;
885
866
  }
886
867
  const entryAnimation = atomLifecycleAnimation(parameters, timing.start, timing.end, durationFrames);
887
- const writerAnimation = typewriterAnimation(atomText, parameters, timing.start, timing.end, durationFrames);
888
868
  const atomLoop = parameters.motion.loopTarget === "active-atom"
889
869
  ? loopAnimation(parameters, durationFrames, activationTiming) : undefined;
890
870
  const responseAnimation = activeResponseAnimation(
@@ -906,20 +886,9 @@ function cueElements(
906
886
  ],
907
887
  ...(entryAnimation === undefined ? {} : { animation: entryAnimation }),
908
888
  });
909
- push({
910
- id: typewriterId,
911
- parent: entryId,
912
- kind: "box",
913
- style: [
914
- { name: "display", value: "inline-flex" },
915
- { name: "min-width", value: "0" },
916
- { name: "transform-origin", value: parameters.layout.direction === "rtl" ? "right center" : "left center" },
917
- ],
918
- ...(writerAnimation === undefined ? {} : { animation: writerAnimation }),
919
- });
920
889
  push({
921
890
  id: loopId,
922
- parent: typewriterId,
891
+ parent: entryId,
923
892
  kind: "box",
924
893
  style: [
925
894
  { name: "display", value: "inline-flex" },
@@ -983,10 +952,53 @@ function cueElements(
983
952
  attributes: [{ name: "data-caption-active-box", value: "isolated" }],
984
953
  });
985
954
  }
955
+ const graphemes = parameters.motion.atomReveal === "typewriter"
956
+ ? surfaces.map((text) =>
957
+ [...new Intl.Segmenter("und", { granularity: "grapheme" }).segment(text)].map((part) => part.segment))
958
+ : [];
959
+ const graphemeCount = graphemes.reduce((sum, word) => sum + word.length, 0);
960
+ const pushText = (element: Omit<VisualTextElement, "order">, wordIndex: number): void => {
961
+ if (parameters.motion.atomReveal !== "typewriter") {
962
+ push(element);
963
+ return;
964
+ }
965
+ // Reveal complete glyphs, not fractions of a Word's width. Variable-width Latin letters,
966
+ // Han characters and combining sequences all retain their actual layout widths.
967
+ const isMargin = (name: string): boolean => name.startsWith("margin-");
968
+ push({
969
+ id: element.id,
970
+ ...(element.parent === undefined ? {} : { parent: element.parent }),
971
+ kind: "box",
972
+ style: [
973
+ { name: "display", value: "inline" },
974
+ { name: "min-width", value: "0" },
975
+ ...element.style.filter(({ name }) => isMargin(name) || name === "opacity"),
976
+ ],
977
+ ...(element.attributes === undefined ? {} : { attributes: element.attributes }),
978
+ });
979
+ const preceding = graphemes.slice(0, wordIndex).reduce((sum, word) => sum + word.length, 0);
980
+ const { attributes: _attributes, ...glyph } = element;
981
+ for (const [index, text] of (graphemes[wordIndex] ?? []).entries()) {
982
+ const revealFrame = timing.start + Math.floor(
983
+ (timing.end - timing.start) * (preceding + index) / Math.max(1, graphemeCount),
984
+ );
985
+ push({
986
+ ...glyph,
987
+ id: `${element.id}-grapheme-${index + 1}`,
988
+ parent: element.id,
989
+ text,
990
+ style: [
991
+ ...element.style.filter(({ name }) => !isMargin(name) && name !== "opacity"),
992
+ { name: "display", value: "inline" },
993
+ ],
994
+ animation: activationStepAnimation("trail", revealFrame, durationFrames, durationFrames),
995
+ });
996
+ }
997
+ };
986
998
  for (const [wordIndex, wordId] of atom.wordIds.entries()) {
987
999
  const text = wordText.get(wordId);
988
1000
  if (text === undefined) throw new Error(`Fine Caption Atom references unknown word ${wordId}`);
989
- push({
1001
+ pushText({
990
1002
  id: `${atomId}-base-${wordIndex + 1}`,
991
1003
  parent: atomId,
992
1004
  kind: "text",
@@ -997,7 +1009,7 @@ function cueElements(
997
1009
  ],
998
1010
  ...glyphPaintFields(parameters.basePaint),
999
1011
  fonts,
1000
- });
1012
+ }, wordIndex);
1001
1013
  }
1002
1014
  const addActivatedTextLayer = (
1003
1015
  suffix: string,
@@ -1026,7 +1038,7 @@ function cueElements(
1026
1038
  });
1027
1039
  for (const [wordIndex, wordId] of atom.wordIds.entries()) {
1028
1040
  const text = wordText.get(wordId)!;
1029
- push({
1041
+ pushText({
1030
1042
  id: `${activeId}-${wordIndex + 1}`,
1031
1043
  parent: activeId,
1032
1044
  kind: "text",
@@ -1041,7 +1053,7 @@ function cueElements(
1041
1053
  ...(kind === "glyph" ? glyphPaintFields(parameters.activePaint) : {}),
1042
1054
  fonts,
1043
1055
  attributes: [{ name: kind === "glyph" ? "data-caption-active-word" : "data-caption-underlined-word", value: wordId }],
1044
- });
1056
+ }, wordIndex);
1045
1057
  }
1046
1058
  };
1047
1059
  if (parameters.karaoke.mode !== "off") addActivatedTextLayer("active", parameters.karaoke.mode, "glyph");
@@ -4,12 +4,17 @@
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "private": true,
6
6
  "type": "module",
7
- "exports": { ".": "./src/index.ts" },
8
- "hypit": { "activation": "./src/activation.ts" },
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "hypit": {
11
+ "activation": "./src/activation.ts"
12
+ },
9
13
  "dependencies": {
10
14
  "@hypit/caption-fine": "workspace:*",
11
15
  "@hypit/composition": "workspace:*",
12
16
  "@hypit/narrative": "workspace:*",
13
- "@hypit/studio-adapter": "workspace:*"
17
+ "@hypit/studio-adapter": "workspace:*",
18
+ "@hypit/fonts-open": "workspace:*"
14
19
  }
15
20
  }
@@ -1,3 +1,4 @@
1
+ import { openFontStudioFields } from "@hypit/fonts-open/studio";
1
2
  import { captionFineMarkupSurfaces, captionFineModuleRef } from "@hypit/caption-fine";
2
3
  import type { FineCaptionSchedule } from "@hypit/caption-fine";
3
4
  import { compositionTypes } from "@hypit/composition";
@@ -12,6 +13,9 @@ import type {
12
13
  import { requiredReferencedValue, requiredSurfaceValue, textLayer } from "@hypit/studio-adapter";
13
14
 
14
15
  const styleSurface = captionFineMarkupSurfaces.find((surface) => surface.name === "style");
16
+
17
+ const fontInspector = openFontStudioFields("program");
18
+
15
19
  const recipeVocabulary = styleSurface?.vocabulary.attributes
16
20
  .find((attribute) => attribute.name === "recipe")?.recipe ?? [];
17
21
 
@@ -83,6 +87,8 @@ const captionColorProperties = new Set([
83
87
  "active-underline-color", "active-box-background", "active-box-border-color",
84
88
  ]);
85
89
  const captionTextProperties = new Set(["loop"]);
90
+ const captionPercentProperties = new Set(["x", "y", "width", "height", "opacity", "active-opacity", "active-scale"]);
91
+ const captionPixelProperties = new Set(["size", "letter-spacing", "word-gap", "radius", "stroke-width", "border-width", "shadow-blur", "glow-blur"]);
86
92
 
87
93
  export const captionFineInspectorFields: readonly StudioInspectorFieldDeclaration[] = recipeVocabulary.map((property) => {
88
94
  const placement = captionPlacement.get(property.name);
@@ -96,6 +102,8 @@ export const captionFineInspectorFields: readonly StudioInspectorFieldDeclaratio
96
102
  control: options !== undefined ? "select" : captionColorProperties.has(property.name) ? "color"
97
103
  : captionTextProperties.has(property.name) ? "text" : "number",
98
104
  ...(options === undefined ? {} : { options }),
105
+ ...(captionPercentProperties.has(property.name) ? { unit: "%", number: { scale: 100, step: 1 } } : {}),
106
+ ...(captionPixelProperties.has(property.name) ? { unit: "px", number: { step: 1 } } : {}),
99
107
  };
100
108
  });
101
109
 
@@ -153,10 +161,11 @@ export const captionFineStudioTrackCompanions: readonly StudioTrackCompanion[] =
153
161
  bindings: [
154
162
  {
155
163
  name: "program",
164
+ referenced: [fontInspector.binding],
156
165
  recipe: { through: ["recipe"], bindings: recipeVocabulary.map(({ name }) => ({ name })) },
157
166
  },
158
167
  ],
159
- inspector: captionFineInspectorFields,
168
+ inspector: [...fontInspector.fields, ...captionFineInspectorFields],
160
169
  requiredValues: ["schedule"], project: projectCaption,
161
170
  lane: { heightPx: 48 },
162
171
  },
@@ -15,6 +15,31 @@ Human output is compact by default. `--json` emits a command-specific view rathe
15
15
  Runtime or Repository objects. `--verbose` adds operational detail; it never turns the command into
16
16
  an internal state dump.
17
17
 
18
+ ## Project and Runtime context
19
+
20
+ `--workspace` explicitly selects the project. Otherwise the nearest `package.json` above the command's
21
+ current directory establishes its root; with none, the current directory is the root. Source and Run
22
+ arguments locate files within that context. Relative command-line paths are resolved from the current
23
+ directory, including when `--workspace` is supplied.
24
+
25
+ Runtime-aware commands use an explicit `--runtime` for that invocation, or read exactly the resolved
26
+ project's `.hypit/runtime` pointer. `runtime use` writes the pointer; a Profile filename by itself does
27
+ not select it. Project selection is also available on `paths`, `doctor`, execution status/control,
28
+ Runtime operations, `programs` and `auth`. Machine-wide `packages` operations have no project selector.
29
+
30
+ `paths` shows the effective locations and whether the Profile came from a command argument, a project
31
+ selection or neither. Its JSON fields `profileSource` and `selectionFile` expose that distinction; the
32
+ selection-file location is shown even when no selection exists. `doctor` states whether it checked
33
+ only project Results or also a selected Runtime. An unselected Runtime is not a completed environment
34
+ diagnosis. Result repository selection and project-package resolution remain independent of Runtime.
35
+
36
+ For a command invoked outside the project, name both the project and its input explicitly:
37
+
38
+ ```bash
39
+ hypit paths --workspace /path/to/video-project
40
+ hypit plan /path/to/video-project/build.svrun --workspace /path/to/video-project
41
+ ```
42
+
18
43
  `plan` lists every Endpoint request in the frozen Build graph. Exact-model packages expose their own
19
44
  port tables and request-assembly edges, so the CLI can show authored prompt, duration and generation
20
45
  settings without searching arbitrary records for a request-shaped object. When an input file will be