@scenar/sdk 0.10.0 → 0.11.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.
@@ -1 +1 @@
1
- {"version":3,"file":"createScenario.d.ts","sourceRoot":"","sources":["../../src/author/createScenario.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAEhB,aAAa,EAEb,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,KAAK,SAAS,YAAY,EACvD,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,GAC1B,gBAAgB,CAAC,KAAK,CAAC,CA+BzB"}
1
+ {"version":3,"file":"createScenario.d.ts","sourceRoot":"","sources":["../../src/author/createScenario.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAEhB,aAAa,EAEb,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,KAAK,SAAS,YAAY,EACvD,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,GAC1B,gBAAgB,CAAC,KAAK,CAAC,CAkCzB"}
@@ -35,12 +35,15 @@ export function createScenario(input) {
35
35
  data: { view: step.view, props: step.props },
36
36
  narration: step.narrationText,
37
37
  interactions: step.interactions,
38
+ presenter: step.presenter,
38
39
  });
39
40
  }
40
41
  return {
41
42
  viewport: input.viewport,
42
43
  views: input.views,
43
44
  steps,
45
+ soundtrack: input.soundtrack,
46
+ titleCards: input.titleCards,
44
47
  };
45
48
  }
46
49
  //# sourceMappingURL=createScenario.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"createScenario.js","sourceRoot":"","sources":["../../src/author/createScenario.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc,CAC5B,KAA2B;IAE3B,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACpD,MAAM,KAAK,GAA4C,EAAE,CAAC;IAE1D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAqB,CAAC;QAEhD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,iCAAiC,CAAC,WAAW,IAAI,CAAC,IAAI,kCAAkC;gBACxF,qBAAqB,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAClD,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,IAAI,CAAC;YACT,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAA6B;YACvE,SAAS,EAAE,IAAI,CAAC,aAAa;YAC7B,YAAY,EAAE,IAAI,CAAC,YAAY;SAChC,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,KAAK;KACN,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"createScenario.js","sourceRoot":"","sources":["../../src/author/createScenario.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc,CAC5B,KAA2B;IAE3B,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACpD,MAAM,KAAK,GAA4C,EAAE,CAAC;IAE1D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAqB,CAAC;QAEhD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,iCAAiC,CAAC,WAAW,IAAI,CAAC,IAAI,kCAAkC;gBACxF,qBAAqB,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAClD,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,IAAI,CAAC;YACT,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAA6B;YACvE,SAAS,EAAE,IAAI,CAAC,aAAa;YAC7B,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,KAAK;QACL,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;KAC7B,CAAC;AACJ,CAAC"}
package/author/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ScenarioStep, StepAction } from "@scenar/core";
1
+ import type { ScenarioStep, Soundtrack, StepAction, TitleCards } from "@scenar/core";
2
2
  /**
3
3
  * A view registry maps opaque view identifiers (strings) to
4
4
  * "anything that accepts props." The SDK is framework-agnostic:
@@ -44,6 +44,12 @@ export type StepInput<Views extends ViewRegistry> = {
44
44
  readonly narrationText?: string;
45
45
  readonly props: PropsOf<Views[K]>;
46
46
  readonly interactions?: readonly StepAction[];
47
+ /**
48
+ * Marks this step for the presenter track: `scenar presenter`
49
+ * generates an avatar clip from the step's narration audio, shown
50
+ * picture-in-picture in both outputs. Requires `narrationText`.
51
+ */
52
+ readonly presenter?: boolean;
47
53
  };
48
54
  }[keyof Views & string];
49
55
  /**
@@ -56,6 +62,23 @@ export interface ScenarioInput<Views extends ViewRegistry> {
56
62
  readonly views: Views;
57
63
  /** Ordered sequence of steps. Must contain at least one entry. */
58
64
  readonly steps: readonly StepInput<Views>[];
65
+ /**
66
+ * Audio treatment: background music with narration ducking and
67
+ * interaction sound effects. Optional — omitted means silent apart
68
+ * from narration.
69
+ */
70
+ readonly soundtrack?: Soundtrack;
71
+ /**
72
+ * Intro/outro title cards framing the scenario. Optional — omitted
73
+ * means the scenario plays exactly as authored.
74
+ *
75
+ * Carried through untouched, like `soundtrack`: card steps are
76
+ * synthesized at bundle assembly by `applyTitleCards` (from
77
+ * `@scenar/core`), which the CLI's render/pack pipelines call for
78
+ * you. When embedding directly, apply it between `createScenario()`
79
+ * and the player.
80
+ */
81
+ readonly titleCards?: TitleCards;
59
82
  }
