# Face Recognition - Filtering Strategies

**Question**: Will this filter out images that don't contain the faces I want?

**Short Answer**: Not by default, but we can add multiple filtering strategies!

---

## 🎯 Current Behavior (Without Filtering)

### Default Immich Integration Workflow

```
Download Image
    ↓
Wait for Immich to Scan
    ↓
Query Immich: "Who's in this photo?"
    ↓
    ├─── Face identified as "John" ──► Copy to /faces/john_doe/
    ├─── Face identified as "Sarah" ─► Copy to /faces/sarah_smith/
    ├─── Face NOT identified ────────► Leave in original location
    └─── NO faces detected ──────────► Leave in original location
```

**Result**:
- ✅ Images with wanted faces → Sorted to person folders
- ⚠️ Images without faces → Stay in original location
- ⚠️ Images with unknown faces → Stay in original location

**This doesn't delete/hide unwanted images, just organizes wanted ones.**

---

## 🎨 Filtering Strategies

### Strategy 1: Whitelist Mode (Only Keep Wanted Faces)

**Concept**: Only keep images that contain faces from your whitelist.

```python
# Configuration
"face_filtering": {
    "mode": "whitelist",
    "wanted_people": ["john_doe", "sarah_smith", "family_member"],
    "unwanted_action": "delete",  # or "move_to_review" or "skip_download"
}
```

**Workflow**:
```
Download Image
    ↓
Wait for Immich Scan
    ↓
Query: "Who's in this photo?"
    ↓
    ├─── Person in whitelist ──────► Keep & Sort to /faces/person_name/
    ├─── Person NOT in whitelist ──► DELETE (or move to /review/)
    └─── No faces / Unknown ───────► DELETE (or move to /review/)
```

**Code Example**:
```python
def process_with_whitelist(file_path: str, whitelist: list):
    """Only keep images with wanted people"""

    # Get faces from Immich
    faces = immich_db.get_faces_for_file(file_path)

    # Check if any wanted person is in the image
    wanted_faces = [f for f in faces if f['person_name'] in whitelist]

    if wanted_faces:
        # Keep image - sort to person's folder
        primary_person = wanted_faces[0]['person_name']
        sort_to_person_folder(file_path, primary_person)
        return {'action': 'kept', 'person': primary_person}
    else:
        # Unwanted - delete or move to review
        action = config.get('unwanted_action', 'delete')

        if action == 'delete':
            os.remove(file_path)
            return {'action': 'deleted', 'reason': 'not in whitelist'}
        elif action == 'move_to_review':
            shutil.move(file_path, '/faces/review_unwanted/')
            return {'action': 'moved_to_review'}
        else:  # skip (leave in place)
            return {'action': 'skipped'}
```

---

### Strategy 2: Blacklist Mode (Remove Unwanted Faces)

**Concept**: Delete/hide images that contain specific unwanted people.

```python
# Configuration
"face_filtering": {
    "mode": "blacklist",
    "unwanted_people": ["stranger", "random_person", "ex_friend"],
    "unwanted_action": "delete",
}
```

**Workflow**:
```
Download Image
    ↓
Query: "Who's in this photo?"
    ↓
    ├─── Contains blacklisted person ──► DELETE
    └─── No blacklisted person ────────► Keep (and sort if wanted)
```

**Code Example**:
```python
def process_with_blacklist(file_path: str, blacklist: list):
    """Remove images with unwanted people"""

    faces = immich_db.get_faces_for_file(file_path)

    # Check for blacklisted faces
    unwanted = [f for f in faces if f['person_name'] in blacklist]

    if unwanted:
        # Contains unwanted person - delete
        os.remove(file_path)
        return {'action': 'deleted', 'reason': f'contains {unwanted[0]["person_name"]}'}
    else:
        # No unwanted faces - process normally
        return process_normally(file_path, faces)
```

---

### Strategy 3: Pre-Download Filtering (Smart Downloading)

**Concept**: Check Immich BEFORE downloading to avoid unwanted downloads.

**Challenge**: File must exist in Immich before we can check faces.

**Solution**: Two-phase approach:
1. Download to temporary location
2. Check faces
3. Keep or delete based on criteria

