# Instagram Story Repost Detection System

## Overview

Automatically detect when Instagram stories are reposts/screenshots of other users' content, then replace low-quality reposts with high-quality originals from the source.

**Key Features:**
- OCR-based repost detection (@username extraction)
- ImgInn for downloading both stories and posts
- Perceptual hash matching for content identification
- Smart account filtering (monitored vs non-monitored)
- Automatic cleanup of temporary downloads
- Database tracking of all replacements

---

## 1. System Architecture

### 1.1 Trigger Point

**Location:** `modules/move_module.py` in the `move_file()` method

**When:** After a file is moved from download directory, **BEFORE** face recognition check

**Affected Modules:** Any module that downloads Instagram stories:
- `instaloader_module.py`
- `imginn_module.py`
- `fastdl_module.py`
- `toolzu_module.py`

```python
# In move_module.py, line ~453 (before face recognition)
def move_file(self, source, destination, timestamp=None, ...):
    # ... existing code ...

    # NEW: Instagram story repost detection
    if self._is_instagram_story(source) and self.batch_context:
        source_username = self.batch_context.get('source', '')
        platform = self.batch_context.get('platform', '')

        if 'instagram' in platform.lower():
            replacement = self._check_repost_and_replace(str(source), source_username)
            if replacement:
                source = Path(replacement)
                self.log(f"Replaced repost with original", "success")

    # ... continue with face recognition ...
```

---

## 2. Detection Process (Step-by-Step)

### Step 2.1: OCR Username Extraction

Extract @username from repost overlay using OCR (Optical Character Recognition).

**Implementation:**
```python
def _extract_username_from_repost(self, file_path: str) -> Optional[str]:
    """
    Extract @username from repost overlay using OCR

    Handles both images and videos (multi-frame extraction for videos)
    """
    import pytesseract
    from PIL import Image
    import cv2

    # For images: direct OCR
    if file_path.endswith(('.jpg', '.jpeg', '.png')):
        img = Image.open(file_path)
        text = pytesseract.image_to_string(img)

    # For videos: extract first 3 frames and OCR each
    else:
        video = cv2.VideoCapture(file_path)
        frames_to_check = [0, int(video.get(cv2.CAP_PROP_FRAME_COUNT) * 0.1),
                          int(video.get(cv2.CAP_PROP_FRAME_COUNT) * 0.5)]

        text = ""
        for frame_num in frames_to_check:
            video.set(cv2.CAP_PROP_POS_FRAMES, frame_num)
            ret, frame = video.read()
            if ret:
                text += pytesseract.image_to_string(frame) + "\n"

    # Parse text to find @username
    import re
    matches = re.findall(r'@([a-zA-Z0-9._]+)', text)

    if matches:
        username = matches[0]  # First @username found
        self.log(f"Detected repost from @{username}")
        return username

    return None  # Not a repost
```

**Example:**
- Input: `evalongoria_20251109_154548_story6.mp4` (repost of @globalgiftfoundation)
- OCR finds: "@globalgiftfoundation"
- Returns: `"globalgiftfoundation"`

---

### Step 2.2: Check if Original User is Monitored

Determine if the original username is already being monitored for downloads.

**Implementation:**
```python
def _is_monitored_account(self, username: str) -> bool:
    """
    Check if username is in download_profiles or search_profiles

    Returns True if user is being actively monitored for downloads
    """
    with self.db.get_connection() as conn:
        cursor = conn.cursor()

        # Check download profiles (scheduled downloads)
        cursor.execute("""
            SELECT 1 FROM download_profiles
            WHERE platform IN ('instagram', 'instaloader', 'fastdl', 'imginn')
            AND source = ?
            AND enabled = 1
            LIMIT 1
        """, (username,))

        if cursor.fetchone():
            return True

        # Check search profiles (scheduled searches)
        cursor.execute("""
            SELECT 1 FROM search_profiles
            WHERE platform IN ('instagram', 'instaloader', 'fastdl', 'imginn')
            AND search_term = ?
            AND enabled = 1
            LIMIT 1
        """, (username,))

        return cursor.fetchone() is not None
```

**Example:**
- Original username: `globalgiftfoundation`
- Check: Is `globalgiftfoundation` in download_profiles or search_profiles?
- Result: `False` (not monitored)

---