60
83
  /**
61
84
  * The output of `createScenario()` — ready to feed into
@@ -65,5 +88,7 @@ export interface AuthoredScenario<Views extends ViewRegistry> {
65
88
  readonly viewport: ViewportConfig | undefined;
66
89
  readonly views: Views;
67
90
  readonly steps: readonly ScenarioStep<AuthoredStepData<Views>>[];
91
+ readonly soundtrack: Soundtrack | undefined;
92
+ readonly titleCards: TitleCards | undefined;
68
93
  }
69
94
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/author/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE7D;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,CAAC;AAErE;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE3F,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,CAAC,KAAK,SAAS,YAAY,IAAI;KACxD,CAAC,IAAI,MAAM,KAAK,GAAG,MAAM,GAAG;QAC3B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACjB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KACnC;CACF,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC;AAExB;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,YAAY,IAAI;KACjD,CAAC,IAAI,MAAM,KAAK,GAAG,MAAM,GAAG;QAC3B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACjB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;KAC/C;CACF,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,YAAY;IACvD,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB,CAAC,KAAK,SAAS,YAAY;IAC1D,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;CAClE"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/author/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAErF;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,CAAC;AAErE;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE3F,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,CAAC,KAAK,SAAS,YAAY,IAAI;KACxD,CAAC,IAAI,MAAM,KAAK,GAAG,MAAM,GAAG;QAC3B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACjB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KACnC;CACF,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC;AAExB;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,YAAY,IAAI;KACjD,CAAC,IAAI,MAAM,KAAK,GAAG,MAAM,GAAG;QAC3B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACjB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;QAC9C;;;;WAIG;QACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;KAC9B;CACF,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,YAAY;IACvD,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB,CAAC,KAAK,SAAS,YAAY;IAC1D,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;IACjE,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC;CAC7C"}
package/index.d.ts CHANGED
@@ -4,5 +4,5 @@ export { loadScenarioFromProto } from "./proto/load-scenario.js";
4
4
  export type { LoadScenarioOptions } from "./proto/load-scenario.js";
5
5
  export { InvalidScenarioError } from "./proto/errors.js";
6
6
  export { PROTO_ACTION_TYPE } from "./proto/proto-types.js";
7
- export type { ProtoActionTypeValue, ProtoScenarioSpec, ProtoStep, ProtoStepAction, ProtoStepActionConfig, ProtoViewportConfig, } from "./proto/proto-types.js";
7
+ export type { ProtoActionTypeValue, ProtoScenarioSpec, ProtoSoundtrackConfig, ProtoStep, ProtoStepAction, ProtoStepActionConfig, ProtoTitleCard, ProtoTitleCardsConfig, ProtoViewportConfig, } from "./proto/proto-types.js";
8
8
  //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,SAAS,EACT,aAAa,EACb,OAAO,GACR,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,SAAS,EACT,aAAa,EACb,OAAO,GACR,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenar/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Typed authoring surface for Scenar scenarios — createScenario() builder and proto adapter.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -28,6 +28,6 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@scenar/core": "0.10.0"
31
+ "@scenar/core": "0.11.1"
32
32
  }
33
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"load-scenario.d.ts","sourceRoot":"","sources":["../../src/proto/load-scenario.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAEhB,YAAY,EACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,KAAK,SAAS,YAAY;IAC7D,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,SAAS,YAAY,EAC9D,IAAI,EAAE,iBAAiB,EACvB,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC,GAClC,gBAAgB,CAAC,KAAK,CAAC,CA4CzB"}
1
+ {"version":3,"file":"load-scenario.d.ts","sourceRoot":"","sources":["../../src/proto/load-scenario.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAEhB,YAAY,EACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAK1D;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,KAAK,SAAS,YAAY;IAC7D,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,SAAS,YAAY,EAC9D,IAAI,EAAE,iBAAiB,EACvB,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC,GAClC,gBAAgB,CAAC,KAAK,CAAC,CA8DzB"}
@@ -1,5 +1,7 @@
1
1
  import { InvalidScenarioError } from "./errors.js";
2
2
  import { mapProtoAction } from "./action-mapper.js";
3
+ import { mapProtoSoundtrack } from "./soundtrack-mapper.js";
4
+ import { mapProtoTitleCards } from "./title-cards-mapper.js";
3
5
  /**
4
6
  * Convert a proto `ScenarioSpec` message into an `AuthoredScenario`
5
7
  * ready for `<ScenarioPlayer>`.
@@ -31,6 +33,13 @@ export function loadScenarioFromProto(spec, options) {
31
33
  `Registered views: ${[...viewNames].join(", ")}.`);
32
34
  }
33
35
  const interactions = protoStep.interactions.map((protoAction, j) => mapProtoAction(protoAction, `${stepPath}.interactions[${j}]`));
36
+ // Cross-field rule from the proto doc contract: a presenter clip is
37
+ // derived from the step's narration audio, so opting in without a
38
+ // script is an authoring contradiction — caught here, before the
39
+ // paid `scenar presenter` command ever runs.
40
+ if (protoStep.presenter && !protoStep.narrationText) {
41
+ throw new InvalidScenarioError(`${stepPath}.presenter`, "presenter requires narration_text — a presenter clip is derived from the step's narration audio.");
42
+ }
34
43
  steps.push({
35
44
  delayMs: protoStep.delayMs,
36
45
  data: {
@@ -39,12 +48,19 @@ export function loadScenarioFromProto(spec, options) {
39
48
  },
40
49
  narration: protoStep.narrationText || undefined,
41
50
  interactions: interactions.length > 0 ? interactions : undefined,
51
+ presenter: protoStep.presenter || undefined,
42
52
  });
43
53
  }
44
54
  return {
45
55
  viewport: spec.viewport ? { width: spec.viewport.width, height: spec.viewport.height } : undefined,
46
56
  views: options.views,
47
57
  steps,
58
+ soundtrack: spec.soundtrack
59
+ ? mapProtoSoundtrack(spec.soundtrack, "soundtrack")
60
+ : undefined,
61
+ titleCards: spec.titleCards
62
+ ? mapProtoTitleCards(spec.titleCards, "titleCards")
63
+ : undefined,
48
64
  };
49
65
  }
50
66
  //# sourceMappingURL=load-scenario.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"load-scenario.js","sourceRoot":"","sources":["../../src/proto/load-scenario.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAUpD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAuB,EACvB,OAAmC;IAEnC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,oBAAoB,CAAC,OAAO,EAAE,gCAAgC,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IACtD,MAAM,KAAK,GAA4C,EAAE,CAAC;IAE1D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC;QAE/B,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;YACpB,MAAM,IAAI,oBAAoB,CAAC,GAAG,QAAQ,OAAO,EAAE,mBAAmB,CAAC,CAAC;QAC1E,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,oBAAoB,CAC5B,GAAG,QAAQ,OAAO,EAClB,IAAI,SAAS,CAAC,IAAI,kCAAkC;gBACpD,qBAAqB,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAClD,CAAC;QACJ,CAAC;QAED,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,CACjE,cAAc,CAAC,WAAW,EAAE,GAAG,QAAQ,iBAAiB,CAAC,GAAG,CAAC,CAC9D,CAAC;QAEF,KAAK,CAAC,IAAI,CAAC;YACT,OAAO,EAAE,SAAS,CAAC,OAAO;YAC1B,IAAI,EAAE;gBACJ,IAAI,EAAE,SAAS,CAAC,IAAI;gBACpB,KAAK,EAAE,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAqC;aACxC;YAC5B,SAAS,EAAE,SAAS,CAAC,aAAa,IAAI,SAAS;YAC/C,YAAY,EAAE,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;SACjE,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS;QAClG,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,KAAK;KACN,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"load-scenario.js","sourceRoot":"","sources":["../../src/proto/load-scenario.ts"],"names":[],"mappings":"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"}
@@ -87,10 +87,40 @@ export interface ProtoStep {
87
87
  readonly narrationText: string;
88
88
  readonly props?: Record<string, unknown>;
89
89
  readonly interactions: readonly ProtoStepAction[];
90
+ readonly presenter?: boolean;
91
+ }
92
+ /**
93
+ * Structural shape of a proto `SoundtrackConfig` message.
94
+ * All fields are proto3 `optional`, so absence is meaningful:
95
+ * an unset volume means "use the engine default".
96
+ */
97
+ export interface ProtoSoundtrackConfig {
98
+ readonly musicSrc?: string;
99
+ readonly musicVolume?: number;
100
+ readonly duckingVolume?: number;
101
+ readonly sfx?: boolean;
102
+ }
103
+ /**
104
+ * Structural shape of a proto `TitleCard` message.
105
+ * Optional fields are proto3 `optional`, so absence is meaningful.
106
+ */
107
+ export interface ProtoTitleCard {
108
+ readonly title: string;
109
+ readonly subtitle?: string;
110
+ readonly logoSrc?: string;
111
+ readonly ctaText?: string;
112
+ readonly durationMs?: number;
113
+ }
114
+ /** Structural shape of a proto `TitleCardsConfig` message. */
115
+ export interface ProtoTitleCardsConfig {
116
+ readonly intro?: ProtoTitleCard;
117
+ readonly outro?: ProtoTitleCard;
90
118
  }
