"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import type { Prisma } from "@prisma/client"; import { getServerSession } from "@/lib/auth/guards"; import { prisma } from "@/lib/db"; import { subjectHasFeature } from "@/lib/billing/subscription"; import { enforceLimit, LimitExceededError } from "@/lib/usage/limits"; import { enqueueEpisodeGeneration } from "@/lib/queue/pgboss"; import { FORMAT_SPEAKERS } from "@/lib/episodes/options"; import { DEFAULT_VOICE_IDS, VOICE_CATALOG } from "@/lib/ai/voices"; const createSchema = z.object({ theme: z.string().min(5).max(500), count: z.number().int().min(2).max(12), tone: z.string().min(1), audience: z.string().max(200).optional(), language: z.string().min(2).max(5), }); export async function createSeriesAction( input: z.infer ): Promise<{ ok: boolean; error?: string; seriesId?: string }> { const session = await getServerSession(); if (!session) return { ok: false, error: "You must be signed in." }; if (!(await subjectHasFeature(session.user.id, "series_generator", session.session.activeOrganizationId))) { return { ok: false, error: "The series generator requires the Pro plan." }; } const parsed = createSchema.safeParse(input); if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input." }; const { planSeason } = await import("@/lib/ai/series"); const { plan } = await planSeason(parsed.data); const series = await prisma.series.create({ data: { userId: session.user.id, organizationId: session.session.activeOrganizationId ?? undefined, title: plan.title, description: plan.description, plannedCount: plan.episodes.length, plan: plan.episodes as unknown as Prisma.InputJsonValue, }, }); revalidatePath("/series"); return { ok: true, seriesId: series.id }; } export async function generateFromSeriesAction( seriesId: string, index: number ): Promise<{ ok: boolean; error?: string; episodeId?: string }> { const session = await getServerSession(); if (!session) return { ok: false, error: "You must be signed in." }; const series = await prisma.series.findUnique({ where: { id: seriesId } }); if (!series || series.userId !== session.user.id) return { ok: false, error: "Not allowed." }; const episodes = (series.plan as unknown as { title: string; topic: string; summary: string }[]) ?? []; const item = episodes[index]; if (!item) return { ok: false, error: "Episode not found in plan." }; try { await enforceLimit(session.user.id, "script", session.session.activeOrganizationId); await enforceLimit(session.user.id, "audio", session.session.activeOrganizationId); } catch (err) { if (err instanceof LimitExceededError) { return { ok: false, error: `Monthly ${err.check.metric} limit reached.` }; } throw err; } const speakers = FORMAT_SPEAKERS.SOLO.map((s, i) => ({ speakerKey: s.speakerKey, displayName: s.defaultName, elevenVoiceId: DEFAULT_VOICE_IDS[s.speakerKey] ?? VOICE_CATALOG[i % VOICE_CATALOG.length].id, })); const episode = await prisma.episode.create({ data: { userId: session.user.id, organizationId: series.organizationId ?? undefined, seriesId: series.id, title: item.title, topic: item.topic, tone: "Conversational", format: "SOLO", language: "en", targetLengthMin: 10, status: "QUEUED", stage: "Queued for generation", speakers: { create: speakers }, jobs: { create: { type: "full", status: "queued" } }, }, }); await enqueueEpisodeGeneration({ episodeId: episode.id, type: "full" }); revalidatePath(`/series/${seriesId}`); return { ok: true, episodeId: episode.id }; }