@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.
Files changed (38) hide show
  1. package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
  2. package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
  3. package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
  4. package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
  5. package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
  6. package/.agents/skills/vidfarm-media/SKILL.md +4 -3
  7. package/.agents/skills/vidfarm-media/references/tts.md +20 -1
  8. package/SKILL.director.md +20 -20
  9. package/SKILL.platform.md +4 -4
  10. package/dist/src/account-pages-legacy.js +2 -2
  11. package/dist/src/app.js +721 -101
  12. package/dist/src/cli.js +11 -10
  13. package/dist/src/devcli/clips.js +64 -64
  14. package/dist/src/editor-chat.js +2 -2
  15. package/dist/src/frontend/homepage-client.js +162 -2
  16. package/dist/src/frontend/homepage-store.js +30 -1
  17. package/dist/src/frontend/homepage-view.js +111 -4
  18. package/dist/src/homepage.js +184 -1
  19. package/dist/src/landing-page.js +367 -0
  20. package/dist/src/primitive-registry.js +278 -2
  21. package/dist/src/reskin/agency-page.js +299 -0
  22. package/dist/src/reskin/calendar-page.js +567 -0
  23. package/dist/src/reskin/chat-page.js +607 -0
  24. package/dist/src/reskin/discover-page.js +1096 -0
  25. package/dist/src/reskin/document.js +663 -0
  26. package/dist/src/reskin/help-page.js +356 -0
  27. package/dist/src/reskin/index-page.js +62 -0
  28. package/dist/src/reskin/inpaint-page.js +541 -0
  29. package/dist/src/reskin/job-runs-page.js +477 -0
  30. package/dist/src/reskin/library-page.js +688 -0
  31. package/dist/src/reskin/login-page.js +262 -0
  32. package/dist/src/reskin/pricing-page.js +388 -0
  33. package/dist/src/reskin/settings-page.js +687 -0
  34. package/dist/src/reskin/theme.js +362 -0
  35. package/dist/src/services/serverless-records.js +54 -0
  36. package/dist/src/services/swipe-customize.js +434 -0
  37. package/package.json +1 -1
  38. 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 / clip-library / generation sourcing. No third-party media accounts — everything is user-owned or generated on the user's keys.
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 / clip 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.
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
- - **Clip library** — `vidfarm clips search "<text>"` over clips hunted from the user's long-form footage (`vidfarm clips scan <video>`).
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
- ## Keyless local TTS Kokoro-82M
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
- ## Clip hunting (long-form → short-form clips)
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 **short clips**. This is the `/clips` surface — browse it at `https://vidfarm.cc/clips` (the Library page's "Finished / Clips" toggle).
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 /clips/scan` (async: returns `202 { scan_id }` immediately):
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 clip: 9:16 | 16:9 | 4:3 | 1:1 (synonyms: vertical/portrait, horizontal/landscape, square)
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 /clips/scan/:scanId` until the source `status` is `complete` (or `failed` — crashed pipelines are reconciled to `failed`, pollers never spin).
239
- - **Results**: `GET /clips/feed?source=<source_video_id>` (or the whole library), hybrid search via `POST /clips/search` `{ query: "someone looks confused" }`, per-clip download at `GET /clips/:clipId/download`. Clips 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.
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 clips 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 clips afterwards.
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 clips scan ./podcast.mp4 --range "12:30-15:45" --duration 30 --aspect vertical --no-text \
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 clips scan --cloud ./podcast.mp4 --duration 30 --aspect 9:16 --tracer my-campaign
254
- vidfarm clips scan --cloud --url "https://youtube.com/watch?v=…" --range "0:00-10:00"
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 clips search "confused reaction after reading a message"
258
- vidfarm clips export <clip-ids…> --to ./picks
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 clips 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 clips on this machine |
550
- | `vidfarm clips scan --cloud <video\|--url <url>> [--tracer <id>]` | temp-file presign/PUT/finalize + `POST /clips/scan` + poll | BACKUP: run the hunt on the deployed pipeline (bills AWS compute only) |
551
- | `vidfarm clips search "…"` / `clips match "…"` / `clips list` / `clips sources` | (local library; NL→criteria via local agent or provider key) | search/reuse the clip library |
552
- | `vidfarm clips preset list\|run\|save` / `clips export <ids…> --to <dir>` | (local library) | saved queries; copy clip MP4s out |
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 clip library: gemini → openai key auto-pick; no key fail-softs to keyword-only), so the library is searchable by *meaning*, not just filename:
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 `/clips/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`.
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 clip hunt with `avoid_text: true` (scene selection — see "Clip hunting") and, if needed, apply this primitive to the individual FINISHED clips.
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 /clips/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`:
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 /clips/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 clips 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.
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 `/clips/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`.
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">Profile notes</label>
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="What should your operators or collaborators know about this account?" data-profile-about data-initial-value="${escapeAttribute(input.about ?? "")}">${escapeHtml(input.about ?? "")}</textarea>
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>