Spaces:
Running
Running
VinOS Agent Claude Opus 4.6 commited on
Commit Β·
1d15a1f
1
Parent(s): 6144942
Phase 8: Video Engine (Remotion Studio HF Space + /videos command)
Browse filesSeparate Remotion-powered HF Space (AIgoose/remotion-studio) with 4 video
templates, Express API, and web UI. VinOS integration via video_engine.js
skill, /videos Telegram command, sendTelegramVideo, proxy dashboard routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CHANGELOG.md +22 -0
- database/video_db.json +1 -0
- public/videos-dashboard.html +212 -0
- server.js +51 -0
- skills/api_caller.js +22 -0
- skills/set_telegram_menu.js +2 -1
- skills/video_engine.js +356 -0
CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
| 2 |
|
| 3 |
---
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
## Phase 0 + 6 + 7 β Quick Wins, SEO-to-Revenue, Self-Optimization
|
| 6 |
**Date:** 2026-03-27
|
| 7 |
|
|
|
|
| 2 |
|
| 3 |
---
|
| 4 |
|
| 5 |
+
## Phase 8 β Video Engine (Remotion Studio + /videos command)
|
| 6 |
+
**Date:** 2026-03-27
|
| 7 |
+
|
| 8 |
+
### New HF Space: `AIgoose/remotion-studio`
|
| 9 |
+
- Separate Remotion-powered video rendering API + web UI
|
| 10 |
+
- 4 video templates: TextExplainer (16:9), ProductPromo (16:9), SocialShort (9:16), SeoSummary (16:9)
|
| 11 |
+
- Docker: Node 20 + Chromium + pre-bundled compositions
|
| 12 |
+
- REST API: `/api/render`, `/api/jobs`, `/api/download/:id`, `/api/templates`
|
| 13 |
+
- Async render queue with progress tracking + 24h auto-cleanup
|
| 14 |
+
- Dark theme web UI with template gallery, create form, job queue, video player
|
| 15 |
+
|
| 16 |
+
### VinOS Integration
|
| 17 |
+
- **New files:** `skills/video_engine.js`, `public/videos-dashboard.html`, `database/video_db.json`
|
| 18 |
+
- **Modified:** `server.js`, `skills/api_caller.js`, `skills/set_telegram_menu.js`, `.env`
|
| 19 |
+
- **New Telegram command:** `/videos` with sub-commands: `create`, `templates`, `status`
|
| 20 |
+
- **New API method:** `sendTelegramVideo` in api_caller.js (50MB limit)
|
| 21 |
+
- **Proxy routes:** `/videos-dashboard`, `/api/videos/templates`, `/api/videos/create`, `/api/videos/jobs`, `/api/videos/download/:id`
|
| 22 |
+
- **Pipeline:** Topic β AI script generation β Remotion render β poll completion β send video to Telegram
|
| 23 |
+
- Updated `/start` message with Video Engine section
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
## Phase 0 + 6 + 7 β Quick Wins, SEO-to-Revenue, Self-Optimization
|
| 28 |
**Date:** 2026-03-27
|
| 29 |
|
database/video_db.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"jobs":[],"stats":{"totalRendered":0,"totalFailed":0}}
|
public/videos-dashboard.html
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>VinOS β Video Engine</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 11 |
+
:root {
|
| 12 |
+
--bg: #090c12; --surface: #111622; --surface2: #161e2e; --border: #1e2d45;
|
| 13 |
+
--accent: #00e5ff; --accent2: #7c3aed; --green: #22d3a0; --orange: #f59e0b; --red: #ef4444;
|
| 14 |
+
--text: #e2e8f0; --muted: #64748b;
|
| 15 |
+
}
|
| 16 |
+
body { font-family: 'Space Grotesk', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; padding: 20px; }
|
| 17 |
+
.container { max-width: 800px; margin: 0 auto; }
|
| 18 |
+
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
| 19 |
+
.header h1 { font-size: 1.6rem; font-weight: 700; background: linear-gradient(135deg, var(--accent), var(--green)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
| 20 |
+
.header .back { color: var(--accent); text-decoration: none; font-size: 0.85rem; }
|
| 21 |
+
.header .back:hover { text-decoration: underline; }
|
| 22 |
+
|
| 23 |
+
.stats-row { display: flex; gap: 12px; margin-bottom: 24px; }
|
| 24 |
+
.stat-card { flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px; text-align: center; }
|
| 25 |
+
.stat-card .val { font-size: 1.5rem; font-weight: 700; color: var(--accent); }
|
| 26 |
+
.stat-card .label { font-size: 0.7rem; color: var(--muted); margin-top: 4px; }
|
| 27 |
+
|
| 28 |
+
.section-title { font-size: 1rem; font-weight: 600; margin-bottom: 14px; color: var(--muted); }
|
| 29 |
+
|
| 30 |
+
/* Template Cards */
|
| 31 |
+
.templates { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 28px; }
|
| 32 |
+
.tmpl { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 18px; cursor: pointer; transition: all 0.2s; }
|
| 33 |
+
.tmpl:hover, .tmpl.selected { border-color: var(--accent); box-shadow: 0 0 20px rgba(0,229,255,0.06); }
|
| 34 |
+
.tmpl.selected { background: var(--surface2); }
|
| 35 |
+
.tmpl .ratio { display: inline-block; font-size: 0.6rem; padding: 2px 8px; border-radius: 6px; background: var(--accent)20; color: var(--accent); font-weight: 600; margin-bottom: 6px; }
|
| 36 |
+
.tmpl h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 4px; }
|
| 37 |
+
.tmpl p { font-size: 0.75rem; color: var(--muted); line-height: 1.4; }
|
| 38 |
+
|
| 39 |
+
/* Create Form */
|
| 40 |
+
.create-form { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 24px; margin-bottom: 28px; display: none; }
|
| 41 |
+
.create-form.visible { display: block; }
|
| 42 |
+
.form-group { margin-bottom: 14px; }
|
| 43 |
+
.form-group label { display: block; font-size: 0.8rem; color: var(--muted); margin-bottom: 4px; }
|
| 44 |
+
.form-group input, .form-group textarea {
|
| 45 |
+
width: 100%; padding: 10px 14px; background: var(--bg); border: 1px solid var(--border);
|
| 46 |
+
border-radius: 8px; color: var(--text); font-family: inherit; font-size: 0.85rem; outline: none;
|
| 47 |
+
}
|
| 48 |
+
.form-group input:focus, .form-group textarea:focus { border-color: var(--accent); }
|
| 49 |
+
.form-group textarea { min-height: 70px; resize: vertical; }
|
| 50 |
+
.btn { padding: 10px 24px; border: none; border-radius: 8px; font-family: inherit; font-weight: 700; cursor: pointer; transition: all 0.2s; font-size: 0.85rem; }
|
| 51 |
+
.btn-primary { background: linear-gradient(135deg, var(--accent), var(--green)); color: #000; }
|
| 52 |
+
.btn-primary:hover { opacity: 0.9; }
|
| 53 |
+
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 54 |
+
.btn-sm { padding: 6px 14px; font-size: 0.75rem; border-radius: 6px; }
|
| 55 |
+
.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
| 56 |
+
|
| 57 |
+
/* Jobs */
|
| 58 |
+
.job { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px; margin-bottom: 10px; }
|
| 59 |
+
.job-row { display: flex; justify-content: space-between; align-items: center; }
|
| 60 |
+
.job-id { font-size: 0.7rem; color: var(--muted); font-family: monospace; }
|
| 61 |
+
.badge { font-size: 0.65rem; padding: 3px 10px; border-radius: 6px; font-weight: 600; text-transform: uppercase; }
|
| 62 |
+
.badge-queued { background: var(--orange)20; color: var(--orange); }
|
| 63 |
+
.badge-rendering { background: var(--accent)20; color: var(--accent); }
|
| 64 |
+
.badge-complete { background: var(--green)20; color: var(--green); }
|
| 65 |
+
.badge-failed { background: var(--red)20; color: var(--red); }
|
| 66 |
+
.badge-expired { background: var(--muted)20; color: var(--muted); }
|
| 67 |
+
.progress { height: 3px; background: var(--border); border-radius: 2px; margin-top: 8px; overflow: hidden; }
|
| 68 |
+
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--green)); transition: width 0.3s; }
|
| 69 |
+
.job-actions { display: flex; gap: 6px; margin-top: 8px; }
|
| 70 |
+
.empty { text-align: center; color: var(--muted); padding: 30px; font-style: italic; font-size: 0.85rem; }
|
| 71 |
+
|
| 72 |
+
/* Player */
|
| 73 |
+
.modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.85); z-index: 100; justify-content: center; align-items: center; }
|
| 74 |
+
.modal.visible { display: flex; }
|
| 75 |
+
.modal video { max-width: 90vw; max-height: 80vh; border-radius: 10px; }
|
| 76 |
+
.modal-close { position: absolute; top: 16px; right: 24px; font-size: 2rem; color: #fff; cursor: pointer; }
|
| 77 |
+
|
| 78 |
+
@media (max-width: 640px) { .templates { grid-template-columns: 1fr; } .stats-row { flex-wrap: wrap; } .stat-card { min-width: 45%; } }
|
| 79 |
+
</style>
|
| 80 |
+
</head>
|
| 81 |
+
<body>
|
| 82 |
+
<div class="container">
|
| 83 |
+
<div class="header">
|
| 84 |
+
<h1>Video Engine</h1>
|
| 85 |
+
<a class="back" href="/">Dashboard</a>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<div class="stats-row">
|
| 89 |
+
<div class="stat-card"><div class="val" id="sTotal">0</div><div class="label">Rendered</div></div>
|
| 90 |
+
<div class="stat-card"><div class="val" id="sActive">0</div><div class="label">Active</div></div>
|
| 91 |
+
<div class="stat-card"><div class="val" id="sTemplates">4</div><div class="label">Templates</div></div>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div class="section-title">Choose Template</div>
|
| 95 |
+
<div class="templates" id="tmplGrid"></div>
|
| 96 |
+
|
| 97 |
+
<div class="create-form" id="createForm">
|
| 98 |
+
<div class="form-group">
|
| 99 |
+
<label>Topic *</label>
|
| 100 |
+
<input type="text" id="inTopic" placeholder="AI Productivity Tips for Founders">
|
| 101 |
+
</div>
|
| 102 |
+
<div class="form-group">
|
| 103 |
+
<label>Duration (seconds)</label>
|
| 104 |
+
<input type="range" id="inDuration" min="10" max="60" value="30" oninput="document.getElementById('durVal').textContent=this.value+'s'">
|
| 105 |
+
<span style="font-size:0.75rem;color:var(--muted)" id="durVal">30s</span>
|
| 106 |
+
</div>
|
| 107 |
+
<button class="btn btn-primary" id="btnCreate" onclick="createVideo()">Generate Video</button>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<div class="section-title">Recent Jobs</div>
|
| 111 |
+
<div id="jobsList"><div class="empty">No jobs yet</div></div>
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<div class="modal" id="playerModal">
|
| 115 |
+
<div class="modal-close" onclick="closePlayer()">×</div>
|
| 116 |
+
<video id="playerVid" controls></video>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<script>
|
| 120 |
+
const esc = s => { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
|
| 121 |
+
let templates = [], selectedKey = null, pollId = null;
|
| 122 |
+
|
| 123 |
+
async function loadTemplates() {
|
| 124 |
+
try {
|
| 125 |
+
const r = await fetch('/api/videos/templates');
|
| 126 |
+
const d = await r.json();
|
| 127 |
+
templates = d.templates || [];
|
| 128 |
+
document.getElementById('tmplGrid').innerHTML = templates.map(t => `
|
| 129 |
+
<div class="tmpl" data-key="${t.key}" onclick="selectTmpl('${t.key}')">
|
| 130 |
+
<div class="ratio">${esc(t.ratio)}</div>
|
| 131 |
+
<h3>${esc(t.name)}</h3>
|
| 132 |
+
<p>${esc(t.description)}</p>
|
| 133 |
+
</div>
|
| 134 |
+
`).join('');
|
| 135 |
+
} catch (e) {
|
| 136 |
+
document.getElementById('tmplGrid').innerHTML = '<div class="empty">Remotion Studio offline</div>';
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function selectTmpl(key) {
|
| 141 |
+
selectedKey = key;
|
| 142 |
+
document.querySelectorAll('.tmpl').forEach(c => c.classList.remove('selected'));
|
| 143 |
+
document.querySelector(`.tmpl[data-key="${key}"]`)?.classList.add('selected');
|
| 144 |
+
document.getElementById('createForm').classList.add('visible');
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
async function createVideo() {
|
| 148 |
+
const topic = document.getElementById('inTopic').value.trim();
|
| 149 |
+
if (!topic || !selectedKey) return alert('Enter a topic and select a template');
|
| 150 |
+
const btn = document.getElementById('btnCreate');
|
| 151 |
+
btn.disabled = true; btn.textContent = 'Submitting...';
|
| 152 |
+
try {
|
| 153 |
+
const r = await fetch('/api/videos/create', {
|
| 154 |
+
method: 'POST',
|
| 155 |
+
headers: { 'Content-Type': 'application/json' },
|
| 156 |
+
body: JSON.stringify({ topic, template: selectedKey, duration: +document.getElementById('inDuration').value })
|
| 157 |
+
});
|
| 158 |
+
const d = await r.json();
|
| 159 |
+
if (d.success) { loadJobs(); startPoll(); }
|
| 160 |
+
else alert(d.error || 'Failed');
|
| 161 |
+
} catch (e) { alert(e.message); }
|
| 162 |
+
btn.disabled = false; btn.textContent = 'Generate Video';
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
async function loadJobs() {
|
| 166 |
+
try {
|
| 167 |
+
const r = await fetch('/api/videos/jobs?limit=15');
|
| 168 |
+
const d = await r.json();
|
| 169 |
+
const jobs = d.jobs || [];
|
| 170 |
+
document.getElementById('sTotal').textContent = jobs.filter(j => j.status === 'complete').length;
|
| 171 |
+
document.getElementById('sActive').textContent = jobs.filter(j => j.status === 'rendering' || j.status === 'queued').length;
|
| 172 |
+
if (!jobs.length) { document.getElementById('jobsList').innerHTML = '<div class="empty">No jobs yet</div>'; return; }
|
| 173 |
+
document.getElementById('jobsList').innerHTML = jobs.map(j => `
|
| 174 |
+
<div class="job">
|
| 175 |
+
<div class="job-row">
|
| 176 |
+
<span class="job-id">${j.id}</span>
|
| 177 |
+
<span class="badge badge-${j.status}">${j.status}</span>
|
| 178 |
+
</div>
|
| 179 |
+
<div style="font-size:0.85rem;margin-top:4px;">${esc(j.template)}</div>
|
| 180 |
+
<div style="font-size:0.7rem;color:var(--muted);">${new Date(j.created).toLocaleString()}</div>
|
| 181 |
+
${j.status === 'rendering' ? `<div class="progress"><div class="progress-fill" style="width:${j.progress||0}%"></div></div>` : ''}
|
| 182 |
+
${j.error ? `<div style="color:var(--red);font-size:0.75rem;margin-top:4px;">${esc(j.error)}</div>` : ''}
|
| 183 |
+
<div class="job-actions">
|
| 184 |
+
${j.status === 'complete' ? `<button class="btn btn-sm btn-primary" onclick="playVid('${j.id}')">Play</button><a href="/api/videos/download/${j.id}" class="btn btn-sm btn-ghost" download>Download</a>` : ''}
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
`).join('');
|
| 188 |
+
const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'rendering');
|
| 189 |
+
if (!hasActive) stopPoll();
|
| 190 |
+
} catch (e) { /* */ }
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
function startPoll() { if (!pollId) pollId = setInterval(loadJobs, 3000); }
|
| 194 |
+
function stopPoll() { if (pollId) { clearInterval(pollId); pollId = null; } }
|
| 195 |
+
|
| 196 |
+
function playVid(id) {
|
| 197 |
+
const v = document.getElementById('playerVid');
|
| 198 |
+
v.src = `/api/videos/download/${id}`;
|
| 199 |
+
document.getElementById('playerModal').classList.add('visible');
|
| 200 |
+
v.play();
|
| 201 |
+
}
|
| 202 |
+
function closePlayer() {
|
| 203 |
+
const v = document.getElementById('playerVid');
|
| 204 |
+
v.pause(); v.src = '';
|
| 205 |
+
document.getElementById('playerModal').classList.remove('visible');
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
loadTemplates();
|
| 209 |
+
loadJobs();
|
| 210 |
+
</script>
|
| 211 |
+
</body>
|
| 212 |
+
</html>
|
server.js
CHANGED
|
@@ -59,6 +59,7 @@ const salesEngine = require('./skills/sales_engine');
|
|
| 59 |
const mayarClient = require('./skills/mayar_client');
|
| 60 |
const costTracker = require('./skills/cost_tracker');
|
| 61 |
const scoutAgent = require('./skills/scout_agent');
|
|
|
|
| 62 |
|
| 63 |
|
| 64 |
|
|
@@ -97,6 +98,47 @@ app.get('/api/landing', (req, res) => {
|
|
| 97 |
} catch (e) { res.json({ success: true, offers: [] }); }
|
| 98 |
});
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
// ============================================
|
| 101 |
// SALES ENGINE API
|
| 102 |
// ============================================
|
|
@@ -1342,6 +1384,11 @@ app.post('/api/telegram-webhook', async (req, res) => {
|
|
| 1342 |
|
| 1343 |
// --- Commands ---
|
| 1344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1345 |
if (userText && userText.toLowerCase().startsWith('/viral')) {
|
| 1346 |
await viralFlow.start(chatId, userText);
|
| 1347 |
return;
|
|
@@ -1894,6 +1941,10 @@ This engine researches any URL or topic and generates content with your options.
|
|
| 1894 |
`/offer eval β Run offer evaluation\n` +
|
| 1895 |
`/revenue β Revenue summary\n` +
|
| 1896 |
`/revenue today β Today's sales only\n\n` +
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1897 |
`π <b>RESEARCH</b>\n` +
|
| 1898 |
`/scout [keyword] β Market viability scout\n` +
|
| 1899 |
`/costs β API spend breakdown\n` +
|
|
|
|
| 59 |
const mayarClient = require('./skills/mayar_client');
|
| 60 |
const costTracker = require('./skills/cost_tracker');
|
| 61 |
const scoutAgent = require('./skills/scout_agent');
|
| 62 |
+
const videoEngine = require('./skills/video_engine');
|
| 63 |
|
| 64 |
|
| 65 |
|
|
|
|
| 98 |
} catch (e) { res.json({ success: true, offers: [] }); }
|
| 99 |
});
|
| 100 |
|
| 101 |
+
// ============================================
|
| 102 |
+
// VIDEO ENGINE API (proxy to Remotion Studio)
|
| 103 |
+
// ============================================
|
| 104 |
+
app.get('/videos-dashboard', (req, res) => {
|
| 105 |
+
res.sendFile(path.join(__dirname, 'public', 'videos-dashboard.html'));
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
app.get('/api/videos/templates', async (req, res) => {
|
| 109 |
+
try {
|
| 110 |
+
const r = await apiCaller.axiosIPv4.get(`${process.env.REMOTION_URL || 'https://AIgoose-remotion-studio.hf.space'}/api/templates`, { timeout: 10000 });
|
| 111 |
+
res.json(r.data);
|
| 112 |
+
} catch (e) { res.json({ templates: [] }); }
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
app.post('/api/videos/create', async (req, res) => {
|
| 116 |
+
try {
|
| 117 |
+
const { topic, template, duration } = req.body;
|
| 118 |
+
// Generate script via video engine then submit
|
| 119 |
+
const chatId = 'web';
|
| 120 |
+
await videoEngine.createVideo(chatId, topic || 'AI Technology', template || 'text-explainer');
|
| 121 |
+
res.json({ success: true, message: 'Video queued. Check the job queue.' });
|
| 122 |
+
} catch (e) { res.json({ success: false, error: e.message }); }
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
app.get('/api/videos/jobs', async (req, res) => {
|
| 126 |
+
try {
|
| 127 |
+
const r = await apiCaller.axiosIPv4.get(`${process.env.REMOTION_URL || 'https://AIgoose-remotion-studio.hf.space'}/api/jobs?limit=${req.query.limit || 20}`, { timeout: 10000 });
|
| 128 |
+
res.json(r.data);
|
| 129 |
+
} catch (e) { res.json({ jobs: [] }); }
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
app.get('/api/videos/download/:id', async (req, res) => {
|
| 133 |
+
try {
|
| 134 |
+
const remotionUrl = process.env.REMOTION_URL || 'https://AIgoose-remotion-studio.hf.space';
|
| 135 |
+
const r = await apiCaller.axiosIPv4.get(`${remotionUrl}/api/download/${req.params.id}`, { responseType: 'stream', timeout: 60000 });
|
| 136 |
+
res.set('Content-Type', 'video/mp4');
|
| 137 |
+
res.set('Content-Disposition', `attachment; filename="${req.params.id}.mp4"`);
|
| 138 |
+
r.data.pipe(res);
|
| 139 |
+
} catch (e) { res.status(404).json({ error: 'Video not available' }); }
|
| 140 |
+
});
|
| 141 |
+
|
| 142 |
// ============================================
|
| 143 |
// SALES ENGINE API
|
| 144 |
// ============================================
|
|
|
|
| 1384 |
|
| 1385 |
// --- Commands ---
|
| 1386 |
|
| 1387 |
+
if (userText && userText.toLowerCase().startsWith('/videos')) {
|
| 1388 |
+
await videoEngine.start(chatId, userText);
|
| 1389 |
+
return;
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
if (userText && userText.toLowerCase().startsWith('/viral')) {
|
| 1393 |
await viralFlow.start(chatId, userText);
|
| 1394 |
return;
|
|
|
|
| 1941 |
`/offer eval β Run offer evaluation\n` +
|
| 1942 |
`/revenue β Revenue summary\n` +
|
| 1943 |
`/revenue today β Today's sales only\n\n` +
|
| 1944 |
+
`π¬ <b>VIDEO ENGINE</b>\n` +
|
| 1945 |
+
`/videos create [topic] β AI-generate + render video\n` +
|
| 1946 |
+
`/videos templates β List video templates\n` +
|
| 1947 |
+
`/videos status β Render queue status\n\n` +
|
| 1948 |
`π <b>RESEARCH</b>\n` +
|
| 1949 |
`/scout [keyword] β Market viability scout\n` +
|
| 1950 |
`/costs β API spend breakdown\n` +
|
skills/api_caller.js
CHANGED
|
@@ -544,4 +544,26 @@ module.exports = {
|
|
| 544 |
return { success: false, error: error.message };
|
| 545 |
}
|
| 546 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
};
|
|
|
|
| 544 |
return { success: false, error: error.message };
|
| 545 |
}
|
| 546 |
},
|
| 547 |
+
|
| 548 |
+
// Telegram (Send Video)
|
| 549 |
+
sendTelegramVideo: async (chatId, filePath, caption = "") => {
|
| 550 |
+
const formData = new FormData();
|
| 551 |
+
formData.append('chat_id', chatId);
|
| 552 |
+
formData.append('video', fs.createReadStream(filePath));
|
| 553 |
+
formData.append('caption', caption);
|
| 554 |
+
formData.append('parse_mode', 'HTML');
|
| 555 |
+
|
| 556 |
+
console.log(`[Telegram] Sending video to ${chatId}: ${path.basename(filePath)}`);
|
| 557 |
+
try {
|
| 558 |
+
const response = await axiosIPv4.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendVideo`, formData, {
|
| 559 |
+
headers: formData.getHeaders(),
|
| 560 |
+
maxContentLength: 50 * 1024 * 1024,
|
| 561 |
+
maxBodyLength: 50 * 1024 * 1024,
|
| 562 |
+
});
|
| 563 |
+
return { success: true, data: response.data };
|
| 564 |
+
} catch (error) {
|
| 565 |
+
console.error("[Telegram] API Video Error:", error.response?.data || error.message);
|
| 566 |
+
return { success: false, error: error.message };
|
| 567 |
+
}
|
| 568 |
+
},
|
| 569 |
};
|
skills/set_telegram_menu.js
CHANGED
|
@@ -40,7 +40,8 @@ const setCommands = async () => {
|
|
| 40 |
{ command: 'costs', description: 'πΈ API Spend Breakdown' },
|
| 41 |
{ command: 'scout', description: 'π Market Viability Scout' },
|
| 42 |
{ command: 'seo2offer', description: 'π― Full SEO-to-Revenue Pipeline' },
|
| 43 |
-
{ command: 'landing', description: 'π Link-in-Bio Page' }
|
|
|
|
| 44 |
];
|
| 45 |
|
| 46 |
try {
|
|
|
|
| 40 |
{ command: 'costs', description: 'πΈ API Spend Breakdown' },
|
| 41 |
{ command: 'scout', description: 'π Market Viability Scout' },
|
| 42 |
{ command: 'seo2offer', description: 'π― Full SEO-to-Revenue Pipeline' },
|
| 43 |
+
{ command: 'landing', description: 'π Link-in-Bio Page' },
|
| 44 |
+
{ command: 'videos', description: 'π¬ AI Video Generator' }
|
| 45 |
];
|
| 46 |
|
| 47 |
try {
|
skills/video_engine.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const fs = require('fs');
|
| 2 |
+
const path = require('path');
|
| 3 |
+
const apiCaller = require('./api_caller');
|
| 4 |
+
const memory = require('./memory');
|
| 5 |
+
|
| 6 |
+
const REMOTION_URL = process.env.REMOTION_URL || 'https://AIgoose-remotion-studio.hf.space';
|
| 7 |
+
const REMOTION_KEY = process.env.REMOTION_API_KEY || '';
|
| 8 |
+
const DB_PATH = path.join(__dirname, '../database/video_db.json');
|
| 9 |
+
const TMP_DIR = path.join(__dirname, '../public/tmp/videos');
|
| 10 |
+
|
| 11 |
+
// Ensure dirs exist
|
| 12 |
+
if (!fs.existsSync(TMP_DIR)) fs.mkdirSync(TMP_DIR, { recursive: true });
|
| 13 |
+
|
| 14 |
+
function readDB() {
|
| 15 |
+
try {
|
| 16 |
+
if (fs.existsSync(DB_PATH)) return JSON.parse(fs.readFileSync(DB_PATH, 'utf8'));
|
| 17 |
+
} catch (e) { /* */ }
|
| 18 |
+
return { jobs: [], stats: { totalRendered: 0, totalFailed: 0 } };
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function writeDB(db) {
|
| 22 |
+
fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function remotionHeaders() {
|
| 26 |
+
const headers = { 'Content-Type': 'application/json' };
|
| 27 |
+
if (REMOTION_KEY) headers['Authorization'] = `Bearer ${REMOTION_KEY}`;
|
| 28 |
+
return headers;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const TEMPLATE_PROMPTS = {
|
| 32 |
+
'text-explainer': {
|
| 33 |
+
system: 'You are a content strategist. Create engaging explainer video content. Output JSON only.',
|
| 34 |
+
format: '{"title":"string","hook":"string","bullets":["string","string","string"],"cta":"string"}',
|
| 35 |
+
},
|
| 36 |
+
'product-promo': {
|
| 37 |
+
system: 'You are a sales copywriter. Create compelling product promo video content. Output JSON only.',
|
| 38 |
+
format: '{"productName":"string","price":"string","benefits":["string","string","string"],"cta":"string"}',
|
| 39 |
+
},
|
| 40 |
+
'social-short': {
|
| 41 |
+
system: 'You are a viral content creator for TikTok/Reels. Create scroll-stopping vertical video content. Output JSON only.',
|
| 42 |
+
format: '{"hook":"string","points":["string","string","string"],"cta":"string"}',
|
| 43 |
+
},
|
| 44 |
+
'seo-summary': {
|
| 45 |
+
system: 'You are an SEO content strategist. Create article summary video content. Output JSON only.',
|
| 46 |
+
format: '{"articleTitle":"string","keyPoints":["string","string","string"],"source":"VinOS Blog","cta":"Read the full article"}',
|
| 47 |
+
},
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
module.exports = {
|
| 51 |
+
async start(chatId, userText) {
|
| 52 |
+
const args = userText.replace(/^\/videos?\s*/i, '').trim();
|
| 53 |
+
|
| 54 |
+
if (!args || args === 'help') {
|
| 55 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 56 |
+
`π¬ <b>Video Engine</b>\n\n` +
|
| 57 |
+
`<b>Commands:</b>\n` +
|
| 58 |
+
`<code>/videos create [topic]</code> β AI generates + renders video\n` +
|
| 59 |
+
`<code>/videos create [topic] --template social-short</code> β specific template\n` +
|
| 60 |
+
`<code>/videos templates</code> β list available templates\n` +
|
| 61 |
+
`<code>/videos status</code> β show render queue\n` +
|
| 62 |
+
`<code>/videos status [id]</code> β check specific job\n\n` +
|
| 63 |
+
`<b>Templates:</b> text-explainer, product-promo, social-short, seo-summary\n\n` +
|
| 64 |
+
`π Web UI: ${REMOTION_URL}`
|
| 65 |
+
);
|
| 66 |
+
return;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if (args.toLowerCase() === 'templates') {
|
| 70 |
+
return await this.listTemplates(chatId);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if (args.toLowerCase().startsWith('status')) {
|
| 74 |
+
const jobId = args.replace(/^status\s*/i, '').trim();
|
| 75 |
+
return await this.checkStatus(chatId, jobId || null);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
if (args.toLowerCase().startsWith('create')) {
|
| 79 |
+
const createArgs = args.replace(/^create\s*/i, '').trim();
|
| 80 |
+
let template = 'text-explainer';
|
| 81 |
+
let topic = createArgs;
|
| 82 |
+
|
| 83 |
+
// Parse --template flag
|
| 84 |
+
const templateMatch = createArgs.match(/--template\s+(\S+)/i);
|
| 85 |
+
if (templateMatch) {
|
| 86 |
+
template = templateMatch[1];
|
| 87 |
+
topic = createArgs.replace(/--template\s+\S+/i, '').trim();
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if (!topic) {
|
| 91 |
+
await apiCaller.sendTelegramMessage(chatId, 'β οΈ Please provide a topic: <code>/videos create AI Productivity Tips</code>');
|
| 92 |
+
return;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
return await this.createVideo(chatId, topic, template);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
await apiCaller.sendTelegramMessage(chatId, 'β Unknown command. Try <code>/videos help</code>');
|
| 99 |
+
},
|
| 100 |
+
|
| 101 |
+
async listTemplates(chatId) {
|
| 102 |
+
try {
|
| 103 |
+
const res = await apiCaller.axiosIPv4.get(`${REMOTION_URL}/api/templates`, {
|
| 104 |
+
headers: remotionHeaders(),
|
| 105 |
+
timeout: 10000,
|
| 106 |
+
});
|
| 107 |
+
const templates = res.data.templates || [];
|
| 108 |
+
|
| 109 |
+
let msg = 'π¬ <b>Video Templates</b>\n\n';
|
| 110 |
+
for (const t of templates) {
|
| 111 |
+
msg += `π <b>${t.name}</b> [${t.ratio}]\n`;
|
| 112 |
+
msg += `${t.description}\n`;
|
| 113 |
+
msg += `Key: <code>${t.key}</code>\n\n`;
|
| 114 |
+
}
|
| 115 |
+
msg += `Usage: <code>/videos create [topic] --template [key]</code>`;
|
| 116 |
+
await apiCaller.sendTelegramMessage(chatId, msg);
|
| 117 |
+
} catch (e) {
|
| 118 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 119 |
+
`β οΈ Remotion Studio unreachable.\nπ ${REMOTION_URL}\nError: ${e.message}`
|
| 120 |
+
);
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
|
| 124 |
+
async createVideo(chatId, topic, template = 'text-explainer') {
|
| 125 |
+
const promptConfig = TEMPLATE_PROMPTS[template];
|
| 126 |
+
if (!promptConfig) {
|
| 127 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 128 |
+
`β οΈ Unknown template: ${template}\nAvailable: ${Object.keys(TEMPLATE_PROMPTS).join(', ')}`
|
| 129 |
+
);
|
| 130 |
+
return;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 134 |
+
`π¬ <b>Video Pipeline Started</b>\n\n` +
|
| 135 |
+
`π Topic: ${topic}\n` +
|
| 136 |
+
`π Template: ${template}\n` +
|
| 137 |
+
`[β β‘β‘β‘β‘] 20% β Generating AI script...`
|
| 138 |
+
);
|
| 139 |
+
|
| 140 |
+
// Step 1: Generate script via LLM
|
| 141 |
+
const prompt = [
|
| 142 |
+
{ role: 'system', content: promptConfig.system },
|
| 143 |
+
{ role: 'user', content: `Topic: "${topic}"\n\nCreate compelling video content for this topic.\nOutput format: ${promptConfig.format}` },
|
| 144 |
+
];
|
| 145 |
+
|
| 146 |
+
const aiResult = await apiCaller.callOpenRouter(prompt);
|
| 147 |
+
let inputProps = {};
|
| 148 |
+
|
| 149 |
+
if (aiResult.success) {
|
| 150 |
+
try {
|
| 151 |
+
inputProps = JSON.parse(aiResult.data.replace(/```json?\n?|```/g, '').trim());
|
| 152 |
+
} catch (e) {
|
| 153 |
+
await apiCaller.sendTelegramMessage(chatId, 'β οΈ AI script generation failed (parse error). Retrying with defaults...');
|
| 154 |
+
inputProps = this._getDefaultProps(template, topic);
|
| 155 |
+
}
|
| 156 |
+
} else {
|
| 157 |
+
inputProps = this._getDefaultProps(template, topic);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Add brand color
|
| 161 |
+
inputProps.brandColor = inputProps.brandColor || '#22d3a0';
|
| 162 |
+
|
| 163 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 164 |
+
`[β β β‘β‘β‘] 40% β Script ready. Sending to Remotion Studio...`
|
| 165 |
+
);
|
| 166 |
+
|
| 167 |
+
// Step 2: Submit render to Remotion Studio
|
| 168 |
+
try {
|
| 169 |
+
const res = await apiCaller.axiosIPv4.post(`${REMOTION_URL}/api/render`, {
|
| 170 |
+
template,
|
| 171 |
+
inputProps,
|
| 172 |
+
duration: 30,
|
| 173 |
+
}, {
|
| 174 |
+
headers: remotionHeaders(),
|
| 175 |
+
timeout: 15000,
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
const { jobId, estimatedTime } = res.data;
|
| 179 |
+
|
| 180 |
+
// Save to local DB
|
| 181 |
+
const db = readDB();
|
| 182 |
+
db.jobs.push({
|
| 183 |
+
id: jobId,
|
| 184 |
+
chatId: String(chatId),
|
| 185 |
+
topic,
|
| 186 |
+
template,
|
| 187 |
+
inputProps,
|
| 188 |
+
status: 'rendering',
|
| 189 |
+
created: new Date().toISOString(),
|
| 190 |
+
});
|
| 191 |
+
writeDB(db);
|
| 192 |
+
|
| 193 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 194 |
+
`[β β β β‘β‘] 60% β Rendering on Remotion Studio...\n\n` +
|
| 195 |
+
`π Job: <code>${jobId}</code>\n` +
|
| 196 |
+
`β± Estimated: ${estimatedTime}\n\n` +
|
| 197 |
+
`I'll notify you when it's done!\nCheck progress: <code>/videos status ${jobId}</code>`
|
| 198 |
+
);
|
| 199 |
+
|
| 200 |
+
// Step 3: Poll for completion
|
| 201 |
+
this._pollAndDeliver(chatId, jobId);
|
| 202 |
+
|
| 203 |
+
} catch (e) {
|
| 204 |
+
const errMsg = e.response?.data?.error || e.message;
|
| 205 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 206 |
+
`β οΈ <b>Render submission failed</b>\n${errMsg}\n\nπ Try the web UI: ${REMOTION_URL}`
|
| 207 |
+
);
|
| 208 |
+
}
|
| 209 |
+
},
|
| 210 |
+
|
| 211 |
+
async _pollAndDeliver(chatId, jobId) {
|
| 212 |
+
const maxAttempts = 60; // 60 * 3s = 3 min max
|
| 213 |
+
let attempts = 0;
|
| 214 |
+
|
| 215 |
+
const poll = async () => {
|
| 216 |
+
attempts++;
|
| 217 |
+
if (attempts > maxAttempts) {
|
| 218 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 219 |
+
`β° Render timed out for job <code>${jobId}</code>.\nCheck: ${REMOTION_URL}`
|
| 220 |
+
);
|
| 221 |
+
return;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
try {
|
| 225 |
+
const res = await apiCaller.axiosIPv4.get(`${REMOTION_URL}/api/jobs/${jobId}`, {
|
| 226 |
+
headers: remotionHeaders(),
|
| 227 |
+
timeout: 10000,
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
const job = res.data;
|
| 231 |
+
|
| 232 |
+
if (job.status === 'complete') {
|
| 233 |
+
// Update local DB
|
| 234 |
+
const db = readDB();
|
| 235 |
+
const local = db.jobs.find(j => j.id === jobId);
|
| 236 |
+
if (local) { local.status = 'complete'; local.completed = new Date().toISOString(); }
|
| 237 |
+
db.stats.totalRendered++;
|
| 238 |
+
writeDB(db);
|
| 239 |
+
|
| 240 |
+
// Download video
|
| 241 |
+
const videoPath = path.join(TMP_DIR, `${jobId}.mp4`);
|
| 242 |
+
try {
|
| 243 |
+
const dlRes = await apiCaller.axiosIPv4.get(`${REMOTION_URL}/api/download/${jobId}`, {
|
| 244 |
+
responseType: 'arraybuffer',
|
| 245 |
+
timeout: 60000,
|
| 246 |
+
});
|
| 247 |
+
fs.writeFileSync(videoPath, Buffer.from(dlRes.data));
|
| 248 |
+
|
| 249 |
+
// Send to Telegram
|
| 250 |
+
await apiCaller.sendTelegramVideo(chatId, videoPath,
|
| 251 |
+
`π¬ <b>Video Ready!</b>\nπ ${job.inputProps?.title || job.inputProps?.hook || 'Video'}\nπ ${job.template}`
|
| 252 |
+
);
|
| 253 |
+
|
| 254 |
+
// Cleanup local copy after sending
|
| 255 |
+
setTimeout(() => {
|
| 256 |
+
try { if (fs.existsSync(videoPath)) fs.unlinkSync(videoPath); } catch (e) { /* */ }
|
| 257 |
+
}, 60000);
|
| 258 |
+
|
| 259 |
+
} catch (dlErr) {
|
| 260 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 261 |
+
`β
<b>Video rendered!</b>\n\n` +
|
| 262 |
+
`π Download: ${REMOTION_URL}/api/download/${jobId}\n` +
|
| 263 |
+
`(Direct send failed: ${dlErr.message})`
|
| 264 |
+
);
|
| 265 |
+
}
|
| 266 |
+
return;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
if (job.status === 'failed') {
|
| 270 |
+
const db = readDB();
|
| 271 |
+
const local = db.jobs.find(j => j.id === jobId);
|
| 272 |
+
if (local) { local.status = 'failed'; local.error = job.error; }
|
| 273 |
+
db.stats.totalFailed++;
|
| 274 |
+
writeDB(db);
|
| 275 |
+
|
| 276 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 277 |
+
`β <b>Render failed</b>\nπ ${jobId}\nπ¬ ${job.error || 'Unknown error'}`
|
| 278 |
+
);
|
| 279 |
+
return;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
// Still rendering β poll again
|
| 283 |
+
setTimeout(poll, 3000);
|
| 284 |
+
} catch (e) {
|
| 285 |
+
// Network error β retry
|
| 286 |
+
setTimeout(poll, 5000);
|
| 287 |
+
}
|
| 288 |
+
};
|
| 289 |
+
|
| 290 |
+
setTimeout(poll, 5000); // First check after 5s
|
| 291 |
+
},
|
| 292 |
+
|
| 293 |
+
async checkStatus(chatId, jobId) {
|
| 294 |
+
if (jobId) {
|
| 295 |
+
// Check specific job
|
| 296 |
+
try {
|
| 297 |
+
const res = await apiCaller.axiosIPv4.get(`${REMOTION_URL}/api/jobs/${jobId}`, {
|
| 298 |
+
headers: remotionHeaders(),
|
| 299 |
+
timeout: 10000,
|
| 300 |
+
});
|
| 301 |
+
const j = res.data;
|
| 302 |
+
await apiCaller.sendTelegramMessage(chatId,
|
| 303 |
+
`π¬ <b>Job Status</b>\n\n` +
|
| 304 |
+
`π ${j.id}\n` +
|
| 305 |
+
`π Template: ${j.template}\n` +
|
| 306 |
+
`π Status: ${j.status}${j.progress ? ` (${j.progress}%)` : ''}\n` +
|
| 307 |
+
`π
Created: ${new Date(j.created).toLocaleString()}\n` +
|
| 308 |
+
(j.status === 'complete' ? `π Download: ${REMOTION_URL}/api/download/${j.id}\n` : '') +
|
| 309 |
+
(j.error ? `β Error: ${j.error}\n` : '')
|
| 310 |
+
);
|
| 311 |
+
} catch (e) {
|
| 312 |
+
await apiCaller.sendTelegramMessage(chatId, `β οΈ Job not found or Remotion unreachable: ${e.message}`);
|
| 313 |
+
}
|
| 314 |
+
} else {
|
| 315 |
+
// List recent jobs
|
| 316 |
+
try {
|
| 317 |
+
const res = await apiCaller.axiosIPv4.get(`${REMOTION_URL}/api/jobs?limit=5`, {
|
| 318 |
+
headers: remotionHeaders(),
|
| 319 |
+
timeout: 10000,
|
| 320 |
+
});
|
| 321 |
+
const jobList = res.data.jobs || [];
|
| 322 |
+
if (jobList.length === 0) {
|
| 323 |
+
await apiCaller.sendTelegramMessage(chatId, 'π No video jobs yet. Create one: <code>/videos create [topic]</code>');
|
| 324 |
+
return;
|
| 325 |
+
}
|
| 326 |
+
let msg = 'π¬ <b>Recent Video Jobs</b>\n\n';
|
| 327 |
+
for (const j of jobList) {
|
| 328 |
+
const icon = j.status === 'complete' ? 'β
' : j.status === 'rendering' ? 'β³' : j.status === 'queued' ? 'π' : 'β';
|
| 329 |
+
msg += `${icon} <code>${j.id}</code>\n ${j.template} β ${j.status}${j.progress ? ` (${j.progress}%)` : ''}\n\n`;
|
| 330 |
+
}
|
| 331 |
+
await apiCaller.sendTelegramMessage(chatId, msg);
|
| 332 |
+
} catch (e) {
|
| 333 |
+
await apiCaller.sendTelegramMessage(chatId, `β οΈ Remotion Studio unreachable: ${e.message}`);
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
},
|
| 337 |
+
|
| 338 |
+
_getDefaultProps(template, topic) {
|
| 339 |
+
const defaults = {
|
| 340 |
+
'text-explainer': { title: topic, hook: `Everything you need to know about ${topic}`, bullets: [`Key insight about ${topic}`, `Why ${topic} matters`, `How to get started`], cta: 'Learn More' },
|
| 341 |
+
'product-promo': { productName: topic, price: 'Rp 49.000', benefits: [`Master ${topic}`, 'Step-by-step guide', 'Lifetime access'], cta: 'Get It Now' },
|
| 342 |
+
'social-short': { hook: `Stop scrolling. ${topic} is changing everything.`, points: [`${topic} saves you hours`, `Most people don't know this`, 'Here\'s the proof'], cta: 'Link in bio' },
|
| 343 |
+
'seo-summary': { articleTitle: topic, keyPoints: [`Key finding about ${topic}`, 'The data behind it', 'What to do next'], source: 'VinOS Blog', cta: 'Read the full article' },
|
| 344 |
+
};
|
| 345 |
+
return defaults[template] || defaults['text-explainer'];
|
| 346 |
+
},
|
| 347 |
+
|
| 348 |
+
getDashboardData() {
|
| 349 |
+
const db = readDB();
|
| 350 |
+
return {
|
| 351 |
+
jobs: db.jobs.slice(-20).reverse(),
|
| 352 |
+
stats: db.stats,
|
| 353 |
+
remotionUrl: REMOTION_URL,
|
| 354 |
+
};
|
| 355 |
+
},
|
| 356 |
+
};
|