joashsam commited on
Commit
f42611c
·
verified ·
1 Parent(s): 881be23

Upload 5 files

Browse files
Files changed (5) hide show
  1. binary_processor.py +893 -0
  2. media.py +1338 -0
  3. models.py +777 -0
  4. mydocs.py +741 -0
  5. 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