@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
@@ -14,9 +14,46 @@ hypit measure main.svml --segment opening --language en --pace normal
14
14
  hypit measure --text "Video editing begins with meaning." --language en --pace normal --rounding ceil
15
15
  ```
16
16
 
17
- The English pace presets are `slow = 4.2`, `normal = 4.6` and `fast = 5.0` syllables per second; a
18
- positive numeric `rate` replaces `pace`. `round` selects the nearest whole second, `ceil` a
19
- never-shorter one, `none` keeps the fraction.
17
+ ## Units and delivery
18
+
19
+ English counts pronunciation syllables, not words or letters. Mandarin Chinese counts each Han
20
+ character as one approximate syllable, with English syllables for embedded English words.
21
+ Traditional and simplified Han use the same counting rule; punctuation separates words without
22
+ adding units. This remains an approximation: connected English speech can reduce syllables,
23
+ and Mandarin erhua can combine written characters into one syllable. An explicit `--language zh`
24
+ keeps a Chinese passage with many English names under its intended delivery policy.
25
+
26
+ Measure the spoken wording. `--segment` reads the Script's pronunciation side of Dual Text;
27
+ `--text` reads literal speech, so spell numbers and letter names as they will be said. A count
28
+ cannot resolve an unspecified pronunciation.
29
+
30
+ | Language | slow | normal | fast | Unit |
31
+ | --- | ---: | ---: | ---: | --- |
32
+ | English (`en`) | 4.2 | 4.6 | 5.6 | syllables/s |
33
+ | Mandarin Chinese (`zh`) | 4.2 | 5.25 | 6.5625 | approximate syllables/s |
34
+
35
+ These are authoring starting points for whole-passage delivery, including ordinary speech pauses.
36
+ `normal` suits conversational explanation; `fast` gives brisk, tightly delivered copy less time.
37
+ The English fast preset is deliberately separated from normal so that choosing brisk delivery
38
+ meaningfully changes the estimate. The Chinese normal preset is retained: available research
39
+ does not establish that it is systematically too slow for whole passages. Neither table is a
40
+ calibration of a particular video or voice model. For a specific delivery, a positive numeric
41
+ `--rate` replaces the preset, including rates outside this table.
42
+
43
+ ```bash
44
+ hypit measure main.svml --segment opening --language zh --pace fast --rounding round
45
+ hypit measure main.svml --segment opening --language zh --rate 6 --rounding none
46
+ ```
47
+
48
+ `seconds = pronunciation units / rate + padding`, followed by rounding. Add `--padding` for extra
49
+ time deliberately reserved for a reaction, demonstration or held pause. Ordinary phrasing is
50
+ already part of the chosen density; adding a generic pause allowance on top lengthens the passage
51
+ again. `round` selects the nearest whole second, `ceil` rounds upward, and `none` keeps the fraction.
52
+ The CLI shows the resolved rate and padding; JSON retains full numeric precision.
53
+
54
+ For 60 Mandarin units, normal gives 11.43 seconds and fast gives 9.14 seconds before rounding.
55
+ For 60 English syllables, normal gives 13.04 seconds and fast gives 10.71 seconds. Equal unit counts
56
+ do not imply equal information, and 60 English words need not contain 60 syllables.
20
57
 
21
58
  The estimate reports the time the wording needs at that delivery. Use it to decide whether a passage
22
59
  needs merging, fuller wording, tightening, or splitting, then choose the request duration with the
@@ -30,3 +67,19 @@ performed media's semantic preparation.
30
67
  ```ts
31
68
  import { estimateSpeechDuration, sealSpeechEstimatePolicy, speechEstimatePolicyFromRecipe } from "@hypit/estimate";
