@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.
- package/README.md +1 -1
- package/dist/public/hyperframes.d.ts +19 -2
- package/examples/provider-package/README.md +2 -2
- package/package.json +2 -1
- package/packages/caption-fine/README.md +4 -4
- package/packages/credential-store-file/README.md +4 -0
- package/packages/credential-store-file/src/store.ts +2 -2
- package/packages/credential-store-platform/README.md +2 -0
- package/packages/hyperframes/README.md +7 -0
- package/packages/hyperframes/src/html-project.ts +99 -0
- package/packages/hyperframes/src/index.ts +2 -0
- package/packages/hyperframes/src/project.ts +28 -0
- package/packages/pixverse/README.md +35 -0
- package/packages/pixverse/package.json +21 -0
- package/packages/pixverse/src/activation.ts +11 -0
- package/packages/pixverse/src/index.ts +122 -0
- package/packages/pixverse/src/surface.ts +146 -0
- package/packages/provider-hiapi/README.md +1 -1
- package/packages/provider-hyperframes-local/README.md +19 -7
- package/packages/provider-hyperframes-local/src/capture.ts +18 -8
- package/packages/provider-hyperframes-local/src/index.ts +1 -1
- package/packages/provider-hyperframes-local/src/output.ts +1 -1
- package/packages/provider-hyperframes-local/src/process-tree.ts +8 -2
- package/packages/provider-hyperframes-local/src/provider.ts +29 -2
- package/packages/provider-hyperframes-local/src/render.ts +45 -20
- package/packages/provider-hyperframes-local/src/sampling.ts +3 -2
- package/packages/provider-hypihub/README.md +3 -4
- package/packages/provider-monid/README.md +1 -1
- package/packages/provider-tokendance/README.md +1 -1
- package/packages/provider-whisperx-local/README.md +8 -5
- package/packages/render-hyperframes/README.md +9 -0
- package/packages/render-hyperframes/src/index.ts +3 -0
- package/packages/render-hyperframes/src/manifest.ts +7 -2
- package/packages/render-hyperframes/src/product.ts +41 -2
- package/packages/runtime-local/src/process-control.ts +4 -1
- package/packages/runtime-local/src/programs.ts +15 -3
- package/packages/runtime-local/src/supervisor.ts +8 -1
- package/packages/script/README.md +11 -4
- package/packages/script/src/format.ts +11 -24
- package/packages/script/src/manifest.ts +2 -2
- package/packages/script/src/parser.ts +16 -21
- package/packages/script/src/surface.ts +5 -3
- package/packages/script/src/types.ts +1 -1
- package/packages/seedance/README.md +11 -18
- package/packages/seedance/src/index.ts +7 -7
- package/packages/seedance/src/surface.ts +9 -8
- package/packages/seedance-kits/README.md +4 -3
- package/packages/source/README.md +1 -1
- package/packages/source/src/header.ts +2 -1
- package/packages/studio/README.md +13 -0
- package/packages/studio/src/feedback-server.ts +7 -0
- package/packages/studio/src/mutation-origin.ts +20 -0
- package/packages/studio/src/parameters.ts +3 -7
- package/packages/studio/src/preview/render.ts +7 -1
- package/packages/studio/src/server.ts +39 -0
- package/packages/studio/src/session.ts +7 -3
- package/packages/studio/src/ui/syntax.ts +11 -10
- package/packages/temporal-markup/EDITING.md +1 -1
- package/packages/video-cli/README.md +47 -4
- package/packages/video-cli/package.json +3 -0
- package/packages/video-cli/src/cli.ts +9 -9
- package/packages/video-cli/src/creation.ts +3 -3
- package/packages/video-cli/src/frame-grid.ts +34 -0
- package/packages/video-cli/src/index.ts +4 -0
- package/packages/video-cli/src/media-frames.ts +25 -0
- package/packages/video-cli/src/media.ts +198 -46
- package/packages/video-cli/src/process.ts +13 -3
- package/packages/video-cli/src/secret-input.ts +14 -0
- package/packages/video-cli/src/snapshot.ts +186 -0
- 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.
|
|
31
|
-
|
|
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}
|
|
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`)
|
|
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(
|
|
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 =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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(
|
|
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
|
|
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|>`
|
|
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
|
-
|
|
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 (
|
|
521
|
-
fail("SCRIPT_DUAL_EMPTY",
|
|
522
|
-
|
|
523
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
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="
|
|
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={
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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" }
|
|
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
|
|
180
|
-
summary: "
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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,
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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) {
|