import * as fs from 'fs'; import * as path from 'path'; import { glob } from 'glob'; interface FeedDataItem { post?: { like_count?: number; view_count?: number; reply_count?: number; remix_count?: number; attachments?: Array<{ kind?: string; id?: string }> }; profile?: { user_id?: string }; } interface FeedDataFile { items?: FeedDataItem[]; } interface ChartDataPoint { timestamp: Date; postCount: number; totalLikes: number; totalViews: number; totalReplies: number; totalRemixes: number; uniqueUsers: number; soraVideos: number; uniqueVideos: number; } export class FeedChartGenerator { private dataDir: string; private outputDir: string; constructor(dataDir = './feed-monitor-results', outputDir = './feed-monitor-results') { this.dataDir = dataDir; this.outputDir = outputDir; } async loadFeedData(): Promise { const pattern = path.join(this.dataDir, 'feed-*.json'); const files = await glob(pattern); const dataPoints: ChartDataPoint[] = []; const seenVideoIds = new Set(); for (const filePath of files.sort()) { try { const content = fs.readFileSync(filePath, 'utf-8'); const data: FeedDataFile = JSON.parse(content); const filename = path.basename(filePath); const timestamp = this.parseTimestamp(filename); if (!timestamp) { continue; } const items = data.items || []; const postCount = items.length; const totalLikes = items.reduce((sum, item) => sum + (item.post?.like_count || 0), 0); const totalViews = items.reduce((sum, item) => sum + (item.post?.view_count || 0), 0); const totalReplies = items.reduce((sum, item) => sum + (item.post?.reply_count || 0), 0); const totalRemixes = items.reduce((sum, item) => sum + (item.post?.remix_count || 0), 0); const uniqueUsers = new Set(items.map(i => i.profile?.user_id)).size; const soraVideos = items.reduce((count, item) => count + ((item.post?.attachments || []).filter(att => att.kind === 'sora').length), 0); // Update cumulative unique videos by extracting video IDs from attachments items.forEach(item => { (item.post?.attachments || []).forEach(att => { if (att.kind === 'sora' && att.id) { seenVideoIds.add(att.id); } }); }); const uniqueVideos = seenVideoIds.size; dataPoints.push({ timestamp, postCount, totalLikes, totalViews, totalReplies, totalRemixes, uniqueUsers, soraVideos, uniqueVideos }); } catch { // skip invalid file } } return dataPoints.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); } private parseTimestamp(filename: string): Date | null { try { let timestampStr = filename.replace('feed-', '').replace('.json', ''); if (timestampStr.endsWith('Z')) { timestampStr = timestampStr.slice(0, -1); timestampStr = timestampStr.replace('T', ' '); } if (timestampStr.includes('-') && !timestampStr.includes('T')) { const parts = timestampStr.split(' '); if (parts.length === 2) { const [datePart, timePart] = parts; const formattedTime = timePart.replace(/-/g, ':'); timestampStr = `${datePart} ${formattedTime}`; } } const date = new Date(timestampStr); if (isNaN(date.getTime())) { return null; } return date; } catch { return null; } } private generateHTML(dataPoints: ChartDataPoint[]): string { const labels = dataPoints.map(dp => { const utc8Time = new Date(dp.timestamp.getTime() + (8 * 60 * 60 * 1000)); return utc8Time.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }); }); const html = ` Feed Monitor Charts (UTC+8)

Feed Monitor Analytics (UTC+8)

Sora Videos & Cumulative Unique Videos
`; return html; } async generateAllCharts(): Promise { if (!fs.existsSync(this.outputDir)) { fs.mkdirSync(this.outputDir, { recursive: true }); } const dataPoints = await this.loadFeedData(); if (dataPoints.length === 0) { console.log('No data found. Please check the data directory path.'); return; } const html = this.generateHTML(dataPoints); const outputPath = path.join(this.outputDir, 'feed-charts.html'); fs.writeFileSync(outputPath, html, 'utf-8'); console.log(`\nCharts generated successfully!`); console.log(`HTML file saved to: ${outputPath}`); console.log(`Open the file in your browser to view the interactive charts.`); } }