From 796a8ff49b490081e58b14810ebabd1c40b8f36e Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Wed, 12 Apr 2023 16:23:59 +0800 Subject: [PATCH] Use env variable for prompts dir, refactor events The prompts directory is now specified as an environment variable to be more compatible with custom locations. The events have also been refactored. --- .env.example | 3 ++ src/common.ts | 10 +++- src/config.ts | 5 ++ src/index.ts | 6 +-- src/summarizer.ts | 82 +++++++++++++++++++++++--------- src/video-types/youtube/index.ts | 24 +++++----- 6 files changed, 89 insertions(+), 41 deletions(-) create mode 100644 src/config.ts diff --git a/.env.example b/.env.example index 775a8f0..90811df 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,6 @@ OPENAI_API_KEY= # OpenAI organization ID. OPENAI_ORGANIZATION_ID= + +# Directory where the prompts are stored. +OPENAI_PROMPTS_DIR= diff --git a/src/common.ts b/src/common.ts index c7ea11d..5ac45a5 100644 --- a/src/common.ts +++ b/src/common.ts @@ -17,10 +17,16 @@ export interface SummarizerEventEmitter extends NodeJS.EventEmitter { on(eventType: 'end', callback: () => void): this; } +export interface OpenAiParams { + apiKey: string; + organizationId?: string; + model?: string; + temperature?: number; +} + export interface CreateBaseSummarizerParams { url: string; language?: string; country?: string; - openaiApiKey: string; - openaiOrganizationId?: string; + openAiParams: OpenAiParams; } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..252e39f --- /dev/null +++ b/src/config.ts @@ -0,0 +1,5 @@ +export namespace openAi { + export const apiKey = process.env.OPENAI_API_KEY as string; + export const organizationId = process.env.OPENAI_ORGANIZATION_ID; + export const promptsDir = process.env.OPENAI_PROMPTS_DIR as string; +} diff --git a/src/index.ts b/src/index.ts index 6f2cd62..a1e628e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,8 +16,7 @@ export const createSummarizer = (params: CreateSummarizerParams): SummarizerEven const { type: videoType, url, - openaiOrganizationId, - openaiApiKey, + openAiParams, language, country, } = params; @@ -26,8 +25,7 @@ export const createSummarizer = (params: CreateSummarizerParams): SummarizerEven case VideoType.YOUTUBE: return new YouTubeSummarizerEventEmitter({ url, - openaiOrganizationId, - openaiApiKey, + openAiParams, language, country, }); diff --git a/src/summarizer.ts b/src/summarizer.ts index 7254eca..c00cc22 100644 --- a/src/summarizer.ts +++ b/src/summarizer.ts @@ -2,8 +2,32 @@ import fetchPonyfill from 'fetch-ponyfill'; import Handlebars from 'handlebars'; import { resolve } from 'path'; import { readFile } from 'fs/promises'; +import * as config from './config'; +import { OpenAiParams } from './common'; + +export interface MakeAiCallParams { + prompts: string[]; + openAiParams: OpenAiParams; +} + +export class AiCallError extends Error { + constructor(message: string, public readonly response: Response) { + super(message); + this.name = 'AiCallError'; + } +} + +const makeAiCall = async (params: MakeAiCallParams): Promise => { + const { + prompts, + openAiParams: { + apiKey, + organizationId, + model = 'gpt-3.5-turbo', + temperature = 0.6, + }, + } = params; -const makeAiCall = async (prompts: string[], apiKey: string, organizationId?: string): Promise => { const headers: Record = { 'Content-Type': 'application/json', Accept: 'application/json', @@ -21,9 +45,8 @@ const makeAiCall = async (prompts: string[], apiKey: string, organizationId?: st method: 'POST', headers, body: JSON.stringify({ - //model: 'gpt-4', - model: 'gpt-3.5-turbo', - temperature: 0.6, + model, + temperature, messages: [ { role: 'user', @@ -35,9 +58,8 @@ const makeAiCall = async (prompts: string[], apiKey: string, organizationId?: st ); if (!response.ok) { - const responseText = await response.text(); - console.log(responseText); - throw new Error(`OpenAI API call failed with status ${response.status}`); + const { error } = await response.json(); + throw new AiCallError(`OpenAI API call failed with status ${response.status}: ${error.message}`, response); } const { choices } = await response.json(); @@ -47,38 +69,54 @@ const makeAiCall = async (prompts: string[], apiKey: string, organizationId?: st }; const compilePrompts = async (filename: string, params: Record): Promise => { - const rawPromptText = await readFile(resolve(__dirname, filename), 'utf-8'); + const rawPromptText = await readFile(resolve(config.openAi.promptsDir, filename), 'utf-8'); const fill = Handlebars.compile(rawPromptText, { noEscape: true }); const filledText = fill(params); return filledText.split('---').map((s) => s.trim()); }; -export const normalizeTranscriptText = async ( +export interface NormalizeTranscriptTextParams { rawTranscriptText: string, - apiKey: string, - organizationId?: string, -) => { + openAiParams: OpenAiParams, +} + +export const normalizeTranscriptText = async (params: NormalizeTranscriptTextParams) => { + const { + rawTranscriptText, + openAiParams, + } = params; const prompts = await compilePrompts( - '../prompts/normalize-transcript-text.hbs', + 'normalize-transcript-text.hbs', { transcript: rawTranscriptText, }, ); - return makeAiCall(prompts, apiKey, organizationId); + return makeAiCall({ + prompts, + openAiParams, + }); }; -export const summarizeTranscript = async ( - transcript: string, - apiKey: string, - organizationId?: string, -) => { +export interface SummarizeTranscriptParams { + normalizedTranscript: string, + openAiParams: OpenAiParams, +} + +export const summarizeTranscript = async (params: SummarizeTranscriptParams) => { + const { + normalizedTranscript, + openAiParams, + } = params; const prompts = await compilePrompts( - '../prompts/summarize-transcript.hbs', + 'summarize-transcript.hbs', { - transcript, + transcript: normalizedTranscript, }, ); - return makeAiCall(prompts, apiKey, organizationId); + return makeAiCall({ + prompts, + openAiParams, + }); }; diff --git a/src/video-types/youtube/index.ts b/src/video-types/youtube/index.ts index 454eaf3..e18d42d 100644 --- a/src/video-types/youtube/index.ts +++ b/src/video-types/youtube/index.ts @@ -8,7 +8,7 @@ import { } from './transcript'; import { normalizeTranscriptText, summarizeTranscript } from '../../summarizer'; -export interface CreateYouTubeSummarizerParams extends CreateBaseSummarizerParams {} +export type CreateYouTubeSummarizerParams = CreateBaseSummarizerParams export class YouTubeSummarizerEventEmitter extends EventEmitter implements SummarizerEventEmitter { constructor(private readonly params: CreateYouTubeSummarizerParams) { @@ -18,8 +18,7 @@ export class YouTubeSummarizerEventEmitter extends EventEmitter implements Summa process() { const { url, - openaiApiKey, - openaiOrganizationId, + openAiParams, ...config } = this.params; const identifier = retrieveVideoId(url); @@ -52,21 +51,20 @@ export class YouTubeSummarizerEventEmitter extends EventEmitter implements Summa }); this.emit('process', { - processType: 'normalize-caption', + processType: 'normalize-transcript', phase: 'start', }); - return normalizeTranscriptText( - transcript.map((item) => item.text).join(' '), - openaiApiKey, - openaiOrganizationId, - ); + return normalizeTranscriptText({ + rawTranscriptText: transcript.map((item) => item.text).join(' '), + openAiParams, + }); }) - .then((normalizedCaption) => { + .then((normalizedTranscript) => { this.emit('process', { processType: 'normalize-transcript', phase: 'success', - content: normalizedCaption, + content: normalizedTranscript, contentType: 'text/plain', }); @@ -75,13 +73,13 @@ export class YouTubeSummarizerEventEmitter extends EventEmitter implements Summa phase: 'start', }); - return summarizeTranscript(normalizedCaption, openaiApiKey, openaiOrganizationId); + return summarizeTranscript({ normalizedTranscript, openAiParams }); }) .then((summary) => { this.emit('process', { processType: 'summarize-transcript', phase: 'success', - data: summary, + content: summary, contentType: 'text/plain', });