@afromero/kin3o 0.1.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.
- package/README.md +204 -0
- package/dist/brand.d.ts +39 -0
- package/dist/brand.js +63 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +249 -0
- package/dist/packager.d.ts +16 -0
- package/dist/packager.js +40 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +30 -0
- package/dist/prompts/examples-interactive.d.ts +339 -0
- package/dist/prompts/examples-interactive.js +139 -0
- package/dist/prompts/examples-mascot.d.ts +765 -0
- package/dist/prompts/examples-mascot.js +319 -0
- package/dist/prompts/examples.d.ts +238 -0
- package/dist/prompts/examples.js +168 -0
- package/dist/prompts/index.d.ts +17 -0
- package/dist/prompts/index.js +21 -0
- package/dist/prompts/system-interactive.d.ts +2 -0
- package/dist/prompts/system-interactive.js +93 -0
- package/dist/prompts/system.d.ts +3 -0
- package/dist/prompts/system.js +94 -0
- package/dist/prompts/tokens.d.ts +6 -0
- package/dist/prompts/tokens.js +28 -0
- package/dist/providers/anthropic.d.ts +2 -0
- package/dist/providers/anthropic.js +25 -0
- package/dist/providers/claude.d.ts +2 -0
- package/dist/providers/claude.js +47 -0
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +60 -0
- package/dist/providers/registry.d.ts +18 -0
- package/dist/providers/registry.js +62 -0
- package/dist/state-machine-validator.d.ts +6 -0
- package/dist/state-machine-validator.js +182 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.js +89 -0
- package/dist/validator.d.ts +8 -0
- package/dist/validator.js +195 -0
- package/examples/interactive-button.lottie +0 -0
- package/examples/mascot.json +760 -0
- package/examples/mascot.lottie +0 -0
- package/examples/pulse.json +75 -0
- package/examples/waveform.json +179 -0
- package/package.json +54 -0
- package/preview/template-interactive.html +223 -0
- package/preview/template.html +133 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Hand-crafted, validated Lottie JSON examples for few-shot prompting */
|
|
2
|
+
export const PULSING_CIRCLE = {
|
|
3
|
+
v: '5.5.2',
|
|
4
|
+
fr: 60,
|
|
5
|
+
ip: 0,
|
|
6
|
+
op: 120,
|
|
7
|
+
w: 512,
|
|
8
|
+
h: 512,
|
|
9
|
+
ddd: 0,
|
|
10
|
+
assets: [],
|
|
11
|
+
layers: [
|
|
12
|
+
{
|
|
13
|
+
ty: 4,
|
|
14
|
+
ind: 0,
|
|
15
|
+
nm: 'Pulsing Circle',
|
|
16
|
+
ip: 0,
|
|
17
|
+
op: 120,
|
|
18
|
+
st: 0,
|
|
19
|
+
ddd: 0,
|
|
20
|
+
ks: {
|
|
21
|
+
a: { a: 0, k: [0, 0] },
|
|
22
|
+
p: { a: 0, k: [256, 256] },
|
|
23
|
+
s: {
|
|
24
|
+
a: 1,
|
|
25
|
+
k: [
|
|
26
|
+
{
|
|
27
|
+
t: 0,
|
|
28
|
+
s: [80, 80],
|
|
29
|
+
o: { x: [0.42], y: [0] },
|
|
30
|
+
i: { x: [0.58], y: [1] },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
t: 60,
|
|
34
|
+
s: [120, 120],
|
|
35
|
+
o: { x: [0.42], y: [0] },
|
|
36
|
+
i: { x: [0.58], y: [1] },
|
|
37
|
+
},
|
|
38
|
+
{ t: 120, s: [80, 80] },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
r: { a: 0, k: 0 },
|
|
42
|
+
o: { a: 0, k: 100 },
|
|
43
|
+
},
|
|
44
|
+
shapes: [
|
|
45
|
+
{
|
|
46
|
+
ty: 'gr',
|
|
47
|
+
nm: 'Circle Group',
|
|
48
|
+
it: [
|
|
49
|
+
{
|
|
50
|
+
ty: 'el',
|
|
51
|
+
nm: 'Ellipse',
|
|
52
|
+
p: { a: 0, k: [0, 0] },
|
|
53
|
+
s: { a: 0, k: [200, 200] },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
ty: 'fl',
|
|
57
|
+
nm: 'Fill',
|
|
58
|
+
c: { a: 0, k: [0.851, 0.467, 0.024, 1] },
|
|
59
|
+
o: { a: 0, k: 100 },
|
|
60
|
+
r: 1,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
ty: 'tr',
|
|
64
|
+
p: { a: 0, k: [0, 0] },
|
|
65
|
+
a: { a: 0, k: [0, 0] },
|
|
66
|
+
s: { a: 0, k: [100, 100] },
|
|
67
|
+
r: { a: 0, k: 0 },
|
|
68
|
+
o: { a: 0, k: 100 },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
bm: 0,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
export const WAVEFORM_BARS = {
|
|
78
|
+
v: '5.5.2',
|
|
79
|
+
fr: 60,
|
|
80
|
+
ip: 0,
|
|
81
|
+
op: 120,
|
|
82
|
+
w: 512,
|
|
83
|
+
h: 512,
|
|
84
|
+
ddd: 0,
|
|
85
|
+
assets: [],
|
|
86
|
+
layers: [
|
|
87
|
+
...[0, 1, 2].map((i) => {
|
|
88
|
+
const x = 206 + i * 50;
|
|
89
|
+
const delay = i * 15;
|
|
90
|
+
return {
|
|
91
|
+
ty: 4,
|
|
92
|
+
ind: i,
|
|
93
|
+
nm: `Bar ${i + 1}`,
|
|
94
|
+
ip: 0,
|
|
95
|
+
op: 120,
|
|
96
|
+
st: 0,
|
|
97
|
+
ddd: 0,
|
|
98
|
+
ks: {
|
|
99
|
+
a: { a: 0, k: [0, 0] },
|
|
100
|
+
p: { a: 0, k: [x, 256] },
|
|
101
|
+
s: { a: 0, k: [100, 100] },
|
|
102
|
+
r: { a: 0, k: 0 },
|
|
103
|
+
o: { a: 0, k: 100 },
|
|
104
|
+
},
|
|
105
|
+
shapes: [
|
|
106
|
+
{
|
|
107
|
+
ty: 'gr',
|
|
108
|
+
nm: `Bar Group ${i + 1}`,
|
|
109
|
+
it: [
|
|
110
|
+
{
|
|
111
|
+
ty: 'rc',
|
|
112
|
+
nm: 'Rectangle',
|
|
113
|
+
p: { a: 0, k: [0, 0] },
|
|
114
|
+
s: {
|
|
115
|
+
a: 1,
|
|
116
|
+
k: [
|
|
117
|
+
{
|
|
118
|
+
t: delay,
|
|
119
|
+
s: [30, 40],
|
|
120
|
+
o: { x: [0.42], y: [0] },
|
|
121
|
+
i: { x: [0.58], y: [1] },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
t: 30 + delay,
|
|
125
|
+
s: [30, 160],
|
|
126
|
+
o: { x: [0.42], y: [0] },
|
|
127
|
+
i: { x: [0.58], y: [1] },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
t: 60 + delay,
|
|
131
|
+
s: [30, 40],
|
|
132
|
+
o: { x: [0.42], y: [0] },
|
|
133
|
+
i: { x: [0.58], y: [1] },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
t: 90 + delay,
|
|
137
|
+
s: [30, 120],
|
|
138
|
+
o: { x: [0.42], y: [0] },
|
|
139
|
+
i: { x: [0.58], y: [1] },
|
|
140
|
+
},
|
|
141
|
+
{ t: 120, s: [30, 40] },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
r: { a: 0, k: 4 },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
ty: 'fl',
|
|
148
|
+
nm: 'Fill',
|
|
149
|
+
c: { a: 0, k: [0.851, 0.467, 0.024, 1] },
|
|
150
|
+
o: { a: 0, k: 100 },
|
|
151
|
+
r: 1,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
ty: 'tr',
|
|
155
|
+
p: { a: 0, k: [0, 0] },
|
|
156
|
+
a: { a: 0, k: [0, 0] },
|
|
157
|
+
s: { a: 0, k: [100, 100] },
|
|
158
|
+
r: { a: 0, k: 0 },
|
|
159
|
+
o: { a: 0, k: 100 },
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
bm: 0,
|
|
165
|
+
};
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
168
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Registry — single entry point for all system prompts.
|
|
3
|
+
*
|
|
4
|
+
* Static mode: buildSystemPrompt() → single Lottie JSON animation
|
|
5
|
+
* Interactive: buildInteractiveSystemPrompt() → multi-animation envelope + state machine
|
|
6
|
+
*
|
|
7
|
+
* Both share LOTTIE_FORMAT_REFERENCE for consistent Lottie spec knowledge.
|
|
8
|
+
* Each mode has its own few-shot examples tuned for that output format.
|
|
9
|
+
*/
|
|
10
|
+
export { LOTTIE_FORMAT_REFERENCE } from './system.js';
|
|
11
|
+
export { buildSystemPrompt } from './system.js';
|
|
12
|
+
export { PULSING_CIRCLE, WAVEFORM_BARS } from './examples.js';
|
|
13
|
+
export { buildInteractiveSystemPrompt } from './system-interactive.js';
|
|
14
|
+
export { INTERACTIVE_BUTTON } from './examples-interactive.js';
|
|
15
|
+
export { MASCOT_STATIC, MASCOT_INTERACTIVE } from './examples-mascot.js';
|
|
16
|
+
export { loadDesignTokens, SOTTO_TOKENS } from './tokens.js';
|
|
17
|
+
export type { DesignTokens } from './tokens.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Registry — single entry point for all system prompts.
|
|
3
|
+
*
|
|
4
|
+
* Static mode: buildSystemPrompt() → single Lottie JSON animation
|
|
5
|
+
* Interactive: buildInteractiveSystemPrompt() → multi-animation envelope + state machine
|
|
6
|
+
*
|
|
7
|
+
* Both share LOTTIE_FORMAT_REFERENCE for consistent Lottie spec knowledge.
|
|
8
|
+
* Each mode has its own few-shot examples tuned for that output format.
|
|
9
|
+
*/
|
|
10
|
+
// ── Shared ──
|
|
11
|
+
export { LOTTIE_FORMAT_REFERENCE } from './system.js';
|
|
12
|
+
// ── Static mode ──
|
|
13
|
+
export { buildSystemPrompt } from './system.js';
|
|
14
|
+
export { PULSING_CIRCLE, WAVEFORM_BARS } from './examples.js';
|
|
15
|
+
// ── Interactive mode ──
|
|
16
|
+
export { buildInteractiveSystemPrompt } from './system-interactive.js';
|
|
17
|
+
export { INTERACTIVE_BUTTON } from './examples-interactive.js';
|
|
18
|
+
// ── Mascot ──
|
|
19
|
+
export { MASCOT_STATIC, MASCOT_INTERACTIVE } from './examples-mascot.js';
|
|
20
|
+
// ── Design tokens ──
|
|
21
|
+
export { loadDesignTokens, SOTTO_TOKENS } from './tokens.js';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { LOTTIE_FORMAT_REFERENCE } from './system.js';
|
|
2
|
+
import { INTERACTIVE_BUTTON } from './examples-interactive.js';
|
|
3
|
+
export function buildInteractiveSystemPrompt(tokens) {
|
|
4
|
+
const sections = [];
|
|
5
|
+
// 1. Role + output rules
|
|
6
|
+
sections.push(`You are an interactive Lottie animation generator. You create multiple Lottie animations plus a dotLottie state machine that wires them together with user interactions (hover, click, etc.).
|
|
7
|
+
|
|
8
|
+
Output ONLY valid JSON matching the envelope format below. No markdown fences, no explanation, no commentary. Your output must be directly parseable by JSON.parse().`);
|
|
9
|
+
// 2. Shared Lottie format reference
|
|
10
|
+
sections.push(LOTTIE_FORMAT_REFERENCE);
|
|
11
|
+
// 3. dotLottie state machine spec
|
|
12
|
+
sections.push(`
|
|
13
|
+
DOTLOTTIE STATE MACHINE FORMAT:
|
|
14
|
+
|
|
15
|
+
The state machine controls transitions between animations based on user interactions.
|
|
16
|
+
|
|
17
|
+
Output envelope format:
|
|
18
|
+
{
|
|
19
|
+
"animations": {
|
|
20
|
+
"<id>": { <complete Lottie JSON> },
|
|
21
|
+
"<id>": { <complete Lottie JSON> }
|
|
22
|
+
},
|
|
23
|
+
"stateMachine": {
|
|
24
|
+
"initial": "<state name>",
|
|
25
|
+
"states": [ ... ],
|
|
26
|
+
"interactions": [ ... ],
|
|
27
|
+
"inputs": [ ... ]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
State types:
|
|
32
|
+
- "PlaybackState": plays an animation. Fields: "name", "type", "animation" (animation ID), "transitions" (array)
|
|
33
|
+
- "GlobalState": listens globally. Fields: "name", "type", "transitions" (array)
|
|
34
|
+
|
|
35
|
+
Transition format:
|
|
36
|
+
{ "type": "Transition", "toState": "<target state name>", "guards": [ ... ] }
|
|
37
|
+
|
|
38
|
+
Guard format (conditional transition):
|
|
39
|
+
- Boolean: { "type": "Boolean", "inputName": "<name>", "conditionType": "Equal", "compareTo": true/false }
|
|
40
|
+
- Numeric: { "type": "Numeric", "inputName": "<name>", "conditionType": "Equal"|"GreaterThan"|"LessThan", "compareTo": <number> }
|
|
41
|
+
- String: { "type": "String", "inputName": "<name>", "conditionType": "Equal", "compareTo": "<value>" }
|
|
42
|
+
|
|
43
|
+
Interaction types (user events that set inputs):
|
|
44
|
+
- "PointerDown": mouse click / tap
|
|
45
|
+
- "PointerUp": mouse release
|
|
46
|
+
- "PointerEnter": mouse hover enter
|
|
47
|
+
- "PointerExit": mouse hover leave
|
|
48
|
+
- "OnComplete": animation playback completes (requires "stateName" field)
|
|
49
|
+
- "OnLoopComplete": animation loop completes (requires "stateName" field)
|
|
50
|
+
|
|
51
|
+
Interaction format:
|
|
52
|
+
{ "type": "<event type>", "actions": [{ "type": "<action type>", "inputName": "<input name>", "value": <value> }] }
|
|
53
|
+
For OnComplete/OnLoopComplete add "stateName": "<which state this applies to>"
|
|
54
|
+
|
|
55
|
+
Action types:
|
|
56
|
+
- "SetBoolean": set a boolean input { "type": "SetBoolean", "inputName": "...", "value": true/false }
|
|
57
|
+
- "Toggle": toggle a boolean input { "type": "Toggle", "inputName": "..." }
|
|
58
|
+
- "Increment": increment a numeric input { "type": "Increment", "inputName": "...", "value": 1 }
|
|
59
|
+
- "Decrement": decrement a numeric input { "type": "Decrement", "inputName": "...", "value": 1 }
|
|
60
|
+
- "SetString": set a string input { "type": "SetString", "inputName": "...", "value": "..." }
|
|
61
|
+
- "SetNumeric": set a numeric input { "type": "SetNumeric", "inputName": "...", "value": 0 }
|
|
62
|
+
|
|
63
|
+
Input types:
|
|
64
|
+
- "Boolean": { "name": "<name>", "type": "Boolean", "value": <default bool> }
|
|
65
|
+
- "Numeric": { "name": "<name>", "type": "Numeric", "value": <default number> }
|
|
66
|
+
- "String": { "name": "<name>", "type": "String", "value": "<default string>" }`);
|
|
67
|
+
// 4. Design rules
|
|
68
|
+
sections.push(`
|
|
69
|
+
INTERACTIVE DESIGN RULES:
|
|
70
|
+
- Keep individual animations SHORT: 60-120 frames (1-2 seconds at 60fps)
|
|
71
|
+
- Maximum 4 states per state machine
|
|
72
|
+
- Each animation: 512x512 canvas, 60fps, shape layers only
|
|
73
|
+
- Animations should be distinct but visually cohesive (same style, similar colors)
|
|
74
|
+
- Use ease-in-out easing by default
|
|
75
|
+
- Make loopable animations where appropriate (idle states)
|
|
76
|
+
- Always include "ddd": 0 and "assets": []
|
|
77
|
+
- Colors: 0-1 floats (NOT 0-255)
|
|
78
|
+
- Groups MUST end with "tr" transform`);
|
|
79
|
+
// 5. Design tokens
|
|
80
|
+
if (tokens) {
|
|
81
|
+
const colorLines = Object.entries(tokens.colors)
|
|
82
|
+
.map(([name, rgba]) => ` ${name}: [${rgba.join(', ')}]`)
|
|
83
|
+
.join('\n');
|
|
84
|
+
sections.push(`
|
|
85
|
+
DESIGN TOKENS — use these colors when appropriate:
|
|
86
|
+
${colorLines}`);
|
|
87
|
+
}
|
|
88
|
+
// 6. Few-shot example
|
|
89
|
+
sections.push(`
|
|
90
|
+
EXAMPLE — Interactive button (idle/hover/pressed states):
|
|
91
|
+
${JSON.stringify(INTERACTIVE_BUTTON)}`);
|
|
92
|
+
return sections.join('\n');
|
|
93
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DesignTokens } from './tokens.js';
|
|
2
|
+
export declare const LOTTIE_FORMAT_REFERENCE = "\nLOTTIE FORMAT REFERENCE:\n\nTop-level required fields:\n- \"v\": string (use \"5.5.2\")\n- \"fr\": number (use 60 for 60fps)\n- \"ip\": 0 (in-point, always 0)\n- \"op\": number (out-point, end frame \u2014 e.g. 120 for 2s at 60fps)\n- \"w\": number (width in pixels)\n- \"h\": number (height in pixels)\n- \"ddd\": 0 (no 3D)\n- \"assets\": [] (empty array)\n- \"layers\": array of layer objects\n\nLayer (shape layer, ty=4):\n- \"ty\": 4 (shape layer)\n- \"ind\": number (unique index)\n- \"nm\": string (layer name)\n- \"ip\": 0, \"op\": same as top-level op\n- \"st\": 0 (start time)\n- \"ddd\": 0\n- \"ks\": transform object\n- \"shapes\": array of shape objects\n- \"bm\": 0 (blend mode, normal)\n\nTransform \"ks\" object:\n- \"a\": anchor point (VECTOR property)\n- \"p\": position (VECTOR property)\n- \"s\": scale (VECTOR property, [100,100] = 100%)\n- \"r\": rotation (SCALAR property, degrees)\n- \"o\": opacity (SCALAR property, 0-100)\n\nPROPERTY TYPES \u2014 this distinction is critical:\n- VECTOR property (position, scale, anchor, size): {\"a\":0,\"k\":[x,y]} or animated {\"a\":1,\"k\":[keyframes]}\n- SCALAR property (rotation, opacity, roundness, stroke width): {\"a\":0,\"k\":0} or animated {\"a\":1,\"k\":[keyframes]}\n- COLOR property: {\"a\":0,\"k\":[r,g,b,1]} with 0-1 floats (NOT 0-255)\n\nKeyframe format:\n{\"t\":frame,\"s\":[values],\"o\":{\"x\":[n],\"y\":[n]},\"i\":{\"x\":[n],\"y\":[n]}}\n- \"t\": frame number\n- \"s\": start values (array for vector, array with single value for scalar)\n- \"o\": out-tangent (ease out), \"i\": in-tangent (ease in)\n- Last keyframe needs only \"t\" and \"s\" (no tangents)\n\nEasing (ease-in-out): \"o\":{\"x\":[0.42],\"y\":[0]}, \"i\":{\"x\":[0.58],\"y\":[1]}\n\nShape types:\n- \"el\": ellipse \u2014 \"p\" (center, vector), \"s\" (size, vector)\n- \"rc\": rectangle \u2014 \"p\" (center, vector), \"s\" (size, vector), \"r\" (roundness, scalar)\n- \"sh\": path \u2014 \"ks\" with bezier {\"c\":bool, \"v\":[[x,y],...], \"i\":[[dx,dy],...], \"o\":[[dx,dy],...]}\n- \"fl\": fill \u2014 \"c\" (color), \"o\" (opacity, scalar), \"r\" (fill rule, 1=nonzero)\n- \"st\": stroke \u2014 \"c\" (color), \"o\" (opacity), \"w\" (width, scalar), \"lc\" (line cap, 2=round), \"lj\" (line join, 2=round)\n- \"gr\": group \u2014 \"it\" array of shapes + MUST end with \"tr\" (group transform)\n- \"tr\": group transform \u2014 same fields as layer transform \"ks\"\n- \"tm\": trim path \u2014 \"s\" (start%, scalar), \"e\" (end%, scalar), \"o\" (offset, scalar)\n\nCRITICAL: Groups MUST have \"tr\" (transform) as the LAST item in their \"it\" array.";
|
|
3
|
+
export declare function buildSystemPrompt(tokens?: DesignTokens): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { PULSING_CIRCLE, WAVEFORM_BARS } from './examples.js';
|
|
2
|
+
export const LOTTIE_FORMAT_REFERENCE = `
|
|
3
|
+
LOTTIE FORMAT REFERENCE:
|
|
4
|
+
|
|
5
|
+
Top-level required fields:
|
|
6
|
+
- "v": string (use "5.5.2")
|
|
7
|
+
- "fr": number (use 60 for 60fps)
|
|
8
|
+
- "ip": 0 (in-point, always 0)
|
|
9
|
+
- "op": number (out-point, end frame — e.g. 120 for 2s at 60fps)
|
|
10
|
+
- "w": number (width in pixels)
|
|
11
|
+
- "h": number (height in pixels)
|
|
12
|
+
- "ddd": 0 (no 3D)
|
|
13
|
+
- "assets": [] (empty array)
|
|
14
|
+
- "layers": array of layer objects
|
|
15
|
+
|
|
16
|
+
Layer (shape layer, ty=4):
|
|
17
|
+
- "ty": 4 (shape layer)
|
|
18
|
+
- "ind": number (unique index)
|
|
19
|
+
- "nm": string (layer name)
|
|
20
|
+
- "ip": 0, "op": same as top-level op
|
|
21
|
+
- "st": 0 (start time)
|
|
22
|
+
- "ddd": 0
|
|
23
|
+
- "ks": transform object
|
|
24
|
+
- "shapes": array of shape objects
|
|
25
|
+
- "bm": 0 (blend mode, normal)
|
|
26
|
+
|
|
27
|
+
Transform "ks" object:
|
|
28
|
+
- "a": anchor point (VECTOR property)
|
|
29
|
+
- "p": position (VECTOR property)
|
|
30
|
+
- "s": scale (VECTOR property, [100,100] = 100%)
|
|
31
|
+
- "r": rotation (SCALAR property, degrees)
|
|
32
|
+
- "o": opacity (SCALAR property, 0-100)
|
|
33
|
+
|
|
34
|
+
PROPERTY TYPES — this distinction is critical:
|
|
35
|
+
- VECTOR property (position, scale, anchor, size): {"a":0,"k":[x,y]} or animated {"a":1,"k":[keyframes]}
|
|
36
|
+
- SCALAR property (rotation, opacity, roundness, stroke width): {"a":0,"k":0} or animated {"a":1,"k":[keyframes]}
|
|
37
|
+
- COLOR property: {"a":0,"k":[r,g,b,1]} with 0-1 floats (NOT 0-255)
|
|
38
|
+
|
|
39
|
+
Keyframe format:
|
|
40
|
+
{"t":frame,"s":[values],"o":{"x":[n],"y":[n]},"i":{"x":[n],"y":[n]}}
|
|
41
|
+
- "t": frame number
|
|
42
|
+
- "s": start values (array for vector, array with single value for scalar)
|
|
43
|
+
- "o": out-tangent (ease out), "i": in-tangent (ease in)
|
|
44
|
+
- Last keyframe needs only "t" and "s" (no tangents)
|
|
45
|
+
|
|
46
|
+
Easing (ease-in-out): "o":{"x":[0.42],"y":[0]}, "i":{"x":[0.58],"y":[1]}
|
|
47
|
+
|
|
48
|
+
Shape types:
|
|
49
|
+
- "el": ellipse — "p" (center, vector), "s" (size, vector)
|
|
50
|
+
- "rc": rectangle — "p" (center, vector), "s" (size, vector), "r" (roundness, scalar)
|
|
51
|
+
- "sh": path — "ks" with bezier {"c":bool, "v":[[x,y],...], "i":[[dx,dy],...], "o":[[dx,dy],...]}
|
|
52
|
+
- "fl": fill — "c" (color), "o" (opacity, scalar), "r" (fill rule, 1=nonzero)
|
|
53
|
+
- "st": stroke — "c" (color), "o" (opacity), "w" (width, scalar), "lc" (line cap, 2=round), "lj" (line join, 2=round)
|
|
54
|
+
- "gr": group — "it" array of shapes + MUST end with "tr" (group transform)
|
|
55
|
+
- "tr": group transform — same fields as layer transform "ks"
|
|
56
|
+
- "tm": trim path — "s" (start%, scalar), "e" (end%, scalar), "o" (offset, scalar)
|
|
57
|
+
|
|
58
|
+
CRITICAL: Groups MUST have "tr" (transform) as the LAST item in their "it" array.`;
|
|
59
|
+
export function buildSystemPrompt(tokens) {
|
|
60
|
+
const sections = [];
|
|
61
|
+
// 1. Role + output rules
|
|
62
|
+
sections.push(`You are a Lottie animation generator. Output ONLY valid JSON. No markdown fences, no explanation, no commentary. Your output must be directly parseable by JSON.parse().`);
|
|
63
|
+
// 2. Lottie format spec
|
|
64
|
+
sections.push(LOTTIE_FORMAT_REFERENCE);
|
|
65
|
+
// 3. Design rules
|
|
66
|
+
sections.push(`
|
|
67
|
+
DESIGN RULES:
|
|
68
|
+
- Frame rate: 60fps
|
|
69
|
+
- Duration: 120-180 frames (2-3 seconds)
|
|
70
|
+
- Canvas: 512x512 pixels, center at [256, 256]
|
|
71
|
+
- Loopable: last keyframe values must match first keyframe values
|
|
72
|
+
- Use ease-in-out easing by default
|
|
73
|
+
- Shape layers only (ty=4)
|
|
74
|
+
- Always include "ddd": 0 and "assets": []
|
|
75
|
+
- Keep animations smooth and proportional
|
|
76
|
+
- Use distinct colors for different elements`);
|
|
77
|
+
// 4. Design tokens (conditional)
|
|
78
|
+
if (tokens) {
|
|
79
|
+
const colorLines = Object.entries(tokens.colors)
|
|
80
|
+
.map(([name, rgba]) => ` ${name}: [${rgba.join(', ')}]`)
|
|
81
|
+
.join('\n');
|
|
82
|
+
sections.push(`
|
|
83
|
+
DESIGN TOKENS — use these colors when appropriate:
|
|
84
|
+
${colorLines}`);
|
|
85
|
+
}
|
|
86
|
+
// 5. Few-shot examples
|
|
87
|
+
sections.push(`
|
|
88
|
+
EXAMPLE 1 — Pulsing circle (scale animation):
|
|
89
|
+
${JSON.stringify(PULSING_CIRCLE)}
|
|
90
|
+
|
|
91
|
+
EXAMPLE 2 — 3-bar waveform (staggered position animation):
|
|
92
|
+
${JSON.stringify(WAVEFORM_BARS)}`);
|
|
93
|
+
return sections.join('\n');
|
|
94
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface DesignTokens {
|
|
2
|
+
colors: Record<string, [number, number, number, number]>;
|
|
3
|
+
}
|
|
4
|
+
export declare const SOTTO_TOKENS: DesignTokens;
|
|
5
|
+
/** Load design tokens from a JSON file, converting hex colors to Lottie RGBA */
|
|
6
|
+
export declare function loadDesignTokens(path?: string): DesignTokens | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { hexToLottieColor } from '../utils.js';
|
|
3
|
+
export const SOTTO_TOKENS = {
|
|
4
|
+
colors: {
|
|
5
|
+
primary: hexToLottieColor('#D97706'),
|
|
6
|
+
accent: hexToLottieColor('#1E3A5F'),
|
|
7
|
+
background: hexToLottieColor('#FEFCF8'),
|
|
8
|
+
surface: hexToLottieColor('#FFFFFF'),
|
|
9
|
+
text: hexToLottieColor('#1A1A1A'),
|
|
10
|
+
muted: hexToLottieColor('#6B7280'),
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
/** Load design tokens from a JSON file, converting hex colors to Lottie RGBA */
|
|
14
|
+
export function loadDesignTokens(path) {
|
|
15
|
+
if (!path)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (path === 'sotto')
|
|
18
|
+
return SOTTO_TOKENS;
|
|
19
|
+
const raw = readFileSync(path, 'utf-8');
|
|
20
|
+
const data = JSON.parse(raw);
|
|
21
|
+
if (!data.colors)
|
|
22
|
+
return undefined;
|
|
23
|
+
const colors = {};
|
|
24
|
+
for (const [name, hex] of Object.entries(data.colors)) {
|
|
25
|
+
colors[name] = hexToLottieColor(hex);
|
|
26
|
+
}
|
|
27
|
+
return { colors };
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
export async function generateWithAnthropic(model, systemPrompt, userPrompt) {
|
|
3
|
+
const apiKey = process.env['ANTHROPIC_API_KEY'];
|
|
4
|
+
if (!apiKey) {
|
|
5
|
+
throw new Error('ANTHROPIC_API_KEY environment variable is required');
|
|
6
|
+
}
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
const client = new Anthropic({ apiKey });
|
|
9
|
+
const response = await client.messages.create({
|
|
10
|
+
model,
|
|
11
|
+
max_tokens: 16384,
|
|
12
|
+
system: systemPrompt,
|
|
13
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
14
|
+
});
|
|
15
|
+
const text = response.content
|
|
16
|
+
.filter((block) => block.type === 'text')
|
|
17
|
+
.map((block) => block.text)
|
|
18
|
+
.join('');
|
|
19
|
+
return {
|
|
20
|
+
content: text,
|
|
21
|
+
provider: 'anthropic',
|
|
22
|
+
model,
|
|
23
|
+
durationMs: Date.now() - start,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { filterCliStderr } from '../utils.js';
|
|
3
|
+
export async function generateWithClaude(model, systemPrompt, userPrompt) {
|
|
4
|
+
const start = Date.now();
|
|
5
|
+
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
|
|
6
|
+
const content = await new Promise((resolve, reject) => {
|
|
7
|
+
const env = { ...process.env };
|
|
8
|
+
delete env.ANTHROPIC_API_KEY;
|
|
9
|
+
const proc = spawn('claude', ['--print', '--model', model], {
|
|
10
|
+
timeout: 240_000,
|
|
11
|
+
env,
|
|
12
|
+
});
|
|
13
|
+
let stdout = '';
|
|
14
|
+
let stderr = '';
|
|
15
|
+
proc.stdout.on('data', (d) => {
|
|
16
|
+
stdout += d.toString();
|
|
17
|
+
});
|
|
18
|
+
proc.stderr.on('data', (d) => {
|
|
19
|
+
stderr += d.toString();
|
|
20
|
+
});
|
|
21
|
+
proc.on('close', (code) => {
|
|
22
|
+
if (code !== 0) {
|
|
23
|
+
const filtered = filterCliStderr(stderr);
|
|
24
|
+
reject(new Error(`claude CLI exited ${code}: ${filtered}`));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
resolve(stdout.trim());
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
proc.on('error', (err) => {
|
|
31
|
+
if (err.code === 'ENOENT') {
|
|
32
|
+
reject(new Error('claude CLI not found. Install Claude Code first.'));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
reject(err);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
proc.stdin.write(fullPrompt);
|
|
39
|
+
proc.stdin.end();
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
content,
|
|
43
|
+
provider: 'claude-code',
|
|
44
|
+
model,
|
|
45
|
+
durationMs: Date.now() - start,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, readFileSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { filterCliStderr } from '../utils.js';
|
|
6
|
+
export async function generateWithCodex(model, systemPrompt, userPrompt) {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
|
|
9
|
+
const tmpFile = join(mkdtempSync(join(tmpdir(), 'codex-')), 'output.txt');
|
|
10
|
+
const args = [
|
|
11
|
+
'exec', '-',
|
|
12
|
+
'--skip-git-repo-check',
|
|
13
|
+
'--ephemeral',
|
|
14
|
+
'-o', tmpFile,
|
|
15
|
+
'--model', model,
|
|
16
|
+
];
|
|
17
|
+
const content = await new Promise((resolve, reject) => {
|
|
18
|
+
const proc = spawn('codex', args, {
|
|
19
|
+
timeout: 240_000,
|
|
20
|
+
env: { ...process.env },
|
|
21
|
+
});
|
|
22
|
+
let stderr = '';
|
|
23
|
+
proc.stderr.on('data', (d) => {
|
|
24
|
+
stderr += d.toString();
|
|
25
|
+
});
|
|
26
|
+
proc.on('close', (code) => {
|
|
27
|
+
const filtered = filterCliStderr(stderr);
|
|
28
|
+
const hint = filtered.includes('401') || filtered.includes('Unauthorized')
|
|
29
|
+
? ' (ensure codex is authenticated via `codex auth`)'
|
|
30
|
+
: '';
|
|
31
|
+
try {
|
|
32
|
+
const output = readFileSync(tmpFile, 'utf-8').trim();
|
|
33
|
+
unlinkSync(tmpFile);
|
|
34
|
+
if (code !== 0)
|
|
35
|
+
reject(new Error(`codex CLI exited ${code}: ${filtered}${hint}`));
|
|
36
|
+
else
|
|
37
|
+
resolve(output);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
reject(new Error(`codex CLI exited ${code}: ${filtered}${hint}`));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
proc.on('error', (err) => {
|
|
44
|
+
if (err.code === 'ENOENT') {
|
|
45
|
+
reject(new Error('codex CLI not found. Install Codex first.'));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
reject(err);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
proc.stdin.write(fullPrompt);
|
|
52
|
+
proc.stdin.end();
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
content,
|
|
56
|
+
provider: 'codex',
|
|
57
|
+
model,
|
|
58
|
+
durationMs: Date.now() - start,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface GenerationResult {
|
|
2
|
+
content: string;
|
|
3
|
+
provider: string;
|
|
4
|
+
model: string;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ProviderConfig {
|
|
8
|
+
displayName: string;
|
|
9
|
+
models: string[];
|
|
10
|
+
defaultModel: string;
|
|
11
|
+
isAvailable: () => Promise<boolean>;
|
|
12
|
+
generate: (model: string, systemPrompt: string, userPrompt: string) => Promise<GenerationResult>;
|
|
13
|
+
}
|
|
14
|
+
export declare const PROVIDERS: Record<string, ProviderConfig>;
|
|
15
|
+
/** Detect which providers are available (binary + auth) */
|
|
16
|
+
export declare function detectAvailableProviders(): Promise<string[]>;
|
|
17
|
+
/** Get default provider (first available in priority order) */
|
|
18
|
+
export declare function getDefaultProvider(): Promise<string | null>;
|