[object Object]

← back to Rentv 2026

rentv social-video-gen: enforce shared per-video cost cap before ElevenLabs TTS (trim narration if over)

05adbf9ef4cca88d173e5bcf7e4ec4d0c413f37d · 2026-08-05 15:53:11 -0700 · Steve

Files touched

Diff

commit 05adbf9ef4cca88d173e5bcf7e4ec4d0c413f37d
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 15:53:11 2026 -0700

    rentv social-video-gen: enforce shared per-video cost cap before ElevenLabs TTS (trim narration if over)
---
 scripts/social-video-gen.mjs | 46 +++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 43 insertions(+), 3 deletions(-)

diff --git a/scripts/social-video-gen.mjs b/scripts/social-video-gen.mjs
index 65edd023..667604fc 100644
--- a/scripts/social-video-gen.mjs
+++ b/scripts/social-video-gen.mjs
@@ -17,7 +17,7 @@
  */
 import fs from 'fs';
 import path from 'path';
-import { fileURLToPath } from 'url';
+import { fileURLToPath, pathToFileURL } from 'url';
 import { execFile } from 'child_process';
 import { promisify } from 'util';
 
@@ -94,8 +94,48 @@ async function main() {
     const frame = path.join(JOBDIR, `${jobId}.frame.png`);
     const out = path.join(VIDDIR, `${jobId}.mp4`);
 
-    patch({ status: 'voicing', script });
-    await tts(script, tmp);
+    // Per-video spend cap (shared guard). ElevenLabs TTS is this pipeline's ONLY paid
+    // cost; estimate it BEFORE the call and, if OVER the shared cap, trim the narration
+    // to the max chars that fit under the cap (clean sentence boundary). Guard failures
+    // must never crash the pipeline — wrap in try/catch and proceed with a warning.
+    let finalScript = script;
+    try {
+      const guardPath = path.join(process.env.HOME || '', '.claude', 'skills', 'cost-tracker', 'scripts', 'video-cost-cap.mjs');
+      const { capCheck, estimateVideoCost } = await import(pathToFileURL(guardPath).href);
+      const v = capCheck({ ttsChars: finalScript.length, ttsProvider: 'elevenlabs' });
+      console.log(v.line); // 💸 video est $X / cap $Y — OK/OVER (honors the "always show costs" rule)
+      if (!v.withinCap) {
+        // Derive the max chars that fit under the cap from the ElevenLabs char rate in
+        // pricing.json (never hardcode a rate), leaving a small margin, then trim to a
+        // clean sentence boundary.
+        const est = estimateVideoCost({ ttsChars: finalScript.length, ttsProvider: 'elevenlabs' });
+        const perChar = finalScript.length > 0 ? est.breakdown.tts / finalScript.length : 0;
+        const budget = est.cap * 0.95; // 5% margin under the cap
+        let maxChars = perChar > 0 ? Math.floor(budget / perChar) : finalScript.length;
+        maxChars = Math.max(0, Math.min(maxChars, finalScript.length));
+        let trimmed = finalScript.slice(0, maxChars);
+        // Prefer a clean sentence boundary (last . ! ?) within the budget.
+        const lastSentence = Math.max(trimmed.lastIndexOf('.'), trimmed.lastIndexOf('!'), trimmed.lastIndexOf('?'));
+        if (lastSentence > 0) trimmed = trimmed.slice(0, lastSentence + 1);
+        else { // no sentence end — fall back to last word boundary so we don't cut mid-word
+          const lastSpace = trimmed.lastIndexOf(' ');
+          if (lastSpace > 0) trimmed = trimmed.slice(0, lastSpace);
+        }
+        trimmed = trimmed.trim();
+        if (trimmed) {
+          finalScript = trimmed;
+          const v2 = capCheck({ ttsChars: finalScript.length, ttsProvider: 'elevenlabs' });
+          console.log(`✂️  trimmed narration ${script.length}→${finalScript.length} chars to fit cap`);
+          console.log(v2.line);
+          patch({ script: finalScript, cost_cap_trimmed: true, cost_cap_orig_chars: script.length });
+        }
+      }
+    } catch (guardErr) {
+      console.warn(`⚠️  cost-cap guard unavailable, proceeding without cap: ${guardErr.message}`);
+    }
+
+    patch({ status: 'voicing', script: finalScript });
+    await tts(finalScript, tmp);
     const nd = await probeDur(tmp);
 
     // Voice-only (Boomer audio): publish the mp3 straight to the served dir and stop — no frame/ffmpeg.

← 96574cc1 auto-save: 2026-08-05T15:43:31 (7 files) — data/deals-regist  ·  back to Rentv 2026  ·  auto-save: 2026-08-05T16:13:44 (8 files) — data/deals-regist f2a81c1c →