FE_Dev / server /services /html-preview /contents /image-prompt.generator.ts
GitHub Actions
Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50
68f7925
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { ImageElement } from './html-image.analyzer';
/**
* 画像生成指示の詳細情報
*/
export interface ImagePromptInstruction {
/** 画像要素のID */
imageId: string;
/** メインプロンプト */
mainPrompt: string;
/** スタイル指示 */
styleInstructions: string;
/** 技術的制約 */
technicalConstraints: string;
/** 完全なFirefly用プロンプト */
fireflyPrompt: string;
/** 推定生成時間(秒) */
estimatedGenerationTime: number;
}
/**
* コンテンツコンテキスト情報
*/
export interface ContentContext {
/** 企業名 */
companyName?: string;
/** サービス/商品名 */
serviceName?: string;
/** 業界・業種 */
industry?: string;
/** ターゲット層 */
targetAudience?: string;
/** ブランドトーン */
brandTone?: string;
/** 全体のテーマ */
overallTheme?: string;
}
/**
* AI画像指示生成サービス
*/
export class ImagePromptGenerator {
private openai?: OpenAI;
private anthropic?: Anthropic;
constructor() {
// OpenAI クライアント初期化
const openaiKey = process.env.OPENAI_TEMPLATE_AI_DEV;
if (openaiKey) {
this.openai = new OpenAI({
apiKey: openaiKey,
});
}
// Claude クライアント初期化
const claudeKey = process.env.CLAUDE_TEMPLATE_AI_DEV;
if (claudeKey) {
this.anthropic = new Anthropic({
apiKey: claudeKey,
});
}
}
/**
* 画像要素配列から画像生成指示を一括生成
*/
async generateImageInstructions(
imageElements: ImageElement[],
contentContext: ContentContext = {},
provider: 'openai' | 'claude' = 'openai',
): Promise<ImagePromptInstruction[]> {
// Starting batch generation
const instructions: ImagePromptInstruction[] = [];
// セクションごとに画像をグループ化
const sectionGroups = new Map<string, { element: ImageElement; index: number }[]>();
imageElements.forEach((element, globalIndex) => {
const sectionKey = `${element.context.sectionType}-${element.context.sectionTitle}`;
if (!sectionGroups.has(sectionKey)) {
sectionGroups.set(sectionKey, []);
}
sectionGroups.get(sectionKey)!.push({ element, index: globalIndex });
});
// 並列処理で画像指示を生成
const promises = imageElements.map(async (element, globalIndex) => {
try {
// セクション内でのインデックスを取得
const sectionKey = `${element.context.sectionType}-${element.context.sectionTitle}`;
const sectionImages = sectionGroups.get(sectionKey) || [];
const sectionIndex = sectionImages.findIndex((img) => img.element.id === element.id);
const instruction = await this.generateSingleImageInstruction(element, contentContext, provider, sectionIndex, sectionImages.length);
return instruction;
} catch (error) {
console.error(`[ImagePromptGenerator] Failed to generate instruction for image ${element.id}:`, error);
// フォールバック指示を生成
return this.generateFallbackInstruction(element, globalIndex);
}
});
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
instructions.push(result.value);
} else {
console.error(`[ImagePromptGenerator] Promise failed for image ${imageElements[index].id}:`, result.reason);
// フォールバック指示を追加
instructions.push(this.generateFallbackInstruction(imageElements[index], index));
}
});
// Generated instructions
return instructions;
}
/**
* 単一画像要素の生成指示を作成
*/
private async generateSingleImageInstruction(
element: ImageElement,
contentContext: ContentContext,
provider: 'openai' | 'claude',
sectionIndex: number = 0,
totalInSection: number = 1,
): Promise<ImagePromptInstruction> {
try {
const prompt = this.buildAnalysisPrompt(element, contentContext, sectionIndex, totalInSection);
let aiResponse: string;
if (provider === 'claude' && this.anthropic) {
aiResponse = await this.generateWithClaude(prompt);
} else if (provider === 'openai' && this.openai) {
aiResponse = await this.generateWithOpenAI(prompt);
} else {
throw new Error(`Provider ${provider} is not available`);
}
return this.parseAIResponse(element.id, aiResponse, sectionIndex);
} catch (error) {
console.error(`[ImagePromptGenerator] Failed to generate instruction for image ${element.id}:`, error);
// フォールバック: デフォルトのインストラクションを返す
return {
imageId: element.id,
mainPrompt: `Professional image for ${element.altText || 'business content'}`,
styleInstructions: 'Modern, clean, professional design with appropriate color scheme',
technicalConstraints: 'Image will be center-cropped to 4:3 aspect ratio (1024x768px), place important elements in central horizontal band',
fireflyPrompt: `Professional business image for section ${sectionIndex + 1}`,
estimatedGenerationTime: 30,
};
}
}
/**
* Claude APIを使用して画像指示を生成
*/
private async generateWithClaude(prompt: string): Promise<string> {
if (!this.anthropic) {
throw new Error('Claude client not initialized');
}
const response = await this.anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1000,
messages: [
{
role: 'user',
content: prompt,
},
],
});
const content = response.content[0];
if (content.type === 'text') {
return content.text;
}
throw new Error('Unexpected response format from Claude');
}
/**
* OpenAI APIを使用して画像指示を生成
*/
private async generateWithOpenAI(prompt: string): Promise<string> {
if (!this.openai) {
throw new Error('OpenAI client not initialized');
}
const response = await this.openai.chat.completions.create({
model: 'gpt-4o',
max_tokens: 1000,
messages: [
{
role: 'user',
content: prompt,
},
],
});
const content = response.choices[0].message.content;
if (!content) {
throw new Error('Empty response from OpenAI');
}
return content;
}
/**
* AI分析用プロンプトを構築
*/
private buildAnalysisPrompt(element: ImageElement, contentContext: ContentContext, sectionIndex: number = 0, totalInSection: number = 1): string {
// セクション内での画像の位置に基づく追加指示
let positionContext = '';
if (totalInSection > 1) {
if (sectionIndex === 0) {
positionContext = '導入・概要を示す画像として、セクションの最初に配置';
} else if (sectionIndex === totalInSection - 1) {
positionContext = 'まとめ・結論を示す画像として、セクションの最後に配置';
} else {
positionContext = `詳細説明用の画像として、セクション内の${sectionIndex + 1}番目に配置`;
}
}
// 画像のバリエーション指示
const variationInstructions = [
'広角で全体像を捉えた構図',
'特定の要素にフォーカスした詳細な構図',
'バランスの取れた中距離からの構図',
'ダイナミックな角度からの印象的な構図',
'俯瞰的な視点からの包括的な構図',
];
const selectedVariation = variationInstructions[sectionIndex % variationInstructions.length];
// ヒーロー画像の場合の特別な構成指示
const heroCompositionRule =
element.context.imageRole === 'main' || element.context.sectionType === 'hero'
? `
# ヒーロー画像の構成ルール(必須)
- 画像上部の空白領域は高さの10%未満に制限
- メインビジュアル要素(人物・オブジェクト・グラフィック)を画像上部から配置
- ヘッダー直下に密度の高い構成で配置
- 無駄な上部余白を避け、インパクトのある配置`
: '';
return `あなたはプロの画像ディレクターです。以下の情報を基に、Adobe Fireflyで生成する画像の詳細な指示を日本語で作成してください。
# 画像要素の情報
- 画像の役割: ${element.context.imageRole}
- セクションタイプ: ${element.context.sectionType}
- セクションタイトル: ${element.context.sectionTitle}
- 画像サイズ: ${element.width}x${element.height}px(注:生成後に上下中央で4:3アスペクト比(1024x768px)にトリミングされます。重要な要素は中央の水平帯に配置してください)
- Alt属性: ${element.altText}
- 周辺コンテキスト: ${element.context.surroundingText}
- セクション内の位置: ${positionContext || '単独の画像'}
- 推奨される構図: ${selectedVariation}${heroCompositionRule}
# コンテンツコンテキスト
- 企業名: ${contentContext.companyName || '未指定'}
- サービス名: ${contentContext.serviceName || '未指定'}
- 業界: ${contentContext.industry || '未指定'}
- ターゲット層: ${contentContext.targetAudience || '未指定'}
- ブランドトーン: ${contentContext.brandTone || '未指定'}
# 出力形式
以下のJSON形式で回答してください:
\`\`\`json
{
"mainPrompt": "画像の主要な内容と構図を具体的に記述",
"styleInstructions": "視覚的スタイル、色調、雰囲気の指示",
"technicalConstraints": "技術的制約やサイズ比率の考慮事項",
"estimatedTime": 30
}
\`\`\`
# 重要な制約
1. 日本語コンテンツに適した画像を想定
2. プロフェッショナルなビジネス向け画像
3. 著作権に配慮したオリジナル要素
4. 人物を含む場合は写真調でリアルな表現とし、特定個人を避ける。人物が登場するシーンでは、イラスト調ではなくプロフェッショナルな写真スタイルで表現
5. ブランドロゴや具体的な商品名は避ける
6. ${element.context.imageRole === 'icon' ? 'シンプルで認識しやすいアイコン的デザイン' : ''}
7. ${element.context.imageRole === 'main' ? 'インパクトがありメインビジュアルとして機能する。上部余白を最小化し、視覚要素を画像上部から開始' : ''}
8. ${element.context.imageRole === 'illustration' ? 'コンテンツを補完する説明的なビジュアル。人物が含まれる場合でも写真調で表現' : ''}
9. 同じセクション内の他の画像と差別化された独自の視点
10. 【重要】画像は上下中央で4:3比率にトリミングされるため、キー要素は中央の水平帯に配置し、上下端は避けること
画像の目的とコンテキストに最適化された、具体的で実行可能な指示を作成してください。必ず「${selectedVariation}」を活用してください。`;
}
/**
* AI応答を解析してImagePromptInstructionに変換
*/
private parseAIResponse(imageId: string, response: string, sectionIndex: number = 0): ImagePromptInstruction {
try {
// JSON部分を抽出 - コードブロックまたは生のJSONを処理
let jsonContent: string;
// まずコードブロック形式を試す
const jsonMatch = response.match(/```json\s*([\s\S]*?)\s*```/);
if (jsonMatch) {
jsonContent = jsonMatch[1];
} else {
// コードブロックがない場合は、JSONオブジェクトを直接探す
const jsonObjectMatch = response.match(/\{[\s\S]*"mainPrompt"[\s\S]*\}/);
if (jsonObjectMatch) {
jsonContent = jsonObjectMatch[0];
} else {
throw new Error('JSON format not found in response');
}
}
// JSON文字列のクリーンアップ処理
// 1. フィールド値内の改行を\\nに置換
jsonContent = jsonContent.replace(/"([^"]*)":\s*"([^"]*)"/g, (match, key, value) => {
// 値内の改行文字を\\nに置換
const cleanedValue = value.replace(/\r\n/g, '\\n').replace(/\r/g, '\\n').replace(/\n/g, '\\n').replace(/\t/g, '\\t');
return `"${key}": "${cleanedValue}"`;
});
// 2. 末尾の余分な文字を除去(例:余分な「」」など)
jsonContent = jsonContent.replace(/(["\]}])[^,\s\]}]*$/gm, '$1');
// 3. 不完全な文字列フィールドを修正
jsonContent = jsonContent.replace(/"([^"]*)":\s*"([^"]*),\s*$/gm, (match, key, value) => {
return `"${key}": "${value}",`;
});
// 4. 最後のフィールドが閉じられていない場合の処理
jsonContent = jsonContent.replace(/"([^"]*)":\s*"([^"]*)\s*$/gm, (match, key, value) => {
return `"${key}": "${value}"`;
});
// 5. 不正な改行や制御文字を含む場合の追加クリーンアップ
const lines = jsonContent.split('\n');
const cleanedLines = lines.map((line) => {
// フィールド値が途中で切れている場合の処理
if (line.includes('": "') && !line.trim().endsWith('",') && !line.trim().endsWith('"')) {
return line + '"';
}
return line;
});
jsonContent = cleanedLines.join('\n');
const parsedResponse = JSON.parse(jsonContent);
const fireflyPrompt = this.buildFireflyPrompt(
parsedResponse.mainPrompt,
parsedResponse.styleInstructions,
parsedResponse.technicalConstraints,
sectionIndex,
imageId,
);
return {
imageId,
mainPrompt: parsedResponse.mainPrompt || '',
styleInstructions: parsedResponse.styleInstructions || '',
technicalConstraints: parsedResponse.technicalConstraints || '',
fireflyPrompt,
estimatedGenerationTime: parsedResponse.estimatedTime || 30,
};
} catch (error) {
console.error(`[ImagePromptGenerator] Failed to parse AI response for ${imageId}:`, error);
console.error('Raw response:', response);
// フォールバック: デフォルト値を返す
console.warn(`[ImagePromptGenerator] Using fallback prompt for ${imageId}`);
return {
imageId,
mainPrompt: 'Professional business image',
styleInstructions: 'Modern, clean design with blue tones',
technicalConstraints: 'Size: 800x400px, landscape orientation',
fireflyPrompt: `Professional business image for section ${sectionIndex + 1}`,
estimatedGenerationTime: 30,
};
}
}
/**
* Firefly用の最終プロンプトを構築
*/
private buildFireflyPrompt(
mainPrompt: string,
styleInstructions: string,
technicalConstraints: string,
sectionIndex: number = 0,
imageId: string = '',
): string {
// 画像の位置に基づくバリエーション
const viewVariations = [
'wide angle perspective',
'detailed close-up view',
'balanced mid-range shot',
'dynamic diagonal composition',
'elevated overview angle',
];
const viewStyle = viewVariations[sectionIndex % viewVariations.length];
// ユニークな要素を追加
const uniqueElement = imageId ? `unique visual element ${imageId.slice(-4)}` : '';
const basePrompt = `${mainPrompt}`;
const styleSection = styleInstructions ? ` Style: ${styleInstructions}, ${viewStyle}` : ` Style: ${viewStyle}`;
const constraintsSection = technicalConstraints ? ` Technical requirements: ${technicalConstraints}` : '';
// ヒーロー画像の場合は上部余白制限を追加
const heroConstraint =
imageId.includes('hero') || imageId.includes('main')
? ', minimal top margin, dense composition from top edge, visual elements starting immediately'
: '';
const fireflyConstraints = ` Professional business photography, high quality, 4K resolution, no identifiable individuals, stock photo aesthetic, clean and modern composition${heroConstraint}, ${uniqueElement}.`;
return `${basePrompt}.${styleSection}.${constraintsSection}.${fireflyConstraints}`;
}
/**
* フォールバック指示を生成(AI生成に失敗した場合)
*/
private generateFallbackInstruction(element: ImageElement, index: number = 0): ImagePromptInstruction {
const roleInstructions = {
main: 'Professional business hero image with minimal top padding, modern corporate design, high impact visual starting from top edge',
icon: 'Simple minimalist icon, clean design, professional style',
illustration: 'Business concept illustration, modern corporate style',
decoration: 'Subtle decorative element, professional background texture',
};
const sectionInstructions = {
problem: 'Business challenge visualization, problem representation',
solution: 'Solution concept, positive business outcome',
features: 'Feature showcase, benefits visualization',
service: 'Service representation, professional offering',
flow: 'Process illustration, step-by-step visualization',
generic: 'Professional business concept, modern corporate design',
};
const mainPrompt = `${roleInstructions[element.context.imageRole as keyof typeof roleInstructions] || roleInstructions.illustration} representing ${sectionInstructions[element.context.sectionType as keyof typeof sectionInstructions] || sectionInstructions.generic}`;
const fireflyPrompt = this.buildFireflyPrompt(
mainPrompt,
'Professional corporate style, modern design',
`Aspect ratio optimized for ${element.width}x${element.height} display`,
index,
element.id,
);
return {
imageId: element.id,
mainPrompt,
styleInstructions: 'Professional corporate style, modern design',
technicalConstraints: `Aspect ratio optimized for ${element.width}x${element.height} display`,
fireflyPrompt,
estimatedGenerationTime: 30,
};
}
/**
* 利用可能なプロバイダーをチェック
*/
getAvailableProviders(): ('openai' | 'claude')[] {
const providers: ('openai' | 'claude')[] = [];
if (this.openai) providers.push('openai');
if (this.anthropic) providers.push('claude');
return providers;
}
}