diff --git a/package.json b/package.json index faf70717..cee68512 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@urql/core": "4.0.7", "ahooks": "^3.7.7", "aplayer-react": "^1.1.0", + "async-lock": "^1.4.0", "canvas": "^2.11.2", "canvas-confetti": "^1.6.0", "chroma-js": "^2.4.2", @@ -148,6 +149,7 @@ "@next/bundle-analyzer": "13.4.1", "@playwright/test": "1.33.0", "@trivago/prettier-plugin-sort-imports": "4.1.1", + "@types/async-lock": "^1.4.0", "@types/canvas-confetti": "^1.6.0", "@types/chroma-js": "^2.4.0", "@types/cookie": "0.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71c443c5..7732def5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,9 @@ dependencies: aplayer-react: specifier: ^1.1.0 version: 1.1.0(react@18.2.0) + async-lock: + specifier: ^1.4.0 + version: 1.4.0 canvas: specifier: ^2.11.2 version: 2.11.2 @@ -357,6 +360,9 @@ devDependencies: '@trivago/prettier-plugin-sort-imports': specifier: 4.1.1 version: 4.1.1(prettier@2.8.8) + '@types/async-lock': + specifier: ^1.4.0 + version: 1.4.0 '@types/canvas-confetti': specifier: ^1.6.0 version: 1.6.0 @@ -4430,6 +4436,10 @@ packages: - supports-color dev: true + /@types/async-lock@1.4.0: + resolution: {integrity: sha512-2+rYSaWrpdbQG3SA0LmMT6YxWLrI81AqpMlSkw3QtFc2HGDufkweQSn30Eiev7x9LL0oyFrBqk1PXOnB9IEgKg==} + dev: true + /@types/bn.js@5.1.1: resolution: {integrity: sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==} dependencies: @@ -5641,6 +5651,10 @@ packages: engines: {node: '>=8'} dev: true + /async-lock@1.4.0: + resolution: {integrity: sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==} + dev: false + /async-mutex@0.2.6: resolution: {integrity: sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==} dependencies: diff --git a/src/app/api/summary/route.ts b/src/app/api/summary/route.ts deleted file mode 100644 index 178635ac..00000000 --- a/src/app/api/summary/route.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { AnalyzeDocumentChain, loadSummarizationChain } from "langchain/chains" -import { OpenAI } from "langchain/llms/openai" -import { PromptTemplate } from "langchain/prompts" -import removeMarkdown from "remove-markdown" - -import { Metadata } from "@prisma/client" - -import { toGateway } from "~/lib/ipfs-parser" -import prisma from "~/lib/prisma.server" -import { cacheGet } from "~/lib/redis.server" -import { NextServerResponse, getQuery } from "~/lib/server-helper" - -let model: OpenAI | undefined - -if (process.env.OPENAI_API_KEY) { - model = new OpenAI({ - openAIApiKey: process.env.OPENAI_API_KEY, - modelName: "gpt-3.5-turbo", - temperature: 0.3, - maxTokens: 400, - }) -} - -const chains = new Map() - -const getOriginalSummary = async (cid: string, lang: string) => { - if (!model) return - try { - let { content } = await (await fetch(toGateway(`ipfs://${cid}`))).json() - - if (content?.length > 5000) { - content = content.slice(0, 5000) - } - if (content?.length < 200) { - return "" - } else if (content) { - console.time(`fetching summary ${cid}, ${lang}`) - - let chain = chains.get(lang) - if (!chain) { - const prompt = new PromptTemplate({ - template: `Summarize this in "${lang}" language: - "{text}" - CONCISE SUMMARY:`, - inputVariables: ["text"], - }) - - const combineDocsChain = loadSummarizationChain(model, { - type: "map_reduce", - combineMapPrompt: prompt, - combinePrompt: prompt, - }) - - chain = new AnalyzeDocumentChain({ - combineDocumentsChain: combineDocsChain, - }) - - chains.set(lang, chain) - } - - const res = await chain.call({ - input_document: removeMarkdown(content, { - useImgAltText: true, - gfm: true, - }), - }) - - console.timeEnd(`fetching summary ${cid}, ${lang}`) - - return res?.text - } - } catch (error) { - console.error(error) - console.timeEnd(`fetching summary ${cid}, ${lang}`) - } -} - -async function getSummary(cid: string, lang: string = "en") { - const summary = await cacheGet({ - key: ["summary", cid, lang], - getValueFun: async () => { - if (["en", "zh", "zh-TW", "ja"].includes(lang)) { - const key = `ai_summary_${lang.replace("-", "").toLowerCase()}` - const meta = await prisma.metadata.findFirst({ - where: { - uri: `ipfs://${cid}`, - }, - }) - if (meta) { - if (meta?.[key as keyof Metadata]) { - return meta?.[key as keyof Metadata] - } else { - const summary = await getOriginalSummary(cid, lang) - if (summary) { - await prisma.metadata.update({ - where: { - uri: `ipfs://${cid}`, - }, - data: { - [key as keyof Metadata]: summary, - }, - }) - - return summary - } - } - } else { - const summary = await getOriginalSummary(cid, lang) - if (summary) { - await prisma.metadata.create({ - data: { - uri: `ipfs://${cid}`, - [key as keyof Metadata]: summary, - }, - }) - - return summary - } - } - } - }, - noUpdate: true, - }) - - return summary -} - -export async function GET(req: Request): Promise { - let { cid, lang } = getQuery(req) - const res = new NextServerResponse() - - if (!cid) { - return res.status(400).send("Bad Request") - } - - return res.status(200).json({ - data: await getSummary(cid as string, lang as string), - }) -} diff --git a/src/models/page.model.ts b/src/models/page.model.ts index 6fe74ce3..463b4d21 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -598,18 +598,6 @@ export async function updateComment( }) } -export async function getSummary({ - cid, - lang, -}: { - cid: string - lang?: string -}) { - return ( - await (await fetch(`/api/summary?cid=${cid}&lang=${lang || "en"}`)).json() - ).data -} - export async function checkMirror(characterId: number) { const notes = await indexer.getNotes({ characterId, diff --git a/src/queries/page.server.ts b/src/queries/page.server.ts index 9df5c231..0ff2a8ca 100644 --- a/src/queries/page.server.ts +++ b/src/queries/page.server.ts @@ -1,6 +1,15 @@ +import AsyncLock from "async-lock" +import { AnalyzeDocumentChain, loadSummarizationChain } from "langchain/chains" +import { OpenAI } from "langchain/llms/openai" +import { PromptTemplate } from "langchain/prompts" +import removeMarkdown from "remove-markdown" + +import { Metadata } from "@prisma/client" import { QueryClient } from "@tanstack/react-query" import { getNoteSlug } from "~/lib/helpers" +import { toGateway } from "~/lib/ipfs-parser" +import prisma from "~/lib/prisma.server" import { cacheDelete, cacheGet } from "~/lib/redis.server" import * as pageModel from "~/models/page.model" @@ -118,3 +127,129 @@ export const fetchGetPagesBySite = async ( }) as Promise> }) } + +// Post summary + +let model: OpenAI | undefined +if (process.env.OPENAI_API_KEY) { + model = new OpenAI({ + openAIApiKey: process.env.OPENAI_API_KEY, + modelName: "gpt-3.5-turbo", + temperature: 0.3, + maxTokens: 400, + }) +} +const chains = new Map() + +const getOriginalSummary = async (cid: string, lang: string) => { + if (!model) return + try { + let { content } = await (await fetch(toGateway(`ipfs://${cid}`))).json() + + if (content?.length > 5000) { + content = content.slice(0, 5000) + } + if (content?.length < 200) { + return + } else if (content) { + console.time(`fetching summary ${cid}, ${lang}`) + + let chain = chains.get(lang) + if (!chain) { + const prompt = new PromptTemplate({ + template: `Summarize this in "${lang}" language: + "{text}" + CONCISE SUMMARY:`, + inputVariables: ["text"], + }) + + const combineDocsChain = loadSummarizationChain(model, { + type: "map_reduce", + combineMapPrompt: prompt, + combinePrompt: prompt, + }) + + chain = new AnalyzeDocumentChain({ + combineDocumentsChain: combineDocsChain, + }) + + chains.set(lang, chain) + } + + const res = await chain.call({ + input_document: removeMarkdown(content, { + useImgAltText: true, + gfm: true, + }), + }) + + console.timeEnd(`fetching summary ${cid}, ${lang}`) + + return res?.text as string + } + } catch (error) { + console.error(error) + console.timeEnd(`fetching summary ${cid}, ${lang}`) + } +} + +const lock = new AsyncLock() + +export async function getSummary({ + cid, + lang = "en", +}: { + cid: string + lang?: string +}) { + const key = `ai_summary_${lang.replace("-", "").toLowerCase()}` + + const summary = (await cacheGet({ + key: ["summary", cid, lang], + getValueFun: async () => { + if (["en", "zh", "zh-TW", "ja"].includes(lang)) { + const meta = await prisma.metadata.findFirst({ + where: { + uri: `ipfs://${cid}`, + }, + }) + if (meta) { + if (meta?.[key as keyof Metadata]) { + return meta?.[key as keyof Metadata] + } else { + lock.acquire(cid, async () => { + const summary = await getOriginalSummary(cid, lang) + if (summary) { + await prisma.metadata.update({ + where: { + uri: `ipfs://${cid}`, + }, + data: { + [key as keyof Metadata]: summary, + }, + }) + } + }) + return + } + } else { + lock.acquire(cid, async () => { + const summary = await getOriginalSummary(cid, lang) + if (summary) { + await prisma.metadata.create({ + data: { + uri: `ipfs://${cid}`, + [key as keyof Metadata]: summary, + }, + }) + } + }) + return + } + } + }, + noUpdate: true, + })) as string | undefined + + return summary +} diff --git a/src/queries/page.ts b/src/queries/page.ts index 4cebad29..5b2a88c3 100644 --- a/src/queries/page.ts +++ b/src/queries/page.ts @@ -359,18 +359,6 @@ export function useGetComments( }) } -export function useGetSummary(input: { cid?: string; lang?: string }) { - return useQuery(["getSummary", input.cid, input.lang], async () => { - if (!input.cid || !input.lang) { - return - } - return pageModel.getSummary({ - cid: input.cid, - lang: input.lang, - }) - }) -} - export function useGetMirrorXyz(input: { address?: string }) { return useQuery(["getMirror", input.address], async () => { if (!input.address) {