@lalalic/markcut 3.1.0 → 3.2.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.
Files changed (36) hide show
  1. package/package.json +5 -1
  2. package/skills/markcut/SKILL.md +14 -17
  3. package/skills/markcut/docs/map-dynamic-camera.md +92 -8
  4. package/skills/markcut/docs/markdown-descriptive.md +1 -1
  5. package/skills/markcut/review.md +480 -0
  6. package/src/descriptive/compiler.ts +60 -1
  7. package/src/descriptive/dsl.ts +27 -7
  8. package/src/descriptive/markdown.ts +2 -1
  9. package/src/descriptive/resolve.test.ts +98 -0
  10. package/src/descriptive/resolve.ts +156 -12
  11. package/src/player/bundle/player.js +222961 -222169
  12. package/src/player/pipeline.mjs +255 -17
  13. package/src/schema/index.ts +4 -0
  14. package/src/types/Effect.tsx +12 -1
  15. package/src/types/Map.tsx +649 -75
  16. package/src/utils/directions.ts +101 -0
  17. package/src/utils/index.ts +11 -0
  18. package/src/utils/route-legs.ts +199 -0
  19. package/tests/dsl.test.ts +35 -0
  20. package/tests/evals/README.md +41 -0
  21. package/tests/evals/dataset.json +170 -0
  22. package/tests/evals/gen_dataset.py +63 -0
  23. package/tests/evals/metrics.py +70 -0
  24. package/tests/evals/openrouter_model.py +143 -0
  25. package/tests/evals/storyboard_app.py +55 -0
  26. package/tests/evals/test_storyboard.py +32 -0
  27. package/tests/fixtures/map-overlay.json +56 -0
  28. package/tests/fixtures/md/map-all-views.md +8 -1
  29. package/tests/fixtures/md/map-children.md +11 -0
  30. package/tests/fixtures/md/map-multimode.md +9 -0
  31. package/tests/fixtures/streetview-walk.json +36 -0
  32. package/tests/md-descriptive.test.ts +75 -0
  33. package/tests/render.test.ts +92 -0
  34. package/tests/route-legs.test.ts +178 -0
  35. package/tests/schema.test.ts +18 -0
  36. package/.vscode/settings.json +0 -3
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Server-side Google Directions REST helper.
3
+ *
4
+ * Used at resolve time (Node) to compute per-leg travel durations so map
5
+ * overlay children (`at:"Label"`) can be auto-timed (arrival + dwell). This
6
+ * is the same Directions REST pattern the spots CLI uses — one request per
7
+ * consecutive waypoint pair so mixed travel modes (FLIGHT/BOAT synthetic)
8
+ * can be interspersed with road legs.
9
+ *
10
+ * Requires GOOGLE_MAPS_API_KEY (Directions API enabled).
11
+ */
12
+ import { makeSyntheticLeg, haversineKm } from "./route-legs";
13
+
14
+ export interface RouteLegTiming {
15
+ mode: string;
16
+ from: { lat: number; lng: number };
17
+ to: { lat: number; lng: number };
18
+ /** Estimated travel time for this leg, in seconds. */
19
+ durationSec: number;
20
+ }
21
+
22
+ function apiKey(): string {
23
+ return (
24
+ (typeof process !== "undefined" && process.env.GOOGLE_MAPS_API_KEY) || ""
25
+ );
26
+ }
27
+
28
+ /** Directions travel modes (non-synthetic). */
29
+ const ROAD_MODES = new Set(["DRIVING", "WALKING", "BICYCLING", "TRANSIT"]);
30
+
31
+ /**
32
+ * Compute the travel time (seconds) for one leg. Road modes hit the
33
+ * Directions REST API; synthetic modes (FLIGHT/BOAT) are estimated from
34
+ * haversine distance at cruise speed — no API call, deterministic.
35
+ *
36
+ * Returns null when the Directions call fails (caller falls back to a
37
+ * straight-line estimate so timing still works offline).
38
+ */
39
+ export async function legDurationSec(
40
+ from: { lat: number; lng: number },
41
+ to: { lat: number; lng: number },
42
+ mode: string,
43
+ key = apiKey(),
44
+ ): Promise<number | null> {
45
+ const m = (mode || "").toUpperCase();
46
+ if (!ROAD_MODES.has(m)) {
47
+ // Synthetic: distance at cruise speed (same math as the renderer).
48
+ return makeSyntheticLeg(from, to, m).durationSec;
49
+ }
50
+ if (!key) {
51
+ // No key → straight-line estimate (consistent, deterministic).
52
+ return straightLineSec(from, to);
53
+ }
54
+
55
+ const url = new URL("https://maps.googleapis.com/maps/api/directions/json");
56
+ url.searchParams.set("origin", `${from.lat},${from.lng}`);
57
+ url.searchParams.set("destination", `${to.lat},${to.lng}`);
58
+ url.searchParams.set("mode", m.toLowerCase());
59
+ url.searchParams.set("key", key);
60
+
61
+ try {
62
+ const res = await fetch(url);
63
+ const data = await res.json();
64
+ const dur = data?.routes?.[0]?.legs?.[0]?.duration?.value;
65
+ if (typeof dur === "number" && dur > 0) return dur;
66
+ return straightLineSec(from, to);
67
+ } catch {
68
+ return straightLineSec(from, to);
69
+ }
70
+ }
71
+
72
+ /** Fallback straight-line estimate (~50 km/h), so timing works without the API. */
73
+ function straightLineSec(
74
+ from: { lat: number; lng: number },
75
+ to: { lat: number; lng: number },
76
+ ): number {
77
+ return (haversineKm(from, to) / 50) * 3600;
78
+ }
79
+
80
+ /**
81
+ * Compute per-leg timing for a full route.
82
+ *
83
+ * `modes[i]` is the travel mode of the leg leaving waypoints[i]
84
+ * (defaults to `defaultMode`). Returns one timing per consecutive pair.
85
+ */
86
+ export async function routeLegTimings(
87
+ waypoints: { lat: number; lng: number; mode?: string }[],
88
+ defaultMode = "DRIVING",
89
+ key = apiKey(),
90
+ ): Promise<RouteLegTiming[]> {
91
+ if (waypoints.length < 2) return [];
92
+ const out: RouteLegTiming[] = [];
93
+ for (let i = 0; i < waypoints.length - 1; i++) {
94
+ const from = waypoints[i]!;
95
+ const to = waypoints[i + 1]!;
96
+ const mode = (from.mode ?? defaultMode).toUpperCase();
97
+ const durationSec = (await legDurationSec(from, to, mode, key)) ?? 0;
98
+ out.push({ mode, from, to, durationSec });
99
+ }
100
+ return out;
101
+ }
@@ -104,6 +104,17 @@ export function getDurationInSeconds(stream: DurationStream, update = true): num
104
104
  return stream.durationInSeconds ?? 0;
