@mevdragon/vidfarm-devcli 0.16.0 → 0.18.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/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
- package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
- package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
- package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
- package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
- package/.agents/skills/vidfarm-media/SKILL.md +4 -3
- package/.agents/skills/vidfarm-media/references/tts.md +20 -1
- package/SKILL.director.md +20 -20
- package/SKILL.platform.md +4 -4
- package/dist/src/account-pages-legacy.js +2 -2
- package/dist/src/app.js +721 -101
- package/dist/src/cli.js +11 -10
- package/dist/src/devcli/clips.js +64 -64
- package/dist/src/editor-chat.js +2 -2
- package/dist/src/frontend/homepage-client.js +162 -2
- package/dist/src/frontend/homepage-store.js +30 -1
- package/dist/src/frontend/homepage-view.js +111 -4
- package/dist/src/homepage.js +184 -1
- package/dist/src/landing-page.js +367 -0
- package/dist/src/primitive-registry.js +278 -2
- package/dist/src/reskin/agency-page.js +299 -0
- package/dist/src/reskin/calendar-page.js +567 -0
- package/dist/src/reskin/chat-page.js +607 -0
- package/dist/src/reskin/discover-page.js +1096 -0
- package/dist/src/reskin/document.js +663 -0
- package/dist/src/reskin/help-page.js +356 -0
- package/dist/src/reskin/index-page.js +62 -0
- package/dist/src/reskin/inpaint-page.js +541 -0
- package/dist/src/reskin/job-runs-page.js +477 -0
- package/dist/src/reskin/library-page.js +688 -0
- package/dist/src/reskin/login-page.js +262 -0
- package/dist/src/reskin/pricing-page.js +388 -0
- package/dist/src/reskin/settings-page.js +687 -0
- package/dist/src/reskin/theme.js +362 -0
- package/dist/src/services/serverless-records.js +54 -0
- package/dist/src/services/swipe-customize.js +434 -0
- package/package.json +1 -1
- package/public/assets/homepage-client-app.js +22 -22
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Farmville SaaS — Design Tokens
|
|
2
|
+
|
|
3
|
+
The complete design system as CSS custom properties. Paste the `:root` block below into your global
|
|
4
|
+
stylesheet and reference the variables everywhere — never hand-pick ad-hoc hex/px values, or the system
|
|
5
|
+
drifts. Values are the real ones distilled from Fillout.com (a Tailwind-v4 default scale + a custom
|
|
6
|
+
honey-gold brand palette + rounded display type).
|
|
7
|
+
|
|
8
|
+
## Table of contents
|
|
9
|
+
1. Fonts (loading + stacks)
|
|
10
|
+
2. The `:root` token block (copy-paste)
|
|
11
|
+
3. Color rationale
|
|
12
|
+
4. Typography scale & usage
|
|
13
|
+
5. Radius, shadow, spacing rationale
|
|
14
|
+
6. Glass / blur
|
|
15
|
+
7. Motion
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Fonts
|
|
20
|
+
|
|
21
|
+
**Display / headings — `Hanken Grotesk`.** Fillout uses a proprietary rounded grotesque called
|
|
22
|
+
*blMelody*. Hanken Grotesk is the closest free, warm, rounded grotesque with an excellent bold. Good
|
|
23
|
+
alternates, all free on Google Fonts, in order of similarity: **Figtree**, **Gantari**, **Onest**,
|
|
24
|
+
**Plus Jakarta Sans**. If you have a license for blMelody, use it and keep Hanken as the fallback.
|
|
25
|
+
|
|
26
|
+
**Body / UI — `Inter`.** Exact match to Fillout. Free.
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
30
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
31
|
+
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Rules of thumb:
|
|
35
|
+
- Headlines, section titles, big numbers, the logo → **Hanken Grotesk** (or blMelody).
|
|
36
|
+
- Body copy, labels, buttons, nav items, form fields → **Inter**.
|
|
37
|
+
- Never set body copy in the display font, and never set a huge headline in Inter — the mix is the point.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 2. The `:root` token block
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
:root {
|
|
45
|
+
/* ---- Fonts ---- */
|
|
46
|
+
--font-display: "Hanken Grotesk", "blMelody", ui-sans-serif, system-ui, sans-serif;
|
|
47
|
+
--font-body: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
48
|
+
--font-mono: ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
|
|
49
|
+
|
|
50
|
+
/* ---- Signature accent: HONEY GOLD (the one "joy" color) ---- */
|
|
51
|
+
--gold-500: #ffc738; /* primary accent */
|
|
52
|
+
--gold-400: #ffd66f; /* lighter */
|
|
53
|
+
--gold-300: #f8d884; /* soft */
|
|
54
|
+
--gold-600: #f6c744; /* focus-ring gold */
|
|
55
|
+
--gold-700: #fcb900; /* deep / hover */
|
|
56
|
+
--gold-tint: #fff5e4; /* pale wash bg */
|
|
57
|
+
--gold-tint2:#ffe9cc; /* warm cream */
|
|
58
|
+
|
|
59
|
+
/* ---- Ink & neutral scale (Tailwind neutral) ---- */
|
|
60
|
+
--ink: #171717; /* primary text, dark buttons, footer */
|
|
61
|
+
--ink-950: #0a0a0a; /* deepest */
|
|
62
|
+
--n-900: #171717;
|
|
63
|
+
--n-800: #262626;
|
|
64
|
+
--n-700: #404040; /* heading text on light */
|
|
65
|
+
--n-600: #525252;
|
|
66
|
+
--n-500: #737373; /* secondary / body text */
|
|
67
|
+
--n-400: #a3a3a3; /* muted, placeholders, disabled */
|
|
68
|
+
--n-300: #d4d4d4; /* strong border */
|
|
69
|
+
--n-200: #ededed; /* hairline border (default) */
|
|
70
|
+
--n-150: #f0f0f0;
|
|
71
|
+
--n-100: #f5f5f5; /* section tint background */
|
|
72
|
+
--n-50: #fbfbfb; /* page background */
|
|
73
|
+
--white: #ffffff; /* card surface */
|
|
74
|
+
|
|
75
|
+
/* ---- Support pastels (icon tiles, nodes, illustration accents) ---- */
|
|
76
|
+
--sky: #3f9fe0; --sky-tint: #e2effd; /* hero sky / info */
|
|
77
|
+
--violet: #9f5bde; --violet-tint: #e4ddfd; /* AI / smart */
|
|
78
|
+
--green: #22c55e; --green-tint: #dcfce7; /* branch / success / nature */
|
|
79
|
+
--coral: #f97316; --coral-tint: #ffe9cc; /* energy / warm */
|
|
80
|
+
--red: #ef4444; --red-tint: #fee2e2; /* destructive */
|
|
81
|
+
|
|
82
|
+
/* ---- Semantic roles ---- */
|
|
83
|
+
--bg: var(--n-50);
|
|
84
|
+
--bg-section: var(--n-100);
|
|
85
|
+
--surface: var(--white);
|
|
86
|
+
--border: var(--n-200);
|
|
87
|
+
--border-strong: var(--n-300);
|
|
88
|
+
--text: var(--n-700);
|
|
89
|
+
--text-muted: var(--n-500);
|
|
90
|
+
--text-faint: var(--n-400);
|
|
91
|
+
--accent: var(--gold-500);
|
|
92
|
+
|
|
93
|
+
/* ---- Radius (curves are signature — nothing sharp) ---- */
|
|
94
|
+
--r-sm: 4px;
|
|
95
|
+
--r-md: 6px; /* inputs, small chips */
|
|
96
|
+
--r-lg: 8px; /* small buttons */
|
|
97
|
+
--r-xl: 12px; /* buttons, icon tiles */
|
|
98
|
+
--r-2xl: 16px; /* cards, nodes */
|
|
99
|
+
--r-3xl: 24px; /* big feature cards, image frames, nav pill */
|
|
100
|
+
--r-full: 9999px; /* nav bar, pill buttons, badges, avatars */
|
|
101
|
+
|
|
102
|
+
/* ---- Shadows (diffuse, low-opacity = soft sunlight, never harsh) ---- */
|
|
103
|
+
--shadow-xs: 0 1px 2px 0 #0000000d;
|
|
104
|
+
--shadow-sm: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
|
|
105
|
+
--shadow-md: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
|
|
106
|
+
--shadow-lg: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;
|
|
107
|
+
--shadow-xl: 0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;
|
|
108
|
+
--shadow-2xl:0 25px 50px -12px #00000040;
|
|
109
|
+
--shadow-card: 0 1px 2px #0000000d, 0 8px 24px -12px #00000014; /* lifted-card ambient */
|
|
110
|
+
--shadow-nav: 0 4px 24px -8px #00000026, 0 1px 2px #0000000d; /* floating glass nav */
|
|
111
|
+
--ring-gold: 0 0 0 3px var(--gold-600), 0 0 14px rgba(246,199,68,.4); /* focus */
|
|
112
|
+
|
|
113
|
+
/* ---- Type scale (rem) ---- */
|
|
114
|
+
--text-xs: 0.75rem; --lh-xs: 1rem;
|
|
115
|
+
--text-sm: 0.875rem; --lh-sm: 1.25rem;
|
|
116
|
+
--text-base:1rem; --lh-base:1.5rem;
|
|
117
|
+
--text-lg: 1.125rem; --lh-lg: 1.75rem;
|
|
118
|
+
--text-xl: 1.25rem; --lh-xl: 1.75rem;
|
|
119
|
+
--text-2xl: 1.5rem; --lh-2xl: 2rem;
|
|
120
|
+
--text-3xl: 1.875rem; --lh-3xl: 2.25rem;
|
|
121
|
+
--text-4xl: 2.25rem; --lh-4xl: 2.5rem;
|
|
122
|
+
--text-5xl: 3rem; --lh-5xl: 1.05;
|
|
123
|
+
--text-6xl: 3.75rem; --lh-6xl: 1.0;
|
|
124
|
+
--text-7xl: 4.5rem; --lh-7xl: 1.0;
|
|
125
|
+
--display: clamp(3rem, 7vw, 6rem); /* hero headline, fluid */
|
|
126
|
+
|
|
127
|
+
/* ---- Font weights ---- */
|
|
128
|
+
--fw-normal: 400; --fw-medium: 500; --fw-semibold: 600;
|
|
129
|
+
--fw-bold: 700; --fw-extrabold: 800;
|
|
130
|
+
|
|
131
|
+
/* ---- Tracking / leading ---- */
|
|
132
|
+
--track-tighter: -0.05em;
|
|
133
|
+
--track-tight: -0.025em; /* headings default */
|
|
134
|
+
--track-normal: 0em;
|
|
135
|
+
--lh-tight: 1.1; --lh-snug: 1.25; --lh-normal: 1.5; --lh-relaxed: 1.6;
|
|
136
|
+
|
|
137
|
+
/* ---- Spacing (4px base) ---- */
|
|
138
|
+
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px;
|
|
139
|
+
--sp-6: 24px; --sp-8: 32px; --sp-10:40px; --sp-12:48px; --sp-16:64px;
|
|
140
|
+
--sp-20:80px; --sp-24:96px; --sp-32:128px;
|
|
141
|
+
--section-y: clamp(4rem, 9vw, 7rem); /* vertical rhythm between sections */
|
|
142
|
+
--container: 75rem; /* 1200px, max content width */
|
|
143
|
+
--gutter: clamp(1rem, 4vw, 2rem);
|
|
144
|
+
|
|
145
|
+
/* ---- Glass / blur ---- */
|
|
146
|
+
--glass-bg: rgba(251,251,251,0.8);
|
|
147
|
+
--glass-blur: 16px;
|
|
148
|
+
|
|
149
|
+
/* ---- Motion ---- */
|
|
150
|
+
--ease: cubic-bezier(0.22, 1, 0.36, 1); /* gentle ease-out */
|
|
151
|
+
--dur-fast: 150ms; --dur: 220ms; --dur-slow: 320ms;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Baseline application */
|
|
155
|
+
html { font-size: 16px; }
|
|
156
|
+
body {
|
|
157
|
+
font-family: var(--font-body);
|
|
158
|
+
color: var(--text);
|
|
159
|
+
background: var(--bg);
|
|
160
|
+
-webkit-font-smoothing: antialiased;
|
|
161
|
+
text-rendering: optimizeLegibility;
|
|
162
|
+
}
|
|
163
|
+
h1,h2,h3,h4 {
|
|
164
|
+
font-family: var(--font-display);
|
|
165
|
+
font-weight: var(--fw-extrabold);
|
|
166
|
+
letter-spacing: var(--track-tight);
|
|
167
|
+
line-height: var(--lh-tight);
|
|
168
|
+
color: var(--ink);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 3. Color rationale
|
|
175
|
+
|
|
176
|
+
- **Honey-gold `#ffc738` is the whole personality.** It is the *only* saturated brand color. Use it
|
|
177
|
+
where you want a smile: the footer's primary CTA, star ratings, a "Publish" chip, the logo, a small
|
|
178
|
+
underline/highlight, a hover glow. If you find yourself using gold on more than ~5–10% of a screen,
|
|
179
|
+
pull back — its power is scarcity. Never pair it with a second competing bright brand color.
|
|
180
|
+
- **Charcoal `#171717` is the workhorse.** Primary buttons on light backgrounds, primary text, the
|
|
181
|
+
footer surface. It reads premium and calm and lets the gold pop.
|
|
182
|
+
- **Everything else is the neutral gray scale.** Backgrounds alternate `#fbfbfb` (page) ↔ `#f5f5f5`
|
|
183
|
+
(tinted section) ↔ `#ffffff` (cards). Borders are the near-invisible `#ededed` hairline. Text steps
|
|
184
|
+
down `#171717` → `#404040` → `#737373` → `#a3a3a3`.
|
|
185
|
+
- **Support pastels are for tiny surfaces only** — icon tiles, workflow nodes, illustration accents,
|
|
186
|
+
chart dots. Each has a saturated glyph color + a pale tint background (violet on `#e4ddfd`, green on
|
|
187
|
+
`#dcfce7`, etc.). They add life without stealing the gold's job.
|
|
188
|
+
- **The illustrated hero introduces sky-blue and green** as *painted* colors, not UI colors. That's
|
|
189
|
+
intentional — the warmth lives in the artwork, the UI stays disciplined.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 4. Typography scale & usage
|
|
194
|
+
|
|
195
|
+
| Element | Font | Size | Weight | Tracking | Line-height |
|
|
196
|
+
|---|---|---|---|---|---|
|
|
197
|
+
| Hero headline | display | `--display` (clamp 3→6rem) | 800 | -0.03em | 1.0 |
|
|
198
|
+
| Section title | display | `--text-5xl`/`4xl` | 800 | -0.025em | 1.05 |
|
|
199
|
+
| Card / feature title | display | `--text-xl`/`2xl` | 700 | -0.02em | 1.2 |
|
|
200
|
+
| Subhead / lede | body | `--text-lg`/`xl` | 400 | 0 | 1.5 |
|
|
201
|
+
| Body copy | body | `--text-base` | 400 | 0 | 1.6 |
|
|
202
|
+
| Small / caption | body | `--text-sm` | 400/500 | 0 | 1.4 |
|
|
203
|
+
| Button label | body | `--text-sm`/`base` | 600 | 0 | 1 |
|
|
204
|
+
| Badge / eyebrow | body | `--text-sm` | 500 | 0 | 1 |
|
|
205
|
+
| Big stat number | display | `--text-5xl`+ | 700 | -0.02em | 1 |
|
|
206
|
+
|
|
207
|
+
**Two-tone hero trick:** split the headline across two lines and set the first line to a translucent
|
|
208
|
+
white and the second to solid white for a soft-to-strong reveal:
|
|
209
|
+
|
|
210
|
+
```css
|
|
211
|
+
.hero-h1 { font: 800 var(--display)/1.0 var(--font-display); letter-spacing:-0.03em; color:#fff; }
|
|
212
|
+
.hero-h1 .soft { color: rgba(255,255,255,0.55); }
|
|
213
|
+
```
|
|
214
|
+
```html
|
|
215
|
+
<h1 class="hero-h1"><span class="soft">Forms that</span><br>do it all</h1>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
On light backgrounds, headings are ink `#171717`; subheads are `#737373`.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 5. Radius, shadow, spacing rationale
|
|
223
|
+
|
|
224
|
+
- **Radius is a personality dial and it's turned way up.** Interactive elements are *fully* rounded
|
|
225
|
+
(nav, buttons, badges → `--r-full`). Containers are generously rounded (cards `16px`, big cards &
|
|
226
|
+
image frames `24px`). Only truly small utility elements go as low as `4–6px`. When in doubt, round
|
|
227
|
+
more. A single sharp corner breaks the whole cozy feeling.
|
|
228
|
+
- **Shadows imitate soft ambient sunlight**, not hard drop-shadows. They're always large-radius and
|
|
229
|
+
low-opacity (`#0000001a` ≈ 10% black; the biggest is 25%). Use `--shadow-card` on resting cards and
|
|
230
|
+
`--shadow-nav` on the floating nav. On hover, step a card from `--shadow-card` to `--shadow-lg` and
|
|
231
|
+
nudge it up 2px.
|
|
232
|
+
- **Spacing is generous and rhythmic.** Sections get `--section-y` (≈ 6rem) of vertical padding.
|
|
233
|
+
Content maxes at `--container` (1200px) and stays centered with `--gutter` side padding. Inside
|
|
234
|
+
cards, pad `24–40px`. Whitespace *is* the premium signal — don't crowd.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 6. Glass / blur
|
|
239
|
+
|
|
240
|
+
The floating nav and any sticky "feature tab" bar use frosted glass:
|
|
241
|
+
|
|
242
|
+
```css
|
|
243
|
+
.glass {
|
|
244
|
+
background: var(--glass-bg); /* rgba(251,251,251,0.8) */
|
|
245
|
+
backdrop-filter: blur(var(--glass-blur)); /* 16px */
|
|
246
|
+
-webkit-backdrop-filter: blur(var(--glass-blur));
|
|
247
|
+
border: 1px solid var(--border);
|
|
248
|
+
box-shadow: var(--shadow-nav);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
Over the illustrated hero, use a whiter translucent fill (`rgba(255,255,255,0.7)`) so nav text stays legible.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 7. Motion
|
|
256
|
+
|
|
257
|
+
Gentle and springy, never bouncy-cartoonish. One easing curve (`--ease`, a soft ease-out) and short
|
|
258
|
+
durations (150–320ms). The canonical interactions:
|
|
259
|
+
|
|
260
|
+
- **Card hover:** `transform: translateY(-2px)` + shadow `--shadow-card` → `--shadow-lg`.
|
|
261
|
+
- **Button hover:** slight brightness/darken + `--shadow-md`; the trailing `→` arrow nudges right ~3px.
|
|
262
|
+
- **Focus:** `box-shadow: var(--ring-gold)` — the gold glow ring.
|
|
263
|
+
- **Section reveal (optional):** fade-up 12px over 320ms as it enters the viewport.
|
|
264
|
+
|
|
265
|
+
```css
|
|
266
|
+
.card { transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
|
|
267
|
+
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
|
|
268
|
+
.btn .arrow { transition: transform var(--dur) var(--ease); }
|
|
269
|
+
.btn:hover .arrow { transform: translateX(3px); }
|
|
270
|
+
:where(a,button,input,textarea):focus-visible { outline: none; box-shadow: var(--ring-gold); }
|
|
271
|
+
```
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vidfarm-media
|
|
3
|
-
description: Media resolution for video workflows — narration TTS, word-level timings, BGM, SFX, and image/clip sourcing through vidfarm. Use when a video needs audio (voiceover/narration, music, sound effects), caption word timings, or media assets. Ships the shared audio engine scripts/audio.mjs (audio_request.json → audio_meta.json) used by faceless-explainer, product-launch-video, and general-video; documents vidfarm tts/stt BYOK primitives, the keyless local Kokoro + whisper.cpp engines, and My Files /
|
|
3
|
+
description: Media resolution for video workflows — narration TTS, voice-matched speech regeneration ("same speaker, new words"), word-level timings, BGM, SFX, and image/clip sourcing through vidfarm. Use when a video needs audio (voiceover/narration, music, sound effects), rewording of existing narration in the original speaker's voice, caption word timings, or media assets. Ships the shared audio engine scripts/audio.mjs (audio_request.json → audio_meta.json) used by faceless-explainer, product-launch-video, and general-video; documents vidfarm tts/stt BYOK primitives, the keyless local Kokoro + whisper.cpp engines, and My Files / raws library / generation sourcing. No third-party media accounts — everything is user-owned or generated on the user's keys.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# vidfarm-media
|
|
7
7
|
|
|
8
|
-
One engine and one rule. The engine: `scripts/audio.mjs` turns a neutral `audio_request.json` into narration files, word timings, a music bed, and SFX — written into the project and described by one meta JSON. The rule: **no third-party media accounts, ever** — audio and images are the user's own files, found in the user's My Files /
|
|
8
|
+
One engine and one rule. The engine: `scripts/audio.mjs` turns a neutral `audio_request.json` into narration files, word timings, a music bed, and SFX — written into the project and described by one meta JSON. The rule: **no third-party media accounts, ever** — audio and images are the user's own files, found in the user's My Files / raws library, generated on the user's provider keys, or produced by keyless local models. If a source needs a sign-up, it is not a source.
|
|
9
9
|
|
|
10
10
|
## Preflight (run once, before the brief)
|
|
11
11
|
|
|
@@ -63,6 +63,7 @@ Contract points:
|
|
|
63
63
|
| Narration, promptable voice (BYOK) | `vidfarm tts "…" --style "calm, warm narrator"` | Local-first on your env key; `--voice alloy`/`Kore`…; `--cloud` = saved-key platform job → `POST /api/v1/primitives/audio/speech` |
|
|
64
64
|
| Narration, zero keys | `npx hyperframes tts "…" -v af_heart --json` | Kokoro-82M, local, WAV + duration in JSON |
|
|
65
65
|
| Transcript + SRT (BYOK) | `vidfarm stt <file\|url>` | Gemini key labels speakers; OpenAI whisper-1 gives real word timestamps |
|
|
66
|
+
| Reword existing narration in the (approximate) original voice | `POST /api/v1/primitives/audio/regenerate-speech` (platform job on saved keys; no devcli command yet) | One job: listens to a video/audio source, profiles the speaker (needs a Gemini key), rewords the transcript per `rewrite_instruction` (or takes exact `text`), regenerates with the closest preset voice + matched style. Always an approximation — OpenAI/Gemini cannot clone voices. Details: `references/tts.md` |
|
|
66
67
|
| Word timings, zero keys | `npx hyperframes transcribe <audio> --json` | whisper.cpp; writes word-level `transcript.json` |
|
|
67
68
|
| Captions straight into a composition | `vidfarm captions generate <dir>` | whisper-1 real timings when an OpenAI key exists; estimates otherwise |
|
|
68
69
|
|
|
@@ -80,6 +81,6 @@ Voices, styles, formats, and the exact word-timestamp paths: `references/tts.md`
|
|
|
80
81
|
Same ownership rule, different surfaces:
|
|
81
82
|
|
|
82
83
|
- **My Files** — `vidfarm files --search "<meaning>"`; annotate assets with `vidfarm annotate-file <id> --notes "…"` so search finds them later; write with `vidfarm put-file`.
|
|
83
|
-
- **
|
|
84
|
+
- **Raws library** — `vidfarm raws search "<text>"` over raws hunted from the user's long-form footage (`vidfarm raws scan <video>`).
|
|
84
85
|
- **Generate on the user's keys** — `vidfarm generate image|video --prompt "…"` (BYOK primitives; `--place <dir>` drops the result straight into a composition).
|
|
85
86
|
- Never hotlink or scrape third-party stock; a capture workflow's own screenshots and the user's brand assets are always fair game.
|
|
@@ -21,7 +21,26 @@ vidfarm tts "Welcome back to the channel" --style "calm, warm bedtime narrator"
|
|
|
21
21
|
- Gemini (`gemini-*-tts`, TitleCase, also the default family for OpenRouter TTS): `Kore, Puck, Charon, Aoede, Fenrir, Leda, Orus, Zephyr, Achernar, Achird, Algenib, Algieba, Alnilam, Autonoe, Callirrhoe, Despina, Enceladus, Erinome, Gacrux, Iapetus, Laomedeia, Pulcherrima, Rasalgethi, Sadachbia, Sadaltager, Schedar, Sulafat, Umbriel, Vindemiatrix, Zubenelgenubi`
|
|
22
22
|
- **Formats**: `--format mp3|wav` (default mp3). `--json` prints `{ ok, provider, model, out, … }`.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Voice-matched regeneration — "same speaker, new words"
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Platform job on saved keys (no devcli command yet); poll the returned job_id.
|
|
28
|
+
POST /api/v1/primitives/audio/regenerate-speech # alias: /api/v1/primitives/regenerate-speech
|
|
29
|
+
{ "tracer": "…", "payload": {
|
|
30
|
+
"source_url": "…", # video or audio; video audio is demuxed automatically
|
|
31
|
+
"rewrite_instruction": "mention the summer sale instead of the discount code",
|
|
32
|
+
"text": "…", # OR: the exact new script (one of the two is required)
|
|
33
|
+
"voice": "…", "instructions": "…", # optional preset override / extra style guidance
|
|
34
|
+
"provider": "…", "model": "…", "language": "…", "output_format": "mp3|wav"
|
|
35
|
+
} }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
One job: listens to the source, profiles the dominant speaker with an audio-native Gemini call (verbatim transcript + gender/age/accent/pace/pitch/energy profile + a dense style direction + the closest preset in both voice families), rewords the transcript per `rewrite_instruction` (keeping length and rhythm) or takes the exact `text`, then regenerates via TTS with the matched preset + profiled style instructions. Result: durable `audioUrl`, `script`, `original_transcript`, `voice_profile`, `voice_match`, plus a `regenerate-speech.json` artifact.
|
|
39
|
+
|
|
40
|
+
- **Honesty rule (non-negotiable): this is an approximation, never a clone.** Neither OpenAI nor Gemini exposes voice cloning through their public APIs — both offer preset voices with style steering only (actual cloning is sales-allowlisted at both, e.g. OpenAI "Custom Voices", Google Chirp 3 Instant Custom Voice). `voice_match` says `"approximate"` or `"none"`; always present it that way to the user. True self-serve cloning would need an ElevenLabs/Cartesia-class provider or a local OSS model (F5-TTS, CosyVoice, XTTS-v2, Chatterbox) — none are wired into vidfarm today.
|
|
41
|
+
- **Voice profiling needs a saved Gemini key** (it is the audio-native LLM). With only openai/openrouter keys the job degrades gracefully: STT transcript + reword still work, but the voice is a default or explicitly passed preset and `voice_match` is `"none"`.
|
|
42
|
+
- Sources over ~40 minutes of speech must be trimmed first (same cap as `/audio/transcribe`).
|
|
43
|
+
- To swap the result into a composition: mute the original span (`/videos/mute` or the layer's volume), then place `audioUrl` as a new audio layer.
|
|
25
44
|
|
|
26
45
|
```bash
|
|
27
46
|
npx hyperframes tts "Welcome back" -v af_heart -o line.wav --json # {"ok":true,"durationSeconds":…,"outputPath":…}
|
package/SKILL.director.md
CHANGED
|
@@ -208,11 +208,11 @@ Use it whenever you need to know what the video says or shows: writing/translati
|
|
|
208
208
|
- **Editor chat (frontend AI)** exposes this as the optional `video_context` tool — the copilot calls it on demand.
|
|
209
209
|
- **Desktop agents (Claude Code / Codex)**: fetch the `video-context.json` route directly (e.g. `vidfarm api GET /api/v1/compositions/<forkId>/video-context.json`) to ground edits in what the video says and shows.
|
|
210
210
|
|
|
211
|
-
##
|
|
211
|
+
## Raws (long-form → short-form raws)
|
|
212
212
|
|
|
213
|
-
Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/X URL, or an upload) into a library of tagged, searchable **
|
|
213
|
+
Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/X URL, or an upload) into a library of tagged, searchable **raws**. This is the `/raws` surface — browse it at `https://vidfarm.cc/library/raws` (the Library page's "Approved / Raws" tabs).
|
|
214
214
|
|
|
215
|
-
**Start a hunt** — `POST /
|
|
215
|
+
**Start a hunt** — `POST /raws/scan` (async: returns `202 { scan_id }` immediately):
|
|
216
216
|
|
|
217
217
|
```jsonc
|
|
218
218
|
{
|
|
@@ -228,34 +228,34 @@ Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/
|
|
|
228
228
|
// Hunt controls (all optional; also expressible inline in the prompt):
|
|
229
229
|
"ranges": ["12:30-15:45", "20:00-22:10"], // ONLY hunt these source windows — big cost saver on long videos
|
|
230
230
|
"target_duration_sec": 30, // SOFT length band, not a hard cut: 10→5-20s, 30→20-40s, 60→40-80s, 120→80-160s
|
|
231
|
-
"aspect": "9:16", // crop every
|
|
231
|
+
"aspect": "9:16", // crop every raw: 9:16 | 16:9 | 4:3 | 1:1 (synonyms: vertical/portrait, horizontal/landscape, square)
|
|
232
232
|
"crop_focus": "center", // center | top | bottom | left | right (top biases toward faces)
|
|
233
233
|
"avoid_text": true, // prefer scenes WITHOUT burned-in captions/on-screen text — a scene-SELECTION filter, NEVER GhostCut on the source
|
|
234
234
|
"tracer": "my-campaign" // rolls up all the hunt's billing/observability events
|
|
235
235
|
}
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
- **Poll** `GET /
|
|
239
|
-
- **Results**: `GET /
|
|
238
|
+
- **Poll** `GET /raws/scan/:scanId` until the source `status` is `complete` (or `failed` — crashed pipelines are reconciled to `failed`, pollers never spin).
|
|
239
|
+
- **Results**: `GET /raws/feed?source=<source_video_id>` (or the whole library), hybrid search via `POST /raws/search` `{ query: "someone looks confused" }`, per-raw download at `GET /raws/:clipId/download`. Raws carry taxonomy tags, a description, a transcript, an `aspect` field when cropped, the hunt's `tracer`, and (with a gemini/openai key) a semantic embedding.
|
|
240
240
|
- **Billing** — **AWS compute only** (`clip_scan_lambda` GB-seconds + Step Functions transitions, a fraction of a cent for typical hunts; the 202 response includes a `compute_estimate`). The AI tagging/refine runs on **your saved provider key (BYOK)** and is never wallet-billed. Submission is wallet-gated (402 when empty).
|
|
241
|
-
- **The original stays temporary** — URL-ingested and uploaded sources live ONLY in your temp folder (`users/…/temporary/clip-sources/…`) with a hard **30-day TTL** (auto-deleted from S3 + the temp-file list). The hunted
|
|
242
|
-
- **Captions rule** — "no captions / no on-screen text" hunts are handled by scene SELECTION (the refine pass drops text-heavy scenes). **Never run GhostCut caption removal on a long-form source** (it is hard-capped at ~15 minutes); to actually erase burned-in text, apply `POST /api/v1/primitives/videos/remove-captions` to individual FINISHED
|
|
241
|
+
- **The original stays temporary** — URL-ingested and uploaded sources live ONLY in your temp folder (`users/…/temporary/clip-sources/…`) with a hard **30-day TTL** (auto-deleted from S3 + the temp-file list). The hunted raws themselves are durable.
|
|
242
|
+
- **Captions rule** — "no captions / no on-screen text" hunts are handled by scene SELECTION (the refine pass drops text-heavy scenes). **Never run GhostCut caption removal on a long-form source** (it is hard-capped at ~15 minutes); to actually erase burned-in text, apply `POST /api/v1/primitives/videos/remove-captions` to individual FINISHED raws afterwards.
|
|
243
243
|
|
|
244
244
|
**devcli (local-first — this is the default way to hunt on your own machine):**
|
|
245
245
|
|
|
246
246
|
```bash
|
|
247
247
|
# Local machine power: local ffmpeg + your local claude/codex CLI subscription (no API key needed)
|
|
248
|
-
vidfarm
|
|
248
|
+
vidfarm raws scan ./podcast.mp4 --range "12:30-15:45" --duration 30 --aspect vertical --no-text \
|
|
249
249
|
--prompt "guest reaction moments"
|
|
250
250
|
|
|
251
251
|
# Provider keys are the fallback (--provider gemini|openai|openrouter); the cloud pipeline is the
|
|
252
252
|
# EXPLICIT backup, never the default:
|
|
253
|
-
vidfarm
|
|
254
|
-
vidfarm
|
|
253
|
+
vidfarm raws scan --cloud ./podcast.mp4 --duration 30 --aspect 9:16 --tracer my-campaign
|
|
254
|
+
vidfarm raws scan --cloud --url "https://youtube.com/watch?v=…" --range "0:00-10:00"
|
|
255
255
|
|
|
256
256
|
# Then search/reuse the library
|
|
257
|
-
vidfarm
|
|
258
|
-
vidfarm
|
|
257
|
+
vidfarm raws search "confused reaction after reading a message"
|
|
258
|
+
vidfarm raws export <raw-ids…> --to ./picks
|
|
259
259
|
```
|
|
260
260
|
|
|
261
261
|
Local scans persist to a SQLite library under `~/.vidfarm` (free compute, subscription-powered evaluation); `--cloud` uploads to your temp folder (30-day TTL) or passes `--url`, runs the deployed pipeline, and bills AWS compute only.
|
|
@@ -546,10 +546,10 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
|
|
|
546
546
|
| `vidfarm get-file <id> [dest] [--print]` | (resolve id → view_url, then stream/print) | read one My Files asset |
|
|
547
547
|
| `vidfarm put-file <file> [--folder <p>] [--as <name>] [--content/--stdin] [--notes "…"]` | `POST /api/v1/user/me/attachments/upload` | write into My Files (persistent) |
|
|
548
548
|
| `vidfarm annotate-file <id\|name> --notes "…"` | `PATCH /api/v1/user/me/attachments/:id` | set metadata notes on one My Files entry (vector-embedded) |
|
|
549
|
-
| `vidfarm
|
|
550
|
-
| `vidfarm
|
|
551
|
-
| `vidfarm
|
|
552
|
-
| `vidfarm
|
|
549
|
+
| `vidfarm raws scan <video> [--range MM:SS-MM:SS] [--duration <s>] [--aspect 9:16] [--no-text] [--prompt "…"]` | (LOCAL: local ffmpeg + local claude/codex agent → `~/.vidfarm` SQLite) | hunt long-form video into short-form raws on this machine |
|
|
550
|
+
| `vidfarm raws scan --cloud <video\|--url <url>> [--tracer <id>]` | temp-file presign/PUT/finalize + `POST /raws/scan` + poll | BACKUP: run the hunt on the deployed pipeline (bills AWS compute only) |
|
|
551
|
+
| `vidfarm raws search "…"` / `raws match "…"` / `raws list` / `raws sources` | (local library; NL→criteria via local agent or provider key) | search/reuse the raws library |
|
|
552
|
+
| `vidfarm raws preset list\|run\|save` / `raws export <ids…> --to <dir>` | (local library) | saved queries; copy raw MP4s out |
|
|
553
553
|
| `vidfarm lint <dir\|composition.html>` | (local static validation) | pre-publish composition check: timing, overlaps, preset names, media src |
|
|
554
554
|
| `vidfarm stills <dir> [--at 0,2.5,…]` | (local in-process render of PNG frames) | visually verify an edit without a full render |
|
|
555
555
|
| `vidfarm doctor` | (local environment triage) | check ffmpeg/node/keys/agent CLI/poisoned env before debugging anything else |
|
|
@@ -574,10 +574,10 @@ Each user has a persistent **My Files** filesystem — their own uploaded videos
|
|
|
574
574
|
|
|
575
575
|
### Metadata notes + vector search (find assets by meaning)
|
|
576
576
|
|
|
577
|
-
Every My Files entry carries an optional **`notes`** field — free-form metadata describing what the file *is*, who/what it depicts, and when to use it. Notes are **vector-embedded** on save (same BYOK embedding seam as the
|
|
577
|
+
Every My Files entry carries an optional **`notes`** field — free-form metadata describing what the file *is*, who/what it depicts, and when to use it. Notes are **vector-embedded** on save (same BYOK embedding seam as the raws library: gemini → openai key auto-pick; no key fail-softs to keyword-only), so the library is searchable by *meaning*, not just filename:
|
|
578
578
|
|
|
579
579
|
- **Annotate** — `vidfarm annotate-file <id|name> --notes "Sprite card for Zara, our mascot: front/side/back views, teal jacket. Use as the reference image whenever generating Zara."` (`PATCH /me/attachments/:id`, body `{ notes }`; empty string clears). Or set notes at write time: `vidfarm put-file zara.png --folder acme/characters/zara --notes "…"`. Web copilot: `browse_files action=annotate` (or `notes` on `action=write`).
|
|
580
|
-
- **Search** — `vidfarm files --search "the fox mascot reference sheet"` (`POST /me/attachments/search`, body `{ query, folder_path?, limit? }`) returns ranked hits with `similarity` + `keyword_match`, mirroring `/
|
|
580
|
+
- **Search** — `vidfarm files --search "the fox mascot reference sheet"` (`POST /me/attachments/search`, body `{ query, folder_path?, limit? }`) returns ranked hits with `similarity` + `keyword_match`, mirroring `/raws/search`: keyword survivors over name/folder/notes are semantically re-ranked, and when nothing keyword-matches it relaxes to a pure semantic pass. Web copilot: `browse_files action=search`.
|
|
581
581
|
- **Annotate what you'll want back.** Filenames alone don't survive months of accumulation — any asset a future session should find (character refs, logo variants, recurring backgrounds, key briefs) deserves notes at save time. `list`/`read` responses include each file's `notes`, so a scan of a folder doubles as a manifest of what's in it.
|
|
582
582
|
|
|
583
583
|
### Character consistency (recurring characters, mascots, spokespeople)
|
|
@@ -719,7 +719,7 @@ Remove burned-in captions/subtitles/on-screen text from any video URL (GhostCut-
|
|
|
719
719
|
- Response: standard primitive job (`202 { job_id }`). The job probes the source, submits to GhostCut, polls to completion server-side (typically a few minutes), and mirrors the result. Poll `GET /api/v1/primitives/jobs/:jobId` until `status: "succeeded"`, then read `primary_file_url` / `video.file_url` (`captionsRemovedVideoUrl` in the output).
|
|
720
720
|
- Billing: `~$0.10 per 30 seconds` of source video (per-chunk, 1-chunk minimum; `ghostcut_subtitle_removal` cost center), plus the small probe cost.
|
|
721
721
|
- For the CURRENT fork's decompose flow, don't re-run this primitive — the caption-free mirror already exists via `GET /api/v1/compositions/:forkId/remove-video-captions`.
|
|
722
|
-
- **Short clips only — hard cap ~15 minutes** (`GHOSTCUT_MAX_DURATION_SEC`, default 900s). Never point this at a long-form source: for "clips without captions" from a long video, run a
|
|
722
|
+
- **Short clips only — hard cap ~15 minutes** (`GHOSTCUT_MAX_DURATION_SEC`, default 900s). Never point this at a long-form source: for "clips without captions" from a long video, run a raws hunt with `avoid_text: true` (scene selection — see "Raws") and, if needed, apply this primitive to the individual FINISHED raws.
|
|
723
723
|
|
|
724
724
|
Example:
|
|
725
725
|
|
package/SKILL.platform.md
CHANGED
|
@@ -247,13 +247,13 @@ Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-thr
|
|
|
247
247
|
|
|
248
248
|
## Clip hunting pipeline (long-form → short-form)
|
|
249
249
|
|
|
250
|
-
`POST /
|
|
250
|
+
`POST /raws/scan` starts a Step Functions STANDARD workflow (`…ClipScanWorkflow`): `probe_and_detect` → `Map(process_scene, maxConcurrency 5)` → `finalize`, all three states dispatching the same `infra/lambda/clip-scan.ts` on an `action` field. Business logic lives in the shared `src/services/clip-curation/` core (also consumed by devcli and the Hono API), with the hunt controls in `hunt.ts`:
|
|
251
251
|
|
|
252
252
|
- **`hunt_spec`** — windows (time ranges; ffmpeg input-seeks per window so only the requested footage is DECODED — the actual cost saving), a soft `duration_band` (`resolveDurationBand`: 10→5-20s, 30→20-40s via ±target/3), output `aspect` + `crop_focus` (smart-centre ffmpeg crop applied to clips, thumbnails, AND tagging keyframes so the model judges the framed output), `avoid_text` (rides the keep/drop/merge refine pass as a selection filter), and `source_url` (RapidAPI ingest → the user's temp folder). The spec travels as ONE opaque JsonPath object through the state machine, so new hunt fields are Lambda-code-only changes — no state-machine redeploy.
|
|
253
253
|
- **Sources are temporary** — URL ingests and uploads land at `users/{id}/temporary/clip-sources/…` with a hard 30-day TTL enforced three ways: tag-scoped S3 lifecycle (`vidfarm-temp=1`, tagged server-side at temp-file finalize/upload), native DynamoDB TTL (`expires_at_epoch_seconds`, top-level on the item), and the list-route sweep. Hunted clips (`clips/{owner}/…`) are durable.
|
|
254
254
|
- **Billing: AWS compute ONLY.** Every Lambda invocation post-hoc bills its GB-seconds to the `clip_scan_lambda` cost center (idempotent on `awsRequestId`); `finalize` adds `step_functions_standard` scaled by scene count (3 + N transitions); URL ingest adds the existing `rapidapi_video_download` pass-through. `jobId = scan_id`, and the caller's `tracer` rides every event (gsi2-tracer rollups). The BYOK tagging/embedding spend (gemini/openai/openrouter keys in the state input) is deliberately never wallet-billed. Submission is wallet-gated (`assertWalletCanQueueJobs` → 402); the 202 response carries a pre-scan `compute_estimate`.
|
|
255
|
-
- **Failure reconciliation** — the workflow has no failure hook into records, so `GET /
|
|
256
|
-
- **devcli is LOCAL-FIRST** — `vidfarm
|
|
255
|
+
- **Failure reconciliation** — the workflow has no failure hook into records, so `GET /raws/scan/:scanId` lazily `DescribeExecution`s a running scan and flips the scan+source records to `failed` (or `complete`) so pollers converge.
|
|
256
|
+
- **devcli is LOCAL-FIRST** — `vidfarm raws scan` runs local ffmpeg and, by default, the user's local claude/codex CLI subscription (`LocalAgentClipClient`, implementing the same `ClipHuntModelClient` interface as the API providers; refine's model call lives on the client as `decideScenes` to make them swappable). Provider keys are the fallback; `--cloud` is the explicit backup that presign-uploads to the temp folder and drives this pipeline.
|
|
257
257
|
- **Bundling gotcha** — the clip-scan and primitive-media Lambdas bundle `ffmpeg-static`/`ffprobe-static` with `forceDockerBundling: true`: ffmpeg-static downloads the binary for the platform it is installed on, so a mac-local bundle ships a darwin ffmpeg the Lambda cannot exec (`exit 126`). They also need `VIDFARM_DATA_DIR=/tmp/vidfarm` because `config.ts` mkdirs at import.
|
|
258
258
|
|
|
259
259
|
## Editor Chat
|
|
@@ -266,7 +266,7 @@ Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-thr
|
|
|
266
266
|
|
|
267
267
|
The chat model uses the **caller's provider keys**. No Vidfarm-owned model calls happen in this path.
|
|
268
268
|
|
|
269
|
-
Beyond the Editor Action API, the chat harness registers shared tools defined in **two synced copies** (`src/app.ts` for the in-process dev server and `infra/lambda/editor-chat.ts` for the deployed Lambda — keep them in lockstep): `http_request` (allowlisted REST/primitive calls), `frontend_action` (tracer/thread state), `video_context`, `load_skill`, and `browse_files`. `load_skill` is the copilot's knowledge-on-demand seam: it fetches vendored skill-pack content (SKILL.md or a reference file) from the host's public `/skill/:name` + `/skill-pack/:name/files/*` mirror routes (backed by `.agents/skills/`, shipped in the api Docker image), size-capped, with the pack catalog + one-liners defined once in `EDITOR_CHAT_SKILL_PACKS` (`src/editor-chat.ts`) and shared by both copies. The stream-loop guardrails (`redactUnverifiedJobIds`, `buildEditorChatGuardrailNotice`, async-job handoff text) also live in `src/editor-chat.ts` and are imported by both copies — never re-implement them locally. `editor_action`'s `replace_composition_html` is lint-gated in the tool `execute` via `src/services/composition-lint.ts` (a rejected result carries `rejected: true` + `lint_errors` and omits `action_type`, so the browser never applies it). `browse_files` reads **and writes** the caller's My Files library — `action=list`/`search`/`read`/`write`/`annotate`. `write` saves a text file (md/txt/csv/json/srt/vtt) via `POST /me/attachments/upload`, or **imports a durable media URL** into the library when `source_url` is passed (how a generated `character_sprite_card.png` gets persisted instead of stranding in a job result). `annotate` sets free-form metadata `notes` on any entry (`PATCH /me/attachments/:id`); notes are **vector-embedded server-side** (BYOK, same gemini→openai auto-pick seam as clip search; no key fail-softs to keyword-only) and `search` runs the hybrid keyword+semantic lookup over every file's name/folder/notes via `POST /me/attachments/search` — the My Files mirror of `/
|
|
269
|
+
Beyond the Editor Action API, the chat harness registers shared tools defined in **two synced copies** (`src/app.ts` for the in-process dev server and `infra/lambda/editor-chat.ts` for the deployed Lambda — keep them in lockstep): `http_request` (allowlisted REST/primitive calls), `frontend_action` (tracer/thread state), `video_context`, `load_skill`, and `browse_files`. `load_skill` is the copilot's knowledge-on-demand seam: it fetches vendored skill-pack content (SKILL.md or a reference file) from the host's public `/skill/:name` + `/skill-pack/:name/files/*` mirror routes (backed by `.agents/skills/`, shipped in the api Docker image), size-capped, with the pack catalog + one-liners defined once in `EDITOR_CHAT_SKILL_PACKS` (`src/editor-chat.ts`) and shared by both copies. The stream-loop guardrails (`redactUnverifiedJobIds`, `buildEditorChatGuardrailNotice`, async-job handoff text) also live in `src/editor-chat.ts` and are imported by both copies — never re-implement them locally. `editor_action`'s `replace_composition_html` is lint-gated in the tool `execute` via `src/services/composition-lint.ts` (a rejected result carries `rejected: true` + `lint_errors` and omits `action_type`, so the browser never applies it). `browse_files` reads **and writes** the caller's My Files library — `action=list`/`search`/`read`/`write`/`annotate`. `write` saves a text file (md/txt/csv/json/srt/vtt) via `POST /me/attachments/upload`, or **imports a durable media URL** into the library when `source_url` is passed (how a generated `character_sprite_card.png` gets persisted instead of stranding in a job result). `annotate` sets free-form metadata `notes` on any entry (`PATCH /me/attachments/:id`); notes are **vector-embedded server-side** (BYOK, same gemini→openai auto-pick seam as clip search; no key fail-softs to keyword-only) and `search` runs the hybrid keyword+semantic lookup over every file's name/folder/notes via `POST /me/attachments/search` — the My Files mirror of `/raws/search`. Write+annotate are what let the copilot persist Getting Started context (About.md, awareness-levels.md, persuasive-angles.md, ad-hooks.md) and character-consistency pairs (`about_character.md` + `character_sprite_card.png`) namescoped under a product/offer folder; the system prompt in `src/editor-chat.ts` drives when to save and annotate. The devcli mirrors are `vidfarm put-file [--notes]`, `annotate-file`, and `files --search`.
|
|
270
270
|
|
|
271
271
|
## Primitives
|
|
272
272
|
|
|
@@ -880,10 +880,10 @@ export function renderSettingsPage(input) {
|
|
|
880
880
|
</div>
|
|
881
881
|
<div class="profile-field">
|
|
882
882
|
<div class="profile-field-head">
|
|
883
|
-
<label for="about">
|
|
883
|
+
<label for="about">Product / offer description</label>
|
|
884
884
|
<button type="submit" data-profile-about-save disabled>Save</button>
|
|
885
885
|
</div>
|
|
886
|
-
<textarea id="about" name="about" placeholder="
|
|
886
|
+
<textarea id="about" name="about" placeholder="Describe your product or offer — what it is, who it's for, and the promise. Swipe mode uses this to auto-recut templates into ads for you." data-profile-about data-initial-value="${escapeAttribute(input.about ?? "")}">${escapeHtml(input.about ?? "")}</textarea>
|
|
887
887
|
</div>
|
|
888
888
|
</form>
|
|
889
889
|
<section class="files-card" data-files-card>
|