tv shows to tv series

This commit is contained in:
2026-04-20 19:37:33 -03:00
parent c319dea2bf
commit 5e02ca267f
20 changed files with 396 additions and 117 deletions

View File

@@ -10,8 +10,8 @@ CREATE TABLE IF NOT EXISTS "recommendations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"main_prompt" text NOT NULL,
"liked_shows" text DEFAULT '' NOT NULL,
"disliked_shows" text DEFAULT '' NOT NULL,
"liked_series" text DEFAULT '' NOT NULL,
"disliked_series" text DEFAULT '' NOT NULL,
"themes" text DEFAULT '' NOT NULL,
"brainstorm_count" integer DEFAULT 100 NOT NULL,
"recommendations" jsonb,

View File

@@ -0,0 +1,2 @@
ALTER TABLE "recommendations" RENAME COLUMN "liked_shows" TO "liked_series";
ALTER TABLE "recommendations" RENAME COLUMN "disliked_shows" TO "disliked_series";

View File

@@ -89,15 +89,15 @@
"primaryKey": false,
"notNull": true
},
"liked_shows": {
"name": "liked_shows",
"liked_series": {
"name": "liked_series",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"disliked_shows": {
"name": "disliked_shows",
"disliked_series": {
"name": "disliked_series",
"type": "text",
"primaryKey": false,
"notNull": true,

View File

@@ -4,7 +4,7 @@ import { z } from 'zod';
import { zodTextFormat } from 'openai/helpers/zod';
const CuratorSchema = z.object({
shows: z.array(z.object({
series: z.array(z.object({
title: z.string(),
explanation: z.string(),
category: z.enum(["Full Match", "Definitely Like", "Might Like", "Questionable", "Will Not Like"]),
@@ -24,7 +24,7 @@ export async function runCurator(
): Promise<CuratorOutput[]> {
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
const allShows = [
const allSeries = [
...(ranking.full_match ?? []).map((t) => ({ title: t, category: 'Full Match' as const })),
...ranking.definitely_like.map((t) => ({ title: t, category: 'Definitely Like' as const })),
...ranking.might_like.map((t) => ({ title: t, category: 'Might Like' as const })),
@@ -32,7 +32,7 @@ export async function runCurator(
...ranking.will_not_like.map((t) => ({ title: t, category: 'Will Not Like' as const })),
];
if (allShows.length === 0) return [];
if (allSeries.length === 0) return [];
const canSearch = useWebSearch && supportsWebSearch;
const preferenceSummary = `User preferences summary:
@@ -53,9 +53,9 @@ Rules:
- cons: up to 3 short bullet points about what the user might not like based on their preferences
- Be honest — explain why "Questionable" or "Will Not Like" ${mediaLabel}s got that rating`;
const chunks: typeof allShows[] = [];
for (let i = 0; i < allShows.length; i += CHUNK_SIZE) {
chunks.push(allShows.slice(i, i + CHUNK_SIZE));
const chunks: typeof allSeries[] = [];
for (let i = 0; i < allSeries.length; i += CHUNK_SIZE) {
chunks.push(allSeries.slice(i, i + CHUNK_SIZE));
}
const results: CuratorOutput[] = [];
@@ -66,14 +66,14 @@ Rules:
temperature: 0.5,
...serviceOptions,
...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
text: { format: zodTextFormat(CuratorSchema, "shows") },
text: { format: zodTextFormat(CuratorSchema, "series") },
instructions,
input: `${preferenceSummary}
${mediaLabel}s to describe:
${showList}`,
}));
results.push(...(response.output_parsed?.shows ?? []));
results.push(...(response.output_parsed?.series ?? []));
}
return results;

View File

@@ -15,8 +15,8 @@ const InterpreterSchema = z.object({
interface InterpreterInput {
main_prompt: string;
liked_shows: string;
disliked_shows: string;
liked_series: string;
disliked_series: string;
themes: string;
media_type: MediaType;
feedback_context?: string;
@@ -43,8 +43,8 @@ Rules:
- Be specific and concrete, not vague
- For "requirements": capture explicit hard requirements the user stated that recommendations must satisfy — things like "must be from the 2000s onward", "must have subtitles", "must feature a female lead". Leave empty if no such constraints were stated.`,
input: `Main prompt: ${input.main_prompt}
Liked ${mediaLabel}s: ${input.liked_shows || '(none)'}
Disliked ${mediaLabel}s: ${input.disliked_shows || '(none)'}
Liked ${mediaLabel}s: ${input.liked_series || '(none)'}
Disliked ${mediaLabel}s: ${input.disliked_series || '(none)'}
Themes and requirements: ${input.themes || '(none)'}${feedbackSection}`,
}));

View File

@@ -19,7 +19,7 @@ export async function runRetrieval(
previousFullMatches: string[] = [],
): Promise<RetrievalOutput> {
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows';
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV series';
const canSearch = useWebSearch && supportsWebSearch;
const response = await parseWithRetry(() => openai.responses.parse({

View File

@@ -38,7 +38,7 @@ function buildSystemPrompt(
seenTitles: string[]
): string {
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows';
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV series';
let prompt = `You are a ${mediaLabel} recommendation specialist. Your task is to recommend titles that match the user's taste profile.

View File

@@ -5,8 +5,8 @@ export const recommendations = pgTable('recommendations', {
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
main_prompt: text('main_prompt').notNull(),
liked_shows: text('liked_shows').notNull().default(''),
disliked_shows: text('disliked_shows').notNull().default(''),
liked_series: text('liked_series').notNull().default(''),
disliked_series: text('disliked_series').notNull().default(''),
themes: text('themes').notNull().default(''),
brainstorm_count: integer('brainstorm_count').notNull().default(100),
media_type: text('media_type').notNull().default('tv_show'),

View File

@@ -39,8 +39,8 @@ function mergeCuratorOutputs(a: CuratorOutput[], b: CuratorOutput[]): CuratorOut
}
interface ContinuousPipelineInput {
likedShows: string;
dislikedShows?: string;
likedSeries: string;
dislikedSeries?: string;
themes?: string;
requirements?: string;
avoid?: string;
@@ -57,8 +57,8 @@ export async function runContinuousPipeline(
): Promise<CuratorOutput[]> {
const startTime = Date.now();
const {
likedShows,
dislikedShows = '',
likedSeries,
dislikedSeries = '',
themes = '',
requirements = '',
avoid = '',
@@ -83,9 +83,9 @@ export async function runContinuousPipeline(
const t0 = Date.now();
interpreterOutput = await runInterpreter({
main_prompt: themes || 'recommend shows based on user preferences',
liked_shows: likedShows,
disliked_shows: dislikedShows,
main_prompt: themes || 'recommend series based on user preferences',
liked_series: likedSeries,
disliked_series: dislikedSeries,
themes: themes,
media_type: mediaType,
});
@@ -267,8 +267,8 @@ export async function runContinuousPipeline(
.set({
title: aiTitle,
main_prompt: themes || 'Continuous recommendations',
liked_shows: likedShows,
disliked_shows: dislikedShows,
liked_series: likedSeries,
disliked_series: dislikedSeries,
themes: themes,
brainstorm_count: totalCount,
media_type: mediaType,

View File

@@ -201,8 +201,8 @@ export async function runPipeline(
const t0 = Date.now();
const interpreterOutput = await runInterpreter({
main_prompt: rec.main_prompt,
liked_shows: rec.liked_shows,
disliked_shows: rec.disliked_shows,
liked_series: rec.liked_series,
disliked_series: rec.disliked_series,
themes: rec.themes,
media_type: mediaType,
...(feedbackContext !== undefined ? { feedback_context: feedbackContext } : {}),

View File

@@ -29,8 +29,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
fastify.post('/recommendations', async (request, reply) => {
const body = request.body as {
main_prompt: string;
liked_shows?: string;
disliked_shows?: string;
liked_series?: string;
disliked_series?: string;
themes?: string;
brainstorm_count?: number;
media_type?: string;
@@ -64,8 +64,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
.values({
title: title || 'Untitled',
main_prompt: body.main_prompt ?? '',
liked_shows: body.liked_shows ?? '',
disliked_shows: body.disliked_shows ?? '',
liked_series: body.liked_series ?? '',
disliked_series: body.disliked_series ?? '',
themes: body.themes ?? '',
brainstorm_count,
media_type,
@@ -85,8 +85,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
// POST /recommendations/continuous — create record and run continuous pipeline with SSE
fastify.post('/recommendations/continuous', async (request, reply) => {
const body = request.body as {
liked_shows: string;
disliked_shows?: string;
liked_series: string;
disliked_series?: string;
themes?: string;
requirements?: string;
avoid?: string;
@@ -108,8 +108,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
.values({
title,
main_prompt: body.themes ?? 'Continuous recommendations',
liked_shows: body.liked_shows,
disliked_shows: body.disliked_shows ?? '',
liked_series: body.liked_series,
disliked_series: body.disliked_series ?? '',
themes: body.themes ?? '',
brainstorm_count: totalCount,
media_type: mediaType,
@@ -155,8 +155,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
// Run the continuous pipeline (it will now update the existing record)
await runContinuousPipeline(recId, {
likedShows: body.liked_shows,
dislikedShows: body.disliked_shows ?? '',
likedSeries: body.liked_series,
dislikedSeries: body.disliked_series ?? '',
themes: body.themes ?? '',
requirements: body.requirements ?? '',
avoid: body.avoid ?? '',
@@ -305,11 +305,11 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
const feedbackContext =
feedbackRows.length > 0
? feedbackRows
.map(
(f) =>
`${mediaLabel}: "${f.item_name}" — Rating: ${f.stars}/3 stars${f.feedback ? ` — Comment: ${f.feedback}` : ''}`,
)
.join('\n')
.map(
(f) =>
`${mediaLabel}: "${f.item_name}" — Rating: ${f.stars}/3 stars${f.feedback ? ` — Comment: ${f.feedback}` : ''}`,
)
.join('\n')
: undefined;
await runPipeline(rec, (event) => {

View File

@@ -83,8 +83,8 @@ export interface ContinuousSession {
export interface ContinuousStartRequest {
mediaType: 'tv_show' | 'movie';
likedShows: string;
dislikedShows?: string;
likedSeries: string;
dislikedSeries?: string;
themes?: string;
requirements?: string;
avoid?: string;