← 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
M scripts/social-video-gen.mjs
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 →