Apply formatting
This commit is contained in:
111
services/tts.ts
111
services/tts.ts
@ -17,18 +17,18 @@ function splitTextIntoChunks(text: string, maxLength: number = 50): string[] {
|
||||
|
||||
// Split by sentences first (Japanese periods and line breaks)
|
||||
const sentences = text.split(/([。!?\n])/);
|
||||
|
||||
|
||||
for (let i = 0; i < sentences.length; i++) {
|
||||
const sentence = sentences[i];
|
||||
if (!sentence) continue;
|
||||
|
||||
|
||||
if (currentChunk.length + sentence.length <= maxLength) {
|
||||
currentChunk += sentence;
|
||||
} else {
|
||||
if (currentChunk.trim()) {
|
||||
chunks.push(currentChunk.trim());
|
||||
}
|
||||
|
||||
|
||||
// If single sentence is too long, split further
|
||||
if (sentence.length > maxLength) {
|
||||
const subChunks = splitLongSentence(sentence, maxLength);
|
||||
@ -39,12 +39,12 @@ function splitTextIntoChunks(text: string, maxLength: number = 50): string[] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (currentChunk.trim()) {
|
||||
chunks.push(currentChunk.trim());
|
||||
}
|
||||
|
||||
return chunks.filter(chunk => chunk.length > 0);
|
||||
|
||||
return chunks.filter((chunk) => chunk.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,10 +57,10 @@ function splitLongSentence(sentence: string, maxLength: number): string[] {
|
||||
|
||||
const chunks: string[] = [];
|
||||
let currentChunk = "";
|
||||
|
||||
|
||||
// Split by commas and common Japanese particles
|
||||
const parts = sentence.split(/([、,,]|[はがでをにと])/);
|
||||
|
||||
|
||||
for (const part of parts) {
|
||||
if (currentChunk.length + part.length <= maxLength) {
|
||||
currentChunk += part;
|
||||
@ -71,11 +71,11 @@ function splitLongSentence(sentence: string, maxLength: number): string[] {
|
||||
currentChunk = part;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (currentChunk.trim()) {
|
||||
chunks.push(currentChunk.trim());
|
||||
}
|
||||
|
||||
|
||||
// If still too long, force split by character limit
|
||||
const finalChunks: string[] = [];
|
||||
for (const chunk of chunks) {
|
||||
@ -87,8 +87,8 @@ function splitLongSentence(sentence: string, maxLength: number): string[] {
|
||||
finalChunks.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return finalChunks.filter(chunk => chunk.length > 0);
|
||||
|
||||
return finalChunks.filter((chunk) => chunk.length > 0);
|
||||
}
|
||||
|
||||
interface VoiceStyle {
|
||||
@ -112,7 +112,9 @@ async function generateAudioForChunk(
|
||||
const queryUrl = `${config.voicevox.host}/audio_query?text=${encodedText}&speaker=${defaultVoiceStyle.styleId}`;
|
||||
const synthesisUrl = `${config.voicevox.host}/synthesis?speaker=${defaultVoiceStyle.styleId}`;
|
||||
|
||||
console.log(`チャンク${chunkIndex + 1}の音声クエリ開始: ${itemId} (${chunkText.length}文字)`);
|
||||
console.log(
|
||||
`チャンク${chunkIndex + 1}の音声クエリ開始: ${itemId} (${chunkText.length}文字)`,
|
||||
);
|
||||
|
||||
const queryResponse = await fetch(queryUrl, {
|
||||
method: "POST",
|
||||
@ -157,11 +159,16 @@ async function generateAudioForChunk(
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const chunkWavPath = path.resolve(outputDir, `${itemId}_chunk_${chunkIndex}.wav`);
|
||||
const chunkWavPath = path.resolve(
|
||||
outputDir,
|
||||
`${itemId}_chunk_${chunkIndex}.wav`,
|
||||
);
|
||||
fs.writeFileSync(chunkWavPath, audioBuffer);
|
||||
|
||||
console.log(`チャンク${chunkIndex + 1}のWAVファイル保存完了: ${chunkWavPath}`);
|
||||
|
||||
|
||||
console.log(
|
||||
`チャンク${chunkIndex + 1}のWAVファイル保存完了: ${chunkWavPath}`,
|
||||
);
|
||||
|
||||
return chunkWavPath;
|
||||
}
|
||||
|
||||
@ -173,25 +180,34 @@ async function concatenateAudioFiles(
|
||||
outputMp3Path: string,
|
||||
): Promise<void> {
|
||||
const ffmpegCmd = ffmpegPath || "ffmpeg";
|
||||
|
||||
|
||||
// Create a temporary file list for FFmpeg concat
|
||||
const tempDir = config.paths.podcastAudioDir;
|
||||
const listFilePath = path.resolve(tempDir, `concat_list_${Date.now()}.txt`);
|
||||
|
||||
|
||||
try {
|
||||
// Write file list in FFmpeg concat format
|
||||
const fileList = wavFiles.map(file => `file '${path.resolve(file)}'`).join('\n');
|
||||
const fileList = wavFiles
|
||||
.map((file) => `file '${path.resolve(file)}'`)
|
||||
.join("\n");
|
||||
fs.writeFileSync(listFilePath, fileList);
|
||||
|
||||
console.log(`音声ファイル結合開始: ${wavFiles.length}個のファイルを結合 -> ${outputMp3Path}`);
|
||||
console.log(
|
||||
`音声ファイル結合開始: ${wavFiles.length}個のファイルを結合 -> ${outputMp3Path}`,
|
||||
);
|
||||
|
||||
const result = Bun.spawnSync([
|
||||
ffmpegCmd,
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", listFilePath,
|
||||
"-codec:a", "libmp3lame",
|
||||
"-qscale:a", "2",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
listFilePath,
|
||||
"-codec:a",
|
||||
"libmp3lame",
|
||||
"-qscale:a",
|
||||
"2",
|
||||
"-y", // Overwrite output file
|
||||
outputMp3Path,
|
||||
]);
|
||||
@ -209,7 +225,7 @@ async function concatenateAudioFiles(
|
||||
if (fs.existsSync(listFilePath)) {
|
||||
fs.unlinkSync(listFilePath);
|
||||
}
|
||||
|
||||
|
||||
// Clean up individual WAV files
|
||||
for (const wavFile of wavFiles) {
|
||||
if (fs.existsSync(wavFile)) {
|
||||
@ -236,12 +252,14 @@ export async function generateTTSWithoutQueue(
|
||||
throw new Error("Script text is required for TTS generation");
|
||||
}
|
||||
|
||||
console.log(`TTS生成開始: ${itemId} (試行回数: ${retryCount + 1}, ${scriptText.length}文字)`);
|
||||
console.log(
|
||||
`TTS生成開始: ${itemId} (試行回数: ${retryCount + 1}, ${scriptText.length}文字)`,
|
||||
);
|
||||
|
||||
// Split text into chunks
|
||||
const chunks = splitTextIntoChunks(scriptText.trim());
|
||||
console.log(`テキストを${chunks.length}個のチャンクに分割: ${itemId}`);
|
||||
|
||||
|
||||
if (chunks.length === 0) {
|
||||
throw new Error("No valid text chunks generated");
|
||||
}
|
||||
@ -259,8 +277,10 @@ export async function generateTTSWithoutQueue(
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
if (!chunk) continue;
|
||||
console.log(`チャンク${i + 1}/${chunks.length}処理中: "${chunk.substring(0, 30)}${chunk.length > 30 ? '...' : ''}"`);
|
||||
|
||||
console.log(
|
||||
`チャンク${i + 1}/${chunks.length}処理中: "${chunk.substring(0, 30)}${chunk.length > 30 ? "..." : ""}"`,
|
||||
);
|
||||
|
||||
const wavPath = await generateAudioForChunk(chunk, i, itemId);
|
||||
generatedWavFiles.push(wavPath);
|
||||
}
|
||||
@ -273,12 +293,15 @@ export async function generateTTSWithoutQueue(
|
||||
if (!firstWavFile) {
|
||||
throw new Error("No WAV files generated");
|
||||
}
|
||||
|
||||
|
||||
const result = Bun.spawnSync([
|
||||
ffmpegCmd,
|
||||
"-i", firstWavFile,
|
||||
"-codec:a", "libmp3lame",
|
||||
"-qscale:a", "2",
|
||||
"-i",
|
||||
firstWavFile,
|
||||
"-codec:a",
|
||||
"libmp3lame",
|
||||
"-qscale:a",
|
||||
"2",
|
||||
"-y",
|
||||
mp3FilePath,
|
||||
]);
|
||||
@ -289,7 +312,7 @@ export async function generateTTSWithoutQueue(
|
||||
: "Unknown error";
|
||||
throw new Error(`FFmpeg conversion failed: ${stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Clean up WAV file
|
||||
fs.unlinkSync(firstWavFile);
|
||||
} else {
|
||||
@ -299,7 +322,6 @@ export async function generateTTSWithoutQueue(
|
||||
|
||||
console.log(`TTS生成完了: ${itemId} (${chunks.length}チャンク)`);
|
||||
return path.basename(mp3FilePath);
|
||||
|
||||
} catch (error) {
|
||||
// Clean up any generated files on error
|
||||
for (const wavFile of generatedWavFiles) {
|
||||
@ -307,7 +329,7 @@ export async function generateTTSWithoutQueue(
|
||||
fs.unlinkSync(wavFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -318,19 +340,24 @@ export async function generateTTS(
|
||||
retryCount: number = 0,
|
||||
): Promise<string> {
|
||||
const maxRetries = 2;
|
||||
|
||||
|
||||
try {
|
||||
return await generateTTSWithoutQueue(itemId, scriptText, retryCount);
|
||||
} catch (error) {
|
||||
console.error(`TTS生成エラー: ${itemId} (試行回数: ${retryCount + 1})`, error);
|
||||
|
||||
console.error(
|
||||
`TTS生成エラー: ${itemId} (試行回数: ${retryCount + 1})`,
|
||||
error,
|
||||
);
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
// Add to queue for retry only on initial failure
|
||||
const { addToQueue } = await import("../services/database.js");
|
||||
await addToQueue(itemId, scriptText, retryCount);
|
||||
throw new Error(`TTS generation failed, added to retry queue: ${error}`);
|
||||
} else {
|
||||
throw new Error(`TTS generation failed after ${maxRetries + 1} attempts: ${error}`);
|
||||
throw new Error(
|
||||
`TTS generation failed after ${maxRetries + 1} attempts: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user