```python
def smart_download(url: str, temp_path: str):
    """Download, check faces, then decide"""

    # Phase 1: Download to temp location
    download_to_temp(url, temp_path)

    # Phase 2: Quick face check (use our own detection or wait for Immich)
    if use_own_detection:
        faces = quick_face_check(temp_path)
    else:
        trigger_immich_scan(temp_path)
        time.sleep(5)  # Wait for Immich
        faces = immich_db.get_faces_for_file(temp_path)

    # Phase 3: Decide
    whitelist = config.get('wanted_people', [])

    if any(f['person_name'] in whitelist for f in faces):
        # Wanted person found - move to permanent location
        final_path = get_permanent_path(temp_path)
        shutil.move(temp_path, final_path)
        return {'action': 'downloaded', 'path': final_path}
    else:
        # No wanted faces - delete temp file
        os.remove(temp_path)
        return {'action': 'rejected', 'reason': 'no wanted faces'}
```

---

### Strategy 4: Confidence-Based Filtering

**Concept**: Only keep high-confidence matches.

```python
def process_with_confidence(file_path: str, min_confidence: float = 0.8):
    """Only keep images with high-confidence face matches"""

    faces = immich_db.get_faces_for_file(file_path)

    # Filter by confidence (would need to add confidence to Immich query)
    high_confidence = [f for f in faces if f.get('confidence', 0) >= min_confidence]

    if high_confidence:
        sort_to_person_folder(file_path, high_confidence[0]['person_name'])
        return {'action': 'kept', 'confidence': high_confidence[0]['confidence']}
    else:
        # Low confidence or no faces
        os.remove(file_path)
        return {'action': 'deleted', 'reason': 'low confidence'}
```

---

### Strategy 5: Multi-Person Filtering

**Concept**: Handle images with multiple people.

```python
def process_multi_person(file_path: str):
    """Handle images with multiple faces"""

    faces = immich_db.get_faces_for_file(file_path)
    whitelist = config.get('wanted_people', [])

    wanted = [f for f in faces if f['person_name'] in whitelist]

    if len(faces) == 0:
        # No faces
        return delete_or_move(file_path, 'no_faces')

    elif len(wanted) == 0:
        # Faces but none wanted
        return delete_or_move(file_path, 'unwanted_faces')

    elif len(wanted) == 1 and len(faces) == 1:
        # Single wanted person - perfect!
        return sort_to_person_folder(file_path, wanted[0]['person_name'])

    elif len(wanted) == 1 and len(faces) > 1:
        # Wanted person + others
        multi_person_action = config.get('multi_person_action', 'keep')

        if multi_person_action == 'keep':
            return sort_to_person_folder(file_path, wanted[0]['person_name'])
        elif multi_person_action == 'move_to_review':
            return move_to_review(file_path, 'multiple_people')
        else:  # delete
            return delete_or_move(file_path, 'multiple_people')

    else:  # Multiple wanted people
        # Copy to each person's folder or move to shared folder
        return handle_multiple_wanted(file_path, wanted)
```

---

## 🔧 Complete Configuration Options

```json
{
  "face_filtering": {
    "enabled": true,
    "mode": "whitelist",

    "whitelist": {
      "enabled": true,
      "wanted_people": [
        "john_doe",
        "sarah_smith",
        "family_member_1"
      ],
      "require_all": false,
      "require_any": true
    },

    "blacklist": {
      "enabled": false,
      "unwanted_people": [
        "stranger",
        "random_person"
      ]
    },

    "face_requirements": {
      "min_faces": 1,
      "max_faces": 3,
      "require_single_person": false,
      "min_confidence": 0.6
    },

    "actions": {
      "no_faces": "keep",
      "unknown_faces": "move_to_review",
      "unwanted_faces": "delete",
      "blacklisted": "delete",
      "multiple_people": "keep",
      "low_confidence": "move_to_review"
    },

    "directories": {
      "review_unwanted": "/faces/review_unwanted/",
      "review_unknown": "/faces/review_unknown/",
      "review_multi": "/faces/review_multiple/",
      "deleted_log": "/faces/deleted_log.json"
    },

    "safety": {
      "enable_deletion": false,
      "require_confirmation": true,
      "keep_deletion_log": true,
      "dry_run": true
    }
  }
}
```

---

## 📊 Filtering Scenarios

