Video Maker ships (L3): ffmpeg end-card composite over the team master videos (member name + link + server-generated QR as PPM), audio preserved, metered; ffmpeg+ttf-dejavu added to the image; tile live

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-28 08:37:59 -05:00
parent 7c3e98eaa9
commit 44816571b6
6 changed files with 315 additions and 2 deletions
+60
View File
@@ -0,0 +1,60 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="Video Maker — the team's promo videos with your own end card: your name, your link, your QR code."><title>Video Maker | The Circle Suite</title><link rel="icon" type="image/png" href="/favicon.png"><link rel="stylesheet" href="/styles.css">
<style>
.vw{max-width:880px;margin:0 auto;padding:24px 20px 70px}
.vw h1{font-size:clamp(26px,4.5vw,36px);margin:6px 0 8px}
.vw .lede{color:var(--muted);line-height:1.6;margin-bottom:18px}
#vdGate{display:none;border:2px solid var(--gold);border-radius:12px;padding:16px 20px;margin-bottom:18px;line-height:1.6}
#vdGate b{color:var(--gold)}
.vd-card{background:var(--card,#0d2236);border:1px solid var(--line,#24425d);border-radius:14px;padding:18px 20px}
.vd-angles{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
.vd-angle{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:999px;
padding:7px 14px;font-size:13.5px;font-weight:700;cursor:pointer}
.vd-angle.on{border-color:var(--teal);background:rgba(78,214,203,.12);color:var(--teal)}
label.vd-lab{display:block;font-weight:700;font-size:14px;margin:0 0 6px}
#vdName{width:100%;max-width:340px;background:var(--bg,#0b1d2e);border:1px solid var(--line);
border-radius:10px;color:var(--text);padding:11px 13px;font-size:15px;font-family:inherit}
.vd-hint{color:var(--muted);font-size:12.5px;margin-top:6px}
.vd-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:16px}
#vdMeter{font-size:12.5px;color:var(--muted);margin-left:auto}
#vdMeter b{color:var(--text)}
#vdErr{display:none;color:#f0a05a;margin-top:12px;font-size:14px;line-height:1.55}
#vdOut{display:none;margin-top:18px;border:2px solid var(--teal);border-radius:14px;padding:16px 20px}
#vdOut .ok{font-weight:800;margin-bottom:8px}
.vd-note{margin-top:20px;font-size:12.5px;color:var(--muted);line-height:1.6;border-top:1px solid var(--line);padding-top:12px}
.spin{display:inline-block;width:13px;height:13px;border:2px solid rgba(255,255,255,.25);border-top-color:var(--gold);
border-radius:50%;animation:sp .8s linear infinite;vertical-align:-2px;margin-right:7px}
@keyframes sp{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.spin{animation:none}}
</style></head>
<body>
<header class="wrap nav"><a class="brand" href="/suite"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span id="brandName">RM Circle</span><small>Video Maker</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/suite">← The Suite</a><a class="btn btn-primary" href="/suite/email">Email Engine</a></div></header>
<main class="vw">
<div class="eyebrow" style="color:var(--gold)">CIRCLE SUITE · LEVEL 3 · FABRICA</div>
<h1>Video <span class="gold">Maker.</span></h1>
<p class="lede">Take the team's produced promo videos and put <strong style="color:var(--text)">your</strong> ending on them — your name, your invite link, and your QR code on screen for six seconds. Download it and post it anywhere.</p>
<div id="vdGate"></div>
<div class="vd-card" id="vdCard">
<label class="vd-lab">Which video?</label>
<div class="vd-angles" id="vdAngles"></div>
<label class="vd-lab" for="vdName">What name should the end card use?</label>
<input type="text" id="vdName" maxlength="42" placeholder="e.g. Marty B.">
<div class="vd-hint">It reads "<span id="vdPrev">Your name</span> invited you" above your link and QR code.</div>
<div class="vd-row">
<button id="vdGo" class="btn btn-primary">🎬 Make my video</button>
<span id="vdMeter"></span>
</div>
<div id="vdErr"></div>
<div id="vdOut">
<div class="ok">✅ Your video is ready</div>
<div class="vd-hint" id="vdSize" style="margin-bottom:10px"></div>
<a id="vdDl" class="btn btn-teal btn-sm" download>⬇ Download the MP4</a>
</div>
</div>
<p class="vd-note"><strong style="color:var(--text)">Teach it forward:</strong> the video does the explaining so your two don't have to — show them how to make their own the day they join. <br>The video itself is the team's produced master; only the ending is yours, so every share stays on-brand and on-message. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-video.js"></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+99
View File
@@ -0,0 +1,99 @@
// Video Maker client. Renders take ~15-40s server-side, so the button holds a
// spinner and the copy warns before the wait rather than after it.
(function () {
'use strict';
var ANGLES = [
{ k: 'two', label: 'You only need two' },
{ k: 'pocket', label: 'Pocket change' },
{ k: 'phone', label: 'Runs from your phone' },
{ k: 'graveyard', label: 'Side-hustle graveyard' },
{ k: 'overview', label: 'General overview' }
];
var $ = function (id) { return document.getElementById(id); };
var angle = 'two', busy = false;
function renderAngles() {
var host = $('vdAngles');
host.innerHTML = '';
ANGLES.forEach(function (x) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'vd-angle' + (x.k === angle ? ' on' : '');
b.textContent = x.label;
b.addEventListener('click', function () { angle = x.k; renderAngles(); });
host.appendChild(b);
});
}
function showMeter(m) {
if (!m || !m.limit) { $('vdMeter').textContent = ''; return; }
$('vdMeter').innerHTML = '<b>' + m.remaining + '</b> of ' + m.limit + ' videos left this month';
}
function gate(msg) {
var g = $('vdGate');
g.style.display = 'block';
g.innerHTML = msg;
$('vdCard').style.opacity = '.55';
$('vdGo').disabled = true;
$('vdName').disabled = true;
}
async function boot() {
renderAngles();
try {
var r = await fetch('/api/public/suite-meters');
if (r.status === 401) { gate('You’re not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> and connect the wallet that holds your position, then come back.'); return; }
if (r.status === 403) { gate('The Video Maker isn’t open for this position yet. <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.'); return; }
if (!r.ok) return;
var d = await r.json();
var m = d.meters && d.meters.video;
if (m && m.reason === 'locked') {
gate('The Video Maker unlocks at <b>Fabrica (level 3)</b>. You’re at level ' + d.level + ' — keep climbing and it opens, along with the Email Engine. <a href="/how-pay-works#exact-costs" style="color:var(--gold);font-weight:700">See what upgrades cost →</a>');
return;
}
showMeter(m);
} catch (e) {}
}
async function run() {
if (busy) return;
busy = true;
$('vdErr').style.display = 'none';
$('vdOut').style.display = 'none';
$('vdGo').disabled = true;
$('vdGo').innerHTML = '<span class="spin"></span>Rendering your video… (about a minute)';
try {
var r = await fetch('/api/public/suite-video', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ angle: angle, name: $('vdName').value.trim() })
});
var d = await r.json();
if (!r.ok) {
$('vdErr').textContent = d.error || 'That didn’t go through — try again.';
$('vdErr').style.display = 'block';
if (d.meter) showMeter(d.meter);
} else {
$('vdDl').href = d.url;
$('vdSize').textContent = 'MP4 · about ' + d.mb + ' MB · ready to post anywhere.';
$('vdOut').style.display = 'block';
showMeter(d.meter);
$('vdOut').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} catch (e) {
$('vdErr').textContent = 'Connection hiccup — try again.';
$('vdErr').style.display = 'block';
}
busy = false;
$('vdGo').disabled = false;
$('vdGo').textContent = '🎬 Make my video';
}
document.addEventListener('DOMContentLoaded', function () {
boot();
$('vdGo').addEventListener('click', run);
$('vdName').addEventListener('input', function () {
$('vdPrev').textContent = $('vdName').value.trim() || 'Your name';
});
});
})();
+1 -1
View File
@@ -16,7 +16,7 @@
{ lv: 2, ico: '✍️', name: 'Copy Engine', desc: 'An AI copywriter tuned to this business: posts, DMs, follow-ups and objection replies with your link filled in.', href: '/suite/copy', live: true },
{ lv: 2, ico: '🧱', name: 'Page Builder', desc: 'Answer four questions, get your own hosted invitation page — your story, your angle video, your QR.', href: '/suite/page', live: true },
{ lv: 3, ico: '📧', name: 'Email Engine', desc: 'Welcome series, follow-up sequences and broadcasts in the team voice — exportable to any autoresponder.', href: '/suite/email', live: true },
{ lv: 3, ico: '🎥', name: 'Video Maker', desc: 'The team’s master promo videos rendered with your personal end-card — your name, your QR, your link.', live: false },
{ lv: 3, ico: '🎥', name: 'Video Maker', desc: 'The team’s master promo videos rendered with your personal end-card — your name, your QR, your link.', href: '/suite/video', live: true },
{ lv: 4, ico: '🗣️', name: 'Voice Profile + Funnels', desc: 'Output that sounds like YOU, plus multi-page funnels on your own team subdomain.', live: false },
{ lv: 5, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising: monthly ad credits on the team’s own network, banner and text placements, rotator priority, and AI campaign packs.', live: false },
{ lv: 6, ico: '🏭', name: 'Funnel Factory', desc: 'Complete hosted funnels with A/B variants, replay funnels (your team webinar as an on-demand registration page with a timed CTA), and a lead CRM.', live: false },