@dondonudonjp/vertexai-imagen-mcp-server
Version:
[DEPRECATED] MCP Server for Vertex AI Imagen image generation. Imagen endpoints shut down 2026-06-30; migrate to the nanoBanana MCP Server (Gemini image models).
239 lines • 11.2 kB
JavaScript
/**
* バッチ画像生成処理
*/
import { readFile } from 'fs/promises';
import { batchJobItemToGenerateImageArgs } from './types/batch.js';
import { getDefaultOutputDirectory } from './utils/path.js';
/**
* バッチ処理実行クラス
*/
export class BatchProcessor {
jobManager;
jobDatabase;
outputDir;
constructor(jobManager, jobDatabase, outputDir) {
this.jobManager = jobManager;
this.jobDatabase = jobDatabase;
this.outputDir = outputDir || getDefaultOutputDirectory();
}
/**
* JSONファイルからバッチ設定を読み込む
*/
async loadBatchConfig(filePath) {
const content = await readFile(filePath, 'utf-8');
const config = JSON.parse(content);
// バリデーション
if (!config.jobs || !Array.isArray(config.jobs) || config.jobs.length === 0) {
throw new Error('Invalid batch config: "jobs" array is required and must not be empty');
}
for (let i = 0; i < config.jobs.length; i++) {
const job = config.jobs[i];
if (!job.prompt || typeof job.prompt !== 'string') {
throw new Error(`Invalid batch config: jobs[${i}].prompt is required and must be a string`);
}
}
return config;
}
/**
* バッチ処理を実行
*/
async executeBatch(config) {
const startTime = Date.now();
const jobIds = [];
const outputDir = config.output_dir || this.outputDir;
console.error(`[BATCH] Starting batch processing with ${config.jobs.length} jobs`);
console.error(`[BATCH] Output directory: ${outputDir}`);
// 全てのジョブを投入
for (let i = 0; i < config.jobs.length; i++) {
const item = config.jobs[i];
const args = batchJobItemToGenerateImageArgs(item, outputDir);
console.error(`[BATCH] Queueing job ${i + 1}/${config.jobs.length}: ${item.prompt.substring(0, 50)}...`);
try {
const jobId = this.jobManager.createJob('generate', args);
jobIds.push(jobId);
if (process.env.DEBUG) {
console.error(`[DEBUG] Job created with ID: ${jobId}`);
}
}
catch (error) {
console.error(`[BATCH] Failed to create job ${i + 1}:`, error);
// エラーが発生してもジョブIDに空文字を追加して、インデックスを保持
jobIds.push('');
}
}
console.error(`[BATCH] All jobs queued. Waiting for completion...`);
// ジョブの完了を待つ
const timeout = config.timeout || 600000; // デフォルト10分
const results = await this.waitForJobsCompletion(jobIds, config.jobs, timeout);
const endTime = Date.now();
const result = {
total: config.jobs.length,
succeeded: results.filter(r => r.status === 'completed').length,
failed: results.filter(r => r.status === 'failed').length,
results,
started_at: new Date(startTime).toISOString(),
finished_at: new Date(endTime).toISOString(),
total_duration_ms: endTime - startTime,
};
console.error(`[BATCH] Batch processing completed:`);
console.error(`[BATCH] Total: ${result.total}`);
console.error(`[BATCH] Succeeded: ${result.succeeded}`);
console.error(`[BATCH] Failed: ${result.failed}`);
console.error(`[BATCH] Duration: ${result.total_duration_ms}ms`);
return result;
}
/**
* 全てのジョブが完了するまで待機
*/
async waitForJobsCompletion(jobIds, items, timeout) {
const results = [];
const startTime = Date.now();
const checkInterval = 2000; // 2秒ごとにチェック
// 初期化:全てのジョブを結果配列に追加
for (let i = 0; i < jobIds.length; i++) {
results.push({
job_id: jobIds[i] || 'N/A',
prompt: items[i].prompt,
// ジョブ作成に失敗した場合のみ failed、それ以外は pending で初期化
status: jobIds[i] ? 'pending' : 'failed',
error: jobIds[i] ? undefined : 'Failed to create job',
});
}
if (process.env.DEBUG) {
console.error(`[DEBUG] Waiting for ${jobIds.length} jobs to complete`);
for (let i = 0; i < jobIds.length; i++) {
const job = jobIds[i] ? this.jobDatabase.getJob(jobIds[i]) : null;
console.error(`[DEBUG] Job ${i + 1}: ID=${jobIds[i]}, initial DB status=${job?.status || 'not found'}`);
}
}
while (true) {
// タイムアウトチェック
if (Date.now() - startTime > timeout) {
console.error('[BATCH] Timeout reached. Cancelling remaining jobs...');
for (let i = 0; i < jobIds.length; i++) {
const jobId = jobIds[i];
if (!jobId)
continue;
const result = results[i];
if (result.status !== 'completed' && result.status !== 'failed') {
this.jobManager.cancelJob(jobId);
result.status = 'cancelled';
result.error = 'Timeout';
}
}
break;
}
// 全てのジョブのステータスをチェック
let allCompleted = true;
if (process.env.DEBUG) {
console.error(`[DEBUG] Checking job statuses (elapsed: ${Date.now() - startTime}ms)`);
}
for (let i = 0; i < jobIds.length; i++) {
const jobId = jobIds[i];
if (!jobId)
continue; // スキップ(ジョブ作成失敗)
const result = results[i];
if (result.status === 'completed' || result.status === 'failed' || result.status === 'cancelled') {
continue; // 既に完了
}
const job = this.jobDatabase.getJob(jobId);
if (!job) {
result.status = 'failed';
result.error = 'Job not found';
if (process.env.DEBUG) {
console.error(`[DEBUG] Job ${jobId} not found in database`);
}
continue;
}
if (process.env.DEBUG && (job.status === 'failed' || job.status === 'completed')) {
console.error(`[DEBUG] Job ${jobId} status: ${job.status}, error: "${job.error || 'none'}"`);
}
if (job.status === 'completed') {
result.status = 'completed';
// GenerateJobのresultはoutputPaths配列を持つので、最初の要素を取得
result.output_path = job.result && 'outputPaths' in job.result
? job.result.outputPaths[0]
: undefined;
result.duration_ms = job.completedAt
? job.completedAt.getTime() - job.createdAt.getTime()
: undefined;
if (process.env.DEBUG) {
console.error(`[DEBUG] Job ${jobId} completed successfully. Output: ${result.output_path}`);
}
}
else if (job.status === 'failed') {
result.status = 'failed';
// エラーメッセージを取得(空文字列や未定義の場合はデフォルトメッセージ)
const errorMsg = job.error && job.error.trim() ? job.error : 'Job failed with no error message';
result.error = errorMsg;
result.duration_ms = job.completedAt
? job.completedAt.getTime() - job.createdAt.getTime()
: undefined;
// デバッグ: エラー内容を確認
if (process.env.DEBUG) {
console.error(`[DEBUG] Job ${jobId} failed. DB error field: "${job.error}", Using error: "${result.error}"`);
if (job.completedAt && job.createdAt) {
console.error(`[DEBUG] Duration calc: completedAt=${job.completedAt.toISOString()}, createdAt=${job.createdAt.toISOString()}, duration=${result.duration_ms}ms`);
}
}
}
else {
allCompleted = false; // まだ実行中のジョブがある
}
}
if (allCompleted) {
break; // 全てのジョブが完了
}
// 進行状況を出力
const completed = results.filter(r => r.status === 'completed').length;
const failed = results.filter(r => r.status === 'failed' || r.status === 'cancelled').length;
const pending = results.length - completed - failed;
console.error(`[BATCH] Progress: ${completed} completed, ${failed} failed, ${pending} pending`);
// 待機
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
return results;
}
/**
* バッチ結果をJSON形式で出力
*/
formatResultAsJson(result) {
return JSON.stringify(result, null, 2);
}
/**
* バッチ結果を人間が読める形式で出力
*/
formatResultAsText(result) {
const lines = [];
lines.push('=== Batch Processing Result ===');
lines.push(`Total Jobs: ${result.total}`);
lines.push(`Succeeded: ${result.succeeded}`);
lines.push(`Failed: ${result.failed}`);
lines.push(`Duration: ${result.total_duration_ms}ms`);
lines.push(`Started: ${result.started_at}`);
lines.push(`Finished: ${result.finished_at}`);
lines.push('');
lines.push('=== Individual Results ===');
for (let i = 0; i < result.results.length; i++) {
const r = result.results[i];
lines.push(`\n[${i + 1}/${result.results.length}] ${r.status.toUpperCase()}`);
lines.push(` Job ID: ${r.job_id}`);
lines.push(` Prompt: ${r.prompt}`);
if (r.output_path) {
lines.push(` Output: ${r.output_path}`);
}
if (r.error && r.error.trim()) {
lines.push(` Error: ${r.error}`);
}
else if (r.status === 'failed' || r.status === 'cancelled') {
// ステータスがfailed/cancelledなのにエラーメッセージがない場合
lines.push(` Error: No error message recorded`);
}
if (r.duration_ms !== undefined) {
lines.push(` Duration: ${r.duration_ms}ms`);
}
}
return lines.join('\n');
}
}
//# sourceMappingURL=batch.js.map