@henteko/kumiki
Version:
A video generation tool that creates videos from JSON configurations
155 lines • 5.48 kB
JavaScript
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import { getNarrationCacheDir } from '../utils/app-dirs.js';
import { logger } from '../utils/logger.js';
export class NarrationCacheService {
cacheDir;
initialized = false;
constructor(cacheDir) {
this.cacheDir = cacheDir || getNarrationCacheDir();
}
async initialize() {
if (this.initialized)
return;
await this.ensureCacheDir();
this.initialized = true;
}
async ensureCacheDir() {
await fs.mkdir(this.cacheDir, { recursive: true });
}
generateCacheKey(key) {
const content = JSON.stringify({
text: key.text,
voice: key.voice || {},
});
return crypto
.createHash('sha256')
.update(content)
.digest('hex');
}
getCachePath(key) {
const hash = this.generateCacheKey(key);
return path.join(this.cacheDir, `${hash}.wav`);
}
async exists(key) {
const cachePath = this.getCachePath(key);
try {
await fs.access(cachePath);
return true;
}
catch {
return false;
}
}
async get(key) {
const cachePath = this.getCachePath(key);
if (await this.exists(key)) {
logger.debug('Narration cache hit', {
text: key.text.substring(0, 50) + '...',
cachePath
});
return cachePath;
}
logger.debug('Narration cache miss', {
text: key.text.substring(0, 50) + '...',
cachePath
});
return null;
}
async set(key, audioPath) {
await this.ensureCacheDir();
const cachePath = this.getCachePath(key);
// Copy audio file to cache
await fs.copyFile(audioPath, cachePath);
logger.debug('Narration cached', {
text: key.text.substring(0, 50) + '...',
cachePath
});
return cachePath;
}
async clear(options) {
try {
const files = await fs.readdir(this.cacheDir);
const wavFilePaths = files
.filter(file => file.endsWith('.wav'))
.map(file => path.join(this.cacheDir, file));
let filesToDelete = wavFilePaths;
if (options?.olderThan) {
const olderThanMs = options.olderThan;
const statPromises = wavFilePaths.map(async (filePath) => {
const stats = await fs.stat(filePath);
const age = Date.now() - stats.mtimeMs;
return age > olderThanMs ? filePath : null;
});
const results = await Promise.all(statPromises);
filesToDelete = results.filter((file) => file !== null);
}
await Promise.all(filesToDelete.map(filePath => fs.unlink(filePath)));
const clearedCount = filesToDelete.length;
logger.info(`Narration cache cleared: ${clearedCount} files`);
return clearedCount;
}
catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
return 0;
}
}
async getStats() {
try {
const files = await fs.readdir(this.cacheDir);
const wavFiles = files.filter(file => file.endsWith('.wav'));
const statsPromises = wavFiles.map(file => fs.stat(path.join(this.cacheDir, file)));
const statsArray = await Promise.all(statsPromises);
const totalSize = statsArray.reduce((sum, stats) => sum + stats.size, 0);
return { totalFiles: wavFiles.length, totalSize };
}
catch (error) {
if (error.code === 'ENOENT') {
return { totalFiles: 0, totalSize: 0 };
}
throw error;
}
}
async getStatus() {
try {
const files = await fs.readdir(this.cacheDir);
const wavFiles = files.filter(file => file.endsWith('.wav'));
if (wavFiles.length === 0) {
return { totalFiles: 0, totalSize: 0 };
}
const statsPromises = wavFiles.map(file => fs.stat(path.join(this.cacheDir, file)));
const statsArray = await Promise.all(statsPromises);
let totalSize = 0;
let oldestTime = null;
let newestTime = null;
for (const stats of statsArray) {
totalSize += stats.size;
const time = stats.mtimeMs;
if (oldestTime === null || time < oldestTime) {
oldestTime = time;
}
if (newestTime === null || time > newestTime) {
newestTime = time;
}
}
return {
totalFiles: wavFiles.length,
totalSize,
oldestEntry: oldestTime ? new Date(oldestTime) : undefined,
newestEntry: newestTime ? new Date(newestTime) : undefined,
};
}
catch (error) {
if (error.code === 'ENOENT') {
return { totalFiles: 0, totalSize: 0 };
}
throw error;
}
}
}
// Singleton instance
export const narrationCacheService = new NarrationCacheService();
//# sourceMappingURL=narration-cache.js.map