-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathjustfile
More file actions
431 lines (335 loc) · 19.9 KB
/
Copy pathjustfile
File metadata and controls
431 lines (335 loc) · 19.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# =============================================================================
# ROBOSYSTEMS CONTENT MACHINE — VIDEO CONTENT PIPELINE
# =============================================================================
#
# QUICK START:
# just new NVDA # Generic template
# just campaign GTBIF cannabis_coverage # Campaign coverage
# just webdeck-pipeline GTBIF # Run the full pipeline
#
# STEP BY STEP:
# just validate TICKER # Gate the authored output
# just voiceover TICKER # Generate ElevenLabs voiceovers
# just webdeck TICKER # Build the animated HTML deck
# just webdeck-render TICKER # Render it via headless Chrome
# just webdeck-mux TICKER # Mux narration + ducked music
# just webdeck-short-pipeline TICKER # The 9:16 short, same engine
#
# Rendering is entirely local (puppeteer + ffmpeg). No cloud render service.
#
# =============================================================================
_env := ".env"
default:
@just --list
[private]
ensure-env:
@test -f {{_env}} || cp .env.example {{_env}}
# ─── Coverage Setup ──────────────────────────────────────────
# Initiate coverage on a company (generic template)
new ticker:
./tools/new_project.sh {{ticker}}
# Initiate coverage on a company with a campaign
campaign ticker campaign_name:
./tools/new_project.sh {{ticker}} {{campaign_name}}
# Re-cover an existing ticker for a new quarter (archives prior outputs -> .history, keeps sources)
recover ticker campaign_name="":
./tools/new_project.sh {{ticker}} "{{campaign_name}}" --recover
# List all coverage projects
projects:
@ls -1 projects/ 2>/dev/null || echo "No projects yet. Run: just new TICKER"
# List available campaigns
campaigns:
@ls -1 campaigns/ 2>/dev/null || echo "No campaigns yet."
# Open a project folder
open project:
open projects/{{project}}
# Print the Cowork cold-start prompt for a project (also copies it to the clipboard, ready to paste into Cowork)
kickoff project:
@./tools/kickoff.sh {{project}}
# ─── QA ──────────────────────────────────────────────────────
# Validate cowork outputs before running pipeline
validate project:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/validate_project.py {{project}}
# Validate and auto-fix common schema issues
validate-fix project:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/validate_project.py {{project}} --fix
# ─── Pipeline Steps ──────────────────────────────────────────
# Generate the Claude Design hand-off brief from the script (deck mode); also copies DESIGN_INSTRUCTIONS + brief to the clipboard
deck-brief project:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/build_deck_brief.py {{project}}
# Slice a Claude Design deck PDF into 1920x1080 slide PNGs (deck mode)
slice project:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/slice_deck.py {{project}}
# Generate the 3 platform thumbnails via OpenAI (brief -> gpt-image-2 -> assets/{yt,x,spot}.png)
thumbnails project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/gen_thumbnails.py {{project}} {{args}}
# Generate voiceover audio via ElevenLabs
voiceover project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/generate_voiceover_audio.py {{project}} {{args}}
# RETIRED - the Shotstack cloud-render path was torn out 2026-08-08
assemble project *args:
@echo "'just assemble' is retired. The Shotstack cloud-render path was removed 2026-08-08."
@echo "Rendering is local now: just webdeck-pipeline {{project}}"
@echo "(tools/assemble_video.py lives in git history if you ever need it back.)"
@exit 1
# RETIRED - the deck path (Claude Design PPTX -> slice -> Shotstack) was torn out 2026-08-08
pipeline project:
@echo "'just pipeline' is retired. It rendered via Shotstack, which is no longer used."
@echo "Use: just webdeck-pipeline {{project}} (validate -> voiceover -> build -> render -> mux)"
@echo "(tools/run_pipeline.sh lives in git history if you ever need it back.)"
@exit 1
# ─── Webdeck (pilot): animated HTML deck → frame render → mux ─
# Full webdeck pipeline: validate → voiceover → build → render → mux (no PPTX, no Shotstack)
# Validates with --pre-render: the render-freshness check would otherwise refuse to let
# the pipeline run precisely when a re-render is what the project needs.
webdeck-pipeline project: (validate-pre-render project) (voiceover project) (webdeck project) (webdeck-render project) (webdeck-mux project)
# Validate everything except render freshness (used as the pipeline's pre-render gate)
validate-pre-render project:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/validate_project.py {{project}} --pre-render
# Build the animated webdeck HTML from script.json + VO durations
webdeck project *args:
python3 tools/build_webdeck.py {{project}} {{args}}
# Render the webdeck to silent.mp4, frame by frame via headless Chrome (1080p30)
webdeck-render project *args:
cd tools/webdeck && node render_webdeck.mjs --html ../../projects/{{project}}/webdeck/{{project}}_webdeck.html --out ../../projects/{{project}}/webdeck/render {{args}}
# QA stills for the long-form (comma-separated seconds), e.g. just webdeck-stills CALM "78,140"
# A still takes ~10s against a ~16-min render - use it to check any template change.
webdeck-stills project times:
cd tools/webdeck && node render_webdeck.mjs --html ../../projects/{{project}}/webdeck/{{project}}_webdeck.html --out ../../projects/{{project}}/webdeck/stills --stills "{{times}}"
# Mux narration (A) and narration+music with ducking (B) onto the silent render
webdeck-mux project *args:
python3 tools/webdeck/mux_webdeck.py {{project}} {{args}}
# ─── Webdeck SHORT (9:16): purpose-built vertical short for X + YT Shorts ─
# Full short pipeline: voice → build → render (1080x1920) → mux → videos/{T}_short.mp4 (+ _short_music.mp4)
webdeck-short-pipeline project: (webdeck-short-vo project) (webdeck-short project) (webdeck-short-render project) (webdeck-short-mux project)
# Voice the 9:16 short script (T_short_script.json) into T_short_segment_* mp3s (--force to re-voice)
webdeck-short-vo project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/generate_voiceover_audio.py {{project}} --short {{args}}
# Build the vertical short HTML from T_short_script.json (+ --estimate for a pre-VO stills check)
webdeck-short project *args:
python3 tools/build_webdeck_short.py {{project}} {{args}}
# Render the short to silent.mp4 frame-by-frame at 1080x1920
webdeck-short-render project *args:
cd tools/webdeck && node render_webdeck.mjs --html ../../projects/{{project}}/webdeck/{{project}}_short.html --out ../../projects/{{project}}/webdeck/short_render --width 1080 --height 1920 {{args}}
# QA stills for the short (comma-separated seconds), e.g. just webdeck-short-stills NFLX "3,10,20"
webdeck-short-stills project times:
cd tools/webdeck && node render_webdeck.mjs --html ../../projects/{{project}}/webdeck/{{project}}_short.html --out ../../projects/{{project}}/webdeck/short_stills --width 1080 --height 1920 --stills "{{times}}"
# Mux narration + ducked music onto the short's silent render -> videos/{T}_short.mp4
webdeck-short-mux project *args:
python3 tools/webdeck/mux_webdeck.py {{project}} --short {{args}}
# ─── YouTube (Data API) ──────────────────────────────────────
# One-time YouTube OAuth (opens a browser; writes YT_REFRESH_TOKEN to .env)
yt-auth:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/upload_youtube.py auth
# Upload the final video to YouTube (private by default; --public to skip the gate)
yt-upload project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/upload_youtube.py upload {{project}} {{args}}
# Flip the uploaded video to public after the watch gate
yt-publish project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/upload_youtube.py publish {{project}} {{args}}
# Upload the 9:16 short as a YouTube Short (#Shorts, own sidecar; long-form link auto-fills if uploaded first)
yt-short project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/upload_youtube.py upload {{project}} --short {{args}}
# Flip the uploaded Short to public after the watch gate
yt-short-publish project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/upload_youtube.py publish {{project}} --short {{args}}
# Post the first comment (written-brief link) on an uploaded video; publish does this automatically (--short / --force / --text)
yt-comment project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/upload_youtube.py comment {{project}} {{args}}
# ─── X (API v2) ──────────────────────────────────────────────
# One-time X auth: verify the user token in .env (or mint one via the PIN flow - run as `! just x-auth`)
x-auth:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/post_x.py auth
# Generate the branded 5:2 X Article cover (local Chrome render - no OpenAI)
article-cover project *args:
python3 tools/gen_article_cover.py {{project}} {{args}}
# Create the brief as an X Article DRAFT (review in the X editor, then --publish)
x-article project *args: (article-cover project)
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/post_x.py article {{project}} {{args}}
# Send the single X post (native video + Article link from the sidecar; --dry-run first)
x-post project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/post_x.py post {{project}} {{args}}
# Post the 9:16 short natively to X (own copy + sidecar; --dry-run first)
x-short project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/post_x.py post {{project}} --short {{args}}
# ─── Analytics (reach/retention feedback loop) ───────────────
# Pull X + YouTube performance into projects/*/analytics.json (all tickers, or named ones)
analytics *tickers:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/pull_analytics.py {{tickers}}
# Channel/account-level reach (YouTube traffic sources + retention, X post impressions) - powers /insights
insights *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/pull_insights.py {{args}}
# ─── Media libraries ─────────────────────────────────────────
# Show the b-roll library + coverage across shoot-list categories
broll:
UV_ENV_FILE={{_env}} uv run python tools/list_broll.py
# Sync assets/broll/manifest.json with the clips present (run after dropping in new b-roll)
broll-sync:
UV_ENV_FILE={{_env}} uv run python tools/sync_broll.py
# Sync assets/music/manifest.json with the tracks present (run after dropping in music)
music-sync:
UV_ENV_FILE={{_env}} uv run python tools/sync_music.py
# Generate a music bed via the ElevenLabs Music API (preset name or literal prompt)
music prompt *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/generate_music.py "{{prompt}}" {{args}}
# 9:16 avatar short from the brief: HeyGen avatar (our voice) + gpt-image backdrop + word-synced captions.
# Default = hook short (videos/{T}_short.mp4, teases the long-form); --qa = two-avatar Q&A short
# (videos/{T}_short_qa.mp4, teases the podcast). Add --test for a free watermarked HeyGen render.
short project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/gen_avatar_short.py {{project}} {{args}}
# Generate BOTH shorts for a name: the hook short (-> long-form) and the Q&A short (-> podcast).
shorts project *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/gen_avatar_short.py {{project}} {{args}}
UV_ENV_FILE={{_env}} uv run python tools/gen_avatar_short.py {{project}} --qa {{args}}
# Assemble a per-platform publish pack (paste-ready copy + S3 media links)
postpack project:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/build_postpack.py {{project}}
# Publish a project's final deliverables to the public S3 artifact store
publish project:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/publish_artifacts.py {{project}}
# Rebuild the research catalog (content/index.json) the /research portal reads
reindex:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/reindex.py
# ─── Tier 1: brief-only coverage (volume → /research + SEO, no video) ─────────
# The brief admits a ticker to the catalog, so a /research page needs no render,
# no voiceover and no upload. Reserve the video treatment (webdeck-pipeline +
# YouTube/X) for the few names per batch whose finding is story-shaped.
# Validate a brief-only project (skips every script/deck/render check)
validate-brief project:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/validate_project.py {{project}} --brief-only
# Validate + publish a brief-only ticker straight to /research
publish-brief project: (validate-brief project)
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/publish_artifacts.py {{project}}
# ─── Blog Pipeline (markdown essays → S3 blog/ + blog/index.json) ─────────────
# Scaffold a new blog post: blog/<slug>/post.md from the template
blog-new slug:
@bash tools/new_blog.sh {{slug}}
# Narrate a post via ElevenLabs TTS → blog/<slug>/<slug>_narration.mp3 (optional)
blog-narrate slug *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/narrate_blog.py {{slug}} {{args}}
# Assemble a paste-ready distribution pack for a post (optional)
blog-social slug:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/build_blog_postpack.py {{slug}}
# Publish a post to S3 (blog/<slug>/) + refresh blog/index.json (auto-narrates; --no-audio to skip)
blog-publish slug *args:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/publish_blog.py {{slug}} {{args}}
# Generate the branded 5:2 X Article cover for a blog post
blog-article-cover slug *args:
python3 tools/gen_article_cover.py {{slug}} --blog {{args}}
# Create a blog post as an X Article DRAFT on @RoboFinSystems (review, then --publish).
# The X Article is the account's best format (~2x plain text) - this is what lets the
# concept/education lane use it instead of only ticker briefs.
blog-x-article slug *args: (blog-article-cover slug)
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/post_x.py article {{slug}} --blog {{args}}
# Rebuild the blog catalog (blog/index.json) the /blog routes read
blog-reindex:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/reindex_blog.py
# Capture YouTube URLs into the catalog via the channel RSS feed (run after uploading)
sync-youtube *tickers:
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/sync_youtube.py {{tickers}}
# ─── Infrastructure (CloudFormation, deployed locally via CLI — no GHA) ───────
# Validate the content infra template (S3 bucket + CloudFront CDN)
infra-validate:
@just ensure-env
@bash tools/deploy_infra.sh validate
# Create/update the content infra stack (creates the new bucket; reads .env)
infra-deploy:
@just ensure-env
@bash tools/deploy_infra.sh deploy
# Print the content stack outputs (bucket, CDN url, distribution id)
infra-outputs:
@just ensure-env
@bash tools/deploy_infra.sh outputs
# One-time: copy existing published content from the legacy bucket into the new one
content-migrate from="robosystems-marketing-assets":
@just ensure-env
@bash tools/migrate_content.sh {{from}}
# ─── Content design system (design-system/ ↔ Claude Design project 746ae7a4) ──
# Rebuild design-system/_ds_bundle.js from the component sources. Run after editing
# components/*.jsx, then push the bundle (+ any changed files) back to Claude Design via
# DesignSync / the /design-sync skill. Tokens, CSS, and templates push as-is — no build.
design-build:
cd design-system && npm install --no-audit --no-fund --silent && npm run build
# ─── Renderer (renderer/ — Playwright: UI capture + 9:16 motion) ──────────────
# Install the renderer's Node deps + the Playwright Chromium browser (run once).
render-setup:
cd renderer && npm install --no-audit --no-fund --silent && npx playwright install chromium
# Capture the live RoboLedger UI (headless login → demo screens → dark-theme stills).
# Needs the UI running (default localhost:3001) + creds. company = showcase slug (e.g. coffee_roaster);
# entity = UI name prefix (e.g. Driftline). Stills → showcase/<company>/captures/ (gitignored product).
# e.g. just render-capture <config> coffee_roaster Driftline
render-capture config company entity="" scenes="home,transactions,close,statements,reports":
node renderer/src/cli.mjs capture --config {{config}} --scenes {{scenes}} --out showcase/{{company}}/captures {{ if entity != "" { "--entity '" + entity + "'" } else { "" } }}
# Render a scene spec (a per-episode product in showcase/<company>/, gitignored) to a silent mp4.
# Mux VO/music downstream in the Python short path. e.g. just render-short showcase/coffee_roaster/driftline.demo.json
render-short spec:
node renderer/src/cli.mjs short --spec {{spec}}
# ─── Product demos (live UI walkthrough: real cursor, real clicks, component zoom) ───
# List the anchors a walkthrough can aim at, read off the RUNNING app rather than
# guessed from source. "fit" is the largest zoom that still frames the element.
# e.g. just demo-probe <config> Driftline "/ledger/close,/reports,/plan"
demo-probe config entity="" routes="/home,/ledger/close,/ledger/statements,/reports,/plan":
node renderer/src/cli.mjs probe --config {{config}} --routes "{{routes}}" {{ if entity != "" { "--entity '" + entity + "'" } else { "" } }}
# 1. Voiceover + timing. Writes durationMs into the spec so narration owns the clock.
demo-narrate spec *args="":
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/demo_narrate.py {{spec}} {{args}}
# 2. Record the walkthrough against the live UI -> silent mp4 in showcase/<company>/renders/.
demo-render spec config *args="":
node renderer/src/cli.mjs demo --spec {{spec}} --config {{config}} {{args}}
# 3. Mux the per-beat VO (+ ducked music bed) onto the silent render.
demo-mux spec *args="":
@just ensure-env
UV_ENV_FILE={{_env}} uv run python tools/demo_mux.py {{spec}} {{args}}
# The whole demo pipeline. Needs the RoboLedger UI running (default localhost:3001).
# e.g. just demo-pipeline showcase/coffee_roaster/driftline.walkthrough.json ~/Projects/robosystems/.local/config.json
demo-pipeline spec config: (demo-narrate spec) (demo-render spec config) (demo-mux spec)
# Single-frame fit check before committing to a full render (~10s vs minutes).
# Shoots the first frame of each beat so a bad zoom target is caught early.
demo-stills spec config:
node renderer/src/cli.mjs demo --spec {{spec}} --config {{config}} --stills
# ─── Utilities ────────────────────────────────────────────────
# Play the final video
play project:
open projects/{{project}}/videos/*_final.mp4
# Get media durations via ffprobe
durations project:
./tools/durations.sh {{project}}
# Clean generated assets (keep sources/scripts/deck, remove videos + sliced PNGs)
clean project:
rm -rf projects/{{project}}/videos projects/{{project}}/charts/png
echo "Cleaned generated assets for {{project}}"