fix: address code review findings - crash fixes, proxy OCR accuracy, lint cleanup

- tools/proxy/server.js: add systemInstruction + temperature 0 (fixes brand name hallucination e.g. Tokai->Tokaisou)
- gemini_service.dart: add cache read/write to proxy path (was missing, cache was dead code in production)
- camera_screen.dart: guard cameras.first crash when no camera available, add mounted checks in gallery loop
- sake_detail_screen.dart: remove unused gemini_service import, add ignore comment for showDialog context lint
- sake_basic_info_section.dart: remove redundant null-assert operators flagged by dart analyze
- dev_menu_screen.dart: remove unused gemini_service import
- 6 service files: remove emoji from log strings (project rule compliance, 60+ instances)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ponshu Developer 2026-04-15 13:30:00 +09:00
parent b3e1f5d0a3
commit 69b446ee17
11 changed files with 682 additions and 634 deletions

View File

@ -9,7 +9,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:gal/gal.dart'; import 'package:gal/gal.dart';
import '../services/gemini_service.dart';
import '../providers/gemini_provider.dart'; import '../providers/gemini_provider.dart';
import '../services/gemini_exceptions.dart'; import '../services/gemini_exceptions.dart';
import '../services/image_compression_service.dart'; import '../services/image_compression_service.dart';
@ -43,6 +42,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
Future<void>? _initializeControllerFuture; Future<void>? _initializeControllerFuture;
bool _isTakingPicture = false; bool _isTakingPicture = false;
DateTime? _quotaLockoutTime; DateTime? _quotaLockoutTime;
String? _cameraError;
double _minZoom = 1.0; double _minZoom = 1.0;
double _maxZoom = 1.0; double _maxZoom = 1.0;
@ -66,6 +66,12 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
Future<void> _initializeCamera() async { Future<void> _initializeCamera() async {
final cameras = await availableCameras(); final cameras = await availableCameras();
if (cameras.isEmpty) {
if (mounted) {
setState(() => _cameraError = 'カメラが見つかりません。カメラのアクセス権限を確認してください。');
}
return;
}
final firstCamera = cameras.first; final firstCamera = cameras.first;
_controller = CameraController( _controller = CameraController(
@ -288,6 +294,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
); );
// 3. Add compressed permanent path to capture list // 3. Add compressed permanent path to capture list
if (!mounted) return;
setState(() { setState(() {
_capturedImages.add(compressedPath); _capturedImages.add(compressedPath);
}); });
@ -296,6 +303,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
} catch (e) { } catch (e) {
debugPrint('Gallery image compression error: $e'); debugPrint('Gallery image compression error: $e');
// Fallback: Use original path (legacy behavior) // Fallback: Use original path (legacy behavior)
if (!mounted) return;
setState(() { setState(() {
_capturedImages.add(img.path); _capturedImages.add(img.path);
}); });
@ -626,6 +634,23 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_cameraError != null) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(backgroundColor: Colors.black, foregroundColor: Colors.white),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
_cameraError!,
style: const TextStyle(color: Colors.white70),
textAlign: TextAlign.center,
),
),
),
);
}
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: FutureBuilder<void>( body: FutureBuilder<void>(

View File

@ -6,7 +6,6 @@ import '../services/analysis_cache_service.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import '../models/sake_item.dart'; import '../models/sake_item.dart';
import '../providers/sake_list_provider.dart'; import '../providers/sake_list_provider.dart';
import '../services/gemini_service.dart';
import '../providers/gemini_provider.dart'; import '../providers/gemini_provider.dart';
class DevMenuScreen extends ConsumerWidget { class DevMenuScreen extends ConsumerWidget {

View File

@ -91,11 +91,11 @@ class SakeBasicInfoSection extends ConsumerWidget {
), ),
if (showMbti) if (showMbti)
GestureDetector( GestureDetector(
onTap: () => onTapMbtiCompatibility(context, mbtiResult!, appColors), onTap: () => onTapMbtiCompatibility(context, mbtiResult, appColors),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: badgeColor!.withValues(alpha: 0.1), color: badgeColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: badgeColor.withValues(alpha: 0.4)), border: Border.all(color: badgeColor.withValues(alpha: 0.4)),
), ),

View File

@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart'; import 'package:lucide_icons/lucide_icons.dart';
import '../models/sake_item.dart'; import '../models/sake_item.dart';
import '../services/gemini_service.dart';
import '../providers/gemini_provider.dart'; import '../providers/gemini_provider.dart';
import '../services/sake_recommendation_service.dart'; import '../services/sake_recommendation_service.dart';
import '../widgets/analyzing_dialog.dart'; import '../widgets/analyzing_dialog.dart';
@ -346,6 +345,8 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
setState(() => _isAnalyzing = true); setState(() => _isAnalyzing = true);
try { try {
// ignore: use_build_context_synchronously
// mounted 334 await
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@ -552,10 +553,10 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
final imageFile = File(imagePath); final imageFile = File(imagePath);
if (await imageFile.exists()) { if (await imageFile.exists()) {
await imageFile.delete(); await imageFile.delete();
debugPrint('🗑️ Deleted image file: $imagePath'); debugPrint(' Deleted image file: $imagePath');
} }
} catch (e) { } catch (e) {
debugPrint('⚠️ Failed to delete image file: $imagePath - $e'); debugPrint(' Failed to delete image file: $imagePath - $e');
} }
} }

