@lalalic/markcut 3.1.1 → 3.2.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.
@@ -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()
@@ -0,0 +1,70 @@
1
+ """DeepEval metrics for the markcut storyboard eval suite.
2
+
3
+ Judge model is the token-capped OpenRouter wrapper so the whole suite
4
+ runs on the workspace's free OpenRouter key.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+
10
+ sys.path.insert(0, os.path.dirname(__file__))
11
+
12
+ from deepeval.metrics import GEval, TaskCompletionMetric
13
+ from deepeval.test_case import LLMTestCaseParams
14
+
15
+ from openrouter_model import OpenRouterLLM
16
+
17
+
18
+ def _judge():
19
+ return OpenRouterLLM(temperature=0.0)
20
+
21
+
22
+ # Trace-level: did the agent finish the storyboard task at all?
23
+ TASK_COMPLETION_METRICS = [
24
+ TaskCompletionMetric(model=_judge(), threshold=0.7),
25
+ ]
26
+
27
+ # Is the storyboard valid markcut markdown following the skill's rules?
28
+ STORYBOARD_FORMAT_METRIC = GEval(
29
+ name="Storyboard Format",
30
+ criteria=(
31
+ "Evaluate whether the actual output is a valid markcut storyboard "
32
+ "in markdown descriptive format. It must: use '## scene' headings "
33
+ "for scenes; use '- image prompt:\"...\"' or similar bullet syntax "
34
+ "for visuals; include narration via script \"...\"; set "
35
+ "isBackground:true on the primary visual of any scene that has a "
36
+ "script or audio (otherwise the scene plays black); and NOT set "
37
+ "manual durations on scripted scenes since the resolver derives "
38
+ "duration from the audio script."
39
+ ),
40
+ evaluation_params=[
41
+ LLMTestCaseParams.INPUT,
42
+ LLMTestCaseParams.ACTUAL_OUTPUT,
43
+ ],
44
+ model=_judge(),
45
+ threshold=0.7,
46
+ )
47
+
48
+ # Does the storyboard tell a compelling viral story?
49
+ STORY_NARRATIVE_METRIC = GEval(
50
+ name="Story Narrative",
51
+ criteria=(
52
+ "Evaluate whether the storyboard tells a compelling short video "
53
+ "story. It should contain a hook (why watch), conflict or "
54
+ "challenge, resolution, emotional beats, a call to action, and "
55
+ "ideally an open ending where appropriate. Score low if it is a "
56
+ "flat list of scenes with no narrative structure."
57
+ ),
58
+ evaluation_params=[
59
+ LLMTestCaseParams.INPUT,
60
+ LLMTestCaseParams.ACTUAL_OUTPUT,
61
+ ],
62
+ model=_judge(),
63
+ threshold=0.6,
64
+ )
65
+
66
+ SINGLE_TURN_TRACE_METRICS = [
67
+ TASK_COMPLETION_METRICS[0],
68
+ STORYBOARD_FORMAT_METRIC,
69
+ STORY_NARRATIVE_METRIC,
70
+ ]
@@ -0,0 +1,143 @@
1
+ """OpenRouter-backed DeepEval LLM with a capped max_tokens budget.
2
+
3
+ The workspace OPENROUTER_API_KEY has a daily credit cap that rejects
4
+ requests asking for 65536 max_tokens, so we wrap the OpenAI-compatible
5
+ client ourselves and ask for far fewer output tokens.
6
+ """
7
+
8
+ import os
9
+ from typing import Optional, List
10
+
11
+ from deepeval.models import DeepEvalBaseLLM
12
+ from deepeval.synthesizer.schema import (
13
+ Response as _ResponseSchema,
14
+ SyntheticData,
15
+ SyntheticDataList,
16
+ )
17
+ from deepeval.metrics.utils import trimAndLoadJson
18
+
19
+ MAX_OUTPUT_TOKENS = 16000
20
+
21
+ # Free-tier models are flaky; rotate through candidates on rate limits.
22
+ FALLBACK_MODELS = [
23
+ "google/gemma-4-31b-it:free",
24
+ "nvidia/nemotron-3-super-120b-a12b:free",
25
+ "inclusionai/ling-3.0-flash-sante:free",
26
+ "minimax/minimax-m2.7:free",
27
+ ]
28
+
29
+
30
+ class OpenRouterLLM(DeepEvalBaseLLM):
31
+ def __init__(
32
+ self,
33
+ model: str = "google/gemma-4-31b-it:free",
34
+ temperature: float = 0.0,
35
+ **kwargs,
36
+ ):
37
+ self.model_id = model
38
+ self.temperature = temperature
39
+ super().__init__(model, **kwargs)
40
+
41
+ def load_model(self):
42
+ from openai import OpenAI
43
+
44
+ return OpenAI(
45
+ api_key=os.environ["OPENROUTER_API_KEY"],
46
+ base_url="https://openrouter.ai/api/v1",
47
+ )
48
+
49
+ def generate(
50
+ self,
51
+ prompt: str,
52
+ schema=None,
53
+ max_output_tokens: int = MAX_OUTPUT_TOKENS,
54
+ ) -> str:
55
+ client = self.load_model()
56
+ if schema is not None:
57
+ # Sending the raw JSON schema makes weak models echo it back;
58
+ # describe the shape in prose instead.
59
+ if schema is SyntheticDataList:
60
+ shape_desc = (
61
+ 'a JSON object {"data": [{"input": "<brief text>"}]} '
62
+ "with as many items as requested"
63
+ )
64
+ elif schema is _ResponseSchema:
65
+ shape_desc = 'a JSON object {"response": "<your answer>"}'
66
+ else:
67
+ shape_desc = (
68
+ "a single JSON object with exactly these keys: "
69
+ f"{list(schema.model_fields.keys())}"
70
+ )
71
+ prompt = (
72
+ "You are a JSON generator. Respond with ONLY one valid JSON "
73
+ "value - no prose, no markdown fences, no commentary, and "
74
+ "never repeat the schema itself. The JSON must be exactly "
75
+ f"{shape_desc}.\n\nTask:\n{prompt}"
76
+ )
77
+
78
+ import time
79
+
80
+ text = None
81
+ last_err = None
82
+ for model_id in [self.model_id] + [
83
+ m for m in FALLBACK_MODELS if m != self.model_id
84
+ ]:
85
+ for attempt in range(3):
86
+ try:
87
+ response = client.chat.completions.create(
88
+ model=model_id,
89
+ messages=[{"role": "user", "content": prompt}],
90
+ temperature=self.temperature,
91
+ max_tokens=min(max_output_tokens, MAX_OUTPUT_TOKENS),
92
+ )
93
+ text = response.choices[0].message.content or ""
94
+ break
95
+ except Exception as err: # rate limits, upstream 429s
96
+ last_err = err
97
+ time.sleep(5 * (attempt + 1))
98
+ if text:
99
+ break
100
+ if not text:
101
+ raise last_err
102
+ if schema is None:
103
+ return text
104
+ # Parse the JSON the prompt asked for into the requested schema
105
+ try:
106
+ data = trimAndLoadJson(text, self)
107
+ except ValueError:
108
+ with open("/tmp/deepeval_last_output.txt", "w") as f:
109
+ f.write(text)
110
+ raise
111
+ if schema is SyntheticDataList:
112
+ return SyntheticDataList(
113
+ data=[SyntheticData(**item) for item in data["data"]]
114
+ )
115
+ if schema is _ResponseSchema and isinstance(data, str):
116
+ return _ResponseSchema(response=data)
117
+ return schema(**data)
118
+
119
+ async def a_generate(
120
+ self,
121
+ prompt: str,
122
+ schema=None,
123
+ max_output_tokens: int = MAX_OUTPUT_TOKENS,
124
+ ) -> str:
125
+ return self.generate(prompt, schema, max_output_tokens)
126
+
127
+ def get_model_name(self) -> str:
128
+ return self.model_id
129
+
130
+ def supports_json_mode(self) -> bool:
131
+ return False
132
+
133
+ def supports_structured_outputs(self) -> bool:
134
+ return False
135
+
136
+ def supports_log_probs(self) -> Optional[bool]:
137
+ return False
138
+
139
+ def supports_multimodal(self) -> bool:
140
+ return False
141
+
142
+ def batch_generate(self, prompts: List[str]) -> List[str]:
143
+ return [self.generate(p) for p in prompts]
@@ -0,0 +1,55 @@
1
+ """Traced markcut storyboard authoring app.
2
+
3
+ Runs the pi agent CLI with the markcut skill against a video brief and
4
+ returns the produced storyboard markdown. The @observe decorator makes
5
+ the run a DeepEval agent trace so traced single-turn evals can score it.
6
+ """
7
+
8
+ import os
9
+ import subprocess
10
+ import tempfile
11
+
12
+ from deepeval.tracing import observe
13
+
14
+ MARKCUT_SKILL = os.path.abspath(
15
+ os.path.join(
16
+ os.path.dirname(__file__), "..", "..", "skills", "markcut", "SKILL.md"
17
+ )
18
+ )
19
+
20
+
21
+ def _read_skill() -> str:
22
+ with open(MARKCUT_SKILL) as f:
23
+ return f.read()
24
+
25
+
26
+ @observe(type="agent", name="markcut-storyboard-author")
27
+ def run_traced_storyboard(brief: str) -> str:
28
+ """Author a markcut storyboard markdown for the given brief."""
29
+ with tempfile.TemporaryDirectory() as workdir:
30
+ prompt = (
31
+ "You are a video storyboard author using the markcut skill.\n"
32
+ f"Video brief: {brief}\n\n"
33
+ "Write the storyboard as a markcut markdown file named "
34
+ "storyboard.md in the current directory. Follow the skill's "
35
+ "rules strictly. When done, print ONLY the final storyboard "
36
+ "markdown to stdout with no extra commentary."
37
+ )
38
+ result = subprocess.run(
39
+ [
40
+ "pi",
41
+ "-p",
42
+ "--no-session",
43
+ "--skill", MARKCUT_SKILL,
44
+ prompt,
45
+ ],
46
+ capture_output=True,
47
+ text=True,
48
+ timeout=600,
49
+ cwd=workdir,
50
+ )
51
+ storyboard_path = os.path.join(workdir, "storyboard.md")
52
+ if os.path.exists(storyboard_path):
53
+ with open(storyboard_path) as f:
54
+ return f.read()
55
+ return result.stdout
@@ -0,0 +1,32 @@
1
+ """Traced single-turn evals for the markcut storyboard authoring agent.
2
+
3
+ Each golden's input is a video brief; the app runs the pi agent CLI with
4
+ the markcut skill to author a storyboard markdown, traced with @observe.
5
+
6
+ Run:
7
+ cd tests/evals && ../../.venv-evals/bin/deepeval test run test_storyboard.py
8
+ """
9
+
10
+ import sys
11
+ import os
12
+
13
+ sys.path.insert(0, os.path.dirname(__file__))
14
+
15
+ import pytest
16
+ from deepeval import assert_test
17
+ from deepeval.dataset import EvaluationDataset, Golden
18
+
19
+ from metrics import SINGLE_TURN_TRACE_METRICS
20
+ from storyboard_app import run_traced_storyboard
21
+
22
+
23
+ dataset = EvaluationDataset()
24
+ dataset.add_goldens_from_json_file(
25
+ file_path=os.path.join(os.path.dirname(__file__), "dataset.json")
26
+ )
27
+
28
+
29
+ @pytest.mark.parametrize("golden", dataset.goldens)
30
+ def test_storyboard_authoring(golden: Golden):
31
+ run_traced_storyboard(golden.input)
32
+ assert_test(golden=golden, metrics=SINGLE_TURN_TRACE_METRICS)
@@ -1,3 +0,0 @@
1
- {
2
- "copilot-infinite.discord.enabled": true
3
- }