@hypit/hypit 0.1.5 → 0.1.6
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/examples/semantic-composition/README.md +2 -1
- package/examples/semantic-composition/chat.svml +1 -1
- package/examples/semantic-composition/hypit.runtime.json +1 -1
- package/package.json +4 -1
- package/packages/caption-fine/README.md +7 -0
- package/packages/cli/src/view.ts +4 -3
- package/packages/film/src/surface.ts +4 -1
- package/packages/runtime-local/README.md +17 -0
- package/packages/script/README.md +15 -3
- package/packages/script/src/manifest.ts +2 -1
- package/packages/script/src/parser.ts +45 -6
- package/packages/studio/LOCALIZATION.md +98 -0
- package/packages/studio/README.md +13 -0
- package/packages/studio/locales/en.json +197 -0
- package/packages/studio/locales/zh-CN.json +191 -0
- package/packages/studio/src/localization-node.ts +46 -0
- package/packages/studio/src/localization.ts +85 -0
- package/packages/studio/src/style.css +15 -3
- package/packages/studio/src/ui/artifact-name.ts +8 -6
- package/packages/studio/src/ui/artifact-preview.ts +5 -4
- package/packages/studio/src/ui/code.ts +17 -16
- package/packages/studio/src/ui/comments.ts +26 -25
- package/packages/studio/src/ui/dropdown.ts +68 -0
- package/packages/studio/src/ui/i18n.ts +109 -0
- package/packages/studio/src/ui/icons.ts +1 -0
- package/packages/studio/src/ui/library.ts +76 -65
- package/packages/studio/src/ui/main.ts +84 -131
- package/packages/studio/src/ui/scrub-preview.ts +8 -6
- package/packages/studio/src/ui/stage.ts +28 -27
- package/packages/studio/src/ui/timeline.ts +17 -14
- package/packages/studio/start.ts +39 -22
|
@@ -28,7 +28,8 @@ node bin/hypit.mjs check examples/semantic-composition/chat.svml --workspace exa
|
|
|
28
28
|
node bin/hypit.mjs build examples/semantic-composition/chat.svrun --workspace examples/semantic-composition --runtime examples/semantic-composition/hypit.runtime.json --follow
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The Profile selects local HyperFrames and FFmpeg.
|
|
31
|
+
The Profile selects local HyperFrames and FFmpeg. Its execution data lives in `.hypit/runtimes/local`;
|
|
32
|
+
`.hypit/runtime` is reserved for the project's Profile-selection file. Use the machine's prepared browser/media tools;
|
|
32
33
|
there is no hosted generation account in this example. The final Output is `final.video`. The same
|
|
33
34
|
Run can be opened in Studio, where its component lane and physical clock work without a Script lane.
|
|
34
35
|
For an independent project, install `@hypit/hypit`, copy the component into `packages/`, replace its workspace
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<import as="style" source="./chat.svs"/>
|
|
10
10
|
|
|
11
11
|
<time:Clock id="animation-clock" frame-rate="30"/>
|
|
12
|
-
<time:Timeline id="animation" clock={animation-clock} end="8s"/>
|
|
12
|
+
<time:Timeline id="animation" clock={animation-clock} end="8s"/>
|
|
13
13
|
<spatial:Canvas id="canvas" width="540" height="960"/>
|
|
14
14
|
<fonts:Stack id="font" family="inter" weight="600" style="normal"/>
|
|
15
15
|
<chat:Scene id="conversation" timeline={animation.timeline} canvas={canvas} font={font}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": "hypit.runtime-local@1",
|
|
3
|
-
"dataRoot": ".hypit/
|
|
3
|
+
"dataRoot": ".hypit/runtimes/local",
|
|
4
4
|
"endpoints": {
|
|
5
5
|
"media.local": { "use": "@hypit/provider-media-local", "config": { "defaultConcurrency": 2 } },
|
|
6
6
|
"hyperframes.local": { "use": "@hypit/provider-hyperframes-local", "config": { "workers": 2, "defaultConcurrency": 1 } }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypit/hypit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"homepage": "https://hypit.ai",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -125,6 +125,8 @@
|
|
|
125
125
|
"packages/*/README.md",
|
|
126
126
|
"packages/temporal-markup/EDITING.md",
|
|
127
127
|
"packages/studio/INSPECTOR.md",
|
|
128
|
+
"packages/studio/LOCALIZATION.md",
|
|
129
|
+
"packages/studio/locales/*.json",
|
|
128
130
|
"services/image-opencv/pyproject.toml",
|
|
129
131
|
"services/image-opencv/package.json",
|
|
130
132
|
"services/image-opencv/uv.lock",
|
|
@@ -172,6 +174,7 @@
|
|
|
172
174
|
"docs:build": "vitepress build docs",
|
|
173
175
|
"docs:preview": "vitepress preview docs",
|
|
174
176
|
"test": "node test/run.mjs",
|
|
177
|
+
"test:runtime-scale": "node test/run.mjs runtime-scale",
|
|
175
178
|
"test:whisperx-service": "uv run --project services/whisperx --frozen python -m unittest discover -s services/whisperx/tests -p \"test_*.py\"",
|
|
176
179
|
"test:image-opencv": "node test/run.mjs image-opencv",
|
|
177
180
|
"hypit": "node bin/hypit.mjs",
|
|
@@ -87,6 +87,13 @@ individual Han characters as Display Words; punctuation stays with its display w
|
|
|
87
87
|
those units for timing and active Paint, while authored Cues remain complete reading phrases.
|
|
88
88
|
Dual Text retains its complete alignment unit even when it displays or speaks several words.
|
|
89
89
|
|
|
90
|
+
`<组件化|>` authors the same unit as `<组件化|组件化>` without repeating the speech. Fine's `step`
|
|
91
|
+
karaoke, active underline and active box treat that unit together using its first-to-last speech
|
|
92
|
+
interval. Ordinary Chinese characters around it retain their own activation times. This is an
|
|
93
|
+
explicit authored group, not a renderer-selected segmentation, and does not introduce a new Cue.
|
|
94
|
+
The underlying character anchors remain available to other Timeline consumers. `wipe` and
|
|
95
|
+
`typewriter` still animate inside the complete unit over its interval when deliberately selected.
|
|
96
|
+
|
|
90
97
|
`word-gap` applies between Latin words and at Chinese/Latin boundaries. Adjacent Han characters and
|
|
91
98
|
full-width punctuation carry no extra word gap. `letter-spacing` controls glyph tracking. Exact
|
|
92
99
|
font fallbacks supply the required glyphs; the layout does not select a font by language.
|
package/packages/cli/src/view.ts
CHANGED
|
@@ -164,6 +164,8 @@ export function buildStatusView(options: {
|
|
|
164
164
|
? "unavailable" as const
|
|
165
165
|
: options.result?.outcome ?? (options.result === undefined ? "missing" as const : "open" as const);
|
|
166
166
|
const issue = options.runtime?.issue;
|
|
167
|
+
const operations = (options.runtime?.operations ?? options.result?.operations ?? [])
|
|
168
|
+
.filter((item) => options.verbose || item.status !== "completed");
|
|
167
169
|
return {
|
|
168
170
|
id: options.id,
|
|
169
171
|
...((options.runtime?.targets ?? options.result?.targets) === undefined ? {} : {
|
|
@@ -197,9 +199,8 @@ export function buildStatusView(options: {
|
|
|
197
199
|
}
|
|
198
200
|
: { message: options.resultReadError! },
|
|
199
201
|
}),
|
|
200
|
-
...(
|
|
201
|
-
operations: groupOperationViews(
|
|
202
|
-
.filter((item) => item.status !== "completed")
|
|
202
|
+
...(operations.length > 0 ? {
|
|
203
|
+
operations: groupOperationViews(operations
|
|
203
204
|
.map((item) => ({
|
|
204
205
|
...(!options.verbose ? {} : "operation" in item ? { id: item.operation } : item.id === undefined ? {} : { id: item.id }),
|
|
205
206
|
...(item.receipt === undefined || (!options.verbose && item.failure === undefined) ? {} : { receipt: item.receipt }),
|
|
@@ -117,7 +117,10 @@ export const decodeFilmSurface: StructuredSurfaceHandler = ({ element, resolveRe
|
|
|
117
117
|
const name = `track-${index + 1}`;
|
|
118
118
|
return { name, kind: trackKind(source), source };
|
|
119
119
|
});
|
|
120
|
-
|
|
120
|
+
const sourceKeys = tracks.map(({ source: { ref } }) => ref.kind === "record"
|
|
121
|
+
? `record\u0000${ref.id}`
|
|
122
|
+
: `component-output\u0000${ref.component}\u0000${ref.output}`);
|
|
123
|
+
if (new Set(sourceKeys).size !== tracks.length) {
|
|
121
124
|
throw new Error(`${element.name} cannot include the same Track more than once`);
|
|
122
125
|
}
|
|
123
126
|
const fragment = createFilmAssemblyFragment({ name: "@hypit/film/surface-assembly@1", tracks });
|
|
@@ -122,6 +122,12 @@ for a submission that never became active and therefore has no Result to save.
|
|
|
122
122
|
Source imports select author packages. Runtime Profile entries select only code allowed to access files,
|
|
123
123
|
credentials, processes or networks. Installing a package changes neither selection.
|
|
124
124
|
|
|
125
|
+
Endpoint scoping happens before activation when explicit instance IDs are supplied, or when every
|
|
126
|
+
requested capability has a binding. Otherwise discovery loads the Profile's Endpoint packages to
|
|
127
|
+
find eligible implementations. Thus unused service readiness is not required, but an uninstalled
|
|
128
|
+
declared package can still prevent unbound discovery. `scopedProfile` owns this distinction; package
|
|
129
|
+
names are not used to guess which capabilities they supply.
|
|
130
|
+
|
|
125
131
|
The lifecycle commands have deliberately narrow meanings:
|
|
126
132
|
|
|
127
133
|
- `hypit runtime init` writes and selects the Distribution's starter Profile; it performs no setup or
|
|
@@ -224,3 +230,14 @@ Managed Programs have independent lifetimes. A healthy WhisperX service keeps it
|
|
|
224
230
|
Distribution changes and shell environment changes still concern the coordinator's bootstrap process.
|
|
225
231
|
Inspect active work before restarting it. Environment-backed credentials use that inherited process
|
|
226
232
|
environment; external writable Credential Stores resolve through their own implementation.
|
|
233
|
+
|
|
234
|
+
### Concurrency checks
|
|
235
|
+
|
|
236
|
+
`pnpm test` exercises 24 Builds with an eight-task remote pool and with all 24 submissions pending
|
|
237
|
+
at once. Both cases verify local progress, per-Build module isolation, one submission per Build,
|
|
238
|
+
capacity release and every completed Result. They use the production execution process and SQLite.
|
|
239
|
+
|
|
240
|
+
`pnpm test:runtime-scale` runs the same checks with 1000 simultaneous remote submissions, on its own.
|
|
241
|
+
This is a load experiment rather than part of every package regression. It reports fixture preparation,
|
|
242
|
+
local progress, completion, executor RSS and cleanup separately; total test time also includes creating
|
|
243
|
+
and removing the temporary Result repositories. Neither suite uses a completion-time performance target.
|
|
@@ -64,7 +64,7 @@ CaptionDocument contracts.
|
|
|
64
64
|
- **Role Cue**: a speaker turn, written as a bare tag such as `<ALICE>` inside a Segment. The next
|
|
65
65
|
Role Cue or the Segment close ends that turn.
|
|
66
66
|
- **Dual Text**: one authored speech span with separate display and spoken projections, written
|
|
67
|
-
`<display text | spoken text>`.
|
|
67
|
+
`<display text | spoken text>`. In `<display text|>`, omitted speech inherits the displayed prose.
|
|
68
68
|
- **Selection marker**: a named semantic range, written `@name ... @/name`.
|
|
69
69
|
- **Moment marker**: a named semantic point, written `@name!`.
|
|
70
70
|
- **CaptionDocument**: the Script-owned caption truth; it contains **Display Words**,
|
|
@@ -73,11 +73,23 @@ CaptionDocument contracts.
|
|
|
73
73
|
`really{emphasis,importance=2,tone=warm}`. Values may be strings, finite numbers or booleans. It
|
|
74
74
|
becomes `CaptionDisplayWord.attributes`; it is not a Selection and does not carry timing.
|
|
75
75
|
|
|
76
|
-
Within Dual Text, an unescaped `@`
|
|
77
|
-
`\@` if an at-sign must be shown. An empty display side, such as `< | spoken words>`, keeps the speech
|
|
76
|
+
Within Dual Text, an unescaped `@` belongs to the source of spoken text: the right side when supplied,
|
|
77
|
+
or the shared left side in `<display text|>`. Write `\@` if an at-sign must be shown. An empty display side, such as `< | spoken words>`, keeps the speech
|
|
78
78
|
tokens and omits them from Caption. `||` is an authored Caption Cue Break and must occur between
|
|
79
79
|
complete Alignment Units.
|
|
80
80
|
|
|
81
|
+
`<组件化|>` is shorthand for `<组件化|组件化>`, using the same exported Caption Alignment Unit.
|
|
82
|
+
It groups the displayed expression without merging its individual speech Tokens or time anchors.
|
|
83
|
+
For example, `把<动效|><组件化|>。|| 之后<直接复用|>。` authors groups inside two Cues.
|
|
84
|
+
Use groups where the expression should be treated together; ordinary prose needs no extra markup.
|
|
85
|
+
Caption Styles choose whether to highlight, reveal or keep the text steady.
|
|
86
|
+
|
|
87
|
+
In a shared side, markers and display attributes do not enter either text projection. Its speech
|
|
88
|
+
Tokens retain offsets into the actually written left-hand text, so Studio can move an anchor inside
|
|
89
|
+
`<组@beat!件化|>` without expanding the shorthand. Attributes still annotate the preceding display
|
|
90
|
+
word, not the whole group. Whitespace-only speech is omitted; a group with no spoken text on either
|
|
91
|
+
side is invalid. This adds no new public value type or protocol version.
|
|
92
|
+
|
|
81
93
|
## Segments, turns and Cues are different boundaries
|
|
82
94
|
|
|
83
95
|
A Segment names a structural production passage. It can contain several Role turns and be performed
|
|
@@ -64,7 +64,8 @@ export const scriptMarkupSurfaces = [
|
|
|
64
64
|
"A Script requires at least one Segment, and natural-language text is refused outside a Segment.",
|
|
65
65
|
"A Segment is opened by its own lower-case name and closed by that exact name, or written self-closing as `<pause/>`; the name is the Segment id, must be unique within the Script, and `script` is reserved. Segments do not nest.",
|
|
66
66
|
"A Role Cue such as `<HOST>` is a bare tag inside a Segment with no close; its turn runs until the next Cue or the end of the Segment, and a Cue may not follow unowned speech in the same Segment. Role state resets when the Segment closes.",
|
|
67
|
-
"Dual Text is written `<display | speech>`: the left side reaches
|
|
67
|
+
"Dual Text is written `<display | speech>`: the left side reaches Caption and the right side reaches dialogue and speech. `<display|>` inherits speech from the displayed prose and forms the same complete alignment unit; its word times remain individual. `<|speech>` speaks without displaying. Both sides empty is invalid.",
|
|
68
|
+
"Inside Dual Text, semantic markers belong to the source of spoken text: the explicit right side, or the shared left side when speech is omitted. Display attributes remain visual metadata and never enter spoken text.",
|
|
68
69
|
"A flat token attribute follows a complete display token as `{name}` or `{name=value}`; multiple attributes use one comma-separated block. Attributes do not nest, do not carry timing, and never split a Dual Alignment Unit.",
|
|
69
70
|
"Selection and Moment markers are zero-width, share one name namespace, and may not split a speech token:",
|
|
70
71
|
[
|
|
@@ -499,16 +499,22 @@ export function parseScript(
|
|
|
499
499
|
const consumeSpeechSide = (
|
|
500
500
|
raw: string,
|
|
501
501
|
absoluteStart: number,
|
|
502
|
-
caption: string,
|
|
502
|
+
caption: string | undefined,
|
|
503
503
|
marks: ParsedCaptionRegion["marks"] = [],
|
|
504
504
|
): void => {
|
|
505
505
|
const startToken = tokens.length;
|
|
506
|
+
// An omitted speech side shares the written display source. Keep its original offsets:
|
|
507
|
+
// marker edits target these words, not a synthetic copy beyond the pipe.
|
|
508
|
+
const shared = caption === undefined;
|
|
509
|
+
const spokenParts: string[] = [];
|
|
510
|
+
const sharedMarks: Array<ParsedCaptionRegion["marks"][number]> = [];
|
|
506
511
|
let partStart = 0;
|
|
507
512
|
let index = 0;
|
|
508
513
|
let emittedCaption = false;
|
|
509
514
|
const addLiteral = (part: string, start: number): void => {
|
|
510
515
|
for (const piece of literalPieces(part, start, true)) {
|
|
511
|
-
|
|
516
|
+
spokenParts.push(piece.value);
|
|
517
|
+
addText(piece.value, shared ? piece.value : emittedCaption ? "" : caption!, piece.start, piece.end, false, piece.positions);
|
|
512
518
|
emittedCaption = true;
|
|
513
519
|
}
|
|
514
520
|
};
|
|
@@ -520,6 +526,29 @@ export function parseScript(
|
|
|
520
526
|
if (raw.startsWith("||", index)) {
|
|
521
527
|
fail("SCRIPT_CAPTION_BREAK_DUAL", "Caption Cue break cannot occur inside Dual Text; split the Dual Text into separate units.", absoluteStart + index);
|
|
522
528
|
}
|
|
529
|
+
if (shared && raw[index] === "{") {
|
|
530
|
+
const before = raw.slice(partStart, index);
|
|
531
|
+
if (!before || /\s$/u.test(before)) {
|
|
532
|
+
fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must immediately follow a display token.", absoluteStart + index);
|
|
533
|
+
}
|
|
534
|
+
addLiteral(before, absoluteStart + partStart);
|
|
535
|
+
const block = parseAttributeBlock(raw.slice(index), absoluteStart + index);
|
|
536
|
+
const displayIndex = displayWordSurfaces(spokenParts.join("")).length - 1;
|
|
537
|
+
if (displayIndex < 0 || tokens.length === startToken) {
|
|
538
|
+
fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must follow a visible display token.", absoluteStart + index);
|
|
539
|
+
}
|
|
540
|
+
if (sharedMarks.some(mark => mark.displayIndex === displayIndex)) {
|
|
541
|
+
fail("SCRIPT_ATTRIBUTE_OVERLAP", "A display token cannot receive two separate attribute blocks.", absoluteStart + index);
|
|
542
|
+
}
|
|
543
|
+
sharedMarks.push({ displayIndex, attributes: block.attributes });
|
|
544
|
+
const token = tokens.at(-1)!;
|
|
545
|
+
tokens[tokens.length - 1] = { ...token, editRange: {
|
|
546
|
+
start: token.editRange.start, end: sourceOffset + absoluteStart + index + block.length,
|
|
547
|
+
} };
|
|
548
|
+
index += block.length;
|
|
549
|
+
partStart = index;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
523
552
|
const marker = parseMarker(raw, index);
|
|
524
553
|
if (!marker) {
|
|
525
554
|
if (raw[index] === "@") fail("SCRIPT_MARKER", "Unescaped @ must begin a valid marker.", absoluteStart + index);
|
|
@@ -532,15 +561,19 @@ export function parseScript(
|
|
|
532
561
|
partStart = index;
|
|
533
562
|
}
|
|
534
563
|
addLiteral(raw.slice(partStart), absoluteStart + partStart);
|
|
564
|
+
const display = caption ?? spokenParts.join("");
|
|
565
|
+
if (shared && tokens.length === startToken) {
|
|
566
|
+
fail("SCRIPT_DUAL_EMPTY", "Dual Text with omitted speech must contain spoken text on its display side.", absoluteStart);
|
|
567
|
+
}
|
|
535
568
|
if (tokens.length > startToken) {
|
|
536
569
|
addCaptionRegion(
|
|
537
|
-
|
|
570
|
+
display,
|
|
538
571
|
current!.id,
|
|
539
572
|
startToken,
|
|
540
573
|
tokens.length,
|
|
541
|
-
cleanProjection(
|
|
574
|
+
cleanProjection(display) ? "alias" : "hidden",
|
|
542
575
|
{ start: sourceOffset + absoluteStart, end: sourceOffset + absoluteStart + raw.length },
|
|
543
|
-
marks,
|
|
576
|
+
shared ? sharedMarks : marks,
|
|
544
577
|
);
|
|
545
578
|
}
|
|
546
579
|
};
|
|
@@ -636,10 +669,16 @@ export function parseScript(
|
|
|
636
669
|
const pipe = findUnescaped(inside, "|");
|
|
637
670
|
if (pipe >= 0) {
|
|
638
671
|
finishLexicalRun();
|
|
672
|
+
const speech = inside.slice(pipe + 1);
|
|
673
|
+
if (!speech.trim()) {
|
|
674
|
+
consumeSpeechSide(inside.slice(0, pipe), offset + 1, undefined);
|
|
675
|
+
finishLexicalRun();
|
|
676
|
+
offset = end + 1;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
639
679
|
assertDualDisplayLiteral(inside.slice(0, pipe), offset + 1);
|
|
640
680
|
const markedDisplay = parseMarkedDisplay(inside.slice(0, pipe), offset + 1);
|
|
641
681
|
const display = markedDisplay.display;
|
|
642
|
-
const speech = inside.slice(pipe + 1);
|
|
643
682
|
if (!speech.replace(/~?@\/?[A-Za-z_][A-Za-z0-9_.-]*!?~?/gu, "").trim()) {
|
|
644
683
|
fail("SCRIPT_DUAL_EMPTY", "Dual Text speech side must not be empty.", offset);
|
|
645
684
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Localizing Studio
|
|
2
|
+
|
|
3
|
+
Studio owns its interface messages; a language pack supplies their wording. The
|
|
4
|
+
English catalog in [locales/en.json](locales/en.json) is the complete list of
|
|
5
|
+
message IDs. [Simplified Chinese](locales/zh-CN.json) uses exactly the same format
|
|
6
|
+
as any additional language. A pack is JSON data, not executable code.
|
|
7
|
+
|
|
8
|
+
## Add a language
|
|
9
|
+
|
|
10
|
+
Copy `locales/en.json`, change `locale` (a BCP 47 tag such as `ja` or `pt-BR`),
|
|
11
|
+
`name` (the language's native name), and translate the message values. Keep the
|
|
12
|
+
message IDs. `direction` is `ltr` or `rtl` and controls translated text; it does
|
|
13
|
+
not reverse the physical video timeline or canvas coordinates.
|
|
14
|
+
|
|
15
|
+
A small, valid starting point is:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"locale": "ja",
|
|
20
|
+
"name": "日本語",
|
|
21
|
+
"direction": "ltr",
|
|
22
|
+
"messages": {
|
|
23
|
+
"app.studio": "スタジオ",
|
|
24
|
+
"app.comments": "コメント",
|
|
25
|
+
"app.language": "言語"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Missing messages use English, so a partial translation is useful immediately.
|
|
31
|
+
Text is displayed as text, never interpreted as HTML. Preserve named variables
|
|
32
|
+
such as `{name}` and `{count}`; their order may change to suit the language.
|
|
33
|
+
Messages with `{count}` can be strings or plural forms:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{ "one": "{count} item", "other": "{count} items" }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use the language's `Intl.PluralRules` categories (`zero`, `one`, `two`, `few`,
|
|
40
|
+
`many`, `other`); `other` is required. Numbers and interface dates use the chosen
|
|
41
|
+
locale. Choose natural full sentences instead of assembling translated fragments.
|
|
42
|
+
|
|
43
|
+
Check and load your file:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
hypit studio --check-locale ./ja.json
|
|
47
|
+
hypit studio --run ./render.svrun --locale-pack ./ja.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The check lists missing translations and unknown IDs and reports malformed
|
|
51
|
+
messages or renamed variables. It does not open a Run or require a Runtime.
|
|
52
|
+
Relative file paths start at the command's working directory; prefix them with
|
|
53
|
+
`./` or `../`. Repeat `--locale-pack` to load multiple languages. Packs are read
|
|
54
|
+
at server startup; restart Studio after editing a pack. An explicitly loaded
|
|
55
|
+
pack with an existing locale replaces that locale's pack, with English fallback
|
|
56
|
+
for any omitted messages.
|
|
57
|
+
|
|
58
|
+
## Share a language pack
|
|
59
|
+
|
|
60
|
+
A local file is sufficient. For reuse, publish it through an ordinary package
|
|
61
|
+
under your own scope; there is no Hypit language registry or separate approval
|
|
62
|
+
step. Export the JSON file in the package's `package.json`, for example:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"name": "@your-scope/studio-ja",
|
|
67
|
+
"version": "1.0.0",
|
|
68
|
+
"files": ["ja.json"],
|
|
69
|
+
"exports": { "./locale.json": "./ja.json" }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Install it using the project's package manager, then load it explicitly:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
hypit studio --run ./render.svrun --locale-pack @your-scope/studio-ja/locale.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Package exports resolve from the project, or `--package-root` when specified.
|
|
80
|
+
Studio reads the resolved JSON without importing package code. It neither scans
|
|
81
|
+
for language packs nor installs missing packages.
|
|
82
|
+
|
|
83
|
+
## Interface behavior and ownership
|
|
84
|
+
|
|
85
|
+
The globe menu to the left of **Studio / Comments** lists loaded languages by
|
|
86
|
+
their native names. The first visit follows the browser's language preferences;
|
|
87
|
+
subsequent visits retain the choice saved in that browser. Switching updates
|
|
88
|
+
marked labels in place, preserving the playhead, selection and comment draft.
|
|
89
|
+
|
|
90
|
+
Only Studio-owned interface text belongs in this catalog. Component names,
|
|
91
|
+
Companion-supplied labels, Source code, user comments and raw service diagnostics
|
|
92
|
+
retain their original text. Language packs do not change the Companion ABI,
|
|
93
|
+
Core, compilation, or rendering. A component remains the owner of its labels.
|
|
94
|
+
|
|
95
|
+
When adding Studio UI, give each message a stable ID in the English catalog and
|
|
96
|
+
use the helpers in `src/ui/i18n.ts` for text and attributes. Do not use English
|
|
97
|
+
wording as IDs, inspect DOM text to guess what needs translation, or put language
|
|
98
|
+
branches into components. Keep interpolation values separate from message text.
|
|
@@ -258,3 +258,16 @@ audibility regions and target-relative fades. Web Audio gain nodes schedule the
|
|
|
258
258
|
on the same clock, including silence and amplification above unity. Visual video elements stay muted;
|
|
259
259
|
the Film's selected AudioTracks own composition sound. Seeking is silent. This path is independent of
|
|
260
260
|
which component emitted a clip.
|
|
261
|
+
|
|
262
|
+
### Interface language
|
|
263
|
+
|
|
264
|
+
The globe menu to the left of **Studio / Comments** selects a language. English
|
|
265
|
+
and Simplified Chinese are included; additional JSON language packs can be loaded
|
|
266
|
+
with `--locale-pack`, from local files or installed package exports. The first
|
|
267
|
+
visit follows browser preferences, and later visits remember the user's choice.
|
|
268
|
+
Switching preserves the playhead, selection and comment draft.
|
|
269
|
+
|
|
270
|
+
[Localizing Studio](LOCALIZATION.md) explains the message catalog, plural forms,
|
|
271
|
+
translation checks and language-pack distribution. Only Studio-owned interface
|
|
272
|
+
text is translated; Companion labels, project content and raw diagnostics retain
|
|
273
|
+
their original text. The Companion ABI is unchanged.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
{
|
|
2
|
+
"locale": "en",
|
|
3
|
+
"name": "English",
|
|
4
|
+
"direction": "ltr",
|
|
5
|
+
"messages": {
|
|
6
|
+
"app.comments": "Comments",
|
|
7
|
+
"app.language": "Language",
|
|
8
|
+
"app.studio": "Studio",
|
|
9
|
+
"app.studio-view": "Studio view",
|
|
10
|
+
"comments.all": "All",
|
|
11
|
+
"comments.caught-up": "All caught up",
|
|
12
|
+
"comments.delete-comment": "Delete comment",
|
|
13
|
+
"comments.done": "Done",
|
|
14
|
+
"comments.draw-soon": "Draw — coming later",
|
|
15
|
+
"comments.edit-comment": "Edit comment",
|
|
16
|
+
"comments.edit-comment-text": "Edit comment text",
|
|
17
|
+
"comments.emoji-soon": "Emoji — coming later",
|
|
18
|
+
"comments.empty": "A note starts here",
|
|
19
|
+
"comments.empty-hint": "Pause on a frame and describe what you would like to change.",
|
|
20
|
+
"comments.filter-comments": "Filter comments",
|
|
21
|
+
"comments.filter-hint": "Switch filters to see the other comments.",
|
|
22
|
+
"comments.go-to-this-frame": "Go to this frame",
|
|
23
|
+
"comments.loading-comments": "Loading comments…",
|
|
24
|
+
"comments.mark-complete": "Mark complete",
|
|
25
|
+
"comments.no-completed-comments": "No completed comments",
|
|
26
|
+
"comments.open": "Open",
|
|
27
|
+
"comments.placeholder": "What would you like to change?",
|
|
28
|
+
"comments.read-failed": "Could not read comments.",
|
|
29
|
+
"comments.refresh-comments": "Refresh comments",
|
|
30
|
+
"comments.reopen-comment": "Reopen comment",
|
|
31
|
+
"comments.save-failed": "Could not save comment.",
|
|
32
|
+
"comments.send-comment": "Send comment",
|
|
33
|
+
"comments.send-shortcut": "Send comment (Enter)",
|
|
34
|
+
"comments.use-current-frame": "Use current frame",
|
|
35
|
+
"comments.write-a-comment": "Write a comment",
|
|
36
|
+
"common.cancel": "Cancel",
|
|
37
|
+
"common.cancelled": "Cancelled",
|
|
38
|
+
"common.compile-failed": "Compile failed",
|
|
39
|
+
"common.completed": "Completed",
|
|
40
|
+
"common.copied": "Copied",
|
|
41
|
+
"common.failed": "Failed",
|
|
42
|
+
"common.invalid-value": "Invalid value",
|
|
43
|
+
"common.needs-attention": "Needs attention",
|
|
44
|
+
"common.queued": "Queued",
|
|
45
|
+
"common.refresh": "Refresh",
|
|
46
|
+
"common.running": "Running",
|
|
47
|
+
"common.save": "Save",
|
|
48
|
+
"common.save-failed": "Save failed",
|
|
49
|
+
"common.saved": "Saved",
|
|
50
|
+
"common.saving": "Saving",
|
|
51
|
+
"common.waiting": "Waiting",
|
|
52
|
+
"inspector.add-color": "Add color",
|
|
53
|
+
"inspector.add-item": "Add item",
|
|
54
|
+
"inspector.anchor.program-end": "Timeline end{detail}",
|
|
55
|
+
"inspector.anchor.program-start": "Timeline start{detail}",
|
|
56
|
+
"inspector.anchor.segment-end": "Segment end{detail}",
|
|
57
|
+
"inspector.anchor.segment-start": "Segment start{detail}",
|
|
58
|
+
"inspector.anchor.token-end": "Word end{detail}",
|
|
59
|
+
"inspector.anchor.token-start": "Word start{detail}",
|
|
60
|
+
"inspector.aspect-ratio": "Aspect ratio",
|
|
61
|
+
"inspector.author": "Author",
|
|
62
|
+
"inspector.build": "Build",
|
|
63
|
+
"inspector.candidates": "Candidates",
|
|
64
|
+
"inspector.canvas": "Canvas",
|
|
65
|
+
"inspector.duration": "Duration",
|
|
66
|
+
"inspector.empty": "No details for this selection",
|
|
67
|
+
"inspector.end": "End",
|
|
68
|
+
"inspector.end-anchor": "End semantic anchor",
|
|
69
|
+
"inspector.frame-rate": "Frame rate",
|
|
70
|
+
"inspector.frames": "Frames",
|
|
71
|
+
"inspector.how": "How",
|
|
72
|
+
"inspector.invalid-fields": "Complete the required fields with valid values to save.",
|
|
73
|
+
"inspector.item-count": {
|
|
74
|
+
"one": "{count} item",
|
|
75
|
+
"other": "{count} items"
|
|
76
|
+
},
|
|
77
|
+
"inspector.moment": "Moment",
|
|
78
|
+
"inspector.moment-anchor": "Moment semantic anchor",
|
|
79
|
+
"inspector.move-down": "Move down",
|
|
80
|
+
"inspector.move-up": "Move up",
|
|
81
|
+
"inspector.project": "Project",
|
|
82
|
+
"inspector.properties": "Properties",
|
|
83
|
+
"inspector.remove-item": "Remove item",
|
|
84
|
+
"inspector.resolution": "Resolution",
|
|
85
|
+
"inspector.run": "Run",
|
|
86
|
+
"inspector.semantic-anchors": "Semantic anchors",
|
|
87
|
+
"inspector.sources": "Sources",
|
|
88
|
+
"inspector.start": "Start",
|
|
89
|
+
"inspector.start-anchor": "Start semantic anchor",
|
|
90
|
+
"inspector.structured-value": "Structured value",
|
|
91
|
+
"inspector.targets": "Targets",
|
|
92
|
+
"inspector.timeline": "Timeline",
|
|
93
|
+
"inspector.timing": "Timing",
|
|
94
|
+
"inspector.tracks": "Tracks",
|
|
95
|
+
"inspector.use-color": "Use {color}",
|
|
96
|
+
"inspector.when": "When",
|
|
97
|
+
"inspector.where": "Where",
|
|
98
|
+
"library.all-media": "All media",
|
|
99
|
+
"library.all-tasks": "All tasks",
|
|
100
|
+
"library.artifacts": "Artifacts",
|
|
101
|
+
"library.audio": "Audio",
|
|
102
|
+
"library.clear-task-filter": "Clear task filter",
|
|
103
|
+
"library.data": "Data",
|
|
104
|
+
"library.display-name": "Display name",
|
|
105
|
+
"library.enter-name": "Enter a name.",
|
|
106
|
+
"library.finished": "Finished",
|
|
107
|
+
"library.image": "Image",
|
|
108
|
+
"library.images": "Images",
|
|
109
|
+
"library.in-progress": "In progress",
|
|
110
|
+
"library.loading-media": "Loading media…",
|
|
111
|
+
"library.loading-more": "Loading more…",
|
|
112
|
+
"library.loading-tasks": "Loading tasks…",
|
|
113
|
+
"library.media-categories": "Media categories",
|
|
114
|
+
"library.media-category": "Media category",
|
|
115
|
+
"library.media-hint": "Images, videos and audio published as file Outputs appear here, including those from ongoing Builds.",
|
|
116
|
+
"library.name-failed": "Could not save the name",
|
|
117
|
+
"library.name-pending": "{name}\nNaming is available when this Build finishes",
|
|
118
|
+
"library.no-files-in-this-category": "No files in this category",
|
|
119
|
+
"library.no-media": "No media",
|
|
120
|
+
"library.no-tasks-here": "No tasks here",
|
|
121
|
+
"library.no-tasks-in-progress": "No tasks in progress",
|
|
122
|
+
"library.preview-name": "Preview {name}",
|
|
123
|
+
"library.preview-unavailable": "Preview unavailable",
|
|
124
|
+
"library.referenced-source-files": "Referenced source files",
|
|
125
|
+
"library.refresh-failed": "Refresh failed: {detail}",
|
|
126
|
+
"library.refresh-failed-retained": "Refresh failed: {detail} · Previous view retained",
|
|
127
|
+
"library.refresh-interrupted": "Refresh interrupted",
|
|
128
|
+
"library.refresh-media": "Refresh media",
|
|
129
|
+
"library.refresh-tasks": "Refresh tasks",
|
|
130
|
+
"library.refreshing": "Refreshing…",
|
|
131
|
+
"library.rename-hint": "{name}\nDouble-click or press F2 to rename",
|
|
132
|
+
"library.return-to-task": "Return to task",
|
|
133
|
+
"library.source": "Source",
|
|
134
|
+
"library.studio-library": "Studio library",
|
|
135
|
+
"library.task-categories": "Task categories",
|
|
136
|
+
"library.task-return": "{name} · Return to task",
|
|
137
|
+
"library.tasks": "Tasks",
|
|
138
|
+
"library.unavailable": "Library unavailable",
|
|
139
|
+
"library.updated": "Updated {time}",
|
|
140
|
+
"library.video": "Video",
|
|
141
|
+
"library.video-loading-preview": "Video · Loading preview",
|
|
142
|
+
"library.videos": "Videos",
|
|
143
|
+
"library.view-media": "View media",
|
|
144
|
+
"library.view-task-media": "View task media",
|
|
145
|
+
"player.audio-waveform": "Audio waveform",
|
|
146
|
+
"player.back-5-seconds": "Back 5 seconds",
|
|
147
|
+
"player.back-to-composition": "Back to composition",
|
|
148
|
+
"player.composition-time": "Composition time",
|
|
149
|
+
"player.elements-at-this-point": "Elements at this point",
|
|
150
|
+
"player.forward-5-seconds": "Forward 5 seconds",
|
|
151
|
+
"player.frame-loading": "Loading frame…",
|
|
152
|
+
"player.frame-unavailable": "Frame unavailable",
|
|
153
|
+
"player.hovered-frame": "Hovered frame",
|
|
154
|
+
"player.media-failed": "This media could not be opened in the browser.",
|
|
155
|
+
"player.media-time": "Media time",
|
|
156
|
+
"player.mute": "Mute",
|
|
157
|
+
"player.next-frame": "Next frame",
|
|
158
|
+
"player.pause": "Pause",
|
|
159
|
+
"player.play": "Play",
|
|
160
|
+
"player.preview": "Preview",
|
|
161
|
+
"player.previous-frame": "Previous frame",
|
|
162
|
+
"player.unmute": "Unmute",
|
|
163
|
+
"source.changed-outside-studio": "Changed outside Studio",
|
|
164
|
+
"source.edit-source": "Edit source",
|
|
165
|
+
"source.keep-lines-unwrapped": "Keep lines unwrapped",
|
|
166
|
+
"source.read-source": "Read source",
|
|
167
|
+
"source.save-failed-status": "Save failed ({status})",
|
|
168
|
+
"source.saving-before-switch": "Saving before switch",
|
|
169
|
+
"source.svml-source": "SVML source",
|
|
170
|
+
"source.unsaved": "Unsaved",
|
|
171
|
+
"source.wrap-lines": "Wrap lines",
|
|
172
|
+
"tasks.copy-error": "Copy error",
|
|
173
|
+
"tasks.copy-manually": "Select the text to copy",
|
|
174
|
+
"tasks.no-runtime": "No Runtime is selected. Saved tasks are available under Finished.",
|
|
175
|
+
"tasks.progress": "{completed}/{total} steps complete",
|
|
176
|
+
"tasks.refresh-hint": "Refresh to read the latest task status.",
|
|
177
|
+
"tasks.saved-only": "Saved project Results · no Runtime selected",
|
|
178
|
+
"timeline.fit-timeline": "Fit timeline",
|
|
179
|
+
"timeline.moment-name": "Moment {name}",
|
|
180
|
+
"timeline.overlapping-timeline-objects": "Overlapping timeline objects",
|
|
181
|
+
"timeline.resize-timeline-track-labels": "Resize timeline track labels",
|
|
182
|
+
"timeline.resize-track-labels": "Resize track labels",
|
|
183
|
+
"timeline.segment-name": "Segment {name}",
|
|
184
|
+
"timeline.segments": "Segments",
|
|
185
|
+
"timeline.select": "Select",
|
|
186
|
+
"timeline.selection-name": "Selection {name}",
|
|
187
|
+
"timeline.selections-and-moments": "Selections and Moments",
|
|
188
|
+
"timeline.words": "Words",
|
|
189
|
+
"timeline.zoom-in": "Zoom in",
|
|
190
|
+
"timeline.zoom-out": "Zoom out",
|
|
191
|
+
"inspector.color-picker": "{label} picker",
|
|
192
|
+
"timeline.segment-count": {
|
|
193
|
+
"one": "{count} segment",
|
|
194
|
+
"other": "{count} segments"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|