### Scenario 1: Only Keep Photos of Specific Person

**Goal**: Download Instagram profile, only keep photos with "john_doe"

**Configuration**:
```json
{
  "face_filtering": {
    "mode": "whitelist",
    "whitelist": {
      "wanted_people": ["john_doe"],
      "require_all": true
    },
    "actions": {
      "unwanted_faces": "delete",
      "unknown_faces": "delete",
      "no_faces": "delete"
    }
  }
}
```

**Result**:
- ✅ Photos with john_doe → Kept in `/faces/john_doe/`
- ❌ Photos without john_doe → Deleted
- ❌ Photos with only strangers → Deleted
- ❌ Photos with no faces → Deleted

---

### Scenario 2: Keep Family Photos, Remove Strangers

**Goal**: Keep photos with any family member, delete strangers

**Configuration**:
```json
{
  "face_filtering": {
    "mode": "whitelist",
    "whitelist": {
      "wanted_people": ["john", "sarah", "mom", "dad", "sister"],
      "require_all": false,
      "require_any": true
    },
    "actions": {
      "unwanted_faces": "delete",
      "multiple_people": "keep"
    }
  }
}
```

**Result**:
- ✅ Photo with john → Kept
- ✅ Photo with john + sarah → Kept
- ✅ Photo with stranger + john → Kept (has john)
- ❌ Photo with only stranger → Deleted

---

### Scenario 3: Avoid Specific People

**Goal**: Remove ex-partner from all downloads

**Configuration**:
```json
{
  "face_filtering": {
    "mode": "blacklist",
    "blacklist": {
      "unwanted_people": ["ex_partner"]
    },
    "actions": {
      "blacklisted": "delete"
    }
  }
}
```

**Result**:
- ❌ Any photo with ex_partner → Deleted
- ✅ All other photos → Kept

---

### Scenario 4: Conservative (Review Unknowns)

**Goal**: Auto-sort known faces, manually review everything else

**Configuration**:
```json
{
  "face_filtering": {
    "mode": "whitelist",
    "whitelist": {
      "wanted_people": ["john", "sarah"]
    },
    "actions": {
      "unwanted_faces": "move_to_review",
      "unknown_faces": "move_to_review",
      "no_faces": "move_to_review"
    },
    "safety": {
      "enable_deletion": false
    }
  }
}
```

**Result**:
- ✅ john/sarah → Auto-sorted to person folders
- 📋 Unknown faces → `/faces/review_unknown/`
- 📋 No faces → `/faces/review_unknown/`
- 📋 Strangers → `/faces/review_unwanted/`

---

## 🛡️ Safety Features

### Dry Run Mode

Test filtering without actually deleting:

```python
def delete_or_move(file_path: str, reason: str):
    """Delete or move file (with dry run support)"""

    dry_run = config.get('safety', {}).get('dry_run', False)

    if dry_run:
        logger.info(f"[DRY RUN] Would delete: {file_path} (reason: {reason})")
        return {'action': 'dry_run_delete', 'reason': reason}
    else:
        os.remove(file_path)
        log_deletion(file_path, reason)
        return {'action': 'deleted', 'reason': reason}
```

### Deletion Log

Keep record of what was deleted:

```json
{
  "deletions": [
    {
      "file": "/path/to/image.jpg",
      "reason": "no_wanted_faces",
      "deleted_at": "2025-01-31T15:30:00",
      "faces_found": ["stranger_1", "stranger_2"],
      "size_bytes": 2048576,
      "checksum": "abc123..."
    }
  ]
}
```

---

## 🎯 Recommended Approach

### Phase 1: Conservative Start
```json
{
  "face_filtering": {
    "enabled": true,
    "mode": "whitelist",
    "whitelist": {
      "wanted_people": ["person1", "person2"]
    },
    "actions": {
      "unwanted_faces": "move_to_review",
      "unknown_faces": "move_to_review"
    },
    "safety": {
      "enable_deletion": false
    }
  }
}
```

**Review for 1-2 weeks**, then adjust.

### Phase 2: Enable Deletion (Carefully)
```json
{
  "safety": {
    "enable_deletion": true,
    "dry_run": true,
    "keep_deletion_log": true
  }
}
```

**Run in dry run mode** for a few days.

