@@ -3,33 +3,58 @@ import path from 'path'
33
44import type { StoryDataAggregate , TtsService } from '../types'
55
6- import { CACHE_DIR } from '../lib/constants'
6+ import { CACHE_DIR , podcastOutro } from '../lib/constants'
77import { readFromCache , writeToCache } from '../utils/cache'
88import { childLogger , log } from '../utils/log'
99
1010const logger = childLogger ( 'AUDIO' )
11- const silence = path . resolve ( __dirname , 'silence-1s.mp3' )
11+ const silenceAudioPath = path . resolve ( __dirname , 'silence-1s.mp3' )
12+
13+ const silenceAudioSegment : PodcastSegmentWithAudio = {
14+ audioFilename : silenceAudioPath ,
15+ title : 'Silence' ,
16+ summary : 'Silence' ,
17+ isSilence : true ,
18+ }
19+
20+ const outroSegment : PodcastSegment = {
21+ summary : podcastOutro ,
22+ storyId : 'outro' ,
23+ title : 'Outro' ,
24+ }
1225
1326type PodcastSegment = {
27+ title : string
1428 storyId : string
1529 summary : string
1630}
1731
32+ type PodcastSegmentWithAudio = { audioFilename : string ; isSilence ?: boolean } & Omit <
33+ PodcastSegment ,
34+ 'storyId'
35+ >
36+
1837type Chapter = { title : string ; start : number ; end : number }
1938
2039export async function generateAudioFromText (
2140 storyData : ( PodcastSegment | StoryDataAggregate ) [ ] ,
2241 ttsService : TtsService ,
23- ) : Promise < string [ ] > {
24- const audioFilenames : string [ ] = [ ]
42+ ) : Promise < PodcastSegmentWithAudio [ ] > {
43+ const segments : PodcastSegmentWithAudio [ ] = [ ]
44+
45+ storyData . push ( outroSegment )
2546
2647 for ( const [ i , story ] of storyData . entries ( ) ) {
2748 const filename = `segment-${ story . storyId } .mp3`
2849
2950 const cached = await readFromCache ( filename )
3051 if ( cached ) {
3152 logger . info ( `[${ i + 1 } /${ storyData . length } ] Using cached audio for ${ filename } ` )
32- audioFilenames . push ( filename )
53+ segments . push ( {
54+ audioFilename : path . resolve ( CACHE_DIR , filename ) ,
55+ title : story . title ,
56+ summary : story . summary as string ,
57+ } )
3358 continue
3459 }
3560
@@ -38,37 +63,46 @@ export async function generateAudioFromText(
3863 const buffer = await ttsService . convert ( story . summary as string )
3964 logger . info ( `Audio file generated: ${ filename } ` )
4065 await writeToCache ( filename , buffer )
41- audioFilenames . push ( filename )
66+ segments . push ( {
67+ audioFilename : path . resolve ( CACHE_DIR , filename ) ,
68+ title : story . title ,
69+ summary : story . summary as string ,
70+ } )
4271 } catch ( error ) {
4372 logger . error ( `Error generating audio for story: ${ story . storyId } \nsummary: ${ story . summary } ` )
4473 logger . error ( error )
4574 }
4675 }
4776
48- return audioFilenames
77+ return segments
4978}
5079
51- export async function joinAudioFiles ( filenames : string [ ] , outputFilename : string ) : Promise < void > {
52- logger . info ( `Merging ${ filenames . length } audio files into ${ outputFilename } ...` )
80+ export async function joinAudioFiles (
81+ segments : PodcastSegmentWithAudio [ ] ,
82+ outputFilename : string ,
83+ ) : Promise < void > {
84+ logger . info ( `Merging ${ segments . length } audio files into ${ outputFilename } ...` )
5385
5486 // insert silence between segments
55- const filesWithSilence = insertBetween ( filenames , silence )
87+ const segmentsWithSilence = insertBetween ( segments , silenceAudioSegment )
5688 logger . debug ( {
57- filesWithSilence,
89+ filesWithSilence : segmentsWithSilence ,
5890 } )
5991
60- const durations = await calculateDurations ( filesWithSilence )
61- log . info ( { msg : 'DURRATTIONNNSSSSSSSSSSSSSSSSSSSS' , durations } )
92+ const durations = await calculateChapters ( segmentsWithSilence )
93+ log . info ( { durations } )
6294
6395 // Write chapter metadata to file
6496 const metadataContent = createMetadataContent ( durations )
6597 await writeToCache ( 'chapters.txt' , metadataContent )
6698
99+ const noChapterOutputFilename = path . resolve ( CACHE_DIR , 'output-no-chapters.mp3' )
100+
67101 await new Promise ( ( resolve , reject ) => {
68102 const command = ffmpeg ( )
69103
70- filesWithSilence
71- . map ( f => ( path . isAbsolute ( f ) ? f : path . resolve ( CACHE_DIR , f ) ) )
104+ segmentsWithSilence
105+ . map ( f => f . audioFilename )
72106 . forEach ( file => {
73107 command . input ( file )
74108 } )
@@ -85,7 +119,7 @@ export async function joinAudioFiles(filenames: string[], outputFilename: string
85119 logger . error ( 'Error occurred:' , err )
86120 reject ( err )
87121 } )
88- . mergeToFile ( outputFilename , CACHE_DIR )
122+ . mergeToFile ( noChapterOutputFilename , CACHE_DIR )
89123 } )
90124
91125 log . info ( 'Writing metadata to file...' )
@@ -106,26 +140,30 @@ export async function joinAudioFiles(filenames: string[], outputFilename: string
106140 logger . error ( 'Error occurred:' , err )
107141 reject ( err )
108142 } )
109- . input ( outputFilename )
143+ . input ( noChapterOutputFilename )
110144 . input ( path . resolve ( CACHE_DIR , 'chapters.txt' ) )
111145 . audioCodec ( 'copy' )
112146 . outputOptions ( '-map_metadata' , '1' )
113147 . outputOptions ( '-metadata' , 'title=Hacker News Recap' )
114- . save ( outputFilename + '.chapters.mp3' )
148+ . save ( outputFilename )
115149 } )
116150
117151 log . info ( 'Audio file created' )
118152}
119153
120- function insertBetween ( array : string [ ] , itemToInsert : string ) : string [ ] {
154+ function insertBetween (
155+ array : PodcastSegmentWithAudio [ ] ,
156+ itemToInsert : PodcastSegmentWithAudio ,
157+ ) : PodcastSegmentWithAudio [ ] {
121158 return array . reduce ( ( acc , current , index ) => {
122159 if ( index > 0 ) {
123160 acc . push ( itemToInsert )
124161 }
125162 acc . push ( current )
126163 return acc
127- } , [ ] as string [ ] )
164+ } , [ ] as PodcastSegmentWithAudio [ ] )
128165}
166+
129167function createMetadataContent ( chapters : Chapter [ ] ) : string {
130168 return (
131169 `;FFMETADATA1\n` +
@@ -143,37 +181,40 @@ title=${chapter.title}
143181 )
144182}
145183
146- function calculateDurations ( filenames : string [ ] ) : Promise < Chapter [ ] > {
184+ function calculateChapters ( segments : PodcastSegmentWithAudio [ ] ) : Promise < Chapter [ ] > {
147185 return new Promise ( ( resolve , reject ) => {
148186 let currentTime = 0
149187 const chapters : Chapter [ ] = [ ]
150188
151- const processFile = ( index : number ) => {
152- if ( index >= filenames . length ) {
189+ const processFile = ( i : number ) => {
190+ if ( i >= segments . length ) {
153191 resolve ( chapters )
154192 return
155193 }
156194
157- const file = path . isAbsolute ( filenames [ index ] )
158- ? filenames [ index ]
159- : path . resolve ( CACHE_DIR , filenames [ index ] )
160- ffmpeg . ffprobe ( file , ( err , metadata ) => {
195+ ffmpeg . ffprobe ( segments [ i ] . audioFilename , ( err , metadata ) => {
161196 if ( err ) {
162197 reject ( err as Error )
163198 return
164199 }
165200
166201 const duration = metadata . format . duration || 0
167- const title = path . basename ( file , path . extname ( file ) )
202+
203+ // If it's silence, just skip to the next file
204+ if ( segments [ i ] . isSilence ) {
205+ currentTime += duration
206+ processFile ( i + 1 )
207+ return
208+ }
168209
169210 chapters . push ( {
170- title,
211+ title : segments [ i ] . title ,
171212 start : currentTime ,
172213 end : currentTime + duration ,
173214 } )
174215
175216 currentTime += duration
176- processFile ( index + 1 )
217+ processFile ( i + 1 )
177218 } )
178219 }
179220
0 commit comments