105
105
  }
106
106
 
107
+ // Map is a leaf whose overlay children are positioned INSIDE it — its own
108
+ // base timing (end) defines the duration, not max(child durations).
109
+ if (stream.type === "map") {
110
+ const d = leafEnd(stream);
111
+ if (update) {
112
+ stream.durationInSeconds = d;
113
+ for (const child of stream.children ?? []) getDurationInSeconds(child, update);
114
+ }
115
+ return d;
116
+ }
117
+
107
118
  // include: if src is set, treat as leaf (duration from base end).
108
119
  // Otherwise fall back to inline children (legacy).
109
120
  if (stream.type === "include") {
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Multi-leg route math for the `map` stream (view:"route").
3
+ *
4
+ * A route is a sequence of legs — one per consecutive waypoint pair. Each
5
+ * waypoint may tag the leg that LEAVES it with its own travel mode
6
+ * (`waypoint.mode ?? map.travelMode`). Supported modes:
7
+ *
8
+ * DRIVING | WALKING | BICYCLING | TRANSIT → Google Directions API
9
+ * FLIGHT | BOAT → synthetic great-circle arc
10
+ * (Directions has no air/water routes; we draw a curved arc and time it
11
+ * from haversine distance at a cruise speed)
12
+ *
13
+ * Pure functions only (deterministic per second) so the animated marker and
14
+ * any future camera can share the same route-time math.
15
+ */
16
+ export interface RouteLegStep {
17
+ path: { lat: number; lng: number }[];
18
+ durationSec: number;
19
+ }
20
+
21
+ export interface RouteLeg {
22
+ mode: string;
23
+ from: { lat: number; lng: number };
24
+ to: { lat: number; lng: number };
25
+ durationSec: number;
26
+ steps: RouteLegStep[];
27
+ }
28
+
29
+ /** Cruise speeds used to estimate synthetic leg durations (km/h). */
30
+ export const FLIGHT_SPEED_KMH = 850;
31
+ export const BOAT_SPEED_KMH = 40;
32
+
33
+ /** Modes with no Directions route — rendered as synthetic great-circle arcs. */
34
+ export function isSyntheticMode(mode?: string): boolean {
35
+ const m = (mode ?? "").toUpperCase();
36
+ return m === "FLIGHT" || m === "BOAT";
37
+ }
38
+
39
+ const EARTH_RADIUS_KM = 6371;
40
+ const toRad = (d: number) => (d * Math.PI) / 180;
41
+ const toDeg = (d: number) => (d * 180) / Math.PI;
42
+
43
+ /** Great-circle (haversine) distance between two lat/lng points, in km. */
44
+ export function haversineKm(
45
+ a: { lat: number; lng: number },
46
+ b: { lat: number; lng: number },
47
+ ): number {
48
+ const dLat = toRad(b.lat - a.lat);
49
+ const dLng = toRad(b.lng - a.lng);
50
+ const h =
51
+ Math.sin(dLat / 2) ** 2 +
52
+ Math.cos(toRad(a.lat)) * Math.cos(toRad(b.lat)) * Math.sin(dLng / 2) ** 2;
53
+ return 2 * EARTH_RADIUS_KM * Math.asin(Math.sqrt(h));
54
+ }
55
+
56
+ /**
57
+ * Interpolate the great-circle arc between two lat/lng points (the "as the
58
+ * crow flies" flight path), returning `points + 1` points including both ends.
59
+ */
60
+ export function greatCirclePath(
61
+ a: { lat: number; lng: number },
62
+ b: { lat: number; lng: number },
63
+ points = 64,
64
+ ): { lat: number; lng: number }[] {
65
+ const φ1 = toRad(a.lat);
66
+ const λ1 = toRad(a.lng);
67
+ const φ2 = toRad(b.lat);
68
+ const λ2 = toRad(b.lng);
69
+ const d = haversineKm(a, b) / EARTH_RADIUS_KM; // angular distance (radians)
70
+ const out: { lat: number; lng: number }[] = [];
71
+ for (let i = 0; i <= points; i++) {
72
+ if (d === 0) {
73
+ out.push({ lat: a.lat, lng: a.lng });
74
+ continue;
75
+ }
76
+ const f = i / points;
77
+ const A = Math.sin((1 - f) * d) / Math.sin(d);
78
+ const B = Math.sin(f * d) / Math.sin(d);
79
+ const x = A * Math.cos(φ1) * Math.cos(λ1) + B * Math.cos(φ2) * Math.cos(λ2);
80
+ const y = A * Math.cos(φ1) * Math.sin(λ1) + B * Math.cos(φ2) * Math.sin(λ2);
81
+ const z = A * Math.sin(φ1) + B * Math.sin(φ2);
82
+ out.push({ lat: toDeg(Math.atan2(z, Math.sqrt(x * x + y * y))), lng: toDeg(Math.atan2(y, x)) });
83
+ }
84
+ return out;
85
+ }
86
+
87
+ /** Build a synthetic leg (FLIGHT ✈️ / BOAT 🚢) between two waypoints. */
88
+ export function makeSyntheticLeg(
89
+ from: { lat: number; lng: number },
90
+ to: { lat: number; lng: number },
91
+ mode: string,
92
+ ): RouteLeg {
93
+ const speedKmh = (mode || "").toUpperCase() === "BOAT" ? BOAT_SPEED_KMH : FLIGHT_SPEED_KMH;
94
+ const durationSec = (haversineKm(from, to) / speedKmh) * 3600;
95
+ return {
96
+ mode: mode.toUpperCase(),
97
+ from,
98
+ to,
99
+ durationSec,
100
+ steps: [{ path: greatCirclePath(from, to), durationSec }],
101
+ };
102
+ }
103
+
104
+ /** Emoji glyph for a leg's travel mode (used for the traveling marker). */
105
+ export function modeEmoji(mode?: string): string {
106
+ switch ((mode ?? "").toUpperCase()) {
107
+ case "FLIGHT": return "✈️";
108
+ case "BOAT": return "🚢";
109
+ case "WALKING": return "🚶";
110
+ case "BICYCLING": return "🚲";
111
+ case "TRANSIT": return "🚌";
112
+ case "DRIVING": return "🚗";
113
+ default: return "📍";
114
+ }
115
+ }
116
+
117
+ /** Position within one leg at normalized progress t ∈ [0,1] (step-timed). */
118
+ export function positionAlongLeg(
119
+ leg: RouteLeg,
120
+ t: number,
121
+ ): { lat: number; lng: number } | null {
122
+ if (leg.steps.length === 0) return null;
123
+ const currentInSecond = t * leg.durationSec;
124
+ let acc = 0;
125
+ for (const step of leg.steps) {
126
+ if (currentInSecond <= acc + step.durationSec) {
127
+ const stepElapsed = currentInSecond - acc;
128
+ const stepProgress = step.durationSec > 0 ? stepElapsed / step.durationSec : 0;
129
+ const idx = Math.min(
130
+ Math.max(0, Math.floor(stepProgress * step.path.length)),
131
+ step.path.length - 1,
132
+ );
133
+ return step.path[idx] ?? null;
134
+ }
135
+ acc += step.durationSec;
136
+ }
137
+ const last = leg.steps[leg.steps.length - 1]!;
138
+ return last.path[last.path.length - 1] ?? null;
139
+ }
140
+
141
+ /**
142
+ * A dwell window where the pin holds still at a waypoint (a map overlay
143
+ * child is showing). `fromSec`/`toSec` are in the same timeline as
144
+ * `routePositionAtLegs`'s `seconds`.
145
+ */
146
+ export interface RouteStopWindow {
147
+ label: string;
148
+ at: { lat: number; lng: number };
149
+ mode: string;
150
+ fromSec: number;
151
+ toSec: number;
152
+ }
153
+
154
+ /**
155
+ * Position of the traveling marker at a given timeline second, plus the mode
156
+ * of the leg it is currently on. Time is split across legs proportionally to
157
+ * each leg's duration. When `stops` is provided, the marker holds still at a
158
+ * waypoint during its dwell window (drive time is compressed around the
159
+ * dwells, matching the resolver's arrival math). Returns null when there is
160
+ * no route.
161
+ */
162
+ export function routePositionAtLegs(
163
+ legs: RouteLeg[],
164
+ actionDuration: number,
165
+ seconds: number,
166
+ stops: RouteStopWindow[] = [],
167
+ ): { lat: number; lng: number; mode: string } | null {
168
+ if (legs.length === 0) return null;
169
+
170
+ // Inside a dwell window → hold at that waypoint.
171
+ const inStop = stops.find((s) => seconds >= s.fromSec && seconds < s.toSec);
172
+ if (inStop) {
173
+ return { lat: inStop.at.lat, lng: inStop.at.lng, mode: inStop.mode };
174
+ }
175
+
176
+ // Drive time = timeline seconds with completed/overlapping dwells removed.
177
+ let dwellBefore = 0;
178
+ for (const s of stops) {
179
+ if (seconds >= s.toSec) dwellBefore += s.toSec - s.fromSec;
180
+ else if (seconds > s.fromSec) dwellBefore += seconds - s.fromSec;
181
+ }
182
+ const driveTime = Math.max(0, seconds - dwellBefore);
183
+ const totalDwell = stops.reduce((s, st) => s + (st.toSec - st.fromSec), 0);
184
+ const driveBudget = Math.max(0.1, actionDuration - totalDwell);
185
+ const total = legs.reduce((s, l) => s + l.durationSec, 0);
186
+ const currentInSecond = driveTime * (total / driveBudget);
187
+ let acc = 0;
188
+ for (const leg of legs) {
189
+ if (currentInSecond <= acc + leg.durationSec) {
190
+ const t = leg.durationSec > 0 ? Math.min(Math.max((currentInSecond - acc) / leg.durationSec, 0), 1) : 0;
191
+ const pos = positionAlongLeg(leg, t);
192
+ return pos ? { ...pos, mode: leg.mode } : null;
193
+ }
194
+ acc += leg.durationSec;
195
+ }
196
+ const last = legs[legs.length - 1]!;
197
+ const pos = positionAlongLeg(last, 1);
198
+ return pos ? { ...pos, mode: last.mode } : null;
199
+ }
package/tests/dsl.test.ts CHANGED
@@ -113,6 +113,41 @@ describe("dsl — parseWaypoints", () => {
113
113
  ]);
114
114
  });