### Phase 3: Full Automation
```json
{
  "actions": {
    "unwanted_faces": "delete",
    "no_faces": "delete"
  },
  "safety": {
    "dry_run": false,
    "keep_deletion_log": true
  }
}
```

**Only after confirming** dry run results look good.

---

## 🔄 Complete Workflow Example

### Download Instagram Profile → Filter → Sort

```python
def process_instagram_download(profile: str):
    """Complete workflow with filtering"""

    # 1. Download all images from profile
    images = download_instagram_profile(profile)

    # 2. Wait for Immich to scan
    trigger_immich_scan()
    time.sleep(10)

    # 3. Process each image with filtering
    results = {
        'kept': 0,
        'deleted': 0,
        'reviewed': 0
    }

    whitelist = config.get('whitelist', {}).get('wanted_people', [])

    for image_path in images:
        # Get faces from Immich
        faces = immich_db.get_faces_for_file(image_path)

        # Check whitelist
        wanted = [f for f in faces if f['person_name'] in whitelist]

        if wanted:
            # Wanted person - keep and sort
            sort_to_person_folder(image_path, wanted[0]['person_name'])
            results['kept'] += 1
        else:
            # No wanted faces - handle based on config
            action = config.get('actions', {}).get('unwanted_faces', 'delete')

            if action == 'delete':
                os.remove(image_path)
                results['deleted'] += 1
            elif action == 'move_to_review':
                move_to_review(image_path)
                results['reviewed'] += 1

    return results

# Results:
# {'kept': 42, 'deleted': 158, 'reviewed': 0}
```

---

## 📈 Statistics & Reporting

Track filtering effectiveness:

```python
def generate_filter_stats():
    """Generate filtering statistics"""

    with sqlite3.connect(db_path) as conn:
        stats = {
            'total_processed': conn.execute(
                "SELECT COUNT(*) FROM face_filter_history"
            ).fetchone()[0],

            'kept': conn.execute(
                "SELECT COUNT(*) FROM face_filter_history WHERE action = 'kept'"
            ).fetchone()[0],

            'deleted': conn.execute(
                "SELECT COUNT(*) FROM face_filter_history WHERE action = 'deleted'"
            ).fetchone()[0],

            'by_person': {},
            'deletion_reasons': {}
        }

        # Stats by person
        cursor = conn.execute("""
            SELECT person_name, COUNT(*)
            FROM face_filter_history
            WHERE action = 'kept'
            GROUP BY person_name
        """)
        stats['by_person'] = dict(cursor.fetchall())

        # Deletion reasons
        cursor = conn.execute("""
            SELECT reason, COUNT(*)
            FROM face_filter_history
            WHERE action = 'deleted'
            GROUP BY reason
        """)
        stats['deletion_reasons'] = dict(cursor.fetchall())

    return stats

# Results:
# {
#   'total_processed': 500,
#   'kept': 200,
#   'deleted': 300,
#   'by_person': {'john': 120, 'sarah': 80},
#   'deletion_reasons': {'no_wanted_faces': 250, 'blacklisted': 50}
# }
```

---

## ✅ Answer to Your Question

**Will this filter out images that don't contain the face I want?**

**Out of the box**: No - it just organizes images with identified faces.

**With filtering enabled**: **YES** - you can configure it to:
- ✅ Delete images without wanted faces
- ✅ Move unwanted images to review folder
- ✅ Only keep specific people (whitelist)
- ✅ Remove specific people (blacklist)
- ✅ Handle multiple faces
- ✅ Confidence thresholds

**Recommended**: Start with "move to review" mode, then enable deletion after testing.

---

## 📝 Implementation Checklist

- [ ] Add whitelist configuration
- [ ] Implement filtering logic
- [ ] Add safety features (dry run, deletion log)
- [ ] Create review directories
- [ ] Add statistics tracking
- [ ] Build filtering UI
- [ ] Test with sample data
- [ ] Enable deletion (carefully!)

---

**Documentation**:
- Immich Integration: `docs/AI_FACE_RECOGNITION_IMMICH_INTEGRATION.md`
- Filtering: This document
- Comparison: `docs/AI_FACE_RECOGNITION_COMPARISON.md`

---

**Last Updated**: 2025-10-31
