@vosjs/studio-core 0.40.3 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,7 @@ The shared layers lower as one engine `stack` entry, `STUDIO_ENTRY_ID` (`'vosso.
45
45
 
46
46
  ## Destinations
47
47
 
48
- `DESTINATIONS` is the table of launch destinations (store listings, Product Hunt, social, OG cards, README loops) with exact pixel sizes, byte and duration ceilings, generated from `channel-specs.json` in `@vosjs/cli` and hash-gated by a test. `destinationById`, `destinationsForChannel`, `exportSizeFor(ratio, resolution)` and `resolveExportSize` size an export; `ExportResolution` is `720p | 1080p | 2k | 4k`.
48
+ `DESTINATIONS` is the table of video destinations (a YouTube demo, an X feed cut, a vertical cut for Shorts and LinkedIn, a GitHub README loop) with exact pixel sizes, byte and duration ceilings and a word policy, generated from `channel-specs.json` in `@vosjs/cli` and hash-gated by a test. `destinationById`, `destinationsForChannel`, `exportSizeFor(ratio, resolution)` and `resolveExportSize` size an export; `ExportResolution` is `720p | 1080p | 2k | 4k`.
49
49
 
50
50
  ## Exports by area
51
51
 
package/dist/index.d.ts CHANGED
@@ -1100,8 +1100,8 @@ interface FrameStyle {
1100
1100
  * How footage meets an off-ratio frame. 'contain' (default) fits the
1101
1101
  * whole card inside the padded area, letterboxing onto the background;
1102
1102
  * 'cover' makes the padded area the card and cover-fills it with footage,
1103
- * cropped around `focus` — what a 440x280 store tile or a 2.5:1 marquee
1104
- * demands ("fill the region"). Absent = contain; every existing doc is
1103
+ * cropped around `focus` — what a 9:16 cut of 16:9 footage demands
1104
+ * ("fill the region"). Absent = contain; every existing doc is
1105
1105
  * byte-identical.
1106
1106
  */
1107
1107
  fit?: 'contain' | 'cover';
@@ -1816,8 +1816,8 @@ interface ProjectDoc {
1816
1816
  media?: Media[];
1817
1817
  /**
1818
1818
  * The frame that stands for this document (OUTPUT seconds): the cover the
1819
- * shelf shows, the still a kit leads with. Absent, `docStillTime` derives
1820
- * it: the document's own last freeze (a poster's rest), else the hero
1819
+ * shelf shows. Absent, `docStillTime` derives it: the document's own
1820
+ * last freeze (the rest a closing hold ends on), else the hero
1821
1821
  * moment after the opening has entered. A template never sets it.
1822
1822
  */
1823
1823
  still?: number;
@@ -3857,8 +3857,8 @@ declare function wrapProgramLength(source: string): string;
3857
3857
  declare function lowerProgramDoc(doc: ProgramAnchorDoc, opts?: LowerProgramOptions): LoweredComposition;
3858
3858
 
3859
3859
  /**
3860
- * A destination is where a release's media goes — the output twin of the
3861
- * doors registry's "a door is what you bring". One row per channel asset,
3860
+ * A destination is where a release's video goes — the output twin of the
3861
+ * doors registry's "a door is what you bring". One row per channel video,
3862
3862
  * derived from the verified channel specs; `vos deliver` loops these and
3863
3863
  * the kit manifest records them.
3864
3864
  */
@@ -3868,58 +3868,29 @@ interface Destination {
3868
3868
  channel: string;
3869
3869
  asset: string;
3870
3870
  label: string;
3871
- kind: 'video' | 'still' | 'still-set';
3872
3871
  /** Reduced aspect ratio, the exportSizeFor convention. */
3873
3872
  ratio: string;
3874
3873
  px: {
3875
3874
  w: number;
3876
3875
  h: number;
3877
3876
  };
3878
- /** still-set only: how many the channel takes. */
3879
- count?: {
3880
- min: number;
3881
- max: number;
3882
- };
3883
- /**
3884
- * Image genre: 'screenshot' = real UI from the take (store policy demands
3885
- * real UX); 'card' = a COMPOSED cover — rendered from the maker's poster
3886
- * program when `vos deliver --poster` has one.
3887
- */
3888
- genre?: 'screenshot' | 'card';
3889
3877
  minSeconds?: number;
3890
3878
  maxSeconds?: number;
3891
3879
  maxBytes?: number;
3892
3880
  /** The format the kit renders. */
3893
- format: 'mp4' | 'png';
3881
+ format: 'mp4';
3894
3882
  /** What the channel accepts, in the spec's own words. */
3895
3883
  accepts: string;
3896
- /** How footage meets an off-ratio frame (a still fills, a video letterboxes). */
3897
- fit: 'contain' | 'cover';
3898
3884
  /**
3899
- * The destination's word policy: 'none' = the picture carries it alone (a
3900
- * tile, a store screenshot), 'allowed', 'expected' = a card that should
3901
- * say something. The picture checks read it.
3885
+ * The destination's word policy: 'none' = the picture carries it alone
3886
+ * (a README loop, so the beat captions drop), 'allowed', 'expected' = a
3887
+ * cut that should say something.
3902
3888
  */
3903
3889
  text: 'none' | 'allowed' | 'expected';
3904
- /**
3905
- * The rect, as fractions of the frame, every text box must sit inside:
3906
- * the platform's own chrome or crop covers the rest.
3907
- */
3908
- safe: {
3909
- x: number;
3910
- y: number;
3911
- w: number;
3912
- h: number;
3913
- };
3914
- /**
3915
- * The poster template a card destination renders from by default (the
3916
- * family that ships with the CLI); absent on screenshots and videos.
3917
- */
3918
- template?: string;
3919
3890
  notes: string;
3920
3891
  }
3921
3892
  declare const CHANNEL_SPECS_VERIFIED = "2026-08-04";
3922
- declare const CHANNEL_SPECS_HASH = "e48926c22737b5dfaabf5cb33916a50a5eccd4b93498c2059a1468bd963a0a86";
3893
+ declare const CHANNEL_SPECS_HASH = "0f12781634495af6af1b49845009f04810d75829cfb0088dbf903d136e330f25";
3923
3894
  declare const DESTINATIONS: Destination[];
3924
3895
  declare function destinationById(id: string): Destination | undefined;
3925
3896
  declare function destinationsForChannel(channel: string): Destination[];
@@ -4346,9 +4317,9 @@ declare function ownFreezes(doc: ProjectDoc): FreezeSpan[];
4346
4317
  /** The still a document with nothing to say gets: the old default. */
4347
4318
  declare const STILL_DEFAULT = 0.5;
4348
4319
  /**
4349
- * The output moment that STANDS for a document: its cover on the shelf,
4350
- * the still a kit leads with. One ladder, one derivation, read by the
4351
- * fleet thumbnail, the stills export and the kit alike; a template can
4320
+ * The output moment that STANDS for a document: its cover on the shelf.
4321
+ * One ladder, one derivation, read by every surface that takes a cover
4322
+ * (the fleet thumbnail, `vos frames --at-still`); a template can
4352
4323
  * never move it, because everything a template lays is stamped `from`.
4353
4324
  *
4354
4325
  * 1. `doc.still`, the author's own choice, clamped to the output.
package/dist/index.js CHANGED
@@ -5061,26 +5061,32 @@ var STUDIO_SETUP = `async (ctx) => {
5061
5061
  fontCache.set(oc.asset.url, font)
5062
5062
  } catch (e) { console.warn('[voila] typeface failed to load', oc.asset.url, e) }
5063
5063
  }
