UNPKG

@henteko/kumiki

Version:

A video generation tool that creates videos from JSON configurations

257 lines 9.3 kB
import crypto from 'node:crypto'; import { existsSync } from 'node:fs'; import { mkdir, readFile, writeFile, stat, unlink } from 'node:fs/promises'; import path from 'node:path'; import { FFmpegService } from '../services/ffmpeg.js'; import { getGeneratedMusicCacheDir } from '../utils/app-dirs.js'; import { logger } from '../utils/logger.js'; export class MusicCache { cacheDir; manifestPath; manifest = null; initialized = false; constructor() { this.cacheDir = getGeneratedMusicCacheDir(); this.manifestPath = path.join(this.cacheDir, 'manifest.json'); } async initialize() { if (this.initialized) return; // キャッシュディレクトリを作成 await mkdir(this.cacheDir, { recursive: true }); // マニフェストを読み込み await this.loadManifest(); this.initialized = true; } async loadManifest() { try { if (existsSync(this.manifestPath)) { const data = await readFile(this.manifestPath, 'utf-8'); this.manifest = JSON.parse(data); } else { this.manifest = { version: '1.0', entries: [], }; await this.saveManifest(); } } catch (error) { logger.error('Failed to load music cache manifest', { error }); this.manifest = { version: '1.0', entries: [], }; } } async saveManifest() { if (!this.manifest) return; try { await writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), 'utf-8'); } catch (error) { logger.error('Failed to save music cache manifest', { error }); } } generateCacheKey(params) { const normalized = JSON.stringify({ prompt: params.prompt, prompts: params.prompts, config: params.config, duration: params.duration, seed: params.seed, model: 'models/lyria-realtime-exp', }, Object.keys(params).sort()); return crypto.createHash('sha256').update(normalized).digest('hex').substring(0, 16); } async get(key, projectPath) { if (!this.manifest) return null; const entry = this.manifest.entries.find(e => e.key === key); if (!entry) return null; // MP3ファイルを探す(新しい形式) let filePath = path.join(this.cacheDir, `${key}.mp3`); if (!existsSync(filePath)) { // 古いWAVファイルも確認(後方互換性) filePath = path.join(this.cacheDir, `${key}.wav`); if (!existsSync(filePath)) { // キャッシュファイルが存在しない場合はエントリを削除 this.manifest.entries = this.manifest.entries.filter(e => e.key !== key); await this.saveManifest(); return null; } } // 使用統計を更新 entry.usage.lastUsed = new Date().toISOString(); entry.usage.useCount++; if (projectPath && !entry.usage.projects.includes(projectPath)) { entry.usage.projects.push(projectPath); } await this.saveManifest(); return filePath; } async save(key, data, params, projectPath) { if (!this.manifest) { throw new Error('Music cache not initialized'); } // 一時的にWAVファイルとして保存 const tempWavPath = path.join(this.cacheDir, `${key}_temp.wav`); await writeFile(tempWavPath, data); // MP3に変換 const mp3Path = path.join(this.cacheDir, `${key}.mp3`); const ffmpeg = FFmpegService.getInstance(); try { // WAVをMP3に変換(ビットレート192kbps) await ffmpeg.execute('ffmpeg', [ '-i', tempWavPath, '-codec:a', 'libmp3lame', '-b:a', '192k', '-y', mp3Path ]); // 一時WAVファイルを削除 await unlink(tempWavPath); } catch (error) { // 変換に失敗した場合は一時ファイルをクリーンアップ if (existsSync(tempWavPath)) { await unlink(tempWavPath); } throw error; } // MP3ファイル情報を取得 const stats = await stat(mp3Path); // マニフェストエントリを作成 const entry = { key, params, metadata: { generatedAt: new Date().toISOString(), model: 'models/lyria-realtime-exp', fileSize: stats.size, mimeType: 'audio/mp3', sampleRate: 48000, channels: 2, actualDuration: params.duration, }, usage: { lastUsed: new Date().toISOString(), useCount: 1, projects: projectPath ? [projectPath] : [], }, }; // 既存のエントリを更新または新規追加 const existingIndex = this.manifest.entries.findIndex(e => e.key === key); if (existingIndex >= 0) { this.manifest.entries[existingIndex] = entry; } else { this.manifest.entries.push(entry); } await this.saveManifest(); return mp3Path; } async clear(options) { if (!this.manifest) return 0; let clearedCount = 0; const now = Date.now(); const newEntries = []; for (const entry of this.manifest.entries) { const shouldDelete = options?.olderThan ? now - new Date(entry.usage.lastUsed).getTime() > options.olderThan : true; if (shouldDelete) { // MP3とWAV両方を削除試行 const mp3Path = path.join(this.cacheDir, `${entry.key}.mp3`); const wavPath = path.join(this.cacheDir, `${entry.key}.wav`); try { if (existsSync(mp3Path)) { await unlink(mp3Path); clearedCount++; } else if (existsSync(wavPath)) { await unlink(wavPath); clearedCount++; } } catch (error) { logger.error('Failed to delete cached music file', { mp3Path, wavPath, error }); } } else { newEntries.push(entry); } } this.manifest.entries = newEntries; await this.saveManifest(); return clearedCount; } getStats() { if (!this.manifest) { return { totalFiles: 0, totalSize: 0, oldestFile: null, newestFile: null, }; } let totalSize = 0; let oldestDate = null; let newestDate = null; // マニフェストからサイズと日付を集計 for (const entry of this.manifest.entries) { // MP3ファイルを優先、なければWAVファイル let filePath = path.join(this.cacheDir, `${entry.key}.mp3`); if (!existsSync(filePath)) { filePath = path.join(this.cacheDir, `${entry.key}.wav`); } if (existsSync(filePath)) { totalSize += entry.metadata.fileSize; const generatedDate = new Date(entry.metadata.generatedAt); if (!oldestDate || generatedDate < oldestDate) { oldestDate = generatedDate; } if (!newestDate || generatedDate > newestDate) { newestDate = generatedDate; } } } return { totalFiles: this.manifest.entries.length, totalSize, oldestFile: oldestDate, newestFile: newestDate, }; } getAllCachedFiles() { if (!this.manifest) return []; const files = []; for (const entry of this.manifest.entries) { // MP3ファイルを優先、なければWAVファイル let filePath = path.join(this.cacheDir, `${entry.key}.mp3`); if (!existsSync(filePath)) { filePath = path.join(this.cacheDir, `${entry.key}.wav`); } if (existsSync(filePath)) { files.push({ path: filePath, params: entry.params, metadata: entry.metadata, }); } } return files; } } // シングルトンインスタンス export const musicCache = new MusicCache(); // キャッシュキー生成のエクスポート export function generateMusicCacheKey(params) { return musicCache.generateCacheKey(params); } //# sourceMappingURL=music-cache.js.map