Communitygithub.com

ai-video-pipeline

Use when automating AI video gen (UGC/TikTok affiliate).

What is ai-video-pipeline?

ai-video-pipeline is a Claude Code agent skill that use when automating AI video gen (UGC/TikTok affiliate).

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/koyawel0694/Ultra-Prompting/tree/main/skills/media/ai-video-pipeline

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

AI Video Pipeline (UGC / TikTok affiliate)

John's production line: product screenshots → storyboard prompt (First Frame UGC Hub strategies) → Google Flow generation → TikTok Studio web upload (paced, 10-min interval) → Telegram status pings → John attaches Shop product in-app. ANTI-REPETITION + CINEMATIC FIX (Aug 6 2026): master prompt v2.2 WIRED INTO THE PIPELINE. Every script generation now MUST: (1) read /home/john/google-flow-automation/master_prompt_v2.2.txt (sections A-J), (2) run python3 /home/john/google-flow-automation/script_rotation.py pick and pick ONLY from allowed_strategies/allowed_hooks/allowed_ctas/allowed_personas/allowed_cameras/allowed_lighting, (3) write the extra prompt.json fields strategy/hook_type/persona/cta_line/camera_movement/lighting, (4) run script_rotation.py record --strategy N --hook ... --cta ... --persona ... --camera ... --lighting ... --starters ... afterwards. Rotation state persists in rotation_state.json (last 10 scripts: no strategy/hook/CTA/persona/camera/lighting back-to-back, no sentence starter from last-10 list, 'yellow basket' CTA max 1 in 3 videos). NEW IN v2.2 (John's request Aug 6): every videoPrompt MUST name exactly ONE camera movement (bank I.1: slow push-in, gentle pull-back, lateral dolly, handheld drift, tilt-down reveal, low-angle push, orbit, rack focus, static micro-movement, POV walk-in — one continuous 10s move, never a static shot) + ONE lighting/atmosphere (bank I.2: golden hour, soft window daylight, cozy evening, moody cinematic, clean studio, morning fresh, warm cafe bokeh, neon night) + FACELESS framing rules (I.3: never show a face — hands/POV/over-shoulder from behind/product-only; voice-over, not an on-screen speaker) + VOICE TIMING spec (H: ≤20 words = ~7-8s speech, starts in second 1, micro-pause between beats, last CTA word by ~second 8, end card holds seconds 8-10). Master prompt copy at /home/john/google-flow-automation/master_prompt_v2.2.txt (Downloads/ai_studio_master_prompt.txt is the synced source of truth — cp after every edit). Status as of Aug 6 2026: SHADOW RUN COMPLETE — PIPELINE VERIFIED END-TO-END. TikTok Studio upload flow MAPPED same evening (upload + AI toggle + upload-time product attach; click path in references/tiktok-studio-upload.md). Route B via CDP (flow_driver.py, Chrome 151 native, GOOGLE FLOW profile): upload → attach → compliant prompt → Omni Flash 10s 9:16 x1 generation → 1080p Upscaled download ALL WORKED. Early generation attempts failed transiently (generic message, no credits charged) but the retry succeeded — see Pitfalls.

Route A — SnapGen API generation (VERIFIED Aug 11 2026 — NEW PRIMARY)

