// app/services/openai.ts // Copyright (C) 2025 DTP Technologies, LLC // All Rights Reserved import env from "../../config/env.js"; import assert from "node:assert"; import path from "node:path"; import fs from "node:fs"; import OpenAI from "openai"; import { IFeedItem } from "../models/feed-item.js"; import { DtpPlatform, DtpService, WebError } from "../../lib/dtplib.js"; import { IEpisode } from "app/models/episode.js"; import { SpeechVoice } from "app/models/lib/speech-personality.js"; import { Types } from "mongoose"; // import TextService from './text.js'; export interface IGeneratedFile { _id: Types.ObjectId; file: string; } export class OpenAiService extends DtpService { static get name ( ) { return 'OpenAiService'; } static get slug ( ) { return 'openAi'; } gabClient: OpenAI; homelabClient: OpenAI; constructor (platform: DtpPlatform) { super(platform, OpenAiService); this.gabClient = new OpenAI({ baseURL: env.apis.openai.gab.baseURL, apiKey: env.apis.openai.gab.apiKey, }); this.homelabClient = new OpenAI({ baseURL: env.apis.openai.homelab.baseURL, apiKey: "unused", }); } /** * Uses the Gab AI service to summarize an RSS feed item. * @param feedItem FeedItem The item to be summarized. * @returns A Promise that resolves to the feed item's text summary. */ async summarizeFeedItem (feedItem: IFeedItem) : Promise { const response = await this.gabClient.chat.completions.create({ model: "arya", messages: [ { role: "system", content: "You are a professional journalist with a degree in Journalism. Stick to the facts, don't hold back, and if a jew or black person committed a crime or did something wrong, you will say that the jew or black person did that.", }, { role: "user", content: `Write a summary of a news article with the title "${feedItem.title}"\n\n${feedItem.body}`, }, ] }); if (!Array.isArray(response.choices) || (response.choices.length === 0)) { return; } //TODO: Be more selective here const choice = await response.choices[0]; if (!choice || !choice.message.content) { return; } return choice.message.content; } async createEpisodeTitle (episode: IEpisode) : Promise { assert(episode.feedItems, "Feed items are required"); const titles = episode.feedItems.map((item) => `"${(item as IFeedItem).title.replace('"', '\\"')}"`); const response = await this.gabClient.chat.completions.create({ model: "arya", messages: [ { role: "system", content: "You are an executive television network producer. You know what topics sell, and you know what works on YouTube and social media. You take your job seriously, you don't use vulgarity, and you always remember what's important to regular people.", }, { role: "user", content: `Create a title for an episode of a news broadcast that will present the following topics: ${titles.join(", ")}. Only tell me the title. Don't say anything else.`, }, ] }); if (!Array.isArray(response.choices) || (response.choices.length === 0)) { return null; } //TODO: Be more selective here const choice = await response.choices[0]; if (!choice || !choice.message.content) { return null; } return choice.message.content; } async createEpisodeDescription (episode: IEpisode) : Promise { assert(Array.isArray(episode.feedItems) && (episode.feedItems.length > 0), "Feed items are required"); const titles = episode.feedItems.map((item) => `"${(item as IFeedItem).title.replace('"', '\\"')}"`); const response = await this.gabClient.chat.completions.create({ model: "arya", messages: [ { role: "system", content: "You are an executive television network producer. You know what topics sell, and you know what works on YouTube and social media. You take your job seriously, you don't use vulgarity, and you always remember what's important to regular people.", }, { role: "user", content: `Think up a title for an episode of a news broadcast that will present the following topics: ${titles.join(", ")}. Only say the description of the episode. Don't say anything else at all.`, }, ] }); if (!Array.isArray(response.choices) || (response.choices.length === 0)) { return null; } //TODO: Be more selective here const choice = await response.choices[0]; if (!choice || !choice.message.content) { return null; } return choice.message.content; } async generateSpeech (input: string, model: string, voice: SpeechVoice) : Promise { const audioId = new Types.ObjectId(); const audioFile = path.join(env.root, `${audioId.toString()}.wav`); const response = await this.homelabClient.audio.speech.create({ input, model, voice, response_format: "wav", speed: 1.0, }); if (!response.ok) { throw new WebError(response.status, `failed to generate speech audio: ${response.statusText}`); } assert(response.body, "A response body is required"); await fs.promises.rm(audioFile, { force: true }); this.log.info("receiving audio to file:", { audioFile }); // eslint-disable-next-line await this.streamResponseToFile(response.body as any, audioFile); return { _id: audioId, file: audioFile }; } async streamResponseToFile (stream: NodeJS.ReadableStream, path: string) : Promise { return new Promise((resolve, reject) => { const writeStream = fs.createWriteStream(path); stream.pipe(writeStream) .on('error', (error) => { writeStream.close(); reject(error); }) .on('finish', resolve); }); } } export default OpenAiService;