View File

@ -33,7 +33,7 @@ class DraftService {
try { try {
final box = Hive.box<SakeItem>('sake_items'); final box = Hive.box<SakeItem>('sake_items');
// 🔧 FIX: draftPhotoPathにimagePathsに保存 // FIX: draftPhotoPathにimagePathsに保存
final firstPhotoPath = photoPaths.isNotEmpty ? photoPaths.first : ''; final firstPhotoPath = photoPaths.isNotEmpty ? photoPaths.first : '';
// Draft用の仮データを作成 // Draft用の仮データを作成
@ -45,7 +45,7 @@ class DraftService {
name: '解析待ち', name: '解析待ち',
brewery: '---', brewery: '---',
prefecture: '---', prefecture: '---',
imagePaths: photoPaths, // 🔧 FIX: imagePaths: photoPaths, // FIX:
rating: null, rating: null,
), ),
hiddenSpecs: HiddenSpecs( hiddenSpecs: HiddenSpecs(
@ -149,7 +149,7 @@ class DraftService {
final result = await geminiService.analyzeSakeLabel(pathsToAnalyze); final result = await geminiService.analyzeSakeLabel(pathsToAnalyze);
debugPrint(' Analysis completed: ${result.name}'); debugPrint(' Analysis completed: ${result.name}');
// Draftを正式なアイテムに更新 // Draftを正式なアイテムに更新
final updatedItem = item.copyWith( final updatedItem = item.copyWith(
@ -176,7 +176,7 @@ class DraftService {
// await updatedItem.save(); // Error: This object is currently not in a box. // await updatedItem.save(); // Error: This object is currently not in a box.
await box.put(itemKey, updatedItem); await box.put(itemKey, updatedItem);
debugPrint('💾 Draft updated to normal item: $itemKey'); debugPrint(' Draft updated to normal item: $itemKey');
return result; return result;
} }

View File

@ -76,8 +76,18 @@ class GeminiService {
return _callDirectApi(imagePaths, customPrompt, forceRefresh: forceRefresh); return _callDirectApi(imagePaths, customPrompt, forceRefresh: forceRefresh);
} }
// 1. forceRefresh=false
if (!forceRefresh && imagePaths.isNotEmpty) {
final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths);
final cached = await AnalysisCacheService.getCached(imageHash);
if (cached != null) {
debugPrint('Proxy cache hit: skipping API call');
return cached;
}
}
try { try {
// 1. () // 2. ()
if (_lastApiCallTime != null) { if (_lastApiCallTime != null) {
final elapsed = DateTime.now().difference(_lastApiCallTime!); final elapsed = DateTime.now().difference(_lastApiCallTime!);
if (elapsed < _minApiInterval) { if (elapsed < _minApiInterval) {
@ -150,11 +160,20 @@ class GeminiService {
if (missing.isNotEmpty) { if (missing.isNotEmpty) {
debugPrint('WARNING: AI response missing keys: $missing. Old schema?'); debugPrint('WARNING: AI response missing keys: $missing. Old schema?');
// We could throw here, but for now let's just log.
// In strict mode, we might want to fail the analysis to force retry.
} }
} }
// API不使用
if (imagePaths.isNotEmpty) {
final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths);
await AnalysisCacheService.saveCache(imageHash, result);
await AnalysisCacheService.registerBrandIndex(
result.name,
imageHash,
forceUpdate: forceRefresh,
);
}
return result; return result;
} else { } else {
// Proxy側での論理エラー () // Proxy側での論理エラー ()

View File

@ -5,7 +5,7 @@ import 'package:path_provider/path_provider.dart';
import '../models/sake_item.dart'; import '../models/sake_item.dart';
import 'image_compression_service.dart'; import 'image_compression_service.dart';
// Critical Fix (Day 5.5): cleanupTempFiles() // Critical Fix (Day 5.5): cleanupTempFiles()
// : getApplicationDocumentsDirectory() _compressed, _gallery // : getApplicationDocumentsDirectory() _compressed, _gallery
// : // :
// : getTemporaryDirectory() // : getTemporaryDirectory()
@ -52,7 +52,7 @@ class ImageBatchCompressionService {
try { try {
// //
if (!await file.exists()) { if (!await file.exists()) {
debugPrint('⚠️ File not found: $originalPath'); debugPrint(' File not found: $originalPath');
newPaths.add(originalPath); // newPaths.add(originalPath); //
failedCount++; failedCount++;
continue; continue;
@ -67,14 +67,14 @@ class ImageBatchCompressionService {
// //
if (originalSize < 500 * 1024) { // 500KB以下なら既に圧縮済み if (originalSize < 500 * 1024) { // 500KB以下なら既に圧縮済み
debugPrint(' Already compressed: $fileName (${(originalSize / 1024).toStringAsFixed(1)}KB)'); debugPrint(' Already compressed: $fileName (${(originalSize / 1024).toStringAsFixed(1)}KB)');
newPaths.add(originalPath); newPaths.add(originalPath);
successCount++; successCount++;
continue; continue;
} }
// Day 5: // Day 5:
debugPrint('🗜️ Compressing: $fileName (${(originalSize / 1024 / 1024).toStringAsFixed(1)}MB)'); debugPrint(' Compressing: $fileName (${(originalSize / 1024 / 1024).toStringAsFixed(1)}MB)');
// 1. targetPathを指定しない // 1. targetPathを指定しない
final tempCompressedPath = await ImageCompressionService.compressForGemini(originalPath); final tempCompressedPath = await ImageCompressionService.compressForGemini(originalPath);
@ -84,14 +84,14 @@ class ImageBatchCompressionService {
final saved = originalSize - compressedSize; final saved = originalSize - compressedSize;
savedBytes += saved; savedBytes += saved;
debugPrint(' Compressed: $fileName - ${(originalSize / 1024).toStringAsFixed(1)}KB → ${(compressedSize / 1024).toStringAsFixed(1)}KB (${(saved / 1024).toStringAsFixed(1)}KB saved)'); debugPrint(' Compressed: $fileName - ${(originalSize / 1024).toStringAsFixed(1)}KB → ${(compressedSize / 1024).toStringAsFixed(1)}KB (${(saved / 1024).toStringAsFixed(1)}KB saved)');
// 3. // 3.
try { try {
await file.delete(); await file.delete();
debugPrint('🗑️ Deleted original: $originalPath'); debugPrint(' Deleted original: $originalPath');
} catch (e) { } catch (e) {
debugPrint('⚠️ Failed to delete original: $e'); debugPrint(' Failed to delete original: $e');
// //
await File(tempCompressedPath).delete(); await File(tempCompressedPath).delete();
newPaths.add(originalPath); newPaths.add(originalPath);
@ -102,9 +102,9 @@ class ImageBatchCompressionService {
// 4. // 4.
try { try {
await File(tempCompressedPath).rename(originalPath); await File(tempCompressedPath).rename(originalPath);
debugPrint('📦 Moved compressed file to: $originalPath'); debugPrint(' Moved compressed file to: $originalPath');
} catch (e) { } catch (e) {
debugPrint('⚠️ Failed to rename file: $e'); debugPrint(' Failed to rename file: $e');
// 使 // 使
newPaths.add(tempCompressedPath); newPaths.add(tempCompressedPath);
failedCount++; failedCount++;
@ -115,7 +115,7 @@ class ImageBatchCompressionService {
successCount++; successCount++;
} catch (e) { } catch (e) {
debugPrint(' Failed to compress: $originalPath - $e'); debugPrint(' Failed to compress: $originalPath - $e');
newPaths.add(originalPath); // newPaths.add(originalPath); //
failedCount++; failedCount++;
} }
@ -158,12 +158,12 @@ class ImageBatchCompressionService {
/// ///
/// ///
/// 🔒 Safe: getTemporaryDirectory() /// Safe: getTemporaryDirectory()
/// ///
/// : (, ) /// : (, )
static Future<(int, int)> cleanupTempFiles() async { static Future<(int, int)> cleanupTempFiles() async {
try { try {
// : getTemporaryDirectory() 使getApplicationDocumentsDirectory() // : getTemporaryDirectory() 使getApplicationDocumentsDirectory()
final directory = await getTemporaryDirectory(); final directory = await getTemporaryDirectory();
final dir = Directory(directory.path); final dir = Directory(directory.path);
@ -182,19 +182,19 @@ class ImageBatchCompressionService {
await entity.delete(); await entity.delete();
deletedCount++; deletedCount++;
deletedBytes += fileSize; deletedBytes += fileSize;
debugPrint('🗑️ Deleted temp file: $fileName (${(fileSize / 1024).toStringAsFixed(1)}KB)'); debugPrint(' Deleted temp file: $fileName (${(fileSize / 1024).toStringAsFixed(1)}KB)');
} catch (e) { } catch (e) {
debugPrint('⚠️ Failed to delete temp file: $fileName - $e'); debugPrint(' Failed to delete temp file: $fileName - $e');
} }
} }
} }
} }
debugPrint(' Cleanup complete: $deletedCount files, ${(deletedBytes / 1024 / 1024).toStringAsFixed(1)}MB'); debugPrint(' Cleanup complete: $deletedCount files, ${(deletedBytes / 1024 / 1024).toStringAsFixed(1)}MB');
return (deletedCount, deletedBytes); return (deletedCount, deletedBytes);
} catch (e) { } catch (e) {
debugPrint(' Cleanup error: $e'); debugPrint(' Cleanup error: $e');
return (0, 0); return (0, 0);
} }
} }

View File

@ -21,7 +21,7 @@ class ImagePathRepairService {
int problematicItems = 0; int problematicItems = 0;
int missingFiles = 0; int missingFiles = 0;
debugPrint('🔍 画像パス診断開始: $totalItems アイテム'); debugPrint(' 画像パス診断開始: $totalItems アイテム');
for (final item in items) { for (final item in items) {
bool hasIssue = false; bool hasIssue = false;
@ -29,7 +29,7 @@ class ImagePathRepairService {
for (final imagePath in item.displayData.imagePaths) { for (final imagePath in item.displayData.imagePaths) {
final file = File(imagePath); final file = File(imagePath);
if (!await file.exists()) { if (!await file.exists()) {
debugPrint(' Missing: $imagePath (${item.displayData.displayName})'); debugPrint(' Missing: $imagePath (${item.displayData.displayName})');
missingFiles++; missingFiles++;
hasIssue = true; hasIssue = true;
} }
@ -40,11 +40,11 @@ class ImagePathRepairService {
} }
} }
debugPrint('📊 診断結果: $totalItems アイテム中 $problematicItems に問題あり ($missingFiles ファイル欠損)'); debugPrint(' 診断結果: $totalItems アイテム中 $problematicItems に問題あり ($missingFiles ファイル欠損)');
return (totalItems, problematicItems, missingFiles); return (totalItems, problematicItems, missingFiles);
} catch (e) { } catch (e) {
debugPrint(' 診断エラー: $e'); debugPrint(' 診断エラー: $e');
return (0, 0, 0); return (0, 0, 0);
} }
} }
@ -76,7 +76,7 @@ class ImagePathRepairService {
} }
} }
debugPrint('📁 利用可能な画像ファイル: ${availableFiles.length}'); debugPrint(' 利用可能な画像ファイル: ${availableFiles.length}');
int repairedItems = 0; int repairedItems = 0;
int repairedPaths = 0; int repairedPaths = 0;
@ -108,10 +108,10 @@ class ImagePathRepairService {
newPaths.add(matchedPath); newPaths.add(matchedPath);
repairedPaths++; repairedPaths++;
needsRepair = true; needsRepair = true;
debugPrint('🔧 Repaired: $oldFileName -> $matchedPath'); debugPrint(' Repaired: $oldFileName -> $matchedPath');
} else { } else {
// //
debugPrint('⚠️ No match for: $oldFileName (${item.displayData.displayName})'); debugPrint(' No match for: $oldFileName (${item.displayData.displayName})');
} }
} }
} }
@ -121,17 +121,17 @@ class ImagePathRepairService {
final updatedItem = item.copyWith( final updatedItem = item.copyWith(
imagePaths: newPaths, imagePaths: newPaths,
); );
await box.put(item.key, updatedItem); // 🔧 Fixed: updatedItem await box.put(item.key, updatedItem); // Fixed: updatedItem
repairedItems++; repairedItems++;
debugPrint(' Updated: ${item.displayData.displayName} (${newPaths.length} paths)'); debugPrint(' Updated: ${item.displayData.displayName} (${newPaths.length} paths)');
} }
} }
debugPrint(' 修復完了: $repairedItems アイテム、$repairedPaths パス'); debugPrint(' 修復完了: $repairedItems アイテム、$repairedPaths パス');
return (repairedItems, repairedPaths); return (repairedItems, repairedPaths);
} catch (e) { } catch (e) {
debugPrint(' 修復エラー: $e'); debugPrint(' 修復エラー: $e');
return (0, 0); return (0, 0);
} }
} }
@ -164,17 +164,17 @@ class ImagePathRepairService {
final size = await entity.length(); final size = await entity.length();
orphanedCount++; orphanedCount++;
totalSize += size; totalSize += size;
debugPrint('🗑️ Orphaned: $fileName (${(size / 1024).toStringAsFixed(1)}KB)'); debugPrint(' Orphaned: $fileName (${(size / 1024).toStringAsFixed(1)}KB)');
} }
} }
} }
} }
debugPrint('📊 孤立ファイル: $orphanedCount 個 (${(totalSize / 1024 / 1024).toStringAsFixed(1)}MB)'); debugPrint(' 孤立ファイル: $orphanedCount 個 (${(totalSize / 1024 / 1024).toStringAsFixed(1)}MB)');
return (orphanedCount, totalSize); return (orphanedCount, totalSize);
} catch (e) { } catch (e) {
debugPrint(' 孤立ファイル検出エラー: $e'); debugPrint(' 孤立ファイル検出エラー: $e');
return (0, 0); return (0, 0);
} }
} }
@ -208,17 +208,17 @@ class ImagePathRepairService {
await entity.delete(); await entity.delete();
deletedCount++; deletedCount++;
deletedSize += size; deletedSize += size;
debugPrint('🗑️ Deleted: $fileName (${(size / 1024).toStringAsFixed(1)}KB)'); debugPrint(' Deleted: $fileName (${(size / 1024).toStringAsFixed(1)}KB)');
} }
} }
} }
} }
debugPrint(' 孤立ファイル削除完了: $deletedCount 個 (${(deletedSize / 1024 / 1024).toStringAsFixed(1)}MB)'); debugPrint(' 孤立ファイル削除完了: $deletedCount 個 (${(deletedSize / 1024 / 1024).toStringAsFixed(1)}MB)');
return (deletedCount, deletedSize); return (deletedCount, deletedSize);
} catch (e) { } catch (e) {
debugPrint(' 孤立ファイル削除エラー: $e'); debugPrint(' 孤立ファイル削除エラー: $e');
return (0, 0); return (0, 0);
} }
} }

