@hypit/hypit 0.2.6 → 0.2.8

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 (70) hide show
  1. package/README.md +1 -1
  2. package/dist/public/hyperframes.d.ts +19 -2
  3. package/examples/provider-package/README.md +2 -2
  4. package/package.json +2 -1
  5. package/packages/caption-fine/README.md +4 -4
  6. package/packages/credential-store-file/README.md +4 -0
  7. package/packages/credential-store-file/src/store.ts +2 -2
  8. package/packages/credential-store-platform/README.md +2 -0
  9. package/packages/hyperframes/README.md +7 -0
  10. package/packages/hyperframes/src/html-project.ts +99 -0
  11. package/packages/hyperframes/src/index.ts +2 -0
  12. package/packages/hyperframes/src/project.ts +28 -0
  13. package/packages/pixverse/README.md +35 -0
  14. package/packages/pixverse/package.json +21 -0
  15. package/packages/pixverse/src/activation.ts +11 -0
  16. package/packages/pixverse/src/index.ts +122 -0
  17. package/packages/pixverse/src/surface.ts +146 -0
  18. package/packages/provider-hiapi/README.md +1 -1
  19. package/packages/provider-hyperframes-local/README.md +19 -7
  20. package/packages/provider-hyperframes-local/src/capture.ts +18 -8
  21. package/packages/provider-hyperframes-local/src/index.ts +1 -1
  22. package/packages/provider-hyperframes-local/src/output.ts +1 -1
  23. package/packages/provider-hyperframes-local/src/process-tree.ts +8 -2
  24. package/packages/provider-hyperframes-local/src/provider.ts +29 -2
  25. package/packages/provider-hyperframes-local/src/render.ts +45 -20
  26. package/packages/provider-hyperframes-local/src/sampling.ts +3 -2
  27. package/packages/provider-hypihub/README.md +3 -4
  28. package/packages/provider-monid/README.md +1 -1
  29. package/packages/provider-tokendance/README.md +1 -1
  30. package/packages/provider-whisperx-local/README.md +8 -5
  31. package/packages/render-hyperframes/README.md +9 -0
  32. package/packages/render-hyperframes/src/index.ts +3 -0
  33. package/packages/render-hyperframes/src/manifest.ts +7 -2
  34. package/packages/render-hyperframes/src/product.ts +41 -2
  35. package/packages/runtime-local/src/process-control.ts +4 -1
  36. package/packages/runtime-local/src/programs.ts +15 -3
  37. package/packages/runtime-local/src/supervisor.ts +8 -1
  38. package/packages/script/README.md +11 -4
  39. package/packages/script/src/format.ts +11 -24
  40. package/packages/script/src/manifest.ts +2 -2
  41. package/packages/script/src/parser.ts +16 -21
  42. package/packages/script/src/surface.ts +5 -3
  43. package/packages/script/src/types.ts +1 -1
  44. package/packages/seedance/README.md +11 -18
  45. package/packages/seedance/src/index.ts +7 -7
  46. package/packages/seedance/src/surface.ts +9 -8
  47. package/packages/seedance-kits/README.md +4 -3
  48. package/packages/source/README.md +1 -1
  49. package/packages/source/src/header.ts +2 -1
  50. package/packages/studio/README.md +13 -0
  51. package/packages/studio/src/feedback-server.ts +7 -0
  52. package/packages/studio/src/mutation-origin.ts +20 -0
  53. package/packages/studio/src/parameters.ts +3 -7
  54. package/packages/studio/src/preview/render.ts +7 -1
  55. package/packages/studio/src/server.ts +39 -0
  56. package/packages/studio/src/session.ts +7 -3
  57. package/packages/studio/src/ui/syntax.ts +11 -10
  58. package/packages/temporal-markup/EDITING.md +1 -1
  59. package/packages/video-cli/README.md +47 -4
  60. package/packages/video-cli/package.json +3 -0
  61. package/packages/video-cli/src/cli.ts +9 -9
  62. package/packages/video-cli/src/creation.ts +3 -3
  63. package/packages/video-cli/src/frame-grid.ts +34 -0
  64. package/packages/video-cli/src/index.ts +4 -0
  65. package/packages/video-cli/src/media-frames.ts +25 -0
  66. package/packages/video-cli/src/media.ts +198 -46
  67. package/packages/video-cli/src/process.ts +13 -3
  68. package/packages/video-cli/src/secret-input.ts +14 -0
  69. package/packages/video-cli/src/snapshot.ts +186 -0
  70. package/packages/yt-dlp/src/download.ts +32 -18
@@ -27,8 +27,10 @@ parser is inside a Segment, a valid bare tag such as `<ALICE>` is a Role Cue. Th
27
27
  not indentation: the compact spelling
28
28
  `<opening><ALICE>I speak first.<BOB>I answer.</opening>` has the same semantic value.
29
29
 
30
- A Role Cue is optional. Text before the first Role Cue is a roleless Turn, and Role state is reset
31
- when every Segment closes; a Role can never leak into the following Segment.
30
+ A Role Cue is optional. A Segment without Role Cues contains a roleless Turn. If Roles are used,
31
+ the first Cue must precede that Segment's spoken text. Role state resets when the Segment closes;
32
+ a Role can never leak into the following Segment. Role labels may use Unicode letters, marks and
33
+ numbers; capitalization does not distinguish Roles from Segments.
32
34
 
