Export to m4b from WAV efficiently

This commit is contained in:
Richard Roberson 2025-02-25 20:01:15 -07:00
parent 79d2e7f613
commit 4a2d75e155
4 changed files with 10 additions and 23 deletions

View file

@ -87,26 +87,16 @@ export async function POST(request: NextRequest) {
await mkdir(intermediateDir); await mkdir(intermediateDir);
} }
// First, write each chapter to a temporary file and get its duration // Process each chapter - no need for initial conversion since input is WAV
const chapterFiles: { path: string; title: string; duration: number }[] = []; const chapterFiles: { path: string; title: string; duration: number }[] = [];
let currentTime = 0; let currentTime = 0;
for (let i = 0; i < data.chapters.length; i++) { for (let i = 0; i < data.chapters.length; i++) {
const chapter = data.chapters[i]; const chapter = data.chapters[i];
const inputPath = join(intermediateDir, `${i}-input.aac`);
const outputPath = join(intermediateDir, `${i}.wav`); const outputPath = join(intermediateDir, `${i}.wav`);
// Write the chapter audio to a temp file // Write the chapter audio directly since it's already WAV
await writeFile(inputPath, Buffer.from(new Uint8Array(chapter.buffer))); await writeFile(outputPath, Buffer.from(new Uint8Array(chapter.buffer)));
// Convert to WAV with consistent format (this helps with timestamp issues)
await runFFmpeg([
'-i', inputPath,
'-acodec', 'pcm_s16le',
'-ar', '44100',
'-ac', '2',
outputPath
]);
// Get the duration of this chapter // Get the duration of this chapter
const duration = await getAudioDuration(outputPath); const duration = await getAudioDuration(outputPath);
@ -116,9 +106,6 @@ export async function POST(request: NextRequest) {
title: chapter.title, title: chapter.title,
duration duration
}); });
// Clean up input file
await unlink(inputPath).catch(console.error);
} }
// Create chapter metadata file // Create chapter metadata file
@ -129,7 +116,7 @@ export async function POST(request: NextRequest) {
); );
// Calculate chapter timings based on actual durations // Calculate chapter timings based on actual durations
chapterFiles.forEach((chapter, index) => { chapterFiles.forEach((chapter) => {
const startMs = Math.floor(currentTime * 1000); const startMs = Math.floor(currentTime * 1000);
currentTime += chapter.duration; currentTime += chapter.duration;
const endMs = Math.floor(currentTime * 1000); const endMs = Math.floor(currentTime * 1000);

View file

@ -29,7 +29,8 @@ export async function POST(req: NextRequest) {
voice: voice as "alloy", voice: voice as "alloy",
input: text, input: text,
speed: speed, speed: speed,
response_format: format === 'aac' ? 'aac' : 'mp3', // Use wav format for audiobook generation to avoid initial conversion
response_format: format === 'audiobook' ? 'wav' : (format === 'aac' ? 'aac' : 'mp3'),
}, { signal: req.signal }); // Pass the abort signal to OpenAI client }, { signal: req.signal }); // Pass the abort signal to OpenAI client
// Get the audio data as array buffer // Get the audio data as array buffer
@ -37,7 +38,7 @@ export async function POST(req: NextRequest) {
const stream = response.body; const stream = response.body;
// Return audio data with appropriate headers // Return audio data with appropriate headers
const contentType = format === 'aac' ? 'audio/aac' : 'audio/mpeg'; const contentType = format === 'audiobook' ? 'audio/wav' : (format === 'aac' ? 'audio/aac' : 'audio/mpeg');
return new NextResponse(stream, { return new NextResponse(stream, {
headers: { headers: {
'Content-Type': contentType 'Content-Type': contentType

View file

@ -224,7 +224,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
text: text.trim(), text: text.trim(),
voice: voice, voice: voice,
speed: voiceSpeed, speed: voiceSpeed,
format: 'aac' format: 'audiobook' // Request WAV format directly
}), }),
signal signal
}); });
@ -244,7 +244,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
let spineIndex = processedSections; let spineIndex = processedSections;
let currentSpineHref: string | undefined; let currentSpineHref: string | undefined;
spine.each((item: any) => { spine.each((item: SpineItem) => {
if (spineIndex === 0) { if (spineIndex === 0) {
currentSpineHref = item.href; currentSpineHref = item.href;
} }

View file

@ -36,7 +36,6 @@ import {
} from '@/utils/pdf'; } from '@/utils/pdf';
import type { PDFDocumentProxy } from 'pdfjs-dist'; import type { PDFDocumentProxy } from 'pdfjs-dist';
import { useParams } from 'next/navigation';
/** /**
* Interface defining all available methods and properties in the PDF context * Interface defining all available methods and properties in the PDF context
@ -234,7 +233,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
text: text.trim(), text: text.trim(),
voice: voice, voice: voice,
speed: voiceSpeed, speed: voiceSpeed,
format: 'aac' format: 'audiobook' // Request WAV format directly
}), }),
signal signal
}); });