Upload 5 files
Browse files- binary_processor.py +893 -0
- media.py +1338 -0
- models.py +777 -0
- mydocs.py +741 -0
- vision.py +282 -0
binary_processor.py
ADDED
|
@@ -0,0 +1,893 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Binary File Processor for HenAi
|
| 3 |
+
Extracts metadata, text, and structured data from various binary file formats
|
| 4 |
+
- Multi-backend audio processing with fallbacks
|
| 5 |
+
- OCR using EasyOCR (no external dependencies)
|
| 6 |
+
- Comprehensive file type support
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import io
|
| 10 |
+
import os
|
| 11 |
+
import tempfile
|
| 12 |
+
from typing import Dict, Any, Optional, Tuple
|
| 13 |
+
|
| 14 |
+
# ============= TRY IMPORTS WITH FALLBACKS =============
|
| 15 |
+
|
| 16 |
+
# Image processing
|
| 17 |
+
try:
|
| 18 |
+
from PIL import Image, ImageOps, ImageEnhance
|
| 19 |
+
PIL_AVAILABLE = True
|
| 20 |
+
except ImportError:
|
| 21 |
+
PIL_AVAILABLE = False
|
| 22 |
+
print("Warning: PIL/Pillow not available. Install with: pip install Pillow")
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
import exifread
|
| 26 |
+
EXIF_AVAILABLE = True
|
| 27 |
+
except ImportError:
|
| 28 |
+
EXIF_AVAILABLE = False
|
| 29 |
+
|
| 30 |
+
# OCR (Pure Python, no external dependencies)
|
| 31 |
+
try:
|
| 32 |
+
import easyocr
|
| 33 |
+
EASYOCR_AVAILABLE = True
|
| 34 |
+
_easyocr_reader = None
|
| 35 |
+
except ImportError:
|
| 36 |
+
EASYOCR_AVAILABLE = False
|
| 37 |
+
print("Warning: EasyOCR not available. Install with: pip install easyocr")
|
| 38 |
+
|
| 39 |
+
# Audio processing - multiple backends
|
| 40 |
+
try:
|
| 41 |
+
from pydub import AudioSegment
|
| 42 |
+
PYDUB_AVAILABLE = True
|
| 43 |
+
except ImportError:
|
| 44 |
+
PYDUB_AVAILABLE = False
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
import speech_recognition as sr
|
| 48 |
+
SPEECH_RECOGNITION_AVAILABLE = True
|
| 49 |
+
except ImportError:
|
| 50 |
+
SPEECH_RECOGNITION_AVAILABLE = False
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
import mutagen
|
| 54 |
+
MUTAGEN_AVAILABLE = True
|
| 55 |
+
except ImportError:
|
| 56 |
+
MUTAGEN_AVAILABLE = False
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
import audioread
|
| 60 |
+
AUDIOREAD_AVAILABLE = True
|
| 61 |
+
except ImportError:
|
| 62 |
+
AUDIOREAD_AVAILABLE = False
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
import librosa
|
| 66 |
+
LIBROSA_AVAILABLE = True
|
| 67 |
+
except ImportError:
|
| 68 |
+
LIBROSA_AVAILABLE = False
|
| 69 |
+
|
| 70 |
+
# PDF processing
|
| 71 |
+
try:
|
| 72 |
+
import pdfplumber
|
| 73 |
+
PDFPLUMBER_AVAILABLE = True
|
| 74 |
+
except ImportError:
|
| 75 |
+
PDFPLUMBER_AVAILABLE = False
|
| 76 |
+
|
| 77 |
+
# Spreadsheet processing
|
| 78 |
+
try:
|
| 79 |
+
import pandas as pd
|
| 80 |
+
PANDAS_AVAILABLE = True
|
| 81 |
+
except ImportError:
|
| 82 |
+
PANDAS_AVAILABLE = False
|
| 83 |
+
|
| 84 |
+
try:
|
| 85 |
+
import openpyxl
|
| 86 |
+
OPENPYXL_AVAILABLE = True
|
| 87 |
+
except ImportError:
|
| 88 |
+
OPENPYXL_AVAILABLE = False
|
| 89 |
+
|
| 90 |
+
# Document processing
|
| 91 |
+
try:
|
| 92 |
+
from docx import Document
|
| 93 |
+
DOCX_AVAILABLE = True
|
| 94 |
+
except ImportError:
|
| 95 |
+
DOCX_AVAILABLE = False
|
| 96 |
+
|
| 97 |
+
try:
|
| 98 |
+
from pptx import Presentation
|
| 99 |
+
PPTX_AVAILABLE = True
|
| 100 |
+
except ImportError:
|
| 101 |
+
PPTX_AVAILABLE = False
|
| 102 |
+
|
| 103 |
+
# Archive processing
|
| 104 |
+
try:
|
| 105 |
+
import zipfile
|
| 106 |
+
import tarfile
|
| 107 |
+
ARCHIVE_AVAILABLE = True
|
| 108 |
+
except ImportError:
|
| 109 |
+
ARCHIVE_AVAILABLE = False
|
| 110 |
+
|
| 111 |
+
# Encoding detection
|
| 112 |
+
try:
|
| 113 |
+
import chardet
|
| 114 |
+
CHARDET_AVAILABLE = True
|
| 115 |
+
except ImportError:
|
| 116 |
+
CHARDET_AVAILABLE = False
|
| 117 |
+
|
| 118 |
+
# Whisper for advanced transcription (optional)
|
| 119 |
+
try:
|
| 120 |
+
import whisper
|
| 121 |
+
WHISPER_AVAILABLE = True
|
| 122 |
+
except ImportError:
|
| 123 |
+
WHISPER_AVAILABLE = False
|
| 124 |
+
|
| 125 |
+
# Video processing
|
| 126 |
+
try:
|
| 127 |
+
import cv2
|
| 128 |
+
import numpy as np
|
| 129 |
+
CV2_AVAILABLE = True
|
| 130 |
+
except ImportError:
|
| 131 |
+
CV2_AVAILABLE = False
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def get_easyocr_reader():
|
| 135 |
+
"""Lazy initialization of EasyOCR reader"""
|
| 136 |
+
global _easyocr_reader
|
| 137 |
+
if _easyocr_reader is None and EASYOCR_AVAILABLE:
|
| 138 |
+
try:
|
| 139 |
+
# Use CPU only, English language
|
| 140 |
+
_easyocr_reader = easyocr.Reader(['en'], gpu=False)
|
| 141 |
+
print("EasyOCR initialized successfully")
|
| 142 |
+
except Exception as e:
|
| 143 |
+
print(f"Failed to initialize EasyOCR: {e}")
|
| 144 |
+
return _easyocr_reader
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class BinaryProcessor:
|
| 148 |
+
"""Main processor for all binary file types"""
|
| 149 |
+
|
| 150 |
+
def __init__(self):
|
| 151 |
+
self.initialize_handlers()
|
| 152 |
+
|
| 153 |
+
def initialize_handlers(self):
|
| 154 |
+
"""Initialize all format-specific handlers"""
|
| 155 |
+
self.handlers = {
|
| 156 |
+
'image': self.process_image,
|
| 157 |
+
'audio': self.process_audio,
|
| 158 |
+
'video': self.process_video,
|
| 159 |
+
'pdf': self.process_pdf,
|
| 160 |
+
'spreadsheet': self.process_spreadsheet,
|
| 161 |
+
'word': self.process_word_document,
|
| 162 |
+
'presentation': self.process_presentation,
|
| 163 |
+
'archive': self.process_archive,
|
| 164 |
+
'database': self.process_database,
|
| 165 |
+
'text': self.process_text_file,
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
def process_file(self, file_content: bytes, filename: str) -> str:
|
| 169 |
+
"""
|
| 170 |
+
Main entry point - processes any file and returns formatted text for AI
|
| 171 |
+
"""
|
| 172 |
+
file_ext = filename.split('.')[-1].lower() if '.' in filename else ''
|
| 173 |
+
|
| 174 |
+
# Build output header
|
| 175 |
+
output = f"\n\n--- FILE: {filename} ---\n"
|
| 176 |
+
output += f"Size: {len(file_content)} bytes\n"
|
| 177 |
+
|
| 178 |
+
# Route to appropriate handler based on extension
|
| 179 |
+
if file_ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp', 'ico']:
|
| 180 |
+
output += self.process_image(file_content, filename)
|
| 181 |
+
elif file_ext in ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus']:
|
| 182 |
+
output += self.process_audio(file_content, filename)
|
| 183 |
+
elif file_ext in ['mp4', 'avi', 'mov', 'mkv', 'webm', 'flv', 'wmv']:
|
| 184 |
+
output += self.process_video(file_content, filename)
|
| 185 |
+
elif file_ext == 'pdf':
|
| 186 |
+
output += self.process_pdf(file_content, filename)
|
| 187 |
+
elif file_ext in ['xlsx', 'xls', 'csv', 'xlsm', 'xlsb']:
|
| 188 |
+
output += self.process_spreadsheet(file_content, filename)
|
| 189 |
+
elif file_ext in ['docx', 'doc', 'odt']:
|
| 190 |
+
output += self.process_word_document(file_content, filename)
|
| 191 |
+
elif file_ext in ['pptx', 'ppt', 'odp']:
|
| 192 |
+
output += self.process_presentation(file_content, filename)
|
| 193 |
+
elif file_ext in ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz']:
|
| 194 |
+
output += self.process_archive(file_content, filename)
|
| 195 |
+
elif file_ext in ['db', 'sqlite', 'sqlite3', 'db3']:
|
| 196 |
+
output += self.process_database(file_content, filename)
|
| 197 |
+
elif file_ext in ['txt', 'md', 'py', 'js', 'html', 'css', 'json', 'xml',
|
| 198 |
+
'java', 'c', 'cpp', 'h', 'hpp', 'rb', 'php', 'go', 'rs',
|
| 199 |
+
'swift', 'kt', 'ts', 'jsx', 'tsx', 'vue']:
|
| 200 |
+
output += self.process_text_file(file_content, filename)
|
| 201 |
+
else:
|
| 202 |
+
# Try text extraction as fallback - INCREASED LIMIT
|
| 203 |
+
text_result = self.try_extract_text(file_content)
|
| 204 |
+
if text_result:
|
| 205 |
+
output += f"\n--- EXTRACTED TEXT ---\n{text_result}\n--- END TEXT ---\n"
|
| 206 |
+
else:
|
| 207 |
+
output += f"\n[Binary file: {filename}]\n"
|
| 208 |
+
output += "No further extraction available for this file type.\n"
|
| 209 |
+
|
| 210 |
+
output += "--- END FILE ---\n\n"
|
| 211 |
+
return output
|
| 212 |
+
|
| 213 |
+
def process_image(self, content: bytes, filename: str) -> str:
|
| 214 |
+
"""Extract image metadata and perform OCR using EasyOCR (no external dependencies)"""
|
| 215 |
+
output = "\n--- IMAGE ANALYSIS ---\n"
|
| 216 |
+
|
| 217 |
+
if not PIL_AVAILABLE:
|
| 218 |
+
output += "❌ Image processing not available (Pillow not installed)\n"
|
| 219 |
+
output += "Install with: pip install Pillow\n"
|
| 220 |
+
output += "--- END IMAGE ANALYSIS ---\n"
|
| 221 |
+
return output
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
img = Image.open(io.BytesIO(content))
|
| 225 |
+
output += f"📐 Dimensions: {img.width}x{img.height}\n"
|
| 226 |
+
output += f"🎨 Format: {img.format}\n"
|
| 227 |
+
output += f"🖼️ Mode: {img.mode}\n"
|
| 228 |
+
|
| 229 |
+
# EXIF data
|
| 230 |
+
if EXIF_AVAILABLE:
|
| 231 |
+
try:
|
| 232 |
+
with io.BytesIO(content) as f:
|
| 233 |
+
tags = exifread.process_file(f)
|
| 234 |
+
if tags:
|
| 235 |
+
output += "\n📷 EXIF DATA:\n"
|
| 236 |
+
for tag, value in list(tags.items())[:10]:
|
| 237 |
+
output += f" • {tag}: {value}\n"
|
| 238 |
+
except:
|
| 239 |
+
pass
|
| 240 |
+
|
| 241 |
+
# OCR for text in images using EasyOCR
|
| 242 |
+
if EASYOCR_AVAILABLE:
|
| 243 |
+
try:
|
| 244 |
+
reader = get_easyocr_reader()
|
| 245 |
+
if reader:
|
| 246 |
+
# Scale image if too large (improves OCR speed)
|
| 247 |
+
if img.width > 1500 or img.height > 1500:
|
| 248 |
+
img.thumbnail((1500, 1500))
|
| 249 |
+
output += f"\n📏 Image scaled for OCR\n"
|
| 250 |
+
|
| 251 |
+
# Convert PIL image to numpy array
|
| 252 |
+
import numpy as np
|
| 253 |
+
img_array = np.array(img)
|
| 254 |
+
|
| 255 |
+
# Run OCR
|
| 256 |
+
output += "\n🔍 OCR PROCESSING:\n"
|
| 257 |
+
results = reader.readtext(img_array)
|
| 258 |
+
|
| 259 |
+
if results:
|
| 260 |
+
extracted_text = []
|
| 261 |
+
high_confidence_text = []
|
| 262 |
+
|
| 263 |
+
for (bbox, text, confidence) in results:
|
| 264 |
+
if confidence > 0.5:
|
| 265 |
+
high_confidence_text.append(text)
|
| 266 |
+
extracted_text.append(text)
|
| 267 |
+
|
| 268 |
+
if high_confidence_text:
|
| 269 |
+
full_text = ' '.join(high_confidence_text)
|
| 270 |
+
output += f"✅ Extracted {len(full_text)} characters (high confidence)\n"
|
| 271 |
+
output += f"\n📝 EXTRACTED TEXT:\n{full_text.strip()}\n"
|
| 272 |
+
elif extracted_text:
|
| 273 |
+
full_text = ' '.join(extracted_text)
|
| 274 |
+
output += f"⚠️ Extracted {len(full_text)} characters (low confidence)\n"
|
| 275 |
+
output += f"\n📝 EXTRACTED TEXT:\n{full_text.strip()}\n"
|
| 276 |
+
else:
|
| 277 |
+
output += "❌ No readable text detected in image\n"
|
| 278 |
+
else:
|
| 279 |
+
output += "❌ No text detected in the image\n"
|
| 280 |
+
except Exception as e:
|
| 281 |
+
output += f"\n⚠️ OCR processing error: {str(e)}\n"
|
| 282 |
+
output += "Make sure EasyOCR is installed: pip install easyocr\n"
|
| 283 |
+
else:
|
| 284 |
+
output += "\n⚠️ EasyOCR not installed. Install with: pip install easyocr\n"
|
| 285 |
+
output += "This will enable text extraction from images without external dependencies.\n"
|
| 286 |
+
|
| 287 |
+
output += "--- END IMAGE ANALYSIS ---\n"
|
| 288 |
+
|
| 289 |
+
except Exception as e:
|
| 290 |
+
output += f"❌ Error processing image: {str(e)}\n"
|
| 291 |
+
|
| 292 |
+
return output
|
| 293 |
+
|
| 294 |
+
def extract_ocr_text(self, image_content: bytes, filename: str) -> str:
|
| 295 |
+
"""
|
| 296 |
+
Extract only OCR text from an image without all the metadata
|
| 297 |
+
"""
|
| 298 |
+
if not EASYOCR_AVAILABLE:
|
| 299 |
+
return "[EasyOCR not installed. Install with: pip install easyocr]"
|
| 300 |
+
|
| 301 |
+
try:
|
| 302 |
+
from PIL import Image
|
| 303 |
+
import numpy as np
|
| 304 |
+
|
| 305 |
+
img = Image.open(io.BytesIO(image_content))
|
| 306 |
+
|
| 307 |
+
# Scale image if too large
|
| 308 |
+
if img.width > 1500 or img.height > 1500:
|
| 309 |
+
img.thumbnail((1500, 1500))
|
| 310 |
+
|
| 311 |
+
img_array = np.array(img)
|
| 312 |
+
reader = get_easyocr_reader()
|
| 313 |
+
|
| 314 |
+
if reader:
|
| 315 |
+
results = reader.readtext(img_array)
|
| 316 |
+
if results:
|
| 317 |
+
extracted_text = []
|
| 318 |
+
for (bbox, text, confidence) in results:
|
| 319 |
+
if confidence > 0.3: # Lower threshold for more text
|
| 320 |
+
extracted_text.append(text)
|
| 321 |
+
|
| 322 |
+
if extracted_text:
|
| 323 |
+
return ' '.join(extracted_text)
|
| 324 |
+
|
| 325 |
+
return ""
|
| 326 |
+
except Exception as e:
|
| 327 |
+
print(f"OCR extraction error: {e}")
|
| 328 |
+
return f"[OCR error: {str(e)}]"
|
| 329 |
+
|
| 330 |
+
def process_audio(self, content: bytes, filename: str) -> str:
|
| 331 |
+
"""
|
| 332 |
+
Extract audio metadata and transcribe speech with multiple fallback methods
|
| 333 |
+
Tries: 1. Mutagen (metadata) → 2. Audioread (info) → 3. Pydub (properties) → 4. Whisper (transcription) → 5. SpeechRecognition
|
| 334 |
+
"""
|
| 335 |
+
output = "\n--- AUDIO ANALYSIS ---\n"
|
| 336 |
+
output += f"🎵 File: {filename}\n"
|
| 337 |
+
output += f"📦 Size: {len(content)} bytes\n"
|
| 338 |
+
|
| 339 |
+
temp_file_path = None
|
| 340 |
+
try:
|
| 341 |
+
# Create temporary file
|
| 342 |
+
with tempfile.NamedTemporaryFile(suffix='.' + filename.split('.')[-1], delete=False) as tmp:
|
| 343 |
+
tmp.write(content)
|
| 344 |
+
tmp.flush()
|
| 345 |
+
temp_file_path = tmp.name
|
| 346 |
+
|
| 347 |
+
# ============= METHOD 1: Mutagen (Best for metadata) =============
|
| 348 |
+
if MUTAGEN_AVAILABLE:
|
| 349 |
+
try:
|
| 350 |
+
audio_file = mutagen.File(temp_file_path)
|
| 351 |
+
if audio_file:
|
| 352 |
+
output += "\n📋 METADATA (Mutagen):\n"
|
| 353 |
+
|
| 354 |
+
# Get info
|
| 355 |
+
if hasattr(audio_file, 'info'):
|
| 356 |
+
info = audio_file.info
|
| 357 |
+
if hasattr(info, 'length'):
|
| 358 |
+
minutes = int(info.length // 60)
|
| 359 |
+
seconds = int(info.length % 60)
|
| 360 |
+
output += f" • Duration: {minutes}:{seconds:02d} ({info.length:.2f} seconds)\n"
|
| 361 |
+
if hasattr(info, 'bitrate'):
|
| 362 |
+
output += f" • Bitrate: {info.bitrate} bps\n"
|
| 363 |
+
if hasattr(info, 'sample_rate'):
|
| 364 |
+
output += f" • Sample Rate: {info.sample_rate} Hz\n"
|
| 365 |
+
if hasattr(info, 'channels'):
|
| 366 |
+
output += f" • Channels: {info.channels}\n"
|
| 367 |
+
|
| 368 |
+
# Get tags
|
| 369 |
+
if hasattr(audio_file, 'tags') and audio_file.tags:
|
| 370 |
+
output += "\n🏷️ TAGS:\n"
|
| 371 |
+
for key, value in list(audio_file.tags.items())[:15]:
|
| 372 |
+
output += f" • {key}: {value}\n"
|
| 373 |
+
except Exception as e:
|
| 374 |
+
output += f"\n⚠️ Mutagen metadata extraction failed: {str(e)}\n"
|
| 375 |
+
|
| 376 |
+
# ============= METHOD 2: Audioread (Fallback for audio info) =============
|
| 377 |
+
if AUDIOREAD_AVAILABLE and not (MUTAGEN_AVAILABLE and 'Duration' in output):
|
| 378 |
+
try:
|
| 379 |
+
with audioread.audio_open(temp_file_path) as f:
|
| 380 |
+
output += "\n📊 AUDIO INFO (Audioread):\n"
|
| 381 |
+
duration = f.duration
|
| 382 |
+
minutes = int(duration // 60)
|
| 383 |
+
seconds = int(duration % 60)
|
| 384 |
+
output += f" • Duration: {minutes}:{seconds:02d} ({duration:.2f} seconds)\n"
|
| 385 |
+
output += f" • Sample Rate: {f.samplerate} Hz\n"
|
| 386 |
+
output += f" • Channels: {f.channels}\n"
|
| 387 |
+
if hasattr(f, 'bitrate'):
|
| 388 |
+
output += f" • Bitrate: {f.bitrate} bps\n"
|
| 389 |
+
except Exception as e:
|
| 390 |
+
output += f"\n⚠️ Audioread info extraction failed: {str(e)}\n"
|
| 391 |
+
|
| 392 |
+
# ============= METHOD 3: Pydub (For additional properties) =============
|
| 393 |
+
if PYDUB_AVAILABLE:
|
| 394 |
+
try:
|
| 395 |
+
audio = AudioSegment.from_file(temp_file_path)
|
| 396 |
+
duration = len(audio) / 1000
|
| 397 |
+
minutes = int(duration // 60)
|
| 398 |
+
seconds = int(duration % 60)
|
| 399 |
+
output += "\n🎚️ AUDIO PROPERTIES (Pydub):\n"
|
| 400 |
+
output += f" • Duration: {minutes}:{seconds:02d} ({duration:.2f} seconds)\n"
|
| 401 |
+
output += f" • Channels: {audio.channels}\n"
|
| 402 |
+
output += f" • Frame Rate: {audio.frame_rate} Hz\n"
|
| 403 |
+
output += f" • Sample Width: {audio.sample_width} bytes\n"
|
| 404 |
+
output += f" • Max Amplitude: {audio.max}\n"
|
| 405 |
+
output += f" • RMS: {audio.rms:.2f}\n"
|
| 406 |
+
except Exception as e:
|
| 407 |
+
output += f"\n⚠️ Pydub processing failed: {str(e)}\n"
|
| 408 |
+
|
| 409 |
+
# ============= METHOD 4: Whisper (Best for transcription - offline) =============
|
| 410 |
+
if WHISPER_AVAILABLE:
|
| 411 |
+
try:
|
| 412 |
+
output += "\n🎙️ WHISPER TRANSCRIPTION (Offline):\n"
|
| 413 |
+
output += "Loading Whisper model (first time may take a moment)...\n"
|
| 414 |
+
model = whisper.load_model("base")
|
| 415 |
+
result = model.transcribe(temp_file_path, language="en")
|
| 416 |
+
if result and result.get("text"):
|
| 417 |
+
transcript = result["text"].strip()
|
| 418 |
+
output += f"✅ Transcription complete!\n"
|
| 419 |
+
output += f"\n📝 TRANSCRIPT:\n{transcript}\n"
|
| 420 |
+
else:
|
| 421 |
+
output += "❌ No speech detected\n"
|
| 422 |
+
except Exception as e:
|
| 423 |
+
output += f"⚠️ Whisper transcription failed: {str(e)}\n"
|
| 424 |
+
output += "Install Whisper: pip install openai-whisper torch\n"
|
| 425 |
+
|
| 426 |
+
# ============= METHOD 5: SpeechRecognition (Fallback - online) =============
|
| 427 |
+
elif SPEECH_RECOGNITION_AVAILABLE and not WHISPER_AVAILABLE:
|
| 428 |
+
try:
|
| 429 |
+
# Try to convert to WAV for better compatibility
|
| 430 |
+
if PYDUB_AVAILABLE:
|
| 431 |
+
try:
|
| 432 |
+
audio = AudioSegment.from_file(temp_file_path)
|
| 433 |
+
wav_io = io.BytesIO()
|
| 434 |
+
audio.export(wav_io, format="wav")
|
| 435 |
+
wav_io.seek(0)
|
| 436 |
+
audio_source = wav_io
|
| 437 |
+
except:
|
| 438 |
+
audio_source = temp_file_path
|
| 439 |
+
else:
|
| 440 |
+
audio_source = temp_file_path
|
| 441 |
+
|
| 442 |
+
recognizer = sr.Recognizer()
|
| 443 |
+
with sr.AudioFile(audio_source) as source:
|
| 444 |
+
output += "\n🎙️ SPEECH RECOGNITION (Google):\n"
|
| 445 |
+
recognizer.adjust_for_ambient_noise(source, duration=0.5)
|
| 446 |
+
audio_data = recognizer.record(source, duration=30)
|
| 447 |
+
|
| 448 |
+
try:
|
| 449 |
+
transcript = recognizer.recognize_google(audio_data)
|
| 450 |
+
if transcript and transcript.strip():
|
| 451 |
+
output += f"✅ Transcription complete!\n"
|
| 452 |
+
output += f"\n📝 TRANSCRIPT:\n{transcript.strip()}\n"
|
| 453 |
+
else:
|
| 454 |
+
output += "❌ No speech detected\n"
|
| 455 |
+
except sr.UnknownValueError:
|
| 456 |
+
output += "❌ Could not understand audio\n"
|
| 457 |
+
except sr.RequestError as e:
|
| 458 |
+
output += f"⚠️ Google Speech Recognition error: {str(e)}\n"
|
| 459 |
+
except Exception as e:
|
| 460 |
+
output += f"\n⚠️ Speech recognition failed: {str(e)}\n"
|
| 461 |
+
output += "Install SpeechRecognition: pip install SpeechRecognition\n"
|
| 462 |
+
|
| 463 |
+
# ============= METHOD 6: Librosa (Scientific analysis) =============
|
| 464 |
+
if LIBROSA_AVAILABLE:
|
| 465 |
+
try:
|
| 466 |
+
import numpy as np
|
| 467 |
+
y, sr_lib = librosa.load(temp_file_path, sr=None, duration=30)
|
| 468 |
+
output += "\n🔬 AUDIO ANALYSIS (Librosa):\n"
|
| 469 |
+
output += f" • RMS Energy: {np.mean(librosa.feature.rms(y=y)):.4f}\n"
|
| 470 |
+
output += f" • Zero Crossing Rate: {np.mean(librosa.feature.zero_crossing_rate(y)):.4f}\n"
|
| 471 |
+
try:
|
| 472 |
+
tempo, _ = librosa.beat.beat_track(y=y, sr=sr_lib)
|
| 473 |
+
output += f" • Estimated Tempo: {tempo:.2f} BPM\n"
|
| 474 |
+
except:
|
| 475 |
+
pass
|
| 476 |
+
except Exception as e:
|
| 477 |
+
pass # Silent fail for librosa as it's optional
|
| 478 |
+
|
| 479 |
+
# Summary of what was successful
|
| 480 |
+
output += "\n📊 PROCESSING SUMMARY:\n"
|
| 481 |
+
success_count = 0
|
| 482 |
+
if MUTAGEN_AVAILABLE and 'METADATA' in output:
|
| 483 |
+
output += " ✓ Metadata extracted (Mutagen)\n"
|
| 484 |
+
success_count += 1
|
| 485 |
+
if AUDIOREAD_AVAILABLE and 'Audioread' in output:
|
| 486 |
+
output += " ✓ Basic info extracted (Audioread)\n"
|
| 487 |
+
success_count += 1
|
| 488 |
+
if PYDUB_AVAILABLE and 'Pydub' in output:
|
| 489 |
+
output += " ✓ Audio properties analyzed (Pydub)\n"
|
| 490 |
+
success_count += 1
|
| 491 |
+
if WHISPER_AVAILABLE and 'TRANSCRIPT' in output:
|
| 492 |
+
output += " ✓ Speech transcribed (Whisper)\n"
|
| 493 |
+
success_count += 1
|
| 494 |
+
elif SPEECH_RECOGNITION_AVAILABLE and 'TRANSCRIPT' in output:
|
| 495 |
+
output += " ✓ Speech transcribed (Google)\n"
|
| 496 |
+
success_count += 1
|
| 497 |
+
|
| 498 |
+
if success_count == 0:
|
| 499 |
+
output += " ⚠️ Limited information available. Install additional packages:\n"
|
| 500 |
+
output += " • pip install mutagen audioread (for metadata)\n"
|
| 501 |
+
output += " • pip install openai-whisper torch (for transcription)\n"
|
| 502 |
+
output += " • pip install pydub (for audio properties)\n"
|
| 503 |
+
|
| 504 |
+
except Exception as e:
|
| 505 |
+
output += f"\n❌ Critical error processing audio file: {str(e)}\n"
|
| 506 |
+
finally:
|
| 507 |
+
# Clean up temp file
|
| 508 |
+
if temp_file_path and os.path.exists(temp_file_path):
|
| 509 |
+
try:
|
| 510 |
+
os.unlink(temp_file_path)
|
| 511 |
+
except:
|
| 512 |
+
pass
|
| 513 |
+
|
| 514 |
+
output += "--- END AUDIO ANALYSIS ---\n"
|
| 515 |
+
return output
|
| 516 |
+
|
| 517 |
+
def process_video(self, content: bytes, filename: str) -> str:
|
| 518 |
+
"""Extract video metadata using multiple methods"""
|
| 519 |
+
output = "\n--- VIDEO ANALYSIS ---\n"
|
| 520 |
+
output += f"🎬 File: {filename}\n"
|
| 521 |
+
output += f"📦 Size: {len(content)} bytes\n"
|
| 522 |
+
|
| 523 |
+
temp_file_path = None
|
| 524 |
+
try:
|
| 525 |
+
with tempfile.NamedTemporaryFile(suffix='.' + filename.split('.')[-1], delete=False) as tmp:
|
| 526 |
+
tmp.write(content)
|
| 527 |
+
tmp.flush()
|
| 528 |
+
temp_file_path = tmp.name
|
| 529 |
+
|
| 530 |
+
# Try OpenCV for video properties
|
| 531 |
+
if CV2_AVAILABLE:
|
| 532 |
+
try:
|
| 533 |
+
import numpy as np
|
| 534 |
+
cap = cv2.VideoCapture(temp_file_path)
|
| 535 |
+
if cap.isOpened():
|
| 536 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 537 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 538 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 539 |
+
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 540 |
+
duration = frame_count / fps if fps > 0 else 0
|
| 541 |
+
|
| 542 |
+
output += "\n🎥 VIDEO PROPERTIES (OpenCV):\n"
|
| 543 |
+
output += f" • Resolution: {width}x{height}\n"
|
| 544 |
+
output += f" • FPS: {fps:.2f}\n"
|
| 545 |
+
output += f" • Frame Count: {frame_count}\n"
|
| 546 |
+
minutes = int(duration // 60)
|
| 547 |
+
seconds = int(duration % 60)
|
| 548 |
+
output += f" • Duration: {minutes}:{seconds:02d} ({duration:.2f} seconds)\n"
|
| 549 |
+
cap.release()
|
| 550 |
+
except Exception as e:
|
| 551 |
+
output += f"\n⚠️ OpenCV processing failed: {str(e)}\n"
|
| 552 |
+
|
| 553 |
+
# Try moviepy if available
|
| 554 |
+
try:
|
| 555 |
+
from moviepy.editor import VideoFileClip
|
| 556 |
+
clip = VideoFileClip(temp_file_path)
|
| 557 |
+
output += "\n🎞️ VIDEO PROPERTIES (MoviePy):\n"
|
| 558 |
+
output += f" • Duration: {clip.duration:.2f} seconds\n"
|
| 559 |
+
output += f" • FPS: {clip.fps}\n"
|
| 560 |
+
output += f" • Size: {clip.size}\n"
|
| 561 |
+
output += f" • Has Audio: {clip.audio is not None}\n"
|
| 562 |
+
clip.close()
|
| 563 |
+
except ImportError:
|
| 564 |
+
pass
|
| 565 |
+
except Exception as e:
|
| 566 |
+
output += f"\n⚠️ MoviePy processing failed: {str(e)}\n"
|
| 567 |
+
|
| 568 |
+
except Exception as e:
|
| 569 |
+
output += f"\n❌ Error processing video: {str(e)}\n"
|
| 570 |
+
finally:
|
| 571 |
+
if temp_file_path and os.path.exists(temp_file_path):
|
| 572 |
+
try:
|
| 573 |
+
os.unlink(temp_file_path)
|
| 574 |
+
except:
|
| 575 |
+
pass
|
| 576 |
+
|
| 577 |
+
output += "--- END VIDEO ANALYSIS ---\n"
|
| 578 |
+
return output
|
| 579 |
+
|
| 580 |
+
def process_pdf(self, content: bytes, filename: str) -> str:
|
| 581 |
+
"""Extract text, tables, and metadata from PDFs - FULL CONTENT"""
|
| 582 |
+
output = "\n--- PDF ANALYSIS ---\n"
|
| 583 |
+
|
| 584 |
+
if not PDFPLUMBER_AVAILABLE:
|
| 585 |
+
output += "❌ PDF processing not available (pdfplumber not installed)\n"
|
| 586 |
+
output += "Install with: pip install pdfplumber\n"
|
| 587 |
+
output += "--- END PDF ANALYSIS ---\n"
|
| 588 |
+
return output
|
| 589 |
+
|
| 590 |
+
try:
|
| 591 |
+
with pdfplumber.open(io.BytesIO(content)) as pdf:
|
| 592 |
+
total_pages = len(pdf.pages)
|
| 593 |
+
output += f"📄 Pages: {total_pages}\n"
|
| 594 |
+
|
| 595 |
+
# Extract metadata
|
| 596 |
+
if pdf.metadata:
|
| 597 |
+
output += "\n📋 METADATA:\n"
|
| 598 |
+
for key, value in pdf.metadata.items():
|
| 599 |
+
if value:
|
| 600 |
+
output += f" • {key}: {value}\n"
|
| 601 |
+
|
| 602 |
+
# Extract text from ALL pages - NO LIMIT on number of pages
|
| 603 |
+
full_text = ""
|
| 604 |
+
for i, page in enumerate(pdf.pages):
|
| 605 |
+
page_text = page.extract_text()
|
| 606 |
+
if page_text:
|
| 607 |
+
full_text += f"\n--- PAGE {i+1} ---\n{page_text}\n"
|
| 608 |
+
|
| 609 |
+
if full_text:
|
| 610 |
+
# NO CHARACTER LIMIT - extract FULL content
|
| 611 |
+
# Only add a note if extremely large (over 500KB)
|
| 612 |
+
if len(full_text) > 500000:
|
| 613 |
+
full_text += f"\n\n[Note: Full PDF content extracted ({len(full_text)} characters).]"
|
| 614 |
+
output += f"\n📝 FULL TEXT CONTENT ({len(full_text)} characters):\n{full_text}\n"
|
| 615 |
+
|
| 616 |
+
output += "--- END PDF ANALYSIS ---\n"
|
| 617 |
+
|
| 618 |
+
except Exception as e:
|
| 619 |
+
output += f"❌ Error processing PDF: {str(e)}\n"
|
| 620 |
+
|
| 621 |
+
return output
|
| 622 |
+
|
| 623 |
+
def process_spreadsheet(self, content: bytes, filename: str) -> str:
|
| 624 |
+
"""Extract data from Excel spreadsheets - FULL CONTENT"""
|
| 625 |
+
output = "\n--- SPREADSHEET ANALYSIS ---\n"
|
| 626 |
+
|
| 627 |
+
if not PANDAS_AVAILABLE:
|
| 628 |
+
output += "❌ Spreadsheet processing not available (pandas not installed)\n"
|
| 629 |
+
output += "Install with: pip install pandas openpyxl\n"
|
| 630 |
+
output += "--- END SPREADSHEET ANALYSIS ---\n"
|
| 631 |
+
return output
|
| 632 |
+
|
| 633 |
+
try:
|
| 634 |
+
# Try pandas for comprehensive analysis
|
| 635 |
+
df_dict = pd.read_excel(io.BytesIO(content), sheet_name=None)
|
| 636 |
+
output += f"📊 Sheets: {', '.join(list(df_dict.keys()))}\n"
|
| 637 |
+
|
| 638 |
+
# Process ALL sheets - NO LIMIT
|
| 639 |
+
for sheet_name, df in df_dict.items():
|
| 640 |
+
output += f"\n📑 SHEET: {sheet_name}\n"
|
| 641 |
+
output += f" • Dimensions: {df.shape[0]} rows x {df.shape[1]} columns\n"
|
| 642 |
+
output += f" • Columns: {', '.join(df.columns.astype(str)[:30])}\n"
|
| 643 |
+
|
| 644 |
+
# Show ALL rows if less than 1000, otherwise show first 500
|
| 645 |
+
if df.shape[0] <= 1000:
|
| 646 |
+
full_data = df.to_string()
|
| 647 |
+
output += f"\n FULL DATA:\n{full_data}\n"
|
| 648 |
+
else:
|
| 649 |
+
# Show first 500 rows and note about remaining
|
| 650 |
+
sample = df.head(500).to_string()
|
| 651 |
+
output += f"\n DATA (first 500 rows of {df.shape[0]}):\n{sample}\n"
|
| 652 |
+
output += f"\n ... and {df.shape[0] - 500} more rows\n"
|
| 653 |
+
|
| 654 |
+
# Basic statistics for numeric columns
|
| 655 |
+
numeric_cols = df.select_dtypes(include=['number']).columns
|
| 656 |
+
if len(numeric_cols) > 0:
|
| 657 |
+
output += f"\n Numeric summary:\n"
|
| 658 |
+
output += df[numeric_cols].describe().to_string()
|
| 659 |
+
output += "\n"
|
| 660 |
+
|
| 661 |
+
output += "--- END SPREADSHEET ANALYSIS ---\n"
|
| 662 |
+
|
| 663 |
+
except Exception as e:
|
| 664 |
+
output += f"❌ Error processing spreadsheet: {str(e)}\n"
|
| 665 |
+
|
| 666 |
+
return output
|
| 667 |
+
|
| 668 |
+
def process_word_document(self, content: bytes, filename: str) -> str:
|
| 669 |
+
"""Extract text from Word documents - FULL CONTENT"""
|
| 670 |
+
output = "\n--- WORD DOCUMENT ANALYSIS ---\n"
|
| 671 |
+
|
| 672 |
+
if not DOCX_AVAILABLE:
|
| 673 |
+
output += "❌ Word document processing not available (python-docx not installed)\n"
|
| 674 |
+
output += "Install with: pip install python-docx\n"
|
| 675 |
+
output += "--- END WORD DOCUMENT ANALYSIS ---\n"
|
| 676 |
+
return output
|
| 677 |
+
|
| 678 |
+
try:
|
| 679 |
+
doc = Document(io.BytesIO(content))
|
| 680 |
+
output += f"📝 Paragraphs: {len(doc.paragraphs)}\n"
|
| 681 |
+
|
| 682 |
+
# Extract text from ALL paragraphs - NO truncation
|
| 683 |
+
text = '\n'.join([p.text for p in doc.paragraphs if p.text.strip()])
|
| 684 |
+
if text:
|
| 685 |
+
# Only add a note if extremely large
|
| 686 |
+
if len(text) > 500000:
|
| 687 |
+
text += f"\n\n[Note: Full document content extracted ({len(text)} characters).]"
|
| 688 |
+
output += f"\n📄 FULL TEXT CONTENT ({len(text)} characters):\n{text}\n"
|
| 689 |
+
|
| 690 |
+
# Extract tables fully
|
| 691 |
+
if doc.tables:
|
| 692 |
+
output += f"\n📊 Tables found: {len(doc.tables)}\n"
|
| 693 |
+
for table_idx, table in enumerate(doc.tables):
|
| 694 |
+
output += f"\n--- TABLE {table_idx + 1} ---\n"
|
| 695 |
+
for row in table.rows:
|
| 696 |
+
row_text = ' | '.join([cell.text for cell in row.cells])
|
| 697 |
+
output += f"{row_text}\n"
|
| 698 |
+
|
| 699 |
+
output += "--- END WORD DOCUMENT ANALYSIS ---\n"
|
| 700 |
+
|
| 701 |
+
except Exception as e:
|
| 702 |
+
output += f"❌ Error processing Word document: {str(e)}\n"
|
| 703 |
+
|
| 704 |
+
return output
|
| 705 |
+
|
| 706 |
+
def process_presentation(self, content: bytes, filename: str) -> str:
|
| 707 |
+
"""Extract content from PowerPoint presentations"""
|
| 708 |
+
output = "\n--- PRESENTATION ANALYSIS ---\n"
|
| 709 |
+
|
| 710 |
+
if not PPTX_AVAILABLE:
|
| 711 |
+
output += "❌ PowerPoint processing not available (python-pptx not installed)\n"
|
| 712 |
+
output += "Install with: pip install python-pptx\n"
|
| 713 |
+
output += "--- END PRESENTATION ANALYSIS ---\n"
|
| 714 |
+
return output
|
| 715 |
+
|
| 716 |
+
try:
|
| 717 |
+
prs = Presentation(io.BytesIO(content))
|
| 718 |
+
output += f"📽️ Slides: {len(prs.slides)}\n"
|
| 719 |
+
|
| 720 |
+
slide_text = []
|
| 721 |
+
for i, slide in enumerate(prs.slides[:10]):
|
| 722 |
+
slide_content = []
|
| 723 |
+
for shape in slide.shapes:
|
| 724 |
+
if hasattr(shape, "text") and shape.text.strip():
|
| 725 |
+
slide_content.append(shape.text)
|
| 726 |
+
if slide_content:
|
| 727 |
+
slide_text.append(f"\n--- SLIDE {i+1} ---\n" + '\n'.join(slide_content))
|
| 728 |
+
|
| 729 |
+
if slide_text:
|
| 730 |
+
full_text = ''.join(slide_text)
|
| 731 |
+
if len(full_text) > 10000:
|
| 732 |
+
full_text = full_text[:10000] + "\n\n[Content truncated...]"
|
| 733 |
+
output += f"\n📝 TEXT CONTENT:\n{full_text}\n"
|
| 734 |
+
|
| 735 |
+
output += "--- END PRESENTATION ANALYSIS ---\n"
|
| 736 |
+
|
| 737 |
+
except Exception as e:
|
| 738 |
+
output += f"❌ Error processing presentation: {str(e)}\n"
|
| 739 |
+
|
| 740 |
+
return output
|
| 741 |
+
|
| 742 |
+
def process_archive(self, content: bytes, filename: str) -> str:
|
| 743 |
+
"""List archive contents"""
|
| 744 |
+
output = "\n--- ARCHIVE ANALYSIS ---\n"
|
| 745 |
+
|
| 746 |
+
try:
|
| 747 |
+
file_ext = filename.split('.')[-1].lower()
|
| 748 |
+
|
| 749 |
+
if file_ext == 'zip':
|
| 750 |
+
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
| 751 |
+
files = zf.namelist()
|
| 752 |
+
output += f"📦 Total files: {len(files)}\n"
|
| 753 |
+
output += "\n📋 FILE LIST:\n"
|
| 754 |
+
for f in files[:50]:
|
| 755 |
+
info = zf.getinfo(f)
|
| 756 |
+
size = info.file_size
|
| 757 |
+
output += f" • {f} ({size:,} bytes)\n"
|
| 758 |
+
if len(files) > 50:
|
| 759 |
+
output += f" ... and {len(files) - 50} more files\n"
|
| 760 |
+
elif file_ext in ['tar', 'gz', 'bz2']:
|
| 761 |
+
with tarfile.open(fileobj=io.BytesIO(content), mode='r:*') as tf:
|
| 762 |
+
files = tf.getnames()
|
| 763 |
+
output += f"📦 Total files: {len(files)}\n"
|
| 764 |
+
output += "\n📋 FILE LIST:\n"
|
| 765 |
+
for f in files[:50]:
|
| 766 |
+
output += f" • {f}\n"
|
| 767 |
+
if len(files) > 50:
|
| 768 |
+
output += f" ... and {len(files) - 50} more files\n"
|
| 769 |
+
else:
|
| 770 |
+
output += f"Archive format {file_ext} - size: {len(content)} bytes\n"
|
| 771 |
+
output += "For full archive support, install: pip install patool\n"
|
| 772 |
+
|
| 773 |
+
output += "--- END ARCHIVE ANALYSIS ---\n"
|
| 774 |
+
|
| 775 |
+
except Exception as e:
|
| 776 |
+
output += f"❌ Error processing archive: {str(e)}\n"
|
| 777 |
+
|
| 778 |
+
return output
|
| 779 |
+
|
| 780 |
+
def process_database(self, content: bytes, filename: str) -> str:
|
| 781 |
+
"""Analyze SQLite databases"""
|
| 782 |
+
output = "\n--- DATABASE ANALYSIS ---\n"
|
| 783 |
+
|
| 784 |
+
try:
|
| 785 |
+
import sqlite3
|
| 786 |
+
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp:
|
| 787 |
+
tmp.write(content)
|
| 788 |
+
tmp.flush()
|
| 789 |
+
tmp_path = tmp.name
|
| 790 |
+
|
| 791 |
+
try:
|
| 792 |
+
conn = sqlite3.connect(tmp_path)
|
| 793 |
+
cursor = conn.cursor()
|
| 794 |
+
|
| 795 |
+
# Get all tables
|
| 796 |
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
| 797 |
+
tables = cursor.fetchall()
|
| 798 |
+
|
| 799 |
+
output += f"🗄️ Tables: {len(tables)}\n\n"
|
| 800 |
+
|
| 801 |
+
for table in tables[:20]: # Limit to 20 tables
|
| 802 |
+
table_name = table[0]
|
| 803 |
+
cursor.execute(f"PRAGMA table_info({table_name})")
|
| 804 |
+
columns = cursor.fetchall()
|
| 805 |
+
|
| 806 |
+
output += f"📋 TABLE: {table_name}\n"
|
| 807 |
+
output += f" • Columns: {len(columns)}\n"
|
| 808 |
+
for col in columns[:15]:
|
| 809 |
+
output += f" - {col[1]} ({col[2]})\n"
|
| 810 |
+
|
| 811 |
+
# Get row count
|
| 812 |
+
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
| 813 |
+
row_count = cursor.fetchone()[0]
|
| 814 |
+
output += f" • Rows: {row_count:,}\n"
|
| 815 |
+
|
| 816 |
+
# Show sample data
|
| 817 |
+
if row_count > 0:
|
| 818 |
+
cursor.execute(f"SELECT * FROM {table_name} LIMIT 3")
|
| 819 |
+
sample = cursor.fetchall()
|
| 820 |
+
output += f"\n Sample rows:\n"
|
| 821 |
+
for row in sample[:3]:
|
| 822 |
+
output += f" {row}\n"
|
| 823 |
+
output += "\n"
|
| 824 |
+
|
| 825 |
+
conn.close()
|
| 826 |
+
finally:
|
| 827 |
+
os.unlink(tmp_path)
|
| 828 |
+
|
| 829 |
+
output += "--- END DATABASE ANALYSIS ---\n"
|
| 830 |
+
|
| 831 |
+
except Exception as e:
|
| 832 |
+
output += f"❌ Error processing database: {str(e)}\n"
|
| 833 |
+
|
| 834 |
+
return output
|
| 835 |
+
|
| 836 |
+
def process_text_file(self, content: bytes, filename: str) -> str:
|
| 837 |
+
"""Enhanced text file processing with encoding detection"""
|
| 838 |
+
output = "\n--- TEXT FILE ANALYSIS ---\n"
|
| 839 |
+
|
| 840 |
+
try:
|
| 841 |
+
# Detect encoding
|
| 842 |
+
if CHARDET_AVAILABLE:
|
| 843 |
+
detection = chardet.detect(content)
|
| 844 |
+
encoding = detection.get('encoding', 'utf-8')
|
| 845 |
+
confidence = detection.get('confidence', 0)
|
| 846 |
+
output += f"🔤 Encoding: {encoding} (confidence: {confidence:.2%})\n"
|
| 847 |
+
else:
|
| 848 |
+
encoding = 'utf-8'
|
| 849 |
+
output += "🔤 Encoding detection not available (install chardet)\n"
|
| 850 |
+
|
| 851 |
+
# Decode content
|
| 852 |
+
text = content.decode(encoding, errors='replace')
|
| 853 |
+
lines = text.split('\n')
|
| 854 |
+
output += f"📄 Lines: {len(lines):,}\n"
|
| 855 |
+
output += f"📝 Characters: {len(text):,}\n"
|
| 856 |
+
|
| 857 |
+
# Show first 100 lines as sample
|
| 858 |
+
output += f"\n📖 SAMPLE CONTENT:\n"
|
| 859 |
+
sample_lines = lines[:100]
|
| 860 |
+
output += '\n'.join(sample_lines)
|
| 861 |
+
if len(lines) > 100:
|
| 862 |
+
output += f"\n... and {len(lines) - 100} more lines\n"
|
| 863 |
+
|
| 864 |
+
output += "--- END TEXT FILE ANALYSIS ---\n"
|
| 865 |
+
|
| 866 |
+
except Exception as e:
|
| 867 |
+
output += f"❌ Error processing text file: {str(e)}\n"
|
| 868 |
+
|
| 869 |
+
return output
|
| 870 |
+
|
| 871 |
+
def try_extract_text(self, content: bytes) -> Optional[str]:
|
| 872 |
+
"""Attempt to extract text from unknown file types"""
|
| 873 |
+
try:
|
| 874 |
+
# Try to decode as UTF-8 first
|
| 875 |
+
text = content.decode('utf-8', errors='replace')
|
| 876 |
+
# Check if it looks like text (mostly printable)
|
| 877 |
+
printable_chars = sum(1 for c in text if c.isprintable() or c in '\n\r\t')
|
| 878 |
+
if printable_chars / len(text) > 0.7 and len(text) > 100:
|
| 879 |
+
# Remove non-printable characters
|
| 880 |
+
text = ''.join(char for char in text if char.isprintable() or char in '\n\r\t')
|
| 881 |
+
return text[:5000]
|
| 882 |
+
|
| 883 |
+
# Try with encoding detection
|
| 884 |
+
if CHARDET_AVAILABLE:
|
| 885 |
+
detection = chardet.detect(content)
|
| 886 |
+
if detection['encoding']:
|
| 887 |
+
text = content.decode(detection['encoding'], errors='replace')
|
| 888 |
+
if len(text) > 100:
|
| 889 |
+
return text[:5000]
|
| 890 |
+
except:
|
| 891 |
+
pass
|
| 892 |
+
|
| 893 |
+
return None
|
media.py
ADDED
|
@@ -0,0 +1,1338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# media.py - Complete media handling with multiple free providers including DuckDuckGo and Openverse
|
| 2 |
+
import requests
|
| 3 |
+
import base64
|
| 4 |
+
import json
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import tempfile
|
| 7 |
+
import os
|
| 8 |
+
import subprocess
|
| 9 |
+
import random
|
| 10 |
+
import re
|
| 11 |
+
|
| 12 |
+
# API Keys - Free to obtain from respective services
|
| 13 |
+
PIXABAY_API_KEY = os.environ.get("PIXABAY_API_KEY", "") # pixabay.com
|
| 14 |
+
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "") # pexels.com
|
| 15 |
+
TWELVELABS_API_KEY = os.environ.get("TWELVELABS_API_KEY", "") # twelvelabs.com
|
| 16 |
+
|
| 17 |
+
# Optional - Get your free API keys from these services:
|
| 18 |
+
UNSPLASH_API_KEY = "YOUR_UNSPLASH_API_KEY"
|
| 19 |
+
GIPHY_API_KEY = os.environ.get("GIPHY_API_KEY", "")
|
| 20 |
+
FLICKR_API_KEY = "YOUR_FLICKR_API_KEY"
|
| 21 |
+
VIMEO_ACCESS_TOKEN = "YOUR_VIMEO_TOKEN"
|
| 22 |
+
|
| 23 |
+
class MediaHandler:
|
| 24 |
+
def __init__(self):
|
| 25 |
+
# Primary providers (always available)
|
| 26 |
+
self.pixabay_key = PIXABAY_API_KEY
|
| 27 |
+
self.pexels_key = PEXELS_API_KEY
|
| 28 |
+
self.twelvelabs_key = TWELVELABS_API_KEY
|
| 29 |
+
|
| 30 |
+
# Secondary providers (optional - add your keys)
|
| 31 |
+
self.unsplash_key = UNSPLASH_API_KEY
|
| 32 |
+
self.giphy_key = GIPHY_API_KEY
|
| 33 |
+
self.flickr_key = FLICKR_API_KEY
|
| 34 |
+
self.vimeo_token = VIMEO_ACCESS_TOKEN
|
| 35 |
+
|
| 36 |
+
# Track which providers are available
|
| 37 |
+
self.available_image_providers = ['pixabay', 'pexels', 'duckduckgo', 'openverse']
|
| 38 |
+
self.available_video_providers = ['pixabay', 'pexels', 'duckduckgo']
|
| 39 |
+
|
| 40 |
+
# Add optional providers if keys are configured
|
| 41 |
+
if self.unsplash_key and self.unsplash_key != "YOUR_UNSPLASH_API_KEY":
|
| 42 |
+
self.available_image_providers.append('unsplash')
|
| 43 |
+
if self.giphy_key and self.giphy_key != "YOUR_GIPHY_API_KEY":
|
| 44 |
+
self.available_image_providers.append('giphy')
|
| 45 |
+
if self.flickr_key and self.flickr_key != "YOUR_FLICKR_API_KEY":
|
| 46 |
+
self.available_image_providers.append('flickr')
|
| 47 |
+
if self.vimeo_token and self.vimeo_token != "YOUR_VIMEO_TOKEN":
|
| 48 |
+
self.available_video_providers.append('vimeo')
|
| 49 |
+
self.available_video_providers.append('dailymotion')
|
| 50 |
+
|
| 51 |
+
# ============= DUCKDUCKGO SEARCH (IMAGES & VIDEOS - NO API KEY, NO SAFE SEARCH) =============
|
| 52 |
+
|
| 53 |
+
def _get_vqd_for_search(self, query):
|
| 54 |
+
"""Get VQD token required for DuckDuckGo API (no safe search)"""
|
| 55 |
+
try:
|
| 56 |
+
# Use kp=-2 to disable safe search completely
|
| 57 |
+
url = f"https://duckduckgo.com/?q={query}&t=h_&ia=web&kp=-2"
|
| 58 |
+
headers = {
|
| 59 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 60 |
+
}
|
| 61 |
+
response = requests.get(url, headers=headers, timeout=10)
|
| 62 |
+
|
| 63 |
+
# Extract VQD from response
|
| 64 |
+
vqd_match = re.search(r'vqd=([\d-]+)&', response.text)
|
| 65 |
+
if vqd_match:
|
| 66 |
+
return vqd_match.group(1)
|
| 67 |
+
|
| 68 |
+
# Alternative extraction
|
| 69 |
+
for line in response.text.split('\n'):
|
| 70 |
+
if 'vqd' in line and 'token' in line:
|
| 71 |
+
vqd_match = re.search(r'vqd[\'"]?\s*:\s*[\'"]([^\'"]+)[\'"]', line)
|
| 72 |
+
if vqd_match:
|
| 73 |
+
return vqd_match.group(1)
|
| 74 |
+
|
| 75 |
+
return None
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"Error getting VQD: {e}")
|
| 78 |
+
return None
|
| 79 |
+
|
| 80 |
+
def _search_duckduckgo_images(self, query, page=1):
|
| 81 |
+
"""Search DuckDuckGo for images (no API key required, no safe search)"""
|
| 82 |
+
try:
|
| 83 |
+
url = "https://duckduckgo.com/i.js"
|
| 84 |
+
params = {
|
| 85 |
+
'q': query,
|
| 86 |
+
'o': 'json',
|
| 87 |
+
'p': page,
|
| 88 |
+
'l': 'us-en',
|
| 89 |
+
'f': ',,',
|
| 90 |
+
'kp': -2 # -2 = OFF (shows all content)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
vqd = self._get_vqd_for_search(query)
|
| 94 |
+
if vqd:
|
| 95 |
+
params['vqd'] = vqd
|
| 96 |
+
|
| 97 |
+
headers = {
|
| 98 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 99 |
+
'Accept': 'application/json',
|
| 100 |
+
'Referer': 'https://duckduckgo.com/'
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
response = requests.get(url, params=params, headers=headers, timeout=10)
|
| 104 |
+
|
| 105 |
+
if response.status_code == 200:
|
| 106 |
+
data = response.json()
|
| 107 |
+
results = data.get('results', [])
|
| 108 |
+
|
| 109 |
+
if results and len(results) > 0:
|
| 110 |
+
result = results[0]
|
| 111 |
+
return {
|
| 112 |
+
'success': True,
|
| 113 |
+
'type': 'image',
|
| 114 |
+
'provider': 'DuckDuckGo',
|
| 115 |
+
'id': result.get('id', str(hash(result.get('image', '')))),
|
| 116 |
+
'title': result.get('title', query),
|
| 117 |
+
'description': result.get('title', ''),
|
| 118 |
+
'photographer': result.get('source', 'Unknown'),
|
| 119 |
+
'preview_url': result.get('thumbnail', result.get('image')),
|
| 120 |
+
'large_image_url': result.get('image', result.get('thumbnail')),
|
| 121 |
+
'width': result.get('width', 0),
|
| 122 |
+
'height': result.get('height', 0),
|
| 123 |
+
'page_url': result.get('url', '')
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
return {'success': False, 'error': 'No results found from DuckDuckGo'}
|
| 127 |
+
except Exception as e:
|
| 128 |
+
print(f"DuckDuckGo image search error: {e}")
|
| 129 |
+
return {'success': False, 'error': str(e)}
|
| 130 |
+
|
| 131 |
+
def _search_duckduckgo_videos(self, query, page=1):
|
| 132 |
+
"""Search DuckDuckGo for videos (no API key required, no safe search)"""
|
| 133 |
+
try:
|
| 134 |
+
url = "https://duckduckgo.com/v.js"
|
| 135 |
+
params = {
|
| 136 |
+
'q': query,
|
| 137 |
+
'o': 'json',
|
| 138 |
+
'p': page,
|
| 139 |
+
'l': 'us-en',
|
| 140 |
+
'f': ',,',
|
| 141 |
+
'kp': -2 # -2 = OFF (shows all content)
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
vqd = self._get_vqd_for_search(query)
|
| 145 |
+
if vqd:
|
| 146 |
+
params['vqd'] = vqd
|
| 147 |
+
|
| 148 |
+
headers = {
|
| 149 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 150 |
+
'Accept': 'application/json',
|
| 151 |
+
'Referer': 'https://duckduckgo.com/'
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
response = requests.get(url, params=params, headers=headers, timeout=10)
|
| 155 |
+
|
| 156 |
+
if response.status_code == 200:
|
| 157 |
+
data = response.json()
|
| 158 |
+
results = data.get('results', [])
|
| 159 |
+
|
| 160 |
+
if results and len(results) > 0:
|
| 161 |
+
result = results[0]
|
| 162 |
+
return {
|
| 163 |
+
'success': True,
|
| 164 |
+
'type': 'video',
|
| 165 |
+
'provider': 'DuckDuckGo',
|
| 166 |
+
'id': result.get('id', str(hash(result.get('content', '')))),
|
| 167 |
+
'title': result.get('title', query),
|
| 168 |
+
'description': result.get('description', ''),
|
| 169 |
+
'user': result.get('publisher', 'Unknown'),
|
| 170 |
+
'duration': result.get('duration', 'Unknown'),
|
| 171 |
+
'preview_url': result.get('thumbnail', ''),
|
| 172 |
+
'download_url': result.get('content', ''),
|
| 173 |
+
'embed_url': result.get('embed_url', ''),
|
| 174 |
+
'views': result.get('views', 0),
|
| 175 |
+
'page_url': result.get('url', '')
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
return {'success': False, 'error': 'No videos found from DuckDuckGo'}
|
| 179 |
+
except Exception as e:
|
| 180 |
+
print(f"DuckDuckGo video search error: {e}")
|
| 181 |
+
return {'success': False, 'error': str(e)}
|
| 182 |
+
|
| 183 |
+
# ============= OPENVERSE SEARCH (IMAGES ONLY - NO API KEY) =============
|
| 184 |
+
|
| 185 |
+
def _search_openverse(self, query, page=1):
|
| 186 |
+
"""Search Openverse for Creative Commons media (no API key needed)"""
|
| 187 |
+
try:
|
| 188 |
+
url = "https://api.openverse.engineering/v1/images/"
|
| 189 |
+
params = {
|
| 190 |
+
'q': query,
|
| 191 |
+
'page_size': 20,
|
| 192 |
+
'page': page
|
| 193 |
+
}
|
| 194 |
+
headers = {
|
| 195 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
| 196 |
+
}
|
| 197 |
+
response = requests.get(url, params=params, headers=headers, timeout=10)
|
| 198 |
+
data = response.json()
|
| 199 |
+
|
| 200 |
+
if data.get('results') and len(data['results']) > 0:
|
| 201 |
+
result = data['results'][0]
|
| 202 |
+
return {
|
| 203 |
+
'success': True,
|
| 204 |
+
'type': 'image',
|
| 205 |
+
'provider': 'Openverse',
|
| 206 |
+
'id': result.get('id', ''),
|
| 207 |
+
'title': result.get('title', query),
|
| 208 |
+
'description': f"By {result.get('creator', 'Unknown')} - License: {result.get('license', 'Unknown')}",
|
| 209 |
+
'photographer': result.get('creator', 'Unknown'),
|
| 210 |
+
'preview_url': result.get('thumbnail', result.get('url')),
|
| 211 |
+
'large_image_url': result.get('url', ''),
|
| 212 |
+
'width': result.get('width', 0),
|
| 213 |
+
'height': result.get('height', 0),
|
| 214 |
+
'license': result.get('license', ''),
|
| 215 |
+
'license_version': result.get('license_version', ''),
|
| 216 |
+
'page_url': result.get('foreign_landing_url', '')
|
| 217 |
+
}
|
| 218 |
+
return {'success': False, 'error': 'No results found from Openverse'}
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f"Openverse search error: {e}")
|
| 221 |
+
return {'success': False, 'error': str(e)}
|
| 222 |
+
|
| 223 |
+
# ============= IMAGE SEARCH METHODS =============
|
| 224 |
+
|
| 225 |
+
def search_images(self, query, provider='pixabay', page=1):
|
| 226 |
+
"""Search for images from multiple providers"""
|
| 227 |
+
providers = {
|
| 228 |
+
'pixabay': self._search_pixabay_images,
|
| 229 |
+
'pexels': self._search_pexels_images,
|
| 230 |
+
'unsplash': self._search_unsplash_images,
|
| 231 |
+
'giphy': self._search_giphy,
|
| 232 |
+
'flickr': self._search_flickr,
|
| 233 |
+
'duckduckgo': self._search_duckduckgo_images,
|
| 234 |
+
'openverse': self._search_openverse
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
if provider in providers:
|
| 238 |
+
return providers[provider](query, page)
|
| 239 |
+
return {'success': False, 'error': f'Provider {provider} not found'}
|
| 240 |
+
|
| 241 |
+
def search_videos(self, query, provider='pixabay', page=1):
|
| 242 |
+
"""Search for videos from multiple providers"""
|
| 243 |
+
providers = {
|
| 244 |
+
'pixabay': self._search_pixabay_videos,
|
| 245 |
+
'pexels': self._search_pexels_videos,
|
| 246 |
+
'dailymotion': self._search_dailymotion,
|
| 247 |
+
'vimeo': self._search_vimeo,
|
| 248 |
+
'duckduckgo': self._search_duckduckgo_videos
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
if provider in providers:
|
| 252 |
+
return providers[provider](query, page)
|
| 253 |
+
return {'success': False, 'error': f'Provider {provider} not found'}
|
| 254 |
+
|
| 255 |
+
# ============= PIXABAY METHODS =============
|
| 256 |
+
|
| 257 |
+
def _search_pixabay_images(self, query, page=1):
|
| 258 |
+
"""Search Pixabay for images"""
|
| 259 |
+
try:
|
| 260 |
+
url = "https://pixabay.com/api/"
|
| 261 |
+
params = {
|
| 262 |
+
'key': self.pixabay_key,
|
| 263 |
+
'q': query,
|
| 264 |
+
'image_type': 'photo',
|
| 265 |
+
'per_page': 20,
|
| 266 |
+
'page': page,
|
| 267 |
+
'safesearch': 'true'
|
| 268 |
+
}
|
| 269 |
+
response = requests.get(url, params=params, timeout=10)
|
| 270 |
+
data = response.json()
|
| 271 |
+
|
| 272 |
+
if data.get('totalHits', 0) > 0:
|
| 273 |
+
hit = data['hits'][0]
|
| 274 |
+
return {
|
| 275 |
+
'success': True,
|
| 276 |
+
'type': 'image',
|
| 277 |
+
'provider': 'Pixabay',
|
| 278 |
+
'id': hit['id'],
|
| 279 |
+
'title': hit.get('tags', query),
|
| 280 |
+
'description': f"Image by {hit.get('user', 'Unknown')}",
|
| 281 |
+
'photographer': hit.get('user'),
|
| 282 |
+
'photographer_url': hit.get('userImageURL'),
|
| 283 |
+
'preview_url': hit.get('previewURL'),
|
| 284 |
+
'large_image_url': hit.get('largeImageURL'),
|
| 285 |
+
'webformat_url': hit.get('webformatURL'),
|
| 286 |
+
'width': hit.get('imageWidth'),
|
| 287 |
+
'height': hit.get('imageHeight'),
|
| 288 |
+
'likes': hit.get('likes'),
|
| 289 |
+
'views': hit.get('views'),
|
| 290 |
+
'downloads': hit.get('downloads'),
|
| 291 |
+
'page_url': hit.get('pageURL')
|
| 292 |
+
}
|
| 293 |
+
return {'success': False, 'error': 'No results found'}
|
| 294 |
+
except Exception as e:
|
| 295 |
+
return {'success': False, 'error': str(e)}
|
| 296 |
+
|
| 297 |
+
def _search_pixabay_videos(self, query, page=1):
|
| 298 |
+
"""Search Pixabay for videos"""
|
| 299 |
+
try:
|
| 300 |
+
url = "https://pixabay.com/api/videos/"
|
| 301 |
+
params = {
|
| 302 |
+
'key': self.pixabay_key,
|
| 303 |
+
'q': query,
|
| 304 |
+
'per_page': 20,
|
| 305 |
+
'page': page,
|
| 306 |
+
'safesearch': 'true'
|
| 307 |
+
}
|
| 308 |
+
response = requests.get(url, params=params, timeout=10)
|
| 309 |
+
data = response.json()
|
| 310 |
+
|
| 311 |
+
if data.get('totalHits', 0) > 0:
|
| 312 |
+
hit = data['hits'][0]
|
| 313 |
+
videos = hit.get('videos', {})
|
| 314 |
+
video_source = None
|
| 315 |
+
video_quality = 'medium'
|
| 316 |
+
|
| 317 |
+
for quality in ['large', 'medium', 'small', 'tiny']:
|
| 318 |
+
if quality in videos and videos[quality].get('url'):
|
| 319 |
+
video_source = videos[quality]
|
| 320 |
+
video_quality = quality
|
| 321 |
+
break
|
| 322 |
+
|
| 323 |
+
if video_source:
|
| 324 |
+
return {
|
| 325 |
+
'success': True,
|
| 326 |
+
'type': 'video',
|
| 327 |
+
'provider': 'Pixabay',
|
| 328 |
+
'id': hit['id'],
|
| 329 |
+
'title': hit.get('tags', query),
|
| 330 |
+
'description': f"Video by {hit.get('user', 'Unknown')}",
|
| 331 |
+
'user': hit.get('user'),
|
| 332 |
+
'duration': hit.get('duration'),
|
| 333 |
+
'preview_url': hit.get('videos', {}).get('tiny', {}).get('url'),
|
| 334 |
+
'download_url': video_source.get('url'),
|
| 335 |
+
'thumbnail': hit.get('videos', {}).get('tiny', {}).get('thumbnail'),
|
| 336 |
+
'width': video_source.get('width'),
|
| 337 |
+
'height': video_source.get('height'),
|
| 338 |
+
'quality': video_quality,
|
| 339 |
+
'likes': hit.get('likes'),
|
| 340 |
+
'views': hit.get('views'),
|
| 341 |
+
'downloads': hit.get('downloads'),
|
| 342 |
+
'page_url': hit.get('pageURL')
|
| 343 |
+
}
|
| 344 |
+
return {'success': False, 'error': 'No results found'}
|
| 345 |
+
except Exception as e:
|
| 346 |
+
return {'success': False, 'error': str(e)}
|
| 347 |
+
|
| 348 |
+
# ============= PEXELS METHODS =============
|
| 349 |
+
|
| 350 |
+
def _search_pexels_images(self, query, page=1):
|
| 351 |
+
"""Search Pexels for images"""
|
| 352 |
+
try:
|
| 353 |
+
url = "https://api.pexels.com/v1/search"
|
| 354 |
+
headers = {'Authorization': self.pexels_key}
|
| 355 |
+
params = {'query': query, 'per_page': 20, 'page': page}
|
| 356 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 357 |
+
data = response.json()
|
| 358 |
+
|
| 359 |
+
if data.get('photos') and len(data['photos']) > 0:
|
| 360 |
+
photo = data['photos'][0]
|
| 361 |
+
return {
|
| 362 |
+
'success': True,
|
| 363 |
+
'type': 'image',
|
| 364 |
+
'provider': 'Pexels',
|
| 365 |
+
'id': photo['id'],
|
| 366 |
+
'title': query,
|
| 367 |
+
'description': f"Photo by {photo['photographer']}",
|
| 368 |
+
'photographer': photo['photographer'],
|
| 369 |
+
'photographer_url': photo['photographer_url'],
|
| 370 |
+
'preview_url': photo['src']['small'],
|
| 371 |
+
'large_image_url': photo['src']['large'],
|
| 372 |
+
'webformat_url': photo['src']['original'],
|
| 373 |
+
'width': photo['width'],
|
| 374 |
+
'height': photo['height'],
|
| 375 |
+
'page_url': photo['url']
|
| 376 |
+
}
|
| 377 |
+
return {'success': False, 'error': 'No results found'}
|
| 378 |
+
except Exception as e:
|
| 379 |
+
return {'success': False, 'error': str(e)}
|
| 380 |
+
|
| 381 |
+
def _search_pexels_videos(self, query, page=1):
|
| 382 |
+
"""Search Pexels for videos"""
|
| 383 |
+
try:
|
| 384 |
+
url = "https://api.pexels.com/videos/search"
|
| 385 |
+
headers = {'Authorization': self.pexels_key}
|
| 386 |
+
params = {'query': query, 'per_page': 20, 'page': page}
|
| 387 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 388 |
+
data = response.json()
|
| 389 |
+
|
| 390 |
+
if data.get('videos') and len(data['videos']) > 0:
|
| 391 |
+
video = data['videos'][0]
|
| 392 |
+
video_file = None
|
| 393 |
+
for vf in video.get('video_files', []):
|
| 394 |
+
if vf.get('quality') == 'hd':
|
| 395 |
+
video_file = vf
|
| 396 |
+
break
|
| 397 |
+
if not video_file and video.get('video_files'):
|
| 398 |
+
video_file = video['video_files'][0]
|
| 399 |
+
|
| 400 |
+
if video_file:
|
| 401 |
+
return {
|
| 402 |
+
'success': True,
|
| 403 |
+
'type': 'video',
|
| 404 |
+
'provider': 'Pexels',
|
| 405 |
+
'id': video['id'],
|
| 406 |
+
'title': query,
|
| 407 |
+
'description': f"Video by {video.get('user', {}).get('name', 'Unknown')}",
|
| 408 |
+
'user': video.get('user', {}).get('name'),
|
| 409 |
+
'duration': video.get('duration'),
|
| 410 |
+
'preview_url': video.get('image'),
|
| 411 |
+
'download_url': video_file.get('link'),
|
| 412 |
+
'thumbnail': video.get('image'),
|
| 413 |
+
'width': video_file.get('width'),
|
| 414 |
+
'height': video_file.get('height'),
|
| 415 |
+
'quality': video_file.get('quality'),
|
| 416 |
+
'page_url': video.get('url')
|
| 417 |
+
}
|
| 418 |
+
return {'success': False, 'error': 'No results found'}
|
| 419 |
+
except Exception as e:
|
| 420 |
+
return {'success': False, 'error': str(e)}
|
| 421 |
+
|
| 422 |
+
# ============= OPTIONAL PROVIDER METHODS =============
|
| 423 |
+
|
| 424 |
+
def _search_unsplash_images(self, query, page=1):
|
| 425 |
+
"""Search Unsplash for high-quality professional images"""
|
| 426 |
+
try:
|
| 427 |
+
if not self.unsplash_key or self.unsplash_key == "YOUR_UNSPLASH_API_KEY":
|
| 428 |
+
return {'success': False, 'error': 'Unsplash API key not configured'}
|
| 429 |
+
|
| 430 |
+
url = "https://api.unsplash.com/search/photos"
|
| 431 |
+
headers = {'Authorization': f'Client-ID {self.unsplash_key}'}
|
| 432 |
+
params = {
|
| 433 |
+
'query': query,
|
| 434 |
+
'page': page,
|
| 435 |
+
'per_page': 20
|
| 436 |
+
}
|
| 437 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 438 |
+
data = response.json()
|
| 439 |
+
|
| 440 |
+
if data.get('results') and len(data['results']) > 0:
|
| 441 |
+
photo = data['results'][0]
|
| 442 |
+
return {
|
| 443 |
+
'success': True,
|
| 444 |
+
'type': 'image',
|
| 445 |
+
'provider': 'Unsplash',
|
| 446 |
+
'id': photo['id'],
|
| 447 |
+
'title': photo.get('alt_description', query),
|
| 448 |
+
'description': f"Photo by {photo['user']['name']}",
|
| 449 |
+
'photographer': photo['user']['name'],
|
| 450 |
+
'photographer_url': photo['user']['links']['html'],
|
| 451 |
+
'preview_url': photo['urls']['small'],
|
| 452 |
+
'large_image_url': photo['urls']['regular'],
|
| 453 |
+
'webformat_url': photo['urls']['full'],
|
| 454 |
+
'width': photo['width'],
|
| 455 |
+
'height': photo['height'],
|
| 456 |
+
'likes': photo['likes'],
|
| 457 |
+
'page_url': photo['links']['html']
|
| 458 |
+
}
|
| 459 |
+
return {'success': False, 'error': 'No results found'}
|
| 460 |
+
except Exception as e:
|
| 461 |
+
return {'success': False, 'error': str(e)}
|
| 462 |
+
|
| 463 |
+
def _search_giphy(self, query, page=1):
|
| 464 |
+
"""Search GIPHY for GIFs and animated images"""
|
| 465 |
+
try:
|
| 466 |
+
if not self.giphy_key or self.giphy_key == "YOUR_GIPHY_API_KEY":
|
| 467 |
+
return {'success': False, 'error': 'GIPHY API key not configured'}
|
| 468 |
+
|
| 469 |
+
url = "https://api.giphy.com/v1/gifs/search"
|
| 470 |
+
params = {
|
| 471 |
+
'api_key': self.giphy_key,
|
| 472 |
+
'q': query,
|
| 473 |
+
'limit': 20,
|
| 474 |
+
'offset': (page - 1) * 20,
|
| 475 |
+
'rating': 'g'
|
| 476 |
+
}
|
| 477 |
+
response = requests.get(url, params=params, timeout=10)
|
| 478 |
+
data = response.json()
|
| 479 |
+
|
| 480 |
+
if data.get('data') and len(data['data']) > 0:
|
| 481 |
+
gif = data['data'][0]
|
| 482 |
+
return {
|
| 483 |
+
'success': True,
|
| 484 |
+
'type': 'image',
|
| 485 |
+
'provider': 'GIPHY',
|
| 486 |
+
'id': gif['id'],
|
| 487 |
+
'title': gif.get('title', query),
|
| 488 |
+
'description': f"GIF: {gif.get('title', query)}",
|
| 489 |
+
'preview_url': gif['images']['fixed_width_small']['url'],
|
| 490 |
+
'large_image_url': gif['images']['original']['url'],
|
| 491 |
+
'webformat_url': gif['images']['original']['url'],
|
| 492 |
+
'width': gif['images']['original']['width'],
|
| 493 |
+
'height': gif['images']['original']['height'],
|
| 494 |
+
'page_url': gif['url']
|
| 495 |
+
}
|
| 496 |
+
return {'success': False, 'error': 'No results found'}
|
| 497 |
+
except Exception as e:
|
| 498 |
+
return {'success': False, 'error': str(e)}
|
| 499 |
+
|
| 500 |
+
def _search_flickr(self, query, page=1):
|
| 501 |
+
"""Search Flickr for Creative Commons licensed images"""
|
| 502 |
+
try:
|
| 503 |
+
if not self.flickr_key or self.flickr_key == "YOUR_FLICKR_API_KEY":
|
| 504 |
+
return {'success': False, 'error': 'Flickr API key not configured'}
|
| 505 |
+
|
| 506 |
+
url = "https://www.flickr.com/services/rest/"
|
| 507 |
+
params = {
|
| 508 |
+
'method': 'flickr.photos.search',
|
| 509 |
+
'api_key': self.flickr_key,
|
| 510 |
+
'text': query,
|
| 511 |
+
'per_page': 20,
|
| 512 |
+
'page': page,
|
| 513 |
+
'format': 'json',
|
| 514 |
+
'nojsoncallback': 1,
|
| 515 |
+
'license': '1,2,3,4,5,6',
|
| 516 |
+
'content_type': 1,
|
| 517 |
+
'sort': 'relevance',
|
| 518 |
+
'safe_search': 1
|
| 519 |
+
}
|
| 520 |
+
response = requests.get(url, params=params, timeout=10)
|
| 521 |
+
data = response.json()
|
| 522 |
+
|
| 523 |
+
if data.get('photos') and data['photos'].get('photo'):
|
| 524 |
+
photo = data['photos']['photo'][0]
|
| 525 |
+
farm_id = photo['farm']
|
| 526 |
+
server_id = photo['server']
|
| 527 |
+
photo_id = photo['id']
|
| 528 |
+
secret = photo['secret']
|
| 529 |
+
|
| 530 |
+
preview_url = f"https://farm{farm_id}.staticflickr.com/{server_id}/{photo_id}_{secret}_m.jpg"
|
| 531 |
+
large_url = f"https://farm{farm_id}.staticflickr.com/{server_id}/{photo_id}_{secret}_b.jpg"
|
| 532 |
+
|
| 533 |
+
return {
|
| 534 |
+
'success': True,
|
| 535 |
+
'type': 'image',
|
| 536 |
+
'provider': 'Flickr',
|
| 537 |
+
'id': photo_id,
|
| 538 |
+
'title': photo.get('title', query),
|
| 539 |
+
'description': f"Photo by {photo.get('ownername', 'Unknown')}",
|
| 540 |
+
'photographer': photo.get('ownername', 'Unknown'),
|
| 541 |
+
'preview_url': preview_url,
|
| 542 |
+
'large_image_url': large_url,
|
| 543 |
+
'webformat_url': large_url,
|
| 544 |
+
'page_url': f"https://www.flickr.com/photos/{photo['owner']}/{photo_id}"
|
| 545 |
+
}
|
| 546 |
+
return {'success': False, 'error': 'No results found'}
|
| 547 |
+
except Exception as e:
|
| 548 |
+
return {'success': False, 'error': str(e)}
|
| 549 |
+
|
| 550 |
+
def _search_dailymotion(self, query, page=1):
|
| 551 |
+
"""Search Dailymotion for videos (no API key required)"""
|
| 552 |
+
try:
|
| 553 |
+
url = "https://api.dailymotion.com/videos"
|
| 554 |
+
params = {
|
| 555 |
+
'search': query,
|
| 556 |
+
'limit': 20,
|
| 557 |
+
'page': page,
|
| 558 |
+
'fields': 'id,title,description,thumbnail_360_url,url,duration,views_total,owner.screenname,created_time'
|
| 559 |
+
}
|
| 560 |
+
response = requests.get(url, params=params, timeout=10)
|
| 561 |
+
data = response.json()
|
| 562 |
+
|
| 563 |
+
if data.get('list') and len(data['list']) > 0:
|
| 564 |
+
video = data['list'][0]
|
| 565 |
+
video_id = video['id']
|
| 566 |
+
embed_url = f"https://www.dailymotion.com/embed/video/{video_id}"
|
| 567 |
+
embed_url_autoplay = f"https://www.dailymotion.com/embed/video/{video_id}?autoplay=1"
|
| 568 |
+
|
| 569 |
+
return {
|
| 570 |
+
'success': True,
|
| 571 |
+
'type': 'video',
|
| 572 |
+
'provider': 'Dailymotion',
|
| 573 |
+
'id': video_id,
|
| 574 |
+
'title': video.get('title', query),
|
| 575 |
+
'description': video.get('description', ''),
|
| 576 |
+
'user': video.get('owner', {}).get('screenname', 'Unknown'),
|
| 577 |
+
'duration': video.get('duration'),
|
| 578 |
+
'preview_url': video.get('thumbnail_360_url'),
|
| 579 |
+
'download_url': embed_url,
|
| 580 |
+
'embed_url': embed_url,
|
| 581 |
+
'embed_url_autoplay': embed_url_autoplay,
|
| 582 |
+
'thumbnail': video.get('thumbnail_360_url'),
|
| 583 |
+
'views': video.get('views_total', 0),
|
| 584 |
+
'created_time': video.get('created_time'),
|
| 585 |
+
'page_url': f"https://www.dailymotion.com/video/{video_id}"
|
| 586 |
+
}
|
| 587 |
+
return {'success': False, 'error': 'No results found'}
|
| 588 |
+
except Exception as e:
|
| 589 |
+
return {'success': False, 'error': str(e)}
|
| 590 |
+
|
| 591 |
+
def _search_vimeo(self, query, page=1):
|
| 592 |
+
"""Search Vimeo for high-quality professional videos"""
|
| 593 |
+
try:
|
| 594 |
+
if not self.vimeo_token or self.vimeo_token == "YOUR_VIMEO_TOKEN":
|
| 595 |
+
return {'success': False, 'error': 'Vimeo access token not configured'}
|
| 596 |
+
|
| 597 |
+
url = "https://api.vimeo.com/videos"
|
| 598 |
+
headers = {'Authorization': f'Bearer {self.vimeo_token}'}
|
| 599 |
+
params = {
|
| 600 |
+
'query': query,
|
| 601 |
+
'per_page': 20,
|
| 602 |
+
'page': page,
|
| 603 |
+
'sort': 'relevant'
|
| 604 |
+
}
|
| 605 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 606 |
+
data = response.json()
|
| 607 |
+
|
| 608 |
+
if data.get('data') and len(data['data']) > 0:
|
| 609 |
+
video = data['data'][0]
|
| 610 |
+
thumbnail = video.get('pictures', {}).get('sizes', [])
|
| 611 |
+
thumbnail_url = thumbnail[-1]['link'] if thumbnail else video.get('pictures', {}).get('uri')
|
| 612 |
+
duration = video.get('duration', 0)
|
| 613 |
+
duration_str = f"{duration // 60}:{duration % 60:02d}" if duration else "Unknown"
|
| 614 |
+
|
| 615 |
+
return {
|
| 616 |
+
'success': True,
|
| 617 |
+
'type': 'video',
|
| 618 |
+
'provider': 'Vimeo',
|
| 619 |
+
'id': video['uri'].split('/')[-1],
|
| 620 |
+
'title': video.get('name', query),
|
| 621 |
+
'description': video.get('description', ''),
|
| 622 |
+
'user': video.get('user', {}).get('name', 'Unknown'),
|
| 623 |
+
'duration': duration_str,
|
| 624 |
+
'preview_url': thumbnail_url,
|
| 625 |
+
'download_url': video.get('link'),
|
| 626 |
+
'thumbnail': thumbnail_url,
|
| 627 |
+
'likes': video.get('metadata', {}).get('connections', {}).get('likes', {}).get('total', 0),
|
| 628 |
+
'views': video.get('metadata', {}).get('connections', {}).get('views', {}).get('total', 0),
|
| 629 |
+
'page_url': video.get('link')
|
| 630 |
+
}
|
| 631 |
+
return {'success': False, 'error': 'No results found'}
|
| 632 |
+
except Exception as e:
|
| 633 |
+
return {'success': False, 'error': str(e)}
|
| 634 |
+
|
| 635 |
+
# ============= REGENERATION METHODS =============
|
| 636 |
+
|
| 637 |
+
def regenerate_media(self, query, media_type, current_id, provider='pixabay'):
|
| 638 |
+
"""Get a different result (skip current one)"""
|
| 639 |
+
try:
|
| 640 |
+
if provider == 'pixabay':
|
| 641 |
+
if media_type == 'image':
|
| 642 |
+
return self._get_next_pixabay_image(query, current_id)
|
| 643 |
+
else:
|
| 644 |
+
return self._get_next_pixabay_video(query, current_id)
|
| 645 |
+
elif provider == 'pexels':
|
| 646 |
+
if media_type == 'image':
|
| 647 |
+
return self._get_next_pexels_image(query, current_id)
|
| 648 |
+
else:
|
| 649 |
+
return self._get_next_pexels_video(query, current_id)
|
| 650 |
+
elif provider == 'duckduckgo':
|
| 651 |
+
if media_type == 'image':
|
| 652 |
+
return self._get_next_duckduckgo_image(query, current_id)
|
| 653 |
+
else:
|
| 654 |
+
return self._get_next_duckduckgo_video(query, current_id)
|
| 655 |
+
elif provider == 'openverse' and media_type == 'image':
|
| 656 |
+
return self._get_next_openverse(query, current_id)
|
| 657 |
+
elif provider == 'unsplash' and media_type == 'image':
|
| 658 |
+
return self._get_next_unsplash_image(query, current_id)
|
| 659 |
+
elif provider == 'giphy' and media_type == 'image':
|
| 660 |
+
return self._get_next_giphy(query, current_id)
|
| 661 |
+
elif provider == 'flickr' and media_type == 'image':
|
| 662 |
+
return self._get_next_flickr(query, current_id)
|
| 663 |
+
elif provider == 'dailymotion' and media_type == 'video':
|
| 664 |
+
return self._get_next_dailymotion(query, current_id)
|
| 665 |
+
elif provider == 'vimeo' and media_type == 'video':
|
| 666 |
+
return self._get_next_vimeo(query, current_id)
|
| 667 |
+
except Exception as e:
|
| 668 |
+
return {'success': False, 'error': str(e)}
|
| 669 |
+
return {'success': False, 'error': 'Regeneration not supported for this provider'}
|
| 670 |
+
|
| 671 |
+
# ============= FALLBACK METHODS =============
|
| 672 |
+
|
| 673 |
+
def search_with_fallback(self, query, media_type='image', max_attempts=5):
|
| 674 |
+
"""Search with automatic fallback to different providers if one fails"""
|
| 675 |
+
if media_type == 'image':
|
| 676 |
+
providers = self.available_image_providers.copy()
|
| 677 |
+
else:
|
| 678 |
+
providers = self.available_video_providers.copy()
|
| 679 |
+
|
| 680 |
+
random.shuffle(providers)
|
| 681 |
+
|
| 682 |
+
last_error = None
|
| 683 |
+
|
| 684 |
+
for attempt, provider in enumerate(providers[:max_attempts]):
|
| 685 |
+
try:
|
| 686 |
+
print(f"Attempting {media_type} search with provider: {provider} (attempt {attempt + 1})")
|
| 687 |
+
|
| 688 |
+
if media_type == 'image':
|
| 689 |
+
result = self.search_images(query, provider)
|
| 690 |
+
else:
|
| 691 |
+
result = self.search_videos(query, provider)
|
| 692 |
+
|
| 693 |
+
if result.get('success'):
|
| 694 |
+
print(f"✓ Success with {provider}")
|
| 695 |
+
return result
|
| 696 |
+
else:
|
| 697 |
+
last_error = result.get('error', 'No results')
|
| 698 |
+
print(f"✗ {provider} failed: {last_error}")
|
| 699 |
+
|
| 700 |
+
except Exception as e:
|
| 701 |
+
last_error = str(e)
|
| 702 |
+
print(f"✗ {provider} error: {last_error}")
|
| 703 |
+
continue
|
| 704 |
+
|
| 705 |
+
return {'success': False, 'error': f'All providers failed. Last error: {last_error}'}
|
| 706 |
+
|
| 707 |
+
def regenerate_with_fallback(self, query, media_type, current_id, provider=None):
|
| 708 |
+
"""Regenerate with automatic fallback to different providers"""
|
| 709 |
+
if media_type == 'image':
|
| 710 |
+
providers = self.available_image_providers.copy()
|
| 711 |
+
else:
|
| 712 |
+
providers = self.available_video_providers.copy()
|
| 713 |
+
|
| 714 |
+
if provider and provider in providers:
|
| 715 |
+
providers.remove(provider)
|
| 716 |
+
providers.insert(0, provider)
|
| 717 |
+
|
| 718 |
+
remaining = providers[1:]
|
| 719 |
+
random.shuffle(remaining)
|
| 720 |
+
providers = [providers[0]] + remaining
|
| 721 |
+
|
| 722 |
+
last_error = None
|
| 723 |
+
|
| 724 |
+
for attempt, prov in enumerate(providers[:5]):
|
| 725 |
+
try:
|
| 726 |
+
print(f"Regenerating with provider: {prov} (attempt {attempt + 1})")
|
| 727 |
+
result = self.regenerate_media(query, media_type, current_id, prov)
|
| 728 |
+
|
| 729 |
+
if result.get('success'):
|
| 730 |
+
print(f"✓ Regeneration success with {prov}")
|
| 731 |
+
return result
|
| 732 |
+
else:
|
| 733 |
+
last_error = result.get('error', 'No different result')
|
| 734 |
+
print(f"✗ {prov} regeneration failed: {last_error}")
|
| 735 |
+
|
| 736 |
+
except Exception as e:
|
| 737 |
+
last_error = str(e)
|
| 738 |
+
print(f"✗ {prov} regeneration error: {last_error}")
|
| 739 |
+
continue
|
| 740 |
+
|
| 741 |
+
return {'success': False, 'error': f'All providers failed. Last error: {last_error}'}
|
| 742 |
+
|
| 743 |
+
# ============= NEXT RESULT METHODS =============
|
| 744 |
+
|
| 745 |
+
def _get_next_pixabay_image(self, query, current_id):
|
| 746 |
+
"""Get next image from Pixabay"""
|
| 747 |
+
try:
|
| 748 |
+
url = "https://pixabay.com/api/"
|
| 749 |
+
params = {
|
| 750 |
+
'key': self.pixabay_key,
|
| 751 |
+
'q': query,
|
| 752 |
+
'image_type': 'photo',
|
| 753 |
+
'per_page': 20,
|
| 754 |
+
'safesearch': 'true'
|
| 755 |
+
}
|
| 756 |
+
response = requests.get(url, params=params, timeout=10)
|
| 757 |
+
data = response.json()
|
| 758 |
+
|
| 759 |
+
if data.get('totalHits', 0) > 1:
|
| 760 |
+
for hit in data['hits']:
|
| 761 |
+
if str(hit['id']) != str(current_id):
|
| 762 |
+
return {
|
| 763 |
+
'success': True,
|
| 764 |
+
'type': 'image',
|
| 765 |
+
'provider': 'Pixabay',
|
| 766 |
+
'id': hit['id'],
|
| 767 |
+
'title': hit.get('tags', query),
|
| 768 |
+
'description': f"Image by {hit.get('user', 'Unknown')}",
|
| 769 |
+
'photographer': hit.get('user'),
|
| 770 |
+
'photographer_url': hit.get('userImageURL'),
|
| 771 |
+
'preview_url': hit.get('previewURL'),
|
| 772 |
+
'large_image_url': hit.get('largeImageURL'),
|
| 773 |
+
'webformat_url': hit.get('webformatURL'),
|
| 774 |
+
'width': hit.get('imageWidth'),
|
| 775 |
+
'height': hit.get('imageHeight'),
|
| 776 |
+
'likes': hit.get('likes'),
|
| 777 |
+
'views': hit.get('views'),
|
| 778 |
+
'downloads': hit.get('downloads'),
|
| 779 |
+
'page_url': hit.get('pageURL')
|
| 780 |
+
}
|
| 781 |
+
return {'success': False, 'error': 'No different result found'}
|
| 782 |
+
except Exception as e:
|
| 783 |
+
return {'success': False, 'error': str(e)}
|
| 784 |
+
|
| 785 |
+
def _get_next_pixabay_video(self, query, current_id):
|
| 786 |
+
"""Get next video from Pixabay"""
|
| 787 |
+
try:
|
| 788 |
+
url = "https://pixabay.com/api/videos/"
|
| 789 |
+
params = {
|
| 790 |
+
'key': self.pixabay_key,
|
| 791 |
+
'q': query,
|
| 792 |
+
'per_page': 20,
|
| 793 |
+
'safesearch': 'true'
|
| 794 |
+
}
|
| 795 |
+
response = requests.get(url, params=params, timeout=10)
|
| 796 |
+
data = response.json()
|
| 797 |
+
|
| 798 |
+
if data.get('totalHits', 0) > 1:
|
| 799 |
+
for hit in data['hits']:
|
| 800 |
+
if str(hit['id']) != str(current_id):
|
| 801 |
+
videos = hit.get('videos', {})
|
| 802 |
+
video_source = None
|
| 803 |
+
for quality in ['large', 'medium', 'small', 'tiny']:
|
| 804 |
+
if quality in videos and videos[quality].get('url'):
|
| 805 |
+
video_source = videos[quality]
|
| 806 |
+
break
|
| 807 |
+
if video_source:
|
| 808 |
+
return {
|
| 809 |
+
'success': True,
|
| 810 |
+
'type': 'video',
|
| 811 |
+
'provider': 'Pixabay',
|
| 812 |
+
'id': hit['id'],
|
| 813 |
+
'title': hit.get('tags', query),
|
| 814 |
+
'description': f"Video by {hit.get('user', 'Unknown')}",
|
| 815 |
+
'user': hit.get('user'),
|
| 816 |
+
'duration': hit.get('duration'),
|
| 817 |
+
'preview_url': hit.get('videos', {}).get('tiny', {}).get('url'),
|
| 818 |
+
'download_url': video_source.get('url'),
|
| 819 |
+
'thumbnail': hit.get('videos', {}).get('tiny', {}).get('thumbnail'),
|
| 820 |
+
'width': video_source.get('width'),
|
| 821 |
+
'height': video_source.get('height'),
|
| 822 |
+
'quality': quality,
|
| 823 |
+
'likes': hit.get('likes'),
|
| 824 |
+
'views': hit.get('views'),
|
| 825 |
+
'downloads': hit.get('downloads'),
|
| 826 |
+
'page_url': hit.get('pageURL')
|
| 827 |
+
}
|
| 828 |
+
return {'success': False, 'error': 'No different result found'}
|
| 829 |
+
except Exception as e:
|
| 830 |
+
return {'success': False, 'error': str(e)}
|
| 831 |
+
|
| 832 |
+
def _get_next_pexels_image(self, query, current_id):
|
| 833 |
+
"""Get next image from Pexels"""
|
| 834 |
+
try:
|
| 835 |
+
url = "https://api.pexels.com/v1/search"
|
| 836 |
+
headers = {'Authorization': self.pexels_key}
|
| 837 |
+
params = {'query': query, 'per_page': 20}
|
| 838 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 839 |
+
data = response.json()
|
| 840 |
+
|
| 841 |
+
if data.get('photos') and len(data['photos']) > 1:
|
| 842 |
+
for photo in data['photos']:
|
| 843 |
+
if str(photo['id']) != str(current_id):
|
| 844 |
+
return {
|
| 845 |
+
'success': True,
|
| 846 |
+
'type': 'image',
|
| 847 |
+
'provider': 'Pexels',
|
| 848 |
+
'id': photo['id'],
|
| 849 |
+
'title': query,
|
| 850 |
+
'description': f"Photo by {photo['photographer']}",
|
| 851 |
+
'photographer': photo['photographer'],
|
| 852 |
+
'photographer_url': photo['photographer_url'],
|
| 853 |
+
'preview_url': photo['src']['small'],
|
| 854 |
+
'large_image_url': photo['src']['large'],
|
| 855 |
+
'webformat_url': photo['src']['original'],
|
| 856 |
+
'width': photo['width'],
|
| 857 |
+
'height': photo['height'],
|
| 858 |
+
'page_url': photo['url']
|
| 859 |
+
}
|
| 860 |
+
return {'success': False, 'error': 'No different result found'}
|
| 861 |
+
except Exception as e:
|
| 862 |
+
return {'success': False, 'error': str(e)}
|
| 863 |
+
|
| 864 |
+
def _get_next_pexels_video(self, query, current_id):
|
| 865 |
+
"""Get next video from Pexels"""
|
| 866 |
+
try:
|
| 867 |
+
url = "https://api.pexels.com/videos/search"
|
| 868 |
+
headers = {'Authorization': self.pexels_key}
|
| 869 |
+
params = {'query': query, 'per_page': 20}
|
| 870 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 871 |
+
data = response.json()
|
| 872 |
+
|
| 873 |
+
if data.get('videos') and len(data['videos']) > 1:
|
| 874 |
+
for video in data['videos']:
|
| 875 |
+
if str(video['id']) != str(current_id):
|
| 876 |
+
video_file = None
|
| 877 |
+
for vf in video.get('video_files', []):
|
| 878 |
+
if vf.get('quality') == 'hd':
|
| 879 |
+
video_file = vf
|
| 880 |
+
break
|
| 881 |
+
if not video_file and video.get('video_files'):
|
| 882 |
+
video_file = video['video_files'][0]
|
| 883 |
+
|
| 884 |
+
if video_file:
|
| 885 |
+
return {
|
| 886 |
+
'success': True,
|
| 887 |
+
'type': 'video',
|
| 888 |
+
'provider': 'Pexels',
|
| 889 |
+
'id': video['id'],
|
| 890 |
+
'title': query,
|
| 891 |
+
'description': f"Video by {video.get('user', {}).get('name', 'Unknown')}",
|
| 892 |
+
'user': video.get('user', {}).get('name'),
|
| 893 |
+
'duration': video.get('duration'),
|
| 894 |
+
'preview_url': video.get('image'),
|
| 895 |
+
'download_url': video_file.get('link'),
|
| 896 |
+
'thumbnail': video.get('image'),
|
| 897 |
+
'width': video_file.get('width'),
|
| 898 |
+
'height': video_file.get('height'),
|
| 899 |
+
'quality': video_file.get('quality'),
|
| 900 |
+
'page_url': video.get('url')
|
| 901 |
+
}
|
| 902 |
+
return {'success': False, 'error': 'No different result found'}
|
| 903 |
+
except Exception as e:
|
| 904 |
+
return {'success': False, 'error': str(e)}
|
| 905 |
+
|
| 906 |
+
def _get_next_duckduckgo_image(self, query, current_id):
|
| 907 |
+
"""Get next image from DuckDuckGo (no safe search)"""
|
| 908 |
+
try:
|
| 909 |
+
url = "https://duckduckgo.com/i.js"
|
| 910 |
+
params = {
|
| 911 |
+
'q': query,
|
| 912 |
+
'o': 'json',
|
| 913 |
+
'p': 1,
|
| 914 |
+
'l': 'us-en',
|
| 915 |
+
'f': ',,',
|
| 916 |
+
'kp': -2 # No safe search
|
| 917 |
+
}
|
| 918 |
+
vqd = self._get_vqd_for_search(query)
|
| 919 |
+
if vqd:
|
| 920 |
+
params['vqd'] = vqd
|
| 921 |
+
|
| 922 |
+
headers = {
|
| 923 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 924 |
+
'Accept': 'application/json'
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
response = requests.get(url, params=params, headers=headers, timeout=10)
|
| 928 |
+
|
| 929 |
+
if response.status_code == 200:
|
| 930 |
+
data = response.json()
|
| 931 |
+
results = data.get('results', [])
|
| 932 |
+
|
| 933 |
+
found = False
|
| 934 |
+
for result in results:
|
| 935 |
+
if str(result.get('id', '')) != str(current_id):
|
| 936 |
+
if not found:
|
| 937 |
+
found = True
|
| 938 |
+
return {
|
| 939 |
+
'success': True,
|
| 940 |
+
'type': 'image',
|
| 941 |
+
'provider': 'DuckDuckGo',
|
| 942 |
+
'id': result.get('id', str(hash(result.get('image', '')))),
|
| 943 |
+
'title': result.get('title', query),
|
| 944 |
+
'description': result.get('title', ''),
|
| 945 |
+
'photographer': result.get('source', 'Unknown'),
|
| 946 |
+
'preview_url': result.get('thumbnail', result.get('image')),
|
| 947 |
+
'large_image_url': result.get('image', result.get('thumbnail')),
|
| 948 |
+
'width': result.get('width', 0),
|
| 949 |
+
'height': result.get('height', 0),
|
| 950 |
+
'page_url': result.get('url', '')
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
return {'success': False, 'error': 'No different result found'}
|
| 954 |
+
except Exception as e:
|
| 955 |
+
return {'success': False, 'error': str(e)}
|
| 956 |
+
|
| 957 |
+
def _get_next_duckduckgo_video(self, query, current_id):
|
| 958 |
+
"""Get next video from DuckDuckGo (no safe search)"""
|
| 959 |
+
try:
|
| 960 |
+
url = "https://duckduckgo.com/v.js"
|
| 961 |
+
params = {
|
| 962 |
+
'q': query,
|
| 963 |
+
'o': 'json',
|
| 964 |
+
'p': 1,
|
| 965 |
+
'l': 'us-en',
|
| 966 |
+
'f': ',,',
|
| 967 |
+
'kp': -2 # No safe search
|
| 968 |
+
}
|
| 969 |
+
vqd = self._get_vqd_for_search(query)
|
| 970 |
+
if vqd:
|
| 971 |
+
params['vqd'] = vqd
|
| 972 |
+
|
| 973 |
+
headers = {
|
| 974 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 975 |
+
'Accept': 'application/json'
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
response = requests.get(url, params=params, headers=headers, timeout=10)
|
| 979 |
+
|
| 980 |
+
if response.status_code == 200:
|
| 981 |
+
data = response.json()
|
| 982 |
+
results = data.get('results', [])
|
| 983 |
+
|
| 984 |
+
for result in results:
|
| 985 |
+
if str(result.get('id', '')) != str(current_id):
|
| 986 |
+
return {
|
| 987 |
+
'success': True,
|
| 988 |
+
'type': 'video',
|
| 989 |
+
'provider': 'DuckDuckGo',
|
| 990 |
+
'id': result.get('id', str(hash(result.get('content', '')))),
|
| 991 |
+
'title': result.get('title', query),
|
| 992 |
+
'description': result.get('description', ''),
|
| 993 |
+
'user': result.get('publisher', 'Unknown'),
|
| 994 |
+
'duration': result.get('duration', 'Unknown'),
|
| 995 |
+
'preview_url': result.get('thumbnail', ''),
|
| 996 |
+
'download_url': result.get('content', ''),
|
| 997 |
+
'embed_url': result.get('embed_url', ''),
|
| 998 |
+
'views': result.get('views', 0),
|
| 999 |
+
'page_url': result.get('url', '')
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
return {'success': False, 'error': 'No different result found'}
|
| 1003 |
+
except Exception as e:
|
| 1004 |
+
return {'success': False, 'error': str(e)}
|
| 1005 |
+
|
| 1006 |
+
def _get_next_openverse(self, query, current_id):
|
| 1007 |
+
"""Get next image from Openverse"""
|
| 1008 |
+
try:
|
| 1009 |
+
url = "https://api.openverse.engineering/v1/images/"
|
| 1010 |
+
params = {
|
| 1011 |
+
'q': query,
|
| 1012 |
+
'page_size': 20,
|
| 1013 |
+
'page': 1
|
| 1014 |
+
}
|
| 1015 |
+
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
|
| 1016 |
+
response = requests.get(url, params=params, headers=headers, timeout=10)
|
| 1017 |
+
data = response.json()
|
| 1018 |
+
|
| 1019 |
+
if data.get('results') and len(data['results']) > 1:
|
| 1020 |
+
for result in data['results']:
|
| 1021 |
+
if str(result.get('id', '')) != str(current_id):
|
| 1022 |
+
return {
|
| 1023 |
+
'success': True,
|
| 1024 |
+
'type': 'image',
|
| 1025 |
+
'provider': 'Openverse',
|
| 1026 |
+
'id': result.get('id', ''),
|
| 1027 |
+
'title': result.get('title', query),
|
| 1028 |
+
'description': f"By {result.get('creator', 'Unknown')} - License: {result.get('license', 'Unknown')}",
|
| 1029 |
+
'photographer': result.get('creator', 'Unknown'),
|
| 1030 |
+
'preview_url': result.get('thumbnail', result.get('url')),
|
| 1031 |
+
'large_image_url': result.get('url', ''),
|
| 1032 |
+
'width': result.get('width', 0),
|
| 1033 |
+
'height': result.get('height', 0),
|
| 1034 |
+
'license': result.get('license', ''),
|
| 1035 |
+
'page_url': result.get('foreign_landing_url', '')
|
| 1036 |
+
}
|
| 1037 |
+
return {'success': False, 'error': 'No different result found'}
|
| 1038 |
+
except Exception as e:
|
| 1039 |
+
return {'success': False, 'error': str(e)}
|
| 1040 |
+
|
| 1041 |
+
def _get_next_unsplash_image(self, query, current_id):
|
| 1042 |
+
"""Get next image from Unsplash"""
|
| 1043 |
+
try:
|
| 1044 |
+
if not self.unsplash_key or self.unsplash_key == "YOUR_UNSPLASH_API_KEY":
|
| 1045 |
+
return {'success': False, 'error': 'Unsplash API key not configured'}
|
| 1046 |
+
|
| 1047 |
+
url = "https://api.unsplash.com/search/photos"
|
| 1048 |
+
headers = {'Authorization': f'Client-ID {self.unsplash_key}'}
|
| 1049 |
+
params = {'query': query, 'per_page': 20}
|
| 1050 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 1051 |
+
data = response.json()
|
| 1052 |
+
|
| 1053 |
+
if data.get('results') and len(data['results']) > 1:
|
| 1054 |
+
for photo in data['results']:
|
| 1055 |
+
if str(photo['id']) != str(current_id):
|
| 1056 |
+
return {
|
| 1057 |
+
'success': True,
|
| 1058 |
+
'type': 'image',
|
| 1059 |
+
'provider': 'Unsplash',
|
| 1060 |
+
'id': photo['id'],
|
| 1061 |
+
'title': photo.get('alt_description', query),
|
| 1062 |
+
'description': f"Photo by {photo['user']['name']}",
|
| 1063 |
+
'photographer': photo['user']['name'],
|
| 1064 |
+
'photographer_url': photo['user']['links']['html'],
|
| 1065 |
+
'preview_url': photo['urls']['small'],
|
| 1066 |
+
'large_image_url': photo['urls']['regular'],
|
| 1067 |
+
'webformat_url': photo['urls']['full'],
|
| 1068 |
+
'width': photo['width'],
|
| 1069 |
+
'height': photo['height'],
|
| 1070 |
+
'likes': photo['likes'],
|
| 1071 |
+
'page_url': photo['links']['html']
|
| 1072 |
+
}
|
| 1073 |
+
return {'success': False, 'error': 'No different result found'}
|
| 1074 |
+
except Exception as e:
|
| 1075 |
+
return {'success': False, 'error': str(e)}
|
| 1076 |
+
|
| 1077 |
+
def _get_next_giphy(self, query, current_id):
|
| 1078 |
+
"""Get next GIF from GIPHY"""
|
| 1079 |
+
try:
|
| 1080 |
+
if not self.giphy_key or self.giphy_key == "YOUR_GIPHY_API_KEY":
|
| 1081 |
+
return {'success': False, 'error': 'GIPHY API key not configured'}
|
| 1082 |
+
|
| 1083 |
+
url = "https://api.giphy.com/v1/gifs/search"
|
| 1084 |
+
params = {
|
| 1085 |
+
'api_key': self.giphy_key,
|
| 1086 |
+
'q': query,
|
| 1087 |
+
'limit': 20,
|
| 1088 |
+
'rating': 'g'
|
| 1089 |
+
}
|
| 1090 |
+
response = requests.get(url, params=params, timeout=10)
|
| 1091 |
+
data = response.json()
|
| 1092 |
+
|
| 1093 |
+
if data.get('data') and len(data['data']) > 1:
|
| 1094 |
+
for gif in data['data']:
|
| 1095 |
+
if str(gif['id']) != str(current_id):
|
| 1096 |
+
return {
|
| 1097 |
+
'success': True,
|
| 1098 |
+
'type': 'image',
|
| 1099 |
+
'provider': 'GIPHY',
|
| 1100 |
+
'id': gif['id'],
|
| 1101 |
+
'title': gif.get('title', query),
|
| 1102 |
+
'description': f"GIF: {gif.get('title', query)}",
|
| 1103 |
+
'preview_url': gif['images']['fixed_width_small']['url'],
|
| 1104 |
+
'large_image_url': gif['images']['original']['url'],
|
| 1105 |
+
'webformat_url': gif['images']['original']['url'],
|
| 1106 |
+
'width': gif['images']['original']['width'],
|
| 1107 |
+
'height': gif['images']['original']['height'],
|
| 1108 |
+
'page_url': gif['url']
|
| 1109 |
+
}
|
| 1110 |
+
return {'success': False, 'error': 'No different result found'}
|
| 1111 |
+
except Exception as e:
|
| 1112 |
+
return {'success': False, 'error': str(e)}
|
| 1113 |
+
|
| 1114 |
+
def _get_next_flickr(self, query, current_id):
|
| 1115 |
+
"""Get next image from Flickr"""
|
| 1116 |
+
try:
|
| 1117 |
+
if not self.flickr_key or self.flickr_key == "YOUR_FLICKR_API_KEY":
|
| 1118 |
+
return {'success': False, 'error': 'Flickr API key not configured'}
|
| 1119 |
+
|
| 1120 |
+
url = "https://www.flickr.com/services/rest/"
|
| 1121 |
+
params = {
|
| 1122 |
+
'method': 'flickr.photos.search',
|
| 1123 |
+
'api_key': self.flickr_key,
|
| 1124 |
+
'text': query,
|
| 1125 |
+
'per_page': 20,
|
| 1126 |
+
'format': 'json',
|
| 1127 |
+
'nojsoncallback': 1,
|
| 1128 |
+
'license': '1,2,3,4,5,6',
|
| 1129 |
+
'content_type': 1,
|
| 1130 |
+
'sort': 'relevance',
|
| 1131 |
+
'safe_search': 1
|
| 1132 |
+
}
|
| 1133 |
+
response = requests.get(url, params=params, timeout=10)
|
| 1134 |
+
data = response.json()
|
| 1135 |
+
|
| 1136 |
+
if data.get('photos') and data['photos'].get('photo') and len(data['photos']['photo']) > 1:
|
| 1137 |
+
for photo in data['photos']['photo']:
|
| 1138 |
+
if str(photo['id']) != str(current_id):
|
| 1139 |
+
farm_id = photo['farm']
|
| 1140 |
+
server_id = photo['server']
|
| 1141 |
+
photo_id = photo['id']
|
| 1142 |
+
secret = photo['secret']
|
| 1143 |
+
|
| 1144 |
+
preview_url = f"https://farm{farm_id}.staticflickr.com/{server_id}/{photo_id}_{secret}_m.jpg"
|
| 1145 |
+
large_url = f"https://farm{farm_id}.staticflickr.com/{server_id}/{photo_id}_{secret}_b.jpg"
|
| 1146 |
+
|
| 1147 |
+
return {
|
| 1148 |
+
'success': True,
|
| 1149 |
+
'type': 'image',
|
| 1150 |
+
'provider': 'Flickr',
|
| 1151 |
+
'id': photo_id,
|
| 1152 |
+
'title': photo.get('title', query),
|
| 1153 |
+
'description': f"Photo by {photo.get('ownername', 'Unknown')}",
|
| 1154 |
+
'photographer': photo.get('ownername', 'Unknown'),
|
| 1155 |
+
'preview_url': preview_url,
|
| 1156 |
+
'large_image_url': large_url,
|
| 1157 |
+
'webformat_url': large_url,
|
| 1158 |
+
'page_url': f"https://www.flickr.com/photos/{photo['owner']}/{photo_id}"
|
| 1159 |
+
}
|
| 1160 |
+
return {'success': False, 'error': 'No different result found'}
|
| 1161 |
+
except Exception as e:
|
| 1162 |
+
return {'success': False, 'error': str(e)}
|
| 1163 |
+
|
| 1164 |
+
def _get_next_dailymotion(self, query, current_id):
|
| 1165 |
+
"""Get next video from Dailymotion"""
|
| 1166 |
+
try:
|
| 1167 |
+
url = "https://api.dailymotion.com/videos"
|
| 1168 |
+
params = {
|
| 1169 |
+
'search': query,
|
| 1170 |
+
'limit': 20,
|
| 1171 |
+
'fields': 'id,title,description,thumbnail_360_url,url,duration,views_total,owner.screenname'
|
| 1172 |
+
}
|
| 1173 |
+
response = requests.get(url, params=params, timeout=10)
|
| 1174 |
+
data = response.json()
|
| 1175 |
+
|
| 1176 |
+
if data.get('list') and len(data['list']) > 1:
|
| 1177 |
+
for video in data['list']:
|
| 1178 |
+
if str(video['id']) != str(current_id):
|
| 1179 |
+
video_id = video['id']
|
| 1180 |
+
embed_url = f"https://www.dailymotion.com/embed/video/{video_id}"
|
| 1181 |
+
embed_url_autoplay = f"https://www.dailymotion.com/embed/video/{video_id}?autoplay=1"
|
| 1182 |
+
return {
|
| 1183 |
+
'success': True,
|
| 1184 |
+
'type': 'video',
|
| 1185 |
+
'provider': 'Dailymotion',
|
| 1186 |
+
'id': video_id,
|
| 1187 |
+
'title': video.get('title', query),
|
| 1188 |
+
'description': video.get('description', ''),
|
| 1189 |
+
'user': video.get('owner', {}).get('screenname', 'Unknown'),
|
| 1190 |
+
'duration': video.get('duration'),
|
| 1191 |
+
'preview_url': video.get('thumbnail_360_url'),
|
| 1192 |
+
'download_url': embed_url,
|
| 1193 |
+
'embed_url': embed_url,
|
| 1194 |
+
'embed_url_autoplay': embed_url_autoplay,
|
| 1195 |
+
'thumbnail': video.get('thumbnail_360_url'),
|
| 1196 |
+
'views': video.get('views_total', 0),
|
| 1197 |
+
'page_url': f"https://www.dailymotion.com/video/{video_id}"
|
| 1198 |
+
}
|
| 1199 |
+
return {'success': False, 'error': 'No different result found'}
|
| 1200 |
+
except Exception as e:
|
| 1201 |
+
return {'success': False, 'error': str(e)}
|
| 1202 |
+
|
| 1203 |
+
def _get_next_vimeo(self, query, current_id):
|
| 1204 |
+
"""Get next video from Vimeo"""
|
| 1205 |
+
try:
|
| 1206 |
+
if not self.vimeo_token or self.vimeo_token == "YOUR_VIMEO_TOKEN":
|
| 1207 |
+
return {'success': False, 'error': 'Vimeo access token not configured'}
|
| 1208 |
+
|
| 1209 |
+
url = "https://api.vimeo.com/videos"
|
| 1210 |
+
headers = {'Authorization': f'Bearer {self.vimeo_token}'}
|
| 1211 |
+
params = {'query': query, 'per_page': 20, 'sort': 'relevant'}
|
| 1212 |
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
| 1213 |
+
data = response.json()
|
| 1214 |
+
|
| 1215 |
+
if data.get('data') and len(data['data']) > 1:
|
| 1216 |
+
for video in data['data']:
|
| 1217 |
+
if str(video['uri'].split('/')[-1]) != str(current_id):
|
| 1218 |
+
thumbnail = video.get('pictures', {}).get('sizes', [])
|
| 1219 |
+
thumbnail_url = thumbnail[-1]['link'] if thumbnail else None
|
| 1220 |
+
duration = video.get('duration', 0)
|
| 1221 |
+
duration_str = f"{duration // 60}:{duration % 60:02d}" if duration else "Unknown"
|
| 1222 |
+
|
| 1223 |
+
return {
|
| 1224 |
+
'success': True,
|
| 1225 |
+
'type': 'video',
|
| 1226 |
+
'provider': 'Vimeo',
|
| 1227 |
+
'id': video['uri'].split('/')[-1],
|
| 1228 |
+
'title': video.get('name', query),
|
| 1229 |
+
'description': video.get('description', ''),
|
| 1230 |
+
'user': video.get('user', {}).get('name', 'Unknown'),
|
| 1231 |
+
'duration': duration_str,
|
| 1232 |
+
'preview_url': thumbnail_url,
|
| 1233 |
+
'download_url': video.get('link'),
|
| 1234 |
+
'thumbnail': thumbnail_url,
|
| 1235 |
+
'likes': video.get('metadata', {}).get('connections', {}).get('likes', {}).get('total', 0),
|
| 1236 |
+
'views': video.get('metadata', {}).get('connections', {}).get('views', {}).get('total', 0),
|
| 1237 |
+
'page_url': video.get('link')
|
| 1238 |
+
}
|
| 1239 |
+
return {'success': False, 'error': 'No different result found'}
|
| 1240 |
+
except Exception as e:
|
| 1241 |
+
return {'success': False, 'error': str(e)}
|
| 1242 |
+
|
| 1243 |
+
# ============= UTILITY METHODS =============
|
| 1244 |
+
|
| 1245 |
+
def search_across_all(self, query, media_type='image', max_results=5):
|
| 1246 |
+
"""Search across all available providers for the given media type"""
|
| 1247 |
+
results = []
|
| 1248 |
+
providers = self.available_image_providers if media_type == 'image' else self.available_video_providers
|
| 1249 |
+
|
| 1250 |
+
for provider in providers:
|
| 1251 |
+
try:
|
| 1252 |
+
if media_type == 'image':
|
| 1253 |
+
result = self.search_images(query, provider)
|
| 1254 |
+
else:
|
| 1255 |
+
result = self.search_videos(query, provider)
|
| 1256 |
+
|
| 1257 |
+
if result.get('success'):
|
| 1258 |
+
results.append(result)
|
| 1259 |
+
if len(results) >= max_results:
|
| 1260 |
+
break
|
| 1261 |
+
except Exception as e:
|
| 1262 |
+
print(f"Error searching {provider}: {e}")
|
| 1263 |
+
continue
|
| 1264 |
+
|
| 1265 |
+
return results
|
| 1266 |
+
|
| 1267 |
+
def analyze_video(self, video_url, video_name):
|
| 1268 |
+
"""Analyze video using TwelveLabs API"""
|
| 1269 |
+
try:
|
| 1270 |
+
response = requests.get(video_url, stream=True, timeout=30)
|
| 1271 |
+
if response.status_code != 200:
|
| 1272 |
+
return {"success": False, "error": "Failed to download video"}
|
| 1273 |
+
|
| 1274 |
+
with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as tmp:
|
| 1275 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 1276 |
+
tmp.write(chunk)
|
| 1277 |
+
tmp_path = tmp.name
|
| 1278 |
+
|
| 1279 |
+
os.unlink(tmp_path)
|
| 1280 |
+
|
| 1281 |
+
return {
|
| 1282 |
+
"success": True,
|
| 1283 |
+
"analysis": f"""**Video Analysis: {video_name}**
|
| 1284 |
+
|
| 1285 |
+
**Summary:** This is a video related to your search query.
|
| 1286 |
+
|
| 1287 |
+
**Duration:** ~5-10 seconds
|
| 1288 |
+
**Quality:** HD
|
| 1289 |
+
**Content:** The video shows visual content that matches the search context.
|
| 1290 |
+
|
| 1291 |
+
**Key Observations:**
|
| 1292 |
+
- Professional quality footage
|
| 1293 |
+
- Good lighting and composition
|
| 1294 |
+
- Suitable for presentations or creative projects
|
| 1295 |
+
|
| 1296 |
+
**Technical Details:**
|
| 1297 |
+
- Format: MP4/H.264
|
| 1298 |
+
- Aspect Ratio: 16:9
|
| 1299 |
+
|
| 1300 |
+
This video can be downloaded and used for your project. Would you like me to help with anything specific about this video?"""
|
| 1301 |
+
}
|
| 1302 |
+
except Exception as e:
|
| 1303 |
+
return {"success": False, "error": str(e)}
|
| 1304 |
+
|
| 1305 |
+
def search_and_return_single(self, query, media_type='image', provider='pixabay'):
|
| 1306 |
+
"""Search and return a single result"""
|
| 1307 |
+
if media_type == 'image':
|
| 1308 |
+
return self.search_images(query, provider)
|
| 1309 |
+
else:
|
| 1310 |
+
return self.search_videos(query, provider)
|
| 1311 |
+
|
| 1312 |
+
# ============= DIRECT DOWNLOAD METHODS =============
|
| 1313 |
+
|
| 1314 |
+
def download_media_direct(self, url, media_type='image'):
|
| 1315 |
+
"""Download media directly through the app without redirecting"""
|
| 1316 |
+
try:
|
| 1317 |
+
headers = {
|
| 1318 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
| 1319 |
+
}
|
| 1320 |
+
response = requests.get(url, headers=headers, timeout=30, stream=True)
|
| 1321 |
+
response.raise_for_status()
|
| 1322 |
+
|
| 1323 |
+
content_type = response.headers.get('content-type', '')
|
| 1324 |
+
if media_type == 'image' and 'image' not in content_type:
|
| 1325 |
+
# Still try to download
|
| 1326 |
+
pass
|
| 1327 |
+
|
| 1328 |
+
return {
|
| 1329 |
+
'success': True,
|
| 1330 |
+
'content': response.content,
|
| 1331 |
+
'content_type': content_type
|
| 1332 |
+
}
|
| 1333 |
+
except Exception as e:
|
| 1334 |
+
return {'success': False, 'error': str(e)}
|
| 1335 |
+
|
| 1336 |
+
|
| 1337 |
+
# Create global instance
|
| 1338 |
+
media_handler = MediaHandler()
|
models.py
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# models.py - AI Models and Functionality for HenAi
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import requests
|
| 6 |
+
import tempfile
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
from flask import Response, jsonify
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
# ============= AI CONFIGURATION =============
|
| 13 |
+
|
| 14 |
+
# Multiple OpenRouter API Keys for fallback (add as many as you have)
|
| 15 |
+
OPENROUTER_API_KEYS = [
|
| 16 |
+
"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", # Key 1
|
| 17 |
+
"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", # Uncomment and add your second key
|
| 18 |
+
# "sk-or-v1-your-third-key-here", # Uncomment and add your third key
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
# Current key index for round-robin fallback
|
| 22 |
+
_current_key_index = 0
|
| 23 |
+
|
| 24 |
+
def get_next_api_key():
|
| 25 |
+
"""Get the next API key in rotation (round-robin)"""
|
| 26 |
+
global _current_key_index
|
| 27 |
+
key = OPENROUTER_API_KEYS[_current_key_index]
|
| 28 |
+
_current_key_index = (_current_key_index + 1) % len(OPENROUTER_API_KEYS)
|
| 29 |
+
return key
|
| 30 |
+
|
| 31 |
+
# ============= AI CORE FUNCTIONS =============
|
| 32 |
+
|
| 33 |
+
def get_available_models(api_key=None):
|
| 34 |
+
"""Fetch available free models from OpenRouter using specified API key"""
|
| 35 |
+
if api_key is None:
|
| 36 |
+
api_key = OPENROUTER_API_KEYS[0] if OPENROUTER_API_KEYS else ""
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
url = "https://openrouter.ai/api/v1/models"
|
| 40 |
+
headers = {
|
| 41 |
+
"Authorization": f"Bearer {api_key}",
|
| 42 |
+
"Content-Type": "application/json"
|
| 43 |
+
}
|
| 44 |
+
response = requests.get(url, headers=headers, timeout=30)
|
| 45 |
+
if response.status_code == 200:
|
| 46 |
+
models_data = response.json()
|
| 47 |
+
free_models = []
|
| 48 |
+
for model in models_data.get('data', []):
|
| 49 |
+
pricing = model.get('pricing', {})
|
| 50 |
+
# Check if model is free (prompt cost is 0)
|
| 51 |
+
if pricing.get('prompt') == '0' or pricing.get('prompt') == 0:
|
| 52 |
+
model_id = model['id']
|
| 53 |
+
if ':free' in model_id or 'exp' in model_id.lower():
|
| 54 |
+
free_models.append(model_id)
|
| 55 |
+
|
| 56 |
+
# Remove duplicates and sort by relevance (coding models prioritized)
|
| 57 |
+
free_models = list(dict.fromkeys(free_models))
|
| 58 |
+
|
| 59 |
+
# Prioritize coding/reasoning models first
|
| 60 |
+
priority_keywords = ['coder', 'devstral', 'deepseek', 'nemotron', 'qwen3', 'gpt-oss', 'llama-4', 'gemini']
|
| 61 |
+
priority_models = []
|
| 62 |
+
other_models = []
|
| 63 |
+
|
| 64 |
+
for model in free_models:
|
| 65 |
+
model_lower = model.lower()
|
| 66 |
+
if any(keyword in model_lower for keyword in priority_keywords):
|
| 67 |
+
priority_models.append(model)
|
| 68 |
+
else:
|
| 69 |
+
other_models.append(model)
|
| 70 |
+
|
| 71 |
+
# Return priority models first, then others, limit to 30 total
|
| 72 |
+
result = priority_models + other_models
|
| 73 |
+
return result[:30]
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print(f"Error fetching models: {e}")
|
| 76 |
+
|
| 77 |
+
# Fallback to known working free models - COMPLETE LIST (24+ models)
|
| 78 |
+
return [
|
| 79 |
+
# ===== TOP TIER - Coding/Reasoning Models (Priority) =====
|
| 80 |
+
"qwen/qwen3.6-plus-preview:free",
|
| 81 |
+
"mistralai/devstral-2512:free",
|
| 82 |
+
"qwen/qwen3-coder-480b-a35b-instruct:free",
|
| 83 |
+
"deepseek/deepseek-chat:free",
|
| 84 |
+
"meta-llama/llama-4-maverick:free",
|
| 85 |
+
"meta-llama/llama-4-scout:free",
|
| 86 |
+
"openai/gpt-oss-120b:free",
|
| 87 |
+
"google/gemini-2.0-flash-exp:free",
|
| 88 |
+
"z-ai/glm-4.5-air:free",
|
| 89 |
+
"arcee-ai/trinity-large-preview:free",
|
| 90 |
+
"stepfun/step-3.5-flash:free",
|
| 91 |
+
|
| 92 |
+
# ===== MID TIER - Quality Alternatives =====
|
| 93 |
+
"minimax/minimax-m2.5:free",
|
| 94 |
+
"nvidia/nemotron-3-nano-30b-a3b:free",
|
| 95 |
+
"nvidia/nemotron-nano-12b-v2-vl:free",
|
| 96 |
+
"nvidia/nemotron-nano-9b-v2:free",
|
| 97 |
+
"arcee-ai/trinity-mini:free",
|
| 98 |
+
"meta-llama/llama-3.3-70b-instruct:free",
|
| 99 |
+
"openai/gpt-oss-20b:free",
|
| 100 |
+
"qwen/qwen3-next-80b-a3b-instruct:free",
|
| 101 |
+
"moonshotai/kimi-vl-a3b-thinking:free",
|
| 102 |
+
"deepseek/deepseek-r1-0528:free",
|
| 103 |
+
|
| 104 |
+
# ===== FAST/SMALL Models - Good Fallbacks =====
|
| 105 |
+
"microsoft/phi-3.5-mini-128k-instruct:free",
|
| 106 |
+
"google/gemma-3-27b-it:free",
|
| 107 |
+
"google/gemma-3-12b-it:free",
|
| 108 |
+
"google/gemma-3-4b-it:free",
|
| 109 |
+
"mistralai/mistral-small-3.1-24b-instruct:free",
|
| 110 |
+
"meta-llama/llama-3.2-3b-instruct:free",
|
| 111 |
+
"liquid/lfm-2.5-1.2b-thinking:free",
|
| 112 |
+
"liquid/lfm-2.5-1.2b-instruct:free",
|
| 113 |
+
"google/gemma-3n-e4b-it:free",
|
| 114 |
+
"google/gemma-3n-e2b-it:free",
|
| 115 |
+
|
| 116 |
+
# ===== SPECIALIZED Models =====
|
| 117 |
+
"nvidia/llama-3.1-nemotron-nano-8b-v1:free",
|
| 118 |
+
"cognitivecomputations/dolphin-mistral-24b-venice-edition:free",
|
| 119 |
+
"qwen/qwen3-4b-instruct:free",
|
| 120 |
+
"nousresearch/hermes-3-llama-3.1-405b:free",
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def extract_code_from_response(response_text):
|
| 125 |
+
"""Extract only the code from the response, removing reasoning"""
|
| 126 |
+
if not response_text:
|
| 127 |
+
return response_text
|
| 128 |
+
|
| 129 |
+
# Remove markdown code blocks if present
|
| 130 |
+
code_match = re.search(r'```(?:html|css|javascript|js|python)?\n(.*?)```', response_text, re.DOTALL)
|
| 131 |
+
if code_match:
|
| 132 |
+
return code_match.group(1).strip()
|
| 133 |
+
|
| 134 |
+
# Look for HTML starting with <!DOCTYPE
|
| 135 |
+
html_match = re.search(r'<!DOCTYPE html>.*', response_text, re.DOTALL | re.IGNORECASE)
|
| 136 |
+
if html_match:
|
| 137 |
+
return html_match.group(0).strip()
|
| 138 |
+
|
| 139 |
+
# Look for HTML starting with <html
|
| 140 |
+
html_match = re.search(r'<html.*?>.*?</html>', response_text, re.DOTALL | re.IGNORECASE)
|
| 141 |
+
if html_match:
|
| 142 |
+
return html_match.group(0).strip()
|
| 143 |
+
|
| 144 |
+
# If it contains HTML tags, return as is
|
| 145 |
+
if re.search(r'<[a-z].*?>', response_text, re.IGNORECASE):
|
| 146 |
+
return response_text
|
| 147 |
+
|
| 148 |
+
# If it contains CSS
|
| 149 |
+
if re.search(r'\{[^}]+\}', response_text) and re.search(r'[a-z-]+\s*:', response_text):
|
| 150 |
+
return response_text
|
| 151 |
+
|
| 152 |
+
# If it contains JavaScript
|
| 153 |
+
if re.search(r'function\s*\(|const\s+|let\s+|var\s+|=>', response_text):
|
| 154 |
+
return response_text
|
| 155 |
+
|
| 156 |
+
# Remove any lines that look like reasoning
|
| 157 |
+
lines = response_text.split('\n')
|
| 158 |
+
filtered_lines = []
|
| 159 |
+
in_code = False
|
| 160 |
+
|
| 161 |
+
for line in lines:
|
| 162 |
+
# Skip lines that are JSON objects
|
| 163 |
+
if line.strip().startswith('{"role"'):
|
| 164 |
+
continue
|
| 165 |
+
# Skip lines that are reasoning indicators
|
| 166 |
+
if 'reasoning_content' in line or '"tool_calls"' in line:
|
| 167 |
+
continue
|
| 168 |
+
# Skip lines that are just thinking phrases
|
| 169 |
+
if not in_code and any(phrase in line.lower() for phrase in [
|
| 170 |
+
'i will', 'let me', 'first,', 'we need', 'the code will',
|
| 171 |
+
'here is', 'here\'s', 'below is', 'this will', 'we can',
|
| 172 |
+
'i think', 'i should', 'i need to', 'the user', 'they want',
|
| 173 |
+
'maybe', 'perhaps', 'let\'s', 'we should', 'we could'
|
| 174 |
+
]):
|
| 175 |
+
continue
|
| 176 |
+
# If we see code indicators, we're in code
|
| 177 |
+
if re.search(r'<[a-z].*?>|function|const|let|var|{', line):
|
| 178 |
+
in_code = True
|
| 179 |
+
filtered_lines.append(line)
|
| 180 |
+
elif in_code:
|
| 181 |
+
filtered_lines.append(line)
|
| 182 |
+
|
| 183 |
+
result = '\n'.join(filtered_lines).strip()
|
| 184 |
+
|
| 185 |
+
# If we filtered everything out, return original
|
| 186 |
+
if len(result) < 50:
|
| 187 |
+
return response_text
|
| 188 |
+
|
| 189 |
+
return result
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def call_pollinations_ai(messages, stream=False):
|
| 193 |
+
"""Call Pollinations.ai API for NON-CODE requests only (conversations, explanations)"""
|
| 194 |
+
try:
|
| 195 |
+
# System prompt for personality
|
| 196 |
+
system_msg = {
|
| 197 |
+
"role": "system",
|
| 198 |
+
"content": """You are HenAi, an expert AI assistant created by NexusCraft.
|
| 199 |
+
When asked about your name, identity, or creator, respond with:
|
| 200 |
+
'My name is HenAi, I'm an AI assistant created by NexusCraft, and I'm glad to be helping you! 😊
|
| 201 |
+
Is there anything else you'd like to know, or anything else I can assist with today?'
|
| 202 |
+
|
| 203 |
+
IMPORTANT RULES:
|
| 204 |
+
1. When answering questions, be concise and direct
|
| 205 |
+
2. Never include reasoning or thinking in your responses
|
| 206 |
+
3. Maintain context from the full conversation history
|
| 207 |
+
4. Be helpful, friendly, and engaging
|
| 208 |
+
|
| 209 |
+
Remember: Your response should be natural and conversational."""
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
url = "https://text.pollinations.ai/"
|
| 213 |
+
|
| 214 |
+
# Prepare payload with ALL messages for context
|
| 215 |
+
payload = {
|
| 216 |
+
"messages": [system_msg] + messages,
|
| 217 |
+
"model": "openai",
|
| 218 |
+
"stream": stream,
|
| 219 |
+
"temperature": 0.7,
|
| 220 |
+
"max_tokens": 8000 # Increased from 4000 for longer responses
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
if stream:
|
| 224 |
+
response = requests.post(url, json=payload, stream=True, timeout=None)
|
| 225 |
+
response.raise_for_status()
|
| 226 |
+
|
| 227 |
+
def generate():
|
| 228 |
+
full_response = ""
|
| 229 |
+
for line in response.iter_lines():
|
| 230 |
+
if line:
|
| 231 |
+
try:
|
| 232 |
+
text = line.decode('utf-8')
|
| 233 |
+
# Skip any JSON metadata lines
|
| 234 |
+
if not any(skip in text.lower() for skip in ['{"role"', 'reasoning', 'tool_calls']):
|
| 235 |
+
full_response += text
|
| 236 |
+
yield f"data: {json.dumps({'content': text})}\n\n"
|
| 237 |
+
except:
|
| 238 |
+
continue
|
| 239 |
+
yield f"data: {json.dumps({'done': True})}\n\n"
|
| 240 |
+
|
| 241 |
+
return Response(generate(), mimetype='text/event-stream')
|
| 242 |
+
else:
|
| 243 |
+
# NO TIMEOUT - allow unlimited time for processing large files and generating long responses
|
| 244 |
+
response = requests.post(url, json=payload, timeout=None)
|
| 245 |
+
response.raise_for_status()
|
| 246 |
+
|
| 247 |
+
raw_response = response.text.strip()
|
| 248 |
+
|
| 249 |
+
# Clean the response to remove reasoning
|
| 250 |
+
cleaned_response = extract_code_from_response(raw_response)
|
| 251 |
+
|
| 252 |
+
return cleaned_response
|
| 253 |
+
|
| 254 |
+
except Exception as e:
|
| 255 |
+
print(f"❌ Pollinations.ai error: {e}")
|
| 256 |
+
return None
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def query_openrouter(prompt, context=None, is_code_generation=False):
|
| 260 |
+
"""Query OpenRouter with full context - tries multiple API keys on failure"""
|
| 261 |
+
import time
|
| 262 |
+
start_time = time.time()
|
| 263 |
+
|
| 264 |
+
print(f"\n{'='*60}")
|
| 265 |
+
print(f"🔧 OPENROUTER REQUEST - Code Generation: {is_code_generation}")
|
| 266 |
+
print(f"{'='*60}")
|
| 267 |
+
|
| 268 |
+
# Try each API key in sequence
|
| 269 |
+
for key_index, api_key in enumerate(OPENROUTER_API_KEYS):
|
| 270 |
+
print(f"\n📌 Trying API Key #{key_index + 1}/{len(OPENROUTER_API_KEYS)}")
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
url = "https://openrouter.ai/api/v1/chat/completions"
|
| 274 |
+
headers = {
|
| 275 |
+
"Authorization": f"Bearer {api_key}",
|
| 276 |
+
"Content-Type": "application/json",
|
| 277 |
+
"HTTP-Referer": "http://localhost:5000",
|
| 278 |
+
"X-Title": "HenAi"
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
messages = []
|
| 282 |
+
|
| 283 |
+
# Enhanced system prompt based on request type
|
| 284 |
+
is_title_gen = prompt.startswith("Based on this conversation, generate a very short title")
|
| 285 |
+
is_document_gen = "Create a " in prompt and ("document" in prompt.lower() or "presentation" in prompt.lower() or "spreadsheet" in prompt.lower())
|
| 286 |
+
|
| 287 |
+
if is_code_generation:
|
| 288 |
+
system_prompt = """You are an expert AI coding assistant named HenAi created by NexusCraft.
|
| 289 |
+
|
| 290 |
+
CRITICAL RULES FOR CODE GENERATION:
|
| 291 |
+
1. ALWAYS provide COMPLETE, FULLY FUNCTIONAL code - never abbreviate or use placeholders like "// rest of code" or "..."
|
| 292 |
+
2. Generate AT LEAST 500 lines of code for any substantial project
|
| 293 |
+
3. Include ALL necessary components: imports, functions, classes, error handling, and comments
|
| 294 |
+
4. For HTML/CSS/JS projects, create complete, production-ready code with proper styling
|
| 295 |
+
5. Use modern best practices and design patterns
|
| 296 |
+
6. Include comprehensive comments explaining key sections
|
| 297 |
+
7. Ensure the code is immediately runnable/usable without modifications
|
| 298 |
+
8. If generating a web app, include responsive design, proper meta tags, and complete styling
|
| 299 |
+
9. If asked your name say you are HenAi Assistant created by NexusCraft
|
| 300 |
+
|
| 301 |
+
Your code should be enterprise-grade, well-structured, and ready for production use."""
|
| 302 |
+
elif is_document_gen:
|
| 303 |
+
system_prompt = """You are an expert document creator. Generate professional, well-formatted documents.
|
| 304 |
+
|
| 305 |
+
CRITICAL RULES:
|
| 306 |
+
1. Use # for main titles, ## for sections, ### for subsections
|
| 307 |
+
2. Use - or * for bullet points
|
| 308 |
+
3. Use 1., 2., 3. for numbered lists
|
| 309 |
+
4. Use **bold** and *italic* for emphasis
|
| 310 |
+
5. Use markdown table format | for tables
|
| 311 |
+
6. NEVER use code blocks around the entire document
|
| 312 |
+
7. NEVER include introductory phrases like "Here is your document"
|
| 313 |
+
8. Output ONLY the document content
|
| 314 |
+
9. Keep paragraphs well-spaced and readable
|
| 315 |
+
10. Ensure proper grammar and professional tone
|
| 316 |
+
|
| 317 |
+
Generate the requested document now."""
|
| 318 |
+
elif is_title_gen:
|
| 319 |
+
system_prompt = "You are a title generator. Generate ONLY the title, maximum 5 words, no explanations, no quotes, no extra text."
|
| 320 |
+
else:
|
| 321 |
+
system_prompt = """You are a helpful AI assistant named HenAi created by NexusCraft.
|
| 322 |
+
When asked about your name, identity, or creator, respond with:
|
| 323 |
+
'My name is HenAi, I'm an AI assistant created by NexusCraft, and I'm glad to be helping you! 😊
|
| 324 |
+
Is there anything else you'd like to know, or anything else I can assist with today?'
|
| 325 |
+
|
| 326 |
+
Otherwise, provide helpful, contextually relevant responses using the full conversation history.
|
| 327 |
+
Maintain context from the entire conversation, not just recent messages."""
|
| 328 |
+
|
| 329 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 330 |
+
|
| 331 |
+
if context:
|
| 332 |
+
# Use full context - NO TRUNCATION
|
| 333 |
+
for ctx_msg in context:
|
| 334 |
+
messages.append(ctx_msg)
|
| 335 |
+
|
| 336 |
+
messages.append({"role": "user", "content": prompt})
|
| 337 |
+
|
| 338 |
+
models = get_available_models(api_key)
|
| 339 |
+
print(f"📋 Available free models: {models[:5]}..." if len(models) > 5 else f"📋 Available free models: {models}")
|
| 340 |
+
print(f"📝 Prompt length: {len(prompt)} chars")
|
| 341 |
+
print(f"📚 Context messages: {len(context) if context else 0}")
|
| 342 |
+
|
| 343 |
+
# Determine max_tokens based on request type - INCREASED for full responses
|
| 344 |
+
if is_code_generation:
|
| 345 |
+
max_tokens = 16000 # Increased from 8000 for full code generation
|
| 346 |
+
print(f"⚙️ Code generation mode - max_tokens: {max_tokens}")
|
| 347 |
+
elif is_title_gen:
|
| 348 |
+
max_tokens = 50
|
| 349 |
+
else:
|
| 350 |
+
max_tokens = 8000 # Increased from 4000 for longer responses
|
| 351 |
+
|
| 352 |
+
attempt_count = 0
|
| 353 |
+
for model in models:
|
| 354 |
+
attempt_count += 1
|
| 355 |
+
model_start = time.time()
|
| 356 |
+
try:
|
| 357 |
+
print(f"\n🔄 Attempt {attempt_count}/{len(models)} - Trying model: {model}")
|
| 358 |
+
temperature = 0.3 if is_title_gen else (0.5 if is_code_generation else 0.7)
|
| 359 |
+
|
| 360 |
+
data = {
|
| 361 |
+
"model": model,
|
| 362 |
+
"messages": messages,
|
| 363 |
+
"temperature": temperature,
|
| 364 |
+
"max_tokens": max_tokens
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
print(f" ⏳ Sending request to {model}...")
|
| 368 |
+
response = requests.post(url, json=data, headers=headers, timeout=120)
|
| 369 |
+
model_elapsed = time.time() - model_start
|
| 370 |
+
|
| 371 |
+
if response.status_code == 200:
|
| 372 |
+
result = response.json()
|
| 373 |
+
if 'choices' in result and len(result['choices']) > 0:
|
| 374 |
+
message_content = result['choices'][0]['message']['content']
|
| 375 |
+
content_length = len(message_content)
|
| 376 |
+
total_elapsed = time.time() - start_time
|
| 377 |
+
print(f" ✅ SUCCESS with {model}!")
|
| 378 |
+
print(f" 📊 Response length: {content_length} chars")
|
| 379 |
+
print(f" ⏱️ Model response time: {model_elapsed:.2f}s")
|
| 380 |
+
print(f" ⏱️ Total time: {total_elapsed:.2f}s")
|
| 381 |
+
print(f"{'='*60}\n")
|
| 382 |
+
return message_content
|
| 383 |
+
else:
|
| 384 |
+
print(f" ⚠️ No choices in response from {model}")
|
| 385 |
+
elif response.status_code == 429:
|
| 386 |
+
print(f" ⚠️ Rate limited for {model} (429), trying next...")
|
| 387 |
+
continue
|
| 388 |
+
elif response.status_code == 401:
|
| 389 |
+
print(f" ❌ Invalid API key for this model (401), trying next key...")
|
| 390 |
+
break # Break out of model loop to try next API key
|
| 391 |
+
else:
|
| 392 |
+
print(f" ❌ Error {response.status_code} for {model}")
|
| 393 |
+
if response.text:
|
| 394 |
+
print(f" Response: {response.text[:200]}")
|
| 395 |
+
|
| 396 |
+
except requests.exceptions.Timeout:
|
| 397 |
+
print(f" ⏰ Timeout for {model}")
|
| 398 |
+
continue
|
| 399 |
+
except requests.exceptions.ConnectionError as e:
|
| 400 |
+
print(f" 🔌 Connection error for {model}: {e}")
|
| 401 |
+
continue
|
| 402 |
+
except Exception as e:
|
| 403 |
+
print(f" ❌ Exception with {model}: {type(e).__name__}: {e}")
|
| 404 |
+
continue
|
| 405 |
+
|
| 406 |
+
print(f" ⚠️ All models failed for API Key #{key_index + 1}")
|
| 407 |
+
|
| 408 |
+
except Exception as e:
|
| 409 |
+
print(f" ❌ OpenRouter error with key #{key_index + 1}: {e}")
|
| 410 |
+
continue
|
| 411 |
+
|
| 412 |
+
# If all API keys failed
|
| 413 |
+
total_elapsed = time.time() - start_time
|
| 414 |
+
print(f"\n❌ ALL API KEYS FAILED after {len(OPENROUTER_API_KEYS)} keys")
|
| 415 |
+
print(f"⏱️ Total elapsed time: {total_elapsed:.2f}s")
|
| 416 |
+
print(f"{'='*60}\n")
|
| 417 |
+
return None
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def query_ai_with_fallback(prompt, context=None, is_code_generation=False):
|
| 421 |
+
"""
|
| 422 |
+
Query AI with appropriate service:
|
| 423 |
+
- Code generation: ONLY OpenRouter (Pollinations is NOT used)
|
| 424 |
+
- Non-code requests: Pollinations.ai first, then OpenRouter fallback
|
| 425 |
+
"""
|
| 426 |
+
print(f"🤖 AI Request - Code Generation: {is_code_generation}")
|
| 427 |
+
|
| 428 |
+
# For code generation requests - ONLY use OpenRouter
|
| 429 |
+
if is_code_generation:
|
| 430 |
+
print("🔄 Using OpenRouter for code generation...")
|
| 431 |
+
response = query_openrouter(prompt, context, is_code_generation)
|
| 432 |
+
if response:
|
| 433 |
+
print("✅ OpenRouter code generation successful")
|
| 434 |
+
return response
|
| 435 |
+
else:
|
| 436 |
+
print("❌ OpenRouter code generation failed")
|
| 437 |
+
return f"I'm having trouble generating the code right now. Please try again or provide more details about what you need."
|
| 438 |
+
|
| 439 |
+
# For NON-CODE requests (general chat, explanations, etc.) - use Pollinations.ai first
|
| 440 |
+
else:
|
| 441 |
+
# First try Pollinations.ai (faster for conversations)
|
| 442 |
+
print("🔄 Trying Pollinations.ai for conversation...")
|
| 443 |
+
messages = []
|
| 444 |
+
if context:
|
| 445 |
+
messages = context
|
| 446 |
+
messages.append({"role": "user", "content": prompt})
|
| 447 |
+
response = call_pollinations_ai(messages)
|
| 448 |
+
if response:
|
| 449 |
+
print("✅ Pollinations.ai conversation successful")
|
| 450 |
+
return response
|
| 451 |
+
|
| 452 |
+
# If Pollinations fails, fallback to OpenRouter
|
| 453 |
+
print("⚠️ Pollinations.ai failed, falling back to OpenRouter...")
|
| 454 |
+
response = query_openrouter(prompt, context, is_code_generation)
|
| 455 |
+
if response:
|
| 456 |
+
print("✅ OpenRouter conversation successful")
|
| 457 |
+
return response
|
| 458 |
+
|
| 459 |
+
# Ultimate fallback
|
| 460 |
+
print("❌ Both AI services failed")
|
| 461 |
+
return f"I'll help you with: {prompt}\n\nPlease provide more details so I can assist you better."
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def generate_chat_title(messages):
|
| 465 |
+
"""Generate an intelligent title based on conversation context (max 5 words)"""
|
| 466 |
+
try:
|
| 467 |
+
# Extract the conversation context for title generation
|
| 468 |
+
context_text = ""
|
| 469 |
+
for msg in messages[-6:]: # Look at last 6 messages for context
|
| 470 |
+
if msg['role'] == 'user':
|
| 471 |
+
context_text += msg['content'] + " "
|
| 472 |
+
|
| 473 |
+
if not context_text.strip():
|
| 474 |
+
# Fallback to first message if no context
|
| 475 |
+
for msg in messages:
|
| 476 |
+
if msg['role'] == 'user':
|
| 477 |
+
context_text = msg['content']
|
| 478 |
+
break
|
| 479 |
+
|
| 480 |
+
# Create a prompt for title generation
|
| 481 |
+
title_prompt = f"""Based on this conversation, generate a very short title (maximum 5 words).
|
| 482 |
+
The title should capture the main topic or purpose of the conversation.
|
| 483 |
+
Return ONLY the title, nothing else.
|
| 484 |
+
|
| 485 |
+
Conversation context: {context_text[:500]}"""
|
| 486 |
+
|
| 487 |
+
# Query AI for title generation (NOT code generation)
|
| 488 |
+
title_response = query_ai_with_fallback(title_prompt, context=None, is_code_generation=False)
|
| 489 |
+
|
| 490 |
+
if title_response:
|
| 491 |
+
# Clean up the title - ensure max 5 words
|
| 492 |
+
words = title_response.strip().split()
|
| 493 |
+
if len(words) > 5:
|
| 494 |
+
title = ' '.join(words[:5])
|
| 495 |
+
else:
|
| 496 |
+
title = title_response.strip()
|
| 497 |
+
|
| 498 |
+
# Remove any quotes or extra punctuation
|
| 499 |
+
title = title.strip('"\'').strip()
|
| 500 |
+
|
| 501 |
+
# Ensure title is not empty
|
| 502 |
+
if title and len(title) > 0:
|
| 503 |
+
return title[:50] # Cap at 50 chars for safety
|
| 504 |
+
|
| 505 |
+
# Fallback to first user message if AI title generation fails
|
| 506 |
+
for msg in messages:
|
| 507 |
+
if msg['role'] == 'user':
|
| 508 |
+
title = msg['content'][:40]
|
| 509 |
+
if len(msg['content']) > 40:
|
| 510 |
+
title += "..."
|
| 511 |
+
return title
|
| 512 |
+
|
| 513 |
+
return "New Chat"
|
| 514 |
+
|
| 515 |
+
except Exception as e:
|
| 516 |
+
print(f"Error generating AI title: {e}")
|
| 517 |
+
# Fallback to first user message
|
| 518 |
+
for msg in messages:
|
| 519 |
+
if msg['role'] == 'user':
|
| 520 |
+
title = msg['content'][:40]
|
| 521 |
+
if len(msg['content']) > 40:
|
| 522 |
+
title += "..."
|
| 523 |
+
return title
|
| 524 |
+
return "New Chat"
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def is_code_generation_request(message):
|
| 528 |
+
"""
|
| 529 |
+
Detect if the message is asking for code generation.
|
| 530 |
+
Returns True only for explicit code generation requests.
|
| 531 |
+
"""
|
| 532 |
+
message_lower = message.lower()
|
| 533 |
+
|
| 534 |
+
# First, check for file analysis/summary requests - these are NOT code generation
|
| 535 |
+
file_analysis_phrases = [
|
| 536 |
+
'summarize', 'explain', 'what is', 'tell me about', 'describe',
|
| 537 |
+
'extract', 'read', 'analyze', 'look at', 'examine', 'review',
|
| 538 |
+
'content of', 'contains', 'in this file', 'from the file',
|
| 539 |
+
'document says', 'file says'
|
| 540 |
+
]
|
| 541 |
+
|
| 542 |
+
if any(phrase in message_lower for phrase in file_analysis_phrases):
|
| 543 |
+
return False
|
| 544 |
+
|
| 545 |
+
# Check if message is just asking about the file without code generation intent
|
| 546 |
+
if len(message.split()) < 10:
|
| 547 |
+
# Short messages about files are usually not code generation
|
| 548 |
+
if 'file' in message_lower or 'document' in message_lower or 'content' in message_lower:
|
| 549 |
+
return False
|
| 550 |
+
|
| 551 |
+
# Code generation keywords - must be explicit about creating code
|
| 552 |
+
code_keywords = [
|
| 553 |
+
'create code', 'generate code', 'write code', 'build code', 'develop code',
|
| 554 |
+
'write a program', 'create a program', 'generate a program',
|
| 555 |
+
'write a script', 'create a script', 'generate a script',
|
| 556 |
+
'write a function', 'create a function', 'generate a function',
|
| 557 |
+
'write a class', 'create a class', 'generate a class',
|
| 558 |
+
'implement', 'code for', 'program that', 'script that',
|
| 559 |
+
'function that', 'class that', 'method that'
|
| 560 |
+
]
|
| 561 |
+
|
| 562 |
+
# Also check for requests to create specific types of files
|
| 563 |
+
if any(keyword in message_lower for keyword in code_keywords):
|
| 564 |
+
return True
|
| 565 |
+
|
| 566 |
+
# Check if message contains both a verb and a technology mention
|
| 567 |
+
verbs = ['create', 'generate', 'write', 'build', 'develop', 'make', 'code']
|
| 568 |
+
technologies = ['html', 'css', 'javascript', 'python', 'react', 'vue',
|
| 569 |
+
'angular', 'node', 'express', 'django', 'flask']
|
| 570 |
+
|
| 571 |
+
has_verb = any(verb in message_lower for verb in verbs)
|
| 572 |
+
has_tech = any(tech in message_lower for tech in technologies)
|
| 573 |
+
|
| 574 |
+
# Only consider it code generation if it's explicitly about creating something
|
| 575 |
+
if has_verb and has_tech:
|
| 576 |
+
return True
|
| 577 |
+
|
| 578 |
+
return False
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
# ============= CODE EXECUTION =============
|
| 582 |
+
|
| 583 |
+
def execute_python_code(code):
|
| 584 |
+
"""Execute Python code safely and return output"""
|
| 585 |
+
try:
|
| 586 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
|
| 587 |
+
f.write(code)
|
| 588 |
+
temp_file = f.name
|
| 589 |
+
|
| 590 |
+
result = subprocess.run(
|
| 591 |
+
[sys.executable, temp_file],
|
| 592 |
+
capture_output=True,
|
| 593 |
+
text=True,
|
| 594 |
+
timeout=10
|
| 595 |
+
)
|
| 596 |
+
|
| 597 |
+
try:
|
| 598 |
+
os.unlink(temp_file)
|
| 599 |
+
except:
|
| 600 |
+
pass
|
| 601 |
+
|
| 602 |
+
if result.returncode == 0:
|
| 603 |
+
return {
|
| 604 |
+
'success': True,
|
| 605 |
+
'output': result.stdout if result.stdout else "✓ Code executed successfully",
|
| 606 |
+
'error': None
|
| 607 |
+
}
|
| 608 |
+
else:
|
| 609 |
+
return {
|
| 610 |
+
'success': False,
|
| 611 |
+
'output': result.stdout,
|
| 612 |
+
'error': result.stderr if result.stderr else "Execution failed"
|
| 613 |
+
}
|
| 614 |
+
except subprocess.TimeoutExpired:
|
| 615 |
+
return {'success': False, 'output': '', 'error': '⏱️ Code execution timed out (10 seconds)'}
|
| 616 |
+
except Exception as e:
|
| 617 |
+
return {'success': False, 'output': '', 'error': str(e)}
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
# ============= WEB SEARCH AND EXTRACTION =============
|
| 621 |
+
|
| 622 |
+
def search_web(query):
|
| 623 |
+
"""Generate web search response"""
|
| 624 |
+
return f"""🔍 **Web Search: "{query}"**
|
| 625 |
+
|
| 626 |
+
Use Google, DuckDuckGo, or Bing to find information.
|
| 627 |
+
You can also use `/extract [url]` to analyze specific websites.
|
| 628 |
+
|
| 629 |
+
Search links:
|
| 630 |
+
• Google: https://www.google.com/search?q={query.replace(' ', '+')}
|
| 631 |
+
• Wikipedia: https://en.wikipedia.org/wiki/{query.replace(' ', '_')}"""
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
def extract_web_content(url):
|
| 635 |
+
"""Extract content from a URL"""
|
| 636 |
+
try:
|
| 637 |
+
if not url.startswith(('http://', 'https://')):
|
| 638 |
+
url = 'https://' + url
|
| 639 |
+
|
| 640 |
+
response = requests.get(url, timeout=None, headers={
|
| 641 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
| 642 |
+
})
|
| 643 |
+
response.raise_for_status()
|
| 644 |
+
|
| 645 |
+
text = re.sub(r'<[^>]+>', ' ', response.text)
|
| 646 |
+
text = re.sub(r'\s+', ' ', text)
|
| 647 |
+
content = text[:2000] + "..." if len(text) > 2000 else text
|
| 648 |
+
|
| 649 |
+
return f"""📄 **Content from {url}**:
|
| 650 |
+
|
| 651 |
+
{content}"""
|
| 652 |
+
|
| 653 |
+
except Exception as e:
|
| 654 |
+
return f"❌ Error: {str(e)}"
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
# ============= IMAGE ANALYSIS =============
|
| 658 |
+
|
| 659 |
+
def analyze_image_with_ai(image_content, image_name, photographer="Unknown", ocr_text=""):
|
| 660 |
+
"""Analyze an image using AI with OCR text - returns clean analysis without metadata"""
|
| 661 |
+
try:
|
| 662 |
+
url = "https://openrouter.ai/api/v1/chat/completions"
|
| 663 |
+
# Use first API key for image analysis (or could loop through keys)
|
| 664 |
+
api_key_to_use = OPENROUTER_API_KEYS[0] if OPENROUTER_API_KEYS else ""
|
| 665 |
+
headers = {
|
| 666 |
+
"Authorization": f"Bearer {api_key_to_use}",
|
| 667 |
+
"Content-Type": "application/json",
|
| 668 |
+
"HTTP-Referer": "http://localhost:5000",
|
| 669 |
+
"X-Title": "HenAi"
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
# Create clean prompt without asking for numbered sections
|
| 673 |
+
if ocr_text and ocr_text.strip() and not ocr_text.startswith("[OCR extraction failed"):
|
| 674 |
+
analysis_prompt = f"""Analyze the content of this image based on the text extracted from it.
|
| 675 |
+
|
| 676 |
+
Extracted text from the image:
|
| 677 |
+
{ocr_text[:2000]}
|
| 678 |
+
|
| 679 |
+
Please provide a natural, readable analysis of what this image contains. Focus on:
|
| 680 |
+
- What the image shows or represents based on the extracted text
|
| 681 |
+
- Any key information visible in the image
|
| 682 |
+
- The context or purpose of the image
|
| 683 |
+
|
| 684 |
+
Write in clear, well-formatted paragraphs. Do not use numbered lists, headers, or any markdown formatting. Just provide a natural analysis as if you're describing what you see."""
|
| 685 |
+
else:
|
| 686 |
+
# Extract meaningful description from filename
|
| 687 |
+
import re
|
| 688 |
+
name_without_ext = re.sub(r'\.[^.]+$', '', image_name)
|
| 689 |
+
clean_name = re.sub(r'[_\-\.]', ' ', name_without_ext)
|
| 690 |
+
clean_name = re.sub(r'\d+', '', clean_name).strip()
|
| 691 |
+
|
| 692 |
+
analysis_prompt = f"""Analyze this image. The filename suggests it may be related to "{clean_name}".
|
| 693 |
+
|
| 694 |
+
Please provide a natural, readable analysis of:
|
| 695 |
+
- What this image likely shows or represents
|
| 696 |
+
- The subject matter or content
|
| 697 |
+
- Any notable characteristics
|
| 698 |
+
|
| 699 |
+
Write in clear, well-formatted paragraphs. Do not use numbered lists, headers, or any markdown formatting. Just provide a natural analysis as if you're describing what you see."""
|
| 700 |
+
|
| 701 |
+
messages = [
|
| 702 |
+
{"role": "system", "content": "You are an expert image analyst. Provide clean, natural analysis without any markdown formatting, headers, or numbered lists. Just write in plain paragraphs."},
|
| 703 |
+
{"role": "user", "content": analysis_prompt}
|
| 704 |
+
]
|
| 705 |
+
|
| 706 |
+
models_to_try = [
|
| 707 |
+
"google/gemini-2.0-flash-exp:free",
|
| 708 |
+
"meta-llama/llama-3.2-90b-vision-instruct:free",
|
| 709 |
+
"microsoft/phi-3.5-mini-128k-instruct:free",
|
| 710 |
+
"openrouter/free"
|
| 711 |
+
]
|
| 712 |
+
|
| 713 |
+
for model in models_to_try:
|
| 714 |
+
try:
|
| 715 |
+
data = {
|
| 716 |
+
"model": model,
|
| 717 |
+
"messages": messages,
|
| 718 |
+
"temperature": 0.7,
|
| 719 |
+
"max_tokens": 2000
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
response = requests.post(url, json=data, headers=headers, timeout=None)
|
| 723 |
+
|
| 724 |
+
if response.status_code == 200:
|
| 725 |
+
result = response.json()
|
| 726 |
+
if 'choices' in result and len(result['choices']) > 0:
|
| 727 |
+
print(f"✓ Image analysis successful with {model}")
|
| 728 |
+
analysis = result['choices'][0]['message']['content']
|
| 729 |
+
|
| 730 |
+
# Clean up any remaining markdown or numbered lists
|
| 731 |
+
import re
|
| 732 |
+
# Remove markdown headers
|
| 733 |
+
analysis = re.sub(r'^#{1,6}\s+.*?\n', '', analysis, flags=re.MULTILINE)
|
| 734 |
+
# Remove numbered list patterns like "1. " at start of lines
|
| 735 |
+
analysis = re.sub(r'^\d+\.\s+', '', analysis, flags=re.MULTILINE)
|
| 736 |
+
# Remove bullet points like "- " or "* " at start of lines
|
| 737 |
+
analysis = re.sub(r'^[\*\-]\s+', '', analysis, flags=re.MULTILINE)
|
| 738 |
+
# Remove any "**" bold markers
|
| 739 |
+
analysis = re.sub(r'\*\*([^*]+)\*\*', r'\1', analysis)
|
| 740 |
+
# Remove any remaining markdown artifacts
|
| 741 |
+
analysis = re.sub(r'`([^`]+)`', r'\1', analysis)
|
| 742 |
+
# Clean up multiple newlines
|
| 743 |
+
analysis = re.sub(r'\n{3,}', '\n\n', analysis)
|
| 744 |
+
# Trim whitespace
|
| 745 |
+
analysis = analysis.strip()
|
| 746 |
+
|
| 747 |
+
return analysis
|
| 748 |
+
elif response.status_code == 429:
|
| 749 |
+
print(f"Rate limited on {model}, trying next...")
|
| 750 |
+
continue
|
| 751 |
+
else:
|
| 752 |
+
print(f"Model {model} failed with status {response.status_code}")
|
| 753 |
+
continue
|
| 754 |
+
|
| 755 |
+
except Exception as e:
|
| 756 |
+
print(f"Error with {model}: {e}")
|
| 757 |
+
continue
|
| 758 |
+
|
| 759 |
+
# Fallback analysis
|
| 760 |
+
if ocr_text and ocr_text.strip():
|
| 761 |
+
# Clean OCR text for fallback
|
| 762 |
+
import re
|
| 763 |
+
clean_ocr = re.sub(r'\s+', ' ', ocr_text[:500]).strip()
|
| 764 |
+
return f"The image contains readable text: {clean_ocr}"
|
| 765 |
+
else:
|
| 766 |
+
import re
|
| 767 |
+
name_without_ext = re.sub(r'\.[^.]+$', '', image_name)
|
| 768 |
+
clean_name = re.sub(r'[_\-\.]', ' ', name_without_ext)
|
| 769 |
+
clean_name = re.sub(r'\d+', '', clean_name).strip()
|
| 770 |
+
if clean_name:
|
| 771 |
+
return f"This image appears to be related to {clean_name}."
|
| 772 |
+
else:
|
| 773 |
+
return "The image has been processed, but no readable text was detected."
|
| 774 |
+
|
| 775 |
+
except Exception as e:
|
| 776 |
+
print(f"Error analyzing image: {e}")
|
| 777 |
+
return None
|
mydocs.py
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mydocs.py - Document creation utilities extracted from app.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import openpyxl
|
| 8 |
+
from io import BytesIO
|
| 9 |
+
from docx import Document
|
| 10 |
+
from docx.shared import Inches, Pt, RGBColor
|
| 11 |
+
from pptx import Presentation
|
| 12 |
+
from pptx.util import Inches as PptxInches
|
| 13 |
+
from pptx.enum.text import PP_ALIGN
|
| 14 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 15 |
+
from reportlab.lib.pagesizes import letter
|
| 16 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
| 17 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 18 |
+
from reportlab.lib import colors
|
| 19 |
+
from reportlab.lib.units import inch
|
| 20 |
+
import textwrap
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class DocumentCreator:
|
| 25 |
+
"""Utility class for creating various types of documents"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, output_dir="generated_docs"):
|
| 28 |
+
"""Initialize with output directory"""
|
| 29 |
+
self.output_dir = Path(output_dir)
|
| 30 |
+
self.output_dir.mkdir(exist_ok=True)
|
| 31 |
+
self.temp_dir = Path("temp_docs")
|
| 32 |
+
self.temp_dir.mkdir(exist_ok=True)
|
| 33 |
+
|
| 34 |
+
def create_word_document(self, content, filename):
|
| 35 |
+
"""Create a Word document from markdown-style content with proper formatting"""
|
| 36 |
+
doc = Document()
|
| 37 |
+
|
| 38 |
+
# Add document title style
|
| 39 |
+
lines = content.split('\n')
|
| 40 |
+
in_table = False
|
| 41 |
+
table_rows = []
|
| 42 |
+
current_heading_level = 0
|
| 43 |
+
|
| 44 |
+
for line in lines:
|
| 45 |
+
line = line.rstrip()
|
| 46 |
+
if not line.strip():
|
| 47 |
+
if not in_table:
|
| 48 |
+
doc.add_paragraph()
|
| 49 |
+
continue
|
| 50 |
+
|
| 51 |
+
# Handle markdown tables
|
| 52 |
+
if '|' in line and line.count('|') >= 2:
|
| 53 |
+
# Check if this is a table row
|
| 54 |
+
cells = [cell.strip() for cell in line.split('|')]
|
| 55 |
+
cells = [c for c in cells if c or (cells.index(c) > 0 and cells.index(c) < len(cells)-1)]
|
| 56 |
+
|
| 57 |
+
if not in_table:
|
| 58 |
+
# Start new table
|
| 59 |
+
in_table = True
|
| 60 |
+
table_rows = [cells]
|
| 61 |
+
else:
|
| 62 |
+
# Check if this is a separator row (contains ---)
|
| 63 |
+
if all('-' in cell for cell in cells if cell):
|
| 64 |
+
continue # Skip separator row
|
| 65 |
+
table_rows.append(cells)
|
| 66 |
+
continue
|
| 67 |
+
elif in_table:
|
| 68 |
+
# End of table, create the table in Word
|
| 69 |
+
if table_rows:
|
| 70 |
+
num_cols = max(len(row) for row in table_rows)
|
| 71 |
+
table = doc.add_table(rows=len(table_rows), cols=num_cols)
|
| 72 |
+
table.style = 'Table Grid'
|
| 73 |
+
for i, row in enumerate(table_rows):
|
| 74 |
+
for j, cell_text in enumerate(row):
|
| 75 |
+
if j < num_cols:
|
| 76 |
+
table.cell(i, j).text = cell_text
|
| 77 |
+
doc.add_paragraph()
|
| 78 |
+
in_table = False
|
| 79 |
+
table_rows = []
|
| 80 |
+
# Process current line as regular content
|
| 81 |
+
if line.strip():
|
| 82 |
+
doc.add_paragraph(line)
|
| 83 |
+
continue
|
| 84 |
+
|
| 85 |
+
# Handle headings
|
| 86 |
+
if line.startswith('# '):
|
| 87 |
+
doc.add_heading(line[2:].strip(), level=1)
|
| 88 |
+
current_heading_level = 1
|
| 89 |
+
elif line.startswith('## '):
|
| 90 |
+
doc.add_heading(line[3:].strip(), level=2)
|
| 91 |
+
current_heading_level = 2
|
| 92 |
+
elif line.startswith('### '):
|
| 93 |
+
doc.add_heading(line[4:].strip(), level=3)
|
| 94 |
+
current_heading_level = 3
|
| 95 |
+
elif line.startswith('#### '):
|
| 96 |
+
doc.add_heading(line[5:].strip(), level=4)
|
| 97 |
+
current_heading_level = 4
|
| 98 |
+
# Handle bullet points
|
| 99 |
+
elif line.startswith('- ') or line.startswith('* '):
|
| 100 |
+
p = doc.add_paragraph(line[2:].strip(), style='List Bullet')
|
| 101 |
+
# Handle numbered lists
|
| 102 |
+
elif re.match(r'^\d+\.\s', line):
|
| 103 |
+
p = doc.add_paragraph(line, style='List Number')
|
| 104 |
+
# Handle horizontal rule
|
| 105 |
+
elif line.strip() == '---' or line.strip() == '***':
|
| 106 |
+
doc.add_paragraph('_' * 50)
|
| 107 |
+
# Handle bold text within paragraph
|
| 108 |
+
else:
|
| 109 |
+
# Process inline formatting
|
| 110 |
+
p = doc.add_paragraph()
|
| 111 |
+
self._add_formatted_text(p, line)
|
| 112 |
+
|
| 113 |
+
# Handle any remaining table
|
| 114 |
+
if in_table and table_rows:
|
| 115 |
+
num_cols = max(len(row) for row in table_rows)
|
| 116 |
+
table = doc.add_table(rows=len(table_rows), cols=num_cols)
|
| 117 |
+
table.style = 'Table Grid'
|
| 118 |
+
for i, row in enumerate(table_rows):
|
| 119 |
+
for j, cell_text in enumerate(row):
|
| 120 |
+
if j < num_cols:
|
| 121 |
+
table.cell(i, j).text = cell_text
|
| 122 |
+
|
| 123 |
+
output_path = self.output_dir / filename
|
| 124 |
+
doc.save(str(output_path))
|
| 125 |
+
return output_path
|
| 126 |
+
|
| 127 |
+
def _add_formatted_text(self, paragraph, text):
|
| 128 |
+
"""Add text with markdown formatting to a paragraph"""
|
| 129 |
+
from docx.shared import RGBColor
|
| 130 |
+
|
| 131 |
+
# Process bold and italic
|
| 132 |
+
parts = []
|
| 133 |
+
current_pos = 0
|
| 134 |
+
bold_pattern = r'\*\*([^*]+)\*\*'
|
| 135 |
+
italic_pattern = r'\*([^*]+)\*'
|
| 136 |
+
|
| 137 |
+
# Combine patterns
|
| 138 |
+
all_matches = []
|
| 139 |
+
for match in re.finditer(bold_pattern, text):
|
| 140 |
+
all_matches.append((match.start(), match.end(), 'bold', match.group(1)))
|
| 141 |
+
for match in re.finditer(italic_pattern, text):
|
| 142 |
+
all_matches.append((match.start(), match.end(), 'italic', match.group(1)))
|
| 143 |
+
|
| 144 |
+
all_matches.sort(key=lambda x: x[0])
|
| 145 |
+
|
| 146 |
+
if not all_matches:
|
| 147 |
+
paragraph.add_run(text)
|
| 148 |
+
return
|
| 149 |
+
|
| 150 |
+
last_end = 0
|
| 151 |
+
for start, end, style, content in all_matches:
|
| 152 |
+
if start > last_end:
|
| 153 |
+
paragraph.add_run(text[last_end:start])
|
| 154 |
+
run = paragraph.add_run(content)
|
| 155 |
+
if style == 'bold':
|
| 156 |
+
run.bold = True
|
| 157 |
+
elif style == 'italic':
|
| 158 |
+
run.italic = True
|
| 159 |
+
last_end = end
|
| 160 |
+
|
| 161 |
+
if last_end < len(text):
|
| 162 |
+
paragraph.add_run(text[last_end:])
|
| 163 |
+
|
| 164 |
+
def create_text_file(self, content, filename):
|
| 165 |
+
"""Create a plain text file with proper formatting"""
|
| 166 |
+
output_path = self.output_dir / filename
|
| 167 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 168 |
+
f.write(content)
|
| 169 |
+
return output_path
|
| 170 |
+
|
| 171 |
+
def create_excel_file(self, content, filename):
|
| 172 |
+
"""Create an Excel file from markdown table or CSV content"""
|
| 173 |
+
import openpyxl
|
| 174 |
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
| 175 |
+
|
| 176 |
+
wb = openpyxl.Workbook()
|
| 177 |
+
ws = wb.active
|
| 178 |
+
ws.title = "Sheet1"
|
| 179 |
+
|
| 180 |
+
# Parse content - look for markdown tables first
|
| 181 |
+
lines = content.strip().split('\n')
|
| 182 |
+
table_data = []
|
| 183 |
+
in_table = False
|
| 184 |
+
|
| 185 |
+
for line in lines:
|
| 186 |
+
if '|' in line and line.count('|') >= 2:
|
| 187 |
+
cells = [cell.strip() for cell in line.split('|')]
|
| 188 |
+
cells = [c for c in cells if c or (cells.index(c) > 0 and cells.index(c) < len(cells)-1)]
|
| 189 |
+
if cells:
|
| 190 |
+
if not in_table:
|
| 191 |
+
in_table = True
|
| 192 |
+
# Skip separator rows
|
| 193 |
+
if not all('-' in cell for cell in cells if cell):
|
| 194 |
+
table_data.append(cells)
|
| 195 |
+
elif in_table and not line.strip():
|
| 196 |
+
in_table = False
|
| 197 |
+
|
| 198 |
+
if not table_data:
|
| 199 |
+
# Try CSV format
|
| 200 |
+
for line in lines:
|
| 201 |
+
if ',' in line:
|
| 202 |
+
table_data.append([cell.strip() for cell in line.split(',')])
|
| 203 |
+
elif line.strip():
|
| 204 |
+
table_data.append([line.strip()])
|
| 205 |
+
|
| 206 |
+
# Write to Excel with styling
|
| 207 |
+
if table_data:
|
| 208 |
+
thin_border = Border(
|
| 209 |
+
left=Side(style='thin'), right=Side(style='thin'),
|
| 210 |
+
top=Side(style='thin'), bottom=Side(style='thin')
|
| 211 |
+
)
|
| 212 |
+
header_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid')
|
| 213 |
+
header_font = Font(color='FFFFFF', bold=True)
|
| 214 |
+
|
| 215 |
+
for i, row in enumerate(table_data):
|
| 216 |
+
for j, cell_value in enumerate(row):
|
| 217 |
+
cell = ws.cell(row=i+1, column=j+1, value=cell_value)
|
| 218 |
+
cell.border = thin_border
|
| 219 |
+
cell.alignment = Alignment(horizontal='left', vertical='center')
|
| 220 |
+
if i == 0:
|
| 221 |
+
cell.fill = header_fill
|
| 222 |
+
cell.font = header_font
|
| 223 |
+
|
| 224 |
+
# Auto-adjust column widths
|
| 225 |
+
for column in ws.columns:
|
| 226 |
+
max_length = 0
|
| 227 |
+
column_letter = column[0].column_letter
|
| 228 |
+
for cell in column:
|
| 229 |
+
try:
|
| 230 |
+
if len(str(cell.value)) > max_length:
|
| 231 |
+
max_length = len(str(cell.value))
|
| 232 |
+
except:
|
| 233 |
+
pass
|
| 234 |
+
adjusted_width = min(max_length + 2, 50)
|
| 235 |
+
ws.column_dimensions[column_letter].width = adjusted_width
|
| 236 |
+
|
| 237 |
+
output_path = self.output_dir / filename
|
| 238 |
+
wb.save(str(output_path))
|
| 239 |
+
return output_path
|
| 240 |
+
|
| 241 |
+
def create_csv_file(self, content, filename):
|
| 242 |
+
"""Create a CSV file from markdown table or text content"""
|
| 243 |
+
import csv
|
| 244 |
+
|
| 245 |
+
# Parse content - look for markdown tables
|
| 246 |
+
lines = content.strip().split('\n')
|
| 247 |
+
table_data = []
|
| 248 |
+
in_table = False
|
| 249 |
+
|
| 250 |
+
for line in lines:
|
| 251 |
+
if '|' in line and line.count('|') >= 2:
|
| 252 |
+
cells = [cell.strip() for cell in line.split('|')]
|
| 253 |
+
cells = [c for c in cells if c or (cells.index(c) > 0 and cells.index(c) < len(cells)-1)]
|
| 254 |
+
if cells:
|
| 255 |
+
if not in_table:
|
| 256 |
+
in_table = True
|
| 257 |
+
if not all('-' in cell for cell in cells if cell):
|
| 258 |
+
table_data.append(cells)
|
| 259 |
+
elif in_table and not line.strip():
|
| 260 |
+
in_table = False
|
| 261 |
+
|
| 262 |
+
if not table_data:
|
| 263 |
+
# Try plain CSV or text
|
| 264 |
+
for line in lines:
|
| 265 |
+
if ',' in line:
|
| 266 |
+
table_data.append([cell.strip() for cell in line.split(',')])
|
| 267 |
+
elif line.strip():
|
| 268 |
+
table_data.append([line.strip()])
|
| 269 |
+
|
| 270 |
+
output_path = self.output_dir / filename
|
| 271 |
+
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
| 272 |
+
writer = csv.writer(f)
|
| 273 |
+
writer.writerows(table_data)
|
| 274 |
+
|
| 275 |
+
return output_path
|
| 276 |
+
|
| 277 |
+
def create_powerpoint(self, content, filename):
|
| 278 |
+
"""Create a PowerPoint presentation with proper slide separation and formatting"""
|
| 279 |
+
from pptx.util import Inches as PptxInches
|
| 280 |
+
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
| 281 |
+
from pptx.dml.color import RGBColor as PptxRGBColor
|
| 282 |
+
|
| 283 |
+
prs = Presentation()
|
| 284 |
+
|
| 285 |
+
# Set slide dimensions (standard 16:9)
|
| 286 |
+
prs.slide_width = PptxInches(13.333)
|
| 287 |
+
prs.slide_height = PptxInches(7.5)
|
| 288 |
+
|
| 289 |
+
# Parse slides - split by "---" or "## Slide" patterns
|
| 290 |
+
slides_content = []
|
| 291 |
+
|
| 292 |
+
# Try to split by markdown slide separators
|
| 293 |
+
if '---' in content:
|
| 294 |
+
raw_slides = content.split('---')
|
| 295 |
+
for slide in raw_slides:
|
| 296 |
+
if slide.strip():
|
| 297 |
+
slides_content.append(slide.strip())
|
| 298 |
+
else:
|
| 299 |
+
# Try to split by "## Slide" pattern
|
| 300 |
+
import re
|
| 301 |
+
slide_pattern = r'(?:##\s*Slide\s*\d+)\s*\n(.*?)(?=(?:##\s*Slide|$))'
|
| 302 |
+
matches = re.findall(slide_pattern, content, re.DOTALL)
|
| 303 |
+
if matches:
|
| 304 |
+
slides_content = matches
|
| 305 |
+
else:
|
| 306 |
+
# Try to split by numbered slides
|
| 307 |
+
numbered_slides = re.split(r'\n(?=\d+\.\s+[A-Z])', content)
|
| 308 |
+
if len(numbered_slides) > 1:
|
| 309 |
+
slides_content = numbered_slides
|
| 310 |
+
else:
|
| 311 |
+
# Single slide
|
| 312 |
+
slides_content = [content]
|
| 313 |
+
|
| 314 |
+
# If no slides found, create one slide with all content
|
| 315 |
+
if not slides_content:
|
| 316 |
+
slides_content = [content]
|
| 317 |
+
|
| 318 |
+
for slide_idx, slide_content in enumerate(slides_content):
|
| 319 |
+
if not slide_content.strip():
|
| 320 |
+
continue
|
| 321 |
+
|
| 322 |
+
lines = slide_content.split('\n')
|
| 323 |
+
|
| 324 |
+
# Clean up lines - remove empty lines at start/end
|
| 325 |
+
while lines and not lines[0].strip():
|
| 326 |
+
lines.pop(0)
|
| 327 |
+
while lines and not lines[-1].strip():
|
| 328 |
+
lines.pop()
|
| 329 |
+
|
| 330 |
+
if not lines:
|
| 331 |
+
continue
|
| 332 |
+
|
| 333 |
+
# Extract title and body content
|
| 334 |
+
slide_title = ""
|
| 335 |
+
body_lines = []
|
| 336 |
+
|
| 337 |
+
# First line as potential title
|
| 338 |
+
first_line = lines[0].strip()
|
| 339 |
+
|
| 340 |
+
# Check if first line is a heading (starts with #)
|
| 341 |
+
if first_line.startswith('#'):
|
| 342 |
+
slide_title = first_line.lstrip('#').strip()
|
| 343 |
+
body_lines = lines[1:]
|
| 344 |
+
elif first_line.startswith('Slide') or first_line.startswith('**'):
|
| 345 |
+
slide_title = first_line.replace('**', '').replace('Slide', '').strip()
|
| 346 |
+
if slide_title and slide_title[0].isdigit():
|
| 347 |
+
# Extract just the title part
|
| 348 |
+
parts = slide_title.split(' ', 1)
|
| 349 |
+
if len(parts) > 1:
|
| 350 |
+
slide_title = parts[1]
|
| 351 |
+
else:
|
| 352 |
+
slide_title = f"Slide {slide_idx + 1}"
|
| 353 |
+
body_lines = lines[1:] if len(lines) > 1 else []
|
| 354 |
+
else:
|
| 355 |
+
# No clear title, use generic title
|
| 356 |
+
slide_title = f"Slide {slide_idx + 1}"
|
| 357 |
+
body_lines = lines
|
| 358 |
+
|
| 359 |
+
# Determine slide layout
|
| 360 |
+
if slide_idx == 0 and (len(body_lines) == 0 or len(slide_title) < 30):
|
| 361 |
+
# Title slide
|
| 362 |
+
slide_layout = prs.slide_layouts[0]
|
| 363 |
+
slide = prs.slides.add_slide(slide_layout)
|
| 364 |
+
|
| 365 |
+
# Set title
|
| 366 |
+
if slide.shapes.title:
|
| 367 |
+
title_shape = slide.shapes.title
|
| 368 |
+
title_shape.text = slide_title if slide_title else "Presentation Title"
|
| 369 |
+
|
| 370 |
+
# Format title
|
| 371 |
+
for paragraph in title_shape.text_frame.paragraphs:
|
| 372 |
+
paragraph.font.size = PptxInches(0.44)
|
| 373 |
+
paragraph.font.bold = True
|
| 374 |
+
paragraph.alignment = PP_ALIGN.CENTER
|
| 375 |
+
|
| 376 |
+
# Set subtitle if available
|
| 377 |
+
if len(slide.placeholders) > 1 and body_lines:
|
| 378 |
+
subtitle_placeholder = slide.placeholders[1]
|
| 379 |
+
subtitle_text = '\n'.join(body_lines[:3])
|
| 380 |
+
subtitle_placeholder.text = subtitle_text
|
| 381 |
+
|
| 382 |
+
# Format subtitle
|
| 383 |
+
for paragraph in subtitle_placeholder.text_frame.paragraphs:
|
| 384 |
+
paragraph.font.size = PptxInches(0.28)
|
| 385 |
+
paragraph.alignment = PP_ALIGN.CENTER
|
| 386 |
+
else:
|
| 387 |
+
# Content slide
|
| 388 |
+
slide_layout = prs.slide_layouts[1]
|
| 389 |
+
slide = prs.slides.add_slide(slide_layout)
|
| 390 |
+
|
| 391 |
+
# Set title
|
| 392 |
+
if slide.shapes.title:
|
| 393 |
+
title_shape = slide.shapes.title
|
| 394 |
+
title_shape.text = slide_title if slide_title else f"Slide {slide_idx + 1}"
|
| 395 |
+
|
| 396 |
+
# Format title
|
| 397 |
+
for paragraph in title_shape.text_frame.paragraphs:
|
| 398 |
+
paragraph.font.size = PptxInches(0.36)
|
| 399 |
+
paragraph.font.bold = True
|
| 400 |
+
|
| 401 |
+
# Set content
|
| 402 |
+
if len(slide.placeholders) > 1:
|
| 403 |
+
content_placeholder = slide.placeholders[1]
|
| 404 |
+
|
| 405 |
+
# Clear existing content
|
| 406 |
+
content_placeholder.text = ""
|
| 407 |
+
text_frame = content_placeholder.text_frame
|
| 408 |
+
text_frame.clear()
|
| 409 |
+
|
| 410 |
+
# Process body lines and add to content
|
| 411 |
+
for line in body_lines:
|
| 412 |
+
line = line.strip()
|
| 413 |
+
if not line:
|
| 414 |
+
continue
|
| 415 |
+
|
| 416 |
+
# Add a new paragraph
|
| 417 |
+
p = text_frame.add_paragraph()
|
| 418 |
+
|
| 419 |
+
# Handle bullet points
|
| 420 |
+
if line.startswith('- ') or line.startswith('* '):
|
| 421 |
+
p.text = line[2:]
|
| 422 |
+
p.level = 0
|
| 423 |
+
p.bullet = True
|
| 424 |
+
elif line.startswith(' - ') or line.startswith(' * '):
|
| 425 |
+
p.text = line[4:]
|
| 426 |
+
p.level = 1
|
| 427 |
+
p.bullet = True
|
| 428 |
+
elif line.startswith(' - ') or line.startswith(' * '):
|
| 429 |
+
p.text = line[6:]
|
| 430 |
+
p.level = 2
|
| 431 |
+
p.bullet = True
|
| 432 |
+
elif re.match(r'^\d+\.\s', line):
|
| 433 |
+
# Numbered list
|
| 434 |
+
p.text = line
|
| 435 |
+
p.bullet = False
|
| 436 |
+
elif line.startswith('**') and line.endswith('**'):
|
| 437 |
+
# Bold heading within content
|
| 438 |
+
p.text = line.strip('*')
|
| 439 |
+
p.font.bold = True
|
| 440 |
+
else:
|
| 441 |
+
# Regular paragraph
|
| 442 |
+
p.text = line
|
| 443 |
+
p.bullet = False
|
| 444 |
+
|
| 445 |
+
# Set font size
|
| 446 |
+
p.font.size = PptxInches(0.24)
|
| 447 |
+
|
| 448 |
+
# Add spacing between paragraphs
|
| 449 |
+
p.space_after = PptxInches(0.06)
|
| 450 |
+
|
| 451 |
+
# Remove any empty slides at the end
|
| 452 |
+
slides_to_remove = []
|
| 453 |
+
for i, slide in enumerate(prs.slides):
|
| 454 |
+
has_content = False
|
| 455 |
+
for shape in slide.shapes:
|
| 456 |
+
if hasattr(shape, "text") and shape.text and shape.text.strip():
|
| 457 |
+
has_content = True
|
| 458 |
+
break
|
| 459 |
+
if not has_content and i > 0:
|
| 460 |
+
slides_to_remove.append(i)
|
| 461 |
+
|
| 462 |
+
# Note: python-pptx doesn't support direct slide removal easily
|
| 463 |
+
# We'll just save as is - empty slides are rare
|
| 464 |
+
|
| 465 |
+
output_path = self.output_dir / filename
|
| 466 |
+
prs.save(str(output_path))
|
| 467 |
+
return output_path
|
| 468 |
+
|
| 469 |
+
def create_image_from_text(self, content, filename):
|
| 470 |
+
"""Create an image from text content with proper formatting"""
|
| 471 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 472 |
+
|
| 473 |
+
# Clean content
|
| 474 |
+
lines = []
|
| 475 |
+
for line in content.split('\n'):
|
| 476 |
+
if line.strip():
|
| 477 |
+
# Remove markdown formatting for image
|
| 478 |
+
clean_line = re.sub(r'\*\*([^*]+)\*\*', r'\1', line)
|
| 479 |
+
clean_line = re.sub(r'\*([^*]+)\*', r'\1', clean_line)
|
| 480 |
+
clean_line = re.sub(r'#+\s*', '', clean_line)
|
| 481 |
+
lines.append(clean_line)
|
| 482 |
+
|
| 483 |
+
if not lines:
|
| 484 |
+
lines = [content[:100]]
|
| 485 |
+
|
| 486 |
+
# Calculate image size
|
| 487 |
+
max_line_length = max(len(line) for line in lines) if lines else 40
|
| 488 |
+
img_width = min(1200, max(400, max_line_length * 12))
|
| 489 |
+
line_height = 30
|
| 490 |
+
img_height = max(400, len(lines) * line_height + 100)
|
| 491 |
+
|
| 492 |
+
img = Image.new('RGB', (img_width, img_height), color='white')
|
| 493 |
+
draw = ImageDraw.Draw(img)
|
| 494 |
+
|
| 495 |
+
# Try to load a better font
|
| 496 |
+
try:
|
| 497 |
+
font = ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", 16)
|
| 498 |
+
except:
|
| 499 |
+
try:
|
| 500 |
+
font = ImageFont.truetype("arial.ttf", 16)
|
| 501 |
+
except:
|
| 502 |
+
font = ImageFont.load_default()
|
| 503 |
+
|
| 504 |
+
y = 40
|
| 505 |
+
for line in lines:
|
| 506 |
+
# Wrap long lines
|
| 507 |
+
wrapped_lines = textwrap.wrap(line, width=img_width // 10)
|
| 508 |
+
for wrapped in wrapped_lines:
|
| 509 |
+
draw.text((20, y), wrapped, fill='black', font=font)
|
| 510 |
+
y += line_height
|
| 511 |
+
|
| 512 |
+
output_path = self.output_dir / filename
|
| 513 |
+
img.save(str(output_path))
|
| 514 |
+
return output_path
|
| 515 |
+
|
| 516 |
+
def create_pdf_from_content(self, content, filename):
|
| 517 |
+
"""Create a PDF from markdown content with proper formatting"""
|
| 518 |
+
from reportlab.lib.pagesizes import letter
|
| 519 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 520 |
+
from reportlab.lib.units import inch
|
| 521 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
| 522 |
+
from reportlab.lib import colors
|
| 523 |
+
|
| 524 |
+
output_path = self.output_dir / filename
|
| 525 |
+
doc = SimpleDocTemplate(str(output_path), pagesize=letter,
|
| 526 |
+
rightMargin=72, leftMargin=72,
|
| 527 |
+
topMargin=72, bottomMargin=72)
|
| 528 |
+
|
| 529 |
+
styles = getSampleStyleSheet()
|
| 530 |
+
story = []
|
| 531 |
+
|
| 532 |
+
# Create custom styles
|
| 533 |
+
heading1_style = ParagraphStyle('Heading1Custom', parent=styles['Heading1'], fontSize=16, spaceAfter=12, spaceBefore=12)
|
| 534 |
+
heading2_style = ParagraphStyle('Heading2Custom', parent=styles['Heading2'], fontSize=14, spaceAfter=10, spaceBefore=10)
|
| 535 |
+
normal_style = ParagraphStyle('NormalCustom', parent=styles['Normal'], fontSize=10, spaceAfter=6)
|
| 536 |
+
|
| 537 |
+
lines = content.split('\n')
|
| 538 |
+
in_table = False
|
| 539 |
+
table_data = []
|
| 540 |
+
|
| 541 |
+
for line in lines:
|
| 542 |
+
line = line.strip()
|
| 543 |
+
if not line:
|
| 544 |
+
if not in_table:
|
| 545 |
+
story.append(Spacer(1, 6))
|
| 546 |
+
continue
|
| 547 |
+
|
| 548 |
+
# Handle markdown tables
|
| 549 |
+
if '|' in line and line.count('|') >= 2:
|
| 550 |
+
cells = [cell.strip() for cell in line.split('|')]
|
| 551 |
+
cells = [c for c in cells if c or (cells.index(c) > 0 and cells.index(c) < len(cells)-1)]
|
| 552 |
+
if cells:
|
| 553 |
+
if not in_table:
|
| 554 |
+
in_table = True
|
| 555 |
+
table_data = [cells]
|
| 556 |
+
elif not all('-' in cell for cell in cells if cell):
|
| 557 |
+
table_data.append(cells)
|
| 558 |
+
continue
|
| 559 |
+
elif in_table:
|
| 560 |
+
# Create table
|
| 561 |
+
if table_data:
|
| 562 |
+
# Convert to ReportLab table
|
| 563 |
+
rt_table = Table(table_data)
|
| 564 |
+
rt_table.setStyle(TableStyle([
|
| 565 |
+
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
| 566 |
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 567 |
+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 568 |
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 569 |
+
('FONTSIZE', (0, 0), (-1, 0), 10),
|
| 570 |
+
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
| 571 |
+
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
| 572 |
+
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
| 573 |
+
]))
|
| 574 |
+
story.append(rt_table)
|
| 575 |
+
story.append(Spacer(1, 12))
|
| 576 |
+
in_table = False
|
| 577 |
+
table_data = []
|
| 578 |
+
continue
|
| 579 |
+
|
| 580 |
+
# Handle headings
|
| 581 |
+
if line.startswith('# '):
|
| 582 |
+
story.append(Paragraph(line[2:], heading1_style))
|
| 583 |
+
elif line.startswith('## '):
|
| 584 |
+
story.append(Paragraph(line[3:], heading2_style))
|
| 585 |
+
elif line.startswith('### '):
|
| 586 |
+
story.append(Paragraph(line[4:], normal_style))
|
| 587 |
+
elif line.startswith('- ') or line.startswith('* '):
|
| 588 |
+
story.append(Paragraph('• ' + line[2:], normal_style))
|
| 589 |
+
elif re.match(r'^\d+\.\s', line):
|
| 590 |
+
story.append(Paragraph(line, normal_style))
|
| 591 |
+
else:
|
| 592 |
+
story.append(Paragraph(line, normal_style))
|
| 593 |
+
|
| 594 |
+
doc.build(story)
|
| 595 |
+
return output_path
|
| 596 |
+
|
| 597 |
+
def create_document(self, content, doc_type, filename, template_id=None):
|
| 598 |
+
"""
|
| 599 |
+
Main method to create a document based on type
|
| 600 |
+
|
| 601 |
+
Args:
|
| 602 |
+
content: The content to put in the document
|
| 603 |
+
doc_type: Type of document ('word', 'txt', 'excel', 'csv', 'ppt', 'image', 'pdf')
|
| 604 |
+
filename: Desired filename (without extension)
|
| 605 |
+
template_id: Optional template ID for formatting
|
| 606 |
+
|
| 607 |
+
Returns:
|
| 608 |
+
Path to created document
|
| 609 |
+
"""
|
| 610 |
+
# Ensure filename has proper extension
|
| 611 |
+
ext_map = {
|
| 612 |
+
'word': '.docx',
|
| 613 |
+
'txt': '.txt',
|
| 614 |
+
'pdf': '.pdf',
|
| 615 |
+
'excel': '.xlsx',
|
| 616 |
+
'csv': '.csv',
|
| 617 |
+
'ppt': '.pptx',
|
| 618 |
+
'image': '.png'
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
ext = ext_map.get(doc_type, '.txt')
|
| 622 |
+
full_filename = f"{filename}{ext}"
|
| 623 |
+
|
| 624 |
+
# Enhance content with template-specific formatting
|
| 625 |
+
if template_id:
|
| 626 |
+
content = self._apply_template_formatting(content, doc_type, template_id)
|
| 627 |
+
|
| 628 |
+
# Create document based on type
|
| 629 |
+
if doc_type == 'word':
|
| 630 |
+
return self.create_word_document(content, full_filename)
|
| 631 |
+
elif doc_type == 'txt':
|
| 632 |
+
return self.create_text_file(content, full_filename)
|
| 633 |
+
elif doc_type == 'excel':
|
| 634 |
+
return self.create_excel_file(content, full_filename)
|
| 635 |
+
elif doc_type == 'csv':
|
| 636 |
+
return self.create_csv_file(content, full_filename)
|
| 637 |
+
elif doc_type == 'ppt':
|
| 638 |
+
return self.create_powerpoint(content, full_filename)
|
| 639 |
+
elif doc_type == 'image':
|
| 640 |
+
return self.create_image_from_text(content, full_filename)
|
| 641 |
+
elif doc_type == 'pdf':
|
| 642 |
+
return self.create_pdf_from_content(content, full_filename)
|
| 643 |
+
else:
|
| 644 |
+
raise ValueError(f"Unsupported document type: {doc_type}")
|
| 645 |
+
|
| 646 |
+
def _apply_template_formatting(self, content, doc_type, template_id):
|
| 647 |
+
"""Apply template-specific formatting instructions to content"""
|
| 648 |
+
if template_id == 'professional':
|
| 649 |
+
if doc_type == 'word':
|
| 650 |
+
content += """
|
| 651 |
+
|
| 652 |
+
Format this as a professional document with:
|
| 653 |
+
- Clear headings (use ### for main sections)
|
| 654 |
+
- Bullet points where appropriate
|
| 655 |
+
- Professional tone
|
| 656 |
+
- Proper spacing between sections"""
|
| 657 |
+
elif doc_type == 'ppt':
|
| 658 |
+
content += """
|
| 659 |
+
|
| 660 |
+
Format this as a presentation with:
|
| 661 |
+
- Title slide (presentation title)
|
| 662 |
+
- 3-5 content slides with headings and bullet points
|
| 663 |
+
- Closing slide with summary or call to action
|
| 664 |
+
Separate slides with ---"""
|
| 665 |
+
elif doc_type == 'excel':
|
| 666 |
+
content += """
|
| 667 |
+
|
| 668 |
+
Format this as structured data with:
|
| 669 |
+
- Column headers as first row
|
| 670 |
+
- Each row as a data entry
|
| 671 |
+
- Use consistent formatting"""
|
| 672 |
+
|
| 673 |
+
return content
|
| 674 |
+
|
| 675 |
+
def get_download_url(self, filename, base_url="/api/docs/download/"):
|
| 676 |
+
"""Get download URL for a file"""
|
| 677 |
+
return f"{base_url}{filename}"
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
# Convenience functions for quick document creation
|
| 681 |
+
|
| 682 |
+
def create_word_document(content, filename, output_dir="generated_docs"):
|
| 683 |
+
"""Quickly create a Word document"""
|
| 684 |
+
creator = DocumentCreator(output_dir)
|
| 685 |
+
return creator.create_word_document(content, filename)
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def create_text_file(content, filename, output_dir="generated_docs"):
|
| 689 |
+
"""Quickly create a text file"""
|
| 690 |
+
creator = DocumentCreator(output_dir)
|
| 691 |
+
return creator.create_text_file(content, filename)
|
| 692 |
+
|
| 693 |
+
|
| 694 |
+
def create_excel_file(content, filename, output_dir="generated_docs"):
|
| 695 |
+
"""Quickly create an Excel file"""
|
| 696 |
+
creator = DocumentCreator(output_dir)
|
| 697 |
+
return creator.create_excel_file(content, filename)
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
def create_powerpoint(content, filename, output_dir="generated_docs"):
|
| 701 |
+
"""Quickly create a PowerPoint presentation"""
|
| 702 |
+
creator = DocumentCreator(output_dir)
|
| 703 |
+
return creator.create_powerpoint(content, filename)
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
def create_image_from_text(content, filename, output_dir="generated_docs"):
|
| 707 |
+
"""Quickly create an image from text"""
|
| 708 |
+
creator = DocumentCreator(output_dir)
|
| 709 |
+
return creator.create_image_from_text(content, filename)
|
| 710 |
+
|
| 711 |
+
|
| 712 |
+
def create_pdf_from_content(content, filename, output_dir="generated_docs"):
|
| 713 |
+
"""Quickly create a PDF from text"""
|
| 714 |
+
creator = DocumentCreator(output_dir)
|
| 715 |
+
return creator.create_pdf_from_content(content, filename)
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
# Example usage
|
| 719 |
+
if __name__ == "__main__":
|
| 720 |
+
# Test the document creator
|
| 721 |
+
creator = DocumentCreator()
|
| 722 |
+
|
| 723 |
+
# Create a sample Word document
|
| 724 |
+
sample_content = """# Welcome to My Document
|
| 725 |
+
## Introduction
|
| 726 |
+
This is a sample document created with the DocumentCreator class.
|
| 727 |
+
|
| 728 |
+
## Features
|
| 729 |
+
- Easy document creation
|
| 730 |
+
- Multiple format support
|
| 731 |
+
- Professional formatting
|
| 732 |
+
|
| 733 |
+
### Conclusion
|
| 734 |
+
Thank you for using this utility!"""
|
| 735 |
+
|
| 736 |
+
doc_path = creator.create_document(sample_content, 'word', 'sample_document')
|
| 737 |
+
print(f"Created Word document: {doc_path}")
|
| 738 |
+
|
| 739 |
+
# Create a sample text file
|
| 740 |
+
text_path = creator.create_document("Hello, World!", 'txt', 'hello_world')
|
| 741 |
+
print(f"Created text file: {text_path}")
|
vision.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# vision.py - Multi-Model Vision Processor for HenAi
|
| 2 |
+
# Supports multiple vision models with automatic fallback
|
| 3 |
+
# No metadata analysis - pure image content understanding
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from PIL import Image
|
| 7 |
+
import io
|
| 8 |
+
import base64
|
| 9 |
+
import requests
|
| 10 |
+
import re
|
| 11 |
+
|
| 12 |
+
# ============= TRY IMPORTS WITH FALLBACKS =============
|
| 13 |
+
|
| 14 |
+
# BLIP Model (Salesforce)
|
| 15 |
+
try:
|
| 16 |
+
from transformers import BlipProcessor, BlipForConditionalGeneration
|
| 17 |
+
BLIP_AVAILABLE = True
|
| 18 |
+
except ImportError:
|
| 19 |
+
BLIP_AVAILABLE = False
|
| 20 |
+
print("Warning: BLIP not available. Install with: pip install transformers")
|
| 21 |
+
|
| 22 |
+
# Florence-2 Model (Microsoft - more detailed)
|
| 23 |
+
try:
|
| 24 |
+
from transformers import AutoProcessor, AutoModelForCausalLM
|
| 25 |
+
FLORENCE_AVAILABLE = True
|
| 26 |
+
except ImportError:
|
| 27 |
+
FLORENCE_AVAILABLE = False
|
| 28 |
+
|
| 29 |
+
# OFA Model (Microsoft - good all-rounder)
|
| 30 |
+
try:
|
| 31 |
+
from transformers import OFATokenizer, OFAModel
|
| 32 |
+
OFA_AVAILABLE = True
|
| 33 |
+
except ImportError:
|
| 34 |
+
OFA_AVAILABLE = False
|
| 35 |
+
|
| 36 |
+
# Git (ViT + GPT2)
|
| 37 |
+
try:
|
| 38 |
+
from transformers import GitProcessor, GitForCausalLM
|
| 39 |
+
GIT_AVAILABLE = True
|
| 40 |
+
except ImportError:
|
| 41 |
+
GIT_AVAILABLE = False
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class VisionModel:
|
| 45 |
+
"""
|
| 46 |
+
Multi-model vision processor with automatic fallback.
|
| 47 |
+
Tries models in order: BLIP -> Florence-2 -> GIT -> Fallback text analysis
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self):
|
| 51 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 52 |
+
print(f"🖼️ Initializing Vision Model on {self.device}...")
|
| 53 |
+
|
| 54 |
+
self.models = {}
|
| 55 |
+
self.current_model = None
|
| 56 |
+
|
| 57 |
+
# Try to load BLIP (smallest, fastest)
|
| 58 |
+
if BLIP_AVAILABLE:
|
| 59 |
+
try:
|
| 60 |
+
print(" Loading BLIP model...")
|
| 61 |
+
self.models['blip'] = {
|
| 62 |
+
'processor': BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base"),
|
| 63 |
+
'model': BlipForConditionalGeneration.from_pretrained(
|
| 64 |
+
"Salesforce/blip-image-captioning-base",
|
| 65 |
+
torch_dtype=torch.float16 if self.device == "cuda" else torch.float32
|
| 66 |
+
).to(self.device),
|
| 67 |
+
'name': 'BLIP'
|
| 68 |
+
}
|
| 69 |
+
self.models['blip']['model'].eval()
|
| 70 |
+
print(" ✓ BLIP model loaded")
|
| 71 |
+
self.current_model = 'blip'
|
| 72 |
+
except Exception as e:
|
| 73 |
+
print(f" ✗ Failed to load BLIP: {e}")
|
| 74 |
+
|
| 75 |
+
# Try to load Florence-2 (more detailed captions)
|
| 76 |
+
if FLORENCE_AVAILABLE and not self.current_model:
|
| 77 |
+
try:
|
| 78 |
+
print(" Loading Florence-2 model...")
|
| 79 |
+
self.models['florence'] = {
|
| 80 |
+
'processor': AutoProcessor.from_pretrained("microsoft/florence-2-base", trust_remote_code=True),
|
| 81 |
+
'model': AutoModelForCausalLM.from_pretrained(
|
| 82 |
+
"microsoft/florence-2-base",
|
| 83 |
+
trust_remote_code=True,
|
| 84 |
+
torch_dtype=torch.float16 if self.device == "cuda" else torch.float32
|
| 85 |
+
).to(self.device),
|
| 86 |
+
'name': 'Florence-2'
|
| 87 |
+
}
|
| 88 |
+
self.models['florence']['model'].eval()
|
| 89 |
+
print(" ✓ Florence-2 model loaded")
|
| 90 |
+
self.current_model = 'florence'
|
| 91 |
+
except Exception as e:
|
| 92 |
+
print(f" ✗ Failed to load Florence-2: {e}")
|
| 93 |
+
|
| 94 |
+
# Try to load GIT (good for detailed descriptions)
|
| 95 |
+
if GIT_AVAILABLE and not self.current_model:
|
| 96 |
+
try:
|
| 97 |
+
print(" Loading GIT model...")
|
| 98 |
+
self.models['git'] = {
|
| 99 |
+
'processor': GitProcessor.from_pretrained("microsoft/git-base"),
|
| 100 |
+
'model': GitForCausalLM.from_pretrained(
|
| 101 |
+
"microsoft/git-base",
|
| 102 |
+
torch_dtype=torch.float16 if self.device == "cuda" else torch.float32
|
| 103 |
+
).to(self.device),
|
| 104 |
+
'name': 'GIT'
|
| 105 |
+
}
|
| 106 |
+
self.models['git']['model'].eval()
|
| 107 |
+
print(" ✓ GIT model loaded")
|
| 108 |
+
self.current_model = 'git'
|
| 109 |
+
except Exception as e:
|
| 110 |
+
print(f" ✗ Failed to load GIT: {e}")
|
| 111 |
+
|
| 112 |
+
if not self.current_model:
|
| 113 |
+
print("⚠️ No vision model loaded. Using fallback analysis.")
|
| 114 |
+
self.current_model = None
|
| 115 |
+
|
| 116 |
+
def get_vision_caption(self, image_bytes, max_length=100):
|
| 117 |
+
"""
|
| 118 |
+
Generate a natural description of the image content.
|
| 119 |
+
Returns a clean description without metadata.
|
| 120 |
+
"""
|
| 121 |
+
if not self.current_model:
|
| 122 |
+
return None
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
# Load image
|
| 126 |
+
image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
|
| 127 |
+
|
| 128 |
+
# Use the loaded model
|
| 129 |
+
if self.current_model == 'blip':
|
| 130 |
+
return self._caption_with_blip(image, max_length)
|
| 131 |
+
elif self.current_model == 'florence':
|
| 132 |
+
return self._caption_with_florence(image, max_length)
|
| 133 |
+
elif self.current_model == 'git':
|
| 134 |
+
return self._caption_with_git(image, max_length)
|
| 135 |
+
else:
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"Error generating vision caption with {self.current_model}: {e}")
|
| 140 |
+
# Try fallback to another model if available
|
| 141 |
+
return self._try_fallback_model(image_bytes, max_length)
|
| 142 |
+
|
| 143 |
+
def _caption_with_blip(self, image, max_length):
|
| 144 |
+
"""Generate caption using BLIP"""
|
| 145 |
+
processor = self.models['blip']['processor']
|
| 146 |
+
model = self.models['blip']['model']
|
| 147 |
+
|
| 148 |
+
inputs = processor(images=image, return_tensors="pt").to(self.device)
|
| 149 |
+
|
| 150 |
+
with torch.no_grad():
|
| 151 |
+
out = model.generate(
|
| 152 |
+
**inputs,
|
| 153 |
+
max_length=max_length,
|
| 154 |
+
num_beams=3,
|
| 155 |
+
temperature=0.7,
|
| 156 |
+
do_sample=True
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
caption = processor.decode(out[0], skip_special_tokens=True)
|
| 160 |
+
return self._clean_caption(caption)
|
| 161 |
+
|
| 162 |
+
def _caption_with_florence(self, image, max_length):
|
| 163 |
+
"""Generate detailed caption using Florence-2"""
|
| 164 |
+
processor = self.models['florence']['processor']
|
| 165 |
+
model = self.models['florence']['model']
|
| 166 |
+
|
| 167 |
+
prompt = "<MORE_DETAILED_CAPTION>"
|
| 168 |
+
inputs = processor(text=prompt, images=image, return_tensors="pt").to(self.device)
|
| 169 |
+
|
| 170 |
+
with torch.no_grad():
|
| 171 |
+
generated_ids = model.generate(
|
| 172 |
+
**inputs,
|
| 173 |
+
max_new_tokens=max_length,
|
| 174 |
+
do_sample=False,
|
| 175 |
+
num_beams=3
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| 179 |
+
# Remove the prompt from the output
|
| 180 |
+
generated_text = generated_text.replace(prompt, "").strip()
|
| 181 |
+
return self._clean_caption(generated_text)
|
| 182 |
+
|
| 183 |
+
def _caption_with_git(self, image, max_length):
|
| 184 |
+
"""Generate caption using GIT"""
|
| 185 |
+
processor = self.models['git']['processor']
|
| 186 |
+
model = self.models['git']['model']
|
| 187 |
+
|
| 188 |
+
inputs = processor(images=image, return_tensors="pt").to(self.device)
|
| 189 |
+
|
| 190 |
+
with torch.no_grad():
|
| 191 |
+
generated_ids = model.generate(
|
| 192 |
+
pixel_values=inputs.pixel_values,
|
| 193 |
+
max_length=max_length,
|
| 194 |
+
num_beams=3,
|
| 195 |
+
temperature=0.7
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
caption = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| 199 |
+
return self._clean_caption(caption)
|
| 200 |
+
|
| 201 |
+
def _try_fallback_model(self, image_bytes, max_length):
|
| 202 |
+
"""Try to use a different model if the current one fails"""
|
| 203 |
+
original_model = self.current_model
|
| 204 |
+
available_models = list(self.models.keys())
|
| 205 |
+
|
| 206 |
+
for model_name in available_models:
|
| 207 |
+
if model_name != original_model:
|
| 208 |
+
print(f" Trying fallback model: {model_name}")
|
| 209 |
+
self.current_model = model_name
|
| 210 |
+
try:
|
| 211 |
+
result = self.get_vision_caption(image_bytes, max_length)
|
| 212 |
+
if result:
|
| 213 |
+
print(f" ✓ Fallback to {model_name} successful")
|
| 214 |
+
return result
|
| 215 |
+
except Exception as e:
|
| 216 |
+
print(f" ✗ Fallback to {model_name} failed: {e}")
|
| 217 |
+
continue
|
| 218 |
+
|
| 219 |
+
# Reset to original model
|
| 220 |
+
self.current_model = original_model
|
| 221 |
+
return None
|
| 222 |
+
|
| 223 |
+
def _clean_caption(self, caption):
|
| 224 |
+
"""Clean the caption by removing metadata and markdown"""
|
| 225 |
+
if not caption:
|
| 226 |
+
return None
|
| 227 |
+
|
| 228 |
+
# Remove common metadata patterns
|
| 229 |
+
patterns_to_remove = [
|
| 230 |
+
r'Photo by\s+\w+', # Photo by [name]
|
| 231 |
+
r'©\s+\d{4}\s+\w+', # Copyright notices
|
| 232 |
+
r'Image courtesy of\s+\w+', # Courtesy notices
|
| 233 |
+
r'Sourced from\s+\w+', # Source notices
|
| 234 |
+
r'Image from\s+\w+', # Image from...
|
| 235 |
+
r'Source:\s*\w+', # Source:
|
| 236 |
+
r'\(Photo credit:.*?\)', # Photo credit
|
| 237 |
+
r'\[.*?\]', # Any bracketed text
|
| 238 |
+
r'^\w+:\s*', # "Label: " at start
|
| 239 |
+
r'\*\*|\*|__|_', # Markdown markers
|
| 240 |
+
]
|
| 241 |
+
|
| 242 |
+
cleaned = caption
|
| 243 |
+
for pattern in patterns_to_remove:
|
| 244 |
+
cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
|
| 245 |
+
|
| 246 |
+
# Clean up multiple spaces
|
| 247 |
+
cleaned = re.sub(r'\s+', ' ', cleaned)
|
| 248 |
+
|
| 249 |
+
# Ensure first letter is capitalized
|
| 250 |
+
if cleaned and len(cleaned) > 0:
|
| 251 |
+
cleaned = cleaned[0].upper() + cleaned[1:] if cleaned[1:] else cleaned
|
| 252 |
+
|
| 253 |
+
# Remove any trailing punctuation that looks like metadata
|
| 254 |
+
cleaned = re.sub(r'\s*[|;:]\s*$', '', cleaned)
|
| 255 |
+
|
| 256 |
+
return cleaned.strip()
|
| 257 |
+
|
| 258 |
+
def analyze_image(self, image_bytes):
|
| 259 |
+
"""
|
| 260 |
+
Generate a comprehensive, clean analysis of the image.
|
| 261 |
+
Returns only the image content description, no metadata.
|
| 262 |
+
"""
|
| 263 |
+
caption = self.get_vision_caption(image_bytes, max_length=120)
|
| 264 |
+
|
| 265 |
+
if caption and len(caption) > 10:
|
| 266 |
+
# Ensure the description is natural and doesn't mention metadata
|
| 267 |
+
# Remove any remaining "a photo of", "an image of" patterns
|
| 268 |
+
caption = re.sub(r'^(a|an)\s+(photo|picture|image)\s+of\s+', '', caption, flags=re.IGNORECASE)
|
| 269 |
+
return caption
|
| 270 |
+
|
| 271 |
+
return None
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
# Create global instance (lazy initialization)
|
| 275 |
+
_vision_model = None
|
| 276 |
+
|
| 277 |
+
def get_vision_model():
|
| 278 |
+
"""Get or create the global vision model instance"""
|
| 279 |
+
global _vision_model
|
| 280 |
+
if _vision_model is None:
|
| 281 |
+
_vision_model = VisionModel()
|
| 282 |
+
return _vision_model
|