Apply formatting

This commit is contained in:
2025-06-08 15:21:58 +09:00
parent b5ff912fcb
commit a728ebb66c
28 changed files with 1809 additions and 1137 deletions

View File

@ -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}`,
);
}
}
}