@hypit/hypit 0.1.14 → 0.2.1

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 (90) hide show
  1. package/README.md +1 -1
  2. package/dist/public/browser-capture.d.ts +9 -3
  3. package/dist/public/caption.d.ts +2 -0
  4. package/dist/public/narrative.d.ts +2 -0
  5. package/dist/public/runtime-kit.d.ts +2 -2
  6. package/dist/public/speech.d.ts +2 -0
  7. package/examples/provider-package/hypit.runtime.json +2 -2
  8. package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
  9. package/package.json +2 -1
  10. package/packages/browser-capture/README.md +9 -2
  11. package/packages/browser-capture/package.json +5 -0
  12. package/packages/browser-capture/src/browser.ts +22 -8
  13. package/packages/browser-capture/src/index.ts +8 -2
  14. package/packages/caption/README.md +1 -1
  15. package/packages/caption/src/display.ts +1 -0
  16. package/packages/caption-fine/README.md +19 -5
  17. package/packages/caption-fine/package.json +1 -0
  18. package/packages/caption-fine/src/joined-box.ts +114 -0
  19. package/packages/caption-fine/src/manifest.ts +1 -1
  20. package/packages/caption-fine/src/render.ts +99 -92
  21. package/packages/caption-fine/src/spacing.ts +8 -61
  22. package/packages/caption-fine-studio/src/index.ts +2 -4
  23. package/packages/cli/README.md +3 -2
  24. package/packages/cli/src/arguments.ts +2 -2
  25. package/packages/cli/src/command.ts +1 -1
  26. package/packages/cli/src/commands/environment.ts +9 -2
  27. package/packages/cli/src/machine-view.ts +1 -1
  28. package/packages/cli/src/output.ts +1 -0
  29. package/packages/credential-store-file/README.md +3 -1
  30. package/packages/credential-store-os/README.md +4 -0
  31. package/packages/credential-store-os/src/store.ts +5 -1
  32. package/packages/credential-store-platform/README.md +62 -0
  33. package/packages/credential-store-platform/package.json +23 -0
  34. package/packages/credential-store-platform/src/activation.ts +39 -0
  35. package/packages/credential-store-platform/src/index.ts +1 -0
  36. package/packages/credential-store-platform/src/store.ts +89 -0
  37. package/packages/narrative/README.md +3 -1
  38. package/packages/narrative/src/schema.ts +1 -0
  39. package/packages/narrative/src/types.ts +2 -0
  40. package/packages/provider-whisperx-local/README.md +26 -10
  41. package/packages/provider-whisperx-local/src/activation.ts +10 -1
  42. package/packages/provider-whisperx-local/src/program.ts +8 -4
  43. package/packages/runtime-host-node/src/index.ts +1 -0
  44. package/packages/runtime-kit/README.md +6 -2
  45. package/packages/runtime-kit/src/index.ts +2 -2
  46. package/packages/runtime-local/README.md +6 -1
  47. package/packages/runtime-local/src/host.ts +2 -0
  48. package/packages/runtime-local/src/index.ts +1 -0
  49. package/packages/runtime-local/src/programs.ts +53 -17
  50. package/packages/script/README.md +81 -34
  51. package/packages/script/bin/migrate-0.2.mjs +90 -0
  52. package/packages/script/src/edit.ts +28 -60
  53. package/packages/script/src/lexical.ts +59 -130
  54. package/packages/script/src/manifest.ts +11 -11
  55. package/packages/script/src/narrative.ts +4 -2
  56. package/packages/script/src/parser.ts +140 -193
  57. package/packages/script/src/types.ts +2 -3
  58. package/packages/studio/src/ui/code.ts +1 -1
  59. package/packages/studio/src/ui/main.ts +2 -2
  60. package/packages/studio/src/ui/markers.ts +3 -3
  61. package/packages/studio/src/ui/selection.ts +1 -1
  62. package/packages/studio/src/ui/syntax.ts +42 -21
  63. package/packages/temporal-markup/README.md +1 -1
  64. package/packages/video-cli/README.md +9 -5
  65. package/packages/video-cli/src/capture.ts +17 -2
  66. package/packages/video-cli/src/creation.ts +4 -6
  67. package/packages/video-cli/src/media.ts +10 -3
  68. package/packages/whisperx/README.md +56 -3
  69. package/packages/whisperx/src/component.ts +2 -5
  70. package/packages/whisperx/src/evidence.ts +4 -3
  71. package/packages/whisperx/src/index.ts +1 -0
  72. package/packages/whisperx/src/manifest.ts +2 -3
  73. package/packages/whisperx/src/surface.ts +3 -6
  74. package/packages/whisperx/src/types.ts +9 -1
  75. package/packages/yt-dlp/README.md +9 -3
  76. package/packages/yt-dlp/package.json +2 -1
  77. package/packages/yt-dlp/src/download.ts +10 -24
  78. package/packages/yt-dlp/src/environment.ts +36 -0
  79. package/packages/yt-dlp/src/index.ts +1 -0
  80. package/services/whisperx/README.md +45 -12
  81. package/services/whisperx/src/hypit_whisperx_service/application.py +3 -0
  82. package/services/whisperx/src/hypit_whisperx_service/check.py +7 -0
  83. package/services/whisperx/src/hypit_whisperx_service/config.py +7 -0
  84. package/services/whisperx/src/hypit_whisperx_service/engine.py +21 -5
  85. package/services/whisperx/src/hypit_whisperx_service/models.py +131 -0
  86. package/services/whisperx/src/hypit_whisperx_service/prepare.py +21 -3
  87. package/services/whisperx/src/hypit_whisperx_service/resources.py +19 -3
  88. package/services/yt-dlp/README.md +14 -15
  89. package/services/yt-dlp/pyproject.toml +1 -1
  90. package/services/yt-dlp/uv.lock +513 -2
