@vosjs/cli 0.79.0 → 0.80.0
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 +71 -25
- package/dist/{assetManifest-U25FJER6.js → assetManifest-ETT3C7ZA.js} +2 -2
- package/dist/{chunk-Z6TLHRFQ.js → chunk-B373NVJE.js} +6 -2
- package/dist/chunk-B373NVJE.js.map +1 -0
- package/dist/{chunk-4ZUHEXUX.js → chunk-PJ6MEJZV.js} +205 -25
- package/dist/chunk-PJ6MEJZV.js.map +1 -0
- package/dist/{chunk-TWAP4BKR.js → chunk-SP5OHTVB.js} +2441 -603
- package/dist/chunk-SP5OHTVB.js.map +1 -0
- package/dist/cli.js +13 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/{run-UKWKAGDK.js → run-2ELHMPTB.js} +4 -4
- package/dist/{shotList-EE3S5LVF.js → shotList-MSNWLVDN.js} +30 -3
- package/dist/shotList-MSNWLVDN.js.map +1 -0
- package/package.json +3 -3
- package/schema/actions.schema.json +200 -4
- package/schema/doc.schema.json +9 -4
- package/skills/VERSION +1 -1
- package/skills/launch-kit/SKILL.md +6 -2
- package/skills/product-video/SKILL.md +27 -0
- package/skills/product-video/references/taste.md +7 -3
- package/skills/vos-cut/SKILL.md +17 -2
- package/dist/chunk-4ZUHEXUX.js.map +0 -1
- package/dist/chunk-TWAP4BKR.js.map +0 -1
- package/dist/chunk-Z6TLHRFQ.js.map +0 -1
- package/dist/shotList-EE3S5LVF.js.map +0 -1
- /package/dist/{assetManifest-U25FJER6.js.map → assetManifest-ETT3C7ZA.js.map} +0 -0
- /package/dist/{run-UKWKAGDK.js.map → run-2ELHMPTB.js.map} +0 -0
|
@@ -190,6 +190,10 @@
|
|
|
190
190
|
"maxLength": 80,
|
|
191
191
|
"description": "a beat's caption: vos deliver lands it as a lower-third at this step's moment on cuts that take words (feed cuts, the demo), never on a loop"
|
|
192
192
|
},
|
|
193
|
+
"zoom": {
|
|
194
|
+
"type": "boolean",
|
|
195
|
+
"description": "the cut's camera on this step: true marks it as its beat's payoff (vos plan --zooms per-beat keeps the zoom on it and only it), false means never zoom here (the planner's proposal on it is rejected); the planner still decides level and focus"
|
|
196
|
+
},
|
|
193
197
|
"read": {
|
|
194
198
|
"type": "string",
|
|
195
199
|
"minLength": 1,
|
|
@@ -280,6 +284,10 @@
|
|
|
280
284
|
"maxLength": 80,
|
|
281
285
|
"description": "a beat's caption: vos deliver lands it as a lower-third at this step's moment on cuts that take words (feed cuts, the demo), never on a loop"
|
|
282
286
|
},
|
|
287
|
+
"zoom": {
|
|
288
|
+
"type": "boolean",
|
|
289
|
+
"description": "the cut's camera on this step: true marks it as its beat's payoff (vos plan --zooms per-beat keeps the zoom on it and only it), false means never zoom here (the planner's proposal on it is rejected); the planner still decides level and focus"
|
|
290
|
+
},
|
|
283
291
|
"read": {
|
|
284
292
|
"type": "string",
|
|
285
293
|
"minLength": 1,
|
|
@@ -357,6 +365,10 @@
|
|
|
357
365
|
"maxLength": 80,
|
|
358
366
|
"description": "a beat's caption: vos deliver lands it as a lower-third at this step's moment on cuts that take words (feed cuts, the demo), never on a loop"
|
|
359
367
|
},
|
|
368
|
+
"zoom": {
|
|
369
|
+
"type": "boolean",
|
|
370
|
+
"description": "the cut's camera on this step: true marks it as its beat's payoff (vos plan --zooms per-beat keeps the zoom on it and only it), false means never zoom here (the planner's proposal on it is rejected); the planner still decides level and focus"
|
|
371
|
+
},
|
|
360
372
|
"read": {
|
|
361
373
|
"type": "string",
|
|
362
374
|
"minLength": 1,
|
|
@@ -448,6 +460,10 @@
|
|
|
448
460
|
"maxLength": 80,
|
|
449
461
|
"description": "a beat's caption: vos deliver lands it as a lower-third at this step's moment on cuts that take words (feed cuts, the demo), never on a loop"
|
|
450
462
|
},
|
|
463
|
+
"zoom": {
|
|
464
|
+
"type": "boolean",
|
|
465
|
+
"description": "the cut's camera on this step: true marks it as its beat's payoff (vos plan --zooms per-beat keeps the zoom on it and only it), false means never zoom here (the planner's proposal on it is rejected); the planner still decides level and focus"
|
|
466
|
+
},
|
|
451
467
|
"read": {
|
|
452
468
|
"type": "string",
|
|
453
469
|
"minLength": 1,
|
|
@@ -617,6 +633,10 @@
|
|
|
617
633
|
"maxLength": 80,
|
|
618
634
|
"description": "a beat's caption: vos deliver lands it as a lower-third at this step's moment on cuts that take words (feed cuts, the demo), never on a loop"
|
|
619
635
|
},
|
|
636
|
+
"zoom": {
|
|
637
|
+
"type": "boolean",
|
|
638
|
+
"description": "the cut's camera on this step: true marks it as its beat's payoff (vos plan --zooms per-beat keeps the zoom on it and only it), false means never zoom here (the planner's proposal on it is rejected); the planner still decides level and focus"
|
|
639
|
+
},
|
|
620
640
|
"read": {
|
|
621
641
|
"type": "string",
|
|
622
642
|
"minLength": 1,
|
|
@@ -639,7 +659,7 @@
|
|
|
639
659
|
"type": "object",
|
|
640
660
|
"required": ["do"],
|
|
641
661
|
"additionalProperties": false,
|
|
642
|
-
"description": "Check the page is as the flow expects, on camera, with no cursor move: exactly one of selector (visible; with enabled, enabled or disabled), text (some visible text contains it)
|
|
662
|
+
"description": "Check the page is as the flow expects, on camera, with no cursor move: exactly one of selector (visible; with enabled, enabled or disabled), text (some visible text contains it), gone (no longer visible) or js (an expression evaluated in the page: holds when its value is truthy, or with equals when it equals that value; for state an app keeps in script, such as a canvas's shapes). A condition that never holds is a skip with its reason (a js check names the value it last got), which --strict turns into exit 2. A js check's last value is kept on meta.steps[].value. A hosted take refuses js.",
|
|
643
663
|
"oneOf": [
|
|
644
664
|
{
|
|
645
665
|
"required": ["selector"]
|
|
@@ -649,6 +669,9 @@
|
|
|
649
669
|
},
|
|
650
670
|
{
|
|
651
671
|
"required": ["gone"]
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
"required": ["js"]
|
|
652
675
|
}
|
|
653
676
|
],
|
|
654
677
|
"properties": {
|
|
@@ -671,6 +694,14 @@
|
|
|
671
694
|
"type": "boolean",
|
|
672
695
|
"description": "with selector: wait for the element to be enabled (true) or disabled (false)"
|
|
673
696
|
},
|
|
697
|
+
"js": {
|
|
698
|
+
"type": "string",
|
|
699
|
+
"minLength": 1,
|
|
700
|
+
"description": "a JavaScript expression evaluated in the page (a promise is awaited): the check holds when its value is truthy, or, with equals, when its value equals that one"
|
|
701
|
+
},
|
|
702
|
+
"equals": {
|
|
703
|
+
"description": "with js: the value the expression must answer, compared as JSON"
|
|
704
|
+
},
|
|
674
705
|
"timeout": {
|
|
675
706
|
"type": "number",
|
|
676
707
|
"exclusiveMinimum": 0,
|
|
@@ -706,8 +737,6 @@
|
|
|
706
737
|
"until": {
|
|
707
738
|
"type": "object",
|
|
708
739
|
"additionalProperties": false,
|
|
709
|
-
"minProperties": 1,
|
|
710
|
-
"maxProperties": 1,
|
|
711
740
|
"properties": {
|
|
712
741
|
"selector": {
|
|
713
742
|
"type": "string",
|
|
@@ -720,8 +749,91 @@
|
|
|
720
749
|
"gone": {
|
|
721
750
|
"type": "string",
|
|
722
751
|
"minLength": 1
|
|
752
|
+
},
|
|
753
|
+
"js": {
|
|
754
|
+
"type": "string",
|
|
755
|
+
"minLength": 1,
|
|
756
|
+
"description": "a JavaScript expression evaluated in the page (a promise is awaited): holds when its value is truthy, or, with equals, when its value equals that one. A hosted take refuses it."
|
|
757
|
+
},
|
|
758
|
+
"equals": {
|
|
759
|
+
"description": "with js: the value the expression must answer, compared as JSON"
|
|
723
760
|
}
|
|
724
|
-
}
|
|
761
|
+
},
|
|
762
|
+
"oneOf": [
|
|
763
|
+
{
|
|
764
|
+
"required": ["selector"],
|
|
765
|
+
"not": {
|
|
766
|
+
"anyOf": [
|
|
767
|
+
{
|
|
768
|
+
"required": ["text"]
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
"required": ["gone"]
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
"required": ["js"]
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
"required": ["equals"]
|
|
778
|
+
}
|
|
779
|
+
]
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
"required": ["text"],
|
|
784
|
+
"not": {
|
|
785
|
+
"anyOf": [
|
|
786
|
+
{
|
|
787
|
+
"required": ["selector"]
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
"required": ["gone"]
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
"required": ["js"]
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
"required": ["equals"]
|
|
797
|
+
}
|
|
798
|
+
]
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
"required": ["gone"],
|
|
803
|
+
"not": {
|
|
804
|
+
"anyOf": [
|
|
805
|
+
{
|
|
806
|
+
"required": ["selector"]
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
"required": ["text"]
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
"required": ["js"]
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"required": ["equals"]
|
|
816
|
+
}
|
|
817
|
+
]
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
"required": ["js"],
|
|
822
|
+
"not": {
|
|
823
|
+
"anyOf": [
|
|
824
|
+
{
|
|
825
|
+
"required": ["selector"]
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
"required": ["text"]
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
"required": ["gone"]
|
|
832
|
+
}
|
|
833
|
+
]
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
]
|
|
725
837
|
},
|
|
726
838
|
"timeout": {
|
|
727
839
|
"type": "number",
|
|
@@ -862,6 +974,10 @@
|
|
|
862
974
|
"maxLength": 80,
|
|
863
975
|
"description": "a beat's caption: vos deliver lands it as a lower-third at this step's moment on cuts that take words (feed cuts, the demo), never on a loop"
|
|
864
976
|
},
|
|
977
|
+
"zoom": {
|
|
978
|
+
"type": "boolean",
|
|
979
|
+
"description": "the cut's camera on this step: true marks it as its beat's payoff (vos plan --zooms per-beat keeps the zoom on it and only it), false means never zoom here (the planner's proposal on it is rejected); the planner still decides level and focus"
|
|
980
|
+
},
|
|
865
981
|
"read": {
|
|
866
982
|
"type": "string",
|
|
867
983
|
"minLength": 1,
|
|
@@ -871,6 +987,86 @@
|
|
|
871
987
|
}
|
|
872
988
|
]
|
|
873
989
|
}
|
|
990
|
+
},
|
|
991
|
+
"peers": {
|
|
992
|
+
"type": "array",
|
|
993
|
+
"maxItems": 4,
|
|
994
|
+
"description": "Other people on the page, OFF CAMERA: each a browser context of its own (its own storage state, so its own user, or signed out) on the same url, driven by its own steps. Only the take's page is filmed and only the take's steps make the cursor track; what a peer does reaches the footage through the app's own sync (its presence, its remote cursors, the shape it drew). A peer step starts with a take step (`with`, as it starts) or after one (`after`, as it ends), named by id, else right after the peer's previous step (the first, as the camera rolls). The take never waits on a peer: wait for what the peer's work shows with the take's own expect or await. A hosted take refuses peers.",
|
|
995
|
+
"items": {
|
|
996
|
+
"type": "object",
|
|
997
|
+
"required": ["id", "steps"],
|
|
998
|
+
"additionalProperties": false,
|
|
999
|
+
"properties": {
|
|
1000
|
+
"id": {
|
|
1001
|
+
"type": "string",
|
|
1002
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$",
|
|
1003
|
+
"description": "a short word lines and meta.peers name the peer by"
|
|
1004
|
+
},
|
|
1005
|
+
"url": {
|
|
1006
|
+
"type": "string",
|
|
1007
|
+
"description": "the page the peer opens (http or https); absent, the take's url"
|
|
1008
|
+
},
|
|
1009
|
+
"viewport": {
|
|
1010
|
+
"type": "object",
|
|
1011
|
+
"required": ["width", "height"],
|
|
1012
|
+
"properties": {
|
|
1013
|
+
"width": {
|
|
1014
|
+
"type": "number"
|
|
1015
|
+
},
|
|
1016
|
+
"height": {
|
|
1017
|
+
"type": "number"
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
"storageState": {
|
|
1022
|
+
"type": "string",
|
|
1023
|
+
"minLength": 1,
|
|
1024
|
+
"description": "a Playwright storage state file (the second person's sign-in), by a path from the actions file's directory or an absolute one. Opened by the browser only: never copied into the take, never uploaded; a pushed script keeps its file name only. Absent, the peer is signed out. A peer that lands on a sign-in refuses the take."
|
|
1025
|
+
},
|
|
1026
|
+
"steps": {
|
|
1027
|
+
"type": "array",
|
|
1028
|
+
"minItems": 1,
|
|
1029
|
+
"maxItems": 200,
|
|
1030
|
+
"description": "The peer's steps: the take's own step shapes (see steps), for wait, move, hover, click, type, press, scroll, drag (path inline only) and expect, without guard, read, caption or settle, plus when each starts. Every step is bounded like a take step; a miss is a skip listed with the take's (peer set), which --strict fails on.",
|
|
1031
|
+
"items": {
|
|
1032
|
+
"type": "object",
|
|
1033
|
+
"required": ["do"],
|
|
1034
|
+
"properties": {
|
|
1035
|
+
"do": {
|
|
1036
|
+
"enum": [
|
|
1037
|
+
"wait",
|
|
1038
|
+
"move",
|
|
1039
|
+
"hover",
|
|
1040
|
+
"click",
|
|
1041
|
+
"type",
|
|
1042
|
+
"press",
|
|
1043
|
+
"scroll",
|
|
1044
|
+
"drag",
|
|
1045
|
+
"expect"
|
|
1046
|
+
]
|
|
1047
|
+
},
|
|
1048
|
+
"with": {
|
|
1049
|
+
"type": "string",
|
|
1050
|
+
"minLength": 1,
|
|
1051
|
+
"description": "start as the take's step with this id starts: the two run together"
|
|
1052
|
+
},
|
|
1053
|
+
"after": {
|
|
1054
|
+
"type": "string",
|
|
1055
|
+
"minLength": 1,
|
|
1056
|
+
"description": "start once the take's step with this id has ended"
|
|
1057
|
+
},
|
|
1058
|
+
"id": {
|
|
1059
|
+
"type": "string",
|
|
1060
|
+
"minLength": 1
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
"not": {
|
|
1064
|
+
"required": ["with", "after"]
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
874
1070
|
}
|
|
875
1071
|
},
|
|
876
1072
|
"$defs": {
|
package/schema/doc.schema.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"segments": {
|
|
29
29
|
"type": "array",
|
|
30
|
-
"description": "Kept SOURCE-time footage spans; the output timeline is their concatenation. Trim dead time by shrinking spans. Empty array = untrimmed. Never carry `rate` here (only lowered/derived segments do).",
|
|
30
|
+
"description": "Kept SOURCE-time footage spans; the output timeline is their concatenation, in list order. Trim dead time by shrinking spans. Empty array = untrimmed. Never carry `rate` here (only lowered/derived segments do). Unlike every other span list, segments may overlap: a segment may play seconds an earlier one already played (a cold open showing the end state, then the whole recording from its start), and every source-anchored span over those seconds (zoom, tilt, camMotion, speed, freeze, the cursor's clicks) plays in each occurrence. A layer anchored to a step starts in the pass where the step plays the longest. A segment with `camera: rest` keeps the camera at rest through its pass.",
|
|
31
31
|
"items": {
|
|
32
32
|
"type": "object",
|
|
33
33
|
"required": ["in", "out"],
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"media": {
|
|
56
56
|
"type": "string",
|
|
57
57
|
"description": "the media this belongs to: a `media[].id`; absent = the primary, `source`"
|
|
58
|
+
},
|
|
59
|
+
"camera": {
|
|
60
|
+
"enum": ["rest"],
|
|
61
|
+
"description": "`rest`: the camera rests through this clip's pass. No zoom, tilt, cam move or follow recenter applies there, even over seconds another pass zooms on (a cold open of the settled whole before the recording plays with its zooms); the cursor, the click effects, the speed spans and the freezes still play. A span that runs into or out of the clip ramps at its edge as at any span's. Absent = the camera plays here as everywhere"
|
|
58
62
|
}
|
|
59
63
|
},
|
|
60
64
|
"additionalProperties": true
|
|
@@ -848,7 +852,8 @@
|
|
|
848
852
|
"description": "Screen-space layers drawn ABOVE the card, outside the zoom transform. OUTPUT-anchored (start is final-cut seconds \u2014 trims/speed never retime a title). A layer about something on the page names it with `pin` and is placed beside it, following it as the camera moves; a layer with no referent is a caption and sits in the margin. Optional; absent = none.",
|
|
849
853
|
"items": {
|
|
850
854
|
"type": "object",
|
|
851
|
-
"required": ["id", "kind", "
|
|
855
|
+
"required": ["id", "kind", "duration", "transform"],
|
|
856
|
+
"anyOf": [{ "required": ["start"] }, { "required": ["anchor"] }],
|
|
852
857
|
"additionalProperties": true,
|
|
853
858
|
"properties": {
|
|
854
859
|
"id": {
|
|
@@ -957,13 +962,13 @@
|
|
|
957
962
|
"start": {
|
|
958
963
|
"type": "number",
|
|
959
964
|
"minimum": 0,
|
|
960
|
-
"description": "OUTPUT seconds.
|
|
965
|
+
"description": "OUTPUT seconds. Required unless the layer carries an `anchor`; with one it is the fallback, used when the anchor cannot resolve, and may be left out (a layer whose anchor resolves nowhere and that has no start is refused by validate)"
|
|
961
966
|
},
|
|
962
967
|
"anchor": {
|
|
963
968
|
"type": "object",
|
|
964
969
|
"required": ["step"],
|
|
965
970
|
"additionalProperties": false,
|
|
966
|
-
"description": "When the layer starts, as a recorder step: the lowering maps the step's moment (meta.steps, SOURCE seconds of its media) through the segments, speed spans and freezes exactly as a source-anchored span is mapped, and adds `offset`. A trim, a speed change or a re-record moves the layer with its step; `start` is the fallback when the step is missing, skipped or cut out (validate says which), and `duration` stays the layer's own. A text layer's {elapsed} fills with the anchored step's off-camera wait (the meta.skips entry naming the step, else its offCameraMs; a step that waited for nothing reads the nearest earlier wait on its media) as a rounded duration, like \"12 minutes\". Not the re-record `anchor` on spans, which the lowering never reads",
|
|
971
|
+
"description": "When the layer starts, as a recorder step: the lowering maps the step's moment (meta.steps, SOURCE seconds of its media) through the segments, speed spans and freezes exactly as a source-anchored span is mapped, and adds `offset`. A trim, a speed change or a re-record moves the layer with its step; `start` is the fallback when the step is missing, skipped or cut out (validate says which) and may be left out, and `duration` stays the layer's own. On a cut that replays the step's seconds the layer starts in the pass where the step plays the longest (the first on a tie). A text layer's {elapsed} fills with the anchored step's off-camera wait (the meta.skips entry naming the step, else its offCameraMs; a step that waited for nothing reads the nearest earlier wait on its media) as a rounded duration, like \"12 minutes\". Not the re-record `anchor` on spans, which the lowering never reads",
|
|
967
972
|
"properties": {
|
|
968
973
|
"step": {
|
|
969
974
|
"type": ["string", "integer"],
|
package/skills/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.26.0
|
|
@@ -91,7 +91,10 @@ labels typed, real-looking data, the feature mid-story.
|
|
|
91
91
|
**Write the story into `actions.json`.** Give steps an `id` (the re-render
|
|
92
92
|
loop and the template anchors address them by it) and a `caption` where a
|
|
93
93
|
beat deserves one (two to eight words; `plan` proposes it as a lower-third
|
|
94
|
-
at that step's moment on the cuts that take words)
|
|
94
|
+
at that step's moment on the cuts that take words), on the step whose
|
|
95
|
+
action it describes, and mark each beat's payoff `"zoom": true` so
|
|
96
|
+
`--zooms per-beat` keeps that zoom and no other (the `product-video`
|
|
97
|
+
skill, step 2, has the rule and the state check). After a click that
|
|
95
98
|
navigates, put a `wait` for the load, so the cut never dwells on a spinner.
|
|
96
99
|
|
|
97
100
|
- **Fresh recording**: the `product-video` skill's loop (explore →
|
|
@@ -231,7 +234,8 @@ by data. Never spread before the seed is signed off.
|
|
|
231
234
|
a frame near the start, not on what shipped.
|
|
232
235
|
- A cold open (`firstlast`): the first frame is the marketing page's
|
|
233
236
|
headline with the product nowhere; trim to the story or let the entrance
|
|
234
|
-
carry it.
|
|
237
|
+
carry it. A cold open of the RESULT is a repeated segment at camera rest
|
|
238
|
+
(the `vos-cut` skill, "Assemble").
|
|
235
239
|
- The loading plane on screen: a click that navigates needs the `wait`
|
|
236
240
|
after it.
|
|
237
241
|
- White words on a paper ground: the end card takes the brand's ink; a
|
|
@@ -134,6 +134,27 @@ Per-channel dimensions and byte budgets: `references/destinations.md`.
|
|
|
134
134
|
from hover-triggered menus (taste.md, flow rules). Check with
|
|
135
135
|
`vos validate actions.json`.
|
|
136
136
|
|
|
137
|
+
**Mark where the camera goes** (`@vosjs/cli` 0.80 and later). A step
|
|
138
|
+
with a `caption` starts a beat; put the caption on the step whose action
|
|
139
|
+
it describes. Mark each beat's PAYOFF `"zoom": true`: the moment the
|
|
140
|
+
viewer should see up close (the drag whose result is the point, the
|
|
141
|
+
click that opens the thing, the typing that names it). Mark a gesture
|
|
142
|
+
that must stay wide `"zoom": false`. Give every marked step an `id`.
|
|
143
|
+
Then `vos plan take --zooms per-beat` keeps exactly the marked zooms
|
|
144
|
+
and rejects the rest for good; the planner still picks level and
|
|
145
|
+
focus. Why marks: no rule inferred from a script matched a person's
|
|
146
|
+
zoom choices (the best got 3 of 5); marks reproduced all 5 and nothing
|
|
147
|
+
else.
|
|
148
|
+
|
|
149
|
+
**Check the app's own state** after any step that sets up the scene (a
|
|
150
|
+
document loaded, a count of items): `{ "do": "expect", "js":
|
|
151
|
+
"window.app.count()", "equals": 14 }`. A take can pass `--strict` with
|
|
152
|
+
the product in the wrong state (loaded twice, a duplicate on screen),
|
|
153
|
+
because the DOM says nothing about what a canvas app holds; with
|
|
154
|
+
`equals` a miss says what the app had. Read state only, never change
|
|
155
|
+
it. A collaboration product wants a second person on the page: `peers`
|
|
156
|
+
in `actions.json` (https://vos.so/llms-full.txt).
|
|
157
|
+
|
|
137
158
|
Then REHEARSE it (`@vosjs/cli` 0.39 and later):
|
|
138
159
|
`vos record --actions actions.json --out take --dry-run`. Every step runs
|
|
139
160
|
against the real page, in order, because a later selector usually exists
|
|
@@ -180,6 +201,8 @@ Per-channel dimensions and byte budgets: `references/destinations.md`.
|
|
|
180
201
|
- export: `{"resolution": "720p|1080p|2k|4k", "fps": 30}` — never above
|
|
181
202
|
the footage (validate warns).
|
|
182
203
|
Then `vos validate take --json` — lints must pass.
|
|
204
|
+
Several takes in one film, a cold open, an end card: the `vos-cut`
|
|
205
|
+
skill, "Assemble".
|
|
183
206
|
|
|
184
207
|
5. **Look before you render** (the taste.md quality loop):
|
|
185
208
|
- `vos frames take --at-zooms --times 0,25%,50%,75%,100% --json` → judge
|
|
@@ -268,3 +291,7 @@ when the ask is a release, not one video.
|
|
|
268
291
|
`EXPOSED` list, and `mask` what is left.
|
|
269
292
|
- A `mask` with `as: "text"` over product copy or a number: that is no
|
|
270
293
|
longer a recording of the product.
|
|
294
|
+
- Picking zooms by hand from a long proposal list (a canvas take proposes
|
|
295
|
+
one per gesture): mark the payoff steps and plan `--zooms per-beat`.
|
|
296
|
+
- Trusting `--strict` for the scene: it proves the steps ran, not that the
|
|
297
|
+
app holds what the beat shows. Check with `expect` + `js` + `equals`.
|
|
@@ -47,8 +47,9 @@ The rubric for judging a take before its render ships. Judge stills
|
|
|
47
47
|
7. **Levels 1.4–2.8 read well**; above 3× is for genuine detail (a number, a
|
|
48
48
|
toggle), and must be brief. Below 1.3× reads as drift, not intent.
|
|
49
49
|
8. **One zoom per beat.** Zooms that chain across unrelated UI read as a lost
|
|
50
|
-
camera. If the planner over-suggested,
|
|
51
|
-
|
|
50
|
+
camera. If the planner over-suggested, mark the payoff steps and plan
|
|
51
|
+
`--zooms per-beat` (SKILL.md, step 2) rather than deleting spans by
|
|
52
|
+
hand — an un-zoomed second is better than an unmotivated one.
|
|
52
53
|
9. **Focus on the subject, not the cursor resting spot.** `cx/cy` are
|
|
53
54
|
normalized [0..1]; check the apex still shows the thing the beat is about,
|
|
54
55
|
with breathing room (the subject inside the middle ~60% of the crop).
|
|
@@ -99,7 +100,10 @@ The two enemies, both measurable:
|
|
|
99
100
|
per ~5s**, and let them happen WIDE (place zoom spans so the camera has
|
|
100
101
|
released before a click that re-layouts; zoom into the RESULT, not the
|
|
101
102
|
trigger). Census: `ffmpeg -vf "select='gt(scene,0.12)'"` on the final
|
|
102
|
-
render — each detection should be a transition you _chose_.
|
|
103
|
+
render — each detection should be a transition you _chose_. Prefer
|
|
104
|
+
`vos check-video <file>`: it also counts a hard cut between two light,
|
|
105
|
+
sparse pictures (thin lines on white cutting to an empty canvas),
|
|
106
|
+
which an average-difference threshold misses.
|
|
103
107
|
|
|
104
108
|
## Judge each still set against
|
|
105
109
|
|
package/skills/vos-cut/SKILL.md
CHANGED
|
@@ -135,7 +135,10 @@ Per beat, against `plan` and the recipe:
|
|
|
135
135
|
decides the level, obey it. `cx`/`cy` = the moment's `focus`. Ids you
|
|
136
136
|
mint: `u1`, `u2`… In an editor recording, zoom the LANE where a span
|
|
137
137
|
appears, never the canvas being dragged. Release a zoom BEFORE a click
|
|
138
|
-
that navigates, so the page swap plays wide.
|
|
138
|
+
that navigates, so the page swap plays wide. A scripted take whose
|
|
139
|
+
steps carry `"zoom": true` marks (the `product-video` skill, step 2):
|
|
140
|
+
`vos plan <take> --zooms per-beat` keeps exactly those, so add a mark
|
|
141
|
+
to `actions.json` and re-plan rather than choosing among proposals.
|
|
139
142
|
- **Speed.** Keep proposals on typing/idle/scroll; add `2×`-`3×` on a stretch
|
|
140
143
|
whose `activity` is low and no beat needs, or under drag clusters; never
|
|
141
144
|
over a beat's payoff, never over playback.
|
|
@@ -143,6 +146,13 @@ Per beat, against `plan` and the recipe:
|
|
|
143
146
|
personality) and a beat earns punctuation. One pose per ~5s, ±5..18°.
|
|
144
147
|
- **Trim.** Head and tail: cut to the first and last thing that matters
|
|
145
148
|
(`segments`), leaving ~0.5s of settle at the end. Loading screens are cut.
|
|
149
|
+
- **Assemble.** A cold open is a SEGMENT that repeats seconds the cut plays
|
|
150
|
+
again later, marked `"camera": "rest"` so it shows the settled whole
|
|
151
|
+
while the main play zooms; never a second join of the same recording.
|
|
152
|
+
An end card or caption with an `anchor` to a step needs no `start`.
|
|
153
|
+
Several takes are one `vos join`; to add, swap or reorder an input
|
|
154
|
+
later, run the same join with `--keep-edits`, which keeps every trim,
|
|
155
|
+
span, caption and still made since. Never rebuild the joined take.
|
|
146
156
|
- **Text.** A caption per beat that has something to say, at a cadence (one
|
|
147
157
|
every 5-10s, 2.5-4s each, never two at once), in the product's words and
|
|
148
158
|
the video's intention; lower-third `y ≈ 0.82`; OUTPUT seconds; never over
|
|
@@ -165,6 +175,7 @@ do not understand.
|
|
|
165
175
|
|
|
166
176
|
```bash
|
|
167
177
|
vos validate <take> # clean; READ the framing warnings, fix them
|
|
178
|
+
vos info <take> # every step's source seconds, and the cut in output seconds
|
|
168
179
|
vos digest <take> --no-frames # after retiming: fresh OUTPUT times, same frames
|
|
169
180
|
vos frames <take> --at-moments --at-zooms --times 0,25%,50%,75%,100%
|
|
170
181
|
vos frames <take> --times <every caption start + 1> # each caption on its own ground
|
|
@@ -246,4 +257,8 @@ never write a status line.
|
|
|
246
257
|
- leave the cold open frozen (a static page records as freeze-then-bang:
|
|
247
258
|
trim it, or speed the load);
|
|
248
259
|
- end wide on an empty page: the last frame is the poster, so the closing
|
|
249
|
-
span holds the result and its proof
|
|
260
|
+
span holds the result and its proof;
|
|
261
|
+
- write a helper script to print step times or where a layer lands:
|
|
262
|
+
`vos info <take>` reads both clocks;
|
|
263
|
+
- rebuild a joined take to add a scene, or join a recording to itself for
|
|
264
|
+
a cold open (`--keep-edits`; a repeated segment).
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin/assetManifest.ts","../src/plugin/pathName.ts","../src/plugin/media.ts","../src/plugin/take.ts","../src/plugin/uploadAsset.ts"],"sourcesContent":["/**\n * A program's declared files (`config.assets`) crossing to and from vos.so.\n *\n * On disk a manifest names files beside the config (`./logo.png`), so a\n * program renders offline and reads like the repository it lives in. On the\n * platform a manifest names hosted assets (`asset:<id>`), the one spelling\n * the platform resolves for every surface: a private file reaches a render\n * page, a remix brings its files along, and a file a program still plays is\n * never collected. A push is the crossing one way, `vos fetch --media` the\n * other; neither rewrites the other side's spelling in place.\n */\nimport { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs'\nimport { join, relative } from 'node:path'\nimport { hostedAssetId, localFile, manifestRefs } from '../programAssets'\nimport { safePathName } from './pathName'\nimport { writeFileSafeSync } from './safeWrite'\nimport { downloadMedia, uploadDocRefs } from './media'\nimport type { DocMediaRef } from './media'\nimport type { UploadTarget } from './uploadAsset'\n\ntype Manifest = Record<string, { ref: string | string[] }>\n\nconst isUrl = (ref: string) => /^(https?:|data:|blob:)/.test(ref)\n\n/** The origins whose `/api/assets/<id>/file` is a hosted file of ours. */\nconst homeOrigins = (origin: string) => [\n origin,\n 'https://vos.so',\n 'https://www.vos.so',\n]\n\n/** `assets/.hosted.json`: which hosted file each file brought home is. */\nconst HOME_INDEX = '.hosted.json'\n\n/** A deep copy of the manifest, so a caller's rewrite never reaches disk. */\nfunction cloneManifest(config: Record<string, unknown>): Manifest {\n return JSON.parse(JSON.stringify(config.assets ?? {})) as Manifest\n}\n\nfunction setRef(\n manifest: Manifest,\n name: string,\n index: number | null,\n next: string,\n): void {\n const decl = manifest[name]\n if (index === null) decl.ref = next\n else (decl.ref as string[])[index] = next\n}\n\nconst where = (name: string, index: number | null) =>\n index === null ? `assets.${name}` : `assets.${name}[${index}]`\n\n/**\n * Upload the manifest's local files and point every entry at its hosted\n * asset as `asset:<id>`; a hosted file spelled as a URL takes the same\n * spelling. `config.assets` is REPLACED on the object (the file on disk is\n * untouched), so everything composed from it afterwards carries the hosted\n * refs. Uploads are content-addressed: a re-push sends nothing.\n */\nexport async function uploadManifest(\n config: Record<string, unknown>,\n dir: string,\n target: UploadTarget,\n log: (line: string) => void,\n): Promise<{ uploaded: number }> {\n const refs = manifestRefs(config)\n if (refs.length === 0) return { uploaded: 0 }\n const manifest = cloneManifest(config)\n const local: DocMediaRef[] = []\n for (const { name, index, ref } of refs) {\n const hosted = hostedAssetId(ref, homeOrigins(target.origin))\n if (hosted) {\n setRef(manifest, name, index, `asset:${hosted}`)\n continue\n }\n if (isUrl(ref)) continue\n // A push uploads what the manifest names, so it holds the same line a\n // render does: only a file inside the program's own directory.\n const found = localFile(ref, dir)\n if ('refused' in found) {\n throw new Error(`${where(name, index)} names ${ref}, ${found.refused}`)\n }\n local.push({\n where: where(name, index),\n // The upload helper reads keys against the directory.\n key: relative(realpathSync(dir), found.file),\n set: (url) => {\n const id = hostedAssetId(url, homeOrigins(target.origin))\n setRef(manifest, name, index, id ? `asset:${id}` : url)\n },\n })\n }\n await uploadDocRefs(local, dir, target, log)\n config.assets = manifest\n return { uploaded: local.length }\n}\n\n/** The manifest entries that are still files on this machine. */\nexport function localManifestRefs(config: Record<string, unknown>): string[] {\n return manifestRefs(config)\n .filter(({ ref }) => !isUrl(ref) && !hostedAssetId(ref, homeOrigins('')))\n .map(({ name, index }) => where(name, index))\n}\n\n/**\n * Bring a fetched program's hosted files home: each `asset:<id>` lands as\n * `assets/<name>.<ext>` beside the config (the bytes choose the extension)\n * and its entry is re-pointed at that file.\n *\n * A file already there is kept only when it IS that hosted file\n * (`assets/.hosted.json` remembers which id each one came from). Kept by\n * name alone, a second fetch after someone changed `logo` left the old\n * picture under the new manifest, and the next push stored the old one\n * again without a word.\n *\n * Returns the rewritten manifest, or null when nothing in it is hosted.\n */\nexport async function pullManifest(\n ctx: { origin: string; key: string | null },\n dir: string,\n config: Record<string, unknown>,\n log: (line: string) => void,\n): Promise<{ assets: Manifest; files: string[] } | null> {\n const home = homeOrigins(ctx.origin)\n const hosted = manifestRefs(config).filter(({ ref }) =>\n hostedAssetId(ref, home),\n )\n if (hosted.length === 0) return null\n const manifest = cloneManifest(config)\n const indexPath = join(dir, 'assets', HOME_INDEX)\n let index: Record<string, string> = {}\n try {\n const parsed: unknown = JSON.parse(readFileSync(indexPath, 'utf8'))\n if (parsed && typeof parsed === 'object') {\n index = parsed as Record<string, string>\n }\n } catch {\n // no index yet: nothing here is known to be a hosted file\n }\n const files: string[] = []\n for (const { name, index: at, ref } of hosted) {\n const id = hostedAssetId(ref, home)!\n // The manifest name is the config's word and becomes a file name here,\n // so it is held to a plain path segment (the asset id stands in).\n const stem = safePathName(at === null ? name : `${name}-${at}`, id)\n const known = Object.keys(index).find(\n (f) => f === stem || f.startsWith(`${stem}.`),\n )\n let file: string\n if (\n known &&\n index[known] === id &&\n existsSync(join(dir, 'assets', known))\n ) {\n file = `assets/${known}`\n } else {\n // A different hosted file under this name: the old copy goes first,\n // or a changed extension would leave both lying side by side.\n if (known) {\n rmSync(join(dir, 'assets', known), { force: true })\n delete index[known]\n }\n const got = await downloadMedia(ctx, `/api/assets/${id}/file`, {\n dir,\n subdir: 'assets',\n stem,\n fallbackExt: '',\n describe: `${where(name, at)} (asset ${id})`,\n notFoundHint: 'a private file needs a content key of its owner',\n })\n file = got.file\n index[file.slice('assets/'.length)] = id\n log(\n ` ${file} ← ${where(name, at)}, asset ${id} (${Math.round(got.bytes / 1024)} kB)`,\n )\n }\n files.push(file)\n setRef(manifest, name, at, `./${file}`)\n }\n writeFileSafeSync(indexPath, JSON.stringify(index, null, 2) + '\\n')\n return { assets: manifest, files }\n}\n\nconst HOSTED_FILE_LITERAL =\n /(?:asset:|\\/api\\/assets\\/)([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})/g\nconst FUNCTION_FIELDS = ['setup', 'createContent', 'createTimeline', 'onFrame']\n\n/**\n * A hosted file named inside a function instead of in the manifest. The\n * platform cannot give a string in code a URL per surface, so the file does\n * not reach a render of a private program and does not follow a remix.\n * One line per function and file, in words that say the fix.\n */\nexport function hostedLiteralWarnings(\n config: Record<string, unknown>,\n): string[] {\n const out: string[] = []\n const scan = (label: string, src: unknown) => {\n if (typeof src !== 'string') return\n const seen = new Set<string>()\n for (const m of src.matchAll(HOSTED_FILE_LITERAL)) {\n if (seen.has(m[1])) continue\n seen.add(m[1])\n out.push(\n `${label} names hosted file ${m[1]} in code. Declare it instead: \"assets\": { \"name\": { \"ref\": \"asset:${m[1]}\" } }, and read ctx.assets.name, so it loads on every render and follows a remix`,\n )\n }\n }\n for (const field of FUNCTION_FIELDS) scan(field, config[field])\n const stack = Array.isArray(config.stack) ? config.stack : []\n for (const entry of stack as Record<string, unknown>[]) {\n for (const field of ['setup', 'createContent', 'onFrame']) {\n scan(`stack.${String(entry.id)}.${field}`, entry[field])\n }\n }\n return out\n}\n","/**\n * A name the server chose (a slug, a title, a file name) as a path segment\n * on this disk. The server's word is never trusted as a path: a separator,\n * a `..`, a control character or a leading dot is dropped, everything else\n * collapses to `[A-Za-z0-9._-]`, and a name with nothing left falls back to\n * the id, which is ours to shape.\n */\nexport function safePathName(\n name: string | undefined,\n fallback: string,\n): string {\n const cleaned = (name ?? '')\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f\\u007f]/g, '')\n .split(/[\\\\/]/)\n .filter((part) => part !== '' && part !== '.' && part !== '..')\n .join('-')\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/^[.-]+/, '')\n .replace(/-+$/, '')\n .slice(0, 80)\n if (cleaned) return cleaned\n const id = fallback.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[.-]+/, '')\n return id || 'file'\n}\n","import { isKeyedOverlay } from '@vosjs/studio-core'\n/**\n * A hosted take comes home: download the recording (and the\n * mic/cam sidecars when the doc carries them) beside a pulled doc.json, and\n * re-anchor the doc's keys to the local files, so `vos digest`, `frames` and\n * `render` run on a pulled take exactly as on a local one. Also writes the\n * take-dir markers (meta.json, cursor.json) from the doc when absent, so the\n * directory IS a take directory to every other verb.\n *\n * Same consent shape as the upload direction: nothing moves without the verb\n * the user ran (`--media`). A file already on disk is kept, never re-fetched.\n */\nimport { createHash } from 'node:crypto'\nimport { createWriteStream, existsSync } from 'node:fs'\nimport {\n mkdir,\n open,\n readFile,\n readdir,\n rename,\n rm,\n stat,\n} from 'node:fs/promises'\nimport { basename, dirname, join } from 'node:path'\nimport { Readable } from 'node:stream'\nimport { pipeline } from 'node:stream/promises'\nimport { localFile } from '../programAssets'\nimport { refuseSymlink, writeFileSafe } from './safeWrite'\nimport { takeMediaFiles, writeJson } from './take'\nimport {\n MEDIA_HEAD_BYTES,\n extensionFor,\n nameForType,\n resolveMediaType,\n} from './container'\nimport { uploadAsset } from './uploadAsset'\nimport { bearerFor, writeSyncState } from './platform'\nimport type { UploadTarget, UploadedAsset } from './uploadAsset'\nimport type {\n AudioClip,\n ObjectClip,\n OverlayClip,\n ProjectDoc,\n} from '@vosjs/studio-core'\n\n/** The names the recorders write their sidecars under. */\nexport const MIC_NAME = 'mic.webm'\nexport const CAM_NAME = 'cam.webm'\n\n/**\n * A take's own media: the stem each one lives under, and the container to\n * assume when the bytes that come back say nothing. The EXTENSION is not part\n * of the identity — a pulled recording wears whatever its hosted asset holds.\n */\nconst SOURCE_MEDIA = [\n { key: 'videoKey', stem: 'recording', fallbackExt: '.webm' },\n { key: 'micKey', stem: 'mic', fallbackExt: '.webm' },\n { key: 'camKey', stem: 'cam', fallbackExt: '.webm' },\n] as const\n\n/** A take-relative media key: neither a URL, a blob, nor a hosted asset path. */\nexport function isTakeRelativeKey(key: string | undefined): key is string {\n // `media:<id>` names a document media (a layer that shows one); the media\n // itself rides the upload door, so the reference is never a file.\n return (\n !!key &&\n !/^(https?:|blob:|data:|media:|\\/\\/)/.test(key) &&\n !key.startsWith('/api/')\n )\n}\n\n/** The file a take-relative key names, under the take directory. */\nexport const takeRelativeFile = (key: string): string => key.replace(/^\\/+/, '')\n\nexport interface DocMediaRef {\n /** Where the key lives, in words (for a log line). */\n where: string\n key: string\n /** Replace the key in the document. */\n set: (next: string) => void\n}\n\n/**\n * A take's own sidecars, when they are local files: the voice (`micKey`)\n * and the webcam (`camKey`). A push used to DROP them (\"local-only\"), while\n * `vos fetch --media` writes them beside the take as `mic.webm` and\n * `cam.webm`: fetch, then push, and the voice and the webcam were gone from\n * the hosted take, in one log line nobody reads.\n */\nexport function sourceSidecarRefs(\n doc: ProjectDoc,\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n for (const [field, where] of [\n ['micKey', 'mic track'],\n ['camKey', 'cam track'],\n ] as const) {\n const key = doc.source[field]\n if (!key || !keep(key)) continue\n out.push({\n where,\n key,\n set: (next) => {\n doc.source[field] = next\n },\n })\n }\n return out\n}\n\n/** The 3D props a document places: each names a model file (a GLB). */\nexport function docObjectRefs(\n doc: { objects?: ObjectClip[] },\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n for (const clip of doc.objects ?? []) {\n const asset = clip.asset as { kind?: string; key?: string }\n if (asset.kind !== 'gltf' || !keep(asset.key)) continue\n out.push({\n where: `object ${clip.id}`,\n key: asset.key as string,\n set: (next) => {\n asset.key = next\n },\n })\n }\n return out\n}\n\n/** The picture and video layers a document places, by key. */\nexport function docOverlayRefs(\n doc: { overlays?: OverlayClip[] },\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n for (const clip of doc.overlays ?? []) {\n // An HTML layer has no key: its content IS its source. Its only files are\n // the faces it brings (`fonts`), each a url the page fetches, so a face\n // still beside the document crosses like any other layer file.\n if (clip.kind === 'html') {\n for (const [i, font] of (clip.fonts ?? []).entries()) {\n if (!keep(font.url)) continue\n out.push({\n where: `overlay ${clip.id} font ${i}`,\n key: font.url,\n set: (next) => {\n font.url = next\n },\n })\n }\n continue\n }\n if (!isKeyedOverlay(clip) || !keep(clip.key)) continue\n out.push({\n where: `overlay ${clip.id}`,\n key: clip.key,\n set: (next) => {\n clip.key = next\n },\n })\n }\n return out\n}\n\n/**\n * The shared layers either kind of document carries, as files: picture and\n * video overlays, 3D props, and added sound. A program document has no\n * recording, frame or end card, so this is all it can name; a take reads\n * the same three through `docMediaRefs`.\n */\nexport function docLayerRefs(\n doc: {\n overlays?: OverlayClip[]\n objects?: ObjectClip[]\n audio?: AudioClip[]\n },\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n return [\n ...docOverlayRefs(doc, keep),\n ...docObjectRefs(doc, keep),\n ...docAudioRefs(doc, keep),\n ]\n}\n\n/**\n * Every media key a document carries beside its recording, mic and cam:\n * image and video overlay clips (a poster's mark), 3D props, the end card's\n * mark, a background image or loop, added sound. The recording's own keys\n * are `KEYS` and handled by their own path. A push rehosts these; a pull\n * brings them home.\n */\nexport function docMediaRefs(\n doc: ProjectDoc,\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n // The take's OTHER media (concat): each recording, and its sidecars,\n // rides the upload door like the primary's; a pull brings them home.\n for (const m of doc.media ?? []) {\n if (keep(m.videoKey))\n out.push({\n where: `media ${m.id}`,\n key: m.videoKey,\n set: (next) => {\n m.videoKey = next\n },\n })\n const mic = m.micKey\n if (mic && keep(mic))\n out.push({\n where: `media ${m.id} mic`,\n key: mic,\n set: (next) => {\n m.micKey = next\n },\n })\n const cam = m.camKey\n if (cam && keep(cam))\n out.push({\n where: `media ${m.id} cam`,\n key: cam,\n set: (next) => {\n m.camKey = next\n },\n })\n }\n out.push(...docOverlayRefs(doc, keep))\n // A prop's model was never walked: a take with a GLB pushed a document\n // whose object keyed a file only the pusher's disk held.\n out.push(...docObjectRefs(doc, keep))\n const mark = doc.endCard?.mark\n if (mark && keep(mark.key))\n out.push({\n where: 'end card mark',\n key: mark.key,\n set: (next) => {\n mark.key = next\n },\n })\n const bg = doc.frame.backgroundMedia\n if (bg && keep(bg.key))\n out.push({\n where: 'background',\n key: bg.key,\n set: (next) => {\n bg.key = next\n },\n })\n out.push(...docAudioRefs(doc, keep))\n return out\n}\n\n/**\n * The sound a document adds (`doc.audio`: a score, a voice-over, an effect),\n * for either kind of document. A take reads it through `docMediaRefs`; a\n * program document, which has no recording or frame, reads it here. A\n * catalog track is an absolute URL and is never walked.\n */\nexport function docAudioRefs(\n doc: { audio?: AudioClip[] },\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n for (const clip of doc.audio ?? []) {\n if (!keep(clip.key)) continue\n out.push({\n where: `audio ${clip.id}`,\n key: clip.key,\n set: (next) => {\n // The same bytes under a new name (uploaded, or brought home): a\n // measurement taken for the old name holds for the new one.\n if (clip.loudness?.src === clip.key) clip.loudness.src = next\n clip.key = next\n },\n })\n }\n return out\n}\n\n/**\n * Upload the files a document names and point its keys at the hosted\n * assets. Each file rides the content-addressed upload door, so a re-push\n * reuses it; the bytes name the type, and the uploaded filename is corrected\n * to match so the asset is never self-contradictory. A key whose file is\n * missing is said and left as it is (the hosted render will 404 on it,\n * honestly). Take pushes and program pushes share it.\n */\nexport async function uploadDocRefs(\n refs: DocMediaRef[],\n dir: string,\n target: UploadTarget,\n log: (line: string) => void,\n): Promise<void> {\n for (const ref of refs) {\n // The key names a file in the take directory or below, never outside\n // it: a fetched or pasted document is not trusted with the disk.\n const local = localFile(takeRelativeFile(ref.key), dir)\n if ('refused' in local) {\n log(\n `warning ${ref.where}: ${ref.key} is not beside the document, so it was not uploaded`,\n )\n continue\n }\n const file = local.file\n const media = await readFile(file)\n const mediaHash = createHash('sha256').update(media).digest('hex')\n const { type } = resolveMediaType({\n head: media.subarray(0, MEDIA_HEAD_BYTES),\n filename: file,\n })\n const name = nameForType(basename(file), type)\n if (name !== basename(file)) {\n log(` ${ref.where}: ${basename(file)} holds ${type} — sent as ${name}`)\n }\n let put: UploadedAsset\n try {\n put = await uploadAsset(target, new Uint8Array(media), {\n filename: name,\n contentType: type,\n contentHash: mediaHash,\n })\n } catch (e) {\n throw new Error(\n `${ref.where} upload failed: ${e instanceof Error ? e.message : String(e)}`,\n )\n }\n ref.set(put.url)\n log(\n ` ${ref.where}: ${ref.key} → asset ${put.id}${put.reused ? ' (reused)' : ''}`,\n )\n for (const note of put.notes) log(` ${ref.where}: ${note}`)\n }\n}\n\n// The extension↔type table and the byte sniffer live in `container.ts`; these\n// stay the door every media caller already knows.\nexport { extensionFor, mediaContentType } from './container'\n\n/** An asset file URL (relative `/api/assets/{id}/file` or absolute). */\nexport function assetIdOf(url: string | undefined): string | null {\n if (!url) return null\n const m = /\\/api\\/assets\\/([A-Za-z0-9_-]+)\\/file/.exec(url)\n return m ? m[1] : null\n}\n\nexport interface MediaPullResult {\n downloaded: { file: string; assetId: string; bytes: number }[]\n kept: string[]\n /** Every hosted file now beside the take, downloaded or kept: its path\n * here and the address it came from. */\n hosted: { file: string; url: string }[]\n}\n\n/** The first bytes of a file, enough for every container signature. */\nexport async function readMediaHead(file: string): Promise<Uint8Array> {\n const handle = await open(file, 'r')\n try {\n const buf = Buffer.alloc(MEDIA_HEAD_BYTES)\n const { bytesRead } = await handle.read(buf, 0, MEDIA_HEAD_BYTES, 0)\n return new Uint8Array(buf.subarray(0, bytesRead))\n } finally {\n await handle.close()\n }\n}\n\nexport interface DownloadTarget {\n dir: string\n /** Directory under the take dir, when the file is filed away. */\n subdir?: string\n /** The name without its extension — the extension is the BYTES' to choose. */\n stem: string\n /** The container to assume when the bytes say nothing. */\n fallbackExt: string\n /** What is being fetched, in words, for an error or a log line. */\n describe: string\n /** Added to a 404, where the reason is usually the credential. */\n notFoundHint?: string\n}\n\n/**\n * One media file, brought home under the name its BYTES earn.\n *\n * Streamed to a `.part` sibling first, then sniffed and renamed, so a take\n * never holds a file whose extension lies — whatever the server declared, and\n * whatever the hosted asset happened to be called. A push of a pulled take\n * re-uploads that file, so a wrong name here becomes a wrong Content-Type on\n * the asset, and a render that dies inside the video element.\n */\n/**\n * Where a document key is fetched from, and whether the content key rides\n * along. The key goes ONLY to the configured origin: a hosted file named\n * as a file-door address on some other host is read from the home door by\n * its id, and any other URL is fetched bare, so a stranger's document\n * can never send the reader's credential elsewhere.\n */\nexport function downloadPlan(\n ctx: { origin: string },\n url: string,\n): { url: string; bearer: boolean } {\n const home = new URL(ctx.origin).origin\n if (!/^https?:/.test(url)) return { url: `${home}${url}`, bearer: true }\n let abs: URL\n try {\n abs = new URL(url)\n } catch {\n return { url, bearer: false }\n }\n if (abs.origin === home) return { url: abs.href, bearer: true }\n const id = assetIdOf(url)\n if (id) return { url: `${home}/api/assets/${id}/file`, bearer: true }\n return { url: abs.href, bearer: false }\n}\n\nexport async function downloadMedia(\n ctx: { origin: string; key: string | null },\n url: string,\n target: DownloadTarget,\n): Promise<{ file: string; bytes: number }> {\n const plan = downloadPlan(ctx, url)\n const res = await fetch(plan.url, {\n headers: plan.bearer ? bearerFor(ctx.key, plan.url) : {},\n })\n if (!res.ok || !res.body) {\n throw new Error(\n `download of ${target.describe} failed (${res.status})${\n res.status === 404 && target.notFoundHint\n ? ` — ${target.notFoundHint}`\n : ''\n }`,\n )\n }\n const into = target.subdir ? join(target.dir, target.subdir) : target.dir\n const partial = join(into, `${target.stem}.part`)\n await mkdir(dirname(partial), { recursive: true })\n // A link already at the path would carry the bytes wherever it points.\n await refuseSymlink(partial)\n try {\n await pipeline(\n Readable.fromWeb(res.body as never),\n createWriteStream(partial),\n )\n const { type } = resolveMediaType({\n head: await readMediaHead(partial),\n declared: res.headers.get('content-type'),\n filename: `${target.stem}${target.fallbackExt}`,\n })\n const name = `${target.stem}${extensionFor(type) || target.fallbackExt}`\n await refuseSymlink(join(into, name))\n await rename(partial, join(into, name))\n const file = target.subdir ? `${target.subdir}/${name}` : name\n return { file, bytes: (await stat(join(target.dir, file))).size }\n } finally {\n // A rename leaves nothing behind; a failed transfer must not.\n await rm(partial, { force: true })\n }\n}\n\nexport async function pullMedia(\n ctx: { origin: string; key: string | null },\n dir: string,\n doc: ProjectDoc,\n log: (line: string) => void,\n): Promise<MediaPullResult> {\n const result: MediaPullResult = { downloaded: [], kept: [], hosted: [] }\n for (const { key, stem, fallbackExt } of SOURCE_MEDIA) {\n const url = doc.source[key]\n const assetId = assetIdOf(url)\n if (!assetId) continue\n // A file already here is kept whatever container it wears, so a second\n // pull never re-fetches and never leaves two of the same footage.\n const present = takeMediaFiles(dir, stem)[0]\n if (present) {\n result.kept.push(basename(present))\n result.hosted.push({ file: basename(present), url: url! })\n doc.source[key] = basename(present)\n continue\n }\n const got = await downloadMedia(ctx, url!, {\n dir,\n stem,\n fallbackExt,\n describe: stem,\n notFoundHint: 'a private recording needs a content key of its owner',\n })\n result.downloaded.push({ file: got.file, assetId, bytes: got.bytes })\n log(` ${got.file} ← asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`)\n result.hosted.push({ file: got.file, url: url! })\n doc.source[key] = got.file\n }\n // The document's other media (a poster's mark, a pasted ground): a hosted\n // asset comes home under media/<assetId>.<ext> and the key re-anchors to\n // it, so the pulled poster renders and re-pushes as a whole.\n for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {\n const assetId = assetIdOf(ref.key)!\n const existing = existsSync(join(dir, 'media'))\n ? (await readdir(join(dir, 'media'))).find(\n (f) => f === assetId || f.startsWith(`${assetId}.`),\n )\n : undefined\n if (existing) {\n const file = `media/${existing}`\n result.kept.push(file)\n result.hosted.push({ file, url: ref.key })\n ref.set(file)\n continue\n }\n const got = await downloadMedia(ctx, ref.key, {\n dir,\n subdir: 'media',\n stem: assetId,\n // A type with no extension here (an svg served as such) keeps the bare\n // asset id, exactly as before.\n fallbackExt: '',\n describe: `${ref.where} (${ref.key})`,\n })\n result.downloaded.push({ file: got.file, assetId, bytes: got.bytes })\n log(\n ` ${got.file} ← ${ref.where}, asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`,\n )\n result.hosted.push({ file: got.file, url: ref.key })\n ref.set(got.file)\n }\n // The take-dir markers: meta.json and cursor.json live on the doc for a\n // hosted take; write them out so loadTake() recognizes the directory.\n const metaPath = join(dir, 'meta.json')\n if (!existsSync(metaPath)) await writeJson(metaPath, doc.source.meta, true)\n const cursorPath = join(dir, 'cursor.json')\n if (!existsSync(cursorPath)) await writeJson(cursorPath, doc.source.cursor)\n await writeFileSafe(join(dir, 'doc.json'), JSON.stringify(doc, null, 2))\n return result\n}\n\n/** The sha256 of a file beside the take, hex. */\nexport async function fileSha256(file: string): Promise<string> {\n return createHash('sha256')\n .update(await readFile(file))\n .digest('hex')\n}\n\n/**\n * Write what a pull brought home into vos.json (`hostedMedia`): each file's\n * path here, the address it came from, and its sha256 now. A claimable push\n * of the take names those originals instead of uploading, and only while\n * the bytes are the ones that were fetched.\n */\nexport async function recordHostedMedia(\n dir: string,\n vosId: string,\n pulled: MediaPullResult,\n): Promise<void> {\n if (pulled.hosted.length === 0) return\n const hostedMedia: Record<string, { ref: string; sha256: string }> = {}\n for (const { file, url } of pulled.hosted) {\n hostedMedia[file] = { ref: url, sha256: await fileSha256(join(dir, file)) }\n }\n writeSyncState(dir, { vosId, hostedMedia })\n}\n","/**\n * The take directory — the CLI's unit of work, human-inspectable on purpose:\n * take/\n * recording.webm encoded footage (CFR; .mp4 when a pull brought it home\n * in that container, so readers RESOLVE the name)\n * frames/ raw screencast JPEGs + frames.json (kept for re-encode)\n * cursor.json synthesized CursorTrack\n * meta.json RecordingMeta (producer: 'cli')\n * actions.json the script that produced it (replay recipe)\n * doc.json ProjectDoc — the agent-editable surface\n */\nimport { mkdir, readFile, rename, rm } from 'node:fs/promises'\nimport { existsSync, readdirSync, statSync } from 'node:fs'\nimport { writeFileSafe } from './safeWrite'\nimport { basename, join } from 'node:path'\nimport { TAKE_MEDIA_EXTENSIONS } from './container'\nimport type { CursorTrack, ProjectDoc, RecordingMeta } from '@vosjs/studio-core'\nimport type { ActionsFile } from './actions'\n\n/** The name `record` writes its footage under — its encoder emits WebM. */\nexport const RECORDING_NAME = 'recording.webm'\n\n/**\n * A take's own media under `dir`, whatever container it came home in.\n *\n * `record` writes WebM, but a PULLED take wears the container its hosted asset\n * actually holds, so nothing may spell `recording.webm` at a read site: a take\n * whose footage is mp4 must still render, digest and push. Ordered by\n * `TAKE_MEDIA_EXTENSIONS`, so the answer is deterministic if a stale sibling\n * survives.\n */\nexport function takeMediaFiles(dir: string, stem: string): string[] {\n return TAKE_MEDIA_EXTENSIONS.map((ext) => join(dir, `${stem}${ext}`)).filter(\n (file) => existsSync(file),\n )\n}\n\n/**\n * The one file a take's media stem names, falling back to what `record` would\n * write — so a caller can `existsSync` the answer and get an honest\n * \"no footage here\" rather than a path that never existed.\n */\nexport function takeMediaFile(\n dir: string,\n stem: string,\n fallback: string,\n): string {\n return takeMediaFiles(dir, stem)[0] ?? join(dir, fallback)\n}\n\n/** The previous cut, preserved by a re-record. */\nexport const PREV_DOC_NAME = 'doc.prev.json'\n\nexport interface TakePaths {\n dir: string\n recording: string\n /** The recording's bare filename — what a doc key or a served URL spells. */\n recordingName: string\n framesDir: string\n framesIndex: string\n cursor: string\n meta: string\n actions: string\n doc: string\n}\n\nexport function takePaths(dir: string): TakePaths {\n const recording = takeMediaFile(dir, 'recording', RECORDING_NAME)\n return {\n dir,\n recording,\n recordingName: basename(recording),\n framesDir: join(dir, 'frames'),\n framesIndex: join(dir, 'frames.json'),\n cursor: join(dir, 'cursor.json'),\n meta: join(dir, 'meta.json'),\n actions: join(dir, 'actions.json'),\n doc: join(dir, 'doc.json'),\n }\n}\n\nexport async function ensureTakeDir(dir: string): Promise<TakePaths> {\n const p = takePaths(dir)\n await mkdir(p.framesDir, { recursive: true })\n return p\n}\n\n/**\n * Drop the screencast JPEGs once the recording they were encoded into is on\n * disk. They are the bulk of a take (one hero take: 437 MB of a 452 MB\n * directory) and nothing needs them afterwards: the digest reads frames\n * from them WHEN PRESENT and from the video otherwise, which is the path\n * every pulled take already runs on. Refuses unless the recording exists and\n * has bytes, so a failed encode never costs the only copy of the footage.\n * Returns the bytes freed, 0 when it left everything alone.\n */\nexport async function dropScreencastFrames(p: TakePaths): Promise<number> {\n if (!existsSync(p.recording) || statSync(p.recording).size === 0) return 0\n if (!existsSync(p.framesDir)) return 0\n let freed = 0\n for (const name of readdirSync(p.framesDir)) {\n try {\n freed += statSync(join(p.framesDir, name)).size\n } catch {\n // a file gone between the listing and the stat frees nothing\n }\n }\n await rm(p.framesDir, { recursive: true, force: true })\n await rm(p.framesIndex, { force: true })\n return freed\n}\n\n/**\n * Re-record into an existing take WITHOUT destroying the cut (the\n * re-render loop's first rung; `rm -rf` here is what made \"re-renders on the\n * next version\" a false claim). What the old footage produced is cleared\n * (recording, cursor, meta, raw frames, stills/, digest/ — all derived and\n * regenerable); what the human's work lives in survives: `doc.json` moves to\n * `doc.prev.json` — the explicit reuse base `vos plan --reuse` re-anchors\n * onto the new recording — and `actions.json` + `vos.json` stay put. The doc\n * is MOVED, never left in place: a doc.json embeds its footage's cursor and\n * meta (`doc.source`), so leaving it beside new footage would be a document\n * lying about its source, and the auto-plan after recording would replan\n * from the OLD cursor. Returns what was kept, for the terminal to say.\n */\nexport async function prepareReRecord(\n dir: string,\n): Promise<{ prevDoc: boolean; kept: string[] }> {\n const p = takePaths(dir)\n const kept: string[] = []\n let prevDoc = false\n if (existsSync(p.doc)) {\n await rename(p.doc, join(dir, PREV_DOC_NAME))\n prevDoc = true\n kept.push(PREV_DOC_NAME)\n }\n if (existsSync(p.actions)) kept.push('actions.json')\n if (existsSync(join(dir, 'vos.json'))) kept.push('vos.json')\n for (const stale of [\n // Every container, not just the one `record` writes: a pulled take's\n // footage may be mp4, and a survivor would outrank the new recording.\n ...takeMediaFiles(dir, 'recording'),\n ...takeMediaFiles(dir, 'mic'),\n ...takeMediaFiles(dir, 'cam'),\n p.cursor,\n p.meta,\n p.framesIndex,\n p.framesDir,\n join(dir, 'stills'),\n join(dir, 'digest'),\n // What the last run's download steps kept: this run downloads again.\n join(dir, 'outputs'),\n // The sound the last run's page played: this run hears it again.\n join(dir, 'page-sound'),\n ]) {\n await rm(stale, { recursive: true, force: true })\n }\n return { prevDoc, kept }\n}\n\nexport async function readJson<T>(file: string): Promise<T> {\n return JSON.parse(await readFile(file, 'utf8')) as T\n}\n\nexport async function writeJson(\n file: string,\n value: unknown,\n pretty = false,\n): Promise<void> {\n await writeFileSafe(\n file,\n pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value),\n )\n}\n\nexport interface TakeData {\n paths: TakePaths\n cursor: CursorTrack\n meta: RecordingMeta\n actions: ActionsFile | null\n doc: ProjectDoc | null\n}\n\n/** Load a take directory; cursor+meta are required, doc/actions optional. */\nexport async function loadTake(dir: string): Promise<TakeData> {\n const paths = takePaths(dir)\n if (!existsSync(paths.meta)) {\n throw new Error(\n `${dir} is not a take directory (missing meta.json) — run record first`,\n )\n }\n const meta = await readJson<RecordingMeta>(paths.meta)\n const cursor = existsSync(paths.cursor)\n ? await readJson<CursorTrack>(paths.cursor)\n : []\n const actions = existsSync(paths.actions)\n ? await readJson<ActionsFile>(paths.actions)\n : null\n const doc = existsSync(paths.doc)\n ? await readJson<ProjectDoc>(paths.doc)\n : null\n return { paths, cursor, meta, actions, doc }\n}\n","/**\n * Uploading a file to vos.so: one door, for every kind and every size.\n *\n * The file is declared first, sent as parts, and sealed. Three reasons it\n * is never one request:\n *\n * - A request body has a ceiling the platform does not set and cannot\n * raise from inside a handler: the edge refuses anything past it before\n * the request arrives, with its own error page rather than a worded\n * refusal. A screen recording of any real length clears that ceiling.\n * - Everything the platform may refuse a file for (its kind, its size, a\n * daily rate, the account's storage) is weighed at the declaring call,\n * before a byte moves, so a refusal costs nothing and arrives in words.\n * - The declaring call is content-addressed: the same bytes pushed twice\n * are recognised and never re-sent, which is what keeps an iterating\n * push from re-uploading a take on every round.\n *\n * The part size is NOT decided here. The platform returns it from the\n * declaring call, so the split can change server-side without a CLI\n * release.\n *\n * What the file IS comes back from the sealing call: the platform reads\n * the stored bytes and answers its kind, the name it is stored under and\n * anything worth saying about it. Nothing here decides what is accepted;\n * `GET /api/limits` lists the kinds and their caps.\n */\nimport { apiJson } from './platform'\nimport type { ApiResult } from './platform'\n\n/** Attempts per part. A part commits nothing until the upload is sealed,\n * so it is always safe to send again. */\nconst PART_ATTEMPTS = 3\n\nexport interface PartPlan {\n /** Part numbers are 1-based. */\n partNumber: number\n start: number\n /** One past the last byte. */\n end: number\n}\n\n/**\n * The slices to send, in order. Every part but the last is exactly\n * `partBytes`; the last is the remainder. The platform holds each part to\n * exactly this length, so the split has to agree with the size that was\n * declared — which is why `partBytes` comes from the platform rather than\n * from a constant here.\n */\nexport function planParts(totalBytes: number, partBytes: number): PartPlan[] {\n if (totalBytes <= 0 || partBytes <= 0) return []\n const parts: PartPlan[] = []\n for (let start = 0; start < totalBytes; start += partBytes) {\n parts.push({\n partNumber: parts.length + 1,\n start,\n end: Math.min(start + partBytes, totalBytes),\n })\n }\n return parts\n}\n\nexport interface UploadedAsset {\n id: string\n /** The path a document names the file by. */\n url: string\n size: number\n /** The platform already had these exact bytes; nothing was sent. */\n reused: boolean\n /** What the bytes are, as the platform read them (video, audio, image,\n * vector, model, font, hdr, recipe, captions). */\n kind: string\n /** The name it is stored under: the extension follows the bytes. */\n filename: string\n /** The frontmatter summary of a recipe, a picture's size, a clip's codec. */\n metadata: Record<string, unknown> | null\n /** What the platform did to the file or wants said about it. */\n notes: string[]\n /** The reference a program's manifest takes. */\n ref: string\n /** Seconds, for what plays. */\n duration: number | null\n}\n\nexport interface UploadTarget {\n origin: string\n /** The bearer: a content key, or a claimable push's session token. */\n key: string\n /**\n * The door's path. The account's by default; a claimable push sends its\n * files through `/api/claim/uploads/files`, which speaks the same\n * protocol under its session token.\n */\n door?: string\n}\n\nexport interface UploadOptions {\n filename: string\n /** Only a hint: the platform types a file by its bytes. It settles the\n * one thing bytes cannot, a WebM that holds sound alone. */\n contentType?: string\n /** sha256 hex — the platform dedupes on it, per owner. */\n contentHash: string\n /** The take's length, so a refusal for one past the hosted cap arrives\n * before anything is sent. The platform measures the file either way. */\n durationSeconds?: number\n /** Parts landed, for a caller that reports a long upload's progress. */\n onPart?: (done: number, total: number) => void\n /** File the asset into one of the caller's folders. */\n folderId?: string\n /**\n * `library`: a file added to keep (`vos asset push`, a recipe).\n * `attached` (the default): a file a document is about to name, which\n * the platform collects if nothing ever does.\n */\n intent?: 'library' | 'attached'\n /**\n * Where the file came from, as the uploader states it: kept beside the\n * file and shown with it. The platform does not check it.\n */\n provenance?: { sourceUrl?: string; license?: string; attribution?: string }\n}\n\nfunction failed(what: string, r: ApiResult): Error {\n const detail = typeof r.body.error === 'string' ? r.body.error : ''\n const hint = typeof r.body.hint === 'string' ? ` ${r.body.hint}` : ''\n return new Error(`${what} (${r.status})${detail ? `: ${detail}${hint}` : ''}`)\n}\n\nfunction asAsset(\n body: Record<string, unknown>,\n reused: boolean,\n): UploadedAsset {\n const asset = (body.asset ?? {}) as Record<string, unknown>\n return {\n id: String(asset.id),\n url: String(asset.fileUrl),\n size: typeof asset.size === 'number' ? asset.size : 0,\n reused,\n kind: typeof asset.kind === 'string' ? asset.kind : '',\n filename: typeof asset.filename === 'string' ? asset.filename : '',\n metadata:\n asset.metadata && typeof asset.metadata === 'object'\n ? (asset.metadata as Record<string, unknown>)\n : null,\n notes: Array.isArray(body.notes)\n ? body.notes.filter((n): n is string => typeof n === 'string')\n : [],\n ref:\n typeof asset.ref === 'string' ? asset.ref : `asset:${String(asset.id)}`,\n duration: typeof asset.duration === 'number' ? asset.duration : null,\n }\n}\n\n/** Worth sending again: the platform was busy or briefly unreachable. A\n * refusal is a decision and will read the same on a second try. */\nfunction transient(status: number): boolean {\n return status >= 500 || status === 429\n}\n\nconst wait = (ms: number) => new Promise((r) => setTimeout(r, ms))\n\n/** Upload one file as an owned asset: declare it, send its parts, seal it. */\nexport async function uploadAsset(\n target: UploadTarget,\n bytes: Uint8Array,\n opts: UploadOptions,\n): Promise<UploadedAsset> {\n // Declare it first. Every quota is weighed here, before a byte moves, so\n // a refusal costs nothing and arrives in words — and the dedupe answers\n // here too, which is what makes re-pushing a hosted take free.\n const door = target.door ?? '/api/assets/uploads'\n const begun = await apiJson(target.origin, door, {\n method: 'POST',\n key: target.key,\n body: {\n filename: opts.filename,\n ...(opts.contentType ? { contentType: opts.contentType } : {}),\n size: bytes.length,\n sha256: opts.contentHash,\n ...(opts.durationSeconds && opts.durationSeconds > 0\n ? { durationSeconds: opts.durationSeconds }\n : {}),\n ...(opts.folderId ? { folderId: opts.folderId } : {}),\n ...(opts.intent ? { intent: opts.intent } : {}),\n },\n })\n if (begun.status !== 201 && begun.status !== 200) {\n throw failed('upload failed', begun)\n }\n // The platform already holds these bytes: nothing to send.\n if (begun.body.asset) return asAsset(begun.body, true)\n\n const uploadId = String(begun.body.uploadId)\n const partBytes = Number(begun.body.partBytes)\n if (!Number.isFinite(partBytes) || partBytes <= 0) {\n throw new Error('upload failed: the platform returned no part size')\n }\n const parts = planParts(bytes.length, partBytes)\n const etags: { partNumber: number; etag: string }[] = []\n\n try {\n for (const part of parts) {\n const chunk = bytes.subarray(part.start, part.end)\n let sent: ApiResult | null = null\n for (let attempt = 1; attempt <= PART_ATTEMPTS; attempt++) {\n try {\n sent = await apiJson(\n target.origin,\n `${door}/${uploadId}/parts/${part.partNumber}`,\n {\n method: 'PUT',\n key: target.key,\n headers: {\n 'Content-Type': 'application/octet-stream',\n 'Content-Length': String(chunk.length),\n },\n raw: chunk,\n },\n )\n } catch (networkError) {\n // Dropped connection, a sleeping machine: worth another try.\n if (attempt === PART_ATTEMPTS) throw networkError\n await wait(500 * 2 ** (attempt - 1))\n continue\n }\n if (sent.status === 200) break\n if (!transient(sent.status) || attempt === PART_ATTEMPTS) {\n throw failed(`part ${part.partNumber} failed`, sent)\n }\n await wait(500 * 2 ** (attempt - 1))\n }\n if (!sent || sent.status !== 200) {\n throw new Error(`part ${part.partNumber} failed`)\n }\n etags.push({\n partNumber: Number(sent.body.partNumber),\n etag: String(sent.body.etag),\n })\n opts.onPart?.(etags.length, parts.length)\n }\n\n // Sealing is where the platform reads the file. A refusal here means\n // the bytes are not what the name said, and it has already taken them\n // back out.\n const sealed = await apiJson(\n target.origin,\n `${door}/${uploadId}/complete`,\n {\n method: 'POST',\n key: target.key,\n body: {\n parts: etags,\n ...(opts.provenance ? { provenance: opts.provenance } : {}),\n },\n },\n )\n if (sealed.status !== 201 && sealed.status !== 200) {\n throw failed('upload failed', sealed)\n }\n // The platform hashes what it stored: bytes this account already held\n // come back as the file it had, whatever was declared.\n return asAsset(sealed.body, sealed.body.reused === true)\n } catch (err) {\n // Hand the parts back rather than leaving them held server-side.\n await apiJson(target.origin, `${door}/${uploadId}`, {\n method: 'DELETE',\n key: target.key,\n }).catch(() => undefined)\n throw err\n }\n}\n"],"mappings":"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","names":["existsSync","join","existsSync","mkdir","readFile","rename","rm","basename","join","where","readFile","basename","join","mkdir","rename","rm","existsSync","join","existsSync"]}
|