### Step 2.3: Determine Download Strategy

Based on monitoring status, choose appropriate download path and database handling.

**Implementation:**
```python
# Monitored account: use normal download paths, add to database
is_monitored = self._is_monitored_account(original_username)

if is_monitored:
    download_path = Path("/opt/immich/md/instagram") / original_username
    add_to_database = True
    self.log(f"@{original_username} is monitored - using normal flow")

else:
    download_path = Path("/tmp/repost_detection") / original_username
    add_to_database = False
    self.log(f"@{original_username} NOT monitored - using temp queue")
```

**Strategy Table:**

| Scenario | Download Path | Database? | Cleanup? |
|----------|--------------|-----------|----------|
| **Monitored** | `/opt/immich/md/instagram/username/` | ✅ Yes | ❌ No - keep files |
| **Not Monitored** | `/tmp/repost_detection/username/` | ❌ No | ✅ Yes - after matching |

---

### Step 2.4: Check Fetch Cache

Avoid re-downloading the same user's content multiple times per day.

**Implementation:**
```python
def _already_fetched_today(self, username: str) -> bool:
    """
    Check if we already downloaded this user's content today

    Uses repost_fetch_cache table to track fetches
    """
    with self.db.get_connection() as conn:
        cursor = conn.cursor()

        # Create cache table if doesn't exist
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS repost_fetch_cache (
                username TEXT PRIMARY KEY,
                last_fetched TEXT NOT NULL,
                content_count INTEGER DEFAULT 0
            )
        """)

        # Check if fetched in last 12 hours
        cursor.execute("""
            SELECT last_fetched FROM repost_fetch_cache
            WHERE username = ?
            AND datetime(last_fetched) > datetime('now', '-12 hours')
        """, (username,))

        result = cursor.fetchone()
        return result is not None
```

**Cache Duration:** 12 hours
- Stories expire after 24 hours
- Re-check every 12 hours catches new stories/posts
- Avoids excessive API calls

---

### Step 2.5: Download Content Using ImgInn

Download both stories and recent posts (last 24 hours) from the original user.

**Implementation:**
```python
def _download_content_via_imginn(self, username: str, destination: Path, add_to_database: bool):
    """
    Download stories AND recent posts from user via ImgInn

    Args:
        username: Instagram username
        destination: Where to save (normal path or /tmp)
        add_to_database: If False, skip database recording (temp processing)
    """
    # Import imginn module
    from modules.imginn_module import ImgInnModule

    # Initialize ImgInn (pass add_to_database flag)
    imginn = ImgInnModule(
        unified_db=self.db if add_to_database else None,  # No DB if temp
        log_callback=self.log
    )

    # Create destination directories
    stories_dir = destination / "stories"
    posts_dir = destination / "posts"
    stories_dir.mkdir(parents=True, exist_ok=True)
    posts_dir.mkdir(parents=True, exist_ok=True)

    # Download stories
    self.log(f"Downloading stories from @{username} via ImgInn...")
    imginn.download_user_stories(
        username=username,
        destination=stories_dir,
        skip_database=not add_to_database  # NEW parameter
    )

    # Download recent posts (last 24 hours only)
    self.log(f"Downloading recent posts from @{username} via ImgInn...")
    imginn.download_user_posts(
        username=username,
        destination=posts_dir,
        max_age_hours=24,  # NEW parameter - only last 24 hours
        skip_database=not add_to_database
    )

    # Mark this fetch in cache
    self._mark_fetched(username)
```

**Downloaded Content:**
1. **Stories:** All current stories (expire after 24h)
2. **Posts:** Only posts from last 24 hours

**File Structure Example:**

```
# Not Monitored (temp queue):
/tmp/repost_detection/globalgiftfoundation/
├── stories/
│   ├── globalgiftfoundation_20251109_120519_576094201.jpg
│   ├── globalgiftfoundation_20251109_124515_AQP029c.mp4
│   └── ...
└── posts/
    ├── globalgiftfoundation_post_20251109_100000.jpg
    └── ...

# Monitored (normal flow):
/opt/immich/md/instagram/globalgiftfoundation/
├── stories/
│   └── ...
└── posts/
    └── ...
```

---

### Step 2.6: Perceptual Hash Matching

Find the original content by comparing perceptual hashes.