32
69
  ```
70
+
71
+ ## Research basis and limits
72
+
73
+ [Burchfield and Bradlow (2014)](https://doi.org/10.1121/1.4874357) measured Mandarin and English
74
+ across clear reading, plain reading and spontaneous speech. Plain-reading means were 5.97 and
75
+ 5.39 canonical syllables/s respectively. Their articulation measure excludes errors, silence
76
+ and non-speech, so those means cannot directly size an entire performance containing pauses.
77
+ [Coupé et al. (2019)](https://doi.org/10.1126/sciadv.aaw2594) likewise counted canonical syllables
78
+ and excluded pauses longer than 150 ms in their cross-language reading study. These studies
79
+ support distinguishing language, pronunciation units and timing denominator; they do not
80
+ establish one ideal pace for social video or validate these preset values.
81
+
82
+ When an already understood reference passage provides useful timing, its spoken-unit count
83
+ divided by its first-to-last-word span offers a starting density, including internal pauses.
84
+ Choose how much of that rhythm belongs in the new performance. Requested clip length alone
85
+ is not measured speaking time, and delivery density does not describe emphasis or expression.
@@ -14,9 +14,9 @@ const PACE_RATE: Readonly<Record<
14
14
  ResolvedSpeechEstimateLanguage,
15
15
  Readonly<Record<SpeechEstimatePace, number>>
16
16
  >> = {
17
- // English is calibrated as delivery density, not pause-free articulation rate.
18
- en: { slow: 4.2, normal: 4.6, fast: 5 },
19
- // Preserve the existing non-English behavior until those languages are audited.
17
+ // Authoring delivery densities, including ordinary pauses, not measured model
18
+ // guarantees or pause-free articulation rates. See README for units and rationale.
19
+ en: { slow: 4.2, normal: 4.6, fast: 5.6 },
20
20
  zh: { slow: 4.2, normal: 5.25, fast: 6.5625 },
21
21
  ja: { slow: 6, normal: 7.5, fast: 9.375 },
22
22
  es: { slow: 4.72, normal: 5.9, fast: 7.375 },
@@ -35,7 +35,7 @@ function looksSpanish(text: string): boolean {
35
35
  }
36
36
 
37
37
  export function detectSpeechEstimateLanguage(text: string): ResolvedSpeechEstimateLanguage {
38
- const han = (text.match(/[\u3400-\u9fff]/gu) ?? []).length;
38
+ const han = (text.match(/\p{Script=Han}/gu) ?? []).length;
39
39
  const kana = (text.match(/[\u3040-\u30ff]/gu) ?? []).length;
40
40
  const ascii = (text.match(/[A-Za-z]/gu) ?? []).length;
41
41
  if (kana > Math.max(han, ascii / 4)) return "ja";
@@ -117,10 +117,9 @@ export function countSpeechEstimateUnits(
117
117
  language: ResolvedSpeechEstimateLanguage,
118
118
  ): number {
119
119
  if (language === "zh" || language === "ja") {
120
- const cjk = (text.match(/[\u3400-\u9fff\u3040-\u30ff]/gu) ?? []).length;
121
- const latin = text
122
- .replace(/[\u3400-\u9fff\u3040-\u30ff]/gu, " ")
123
- .split(/\s+/u)
120
+ const characters = /[\p{Script=Han}\u3040-\u30ff]/gu;
121
+ const cjk = (text.match(characters) ?? []).length;
122
+ const latin = words(text.replace(characters, " "))
124
123
  .filter((word) => /[A-Za-z]/u.test(word));
125
124
  return cjk + latin.reduce((sum, word) => sum + englishSyllables(word), 0);
126
125
  }
@@ -1,5 +1,10 @@
1
1
  # `@hypit/fonts-open`
2
2
 
3
+ The optional `@hypit/fonts-open/studio` export supplies data-only catalog choices for Companions.
4
+ `openFontStudioFields(owner)` returns the explicit nested font binding and Inspector fields for a
5
+ Style reference such as `program` or `style`. It edits the referenced catalog face's family, keeping
6
+ its authored weight, style and fallback faces. Ordinary font validation still applies.
7
+
3
8
  Workspace package exposing 109 redistributable open font families as exact,
4
9
  Resource-backed `FontArtifactRef` values. The installed packages carry their font bytes and license
5
10
  files. Author compilation and rendering never download fonts or inspect system font directories.
@@ -5,7 +5,8 @@
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "exports": {
8
- ".": "./src/index.ts"
8
+ ".": "./src/index.ts",
9
+ "./studio": "./src/studio.ts"
9
10
  },
10
11
  "hypit": {
11
12
  "activation": "./src/activation.ts"
@@ -126,5 +127,8 @@
126
127
  "@hypit/package-loader-node": "workspace:*",
127
128
  "@hypit/protocol": "workspace:*",
128
129
  "@hypit/markup": "workspace:*"
130
+ },
131
+ "devDependencies": {
132
+ "@hypit/studio-adapter": "workspace:*"
129
133
  }
130
134
  }
@@ -0,0 +1,21 @@
1
+ import type { StudioInspectorFieldDeclaration, StudioSourceBindingDeclaration } from "@hypit/studio-adapter";
2
+ import { openFontFamilies } from "./catalog.js";
3
+
4
+ /** Optional Companion declarations for the primary catalog face reached through a Style's font. */
5
+ export function openFontStudioFields(owner: string): {
6
+ readonly binding: StudioSourceBindingDeclaration;
7
+ readonly fields: readonly StudioInspectorFieldDeclaration[];
8
+ } {
9
+ return {
10
+ binding: { name: "font", referenced: [{ name: "family", writable: true }] },
11
+ fields: [{
12
+ binding: `${owner}.font.family`, label: "Font Family", domain: "how",
13
+ page: { id: "font", label: "Font" }, section: { id: "face", label: "Primary Face" },
14
+ summary: "Changes the shared catalog face. Its authored weight and style must be available in the selected family.",
15
+ control: "select",
16
+ options: Object.entries(openFontFamilies).map(([value, family]) => ({
17
+ value, label: family.label, description: `${family.category} · ${family.intendedUse}`,
18
+ })),
19
+ }],
20
+ };
21
+ }
@@ -58,7 +58,7 @@ placeMedia([
58
58
  placeMedia(["stack-order"], "where", "Frame", "Stacking");
59
59
  placeMedia(["clip", "radius", "padding"], "where", "Frame", "Geometry");
60
60
  placeMedia(["opacity", "blur", "brightness", "contrast", "saturation"], "how", "Image", "Image");
61
- placeMedia(["playback", "trim-start", "trim-end"], "how", "Playback", "Playback");
61
+ placeMedia(["playback", "trim-start", "trim-end"], "when", "Playback", "Playback");
62
62
  placeMedia(["border-width", "border-style", "border-color", "shadows", "frame-paint"], "how", "Frame", "Paint");
63
63
  placeMedia(["enter", "enter-frames", "enter-easing", "enter-direction", "enter-amount", "enter-origin"], "when", "Enter", "Enter");
64
64
  placeMedia(["sustain"], "when", "Sustain", "Sustain");
@@ -66,6 +66,8 @@ placeMedia(["exit", "exit-frames", "exit-easing", "exit-direction", "exit-amount
66
66
 
67
67
  const mediaColorProperties = new Set(["border-color"]);
68
68
  const mediaTextProperties = new Set(["padding", "shadows", "frame-paint", "sustain"]);
69
+ const mediaPercentProperties = new Set(["frame-x", "frame-y", "content-x", "content-y", "opacity", "brightness", "contrast", "saturation"]);
70
+ const mediaPixelProperties = new Set(["fit-offset-x", "fit-offset-y", "radius", "blur", "border-width"]);
69
71
 
70
72
  function mediaRecipe(name: "appearance" | "motion"): {
71
73
  readonly bindings: readonly { readonly name: string }[];
@@ -88,6 +90,9 @@ function mediaRecipe(name: "appearance" | "motion"): {
88
90
  ...placement,
89
91
  ...(property.summary === undefined ? {} : { summary: property.summary }),
90
92
  control,
93
+ ...(mediaPercentProperties.has(property.name) ? { unit: "%", number: { scale: 100, step: 1 } } : {}),
94
+ ...(mediaPixelProperties.has(property.name) ? { unit: "px", number: { step: 1 } } : {}),
95
+ ...(["enter-frames", "exit-frames", "trim-start", "trim-end"].includes(property.name) ? { unit: "f", number: { step: 1 } } : {}),
91
96
  ...(options === undefined ? {} : { options }),
92
97
  };
93
98
  }),
@@ -209,7 +214,7 @@ export const mediaTrackStudioTrackCompanions: readonly StudioTrackCompanion[] =
209
214
  ...frameParameters.filter(({ writable }) => writable === true).map(({ name }) => ({
210
215
  binding: `frame.${name}`, label: title(name), domain: "where" as const,
211
216
  page: { id: frameSizeParameters.has(name) ? "size" : "placement", label: frameSizeParameters.has(name) ? "Size" : "Placement" },
212
- section: { id: "frame", label: "Frame" }, control: "text" as const,
217
+ section: { id: "frame", label: "Frame" }, control: "number" as const, number: { suffixes: ["%", "px"], step: 1 },
213
218
  })),
214
219
  ...extentParameters.map(({ name }) => ({
215
220
  binding: `extent.${name}`, label: title(name), domain: "where" as const,
@@ -30,6 +30,41 @@ Provider; it is a different deployment package but must return the same
30
30
  The configured model, device, compute mode and batch size are checked through `/health` before use.
31
31
  This prevents a warm process with an incompatible inference configuration from accepting work.
32
32
 
33
+ ## Choose model and hardware
34
+
35
+ The default `small` / `cpu` / `int8` is a modest-machine execution default, not a quality ranking.
36
+ For multilingual transcription with quality as the priority and suitable hardware, consider
37
+ `large-v3`. For example, an NVIDIA CUDA installation with sufficient memory can explicitly select:
38
+
39
+ ```json
40
+ "whisperx.local": {
41
+ "use": "@hypit/provider-whisperx-local",
42
+ "config": {
43
+ "expectedModel": "large-v3",
44
+ "expectedDevice": "cuda",
45
+ "expectedCompute": "float16",
46
+ "expectedBatchSize": 4
47
+ }
48
+ }
49
+ ```
50
+
51
+ This is an example deployment, not a promise that every GPU has enough memory. Adjust batch size
52
+ and compute mode for the actual device. On CPU, `int8` is the practical starting point; a larger
53
+ model still takes more preparation and inference work. The packaged faster-whisper backend uses
54
+ CTranslate2: Apple Silicon does not imply CUDA or support for a PyTorch `mps` setting here.
55
+ An already ready smaller model can be useful for a short clear reference. Choose the local model
56
+ or hosted service for the actual language, material and time available rather than installing
57
+ successively larger models as a routine sequence.
58
+
59
+ `expectedModel` selects speech recognition. WhisperX separately loads the language-specific
60
+ alignment model when that language is first requested. A larger ASR model can improve the words
61
+ fed into alignment, but does not select a larger aligner or guarantee better timing by itself.
62
+ The health response identifies the loaded ASR configuration; it does not establish that every
63
+ language's alignment weights are cached. See [WhisperX usage](https://github.com/m-bain/whisperX#usage-)
64
+ and [faster-whisper deployment](https://github.com/SYSTRAN/faster-whisper#usage).
65
+
66
+ ## Preparation and downloads
67
+
33
68
  The Runtime installs and starts the packaged service in the machine Program Home when this Endpoint
34
69
  is selected. The environment is reused across projects and sessions. The current local package and
35
70
  service are trusted code; this is not a community-plugin sandbox.
@@ -42,3 +77,25 @@ its configuration through `/health`. Inspect an existing Profile's address and e
42
77
  when locating that service. A custom `serviceCommand` supplies its own installation and start command.
43
78
  `hypit programs status` checks the Programs selected by the supplied Profile; `programs up` prepares
44
79
  and starts them. The managed installation does not require a global `whisperx` shell command.
80
+
81
+ Preparation and service processes inherit the environment of the command starting them. Set
82
+ network and cache variables there before `programs up` or `runtime up`. A service already running
83
+ retains its earlier environment. Inspect its reported log before deciding whether a selected
84
+ Program needs restarting, and account for active work using it.
85
+
86
+ Preparation commands write `install.log`; the running service writes `program.log`, with stderr in
87
+ `program.err.log` on Windows. Inspect the stderr file for Python model-loading and download messages.
88
+ The service health endpoint becomes available after ASR loading. A startup readiness wait expiring
89
+ can leave that process still loading; check its reported PID and logs before starting another process.
90
+
91
+ Python installation, Python packages, NLTK sentence data, ASR weights and language-alignment weights
92
+ are separate downloads. `UV_PYTHON_INSTALL_MIRROR` configures a Python distribution mirror;
93
+ `HF_ENDPOINT` and `HF_HOME` configure Hugging Face access/cache. Alignment models can also come
94
+ from torchaudio's download hosts. These settings do not redirect NLTK's own downloader.
95
+
96
+ Managed installation uses `uv sync --frozen` with the distributed lockfile, including its artifact
97
+ URLs. `PIP_INDEX_URL` does not configure uv, and `UV_DEFAULT_INDEX` is an index-resolution setting,
98
+ not a general rewrite of those frozen URLs. Inspect the actual transfer destination. For blocked
99
+ locked downloads, use an appropriate network/proxy route or an already populated compatible cache;
100
+ an operator-managed installation can instead provide `serviceCommand` with the expected service
101
+ identity. Ordinary production keeps the distributed dependency selection intact.
@@ -183,6 +183,7 @@ function iconElement(input: {
183
183
 
184
184
  function present(input: {
185
185
  readonly id: string;
186
+ readonly subjectId: string;
186
187
  readonly start: number;
187
188
  readonly end: number;
188
189
  readonly stacking: number;
@@ -191,6 +192,7 @@ function present(input: {
191
192
  }): VisualPresent {
192
193
  return {
193
194
  id: input.id,
195
+ subjectId: input.subjectId,
194
196
  span: { startFrame: input.start, endFrameExclusive: input.end },
195
197
  stacking: { order: input.stacking, tieBreak: input.tieBreak },
196
198
  elements: input.elements,
@@ -307,7 +309,7 @@ export function renderTierBoard(space: ProgramSpace, program: TierBoardProgram):
307
309
  }));
308
310
  }
309
311
  presents.push(present({
310
- id: `${program.id}:board`, start: schedule.outer.startFrame, end: schedule.outer.endFrameExclusive,
312
+ id: `${program.id}:board`, subjectId: program.id, start: schedule.outer.startFrame, end: schedule.outer.endFrameExclusive,
311
313
  stacking: style.boardStackingOrder, tieBreak: `${program.id}:0000:board`, elements: boardElements,
312
314
  }));
313
315
  const entries = new Map(schedule.entries.map((entry) => [entry.itemId, entry]));
@@ -351,7 +353,7 @@ export function renderTierBoard(space: ProgramSpace, program: TierBoardProgram):
351
353
  : (localFrame) => fromHighTierItemPose(
352
354
  localFrame, duration, stage, absoluteCell, style.motion.appearFrames, style.motion.moveFrames));
353
355
  presents.push(present({
354
- id: `${program.id}:item:${item.id}:reveal`,
356
+ id: `${program.id}:item:${item.id}:reveal`, subjectId: item.id,
355
357
  start: entry.window.startFrame,
356
358
  end: entry.window.endFrameExclusive,
357
359
  stacking: item.stackingOrder ?? (item.entry === "drop" ? style.stageStackingOrder : style.itemStackingOrder),
@@ -360,7 +362,7 @@ export function renderTierBoard(space: ProgramSpace, program: TierBoardProgram):
360
362
  }));
361
363
  }
362
364
  if (entry.settled.endFrameExclusive > entry.settled.startFrame) presents.push(present({
363
- id: `${program.id}:item:${item.id}:settled`,
365
+ id: `${program.id}:item:${item.id}:settled`, subjectId: item.id,
364
366
  start: entry.settled.startFrame,
365
367
  end: entry.settled.endFrameExclusive,
366
368
  stacking: item.stackingOrder ?? style.itemStackingOrder,
@@ -506,7 +508,7 @@ export function renderColumn(space: ProgramSpace, program: ColumnProgram): Visua
506
508
  ],
507
509
  }));
508
510
  }
509
- presents.push(present({ id: `${program.id}:board`, start: schedule.outer.startFrame, end: schedule.outer.endFrameExclusive,
511
+ presents.push(present({ id: `${program.id}:board`, subjectId: program.id, start: schedule.outer.startFrame, end: schedule.outer.endFrameExclusive,
510
512
  stacking: style.boardStackingOrder, tieBreak: `${program.id}:0000:board`, elements: boardElements }));
511
513
  for (const [index, item] of program.items.entries()) {
512
514
  const entry = entries.get(item.id)!;
@@ -549,13 +551,13 @@ export function renderColumn(space: ProgramSpace, program: ColumnProgram): Visua
549
551
  stageSize: style.stageSizePx, finalSize: contentSize, easing: style.motion.easing,
550
552
  });
551
553
  presents.push(present({
552
- id: `${program.id}:item:${item.id}:stage`, start: entry.window.startFrame, end: entry.window.endFrameExclusive,
554
+ id: `${program.id}:item:${item.id}:stage`, subjectId: item.id, start: entry.window.startFrame, end: entry.window.endFrameExclusive,
553
555
  stacking: item.stackingOrder ?? style.stageStackingOrder,
554
556
  tieBreak: `${program.id}:item:${String(item.rank).padStart(4, "0")}:${item.id}:stage`, elements: itemElements(rootAnimation),
555
557
  }));
556
558
  }
557
559
  if (entry.settled.endFrameExclusive > entry.settled.startFrame) presents.push(present({
558
- id: `${program.id}:item:${item.id}:settled`, start: entry.settled.startFrame, end: entry.settled.endFrameExclusive,
560
+ id: `${program.id}:item:${item.id}:settled`, subjectId: item.id, start: entry.settled.startFrame, end: entry.settled.endFrameExclusive,
559
561
  stacking: item.stackingOrder ?? style.itemStackingOrder,
560
562
  tieBreak: `${program.id}:item:${String(item.rank).padStart(4, "0")}:${item.id}:settled`, elements: itemElements(),
561
563
  }));
@@ -599,7 +601,7 @@ export function renderTopThree(space: ProgramSpace, program: TopThreeProgram): V
599
601
  }));
600
602
  }
601
603
  const presents: VisualPresent[] = [present({
602
- id: `${program.id}:slots`, start: schedule.outer.startFrame, end: schedule.outer.endFrameExclusive,
604
+ id: `${program.id}:slots`, subjectId: program.id, start: schedule.outer.startFrame, end: schedule.outer.endFrameExclusive,
603
605
  stacking: style.boardStackingOrder, tieBreak: `${program.id}:0000:slots`, elements: boardElements,
604
606
  })];
605
607
  for (const [index, item] of program.items.entries()) {
@@ -640,12 +642,12 @@ export function renderTopThree(space: ProgramSpace, program: TopThreeProgram): V
640
642
  return elements;
641
643
  };
642
644
  presents.push(present({
643
- id: `${program.id}:item:${item.id}:stage`, start: entry.stage.startFrame, end: entry.stage.endFrameExclusive,
645
+ id: `${program.id}:item:${item.id}:stage`, subjectId: item.id, start: entry.stage.startFrame, end: entry.stage.endFrameExclusive,
644
646
  stacking: item.stackingOrder ?? style.itemStackingOrder,
645
647
  tieBreak: `${program.id}:item:${String(index).padStart(4, "0")}:${item.id}:stage`, elements: itemElements(true),
646
648
  }));
647
649
  if (entry.settled.endFrameExclusive > entry.settled.startFrame) presents.push(present({
648
- id: `${program.id}:item:${item.id}:settled`, start: entry.settled.startFrame, end: entry.settled.endFrameExclusive,
650
+ id: `${program.id}:item:${item.id}:settled`, subjectId: item.id, start: entry.settled.startFrame, end: entry.settled.endFrameExclusive,
649
651
  stacking: item.stackingOrder ?? style.itemStackingOrder,
650
652
  tieBreak: `${program.id}:item:${String(index).padStart(4, "0")}:${item.id}:settled`, elements: itemElements(false),
651
653
  }));
@@ -1,3 +1,14 @@
1
1
  # `@hypit/ranking-studio`
2
2
 
3
3
  Hypit Studio Companion for the Ranking module's Column, Tier and Top Three surfaces.
4
+
5
+ The root lane represents the board; `attachments` expose reveal or activation
6
+ events on a detail lane. Both use Studio's ordinary item selection and overlap
7
+ behavior. Their time ranges remain the authored event ranges.
8
+
9
+ Ranking's renderer assigns `subjectId` to each item's rendered phases. The
10
+ Companion associates those phases with the item's `renderIds`, so clicking an
11
+ entering or settled icon selects the same detail entity. The board and preset
12
+ items without a detail event remain associated with the root. Studio measures
13
+ the visible parts at the current frame; the Companion does not duplicate motion
14
+ geometry or infer identities from rendered ID strings.
@@ -4,11 +4,16 @@
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/ranking": "workspace:*",
11
15
  "@hypit/composition": "workspace:*",
12
- "@hypit/studio-adapter": "workspace:*"
16
+ "@hypit/studio-adapter": "workspace:*",
17
+ "@hypit/fonts-open": "workspace:*"
13
18
  }
14
19
  }
@@ -1,9 +1,13 @@
1
+ import { openFontStudioFields } from "@hypit/fonts-open/studio";
1
2
  import { rankingMarkupSurfaces, rankingModuleRef, rankingTypes } from "@hypit/ranking";
2
3
  import type { RankingProgram, RankingSchedule } from "@hypit/ranking";
3
4
  import { compositionTypes } from "@hypit/composition";
4
5
  import type { StudioTrackCompanion, StudioTrackCompanionContext, StudioEntityDraft, StudioInspectorFieldDeclaration, StudioSourceBindingDeclaration } from "@hypit/studio-adapter";
5
6
  import { artifactPreview, authoredChildFor, previewLayer, requiredSurfaceValue, temporalLineageFor, temporalSemanticSource } from "@hypit/studio-adapter";
6
7
 
8
+
9
+ const fontInspector = openFontStudioFields("style");
10
+
7
11
  const frameParameters: readonly StudioSourceBindingDeclaration[] = [
8
12
  { name: "within" },
9
13
  ...["left", "top", "right", "bottom", "x", "y", "width", "height"].map((name) => ({ name, writable: true })),
@@ -76,7 +80,7 @@ const rankingInspectorPlacement = {
76
80
  } as const satisfies Readonly<Record<string, RankingInspectorPlacement>>;
77
81
 
78
82
  function rankingInspector(surface: "column-style" | "tier-style" | "top-three-style"): readonly StudioInspectorFieldDeclaration[] {
79
- return rankingProperties(surface).map((property) => {
83
+ return [...fontInspector.fields, ...rankingProperties(surface).map((property) => {
80
84
  const placement = rankingInspectorPlacement[property.name as keyof typeof rankingInspectorPlacement];
81
85
  if (placement === undefined) throw new Error(`Ranking Studio has no explicit placement for ${property.name}.`);
82
86
  return {
@@ -85,12 +89,13 @@ function rankingInspector(surface: "column-style" | "tier-style" | "top-three-st
85
89
  ...placement,
86
90
  ...(property.summary === undefined ? {} : { summary: property.summary }),
87
91
  };
88
- });
92
+ })];
89
93
  }
90
94
 
91
95
  function rankingStyle(surface: "column-style" | "tier-style" | "top-three-style"): StudioSourceBindingDeclaration {
92
96
  return {
93
97
  name: "style",
98
+ referenced: [fontInspector.binding],
94
99
  recipe: { through: ["recipe"], bindings: rankingProperties(surface).map(({ name, schema, fallback }) => ({
95
100
  name,
96
101
  schema,
@@ -133,6 +138,7 @@ function projectRanking(context: StudioTrackCompanionContext): readonly StudioEn
133
138
  return [{
134
139
  id: `${context.track.outputRef}:entity:${entry.itemId}`,
135
140
  authoredId: entry.itemId,
141
+ renderIds: context.spans.filter((span) => span.subjectId === entry.itemId).map((span) => span.id),
136
142
  ...(semanticSource?.id === undefined ? {} : { markerId: semanticSource.id }),
137
143
  display: {
138
144
  title: label,
@@ -147,7 +153,8 @@ function projectRanking(context: StudioTrackCompanionContext): readonly StudioEn
147
153
  ...(temporal === undefined ? {} : { temporal }),
148
154
  }];
149
155
  }).sort((left, right) => left.startFrame - right.startFrame || left.id.localeCompare(right.id));
150
- return [group, ...reveals];
156
+ const childRenderIds = new Set(reveals.flatMap((entity) => entity.renderIds ?? []));
157
+ return [{ ...group, renderIds: group.renderIds!.filter((id) => !childRenderIds.has(id)) }, ...reveals];
151
158
  }
152
159
 
153
160
  const commonBindings: readonly StudioSourceBindingDeclaration[] = [
@@ -160,7 +167,7 @@ const frameInspector: readonly StudioInspectorFieldDeclaration[] = frameParamete
160
167
  .filter(({ writable }) => writable === true)
161
168
  .map(({ name }) => ({
162
169
  binding: `frame.${name}`, label: title(name), domain: "where",
163
- page: { id: "frame", label: "Frame" }, section: { id: "frame", label: "Frame" }, control: "text",
170
+ page: { id: "frame", label: "Frame" }, section: { id: "frame", label: "Frame" }, control: "number", number: { suffixes: ["%", "px"], step: 1 },
164
171
  }));
165
172
 
166
173
  export const rankingStudioTrackCompanions: readonly StudioTrackCompanion[] = [
@@ -3,6 +3,8 @@ import type {
3
3
  } from "@hypit/protocol";
4
4
 
5
5
  export type BuildPublishedOutput = {
6
+ /** Author-provided display name; the Output identifier remains unchanged. */
7
+ readonly displayName?: string;
6
8
  readonly name: string;
7
9
  readonly ref: LogicalOutputRef;
8
10
  };
@@ -119,6 +119,7 @@ export type RuntimeHostExecution = RuntimeHostControl & RuntimeHostCredentialCon
119
119
  readonly repository: BuildResultRepositoryLocation;
120
120
  readonly title?: string;
121
121
  readonly forwards?: readonly BuildResultForward[];
122
+ readonly resourceReferences?: Readonly<Record<string, import("@hypit/build-result").BuildResultFileRef>>;
122
123
  };
123
124
  }, options?: {
124
125
  readonly follow?: boolean;
@@ -3,6 +3,16 @@
3
3
  The default local Runtime for Hypit. It owns the Worker, scheduler and active SQLite execution
4
4
  worklist. Project-owned Build Results hold finished public Outputs.
5
5
 
6
+ Managed Program preparation writes subprocess stdout and stderr directly to that Program's
7
+ `install.log`, so dependency-download output is readable before installation finishes. Installation
8
+ and startup progress expose `logPath`; failed installation reports retain it with a short error.
9
+ The files belong to the Program's configured state directory. Service output uses `program.log`
10
+ and, on Windows, a separate `program.err.log` for stderr. Keeping installation output separate
11
+ preserves it when Windows opens fresh service logs at startup.
12
+ The CLI retains Program failure reasons, PIDs and log paths in `programs` and `runtime up` reports.
13
+ Human output stays compact for successful preparation; `programs status --verbose` also shows
14
+ ready helpers, and JSON retains the reported details independently of verbosity.
15
+
6
16
  A Runtime Profile selects only the environmental parts that genuinely vary:
7
17
 
8
18
  ```json