@@ -8,19 +8,22 @@ import type {
8
8
  VisualColorPaint,
9
9
  VisualElement,
10
10
  VisualKeyframe,
11
+ VisualProgramElement,
11
12
  VisualStyleDeclaration,
12
13
  VisualTextElement,
13
14
  VisualTextPaintLayer,
14
15
  VisualTrack,
15
16
  } from "@hypit/composition";
16
- import type { CaptionAlignmentUnit, CaptionDocument } from "@hypit/narrative";
17
+ import type { CaptionAlignmentUnit, CaptionDocument, CaptionDisplayWord } from "@hypit/narrative";
17
18
  import { assertProgramSpaceIdentity, programSpaceFrameCount } from "@hypit/program-space";
18
19
  import { assertSpatialRegionTimeline } from "@hypit/spatial";
19
20
  import type { CanvasSpace, SpatialFrame, SpatialRegionTimeline } from "@hypit/spatial";
20
21
 
21
22
  import { assertFineCaptionParameters, FINE_CAPTION_FAMILY } from "./style.js";
22
23
  import { assertFineCaptionSchedule } from "./schedule.js";
23
- import { joinSurfaces, uniformGap, wordGapBetween, wordGaps } from "./spacing.js";
24
+ import { uniformGap, wordGaps } from "./spacing.js";
25
+ import { browserProgram } from "@hypit/hyperframes";
26
+ import { joinedBoxSetup } from "./joined-box.js";
24
27
  import type {
25
28
  FineCaptionActiveUnderline,
26
29
  FineCaptionGlyphPaint,
@@ -621,20 +624,17 @@ function trackedPlacementAnimation(
621
624
  return { keyframes };
622
625
  }
623
626
 
624
- function structuralRowCount(
625
- atoms: readonly CaptionAlignmentUnit[],
626
- maxWordsPerLine: number,
627
- ): number {
628
- let rows = atoms.length === 0 ? 0 : 1;
627
+ function structuralRowStarts(atoms: readonly CaptionAlignmentUnit[], maxWordsPerLine?: number): ReadonlySet<number> {
628
+ const starts = new Set<number>();
629
629
  let wordsOnRow = 0;
630
- for (const atom of atoms) {
631
- if (wordsOnRow > 0 && wordsOnRow + atom.wordIds.length > maxWordsPerLine) {
632
- rows += 1;
630
+ for (const [index, atom] of atoms.entries()) {
631
+ if (index === 0 || (maxWordsPerLine !== undefined && wordsOnRow > 0 && wordsOnRow + atom.wordIds.length > maxWordsPerLine)) {
632
+ starts.add(index);
633
633
  wordsOnRow = 0;
634
634
  }
635
635
  wordsOnRow += atom.wordIds.length;
636
636
  }
637
- return rows;
637
+ return starts;
638
638
  }
639
639
 
640
640
  /**
@@ -665,7 +665,7 @@ function cueElements(
665
665
  atoms: readonly CaptionAlignmentUnit[],
666
666
  atomFrames: ReadonlyMap<string, { readonly start: number; readonly end: number }>,
667
667
  parameters: FineCaptionParameters,
668
- wordText: ReadonlyMap<string, string>,
668
+ wordText: ReadonlyMap<string, CaptionDisplayWord>,
669
669
  durationFrames: number,
670
670
  styleId: string,
671
671
  trackedPlacement?: {
@@ -673,7 +673,7 @@ function cueElements(
673
673
  readonly canvas: CanvasSpace;
674
674
  },
675
675
  ): VisualElement[] {
676
- type UnorderedVisualElement = Omit<VisualBoxElement, "order"> | Omit<VisualTextElement, "order">;
676
+ type UnorderedVisualElement = Omit<VisualBoxElement, "order"> | Omit<VisualTextElement, "order"> | Omit<VisualProgramElement, "order">;
677
677
  const elements: VisualElement[] = [];
678
678
  let order = 0;
679
679
  const push = (element: UnorderedVisualElement): void => {
@@ -694,21 +694,29 @@ function cueElements(
694
694
  const cueLoop = parameters.motion.loopTarget === "cue" ? loopAnimation(parameters, durationFrames) : undefined;
695
695
  const activationFrames = exclusiveActivationFrames(atoms, atomFrames);
696
696
  const gapPx = `${compactNumber(parameters.layout.wordGapPx)}px`;
697
- const atomSurfaces = atoms.map((atom) => atom.wordIds.map((wordId) => wordText.get(wordId) ?? ""));
698
- // A Cue whose boundaries all agree carries one `column-gap`, which is also what keeps a row that
699
- // wraps from opening on a margin. A Cue that mixes scripts spaces each element instead.
700
- const atomGaps = atomSurfaces.map((surfaces, index) => {
701
- const previous = atomSurfaces[index - 1];
702
- return previous === undefined ? false : wordGapBetween(previous.at(-1) ?? "", surfaces[0] ?? "");
703
- });
704
- const cueGap = uniformGap(atomGaps);
697
+ const atomWords = atoms.map(atom => atom.wordIds.map(wordId => {
698
+ const word = wordText.get(wordId);
699
+ if (!word) throw new Error(`Fine Caption Atom references unknown word ${wordId}`);
700
+ return word;
701
+ }));
702
+ const atomSurfaces = atomWords.map(words => words.map(word => word.text));
703
+ const atomGaps = atomWords.map((words, index) => index > 0 && words[0]!.separatorBefore === " ");
705
704
  // The Visual IR carries physical margins, so the leading edge follows the Cue's own direction.
706
705
  const marginStart = parameters.layout.direction === "rtl" ? "margin-right" : "margin-left";
707
706
  const spacedStyle = (spaced: boolean): readonly VisualStyleDeclaration[] =>
708
707
  spaced ? [{ name: marginStart, value: gapPx }] : [];
708
+ const atomFlow = (words: readonly CaptionDisplayWord[]): VisualStyleDeclaration[] => [
709
+ ...(uniformGap(wordGaps(words)) === true ? [{ name: "column-gap", value: gapPx }] : []),
710
+ { name: "display", value: "inline-flex" },
711
+ { name: "min-width", value: "0" },
712
+ { name: "overflow-wrap", value: "anywhere" },
713
+ { name: "white-space", value: "normal" },
714
+ { name: "word-break", value: parameters.layout.wrap === "grapheme" ? "break-all" : "normal" },
715
+ ];
709
716
  const fonts = parameters.typography.exactFonts;
717
+ const rowStarts = structuralRowStarts(atoms, parameters.layout.maxWordsPerLine);
710
718
  if (parameters.layout.maxLines !== undefined) {
711
- const rows = structuralRowCount(atoms, parameters.layout.maxWordsPerLine!);
719
+ const rows = rowStarts.size;
712
720
  if (rows > parameters.layout.maxLines) {
713
721
  throw new Error(`Fine Caption Style ${styleId} constructs ${rows} rows for a Cue whose maximum is ${parameters.layout.maxLines}`);
714
722
  }
@@ -766,10 +774,10 @@ function cueElements(
766
774
  { name: "border-radius", value: `${compactNumber(parameters.cueBox.radiusPx)}px` },
767
775
  { name: "border-style", value: "solid" },
768
776
  { name: "border-width", value: `${compactNumber(parameters.cueBox.borderWidthPx)}px` },
769
- ...(cueGap === true ? [{ name: "column-gap", value: gapPx }] as const : []),
770
777
  { name: "direction", value: parameters.layout.direction },
771
- { name: "display", value: "flex" },
772
- { name: "flex-wrap", value: "wrap" },
778
+ { name: "display", value: "block" },
779
+ { name: "font-size", value: "0px" },
780
+ { name: "line-height", value: "0" },
773
781
  { name: "justify-content", value: parameters.layout.textAlign === "left" ? "flex-start"
774
782
  : parameters.layout.textAlign === "right" ? "flex-end" : "center" },
775
783
  { name: "max-width", value: "100%" },
@@ -781,64 +789,73 @@ function cueElements(
781
789
  ],
782
790
  });
783
791
 
792
+ const pushSeparator = (id: string, parent: string): void => push({
793
+ id, parent, kind: "text", text: " ", fonts,
794
+ style: [
795
+ { name: "display", value: "inline" },
796
+ { name: "font-size", value: "0px" },
797
+ { name: "letter-spacing", value: gapPx },
798
+ { name: "line-height", value: "0" },
799
+ { name: "white-space", value: "normal" },
800
+ ],
801
+ });
802
+
784
803
  if (parameters.activeBox.mode === "trail" && parameters.activeBox.continuity === "joined") {
785
- for (const [prefixIndex, atom] of atoms.entries()) {
786
- const timing = atomFrames.get(atom.id);
787
- if (timing === undefined) throw new Error(`Fine Caption is missing timing for Atom ${atom.id}`);
788
- const next = atoms[prefixIndex + 1];
804
+ // Fine owns this drawing program. The renderer only executes its existing browser-program
805
+ // format; it learns nothing about captions, activation units or their layout rules.
806
+ push({
807
+ id: "joined-boxes", parent: "cue", kind: "program",
808
+ style: [
809
+ { name: "position", value: "absolute" }, { name: "inset", value: "0" },
810
+ { name: "padding", value: `${compactNumber(parameters.cueBox.paddingYPx)}px ${compactNumber(parameters.cueBox.paddingXPx)}px` },
811
+ ],
812
+ program: browserProgram({
813
+ html: "{{joined-layout}}" + atoms.map((_, index) => `{{joined-box-${index + 1}}}`).join(""),
814
+ setup: joinedBoxSetup,
815
+ css: ":scope { pointer-events: none; }",
816
+ data: {
817
+ wordCounts: atomWords.map(words => words.length),
818
+ background: parameters.activeBox.background, borderColor: parameters.activeBox.borderColor,
819
+ borderWidth: parameters.activeBox.borderWidthPx, radius: parameters.activeBox.radiusPx,
820
+ paddingX: parameters.activeBox.paddingXPx, paddingY: parameters.activeBox.paddingYPx,
821
+ },
822
+ }),
823
+ });
824
+ push({ id: "joined-layout", parent: "joined-boxes", kind: "box",
825
+ style: [{ name: "visibility", value: "hidden" }],
826
+ attributes: [{ name: "data-fine-box-layout", value: "" }],
827
+ });
828
+ // The complete stationary Cue supplies layout, including the inactive suffix. Its actual
829
+ // word fragments determine the background; decoration contributes no width to text flow.
830
+ for (const [index, words] of atomWords.entries()) {
831
+ if (index > 0 && rowStarts.has(index)) push({ id: `joined-break-${index}`, parent: "joined-layout", kind: "box",
832
+ style: [{ name: "display", value: "block" }, { name: "height", value: "0" }],
833
+ });
834
+ if (!rowStarts.has(index) && words[0]!.separatorBefore === " ") pushSeparator(`joined-gap-${index}`, "joined-layout");
835
+ const groupId = `joined-group-${index}`;
836
+ push({ id: groupId, parent: "joined-layout", kind: "box", style: [...atomFlow(words), { name: "max-width", value: "100%" }] });
837
+ const gaps = wordGaps(words);
838
+ for (const [wordIndex, word] of words.entries()) {
839
+ push({ id: `${groupId}-word-${wordIndex}`, parent: groupId, kind: "text", text: word.text, fonts,
840
+ style: [...transparentGlyphStyle(parameters),
841
+ ...(uniformGap(gaps) === undefined ? spacedStyle(gaps[wordIndex] ?? false) : [])],
842
+ attributes: [{ name: "data-fine-box-word", value: "" }],
843
+ });
844
+ }
845
+ }
846
+ for (const [index, atom] of atoms.entries()) {
847
+ const timing = atomFrames.get(atom.id)!;
848
+ const next = atoms[index + 1];
789
849
  const nextStart = next === undefined ? durationFrames : atomFrames.get(next.id)?.start;
790
850
  if (nextStart === undefined) throw new Error("Fine Caption is missing timing for the next Atom");
791
- // A no-break space inside an atom keeps its words on one row; the boundaries between atoms
792
- // take an ordinary space. Both are spaced only where the surfaces meeting there call for one.
793
- const prefixText = joinSurfaces(
794
- atoms.slice(0, prefixIndex + 1).map((prefixAtom, index) => {
795
- for (const wordId of prefixAtom.wordIds) {
796
- if (!wordText.has(wordId)) throw new Error(`Fine Caption Atom references unknown word ${wordId}`);
797
- }
798
- return joinSurfaces(atomSurfaces[index] ?? [], "\u00A0");
799
- }),
800
- " ",
801
- );
802
- const layerId = `joined-box-${prefixIndex + 1}`;
803
- push({
804
- id: layerId,
805
- parent: "cue",
806
- kind: "box",
807
- style: [
808
- { name: "box-sizing", value: "border-box" },
809
- { name: "direction", value: parameters.layout.direction },
810
- { name: "inset", value: "0" },
811
- { name: "padding", value: `${compactNumber(parameters.cueBox.paddingYPx)}px ${compactNumber(parameters.cueBox.paddingXPx)}px` },
812
- { name: "position", value: "absolute" },
813
- { name: "text-align", value: parameters.layout.textAlign },
814
- ],
851
+ push({ id: `joined-box-${index + 1}`, parent: "joined-boxes", kind: "box",
852
+ style: [{ name: "position", value: "absolute" }, { name: "inset", value: "0" }],
815
853
  animation: activeBoxAnimation(parameters, timing.start, nextStart, durationFrames, "current"),
816
854
  attributes: [{ name: "data-caption-active-box", value: "joined" }],
817
855
  });
818
- push({
819
- id: `${layerId}-text`,
820
- parent: layerId,
821
- kind: "text",
822
- text: prefixText,
823
- style: [
824
- ...transparentGlyphStyle(parameters).filter(({ name }) => name !== "white-space"),
825
- { name: "-webkit-box-decoration-break", value: "clone" },
826
- { name: "background", value: parameters.activeBox.background },
827
- { name: "border-color", value: parameters.activeBox.borderColor },
828
- { name: "border-radius", value: `${compactNumber(parameters.activeBox.radiusPx)}px` },
829
- { name: "border-style", value: "solid" },
830
- { name: "border-width", value: `${compactNumber(parameters.activeBox.borderWidthPx)}px` },
831
- { name: "box-decoration-break", value: "clone" },
832
- { name: "display", value: "inline" },
833
- { name: "padding", value: `${compactNumber(parameters.activeBox.paddingYPx)}px ${compactNumber(parameters.activeBox.paddingXPx)}px` },
834
- { name: "white-space", value: "normal" },
835
- ],
836
- fonts,
837
- });
838
856
  }
839
857
  }
840
858
 
841
- let wordsOnLine = 0;
842
859
  for (const [atomIndex, atom] of atoms.entries()) {
843
860
  const timing = atomFrames.get(atom.id);
844
861
  if (timing === undefined) throw new Error(`Fine Caption is missing timing for Atom ${atom.id}`);
@@ -848,22 +865,20 @@ function cueElements(
848
865
  const surfaces = atomSurfaces[atomIndex] ?? [];
849
866
  // The base glyphs and the activated copy stacked over them are laid out from this one list, so
850
867
  // the karaoke wipe keeps sitting on the letterforms it reveals.
851
- const gaps = wordGaps(surfaces);
868
+ const gaps = wordGaps(atomWords[atomIndex]!);
852
869
  const wordGap = uniformGap(gaps);
853
870
  const entryId = `${atomId}-entry`;
854
871
  const loopId = `${atomId}-loop`;
855
872
  const responseId = `${atomId}-response`;
856
- if (parameters.layout.maxWordsPerLine !== undefined
857
- && wordsOnLine > 0
858
- && wordsOnLine + atom.wordIds.length > parameters.layout.maxWordsPerLine) {
873
+ if (atomIndex > 0 && rowStarts.has(atomIndex)) {
859
874
  push({
860
875
  id: `line-break-${atomIndex}`,
861
876
  parent: "cue",
862
877
  kind: "box",
863
- style: [{ name: "flex-basis", value: "100%" }, { name: "height", value: "0" }],
878
+ style: [{ name: "display", value: "block" }, { name: "height", value: "0" }],
864
879
  });
865
- wordsOnLine = 0;
866
880
  }
881
+ if (!rowStarts.has(atomIndex) && atomGaps[atomIndex]) pushSeparator(`gap-${atomIndex}`, "cue");
867
882
  const entryAnimation = atomLifecycleAnimation(parameters, timing.start, timing.end, durationFrames);
868
883
  const atomLoop = parameters.motion.loopTarget === "active-atom"
869
884
  ? loopAnimation(parameters, durationFrames, activationTiming) : undefined;
@@ -880,8 +895,6 @@ function cueElements(
880
895
  style: [
881
896
  { name: "display", value: "inline-flex" },
882
897
  { name: "min-width", value: "0" },
883
- // A row that wraps opens at its own edge, so an atom that starts one carries no margin.
884
- ...(cueGap === undefined && wordsOnLine > 0 ? spacedStyle(atomGaps[atomIndex] ?? false) : []),
885
898
  { name: "transform-origin", value: "center center" },
886
899
  ],
887
900
  ...(entryAnimation === undefined ? {} : { animation: entryAnimation }),
@@ -913,13 +926,8 @@ function cueElements(
913
926
  parent: responseId,
914
927
  kind: "box",
915
928
  style: [
916
- ...(wordGap === true ? [{ name: "column-gap", value: gapPx }] as const : []),
917
- { name: "display", value: "inline-flex" },
918
- { name: "min-width", value: "0" },
919
- { name: "overflow-wrap", value: "anywhere" },
929
+ ...atomFlow(atomWords[atomIndex]!),
920
930
  { name: "position", value: "relative" },
921
- { name: "white-space", value: "normal" },
922
- { name: "word-break", value: parameters.layout.wrap === "grapheme" ? "break-all" : "normal" },
923
931
  ],
924
932
  attributes: [{ name: "data-caption-atom", value: atom.id }],
925
933
  });
@@ -996,7 +1004,7 @@ function cueElements(
996
1004
  }
997
1005
  };
998
1006
  for (const [wordIndex, wordId] of atom.wordIds.entries()) {
999
- const text = wordText.get(wordId);
1007
+ const text = wordText.get(wordId)?.text;
1000
1008
  if (text === undefined) throw new Error(`Fine Caption Atom references unknown word ${wordId}`);
1001
1009
  pushText({
1002
1010
  id: `${atomId}-base-${wordIndex + 1}`,
@@ -1037,7 +1045,7 @@ function cueElements(
1037
1045
  attributes: [{ name: kind === "glyph" ? "data-caption-karaoke" : "data-caption-active-underline", value: mode }],
1038
1046
  });
1039
1047
  for (const [wordIndex, wordId] of atom.wordIds.entries()) {
1040
- const text = wordText.get(wordId)!;
1048
+ const text = wordText.get(wordId)!.text;
1041
1049
  pushText({
1042
1050
  id: `${activeId}-${wordIndex + 1}`,
1043
1051
  parent: activeId,
@@ -1060,7 +1068,6 @@ function cueElements(
1060
1068
  if (parameters.activeUnderline.mode !== "off") {
1061
1069
  addActivatedTextLayer("underline", parameters.activeUnderline.mode, "underline");
1062
1070
  }
1063
- wordsOnLine += atom.wordIds.length;
1064
1071
  }
1065
1072
  return elements;
1066
1073
  }
@@ -1081,7 +1088,7 @@ export function renderFineCaption(
1081
1088
  throw new Error("Fine Caption inputs belong to different Timelines or Narratives");
1082
1089
  }
1083
1090
  const styles = new Map(program.styles.map((style) => [style.id, style]));
1084
- const wordText = new Map(document.words.map((word) => [word.id, word.text]));
1091
+ const wordText = new Map(document.words.map((word) => [word.id, word]));
1085
1092
  const atomById = new Map(document.units.map((atom) => [atom.id, atom]));
1086
1093
  for (const style of styles.values()) {
1087
1094
  if (style.rendering === null) continue;
@@ -1,69 +1,16 @@
1
- /**
2
- * Where a word gap belongs between two adjacent display surfaces.
3
- *
4
- * A Display Word is one lexical unit, and in Han, Hiragana and Katakana that unit is a single
5
- * character. A gap applied to every boundary therefore sets Chinese and Japanese as though every
6
- * character were a word, so each boundary decides its own gap from the two characters that meet
7
- * across it.
8
- *
9
- * The classes follow pangu.js: a CJK character beside a half-width letter, digit, bracket or
10
- * operator takes one space. The gap pangu inserts is an ordinary space, so a boundary either
11
- * carries the Style's `word-gap` or carries nothing.
12
- */
1
+ import type { CaptionDisplayWord } from "@hypit/narrative";
13
2
 
14
- /** The three scripts the Script tokenizer treats as one character per lexical unit. */
15
- const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u;
3
+ type Surface = Pick<CaptionDisplayWord, "text" | "separatorBefore">;
16
4
 
17
- /**
18
- * Full-width punctuation, including the bracket, quotation and dash pairs.
19
- *
20
- * These glyphs are drawn inside a full-width em box with their own side bearing, so a gap beside
21
- * one paints a second space over the one the font already carries. The ranges cover CJK
22
- * punctuation, the full-width ASCII forms, the curly quotes and the ellipsis, and stop short of
23
- * the full-width digits and letters at `0-9`, `A-Z` and `a-z`, which
24
- * are ordinary word characters.
25
- */
26
- const FULL_WIDTH_PUNCTUATION =
27
- /[、-〃〈-】〔-〟!-/:-@[-`{-・‘’“”—…]/u;
28
-
29
- /**
30
- * Half-width brackets and quotation marks take their space on the outside of the pair, which falls
31
- * out of reading the boundary characters: `displayWordSurfaces` attaches an opener to the surface
32
- * that follows it and a closer to the surface before it, so `中文(注)中文` meets `(` on the right
33
- * of one boundary and `)` on the left of the next, and both take a gap.
34
- *
35
- * A boundary with no CJK character on either side keeps the gap it has today. Latin spacing is not
36
- * what this decides.
37
- */
38
- export function wordGapBetween(previous: string, next: string): boolean {
39
- const left = [...previous].at(-1);
40
- const right = [...next].at(0);
41
- if (left === undefined || right === undefined) return false;
42
- if (FULL_WIDTH_PUNCTUATION.test(left) || FULL_WIDTH_PUNCTUATION.test(right)) return false;
43
- return !(CJK.test(left) && CJK.test(right));
44
- }
45
-
46
- /** Every boundary in order. The first surface opens the run, so its entry is always `false`. */
47
- export function wordGaps(surfaces: readonly string[]): readonly boolean[] {
48
- return surfaces.map((surface, index) => {
49
- const previous = surfaces[index - 1];
50
- return previous === undefined ? false : wordGapBetween(previous, surface);
51
- });
5
+ /** The first displayed word starts a line; all other gaps come from authored content. */
6
+ export function wordGaps(words: readonly Surface[]): readonly boolean[] {
7
+ return words.map((word, index) => index > 0 && word.separatorBefore === " ");
52
8
  }
53
9
 
54
- /**
55
- * A run whose boundaries all agree carries one `column-gap` on its container. A mixed run cannot,
56
- * because `column-gap` is uniform, and takes a margin per element instead.
57
- */
10
+ /** A uniform run can use column-gap; a mixed run requires individual separators. */
58
11
  export function uniformGap(gaps: readonly boolean[]): boolean | undefined {
59
12
  const boundaries = gaps.slice(1);
60
13
  if (boundaries.length === 0) return false;
61
- if (boundaries.every((gap) => gap)) return true;
62
- return boundaries.some((gap) => gap) ? undefined : false;
63
- }
64
-
65
- /** Join surfaces back into prose, spacing only the boundaries that carry a gap. */
66
- export function joinSurfaces(surfaces: readonly string[], separator: string): string {
67
- const gaps = wordGaps(surfaces);
68
- return surfaces.map((surface, index) => (gaps[index] ? `${separator}${surface}` : surface)).join("");
14
+ if (boundaries.every(gap => gap)) return true;
15
+ return boundaries.some(gap => gap) ? undefined : false;
69
16
  }
@@ -114,10 +114,8 @@ function cueText(document: CaptionDocument | undefined, unitIds: readonly string
114
114
  const selected = new Set(unitIds);
115
115
  return document.words
116
116
  .filter((word) => selected.has(word.unitId))
117
- .map((word) => word.text)
118
- .join(" ")
119
- .replace(/([\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}])\s+(?=[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}])/gu, "$1")
120
- .trim();
117
+ .map((word, index) => (index === 0 ? "" : word.separatorBefore) + word.text)
118
+ .join("");
121
119
  }
122
120
 
123
121
  export function projectCaptionContents(context: StudioTrackCompanionContext): readonly StudioEntityDraft[] {
@@ -153,13 +153,14 @@ paths. A deleted Source or directory remains queryable: only its existing ancest
153
153
  the missing path suffix is retained. This is local argument handling, with no saved alias inventory.
154
154
  CLI, Studio and creation tools call that same package; the CLI is not another environment owner.
155
155
 
156
- `doctor`, `programs up|status|down`, and `runtime up` accept repeated `--endpoint <instance>` values.
156
+ `doctor`, `programs prepare|up|status|down`, and `runtime up` accept repeated `--endpoint <instance>` values.
157
157
  The same Endpoint scope reaches package preparation and Program operations. Omission means the whole
158
158
  Profile. Build preflight instead uses the Endpoints resolved for that Build's concrete requests.
159
159
  An unrelated offered capability does not add another credential or Program requirement.
160
160
  Program rows show the configured Endpoint selector alongside an internal Program ID when they differ.
161
161
  The `programs` JSON `ok` field reports whether this command succeeded; `ready` reports service
162
- readiness. Successful stopping can therefore report `ok: true` with `ready: false`. A declined stop remains visible even
162
+ readiness for `up`/`status`, and preparation readiness for `prepare`. The latter does not start the
163
+ service. Successful stopping can therefore report `ok: true` with `ready: false`. A declined stop remains visible even
163
164
  while the service is still preparing and cannot yet answer its health probe.
164
165
 
165
166
  Upstream package installation reports an `install.log` path at preparation time. Exact package
@@ -349,8 +349,8 @@ function parseRuntimeCommand(tail: readonly string[]): RuntimeSelectionCommand |
349
349
 
350
350
  function parseProgramsCommand(tail: readonly string[]): ProgramsCommand {
351
351
  const [action, ...values] = tail;
352
- if (action !== "up" && action !== "down" && action !== "status") {
353
- throw new Error("programs takes up, down or status");
352
+ if (action !== "prepare" && action !== "up" && action !== "down" && action !== "status") {
353
+ throw new Error("programs takes prepare, up, down or status");
354
354
  }
355
355
  const [profile, rest] = optionalPositional(values);
356
356
  const options = commandOptions(`programs ${action}`, rest,
@@ -146,7 +146,7 @@ export type ProgramsCommand =
146
146
  | (CommandBase & RuntimeOption & {
147
147
  readonly command: "programs";
148
148
  readonly endpoints?: readonly string[];
149
- readonly action: "down" | "status";
149
+ readonly action: "prepare" | "down" | "status";
150
150
  readonly limit: number;
151
151
  });
152
152
 
@@ -175,7 +175,7 @@ export async function runEnvironmentCommand(input: {
175
175
  if (runtimeProfile === undefined) throw new Error("programs requires a Runtime Profile");
176
176
  const profile = resolve(runtimeProfile);
177
177
  const host = await runtimeHost(profile);
178
- if (args.action === "up") {
178
+ if (args.action === "up" || args.action === "prepare") {
179
179
  await host.prepare({ ...(args.endpoints === undefined ? {} : { endpoints: args.endpoints }), ...(reportPackageProgress === undefined ? {} : { onProgress: reportPackageProgress }) });
180
180
  }
181
181
  const controller = await runtimeController(profile);
@@ -185,6 +185,11 @@ export async function runEnvironmentCommand(input: {
185
185
  ...(args.maxWaitMs === undefined ? {} : { maxWaitMs: args.maxWaitMs }),
186
186
  ...(reportProgramProgress === undefined ? {} : { onProgress: reportProgramProgress }),
187
187
  })
188
+ : args.action === "prepare"
189
+ ? await controller.programs.prepare({
190
+ ...(args.endpoints === undefined ? {} : { endpoints: args.endpoints }),
191
+ ...(reportProgramProgress === undefined ? {} : { onProgress: reportProgramProgress }),
192
+ })
188
193
  : args.action === "down"
189
194
  ? await controller.programs.down(args.endpoints === undefined ? {} : { endpoints: args.endpoints })
190
195
  : await controller.programs.report(args.endpoints === undefined ? {} : { endpoints: args.endpoints });
@@ -199,7 +204,9 @@ export async function runEnvironmentCommand(input: {
199
204
  const urgent = relevant.filter(needsAttention);
200
205
  const shownPrograms = [...urgent, ...relevant.filter((item) => !needsAttention(item)).slice(0, Math.max(0, args.limit - urgent.length))];
201
206
  const omittedPrograms = relevant.length - shownPrograms.length;
202
- const title = args.action === "up"
207
+ const title = args.action === "prepare"
208
+ ? lifecycleOk ? "External program resources prepared" : "External program preparation needs attention"
209
+ : args.action === "up"
203
210
  ? lifecycleOk ? "External programs ready" : "External programs need attention"
204
211
  : args.action === "down"
205
212
  ? lifecycleOk ? "External programs stopped" : "External program stop needs attention"
@@ -20,7 +20,7 @@ export type OperationalMachineView =
20
20
  | { readonly format: "hypit.cli-runtime-selection@1"; readonly selected: boolean; readonly removed?: boolean; readonly profile?: string; readonly project?: string }
21
21
  | { readonly format: "hypit.cli-paths@1"; readonly project: string; readonly projectState: string; readonly profileSource: "argument" | "project" | "none"; readonly selectionFile: string; readonly profile?: string; readonly runtimeData?: string; readonly hostState: string; readonly machinePackages: string; readonly distribution?: string }
22
22
  | { readonly format: "hypit.cli-package@1"; readonly action: "install" | "status"; readonly package: string; readonly ready: boolean }
23
- | { readonly format: "hypit.cli-programs@1"; readonly action: "up" | "down" | "status"; readonly ok: boolean; readonly ready: boolean; readonly programCount: number; readonly readyCount: number; readonly programs: readonly ProgramStateView[]; readonly omittedPrograms?: number }
23
+ | { readonly format: "hypit.cli-programs@1"; readonly action: "prepare" | "up" | "down" | "status"; readonly ok: boolean; readonly ready: boolean; readonly programCount: number; readonly readyCount: number; readonly programs: readonly ProgramStateView[]; readonly omittedPrograms?: number }
24
24
  | { readonly format: "hypit.cli-runtime-up@1"; readonly ready: boolean; readonly worker: string; readonly preparedPackages: number; readonly programs: { readonly total: number; readonly ready: number; readonly items: readonly ProgramStateView[] } }
25
25
  | { readonly format: "hypit.cli-runtime-logs@1"; readonly lines: readonly string[]; readonly totalLines: number; readonly omittedLines: number; readonly path?: string }
26
26
  | { readonly format: "hypit.cli-runtime-down@1"; readonly worker: string }
@@ -878,6 +878,7 @@ function commandHelp(topic: string, colors: Palette): readonly string[] | undefi
878
878
  colors.accent(colors.strong("hypit programs")),
879
879
  colors.dim("Prepare and operate external programs declared by Endpoints in one Runtime Profile."),
880
880
  "",
881
+ " hypit programs prepare [--runtime <profile>] [--endpoint <instance>] # resources only; does not start services",
881
882
  " hypit programs up [--runtime <profile>] [--workspace <project>] [--max-wait-ms <ms>]",
882
883
  " hypit programs status [--runtime <profile>] [--workspace <project>]",
883
884
  " hypit programs down [--runtime <profile>] [--workspace <project>]",
@@ -4,7 +4,9 @@ A writable CredentialStore for a deployment that explicitly chooses local file s
4
4
  Linux without an OS credential locker. It stores unencrypted credential values in an owner-private
5
5
  directory outside the video project. On Windows the selected directory must have a private user ACL;
6
6
  POSIX file modes do not set Windows ACLs. Choose `@hypit/credential-store-os` when using macOS Keychain
7
- or Windows Credential Locker, or `@hypit/credential-store-env` for externally supplied, read-only values.
7
+ or Windows Credential Locker, `@hypit/credential-store-platform` for OS storage on macOS/Windows
8
+ and this file Store on Linux or `@hypit/credential-store-env` for externally
9
+ supplied, read-only values.
8
10
 
9
11
  ## Select it before login
10
12
 
@@ -11,3 +11,7 @@ bytes do not appear in command arguments or temporary files.
11
11
 
12
12
  Use `hypit auth login <endpoint> --runtime <profile>` to write the selected credential and
13
13
  `hypit auth logout` to remove it.
14
+
15
+ A Profile that must also run on Linux can select `@hypit/credential-store-platform`:
16
+ that package uses this Store on macOS and Windows, and an unencrypted, owner-private file on Linux.
17
+ Selecting `@hypit/credential-store-os` directly continues to require macOS or Windows.
@@ -141,7 +141,11 @@ function platformBackend(service: string) {
141
141
  return { read: macosReader(service), write: macosWriter(service), remove: macosDeleter(service) };
142
142
  }
143
143
  if (process.platform === "win32") return windowsBackend();
144
- throw new Error("OS CredentialStore supports macOS and Windows only");
144
+ // A Profile that selects this Store cannot be repaired by anything the user does here, and the
145
+ // other two Stores are the answer, so name them where the failure is read.
146
+ throw new Error("OS CredentialStore supports macOS and Windows only; select "
147
+ + "@hypit/credential-store-file (owner-private local file) or @hypit/credential-store-env "
148
+ + "(externally supplied value) in this Profile's credentials instead");
145
149
  }
146
150
 
147
151
  /** One logical writable store backed by the current user's OS credential locker. */
@@ -0,0 +1,62 @@
1
+ # `@hypit/credential-store-platform`
2
+
3
+ An explicitly selected CredentialStore policy: macOS Keychain on macOS, Windows Credential Locker
4
+ on Windows, and an unencrypted, owner-private file on Linux. Other platforms are unsupported by this
5
+ package and report an error. Linux may offer other credential services; this package chooses file
6
+ storage there and does not detect or integrate those services.
7
+
8
+ A Profile can select this package to use that policy on Linux, macOS and Windows.
9
+ `@hypit/credential-store-os`, `@hypit/credential-store-file` and `@hypit/credential-store-env`
10
+ remain selectable directly for a deployment with a different storage choice.
11
+
12
+ ## Select it
13
+
14
+ ```json
15
+ {
16
+ "credentials": {
17
+ "platform": { "use": "@hypit/credential-store-platform" }
18
+ },
19
+ "endpoints": {
20
+ "hypihub.default": {
21
+ "use": "@hypit/provider-hypihub",
22
+ "config": {
23
+ "baseUrl": "https://hypit.ai",
24
+ "apiKey": { "store": "platform", "key": "hypihub.oauth" }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ The Endpoint still owns its acquisition flow and the Store only persists the result:
32
+
33
+ ```bash
34
+ hypit auth login hypihub.default --runtime <profile>
35
+ hypit auth status hypihub.default --runtime <profile>
36
+ hypit auth logout hypihub.default --runtime <profile>
37
+ ```
38
+
39
+ ## What it does not do
40
+
41
+ The choice is made once per host, from the platform the Runtime is running on. This is not a lookup
42
+ chain: a miss or failure in one store never tries another, and no credential is migrated between them. A
43
+ credential stored in a locker stays in that locker; a credential stored in a file stays in that file.
44
+
45
+ ## Configuration
46
+
47
+ - `path` selects the directory for Linux file storage, resolved against the Host state root printed by
48
+ `hypit paths`. The default is the same `credentials` directory `@hypit/credential-store-file` uses,
49
+ so on Linux, switching between the two Stores finds the credential already stored.
50
+ - `service` selects the locker service name. The OS Store's own default applies when it is absent.
51
+
52
+ ## Storage on each platform
53
+
54
+ On macOS and Windows the credential is held by the platform locker, with exactly the rules of
55
+ `@hypit/credential-store-os`: it is encrypted and access-controlled by the operating system, and it
56
+ is not readable from another machine.
57
+
58
+ On Linux the credential is held by an owner-private document with exactly the rules of
59
+ `@hypit/credential-store-file`: unencrypted JSON, one document per key, directory mode `0700`, file
60
+ mode `0600`, outside the video project, and never enumerated or indexed. Filesystem path-length
61
+ limits apply to an overlong key, which fails rather than selecting another name. A damaged document
62
+ can be replaced by `auth login` or deleted by `auth logout` without decoding the old value.
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@hypit/credential-store-platform",
3
+ "version": "0.0.0-dev",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "private": true,
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "hypit": {
15
+ "activation": "./src/activation.ts"
16
+ },
17
+ "dependencies": {
18
+ "@hypit/credential-store-file": "workspace:*",
19
+ "@hypit/credential-store-os": "workspace:*",
20
+ "@hypit/runtime": "workspace:*",
21
+ "@hypit/runtime-kit": "workspace:*"
22
+ }
23
+ }