**Implementation:**
```python
def _find_matching_original(self, repost_path: str, search_dir: Path) -> Optional[str]:
    """
    Find matching original content using perceptual hashing

    Searches both stories/ and posts/ subdirectories

    Args:
        repost_path: Path to the repost file (e.g., evalongoria story)
        search_dir: Directory to search (e.g., /tmp/.../globalgiftfoundation/)

    Returns:
        Path to best matching original, or None
    """
    import imagehash
    from PIL import Image
    import cv2

    # Calculate hash of repost
    repost_hash = self._get_perceptual_hash(repost_path)
    if not repost_hash:
        return None

    # Search both stories and posts
    best_match = None
    best_distance = 999
    threshold = 10  # Hamming distance threshold (0-64 scale)

    for subdir in ["stories", "posts"]:
        content_dir = search_dir / subdir
        if not content_dir.exists():
            continue

        for file_path in content_dir.rglob("*"):
            if not file_path.is_file():
                continue

            # Skip non-media files
            if file_path.suffix.lower() not in ['.jpg', '.jpeg', '.png', '.mp4', '.mov']:
                continue

            # Calculate hash
            candidate_hash = self._get_perceptual_hash(str(file_path))
            if not candidate_hash:
                continue

            # Compare (Hamming distance)
            distance = repost_hash - candidate_hash

            if distance < threshold and distance < best_distance:
                best_distance = distance
                best_match = str(file_path)
                self.log(f"Better match found: {file_path.name} (distance: {distance})")

    if best_match:
        self.log(f"✓ Found original: {Path(best_match).name} (distance: {best_distance})")
        return best_match
    else:
        self.log(f"✗ No matching original found for {Path(repost_path).name}")
        return None


def _get_perceptual_hash(self, file_path: str):
    """Calculate perceptual hash for image or video"""
    import imagehash
    from PIL import Image
    import cv2

    try:
        # Image: direct hash
        if file_path.endswith(('.jpg', '.jpeg', '.png')):
            img = Image.open(file_path)
            return imagehash.dhash(img)  # Difference hash (good for cropped/resized)

        # Video: hash middle frame
        else:
            video = cv2.VideoCapture(file_path)
            frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
            video.set(cv2.CAP_PROP_POS_FRAMES, frame_count // 2)  # Middle frame
            ret, frame = video.read()

            if ret:
                # Convert BGR to RGB
                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                img = Image.fromarray(frame_rgb)
                return imagehash.dhash(img)

    except Exception as e:
        self.log(f"Hash calculation failed for {Path(file_path).name}: {e}", "debug")
        return None
```

**Hash Comparison Guide:**
- **Hamming Distance 0:** Identical images
- **Hamming Distance 1-5:** Very similar (same image, minor edits)
- **Hamming Distance 6-10:** Similar (likely same content, different quality)
- **Hamming Distance >10:** Different content

**Example Matching Process:**
```
Repost: evalongoria_story6.mp4 (screenshot of @globalgiftfoundation's post)
Hash: 0110101011001010...

Checking globalgiftfoundation/stories/*.* ... no match
Checking globalgiftfoundation/posts/*.* ...
  - post_100000.jpg: distance 3 ✓ MATCH!

Best match: globalgiftfoundation_post_20251109_100000.jpg (distance: 3)
```

---

### Step 2.7: Replace Repost with Original

Replace the low-quality repost with the high-quality original.

**Implementation:**
```python
def _replace_repost_with_original(self, repost_path: str, original_path: str) -> str:
    """
    Replace repost file with original high-quality file

    Args:
        repost_path: Path to repost (e.g., evalongoria story)
        original_path: Path to original (e.g., globalgiftfoundation post)

    Returns:
        Path to replacement file (copied to repost's location)
    """
    import shutil

    repost_file = Path(repost_path)
    original_file = Path(original_path)

    # Create replacement filename (keep original naming convention)
    # Example: evalongoria_story6.mp4 → evalongoria_story6_ORIGINAL.jpg
    replacement_name = f"{repost_file.stem}_ORIGINAL{original_file.suffix}"
    replacement_path = repost_file.parent / replacement_name

    # Copy original to repost location
    shutil.copy2(original_file, replacement_path)
    self.log(f"Copied original: {original_file.name} → {replacement_path.name}")

    # Delete the repost (low quality screenshot)
    try:
        repost_file.unlink()
        self.log(f"Deleted repost: {repost_file.name}")
    except Exception as e:
        self.log(f"Failed to delete repost: {e}", "warning")

    # Update database to track replacement
    if self.db:
        self._record_repost_replacement(
            repost_path=str(repost_file),
            original_path=str(original_file),
            replacement_path=str(replacement_path)
        )

    return str(replacement_path)
```