115
115
 
116
+ it("parses a bare travel mode without an empty media slot (smart)", () => {
117
+ expect(parseWaypoints('[40.7,-74.0,"NYC",FLIGHT; 34.05,-118.25,"LA","photo.jpg",BOAT]')).toEqual([
118
+ { lat: 40.7, lng: -74.0, label: "NYC", media: undefined, mode: "FLIGHT" },
119
+ { lat: 34.05, lng: -118.25, label: "LA", media: "photo.jpg", mode: "BOAT" },
120
+ ]);
121
+ });
122
+
123
+ it("parses a bare travel mode as the only trailing field", () => {
124
+ expect(parseWaypoints('[40.7,-74.0,FLIGHT]')).toEqual([
125
+ { lat: 40.7, lng: -74.0, label: undefined, media: undefined, mode: "FLIGHT" },
126
+ ]);
127
+ });
128
+
129
+ it("parses a bare mode even before the label", () => {
130
+ expect(parseWaypoints('[40.7,-74.0,BOAT,"Pier"]')).toEqual([
131
+ { lat: 40.7, lng: -74.0, label: "Pier", media: undefined, mode: "BOAT" },
132
+ ]);
133
+ });
134
+
135
+ it("uppercases a lowercase bare mode", () => {
136
+ expect(parseWaypoints('[40.7,-74.0,"NYC",flight]')[0]!.mode).toBe("FLIGHT");
137
+ });
138
+
139
+ it("still accepts empty quoted media slots (backward compat)", () => {
140
+ expect(parseWaypoints('[40.7,-74.0,"NYC","",BOAT]')).toEqual([
141
+ { lat: 40.7, lng: -74.0, label: "NYC", media: undefined, mode: "BOAT" },
142
+ ]);
143
+ });
144
+
145
+ it("treats a quoted mode word as a literal label/media (escape hatch)", () => {
146
+ expect(parseWaypoints('[40.7,-74.0,"NYC","FLIGHT"]')).toEqual([
147
+ { lat: 40.7, lng: -74.0, label: "NYC", media: "FLIGHT", mode: undefined },
148
+ ]);
149
+ });
150
+
116
151
  it("returns empty array for non-bracket input", () => {
117
152
  expect(parseWaypoints("not a list")).toEqual([]);
118
153
  });
