UNPKG

@henteko/kumiki

Version:

A video generation tool that creates videos from JSON configurations

209 lines 7.33 kB
import crypto from 'node:crypto'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import { getGeneratedImageCacheDir } from '../utils/app-dirs.js'; import { logger } from '../utils/logger.js'; export class ImageCache { cacheDir; manifestPath; manifest = null; constructor(cacheDir) { this.cacheDir = cacheDir || getGeneratedImageCacheDir(); this.manifestPath = path.join(this.cacheDir, 'manifest.json'); } async initialize() { // キャッシュディレクトリを作成 await fs.mkdir(this.cacheDir, { recursive: true }); // マニフェストを読み込む await this.loadManifest(); } /** * キャッシュから画像を取得 */ async get(cacheKey) { const entry = this.manifest?.entries.find(e => e.key === cacheKey); if (!entry) { return null; } const imagePath = path.join(this.cacheDir, `${cacheKey}.png`); try { await fs.access(imagePath); // 使用統計を更新 entry.usage.lastUsed = new Date().toISOString(); entry.usage.useCount++; await this.saveManifest(); logger.debug('Cache hit', { cacheKey, path: imagePath }); return imagePath; } catch { // ファイルが存在しない場合はエントリを削除 if (this.manifest) { this.manifest.entries = this.manifest.entries.filter(e => e.key !== cacheKey); await this.saveManifest(); } return null; } } /** * 画像をキャッシュに保存 */ async save(cacheKey, imageData, params) { const imagePath = path.join(this.cacheDir, `${cacheKey}.png`); // 画像データを保存 await fs.writeFile(imagePath, imageData); // ファイルサイズを取得 const stats = await fs.stat(imagePath); // マニフェストに追加 const entry = { key: cacheKey, params, metadata: { generatedAt: new Date().toISOString(), model: 'gemini-2.0-flash-preview-image-generation', fileSize: stats.size, mimeType: 'image/png', }, usage: { lastUsed: new Date().toISOString(), useCount: 1, projects: [], }, }; if (!this.manifest) { this.manifest = { version: '1.0', entries: [] }; } // 既存のエントリを削除して新規追加 this.manifest.entries = this.manifest.entries.filter(e => e.key !== cacheKey); this.manifest.entries.push(entry); await this.saveManifest(); logger.info('Image cached', { cacheKey, path: imagePath, size: stats.size, prompt: params.prompt, }); return imagePath; } /** * キャッシュをクリア */ async clear(options) { if (!this.manifest) { return; } let entriesToRemove = []; if (options?.olderThan) { entriesToRemove = this.manifest.entries.filter(entry => { const lastUsed = new Date(entry.usage.lastUsed); return lastUsed < options.olderThan; }); } else { entriesToRemove = [...this.manifest.entries]; } // ファイルを削除 for (const entry of entriesToRemove) { const imagePath = path.join(this.cacheDir, `${entry.key}.png`); try { await fs.unlink(imagePath); logger.debug('Removed cached image', { key: entry.key }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.warning('Failed to remove cached image', { key: entry.key, error: errorMessage }); } } // マニフェストを更新 if (options?.olderThan) { this.manifest.entries = this.manifest.entries.filter(entry => !entriesToRemove.includes(entry)); } else { this.manifest.entries = []; } await this.saveManifest(); logger.info('Cache cleared', { removed: entriesToRemove.length, remaining: this.manifest.entries.length, }); } /** * キャッシュのサイズを取得 */ async getSize() { if (!this.manifest) { return { totalSize: 0, fileCount: 0 }; } let totalSize = 0; let fileCount = 0; for (const entry of this.manifest.entries) { const imagePath = path.join(this.cacheDir, `${entry.key}.png`); try { const stats = await fs.stat(imagePath); totalSize += stats.size; fileCount++; } catch { // ファイルが存在しない場合は無視 } } return { totalSize, fileCount }; } /** * キャッシュ状況を取得 */ async getStatus() { const { totalSize, fileCount } = await this.getSize(); if (!this.manifest || this.manifest.entries.length === 0) { return { totalFiles: fileCount, totalSize }; } const sortedEntries = [...this.manifest.entries].sort((a, b) => new Date(a.metadata.generatedAt).getTime() - new Date(b.metadata.generatedAt).getTime()); return { totalFiles: fileCount, totalSize, oldestEntry: sortedEntries[0] ? new Date(sortedEntries[0].metadata.generatedAt) : undefined, newestEntry: sortedEntries.length > 0 ? new Date(sortedEntries[sortedEntries.length - 1].metadata.generatedAt) : undefined, }; } /** * マニフェストを読み込む */ async loadManifest() { try { const data = await fs.readFile(this.manifestPath, 'utf-8'); this.manifest = JSON.parse(data); logger.debug('Manifest loaded', { entries: this.manifest.entries.length }); } catch { // マニフェストが存在しない場合は新規作成 this.manifest = { version: '1.0', entries: [] }; logger.debug('Created new manifest'); } } /** * マニフェストを保存 */ async saveManifest() { if (!this.manifest) { return; } await fs.writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), 'utf-8'); } } /** * キャッシュキーを生成 */ export function generateCacheKey(params) { const normalized = JSON.stringify({ prompt: params.prompt, style: params.style || 'photorealistic', aspectRatio: params.aspectRatio || '16:9', seed: params.seed, }, Object.keys(params).sort()); return crypto.createHash('sha256').update(normalized).digest('hex').substring(0, 16); } // シングルトンインスタンス export const imageCache = new ImageCache(); //# sourceMappingURL=image-cache.js.map