feat: add loading of recommendations to movie page

This commit is contained in:
2023-09-08 16:52:26 +02:00
parent 04c3578d61
commit 67c2785b80
9 changed files with 269 additions and 47 deletions

View File

@ -47,6 +47,16 @@ export async function fetchStream(url: string, cb: (chunk: string) => void) {
}
}
export function hashString(message: string) {
let hash = 0;
for (let i = 0; i < message.length; i++) {
const char = message.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
export const createStreamResponse = () => {
let controller: ReadableStreamController<ArrayBufferView>;
const body = new ReadableStream({

View File

@ -1,5 +1,7 @@
import { OpenAI } from "https://deno.land/x/openai@1.4.2/mod.ts";
import { OPENAI_API_KEY } from "@lib/env.ts";
import { cacheFunction } from "@lib/cache/cache.ts";
import { hashString } from "@lib/helpers.ts";
const openAI = OPENAI_API_KEY && new OpenAI(OPENAI_API_KEY);
@ -9,8 +11,8 @@ function extractListFromResponse(response?: string): string[] {
.split(/[\n,]/)
.map((line) => line.trim())
.filter((line) => !line.endsWith(":"))
.map((line) => line.replace(/^[^(a-zA-Z)]*/, "").trim())
.filter((line) => line.length > 0);
.map((line) => line.replace(/^[^\s]*/, "").trim())
.filter((line) => line.length > 2);
}
export async function summarize(content: string) {
@ -73,7 +75,7 @@ export async function extractAuthorName(content: string) {
return author;
}
export async function createKeywords(
export async function createGenres(
type: string,
description: string,
title = "unknown",
@ -85,7 +87,7 @@ export async function createKeywords(
{
"role": "system",
"content":
`you create some keywords that can be used in a recommendation system. The keywords are based on a ${type} description or title. If you do not know the title, take into account the description aswell. Create a range of keywords from very specific ones that describe the general vibe. Also include some that describe the genre. ${
`you create some keywords that can be used in a recommendation system. The keywords are based on a ${type} description or title. If you do not know the title, take into account the description aswell. Create a range of keywords from very specific ones that describe the general vibe. ${
title ? `The name of the ${type} is ${title}` : ""
}`,
},
@ -107,6 +109,82 @@ export async function createKeywords(
.map((v) => v.replaceAll(" ", "-"));
}
export async function createKeywords(
type: string,
description: string,
title = "unknown",
) {
if (!openAI) return;
const chatCompletion = await openAI.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
{
"role": "system",
"content":
`you create some keywords that can be used in a recommendation system. The keywords are based on a ${type} description or title. If you do not know the title, take into account the description aswell. Create a range of keywords from very specific ones that describe the general vibe.
title: ${title}
description: ${description.slice(0, 2000).replaceAll("\n", " ")}}
`,
},
{
"role": "user",
"content": "return a list of around 20 keywords seperated by commas",
},
],
});
const res = chatCompletion.choices[0].message.content?.toLowerCase();
return extractListFromResponse(res)
.map((v) => v.replaceAll(" ", "-"));
}
export const getMovieRecommendations = (keywords: string, exclude: string[]) =>
cacheFunction({
fn: async () => {
if (!openAI) return;
const chatCompletion = await openAI.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
{
"role": "system",
"content":
`Could you recommend me 10 movies based on the following attributes:
${keywords}
The movies should be similar to but not include ${
exclude.join(", ")
} or remakes of that.
respond with a plain unordered list each item starting with the year the movie was released and then the title of the movie seperated by a -`,
},
],
});
const res = chatCompletion.choices[0].message.content?.toLowerCase();
if (!res) return;
console.log("REsult:");
console.log(res);
const list = extractListFromResponse(res);
console.log({ list });
return res.split("\n").map((entry) => {
const [year, ...title] = entry.split("-");
return {
year: parseInt(year.trim()),
title: title.join(" ").replaceAll('"', "").trim(),
};
}).filter((y) => !Number.isNaN(y.year));
},
id: `openai:movierecs:${hashString(`${keywords}:${exclude.join()}`)}`,
});
export async function createTags(content: string) {
if (!openAI) return;
const chatCompletion = await openAI.createChatCompletion({
@ -126,4 +204,3 @@ export async function createTags(content: string) {
return extractListFromResponse(res).map((v) => v.replaceAll(" ", "-"));
}

View File

@ -1,5 +1,6 @@
import * as cache from "@lib/cache/cache.ts";
import * as openai from "@lib/openai.ts";
import * as tmdb from "@lib/tmdb.ts";
import { GenericResource } from "@lib/types.ts";
import { parseRating } from "@lib/helpers.ts";
@ -57,11 +58,39 @@ export async function createRecommendationResource(
cache.set(cacheId, JSON.stringify(resource));
}
export function getRecommendation(id: string, type: string) {
return cache.get(`recommendations:${type}:${id}`);
export async function getRecommendation(
id: string,
type: string,
): Promise<RecommendationResource | null> {
const res = await cache.get(`recommendations:${type}:${id}`) as string;
try {
return JSON.parse(res);
} catch (_) {
return null;
}
}
export async function getAllRecommendations() {
export async function getSimilarMovies(id: string) {
const recs = await getRecommendation(id, "movie");
if (!recs?.keywords?.length) return;
const recommendations = await openai.getMovieRecommendations(
recs.keywords.join(),
[recs.id],
);
if (!recommendations) return;
const movies = await Promise.all(recommendations.map(async (rec) => {
const m = await tmdb.searchMovie(rec.title, rec.year);
return m?.results?.[0];
}));
return movies.filter(Boolean);
}
export async function getAllRecommendations(): Promise<
RecommendationResource[]
> {
const keys = await cache.keys("recommendations:movie:*");
return Promise.all(keys.map((k) => cache.get(k))).then((res) =>
res.map((r) => JSON.parse(r))

View File

@ -4,10 +4,10 @@ const moviedb = new MovieDb(Deno.env.get("TMDB_API_KEY") || "");
const CACHE_INTERVAL = 1000 * 60 * 24 * 30;
export const searchMovie = (query: string) =>
export const searchMovie = (query: string, year?: number) =>
cache.cacheFunction({
fn: () => moviedb.searchMovie({ query }),
id: `query:moviesearch:${query}`,
fn: () => moviedb.searchMovie({ query, year }),
id: `query:moviesearch:${query}${year ? `-${year}` : ""}`,
options: {
expires: CACHE_INTERVAL,
},