@vanillaskyai/video 0.10.1 → 0.10.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.10.2
8
+
9
+ - Keep generated narration playing in Safari after a delayed opening-to-body handoff by reusing the permitted audio element; isolate late playback failures from subsequent lines.
10
+ - In drop mode, a media miss without usable authored fallback skips only that scene and preserves later valid scenes and the ending. Strict mode still fails. The skipped beat is reported; its content cannot be reconstructed without a usable asset or fallback.
11
+ - Guide first-shot narration to fit its five-second footage budget and develop the answer in later scenes, without imposing a fixed sentence length on the rest of the video.
12
+
7
13
  ## 0.10.1
8
14
 
9
15
  - Continue narration through failed built-in media using the authored fallback or the existing graphic on black. Keep the recovery anchor after URL resolution, and observe actual mounted images for photo readiness.
package/dist/react.js CHANGED
@@ -1550,6 +1550,8 @@ function createVideoChatVoice(options = {}) {
1550
1550
  const lines = /* @__PURE__ */ new Map();
1551
1551
  const pendingLoads = /* @__PURE__ */ new Set();
1552
1552
  let sounding;
1553
+ let generatedElement;
1554
+ let stopGenerated;
1553
1555
  let browserFinish;
1554
1556
  let held = false;
1555
1557
  let silent = false;
@@ -1674,7 +1676,12 @@ function createVideoChatVoice(options = {}) {
1674
1676
  },
1675
1677
  resume() {
1676
1678
  held = false;
1677
- if (sounding) void sounding.play().catch(() => playbackFailure?.());
1679
+ if (sounding) {
1680
+ const fail = playbackFailure;
1681
+ void sounding.play().catch(() => {
1682
+ if (!held) fail?.();
1683
+ });
1684
+ }
1678
1685
  if (!silent) globalThis.speechSynthesis?.resume();
1679
1686
  },
1680
1687
  setMuted(muted) {
@@ -1753,9 +1760,11 @@ function createVideoChatVoice(options = {}) {
1753
1760
  }
1754
1761
  let playbackFailed = false;
1755
1762
  try {
1756
- const element = new Audio(line.src);
1763
+ stopGenerated?.();
1764
+ const element = generatedElement ??= new Audio();
1765
+ element.src = line.src;
1757
1766
  element.muted = silent;
1758
- if (offsetSeconds !== void 0) element.currentTime = offsetSeconds;
1767
+ element.currentTime = offsetSeconds ?? 0;
1759
1768
  sounding = element;
1760
1769
  await new Promise((resolve) => {
1761
1770
  let finished = false;
@@ -1771,6 +1780,7 @@ function createVideoChatVoice(options = {}) {
1771
1780
  element.onplaying = null;
1772
1781
  element.onended = null;
1773
1782
  element.onerror = null;
1783
+ if (stopGenerated === stop) stopGenerated = void 0;
1774
1784
  if (sounding === element) {
1775
1785
  sounding = void 0;
1776
1786
  playbackFailure = void 0;
@@ -1778,13 +1788,17 @@ function createVideoChatVoice(options = {}) {
1778
1788
  resolve();
1779
1789
  };
1780
1790
  const stop = () => {
1791
+ if (finished) return;
1781
1792
  element.pause();
1782
1793
  finish();
1783
1794
  };
1784
1795
  const fail = () => {
1785
- playbackFailed = true;
1786
- stop();
1796
+ if (!finished) {
1797
+ playbackFailed = true;
1798
+ stop();
1799
+ }
1787
1800
  };
1801
+ stopGenerated = stop;
1788
1802
  const clearWatchdog = watchSpeech(Math.max(line.seconds, estimatedBrowserSeconds(text)), fail);
1789
1803
  speechStops.add(stop);
1790
1804
  playbackFailure = fail;
@@ -1799,7 +1813,9 @@ function createVideoChatVoice(options = {}) {
1799
1813
  element.onerror = fail;
1800
1814
  signal.addEventListener("abort", stop, { once: true });
1801
1815
  try {
1802
- if (!held) void element.play().catch(fail);
1816
+ if (!held) void element.play().catch(() => {
1817
+ if (!held) fail();
1818
+ });
1803
1819
  } catch {
1804
1820
  fail();
1805
1821
  }
@@ -1825,6 +1841,11 @@ function createVideoChatVoice(options = {}) {
1825
1841
  speechStops.clear();
1826
1842
  sounding?.pause();
1827
1843
  sounding = void 0;
1844
+ generatedElement?.pause();
1845
+ generatedElement?.removeAttribute?.("src");
1846
+ generatedElement?.load?.();
1847
+ generatedElement = void 0;
1848
+ stopGenerated = void 0;
1828
1849
  stopBrowser();
1829
1850
  for (const line of lines.values()) {
1830
1851
  if (line.source === "generated") URL.revokeObjectURL(line.src);
package/dist/server.js CHANGED
@@ -670,27 +670,38 @@ function createMediaResolvingPlanner(options) {
670
670
  return false;
671
671
  }
672
672
  };
673
- const resolveOne = (part, openingReady2) => resolvePartVariables({
674
- part,
675
- requestId: context.request.requestId,
676
- templateId: part.type === "scene.add" ? part.scene.templateId : void 0,
677
- input: context.request.input,
678
- signal: context.signal,
679
- templates: options.templates,
680
- resolveMedia: options.resolveMedia,
681
- approveUrl: options.approveUrl,
682
- openingReady: openingReady2 ?? options.isOpeningReady(context.request.input),
683
- claimBudget,
684
- onFallback: () => getGenerationLifecycleSink(context)?.reportWarning?.({
685
- code: "provider_warning",
686
- category: "provider",
687
- message: "Some visuals use a simple background.",
688
- recoverable: true
689
- })
690
- });
673
+ const resolveOne = async (part, openingReady2) => {
674
+ try {
675
+ return await resolvePartVariables({
676
+ part,
677
+ requestId: context.request.requestId,
678
+ templateId: part.type === "scene.add" ? part.scene.templateId : void 0,
679
+ input: context.request.input,
680
+ signal: context.signal,
681
+ templates: options.templates,
682
+ resolveMedia: options.resolveMedia,
683
+ approveUrl: options.approveUrl,
684
+ openingReady: openingReady2 ?? options.isOpeningReady(context.request.input),
685
+ claimBudget,
686
+ onFallback: () => getGenerationLifecycleSink(context)?.reportWarning?.({
687
+ code: "provider_warning",
688
+ category: "provider",
689
+ message: "Some visuals use a simple background.",
690
+ recoverable: true
691
+ })
692
+ });
693
+ } catch (cause) {
694
+ if (context.signal.aborted || part.type !== "scene.add") throw cause;
695
+ const error = cause instanceof Error ? cause : new Error(String(cause));
696
+ if (!getGenerationLifecycleSink(context)?.rejectPart?.(error)) throw error;
697
+ return void 0;
698
+ }
699
+ };
691
700
  if (limit === 1) {
692
701
  for await (const part of options.planner(context)) {
693
- if (preflight(part)) yield await resolveOne(part);
702
+ if (!preflight(part)) continue;
703
+ const resolved = await resolveOne(part);
704
+ if (resolved) yield resolved;
694
705
  }
695
706
  return;
696
707
  }
@@ -748,7 +759,7 @@ function createMediaResolvingPlanner(options) {
748
759
  pending.shift();
749
760
  notifyProducer();
750
761
  if ("cause" in result) throw result.cause;
751
- yield result.part;
762
+ if (result.part) yield result.part;
752
763
  }
753
764
  await producer;
754
765
  if (producerError !== void 0) throw producerError;
@@ -931,6 +942,7 @@ function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlr
931
942
  generatedVideoAvailable ? 'First emit one host-consumed opening JSON object: {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"literal subject","firstShot":{"text":"short grounded fallback","narration":"a natural spoken beat advancing the hook","mediaKeyword":"2-8 concrete words","shotDirection":"optional framing and action"}}. Include firstShot only when a distinctive illustrative shot serves the story; omit it for an abstract explanation best opened with graphics. When present, the host inserts it once and begins generation immediately; do not emit it again.' : openingAlreadyProvided ? "Start directly with scene.add; the opening has already been supplied." : 'First emit {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"literal subject"}. This is host-consumed, not a scene.',
932
943
  openingAlreadyProvided ? "Preserve the supplied opening exactly; continue it without repeating its words or claim." : void 0,
933
944
  "The spokenHook is already heard before the first scene. Start firstShot.narration with the next fact, cause, action or consequence; never restate the hook. The first ordinary scene must then advance beyond firstShot rather than repeat its narration or opening claim. If firstShot is omitted, that first scene must advance the hook directly. firstShot.text, fallbackText and chapter titles must also express that new beat, not reuse the hook or prior shot copy; write a concise visual anchor of roughly 2\u20136 words within the schema budget, never a full narration sentence, clipped statistic or qualifying clause.",
945
+ "The optional firstShot has five seconds of footage: give its narration one brief sentence of roughly 8\u201310 spoken words that fits that budget. Carry further explanation, actions or dialogue in later scenes instead of squeezing the answer into the first shot. This limit applies only to firstShot; pace later narration to its own scene.",
934
946
  "Continue with only as many scenes as the story and duration need. Never pad to a fixed count. Every emitted scene carries narration beside variables and timing; no additional narration request should be needed.",
935
947
  "Make most of the film relevant footage: use full-bleed media for action, subjects and atmosphere, with graphics only to clarify a specific relationship or piece of evidence. Maintain consistent setting, lighting and subject while varying shot scale. Avoid unrelated cinematic montages.",
936
948
  "Use the installed catalog: ordered events, comparison, exact quote, one key figure, chapter or a message only when its narrative job fits. For an ordinary explanation, roughly one graphic-led explanatory beat per 30 seconds is a starting point, plus a brief chapter opening when needed. Let other intents use the visual form they need. Do not place graphic beats consecutively unless no honest relevant media is available. Give media-capable graphics a relevant background when it supports the meaning; keep black when no matching media exists. Do not force every template into a video or use lists of bullets.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillaskyai/video",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Open-source voice-and-video chat SDK for AI applications.",
5
5
  "keywords": [
6
6
  "video-chat",
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@vanillaskyai/video": "0.10.1",
12
+ "@vanillaskyai/video": "0.10.2",
13
13
  "react": "^19.2.8",
14
14
  "react-dom": "^19.2.8",
15
15
  "@ai-sdk/anthropic": "^3.0.0",