@@ -57,7 +57,8 @@ class DetectionAnalyzer @Inject constructor(
5757 private val aiSettingsRepository : AiSettingsRepository ,
5858 private val detectionRepository : DetectionRepository ,
5959 private val geminiNanoClient : GeminiNanoClient ,
60- private val mediaPipeLlmClient : MediaPipeLlmClient
60+ private val mediaPipeLlmClient : MediaPipeLlmClient ,
61+ private val falsePositiveAnalyzer : FalsePositiveAnalyzer
6162) {
6263 companion object {
6364 private const val TAG = " DetectionAnalyzer"
@@ -186,12 +187,14 @@ class DetectionAnalyzer @Inject constructor(
186187 * Uses mutex to prevent race conditions during concurrent initialization attempts.
187188 */
188189 suspend fun initializeModel (): Boolean = modelStateMutex.withLock {
190+ Log .i(TAG , " === initializeModel START ===" )
189191 withContext(Dispatchers .IO ) {
190192 try {
191193 val settings = aiSettingsRepository.settings.first()
194+ Log .d(TAG , " initializeModel settings: enabled=${settings.enabled} , selectedModel=${settings.selectedModel} " )
192195
193196 if (! settings.enabled) {
194- Log .d (TAG , " AI analysis is disabled" )
197+ Log .w (TAG , " AI analysis is disabled in settings - returning false " )
195198 return @withContext false
196199 }
197200
@@ -349,17 +352,33 @@ class DetectionAnalyzer @Inject constructor(
349352 */
350353 suspend fun analyzeDetection (detection : Detection ): AiAnalysisResult = withContext(Dispatchers .IO ) {
351354 val startTime = System .currentTimeMillis()
355+ Log .i(TAG , " === analyzeDetection START ===" )
356+ Log .d(TAG , " Detection: ${detection.id} (${detection.deviceType} )" )
352357
353358 // Lazy initialization: ensure model is loaded before analysis
354359 // This handles cases where model was downloaded but not yet initialized
355360 val settings = aiSettingsRepository.settings.first()
356361 val modelFromSettings = AiModel .fromId(settings.selectedModel)
362+
363+ Log .d(TAG , " Settings check:" )
364+ Log .d(TAG , " - enabled: ${settings.enabled} " )
365+ Log .d(TAG , " - analyzeDetections: ${settings.analyzeDetections} " )
366+ Log .d(TAG , " - selectedModel: ${settings.selectedModel} " )
367+ Log .d(TAG , " - modelFromSettings: ${modelFromSettings.id} (${modelFromSettings.displayName} )" )
368+ Log .d(TAG , " - currentModel (in-memory): ${currentModel.id} " )
369+ Log .d(TAG , " - isModelLoaded: $isModelLoaded " )
370+ Log .d(TAG , " - mediaPipeLlmClient.isReady(): ${mediaPipeLlmClient.isReady()} " )
371+
357372 if (settings.enabled && modelFromSettings != AiModel .RULE_BASED && ! isModelLoaded) {
358- Log .d (TAG , " Model not loaded, attempting lazy initialization for: ${modelFromSettings.displayName} " )
373+ Log .i (TAG , " Model not loaded, attempting lazy initialization for: ${modelFromSettings.displayName} " )
359374 val initialized = initializeModel()
375+ Log .d(TAG , " Lazy initialization result: $initialized " )
376+ Log .d(TAG , " After init - isModelLoaded: $isModelLoaded , mediaPipeReady: ${mediaPipeLlmClient.isReady()} " )
360377 if (! initialized) {
361378 Log .w(TAG , " Lazy initialization failed, will use rule-based fallback" )
362379 }
380+ } else {
381+ Log .d(TAG , " Skipping lazy init: enabled=${settings.enabled} , modelFromSettings=${modelFromSettings.id} , isModelLoaded=$isModelLoaded " )
363382 }
364383
365384 // Check if already analyzing - return early with informative message
@@ -391,6 +410,7 @@ class DetectionAnalyzer @Inject constructor(
391410
392411 // Settings already loaded above for lazy initialization
393412 if (! settings.enabled || ! settings.analyzeDetections) {
413+ Log .w(TAG , " Returning 'AI analysis is disabled': enabled=${settings.enabled} , analyzeDetections=${settings.analyzeDetections} " )
394414 return @withContext AiAnalysisResult (
395415 success = false ,
396416 error = " AI analysis is disabled"
@@ -432,8 +452,24 @@ class DetectionAnalyzer @Inject constructor(
432452
433453 // Generate analysis
434454 val result = generateAnalysis(detection, contextualInsights, settings)
455+
456+ // Check for cancellation before FP analysis
457+ coroutineContext.ensureActive()
458+
459+ // Run false positive analysis if enabled
460+ val fpResult = if (settings.enableFalsePositiveFiltering) {
461+ val contextInfo = buildFpContextInfo(detection, contextualInsights)
462+ falsePositiveAnalyzer.analyzeForFalsePositive(detection, contextInfo)
463+ } else null
464+
435465 val processingTime = System .currentTimeMillis() - startTime
436- val finalResult = result.copy(processingTimeMs = processingTime)
466+ val finalResult = result.copy(
467+ processingTimeMs = processingTime,
468+ isFalsePositive = fpResult?.isFalsePositive ? : false ,
469+ falsePositiveConfidence = fpResult?.confidence ? : 0f ,
470+ falsePositiveBanner = fpResult?.bannerMessage,
471+ falsePositiveReasons = fpResult?.allReasons?.map { it.description } ? : emptyList()
472+ )
437473
438474 // Check for cancellation before caching
439475 coroutineContext.ensureActive()
@@ -489,6 +525,48 @@ class DetectionAnalyzer @Inject constructor(
489525 Log .d(TAG , " Analysis cancellation requested" )
490526 }
491527
528+ // ==================== FALSE POSITIVE ANALYSIS ====================
529+
530+ /* *
531+ * Check a single detection for false positive likelihood.
532+ * Returns the FP result with banner message if applicable.
533+ */
534+ suspend fun checkForFalsePositive (detection : Detection ): FalsePositiveResult {
535+ return falsePositiveAnalyzer.analyzeForFalsePositive(detection)
536+ }
537+
538+ /* *
539+ * Filter a list of detections, removing likely false positives.
540+ * Returns filtered results with FP explanations.
541+ *
542+ * @param detections List of detections to filter
543+ * @param confidenceThreshold Minimum FP confidence to filter (0.0-1.0, default 0.6)
544+ */
545+ suspend fun filterFalsePositives (
546+ detections : List <Detection >,
547+ confidenceThreshold : Float = 0.6f
548+ ): FilteredDetections {
549+ return falsePositiveAnalyzer.filterFalsePositives(
550+ detections = detections,
551+ threshold = confidenceThreshold
552+ )
553+ }
554+
555+ /* *
556+ * Batch analyze detections for false positives.
557+ * Returns a map of detection ID to FP result.
558+ */
559+ suspend fun batchCheckFalsePositives (
560+ detections : List <Detection >
561+ ): Map <String , FalsePositiveResult > {
562+ return falsePositiveAnalyzer.analyzeMultiple(detections)
563+ }
564+
565+ /* *
566+ * Get the false positive analyzer for direct access if needed.
567+ */
568+ fun getFalsePositiveAnalyzer (): FalsePositiveAnalyzer = falsePositiveAnalyzer
569+
492570 private suspend fun gatherContextualInsights (detection : Detection ): ContextualInsights {
493571 val allDetections = detectionRepository.getAllDetectionsSnapshot()
494572
@@ -561,12 +639,46 @@ class DetectionAnalyzer @Inject constructor(
561639 return r * c
562640 }
563641
642+ /* *
643+ * Build context information for false positive analysis.
644+ * Uses contextual insights and detection location to determine user context.
645+ */
646+ private fun buildFpContextInfo (
647+ detection : Detection ,
648+ contextualInsights : ContextualInsights ?
649+ ): FpContextInfo {
650+ // Determine time of day
651+ val currentHour = java.util.Calendar .getInstance().get(java.util.Calendar .HOUR_OF_DAY )
652+ val isNightTime = currentHour < 6 || currentHour >= 22
653+
654+ // Check if at a known/familiar location
655+ val isKnownLocation = contextualInsights?.isKnownLocation ? : false
656+
657+ return FpContextInfo (
658+ isAtHome = isKnownLocation && detection.latitude != null ,
659+ homeLatitude = if (isKnownLocation) detection.latitude else null ,
660+ homeLongitude = if (isKnownLocation) detection.longitude else null ,
661+ isAtWork = false , // Would need user preference storage
662+ isKnownSafeArea = isKnownLocation,
663+ isNightTime = isNightTime,
664+ recentlyTraveled = false // Would need location history
665+ )
666+ }
667+
564668 @Suppress(" UNUSED_PARAMETER" )
565669 private suspend fun generateAnalysis (
566670 detection : Detection ,
567671 contextualInsights : ContextualInsights ? ,
568672 settings : AiSettings // Reserved for future use with inference configuration
569673 ): AiAnalysisResult {
674+ Log .i(TAG , " === generateAnalysis START ===" )
675+ Log .d(TAG , " State at generateAnalysis:" )
676+ Log .d(TAG , " - currentModel: ${currentModel.id} (${currentModel.displayName} )" )
677+ Log .d(TAG , " - isModelLoaded: $isModelLoaded " )
678+ Log .d(TAG , " - geminiNanoInitialized: $geminiNanoInitialized " )
679+ Log .d(TAG , " - mediaPipeLlmClient.isReady(): ${mediaPipeLlmClient.isReady()} " )
680+ Log .d(TAG , " - mediaPipeLlmClient.getStatus(): ${mediaPipeLlmClient.getStatus()} " )
681+
570682 // Use Gemini Nano if available and selected
571683 if (currentModel == AiModel .GEMINI_NANO && geminiNanoInitialized) {
572684 val geminiResult = geminiNanoClient.analyzeDetection(detection)
0 commit comments