Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | import { NextResponse } from 'next/server' import { existsSync } from 'fs' import { join } from 'path' import { sql } from 'drizzle-orm' import { db } from '@/db' import { ttsCollectedClips, ttsCollectedClipSay } from '@/db/schema' import { withAuth } from '@/lib/auth/withAuth' const AUDIO_DIR = join(process.cwd(), 'data', 'audio') /** * POST /api/audio/collected-clips * * Upserts a batch of collected clips from the client. * Increments play_count, updates last_seen_at, and upserts say entries. * * Payload: { clips: [{ clipId, say?, tone, playCount }] } * - clipId: human-readable clip ID * - say: optional locale -> text map (e.g. { "en-US": "Hello" }) * - tone: freeform tone/instruction for TTS generation * - playCount: number of times played since last flush */ export const POST = withAuth(async (request) => { try { const body = await request.json() const clips = body?.clips as | Array<{ clipId: string say?: Record<string, string> tone: string playCount: number }> | undefined if (!Array.isArray(clips) || clips.length === 0) { return NextResponse.json({ ok: true, upserted: 0 }) } const now = new Date().toISOString() let upserted = 0 for (const clip of clips) { if (!clip.clipId) continue // Upsert the clip row await db .insert(ttsCollectedClips) .values({ id: clip.clipId, tone: clip.tone || '', playCount: clip.playCount || 0, firstSeenAt: now, lastSeenAt: now, }) .onConflictDoUpdate({ target: ttsCollectedClips.id, set: { playCount: sql`${ttsCollectedClips.playCount} + ${clip.playCount || 0}`, lastSeenAt: now, }, }) // Upsert say entries if (clip.say) { for (const [locale, text] of Object.entries(clip.say)) { if (!text) continue await db .insert(ttsCollectedClipSay) .values({ clipId: clip.clipId, locale, text, }) .onConflictDoUpdate({ target: [ttsCollectedClipSay.clipId, ttsCollectedClipSay.locale], set: { text }, }) } } upserted++ } return NextResponse.json({ ok: true, upserted }) } catch (error) { console.error('Error upserting collected clips:', error) return NextResponse.json({ error: 'Failed to upsert collected clips' }, { status: 500 }) } }) /** * GET /api/audio/collected-clips * * Returns all collected clips with their say entries, sorted by play count descending. * Optional `?voice=onyx` param adds per-clip generation status for that voice. */ export const GET = withAuth(async (request) => { try { const voice = request.nextUrl.searchParams.get('voice') const clips = await db .select() .from(ttsCollectedClips) .orderBy(sql`${ttsCollectedClips.playCount} DESC`) // Fetch all say entries and group by clipId const sayEntries = await db.select().from(ttsCollectedClipSay) const sayByClipId = new Map<string, Record<string, string>>() for (const entry of sayEntries) { let map = sayByClipId.get(entry.clipId) if (!map) { map = {} sayByClipId.set(entry.clipId, map) } map[entry.locale] = entry.text } // Build response with say maps attached const clipsWithSay = clips.map((clip) => ({ ...clip, say: sayByClipId.get(clip.id) ?? null, })) let generatedFor: Record<string, boolean> | undefined let deactivatedFor: Record<string, boolean> | undefined if (voice) { const voiceDir = join(AUDIO_DIR, voice) generatedFor = {} deactivatedFor = {} for (const clip of clips) { generatedFor[clip.id] = existsSync(join(voiceDir, `${clip.id}.mp3`)) || existsSync(join(voiceDir, `cc-${clip.id}.mp3`)) || existsSync(join(voiceDir, `${clip.id}.webm`)) || existsSync(join(voiceDir, `cc-${clip.id}.webm`)) deactivatedFor[clip.id] = existsSync(join(voiceDir, '.deactivated', `cc-${clip.id}.mp3`)) || existsSync(join(voiceDir, '.deactivated', `cc-${clip.id}.webm`)) || existsSync(join(voiceDir, '.deactivated', `${clip.id}.mp3`)) || existsSync(join(voiceDir, '.deactivated', `${clip.id}.webm`)) } } return NextResponse.json({ clips: clipsWithSay, ...(generatedFor ? { generatedFor } : {}), ...(deactivatedFor ? { deactivatedFor } : {}), }) } catch (error) { console.error('Error fetching collected clips:', error) return NextResponse.json({ error: 'Failed to fetch collected clips' }, { status: 500 }) } }) |