91
119
  /** Structural shape of the proto `ScenarioSpec` message. */
92
120
  export interface ProtoScenarioSpec {
93
121
  readonly viewport?: ProtoViewportConfig;
94
122
  readonly steps: readonly ProtoStep[];
123
+ readonly soundtrack?: ProtoSoundtrackConfig;
124
+ readonly titleCards?: ProtoTitleCardsConfig;
95
125
  }
96
126
  //# sourceMappingURL=proto-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proto-types.d.ts","sourceRoot":"","sources":["../../src/proto/proto-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,kDAAkD;AAClD,eAAO,MAAM,iBAAiB;;;;;;;;;;CAUpB,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAE9F,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,sEAAsE;AACtE,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,0BAA0B,CAAC;IAAC,KAAK,EAAE,6BAA6B,CAAA;CAAE,GAC1E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC;AAE3C,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;CACxC;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,YAAY,EAAE,SAAS,eAAe,EAAE,CAAC;CACnD;AAED,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;CACtC"}
1
+ {"version":3,"file":"proto-types.d.ts","sourceRoot":"","sources":["../../src/proto/proto-types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { Soundtrack } from "@scenar/core";
2
+ import type { ProtoSoundtrackConfig } from "./proto-types.js";
3
+ /**
4
+ * Map a proto `SoundtrackConfig` message to the engine's `Soundtrack`
5
+ * type, enforcing the same rules the proto declares via `buf.validate`
6
+ * (musicSrc non-empty when present, volumes within [0, 1]).
7
+ *
8
+ * All proto fields are `optional`, so absence passes through as
9
+ * `undefined` — the engine resolves defaults at playback time, keeping
10
+ * an authored 0.0 distinguishable from "use the default".
11
+ *
12
+ * @param proto - The proto SoundtrackConfig message (structural shape).
13
+ * @param path - JSON-path prefix for error reporting.
14
+ */
15
+ export declare function mapProtoSoundtrack(proto: ProtoSoundtrackConfig, path: string): Soundtrack;
16
+ //# sourceMappingURL=soundtrack-mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"soundtrack-mapper.d.ts","sourceRoot":"","sources":["../../src/proto/soundtrack-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE9D;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,MAAM,GACX,UAAU,CAiBZ"}
@@ -0,0 +1,34 @@
1
+ import { InvalidScenarioError } from "./errors.js";
2
+ /**
3
+ * Map a proto `SoundtrackConfig` message to the engine's `Soundtrack`
4
+ * type, enforcing the same rules the proto declares via `buf.validate`
5
+ * (musicSrc non-empty when present, volumes within [0, 1]).
6
+ *
7
+ * All proto fields are `optional`, so absence passes through as
8
+ * `undefined` — the engine resolves defaults at playback time, keeping
9
+ * an authored 0.0 distinguishable from "use the default".
10
+ *
11
+ * @param proto - The proto SoundtrackConfig message (structural shape).
12
+ * @param path - JSON-path prefix for error reporting.
13
+ */
14
+ export function mapProtoSoundtrack(proto, path) {
15
+ if (proto.musicSrc !== undefined && proto.musicSrc.length === 0) {
16
+ throw new InvalidScenarioError(`${path}.musicSrc`, "musicSrc must be a non-empty asset reference when present.");
17
+ }
18
+ validateVolume(proto.musicVolume, `${path}.musicVolume`);
19
+ validateVolume(proto.duckingVolume, `${path}.duckingVolume`);
20
+ return {
21
+ musicSrc: proto.musicSrc,
22
+ musicVolume: proto.musicVolume,
23
+ duckingVolume: proto.duckingVolume,
24
+ sfx: proto.sfx,
25
+ };
26
+ }
27
+ function validateVolume(value, path) {
28
+ if (value === undefined)
29
+ return;
30
+ if (value < 0 || value > 1) {
31
+ throw new InvalidScenarioError(path, `must be between 0.0 and 1.0, got ${value}.`);
32
+ }
33
+ }
34
+ //# sourceMappingURL=soundtrack-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"soundtrack-mapper.js","sourceRoot":"","sources":["../../src/proto/soundtrack-mapper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAA4B,EAC5B,IAAY;IAEZ,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,oBAAoB,CAC5B,GAAG,IAAI,WAAW,EAClB,4DAA4D,CAC7D,CAAC;IACJ,CAAC;IAED,cAAc,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,IAAI,cAAc,CAAC,CAAC;IACzD,cAAc,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,IAAI,gBAAgB,CAAC,CAAC;IAE7D,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,GAAG,EAAE,KAAK,CAAC,GAAG;KACf,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAyB,EAAE,IAAY;IAC7D,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,oBAAoB,CAAC,IAAI,EAAE,oCAAoC,KAAK,GAAG,CAAC,CAAC;IACrF,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { TitleCards } from "@scenar/core";
2
+ import type { ProtoTitleCardsConfig } from "./proto-types.js";
3
+ /**
4
+ * Map a proto `TitleCardsConfig` message to the engine's `TitleCards`
5
+ * type, enforcing the same rules the proto declares via `buf.validate`
6
+ * (title required and non-empty; optional strings non-empty when
7
+ * present; durationMs positive when present).
8
+ *
9
+ * Mapping only — card synthesis happens later, at bundle assembly, via
10
+ * `applyTitleCards`. The loader carries the config through untouched,
11
+ * exactly like `soundtrack`.
12
+ *
13
+ * @param proto - The proto TitleCardsConfig message (structural shape).
14
+ * @param path - JSON-path prefix for error reporting.
15
+ */
16
+ export declare function mapProtoTitleCards(proto: ProtoTitleCardsConfig, path: string): TitleCards;
17
+ //# sourceMappingURL=title-cards-mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"title-cards-mapper.d.ts","sourceRoot":"","sources":["../../src/proto/title-cards-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAkB,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE9E;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,MAAM,GACX,UAAU,CAKZ"}
@@ -0,0 +1,44 @@
1
+ import { InvalidScenarioError } from "./errors.js";
2
+ /**
3
+ * Map a proto `TitleCardsConfig` message to the engine's `TitleCards`
4
+ * type, enforcing the same rules the proto declares via `buf.validate`
5
+ * (title required and non-empty; optional strings non-empty when
6
+ * present; durationMs positive when present).
7
+ *
8
+ * Mapping only — card synthesis happens later, at bundle assembly, via
9
+ * `applyTitleCards`. The loader carries the config through untouched,
10
+ * exactly like `soundtrack`.
11
+ *
12
+ * @param proto - The proto TitleCardsConfig message (structural shape).
13
+ * @param path - JSON-path prefix for error reporting.
14
+ */
15
+ export function mapProtoTitleCards(proto, path) {
16
+ return {
17
+ intro: proto.intro ? mapCard(proto.intro, `${path}.intro`) : undefined,
18
+ outro: proto.outro ? mapCard(proto.outro, `${path}.outro`) : undefined,
19
+ };
20
+ }
21
+ function mapCard(proto, path) {
22
+ if (!proto.title) {
23
+ throw new InvalidScenarioError(`${path}.title`, "title is required.");
24
+ }
25
+ validateNonEmpty(proto.subtitle, `${path}.subtitle`);
26
+ validateNonEmpty(proto.logoSrc, `${path}.logoSrc`);
27
+ validateNonEmpty(proto.ctaText, `${path}.ctaText`);
28
+ if (proto.durationMs !== undefined && proto.durationMs <= 0) {
29
+ throw new InvalidScenarioError(`${path}.durationMs`, `must be greater than 0, got ${proto.durationMs}.`);
30
+ }
31
+ return {
32
+ title: proto.title,
33
+ subtitle: proto.subtitle,
34
+ logoSrc: proto.logoSrc,
35
+ ctaText: proto.ctaText,
36
+ durationMs: proto.durationMs,
37
+ };
38
+ }
39
+ function validateNonEmpty(value, path) {
40
+ if (value !== undefined && value.length === 0) {
41
+ throw new InvalidScenarioError(path, "must be non-empty when present.");
42
+ }
43
+ }
44
+ //# sourceMappingURL=title-cards-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"title-cards-mapper.js","sourceRoot":"","sources":["../../src/proto/title-cards-mapper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAA4B,EAC5B,IAAY;IAEZ,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;QACtE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;KACvE,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,KAAqB,EAAE,IAAY;IAClD,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACjB,MAAM,IAAI,oBAAoB,CAAC,GAAG,IAAI,QAAQ,EAAE,oBAAoB,CAAC,CAAC;IACxE,CAAC;IACD,gBAAgB,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,IAAI,WAAW,CAAC,CAAC;IACrD,gBAAgB,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,UAAU,CAAC,CAAC;IACnD,gBAAgB,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,UAAU,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,oBAAoB,CAC5B,GAAG,IAAI,aAAa,EACpB,+BAA+B,KAAK,CAAC,UAAU,GAAG,CACnD,CAAC;IACJ,CAAC;IAED,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,UAAU,EAAE,KAAK,CAAC,UAAU;KAC7B,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAyB,EAAE,IAAY;IAC/D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,oBAAoB,CAAC,IAAI,EAAE,iCAAiC,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC"}
@@ -53,6 +53,32 @@ describe("createScenario", () => {
53
53
  expect(scenario.steps[1]!.interactions![0]!.text).toBe("quickstart");
54
54
  });
55
55
 
56
+ it("carries presenter through to the built step", () => {
57
+ const scenario = createScenario({
58
+ views: { settings: SettingsView },
59
+ steps: [
60
+ {
61
+ view: "settings",
62
+ delayMs: 0,
63
+ narrationText: "Welcome.",
64
+ props: { org: "acme" },
65
+ presenter: true,
66
+ },
67
+ ],
68
+ });
69
+
70
+ expect(scenario.steps[0]!.presenter).toBe(true);
71
+ });
72
+
73
+ it("leaves presenter undefined when not authored", () => {
74
+ const scenario = createScenario({
75
+ views: { settings: SettingsView },
76
+ steps: [{ view: "settings", delayMs: 0, props: { org: "a" } }],
77
+ });
78
+
79
+ expect(scenario.steps[0]!.presenter).toBeUndefined();
80
+ });
81
+
56
82
  it("preserves view registry on the output", () => {
57
83
  const views = { settings: SettingsView };
58
84
  const scenario = createScenario({
@@ -103,4 +129,46 @@ describe("createScenario", () => {
103
129
 
104
130
  expect(scenario.steps[0]!.narration).toBe("Hello");
105
131
  });
132
+
133
+ it("carries the soundtrack through to the authored scenario", () => {
134
+ const soundtrack = { musicSrc: "./soundtrack/music.mp3", sfx: true };
135
+ const scenario = createScenario({
136
+ views: { settings: SettingsView },
137
+ steps: [{ view: "settings", delayMs: 0, props: { org: "a" } }],
138
+ soundtrack,
139
+ });
140
+
141
+ expect(scenario.soundtrack).toBe(soundtrack);
142
+ });
143
+
144
+ it("carries title cards through without synthesizing card steps", () => {
145
+ const titleCards = { intro: { title: "Acme" }, outro: { title: "Try it" } };
146
+ const scenario = createScenario({
147
+ views: { settings: SettingsView },
148
+ steps: [{ view: "settings", delayMs: 0, props: { org: "a" } }],
149
+ titleCards,
150
+ });
151
+
152
+ // Config is carried, not applied: still exactly the authored steps.
153
+ expect(scenario.titleCards).toBe(titleCards);
154
+ expect(scenario.steps).toHaveLength(1);
155
+ });
156
+
157
+ it("leaves titleCards undefined when not authored", () => {
158
+ const scenario = createScenario({
159
+ views: { settings: SettingsView },
160
+ steps: [{ view: "settings", delayMs: 0, props: { org: "a" } }],
161
+ });
162
+
163
+ expect(scenario.titleCards).toBeUndefined();
164
+ });
165
+
166
+ it("leaves soundtrack undefined when not authored", () => {
167
+ const scenario = createScenario({
168
+ views: { settings: SettingsView },
169
+ steps: [{ view: "settings", delayMs: 0, props: { org: "a" } }],
170
+ });
171
+
172
+ expect(scenario.soundtrack).toBeUndefined();
173
+ });
106
174
  });
@@ -181,4 +181,150 @@ describe("loadScenarioFromProto", () => {
181
181
  const scenario = loadScenarioFromProto(modified, { views });
182
182
  expect(scenario.steps[0]!.narration).toBeUndefined();
183
183
  });
184
+
185
+ it("maps the soundtrack through, preserving explicit zeros", () => {
186
+ const modified: ProtoScenarioSpec = {
187
+ ...makeValidScenario(),
188
+ soundtrack: {
189
+ musicSrc: "./soundtrack/music.mp3",
190
+ musicVolume: 0.3,
191
+ duckingVolume: 0,
192
+ sfx: true,
193
+ },
194
+ };
195
+
196
+ const scenario = loadScenarioFromProto(modified, { views });
197
+ expect(scenario.soundtrack).toEqual({
198
+ musicSrc: "./soundtrack/music.mp3",
199
+ musicVolume: 0.3,
200
+ duckingVolume: 0,
201
+ sfx: true,
202
+ });
203
+ });
204
+
205
+ it("leaves soundtrack undefined when the spec has none", () => {
206
+ const scenario = loadScenarioFromProto(makeValidScenario(), { views });
207
+ expect(scenario.soundtrack).toBeUndefined();
208
+ });
209
+
210
+ it("preserves unset soundtrack fields as undefined (engine defaults apply)", () => {
211
+ const modified: ProtoScenarioSpec = {
212
+ ...makeValidScenario(),
213
+ soundtrack: { musicSrc: "./music.mp3" },
214
+ };
215
+
216
+ const scenario = loadScenarioFromProto(modified, { views });
217
+ expect(scenario.soundtrack?.musicVolume).toBeUndefined();
218
+ expect(scenario.soundtrack?.duckingVolume).toBeUndefined();
219
+ expect(scenario.soundtrack?.sfx).toBeUndefined();
220
+ });
221
+
222
+ it.each([
223
+ ["musicVolume", { musicVolume: 1.5 }, "soundtrack.musicVolume"],
224
+ ["duckingVolume", { duckingVolume: -0.1 }, "soundtrack.duckingVolume"],
225
+ ])("throws when %s is out of range", (_name, soundtrack, expectedPath) => {
226
+ const modified: ProtoScenarioSpec = { ...makeValidScenario(), soundtrack };
227
+
228
+ try {
229
+ loadScenarioFromProto(modified, { views });
230
+ expect.fail("should have thrown");
231
+ } catch (e) {
232
+ const err = e as InvalidScenarioError;
233
+ expect(err.path).toBe(expectedPath);
234
+ }
235
+ });
236
+
237
+ it("throws when musicSrc is present but empty", () => {
238
+ const modified: ProtoScenarioSpec = {
239
+ ...makeValidScenario(),
240
+ soundtrack: { musicSrc: "" },
241
+ };
242
+
243
+ try {
244
+ loadScenarioFromProto(modified, { views });
245
+ expect.fail("should have thrown");
246
+ } catch (e) {
247
+ const err = e as InvalidScenarioError;
248
+ expect(err.path).toBe("soundtrack.musicSrc");
249
+ }
250
+ });
251
+
252
+ it("carries title cards through without synthesizing card steps", () => {
253
+ const modified: ProtoScenarioSpec = {
254
+ ...makeValidScenario(),
255
+ titleCards: {
256
+ intro: { title: "Acme", subtitle: "Ship fast" },
257
+ outro: { title: "Try it", ctaText: "acme.dev" },
258
+ },
259
+ };
260
+
261
+ const scenario = loadScenarioFromProto(modified, { views });
262
+ // Config is carried, not applied: still exactly the authored steps.
263
+ expect(scenario.steps).toHaveLength(2);
264
+ expect(scenario.titleCards?.intro?.title).toBe("Acme");
265
+ expect(scenario.titleCards?.outro?.ctaText).toBe("acme.dev");
266
+ });
267
+
268
+ it("leaves titleCards undefined when the spec has none", () => {
269
+ const scenario = loadScenarioFromProto(makeValidScenario(), { views });
270
+ expect(scenario.titleCards).toBeUndefined();
271
+ });
272
+
273
+ it("throws when a title card's title is empty", () => {
274
+ const modified: ProtoScenarioSpec = {
275
+ ...makeValidScenario(),
276
+ titleCards: { intro: { title: "" } },
277
+ };
278
+
279
+ try {
280
+ loadScenarioFromProto(modified, { views });
281
+ expect.fail("should have thrown");
282
+ } catch (e) {
283
+ const err = e as InvalidScenarioError;
284
+ expect(err.path).toBe("titleCards.intro.title");
285
+ }
286
+ });
287
+
288
+ it("maps presenter through on a narrated step", () => {
289
+ const proto = makeValidScenario();
290
+ const modified: ProtoScenarioSpec = {
291
+ ...proto,
292
+ steps: [{ ...proto.steps[0]!, presenter: true }, proto.steps[1]!],
293
+ };
294
+
295
+ const scenario = loadScenarioFromProto(modified, { views });
296
+ expect(scenario.steps[0]!.presenter).toBe(true);
297
+ expect(scenario.steps[1]!.presenter).toBeUndefined();
298
+ });
299
+
300
+ it("normalizes presenter: false to undefined (absence and false mean the same)", () => {
301
+ const proto = makeValidScenario();
302
+ const modified: ProtoScenarioSpec = {
303
+ ...proto,
304
+ steps: [{ ...proto.steps[0]!, presenter: false }, proto.steps[1]!],
305
+ };
306
+
307
+ const scenario = loadScenarioFromProto(modified, { views });
308
+ expect(scenario.steps[0]!.presenter).toBeUndefined();
309
+ });
310
+
311
+ it("throws when presenter is set on a step without narration", () => {
312
+ const proto = makeValidScenario();
313
+ // Step 1 has narrationText: "" — opting it in is the contradiction.
314
+ const modified: ProtoScenarioSpec = {
315
+ ...proto,
316
+ steps: [proto.steps[0]!, { ...proto.steps[1]!, presenter: true }],
317
+ };
318
+
319
+ try {
320
+ loadScenarioFromProto(modified, { views });
321
+ expect.fail("should have thrown");
322
+ } catch (e) {
323
+ const err = e as InvalidScenarioError;
324
+ expect(err.path).toBe("steps[1].presenter");
325
+ expect(err.reason).toBe(
326
+ "presenter requires narration_text — a presenter clip is derived from the step's narration audio.",
327
+ );
328
+ }
329
+ });
184
330
  });
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { InvalidScenarioError } from "../proto/errors.js";
3
+ import { mapProtoTitleCards } from "../proto/title-cards-mapper.js";
4
+
5
+ describe("mapProtoTitleCards", () => {
6
+ it("maps a full config field for field", () => {
7
+ const cards = mapProtoTitleCards(
8
+ {
9
+ intro: {
10
+ title: "Acme Deploy",
11
+ subtitle: "Ship in seconds",
12
+ logoSrc: "./logo.png",
13
+ durationMs: 4000,
14
+ },
15
+ outro: { title: "Try it today", ctaText: "acme.dev/start" },
16
+ },
17
+ "titleCards",
18
+ );
19
+
20
+ expect(cards.intro).toEqual({
21
+ title: "Acme Deploy",
22
+ subtitle: "Ship in seconds",
23
+ logoSrc: "./logo.png",
24
+ ctaText: undefined,
25
+ durationMs: 4000,
26
+ });
27
+ expect(cards.outro).toEqual({
28
+ title: "Try it today",
29
+ subtitle: undefined,
30
+ logoSrc: undefined,
31
+ ctaText: "acme.dev/start",
32
+ durationMs: undefined,
33
+ });
34
+ });
35
+
36
+ it("maps a config with only one side set", () => {
37
+ const cards = mapProtoTitleCards({ outro: { title: "Bye" } }, "titleCards");
38
+ expect(cards.intro).toBeUndefined();
39
+ expect(cards.outro?.title).toBe("Bye");
40
+ });
41
+
42
+ it.each([
43
+ ["intro", { intro: { title: "" } }, /titleCards\.intro\.title.*required/],
44
+ ["outro", { outro: { title: "" } }, /titleCards\.outro\.title.*required/],
45
+ ])("rejects an empty title on the %s card", (_side, proto, message) => {
46
+ expect(() => mapProtoTitleCards(proto, "titleCards")).toThrowError(message);
47
+ expect(() => mapProtoTitleCards(proto, "titleCards")).toThrowError(InvalidScenarioError);
48
+ });
49
+
50
+ it.each([
51
+ ["subtitle", { intro: { title: "T", subtitle: "" } }, /subtitle.*non-empty/],
52
+ ["logoSrc", { intro: { title: "T", logoSrc: "" } }, /logoSrc.*non-empty/],
53
+ ["ctaText", { intro: { title: "T", ctaText: "" } }, /ctaText.*non-empty/],
54
+ ])("rejects an empty %s when present", (_field, proto, message) => {
55
+ expect(() => mapProtoTitleCards(proto, "titleCards")).toThrowError(message);
56
+ });
57
+
58
+ it.each([0, -1000])("rejects a non-positive durationMs (%d)", (durationMs) => {
59
+ expect(() =>
60
+ mapProtoTitleCards({ intro: { title: "T", durationMs } }, "titleCards"),
61
+ ).toThrowError(/durationMs.*greater than 0/);
62
+ });
63
+ });
@@ -52,6 +52,7 @@ export function createScenario<Views extends ViewRegistry>(
52
52
  data: { view: step.view, props: step.props } as AuthoredStepData<Views>,
53
53
  narration: step.narrationText,
54
54
  interactions: step.interactions,
55
+ presenter: step.presenter,
55
56
  });
56
57
  }
