From 625775ea29b17414b9bc53d0c339b8585dd651ec Mon Sep 17 00:00:00 2001 From: Raditya Harya Date: Tue, 16 Apr 2024 13:40:27 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20Use=20Langchain=F0=9F=90=A4=F0=9F=94=97?= =?UTF-8?q?=20as=20LLM=20interface=20&=20Multimodal=20support=20?= =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 127843 -> 161362 bytes package.json | 6 + src/commands/ask.ts | 2 +- src/commands/chat.ts | 2 +- src/commands/image.ts | 2 +- src/commands/sauce.ts | 126 ++------------ src/config.ts | 3 + src/events/interaction-create.ts | 2 +- src/events/message-create.ts | 19 +- src/lib/helpers.ts | 1 + src/lib/llm.ts | 290 +++++++++++++++++++++++++++++++ src/lib/openai.ts | 205 ---------------------- src/lib/tracemoe.ts | 138 +++++++++++++++ src/utils/tempFile.ts | 57 ++++++ 14 files changed, 527 insertions(+), 326 deletions(-) create mode 100644 src/lib/llm.ts delete mode 100644 src/lib/openai.ts create mode 100644 src/lib/tracemoe.ts create mode 100644 src/utils/tempFile.ts diff --git a/bun.lockb b/bun.lockb index f12fa61a1136f3974cc87635122090c5f25550fb..864c7b9a1567b4ae10b082296c9ec85704df8009 100755 GIT binary patch delta 46246 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�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 diff --git a/package.json b/package.json index 692968c..65bec50 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,12 @@ "prisma:studio": "prisma studio" }, "dependencies": { + "@langchain/community": "^0.0.48", + "@langchain/google-genai": "^0.0.11", + "@langchain/openai": "^0.0.28", "@prisma/client": "5.12.1", "@types/body-parser": "^1.19.5", + "@types/mime-types": "^2.1.4", "axios": "^1.6.8", "croner": "^8.0.2", "date-fns": "^3.6.0", @@ -33,12 +37,14 @@ "form-data": "^4.0.0", "gpt3-tokenizer": "^1.1.5", "hono": "^4.2.4", + "langchain": "^0.1.33", "lodash": "^4.17.21", "openai": "^4.33.1", "pino": "^8.20.0", "prisma": "^5.12.1", "rate-limiter-flexible": "^5.0.0", "rss-parser": "^3.13.0", + "sharp": "^0.33.3", "tiny-glob": "^0.2.9", "tmp-promise": "^3.0.3" }, diff --git a/src/commands/ask.ts b/src/commands/ask.ts index f157134..28d1554 100644 --- a/src/commands/ask.ts +++ b/src/commands/ask.ts @@ -6,7 +6,7 @@ import { import { createErrorEmbed } from '@/lib/embeds'; import { buildContext } from '@/lib/helpers'; -import { CompletionStatus, createChatCompletion } from '@/lib/openai'; +import { CompletionStatus, createChatCompletion } from '@/lib/llm'; export default new Command({ data: new SlashCommandBuilder() diff --git a/src/commands/chat.ts b/src/commands/chat.ts index cd10660..0a814e2 100644 --- a/src/commands/chat.ts +++ b/src/commands/chat.ts @@ -28,7 +28,7 @@ import { CompletionStatus, createChatCompletion, generateTitle, -} from '@/lib/openai'; +} from '@/lib/llm'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); diff --git a/src/commands/image.ts b/src/commands/image.ts index 5d6b69c..1bbaeed 100644 --- a/src/commands/image.ts +++ b/src/commands/image.ts @@ -6,7 +6,7 @@ import { } from 'discord.js'; import { createErrorEmbed } from '@/lib/embeds'; -import { CompletionStatus, createImage } from '@/lib/openai'; +import { CompletionStatus, createImage } from '@/lib/llm'; export default new Command({ data: new SlashCommandBuilder() diff --git a/src/commands/sauce.ts b/src/commands/sauce.ts index 31b112f..5dc9ea7 100644 --- a/src/commands/sauce.ts +++ b/src/commands/sauce.ts @@ -6,119 +6,13 @@ import { SlashCommandBuilder, } from 'discord.js'; import { createErrorEmbed } from '@/lib/embeds'; +import { + getAnimeDetails, + getAnimeSauce, + TraceMoeResultItem, +} from '@/lib/tracemoe'; +import { tempFile } from '@/utils/tempFile'; import fs from 'fs'; -import os from 'os'; -import path from 'path'; -import FormData from 'form-data'; -import axios from 'axios'; - -type TraceMoeResultItem = { - anilist: number; - filename: string; - episode: number | null; - from: number; - to: number; - similarity: number; - video: string; - image: string; -}; - -type TraceMoeResult = { - frameCount: number; - error: string; - result: TraceMoeResultItem[]; - limit: { - limit: number; - remaining: number; - reset: number; - }; -}; - -async function downloadImage(url: string): Promise { - console.log('🚀 ~ downloadImage ~ url:', url); - let response; - try { - response = await axios.get(url, { responseType: 'arraybuffer' }); - } catch (error: any) { - if (axios.isAxiosError(error)) { - throw new Error( - `Failed to fetch the image at url: ${url}. Error: ${error.message}`, - ); - } - throw error; - } - - const buffer = response.data; - - const tempFilePath = path.join(os.tmpdir(), 'tempImage.jpg'); - try { - fs.writeFileSync(tempFilePath, buffer); - } catch (error: any) { - throw new Error( - `Failed to write the image to file at path: ${tempFilePath}. Error: ${error.message}`, - ); - } - - return tempFilePath; -} - -async function getAnimeSauce(tempFilePath: string): Promise { - console.log('🚀 ~ getAnimeSauce ~ tempFilePath:', tempFilePath); - const formData = new FormData(); - formData.append('image', fs.createReadStream(tempFilePath)); - const traceResponse = await axios.post( - 'https://api.trace.moe/search?cutBorders', - formData, - { - headers: formData.getHeaders(), - }, - ); - if (traceResponse.status !== 200) - throw new Error('Failed to get anime sauce'); - return { - ...traceResponse.data, - limit: { - limit: Number(traceResponse.headers['x-ratelimit-limit']), - remaining: Number(traceResponse.headers['x-ratelimit-remaining']), - reset: Number(traceResponse.headers['x-ratelimit-reset']), - }, - }; -} - -async function getAnimeDetails(anilistId: number) { - console.log('🚀 ~ getAnimeDetails ~ getAnimeDetails:', anilistId); - const anilistResponse = await axios.post( - 'https://graphql.anilist.co', - { - query: ` - query ($id: Int) { - Media(id: $id, type: ANIME) { - title { - romaji - english - native - } - siteUrl - episodes - genres - averageScore - } - } - `, - variables: { - id: anilistId, - }, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - if (anilistResponse.status !== 200) - throw new Error('Failed to get anime details'); - return anilistResponse.data; -} export default new Command({ data: new SlashCommandBuilder() @@ -154,8 +48,10 @@ export default new Command({ await interaction.deferReply({ ephemeral: false }); try { - const tempFilePath = await downloadImage(input.attachment.url); - const traceResult = await getAnimeSauce(tempFilePath); + const file = await tempFile(input.attachment.url); + const traceResult = await getAnimeSauce({ + tempFilePath: file.path, + }); await interaction.editReply({ content: 'Searching for anime sauce...', }); @@ -225,7 +121,7 @@ export default new Command({ await interaction.followUp({ files: [{ attachment: match.video, name: 'video.mp4' }], }); - fs.unlinkSync(tempFilePath); + fs.unlinkSync(file.path); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { console.error(error); diff --git a/src/config.ts b/src/config.ts index 266cfda..33d7a89 100644 --- a/src/config.ts +++ b/src/config.ts @@ -36,6 +36,9 @@ const config = { system_prompt: process.env.OPENAI_SYSTEM_PROMPT || 'You are a helpful assistant.', }, + google_genai: { + api_key: process.env.GOOGLE_GENAI_API_KEY, + }, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/events/interaction-create.ts b/src/events/interaction-create.ts index ef4d40d..7b2fa92 100644 --- a/src/events/interaction-create.ts +++ b/src/events/interaction-create.ts @@ -17,7 +17,7 @@ import { RateLimiterMemory } from 'rate-limiter-flexible'; import { createActionRow, createRegenerateButton } from '@/lib/buttons'; import { createErrorEmbed } from '@/lib/embeds'; import { buildThreadContext, isApiError } from '@/lib/helpers'; -import { CompletionStatus, createChatCompletion } from '@/lib/openai'; +import { CompletionStatus, createChatCompletion } from '@/lib/llm'; const rateLimiter = new RateLimiterMemory({ points: 3, duration: 60 }); diff --git a/src/events/message-create.ts b/src/events/message-create.ts index b52f690..3635c99 100644 --- a/src/events/message-create.ts +++ b/src/events/message-create.ts @@ -27,9 +27,10 @@ import { type CompletionResponse, CompletionStatus, createChatCompletion, -} from '@/lib/openai'; +} from '@/lib/llm'; import { PrismaClient } from '@prisma/client'; import logger from '@/utils/logger'; +import { tempFile } from '@/utils/tempFile'; const prisma = new PrismaClient(); @@ -143,10 +144,24 @@ async function handleDirectMessage( await channel.sendTyping(); + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + if (attachment) { + const file = await tempFile(attachment.url); + message.content = `data:${file.mimeType};base64,${file.base64}`; + } + } + + const typingInterval = setInterval(() => { + channel.sendTyping(); + }, 5000); + const completion = await createChatCompletion( buildDirectMessageContext(messages, message.content, client.user.id), ); + clearInterval(typingInterval); + if (completion.status !== CompletionStatus.Ok) { await handleFailedRequest( channel, @@ -182,7 +197,7 @@ export default new Event({ if ( message.author.id === client.user.id || message.type !== MessageType.Default || - !message.content || + (!message.content && !message.attachments.size) || !isEmpty(message.embeds) || !isEmpty(message.mentions.members) ) { diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 7fbb4e6..ff2ef88 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -11,6 +11,7 @@ import type OpenAI from 'openai'; import config from '@/config'; +// TODO: inject multimodal context metadata here export function buildContext( messages: Array, userMessage: string, diff --git a/src/lib/llm.ts b/src/lib/llm.ts new file mode 100644 index 0000000..73d467e --- /dev/null +++ b/src/lib/llm.ts @@ -0,0 +1,290 @@ +import axios from 'axios'; +import { truncate } from 'lodash'; +import OpenAI from 'openai'; +import logger from '@/utils/logger'; +import config from '@/config'; +import { + HumanMessage, + SystemMessage, + AIMessage, +} from '@langchain/core/messages'; +import { ChatOpenAI } from '@langchain/openai'; +import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; +import { getAnimeDetails, getAnimeSauce, TraceMoeResultItem } from './tracemoe'; + +const openai = new OpenAI({ + apiKey: config.openai.api_key, + baseURL: config.openai.base_url, +}); + +const chat = new ChatOpenAI({ + apiKey: config.openai.api_key, + model: config.openai.model, + configuration: { + baseURL: config.openai.base_url, + }, +}); + +const vision = new ChatGoogleGenerativeAI({ + model: 'gemini-pro-vision', + maxOutputTokens: 2048, + apiKey: config.google_genai.api_key, +}); + +export enum CompletionStatus { + Ok = 0, + Moderated = 1, + ContextLengthExceeded = 2, + InvalidRequest = 3, + UnexpectedError = 4, +} + +export interface CompletionResponse { + status: CompletionStatus; + message: string; +} + +async function traceAnimeContext(base64Image: string) { + let additionalContext = ''; + + try { + const traceResult = await getAnimeSauce({ + base64Image: base64Image, + }); + + const match = traceResult.result.reduce( + (prev: TraceMoeResultItem | null, current: TraceMoeResultItem) => { + if (!prev || current.similarity > prev.similarity) { + return current; + } + return prev; + }, + null, + ); + + if (match) { + const anilistResult = await getAnimeDetails(match.anilist); + const anime = { + title: + anilistResult.data.Media.title.english || + anilistResult.data.Media.title.romaji || + anilistResult.data.Media.title.native, + episode: match.episode, + episodes: anilistResult.data.Media.episodes, + genres: anilistResult.data.Media.genres, + score: anilistResult.data.Media.averageScore, + description: anilistResult.data.Media.description, + video: match.video, + image: match.image, + }; + + additionalContext = `The image is from the anime titled "${anime.title}". This anime falls under the genres: ${anime.genres.join(', ')}. It has an average score of ${anime.score}. The specific scene in the image is from episode ${anime.episode} out of the total ${anime.episodes} episodes. Here is a brief description of the anime: "${anime.description}". Do note that the context provided is based on the image and may not be 100% accurate.`; + } + } catch (error) { + console.error('Error tracing anime context:', error); + } + + return additionalContext; +} + +async function generateImageContext(file: string) { + const additionalContext = await traceAnimeContext(file); + return additionalContext; +} + +// TODO: Save context metadata in db and asign it to the history? +async function identifyImage(file: string) { + const additionalContext = await generateImageContext(file); + + const prompt = `You received an image. Describe the image in detail and extract any useful information from it.`; + + const input = [ + new HumanMessage({ + content: [ + { + type: 'text', + text: prompt, + }, + { + type: 'image_url', + image_url: file, + }, + ], + }), + ]; + const res = await vision.invoke(input); + const response = `The image you received in the discord chat has the following description: ${res.content}. Based on the additional context, it appears that ${additionalContext}. Please provide the user with any relevant information and share your thoughts on the image. If the additional context does not align with the description, please disregard it. If it does align, please provide as much context as possible.`; + return response; +} +export async function createChatCompletion( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + messages: Array, +): Promise { + try { + const chatMessages = messages.map(async (message) => { + switch (message.role) { + case 'system': + return new SystemMessage(message.content); + case 'user': { + if (message.content.startsWith('data:image')) { + return new SystemMessage( + await identifyImage(message.content as string), + ); + } + return new HumanMessage(message.content); + } + case 'assistant': + return new AIMessage(message.content); + default: + throw new Error(`Invalid message role: ${message.role}`); + } + }); + + const completion = await chat.invoke(await Promise.all(chatMessages)); + const message = completion.content; + if (message) { + return { + status: CompletionStatus.Ok, + message: truncate(message.toString(), { length: 2000 }), + }; + } + } catch (err) { + logger.error(err, 'Error while processing chat completion'); + return { + status: CompletionStatus.UnexpectedError, + message: err instanceof Error ? err.message : (err as string), + }; + } + return { + status: CompletionStatus.UnexpectedError, + message: 'There was an unexpected error while processing your request.', + }; +} + +export async function createImage(prompt: string): Promise { + try { + const moderation = await openai.moderations.create({ + input: prompt, + }); + + const result = moderation.results[0]; + + if (result.flagged) { + return { + status: CompletionStatus.Moderated, + message: 'Your prompt has been blocked by moderation.', + }; + } + + const image = await openai.images.generate({ + prompt, + }); + + const imageUrl = image.data[0].url; + + if (imageUrl) { + return { + status: CompletionStatus.Ok, + message: imageUrl, + }; + } + } catch (err) { + if (axios.isAxiosError(err)) { + const error = err.response?.data?.error; + + if (error && error.code === 'context_length_exceeded') { + return { + status: CompletionStatus.ContextLengthExceeded, + message: + 'The request has exceeded the token limit. Try again with a shorter message or start another conversation.', + }; + } else if (error && error.type === 'invalid_request_error') { + logError(err); + + return { + status: CompletionStatus.InvalidRequest, + message: error.message, + }; + } + } else { + logError(err); + + return { + status: CompletionStatus.UnexpectedError, + message: err instanceof Error ? err.message : (err as string), + }; + } + } + + return { + status: CompletionStatus.UnexpectedError, + message: 'There was an unexpected error processing your request.', + }; +} + +export async function generateTitle( + userMessage: string, + botMessage: string, +): Promise { + const messages = [ + { + role: 'system', + content: 'You are a helpful assistant.', + }, + { + role: 'user', + content: userMessage, + }, + { + role: 'assistant', + content: botMessage, + }, + { + role: 'user', + content: 'Create a title for our conversation in 6 words or less.', + }, + ] as OpenAI.Chat.ChatCompletionMessageParam[]; + + try { + const completion = await openai.chat.completions.create({ + messages, + model: config.openai.model, + temperature: 0.5, + }); + + const message = completion.choices[0].message; + + if (message) { + let title = message.content?.trim(); + + if (title?.startsWith('"') && title.endsWith('"')) { + title = title.slice(1, -1); + } + + while (title?.endsWith('.')) { + title = title.slice(0, -1); + } + + return title || ''; + } + } catch (err) { + logError(err); + } + + return ''; +} + +function logError(err: unknown): void { + if (axios.isAxiosError(err)) { + if (err.response) { + logger.error( + { status: err.response.status, data: err.response.data }, + 'Axios error with response', + ); + } else { + logger.error(err.message, 'Axios error without response'); + } + } else { + logger.error(err, 'Unknown error'); + } +} diff --git a/src/lib/openai.ts b/src/lib/openai.ts deleted file mode 100644 index 67aee81..0000000 --- a/src/lib/openai.ts +++ /dev/null @@ -1,205 +0,0 @@ -import axios from 'axios'; -import { truncate } from 'lodash'; -import OpenAI from 'openai'; -import logger from '@/utils/logger'; -import config from '@/config'; - -const openai = new OpenAI({ - apiKey: config.openai.api_key, - baseURL: config.openai.base_url, -}); - -export enum CompletionStatus { - Ok = 0, - Moderated = 1, - ContextLengthExceeded = 2, - InvalidRequest = 3, - UnexpectedError = 4, -} - -export interface CompletionResponse { - status: CompletionStatus; - message: string; -} - -export async function createChatCompletion( - messages: Array, -): Promise { - try { - logger.info('🤖 Generating response'); - const completion = await openai.chat.completions.create({ - messages, - model: config.openai.model, - temperature: Number(config.openai.temperature), - max_tokens: Number(config.openai.max_tokens), - }); - - const message = completion.choices[0].message; - - if (message) { - return { - status: CompletionStatus.Ok, - message: truncate(message.content?.trim(), { length: 2000 }), - }; - } - } catch (err) { - if (axios.isAxiosError(err)) { - const error = err.response?.data?.error; - - if (error && error.code === 'context_length_exceeded') { - return { - status: CompletionStatus.ContextLengthExceeded, - message: - 'The request has exceeded the context limit. Try again with a shorter message or start another conversation.', - }; - } else if (error && error.type === 'invalid_request_error') { - logError(err); - - return { - status: CompletionStatus.InvalidRequest, - message: error.message, - }; - } - } else { - logError(err); - - return { - status: CompletionStatus.UnexpectedError, - message: err instanceof Error ? err.message : (err as string), - }; - } - } - - return { - status: CompletionStatus.UnexpectedError, - message: 'There was an unexpected error while processing your request.', - }; -} - -export async function createImage(prompt: string): Promise { - try { - const moderation = await openai.moderations.create({ - input: prompt, - }); - - const result = moderation.results[0]; - - if (result.flagged) { - return { - status: CompletionStatus.Moderated, - message: 'Your prompt has been blocked by moderation.', - }; - } - - const image = await openai.images.generate({ - prompt, - }); - - const imageUrl = image.data[0].url; - - if (imageUrl) { - return { - status: CompletionStatus.Ok, - message: imageUrl, - }; - } - } catch (err) { - if (axios.isAxiosError(err)) { - const error = err.response?.data?.error; - - if (error && error.code === 'context_length_exceeded') { - return { - status: CompletionStatus.ContextLengthExceeded, - message: - 'The request has exceeded the token limit. Try again with a shorter message or start another conversation.', - }; - } else if (error && error.type === 'invalid_request_error') { - logError(err); - - return { - status: CompletionStatus.InvalidRequest, - message: error.message, - }; - } - } else { - logError(err); - - return { - status: CompletionStatus.UnexpectedError, - message: err instanceof Error ? err.message : (err as string), - }; - } - } - - return { - status: CompletionStatus.UnexpectedError, - message: 'There was an unexpected error processing your request.', - }; -} - -export async function generateTitle( - userMessage: string, - botMessage: string, -): Promise { - const messages = [ - { - role: 'system', - content: 'You are a helpful assistant.', - }, - { - role: 'user', - content: userMessage, - }, - { - role: 'assistant', - content: botMessage, - }, - { - role: 'user', - content: 'Create a title for our conversation in 6 words or less.', - }, - ] as OpenAI.Chat.ChatCompletionMessageParam[]; - - try { - const completion = await openai.chat.completions.create({ - messages, - model: config.openai.model, - temperature: 0.5, - }); - - const message = completion.choices[0].message; - - if (message) { - let title = message.content?.trim(); - - if (title?.startsWith('"') && title.endsWith('"')) { - title = title.slice(1, -1); - } - - while (title?.endsWith('.')) { - title = title.slice(0, -1); - } - - return title || ''; - } - } catch (err) { - logError(err); - } - - return ''; -} - -function logError(err: unknown): void { - if (axios.isAxiosError(err)) { - if (err.response) { - logger.error( - { status: err.response.status, data: err.response.data }, - 'Axios error with response', - ); - } else { - logger.error(err.message, 'Axios error without response'); - } - } else { - logger.error(err, 'Unknown error'); - } -} diff --git a/src/lib/tracemoe.ts b/src/lib/tracemoe.ts new file mode 100644 index 0000000..e0f5e73 --- /dev/null +++ b/src/lib/tracemoe.ts @@ -0,0 +1,138 @@ +import fs from 'fs'; +import FormData from 'form-data'; +import axios from 'axios'; +import sharp from 'sharp'; + +export type TraceMoeResultItem = { + anilist: number; + filename: string; + episode: number | null; + from: number; + to: number; + similarity: number; + video: string; + image: string; +}; + +export type TraceMoeResult = { + frameCount: number; + error: string; + result: TraceMoeResultItem[]; + limit: { + limit: number; + remaining: number; + reset: number; + }; +}; + +async function processImage(imageSource: Buffer | string) { + return await sharp(imageSource).resize({ width: 500 }).jpeg().toBuffer(); +} + +function appendImageToFormData(formData: FormData, imageBuffer: Buffer) { + formData.append('image', imageBuffer, { + filename: 'blob', + contentType: 'image/jpeg', + }); +} +export async function getAnimeSauce({ + tempFilePath, + base64Image, +}: { + tempFilePath?: string; + base64Image?: string; +}): Promise { + const formData = new FormData(); + let imageBuffer: Buffer; + + if (base64Image) { + const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, ''); + imageBuffer = Buffer.from(base64Data, 'base64'); + } else if (tempFilePath) { + imageBuffer = fs.readFileSync(tempFilePath); + } else { + throw new Error('Either a file path or a base64 string must be provided'); + } + + try { + const resizedBuffer = await processImage(imageBuffer); + appendImageToFormData(formData, resizedBuffer); + } catch (err) { + console.error('Error processing image data:', err); + console.error('Base64 string:', base64Image); + } + + const traceResponse = (await axios.post( + 'https://api.trace.moe/search?cutBorders', + formData, + { + headers: { + ...formData.getHeaders(), + 'Accept-Encoding': 'gzip, deflate', + }, + }, + )) as { + status: number; + data: TraceMoeResult; + headers: Record; + }; + + if (traceResponse.status !== 200) { + console.error(traceResponse.data); + throw new Error('Failed to get anime sauce'); + } + + if (tempFilePath) { + fs.unlink(tempFilePath, (err) => { + if (err) { + console.error('Failed to delete temp file:', err); + } + }); + } + + return { + ...traceResponse.data, + limit: { + limit: Number(traceResponse.headers['x-ratelimit-limit']), + remaining: Number(traceResponse.headers['x-ratelimit-remaining']), + reset: Number(traceResponse.headers['x-ratelimit-reset']), + }, + }; +} + +export async function getAnimeDetails(anilistId: number) { + console.log('🚀 ~ getAnimeDetails ~ getAnimeDetails:', anilistId); + const anilistResponse = await axios.post( + 'https://graphql.anilist.co', + { + query: ` + query ($id: Int) { + Media(id: $id, type: ANIME) { + title { + romaji + english + native + } + siteUrl + episodes + genres + averageScore + description(asHtml: false) + } + } + `, + variables: { + id: anilistId, + }, + }, + { + headers: { + 'Content-Type': 'application/json', + 'Accept-Encoding': 'gzip, deflate', //https://github.com/oven-sh/bun/issues/267#issuecomment-2044596837 + }, + }, + ); + if (anilistResponse.status !== 200) + throw new Error('Failed to get anime details'); + return anilistResponse.data; +} diff --git a/src/utils/tempFile.ts b/src/utils/tempFile.ts new file mode 100644 index 0000000..df18cd7 --- /dev/null +++ b/src/utils/tempFile.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import axios from 'axios'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import mime from 'mime-types'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * Downloads file from the specified URL and saves it as a temporary file. + * @param url - The URL of the image to download. + * @returns A promise that resolves to the path of the temporary file, the MIME type, the buffer, and the base64 string. + * @throws If there is an error while fetching or saving the image. + */ +export async function tempFile( + url: string, +): Promise<{ path: string; mimeType: string; buffer: Buffer; base64: string }> { + let response; + try { + response = await axios.get(url, { responseType: 'arraybuffer' }); + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `Failed to fetch the file at url: ${url}. Error: ${error.message}`, + ); + } + throw error; + } + + const buffer = response.data; + const mimeType = response.headers['content-type']; + const extension = mime.extension(mimeType); + + if (!extension) { + throw new Error(`Unsupported MIME type: ${mimeType}`); + } + + const filename = `${uuidv4()}.${extension}`; + const tempFilePath = path.join(os.tmpdir(), filename); + + try { + fs.writeFileSync(tempFilePath, buffer); + } catch (error: any) { + throw new Error( + `Failed to write the file at path: ${tempFilePath}. Error: ${error.message}`, + ); + } + + const base64 = buffer.toString('base64'); + + return { + path: tempFilePath, + mimeType, + buffer, + base64, + }; +}