GROUND
FLOOR
NEWS
Ground Floor News
Live News Intelligence · Multi-Source · Bias Aware
Loading...
↺ Refresh
🔴 BREAKING
Loading headlines...
🌐 All
⭐ Top
🌍 World
🏛️ Politics
💼 Business
💻 Tech
🔬 Science
🏥 Health
🏆 Sports
🎬 Entertainment
◀ Left
◼ Center
▶ Right
Sources:
–
Stories:
–
Articles:
–
Selected for video:
0
Bias:
Left
Center
Right
Loading news from 13 sources...
✕
'; let _ytPollTimer = null; // ─── Bias helpers ──────────────────────────────────────────────────────────── const BIAS_COLORS = {LL:'#1d4ed8',L:'#3b82f6',CL:'#60a5fa',C:'#818cf8',CR:'#f97316',R:'#ef4444',RR:'#991b1b'}; const BIAS_LABELS = {LL:'Far Left',L:'Left',CL:'Ctr-Left',C:'Center',CR:'Ctr-Right',R:'Right',RR:'Far Right'}; function biasColor(b) { return BIAS_COLORS[b] || '#6366f1'; } function biasLabel(b) { return BIAS_LABELS[b] || b; } function biasGroup(b) { return ['LL','L','CL'].includes(b)?'left':['CR','R','RR'].includes(b)?'right':'center'; } // ─── Time helpers ───────────────────────────────────────────────────────────── function ageLabel(h) { if (h < 0.1) return 'Just now'; if (h < 1) return `${Math.round(h*60)}m ago`; if (h < 24) return `${Math.round(h)}h ago`; return `${Math.round(h/24)}d ago`; } // ── VM stubs (video maker removed in this build) ── function openVideoMaker(){} function closeVideoMaker(){} function buildVideo(){} function renderVmSelList(){} // ─── Load news ──────────────────────────────────────────────────────────────── async function refreshNews() { document.getElementById('newsGrid').innerHTML = '
Fetching from 43 sources...
'; try { const res = await fetch('/api/news'); const data = await res.json(); allClusters = data.clusters || []; renderNews(); updateStats(data); updateTicker(data.clusters); document.getElementById('lastUpdate').textContent = 'Updated ' + new Date().toLocaleTimeString(); } catch(e) { document.getElementById('newsGrid').innerHTML = '
Failed to load news. Check console.
'; console.error(e); } } // ── Inject in-feed ads every 6 clusters ── function injectInFeedAds() { const grid = document.getElementById('newsGrid'); const cards = [...grid.children]; cards.forEach((card, i) => { if ((i + 1) % 6 === 0) { const ad = document.createElement('div'); ad.className = 'news-card'; ad.style.cssText = 'display:flex;align-items:center;justify-content:center;min-height:120px;background:rgba(255,255,255,0.02);border:1px dashed rgba(255,255,255,0.1)'; ad.innerHTML = `
`; card.insertAdjacentElement('afterend', ad); if (window.adsbygoogle) (adsbygoogle = window.adsbygoogle || []).push({}); } }); } function updateStats(data) { document.getElementById('srcCount').textContent = data.source_count || '–'; document.getElementById('storyCount').textContent = data.cluster_count || '–'; document.getElementById('artCount').textContent = data.total_count || data.article_count || '–'; } function updateTicker(clusters) { const headlines = (clusters||[]).slice(0,12).map(c=>c[0].title).join(' · '); document.getElementById('tickerText').textContent = '⚡ ' + headlines; } // ─── Category filter ────────────────────────────────────────────────────────── function setCategory(cat) { currentCat = cat; document.querySelectorAll('.cat-btn').forEach(b => b.classList.remove('active')); event.target.classList.add('active'); renderNews(); } function filterClusters(clusters) { if (currentCat === 'all') return clusters; if (currentCat === 'left') return clusters.filter(c=>c.some(a=>['LL','L','CL'].includes(a.bias))); if (currentCat === 'center') return clusters.filter(c=>c.some(a=>a.bias==='C')); if (currentCat === 'right') return clusters.filter(c=>c.some(a=>['CR','R','RR'].includes(a.bias))); return clusters.filter(c=>c.some(a=>(a.cat||[]).includes(currentCat))); } // ─── Render news grid ───────────────────────────────────────────────────────── function renderNews() { const grid = document.getElementById('newsGrid'); const clusters = filterClusters(allClusters); if (!clusters.length) { grid.innerHTML = '
No stories in this category yet.
'; return; } grid.innerHTML = clusters.slice(0,200).map(renderCluster).join(''); injectInFeedAds(); } function renderCluster(cluster) { const a = cluster[0]; const isAdded = selectedArts.some(s=>s.url===a.url); const leftCount = cluster.filter(x=>['LL','L','CL'].includes(x.bias)).length; const centerCount = cluster.filter(x=>x.bias==='C').length; const rightCount = cluster.filter(x=>['CR','R','RR'].includes(x.bias)).length; const total = cluster.length; const newsEmoji = pickEmoji(a.title); const extraSrcs = cluster.slice(1,4).map(x=> `
${x.source}
` ).join(''); // Use data-url to avoid special-character issues in onclick strings const safeUrl = a.url.replace(/"/g,'"'); return `
${newsEmoji}
${a.source}
${biasLabel(a.bias)}
${ageLabel(a.age_h||0)}
${a.title}
${a.desc ? `
${a.desc}
` : ''} ${cluster.length>1 ? `
${extraSrcs}${cluster.length>4?`
+${cluster.length-4} more
`:''}
`:''}
`; } function pickEmoji(title) { const t = title.toLowerCase(); if (/war|attack|bomb|missile|military|shooting/.test(t)) return '⚔️'; if (/election|vote|president|congress|senate|democrat|republican/.test(t)) return '🏛️'; if (/economy|market|stock|trade|inflation|gdp|bank/.test(t)) return '📈'; if (/climate|storm|hurricane|flood|fire|earthquake/.test(t)) return '🌪️'; if (/health|covid|vaccine|hospital|disease|cancer/.test(t)) return '🏥'; if (/tech|ai|apple|google|microsoft|elon|space|nasa/.test(t)) return '🚀'; if (/crime|police|arrest|court|judge|trial/.test(t)) return '⚖️'; if (/sports|game|championship|nfl|nba|soccer/.test(t)) return '🏆'; if (/china|russia|ukraine|israel|iran|europe/.test(t)) return '🌍'; return '📰'; } // ─── Story selection for video ──────────────────────────────────────────────── // Uses article URL as the stable unique key (avoids hash collision issues) function findClusterByUrl(url) { for (const c of allClusters) { if (c.some(a=>a.url===url)) return c; } return null; } function toggleStory(url) { if (!url) return; const idx = selectedArts.findIndex(a=>a.url===url); if (idx >= 0) { selectedArts.splice(idx,1); } else { // Prevent duplicates (safety guard) const cluster = findClusterByUrl(url); if (cluster && !selectedArts.some(a=>a.url===cluster[0].url)) { selectedArts.push({...cluster[0], cluster}); } } updateSelCount(); // Only re-render news grid if visible (not inside video maker) if (true) { renderNews(); } else { // Inside video maker — just update the card buttons via class toggle document.querySelectorAll('.add-btn').forEach(btn=>{ const burl = btn.dataset.url; const added = selectedArts.some(a=>a.url===burl); btn.classList.toggle('added', added); btn.textContent = added ? '✓ Added' : '+ Video'; }); } renderVmSelList(); } function updateSelCount() { const n = selectedArts.length; const el1 = document.getElementById('selCount'); const el2 = document.getElementById('vmSelCount'); if (el1) el1.textContent = n; if (el2) el2.textContent = n; } function renderVmSelList() { const el = document.getElementById('vmSelList'); if (!el) return; if (!selectedArts.length) { el.innerHTML = '
No stories selected. Add from the news feed or auto-select below.
'; return; } el.innerHTML = selectedArts.map((a,i)=>`
${i+1}
${a.title}
${a.source} · ${biasLabel(a.bias)} · ${ageLabel(a.age_h||0)}
✕
`).join(''); } function autoSelectStories() { selectedArts = []; const wpm = parseInt(document.getElementById('vmSpeed').value) || 175; // ~130 words per story in a broadcast script (intro + body + transition) const wordsPerStory = 130; const targetWords = Math.round(vmDuration * wpm / 60); const needed = Math.min(40, Math.max(1, Math.ceil(targetWords / wordsPerStory))); // Respect current category filter when building the pool const pool = allClusters.filter(c => { if (!c.length) return false; if (currentCat === 'all') return true; return c.some(a => { if (currentCat === 'left') return ['LL','L','CL'].includes(a.bias); if (currentCat === 'center') return a.bias === 'C'; if (currentCat === 'right') return ['CR','R','RR'].includes(a.bias); return (a.cat||[]).includes(currentCat); }); }); // Sort by cluster size (most-covered = most newsworthy), then recency const top = pool .sort((a,b) => b.length - a.length || (a[0].age_h||99) - (b[0].age_h||99)) .slice(0, needed); top.forEach(c => selectedArts.push({...c[0], cluster:c})); updateSelCount(); renderVmSelList(); renderNews(); // Show feedback const mins = Math.round(vmDuration/60); const el = document.getElementById('vmAutoFillMsg'); if (el) { el.textContent = `✓ ${top.length} stories selected for ~${mins} min video`; el.style.display='block'; setTimeout(()=>el.style.display='none',4000); } } function clearSelectedStories() { selectedArts = []; updateSelCount(); renderVmSelList(); renderNews(); } // ─── Article Detail ──────────────────────────────────────────────────────────── function openDetail(url) { const cluster = findClusterByUrl(url); if (!cluster) return; const a = cluster[0]; const isAdded = selectedArts.some(s=>s.url===a.url); document.getElementById('detailContent').innerHTML = `
${biasLabel(a.bias)}
${a.source} · ${ageLabel(a.age_h||0)}
${a.title}
${a.desc?`
${a.desc}
`:''}
Read full article →
${cluster.length>1?`
Also covered by ${cluster.length-1} other source${cluster.length>2?'s':''}
${cluster.slice(1).map(x=>`
${biasLabel(x.bias)}
${x.source}
Read →
`).join('')}
`:''}
${isAdded?'✓ Added to Video':'+ Add to Video'}
`; document.getElementById('detailOverlay').classList.add('active'); } function closeDetail(e) { if (!e || e.target === document.getElementById('detailOverlay')) { document.getElementById('detailOverlay').classList.remove('active'); } } function openVideoMaker() { renderVmSelList(); // vm removed loadVoices(); } function closeVideoMaker() { // vm removed } // ─── Clickbait Title Generator ──────────────────────────────────────────────── function genClickbaitTitle() { const hooks = [ 'You Won\'t Believe What Happened Today', 'BREAKING: Everything You Need to Know Right Now', 'The Story the Media Isn\'t Telling You', 'This Changes EVERYTHING — Top Stories Explained', 'What\'s REALLY Happening in the World Today', 'They Don\'t Want You To See This News', 'URGENT: Major Developments You Can\'t Miss', 'The Truth Behind Today\'s Biggest Headlines', 'Mainstream Media Won\'t Cover THIS — We Will', 'EXPOSED: The Headlines Behind the Headlines', 'Everything Is Happening At Once — Here\'s The Breakdown', 'This Is Bigger Than They\'re Letting On', 'Today\'s News Will Leave You Speechless', 'The Real Story Nobody\'s Talking About', 'WARNING: These Headlines Are About to Change Everything', ]; const top = selectedArts.length ? selectedArts[0].title : ''; const now = new Date(); const mo = now.toLocaleString('en-US',{month:'long'}); const yr = now.getFullYear(); // Pick a hook and optionally append a topic keyword from the top story const hook = hooks[Math.floor(Math.random()*hooks.length)]; let title = hook; if (top) { // Extract the most dramatic 3-4 word phrase from the top story const kws = top.replace(/[^\w\s]/g,'').split(/\s+/).filter(w=>w.length>4).slice(0,4).join(' '); if (kws && Math.random()>0.4) title = hook + ' | ' + kws.toUpperCase(); } title += ` [${mo} ${yr}]`; document.getElementById('vmTitle').value = title; updateFilenamePreview(); } // ─── Auto-Tags Generator ─────────────────────────────────────────────────────── function genTags() { const base = [ 'ground floor news','breaking news','news today','top stories', `news ${new Date().getFullYear()}`, 'daily news update', 'world news', 'news headlines', 'latest news', 'must watch news', 'news right now', 'live news', 'real news', 'unfiltered news', 'independent news', 'ground floor report', 'news breakdown', 'what\'s happening', 'top headlines today', 'news summary', ]; const catTags = { politics: ['politics today','political news','congress','white house','government news','election news'], business: ['business news','stock market','economy today','finance news','wall street','market update'], tech: ['technology news','tech today','ai news','silicon valley','gadgets','innovation'], science: ['science news','discovery','research','space news','climate','environment'], health: ['health news','medical news','wellness','public health','healthcare'], sports: ['sports news','sports update','sports today','game recap','athletics'], entertainment:['entertainment news','celebrity news','hollywood','movies','music news'], world: ['world news','international news','global news','foreign affairs','geopolitics'], }; const cat = (document.getElementById('vmCategory')?.value||'').toLowerCase(); const extra = catTags[cat] || catTags.world; // Pull keywords from selected story titles const storyKws = [...new Set( selectedArts.flatMap(a => a.title.toLowerCase().replace(/[^\w\s]/g,'').split(/\s+/).filter(w=>w.length>5)) )].slice(0,6); const sources = [...new Set(selectedArts.map(a=>a.source))].slice(0,4).map(s=>s.toLowerCase()); const all = [...new Set([...base, ...extra, ...storyKws, ...sources])].slice(0,30); document.getElementById('vmTags').value = all.join(', '); } // ─── Detailed Description Generator ─────────────────────────────────────────── function genDescription() { const title = document.getElementById('vmTitle').value.trim() || 'Ground Floor News Daily Report'; const creator = document.getElementById('vmCreator').value.trim() || 'Ground Floor News'; const series = document.getElementById('vmSeries').value.trim(); const ep = document.getElementById('vmEpisode').value; const category = document.getElementById('vmCategory')?.value || 'News'; const lang = document.getElementById('vmLang')?.value || 'en-US'; const now = new Date(); const dateStr = now.toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'}); const timeStr = now.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit'}); const yr = now.getFullYear(); const srcList = [...new Set(selectedArts.map(a=>a.source))]; const biases = [...new Set(selectedArts.map(a=>a.bias))]; const cats = [...new Set(selectedArts.flatMap(a=>a.cat||[]))]; // Build timestamped story list const wpm = parseInt(document.getElementById('vmSpeed')?.value)||175; let cumSec = 30; // rough intro offset const storyLines = selectedArts.map((a,i)=>{ const m = Math.floor(cumSec/60), s = String(cumSec%60).padStart(2,'0'); const stamp = `${m}:${s}`; cumSec += Math.round((a.desc||a.title).split(/\s+/).length / wpm * 60) + 15; return `${stamp} — ${a.title} (${a.source})`; }); // Hashtags const hashCats = cats.slice(0,5).map(c=>'#'+c.charAt(0).toUpperCase()+c.slice(1)); const hashBase = ['#GroundFloorNews','#BreakingNews','#NewsToday','#Headlines','#DailyNews', '#WorldNews','#TopStories','#NewsUpdate','#RealNews','#IndependentNews']; const desc = [ `📰 ${title}`, `${creator}${series?' | '+series:''}${ep?' | Episode '+ep:''}`, `📅 ${dateStr} · 🕐 ${timeStr}`, '', `🔔 SUBSCRIBE for daily unfiltered news from the ground up.`, '', `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `📋 WHAT'S IN THIS VIDEO`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, ...storyLines, '', `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `🌐 SOURCES FEATURED`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, srcList.map((s,i)=>`${i+1}. ${s}`).join('\n'), '', `Bias spectrum covered: ${biases.join(', ')} — ${biases.length} viewpoints.`, '', `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `📌 ABOUT GROUND FLOOR NEWS`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `Ground Floor News aggregates and presents top headlines from 40+ verified sources`, `across the political spectrum — Left, Center, and Right — so YOU decide what to believe.`, `No spin. No agenda. Just the news, from the ground floor up.`, '', `Categories covered: ${cats.join(', ')}.`, `Language: ${lang}.`, '', `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `💼 SPONSORED BY`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `CR Specialists — Trusted roofing & storm restoration experts.`, `After any storm, visit crspecialists.net for a FREE roof inspection. Ask for Keith.`, '', `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `⚠️ DISCLAIMER`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `This video aggregates news from publicly available sources for informational purposes.`, `All headlines are attributed to their original publishers. Ground Floor News does not`, `claim ownership of any third-party content referenced herein. © ${yr} ${creator}.`, '', [...new Set([...hashBase,...hashCats])].join(' '), ].join('\n'); document.getElementById('vmDescription').value = desc; } // ─── Video Info Panel ───────────────────────────────────────────────────────── function toggleVmInfo() { const body = document.getElementById('vmInfoBody'); const arrow = document.getElementById('vmInfoArrow'); const open = body.style.display === 'block'; body.style.display = open ? 'none' : 'block'; arrow.style.transform = open ? '' : 'rotate(180deg)'; if (!open) updateFilenamePreview(); } function updateFilenamePreview() { const title = (document.getElementById('vmTitle').value||'GroundFloorNews').replace(/\s+/g,'_').replace(/[^a-zA-Z0-9_]/g,''); const creator = (document.getElementById('vmCreator').value||'').replace(/\s+/g,'_').replace(/[^a-zA-Z0-9_]/g,''); const ep = document.getElementById('vmEpisode').value; const showEp = document.getElementById('vmShowEpisode').checked; const showDt = document.getElementById('vmShowDate').checked; const date = showDt ? '_' + new Date().toISOString().slice(0,10) : ''; const epStr = (showEp && ep) ? `_E${String(ep).padStart(3,'0')}` : ''; const prefix = creator ? `${creator}_` : ''; document.getElementById('vmFilenamePreview').textContent = `${prefix}${title}${epStr}${date}.webm`; } function getVideoFilename() { const title = (document.getElementById('vmTitle').value||'GroundFloorNews').replace(/\s+/g,'_').replace(/[^a-zA-Z0-9_]/g,''); const creator = (document.getElementById('vmCreator').value||'').replace(/\s+/g,'_').replace(/[^a-zA-Z0-9_]/g,''); const ep = document.getElementById('vmEpisode').value; const showEp = document.getElementById('vmShowEpisode').checked; const showDt = document.getElementById('vmShowDate').checked; const date = showDt ? '_' + new Date().toISOString().slice(0,10) : ''; const epStr = (showEp && ep) ? `_E${String(ep).padStart(3,'0')}` : ''; const prefix = creator ? `${creator}_` : ''; return `${prefix}${title}${epStr}${date}.webm`; } // Live-update filename preview whenever any info field changes document.addEventListener('DOMContentLoaded', ()=>{ ['vmTitle','vmCreator','vmEpisode','vmShowEpisode','vmShowDate'].forEach(id=>{ const el = document.getElementById(id); if (el) el.addEventListener('input', updateFilenamePreview); if (el) el.addEventListener('change', updateFilenamePreview); }); updateFilenamePreview(); // Auto-fill description when script is generated const origAuto = window.autoGenerateScript; if (origAuto) window._autoGenOrig = origAuto; }); function setAudience(a) { vmAudience = a; document.querySelectorAll('.vm-audience-btn').forEach(b=>{ const on = b.dataset.aud === a; b.style.borderColor = on ? 'rgba(99,102,241,0.6)' : 'var(--border)'; b.style.background = on ? 'rgba(99,102,241,0.18)' : 'var(--glass)'; }); } setAudience('general'); function setDuration(s) { vmDuration = s; document.querySelectorAll('.vm-dur-pill').forEach(b=>b.classList.toggle('active',parseInt(b.dataset.dur)===s)); if (selectedArts.length) autoSelectStories(); } function updateWordCount() { const sc = document.getElementById('vmScript').value.trim(); const wc = sc ? sc.split(/\s+/).length : 0; const wpm = parseInt(document.getElementById('vmSpeed').value)||175; const mins = Math.round(wc/wpm*10)/10; document.getElementById('vmWordCount').textContent = `${wc} words · ~${mins} min`; } // ─── Auto-generate script ───────────────────────────────────────────────────── function autoGenerateScript() { if (!selectedArts.length) { autoSelectStories(); } const wpm = parseInt(document.getElementById('vmSpeed').value)||175; const targetWords = Math.round(vmDuration * wpm / 60); const isLong = vmDuration >= 600; const now = new Date(); const dateStr = now.toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'}); const timeStr = now.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit'}); const SPONSOR = `This report is brought to you by CR Specialists — trusted roofing and storm restoration experts. After any storm, visit crspecialists.net for a free roof inspection. Ask for Keith.`; let parts = []; if (vmAudience === 'general') { parts.push(`Good ${now.getHours()<12?'morning':now.getHours()<17?'afternoon':'evening'}. It is ${timeStr} on ${dateStr}. Here is your Ground Floor News briefing.`); parts.push(`Today's top stories are coming to you from ${[...new Set(selectedArts.map(a=>a.source))].join(', ')}.`); parts.push(SPONSOR); selectedArts.forEach((a,i)=>{ parts.push(`Story ${i+1}. ${a.title}.`); if (a.desc) parts.push(a.desc); if (a.cluster && a.cluster.length > 1) { const srcs = a.cluster.slice(1,4).map(x=>x.source).join(', '); parts.push(`This story is also being covered by ${srcs}.`); } if (isLong) parts.push(`Here is some additional context on this developing situation. Multiple sources across the political spectrum are watching this story closely.`); }); parts.push(`That is the news for ${dateStr}. Stay informed, stay aware.`); parts.push(SPONSOR); parts.push(`Subscribe for daily news briefings. See you next time.`); } else if (vmAudience === 'opinion') { parts.push(`Welcome. I am going to break down today's biggest stories and tell you what the media is not telling you.`); parts.push(`It is ${dateStr}. Here is what matters — and what you should be thinking about.`); selectedArts.forEach((a,i)=>{ parts.push(`Let's talk about this: ${a.title}.`); if (a.desc) parts.push(a.desc); const leftCov = (a.cluster||[a]).filter(x=>['LL','L','CL'].includes(x.bias)).map(x=>x.source); const rightCov = (a.cluster||[a]).filter(x=>['CR','R','RR'].includes(x.bias)).map(x=>x.source); if (leftCov.length && rightCov.length) { parts.push(`This story is getting coverage from both sides. On the left: ${leftCov.slice(0,2).join(' and ')}. On the right: ${rightCov.slice(0,2).join(' and ')}. The framing is very different depending on where you get your news.`); } else if (leftCov.length && !rightCov.length) { parts.push(`Notice that this story is only being covered by left-leaning outlets — ${leftCov.slice(0,3).join(', ')}. Right-leaning media has not touched it. Ask yourself why.`); } else if (rightCov.length && !leftCov.length) { parts.push(`This is getting attention on the right — ${rightCov.slice(0,3).join(', ')} — but left-leaning outlets are largely silent. That tells you something.`); } if (isLong) parts.push(`The real question here is not what happened, but why it is being reported this way, and what the story behind the story actually is.`); }); parts.push(`That is my take for today. The media landscape is more divided than ever. Ground Floor News tracks all of it so you can see the full picture.`); parts.push(SPONSOR); parts.push(`Like and subscribe for daily coverage breakdowns.`); } else if (vmAudience === 'business') { parts.push(`Good ${now.getHours()<12?'morning':now.getHours()<17?'afternoon':'evening'}. Welcome to your business news briefing for ${dateStr}. Here is what is moving markets and shaping the economy today.`); parts.push(SPONSOR); const bizStories = selectedArts.filter(a=>/economy|market|stock|trade|inflation|fed|bank|earnings|gdp|dollar|energy|oil|tech|merger|billion|million|invest/i.test(a.title+' '+a.desc)); const genStories = selectedArts.filter(a=>!bizStories.includes(a)); const ordered = [...bizStories, ...genStories]; ordered.forEach((a,i)=>{ parts.push(`Business headline ${i+1}: ${a.title}.`); if (a.desc) parts.push(a.desc); if (isLong) parts.push(`What this means for investors and business leaders: keep watching this space as developments could impact consumer sentiment and supply chains in the coming weeks.`); }); if (isLong) { parts.push(`The broader economic picture today suggests that market participants are closely monitoring inflation data and central bank policy signals.`); parts.push(`For business owners, today's headlines underscore the importance of monitoring geopolitical risk and supply chain disruptions as part of your strategic planning.`); } parts.push(`That is your business briefing for ${dateStr}.`); parts.push(SPONSOR); parts.push(`Subscribe for daily market-aware news summaries.`); } // Pad for very long videos if (vmDuration >= 900) { const recap = [ `Let us take a moment to recap today's major stories.`, ...selectedArts.slice(0,3).map(a=>`${a.title}. This story is being tracked by ${(a.cluster||[a]).length} sources.`), SPONSOR, `Ground Floor News pulls from over 40 news sources across the political spectrum so you see the full picture every time.`, `Make sure to like and subscribe so you never miss a daily update.`, ]; while (parts.join(' ').split(/\s+/).length < targetWords * 0.80) { parts.splice(parts.length-2, 0, ...recap); } } let script = parts.join('\n\n'); const words = script.split(/\s+/).length; if (words > targetWords * 1.25) { const trim = parts.slice(0, Math.max(5, Math.floor(parts.length * targetWords / words))).concat(parts.slice(-2)); script = trim.join('\n\n'); } document.getElementById('vmScript').value = script; updateWordCount(); // Auto-fill tags and description if empty if (selectedArts.length) { const tagsEl = document.getElementById('vmTags'); if (tagsEl && !tagsEl.value.trim()) genTags(); const descEl = document.getElementById('vmDescription'); if (descEl && !descEl.value.trim()) genDescription(); } // Update title with date if box checked const showDt = document.getElementById('vmShowDate')?.checked; const titleEl = document.getElementById('vmTitle'); if (showDt && titleEl && !/\d{4}/.test(titleEl.value)) { const d = new Date(); titleEl.value = `Ground Floor News Daily Report — ${d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}`; } updateFilenamePreview(); } // ─── Images ─────────────────────────────────────────────────────────────────── async function searchImages() { const q = document.getElementById('vmImgQuery').value.trim() || 'news'; const grid = document.getElementById('vmImgGrid'); grid.innerHTML = '
Searching...
'; try { const res = await fetch(`/api/pexels?q=${encodeURIComponent(q)}&count=15&orientation=landscape`); const data = await res.json(); grid.innerHTML = data.photos.map(p=>`
`).join(''); } catch(e) { grid.innerHTML = '
Search failed
'; } } function quickImg(q) { document.getElementById('vmImgQuery').value=q; searchImages(); } function toggleImg(url, el) { const i = vmImgSel.indexOf(url); if (i>=0) { vmImgSel.splice(i,1); el.classList.remove('selected'); } else { vmImgSel.push(url); el.classList.add('selected'); } document.getElementById('vmImgCount').textContent = vmImgSel.length + ' selected'; } function clearImgs() { vmImgSel=[]; document.querySelectorAll('.vm-img-thumb').forEach(e=>e.classList.remove('selected')); document.getElementById('vmImgCount').textContent='0 selected'; } function addCustomImgs(files) { const grid = document.getElementById('vmCustomGrid'); Array.from(files).forEach(f=>{ const reader = new FileReader(); reader.onload = e => { vmCustomImgs.push(e.target.result); const img = document.createElement('img'); img.className='vm-img-thumb selected'; img.src=e.target.result; grid.appendChild(img); }; reader.readAsDataURL(f); }); } // ─── Voices ─────────────────────────────────────────────────────────────────── async function loadVoices() { try { const res = await fetch('/api/voices'); const data = await res.json(); const sel = document.getElementById('vmVoice'); // Group by locale const groups = {}; (data.voices||[]).forEach(v => { const g = v.locale || 'Other'; if (!groups[g]) groups[g] = []; groups[g].push(v); }); const localeLabel = {US:'🇺🇸 United States', UK:'🇬🇧 United Kingdom', AU:'🇦🇺 Australia', CA:'🇨🇦 Canada', IE:'🇮🇪 Ireland', IN:'🇮🇳 India'}; sel.innerHTML = Object.keys(groups).map(loc => { const opts = groups[loc].map(v => { const gIcon = v.gender==='Male' ? '♂' : '♀'; const label = `${v.name} ${gIcon}`; const sel = v.id==='en-US-AndrewNeural' ? ' selected' : ''; return `
${label}
`; }).join(''); return `
${opts}
`; }).join(''); } catch(e) {} } function previewVoice() { const txt = document.getElementById('vmScript').value.slice(0,200) || 'Good evening. Welcome to Ground Floor News — your daily briefing from the ground up.'; fetch('/api/tts',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({text:txt.slice(0,250),voice:document.getElementById('vmVoice').value})}) .then(r=>r.arrayBuffer()).then(buf=>{ if(vmPreviewAudio){vmPreviewAudio.pause();} const blob=new Blob([buf],{type:'audio/mpeg'}); vmPreviewAudio=new Audio(URL.createObjectURL(blob)); vmPreviewAudio.play(); }).catch(e=>console.error(e)); } function stopPreview() { if(vmPreviewAudio){vmPreviewAudio.pause();vmPreviewAudio=null;} } // ─── Build video ────────────────────────────────────────────────────────────── function setVmProgress(pct, msg) { document.getElementById('vmProgressBar').style.width = pct+'%'; document.getElementById('vmStatus').textContent = msg; } async function proxyLoadImage(url) { const src = url.startsWith('data:') ? url : `/api/proxy-image?url=${encodeURIComponent(url)}`; return new Promise((res,rej)=>{ const img=new Image(); img.crossOrigin='anonymous'; img.onload=()=>res(img); img.onerror=e=>rej(new Error('img load fail: '+url.slice(0,40))); img.src=src; }); } function roundRect(ctx,x,y,w,h,r){ ctx.beginPath(); if(ctx.roundRect){ctx.roundRect(x,y,w,h,r);} else{ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.arcTo(x+w,y,x+w,y+r,r);ctx.lineTo(x+w,y+h-r);ctx.arcTo(x+w,y+h,x+w-r,y+h,r);ctx.lineTo(x+r,y+h);ctx.arcTo(x,y+h,x,y+h-r,r);ctx.lineTo(x,y+r);ctx.arcTo(x,y,x+r,y,r);ctx.closePath();} } // segments = [{text, article, image, query, start, end}, ...] // outroStart = seconds at which outro begins function drawFrame(ctx, segments, elapsed, w, h, title, outroStart) { const sc = w/1920; const introEnd = segments[0]?.start || 3; outroStart = outroStart || (segments[segments.length-1]?.end || 30); ctx.fillStyle='#060614'; ctx.fillRect(0,0,w,h); // INTRO if (elapsed < introEnd) { const t = introEnd > 0 ? elapsed/introEnd : 1; const bg=ctx.createLinearGradient(0,0,w,h); bg.addColorStop(0,'#0a0a2e'); bg.addColorStop(1,'#060614'); ctx.fillStyle=bg; ctx.fillRect(0,0,w,h); const iCreator = document.getElementById('vmCreator')?.value.trim() || 'Ground Floor News'; const iSeries = document.getElementById('vmSeries')?.value.trim() || ''; const iEp = document.getElementById('vmEpisode')?.value; const iShowEp = document.getElementById('vmShowEpisode')?.checked && iEp; ctx.globalAlpha=Math.min(1,t*2); ctx.fillStyle='#6366f1'; ctx.font=`900 ${56*sc}px Inter,system-ui,sans-serif`; ctx.textAlign='center'; ctx.fillText('📰 ' + iCreator, w/2, h*0.38); if (iSeries) { ctx.fillStyle='#a5b4fc'; ctx.font=`600 ${22*sc}px Inter,system-ui,sans-serif`; ctx.fillText(iSeries + (iShowEp ? ` — Episode ${iEp}` : ''), w/2, h*0.49); } ctx.fillStyle='#e2e8f0'; ctx.font=`600 ${24*sc}px Inter,system-ui,sans-serif`; ctx.fillText(title, w/2, iSeries ? h*0.58 : h*0.52); ctx.fillStyle='#94a3b8'; ctx.font=`400 ${16*sc}px Inter,system-ui,sans-serif`; ctx.fillText(new Date().toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'}), w/2, iSeries ? h*0.66 : h*0.62); ctx.globalAlpha=1; return; } // OUTRO if (elapsed >= outroStart) { const creator = document.getElementById('vmCreator').value.trim() || 'Ground Floor News'; const series = document.getElementById('vmSeries').value.trim(); const bg=ctx.createLinearGradient(0,0,w,h); bg.addColorStop(0,'#0a0a2e'); bg.addColorStop(1,'#060614'); ctx.fillStyle=bg; ctx.fillRect(0,0,w,h); ctx.fillStyle='#6366f1'; ctx.font=`900 ${52*sc}px Inter,system-ui,sans-serif`; ctx.textAlign='center'; ctx.fillText('Thanks for watching', w/2, h*0.36); ctx.fillStyle='#facc15'; ctx.font=`700 ${28*sc}px Inter,system-ui,sans-serif`; ctx.fillText('Subscribe for daily news', w/2, h*0.48); ctx.fillStyle='#e2e8f0'; ctx.font=`600 ${22*sc}px Inter,system-ui,sans-serif`; ctx.fillText(creator + (series ? ' · ' + series : ''), w/2, h*0.59); ctx.fillStyle='#94a3b8'; ctx.font=`400 ${16*sc}px Inter,system-ui,sans-serif`; ctx.fillText('crspecialists.net · Ask for Keith', w/2, h*0.67); return; } // CONTENT — find segment matching current time const seg = segments.find(s => elapsed >= s.start && elapsed < s.end) || (elapsed < segments[0].start ? segments[0] : segments[segments.length-1]); if (!seg) return; // Crossfade: fade in first 10% and fade out last 10% of each segment const segLen = Math.max(0.1, seg.end - seg.start); const frac = Math.max(0, Math.min(1, (elapsed - seg.start) / segLen)); const fadeW = Math.min(0.15, 1.5 / segLen); const fade = frac < fadeW ? frac/fadeW : (1-frac) < fadeW ? (1-frac)/fadeW : 1; ctx.globalAlpha = Math.max(0.05, fade); if (seg.image) { const img=seg.image, isc=Math.max(w/img.naturalWidth,h/img.naturalHeight); const iw=img.naturalWidth*isc, ih=img.naturalHeight*isc; ctx.drawImage(img, (w-iw)/2, (h-ih)/2, iw, ih); } else { const fb=ctx.createLinearGradient(0,0,w,h); fb.addColorStop(0,'#0a0a2e'); fb.addColorStop(1,'#1a1a4e'); ctx.fillStyle=fb; ctx.fillRect(0,0,w,h); } ctx.globalAlpha=1; // Overlay gradient const ov=ctx.createLinearGradient(0,h*0.55,0,h); ov.addColorStop(0,'rgba(6,6,20,0)'); ov.addColorStop(0.5,'rgba(6,6,20,0.75)'); ov.addColorStop(1,'rgba(6,6,20,0.97)'); ctx.fillStyle=ov; ctx.fillRect(0,0,w,h); // Headline bar — show matched article or snippet of current paragraph const art = seg.article; if (art) { const bc = biasColor(art.bias)||'#6366f1'; ctx.fillStyle=bc; roundRect(ctx, 40*sc, h-180*sc, 8*sc, 70*sc, 3*sc); ctx.fill(); ctx.fillStyle='rgba(0,0,0,0.6)'; roundRect(ctx, 55*sc, h-180*sc, w-95*sc, 75*sc, 8*sc); ctx.fill(); ctx.fillStyle='#fff'; ctx.font=`700 ${20*sc}px Inter,system-ui,sans-serif`; ctx.textAlign='left'; const ht = art.title.length>90 ? art.title.slice(0,87)+'...' : art.title; ctx.fillText(ht, 72*sc, h-148*sc); ctx.fillStyle=bc; ctx.font=`600 ${13*sc}px Inter,system-ui,sans-serif`; ctx.fillText(art.source+' · '+biasLabel(art.bias), 72*sc, h-127*sc); } else { // Non-story segment: show first line of paragraph text const snippet = seg.text.replace(/\n/g,' ').slice(0,88); ctx.fillStyle='rgba(0,0,0,0.6)'; roundRect(ctx, 40*sc, h-160*sc, w-80*sc, 55*sc, 8*sc); ctx.fill(); ctx.fillStyle='#e2e8f0'; ctx.font=`600 ${17*sc}px Inter,system-ui,sans-serif`; ctx.textAlign='left'; ctx.fillText(snippet.length===88 ? snippet+'...' : snippet, 60*sc, h-127*sc); } // Lower bar const showWm = document.getElementById('vmShowWatermark')?.checked !== false; const creator = document.getElementById('vmCreator')?.value.trim() || 'Ground Floor News'; const series = document.getElementById('vmSeries')?.value.trim() || ''; const ep = document.getElementById('vmEpisode')?.value; const showEp = document.getElementById('vmShowEpisode')?.checked && ep; const showDt = document.getElementById('vmShowDate')?.checked !== false; const category = document.getElementById('vmCategory')?.value || 'News'; ctx.fillStyle='rgba(6,6,20,0.88)'; ctx.fillRect(0, h-44*sc, w, 44*sc); ctx.fillStyle='rgba(99,102,241,0.9)'; ctx.fillRect(0, h-44*sc, 4*sc, 44*sc); // Left: channel name + series ctx.fillStyle='#fff'; ctx.font=`800 ${15*sc}px Inter,system-ui,sans-serif`; ctx.textAlign='left'; const channelLabel = (series ? `${creator} · ${series}` : creator) + (showEp ? ` E${String(ep).padStart(3,'0')}` : ''); ctx.fillText(channelLabel, 18*sc, h-24*sc); ctx.fillStyle='rgba(255,255,255,0.45)'; ctx.font=`400 ${11*sc}px Inter,system-ui,sans-serif`; ctx.fillText(category, 18*sc, h-10*sc); // Right: date + sponsor ctx.textAlign='right'; if (showDt) { ctx.fillStyle='rgba(255,255,255,0.6)'; ctx.font=`400 ${12*sc}px Inter,system-ui,sans-serif`; ctx.fillText(new Date().toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}), w-18*sc, h-24*sc); } ctx.fillStyle='#facc15'; ctx.font=`600 ${11*sc}px Inter,system-ui,sans-serif`; ctx.fillText('crspecialists.net · Ask for Keith', w-18*sc, h-10*sc); // Watermark bug (top-right corner) — shown if enabled if (showWm) { ctx.globalAlpha=0.55; ctx.fillStyle='rgba(0,0,0,0.5)'; roundRect(ctx, w-180*sc, 14*sc, 160*sc, 28*sc, 6*sc); ctx.fill(); ctx.fillStyle='#a5b4fc'; ctx.font=`700 ${13*sc}px Inter,system-ui,sans-serif`; ctx.textAlign='right'; ctx.fillText('📰 ' + creator, w-22*sc, 32*sc); ctx.globalAlpha=1; } } // ── Determine contextual Pexels search query for a script paragraph ────────── function segQuery(text) { const p = text; // Match to a selected article by keyword overlap (title words in paragraph) const art = selectedArts.find(a => { const words = a.title.toLowerCase().split(/\s+/).filter(x=>x.length>4); return words.length>0 && words.filter(w=>p.toLowerCase().includes(w)).length >= Math.min(2, Math.ceil(words.length*0.4)); }); if (art) return art.title.split(/\s+/).slice(0,5).join(' '); if (/cr specialist|crspecialists|roofing|storm restoration|free roof/i.test(p)) return 'roofing contractor storm damage roof repair'; if (/subscribe|thanks for watching|see you next|that is the news/i.test(p)) return 'news broadcast television studio anchor'; if (/good morning|good afternoon|good evening|here is your.*briefing|welcome.*newscope/i.test(p)) return 'morning news anchor television broadcast'; if (/economy|market|stock|invest|inflation|gdp|earnings|\bfed\b|central bank/i.test(p)) return 'stock market financial economy business'; if (/politic|congress|senate|president|white house|government|election/i.test(p)) return 'politics government capitol washington'; if (/war|military|conflict|troops|ukraine|nato|defense|attack/i.test(p)) return 'military conflict international world news'; if (/climate|environment|carbon|emissions|solar|energy|fossil/i.test(p)) return 'climate environment nature renewable energy'; if (/tech|artificial intelligence|\bai\b|software|silicon valley|startup/i.test(p)) return 'technology artificial intelligence digital innovation'; if (/health|covid|virus|pandemic|hospital|vaccine|drug|fda/i.test(p)) return 'health medical hospital science research'; if (/crime|police|murder|court|justice|law|arrest/i.test(p)) return 'law enforcement police justice court'; // Fallback: extract proper nouns const caps = (p.match(/\b[A-Z][a-z]{3,}\b/g)||[]).slice(0,3).join(' '); return caps || 'breaking news world events'; } async function buildVideo() { const script = document.getElementById('vmScript').value.trim(); if (!script) { alert('Generate or write a script first.'); return; } document.getElementById('vmResult').style.display='none'; document.getElementById('vmProgressWrap').style.display='block'; setVmProgress(3,'Analyzing script...'); const [outW,outH] = document.getElementById('vmRes').value.split('x').map(Number); const title = document.getElementById('vmTitle').value || 'Ground Floor News Report'; try { // ── 1. Split script into paragraphs, assign query + article per segment ── const paragraphs = script.split(/\n\n+/).filter(p=>p.trim().length>0); const segMeta = paragraphs.map(p => { const art = selectedArts.find(a => { const words = a.title.toLowerCase().split(/\s+/).filter(x=>x.length>4); return words.length>0 && words.filter(w=>p.toLowerCase().includes(w)).length >= Math.min(2,Math.ceil(words.length*0.4)); }); return { text:p, article:art||null, query:segQuery(p), words:p.split(/\s+/).length }; }); // ── 2. Load manually selected images as fallback pool ─────────────────── const manualPool = []; for (const url of [...vmImgSel,...vmCustomImgs]) { try { manualPool.push(await proxyLoadImage(url)); } catch(e) {} } // ── 3. Fetch one contextual Pexels image per unique query (parallel) ──── setVmProgress(7, `Fetching ${[...new Set(segMeta.map(s=>s.query))].length} contextual images...`); const uniqueQueries = [...new Set(segMeta.map(s=>s.query))]; const qImgCache = {}; await Promise.allSettled(uniqueQueries.map(async (q,qi) => { try { const r = await fetch(`/api/pexels?q=${encodeURIComponent(q)}&count=3&orientation=landscape`); const d = await r.json(); const url = (d.photos||[])[0]?.large; if (url) qImgCache[q] = await proxyLoadImage(url); } catch(e) { console.warn('pexels fail:', q); } setVmProgress(7+Math.round(13*(qi+1)/uniqueQueries.length), `Image ${qi+1}/${uniqueQueries.length}: ${q.slice(0,30)}...`); })); // Assign image to each segment (Pexels result, or fallback from manual pool) let fallbackIdx = 0; segMeta.forEach(sm => { sm.image = qImgCache[sm.query] || (manualPool.length ? manualPool[fallbackIdx++ % manualPool.length] : null); }); // ── 4. Generate TTS ───────────────────────────────────────────────────── setVmProgress(22, 'Generating voice narration...'); const voiceName = document.getElementById('vmVoice').value||'Samantha'; const ttsRate = parseInt(document.getElementById('vmSpeed').value)||175; const ttsResp = await fetch('/api/tts',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({text:script,voice:voiceName,rate:ttsRate})}); if (!ttsResp.ok) throw new Error('TTS failed: '+ttsResp.status); const ttsAB = await ttsResp.arrayBuffer(); setVmProgress(30,'Decoding audio...'); const audioCtx = new AudioContext({sampleRate:44100}); const audioBuffer = await audioCtx.decodeAudioData(ttsAB.slice(0)); const audioDur = audioBuffer.duration; // ── 5. Build segments — timing proportional to word count ─────────────── const introLen = Math.min(3, audioDur*0.05); const outroLen = Math.min(4, audioDur*0.06); const contentDur = audioDur; const totalWords = segMeta.reduce((a,b)=>a+b.words,0)||1; let t = introLen; const segments = segMeta.map(sm => { const dur = Math.max(1.5, (sm.words/totalWords)*contentDur); const seg = { text:sm.text, article:sm.article, image:sm.image, start:t, end:t+dur }; t += dur; return seg; }); // Normalize so content fills exactly audioDur const rawEnd = segments[segments.length-1]?.end || (introLen+contentDur); const scale = contentDur / Math.max(0.01, rawEnd-introLen); let ct = introLen; segments.forEach(seg => { const d = (seg.end-seg.start)*scale; seg.start=ct; seg.end=ct+d; ct=seg.end; }); const outroStart = segments[segments.length-1]?.end || (introLen+contentDur); const actualDur = Math.max(vmDuration, outroStart+outroLen); // ── 6. Set up canvas + audio graph ────────────────────────────────────── const canvas = document.createElement('canvas'); canvas.width=outW; canvas.height=outH; canvas.style.cssText='position:fixed;bottom:10px;right:10px;width:320px;height:180px;z-index:9999;border:2px solid #6366f1;border-radius:8px;'; document.body.appendChild(canvas); const ctx = canvas.getContext('2d'); drawFrame(ctx, segments, 0, outW, outH, title, outroStart); const stream = canvas.captureStream(0); const vTrack = stream.getVideoTracks()[0]; const dest = audioCtx.createMediaStreamDestination(); const audioSrc = audioCtx.createBufferSource(); audioSrc.buffer = audioBuffer; audioSrc.connect(dest); audioSrc.connect(audioCtx.destination); dest.stream.getAudioTracks().forEach(tk=>stream.addTrack(tk)); let mimeType='video/webm;codecs=vp9,opus'; if (!MediaRecorder.isTypeSupported(mimeType)&&MediaRecorder.isTypeSupported('video/mp4')) mimeType='video/mp4'; const bitsPerSec = outW>=3840?35_000_000:outW>=1920?8_000_000:4_000_000; const recorder = new MediaRecorder(stream,{mimeType,videoBitsPerSecond:bitsPerSec}); const chunks = []; recorder.ondataavailable = e=>{ if(e.data.size>0) chunks.push(e.data); }; // ── 7. Record ─────────────────────────────────────────────────────────── setVmProgress(35,'Recording video...'); recorder.start(500); audioSrc.start(); const startT = performance.now(); let lastSeg = -1; function loop(){ const elapsed = (performance.now()-startT)/1000; if (elapsed > actualDur+0.5) { try{ audioSrc.stop(); }catch(e){} audioCtx.close(); recorder.stop(); return; } // Redraw on segment change or ~30fps during transitions const curSeg = segments.findIndex(s=>elapsed>=s.start&&elapsed
{ recorder.onstop=r; }); try{ document.body.removeChild(canvas); }catch(e){} setVmProgress(98,'Finalizing...'); vmBlob = new Blob(chunks,{type:mimeType}); document.getElementById('vmVideo').src = URL.createObjectURL(vmBlob); document.getElementById('vmResult').style.display = 'block'; setVmProgress(100,`Done! ${segments.length} segments · ${Math.round(actualDur)}s`); setTimeout(()=>{ document.getElementById('vmProgressWrap').style.display='none'; },2000); // Auto-stream if (document.getElementById('ytAuto')?.checked) { const k = document.getElementById('ytStreamKey')?.value.trim(); if (k) setTimeout(()=>startYTStream(),1500); } } catch(e) { setVmProgress(0,'❌ '+e.message); console.error(e); } btn.disabled=false; btn.textContent='🎬 Build News Video'; } function downloadVideo() { if(!vmBlob) return; const a=document.createElement('a'); a.href=URL.createObjectURL(vmBlob); a.download=getVideoFilename(); a.click(); } // ─── Thumbnail ───────────────────────────────────────────────────────────────── // ─── YouTube Channel Icon (800×800) ────────────────────────────────────────── function genYouTubeIcon() { const S = 800; const c = document.createElement('canvas'); c.width = S; c.height = S; const ctx = c.getContext('2d'); // ── Background: deep dark with gradient fill ──────────────────────────────── const bg = ctx.createLinearGradient(0, 0, S, S); bg.addColorStop(0, '#07071a'); bg.addColorStop(0.5, '#0d0d28'); bg.addColorStop(1, '#050510'); ctx.fillStyle = bg; ctx.fillRect(0, 0, S, S); // ── Subtle radial glow center ─────────────────────────────────────────────── const glow = (x,y,r,col) => { const g = ctx.createRadialGradient(x,y,0,x,y,r); g.addColorStop(0, col); g.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = g; ctx.fillRect(0, 0, S, S); }; glow(S*0.35, S*0.42, 380, 'rgba(99,102,241,0.28)'); glow(S*0.72, S*0.25, 260, 'rgba(168,85,247,0.18)'); glow(S*0.6, S*0.75, 200, 'rgba(250,204,21,0.10)'); // ── Corner accent lines ───────────────────────────────────────────────────── ctx.strokeStyle = 'rgba(99,102,241,0.35)'; ctx.lineWidth = 3; // top-left bracket ctx.beginPath(); ctx.moveTo(40,80); ctx.lineTo(40,40); ctx.lineTo(80,40); ctx.stroke(); // top-right bracket ctx.beginPath(); ctx.moveTo(S-80,40); ctx.lineTo(S-40,40); ctx.lineTo(S-40,80); ctx.stroke(); // bottom-left bracket ctx.beginPath(); ctx.moveTo(40,S-80); ctx.lineTo(40,S-40); ctx.lineTo(80,S-40); ctx.stroke(); // bottom-right bracket ctx.beginPath(); ctx.moveTo(S-80,S-40); ctx.lineTo(S-40,S-40); ctx.lineTo(S-40,S-80); ctx.stroke(); // ── Horizontal divider lines ──────────────────────────────────────────────── ctx.strokeStyle = 'rgba(99,102,241,0.2)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(60, 200); ctx.lineTo(S-60, 200); ctx.stroke(); ctx.beginPath(); ctx.moveTo(60, S-200); ctx.lineTo(S-60, S-200); ctx.stroke(); // ── Live dot (top-right) ──────────────────────────────────────────────────── ctx.fillStyle = '#ef4444'; ctx.beginPath(); ctx.arc(S-68, 68, 28, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'rgba(239,68,68,0.3)'; ctx.beginPath(); ctx.arc(S-68, 68, 46, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = '900 18px Inter,Arial,sans-serif'; ctx.textAlign = 'center'; ctx.fillText('LIVE', S-68, 75); // ── Main text: GROUND ─────────────────────────────────────────────────────── ctx.textAlign = 'center'; ctx.shadowColor = '#facc15'; ctx.shadowBlur = 30; ctx.fillStyle = '#facc15'; ctx.font = '900 118px Inter,Arial,sans-serif'; ctx.fillText('GROUND', S/2, 310); ctx.shadowBlur = 0; // ── FLOOR ─────────────────────────────────────────────────────────────────── ctx.shadowColor = '#fff'; ctx.shadowBlur = 14; ctx.fillStyle = '#ffffff'; ctx.font = '900 128px Inter,Arial,sans-serif'; ctx.fillText('FLOOR', S/2, 444); ctx.shadowBlur = 0; // ── NEWS with indigo gradient ─────────────────────────────────────────────── const tg = ctx.createLinearGradient(S*0.2, 0, S*0.8, 0); tg.addColorStop(0, '#6366f1'); tg.addColorStop(0.5, '#a855f7'); tg.addColorStop(1, '#6366f1'); ctx.shadowColor = '#6366f1'; ctx.shadowBlur = 28; ctx.fillStyle = tg; ctx.font = '900 148px Inter,Arial,sans-serif'; ctx.fillText('NEWS', S/2, 596); ctx.shadowBlur = 0; // ── Tagline ───────────────────────────────────────────────────────────────── ctx.fillStyle = 'rgba(255,255,255,0.38)'; ctx.font = '500 28px Inter,Arial,sans-serif'; ctx.letterSpacing = '0.15em'; ctx.fillText('FROM THE GROUND UP', S/2, 660); // ── Bottom bar ────────────────────────────────────────────────────────────── const bar = ctx.createLinearGradient(0, S-70, S, S); bar.addColorStop(0, 'rgba(99,102,241,0.75)'); bar.addColorStop(1, 'rgba(5,5,16,0.9)'); ctx.fillStyle = bar; ctx.fillRect(0, S-70, S, 70); ctx.fillStyle = '#fff'; ctx.font = '700 26px Inter,Arial,sans-serif'; ctx.textAlign = 'left'; ctx.fillText('crspecialists.net', 44, S-30); ctx.textAlign = 'right'; ctx.fillStyle = '#facc15'; ctx.fillText('Ask for Keith', S-44, S-30); // ── Download ──────────────────────────────────────────────────────────────── c.toBlob(blob => { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'GroundFloorNews_YT_Icon_800x800.png'; a.click(); }, 'image/png'); } function genThumbnail() { const W=1280,H=720; const c=document.createElement('canvas'); c.width=W; c.height=H; const ctx=c.getContext('2d'); const dateStr=new Date().toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}); const topStory=selectedArts[0]; const storyCount=selectedArts.length; const sourceCount=[...new Set(allClusters.flat().map(a=>a.source))].length; // Background const bg=ctx.createLinearGradient(0,0,W,H); bg.addColorStop(0,'#060614'); bg.addColorStop(0.5,'#0e0820'); bg.addColorStop(1,'#020408'); ctx.fillStyle=bg; ctx.fillRect(0,0,W,H); // Glow blobs const glow=(x,y,r,col)=>{const g=ctx.createRadialGradient(x,y,0,x,y,r);g.addColorStop(0,col);g.addColorStop(1,'rgba(0,0,0,0)');ctx.fillStyle=g;ctx.fillRect(0,0,W,H);}; glow(160,360,400,'rgba(99,102,241,0.25)'); glow(1000,200,320,'rgba(168,85,247,0.15)'); // Diagonal slash ctx.save(); ctx.globalAlpha=0.1; ctx.fillStyle='#6366f1'; ctx.beginPath(); ctx.moveTo(W*0.55,0); ctx.lineTo(W*0.68,0); ctx.lineTo(W*0.42,H); ctx.lineTo(W*0.29,H); ctx.closePath(); ctx.fill(); ctx.restore(); // Emoji ctx.font='180px system-ui'; ctx.textAlign='center'; ctx.globalAlpha=0.15; ctx.fillText('📰',160,510); ctx.globalAlpha=1; // LIVE badge ctx.fillStyle='#ef4444'; roundRect(ctx,40,38,180,54,10); ctx.fill(); ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(68,65,9,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#ef4444'; ctx.beginPath(); ctx.arc(68,65,4,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#fff'; ctx.font='800 22px Inter,system-ui,sans-serif'; ctx.textAlign='left'; ctx.fillText('LIVE NEWS',90,74); // Date ctx.fillStyle='rgba(255,255,255,0.08)'; roundRect(ctx,235,38,210,54,10); ctx.fill(); ctx.strokeStyle='rgba(255,255,255,0.15)'; ctx.lineWidth=1.5; roundRect(ctx,235,38,210,54,10); ctx.stroke(); ctx.fillStyle='#e2e8f0'; ctx.font='700 20px Inter,system-ui,sans-serif'; ctx.textAlign='center'; ctx.fillText(dateStr.toUpperCase(),340,75); // Style badge const styleNames={general:'📺 BROADCAST',opinion:'🗣️ COMMENTARY',business:'💼 BUSINESS'}; ctx.fillStyle='rgba(99,102,241,0.8)'; roundRect(ctx,460,38,240,54,10); ctx.fill(); ctx.fillStyle='#fff'; ctx.font='700 18px Inter,system-ui,sans-serif'; ctx.textAlign='center'; ctx.fillText(styleNames[vmAudience]||'📺 NEWS',580,75); // ── Clickbait headline (pull from vmTitle or generate) ────────────────────── const vidTitle = document.getElementById('vmTitle')?.value || 'BREAKING NEWS'; // Split title for two-line display at ~30 chars const words = vidTitle.replace(/\[.*?\]/g,'').trim().split(/\s+/); let line1='', line2='', line3=''; let tmp=''; const lines=[]; words.forEach(w=>{ const t=(tmp?tmp+' ':'')+w; if(t.length>28&&tmp){lines.push(tmp);tmp=w;}else{tmp=t;} }); if(tmp) lines.push(tmp); [line1,line2,line3] = [lines[0]||'',lines[1]||'',lines[2]||'']; // Draw lines with big yellow/white impact font const lineH = 95; const startY = line3 ? 240 : (line2 ? 270 : 320); [line1,line2,line3].filter(Boolean).forEach((ln,i)=>{ const isOdd = i%2===0; ctx.shadowColor = isOdd ? '#facc15' : '#6366f1'; ctx.shadowBlur=30; ctx.fillStyle = isOdd ? '#facc15' : '#fff'; ctx.font=`900 ${line3?76:84}px Inter,system-ui,sans-serif`; ctx.textAlign='left'; ctx.fillText(ln.toUpperCase(), 42, startY + i*lineH); }); ctx.shadowBlur=0; // Story strip if (topStory) { ctx.fillStyle='rgba(239,68,68,0.85)'; roundRect(ctx,0,H-146,W,54,0); ctx.fill(); ctx.fillStyle='#fff'; ctx.font='700 21px Inter,system-ui,sans-serif'; ctx.textAlign='left'; const ht=topStory.title.length>80?topStory.title.slice(0,77)+'…':topStory.title; ctx.fillText('🔴 ' + ht, 20, H-113); ctx.fillStyle='rgba(255,255,255,0.7)'; ctx.font='500 15px Inter,system-ui,sans-serif'; ctx.fillText(topStory.source + ' · ' + (biasLabel(topStory.bias)||''), 20, H-96); } // Stats row const stats=[ {v:selectedArts.length||storyCount, l:'STORIES'}, {v:sourceCount, l:'SOURCES'}, {v:[...new Set(allClusters.flat().map(a=>a.bias))].length, l:'VIEWPOINTS'}, ]; let sx=42; stats.forEach(s=>{ ctx.fillStyle='rgba(0,0,0,0.55)'; roundRect(ctx,sx,H-88,180,38,8); ctx.fill(); ctx.strokeStyle='rgba(255,255,255,0.18)'; ctx.lineWidth=1.5; roundRect(ctx,sx,H-88,180,38,8); ctx.stroke(); ctx.fillStyle='#facc15'; ctx.font='900 22px Inter,system-ui,sans-serif'; ctx.textAlign='left'; ctx.fillText(String(s.v), sx+10, H-63); ctx.fillStyle='rgba(255,255,255,0.65)'; ctx.font='600 12px Inter,system-ui,sans-serif'; ctx.fillText(s.l, sx+44, H-63); sx+=196; }); // Right badge — story count ctx.fillStyle='rgba(239,68,68,0.92)'; roundRect(ctx,W-220,H-280,200,130,16); ctx.fill(); ctx.strokeStyle='#fff'; ctx.lineWidth=3; roundRect(ctx,W-220,H-280,200,130,16); ctx.stroke(); ctx.fillStyle='#fff'; ctx.font='900 68px Inter,system-ui,sans-serif'; ctx.textAlign='center'; ctx.fillText(String(allClusters.length||storyCount), W-120, H-185); ctx.font='700 16px Inter,system-ui,sans-serif'; ctx.fillText('LIVE STORIES', W-120, H-163); // Bottom bar const bar=ctx.createLinearGradient(0,H-44,W,H); bar.addColorStop(0,'#0a0a1a'); bar.addColorStop(1,'#1a1040'); ctx.fillStyle=bar; ctx.fillRect(0,H-44,W,44); ctx.fillStyle='rgba(99,102,241,0.9)'; ctx.fillRect(0,H-44,4,44); ctx.fillStyle='#fff'; ctx.font='800 17px Inter,system-ui,sans-serif'; ctx.textAlign='left'; ctx.fillText('📰 GROUND FLOOR NEWS', 18, H-16); ctx.fillStyle='#facc15'; ctx.font='700 15px Inter,system-ui,sans-serif'; ctx.textAlign='right'; ctx.fillText('crspecialists.net · Ask for Keith', W-18, H-16); const creator = document.getElementById('vmCreator')?.value.trim() || 'GroundFloorNews'; const safeCreator = creator.replace(/\s+/g,'_').replace(/[^a-zA-Z0-9_]/g,''); c.toBlob(blob=>{ const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`${safeCreator}_Thumb_${new Date().toISOString().slice(0,10)}.png`; a.click(); },'image/png'); } // ─── YouTube Stream ─────────────────────────────────────────────────────────── let _ytPoll=null, _ytElapsedSec=0, _ytElapsedTimer=null; function ytLog(m){const el=document.getElementById('ytLog');if(!el)return;el.style.display='block';el.innerHTML+=`
[${new Date().toLocaleTimeString()}]
${m}
`;el.scrollTop=el.scrollHeight;} function ytSetStatus(m,c){const el=document.getElementById('ytStatus');if(el){el.textContent=m;el.style.color=c||'var(--text3)';}} function ytShowLive(on){ document.getElementById('ytLiveBadge').style.display=on?'inline-flex':'none'; document.getElementById('ytLiveStats').style.display=on?'grid':'none'; document.getElementById('ytChStatusBadge').textContent=on?'🔴 LIVE':'⚪ Offline'; document.getElementById('ytChStatusBadge').style.color=on?'#ef4444':'var(--text3)'; if(on){ _ytElapsedSec=0; if(_ytElapsedTimer) clearInterval(_ytElapsedTimer); _ytElapsedTimer=setInterval(()=>{ _ytElapsedSec++; const m=Math.floor(_ytElapsedSec/60), s=String(_ytElapsedSec%60).padStart(2,'0'); const el=document.getElementById('ytElapsed'); if(el) el.textContent=`${m}:${s}`; // Simulate bitrate health const mbps=(5.8+Math.random()*0.4).toFixed(2); const el2=document.getElementById('ytBitrate'); if(el2) el2.textContent=mbps+' Mbps'; },1000); } else { if(_ytElapsedTimer){clearInterval(_ytElapsedTimer);_ytElapsedTimer=null;} } } async function ytFetchChannel(){ const handle=document.getElementById('ytChannelHandle').value.trim(); if(!handle){ytLog('Enter a channel handle or URL.');return;} ytSetStatus('Fetching channel info...','#6366f1'); ytLog('Looking up: '+handle); try{ const r=await fetch(`/api/youtube_channel_info?handle=${encodeURIComponent(handle)}`); const d=await r.json(); if(d.error) throw new Error(d.error); document.getElementById('ytChName').textContent=d.name||'Unknown Channel'; document.getElementById('ytChSubs').textContent=d.subs?(d.subs+' subscribers'):'YouTube Channel'; const thumb=document.getElementById('ytChThumb'); if(d.thumb){thumb.src=d.thumb;}else{thumb.style.background='rgba(239,68,68,0.3)';} document.getElementById('ytChannelCard').style.display='flex'; ytSetStatus('✓ Channel connected','#22c55e'); ytLog('Connected to: '+d.name); }catch(e){ ytSetStatus('Channel not found','#ef4444'); ytLog('Error: '+e.message); } } async function startYTStream(){ if(!vmBlob){alert('Build a video first.');return;} const key=(document.getElementById('ytStreamKey')||{value:''}).value.trim(); if(!key){alert('Enter your YouTube Stream Key from YouTube Studio.');return;} const loop=document.getElementById('ytLoop')?.checked; const quality=document.getElementById('ytQuality')?.value||'6000k'; const btn=document.getElementById('ytGoLiveBtn'); btn.disabled=true; btn.textContent='⏳ Starting...'; document.getElementById('ytStopBtn').style.display='inline-block'; document.getElementById('ytLog').innerHTML=''; document.getElementById('ytLog').style.display='block'; ytSetStatus('⏳ Uploading video...','#6366f1'); ytLog(`Video: ${(vmBlob.size/1024/1024).toFixed(1)} MB · Quality: ${quality} · Loop: ${loop?'Yes':'No'}`); try{ const params=new URLSearchParams({key,loop:loop?'1':'0',mime:vmBlob.type,maxrate:quality}); const resp=await fetch(`/api/youtube_stream?${params}`,{method:'POST',headers:{'Content-Type':vmBlob.type},body:vmBlob}); const result=await resp.json(); if(!resp.ok||result.error) throw new Error(result.error||'Server error'); btn.disabled=false; btn.textContent='🔴 Go Live'; ytSetStatus('🔴 LIVE — Streaming','#ef4444'); ytLog('✅ Stream live! RTMP connected. PID '+result.pid); ytShowLive(true); if(_ytPoll) clearInterval(_ytPoll); _ytPoll=setInterval(pollYTStream,6000); }catch(e){ ytSetStatus('❌ '+e.message,'#ef4444'); ytLog('❌ Error: '+e.message); btn.disabled=false; btn.textContent='🔴 Go Live'; document.getElementById('ytStopBtn').style.display='none'; ytShowLive(false); } } async function stopYTStream(){ if(_ytPoll){clearInterval(_ytPoll);_ytPoll=null;} ytSetStatus('Stopping stream...','#f59e0b'); ytLog('Stopping RTMP stream...'); try{await fetch('/api/youtube_stream_stop',{method:'POST'});}catch(e){} ytSetStatus('Stream stopped','var(--text3)'); ytLog('Stream ended.'); document.getElementById('ytStopBtn').style.display='none'; document.getElementById('ytGoLiveBtn').textContent='🔴 Go Live'; ytShowLive(false); } async function pollYTStream(){ try{ const d=await (await fetch('/api/youtube_stream_status')).json(); if(d.running){ ytSetStatus('🔴 LIVE — Streaming','#ef4444'); } else { ytSetStatus('Stream ended','var(--text3)'); ytLog('Stream ended by server.'); if(_ytPoll){clearInterval(_ytPoll);_ytPoll=null;} document.getElementById('ytStopBtn').style.display='none'; ytShowLive(false); } }catch(e){} } // ─── Init ───────────────────────────────────────────────────────────────────── refreshNews();