@@ -94,6 +104,15 @@ through the same adapter registry as an explicit `@hypit/build-result-s3` select
94
104
  filesystem Repository shortcut.
95
105
  Runtime working Resources remain internal and Build-local; there is no ResourceStore selector. After
96
106
  a Result has an outcome, history is read from the selected repository, not Runtime SQLite.
107
+ The submission passes known Resource references to the Result writer, separately from the execution
108
+ graph. Staging bytes for a running Build does not make them new Result files: external and reused
109
+ resources keep their addresses even when a Producer embeds them inside a new Composite value.
110
+
111
+ While execution advances, Result synchronization publishes newly accepted public Outputs. The
112
+ repository leaves its files or objects untouched when no new public Output is available; internal
113
+ execution progress remains in SQLite. Once execution has a final decision, all accepted public Outputs
114
+ and the outcome are saved before active state and working Resources are removed. This order applies
115
+ to completed, failed and cancelled Builds alike.
97
116
 
98
117
  Saving a finished Result is a separate, idempotent storage action. If that write is interrupted, the
99
118
  Build keeps its already-decided outcome and reports exact operator attention. `hypit result finish
@@ -36,6 +36,7 @@ export type ManagedProgramReport = {
36
36
  export type ManagedProgramProgress = {
37
37
  readonly id: string;
38
38
  readonly phase: "checking" | "installing" | "starting" | "waiting" | "ready";
39
+ readonly logPath?: string;
39
40
  };
40
41
 
41
42
  export type ManagedProgramOptions = LoadRuntimeConfigOptions & {
@@ -97,24 +98,31 @@ async function readPid(root: string, program: ManagedProgram): Promise<number |
97
98
  }
98
99
  }
99
100
 
100
- function run(root: string, command: ManagedProgramCommand): Promise<{ ok: boolean; detail: string }> {
101
- return new Promise((resolve) => {
102
- const child = spawn(command.command, [...command.args], {
103
- cwd: command.cwd ?? root,
104
- env: { ...process.env, ...command.env },
105
- shell: false,
106
- windowsHide: true,
107
- stdio: ["ignore", "pipe", "pipe"],
101
+ async function run(root: string, command: ManagedProgramCommand, logPath: string): Promise<{ ok: boolean; detail: string }> {
102
+ const log = await open(logPath, "a+");
103
+ try {
104
+ const offset = (await log.stat()).size;
105
+ const exit = await new Promise<{ code: number | null; error?: string }>((resolve) => {
106
+ const child = spawn(command.command, [...command.args], {
107
+ cwd: command.cwd ?? root,
108
+ env: { ...process.env, ...command.env },
109
+ shell: false,
110
+ windowsHide: true,
111
+ stdio: ["ignore", log.fd, log.fd],
112
+ });
113
+ child.on("error", (error) => resolve({ code: null, error: error.message }));
114
+ child.on("close", (code) => resolve({ code }));
108
115
  });
109
- let output = "";
110
- child.stdout.on("data", (chunk: Buffer) => { output += chunk.toString(); });
111
- child.stderr.on("data", (chunk: Buffer) => { output += chunk.toString(); });
112
- child.on("error", (error) => resolve({ ok: false, detail: error.message }));
113
- child.on("close", (code) => resolve({
114
- ok: code === 0,
115
- detail: code === 0 ? "" : (output.trim().split("\n").at(-1) ?? `exited ${code}`),
116
- }));
117
- });
116
+ if (exit.code === 0) return { ok: true, detail: "" };
117
+ const end = (await log.stat()).size;
118
+ const start = Math.max(offset, end - 8192);
119
+ const tail = Buffer.alloc(end - start);
120
+ const { bytesRead } = await log.read(tail, 0, tail.length, start);
121
+ const line = tail.subarray(0, bytesRead).toString().trim().split(/\r?\n/u).at(-1);
122
+ return { ok: false, detail: exit.error ?? (line || `exited ${exit.code}`) };
123
+ } finally {
124
+ await log.close();
125
+ }
118
126
  }
119
127
 
120
128
  /**
@@ -254,20 +262,23 @@ async function bringUp(
254
262
  }
255
263
 
256
264
  let installed = false;
265
+ const logPath = join(directory(root, program), "program.log");
266
+ const installLogPath = join(directory(root, program), "install.log");
257
267
  if (program.installation !== undefined) {
258
268
  const installation = await program.installation.probe();
259
269
  if (installation.state !== "ready") {
260
- onProgress?.({ id: program.id, phase: "installing" });
261
270
  await mkdir(directory(root, program), { recursive: true });
271
+ await rotateLog(installLogPath);
272
+ onProgress?.({ id: program.id, phase: "installing", logPath: installLogPath });
262
273
  for (const command of program.installation.commands) {
263
- const result = await run(root, command);
274
+ const result = await run(root, command, installLogPath);
264
275
  if (!result.ok) {
265
- return { ...base, action: "unchanged", state: initial, detail: `${command.command} failed: ${result.detail}` };
276
+ return { ...base, action: "unchanged", state: initial, logPath: installLogPath, detail: `${command.command} failed: ${result.detail}` };
266
277
  }
267
278
  }
268
279
  const after = await program.installation.probe();
269
280
  if (after.state !== "ready") {
270
- return { ...base, action: "unchanged", state: initial, detail: `installation is ${after.state}: ${after.detail}` };
281
+ return { ...base, action: "unchanged", state: initial, logPath: installLogPath, detail: `installation is ${after.state}: ${after.detail}` };
271
282
  }
272
283
  installed = true;
273
284
  }
@@ -276,12 +287,16 @@ async function bringUp(
276
287
  // Nothing to keep running: installation was the whole job.
277
288
  const state = await program.probe();
278
289
  if (state.state === "ready") onProgress?.({ id: program.id, phase: "ready" });
279
- return { ...base, action: state.state === "ready" && installed ? "installed" : "unchanged", state };
290
+ return {
291
+ ...base,
292
+ action: state.state === "ready" && installed ? "installed" : "unchanged",
293
+ state,
294
+ ...(installed ? { logPath: installLogPath } : {}),
295
+ };
280
296
  }
281
297
 
282
- onProgress?.({ id: program.id, phase: "starting" });
298
+ onProgress?.({ id: program.id, phase: "starting", logPath });
283
299
  await mkdir(directory(root, program), { recursive: true });
284
- const logPath = join(directory(root, program), "program.log");
285
300
  await rotateLog(logPath);
286
301
  const log = await open(logPath, "a");
287
302
  try {
@@ -268,6 +268,7 @@ export async function createLocalRuntime(
268
268
  try {
269
269
  const publishedOutputs = request.catalog.publishedOutputs.map((published) => ({
270
270
  name: published.name,
271
+ ...(published.displayName === undefined ? {} : { displayName: published.displayName }),
271
272
  output: published.ref.id,
272
273
  }));
273
274
  const names = new Map<string, string>();
@@ -291,6 +292,7 @@ export async function createLocalRuntime(
291
292
  targets,
292
293
  publishedOutputs,
293
294
  ...(resultRequest.forwards === undefined ? {} : { forwards: resultRequest.forwards }),
295
+ ...(resultRequest.resourceReferences === undefined ? {} : { resourceReferences: resultRequest.resourceReferences }),
294
296
  });
295
297
  } finally {
296
298
  await opened.close?.();