View File

@ -47,7 +47,7 @@ class SakenowaAutoMatchingService {
bool autoApply = false, bool autoApply = false,
}) async { }) async {
try { try {
debugPrint('🔍 [SakenowaAutoMatching] 開始: ${sakeItem.displayData.displayName}'); debugPrint(' [SakenowaAutoMatching] 開始: ${sakeItem.displayData.displayName}');
// //
final brands = await _sakenowaService.getBrands(); final brands = await _sakenowaService.getBrands();
@ -55,7 +55,7 @@ class SakenowaAutoMatchingService {
final areas = await _sakenowaService.getAreas(); final areas = await _sakenowaService.getAreas();
final flavorCharts = await _sakenowaService.getFlavorCharts(); final flavorCharts = await _sakenowaService.getFlavorCharts();
debugPrint('📊 [SakenowaAutoMatching] データ取得完了: ${brands.length} brands'); debugPrint(' [SakenowaAutoMatching] データ取得完了: ${brands.length} brands');
// //
final breweryMap = {for (var b in breweries) b.id: b}; final breweryMap = {for (var b in breweries) b.id: b};
@ -80,7 +80,7 @@ class SakenowaAutoMatchingService {
// //
if (bestScore < minScore) { if (bestScore < minScore) {
debugPrint(' [SakenowaAutoMatching] スコア不足: $bestScore < $minScore'); debugPrint(' [SakenowaAutoMatching] スコア不足: $bestScore < $minScore');
return MatchResult( return MatchResult(
score: bestScore, score: bestScore,
isConfident: false, isConfident: false,
@ -92,7 +92,7 @@ class SakenowaAutoMatchingService {
final area = brewery != null ? areaMap[brewery.areaId] : null; final area = brewery != null ? areaMap[brewery.areaId] : null;
final chart = bestBrand != null ? chartMap[bestBrand.id] : null; final chart = bestBrand != null ? chartMap[bestBrand.id] : null;
debugPrint(' [SakenowaAutoMatching] マッチング成功!'); debugPrint(' [SakenowaAutoMatching] マッチング成功!');
debugPrint(' 銘柄: ${bestBrand?.name} (スコア: $bestScore)'); debugPrint(' 銘柄: ${bestBrand?.name} (スコア: $bestScore)');
debugPrint(' 酒蔵: ${brewery?.name}'); debugPrint(' 酒蔵: ${brewery?.name}');
debugPrint(' 地域: ${area?.name}'); debugPrint(' 地域: ${area?.name}');
@ -113,7 +113,7 @@ class SakenowaAutoMatchingService {
return result; return result;
} catch (e, stackTrace) { } catch (e, stackTrace) {
debugPrint('💥 [SakenowaAutoMatching] エラー: $e'); debugPrint(' [SakenowaAutoMatching] エラー: $e');
debugPrint('Stack trace: $stackTrace'); debugPrint('Stack trace: $stackTrace');
return MatchResult( return MatchResult(
score: 0.0, score: 0.0,
@ -127,12 +127,12 @@ class SakenowaAutoMatchingService {
/// DisplayDataのsakenowaフィールドとHiddenSpecsを更新 /// DisplayDataのsakenowaフィールドとHiddenSpecsを更新
Future<void> applyMatch(SakeItem sakeItem, MatchResult result) async { Future<void> applyMatch(SakeItem sakeItem, MatchResult result) async {
if (!result.hasMatch) { if (!result.hasMatch) {
debugPrint('⚠️ [SakenowaAutoMatching] マッチなし、適用スキップ'); debugPrint(' [SakenowaAutoMatching] マッチなし、適用スキップ');
return; return;
} }
try { try {
debugPrint('💾 [SakenowaAutoMatching] マッチング結果を適用中...'); debugPrint(' [SakenowaAutoMatching] マッチング結果を適用中...');
// DisplayData更新 // DisplayData更新
final updatedDisplayData = sakeItem.displayData.copyWith( final updatedDisplayData = sakeItem.displayData.copyWith(
@ -166,12 +166,12 @@ class SakenowaAutoMatchingService {
// Hiveに保存 // Hiveに保存
await sakeItem.save(); await sakeItem.save();
debugPrint(' [SakenowaAutoMatching] 適用完了!'); debugPrint(' [SakenowaAutoMatching] 適用完了!');
debugPrint(' displayName: ${sakeItem.displayData.displayName}'); debugPrint(' displayName: ${sakeItem.displayData.displayName}');
debugPrint(' displayBrewery: ${sakeItem.displayData.displayBrewery}'); debugPrint(' displayBrewery: ${sakeItem.displayData.displayBrewery}');
debugPrint(' displayPrefecture: ${sakeItem.displayData.displayPrefecture}'); debugPrint(' displayPrefecture: ${sakeItem.displayData.displayPrefecture}');
} catch (e, stackTrace) { } catch (e, stackTrace) {
debugPrint('💥 [SakenowaAutoMatching] 適用エラー: $e'); debugPrint(' [SakenowaAutoMatching] 適用エラー: $e');
debugPrint('Stack trace: $stackTrace'); debugPrint('Stack trace: $stackTrace');
} }
} }
@ -188,7 +188,7 @@ class SakenowaAutoMatchingService {
double minScore = 0.7, double minScore = 0.7,
bool autoApply = false, bool autoApply = false,
}) async { }) async {
debugPrint('🔄 [SakenowaAutoMatching] バッチ処理開始: ${sakeItems.length}'); debugPrint(' [SakenowaAutoMatching] バッチ処理開始: ${sakeItems.length}');
int successCount = 0; int successCount = 0;
@ -204,7 +204,7 @@ class SakenowaAutoMatchingService {
} }
} }
debugPrint(' [SakenowaAutoMatching] バッチ処理完了: $successCount/${sakeItems.length} 成功'); debugPrint(' [SakenowaAutoMatching] バッチ処理完了: $successCount/${sakeItems.length} 成功');
return successCount; return successCount;
} }
@ -213,7 +213,7 @@ class SakenowaAutoMatchingService {
/// ///
/// ///
Future<void> clearSakenowaData(SakeItem sakeItem) async { Future<void> clearSakenowaData(SakeItem sakeItem) async {
debugPrint('🧹 [SakenowaAutoMatching] さけのわデータクリア: ${sakeItem.displayData.displayName}'); debugPrint(' [SakenowaAutoMatching] さけのわデータクリア: ${sakeItem.displayData.displayName}');
final clearedDisplayData = sakeItem.displayData.copyWith( final clearedDisplayData = sakeItem.displayData.copyWith(
sakenowaName: null, sakenowaName: null,
@ -231,6 +231,6 @@ class SakenowaAutoMatchingService {
await sakeItem.save(); await sakeItem.save();
debugPrint(' [SakenowaAutoMatching] クリア完了'); debugPrint(' [SakenowaAutoMatching] クリア完了');
} }
} }

View File

@ -19,7 +19,7 @@ class ShukoDiagnosisService {
double totalBody = 0; double totalBody = 0;
int count = 0; int count = 0;
debugPrint('🍶🍶🍶 SHUKO DIAGNOSIS START: Total items = ${items.length}'); debugPrint(' SHUKO DIAGNOSIS START: Total items = ${items.length}');
for (var item in items) { for (var item in items) {
final stats = item.hiddenSpecs.sakeTasteStats; final stats = item.hiddenSpecs.sakeTasteStats;
@ -27,7 +27,7 @@ class ShukoDiagnosisService {
// Skip items with empty tasteStats (all zeros) // Skip items with empty tasteStats (all zeros)
if (stats.aroma == 0 && stats.bitterness == 0 && stats.sweetness == 0 && if (stats.aroma == 0 && stats.bitterness == 0 && stats.sweetness == 0 &&
stats.acidity == 0 && stats.body == 0) { stats.acidity == 0 && stats.body == 0) {
debugPrint('🍶 SKIPPED item (all zeros)'); debugPrint(' SKIPPED item (all zeros)');
continue; continue;
} }
@ -39,10 +39,10 @@ class ShukoDiagnosisService {
count++; count++;
} }
debugPrint('🍶🍶🍶 Analyzed $count out of ${items.length} items'); debugPrint(' Analyzed $count out of ${items.length} items');
if (count == 0) { if (count == 0) {
debugPrint('🍶🍶🍶 WARNING: No items to analyze, returning empty profile'); debugPrint(' WARNING: No items to analyze, returning empty profile');
return ShukoProfile.empty(); return ShukoProfile.empty();
} }
@ -68,7 +68,7 @@ class ShukoDiagnosisService {
ShukoTitle _determineTitle(SakeTasteStats stats) { ShukoTitle _determineTitle(SakeTasteStats stats) {
// DEBUG: Print average stats // DEBUG: Print average stats
debugPrint('🔍 DEBUG avgStats: aroma=${stats.aroma.toStringAsFixed(2)}, bitterness=${stats.bitterness.toStringAsFixed(2)}, sweetness=${stats.sweetness.toStringAsFixed(2)}, acidity=${stats.acidity.toStringAsFixed(2)}, body=${stats.body.toStringAsFixed(2)}'); debugPrint(' DEBUG avgStats: aroma=${stats.aroma.toStringAsFixed(2)}, bitterness=${stats.bitterness.toStringAsFixed(2)}, sweetness=${stats.sweetness.toStringAsFixed(2)}, acidity=${stats.acidity.toStringAsFixed(2)}, body=${stats.body.toStringAsFixed(2)}');
// Scoring-based logic to handle overlapping traits // Scoring-based logic to handle overlapping traits
final Map<String, double> scores = {}; final Map<String, double> scores = {};
@ -97,17 +97,17 @@ class ShukoDiagnosisService {
scores['バランスの賢者'] = _calculateBalanceScore(stats); scores['バランスの賢者'] = _calculateBalanceScore(stats);
// DEBUG: Print all scores // DEBUG: Print all scores
debugPrint('🔍 DEBUG scores: ${scores.entries.map((e) => '${e.key}=${e.value.toStringAsFixed(2)}').join(', ')}'); debugPrint(' DEBUG scores: ${scores.entries.map((e) => '${e.key}=${e.value.toStringAsFixed(2)}').join(', ')}');
// Find the title with the highest score // Find the title with the highest score
final maxEntry = scores.entries.reduce((a, b) => a.value > b.value ? a : b); final maxEntry = scores.entries.reduce((a, b) => a.value > b.value ? a : b);
debugPrint('🔍 DEBUG winner: ${maxEntry.key} with score ${maxEntry.value.toStringAsFixed(2)}'); debugPrint(' DEBUG winner: ${maxEntry.key} with score ${maxEntry.value.toStringAsFixed(2)}');
// Threshold: require minimum score to avoid false positives // Threshold: require minimum score to avoid false positives
// Lowered to 1.5 to be more forgiving for "Standard" sake // Lowered to 1.5 to be more forgiving for "Standard" sake
if (maxEntry.value < 1.0) { if (maxEntry.value < 1.0) {
debugPrint('🔍 DEBUG: Score too low (${maxEntry.value.toStringAsFixed(2)} < 1.0), returning default title'); debugPrint(' DEBUG: Score too low (${maxEntry.value.toStringAsFixed(2)} < 1.0), returning default title');
// Proposed New Default Titles // Proposed New Default Titles
return const ShukoTitle( return const ShukoTitle(
title: '酒道の旅人', title: '酒道の旅人',

View File

@ -46,9 +46,13 @@ redisClient.on('connect', () => {
const genAI = new GoogleGenerativeAI(API_KEY); const genAI = new GoogleGenerativeAI(API_KEY);
const model = genAI.getGenerativeModel({ const model = genAI.getGenerativeModel({
model: "gemini-2.5-flash", model: "gemini-2.5-flash",
systemInstruction: "あなたは画像内のテキストを一字一句正確に読み取る専門家です。" +
"ラベルに記載された銘柄名・蔵元名は絶対に変更・補完しないでください。" +
"あなたの知識でラベルの文字を上書きすることは厳禁です。" +
"ラベルに「東魁」とあれば、「東魁盛」を知っていても必ず「東魁」と出力してください。",
generationConfig: { generationConfig: {
responseMimeType: "application/json", responseMimeType: "application/json",
temperature: 0.2, temperature: 0,
} }
}); });