**File Transformation Example:**
```
Before:
  /tmp/downloads/evalongoria/evalongoria_story6.mp4 (repost, 720p)

After:
  /tmp/downloads/evalongoria/evalongoria_story6_ORIGINAL.jpg (original, 1080p)
```

---

### Step 2.8: Database Tracking

Track all repost replacements in the database for auditing.

**Implementation:**
```python
def _record_repost_replacement(self, repost_path: str, original_path: str, replacement_path: str):
    """
    Track repost replacements in database

    Creates repost_replacements table to track what was replaced
    """
    with self.db.get_connection(for_write=True) as conn:
        cursor = conn.cursor()

        # Create tracking table
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS repost_replacements (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                repost_path TEXT NOT NULL,
                repost_filename TEXT NOT NULL,
                repost_source TEXT NOT NULL,
                original_path TEXT NOT NULL,
                original_username TEXT NOT NULL,
                replacement_path TEXT NOT NULL,
                detected_at TEXT DEFAULT CURRENT_TIMESTAMP,
                hash_distance INTEGER
            )
        """)

        # Extract usernames
        repost_source = Path(repost_path).parent.name  # e.g., evalongoria
        original_username = self._extract_username_from_path(original_path)

        # Insert record
        cursor.execute("""
            INSERT INTO repost_replacements
            (repost_path, repost_filename, repost_source, original_path, original_username, replacement_path)
            VALUES (?, ?, ?, ?, ?, ?)
        """, (
            repost_path,
            Path(repost_path).name,
            repost_source,
            original_path,
            original_username,
            replacement_path
        ))

        self.log(f"Recorded replacement: {repost_source} → @{original_username}")
```

**Database Schema:**
```sql
CREATE TABLE repost_replacements (
    id INTEGER PRIMARY KEY,
    repost_path TEXT,           -- Original repost file path
    repost_filename TEXT,        -- evalongoria_story6.mp4
    repost_source TEXT,          -- evalongoria
    original_path TEXT,          -- Path to high-quality original
    original_username TEXT,      -- globalgiftfoundation
    replacement_path TEXT,       -- Final file path
    detected_at TEXT,            -- Timestamp
    hash_distance INTEGER        -- How similar (0-10)
)
```

---

### Step 2.9: Cleanup Temporary Files

Clean up downloaded content for non-monitored accounts.

**Implementation:**
```python
def _cleanup_temp_downloads(self, temp_dir: Path, keep_file: str = None):
    """
    Clean up temporary downloads for non-monitored accounts

    Args:
        temp_dir: Directory to clean (e.g., /tmp/repost_detection/username/)
        keep_file: Optional file to preserve (the matched original)
    """
    import shutil

    keep_path = Path(keep_file) if keep_file else None
    deleted_count = 0

    # Delete all files except the keeper
    for file_path in temp_dir.rglob("*"):
        if not file_path.is_file():
            continue

        if keep_path and file_path == keep_path:
            continue  # Skip the matched file

        try:
            file_path.unlink()
            deleted_count += 1
        except Exception as e:
            self.log(f"Failed to delete temp file: {e}", "debug")

    # Remove empty directories
    try:
        for subdir in [temp_dir / "stories", temp_dir / "posts"]:
            if subdir.exists() and not any(subdir.iterdir()):
                subdir.rmdir()

        if not any(temp_dir.iterdir()):
            temp_dir.rmdir()

    except Exception as e:
        self.log(f"Failed to cleanup directories: {e}", "debug")

    self.log(f"Cleaned up {deleted_count} temporary files")
```

**Cleanup Logic:**

| Account Status | Files Downloaded | What Gets Cleaned? |
|----------------|------------------|-------------------|
| **Monitored** | Stories + Posts → `/opt/immich/md/instagram/user/` | Nothing - keep all files |
| **Not Monitored** | Stories + Posts → `/tmp/repost_detection/user/` | Delete all except matched file |