57
58
 
@@ -59,5 +60,7 @@ export function createScenario<Views extends ViewRegistry>(
59
60
  viewport: input.viewport,
60
61
  views: input.views,
61
62
  steps,
63
+ soundtrack: input.soundtrack,
64
+ titleCards: input.titleCards,
62
65
  };
63
66
  }
@@ -1,4 +1,4 @@
1
- import type { ScenarioStep, StepAction } from "@scenar/core";
1
+ import type { ScenarioStep, Soundtrack, StepAction, TitleCards } from "@scenar/core";
2
2
 
3
3
  /**
4
4
  * A view registry maps opaque view identifiers (strings) to
@@ -49,6 +49,12 @@ export type StepInput<Views extends ViewRegistry> = {
49
49
  readonly narrationText?: string;
50
50
  readonly props: PropsOf<Views[K]>;
51
51
  readonly interactions?: readonly StepAction[];
52
+ /**
53
+ * Marks this step for the presenter track: `scenar presenter`
54
+ * generates an avatar clip from the step's narration audio, shown
55
+ * picture-in-picture in both outputs. Requires `narrationText`.
56
+ */
57
+ readonly presenter?: boolean;
52
58
  };
53
59
  }[keyof Views & string];
54
60
 
@@ -62,6 +68,23 @@ export interface ScenarioInput<Views extends ViewRegistry> {
62
68
  readonly views: Views;
63
69
  /** Ordered sequence of steps. Must contain at least one entry. */
64
70
  readonly steps: readonly StepInput<Views>[];
71
+ /**
72
+ * Audio treatment: background music with narration ducking and
73
+ * interaction sound effects. Optional — omitted means silent apart
74
+ * from narration.
75
+ */
76
+ readonly soundtrack?: Soundtrack;
77
+ /**
78
+ * Intro/outro title cards framing the scenario. Optional — omitted
79
+ * means the scenario plays exactly as authored.
80
+ *
81
+ * Carried through untouched, like `soundtrack`: card steps are
82
+ * synthesized at bundle assembly by `applyTitleCards` (from
83
+ * `@scenar/core`), which the CLI's render/pack pipelines call for
84
+ * you. When embedding directly, apply it between `createScenario()`
85
+ * and the player.
86
+ */
87
+ readonly titleCards?: TitleCards;
65
88
  }
