Split endpoints to multiple ones with defined responsibilities. Could be deployed as their own services. Also consume core SDK based on mio-ai.master
@@ -1,7 +1,6 @@ | |||||
import * as config from './config'; | import * as config from './config'; | ||||
import { createServer } from './server'; | import { createServer } from './server'; | ||||
import { addHealthRoutes, addSummaryRoutes } from './routes'; | |||||
import { addHealthRoutes, addSummaryRoutes, addTranscriptRoutes } from './routes'; | |||||
const server = createServer({ | const server = createServer({ | ||||
logger: process.env.NODE_ENV !== 'test', | logger: process.env.NODE_ENV !== 'test', | ||||
@@ -9,6 +8,7 @@ const server = createServer({ | |||||
addHealthRoutes(server); | addHealthRoutes(server); | ||||
addSummaryRoutes(server); | addSummaryRoutes(server); | ||||
addTranscriptRoutes(server); | |||||
server.listen( | server.listen( | ||||
{ | { | ||||
@@ -20,5 +20,5 @@ server.listen( | |||||
server.log.error(err.message); | server.log.error(err.message); | ||||
process.exit(1); | process.exit(1); | ||||
} | } | ||||
} | |||||
}, | |||||
); | ); |
@@ -1,11 +1,8 @@ | |||||
import { RouteHandlerMethod } from 'fastify'; | |||||
import {CreateSummarizerParams, SummarizerProcessParams} from '@modal-sh/webvideo-transcript-summary-core'; | |||||
import * as config from '../../config'; | |||||
import { constants } from 'http2'; | |||||
import { SummaryService, SummaryServiceImpl } from './SummaryService'; | import { SummaryService, SummaryServiceImpl } from './SummaryService'; | ||||
import { Controller } from '../../packages/fastify-controller'; | |||||
export interface SummaryController { | |||||
summarizeVideoTranscript: RouteHandlerMethod; | |||||
} | |||||
export interface SummaryController extends Controller<'summarizeVideoTranscript'> {} | |||||
export class SummaryControllerImpl implements SummaryController { | export class SummaryControllerImpl implements SummaryController { | ||||
constructor( | constructor( | ||||
@@ -14,33 +11,18 @@ export class SummaryControllerImpl implements SummaryController { | |||||
// noop | // noop | ||||
} | } | ||||
readonly summarizeVideoTranscript: RouteHandlerMethod = async (request, reply) => { | |||||
const params = request.body as CreateSummarizerParams & SummarizerProcessParams; | |||||
readonly summarizeVideoTranscript: SummaryController['summarizeVideoTranscript'] = async (request, reply) => { | |||||
try { | try { | ||||
this.summaryService.initializeSummarizer({ | |||||
type: params.type, | |||||
openAiParams: { | |||||
apiKey: config.openAi.apiKey, | |||||
organizationId: config.openAi.organizationId, | |||||
temperature: params.openAiParams?.temperature ?? 0.6, | |||||
model: params.openAiParams?.model ?? 'gpt-3.5-turbo', | |||||
}, | |||||
}); | |||||
const summaryResult = await this.summaryService.summarizeVideoTranscript( | |||||
{ | |||||
url: params.url, | |||||
language: params.language ?? 'en', | |||||
country: params.country ?? 'US', | |||||
}, | |||||
); | |||||
reply.send(summaryResult); | |||||
const stream = this.summaryService.createSummaryStream(request.body as string); | |||||
reply.header('Content-Type', 'application/octet-stream'); | |||||
reply.raw.statusMessage = 'Summary Generated'; | |||||
return reply.send(stream); | |||||
} catch (errRaw) { | } catch (errRaw) { | ||||
const err = errRaw as Error; | const err = errRaw as Error; | ||||
request.server.log.error(err); | request.server.log.error(err); | ||||
reply | |||||
.code(500) | |||||
.send(); | |||||
reply.code(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR) | |||||
reply.raw.statusMessage = 'Summary Failed'; | |||||
reply.send(); | |||||
} | } | ||||
}; | }; | ||||
} | } |
@@ -1,78 +1,32 @@ | |||||
import { | import { | ||||
createSummarizer, | createSummarizer, | ||||
CreateSummarizerParams, SummarizerEventEmitter, | |||||
SummarizerProcessParams, | |||||
SummarizerEventEmitter, | |||||
OPENAI_API_VERSION, | |||||
} from '@modal-sh/webvideo-transcript-summary-core'; | } from '@modal-sh/webvideo-transcript-summary-core'; | ||||
export interface SummaryResult { | |||||
summary: string; | |||||
normalizedTranscript: string; | |||||
rawTranscript: string; | |||||
} | |||||
import { Readable } from '../../packages/event-emitter-to-readable-stream'; | |||||
import * as config from '../../config'; | |||||
export interface SummaryService { | export interface SummaryService { | ||||
initializeSummarizer(params: CreateSummarizerParams): void; | |||||
summarizeVideoTranscript(processParams: SummarizerProcessParams): Promise<Partial<SummaryResult>>; | |||||
createSummaryStream(transcriptText: string): NodeJS.ReadableStream; | |||||
} | } | ||||
export class SummaryServiceImpl implements SummaryService { | export class SummaryServiceImpl implements SummaryService { | ||||
private summarizer?: SummarizerEventEmitter; | |||||
private readonly summarizer: SummarizerEventEmitter; | |||||
constructor(private readonly logger = console) { | |||||
constructor() { | |||||
// noop | // noop | ||||
this.summarizer = createSummarizer({ | |||||
apiKey: config.openAi.apiKey, | |||||
organizationId: config.openAi.organizationId, | |||||
apiVersion: OPENAI_API_VERSION, | |||||
}); | |||||
} | } | ||||
initializeSummarizer(params: CreateSummarizerParams): void { | |||||
this.summarizer = createSummarizer(params); | |||||
} | |||||
summarizeVideoTranscript(processParams: SummarizerProcessParams) { | |||||
return new Promise<Partial<SummaryResult>>((resolve, reject) => { | |||||
if (!this.summarizer) { | |||||
reject(new Error('Summarizer is not initialized')); | |||||
return; | |||||
} | |||||
const successEvent = {} as Partial<SummaryResult>; | |||||
let error: Error; | |||||
this.summarizer.on('process', (data) => { | |||||
this.logger.log('process', data); | |||||
if (data.phase === 'success') { | |||||
switch (data.processType) { | |||||
case 'fetch-transcript': | |||||
successEvent.rawTranscript = ( | |||||
JSON.parse(data.content as string) as { text: string }[] | |||||
) | |||||
.map((item) => item.text).join(' '); | |||||
break; | |||||
case 'normalize-transcript': | |||||
successEvent.normalizedTranscript = data.content as string; | |||||
break; | |||||
case 'summarize-transcript': | |||||
successEvent.summary = data.content as string; | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
} | |||||
}); | |||||
this.summarizer.on('error', (err) => { | |||||
this.logger.log('error', err); | |||||
error = err; | |||||
}); | |||||
this.summarizer.on('end', () => { | |||||
this.logger.log('end'); | |||||
if (error) { | |||||
reject(error); | |||||
return; | |||||
} | |||||
resolve(successEvent); | |||||
}); | |||||
this.summarizer.process(processParams); | |||||
}); | |||||
createSummaryStream(transcriptText: string) { | |||||
const stream = Readable.fromEventEmitter( | |||||
this.summarizer, | |||||
) as unknown as NodeJS.ReadableStream & { tokenCount: number }; | |||||
this.summarizer.summarize(transcriptText); | |||||
return stream; | |||||
} | } | ||||
} | } |
@@ -0,0 +1,53 @@ | |||||
import { Controller } from '../../packages/fastify-controller'; | |||||
import { TranscriptService, TranscriptServiceImpl } from './TranscriptService'; | |||||
import { BaseTranscriptItem, VideoType, YouTube } from '@modal-sh/webvideo-transcript-summary-core'; | |||||
import { constants } from 'http2'; | |||||
export interface TranscriptController extends Controller< | |||||
'getVideoTranscript' | |||||
| 'normalizeVideoTranscriptText' | |||||
> {} | |||||
export class TranscriptControllerImpl implements TranscriptController { | |||||
constructor( | |||||
private readonly transcriptService: TranscriptService = new TranscriptServiceImpl(), | |||||
) { | |||||
// noop | |||||
} | |||||
readonly getVideoTranscript: TranscriptController['getVideoTranscript'] = async (request, reply) => { | |||||
try { | |||||
const { videoType, videoId } = request.params as { videoType: VideoType; videoId: string }; | |||||
const transcript = await this.transcriptService | |||||
.getVideoTranscript(videoType, { url: videoId }); | |||||
reply.raw.statusMessage = 'Transcript Fetched Successfully'; | |||||
reply.send(transcript); | |||||
} catch (errRaw) { | |||||
const err = errRaw as Error; | |||||
request.server.log.error(err); | |||||
reply.code(constants.HTTP_STATUS_BAD_GATEWAY); | |||||
reply.raw.statusMessage = 'Transcript Fetch Failed'; | |||||
reply.send(); | |||||
} | |||||
}; | |||||
readonly normalizeVideoTranscriptText: TranscriptController['normalizeVideoTranscriptText'] = async (request, reply) => { | |||||
try { | |||||
const transcriptItems = request.body as BaseTranscriptItem[]; | |||||
const normalizedText = await this.transcriptService | |||||
.normalizeVideoTranscriptText(transcriptItems); | |||||
reply.raw.statusMessage = 'Transcript Text Normalized Successfully'; | |||||
reply.send(normalizedText); | |||||
} catch (errRaw) { | |||||
const err = errRaw as Error; | |||||
request.server.log.error(err); | |||||
reply.code(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); | |||||
reply.raw.statusMessage = 'Transcript Text Normalization Failed'; | |||||
reply.send(); | |||||
} | |||||
}; | |||||
} |
@@ -0,0 +1,37 @@ | |||||
import { | |||||
createTranscriptFetcher, | |||||
VideoType, | |||||
createSummarizer, | |||||
BaseTranscriptItem, | |||||
OPENAI_API_VERSION, | |||||
} from '@modal-sh/webvideo-transcript-summary-core'; | |||||
import * as config from '../../config'; | |||||
export interface TranscriptService { | |||||
getVideoTranscript( | |||||
videoType: VideoType, | |||||
...etcParams: Parameters<ReturnType<typeof createTranscriptFetcher>> | |||||
): Promise<BaseTranscriptItem[]>; | |||||
normalizeVideoTranscriptText(text: BaseTranscriptItem[]): Promise<string>; | |||||
} | |||||
export class TranscriptServiceImpl implements TranscriptService { | |||||
async getVideoTranscript( | |||||
videoType: VideoType, | |||||
...etcParams: Parameters<ReturnType<typeof createTranscriptFetcher>> | |||||
) { | |||||
const transcriptFetcher = createTranscriptFetcher({ type: videoType }); | |||||
return transcriptFetcher(...etcParams); | |||||
} | |||||
async normalizeVideoTranscriptText(text: BaseTranscriptItem[]) { | |||||
const summarizer = createSummarizer({ | |||||
apiKey: config.openAi.apiKey, | |||||
organizationId: config.openAi.organizationId, | |||||
apiVersion: OPENAI_API_VERSION, | |||||
}); | |||||
return summarizer.normalize(text); | |||||
} | |||||
} |
@@ -0,0 +1,34 @@ | |||||
import { Readable as NodeReadable } from 'stream'; | |||||
export interface Options<T extends NodeJS.EventEmitter> { | |||||
dataEvent?: string; | |||||
endEvent?: string; | |||||
onBeforeEnd?: (emitter: T) => void; | |||||
} | |||||
export namespace Readable { | |||||
export const fromEventEmitter = <T extends NodeJS.EventEmitter>( | |||||
eventEmitter: T, | |||||
options = {} as Options<T>, | |||||
) => { | |||||
const stream = new NodeReadable({ | |||||
read() { | |||||
// noop | |||||
}, | |||||
}); | |||||
const dataEventHandler = (d: unknown) => { | |||||
stream.emit('data', d); | |||||
}; | |||||
const dataEvent = options?.dataEvent ?? 'data'; | |||||
eventEmitter.on(dataEvent, dataEventHandler); | |||||
eventEmitter.on(options?.endEvent ?? 'end', () => { | |||||
options?.onBeforeEnd?.(eventEmitter); | |||||
stream.emit('end'); | |||||
eventEmitter.off(dataEvent, dataEventHandler); | |||||
}); | |||||
return stream; | |||||
}; | |||||
} |
@@ -0,0 +1,5 @@ | |||||
import { RouteHandlerMethod } from 'fastify'; | |||||
export type Controller<T extends string> = { | |||||
[P in T]: RouteHandlerMethod; | |||||
} |
@@ -1,5 +1,6 @@ | |||||
import { FastifyInstance } from 'fastify'; | import { FastifyInstance } from 'fastify'; | ||||
import { SummaryController, SummaryControllerImpl } from './modules/summary'; | import { SummaryController, SummaryControllerImpl } from './modules/summary'; | ||||
import { TranscriptController, TranscriptControllerImpl } from './modules/transcript/TranscriptController'; | |||||
export const addHealthRoutes = (server: FastifyInstance) => { | export const addHealthRoutes = (server: FastifyInstance) => { | ||||
server | server | ||||
@@ -17,14 +18,29 @@ export const addHealthRoutes = (server: FastifyInstance) => { | |||||
reply.send({ status: 'ok' }); | reply.send({ status: 'ok' }); | ||||
}, | }, | ||||
}); | }); | ||||
} | |||||
}; | |||||
export const addSummaryRoutes = (server: FastifyInstance) => { | export const addSummaryRoutes = (server: FastifyInstance) => { | ||||
const summaryController: SummaryController = new SummaryControllerImpl(); | const summaryController: SummaryController = new SummaryControllerImpl(); | ||||
server | server | ||||
.route({ | .route({ | ||||
method: 'POST', | method: 'POST', | ||||
url: '/api/summary', | |||||
url: '/api/summarize', | |||||
handler: summaryController.summarizeVideoTranscript, | handler: summaryController.summarizeVideoTranscript, | ||||
}); | }); | ||||
}; | }; | ||||
export const addTranscriptRoutes = (server: FastifyInstance) => { | |||||
const transcriptController: TranscriptController = new TranscriptControllerImpl(); | |||||
server | |||||
.route({ | |||||
method: 'GET', | |||||
url: '/api/transcripts/get/:videoType/:videoId', | |||||
handler: transcriptController.getVideoTranscript, | |||||
}) | |||||
.route({ | |||||
method: 'POST', | |||||
url: '/api/transcripts/normalize', | |||||
handler: transcriptController.normalizeVideoTranscriptText, | |||||
}); | |||||
}; |