---

## 3. Required Module Changes

### 3.1 ImgInn Module - Add `skip_database` Parameter

**File:** `modules/imginn_module.py`

**Modification 1 - Stories Download:**
```python
def download_user_stories(self, username: str, destination: Path, skip_database: bool = False):
    """
    Download all stories from user

    Args:
        skip_database: If True, don't record downloads (for temp processing)
    """
    # ... fetch stories from ImgInn ...

    for story_url in story_urls:
        file_path = self._download_file(story_url, destination)

        # Only record if not skipping database
        if not skip_database and self.unified_db:
            self.unified_db.record_download(
                url=story_url,
                file_path=str(file_path),
                platform='imginn',
                source=username,
                content_type='story'
            )
```

**Modification 2 - Posts Download with Time Filter:**
```python
def download_user_posts(self, username: str, destination: Path,
                       max_age_hours: int = None, skip_database: bool = False):
    """
    Download posts from user

    Args:
        max_age_hours: Only download posts newer than N hours (e.g., 24)
        skip_database: If True, don't record downloads
    """
    from datetime import datetime, timedelta

    # ... fetch posts from ImgInn ...

    # Filter by age if specified
    if max_age_hours:
        cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
        post_urls = [p for p in post_urls if p['timestamp'] > cutoff_time]

    for post in post_urls:
        file_path = self._download_file(post['url'], destination)

        # Only record if not skipping database
        if not skip_database and self.unified_db:
            self.unified_db.record_download(
                url=post['url'],
                file_path=str(file_path),
                platform='imginn',
                source=username,
                content_type='post'
            )
```

---

### 3.2 Move Module - Add Detection Method

**File:** `modules/move_module.py`

**Add Methods:**
```python
def _is_instagram_story(self, file_path: Path) -> bool:
    """Check if file is an Instagram story"""
    path_str = str(file_path).lower()
    return 'story' in path_str or 'stories' in path_str

def _check_repost_and_replace(self, file_path: str, source_username: str) -> Optional[str]:
    """
    Check if file is Instagram story repost and replace with original

    Returns replacement file path if found, None otherwise
    """
    try:
        from modules.instagram_repost_detector import InstagramRepostDetector

        # Initialize detector
        detector = InstagramRepostDetector(
            unified_db=self.unified_db,
            log_callback=self.log
        )

        # Run detection
        replacement = detector.check_and_replace_repost(file_path, source_username)

        return replacement

    except Exception as e:
        self.log(f"Repost detection failed: {e}", "debug")
        return None
```

---

## 4. Configuration & Settings

### 4.1 Database Settings

```sql
-- Add repost detection settings
INSERT INTO settings (key, value) VALUES ('repost_detection', '{
    "enabled": true,
    "ocr_confidence_threshold": 60,
    "hash_distance_threshold": 10,
    "fetch_cache_hours": 12,
    "max_posts_age_hours": 24,
    "cleanup_temp_files": true
}');
```

### 4.2 Frontend Settings Interface

```typescript
interface RepostDetectionSettings {
    enabled: boolean;                   // Enable/disable repost detection
    ocrConfidenceThreshold: number;     // OCR confidence (0-100)
    hashDistanceThreshold: number;      // Max Hamming distance (0-64)
    fetchCacheHours: number;            // How long to cache fetches (default: 12)
    maxPostsAgeHours: number;           // How far back to check posts (default: 24)
    cleanupTempFiles: boolean;          // Delete non-monitored downloads
}
```

---

## 5. Complete Workflow Example

**Scenario:** Eva Longoria reposts a story from Global Gift Foundation

