Spaces:
Sleeping
Sleeping
| 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); | |
| } | |
| } | |