66
89
 
67
90
  /**
@@ -72,4 +95,6 @@ export interface AuthoredScenario<Views extends ViewRegistry> {
72
95
  readonly viewport: ViewportConfig | undefined;
73
96
  readonly views: Views;
74
97
  readonly steps: readonly ScenarioStep<AuthoredStepData<Views>>[];
98
+ readonly soundtrack: Soundtrack | undefined;
99
+ readonly titleCards: TitleCards | undefined;
75
100
  }
package/src/index.ts CHANGED
@@ -23,8 +23,11 @@ export { PROTO_ACTION_TYPE } from "./proto/proto-types.js";
23
23
  export type {
24
24
  ProtoActionTypeValue,
25
25
  ProtoScenarioSpec,
26
+ ProtoSoundtrackConfig,
26
27
  ProtoStep,
27
28
  ProtoStepAction,
28
29
  ProtoStepActionConfig,
30
+ ProtoTitleCard,
31
+ ProtoTitleCardsConfig,
29
32
  ProtoViewportConfig,
30
33
  } from "./proto/proto-types.js";
@@ -7,6 +7,8 @@ import type {
7
7
  import { InvalidScenarioError } from "./errors.js";
8
8
  import type { ProtoScenarioSpec } from "./proto-types.js";
9
9
  import { mapProtoAction } from "./action-mapper.js";
10
+ import { mapProtoSoundtrack } from "./soundtrack-mapper.js";
11
+ import { mapProtoTitleCards } from "./title-cards-mapper.js";
10
12
 
11
13
  /**
12
14
  * Options for {@link loadScenarioFromProto}.
@@ -61,6 +63,17 @@ export function loadScenarioFromProto<Views extends ViewRegistry>(
61
63
  mapProtoAction(protoAction, `${stepPath}.interactions[${j}]`),
62
64
  );
63
65
 
66
+ // Cross-field rule from the proto doc contract: a presenter clip is
67
+ // derived from the step's narration audio, so opting in without a
68
+ // script is an authoring contradiction — caught here, before the
69
+ // paid `scenar presenter` command ever runs.
70
+ if (protoStep.presenter && !protoStep.narrationText) {
71
+ throw new InvalidScenarioError(
72
+ `${stepPath}.presenter`,
73
+ "presenter requires narration_text — a presenter clip is derived from the step's narration audio.",
74
+ );
75
+ }
76
+
64
77
  steps.push({
65
78
  delayMs: protoStep.delayMs,
66
79
  data: {
@@ -69,6 +82,7 @@ export function loadScenarioFromProto<Views extends ViewRegistry>(
69
82
  } as AuthoredStepData<Views>,
70
83
  narration: protoStep.narrationText || undefined,
71
84
  interactions: interactions.length > 0 ? interactions : undefined,
85
+ presenter: protoStep.presenter || undefined,
72
86
  });
73
87
  }
74
88
 
@@ -76,5 +90,11 @@ export function loadScenarioFromProto<Views extends ViewRegistry>(
76
90
  viewport: spec.viewport ? { width: spec.viewport.width, height: spec.viewport.height } : undefined,
77
91
  views: options.views,
78
92
  steps,
93
+ soundtrack: spec.soundtrack
94
+ ? mapProtoSoundtrack(spec.soundtrack, "soundtrack")
95
+ : undefined,
96
+ titleCards: spec.titleCards
97
+ ? mapProtoTitleCards(spec.titleCards, "titleCards")
98
+ : undefined,
79
99
  };
80
100
  }
@@ -83,10 +83,43 @@ export interface ProtoStep {
83
83
  readonly narrationText: string;
84
84
  readonly props?: Record<string, unknown>;
85
85
  readonly interactions: readonly ProtoStepAction[];
86
+ readonly presenter?: boolean;
87
+ }
88
+
89
+ /**
90
+ * Structural shape of a proto `SoundtrackConfig` message.
91
+ * All fields are proto3 `optional`, so absence is meaningful:
92
+ * an unset volume means "use the engine default".
93
+ */
94
+ export interface ProtoSoundtrackConfig {
95
+ readonly musicSrc?: string;
96
+ readonly musicVolume?: number;
97
+ readonly duckingVolume?: number;
98
+ readonly sfx?: boolean;
99
+ }
100
+
101
+ /**
102
+ * Structural shape of a proto `TitleCard` message.
103
+ * Optional fields are proto3 `optional`, so absence is meaningful.
104
+ */
105
+ export interface ProtoTitleCard {
106
+ readonly title: string;
107
+ readonly subtitle?: string;
108
+ readonly logoSrc?: string;
109
+ readonly ctaText?: string;
110
+ readonly durationMs?: number;
111
+ }
112
+
113
+ /** Structural shape of a proto `TitleCardsConfig` message. */
114
+ export interface ProtoTitleCardsConfig {
115
+ readonly intro?: ProtoTitleCard;
116
+ readonly outro?: ProtoTitleCard;
86
117
  }
