diff --git a/lib/services/gemini_service.dart b/lib/services/gemini_service.dart index 100de5c..357b2de 100644 --- a/lib/services/gemini_service.dart +++ b/lib/services/gemini_service.dart @@ -261,14 +261,9 @@ $extractedText throw Exception('Gemini API Key is missing. Please set GEMINI_API_KEY.'); } - final model = GenerativeModel( - model: 'gemini-2.5-flash', // ⚠️ FIXED MODEL NAME - DO NOT CHANGE without explicit user approval (confirmed working on 2026-01-17) - apiKey: apiKey, - generationConfig: GenerationConfig( - responseMimeType: 'application/json', - temperature: 0.2, // チャート一貫性向上のため 0.4→0.2 に変更 (2026-02-09) - ), - ); + // モデル候補: 503/UNAVAILABLE 時にフォールバック + const primaryModel = 'gemini-2.5-flash'; // ⚠️ FIXED - confirmed 2026-01-17 + const fallbackModel = 'gemini-2.0-flash'; // 503 連続時のフォールバック // Prepare Prompt final promptText = customPrompt ?? ''' @@ -296,37 +291,74 @@ $extractedText 値が不明な場合は null または 適切な推測値を入れてください。 '''; - // Prepare Content + // Prepare Content parts (画像バイト読み込みは一度だけ) final contentParts = [TextPart(promptText)]; for (var path in imagePaths) { - // 撮影時に圧縮済み final bytes = await File(path).readAsBytes(); contentParts.add(DataPart('image/jpeg', bytes)); } - try { - final response = await model.generateContent([Content.multi(contentParts)]); + // 503 時: リトライ(指数バックオフ)→ フォールバックモデル + const maxRetries = 3; + final modelsToTry = [primaryModel, primaryModel, primaryModel, fallbackModel]; - final jsonString = response.text; - if (jsonString == null) throw Exception('Empty response from Gemini'); + for (int attempt = 0; attempt <= maxRetries; attempt++) { + final modelName = modelsToTry[attempt]; + final isLastAttempt = attempt == maxRetries; - final jsonMap = jsonDecode(jsonString); - final result = SakeAnalysisResult.fromJson(jsonMap); + try { + if (attempt > 0) { + final waitSec = attempt == maxRetries ? 2 : (attempt * 3); + debugPrint('Retry $attempt/$maxRetries (model: $modelName, wait: ${waitSec}s)...'); + await Future.delayed(Duration(seconds: waitSec)); + } - // 3. キャッシュに保存(次回は即座に返せる) - if (imagePaths.isNotEmpty) { - final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths); - await AnalysisCacheService.saveCache(imageHash, result); - // 4. 銘柄名インデックスに登録(v1.0.15: チャート一貫性向上) - await AnalysisCacheService.registerBrandIndex(result.name, imageHash); + final model = GenerativeModel( + model: modelName, + apiKey: apiKey, + generationConfig: GenerationConfig( + responseMimeType: 'application/json', + temperature: 0.2, + ), + ); + + final response = await model.generateContent([Content.multi(contentParts)]); + + final jsonString = response.text; + if (jsonString == null) throw Exception('Empty response from Gemini'); + + final jsonMap = jsonDecode(jsonString); + final result = SakeAnalysisResult.fromJson(jsonMap); + + // 3. キャッシュに保存(次回は即座に返せる) + if (imagePaths.isNotEmpty) { + final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths); + await AnalysisCacheService.saveCache(imageHash, result); + // 4. 銘柄名インデックスに登録(v1.0.15: チャート一貫性向上) + await AnalysisCacheService.registerBrandIndex(result.name, imageHash); + } + + if (attempt > 0) debugPrint('Succeeded on attempt $attempt (model: $modelName)'); + return result; + + } catch (e) { + final errStr = e.toString(); + final is503 = errStr.contains('503') || errStr.contains('UNAVAILABLE') || errStr.contains('high demand'); + debugPrint('Direct API Error (attempt $attempt, model: $modelName): $e'); + + if (isLastAttempt || !is503) { + // 最終試行 or 503以外のエラーはそのまま投げる + if (is503) { + throw Exception('AIサーバーが混雑しています。しばらく待ってから再試行してください。'); + } + throw Exception('AI解析エラー(Direct): $e'); + } + // 503 → 次のリトライへ } - - return result; - - } catch (e) { - debugPrint('Direct API Error: $e'); - throw Exception('AI解析エラー(Direct): $e'); } + + // ここには到達しない + throw Exception('AI解析に失敗しました。再試行してください。'); } }