everything / apps /viralcat.js
everydaycats's picture
Update apps/viralcat.js
eccb28b verified
// apps/viralcat.js
import express from 'express';
import multer from 'multer';
import { generateCompletion } from '../ai_engine.js';
import { supabase } from '../config/supabaseClient.js';
import fetch from 'node-fetch';
const router = express.Router();
const FASTAPI_URL = process.env.FASTAPI_SERVER_URL || 'http://localhost:8000';
const upload = multer({ storage: multer.memoryStorage() });
const adminAuth = (req, res, next) => {
const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
const[login, password] = Buffer.from(b64auth, 'base64').toString().split(':');
if (login === 'admin' && password === process.env.VIRAL_CAT_ADMIN_PASS) return next();
res.set('WWW-Authenticate', 'Basic realm="Viral Cat Admin"');
res.status(401).send('Auth required.');
};
const getBase64Payload = (req) => {
if (req.file) return req.file.buffer.toString('base64');
if (req.body.video_base64) return req.body.video_base64;
return null;
};
const handleMediaProcessing = async (req, num_frames = 14, get_transcript = true) => {
const video_base64 = getBase64Payload(req);
if (!video_base64) throw new Error("No video data found in request");
const mediaRes = await fetch(`${FASTAPI_URL}/process-video`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ video_base64, num_frames, get_transcript })
});
const mediaData = await mediaRes.json();
if (!mediaData.success) throw new Error(mediaData.error);
return mediaData;
};
// โ”€โ”€ PARALLEL MAP-REDUCE WITH OPENROUTER โ”€โ”€
const performDeepAnalysis = async (frames, transcript, targetNiche = null) => {
console.log(`[DEEP ANALYZE] Starting Parallel Map-Reduce on ${frames.length} frames...`);
// 1. Cap at 14 frames max
const cappedFrames = frames.slice(0, 14);
const chunks =[];
// 2. Chunk into batches of 7 (OpenRouter's max attachment limit)
for (let i = 0; i < cappedFrames.length; i += 7) {
chunks.push(cappedFrames.slice(i, i + 7));
}
// 3. Process all chunks AT THE SAME TIME (No more sleep delays!)
console.log(`-> Firing ${chunks.length} parallel worker instances to OpenRouter...`);
const scenePromises = chunks.map((chunk, index) => {
const prompt = `Analyze this ${chunk.length}-image sequence. Return ONLY a JSON object with a single key "description" containing a brief summary of the action, camera movement, lighting, and subjects.`;
return generateCompletion({
model: "qwen", // Qwen is cheapest and fastest for this vision task
prompt,
system_prompt: "You are a precise video frame analyzer. Output strictly JSON.",
images: chunk
});
});
// Wait for all workers to finish simultaneously
const sceneResults = await Promise.all(scenePromises);
const sceneDescriptions = sceneResults.map((res, i) => {
try {
const parsed = JSON.parse(res.data);
return `[Scene Segment ${i + 1}]:\n${parsed.description}`;
} catch (e) {
return `[Scene Segment ${i + 1}]:\n${res.data}`; // Fallback if JSON parse fails
}
});
const combinedVisualData = sceneDescriptions.join("\n\n");
console.log(`[DEEP ANALYZE] Map phase complete. Synthesizing output...`);
if (targetNiche) {
// ๐Ÿšจ REDUCE PHASE: Custom Remix
const finalPrompt = `
TASK: Rewrite an existing viral video script for a NEW NICHE.
[ORIGINAL VISUAL TIMELINE]
${combinedVisualData}[ORIGINAL AUDIO TRANSCRIPT]
${transcript}
[NEW TARGET NICHE]
User's Niche: "${targetNiche}"
INSTRUCTIONS:
1. DECONSTRUCT THE AURA: Analyze exactly what made the original video go viral.
2. Map those EXACT psychological triggers onto the user's new niche.
3. Return ONLY a JSON object with a single key "script_markdown" containing the fully formatted markdown script.
Markdown format inside the JSON string should be:
## ๐Ÿง  The Viral Aura
[Explanation]
## ๐ŸŽฌ The Hook (0-3s)
**Visual:** [What to show]
**Audio:** [What to say]
## ๐Ÿ“– The Body
...etc`;
const result = await generateCompletion({ model: "maverick", prompt: finalPrompt, system_prompt: "You are Viral Cat ๐Ÿ“ˆ๐Ÿˆ. Output strictly JSON." });
return { success: true, data: JSON.parse(result.data).script_markdown };
} else {
// ๐Ÿšจ REDUCE PHASE: Database Template Extraction
const finalPrompt = `[VISUAL TIMELINE]\n${combinedVisualData}\n[TRANSCRIPT]\n${transcript}\n
Analyze the data and return ONLY a JSON object with these exact keys:
{
"transcript": "Cleaned dialogue",
"environment": "Setting/lighting description",
"pacing": "Edit style and camera movement",
"viral_factors": "Analyze the psychology, specific word choices, hooks, and timing that made this viral"
}`;
return await generateCompletion({ model: "maverick", prompt: finalPrompt, system_prompt: "Elite metadata extractor. Output strictly JSON." });
}
};
router.get('/trending', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const search = req.query.search || '';
const from = (page - 1) * limit;
const to = from + limit - 1;
let query = supabase.from('viral_cat_trending').select('*', { count: 'exact' });
if (search) {
query = query.or(`title.ilike.%${search}%,platform.ilike.%${search}%`);
}
const { data, count, error } = await query.order('created_at', { ascending: false }).range(from, to);
if (error) throw error;
const hasMore = (from + data.length) < count;
res.json({ success: true, data, hasMore, total: count });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
router.post('/admin/template', upload.single('video_file'), async (req, res) => {
try {
const { title, video_url, platform, use_deep_analysis } = req.body;
const isDeep = (use_deep_analysis === 'true' || use_deep_analysis === true);
// Max 14 frames for deep, 7 for fast (fits in 1 OpenRouter request)
const framesToExtract = isDeep ? 14 : 7;
const mediaData = await handleMediaProcessing(req, framesToExtract, true);
let aiResult;
if (isDeep) {
aiResult = await performDeepAnalysis(mediaData.frames, mediaData.transcript);
} else {
const fastFrames = mediaData.frames.slice(0, 7);
const aiPrompt = `Analyze transcript and frames.\nTRANSCRIPT:\n${mediaData.transcript}\n
Return ONLY a JSON object with these exact keys:
{
"transcript": "Cleaned dialogue",
"environment": "Setting/lighting description",
"pacing": "Edit style and camera movement",
"viral_factors": "Identify psychological hooks and word choices"
}`;
aiResult = await generateCompletion({ model: "maverick", prompt: aiPrompt, images: fastFrames, system_prompt: "Fast metadata extractor. Output strictly JSON." });
}
// ๐Ÿšจ CLEAN JSON PARSING (No more Regex!)
const parsedData = JSON.parse(aiResult.data);
const { error } = await supabase.from('viral_cat_trending').insert([{
platform, video_url, title,
thumbnail_url: `data:image/jpeg;base64,${mediaData.thumbnail}`,
transcript: parsedData.transcript || mediaData.transcript,
ai_environment_data: parsedData.environment || "N/A",
ai_scene_changes: parsedData.pacing || "N/A",
ai_viral_factors: parsedData.viral_factors || "N/A"
}]);
if (error) throw error;
res.json({ success: true });
} catch (err) {
console.error("Admin Error:", err.message);
res.status(500).json({ success: false, error: err.message });
}
});
router.post('/remix', async (req, res) => {
try {
const { user_input, transcript, ai_environment_data, ai_scene_changes, ai_viral_factors } = req.body;
const aiPrompt = `TASK: Rewrite script for a NEW NICHE.
[ORIGINAL DATA]
Pacing: ${ai_scene_changes}
Environment: ${ai_environment_data}
Viral Psychology (The Aura): ${ai_viral_factors}
Transcript: ${transcript}
[NEW NICHE]
"${user_input}"
INSTRUCTIONS: Map the original viral psychology and pacing onto the user's new niche.
Return ONLY a JSON object with a single key "script_markdown" containing the fully formatted markdown script.`;
const result = await generateCompletion({ model: "maverick", prompt: aiPrompt, system_prompt: "You are Viral Cat ๐Ÿ“ˆ๐Ÿˆ. Output strictly JSON." });
// Extract the markdown string from the JSON response
const finalMarkdown = JSON.parse(result.data).script_markdown;
res.json({ success: true, data: finalMarkdown });
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
});
router.post('/custom_remix', upload.single('video_file'), async (req, res) => {
try {
const { user_niche } = req.body;
const mediaData = await handleMediaProcessing(req, 14, true);
const result = await performDeepAnalysis(mediaData.frames, mediaData.transcript, user_niche);
res.json({ success: true, data: result.data, thumbnail: mediaData.thumbnail });
} catch (err) {
console.error("Custom Remix Error:", err.message);
res.status(500).json({ success: false, error: err.message });
}
});
router.delete('/admin/template/:id', async (req, res) => {
await supabase.from('viral_cat_trending').delete().eq('id', req.params.id);
res.json({ success: true });
});
// Admin Dashboard HTML (Unchanged)
router.get('/admin', (req, res) => {
res.send(`<!DOCTYPE html><html><head><title>Viral Cat Admin</title><script src="https://cdn.tailwindcss.com"></script></head><body class="bg-[#090A0F] text-white p-8"><h1 class="text-3xl font-bold text-[#12D8C3] mb-8">Viral Cat Admin</h1><div class="grid grid-cols-2 gap-8"><form id="f" class="space-y-4 bg-[#16181F] p-6 rounded-2xl"><input name="title" placeholder="Title" class="w-full p-2 bg-black border border-gray-700 rounded" required/><input name="video_url" placeholder="Embed URL" class="w-full p-2 bg-black border border-gray-700 rounded" required/><select name="platform" class="w-full p-2 bg-black border border-gray-700 rounded"><option value="tiktok">TikTok</option><option value="instagram">Instagram</option><option value="youtube">YouTube</option></select><div class="flex items-center gap-2 mt-2 mb-2"><input type="checkbox" id="deep" name="use_deep_analysis" value="true" checked class="w-5 h-5 accent-[#12D8C3]"><label for="deep" class="text-gray-300 text-sm">Use Map-Reduce Deep Analysis (Slower but 10x more accurate)</label></div><input type="file" id="video_file" accept="video/mp4" class="w-full" required/><button type="submit" id="submitBtn" class="w-full bg-[#12D8C3] text-black p-3 rounded font-bold">Process Video</button></form><div id="list" class="space-y-2"></div></div><script>const toB64 = f => new Promise((res,rej)=>{const r=new FileReader();r.readAsDataURL(f);r.onload=()=>res(r.result.split(',')[1]);});async function load(){const r=await fetch('/api/viralcat/trending');const j=await r.json();document.getElementById('list').innerHTML=j.data.map(t=>\`<div class="p-4 bg-gray-900 flex justify-between rounded-lg"><div><span class="text-xs bg-[#12D8C3] text-black px-2 py-1 rounded font-bold">\${t.platform.toUpperCase()}</span><p class="text-sm mt-1 font-bold">\${t.title}</p></div><button onclick="del('\${t.id}')" class="text-red-500">Delete</button></div>\`).join('');} async function del(id){await fetch('/api/viralcat/admin/template/'+id,{method:'DELETE'});load();} document.getElementById('f').onsubmit=async e=>{e.preventDefault();const b=e.target.querySelector('button'); const isDeep = document.getElementById('deep').checked; b.innerText = isDeep ? "Running Deep Map-Reduce... (Wait ~30s)" : "Processing... (Wait ~10s)"; b.disabled = true; try { const b64 = await toB64(document.getElementById('video_file').files[0]); const payload = { title: e.target.title.value, video_url: e.target.video_url.value, platform: e.target.platform.value, use_deep_analysis: isDeep, video_base64: b64 }; const res = await fetch('/api/viralcat/admin/template', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) }); if(res.ok) { alert("Done!"); load(); } else { alert("Error: " + await res.text()); } } catch(err) { alert(err.message); } b.innerText="Process Video"; b.disabled=false;}; load();</script></body></html>`);
});
export default router;