FE_Test / server /services /html-preview /contents /firefly.client.ts
GitHub Actions
Deploy from GitHub Actions [test] - 2025-10-31 10:18:25
5f2aab6
export interface FireflyImageRequest {
prompt: string;
width?: number;
height?: number;
seed?: number;
num_variations?: number;
}
export interface FireflyImageResponse {
outputs: Array<{
image: {
id: string;
presignedUrl: string;
};
seed: number;
}>;
}
export class FireflyClient {
private clientId: string;
private clientSecret: string;
private accessToken?: string;
private tokenExpiry?: number;
private baseUrl = 'https://firefly-api.adobe.io/v2/images/generate';
private tokenUrl = 'https://ims-na1.adobelogin.com/ims/token/v3';
constructor(clientId: string, clientSecret: string) {
this.clientId = clientId;
this.clientSecret = clientSecret;
}
// 既存のアクセストークンを使用する場合のコンストラクタ(テスト用)
static withAccessToken(clientId: string, accessToken: string): FireflyClient {
const client = new FireflyClient(clientId, '');
client.accessToken = accessToken;
client.tokenExpiry = Date.now() + 24 * 60 * 60 * 1000; // 24時間後
return client;
}
private async getAccessToken(): Promise<string> {
// トークンが有効ならそのまま使用
if (this.accessToken && this.tokenExpiry && Date.now() < this.tokenExpiry) {
// Using cached access token
return this.accessToken;
}
// Fetching new access token
try {
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
scope: 'openid,AdobeID,firefly_api,ff_apis',
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error('[FireflyClient] Token request failed:', {
status: response.status,
statusText: response.statusText,
errorText,
});
throw new Error(`Adobe IMS token error: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
this.accessToken = data.access_token;
this.tokenExpiry = Date.now() + data.expires_in * 1000 - 60000; // 1分前に期限切れとして扱う
if (!this.accessToken) {
console.error('[FireflyClient] No access token in response:', data);
throw new Error('Failed to get access token');
}
// New access token obtained
return this.accessToken;
} catch (error) {
console.error('[FireflyClient] Failed to get access token:', error);
console.error('[FireflyClient] Error details:', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
}
async generateImage(request: FireflyImageRequest): Promise<string> {
// Starting image generation with retry logic for quota errors
return this.generateImageWithRetry(request, 0);
}
private async generateImageWithRetry(request: FireflyImageRequest, retryCount: number): Promise<string> {
const maxRetries = 2; // 最大2回リトライ (合計3回試行)
let accessToken: string;
try {
accessToken = await this.getAccessToken();
// Access token obtained
} catch (error) {
console.error('[FireflyClient] Failed to get access token for image generation:', error);
throw error;
}
// プロンプトの文字数制限チェック
const truncatedPrompt = this.truncatePrompt(request.prompt, 1024);
const requestBody = {
prompt: truncatedPrompt,
size: {
width: request.width || 800,
height: request.height || 600,
},
seeds: request.seed ? [request.seed] : undefined,
num_variations: request.num_variations || 1,
};
// Sending request to Firefly API
let response: Response;
try {
response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-API-Key': this.clientId,
},
body: JSON.stringify(requestBody),
});
} catch (error) {
console.error('[FireflyClient] Network error during API call:', error);
throw new Error(`Network error: ${error instanceof Error ? error.message : String(error)}`);
}
// Response received
if (!response.ok) {
const errorText = await response.text();
// 429 (Too Many Requests) エラーの場合の特別処理
if (response.status === 429) {
console.warn(`[FireflyClient] Quota exceeded (429), attempt ${retryCount + 1}/${maxRetries + 1}:`, {
status: response.status,
statusText: response.statusText,
error: errorText,
});
if (retryCount < maxRetries) {
// アクセストークンを強制的に無効化して再生成
console.log('[FireflyClient] Forcing access token regeneration due to quota error');
this.accessToken = undefined;
this.tokenExpiry = undefined;
// 指数バックオフでリトライ (1秒, 2秒, 4秒...)
const delayMs = Math.pow(2, retryCount) * 1000;
console.log(`[FireflyClient] Retrying after ${delayMs}ms delay...`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return this.generateImageWithRetry(request, retryCount + 1);
} else {
console.error('[FireflyClient] Max retries exceeded for quota error');
throw new Error(`Adobe Firefly quota exceeded. Please try again later. Error: ${response.status} ${response.statusText} - ${errorText}`);
}
}
// その他のエラー
console.error('Firefly APIエラー:', {
status: response.status,
statusText: response.statusText,
error: errorText,
});
throw new Error(`Firefly API error: ${response.status} ${response.statusText} - ${errorText}`);
}
const data: FireflyImageResponse = await response.json();
// Response data received
if (!data.outputs || data.outputs.length === 0) {
console.error('[FireflyClient] No images in response');
throw new Error('No images generated');
}
const imageUrl = data.outputs[0].image.presignedUrl;
if (retryCount > 0) {
console.log(`[FireflyClient] Image generation succeeded after ${retryCount} retries`);
}
// Image generated successfully
return imageUrl;
}
async generateImageForContext(context: string, altText: string, width: number = 800, height: number = 600): Promise<string> {
// コンテキストとaltTextから適切なプロンプトを生成
const prompt = this.buildPromptFromContext(context, altText);
return this.generateImage({
prompt,
width,
height,
});
}
private buildPromptFromContext(context: string, altText: string): string {
// コンテキストに基づいて適切なプロンプトを構築
const contextLower = context.toLowerCase();
// 基本的なプロンプト構築ロジック
let prompt = '';
// 日本語キーワードと英語キーワードの両方をチェック
if (contextLower.includes('ニュース') || contextLower.includes('更新情報') || contextLower.includes('news')) {
prompt = `Modern news announcement illustration, corporate updates, clean business design, ${altText}`;
} else if (contextLower.includes('問題提起') || contextLower.includes('共感') || contextLower.includes('問題') || contextLower.includes('課題')) {
prompt = `Business challenge illustration, problem visualization, professional concern representation, ${altText}`;
} else if (
contextLower.includes('解決策') ||
contextLower.includes('ベネフィット') ||
contextLower.includes('解決') ||
contextLower.includes('改善')
) {
prompt = `Solution concept illustration, positive business outcome, success visualization, ${altText}`;
} else if (
contextLower.includes('ai') ||
contextLower.includes('テクノロジー') ||
contextLower.includes('技術') ||
contextLower.includes('innovation')
) {
prompt = `AI technology illustration, futuristic business concept, digital transformation, ${altText}`;
} else if (contextLower.includes('商品') || contextLower.includes('サービス') || contextLower.includes('特徴')) {
prompt = `Professional product showcase, service features visualization, clean commercial photography, ${altText}`;
} else if (contextLower.includes('成長') || contextLower.includes('加速') || contextLower.includes('ハーモニー')) {
prompt = `Business growth visualization, upward trend illustration, harmony and collaboration concept, ${altText}`;
} else if (contextLower.includes('フロー') || contextLower.includes('ステップ') || contextLower.includes('流れ')) {
prompt = `Step-by-step process illustration, workflow diagram, clear infographic style, ${altText}`;
} else if (
contextLower.includes('business') ||
contextLower.includes('meeting') ||
contextLower.includes('team') ||
contextLower.includes('ビジネス')
) {
prompt = `Professional business concept, modern office environment, collaborative workspace atmosphere, ${altText}`;
} else {
prompt = `Professional business illustration, modern corporate design, clean and sophisticated, ${altText}`;
}
const fullPrompt = `${prompt}, professional corporate style, no identifiable individuals, stock photo aesthetic, high quality, 4K resolution`;
// Firefly APIの1024文字制限に対応
return this.truncatePrompt(fullPrompt, 1024);
}
private truncatePrompt(prompt: string, maxLength: number): string {
if (prompt.length <= maxLength) {
return prompt;
}
// 安全マージンを設ける(文字切れを防ぐため)
const safeLength = maxLength - 50;
// 単語境界で切る
const truncated = prompt.substring(0, safeLength);
const lastSpaceIndex = truncated.lastIndexOf(' ');
if (lastSpaceIndex > safeLength * 0.8) {
return truncated.substring(0, lastSpaceIndex) + '...';
}
return truncated + '...';
}
private extractKeywords(text: string): string[] {
// 簡単なキーワード抽出
return text
.toLowerCase()
.split(/[\s!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]+/)
.filter((word) => word.length > 1);
}
}