33
35
  An empty Segment such as `<empty></empty>` (or `<empty/>`) is valid. `empty` is an ordinary
34
36
  author-chosen name, not a reserved keyword. It retains the Segment identity and both boundary anchors
@@ -101,7 +103,7 @@ Units; it does not split the Segment, cut the picture or end a Selection.
101
103
  <script id="story">
102
104
  <exchange>
103
105
  <HOST> I use it || every day, || since <2012 | twenty twelve>.
104
- <GUEST> Even @{proof} on holiday @{/proof}?
106
+ <GUEST> Even @{proof}on holiday?@{/proof}
105
107
  <HOST> @{answer!} Especially then.
106
108
  </exchange>
107
109
  </script>
@@ -118,13 +120,18 @@ These units support precise timing and highlighting. A Caption Cue can hold a wh
118
120
  `||` chooses its handoff independently of character counts or visual line wrapping.
119
121
 
120
122
  Annotations do not create speech boundaries. Comments are transparent (`hel<!--note-->lo`
121
- remains `hello`); a postfix attribute or `||` inside a word is invalid. Script analyzes a complete
123
+ remains `hello`), including when an attribute follows a comment or zero-width marker:
124
+ `hello<!--note-->{emphasis}` and `hello@{beat!}{emphasis}` still annotate `hello`.
125
+ An authored space before the attribute remains invalid. A postfix attribute or `||` inside a word
126
+ is invalid. Script analyzes a complete
122
127
  prose run before binding these constructs. An explicit Dual correspondence and a speaker/Segment
123
128
  boundary remain authored structure. Shared Dual groups still expose their internal speech anchors.
124
129
 
125
130
  A Dual display side is literal authored text, including symbols and emoji: `<😀|smile>` and
126
131
  `<.|dot>` have explicit speech correspondence and require no invented speech token for the symbol.
127
132
  A literal-only display is one display surface within that correspondence.
133
+ Every Dual needs at least one spoken word on its explicit or shared speech side. `<API|...>` cannot
134
+ provide timed correspondence for `API` and is rejected; punctuation and markers are not spoken words.
128
135
 
129
136
  ## Display spelling and separators
130
137
 
@@ -3,38 +3,25 @@ import { canonicalStringify } from "@hypit/protocol";
3
3
  import { ScriptSyntaxError } from "./error.js";
4
4
  import { narrativeValue } from "./narrative.js";
5
5
  import { parseScript } from "./parser.js";
6
-
7
- const ROLE = /^[\p{L}\p{M}\p{N}_](?:[\p{L}\p{M}\p{N}_. -]{0,30}[\p{L}\p{M}\p{N}_.-])?$/u;
6
+ import type { ParsedSegment } from "./types.js";
8
7
 
9
8
  function compact(value: string): string {
10
9
  return value.replace(/\s+/gu, " ").trim();
11
10
  }
12
11
 
