ponshu-room-lite/lib/services/draft_service.dart

272 lines
8.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:uuid/uuid.dart';
import '../models/sake_item.dart';
import 'gemini_service.dart';
/// Draft解析待ちアイテム管理サービス
///
/// Phase 1緊急対応: オフライン時に撮影した写真を一時保存し、
/// オンライン復帰時に自動解析する機能を提供します。
///
/// 使用フロー:
/// 1. オフライン時: saveDraft() で写真を一時保存
/// 2. オンライン復帰時: getPendingDrafts() で一覧取得
/// 3. analyzeDraft() で個別解析、または analyzeAllDrafts() で一括解析
class DraftService {
static const _uuid = Uuid();
/// Draft解析待ちアイテムを保存
///
/// オフライン時にカメラで撮影した写真を一時保存します。
/// 写真パスはそのまま保持され、後でAI解析時に使用されます。
///
/// [photoPaths] 保存した写真の絶対パスリスト(ギャラリーに既に保存済みであること)
///
/// Returns: 作成されたDraft SakeItemのkey
///
/// Usage:
/// ```dart
/// final draftKey = await DraftService.saveDraft([imagePath1, imagePath2]);
/// ```
static Future<String> saveDraft(List<String> photoPaths) async {
try {
final box = Hive.box<SakeItem>('sake_items');
// 🔧 FIX: 最初の画像をdraftPhotoPathに、全画像をimagePathsに保存
final firstPhotoPath = photoPaths.isNotEmpty ? photoPaths.first : '';
// Draft用の仮データを作成
final draftItem = SakeItem(
id: _uuid.v4(),
isPendingAnalysis: true,
draftPhotoPath: firstPhotoPath,
displayData: DisplayData(
name: '解析待ち',
brewery: '---',
prefecture: '---',
imagePaths: photoPaths, // 🔧 FIX: すべての画像を保存
rating: null,
),
hiddenSpecs: HiddenSpecs(
description: 'オフライン時に撮影された写真です。オンライン復帰後に自動解析されます。',
tasteStats: {},
flavorTags: [],
),
userData: UserData(
isFavorite: false,
isUserEdited: false,
markup: 3.0,
),
gamification: Gamification(ponPoints: 0),
metadata: Metadata(
createdAt: DateTime.now(),
aiConfidence: null,
),
);
await box.add(draftItem);
debugPrint('📝 Draft saved: ${draftItem.id}');
return draftItem.id;
} catch (e) {
debugPrint('⚠️ Draft save error: $e');
rethrow;
}
}
/// すべての解析待ちDraftを取得
///
/// Returns: 解析待ちアイテムのリスト
///
/// Usage:
/// ```dart
/// final drafts = await DraftService.getPendingDrafts();
/// print('未解析: ${drafts.length}件');
/// ```
static Future<List<SakeItem>> getPendingDrafts() async {
try {
final box = Hive.box<SakeItem>('sake_items');
final allItems = box.values.toList();
final pendingDrafts = allItems.where((item) => item.isPendingAnalysis).toList();
debugPrint('📋 Pending drafts: ${pendingDrafts.length}');
return pendingDrafts;
} catch (e) {
debugPrint('⚠️ Get pending drafts error: $e');
return [];
}
}
/// 解析待ちDraftの件数を取得
///
/// Returns: 解析待ちアイテム数
static Future<int> getPendingCount() async {
final drafts = await getPendingDrafts();
return drafts.length;
}
/// 特定のDraftを解析し、正式なSakeItemに変換
///
/// [itemKey] DraftアイテムのHive key
///
/// Returns: 解析結果のSakeAnalysisResult
///
/// Throws: AI解析エラー、ネットワークエラーなど
///
/// Usage:
/// ```dart
/// try {
/// final result = await DraftService.analyzeDraft(itemKey);
/// print('解析完了: ${result.name}');
/// } catch (e) {
/// print('解析失敗: $e');
/// }
/// ```
static Future<SakeAnalysisResult> analyzeDraft(dynamic itemKey) async {
final box = Hive.box<SakeItem>('sake_items');
final item = box.get(itemKey);
if (item == null) {
throw Exception('Draft item not found: $itemKey');
}
if (!item.isPendingAnalysis) {
throw Exception('Item is not a draft: $itemKey');
}
final photoPath = item.draftPhotoPath;
if (photoPath == null || photoPath.isEmpty) {
throw Exception('Draft has no photo path: $itemKey');
}
debugPrint('🔍 Analyzing draft: $itemKey (${item.displayData.displayName})');
// 🔧 FIX: 複数画像がある場合はすべて使用
final imagePaths = item.displayData.imagePaths;
final pathsToAnalyze = imagePaths.isNotEmpty ? imagePaths : [photoPath];
// Gemini Vision API呼び出し
final geminiService = GeminiService();
final result = await geminiService.analyzeSakeLabel(pathsToAnalyze);
debugPrint('✅ Analysis completed: ${result.name}');
// Draftを正式なアイテムに更新
final updatedItem = item.copyWith(
name: result.name,
brand: result.brand,
prefecture: result.prefecture,
description: result.description,
catchCopy: result.catchCopy,
flavorTags: result.flavorTags,
tasteStats: result.tasteStats,
confidenceScore: result.confidenceScore,
// 新規フィールド
specificDesignation: result.type,
alcoholContent: result.alcoholContent,
polishingRatio: result.polishingRatio,
sakeMeterValue: result.sakeMeterValue,
riceVariety: result.riceVariety,
yeast: result.yeast,
manufacturingYearMonth: result.manufacturingYearMonth,
// Draft状態を解除
isPendingAnalysis: false,
draftPhotoPath: null,
);
// await updatedItem.save(); // Error: This object is currently not in a box.
await box.put(itemKey, updatedItem);
debugPrint('💾 Draft updated to normal item: $itemKey');
return result;
}
/// すべての解析待ちDraftを一括解析
///
/// Returns: {成功件数, 失敗件数, エラーメッセージリスト}
///
/// Usage:
/// ```dart
/// final result = await DraftService.analyzeAllDrafts((progress, total) {
/// print('進捗: $progress / $total');
/// });
/// print('成功: ${result['success']}, 失敗: ${result['failed']}');
/// ```
static Future<Map<String, dynamic>> analyzeAllDrafts({
Function(int progress, int total)? onProgress,
}) async {
final drafts = await getPendingDrafts();
final total = drafts.length;
if (total == 0) {
debugPrint('📭 No pending drafts to analyze');
return {'success': 0, 'failed': 0, 'errors': []};
}
int successCount = 0;
int failedCount = 0;
final List<String> errors = [];
debugPrint('🚀 Analyzing $total drafts...');
for (int i = 0; i < total; i++) {
final draft = drafts[i];
final itemKey = draft.key;
try {
onProgress?.call(i + 1, total);
await analyzeDraft(itemKey);
successCount++;
debugPrint('✅ [$i+1/$total] Success: ${draft.displayData.displayName}');
} catch (e) {
failedCount++;
final errorMsg = '[$i+1/$total] ${draft.displayData.displayName}: $e';
errors.add(errorMsg);
debugPrint('$errorMsg');
// 失敗してもループは継続他のDraftを解析
}
}
debugPrint('🎉 Batch analysis completed: $successCount成功, $failedCount失敗');
return {
'success': successCount,
'failed': failedCount,
'errors': errors,
};
}
/// Draft を削除(解析を諦める場合)
///
/// [itemKey] DraftアイテムのHive key
///
/// Usage:
/// ```dart
/// await DraftService.deleteDraft(itemKey);
/// ```
static Future<void> deleteDraft(dynamic itemKey) async {
final box = Hive.box<SakeItem>('sake_items');
final item = box.get(itemKey);
if (item != null && item.isPendingAnalysis) {
await box.delete(itemKey);
debugPrint('🗑️ Draft deleted: $itemKey');
}
}
/// すべてのDraftを削除
///
/// Returns: 削除件数
static Future<int> deleteAllDrafts() async {
final drafts = await getPendingDrafts();
final count = drafts.length;
for (final draft in drafts) {
await deleteDraft(draft.key);
}
debugPrint('🗑️ All drafts deleted: $count件');
return count;
}
}