← back to Crcp Pitch Video

build-client-videos.js

85 lines

/* Build 3 client-specific videos (loan officer / escrow / title) from the shared walkthrough
 * screenshots — each with its own intro card, tailored captions + cloned-voice narration. */
const fs = require('fs'), path = require('path'), { execSync } = require('child_process'), https = require('https');

const M = JSON.parse(fs.readFileSync(path.join(__dirname, 'walkthrough/manifest.json'), 'utf8'));
const boxOf = i => M[i] && M[i].box;
const shotOf = i => `walkthrough/${M[i].shot}`;
const VOICE = 'Xa9qV4wNbvSkdUWsYLzq'; let KEY = '';
for (const p of [process.env.HOME + '/Projects/secrets-manager/.env', process.env.HOME + '/.claude/skills/clone-voice/.env']) {
  try { for (const l of fs.readFileSync(p, 'utf8').split('\n')) if (l.includes('ELEVENLABS_API_KEY')) { KEY = l.split('=')[1].trim().replace(/"/g, ''); break; } } catch {}
  if (KEY) break;
}
const FPS = 30;

// Each client: intro + ordered steps referencing shot indices, with client-specific caption + VO.
const CLIENTS = [
  {
    id: 'loan-officer', name: 'Loan Officers', accent: '#6BCB77',
    intro: { head: 'For Loan Officers', sub: 'Turn top agents into a borrower pipeline', vo: "Here's how a loan officer uses C-R-C-P to build a steady pipeline of borrowers." },
    steps: [
      [0, 'Your borrowers come from agents', "Your borrowers come from agents — and every active agent is right here."],
      [12, 'Start where loans are biggest', "Start where the loans are biggest. The luxury markets, where a single deal is a jumbo loan."],
      [6, 'Sort by activity', "Sort by listings to surface the busiest agents — the ones with buyers who need financing right now."],
      [9, 'See their live deal flow', "Open an agent to see their current listings and recent sales — proof of real, active deal flow."],
      [10, 'Pitch a co-marketing play', "Generate a co-marketing pitch: offer to pre-qualify their buyers, or co-host an open house."],
      [11, 'Load your dialer', "Export your target list and load it straight into your dialer. That's your week of calls, done."],
    ],
  },
  {
    id: 'escrow-officer', name: 'Escrow Officers', accent: '#58a6ff',
    intro: { head: 'For Escrow Officers', sub: 'Win the agents, win their escrows', vo: "Here's how an escrow officer uses C-R-C-P to win more openings." },
    steps: [
      [0, 'The agent picks escrow', "The agent chooses escrow — so your job is knowing exactly which agents to court."],
      [13, 'Pick your market', "Pick your market, and load every licensed agent working it."],
      [6, 'Find the highest-volume agents', "Sort by activity. The highest-volume agents open the most escrows — those are your priorities."],
      [9, 'Read their pipeline', "Open an agent to read their pipeline: how many deals are about to need an escrow officer."],
      [4, 'Target a whole brokerage', "Or target an entire brokerage at once. Win the office, and you win its escrows."],
      [11, 'Build your outreach list', "Draft your intro and export the list. Now you're first in line, before the deal even opens."],
    ],
  },
  {
    id: 'title-rep', name: 'Title Reps', accent: '#C8A24B',
    intro: { head: 'For Title Reps', sub: 'Own the relationships that carry the biggest premiums', vo: "Here's how a title rep uses C-R-C-P to build the relationships that matter." },
    steps: [
      [0, 'Title is a relationship business', "Title is a relationship business — and C-R-C-P hands you the relationships worth building."],
      [12, 'Go where inventory is priciest', "Target the markets with the priciest inventory — the biggest title premiums in the country."],
      [5, 'See where each agent works', "Turn on State and confidence to see exactly where each agent is licensed and active."],
      [6, 'Find the deal-controllers', "Sort by activity to find the agents controlling the most transactions in that market."],
      [9, 'Walk in informed', "Study their listings and recent sales before you reach out — so you walk in already knowing their business."],
      [10, 'A relationship-first intro', "Draft a relationship-first intro, build your call list, and start showing up before your competitor does."],
    ],
  },
];

function tts(text, out) {
  return new Promise((res, rej) => {
    const body = JSON.stringify({ text, model_id: 'eleven_multilingual_v2', voice_settings: { stability: 0.5, similarity_boost: 0.8, style: 0.15 } });
    const req = https.request(`https://api.elevenlabs.io/v1/text-to-speech/${VOICE}`, { method: 'POST', headers: { 'xi-api-key': KEY, 'Content-Type': 'application/json', Accept: 'audio/mpeg' } }, r => { const c = []; r.on('data', d => c.push(d)); r.on('end', () => { fs.writeFileSync(out, Buffer.concat(c)); res(); }); });
    req.on('error', rej); req.write(body); req.end();
  });
}
const dur = f => parseFloat(execSync(`ffprobe -v error -show_entries format=duration -of csv=p=0 "${f}"`).toString().trim());

(async () => {
  let totalChars = 0;
  for (const c of CLIENTS) {
    const aud = path.join(__dirname, `client-audio/${c.id}`); fs.mkdirSync(aud, { recursive: true });
    const scenes = [];
    // intro
    await tts(c.intro.vo, path.join(aud, 'intro.mp3')); totalChars += c.intro.vo.length;
    const introDur = dur(path.join(aud, 'intro.mp3'));
    scenes.push({ kind: 'intro', head: c.intro.head, sub: c.intro.sub, accent: c.accent, durFrames: Math.ceil((introDur + 1.2) * FPS), audioStartSec: 0 });
    for (let k = 0; k < c.steps.length; k++) {
      const [si, cap, vo] = c.steps[k];
      const mp3 = path.join(aud, `s${k}.mp3`); await tts(vo, mp3); totalChars += vo.length;
      const d = dur(mp3);
      scenes.push({ kind: 'step', shot: shotOf(si), box: boxOf(si), cap, label: c.name, accent: c.accent, durFrames: Math.ceil((d + 1.35) * FPS) });
    }
    let acc = 0; for (const s of scenes) { s.audioStartSec = acc / FPS; acc += s.durFrames; }
    fs.writeFileSync(path.join(__dirname, `src/client-${c.id}.json`), JSON.stringify({ fps: FPS, total: acc, name: c.name, accent: c.accent, scenes }, null, 1));
    console.log(`  ${c.id}: ${scenes.length} scenes, ${(acc / FPS).toFixed(0)}s`);
  }
  console.log(`✔ 3 client videos, ${totalChars} chars (~$${(totalChars / 1000 * 0.30).toFixed(2)})`);
})();