13
- function formatSegment(raw: string, id: string, selfClosing: boolean): string[] {
12
+ function formatSegment(source: string, segment: ParsedSegment): string[] {
13
+ const { id, selfClosing, contentRange, atoms } = segment;
14
14
  if (selfClosing) return [`<${id}/>`];
15
- const openEnd = raw.indexOf(">") + 1;
16
- const closeStart = raw.lastIndexOf(`</${id}>`);
17
- const body = raw.slice(openEnd, closeStart);
18
15
  const lines: string[] = [`<${id}>`];
19
16
  const turns: string[] = [];
20
- let chunkStart = 0;
21
- let cursor = 0;
22
- while (cursor < body.length) {
23
- if (body[cursor] !== "<" || body.startsWith("<!--", cursor)) {
24
- cursor += 1;
25
- continue;
26
- }
27
- const end = body.indexOf(">", cursor + 1);
28
- if (end < 0) break;
29
- const inside = body.slice(cursor + 1, end);
30
- if (!inside.includes("|") && ROLE.test(inside)) {
31
- const before = compact(body.slice(chunkStart, cursor));
32
- if (before) turns.push(before);
33
- chunkStart = cursor;
34
- }
35
- cursor = end + 1;
17
+ let chunkStart = contentRange.start;
18
+ for (const atom of atoms) {
19
+ if (atom.kind !== "role") continue;
20
+ const before = compact(source.slice(chunkStart, atom.range.start));
21
+ if (before) turns.push(before);
22
+ chunkStart = atom.range.start;
36
23
  }
37
- const tail = compact(body.slice(chunkStart));
24
+ const tail = compact(source.slice(chunkStart, contentRange.end));
38
25
  if (tail) turns.push(tail);
39
26
  lines.push(...turns.map((turn) => ` ${turn}`));
40
27
  lines.push(`</${id}>`);
@@ -64,7 +51,7 @@ export function formatScript(sourceName: string, source: string): string {
64
51
  } else if (output.length) {
65
52
  output.push("");
66
53
  }
67
- output.push(...formatSegment(source.slice(start, end), segment.id, segment.selfClosing));
54
+ output.push(...formatSegment(source, segment));
68
55
  cursor = end;
69
56
  }
70
57
  const tail = formatOutside(source.slice(cursor));
@@ -64,9 +64,9 @@ 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 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.",
67
+ "Dual Text is written `<display | speech>`: the left side reaches Caption and the right side reaches dialogue and speech. `<display|>` shares the displayed prose with speech and forms the same complete alignment unit; its word times remain individual. `<|speech>` speaks without displaying. The explicit or shared speech must contain a spoken word; markers and punctuation alone supply no correspondence.",
68
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.",
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
+ "A flat token attribute follows a complete display token as `{name}` or `{name=value}`; multiple attributes use one comma-separated block. Zero-width markers and ordinary-prose comments do not interrupt that attachment; prose whitespace does. Attributes do not nest, do not carry timing, and never split a Dual Alignment Unit.",
70
70
  "Selection and Moment markers are fully enclosed in `@{...}` with all sigils inside. They are zero-width, share one name namespace, and may not split a speech token. Surrounding prose spaces remain content; do not add spaces to delimit a name:",
71
71
  [
72
72
  "| Marker | Meaning |",
@@ -492,10 +492,10 @@ export function parseScript(
492
492
  if (!shared && raw[index] === "{") fail("SCRIPT_DUAL_SPEECH_ATTRIBUTE", "Display attributes belong to the display side; escape literal braces in speech.", absoluteStart + index);
493
493
  if (shared && raw[index] === "{") {
494
494
  const before = raw.slice(partStart, index);
495
- if (!before || /\s$/u.test(before)) {
495
+ addLiteral(before, absoluteStart + partStart);
496
+ if (!runText || /\s$/u.test(runText)) {
496
497
  fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must immediately follow a display token.", absoluteStart + index);
497
498
  }
498
- addLiteral(before, absoluteStart + partStart);
499
499
  const block = parseAttributeBlock(raw.slice(index), absoluteStart + index);
500
500
  runAttributes.push({ position: runText.length, offset: absoluteStart + index,
501
501
  end: absoluteStart + index + block.length, attributes: block.attributes });
@@ -517,20 +517,20 @@ export function parseScript(
517
517
  addLiteral(raw.slice(partStart), absoluteStart + partStart);
518
518
  const display = caption ?? spokenParts.join("");
519
519
  const sharedMarks = finishLexicalRun();
520
- if (shared && tokens.length === startToken) {
521
- fail("SCRIPT_DUAL_EMPTY", "Dual Text with omitted speech must contain spoken text on its display side.", absoluteStart);
522
- }
523
- if (tokens.length > startToken) {
524
- addCaptionRegion(
525
- display,
526
- current!.id,
527
- startToken,
528
- tokens.length,
529
- cleanProjection(display) ? "alias" : "hidden",
530
- { start: sourceOffset + absoluteStart, end: sourceOffset + absoluteStart + raw.length },
531
- shared ? sharedMarks : marks,
532
- );
520
+ if (tokens.length === startToken) {
521
+ fail("SCRIPT_DUAL_EMPTY", shared
522
+ ? "Dual Text with omitted speech must contain spoken text on its display side."
523
+ : "Dual Text speech side must contain a spoken word, not only markers or punctuation.", absoluteStart);
533
524
  }
525
+ addCaptionRegion(
526
+ display,
527
+ current!.id,
528
+ startToken,
529
+ tokens.length,
530
+ cleanProjection(display) ? "alias" : "hidden",
531
+ { start: sourceOffset + absoluteStart, end: sourceOffset + absoluteStart + raw.length },
532
+ shared ? sharedMarks : marks,
533
+ );
534
534
  };
535
535
 
536
536
  const closeCurrent = (end: number, selfClosing: boolean, contentEnd = end): void => {
@@ -626,18 +626,13 @@ export function parseScript(
626
626
  const speech = inside.slice(pipe + 1);
627
627
  if (!speech.trim()) {
628
628
  consumeSpeechSide(inside.slice(0, pipe), offset + 1, undefined);
629
- finishLexicalRun();
630
629
  offset = end + 1;
631
630
  continue;
632
631
  }
633
632
  assertDualDisplayLiteral(inside.slice(0, pipe), offset + 1);
634
633
  const markedDisplay = parseMarkedDisplay(inside.slice(0, pipe), offset + 1);
635
634
  const display = markedDisplay.display;
636
- if (!speech.replace(/@\{[^{}]*\}/gu, "").trim()) {
637
- fail("SCRIPT_DUAL_EMPTY", "Dual Text speech side must not be empty.", offset);
638
- }
639
635
  consumeSpeechSide(speech, offset + pipe + 2, display, markedDisplay.marks);
640
- finishLexicalRun();
641
636
  offset = end + 1;
642
637
  continue;
643
638
  }
@@ -674,7 +669,7 @@ export function parseScript(
674
669
  for (const piece of literalPieces(raw, textStart)) addText(piece.value, piece.value, piece.start, piece.end, true, piece.positions);
675
670
  if (source[offset] === "{") {
676
671
  const block = parseAttributeBlock(source.slice(offset), offset);
677
- if (!raw || /\s$/u.test(raw)) {
672
+ if (!runText || /\s$/u.test(runText)) {
678
673
  fail("SCRIPT_ATTRIBUTE_TARGET", "A token attribute must immediately follow a display token.", offset);
679
674
  }
680
675
  runAttributes.push({ position: runText.length, offset, end: offset + block.length, attributes: block.attributes });
@@ -24,6 +24,10 @@ function findClose(input: ScriptSurfaceInput): { readonly start: number; readonl
24
24
  const close = `</${input.tag}>`;
25
25
  let cursor = input.contentStart;
26
26
  while (cursor < input.source.length) {
27
+ if (input.source[cursor] === "\\") {
28
+ cursor += 2;
29
+ continue;
30
+ }
27
31
  if (input.source.startsWith("<!--", cursor)) {
28
32
  const commentEnd = input.source.indexOf("-->", cursor + 4);
29
33
  if (commentEnd < 0) {
@@ -33,9 +37,7 @@ function findClose(input: ScriptSurfaceInput): { readonly start: number; readonl
33
37
  continue;
34
38
  }
35
39
  if (input.source.startsWith(close, cursor)) {
36
- let slashes = 0;
37
- for (let before = cursor - 1; before >= 0 && input.source[before] === "\\"; before -= 1) slashes += 1;
38
- if (slashes % 2 === 0) return { start: cursor, end: cursor + close.length };
40
+ return { start: cursor, end: cursor + close.length };
39
41
  }
40
42
  cursor += 1;
41
43
  }
@@ -35,7 +35,7 @@ export type ParsedSegment = NarrativeSegment & {
35
35
  readonly index: number;
36
36
  readonly atoms: readonly ParsedAtom[];
37
37
  readonly range: SourceRange;
38
- /** Exact body range between the Segment tags, used only for source-preserving marker edits. */
38
+ /** Exact body range between Segment tags for Script-owned source edits and formatting. */
39
39
  readonly contentRange: SourceRange;
40
40
  readonly selfClosing: boolean;
41
41
  };
@@ -34,22 +34,20 @@ responsible for any additional service-specific input limits.
34
34
 
35
35
  ## Visual reference metadata
36
36
 
37
- Declare whether each image or video contains a person/avatar reference, including an AI-generated
38
- human likeness. This describes the supplied material, independently of the prompt's requested action:
37
+ Every supplied image or video must explicitly declare `person-reference`: `true` if it contains
38
+ a person, `false` otherwise. Classify the supplied material, not the requested result.
39
39
 
40
40
  ```xml
41
- <seedance:ReferenceVideo id="dance" model="mini" prompt={direction} duration="8">
41
+ <seedance:ReferenceVideo id="take" model="mini" prompt={direction} duration="8">
42
42
  <seedance:Reference image={presenter.image} person-reference="true"/>
43
- <seedance:Reference video={motion.video} person-reference="true"/>
43
+ <seedance:Reference video={presenter.video} person-reference="true"/>
44
44
  <seedance:Reference image={room.image} person-reference="false"/>
45
45
  </seedance:ReferenceVideo>
46
46
  ```
47
47
 
48
- `person-reference` is optional, accepts literal `true` or `false`, and applies to image/video, not
49
- voice audio. Omission carries no classification; it is distinct from explicitly declaring false.
50
- Inspect the actual reference when deciding the value. For `FrameVideo`, use
51
- `first-frame-person-reference` and `last-frame-person-reference` beside their respective frame
52
- inputs. A last-frame classification requires a last-frame input.
48
+ Missing or non-boolean declarations are rejected; there is no default or automatic face detection.
49
+ Audio must omit this field. `FrameVideo` requires `first-frame-person-reference` and, when a last
50
+ frame is supplied, `last-frame-person-reference`. A last-frame classification requires a last frame.
53
51
 
54
52
  | Supplied visual input | Authored attribute | Request port |
55
53
  | --- | --- | --- |
@@ -66,23 +64,18 @@ These forms apply to `standard`, `fast`, `mini` and `2.5`. For example:
66
64
  last-frame={empty-room.image} last-frame-person-reference="false"/>
67
65
  ```
68
66
 
69
- Classify the material supplied to this request, not the intended result. A dance video with a person
70
- still needs `true` when used only for motion, even if the prompt asks for a different performer.
71
- An empty room stays `false` when the prompt asks to add a person. Inspect video across the selected
72
- excerpt, not only its opening frame. This flag neither detects faces nor locks or names an identity.
73
- Identity and action direction remain in the prompt and references.
67
+ Inspect the selected video excerpt, not only its opening frame. An empty room stays `false` when
68
+ the prompt asks to add a person. The flag does not lock identity; direction and references own that.
74
69
 
75
70
  The SVML author declares this parameter on each reference input. Admitted files, generated
76
71
  images/videos and reused Results use the same attributes. For a future output, declare the intended
77
72
  reference classification explicitly; if its contents are uncertain, generate and inspect that
78
73
  material before using it downstream.
79
74
 
80
- The model's media ports carry this as `fields.personReference`. Providers interpret it through their
81
- service's media handling; it is not a prompt sentence or a Core-level identity. HypiHub sends it as
75
+ Direct requests require the same boolean in `fields.personReference`. Providers interpret it through
76
+ their service's media handling; it is not a prompt sentence or a Core-level identity. HypiHub sends it as
82
77
  `is_person_reference` when uploading the file, then uses the returned URL in the ordinary video
83
78
  request. A project Provider maps it according to its own API.
84
- Omission does not request automatic face detection. HypiHub currently treats omitted upload flags
85
- as unmarked (`false`); declare `true` explicitly for a person reference that needs its preparation.
86
79
 
87
80
  Video references can carry motion or camera behavior while image references carry the target
88
81
  appearance. Request duration and reference-clip duration are different limits. Check the selected
@@ -17,7 +17,7 @@ export type SeedanceModel = typeof seedanceModels[number];
17
17
 
18
18
  const ASPECT_RATIOS = ["1:1", "4:3", "3:4", "16:9", "9:16", "21:9", "adaptive"] as const;
19
19
 
20
- const PERSON_REFERENCE_FIELDS = [{ name: "personReference", value: { kind: "boolean" }, optional: true }] as const;
20
+ const PERSON_REFERENCE_FIELDS = [{ name: "personReference", value: { kind: "boolean" } }] as const;
21
21
 
22
22
  const SEEDANCE_25_DURATIONS = [-1, ...Array.from({ length: 27 }, (_item, index) => index + 4)] as const;
23
23
 
@@ -175,9 +175,9 @@ const seedanceCommonAttributes: readonly SurfaceAttributeVocabulary[] = [
175
175
  },
176
176
  ];
177
177
 
178
- const personReferenceAttribute = (name: string): SurfaceAttributeVocabulary => ({
179
- name, kind: "literal", required: false, values: ["true", "false"],
180
- summary: "Declares whether this visual input contains a person/avatar reference, including an AI-generated human likeness. Omission leaves it unclassified.",
178
+ const personReferenceAttribute = (name: string, required = false): SurfaceAttributeVocabulary => ({
179
+ name, kind: "literal", required, values: ["true", "false"],
180
+ summary: "Required for each supplied image/video, including first/last frames: true if it contains a person, false otherwise. Audio must omit it.",
181
181
  });
182
182
 
183
183
  const seedanceVideoPort: readonly SurfacePortVocabulary[] = [{
@@ -235,7 +235,7 @@ export const seedanceMarkupSurfaces = [
235
235
  summary: "Generates one video with an exact Seedance model from a Text prompt and the images the video opens and closes on.",
236
236
  attributes: [
237
237
  ...seedanceCommonAttributes,
238
- personReferenceAttribute("first-frame-person-reference"),
238
+ personReferenceAttribute("first-frame-person-reference", true),
239
239
  personReferenceAttribute("last-frame-person-reference"),
240
240
  {
241
241
  name: "first-frame",
@@ -253,7 +253,7 @@ export const seedanceMarkupSurfaces = [
253
253
  },
254
254
  ],
255
255
  ports: seedanceVideoPort,
256
- example: '<seedance:FrameVideo id="bridge" model="fast" prompt={direction} duration="5" first-frame={first.image} last-frame={last.image}/>',
256
+ example: '<seedance:FrameVideo id="bridge" model="fast" prompt={direction} duration="5" first-frame={first.image} first-frame-person-reference="true" last-frame={last.image} last-frame-person-reference="false"/>',
257
257
  notes: [
258
258
  ...seedanceSettingNotes,
259
259
  "Both frames are ordinary image Artifact edges; the Surface copies no runtime media into request metadata.",
@@ -316,7 +316,7 @@ export const seedanceMarkupSurfaces = [
316
316
  notes: [
317
317
  ...seedanceSettingNotes,
318
318
  "The element requires at least one `Reference` child, and the model's port limits cap how many of each role it accepts.",
319
- "`person-reference` applies to image/video references; voice audio carries no visual classification. The Provider transports the declared fact according to its API.",
319
+ "Every image/video Reference requires `person-reference=\"true|false\"`. Classify the supplied material; audio must omit the field. The Provider transports it according to its API.",
320
320
  "A `Reference` carries exactly one of `image`, `video` or `audio`, and is empty.",
321
321
  ],
322
322
  },
@@ -146,8 +146,10 @@ function booleanAttribute(element: StructuredElement, name: string, fallback: bo
146
146
  }
147
147
 
148
148
  function personReferenceFields(element: StructuredElement, name: string) {
149
- return element.attributes[name] === undefined ? undefined
150
- : { personReference: booleanAttribute(element, name, false) };
149
+ if (element.attributes[name] === undefined) {
150
+ throw new Error(`${element.name}.${name} is required for this visual reference; set true if it contains a person, false otherwise`);
151
+ }
152
+ return { personReference: booleanAttribute(element, name, false) };
151
153
  }
152
154
 
153
155
  function enumeratedPort(table: GenerationPortTable, name: string): readonly (string | number)[] {
@@ -217,8 +219,8 @@ function referenceInputs(
217
219
  const kinds = accepted.filter((kind) => child.attributes[kind] !== undefined);
218
220
  if (kinds.length !== 1) throw new Error(`${child.name} requires exactly one of ${accepted.join(", ")}`);
219
221
  const role = kinds[0]!;
220
- const fields = personReferenceFields(child, "person-reference");
221
- if (role === "audio" && fields !== undefined) throw new Error(`${child.name}.person-reference applies to image or video, not audio`);
222
+ if (role === "audio" && child.attributes["person-reference"] !== undefined) throw new Error(`${child.name}.person-reference applies to image or video, not audio`);
223
+ const fields = role === "audio" ? undefined : personReferenceFields(child, "person-reference");
222
224
  result.push({
223
225
  ...(fields === undefined ? {} : { fields }),
224
226
  role,
@@ -242,13 +244,12 @@ function frameInputs(
242
244
  ): MediaInput[] {
243
245
  const first = mediaReference(resolved(element, "first-frame", resolveReference), "image", `${element.name}.first-frame`);
244
246
  const firstFields = personReferenceFields(element, "first-frame-person-reference");
245
- const result: MediaInput[] = [{ port: "firstFrame", role: "image", source: first, ...(firstFields === undefined ? {} : { fields: firstFields }) }];
246
- const lastFields = personReferenceFields(element, "last-frame-person-reference");
247
- if (lastFields !== undefined && element.attributes["last-frame"] === undefined) throw new Error(`${element.name}.last-frame-person-reference requires last-frame`);
247
+ const result: MediaInput[] = [{ port: "firstFrame", role: "image", source: first, fields: firstFields }];
248
+ if (element.attributes["last-frame-person-reference"] !== undefined && element.attributes["last-frame"] === undefined) throw new Error(`${element.name}.last-frame-person-reference requires last-frame`);
248
249
  if (element.attributes["last-frame"] !== undefined) {
249
250
  result.push({
250
251
  port: "lastFrame",
251
- ...(lastFields === undefined ? {} : { fields: lastFields }),
252
+ fields: personReferenceFields(element, "last-frame-person-reference"),
252
253
  role: "image",
253
254
  source: mediaReference(resolved(element, "last-frame", resolveReference), "image", `${element.name}.last-frame`),
254
255
  });
@@ -23,8 +23,8 @@ ordinary explicit graph edges:
23
23
 
24
24
  <seedance:ReferenceVideo id="broll" model="mini" prompt={broll-prompt}
25
25
  duration="5" resolution="720p" aspect-ratio="9:16">
26
- <seedance:Reference image={scene}/>
27
- <seedance:Reference image={product}/>
26
+ <seedance:Reference image={scene} person-reference="false"/>
27
+ <seedance:Reference image={product} person-reference="false"/>
28
28
  </seedance:ReferenceVideo>
29
29
  ```
30
30
 
@@ -154,7 +154,8 @@ For a multi-scene B-roll montage, `story` can name the references in scene order
154
154
  action. Choose an edit language compatible with cuts. “Continuous within each scene” and “one
155
155
  continuous shot for the whole montage” ask for different results.
156
156
 
157
- Visual input classification belongs to the Seedance Reference, separately from Kit text. The
157
+ Every Seedance image/video Reference requires `person-reference="true|false"`: true if the supplied
158
+ material contains a person, false otherwise. Audio must omit it. The
158
159
  [Seedance author package](../seedance/README.md#visual-reference-metadata) explains `person-reference`
159
160
  and its frame variants. The Kit supplies direction; the actual reference edges supply media and
160
161
  metadata. For movement-led work, a reference video can carry the motion while the text explains what
@@ -9,5 +9,5 @@ It recognizes exactly one mandatory bounded Header:
9
9
  ```
10
10
 
11
11
  The Header selects an exact trusted Frontend. There is no suffix dispatch and no default parser.
12
- The package masks the Header while preserving character offsets, but does not recognize imports,
12
+ The package masks the Header while preserving UTF-16 source offsets and line breaks, but does not recognize imports,
13
13
  XML, Script, Recipes, Run syntax or domain Types. Those belong to the selected Frontend.
@@ -75,6 +75,7 @@ export function parseSourceHeader(sourceName: string, text: string): SourceHeade
75
75
 
76
76
  /** Preserve every original offset while making the Header ordinary whitespace to body Frontends. */
77
77
  export function maskSourceHeader(text: string, header: SourceHeader): string {
78
- const prefix = text.slice(0, header.end).replace(/[^\r\n]/gu, " ");
78
+ // Source ranges use UTF-16 offsets: replace each code unit, not each code point.
79
+ const prefix = text.slice(0, header.end).replace(/[^\r\n]/g, " ");
79
80
  return `${prefix}${text.slice(header.end)}`;
80
81
  }
@@ -1,5 +1,13 @@
1
1
  # Hypit Studio
2
2
 
3
+ For direct picture inspection, `hypit snapshot --studio <studio-url>` reads the current compiled
4
+ `HyperframesDocument` from `GET /__studio/document` and its existing `/__studio/material/<resource>`
5
+ resources. `GET /__studio/visual.html` exposes the same materialized picture without the interactive
6
+ Studio playback shim or audio. Both representations come from the same compilation as the displayed
7
+ preview. The snapshot invocation uses the selected Runtime Profile's frame Provider and creates no
8
+ Build. During a pending Source compilation the export route reports that state; a failed update
9
+ reports its error instead of returning the previous picture as current.
10
+
3
11
  The single official Web Studio for SVML. It opens an explicit Run Source,
4
12
  traces its Film or Render target back to the semantic and visual projections
5
13
  Studio can edit, runs deterministic Producers and explicitly permitted transient Needs, and composites the
@@ -159,6 +167,11 @@ the selected entity. A reference can resolve to a shared Frame or Recipe, so one
159
167
  may affect several consumers. Structured fields save together when editing ends and the value is complete; missing required values stay in the editor with a completion hint.
160
168
  Check save status; source conflicts reject stale edits rather than overwrite newer files.
161
169
 
170
+ Script source ranges come from its raw Surface and Companion. Marker edits use Script's parsed
171
+ anchors and preserve unrelated prose, whitespace and word attributes; the displayed timeline words
172
+ are not a replacement text source. Recipe parameter reads use the same Source Header preparation
173
+ as compilation, preserving UTF-16 offsets for the exact property being edited.
174
+
162
175
  The Timeline owns the editor's complete range; displayed objects do not extend it. Take placement
163
176
  and complete extent are reference information in Studio.
164
177
 
@@ -3,6 +3,7 @@ import { relative } from "node:path";
3
3
  import type { Plugin } from "vite";
4
4
  import { createFeedbackStore, FeedbackConflict } from "./feedback-store.js";
5
5
  import { readFeedbackMutation } from "./feedback.js";
6
+ import { allowsStudioMutation } from "./mutation-origin.js";
6
7
  import type { FeedbackDocument, FeedbackView } from "./feedback.js";
7
8
 
8
9
  /** Review storage is separate from compilation, Results and Agent delivery. */
@@ -23,6 +24,12 @@ export function studioFeedbackPlugin(workspaceRoot: string, runPath: string): Pl
23
24
  server.httpServer?.once("close", () => watcher.close());
24
25
  server.middlewares.use((request, response, next) => {
25
26
  if (new URL(request.url ?? "/", "http://studio.hypit.local").pathname !== "/__studio/feedback") return next();
27
+ if (request.method === "POST" && !allowsStudioMutation(request.headers)) {
28
+ response.statusCode = 403;
29
+ response.setHeader("content-type", "application/json; charset=utf-8");
30
+ response.end(JSON.stringify({ error: "Studio mutation must come from this local Studio session." }));
31
+ return;
32
+ }
26
33
  response.setHeader("content-type", "application/json; charset=utf-8");
27
34
  response.setHeader("cache-control", "no-store");
28
35
  void (async () => {
@@ -0,0 +1,20 @@
1
+ import type { IncomingHttpHeaders } from "node:http";
2
+
3
+ /** Studio writes belong to the local page serving this Studio session. */
4
+ export function allowsStudioMutation(headers: IncomingHttpHeaders): boolean {
5
+ const host = headers.host;
6
+ if (host === undefined) return false;
7
+ let target: URL;
8
+ try { target = new URL(`http://${host}`); } catch { return false; }
9
+ if (!["localhost", "127.0.0.1", "[::1]"].includes(target.hostname) || target.host !== host.toLowerCase()) return false;
10
+
11
+ const origin = headers.origin;
12
+ if (origin !== undefined) {
13
+ let source: URL;
14
+ try { source = new URL(origin); } catch { return false; }
15
+ if (source.origin !== origin || source.origin !== target.origin) return false;
16
+ }
17
+
18
+ const site = headers["sec-fetch-site"];
19
+ return site === undefined || site === "same-origin" || site === "none";
20
+ }
@@ -17,6 +17,7 @@ import type {
17
17
  } from "@hypit/studio-adapter";
18
18
  import { parseSvs } from "@hypit/svs";
19
19
  import { parseOpeningTag } from "@hypit/markup";
20
+ import { prepareAuthorSource } from "@hypit/elaborator";
20
21
  import { parameterControlForSchema, parameterRecordSchema } from "./parameter-values.js";
21
22
  import type { CanonicalValue } from "@hypit/protocol";
22
23
 
@@ -132,12 +133,6 @@ function sourceAbsolute(root: string, path: string, base?: string): string {
132
133
  return resolve(directory, path);
133
134
  }
134
135
 
135
- function svsText(source: string): string {
136
- const header = /^\s*<\?svml[\s\S]*?\?>/u.exec(source);
137
- if (header === null) return source;
138
- return `${header[0].replace(/[^\r\n]/gu, " ")}${source.slice(header[0].length)}`;
139
- }
140
-
141
136
  function recipeParameters(input: {
142
137
  readonly root: string;
143
138
  readonly files: readonly StudioSourceFile[];
@@ -176,7 +171,8 @@ function recipeParameters(input: {
176
171
  const source = sourceFor(input.root, imported.source, input.files, input.current.path);
177
172
  if (source === undefined || source.language !== "svs") return [];
178
173
  const recipePath = parts.join(".");
179
- const parsed = parseSvs(source.path, svsText(source.text));
174
+ const prepared = prepareAuthorSource({ id: source.path, name: source.path, text: source.text });
175
+ const parsed = parseSvs(source.path, prepared.text);
180
176
  const recipe = parsed.recipes.find((item) => item.value.path === recipePath);
181
177
  if (recipe === undefined) return [];
182
178
  return input.recipe.bindings.flatMap((declaration): readonly StudioSourceBinding[] => {
@@ -1,5 +1,6 @@
1
1
  import type { AudioTrack, Composition } from "@hypit/composition";
2
2
  import { compileHyperframesDocument, materializeHyperframesHtml } from "@hypit/hyperframes";
3
+ import type { HyperframesDocument } from "@hypit/hyperframes";
3
4
  import type { ProgramSpace } from "@hypit/program-space";
4
5
 
5
6
  import { injectRuntimeShim } from "./runtime-shim.js";
@@ -19,6 +20,11 @@ export type RenderInput = {
19
20
  * material all come from the projection selected by the Run.
20
21
  */
21
22
  export function renderPreview(input: RenderInput): string {
23
+ return renderStudioProgramme(input).preview;
24
+ }
25
+
26
+ /** The same compiled picture serves immediate frame capture and interactive playback. */
27
+ export function renderStudioProgramme(input: RenderInput): { readonly document: HyperframesDocument; readonly html: string; readonly preview: string } {
22
28
  const document = compileHyperframesDocument(input.composition, input.space);
23
29
  const html = materializeHyperframesHtml(document, (artifact) => {
24
30
  // The only Artifacts a preview can reference are files the author already
@@ -51,5 +57,5 @@ export function renderPreview(input: RenderInput): string {
51
57
  fadeInSamples: clip.fadeInSamples, fadeOutSamples: clip.fadeOutSamples,
52
58
  }))}"></audio>`;
53
59
  }).join("");
54
- return injectRuntimeShim(html, audio);
60
+ return { document, html, preview: injectRuntimeShim(html, audio) };
55
61
  }
@@ -15,6 +15,7 @@ import type { ServedFile } from "./compile.js";
15
15
  import type { StudioDomain } from "./domain.js";
16
16
  import type { StudioCompanionRegistry } from "./studio-registry.js";
17
17
  import { loadStudioRun } from "./run.js";
18
+ import { allowsStudioMutation } from "./mutation-origin.js";
18
19
  import { parameterAuthorValue, parameterOption, serializeParameterValue, serializeAttributeGroup, validateParameterValue } from "./parameter-values.js";
19
20
  import { readStudioSession } from "./session.js";
20
21
  import type { Range, StudioFailure, StudioLibraryRequest, StudioLibraryView, StudioMutation, StudioSnapshot } from "./shared.js";
@@ -80,6 +81,8 @@ class StudioMutationRejected extends Error {}
80
81
 
81
82
  export function studioPlugin(options: StudioPluginOptions): Plugin {
82
83
  let snapshot: StudioSnapshot | undefined;
84
+ let visualHtml: string | undefined;
85
+ let visualDocument: import("@hypit/hyperframes").HyperframesDocument | undefined;
83
86
  let failure: StudioFailure | undefined;
84
87
  let material: ReadonlyMap<string, ServedFile> = new Map();
85
88
  let revision = 0;
@@ -158,6 +161,8 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
158
161
  if (attempt !== requestedRevision) return;
159
162
  revision = attempt;
160
163
  snapshot = result.snapshot;
164
+ visualHtml = result.visualHtml;
165
+ visualDocument = result.document;
161
166
  material = result.material;
162
167
  failure = undefined;
163
168
  if (notify) server?.ws.send({ type: "custom", event: "studio:snapshot", data: snapshot });
@@ -473,6 +478,10 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
473
478
  value.middlewares.use((request, response, next) => {
474
479
  const url = new URL(request.url ?? "/", "http://studio.hypit.local");
475
480
  if (request.method === "PUT" && url.pathname === "/__studio/source") {
481
+ if (!allowsStudioMutation(request.headers)) {
482
+ json(response, 403, { error: "Cross-origin Studio mutations are prohibited." });
483
+ return;
484
+ }
476
485
  void (async () => {
477
486
  let acquired = false;
478
487
  try {
@@ -524,6 +533,10 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
524
533
  return;
525
534
  }
526
535
  if (request.method === "PUT" && url.pathname === "/__studio/artifact-name") {
536
+ if (!allowsStudioMutation(request.headers)) {
537
+ json(response, 403, { error: "Cross-origin Studio mutations are prohibited." });
538
+ return;
539
+ }
527
540
  void (async () => {
528
541
  try {
529
542
  const chunks: Buffer[] = [];
@@ -546,6 +559,10 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
546
559
  return;
547
560
  }
548
561
  if (request.method === "POST" && url.pathname === "/__studio/mutation") {
562
+ if (!allowsStudioMutation(request.headers)) {
563
+ json(response, 403, { error: "Cross-origin Studio mutations are prohibited." });
564
+ return;
565
+ }
549
566
  void (async () => {
550
567
  try {
551
568
  const chunks: Buffer[] = [];
@@ -580,6 +597,28 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
580
597
  next();
581
598
  return;
582
599
  }
600
+ if (url.pathname === "/__studio/visual.html" || url.pathname === "/__studio/document") {
601
+ void (async () => {
602
+ if (timer !== undefined || publishing > 0) {
603
+ json(response, 409, { error: "Studio is compiling a Source change; capture after the updated preview is ready." });
604
+ return;
605
+ }
606
+ if (snapshot === undefined && failure === undefined) await publish(++requestedRevision);
607
+ if (failure !== undefined || visualHtml === undefined) {
608
+ json(response, 500, failure ?? { error: "Studio has no compiled picture." });
609
+ return;
610
+ }
611
+ if (url.pathname === "/__studio/document") {
612
+ json(response, 200, visualDocument);
613
+ return;
614
+ }
615
+ response.statusCode = 200;
616
+ response.setHeader("content-type", "text/html; charset=utf-8");
617
+ response.setHeader("cache-control", "no-store");
618
+ response.end(request.method === "HEAD" ? undefined : visualHtml);
619
+ })();
620
+ return;
621
+ }
583
622
  if (url.pathname === "/__studio/session") {
584
623
  void (async () => {
585
624
  if (snapshot === undefined && failure === undefined) {