Initial commit: PodcastYes — AI podcast platform

This commit is contained in:
Leon Serfaty
2026-06-07 03:58:32 -04:00
commit 155507f21a
151 changed files with 19826 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
// Quick DB verification. Run: npx tsx scripts/check-db.ts
import "dotenv/config";
import { prisma } from "@/lib/db";
async function main() {
const tables = await prisma.$queryRawUnsafe<{ table_name: string }[]>(
"select table_name from information_schema.tables where table_schema = 'public' order by table_name"
);
const names = tables.map((t) => t.table_name);
console.log(`Tables (${names.length}):`, names.join(", "));
const expected = [
"user",
"session",
"account",
"verification",
"organization",
"member",
"invitation",
"subscription",
"episode",
"script",
"audio_asset",
"cover_art",
"generation_job",
"plan",
"usage_record",
"api_key",
"audit_log",
];
const have = new Set(names);
const missing = expected.filter((t) => !have.has(t));
console.log(missing.length ? `MISSING: ${missing.join(", ")}` : "✓ All expected tables present");
const [users, plans] = await Promise.all([prisma.user.count(), prisma.plan.count()]);
console.log(`Users: ${users} · Plans: ${plans}`);
}
main()
.catch((e) => {
console.error("Check failed:", e.message ?? e);
process.exit(1);
})
.finally(() => prisma.$disconnect());
+23
View File
@@ -0,0 +1,23 @@
// Promote a user to admin by email. Run: npx tsx scripts/make-admin.ts you@email.com
import "dotenv/config";
import { prisma } from "@/lib/db";
async function main() {
const email = process.argv[2];
if (!email) {
console.error("Usage: tsx scripts/make-admin.ts <email>");
process.exit(1);
}
const user = await prisma.user.update({
where: { email },
data: { role: "admin" },
});
console.log(`${user.email} is now an admin.`);
}
main()
.catch((err) => {
console.error("Failed:", err.message ?? err);
process.exit(1);
})
.finally(() => prisma.$disconnect());
+22
View File
@@ -0,0 +1,22 @@
// After `next build` with output:"standalone", Next does NOT copy static assets or
// /public into the standalone folder. PM2 runs .next/standalone/server.js, so copy
// them in here. Safe to run anywhere — no-ops if there's no standalone output.
import { cp } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
const root = process.cwd();
const standalone = path.join(root, ".next", "standalone");
if (!existsSync(standalone)) {
console.log("postbuild: no standalone output, skipping");
process.exit(0);
}
await cp(path.join(root, ".next", "static"), path.join(standalone, ".next", "static"), {
recursive: true,
});
if (existsSync(path.join(root, "public"))) {
await cp(path.join(root, "public"), path.join(standalone, "public"), { recursive: true });
}
console.log("postbuild: ✓ copied static (+ public) into .next/standalone");
+44
View File
@@ -0,0 +1,44 @@
// Verify the Better Auth ↔ Prisma schema by performing a real signup.
// Run: npx tsx scripts/test-auth.ts
import "dotenv/config";
import { auth } from "@/lib/auth/auth";
import { prisma } from "@/lib/db";
async function main() {
const email = `verify_${Date.now()}@podcastyes.test`;
try {
await auth.api.signUpEmail({
body: { email, password: "Password123!", name: "Verify User" },
});
console.log("signUpEmail: completed");
} catch (e) {
// The nextCookies plugin may throw when run outside a Next request — the DB
// write happens first, so we still verify via the row below.
console.log("signUpEmail threw (often just the cookie step):", (e as Error).message);
}
const user = await prisma.user.findUnique({
where: { email },
include: { accounts: true, sessions: true },
});
if (user) {
console.log(
`✓ Better Auth schema works — user=${user.email} role=${user.role} ` +
`accounts=${user.accounts.length} (provider=${user.accounts[0]?.providerId}) ` +
`hasPassword=${!!user.accounts[0]?.password}`
);
// Clean up the test user.
await prisma.user.delete({ where: { id: user.id } });
console.log("✓ test user cleaned up");
} else {
console.log("✗ No user row created — schema mismatch likely");
}
}
main()
.catch((e) => {
console.error("Failed:", e.message ?? e);
process.exit(1);
})
.finally(() => prisma.$disconnect());
+32
View File
@@ -0,0 +1,32 @@
// Dev sanity check for the audio segmenter. Run: npx tsx scripts/test-segment.ts
import { segmentTurns, splitLongText, flattenTurns } from "@/lib/ai/pipeline/segment";
import type { StructuredScript } from "@/lib/ai/types";
const turns = [
{ text: "Hello and welcome to the show.", voiceId: "A" },
{ text: "x".repeat(2500), voiceId: "B" }, // oversized single turn
{ text: "Thanks for having me.", voiceId: "A" },
{ text: "Let's dive in.", voiceId: "A" },
];
const segs = segmentTurns(turns, 1800);
console.log(`segmentTurns -> ${segs.length} segments`);
segs.forEach((s, i) =>
console.log(` #${i}: chars=${s.characters} voices=${s.uniqueVoices} turns=${s.turns.length}`)
);
const parts = splitLongText("One. Two. Three. " + "w".repeat(40), 15);
console.log(`splitLongText -> ${parts.length} parts (max len ${Math.max(...parts.map((p) => p.length))})`);
const script: StructuredScript = {
title: "T",
sections: [
{ id: "a", title: "A", turns: [{ speakerKey: "host", text: "Hi" }, { speakerKey: "ghost", text: "" }] },
{ id: "b", title: "B", turns: [{ speakerKey: "guest", text: "Hello" }] },
],
};
const flat = flattenTurns(script, { host: "V1", guest: "V2" }, "V1");
console.log(`flattenTurns -> ${flat.length} turns (empty dropped), voices: ${flat.map((t) => t.voiceId).join(",")}`);
const allUnder = segs.every((s) => s.characters <= 1800);
console.log(allUnder ? "PASS: all segments within limit" : "FAIL: a segment exceeds the limit");