/** * HuggingFace Spaces環境での画像メモリキャッシュ * ファイルシステム書き込み制限を回避するためメモリ内で管理 */ export class ImageMemoryCache { private static instance: ImageMemoryCache; private cache = new Map(); private maxSize = 100 * 1024 * 1024; // 100MB制限 private maxAge = 30 * 60 * 1000; // 30分 private currentSize = 0; private constructor() { // 定期的に古いエントリを削除 setInterval(() => this.cleanup(), 5 * 60 * 1000); // 5分ごと } static getInstance(): ImageMemoryCache { if (!ImageMemoryCache.instance) { ImageMemoryCache.instance = new ImageMemoryCache(); } return ImageMemoryCache.instance; } /** * 画像をキャッシュに保存 */ store(id: string, buffer: Buffer, contentType: string = 'image/webp'): string { const size = buffer.length; // サイズ制限チェック if (this.currentSize + size > this.maxSize) { this.evictOldest(); } this.cache.set(id, { buffer, contentType, createdAt: Date.now(), }); this.currentSize += size; console.log(`[ImageCache] Stored ${id} (${(size / 1024).toFixed(1)}KB), total: ${(this.currentSize / 1024 / 1024).toFixed(1)}MB`); // APIエンドポイント経由のURL返却 return `/api/cached-images/${id}`; } /** * 画像を取得 */ get(id: string): { buffer: Buffer; contentType: string } | null { const entry = this.cache.get(id); if (!entry) return null; // 有効期限チェック if (Date.now() - entry.createdAt > this.maxAge) { this.remove(id); return null; } return { buffer: entry.buffer, contentType: entry.contentType, }; } /** * 画像を削除 */ private remove(id: string): void { const entry = this.cache.get(id); if (entry) { this.currentSize -= entry.buffer.length; this.cache.delete(id); } } /** * 最古のエントリを削除 */ private evictOldest(): void { let oldest: { id: string; createdAt: number } | null = null; for (const [id, entry] of this.cache.entries()) { if (!oldest || entry.createdAt < oldest.createdAt) { oldest = { id, createdAt: entry.createdAt }; } } if (oldest) { console.log(`[ImageCache] Evicting oldest entry: ${oldest.id}`); this.remove(oldest.id); } } /** * 期限切れエントリをクリーンアップ */ private cleanup(): void { const now = Date.now(); const expired: string[] = []; for (const [id, entry] of this.cache.entries()) { if (now - entry.createdAt > this.maxAge) { expired.push(id); } } if (expired.length > 0) { console.log(`[ImageCache] Cleaning up ${expired.length} expired entries`); expired.forEach((id) => this.remove(id)); } } /** * キャッシュ状態を取得 */ getStats() { return { entries: this.cache.size, sizeBytes: this.currentSize, sizeMB: (this.currentSize / 1024 / 1024).toFixed(2), maxSizeMB: (this.maxSize / 1024 / 1024).toFixed(2), }; } }