Ionosphere.tv
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf: derive waveform density from diarization, eliminate words array

WaveformBand now computes speech density from diarization segments
instead of requiring the 3MB words array. This works at all zoom
levels — the diarization data (151KB, already loaded) provides both
speaker identity and speech timing.

getTrack response drops from 2.8MB to ~155KB (talks + diarization).
The words array is no longer fetched at any zoom level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+38 -33
+8 -9
apps/ionosphere-appview/src/routes.ts
··· 675 675 }); 676 676 } 677 677 678 - // Default: exclude transcript (9MB+ facets) — fetched lazily via ?include=transcript 679 - const include = c.req.query("include"); 680 - if (include !== "transcript") { 681 - c.header("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400"); 682 - const { transcript, ...rest } = data; 683 - return c.json(rest); 684 - } 685 - 678 + // Default: exclude transcript (9MB+ facets) and words (3MB, empty speakers) 679 + // Transcript fetched lazily via chunked endpoint; words only used at high zoom 680 + const include = c.req.query("include")?.split(",") ?? []; 686 681 c.header("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400"); 687 - return c.json(data); 682 + const { transcript, words, ...rest } = data; 683 + const result: any = { ...rest }; 684 + if (include.includes("transcript")) result.transcript = transcript; 685 + if (include.includes("words")) result.words = words; 686 + return c.json(result); 688 687 }); 689 688 690 689 // --- Chunked track transcript (HLS-inspired) ---
+28 -23
apps/ionosphere/src/app/components/WaveformBand.tsx
··· 5 5 import { useTimelineEngine } from "@/lib/timeline-engine"; 6 6 7 7 interface WaveformBandProps { 8 - words: Array<{ start: number; end: number; speaker: string }>; 9 8 diarization: Array<{ start: number; end: number; speaker: string }>; 10 9 allSpeakers: string[]; 11 10 zoomLevel: number; ··· 15 14 interface Bin { 16 15 startTime: number; 17 16 endTime: number; 18 - wordCount: number; 17 + speechDensity: number; // 0-1: fraction of bin covered by speech 19 18 dominantSpeaker: string; 20 19 } 21 20 22 21 export default function WaveformBand({ 23 - words, 24 22 diarization, 25 23 allSpeakers, 26 24 zoomLevel, ··· 33 31 34 32 const useWaveform = zoomLevel >= 4; 35 33 34 + // Compute speech density bins from diarization segments (no words array needed) 36 35 const bins = useMemo(() => { 37 - if (!useWaveform || words.length === 0) return []; 36 + if (!useWaveform || diarization.length === 0) return []; 38 37 39 38 const binCount = Math.min(400, Math.max(50, Math.round(windowDuration * 2))); 40 39 const binDuration = windowDuration / binCount; ··· 43 42 for (let i = 0; i < binCount; i++) { 44 43 const binStart = windowStart + i * binDuration; 45 44 const binEnd = binStart + binDuration; 46 - const speakerCounts = new Map<string, number>(); 47 - let count = 0; 45 + const speakerDurations = new Map<string, number>(); 46 + let totalSpeech = 0; 48 47 49 - for (const w of words) { 50 - if (w.end < binStart) continue; 51 - if (w.start > binEnd) break; 52 - count++; 53 - speakerCounts.set(w.speaker, (speakerCounts.get(w.speaker) || 0) + 1); 48 + for (const seg of diarization) { 49 + if (seg.end <= binStart) continue; 50 + if (seg.start >= binEnd) break; 51 + // Overlap between segment and bin 52 + const overlapStart = Math.max(seg.start, binStart); 53 + const overlapEnd = Math.min(seg.end, binEnd); 54 + const overlap = overlapEnd - overlapStart; 55 + if (overlap > 0) { 56 + totalSpeech += overlap; 57 + speakerDurations.set(seg.speaker, (speakerDurations.get(seg.speaker) || 0) + overlap); 58 + } 54 59 } 55 60 56 61 let dominant = ""; 57 - let maxCount = 0; 58 - for (const [spk, cnt] of speakerCounts) { 59 - if (cnt > maxCount) { dominant = spk; maxCount = cnt; } 62 + let maxDur = 0; 63 + for (const [spk, dur] of speakerDurations) { 64 + if (dur > maxDur) { dominant = spk; maxDur = dur; } 60 65 } 61 66 62 - result.push({ startTime: binStart, endTime: binEnd, wordCount: count, dominantSpeaker: dominant }); 67 + result.push({ 68 + startTime: binStart, 69 + endTime: binEnd, 70 + speechDensity: totalSpeech / binDuration, 71 + dominantSpeaker: dominant, 72 + }); 63 73 } 64 74 65 75 return result; 66 - }, [words, windowStart, windowEnd, windowDuration, useWaveform]); 67 - 68 - const maxWordCount = useMemo( 69 - () => Math.max(1, ...bins.map((b) => b.wordCount)), 70 - [bins], 71 - ); 76 + }, [diarization, windowStart, windowEnd, windowDuration, useWaveform]); 72 77 73 78 const visibleDiarization = useMemo(() => { 74 79 if (useWaveform) return []; ··· 122 127 > 123 128 {useWaveform 124 129 ? bins.map((bin, i) => { 125 - if (bin.wordCount === 0) return null; 130 + if (bin.speechDensity === 0) return null; 126 131 const left = ((bin.startTime - windowStart) / windowDuration) * 100; 127 132 const width = ((bin.endTime - bin.startTime) / windowDuration) * 100; 128 - const height = (bin.wordCount / maxWordCount) * 100; 133 + const height = Math.min(bin.speechDensity, 1) * 100; 129 134 130 135 return ( 131 136 <div
+2 -1
apps/ionosphere/src/app/tracks/[stream]/TrackViewContent.tsx
··· 255 255 const [panCenter, setPanCenter] = useState<number | null>(null); 256 256 const [containerWidth, setContainerWidth] = useState(800); 257 257 258 + // WaveformBand now derives density from diarization — no words array needed 259 + 258 260 const timelineContainerRef = useRef<HTMLDivElement>(null); 259 261 const timelineBarRef = useRef<HTMLDivElement>(null); 260 262 const saveRef = useRef<(() => void) | null>(null); ··· 443 445 {track.diarization.length > 0 && ( 444 446 <div className="mt-1"> 445 447 <WaveformBand 446 - words={track.words ?? []} 447 448 diarization={track.diarization} 448 449 allSpeakers={allSpeakers} 449 450 zoomLevel={zoomLevel}