@@ -0,0 +1,41 @@
1
+ # markcut storyboard evals
2
+
3
+ DeepEval suite that scores an agent authoring markcut storyboards from a
4
+ video brief, using the markcut skill.
5
+
6
+ ## Components
7
+
8
+ | File | Purpose |
9
+ | --- | --- |
10
+ | `openrouter_model.py` | Token-capped OpenRouter LLM wrapper (free-tier friendly, with model fallback rotation) |
11
+ | `gen_dataset.py` | Generates goldens from scratch via the Synthesizer |
12
+ | `dataset.json` | The golden dataset (committed, editable) |
13
+ | `storyboard_app.py` | Traced app: runs `pi` CLI with the markcut skill to author a storyboard |
14
+ | `metrics.py` | Judge metrics: TaskCompletion, Storyboard Format GEval, Story Narrative GEval |
15
+ | `test_storyboard.py` | pytest traced single-turn evals |
16
+
17
+ ## Regenerate the dataset
18
+
19
+ ```bash
20
+ .venv-evals/bin/python tests/evals/gen_dataset.py
21
+ ```
22
+
23
+ ## Run the evals
24
+
25
+ ```bash
26
+ cd tests/evals && ../../.venv-evals/bin/deepeval test run test_storyboard.py \
27
+ --identifier "iterating-on-storyboard-authoring-round-1"
28
+ ```
29
+
30
+ Each test runs the pi agent on the golden's video brief and asserts
31
+ trace-level metrics. Failures typically indicate the agent violated a
32
+ skill rule (e.g. `duration:` on scripted scenes, missing
33
+ `isBackground:true`) or produced weak narrative structure.
34
+
35
+ ## Notes
36
+
37
+ - Evaluation model runs on OpenRouter free models; the wrapper caps
38
+ max_tokens at 16000 to fit the workspace key's daily budget and
39
+ rotates through fallback models on rate limits.
40
+ - Traces are local (Confident AI not enabled); view latest results via
41
+ `deepeval view --latest` offline report in `.deepeval/`.
@@ -0,0 +1,170 @@
1
+ [
2
+ {
3
+ "input": "30s cinematic coastal drive vlog w/ voiceover detailing scenic landmarks & geography",
4
+ "actual_output": null,
5
+ "expected_output": null,
6
+ "context": null,
7
+ "source_file": null
8
+ },
9
+ {
10
+ "input": "Detail how teaser reveals specs, competitive edge, and launch strategy.",
11
+ "actual_output": null,
12
+ "expected_output": null,
13
+ "context": null,
14
+ "source_file": null
15
+ },
16
+ {
17
+ "input": "Draft a 2‑speaker dialogue explaining quantum computing, with visual analogy and real‑world app.",
18
+ "actual_output": null,
19
+ "expected_output": null,
20
+ "context": null,
21
+ "source_file": null
22
+ },
23
+ {
24
+ "input": "Develop a 5‑min pasta recipe paired with lo‑fi background music for enhanced cooking experience",
25
+ "actual_output": null,
26
+ "expected_output": null,
27
+ "context": null,
28
+ "source_file": null
29
+ },
30
+ {
31
+ "input": "How did 1960s Ford integrate sustainability into its corporate brand story?",
32
+ "actual_output": null,
33
+ "expected_output": null,
34
+ "context": null,
35
+ "source_file": null
36
+ },
37
+ {
38
+ "input": "Create a cinematic sci-fi thriller trailer, <2 min, featuring dystopian cityscape and AI antagonist",
39
+ "actual_output": null,
40
+ "expected_output": null,
41
+ "context": null,
42
+ "source_file": null
43
+ },
44
+ {
45
+ "input": "Compare walkthrough of modern luxury villa vs historic estate: design, amenities, layout.",
46
+ "actual_output": null,
47
+ "expected_output": null,
48
+ "context": null,
49
+ "source_file": null
50
+ },
51
+ {
52
+ "input": "Compare high-energy cuts vs. slow cinematic pacing for fitness motivation shorts.",
53
+ "actual_output": null,
54
+ "expected_output": null,
55
+ "context": null,
56
+ "source_file": null
57
+ },
58
+ {
59
+ "input": "Produce a short historical documentary snippet on the Indus. Rev., focusing on inventions",
60
+ "actual_output": null,
61
+ "expected_output": null,
62
+ "context": null,
63
+ "source_file": null
64
+ },
65
+ {
66
+ "input": "Compare dashboard walkthroughs in SW A and B tutorials, noting UI similarities and differences.",
67
+ "actual_output": null,
68
+ "expected_output": null,
69
+ "context": null,
70
+ "source_file": null
71
+ },
72
+ {
73
+ "input": "Compare a 15‑s skincare ad with strong CTA vs a 30‑s ad: effectiveness, engagement, conversion.",
74
+ "actual_output": null,
75
+ "expected_output": null,
76
+ "context": null,
77
+ "source_file": null
78
+ },
79
+ {
80
+ "input": "Describe how pressure adaptations let deep-sea creatures survive extreme conditions in the clip.",
81
+ "actual_output": null,
82
+ "expected_output": null,
83
+ "context": null,
84
+ "source_file": null
85
+ },
86
+ {
87
+ "input": "30s cinematic coastal drive vlog w/ voiceover detailing scenic landmarks & geography",
88
+ "actual_output": null,
89
+ "expected_output": null,
90
+ "context": null,
91
+ "source_file": null
92
+ },
93
+ {
94
+ "input": "Detail how teaser reveals specs, competitive edge, and launch strategy.",
95
+ "actual_output": null,
96
+ "expected_output": null,
97
+ "context": null,
98
+ "source_file": null
99
+ },
100
+ {
101
+ "input": "Draft a 2‑speaker dialogue explaining quantum computing, with visual analogy and real‑world app.",
102
+ "actual_output": null,
103
+ "expected_output": null,
104
+ "context": null,
105
+ "source_file": null
106
+ },
107
+ {
108
+ "input": "Develop a 5‑min pasta recipe paired with lo‑fi background music for enhanced cooking experience",
109
+ "actual_output": null,
110
+ "expected_output": null,
111
+ "context": null,
112
+ "source_file": null
113
+ },
114
+ {
115
+ "input": "How did 1960s Ford integrate sustainability into its corporate brand story?",
116
+ "actual_output": null,
117
+ "expected_output": null,
118
+ "context": null,
119
+ "source_file": null
120
+ },
121
+ {
122
+ "input": "Create a cinematic sci-fi thriller trailer, <2 min, featuring dystopian cityscape and AI antagonist",
123
+ "actual_output": null,
124
+ "expected_output": null,
125
+ "context": null,
126
+ "source_file": null
127
+ },
128
+ {
129
+ "input": "Compare walkthrough of modern luxury villa vs historic estate: design, amenities, layout.",
130
+ "actual_output": null,
131
+ "expected_output": null,
132
+ "context": null,
133
+ "source_file": null
134
+ },
135
+ {
136
+ "input": "Compare high-energy cuts vs. slow cinematic pacing for fitness motivation shorts.",
137
+ "actual_output": null,
138
+ "expected_output": null,
139
+ "context": null,
140
+ "source_file": null
141
+ },
142
+ {
143
+ "input": "Produce a short historical documentary snippet on the Indus. Rev., focusing on inventions",
144
+ "actual_output": null,
145
+ "expected_output": null,
146
+ "context": null,
147
+ "source_file": null
148
+ },
149
+ {
150
+ "input": "Compare dashboard walkthroughs in SW A and B tutorials, noting UI similarities and differences.",
151
+ "actual_output": null,
152
+ "expected_output": null,
153
+ "context": null,
154
+ "source_file": null
155
+ },
156
+ {
157
+ "input": "Compare a 15‑s skincare ad with strong CTA vs a 30‑s ad: effectiveness, engagement, conversion.",
158
+ "actual_output": null,
159
+ "expected_output": null,
160
+ "context": null,
161
+ "source_file": null
162
+ },
163
+ {
164
+ "input": "Describe how pressure adaptations let deep-sea creatures survive extreme conditions in the clip.",
165
+ "actual_output": null,
166
+ "expected_output": null,
167
+ "context": null,
168
+ "source_file": null
169
+ }
170
+ ]
@@ -0,0 +1,63 @@
1
+ """Generate the markcut storyboard goldens with a token-capped OpenRouter model.
2
+
3
+ Run:
4
+ OPENROUTER_API_KEY=... .venv-evals/bin/python tests/evals/gen_dataset.py
5
+ """
6
+
7
+ import os
8
+
9
+ from deepeval.synthesizer import Synthesizer
10
+ from deepeval.synthesizer.config import StylingConfig
11
+
12
+ from openrouter_model import OpenRouterLLM
13
+
14
+ SCENARIO = (
15
+ "Video creators and AI agents using the markcut markdown-to-video skill "
16
+ "to author video storyboards from a brief. markcut uses a markdown "
17
+ "descriptive format: '## scene' headings, '- image prompt:...' bullets, "
18
+ "script \"...\" narration, isBackground:true for visuals under scripted "
19
+ "scenes, map stream type with view/tween camera moves for route vlogs, "
20
+ "built-in components, TTS/TTI/STT media pipeline, and golden rules such "
21
+ "as never set duration on scripted scenes, keep manual assets in the "
22
+ "assets/ folder, and review md then compiled json then rendered video."
23
+ )
24
+ TASK = (
25
+ "Act as a storyboard author: given a video brief, produce a markcut "
26
+ "storyboard in markdown descriptive format that follows the skill's "
27
+ "scene/isBackground/duration rules and narrative structure (hook, "
28
+ "conflict, resolution, emotion, call to action)."
29
+ )
30
+ INPUT_FORMAT = (
31
+ "A short video brief, e.g. 'a 30s travel vlog of a coastal route with "
32
+ "narration', 'a product launch teaser with stats', 'a two-speaker "
33
+ "dialogue explainer', 'a recipe short with background music'."
34
+ )
35
+ EXPECTED_OUTPUT_FORMAT = (
36
+ "A complete markcut storyboard markdown using ## scene headings, "
37
+ "- image/script bullets, correct isBackground usage, and no manual "
38
+ "durations on scripted scenes."
39
+ )
40
+
41
+
42
+ def main():
43
+ model = OpenRouterLLM()
44
+ synthesizer = Synthesizer(
45
+ model=model,
46
+ styling_config=StylingConfig(
47
+ scenario=SCENARIO,
48
+ task=TASK,
49
+ input_format=INPUT_FORMAT,
50
+ expected_output_format=EXPECTED_OUTPUT_FORMAT,
51
+ ),
52
+ )
53
+ goldens = synthesizer.generate_goldens_from_scratch(num_goldens=12)
54
+ synthesizer.save_as(
55
+ file_type="json",
56
+ directory="tests/evals",
57
+ file_name="dataset",
58
+ )
59
+ print(f"Saved {len(goldens)} goldens to tests/evals/dataset.json")
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()