5064
- // Audio clips (music/SFX): pre-decode into a shared cache so first play is
5065
- // instant. The AudioContext starts suspended (autoplay policy) \u2014 onFrame
5066
- // resumes it on the first play. Failures degrade to a silent clip.
5064
+ // Audio clips (music/SFX): start decoding into the shared cache now, but
5065
+ // never hold the first frame for it. Playback starts on a press, so the
5066
+ // decodes almost always land first; one that lands later clears the
5067
+ // scheduler's signature and the next playing frame schedules it at the
5068
+ // right offset (the onFrame path does the same for clips added later).
5069
+ // Exports never read this cache: they mix offline. The AudioContext starts
5070
+ // suspended (autoplay policy); onFrame resumes it on the first play.
5071
+ // Failures degrade to a silent clip.
5067
5072
  const clips = ctx.data.audio || []
5068
5073
  if (clips.length && window.AudioContext) {
5069
5074
  const actx = ns.audioCtx || (ns.audioCtx = new window.AudioContext())
5070
5075
  const bufs = ns.audioBuffers || (ns.audioBuffers = new Map())
5071
5076
  const pend = ns.audioPending || (ns.audioPending = new Set())
5072
- await Promise.all(clips.map(async (c) => {
5073
- if (bufs.has(c.key) || pend.has(c.key)) return
5077
+ for (const c of clips) {
5078
+ if (bufs.has(c.key) || pend.has(c.key)) continue
5074
5079
  pend.add(c.key)
5075
- try {
5076
- const res = await fetch(c.key)
5077
- bufs.set(c.key, await actx.decodeAudioData(await res.arrayBuffer()))
5078
- } catch (e) {
5079
- console.warn('[voila] audio decode failed', c.key, e)
5080
- } finally {
5081
- pend.delete(c.key)
5082
- }
5083
- }))
5080
+ fetch(c.key)
5081
+ .then((res) => res.arrayBuffer())
5082
+ .then((ab) => actx.decodeAudioData(ab))
5083
+ .then((b) => {
5084
+ bufs.set(c.key, b)
5085
+ if (ns.audioSched) ns.audioSched.sig = ''
5086
+ })
5087
+ .catch((e) => console.warn('[voila] audio decode failed', c.key, e))
5088
+ .then(() => pend.delete(c.key))
5089
+ }
5084
5090
  }
