11import { NextRequest , NextResponse } from 'next/server' ;
22import { spawn } from 'child_process' ;
3- import { writeFile , readFile , mkdir , unlink , rmdir } from 'fs/promises' ;
3+ import { writeFile , mkdir , unlink , rmdir } from 'fs/promises' ;
4+ import { createReadStream } from 'fs' ;
45import { existsSync } from 'fs' ;
56import { join } from 'path' ;
67import { randomUUID } from 'crypto' ;
@@ -65,9 +66,19 @@ async function runFFmpeg(args: string[]): Promise<void> {
6566 } ) ;
6667}
6768
69+ async function cleanup ( files : string [ ] , directories : string [ ] ) {
70+ await Promise . all ( [
71+ ...files . map ( f => unlink ( f ) . catch ( console . error ) ) ,
72+ ...directories . map ( d => rmdir ( d ) . catch ( console . error ) )
73+ ] ) ;
74+ }
75+
6876export async function POST ( request : NextRequest ) {
77+ const tempFiles : string [ ] = [ ] ;
78+ const tempDirs : string [ ] = [ ] ;
79+
6980 try {
70- // Parse the request body
81+ // Parse the request body as a stream
7182 const data : ConversionRequest = await request . json ( ) ;
7283
7384 // Create temp directory if it doesn't exist
@@ -82,40 +93,65 @@ export async function POST(request: NextRequest) {
8293 const metadataPath = join ( tempDir , `${ id } .txt` ) ;
8394 const intermediateDir = join ( tempDir , `${ id } -intermediate` ) ;
8495
96+ tempFiles . push ( outputPath , metadataPath ) ;
97+ tempDirs . push ( intermediateDir ) ;
98+
8599 // Create intermediate directory
86100 if ( ! existsSync ( intermediateDir ) ) {
87101 await mkdir ( intermediateDir ) ;
88102 }
89103
90- // Process each chapter - no need for initial conversion since input is WAV
104+ // Process chapters sequentially to avoid memory issues
91105 const chapterFiles : { path : string ; title : string ; duration : number } [ ] = [ ] ;
92106 let currentTime = 0 ;
93107
94108 for ( let i = 0 ; i < data . chapters . length ; i ++ ) {
95109 const chapter = data . chapters [ i ] ;
110+ const inputPath = join ( intermediateDir , `${ i } -input.mp3` ) ;
96111 const outputPath = join ( intermediateDir , `${ i } .wav` ) ;
97112
98- // Write the chapter audio directly since it's already WAV
99- await writeFile ( outputPath , Buffer . from ( new Uint8Array ( chapter . buffer ) ) ) ;
113+ tempFiles . push ( inputPath , outputPath ) ;
114+
115+ // Write the chapter audio to a temp file using a Buffer chunk size of 64KB
116+ const chunkSize = 64 * 1024 ; // 64KB chunks
117+ const buffer = Buffer . from ( new Uint8Array ( chapter . buffer ) ) ;
118+ const chunks : Buffer [ ] = [ ] ;
119+
120+ for ( let offset = 0 ; offset < buffer . length ; offset += chunkSize ) {
121+ chunks . push ( buffer . slice ( offset , offset + chunkSize ) ) ;
122+ }
123+
124+ await writeFile ( inputPath , Buffer . concat ( chunks ) ) ;
125+ chunks . length = 0 ; // Clear chunks array
126+
127+ // Convert to WAV with consistent format
128+ await runFFmpeg ( [
129+ '-i' , inputPath ,
130+ '-acodec' , 'pcm_s16le' ,
131+ '-ar' , '44100' ,
132+ '-ac' , '2' ,
133+ outputPath
134+ ] ) ;
100135
101- // Get the duration of this chapter
102136 const duration = await getAudioDuration ( outputPath ) ;
103137
104138 chapterFiles . push ( {
105139 path : outputPath ,
106140 title : chapter . title ,
107141 duration
108142 } ) ;
143+
144+ // Clean up input file early
145+ await unlink ( inputPath ) . catch ( console . error ) ;
146+ const index = tempFiles . indexOf ( inputPath ) ;
147+ if ( index > - 1 ) {
148+ tempFiles . splice ( index , 1 ) ;
149+ }
109150 }
110151
111152 // Create chapter metadata file
112153 const metadata : string [ ] = [ ] ;
113- metadata . push (
114- `title=Kokoro Audiobook` ,
115- `artist=KokoroTTS` ,
116- ) ;
117154
118- // Calculate chapter timings based on actual durations
119155 chapterFiles . forEach ( ( chapter ) => {
120156 const startMs = Math . floor ( currentTime * 1000 ) ;
121157 currentTime += chapter . duration ;
@@ -134,6 +170,8 @@ export async function POST(request: NextRequest) {
134170
135171 // Create list file for concat
136172 const listPath = join ( tempDir , `${ id } -list.txt` ) ;
173+ tempFiles . push ( listPath ) ;
174+
137175 await writeFile (
138176 listPath ,
139177 chapterFiles . map ( f => `file '${ f . path } '` ) . join ( '\n' )
@@ -152,24 +190,46 @@ export async function POST(request: NextRequest) {
152190 outputPath
153191 ] ) ;
154192
155- // Read the converted file
156- const m4bData = await readFile ( outputPath ) ;
157-
158- // Clean up temp files
159- await Promise . all ( [
160- ...chapterFiles . map ( f => unlink ( f . path ) ) ,
161- unlink ( metadataPath ) ,
162- unlink ( listPath ) ,
163- unlink ( outputPath ) ,
164- rmdir ( intermediateDir )
165- ] . map ( p => p . catch ( console . error ) ) ) ;
193+ // Create a readable stream from the output file
194+ const fileStream = createReadStream ( outputPath ) ;
195+
196+ // Create a web-compatible ReadableStream from the Node.js stream
197+ const webStream = new ReadableStream ( {
198+ start ( controller ) {
199+ fileStream . on ( 'data' , ( chunk ) => {
200+ controller . enqueue ( chunk ) ;
201+ } ) ;
202+
203+ fileStream . on ( 'end' , ( ) => {
204+ controller . close ( ) ;
205+ // Clean up only after the stream has been fully sent
206+ cleanup ( tempFiles , tempDirs ) . catch ( console . error ) ;
207+ } ) ;
208+
209+ fileStream . on ( 'error' , ( error ) => {
210+ console . error ( 'Stream error:' , error ) ;
211+ controller . error ( error ) ;
212+ cleanup ( tempFiles , tempDirs ) . catch ( console . error ) ;
213+ } ) ;
214+ } ,
215+ cancel ( ) {
216+ fileStream . destroy ( ) ;
217+ cleanup ( tempFiles , tempDirs ) . catch ( console . error ) ;
218+ }
219+ } ) ;
166220
167- return new NextResponse ( m4bData , {
221+ // Return the streaming response
222+ return new NextResponse ( webStream , {
168223 headers : {
169224 'Content-Type' : 'audio/mp4' ,
225+ 'Transfer-Encoding' : 'chunked'
170226 } ,
171227 } ) ;
228+
172229 } catch ( error ) {
230+ // Clean up in case of error
231+ await cleanup ( tempFiles , tempDirs ) . catch ( console . error ) ;
232+
173233 console . error ( 'Error converting audio:' , error ) ;
174234 return NextResponse . json (
175235 { error : 'Failed to convert audio format' } ,
0 commit comments