@hypit/hypit 0.1.2 → 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.
- package/README.md +1 -1
- package/dist/public/studio-adapter.d.ts +31 -2
- package/package.json +4 -1
- package/packages/audio-track-studio/src/index.ts +7 -4
- package/packages/build-result/README.md +58 -0
- package/packages/build-result/package.json +2 -1
- package/packages/build-result/src/decode.ts +10 -8
- package/packages/build-result/src/file-reference.ts +42 -0
- package/packages/build-result/src/index.ts +3 -0
- package/packages/build-result/src/replace-file-windows.ts +58 -0
- package/packages/build-result/src/replace-file.ts +11 -0
- package/packages/build-result/src/store.ts +44 -13
- package/packages/build-result/src/types.ts +38 -8
- package/packages/build-result/src/writer.ts +12 -1
- package/packages/build-result-fs/README.md +23 -1
- package/packages/build-result-s3/README.md +5 -0
- package/packages/build-result-s3/src/repository.ts +19 -5
- package/packages/caption-fine/README.md +17 -2
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +48 -36
- package/packages/caption-fine-studio/package.json +8 -3
- package/packages/caption-fine-studio/src/index.ts +10 -1
- package/packages/cli/README.md +25 -0
- package/packages/cli/src/arguments.ts +16 -11
- package/packages/cli/src/build-planning.ts +6 -1
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +42 -13
- package/packages/cli/src/machine-view.ts +12 -3
- package/packages/cli/src/main.ts +4 -0
- package/packages/cli/src/output.ts +28 -12
- package/packages/cli/src/result-export.ts +22 -13
- package/packages/cli/src/run-file.ts +19 -8
- package/packages/comment-sticker-studio/src/index.ts +2 -1
- package/packages/compiler-node/src/compiler.ts +1 -0
- package/packages/estimate/README.md +56 -3
- package/packages/estimate/src/program.ts +7 -8
- package/packages/fonts-open/README.md +5 -0
- package/packages/fonts-open/package.json +5 -1
- package/packages/fonts-open/src/studio.ts +21 -0
- package/packages/gpt-image-kits/README.md +17 -12
- package/packages/gpt-image-kits/kits/phone-ugc-v1.svs +8 -8
- package/packages/media-track-studio/src/index.ts +7 -2
- package/packages/provider-whisperx-local/README.md +57 -0
- package/packages/ranking/src/render.ts +11 -9
- package/packages/ranking-studio/README.md +11 -0
- package/packages/ranking-studio/package.json +8 -3
- package/packages/ranking-studio/src/index.ts +11 -4
- package/packages/runtime/src/catalog.ts +2 -0
- package/packages/runtime-host-node/src/index.ts +1 -0
- package/packages/runtime-local/README.md +19 -0
- package/packages/runtime-local/src/programs.ts +39 -24
- package/packages/runtime-local/src/runtime.ts +2 -0
- package/packages/script/README.md +22 -0
- package/packages/script/src/edit.ts +123 -73
- package/packages/script/src/lexical.ts +40 -6
- package/packages/script/src/parser.ts +36 -8
- package/packages/script/src/types.ts +4 -0
- package/packages/seedance-kits/README.md +3 -1
- package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
- package/packages/speech-alignment/src/align.ts +45 -21
- package/packages/studio/INSPECTOR.md +104 -0
- package/packages/studio/README.md +65 -4
- package/packages/studio/src/build-library.ts +99 -84
- package/packages/studio/src/library-media.ts +19 -0
- package/packages/studio/src/parameter-values.ts +41 -1
- package/packages/studio/src/parameters.ts +20 -21
- package/packages/studio/src/server.ts +58 -30
- package/packages/studio/src/shared.ts +20 -6
- package/packages/studio/src/studio-registry.ts +22 -0
- package/packages/studio/src/style.css +135 -144
- package/packages/studio/src/temporal-edit.ts +131 -0
- package/packages/studio/src/ui/artifact-name.ts +80 -0
- package/packages/studio/src/ui/artifact-preview.ts +99 -0
- package/packages/studio/src/ui/icons.ts +5 -1
- package/packages/studio/src/ui/library.ts +373 -192
- package/packages/studio/src/ui/main.ts +153 -31
- package/packages/studio/src/ui/material-preview.ts +1 -1
- package/packages/studio/src/ui/overlay.ts +21 -20
- package/packages/studio/src/ui/sidebar-panel.ts +31 -0
- package/packages/studio/src/ui/stage.ts +124 -8
- package/packages/studio/src/ui/timeline.ts +16 -179
- package/packages/studio/start.ts +15 -3
- package/packages/studio-adapter/README.md +21 -0
- package/packages/studio-adapter/src/index.ts +26 -1
- package/packages/temporal-markup/EDITING.md +183 -0
- package/packages/temporal-markup/README.md +4 -0
- package/packages/typography-track-studio/package.json +8 -3
- package/packages/typography-track-studio/src/index.ts +5 -1
- package/packages/video-cli/README.md +12 -3
- package/packages/video-cli/src/creation.ts +11 -5
- package/packages/whisperx/README.md +7 -0
- package/packages/workspace/src/index.ts +2 -0
- package/packages/workspace-fs-node/README.md +4 -1
- 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
|
-
|
|
16
|
-
readonly
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
334
|
-
*
|
|
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
|
|
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
|
-
|
|
289
|
-
|
|
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.
|
|
101
|
-
|
|
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: "
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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": {
|
|
8
|
-
|
|
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
|
},
|
package/packages/cli/README.md
CHANGED
|
@@ -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
|