← 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)})`);
})();