87
118
 
88
119
  /** Structural shape of the proto `ScenarioSpec` message. */
89
120
  export interface ProtoScenarioSpec {
90
121
  readonly viewport?: ProtoViewportConfig;
91
122
  readonly steps: readonly ProtoStep[];
123
+ readonly soundtrack?: ProtoSoundtrackConfig;
124
+ readonly titleCards?: ProtoTitleCardsConfig;
92
125
  }
@@ -0,0 +1,44 @@
1
+ import type { Soundtrack } from "@scenar/core";
2
+ import { InvalidScenarioError } from "./errors.js";
3
+ import type { ProtoSoundtrackConfig } from "./proto-types.js";
4
+
5
+ /**
6
+ * Map a proto `SoundtrackConfig` message to the engine's `Soundtrack`
7
+ * type, enforcing the same rules the proto declares via `buf.validate`
8
+ * (musicSrc non-empty when present, volumes within [0, 1]).
9
+ *
10
+ * All proto fields are `optional`, so absence passes through as
11
+ * `undefined` — the engine resolves defaults at playback time, keeping
12
+ * an authored 0.0 distinguishable from "use the default".
13
+ *
14
+ * @param proto - The proto SoundtrackConfig message (structural shape).
15
+ * @param path - JSON-path prefix for error reporting.
16
+ */
17
+ export function mapProtoSoundtrack(
18
+ proto: ProtoSoundtrackConfig,
19
+ path: string,
20
+ ): Soundtrack {
21
+ if (proto.musicSrc !== undefined && proto.musicSrc.length === 0) {
22
+ throw new InvalidScenarioError(
23
+ `${path}.musicSrc`,
24
+ "musicSrc must be a non-empty asset reference when present.",
25
+ );
26
+ }
27
+
28
+ validateVolume(proto.musicVolume, `${path}.musicVolume`);
29
+ validateVolume(proto.duckingVolume, `${path}.duckingVolume`);
30
+
31
+ return {
32
+ musicSrc: proto.musicSrc,
33
+ musicVolume: proto.musicVolume,
34
+ duckingVolume: proto.duckingVolume,
35
+ sfx: proto.sfx,
36
+ };
37
+ }
38
+
39
+ function validateVolume(value: number | undefined, path: string): void {
40
+ if (value === undefined) return;
41
+ if (value < 0 || value > 1) {
42
+ throw new InvalidScenarioError(path, `must be between 0.0 and 1.0, got ${value}.`);
43
+ }
44
+ }
@@ -0,0 +1,55 @@
1
+ import type { TitleCard, TitleCards } from "@scenar/core";
2
+ import { InvalidScenarioError } from "./errors.js";
3
+ import type { ProtoTitleCard, ProtoTitleCardsConfig } from "./proto-types.js";
4
+
5
+ /**
6
+ * Map a proto `TitleCardsConfig` message to the engine's `TitleCards`
7
+ * type, enforcing the same rules the proto declares via `buf.validate`
8
+ * (title required and non-empty; optional strings non-empty when
9
+ * present; durationMs positive when present).
10
+ *
11
+ * Mapping only — card synthesis happens later, at bundle assembly, via
12
+ * `applyTitleCards`. The loader carries the config through untouched,
13
+ * exactly like `soundtrack`.
14
+ *
15
+ * @param proto - The proto TitleCardsConfig message (structural shape).
16
+ * @param path - JSON-path prefix for error reporting.
17
+ */
18
+ export function mapProtoTitleCards(
19
+ proto: ProtoTitleCardsConfig,
20
+ path: string,
21
+ ): TitleCards {
22
+ return {
23
+ intro: proto.intro ? mapCard(proto.intro, `${path}.intro`) : undefined,
24
+ outro: proto.outro ? mapCard(proto.outro, `${path}.outro`) : undefined,
25
+ };
26
+ }
27
+
28
+ function mapCard(proto: ProtoTitleCard, path: string): TitleCard {
29
+ if (!proto.title) {
30
+ throw new InvalidScenarioError(`${path}.title`, "title is required.");
31
+ }
32
+ validateNonEmpty(proto.subtitle, `${path}.subtitle`);
33
+ validateNonEmpty(proto.logoSrc, `${path}.logoSrc`);
34
+ validateNonEmpty(proto.ctaText, `${path}.ctaText`);
35
+ if (proto.durationMs !== undefined && proto.durationMs <= 0) {
36
+ throw new InvalidScenarioError(
37
+ `${path}.durationMs`,
38
+ `must be greater than 0, got ${proto.durationMs}.`,
39
+ );
40
+ }
41
+
42
+ return {
43
+ title: proto.title,
44
+ subtitle: proto.subtitle,
45
+ logoSrc: proto.logoSrc,
46
+ ctaText: proto.ctaText,
47
+ durationMs: proto.durationMs,
48
+ };
49
+ }
50
+
51
+ function validateNonEmpty(value: string | undefined, path: string): void {
52
+ if (value !== undefined && value.length === 0) {
53
+ throw new InvalidScenarioError(path, "must be non-empty when present.");
54
+ }
55
+ }