```
1. USER: evalongoria (monitored)
   - Posts story: evalongoria_story6.mp4
   - Story is a repost of @globalgiftfoundation's post

2. IMGINN downloads story to /tmp/downloads/evalongoria/

3. MOVE MODULE starts moving file

4. REPOST DETECTOR triggers:
   ├─ OCR extracts: "@globalgiftfoundation"
   ├─ Check: Is globalgiftfoundation monitored? → NO
   ├─ Use temp queue: /tmp/repost_detection/globalgiftfoundation/
   ├─ Check cache: Already fetched today? → NO
   ├─ Download via ImgInn:
   │  ├─ Stories → /tmp/repost_detection/globalgiftfoundation/stories/
   │  └─ Posts (24h) → /tmp/repost_detection/globalgiftfoundation/posts/
   ├─ Perceptual hash matching:
   │  ├─ Repost hash: 0110101011001010...
   │  ├─ Check stories/*.* → no match
   │  ├─ Check posts/*.* → MATCH! (distance: 3)
   │  └─ Best: globalgiftfoundation_post_100000.jpg
   ├─ Replace repost:
   │  ├─ Copy: post_100000.jpg → evalongoria_story6_ORIGINAL.jpg
   │  └─ Delete: evalongoria_story6.mp4 (repost)
   ├─ Record in database (repost_replacements table)
   └─ Cleanup: Delete all temp files in /tmp/repost_detection/

5. MOVE MODULE continues with replacement file
   ├─ Face recognition check
   └─ Move to final destination: /opt/immich/md/evalongoria/...

6. RESULT: High-quality original instead of low-quality repost screenshot
```

---

## 6. Edge Cases & Error Handling

| Situation | Behavior |
|-----------|----------|
| **No @username detected** | Skip - not a repost |
| **Original user IS monitored** | Use normal download path, keep all files |
| **Original user NOT monitored** | Use temp queue, cleanup after matching |
| **No matching content found** | Keep repost as-is, log warning |
| **Already fetched today** | Skip download, search cached files |
| **OCR fails** | Skip detection, process normally |
| **ImgInn download fails** | Log error, keep original repost |
| **Video repost** | Multi-frame OCR + hash middle frame |
| **Multiple matches found** | Use best match (lowest hash distance) |
| **Perceptual hash calculation fails** | Skip detection, process normally |

---

## 7. Performance Considerations

### 7.1 Optimization Strategies

**Fetch Caching:**
- Cache username fetches for 12 hours
- Avoids re-downloading same user multiple times per day
- Stored in `repost_fetch_cache` table

**Async Downloads:**
- ImgInn downloads could run in background (future enhancement)
- Don't block move operation for other files

**Smart Cleanup:**
- Only cleanup temp files for non-monitored accounts
- Monitored accounts keep all files for normal processing

**Hash Pre-computation:**
- Could pre-compute hashes for downloaded content (future enhancement)
- Speeds up matching when multiple reposts detected

### 7.2 Resource Usage

**Disk Space:**
- Temporary downloads: ~50-200MB per user (stories + posts)
- Cleaned up after matching (non-monitored accounts only)

**Network:**
- One-time download per user per 12 hours
- Average: 20-50 files per user (stories + recent posts)

**CPU:**
- OCR: ~1-2 seconds per file
- Perceptual hashing: ~0.5 seconds per file
- Total overhead: ~5-10 seconds per repost detected

**Memory:**
- ImgInn module: ~50-100MB
- OpenCV/PIL: ~100-200MB
- Tesseract: ~50MB

---

## 8. Dependencies

### 8.1 Python Libraries

```bash
pip3 install imagehash pillow opencv-python pytesseract
```

### 8.2 System Packages

```bash
# Ubuntu/Debian
sudo apt-get install tesseract-ocr tesseract-ocr-eng

# Check installation
tesseract --version
```

---

## 9. Testing Strategy

### 9.1 Unit Tests

```python
# Test OCR username extraction
def test_ocr_extraction():
    detector = InstagramRepostDetector(...)
    username = detector._extract_username_from_repost("repost_example.jpg")
    assert username == "expectedusername"

# Test monitored account check
def test_monitored_check():
    detector = InstagramRepostDetector(...)
    assert detector._is_monitored_account("evalongoria") == True
    assert detector._is_monitored_account("randomuser") == False

# Test perceptual hash matching
def test_hash_matching():
    detector = InstagramRepostDetector(...)
    match = detector._find_matching_original("repost.jpg", Path("/tmp/test/"))
    assert match is not None
```

### 9.2 Integration Tests

```python
# Test full workflow
def test_full_repost_detection():
    # Create test repost file with @username overlay
    # Run detection
    # Verify replacement file exists
    # Verify database entry created
    # Verify temp files cleaned up
```

### 9.3 Manual Testing Checklist