5085
5091
  }`;
5086
5092
  var STUDIO_CONTENT = `(ctx) => {
@@ -10278,14 +10284,13 @@ function lowerProgramDoc(doc, opts = {}) {
10278
10284
 
10279
10285
  // src/destinations.ts
10280
10286
  var CHANNEL_SPECS_VERIFIED = "2026-08-04";
10281
- var CHANNEL_SPECS_HASH = "e48926c22737b5dfaabf5cb33916a50a5eccd4b93498c2059a1468bd963a0a86";
10287
+ var CHANNEL_SPECS_HASH = "0f12781634495af6af1b49845009f04810d75829cfb0088dbf903d136e330f25";
10282
10288
  var DESTINATIONS = [
10283
10289
  {
10284
10290
  id: "youtube-main-demo",
10285
10291
  channel: "youtube",
10286
10292
  asset: "main-demo",
10287
10293
  label: "YouTube demo",
10288
- kind: "video",
10289
10294
  ratio: "16:9",
10290
10295
  px: {
10291
10296
  w: 1920,
@@ -10295,14 +10300,7 @@ var DESTINATIONS = [
10295
10300
  maxSeconds: 120,
10296
10301
  format: "mp4",
10297
10302
  accepts: "mp4 (H.264+AAC)",
10298
- fit: "contain",
10299
10303
  text: "allowed",
10300
- safe: {
10301
- x: 0.05,
10302
- y: 0.08,
10303
- w: 0.9,
10304
- h: 0.84
10305
- },
10306
10304
  notes: "Captions burned in. The same public upload serves the Chrome Web Store promo video and Product Hunt video (both take YouTube URLs only)."
10307
10305
  },
10308
10306
  {
@@ -10310,7 +10308,6 @@ var DESTINATIONS = [
10310
10308
  channel: "x",
10311
10309
  asset: "feed-cut",
10312
10310
  label: "X feed cut",
10313
- kind: "video",
10314
10311
  ratio: "16:9",
10315
10312
  px: {
10316
10313
  w: 1920,
@@ -10321,14 +10318,7 @@ var DESTINATIONS = [
10321
10318
  maxBytes: 536870912,
10322
10319
  format: "mp4",
10323
10320
  accepts: "mp4 (H.264+AAC ONLY \u2014 HEVC/VP9/AV1 rejected)",
10324
- fit: "contain",
10325
10321
  text: "allowed",
10326
- safe: {
10327
- x: 0.05,
10328
- y: 0.08,
10329
- w: 0.9,
10330
- h: 0.84
10331
- },
10332
10322
  notes: "30\u201360s plays best; 140s/512MB is the free-tier ceiling. 1:1 also allowed. Native upload, never a link post."
10333
10323
  },
10334
10324
  {
@@ -10336,7 +10326,6 @@ var DESTINATIONS = [
10336
10326
  channel: "shorts-linkedin",
10337
10327
  asset: "vertical-cut",
10338
10328
  label: "Shorts / LinkedIn vertical cut",
10339
- kind: "video",
10340
10329
  ratio: "9:16",
10341
10330
  px: {
10342
10331
  w: 1080,
@@ -10346,14 +10335,7 @@ var DESTINATIONS = [
10346
10335
  maxSeconds: 90,
10347
10336
  format: "mp4",
10348
10337
  accepts: "mp4 (H.264+AAC)",
10349
- fit: "contain",
10350
10338
  text: "expected",
10351
- safe: {
10352
- x: 0.0833,
10353
- y: 0.1979,
10354
- w: 0.8333,
10355
- h: 0.6042
10356
- },
10357
10339
  notes: "Critical text inside a centered ~900\xD71160 safe zone \u2014 platform chrome covers the rest. Serves YouTube Shorts and LinkedIn native vertical."
10358
10340
  },
10359
10341
  {
@@ -10361,7 +10343,6 @@ var DESTINATIONS = [
10361
10343
  channel: "github",
10362
10344
  asset: "readme-loop",
10363
10345
  label: "GitHub README loop",
10364
- kind: "video",
10365
10346
  ratio: "16:9",
10366
10347
  px: {
10367
10348
  w: 1920,
@@ -10372,298 +10353,8 @@ var DESTINATIONS = [
10372
10353
  maxBytes: 10485760,
10373
10354
  format: "mp4",
10374
10355
  accepts: "mp4 (H.264)",
10375
- fit: "contain",
10376
10356
  text: "none",
10377
- safe: {
10378
- x: 0.05,
10379
- y: 0.08,
10380
- w: 0.9,
10381
- h: 0.84
10382
- },
10383
10357
  notes: "\u226410MB is the free-plan attachment ceiling; two-pass target bitrate from duration."
10384
- },
10385
- {
10386
- id: "youtube-thumbnail",
10387
- channel: "youtube",
10388
- asset: "thumbnail",
10389
- label: "YouTube thumbnail",
10390
- kind: "still",
10391
- ratio: "16:9",
10392
- px: {
10393
- w: 1280,
10394
- h: 720
10395
- },
10396
- genre: "card",
10397
- maxBytes: 2097152,
10398
- format: "png",
10399
- accepts: "png|jpg",
10400
- fit: "cover",
10401
- text: "expected",
10402
- safe: {
10403
- x: 0.05,
10404
- y: 0.08,
10405
- w: 0.9,
10406
- h: 0.84
10407
- },
10408
- template: "split-cover",
10409
- notes: ""
10410
- },
10411
- {
10412
- id: "cws-screenshot",
10413
- channel: "cws",
10414
- asset: "screenshot",
10415
- label: "Chrome Web Store screenshot",
10416
- kind: "still-set",
10417
- ratio: "8:5",
10418
- px: {
10419
- w: 1280,
10420
- h: 800
10421
- },
10422
- count: {
10423
- min: 1,
10424
- max: 5
10425
- },
10426
- genre: "screenshot",
10427
- format: "png",
10428
- accepts: "png|jpg",
10429
- fit: "cover",
10430
- text: "none",
10431
- safe: {
10432
- x: 0,
10433
- y: 0,
10434
- w: 1,
10435
- h: 1
10436
- },
10437
- notes: "Real UX only \u2014 misleading listing images are a removal-grade violation. Full bleed, square corners."
10438
- },
10439
- {
10440
- id: "cws-small-promo-tile",
10441
- channel: "cws",
10442
- asset: "small-promo-tile",
10443
- label: "Chrome Web Store small promo tile",
10444
- kind: "still",
10445
- ratio: "11:7",
10446
- px: {
10447
- w: 440,
10448
- h: 280
10449
- },
10450
- genre: "card",
10451
- format: "png",
10452
- accepts: "png|jpg",
10453
- fit: "cover",
10454
- text: "none",
10455
- safe: {
10456
- x: 0,
10457
- y: 0,
10458
- w: 1,
10459
- h: 1
10460
- },
10461
- template: "card-on-gradient",
10462
- notes: "Listings without one rank lower. No text; fill the region; subject centered."
10463
- },
10464
- {
10465
- id: "cws-marquee",
10466
- channel: "cws",
10467
- asset: "marquee",
10468
- label: "Chrome Web Store marquee",
10469
- kind: "still",
10470
- ratio: "5:2",
10471
- px: {
10472
- w: 1400,
10473
- h: 560
10474
- },
10475
- genre: "card",
10476
- format: "png",
10477
- accepts: "png|jpg",
10478
- fit: "cover",
10479
- text: "allowed",
10480
- safe: {
10481
- x: 0.05,
10482
- y: 0.08,
10483
- w: 0.9,
10484
- h: 0.84
10485
- },
10486
- template: "split-cover",
10487
- notes: "Carousel eligibility."
10488
- },
10489
- {
10490
- id: "cws-icon",
10491
- channel: "cws",
10492
- asset: "icon",
10493
- label: "Chrome Web Store icon",
10494
- kind: "still",
10495
- ratio: "1:1",
10496
- px: {
10497
- w: 128,
10498
- h: 128
10499
- },
10500
- format: "png",
10501
- accepts: "png",
10502
- fit: "cover",
10503
- text: "none",
10504
- safe: {
10505
- x: 0,
10506
- y: 0,
10507
- w: 1,
10508
- h: 1
10509
- },
10510
- notes: "96\xD796 art + 16px transparent padding."
10511
- },
10512
- {
10513
- id: "producthunt-thumbnail",
10514
- channel: "producthunt",
10515
- asset: "thumbnail",
10516
- label: "Product Hunt thumbnail",
10517
- kind: "still",
10518
- ratio: "1:1",
10519
- px: {
10520
- w: 240,
10521
- h: 240
10522
- },
10523
- genre: "card",
10524
- maxBytes: 3145728,
10525
- format: "png",
10526
- accepts: "gif|png",
10527
- fit: "cover",
10528
- text: "none",
10529
- safe: {
10530
- x: 0.06,
10531
- y: 0.06,
10532
- w: 0.88,
10533
- h: 0.88
10534
- },
10535
- template: "card-on-gradient",
10536
- notes: "GIF animates on hover only \u2014 the first frame must stand alone."
10537
- },
10538
- {
10539
- id: "producthunt-gallery",
10540
- channel: "producthunt",
10541
- asset: "gallery",
10542
- label: "Product Hunt gallery",
10543
- kind: "still-set",
10544
- ratio: "127:76",
10545
- px: {
10546
- w: 1270,
10547
- h: 760
10548
- },
10549
- count: {
10550
- min: 4,
10551
- max: 8
10552
- },
10553
- genre: "screenshot",
10554
- format: "png",
10555
- accepts: "png|jpg|gif",
10556
- fit: "cover",
10557
- text: "none",
10558
- safe: {
10559
- x: 0,
10560
- y: 0,
10561
- w: 1,
10562
- h: 1
10563
- },
10564
- notes: "First image is the hero."
10565
- },
10566
- {
10567
- id: "x-feed-image",
10568
- channel: "x",
10569
- asset: "feed-image",
10570
- label: "X feed image",
10571
- kind: "still",
10572
- ratio: "16:9",
10573
- px: {
10574
- w: 1200,
10575
- h: 675
10576
- },
10577
- genre: "card",
10578
- format: "png",
10579
- accepts: "png|jpg",
10580
- fit: "cover",
10581
- text: "allowed",
10582
- safe: {
10583
- x: 0.05,
10584
- y: 0.08,
10585
- w: 0.9,
10586
- h: 0.84
10587
- },
10588
- template: "split-cover",
10589
- notes: ""
10590
- },
10591
- {
10592
- id: "linkedin-feed-image",
10593
- channel: "linkedin",
10594
- asset: "feed-image",
10595
- label: "LinkedIn feed image",
10596
- kind: "still",
10597
- ratio: "400:209",
10598
- px: {
10599
- w: 1200,
10600
- h: 627
10601
- },
10602
- genre: "card",
10603
- format: "png",
10604
- accepts: "png|jpg",
10605
- fit: "cover",
10606
- text: "expected",
10607
- safe: {
10608
- x: 0.05,
10609
- y: 0.08,
10610
- w: 0.9,
10611
- h: 0.84
10612
- },
10613
- template: "split-cover",
10614
- notes: "1080\xD71350 vertical also performs in the mobile feed."
10615
- },
10616
- {
10617
- id: "og-card",
10618
- channel: "og",
10619
- asset: "card",
10620
- label: "OG card",
10621
- kind: "still",
10622
- ratio: "40:21",
10623
- px: {
10624
- w: 1200,
10625
- h: 630
10626
- },
10627
- genre: "card",
10628
- maxBytes: 1048576,
10629
- format: "png",
10630
- accepts: "png|jpg",
10631
- fit: "cover",
10632
- text: "expected",
10633
- safe: {
10634
- x: 0.05,
10635
- y: 0.0238,
10636
- w: 0.9,
10637
- h: 0.9524
10638
- },
10639
- template: "split-cover",
10640
- notes: "Text in the center ~1080\xD7600. Set twitter:card=summary_large_image explicitly \u2014 og:image alone gets the small card."
10641
- },
10642
- {
10643
- id: "github-social-preview",
10644
- channel: "github",
10645
- asset: "social-preview",
10646
- label: "GitHub social preview",
10647
- kind: "still",
10648
- ratio: "2:1",
10649
- px: {
10650
- w: 1280,
10651
- h: 640
10652
- },
10653
- genre: "card",
10654
- maxBytes: 1048576,
10655
- format: "png",
10656
- accepts: "png|jpg",
10657
- fit: "cover",
10658
- text: "expected",
10659
- safe: {
10660
- x: 0.0391,
10661
- y: 0.0781,
10662
- w: 0.9219,
10663
- h: 0.8438
10664
- },
10665
- template: "card-on-gradient",
10666
- notes: "Key text \u226550px from every edge."
10667
10358
  }
10668
10359
  ];
10669
10360
  function destinationById(id) {