snapgen_client.py is now the FIRST generation choice (AUTO_PROMPT patched Aug 11): pure-HTTP omni-flash generation via api.snapgen.ai — no Chrome/CDP/X11, no Flow credits, no freeze/dialog/double-gen failure class. Usage: timeout 400 ~/.venvs/tiktok/bin/python /home/john/google-flow-automation/snapgen_client.py <job-dir> [--no-upscale] [--max-retries 3] — reads prompt.json videoPrompt + product_image*.png, submits model=omni-flash 10s 9:16 1080p (frame mode ≤2 imgs, ingredient 3), polls history ~2m15s, downloads signed mp4, ffmpeg-upscales to 1080x1920 (API returns NATIVE 720x1280 — NO upscale endpoint exists; --no-upscale keeps 720p), writes generated_video.json. Exit 0 done / 1 fatal / 2 transient. 30 credits per video; SNAPGEN_API_KEY in project .env (John's account PP0001). RESUME-AFTER-CRASH (Aug 12 — both clients): the uuid is persisted to <job-dir>/.snapgen_uuid (omni) / <seg-dir>/<out>.uuid (veo) IMMEDIATELY after submit; on re-run the client polls that uuid from history BEFORE submitting fresh (the server KEEPS GENERATING after the client dies — VERIFIED Aug 11: 3 orphaned Veo submissions completed and were recovered; re-submitting would double-spend credits). Resume marker removed on success; veo resume is keyed on the missing <out>.json record (not the mp4 — an untrimmed partial mp4 must NOT count as done). Fallback chain: snapgen → flow_batch.py → manual CDP. Voiceover improvises its own Taglish wording from the prompt direction (same beats, not verbatim script). Image gen nano-banana-pro is FREE via API (5rq/min) for still-image client work — NOT scripted (John ruled Aug 11: Nano Banana stays BROWSER-only). Full details + verification: references/snapgen-api.md + /home/john/google-flow-automation/SNAPGEN_API_ANALYSIS.md.

/generate interactive flow + Veo 3.1 fast path (VERIFIED Aug 11 2026)

Veo 3.1 fast via SnapGen API VERIFIED (Aug 11): 4 credits, 8.0s, 9:16 ACCEPTED (docs said 16:9-only — API is permissive), frame-to-video with ref image, GENERATES THE TAGLISH VOICEOVER following the script (transcribed verbatim: "Magaan, komportable, pang araw-araw. Hanapin niyo sa TikTok Shop." — unlike omni-flash it does NOT improvise much), native 720x1280 → ffmpeg 1080p, has_watermark=0, ~2m15s. Bot command /generate (telegram_bot.py, ConversationHandler + inline keyboards, live Aug 11): /generate → [TikTok Affiliate | Regular video*] (stub) → [Veo 3.1 fast | Omni Flash] → scripting style (CCC/Budol/POV/IRR/TV Commercial) → count 1-4 → writes job_orders/generate_.json (product_folder = LATEST auto- folder with product_image.png + product_page.png; if none, bot says send photos first) → spawns a FRESH agent with GENERATE_PROMPT (--max-turns 400, log job_orders/run_generate_.log) → agent: OCR facts → splits the script into N segments (Veo ≤16 words/seg 8s, Omni ≤20/seg 10s; TV-ad camera choreography with ONE move per segment + continuity across segments) → Veo path: flow_frame_gen.py first frame → N × snapgen_veo_client.py → stitch if >1 → sends final mp4 via Telegram sendVideo → step (9): sends 'Upload to TikTok Studio?' inline buttons (genup:yes: / genup:no:) → the BOT's gen_upload_choice handler resolves the basename under ~/tiktok-jobs/ and on Yes spawns an UPLOAD-ONLY agent (UPLOAD_PROMPT — final.mp4+prompt.json already exist, upload/schedule ONLY: next free 6AM/6PM slot → tiktok_studio_upload.py → verify Posts page → pings); on No the video stays local. Omni path: N × flow_batch.py (ingredients) → stitch → send → same step (9). HARD RULE baked into GENERATE_PROMPT (Aug 11): the /generate agent NEVER touches TikTok Studio — no tiktok_studio_upload.py, no Studio pages; step (9) buttons are the ONLY route to an upload, and the bot (not the agent) runs it. Frame-gen failure fallback (Aug 11): retry once, then REUSE the previous good frame (frame_.png) for the failed segment; if ALL frames fail, run Veo segments with no --ref or fall back to Omni — never block the order on one frame, never improvise a Studio upload. /generate TARGETS THE LATEST STAGED FOLDER — stale-product trap (Aug 11): latest_product_folder() picks the newest auto- folder that ALREADY has product_image.png + product_page.png. If John sends NEW photos for a DIFFERENT product and runs /generate in the same minute, the job may target the PREVIOUS product's folder (real: 06:17 /generate grabbed auto-20260811_054254/Smasher while his new photos sat unstaged in Product Folder — his intake order then queued behind a generate job on the wrong product, and he asked "why is this?"). The "⏳ yours is queued" reply is CORRECT concurrency-guard behavior, not a bug — but when a /generate order is queued, confirm which auto-* folder it points at BEFORE letting it run; if it's the wrong product, kill the agent + delete its generate_.json so the queued intake (staging the new folder) becomes the active job.

DETACHED-RUN PATTERN (Aug 11 UPDATE — setsid is now BLOCKED): the terminal tool refuses setsid bash -c '...' & in foreground ("shell-level background wrappers"). Use systemd-run instead — same detach semantics, fully outside the tool's process group: systemd-run --user --unit=<name> --collect bash -c '<cmd> > <log> 2>&1; echo $? > <exitfile>', then poll cat <exitfile> every ~30s. Verify launch with systemctl --user is-active <name> + tail the log. Units self-clean with --collect. VERIFIED Aug 11 (seg_2 gen + stitch both run this way; resume-after-crash worked cleanly). RESUME-AFTER-CRASH rule (Aug 11, real): when a pipeline agent dies mid-order, the job folder holds verified state — CHECK IT before regenerating anything. seg_1.mp4 + seg_1.mp4.json (ffprobe-verified) + seg_1.json/seg_2.json (script already written AND rotation already recorded) + first_frame.png all survive the crash. Reuse them; generate only the missing segment. Do NOT re-run script_rotation.py record (double-count corrupts the anti-repetition state) and do NOT rewrite seg JSONs (seg_1.mp4 was made from that exact prompt). NEW SCRIPTS (all VERIFIED exit 0 Aug 11):

  • flow_frame_gen.py <job-dir> --prompt "<9:16 image prompt>" [--out first_frame.png] — Nano Banana 2 first frame via Flow CDP (BROWSER ONLY — John's rule, never the image API): new project → library upload → attach_smart (only clicks the plus if the library is NOT already open — the plus TOGGLES and a second click closes it, silently killing Add to Prompt; VERIFIED fix) → settings popup via settings_chip (prompt-bar chip at y~714 bottom, text "Video · 10s crop_9_16 x1"/"🍌 Nano Banana 2 …" — find_btn's first-match hits the TOP-BAR gear, use the y>550 + smallest-button probe) → role=tab 'Image' (tab TEXT IS GLYPH-PREFIXED: "imageImage"/"videocamVideo" — match with includes('Image') NOT ==) → aspect crop_9_16 tab → Agent pill grey check → type + focus+Enter submit (arrow dead for images) → wait for image card → download_frame via the cookie+curl collector (dump_cdp_cookies.py + flow_collect_media.py; pick the 9:16 image; grid render ~768x1376 — fine as a Veo ref; NO viewer clicking — the viewer's Download menu is a landmine: only "1K Original size" shows, and a mistimed click hits the adjacent TRASH icon ("1 Item moved to trash" — restore from the library Trash view → Restore All). COLLECTOR FIX Aug 12: the character sheet (1536x2752) is ALSO 9:16, so the old aspect-only picker grabbed the SHEET instead of the generated frame (frame_1 + frame_3 of the 08:27 run — agent had to manual-fix). download_frame now EXCLUDES the character sheet's exact dimensions and prefers (768,1376) outright. Fresh projects default to IMAGE mode already (Nano Banana 2 crop_9_16).
  • snapgen_veo_client.py <job-dir> --prompt "<seg>" --ref <frame.png> --out seg_<i>.mp4 [--model veo-3.1-fast] [--duration 8] [--aspect 9:16] [--no-upscale] — one segment via the API (reuses snapgen_client's verified functions; writes job-dir/segments/). Exit 0/1/2.
  • stitch_videos.py <out.mp4> <in...> or <job-dir> --out final.mp4 (globs segments/*.mp4 sorted; PATCHED Aug 12: *.raw.mp4 natives are now EXCLUDED from the glob — the old 'move the raws out first' rule is now a code guard, no more mid-video 720p inserts) — uniform re-encode (1080x1920 24fps crf18 aac44.1 pad) + concat with RE-ENCODE (not -c copy: -c copy writes a wrong 48/1 r_frame_rate header when input timebases differ, e.g. after trim_dead_air's select/setpts; re-encode gives a clean 24/1 — patched Aug 11). VERIFIED: 10s+8s → 18.0s h264 1080x1920. EDIT PASS + CAPTIONS (Aug 15): --edit auto is now the DEFAULT — prompt-aware per-segment camera moves (segments whose prompt names camera motion get NO extra move; others get gentle push-in/out by position), light grade (contrast 1.03/sat 1.05), and 0.25s crossfades between segments (video xfade + audio acrossfade, c1=tri:c2=tri — this ffmpeg rejects 'fade'). --caption (Regular/Normal mode ONLY — affiliate NEVER) transcribes the FINAL stitched video with Groq (free, ~/video-tools/groq_transcribe.py adapter → deepcaption build_captions.py → ~/video-tools/burn_ass.py native-res burn, style minimal default). Missing key/tools → captions skipped with warning, stitch never fails. All modes verified Aug 15 on real vox_bf2 segments (22.97s crossfaded, captioned frame vision-confirmed). See skill groq-video-captions for the full free-caption playbook.
  • trim_dead_air.py <in.mp4> [--out ...] — DEAD-AIR TRIM (John's rule Aug 11: "cut the parts where no one is talking"): Veo/Omni clips carry long silent tails (VERIFIED: seg_1 speech 0-3.26s of 8s = 4.7s dead; 43% of the first stitched test video was room tone). Detects speech with faster-whisper word timestamps (ground truth — energy thresholds FAIL here because the ambient tail sits above any fixed RMS cutoff), keeps [first_word−0.25s, last_word+0.35s], rebuilds via ffmpeg select/aselect single pass. Run with ~/.venvs/crypto/bin/python (only venv with faster-whisper). Exit 0 trimmed / 2 nothing to trim / 3 error (original kept — NON-FATAL everywhere). AUTO-WIRED into snapgen_client.py, snapgen_veo_client.py (--no-trim to disable), and flow_batch.py (after download) — every generated segment is trimmed at the source; the /generate agent must NOT regenerate segments to "restore" the nominal 8s/10s length (VERIFIED: seg_1 8s→3.6s is correct). Trim re-encodes at crf 18 so the frame rate/header stay clean. *PITFALL (mostly fixed Aug 12): snapgen_veo_client leaves <out>.raw.mp4 (native 720p) in segments/ — stitch_videos.py now EXCLUDES .raw.mp4 from its glob, so a mid-video 720p insert can't happen anymore; still good hygiene to move raws to job-dir/raw_720p/. Also: --out final.mp4 resolves against the SCRIPT's CWD, not the job dir — pass an absolute path or cd into the job dir first (a relative --out with cwd=google-flow-automation silently writes final.mp4 there).
  • GENERATE_PROMPT lives in telegram_bot.py (single source of truth; .format() placeholders: config/mode/caption/model/style/count/product_folder/chat_id/duration — NO unescaped braces in the template, literal JSON braces crash format). Restart the bot after editing. Aug 15: /generate wizard now supports BOTH modes (regular no longer dead-ends) — regular mode asks "Add word-synced captions?" (gen:cap:yes/no) and threads mode+caption through cfg → spawn → stitch command (--edit auto always; --caption --caption-style minimal when mode=regular and caption=True).
  • SPAWN PATH = Popen start_new_session (Aug 11, 3 crashes root-caused): generate agents MUST be spawned bot-style (subprocess.Popen([HERMES_BIN,"chat","--max-turns","400","-q",prompt], start_new_session=True, stdout=logfile, stderr=logfile)) — VERIFIED Aug 11: a test agent spawned via the gateway's terminal background tool got SIGTERM'd ("Interrupted during API call", _signal_handler_q KeyboardInterrupt) 3× at 4-7 min in, even with detached commands; the SAME agent bot-style survived to completion. The terminal tool kills process groups on timeout; the gateway session's process tree is a kill zone for long agents. Production spawn (bot) was never affected — only my test harness.
  • RESUME-AFTER-CRASH (VERIFIED Aug 11, agent 4 did it cleanly): if a generate agent dies mid-job, a re-run should VERIFY artifacts (first_frame.png, seg_.mp4 + seg_.exit, final.mp4) and reuse them instead of regenerating — only fill the missing pieces (seg_2 was the only gap in the crashed run). Rotation must NOT be re-recorded (would corrupt anti-repetition state). Stitch globs ALL *.mp4 in segments/ — move .raw.mp4 natives out before stitching or they get inserted mid-video (agent's catch, Aug 11).
  • CONCURRENT-JOB GAP (FIXED Aug 11 evening — was the 'Mall Smasher mixup' class): gen_count now calls agent_running() BEFORE spawning; if busy, the generate config is pushed to job_orders/.genqueue and drain_queue spawns it when idle (drain_queue now drains BOTH .queue and .genqueue, oldest-first). agent_running() was ALSO broadened to match all THREE pipeline markers (intake A new TikTok product-video job order arrived / generate A TikTok Affiliate video generation order arrived / upload A TikTok Studio upload order arrived) — previously it only matched intake agents, so a /generate job and an intake job could run SIMULTANEOUSLY (both driving Chrome/CDP + both pinging John — the Aug 11 charjan+Smasher double-run). Upload-only agents (from the genup:yes buttons, below) are guarded the same way.
  • BRIDGE FALSE-POSITIVE on the pipeline marker: the bridge chat assistant's prompt (BRIDGE_PROMPT in telegram_bot.py) QUOTES the literal string 'TikTok product-video job order' — so pgrep -f "hermes chat" + cmdline-contains-marker flags BRIDGE agents as pipeline jobs (bit us Aug 11: a bridge agent was misidentified as a running pipeline agent). Distinguish pipeline agents by the order/generate config filename in the cmdline, 'Read the job order JSON', or the --max-turns 400 flag. Never kill the bridge agent to "clear" a job — it is how John chats with the bot.
  • CANCEL / CLEAN-SLATE for BOTH flows (exercised live Aug 11; NOW A BOT COMMAND Aug 12): John can do it himself from Telegram: /clear (asks Yes/No first; /cancelall is an alias, added Aug 12). ARTIFACT WIPE (Aug 12, John's call): /clear now ALSO deletes working artifacts (segments/, final.mp4, .snapgen_uuid) in ALL ~/tiktok-jobs/auto-* folders — the 09:03 clear "didn't work" because segments survived and the next /generate REUSED them (0 credits, delivered the old job's video in 2 min). Post-clear /generate now starts truly fresh. Product images + character.png stay; already-delivered videos keep their Telegram copy. It empties BOTH queues FIRST (drain_queue must find nothing to spawn — the Aug 11 orphan-respawn gotcha), then kills pipeline agents by precise cmdline match (job_orders/order_ / job_orders/generate_ / 'A TikTok Affiliate video generation order' / 'A TikTok Studio upload order' / 'Read the job folder') — the BRIDGE agent is explicitly EXCLUDED ('You are Hermes, John's AI assistant' in cmdline → skip: it's how John chats, not a job), SIGTERM then SIGKILL after 2s — then deletes ALL order_/generate_.json configs. Working artifacts ARE wiped too (see above). Manual fallback (when the bot itself is down): (1) find agent PIDs with pgrep -af "hermes chat" and INSPECT cmdlines — marker-pkill can MISS (the intake agent's cmdline did not match an order-filename pattern; kill by PID after ps inspection and verify with ps -p <pid>); (2) empty job_orders/.queue; (3) delete BOTH order_.json AND generate_.json plus their staging folders (auto-, including half-written frame_*.run.log/.exit). A killed agent does NOT refund spent credits — the killed Mall Smasher intake had already generated a 30-credit SnapGen video before the kill (folder deleted anyway per John). Kill EARLY if the product is wrong; expect sunk credits on late kills.
  • REBOOT-SEQUENCE GOTCHA (Aug 11 — orphan respawn, real incident): killing the running generate agent frees agent_running() — if the OLD bot instance is still up (pre-restart code), its drain_queue tick (every 30s) can spawn a NEW intake agent for a queue entry you are about to delete (old bot drained order_20260811_061348 seconds before it was deleted; the orphan 'hermes chat' intake agent survived the bot restart via start_new_session and needed kill -9). CORRECT ORDER for "restart the bot and clear pending jobs": (1) pgrep -af "hermes chat" + INSPECT cmdlines, kill -9 <pid> each pipeline agent (SIGTERM can be ignored — 71476 stayed alive; escalate to -9 after ~2s); (2) RE-CHECK pgrep — if the old bot already respawned one, kill it too before touching queues; (3) > job_orders/.queue + rm -f job_orders/.genqueue; (4) delete TODAY's order_.json + generate_.json (old Aug 5-8 orders are inert — they only run if re-queued; leave them); (5) systemctl --user restart tiktok-bot; (6) verify via the bot's own agent_running() + both queue lists empty. NOTE: an agent's in-flight curl sendVideo dies with its process group — a video John never received may sit in the job folder; offer to re-send rather than assuming delivery.
  • getUpdates 409 CONFLICT LOOP — RESOLVED Aug 12 (final: dedicated bot): the tiktok-bot and the LOCAL hermes-gateway.service were configured with the SAME Telegram token (google-flow-automation/.env TELEGRAM_TOKEN == ~/.hermes/.env TELEGRAM_BOT_TOKEN) → two pollers, recurring 'Conflict: terminated by other getUpdates request'. FINAL FIX (Aug 12): John created a DEDICATED gateway bot via BotFather (@HermesPHbbot, token 8935297182:...) and I swapped it into ~/.hermes/.env TELEGRAM_BOT_TOKEN (backup: ~/.hermes/.env.bak_before_tg_swap) + restarted hermes-gateway. Now: gateway polls @HermesPHbbot, pipeline bot polls @Tiktokvids0694bot — ZERO conflicts, both clean. NOTE: a temporary disable (removing the token) was tried first — John's Hermes-TG channel IS the local gateway (not just the VPS Hermie), so disabling cut him off; the dedicated bot is the correct permanent fix. Gateway's TELEGRAM_ALLOWED_USERS / TELEGRAM_HOME_CHANNEL in ~/.hermes/.env were untouched by the swap.

DEFAULT CHARACTER — JOHN'S AI CHARACTER IN EVERY GENERATION (Aug 12): canonical sheet at /home/john/google-flow-automation/character.png (copy of auto-20260811_charjan/character.png, Jan's 6-panel sheet, 1536x2752). Wiring: (1) bot's gen_count copies the default into the /generate target folder if character.png is missing (per-job character.png always wins); (2) AUTO_PROMPT step 2 tells intake agents to copy it into the staged auto- folder; (3) GENERATE_PROMPT step 2 has the same copy-fallback for queued/resumed runs; (4) snapgen_client.py product_images() now PREPENDS character.png to the ref list (Route A gap closed Aug 12 — flow_batch/flow_frame_gen already attached it; snapgen did NOT, so intake videos would have ignored the character). Result: every video is now a character video (overrides faceless rule I.3); voice must match the character's gender (Jan = male → MALE). To swap the character later: replace google-flow-automation/character.png (or drop a different character.png in a specific job folder — per-job wins). CHARACTER / PERSONA SUPPORT (VERIFIED Aug 11 — John's "Jan" test): drop a presenter reference sheet in the job folder as character.png and the pipeline treats it as a character (like AI Studio's Characters feature): flow_frame_gen.py + flow_batch.py AUTO-ATTACH character.png alongside the product images, and the GENERATE_PROMPT tells the agent to vision-analyze the sheet, build a CHARACTER BLOCK (name + visual description: age/ethnicity/hair/clothing/style), and include name+description in EVERY segment + frame prompt for cross-segment consistency. Character videos OVERRIDE the faceless rule (I.3) — the character is the on-screen presenter; keep no-text/no-watermark. VERIFIED: Jan (25, Filipino-Chinese, blonde-highlighted curtain hair, cream hoodie) rendered consistently across 3 segments from a 6-panel reference sheet. PER-SEGMENT FIRST FRAMES (John's design, Aug 11): each Veo segment gets its OWN first frame (frame_.png via flow_frame_gen with the segment's scene prompt) — segment 1 hook scene, segment 2 detail scene, segment 3 CTA/end-card scene (yellow basket) — then the video animates from its matching frame. Better than one shared frame; frames are free (Nano Banana 2). The seg_.json now carries a frame_prompt field per segment. stitch_videos.py --out resolves RELATIVE to the job dir (fixed Aug 11 — used to land in CWD).

VOICE — STRONG REQUIREMENT (John's rule Aug 11): every videoPrompt MUST state the voice gender EXPLICITLY — MALE or FEMALE, never ambiguous (character videos: the voice gender MUST exactly match the character's gender from the character block). Delivery: the speaker is a NATURAL-BORN native Taglish speaker with PERFECT INTONATION — native rhythm, stress, prosody; zero foreign/AI accent; never robotic/announcer/newscaster. Baked into master_prompt_v2.2.txt (ROLE + section H.3), GENERATE_PROMPT step 2, and AUTO_PROMPT step 3. LANGUAGE — STRONG REQUIREMENT (John, Aug 12 — after the 08:46 Veo video's voiceover sounded awkward/slangy in English): master prompt H.6 added — the voiceover is TAGLISH, Tagalog-FIRST with light natural code-switching, spoken with a NATURAL FILIPINO ACCENT; English words ONLY where natural (product terms, 'check it out', 'TikTok Shop') and pronounced the FILIPINO way; ABSOLUTELY FORBIDDEN: English slang ('gonna/wanna/gotta/ain't/dude/bro/yo/lit/no cap'), full English sentences, forced American accent, English filler words ('like', 'um'); if an English word would sound awkward, SAY IT IN TAGALOG instead. Same statement wired into GENERATE_PROMPT step 2 and AUTO_PROMPT's videoPrompt spec. Sync: master_prompt_v2.2.txt → ~/Downloads/ai_studio_master_prompt.txt (done Aug 12). PENDING (tomorrow): generate a test video with the strengthened voice prompting to verify Veo 3.1 fast honors 'MALE voice, perfect intonation' as strongly as the visuals honor the character.

Default strategy = CCC (John's rule Aug 8 2026)

The house style is CCC (strategy 4, CONTEXT-CLARITY-CURIOSITY) — it follows TikTok affiliate guidelines/policy (factual context → clear value → curiosity-driven soft CTA; no hype/prices/urgency). Implemented in script_rotation.py pick():

  • CCC is ordered FIRST in allowed_strategies whenever it is not forbidden (i.e. whenever strategy 4 was not used in the last video).
  • The brief's rules explicitly state: "DEFAULT STRATEGY = CCC ... use it whenever it appears in allowed_strategies; only deviate if 4 is forbidden because it was used in the last video."
  • record still logs the actual strategy used, so the anti-repetition rule (no same strategy back-to-back) still works — CCC will rotate out for ONE video after a CCC video, then return as default.
  • Pipeline agents: pick CCC unless the brief's forbidden.strategies contains 4.

Phase-2 script hardening (Aug 8 2026 — all 3 patched, verified compile, NOT yet exercised in a live job)

  1. script_rotation.py: CCC default (above).
  2. flow_batch.py attach_cards(): VERIFIED fix — the library file list is a LIST of 40×40 thumb rows (x≈456, y≈114/164/214/264); the old code targeted >150px cards (those open Nano Banana Pro editor / are not selectable). Now: shift-click ALL rows (mods=8), THEN click "Add to Prompt" at its CENTER once (was: clicking Add-to-Prompt after the FIRST card and returning early — never attached all images).
  3. tiktok_studio_upload.py:
    • +Add finder prefers the REAL 280px-wide Add button (the 742px wrapper swallows CDP mclicks → "Link type modal did not open"); if the mclick still fails, auto-fallback JS-clicks the 280px button (b.click() on live element works).
    • Escape pressed before clicking Next (an open TUXSelect dropdown covers Next — verified Aug 8).
    • caption builder guarantees a space before the first '#' (fixes the "Sho#Bostanten" merge).
    • CAPTION-MANGLING ROOT CAUSE FIXED Aug 12 (real incident, Smasher Struer re-run): the old fill_caption used chunked Input.insertText — Draft.js overrides the DOM selection with its internal EditorState (caret at end), so the insert APPENDS instead of replacing → the stored caption ended up duplicated + hashtags reordered before the description (post's own JSON-LD started mid-hashtag: "sPH #MensShoes #TikTokShopPHDati mabigat..." — deleted + re-scheduled). FIX (now in the script): ONE Runtime.evaluate — focus + selectNodeContents + document.execCommand('insertText', false, <caption>) — then VERIFY textContent == caption EXACTLY (retry once, raise "caption did not register exactly" on mismatch). load_prompt ALSO dedupes now: re.sub(r"(?:\s+#\S+)+$", "", desc) strips trailing hashtags from the description before appending the hashtag array (descriptions often already end with the 5 hashtags). VERIFIED Aug 12: "caption typed (214 chars, expected 214; match=True)" + post JSON-LD starts with the description. Also verified: the ROW's <a> textContent shows caption + separately-rendered hashtag CHIPS concatenated (10-15 hashtags for a 5-hashtag caption, no space after the last in-caption tag) — NEVER judge caption correctness from the row text; trust the script's exact-match log or the JSON-LD START-order.
    • UPLOAD-WAIT HARDENED (Aug 12): the old wait_for on innerText 'Uploaded' died BLIND on Aug 11 ("timeout waiting for video upload done (120s); last=None" — the upload had actually stalled at the native-dialog step). Now: 240s timeout, success = 'Uploaded'/'Replace'/'Processing' text OR a <video> element with a source; on timeout it dumps the first 500 chars of body text so the manual fallback knows where it stuck. LIVE-VERIFICATION NOTE (Aug 8 evening, Hiking Shoes run): flow_batch.py WAS exercised in a live job — uploads landed but attach found only "1 list rows" → FATAL "Add to Prompt not found" → manual CDP fallback used (worked). tiktok_studio_upload.py got further than ever (upload → caption → +Add fallback JS-click → BUY HERE attached — the product WAS in the showcase — AI ON, no location) but FATAL'd at "date input not found" (schedule step; the script's date-input selector failed). Recovery used: continue the live editor per the recovery map — Schedule radio → calendar day cell (click CENTER of the 32x32 span, corner misses) → time wheels (scrollTop + dispatchEvent(new Event('scroll',{bubbles:true})) is REQUIRED — without the scroll event the minute commit silently fails and the input keeps the old minute; commit order minute-center then hour-center, then Escape) → final Schedule button (text exactly "Schedule", width>100) → redirect to /tiktokstudio/content = committed. So BOTH Phase-2 scripts still need their manual continuation paths ready. LIVE-VERIFICATION NOTE 2 (Aug 8 ~23:45, CESIVEAR run): (1) flow_batch.py attach flaked again the SAME way ("found 1 list rows; shift-clicking ALL → FATAL: Add to Prompt not found") — manual CDP fallback worked (single image: mclick 476 134 left shift on the 40x40 row at (456,114), then Add to Prompt at its CENTER (890,600) — attached 1 chip instantly). (2) tiktok_studio_upload.py exited code 3 "product not in showcase" — BUT the product WAS in the showcase (search found 【CESIVEAR】... id 1732712054835348723 Active). The exit-3 verdict is a FALSE NEGATIVE — always continue the live editor session and search the product manually before telling John to add it to the showcase. The editor session persisted (upload + caption committed server-side); manual continuation: +Add JS-click → Next (1188,667 for the wide modal, NOT 896,460) → radio (183,359) → Next → name-confirm input #10 → native-setter "BUY HERE" → Add (896,483) → chip verified. Also VERIFIED: the script's caption builder can still mangle the description ("camel canv#casualshoes ... #tiktokshopMahaba ang lakad..." — # merged into the last word + hashtags duplicated) — always re-verify the Draft editor text after the script's type step and one-shot execCommand-replace it (focus + selectNodeContents + execCommand('insertText')).

Session-boundary rule (Aug 8 2026 — HARD RULE, John's request after 2 stalled runs)

NEVER run a full job order inline in the long-lived gateway session. Both Aug 8 runs (Bostanten, Smasher) hit the per-session tool-iteration cap mid-pipeline (the 1080p download step). The gateway/Telegram session has a finite iteration budget; a whole job (OCR→prompt→Flow→download→Studio→schedule) blows through it.

FIX — spawn a FRESH agent per job order, exactly like telegram_bot.py's spawn_agent() does:

cd /home/john/google-flow-automation && ~/.venvs/tiktok/bin/python spawn_pipeline_agent.py <job-folder> --product "<name>"
  • spawn_pipeline_agent.py (BUILT Aug 8): writes job_orders/order_<ts>.json (same schema the bot uses), extracts the LIVE AUTO_PROMPT from telegram_bot.py (single source of truth — stays in sync), and spawns hermes chat --max-turns 400 -q "<prompt>" with start_new_session=True, log to job_orders/run_<ts>.log.
  • The spawned agent has its OWN session + 400-turn budget — the gateway session is untouched, and the agent pings John at each stage (classified → generated → scheduled) via sendMessage.
  • PROVEN LIVE (Aug 8, HYGGE loafers = first spawner-run job): complete end-to-end in 23m26s — 207 messages (205 tool calls) against the 400 budget, zero iteration-cap stalls, all 3 pings sent, scheduled + verified on Posts page. Tool mix was 92 terminal / 4 vision — the fresh agent naturally prefers DOM queries over screenshot+vision round-trips. This is the pattern to trust; do not go back to inline execution.
  • The gateway session's role is limited to: intake (receive images, stage job folder with convention names) → spawn → monitor the log (tail -f job_orders/run_<ts>.log) → report to John. If a step needs a manual nudge (e.g. the Chrome permission dialog), the gateway session handles THAT ONE action, not the whole pipeline.
  • IMPORTANT: run the spawner with ~/.venvs/tiktok/bin/python PREFIXED BY timeout (guard false-positive on venv pythons — see Aug 8 pitfalls). E.g. timeout 30 ~/.venvs/tiktok/bin/python spawn_pipeline_agent.py ....

Monitoring the spawned agent + PC shutdown (John's "turn off my pc after scheduling" pattern, Aug 8)

  • Watch the agent's log for completion, don't poll blindly: scripts/watch-agent-log.sh <order-token> <log-path> (e.g. scripts/watch-agent-log.sh order_20260808_234547 /home/john/google-flow-automation/job_orders/run_20260808_234547.log) polls every 20s until either (a) the log contains a REAL schedule confirmation (prints SCHEDULED_CONFIRMED + the matched line) or (b) the agent process exits (prints AGENT_EXITED + last error/schedule lines) or (c) 40min timeout. Run it with terminal(background=true, notify_on_complete=true) — the notification is what wakes the gateway session to take the follow-up action.
  • CRITICAL — anchor on CONCRETE DATES, never template phrases (Aug 8, real false-positive incident): the AUTO_PROMPT is echoed VERBATIM into run_<ts>.log (it IS the agent's prompt), so it contains lines like 📅 Scheduled for <slot> — done! and 1080p Upscaled. A watcher grepping Scheduled for|scheduled Aug|Scheduled — fires on the TEMPLATE text seconds after spawn and reports success before the video is even generated (cost a false shutdown this session). Grep only for concrete output: Scheduled for Aug [0-9], scheduled Aug [0-9], 📅 Aug [0-9], or job-folder artifacts (generated_video.json + mp4 + Posts-page verification). Same rule for download polling: don't grep "1080p Upscaled" (it's in the prompt), grep for the ~/Downloads/<slug>_<ts>.mp4 filename instead.
  • CUSTOM COMPLETION MARKER TRAP (Aug 12 — NEW variant, almost powered off the PC mid-run): for the VENCEWAY redo I told the agent to print REDO_COMPLETE after step 9, then the watcher grepped the raw log for it — BUT the marker string lived in the EXTRA INSTRUCTIONS inside the prompt, and hermes chat -q echoes the whole prompt (Query block) into the log at spawn → the watcher matched the echo ~1 min in and reported success while the agent was still writing segment prompts (final.mp4 didn't even exist yet). CAUGHT by the pre-poweroff verification (final.mp4 missing + agent still alive). FIX: a completion marker is only reliable if matched in the POST-BANNER log tail (sed -n '/Initializing/,$p' <log> | grep <marker>) AND the agent process has exited (pgrep -f <config-stem> empty). Also: verify the deliverable artifact exists (final.mp4 mtime) before scheduling the poweroff, ALWAYS — the shutdown is irreversible, the log is not.
  • Delayed poweroff so the confirmation message reaches John first: systemd-run --user --on-active=25s --unit=hermes-shutdown /bin/systemctl poweroff — detached systemd timer, fires ~25s later (long enough for the final Telegram reply to deliver), survives the gateway process dying, and is the ONLY reliable shutdown route from inside the gateway session. Used and verified 3× Aug 8 (PC powered off cleanly each time, gateway auto-recovers on next boot via linger). Check for stray Flatpak Chrome and running agents BEFORE scheduling the poweroff (kill leftovers first so the next boot is clean).
  • John's AFK window-close request: "close all windows but keep the PC on" — enumerate with wmctrl -l, close with wmctrl -ic <wid> (graceful). An xed "Unsaved Document" may resist (save-confirm dialog): screenshot + vision-check for real content FIRST; if the content matters (e.g. a pasted token/key) and John confirms it's already backed up elsewhere, dismiss with Escape then xdotool windowkill <wid>. Only the Desktop window should remain, and systemctl --user is-active hermes-gateway must stay active.

Aug 8 2026 session — VERIFIED end-to-end (Bostanten) + new pitfalls (Smasher)

Two full runs this session. Bostanten German training shoes = FULL SUCCESS (generated, 1080p, product attached "BUY HERE", scheduled Aug 9 6:00 PM, verified on Posts page). Smasher Struer sneakers = ALSO FULL SUCCESS (completed after a reboot): generated (orbit camera), 1080p upscaled downloaded, "BUY HERE" attached, scheduled Aug 10 6:00 AM, verified on Posts page. The 1080p download snag was the Chrome permission dialog (below) — once granted it PERSISTS across reboots (post-reboot re-click worked first try).

The working manual path (repeat this — it works)

  1. chrome-up.sh needs DISPLAY=:0 exported (gateway shell has no DISPLAY after reboot; X is up at :0 with john logged in). export DISPLAY=:0 before launching.
  2. flow_batch.py attach is STILL flaky ("selected cards but Add to Prompt never appeared" every time). Plan for the manual fallback:
    • Clear stale prompt-bar chip: JS-click the "close/Clear prompt" button (found by rect ~(964,595)) — must be 0 chips before attaching.
    • Open library: JS-click button[aria-label="add_2Create"] (~414,698).
    • Attach: CDP mclick X Y left shift on the library LIST rows (40x40 thumbs, x≈476, y≈92-184 — re-query live). This WORKS reliably. Then JS-find "Add to Prompt" and mclick its CENTER (x+w/2, y+h/2 — was ~890,600).
    • Verify chip_count=1 (or 2) before proceeding.
  3. Video mode PERSISTS across sessions — the prompt-bar chip may already read "Video · 10s crop_9_16 x1" (Omni Flash). If so, do NOT open the picker — just type the prompt. Only click the model chip (JS click ~882,714) if it shows "Nano Banana" (Image mode).
  4. Submit via focus + Enter (collapse selection to end first), NOT the arrow. Prompt resets to placeholder = generation started.
  5. Generation ~1.5-2.5 min (40% → done). Video card = leftmost, hover overlay 3-dot appears with CDP hover — see pitfall below for the reliable hover recipe.
  6. 3-dot → Download → 1080p Upscaled at its CENTER (item is 56px tall, cy≈462 — clicking the y=442 "center" from an old note MISSES, hits 720p) → file lands in ~/Downloads, VERIFY with ffprobe (must be 1080x1920, ~8MB; 720p = 1280x720, ~2.5MB).

NEW PITFALLS (Aug 8)

  • Chrome "Download multiple files" permission dialog (FIRST TIME only per site — and it PERSISTS across reboots): Flow's first 1080p download triggers a BROWSER-LEVEL permission prompt ("labs.google wants to Download multiple files" — Block/Allow) that CDP page probes CANNOT see (it's browser chrome, not page DOM). Symptom: 1080p clicks appear to do nothing; only the 720p original lands. FIX: full-window screenshot via gnome-screenshot -f /tmp/x.png (import/scrot NOT installed on Mint), vision-locate the dialog, then accept with keyboard: xdotool windowactivate --sync $WID; xdotool key Tab Tab Return (Allow is the default-focused/2nd-tab target — keyboard avoids pixel guessing). After Allow, RE-CLICK 1080p. VERIFIED Aug 8 late: the grant survives a full PC reboot — after restarting, the same grid-menu 1080p click downloaded the upscaled file on the first attempt (no dialog reappeared). So this is a one-time fix per Chrome profile, not a recurring st

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Related Skills