- [ ] Upload story with @username overlay
- [ ] Verify OCR extracts correct username
- [ ] Verify monitored account check works
- [ ] Verify non-monitored account uses temp queue
- [ ] Verify content is downloaded via ImgInn
- [ ] Verify perceptual hash finds match
- [ ] Verify repost is replaced with original
- [ ] Verify database tracking works
- [ ] Verify temp files are cleaned up
- [ ] Verify works for both images and videos

---

## 10. Future Enhancements

### 10.1 Potential Improvements

**Machine Learning Enhancement:**
- Train ML model to detect repost overlays more reliably
- Reduce false positives from OCR errors

**Multi-source Support:**
- Support TikTok reposts
- Support Twitter/X reposts
- Support YouTube Shorts reposts

**Batch Processing:**
- Process multiple reposts in parallel
- Shared download for same original user

**UI Features:**
- Dashboard showing repost detection statistics
- Manual review queue for uncertain matches
- Settings page for thresholds and configuration

**Advanced Matching:**
- Video fingerprinting for better video matching
- Audio fingerprinting for video content
- EXIF metadata comparison

### 10.2 Known Limitations

**OCR Accuracy:**
- May fail on stylized fonts or low-resolution images
- Can be confused by similar text patterns

**Perceptual Hashing:**
- May fail on heavily cropped or filtered content
- False positives possible with similar-looking images

**Content Availability:**
- Original content may not be available if deleted
- Stories expire after 24 hours
- Posts may be deleted or made private

**Performance:**
- Processing adds 5-10 seconds per repost
- Network dependency for content downloads

---

## 11. Maintenance & Monitoring

### 11.1 Logging

All operations are logged with appropriate levels:
- **DEBUG:** Hash calculations, file operations
- **INFO:** Repost detection, matching results
- **WARNING:** OCR failures, missing content
- **ERROR:** Download failures, database errors

### 11.2 Metrics to Track

- Reposts detected per day
- Successful replacements vs failures
- Average processing time per repost
- Cache hit rate (fetches avoided)
- Non-monitored account downloads (temp queue usage)

### 11.3 Database Queries

```sql
-- Repost detection statistics
SELECT
    repost_source,
    original_username,
    COUNT(*) as replacement_count,
    AVG(hash_distance) as avg_distance
FROM repost_replacements
GROUP BY repost_source, original_username
ORDER BY replacement_count DESC;

-- Cache efficiency
SELECT
    COUNT(*) as total_fetches,
    SUM(content_count) as total_files_downloaded
FROM repost_fetch_cache
WHERE last_fetched > datetime('now', '-7 days');

-- Recent replacements
SELECT * FROM repost_replacements
ORDER BY detected_at DESC
LIMIT 50;
```

---

## 12. Summary

### Why This Design Works

✅ **Quality:** ImgInn for both stories and posts (consistent quality)
✅ **Smart:** Only adds monitored accounts to database
✅ **Efficient:** Caches fetches, avoids re-downloading
✅ **Clean:** Auto-cleanup temp files for non-monitored accounts
✅ **Trackable:** Database records all replacements
✅ **Flexible:** Works with images and videos
✅ **Reliable:** Perceptual hashing handles crops/resizes
✅ **Maintainable:** Clear separation of concerns, modular design

### Key Benefits

1. **Bandwidth Savings:** Download original once, not multiple low-quality reposts
2. **Storage Optimization:** High-quality originals vs many low-quality screenshots
3. **User Experience:** Consistent quality across all content
4. **Database Hygiene:** Non-monitored accounts don't pollute database
5. **Auditable:** Complete tracking of all replacements

---

## 13. Implementation Checklist

- [ ] Create `modules/instagram_repost_detector.py` module
- [ ] Modify `modules/imginn_module.py` (add skip_database parameter)
- [ ] Modify `modules/move_module.py` (add detection trigger)
- [ ] Add database tables (repost_fetch_cache, repost_replacements)
- [ ] Add settings table entry (repost_detection config)
- [ ] Install dependencies (imagehash, pytesseract, tesseract-ocr)
- [ ] Create unit tests
- [ ] Create integration tests
- [ ] Update frontend settings page
- [ ] Update documentation
- [ ] Manual testing with real reposts
- [ ] Monitor performance and tune thresholds

---

**Document Version:** 1.0
**Last Updated:** 2025-11-09
**Author:** Claude Code
**Status:** Design Complete - Awaiting Implementation Approval
