Skip to content

Commit 0b28a4b

Browse files
committed
feat: tts service for switching out providers
1 parent e43d4de commit 0b28a4b

File tree

4 files changed

+63
-38
lines changed

4 files changed

+63
-38
lines changed

src/index.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { generatePodcastIntro, summarize } from './lib/ai'
44
import { generateAudioFromText, joinAudioFiles } from './lib/audio'
55
import { OUTPUT_DIR, podcastOutro as outro } from './lib/constants'
66
import { fetchTopStories } from './lib/hn'
7+
import { getTtsService } from './lib/services'
78
import { generateShowNotes } from './lib/show-notes'
89
import { initCacheDir } from './utils/cache'
910
import { loadEnvIfExists } from './utils/env'
@@ -17,14 +18,18 @@ const args = process.argv.slice(2)
1718
async function main() {
1819
await initOutputDir()
1920
await initCacheDir()
21+
const ttsService = getTtsService()
2022
const storyData = await fetchTopStories(args[0] ? parseInt(args[0]) : 10)
2123
const intro = await generatePodcastIntro(storyData)
2224
const summaries = await summarize(storyData)
23-
const audioFilenames = await generateAudioFromText([
24-
{ summary: intro.text, storyId: intro.cacheKey },
25-
...summaries,
26-
{ summary: outro, storyId: 'outro' },
27-
])
25+
const audioFilenames = await generateAudioFromText(
26+
[
27+
{ summary: intro.text, storyId: intro.cacheKey },
28+
...summaries,
29+
{ summary: outro, storyId: 'outro' },
30+
],
31+
ttsService,
32+
)
2833
await joinAudioFiles(audioFilenames, path.resolve(OUTPUT_DIR, 'output.mp3'))
2934
await generateShowNotes({ stories: summaries, introText: intro.text })
3035
log.info('Done!')

src/lib/audio.ts

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ffmpeg from 'fluent-ffmpeg'
22
import path from 'path'
33

4-
import type { StoryDataAggregate } from '../types'
4+
import type { StoryDataAggregate, TtsService } from '../types'
55

66
import { CACHE_DIR } from '../lib/constants'
77
import { readFromCache, writeToCache } from '../utils/cache'
@@ -18,6 +18,7 @@ type PodcastSegment = {
1818

1919
export async function generateAudioFromText(
2020
storyData: (PodcastSegment | StoryDataAggregate)[],
21+
ttsService: TtsService,
2122
): Promise<string[]> {
2223
const audioFilenames: string[] = []
2324

@@ -33,30 +34,9 @@ export async function generateAudioFromText(
3334

3435
logger.info(`[${i + 1}/${storyData.length}] Generating audio: ${story.storyId}...`)
3536
try {
36-
if (process.env.VOICE_SERVICE === 'elevenlabs') {
37-
const audioStream = await getElevenLabsClient().textToSpeech.convert(
38-
'56AoDkrOh6qfVPDXZ7Pt', // Cassidy
39-
{
40-
text: story.summary as string,
41-
model_id: 'eleven_turbo_v2',
42-
},
43-
)
44-
logger.info('Received back audio stream')
45-
const buffer = await streamToBuffer(audioStream)
46-
logger.info(`Audio file generated: ${filename}`)
47-
await writeToCache(filename, buffer)
48-
} else {
49-
const mp3 = await getOpenAIClient().audio.speech.create({
50-
model: 'tts-1-hd',
51-
voice: 'nova',
52-
input: story.summary as string,
53-
})
54-
55-
const buffer = Buffer.from(await mp3.arrayBuffer())
56-
logger.info(`Audio file generated: ${filename}`)
57-
await writeToCache(filename, buffer)
58-
}
59-
37+
const buffer = await ttsService.convert(story.summary as string)
38+
logger.info(`Audio file generated: ${filename}`)
39+
await writeToCache(filename, buffer)
6040
audioFilenames.push(filename)
6141
} catch (error) {
6242
logger.error(`Error generating audio for story: ${story.storyId}\nsummary: ${story.summary}`)
@@ -107,11 +87,3 @@ function insertBetween(array: string[], itemToInsert: string): string[] {
10787
return acc
10888
}, [] as string[])
10989
}
110-
111-
async function streamToBuffer(stream: NodeJS.ReadableStream) {
112-
const chunks: (Buffer | string)[] = []
113-
for await (const chunk of stream) {
114-
chunks.push(chunk)
115-
}
116-
return Buffer.concat(chunks as Buffer[])
117-
}

src/lib/services.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { TtsService } from '../types'
2+
3+
import { log } from '../utils/log'
4+
import { getElevenLabsClient, getOpenAI } from './clients'
5+
6+
export const getTtsService: () => TtsService = () => {
7+
log.info(`Using voice service: ${process.env.VOICE_SERVICE || 'openai'}`)
8+
if (process.env.VOICE_SERVICE === 'elevenlabs') {
9+
const client = getElevenLabsClient()
10+
return {
11+
convert: async (text: string) => {
12+
const audioStream = await client.textToSpeech.convert(
13+
'56AoDkrOh6qfVPDXZ7Pt', // Cassidy
14+
{
15+
text,
16+
model_id: 'eleven_turbo_v2',
17+
},
18+
)
19+
return await streamToBuffer(audioStream)
20+
},
21+
}
22+
} else {
23+
const client = getOpenAI()
24+
return {
25+
convert: async (text: string) => {
26+
const mp3 = await client.audio.speech.create({
27+
model: 'tts-1-hd',
28+
voice: 'nova',
29+
input: text,
30+
})
31+
32+
return Buffer.from(await mp3.arrayBuffer())
33+
},
34+
}
35+
}
36+
}
37+
38+
async function streamToBuffer(stream: NodeJS.ReadableStream) {
39+
const chunks: (Buffer | string)[] = []
40+
for await (const chunk of stream) {
41+
chunks.push(chunk)
42+
}
43+
return Buffer.concat(chunks as Buffer[])
44+
}

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,7 @@ export type ResponseData = {
104104
query: string
105105
serverTimeMS: number
106106
}
107+
108+
export type TtsService = {
109+
convert: (text: string) => Promise<Buffer>
110+
}

0 commit comments

Comments
 (0)