From 582553ccfade915818b180f6b3337a8f14d8297a Mon Sep 17 00:00:00 2001 From: Ponshu Developer Date: Thu, 23 Apr 2026 13:07:55 +0900 Subject: [PATCH] =?UTF-8?q?fix(ai):=20=E5=86=8D=E8=A7=A3=E6=9E=90=E3=82=92?= =?UTF-8?q?=E5=B0=82=E7=94=A8=E3=83=97=E3=83=AD=E3=83=B3=E3=83=97=E3=83=88?= =?UTF-8?q?+temperature=3D0.3=E3=81=AB=E5=A4=89=E6=9B=B4=EF=BC=88=E6=9D=B1?= =?UTF-8?q?=E9=AD=81hallucination=E5=AF=BE=E7=AD=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reanalyzeSakeLabel() を新設: 前回のname/brandを渡し「本当に正しいか?」と問い直す - 通常解析(temperature=0)と再解析(temperature=0.3)を分離 → 同じ画像で毎回同じ誤答を返す問題を解消 - _callDirectApi に temperature パラメータを追加 --- lib/screens/sake_detail_screen.dart | 8 +++- lib/services/gemini_service.dart | 72 ++++++++++++++++++++++++++++- pubspec.yaml | 2 +- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/lib/screens/sake_detail_screen.dart b/lib/screens/sake_detail_screen.dart index 97ee64f..16ab161 100644 --- a/lib/screens/sake_detail_screen.dart +++ b/lib/screens/sake_detail_screen.dart @@ -345,7 +345,13 @@ class _SakeDetailScreenState extends ConsumerState { showDialog(context: context, barrierDismissible: false, builder: (context) => const AnalyzingDialog()); final geminiService = ref.read(geminiServiceProvider); - final result = await geminiService.analyzeSakeLabel(existingPaths, forceRefresh: true); + // 再解析専用メソッド: 前回の name/brand を渡してモデルに再考させる + // temperature=0.3 で非決定論的にすることで hallucination の繰り返しを防ぐ + final result = await geminiService.reanalyzeSakeLabel( + existingPaths, + previousName: _sake.displayData.displayName, + previousBrand: _sake.displayData.displayBrewery, + ); final newItem = _sake.copyWith( name: result.name ?? _sake.displayData.displayName, diff --git a/lib/services/gemini_service.dart b/lib/services/gemini_service.dart index 7d7870b..dfc3eed 100644 --- a/lib/services/gemini_service.dart +++ b/lib/services/gemini_service.dart @@ -93,6 +93,74 @@ name・brand を出力する直前に以下を確認してください: ); } + /// 再解析専用メソッド: 前回の結果を「疑わしい」として渡し、モデルに再考させる + /// + /// 通常の analyzeSakeLabel と異なる点: + /// - 前回の name/brand を明示的に伝え「本当にこれが正しいか?」と問い直す + /// - temperature=0.3 で確定論的でなくする(同じ入力でも違う出力の余地を作る) + /// - これにより 東魁→東魁盛 のような hallucination が再解析でも繰り返されにくくなる + Future reanalyzeSakeLabel( + List imagePaths, { + String? previousName, + String? previousBrand, + }) async { + final prevNameStr = previousName != null ? '「$previousName」' : '不明'; + final prevBrandStr = previousBrand != null ? '「$previousBrand」' : '不明'; + + final challengePrompt = ''' +【再解析モード — 前回の回答を検証してください】 + +前回の解析では以下の結果が返されました: +- name(銘柄名): $prevNameStr +- brand(蔵元名): $prevBrandStr + +この回答をユーザーが確認し、誤りの可能性があると指摘しました。 +添付画像を最初から丁寧に見直してください。 + +## 【必須確認ステップ】 +1. ラベル内の文字を1文字ずつ目で追ってください +2. 前回の name=$prevNameStr の各漢字がラベルに実際に存在するか確認してください +3. 存在しない文字が含まれていれば、ラベルに見えている文字のみに修正してください +4. 「ラベルに N 文字しか見えないなら N 文字のみ返す」を厳守してください + +## 【絶対ルール】name・brand・prefectureの読み取り(OCR厳守) +- ラベルに印刷されている文字だけを一字一句そのまま出力してください +- あなたが知っている「正式名称」への変換・補完は禁止 +- 【禁止例】「東魁」→「東魁盛」禁止 / 「男山」→「男山本醸造」禁止 +- ラベルに都道府県名がなければ prefecture は null(推測禁止) + +## その他のフィールド(推定可) +ラベル情報+日本酒の一般知識を使って推定してください。 + +## 出力形式 +以下のJSONのみ返す(説明文不要): +{ + "name": "ラベルに写っている銘柄名(補完禁止)", + "brand": "ラベルに写っている蔵元名(補完禁止)", + "prefecture": "ラベルに書かれた都道府県名(なければnull)", + "type": "特定名称(なければnull)", + "description": "説明文(100文字程度)", + "catchCopy": "20文字以内のキャッチコピー", + "confidenceScore": 80, + "flavorTags": ["フルーティー", "辛口"], + "tasteStats": {"aroma":3,"sweetness":3,"acidity":3,"bitterness":3,"body":3}, + "alcoholContent": 15.0, + "polishingRatio": 50, + "sakeMeterValue": 3.0, + "riceVariety": null, + "yeast": null, + "manufacturingYearMonth": null +} +'''; + + return _callDirectApi( + imagePaths, + challengePrompt, + forceRefresh: true, + temperature: 0.3, + ); + } + /// 共通実装: ProxyへのAPIコール Future _callProxyApi({ required List imagePaths, @@ -215,7 +283,7 @@ name・brand を出力する直前に以下を確認してください: } /// Direct Cloud API Implementation (No Proxy) - Future _callDirectApi(List imagePaths, String? customPrompt, {bool forceRefresh = false}) async { + Future _callDirectApi(List imagePaths, String? customPrompt, {bool forceRefresh = false, double temperature = 0}) async { // 1. キャッシュチェック(同じ画像なら即座に返す) // forceRefresh=trueの場合はキャッシュをスキップ if (!forceRefresh && imagePaths.isNotEmpty) { @@ -316,7 +384,7 @@ name・brand を出力する直前に以下を確認してください: ), generationConfig: GenerationConfig( responseMimeType: 'application/json', - temperature: 0, + temperature: temperature, ), ); diff --git a/pubspec.yaml b/pubspec.yaml index a52d7be..38b79b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.46+53 +version: 1.0.47+54 environment: sdk: ^3.10.1