Răsfoiți Sursa

Merge resolved: integrated remote changes

Student Yadav 3 luni în urmă
părinte
comite
5fbb3cb397
73 a modificat fișierele cu 1747 adăugiri și 356 ștergeri
  1. 0 0
      attr_extraction/__init__.py
  2. 3 0
      attr_extraction/admin.py
  3. 6 0
      attr_extraction/apps.py
  4. 0 0
      attr_extraction/migrations/__init__.py
  5. 54 0
      attr_extraction/models.py
  6. 13 0
      attr_extraction/serializers.py
  7. 322 0
      attr_extraction/services/attribute_extractor.py
  8. 78 0
      attr_extraction/tasks.py
  9. 3 0
      attr_extraction/tests.py
  10. 15 0
      attr_extraction/urls.py
  11. 99 0
      attr_extraction/views.py
  12. 30 0
      celery.py
  13. 28 2
      content_quality_tool/settings.py
  14. 4 2
      content_quality_tool/urls.py
  15. 112 0
      content_quality_tool_public/templates/attr-extraction.html
  16. 8 0
      content_quality_tool_public/templates/footer.html
  17. 85 134
      content_quality_tool_public/templates/get-data.html
  18. 66 0
      content_quality_tool_public/templates/header.html
  19. 41 204
      content_quality_tool_public/templates/index.html
  20. 37 0
      content_quality_tool_public/templates/sidebar.html
  21. 1 8
      content_quality_tool_public/urls.py
  22. 6 1
      content_quality_tool_public/views.py
  23. 7 5
      core/services/gemini_service.py
  24. BIN
      media/uploads/wallet.jpg
  25. BIN
      media/uploads/wallet_DJy17YO.jpg
  26. BIN
      media/uploads/wallet_OnM0xil.jpg
  27. BIN
      media/uploads/wallet_Uk6Hy0q.jpg
  28. BIN
      media/uploads/wallet_k4xV4eu.jpg
  29. BIN
      media/uploads/wallet_oqDJD4Z.jpg
  30. BIN
      media/uploads/wallet_pdWTLhA.jpg
  31. BIN
      media/uploads/wallet_sz9Dovt.jpg
  32. 0 0
      video_generator/__init__.py
  33. 3 0
      video_generator/admin.py
  34. 6 0
      video_generator/apps.py
  35. 10 0
      video_generator/decorators.py
  36. 0 0
      video_generator/migrations/__init__.py
  37. 3 0
      video_generator/models.py
  38. 378 0
      video_generator/static/css/upload.css
  39. BIN
      video_generator/static/fonts/aptos-font/Aptos.eot
  40. BIN
      video_generator/static/fonts/aptos-font/Aptos.woff
  41. BIN
      video_generator/static/fonts/aptos-font/Aptos.woff2
  42. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-black-italic.ttf
  43. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-black.ttf
  44. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-bold.ttf
  45. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-extrabold-italic 2.ttf
  46. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-extrabold-italic.ttf
  47. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-extrabold.ttf
  48. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-italic.ttf
  49. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-light-italic.ttf
  50. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-light.ttf
  51. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos-semibold.ttf
  52. BIN
      video_generator/static/fonts/aptos-font/__MACOSX/._aptos.ttf
  53. BIN
      video_generator/static/fonts/aptos-font/aptos-black-italic.ttf
  54. BIN
      video_generator/static/fonts/aptos-font/aptos-black.ttf
  55. BIN
      video_generator/static/fonts/aptos-font/aptos-bold.ttf
  56. BIN
      video_generator/static/fonts/aptos-font/aptos-extrabold-italic 2.ttf
  57. BIN
      video_generator/static/fonts/aptos-font/aptos-extrabold-italic.ttf
  58. BIN
      video_generator/static/fonts/aptos-font/aptos-extrabold.ttf
  59. BIN
      video_generator/static/fonts/aptos-font/aptos-italic.ttf
  60. BIN
      video_generator/static/fonts/aptos-font/aptos-light-italic.ttf
  61. BIN
      video_generator/static/fonts/aptos-font/aptos-light.ttf
  62. BIN
      video_generator/static/fonts/aptos-font/aptos-semibold.ttf
  63. BIN
      video_generator/static/fonts/aptos-font/aptos.ttf
  64. BIN
      video_generator/static/images/45.jpg
  65. BIN
      video_generator/static/images/left.png
  66. BIN
      video_generator/static/images/logo-mini.png
  67. BIN
      video_generator/static/images/logo.png
  68. BIN
      video_generator/static/images/user2-160x160.jpg
  69. 62 0
      video_generator/static/js/upload.js
  70. 159 0
      video_generator/templates/img-upload.html
  71. 3 0
      video_generator/tests.py
  72. 13 0
      video_generator/urls.py
  73. 92 0
      video_generator/views.py

+ 0 - 0
attr_extraction/__init__.py


+ 3 - 0
attr_extraction/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
attr_extraction/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AttrExtractionConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'attr_extraction'

+ 0 - 0
attr_extraction/migrations/__init__.py


+ 54 - 0
attr_extraction/models.py

@@ -0,0 +1,54 @@
+# models.py
+from django.db import models
+from django.contrib.postgres.fields import JSONField
+
+class Product(models.Model):
+    title = models.CharField(max_length=500)
+    description = models.TextField()
+    short_description = models.TextField(blank=True)
+    attributes_extracted = models.BooleanField(default=False)
+    created_at = models.DateTimeField(auto_now_add=True)
+    updated_at = models.DateTimeField(auto_now=True)
+    
+    class Meta:
+        db_table = 'products'
+        indexes = [
+            models.Index(fields=['attributes_extracted', 'created_at']),
+        ]
+
+class ProductImage(models.Model):
+    product = models.ForeignKey(Product, related_name='images', on_delete=models.CASCADE)
+    image = models.ImageField(upload_to='products/')
+    order = models.PositiveIntegerField(default=0)
+    
+    class Meta:
+        db_table = 'product_images'
+        ordering = ['order']
+
+class ProductAttribute(models.Model):
+    product = models.ForeignKey(Product, related_name='attributes', on_delete=models.CASCADE)
+    attribute_name = models.CharField(max_length=100, db_index=True)
+    attribute_value = models.TextField()
+    confidence_score = models.FloatField(default=0.0)
+    extraction_method = models.CharField(
+        max_length=20,
+        choices=[('nlp', 'NLP'), ('llm', 'LLM'), ('hybrid', 'Hybrid')],
+        default='hybrid'
+    )
+    needs_review = models.BooleanField(default=False)
+    reviewed = models.BooleanField(default=False)
+    created_at = models.DateTimeField(auto_now_add=True)
+    
+    class Meta:
+        db_table = 'product_attributes'
+        unique_together = ['product', 'attribute_name']
+        indexes = [
+            models.Index(fields=['attribute_name', 'confidence_score']),
+            models.Index(fields=['needs_review', 'reviewed']),
+        ]
+    
+    def save(self, *args, **kwargs):
+        # Auto-flag low confidence for review
+        if self.confidence_score < 0.7:
+            self.needs_review = True
+        super().save(*args, **kwargs)

+ 13 - 0
attr_extraction/serializers.py

@@ -0,0 +1,13 @@
+# serializers.py
+from rest_framework import serializers
+from .models import ProductAttribute
+
+class ProductAttributeSerializer(serializers.ModelSerializer):
+    product_title = serializers.CharField(source='product.title', read_only=True)
+    
+    class Meta:
+        model = ProductAttribute
+        fields = ['id', 'product', 'product_title', 'attribute_name', 
+                  'attribute_value', 'confidence_score', 'extraction_method',
+                  'needs_review', 'reviewed', 'created_at']
+

+ 322 - 0
attr_extraction/services/attribute_extractor.py

@@ -0,0 +1,322 @@
+# services/attribute_extractor.py
+import re
+import spacy
+from typing import Dict, List, Optional
+from anthropic import Anthropic
+import base64
+from PIL import Image
+import pytesseract
+from collections import defaultdict
+
+class HybridAttributeExtractor:
+    """
+    Hybrid extractor using NLP for structured data and LLM for complex/ambiguous cases
+    """
+    
+    def __init__(self, anthropic_api_key: str, product_type_mappings: Dict = None):
+        self.nlp = spacy.load("en_core_web_sm")
+        self.client = Anthropic(api_key=anthropic_api_key)
+        self.product_type_mappings = product_type_mappings or self._load_default_mappings()
+        
+        # Define patterns for common attributes
+        self.patterns = {
+            'size': [
+                r'\b(XXS|XS|S|M|L|XL|XXL|XXXL)\b',
+                r'\b(\d+(?:\.\d+)?)\s*(inch|inches|cm|mm|meter|metres?|ft|feet|")\b',
+                r'\b(small|medium|large|extra large)\b'
+            ],
+            'color': [
+                r'\b(black|white|red|blue|green|yellow|orange|purple|pink|brown|gray|grey|silver|gold|beige|navy|maroon|olive|teal|turquoise|lavender|cream|ivory)\b'
+            ],
+            'weight': [
+                r'\b(\d+(?:\.\d+)?)\s*(kg|g|lb|lbs|oz|pounds?|grams?|kilograms?)\b'
+            ],
+            'material': [
+                r'\b(cotton|polyester|silk|wool|leather|denim|linen|nylon|spandex|rayon|acrylic|metal|plastic|wood|glass|ceramic|steel|aluminum|rubber)\b'
+            ],
+            'brand': [
+                r'(?:by|from|brand:?)\s+([A-Z][a-zA-Z0-9\s&]+?)(?:\s|$|,|\.|;)'
+            ]
+        }
+        
+        # Confidence thresholds
+        self.confidence_threshold = 0.6
+        
+    def extract_attributes(self, product_data: Dict) -> Dict:
+        """
+        Main extraction method - uses NLP first, LLM for gaps
+        """
+        # Phase 1: Quick NLP extraction
+        nlp_attributes = self._extract_with_nlp(
+            product_data.get('title', ''),
+            product_data.get('description', '')
+        )
+        
+        # Phase 2: OCR from images if provided
+        ocr_text = ""
+        if product_data.get('images'):
+            ocr_text = self._extract_text_from_images(product_data['images'])
+            if ocr_text:
+                ocr_attributes = self._extract_with_nlp("", ocr_text)
+                nlp_attributes = self._merge_attributes(nlp_attributes, ocr_attributes)
+        
+        # Phase 3: Always call LLM to enrich and validate NLP results
+        llm_attributes = self._extract_with_llm(
+            product_data,
+            nlp_attributes,
+            ocr_text
+        )
+        final_attributes = self._merge_attributes(nlp_attributes, llm_attributes)
+        
+        return final_attributes
+    
+    def _extract_with_nlp(self, title: str, description: str) -> Dict:
+        """
+        Fast extraction using regex and spaCy
+        """
+        text = f"{title} {description}".lower()
+        attributes = defaultdict(list)
+        
+        # Pattern matching for structured attributes
+        for attr_type, patterns in self.patterns.items():
+            for pattern in patterns:
+                matches = re.finditer(pattern, text, re.IGNORECASE)
+                for match in matches:
+                    value = match.group(1) if match.groups() else match.group(0)
+                    attributes[attr_type].append(value.strip())
+        
+        # Named Entity Recognition for brands, organizations
+        doc = self.nlp(title + " " + description)
+        for ent in doc.ents:
+            if ent.label_ == "ORG" and 'brand' not in attributes:
+                attributes['brand'].append(ent.text)
+            elif ent.label_ == "PRODUCT":
+                attributes['product_type'].append(ent.text)
+            elif ent.label_ == "MONEY":
+                attributes['price'].append(ent.text)
+        
+        # Deduplicate and clean
+        cleaned_attributes = {}
+        for key, values in attributes.items():
+            if values:
+                # Take most common or first occurrence
+                cleaned_attributes[key] = list(set(values))[0] if len(set(values)) == 1 else values
+                cleaned_attributes[f'{key}_confidence'] = 0.8 if len(set(values)) == 1 else 0.5
+        
+        return cleaned_attributes
+    
+    def _extract_text_from_images(self, image_paths: List[str]) -> str:
+        """
+        Extract text from product images using OCR
+        """
+        extracted_text = []
+        
+        for img_path in image_paths[:3]:  # Limit to 3 images
+            try:
+                img = Image.open(img_path)
+                text = pytesseract.image_to_string(img)
+                if text.strip():
+                    extracted_text.append(text.strip())
+            except Exception as e:
+                print(f"OCR error for {img_path}: {e}")
+        
+        return " ".join(extracted_text)
+    
+    def _needs_llm_extraction(self, attributes: Dict, product_data: Dict) -> bool:
+        """
+        Determine if LLM extraction is needed based on confidence and completeness
+        """
+        # Check if critical attributes are missing
+        critical_attrs = ['category', 'brand', 'color', 'size']
+        missing_critical = any(attr not in attributes for attr in critical_attrs)
+        
+        # Check confidence levels
+        low_confidence = any(
+            attributes.get(f'{key}_confidence', 0) < self.confidence_threshold
+            for key in attributes.keys() if not key.endswith('_confidence')
+        )
+        
+        # Check if description is complex/unstructured
+        description = product_data.get('description', '')
+        is_complex = len(description.split()) > 100 or 'features' in description.lower()
+        
+        return missing_critical or low_confidence or is_complex
+    
+    def _extract_with_llm(self, product_data: Dict, existing_attrs: Dict, ocr_text: str) -> Dict:
+        """
+        Use LLM to extract comprehensive attributes and validate NLP results
+        """
+        prompt = f"""Analyze this product and extract ALL possible attributes with high accuracy.
+
+Title: {product_data.get('title', 'N/A')}
+Description: {product_data.get('description', 'N/A')}
+Short Description: {product_data.get('short_description', 'N/A')}
+Text from images (OCR): {ocr_text if ocr_text else 'N/A'}
+
+NLP Pre-extracted attributes (validate and enhance): {existing_attrs}
+
+Extract a comprehensive JSON object with these fields (include all that apply):
+
+**Basic Info:**
+- category: specific product category/type
+- subcategory: more specific classification
+- brand: brand name
+- model: model number/name
+- product_line: product series/collection
+
+**Physical Attributes:**
+- color: all colors (list if multiple)
+- size: size information (with units)
+- dimensions: length/width/height with units
+- weight: weight with units
+- material: materials used (list all)
+- finish: surface finish/texture
+
+**Technical Specs (if applicable):**
+- specifications: key technical specs as object
+- compatibility: what it works with
+- capacity: storage/volume capacity
+- power: power requirements/battery info
+
+**Commercial Info:**
+- condition: new/used/refurbished
+- warranty: warranty information
+- country_of_origin: manufacturing country
+- certifications: safety/quality certifications
+
+**Descriptive:**
+- key_features: list of 5-8 main features
+- benefits: main benefits/use cases
+- target_audience: who this is for
+- usage_instructions: how to use (if mentioned)
+- care_instructions: care/maintenance info
+- style: style/aesthetic (modern, vintage, etc)
+- season: seasonal relevance (if applicable)
+- occasion: suitable occasions (if applicable)
+
+**Additional:**
+- package_contents: what's included
+- variants: available variants/options
+- tags: relevant search tags (list)
+
+Only include fields where you have high confidence. Use null for uncertain values.
+For lists, provide all relevant items. Be thorough and extract every possible detail."""
+
+        content = [{"type": "text", "text": prompt}]
+        
+        # Add images if available
+        if product_data.get('images'):
+            for img_path in product_data['images'][:3]:  # Include up to 3 images for better context
+                try:
+                    with open(img_path, 'rb') as f:
+                        img_data = base64.b64encode(f.read()).decode()
+                    
+                    # Determine media type
+                    media_type = "image/jpeg"
+                    if img_path.lower().endswith('.png'):
+                        media_type = "image/png"
+                    elif img_path.lower().endswith('.webp'):
+                        media_type = "image/webp"
+                    
+                    content.append({
+                        "type": "image",
+                        "source": {
+                            "type": "base64",
+                            "media_type": media_type,
+                            "data": img_data
+                        }
+                    })
+                except Exception as e:
+                    print(f"Error processing image {img_path}: {e}")
+        
+        try:
+            response = self.client.messages.create(
+                model="claude-sonnet-4-20250514",
+                max_tokens=2048,  # Increased for comprehensive extraction
+                messages=[{"role": "user", "content": content}]
+            )
+            
+            # Parse JSON response
+            import json
+            llm_result = json.loads(response.content[0].text)
+            
+            # Add high confidence to LLM results
+            for key in llm_result:
+                if llm_result[key] is not None:
+                    llm_result[f'{key}_confidence'] = 0.95
+            
+            return llm_result
+        
+        except Exception as e:
+            print(f"LLM extraction error: {e}")
+            return {}
+    
+    def _identify_missing_attributes(self, existing_attrs: Dict) -> List[str]:
+        """
+        Identify which attributes are missing or low confidence
+        """
+        important_attrs = ['category', 'brand', 'color', 'size', 'material', 'key_features']
+        missing = []
+        
+        for attr in important_attrs:
+            if attr not in existing_attrs or existing_attrs.get(f'{attr}_confidence', 0) < 0.7:
+                missing.append(attr)
+        
+        return missing
+    
+    def _merge_attributes(self, base: Dict, additional: Dict) -> Dict:
+        """
+        Intelligently merge attributes, preferring LLM for new attributes and validation
+        """
+        merged = {}
+        
+        # Start with all NLP attributes
+        for key, value in base.items():
+            if not key.endswith('_confidence'):
+                merged[key] = value
+                merged[f'{key}_confidence'] = base.get(f'{key}_confidence', 0.7)
+        
+        # Add or override with LLM attributes
+        for key, value in additional.items():
+            if key.endswith('_confidence'):
+                continue
+            
+            if value is None:
+                # Keep NLP value if LLM returns null
+                continue
+            
+            # LLM found new attribute or better value
+            if key not in merged:
+                merged[key] = value
+                merged[f'{key}_confidence'] = additional.get(f'{key}_confidence', 0.95)
+            else:
+                # Compare values - if different, prefer LLM but mark for review
+                llm_conf = additional.get(f'{key}_confidence', 0.95)
+                nlp_conf = merged.get(f'{key}_confidence', 0.7)
+                
+                if str(value).lower() != str(merged[key]).lower():
+                    # Values differ - use LLM but add conflict flag
+                    merged[key] = value
+                    merged[f'{key}_confidence'] = llm_conf
+                    merged[f'{key}_nlp_value'] = base.get(key)  # Store NLP value for reference
+                    merged[f'{key}_conflict'] = True
+                else:
+                    # Values match - boost confidence
+                    merged[key] = value
+                    merged[f'{key}_confidence'] = min(0.99, (llm_conf + nlp_conf) / 2 + 0.1)
+        
+        return merged
+
+
+# Example usage
+if __name__ == "__main__":
+    extractor = HybridAttributeExtractor(anthropic_api_key="your-api-key")
+    
+    product = {
+        'title': 'Nike Air Max 270 Running Shoes - Black/White',
+        'description': 'Premium running shoes with Max Air cushioning. Breathable mesh upper, rubber outsole. Perfect for daily training.',
+        'images': ['path/to/image1.jpg', 'path/to/image2.jpg']
+    }
+    
+    attributes = extractor.extract_attributes(product)
+    print(attributes)

+ 78 - 0
attr_extraction/tasks.py

@@ -0,0 +1,78 @@
+# tasks.py
+from celery import shared_task
+from django.core.cache import cache
+from .models import Product, ProductAttribute
+from .services.attribute_extractor import HybridAttributeExtractor
+import json
+import hashlib
+
+@shared_task(bind=True, max_retries=3)
+def extract_product_attributes(self, product_id: int):
+    """
+    Celery task to extract attributes from a product
+    """
+    try:
+        product = Product.objects.get(id=product_id)
+        
+        # Check cache first
+        cache_key = f"product_attrs_{product.id}_{product.updated_at.timestamp()}"
+        cached_attrs = cache.get(cache_key)
+        
+        if cached_attrs:
+            return cached_attrs
+        
+        # Prepare product data
+        product_data = {
+            'title': product.title,
+            'description': product.description,
+            'short_description': product.short_description,
+            'images': [img.image.path for img in product.images.all()]
+        }
+        
+        # Extract attributes
+        extractor = HybridAttributeExtractor(
+            anthropic_api_key=settings.ANTHROPIC_API_KEY
+        )
+        attributes = extractor.extract_attributes(product_data)
+        
+        # Save to database
+        for attr_name, attr_value in attributes.items():
+            if not attr_name.endswith('_confidence'):
+                confidence = attributes.get(f'{attr_name}_confidence', 0.5)
+                
+                ProductAttribute.objects.update_or_create(
+                    product=product,
+                    attribute_name=attr_name,
+                    defaults={
+                        'attribute_value': json.dumps(attr_value) if isinstance(attr_value, (list, dict)) else str(attr_value),
+                        'confidence_score': confidence,
+                        'extraction_method': 'hybrid'
+                    }
+                )
+        
+        # Cache for 24 hours
+        cache.set(cache_key, attributes, 86400)
+        
+        # Update product status
+        product.attributes_extracted = True
+        product.save()
+        
+        return attributes
+        
+    except Product.DoesNotExist:
+        return {'error': 'Product not found'}
+    except Exception as e:
+        # Retry with exponential backoff
+        raise self.retry(exc=e, countdown=60 * (2 ** self.request.retries))
+
+
+@shared_task
+def batch_extract_attributes(product_ids: list):
+    """
+    Process multiple products in batch
+    """
+    results = {}
+    for product_id in product_ids:
+        result = extract_product_attributes.delay(product_id)
+        results[product_id] = result.id
+    return results

+ 3 - 0
attr_extraction/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 15 - 0
attr_extraction/urls.py

@@ -0,0 +1,15 @@
+from django.urls import path
+from .views import (
+    ExtractAttributesView, 
+    BatchExtractAttributesView,
+    ProductAttributesView,
+    AttributeReviewView
+)
+
+urlpatterns = [
+    path('products/<int:product_id>/extract/', ExtractAttributesView.as_view()),
+    path('products/batch-extract/', BatchExtractAttributesView.as_view()),
+    path('products/<int:product_id>/attributes/', ProductAttributesView.as_view()),
+    path('attributes/review/', AttributeReviewView.as_view()),
+    path('attributes/<int:attribute_id>/review/', AttributeReviewView.as_view()),
+]

+ 99 - 0
attr_extraction/views.py

@@ -0,0 +1,99 @@
+from django.shortcuts import render
+
+# Create your views here.
+# views.py
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status
+from .tasks import extract_product_attributes, batch_extract_attributes
+from .models import Product, ProductAttribute
+from .serializers import ProductAttributeSerializer
+
+class ExtractAttributesView(APIView):
+    """
+    Trigger attribute extraction for a product
+    """
+    def post(self, request, product_id):
+        try:
+            product = Product.objects.get(id=product_id)
+            
+            # Trigger async task
+            task = extract_product_attributes.delay(product_id)
+            
+            return Response({
+                'message': 'Extraction started',
+                'task_id': task.id,
+                'product_id': product_id
+            }, status=status.HTTP_202_ACCEPTED)
+            
+        except Product.DoesNotExist:
+            return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND)
+
+
+class BatchExtractAttributesView(APIView):
+    """
+    Trigger batch extraction
+    """
+    def post(self, request):
+        product_ids = request.data.get('product_ids', [])
+        
+        if not product_ids:
+            return Response({'error': 'No product IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
+        
+        task_results = batch_extract_attributes.delay(product_ids)
+        
+        return Response({
+            'message': f'Batch extraction started for {len(product_ids)} products',
+            'task_id': task_results.id
+        }, status=status.HTTP_202_ACCEPTED)
+
+
+class ProductAttributesView(APIView):
+    """
+    Get extracted attributes for a product
+    """
+    def get(self, request, product_id):
+        try:
+            product = Product.objects.get(id=product_id)
+            attributes = ProductAttribute.objects.filter(product=product)
+            
+            serializer = ProductAttributeSerializer(attributes, many=True)
+            
+            return Response({
+                'product_id': product_id,
+                'attributes_extracted': product.attributes_extracted,
+                'attributes': serializer.data
+            })
+            
+        except Product.DoesNotExist:
+            return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND)
+
+
+class AttributeReviewView(APIView):
+    """
+    Review and update low-confidence attributes
+    """
+    def get(self, request):
+        # Get attributes needing review
+        attributes = ProductAttribute.objects.filter(
+            needs_review=True,
+            reviewed=False
+        ).select_related('product')[:50]
+        
+        serializer = ProductAttributeSerializer(attributes, many=True)
+        return Response(serializer.data)
+    
+    def patch(self, request, attribute_id):
+        try:
+            attribute = ProductAttribute.objects.get(id=attribute_id)
+            
+            # Update attribute
+            attribute.attribute_value = request.data.get('attribute_value', attribute.attribute_value)
+            attribute.reviewed = True
+            attribute.confidence_score = 1.0  # Human verified
+            attribute.save()
+            
+            return Response({'message': 'Attribute updated'})
+            
+        except ProductAttribute.DoesNotExist:
+            return Response({'error': 'Attribute not found'}, status=status.HTTP_404_NOT_FOUND)

+ 30 - 0
celery.py

@@ -0,0 +1,30 @@
+# celery.py (in your project root)
+import os
+from celery import Celery
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings')
+
+app = Celery('your_project')
+app.config_from_object('django.conf:settings', namespace='CELERY')
+app.autodiscover_tasks()
+
+
+# settings.py additions
+CELERY_BROKER_URL = 'redis://localhost:6379/0'
+CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
+CELERY_TASK_SERIALIZER = 'json'
+CELERY_ACCEPT_CONTENT = ['json']
+CELERY_RESULT_SERIALIZER = 'json'
+CELERY_TIMEZONE = 'UTC'
+
+CACHES = {
+    'default': {
+        'BACKEND': 'django_redis.cache.RedisCache',
+        'LOCATION': 'redis://127.0.0.1:6379/1',
+        'OPTIONS': {
+            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
+        }
+    }
+}
+
+ANTHROPIC_API_KEY = os.environ.get('ANTHROPIC_API_KEY')

+ 28 - 2
content_quality_tool/settings.py

@@ -16,6 +16,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
 # SECURITY WARNING: keep the secret key used in production secret!
 SECRET_KEY = 'django-insecure-$6far8v=798or1wru24=zq&k*9&frm+dk%c!*w!a4wfb#z1_+3'
 # SECURITY WARNING: don't run with debug turned on in production!
+MINIMAX_API_KEY = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiJBYmh5dWRheSBBbmR1Z3VsYSIsIlVzZXJOYW1lIjoiQWJoeXVkYXkgQW5kdWd1bGEiLCJBY2NvdW50IjoiIiwiU3ViamVjdElEIjoiMTk3NjIwMTUxODMxOTE0OTMxMyIsIlBob25lIjoiIiwiR3JvdXBJRCI6IjE5NzYyMDE1MTgzMTA3NTY3NzUiLCJQYWdlTmFtZSI6IiIsIk1haWwiOiJhYmh5dWRheWFuZHVndWxhMjEzQGdtYWlsLmNvbSIsIkNyZWF0ZVRpbWUiOiIyMDI1LTEwLTA5IDIxOjAyOjE0IiwiVG9rZW5UeXBlIjoxLCJpc3MiOiJtaW5pbWF4In0.Dw6ug5FCzz_E0MOoBQ4fNN-ksaCJrwfscP5_fpmL7nxhlPJraoDHDFiznoqd5skLtIOKgsLsMNJMcutXyKTBxqqaKVGgKesZ_0JU8bAPwuPqe0MJ7ko3sZ0i858lFIqi8vH2rfgwqvvu9np2a2pQh2zvD-7LAZG4xCwcMHY7_19037s5EgONPYP7Lc_5caiF4DmNR4u7U4cunFfwGEGydpNSBP2xlk_dCRblsRNZFp-l9IkXtgi9EGOCzophhJvP8YiTMVZ2vtitt4c7YACqxtAjXT9774p299CqHAuuHcEv3MXiv0f1Zp4ERkfbsjYawYIewlfPgyw3bmFdm1py_Q'
 DEBUG = True
 ALLOWED_HOSTS = ['*', '172.29.7.103']
 # Application definition
@@ -28,6 +29,7 @@ INSTALLED_APPS = [
     'django.contrib.staticfiles',
     'core',
     'rest_framework',
+    'attr_extraction',
 ]
 MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
@@ -43,7 +45,7 @@ ROOT_URLCONF = 'content_quality_tool.urls'
 TEMPLATES = [
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [BASE_DIR / "templates", BASE_DIR/ 'content_quality_tool_public/templates'],
+        'DIRS': [BASE_DIR / "templates", BASE_DIR/ 'content_quality_tool_public/templates', BASE_DIR/ 'video_generator/templates'],
         'APP_DIRS': True,
         'OPTIONS': {
             'context_processors': [
@@ -90,6 +92,7 @@ USE_TZ = True
 STATIC_URL = 'static/'
 STATICFILES_DIRS = [
     os.path.join(BASE_DIR, "content_quality_tool_public/static"),
+    os.path.join(BASE_DIR, "video_generator/static")
 ]
 # Default primary key field type
 # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
@@ -110,4 +113,27 @@ MESSAGE_TAGS = {
     messages.INFO: 'info',
     messages.WARNING: 'warning',
     messages.DEBUG: 'debug',
-}
+}
+
+
+
+
+# settings.py additions
+CELERY_BROKER_URL = 'redis://localhost:6379/0'
+CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
+CELERY_TASK_SERIALIZER = 'json'
+CELERY_ACCEPT_CONTENT = ['json']
+CELERY_RESULT_SERIALIZER = 'json'
+CELERY_TIMEZONE = 'UTC'
+
+CACHES = {
+    'default': {
+        'BACKEND': 'django_redis.cache.RedisCache',
+        'LOCATION': 'redis://127.0.0.1:6379/1',
+        'OPTIONS': {
+            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
+        }
+    }
+}
+
+ANTHROPIC_API_KEY = os.environ.get('ANTHROPIC_API_KEY')

+ 4 - 2
content_quality_tool/urls.py

@@ -17,13 +17,15 @@ Including another URLconf
 from django.contrib import admin
 from django.urls import path
 from django.urls import path, include
-from content_quality_tool_public import views
+from content_quality_tool_public import views as content_views
+from video_generator import views as video_views
 # from template import views
 
 urlpatterns = [
     path('admin/', admin.site.urls),
-    path("", views.login_view, name="login_view"),
+    path("", content_views.login_view, name="login_view"),
     path('', include('content_quality_tool_public.urls')),  # Your app's routes
+    path('video/', include('video_generator.urls')),  # Your app's routes
 
     # api url
     path("core/", include("core.urls")),

+ 112 - 0
content_quality_tool_public/templates/attr-extraction.html

@@ -0,0 +1,112 @@
+{% load static %}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Attribute Extraction</title>
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3@5.0.12/index.css"
+        integrity="sha256-tXJfXfp6Ewt1ilPzLDtQnJV4hclT9XuaZUKyUvmyr+Q=" crossorigin="anonymous">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/styles/overlayscrollbars.min.css"
+        integrity="sha256-dSokZseQNT08wYEWiz5iLI8QPlKxG+TswNRD8k35cpg=" crossorigin="anonymous">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.min.css"
+        integrity="sha256-Qsx5lrStHZyR9REqhUF8iQt73X06c8LGIUPzpOhwRrI=" crossorigin="anonymous">
+  <link rel="stylesheet" href="{% static './css/adminlte.css' %}">
+  <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+  <link rel="stylesheet" href="{% static './css/select2-bootstrap4.min.css' %}">
+  <link rel="stylesheet" href="{% static 'css/custom.css' %}">
+
+</head>
+<body class="layout-fixed sidebar-expand-lg sidebar-mini app-loaded sidebar-collapse">
+  <div class="app-wrapper"> <!--begin::Header-->
+    {% include 'header.html' %}
+    {% include 'sidebar.html' %}
+    <main class="app-main"> <!--begin::App Content Header-->
+      <div class="app-content-header"> <!--begin::Container-->
+          <div class="container-fluid"> <!--begin::Row-->
+              <div class="row">
+                  <div class="col-sm-6">
+                      <h3 class="mb-0">⛏️ Attribute Extraction</h3>
+                  </div>
+                  <div class="col-sm-6">
+                      <ol class="breadcrumb float-sm-end">
+                          <li class="breadcrumb-item"><a href="{% url 'file-upload' %}">Home</a></li>
+                          <li class="breadcrumb-item active" aria-current="page"><a href="{% url 'generate-video' %}"></a>
+                              ⛏️ Attribute Extraction</a>
+                          </li>
+                      </ol>
+                  </div>
+              </div> <!--end::Row-->
+          </div> <!--end::Container-->
+      </div>
+      <div class="app-content-header"> <!--begin::Container-->
+          <div class="container-fluid "> <!--begin::Row-->
+            
+          </div>
+      </div>
+    </main>
+
+    <!-- Video Modal -->
+    <div class="modal fade" id="videoModal" tabindex="-1" aria-hidden="true">
+      <div class="modal-dialog modal-dialog-centered modal-lg">
+        <div class="modal-content">
+          <div class="modal-header">
+            <h5 class="modal-title">🎉 Your Video is Ready!</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+          </div>
+          <div class="modal-body">
+            <video id="outputVideo" class="w-100" controls></video>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    {% include 'footer.html' %}
+  </div>
+
+  <script src="https://code.jquery.com/jquery-3.7.1.min.js"
+        integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
+    <script src="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/browser/overlayscrollbars.browser.es6.min.js"
+        integrity="sha256-H2VM7BKda+v2Z4+DRy69uknwxjyDRhszjXFhsL4gD3w=" crossorigin="anonymous"></script>
+    <!--end::Third Party Plugin(OverlayScrollbars)--><!--begin::Required Plugin(popperjs for Bootstrap 5)-->
+    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
+        integrity="sha256-whL0tQWoY1Ku1iskqPFvmZ+CHsvmRWx/PIoEvIeWh4I=" crossorigin="anonymous"></script>
+    <!--end::Required Plugin(popperjs for Bootstrap 5)--><!--begin::Required Plugin(Bootstrap 5)-->
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"
+        integrity="sha256-YMa+wAM6QkVyz999odX7lPRxkoYAan8suedu4k2Zur8=" crossorigin="anonymous"></script>
+    <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
+    <!--end::Required Plugin(Bootstrap 5)--><!--begin::Required Plugin(AdminLTE)-->
+    <script src="{% static './js/adminlte.js' %}"></script>
+    <script>
+        const SELECTOR_SIDEBAR_WRAPPER = ".sidebar-wrapper";
+        const Default = {
+            scrollbarTheme: "os-theme-light",
+            scrollbarAutoHide: "leave",
+            scrollbarClickScroll: true,
+        };
+        document.addEventListener("DOMContentLoaded", function () {
+            const sidebarWrapper = document.querySelector(SELECTOR_SIDEBAR_WRAPPER);
+            if (
+                sidebarWrapper &&
+                typeof OverlayScrollbarsGlobal?.OverlayScrollbars !== "undefined"
+            ) {
+                OverlayScrollbarsGlobal.OverlayScrollbars(sidebarWrapper, {
+                    scrollbars: {
+                        theme: Default.scrollbarTheme,
+                        autoHide: Default.scrollbarAutoHide,
+                        clickScroll: Default.scrollbarClickScroll,
+                    },
+                });
+            }
+        });
+        $(document).ready(function () {
+            $('.select2').select2({
+                theme: 'bootstrap4',
+                placeholder: 'Select Competitors'
+            });
+        });
+    </script> 
+    
+</body>
+</html>

+ 8 - 0
content_quality_tool_public/templates/footer.html

@@ -0,0 +1,8 @@
+<footer class="app-footer">
+    <strong>
+        Copyright &copy; 2014-2025&nbsp;
+        <a href="https://www.luminadatamatics.com/" target="_blank" class="text-decoration-none">Lumina
+            Datamatics LTD</a>.
+    </strong>
+    All rights reserved.
+</footer> 

+ 85 - 134
content_quality_tool_public/templates/get-data.html

@@ -4,14 +4,9 @@
 
 <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-    <title>Dashboard</title><!--begin::Primary Meta Tags-->
+    <title>Tool Check</title><!--begin::Primary Meta Tags-->
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta name="title" content="AdminLTE | Dashboard v2">
-    <meta name="author" content="ColorlibHQ">
-    <meta name="description"
-        content="AdminLTE is a Free Bootstrap 5 Admin Dashboard, 30 example pages using Vanilla JS.">
-    <meta name="keywords"
-        content="bootstrap 5, bootstrap, bootstrap 5 admin dashboard, bootstrap 5 dashboard, bootstrap 5 charts, bootstrap 5 calendar, bootstrap 5 datepicker, bootstrap 5 tables, bootstrap 5 datatable, vanilla js datatable, colorlibhq, colorlibhq dashboard, colorlibhq admin dashboard">
+    <meta name="title" content="CQT | GET DATA">
     <!--end::Primary Meta Tags--><!--begin::Fonts-->
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3@5.0.12/index.css"
         integrity="sha256-tXJfXfp6Ewt1ilPzLDtQnJV4hclT9XuaZUKyUvmyr+Q=" crossorigin="anonymous">
@@ -166,103 +161,20 @@
 
 <body class="layout-fixed sidebar-expand-lg sidebar-mini app-loaded sidebar-collapse"> <!--begin::App Wrapper-->
     <div class="app-wrapper"> <!--begin::Header-->
-        <nav class="app-header navbar navbar-expand bg-body"> <!--begin::Container-->
-            <div class="container-fluid"> <!--begin::Start Navbar Links-->
-                <ul class="navbar-nav">
-                    <li class="nav-item"> <a class="nav-link" data-lte-toggle="sidebar" href="#" role="button"> <i
-                                class="bi bi-list"></i> </a> </li>
-                    <li class="nav-item d-none d-md-block"> <a  href="{% url 'file-upload' %}" class="nav-link">Home</a> </li>
-                    <!-- <li class="nav-item d-none d-md-block"> <a href="#" class="nav-link">Contact</a> </li> -->
-                </ul> <!--end::Start Navbar Links--> <!--begin::End Navbar Links-->
-                <ul class="navbar-nav ms-auto"> <!--begin::Navbar Search-->
-                    <!-- <li class="nav-item"> <a class="nav-link" data-widget="navbar-search" href="search.html"
-                            role="button"> <i class="bi bi-search"></i> </a> </li> 
-
-                    <li class="nav-item dropdown"> <a class="nav-link" data-bs-toggle="dropdown" href="#"> <i
-                                class="bi bi-bell-fill"></i> <span class="navbar-badge badge text-bg-warning">15</span>
-                        </a>
-                        <div class="dropdown-menu dropdown-menu-lg dropdown-menu-end"> <span
-                                class="dropdown-item dropdown-header">3 Notifications</span>
-                            <div class="dropdown-divider"></div> <a href="#" class="dropdown-item"> <i
-                                    class="bi bi-people-fill me-2"></i> filename_22_09_2024.xml is ready for review.
-                                <span class="float-end text-secondary fs-7">1 hours</span> </a>
-                            <div class="dropdown-divider"></div> <a href="#" class="dropdown-item"> <i
-                                    class="bi bi-people-fill me-2"></i> filename_22_09_2024.xml is uploaded
-                                successfully.
-                                <span class="float-end text-secondary fs-7">12 hours</span> </a>
-                            <div class="dropdown-divider"></div> <a href="#" class="dropdown-item"> <i
-                                    class="bi bi-file-earmark-fill me-2"></i> filename_12_09_2024.xml is uploaded
-                                successfully.
-                                <span class="float-end text-secondary fs-7">2 days</span> </a>
-                            <div class="dropdown-divider"></div> <a href="#" class="dropdown-item dropdown-footer">
-                                See All Notifications
-                            </a>
-                        </div>
-                    </li>  -->
-                    <li class="nav-item"> <a class="nav-link" href="#" data-lte-toggle="fullscreen"> <i
-                                data-lte-icon="maximize" class="bi bi-arrows-fullscreen"></i> <i
-                                data-lte-icon="minimize" class="bi bi-fullscreen-exit" style="display: none;"></i> </a>
-                    </li> <!--end::Fullscreen Toggle--> <!--begin::User Menu Dropdown-->
-                    <li class="nav-item dropdown user-menu"> <a href="#" class="nav-link dropdown-toggle"
-                            data-bs-toggle="dropdown"> <img src="{% static './images/user2-160x160.jpg' %}"
-                                class="user-image rounded-circle shadow" alt="User Image"> <span
-                                class="d-none d-md-inline">{{ request.session.user_email }}</span> </a>
-                        <ul class="dropdown-menu dropdown-menu-lg dropdown-menu-end"> <!--begin::User Image-->
-                            <li class="user-header text-bg-secondary"> <img src="{% static './images/user2-160x160.jpg' %}"
-                                    class="rounded-circle shadow" alt="User Image">
-                                <p>
-                                    {{ request.session.user_email }} - Admin
-                                    <!-- <small>Since Nov. 2023</small> -->
-                                </p>
-                            </li> <!--end::User Image--> <!--begin::Menu Body-->
-
-                            <li class="user-footer"> <a href="#" class="btn btn-default btn-flat">Profile</a> <a
-                                    href="{% url 'logout' %}" class="btn btn-default btn-flat float-end">Sign out</a> </li>
-                            <!--end::Menu Footer-->
-                        </ul>
-                    </li> <!--end::User Menu Dropdown-->
-                </ul> <!--end::End Navbar Links-->
-            </div> <!--end::Container-->
-        </nav> <!--end::Header--> <!--begin::Sidebar-->
-        <aside class="app-sidebar shadow"> <!--begin::Sidebar Brand-->
-            <div class="sidebar-brand"> <!--begin::Brand Link--> <a href="{% url 'file-upload' %}" class="brand-link">
-                    <!--begin::Brand Image--> <img src="{% static './images/logo-mini.png' %}" alt="Lumina Datamatics"
-                        class="brand-image logo-mini"> <!--end::Brand Image--> <!--begin::Brand Text--> <span
-                        class="brand-text fw-light"><img style="position:relative; left: -40px;"
-                            src="{% static './images/logo.png' %}" alt="Lumina Datamatics" class="brand-image"></span>
-                    <!--end::Brand Text--> </a>
-            </div> <!--end::Sidebar Brand--> <!--begin::Sidebar Wrapper-->
-            <div class="sidebar-wrapper">
-                <nav class="mt-2"> <!--begin::Sidebar Menu-->
-                    <ul class="nav sidebar-menu flex-column" data-lte-toggle="treeview" role="menu"
-                        data-accordion="false">
-
-                        <li class="nav-item"> <a href="{% url 'file-upload' %}" class="nav-link"> <i
-                                    class="nav-icon bi bi-upload"></i>
-                                <p>Upload</p>
-                            </a> </li>
-                        <li class="nav-item"> <a href="{% url 'tool-check' %}" class="nav-link active"> <i
-                                    class="nav-icon bi bi-house"></i>
-                                <p>Home</p>
-                            </a> </li>    
-                        
-
-                    </ul> <!--end::Sidebar Menu-->
-                </nav>
-            </div> <!--end::Sidebar Wrapper-->
-        </aside> <!--end::Sidebar--> <!--begin::App Main-->
+        {% include 'header.html' %}
+        {% include 'sidebar.html' %}
         <main class="app-main"> <!--begin::App Content Header-->
             <div class="app-content-header"> <!--begin::Container-->
                 <div class="container-fluid"> <!--begin::Row-->
                     <div class="row">
                         <div class="col-sm-6">
-                            <h3 class="mb-0">Dashboard</h3>
+                            <h3 class="mb-0">📑 Dashboard</h3>
                         </div>
                         <div class="col-sm-6">
                             <ol class="breadcrumb float-sm-end">
-                                <li class="breadcrumb-item"><a href="./upload.html">Upload</a></li>
+                                <li class="breadcrumb-item"><a href="{% url 'tool-check' %}">Home</a></li>
                                 <li class="breadcrumb-item active" aria-current="page">
-                                    Dashboard
+                                    📑 Dashboard
                                 </li>
                             </ol>
                         </div>
@@ -295,17 +207,7 @@
 
             </div> <!--end::Container-->
         </main> <!--end::App Main--> <!--begin::Footer-->
-        <footer class="app-footer"> <!--begin::To the end-->
-            <!-- <div class="float-end d-none d-sm-inline">Anything you want</div>--> <!--end::To the end-->
-            <!--begin::Copyright--> <strong>
-                Copyright &copy; 2014-2025&nbsp;
-                <a href="https://www.luminadatamatics.com/" target="_blank" class="text-decoration-none">Lumina
-                    Datamatics
-                    LTD</a>.
-            </strong>
-            All rights reserved.
-            <!--end::Copyright-->
-        </footer> <!--end::Footer-->
+        {% include 'footer.html' %}
     </div> <!--end::App Wrapper--> <!--begin::Script--> <!--begin::Third Party Plugin(OverlayScrollbars)-->
     <script src="https://code.jquery.com/jquery-3.7.1.min.js"
         integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
@@ -374,7 +276,9 @@
 
                         Object.entries(base_keys).forEach(([key, value]) => {
                             let name = key;
+                            
                             key = key.toLowerCase();
+                            console.log("key",key);
                             console.log(element.breakdown[value], value);
                             var per = 0
                             if (element.breakdown[value]) {
@@ -385,38 +289,85 @@
                                 // console.log(element.categorized_feedback[key].issues);
                                 intial_desc = element.categorized_feedback[key].issues.join(', ');
                             }
-                            
-
-                            initial_score += `<tr>
-                                                    <td class="wow bounceInLeft">
-                                                        <a>
-                                                            `+ name + `
-                                                        </a>
-                                                    </td>
-
-                                                    <td class="project_progress wow bounceInRight">
-                                                        <div class="progress progress-sm">
-                                                            <div class="progress-bar bg-green" role="progressbar"
-                                                                aria-valuenow="`+ per + `" aria-valuemin="0" aria-valuemax="100"
-                                                                style="width: `+ per + `%">
+                            if(key == "image"){
+                                // Build the breakdown progress HTML
+                                let breakdownHtml = '';
+
+                                for (const [key, value] of Object.entries(element.image_breakdown)) {
+                                    breakdownHtml += `
+                                        <div style="margin-bottom: 10px;">
+                                            <strong>${key.charAt(0).toUpperCase() + key.slice(1)}</strong>
+                                            <div class="progress progress-sm mt-1">
+                                                <div class="progress-bar bg-success" role="progressbar"
+                                                    aria-valuenow="${value}" aria-valuemin="0" aria-valuemax="100"
+                                                    style="width: ${value}%">
+                                                </div>
+                                            </div>
+                                            <small>${value}%</small>
+                                        </div>
+                                    `;
+                                }
+                                initial_score += `<tr>
+                                                        <td class="wow bounceInLeft">
+                                                            <a>
+                                                                `+ name + `
+                                                            </a>
+                                                        </td>
+
+                                                        <td class="project_progress wow bounceInRight">
+                                                            <div class="progress progress-sm">
+                                                                <div class="progress-bar bg-green" role="progressbar"
+                                                                    aria-valuenow="`+ per + `" aria-valuemin="0" aria-valuemax="100"
+                                                                    style="width: `+ per + `%">
+                                                                </div>
                                                             </div>
+                                                            <small>
+                                                                `+ per + `%
+                                                            </small>
+                                                        </td>
+
+                                                    </tr>
+                                                    <tr>
+                                                    <td class="wow bounceInLeft" colspan='2' data-wow-delay="0.2s">
+                                                        <div style='max-height:100px;overflow-y:auto;'>  
+                                                            `+ breakdownHtml + `
                                                         </div>
+                                                        </td>
+                                                    </tr>`;
+                            
+                            }else{
+                                initial_score += `<tr>
+                                                        <td class="wow bounceInLeft">
+                                                            <a>
+                                                                `+ name + `
+                                                            </a>
+                                                        </td>
+
+                                                        <td class="project_progress wow bounceInRight">
+                                                            <div class="progress progress-sm">
+                                                                <div class="progress-bar bg-green" role="progressbar"
+                                                                    aria-valuenow="`+ per + `" aria-valuemin="0" aria-valuemax="100"
+                                                                    style="width: `+ per + `%">
+                                                                </div>
+                                                            </div>
+                                                            <small>
+                                                                `+ per + `%
+                                                            </small>
+                                                        </td>
+
+                                                    </tr>
+                                                    <tr>
+                                                    <td class="wow bounceInLeft" colspan='2' data-wow-delay="0.2s">
+                                                        <div style='max-height:100px;overflow-y:auto;'>  
                                                         <small>
-                                                            `+ per + `%
+                                                            `+ intial_desc + `
                                                         </small>
-                                                    </td>
-
-                                                </tr>
-                                                <tr>
-                                                <td class="wow bounceInLeft" colspan='2' data-wow-delay="0.2s">
-                                                    <div style='max-height:100px;overflow-y:auto;'>  
-                                                    <small>
-                                                        `+ intial_desc + `
-                                                    </small>
-                                                    </div>
-                                                    </td>
-                                                </tr>`;
-                        })
+                                                        </div>
+                                                        </td>
+                                                    </tr>`;
+                            }                        
+                        });
+                        
 
                         Object.keys(base_keys).forEach(k => {
                             let name = k;

+ 66 - 0
content_quality_tool_public/templates/header.html

@@ -0,0 +1,66 @@
+{% load static %}
+<style>
+    .circle-user-big{
+        display: inline-flex;
+        justify-content: center;
+        align-items: center;
+        background-color: #007bff;
+        color: white;
+        font-size: 66px;
+        width: 100px;  /* You can adjust this as needed */
+        height: 100px; /* You should set width and height to the same value */
+        border-radius: 50%;  /* Makes it a circle */
+        text-align: center;  /* Centers the text if it's multi-line */
+    }
+</style>
+
+<nav class="app-header navbar navbar-expand bg-body"> <!--begin::Container-->
+    <div class="container-fluid"> <!--begin::Start Navbar Links-->
+        <ul class="navbar-nav">
+            <li class="nav-item"> <a class="nav-link" data-lte-toggle="sidebar" href="#" role="button"> <i
+                        class="bi bi-list"></i> </a> </li>
+            <!-- <li class="nav-item d-none d-md-block"> <a  href="{% url 'file-upload' %}" class="nav-link">File Upload</a> 
+            </li>
+            <li class="nav-item d-none d-md-block"> <a  href="{% url 'tool-check' %}" class="nav-link">Home</a> 
+            </li> -->
+        </ul> <!--end::Start Navbar Links--> <!--begin::End Navbar Links-->
+        <ul class="navbar-nav ms-auto"> <!--begin::Navbar Search-->
+            <li class="nav-item"> <a class="nav-link" href="#" data-lte-toggle="fullscreen"> <i
+                        data-lte-icon="maximize" class="bi bi-arrows-fullscreen"></i> <i
+                        data-lte-icon="minimize" class="bi bi-fullscreen-exit" style="display: none;"></i> </a>
+            </li> <!--end::Fullscreen Toggle--> <!--begin::User Menu Dropdown-->
+            <li class="nav-item dropdown user-menu"> <a href="#" class="nav-link dropdown-toggle"
+                    data-bs-toggle="dropdown"> 
+                            {% if request.session.user_email %}
+                                <span class="user-image rounded-circle shadow" class="user-image rounded-circle shadow" style="display: inline-flex; justify-content: center; align-items: center; background-color: #007bff; color: white; font-size: 20px;">
+                                    {{ request.session.user_email|slice:":1"|upper }}
+                                </span>
+                            {% else %}
+                                <img src="{% static 'dist/img/user2-160x160.jpg' %}" class="user-image rounded-circle shadow" alt="User Image">
+                            {% endif %}
+                        <span
+                        class="d-none d-md-inline">{{ request.session.user_email }}</span> </a>
+                <ul class="dropdown-menu dropdown-menu-lg dropdown-menu-end"> <!--begin::User Image-->
+                    <li class="user-header text-bg-secondary">
+                            {% if request.session.user_email %}
+                                <span class="rounded-circle shadow circle-user-big" >
+                                    {{ request.session.user_email|slice:":1"|upper }}
+                                </span>
+                            {% else %}
+                                <img src="{% static 'dist/img/user2-160x160.jpg' %}" class="rounded-circle shadow" alt="User Image">
+                            {% endif %} 
+                        <p>
+                            {{ request.session.user_email }} - Admin
+                            <!-- <small>Since Nov. 2023</small> -->
+                        </p>
+                    </li> <!--end::User Image--> <!--begin::Menu Body-->
+
+                    <li class="user-footer"> <a href="#" class="btn btn-default btn-flat">Profile</a> <a
+                            href="{% url 'logout' %}" class="btn btn-default btn-flat float-end">Sign out</a> </li>
+                    <!--end::Menu Footer-->
+                </ul>
+            </li> <!--end::User Menu Dropdown-->
+        </ul> <!--end::End Navbar Links-->
+    </div> <!--end::Container-->
+</nav> <!--end::Header--> <!--begin::Sidebar-->
+        

+ 41 - 204
content_quality_tool_public/templates/index.html

@@ -6,13 +6,7 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <title>Upload</title><!--begin::Primary Meta Tags-->
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta name="title" content="AdminLTE | Dashboard v2">
-    <meta name="author" content="ColorlibHQ">
-    <meta name="description"
-        content="AdminLTE is a Free Bootstrap 5 Admin Dashboard, 30 example pages using Vanilla JS.">
-    <meta name="keywords"
-        content="bootstrap 5, bootstrap, bootstrap 5 admin dashboard, bootstrap 5 dashboard, bootstrap 5 charts, bootstrap 5 calendar, bootstrap 5 datepicker, bootstrap 5 tables, bootstrap 5 datatable, vanilla js datatable, colorlibhq, colorlibhq dashboard, colorlibhq admin dashboard">
-    <!--end::Primary Meta Tags--><!--begin::Fonts-->
+    <meta name="title" content="CQT | Upload">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3@5.0.12/index.css"
         integrity="sha256-tXJfXfp6Ewt1ilPzLDtQnJV4hclT9XuaZUKyUvmyr+Q=" crossorigin="anonymous">
     <!--end::Fonts--><!--begin::Third Party Plugin(OverlayScrollbars)-->
@@ -34,8 +28,6 @@
             top: 3px;
             font-size: 14px;
         }
-    </style>
-    <style>
         #full-page-loader {
             position: fixed;
             top: 0;
@@ -60,102 +52,20 @@
 <body class="layout-fixed sidebar-expand-lg sidebar-mini app-loaded sidebar-collapse">
     <!--begin::App Wrapper-->
     <div class="app-wrapper"> <!--begin::Header-->
-        <nav class="app-header navbar navbar-expand bg-body"> <!--begin::Container-->
-            <div class="container-fluid"> <!--begin::Start Navbar Links-->
-                <ul class="navbar-nav">
-                    <li class="nav-item"> <a class="nav-link" data-lte-toggle="sidebar" href="#" role="button"> <i
-                                class="bi bi-list"></i> </a> </li>
-                    <li class="nav-item d-none d-md-block"> <a  href="{% url 'file-upload' %}" class="nav-link">Home</a> </li>
-                    <!-- <li class="nav-item d-none d-md-block"> <a href="#" class="nav-link">Contact</a> </li> -->
-                </ul> <!--end::Start Navbar Links--> <!--begin::End Navbar Links-->
-                <ul class="navbar-nav ms-auto"> <!--begin::Navbar Search-->
-                    <!-- <li class="nav-item"> <a class="nav-link" data-widget="navbar-search" href="search.html"
-                            role="button"> <i class="bi bi-search"></i> </a> </li> 
-
-                    <li class="nav-item dropdown"> <a class="nav-link" data-bs-toggle="dropdown" href="#"> <i
-                                class="bi bi-bell-fill"></i> <span class="navbar-badge badge text-bg-warning">15</span>
-                        </a>
-                        <div class="dropdown-menu dropdown-menu-lg dropdown-menu-end"> <span
-                                class="dropdown-item dropdown-header">3 Notifications</span>
-                            <div class="dropdown-divider"></div> <a href="#" class="dropdown-item"> <i
-                                    class="bi bi-people-fill me-2"></i> filename_22_09_2024.xml is ready for review.
-                                <span class="float-end text-secondary fs-7">1 hours</span> </a>
-                            <div class="dropdown-divider"></div> <a href="#" class="dropdown-item"> <i
-                                    class="bi bi-people-fill me-2"></i> filename_22_09_2024.xml is uploaded
-                                successfully.
-                                <span class="float-end text-secondary fs-7">12 hours</span> </a>
-                            <div class="dropdown-divider"></div> <a href="#" class="dropdown-item"> <i
-                                    class="bi bi-file-earmark-fill me-2"></i> filename_12_09_2024.xml is uploaded
-                                successfully.
-                                <span class="float-end text-secondary fs-7">2 days</span> </a>
-                            <div class="dropdown-divider"></div> <a href="#" class="dropdown-item dropdown-footer">
-                                See All Notifications
-                            </a>
-                        </div>
-                    </li>  -->
-                    <li class="nav-item"> <a class="nav-link" href="#" data-lte-toggle="fullscreen"> <i
-                                data-lte-icon="maximize" class="bi bi-arrows-fullscreen"></i> <i
-                                data-lte-icon="minimize" class="bi bi-fullscreen-exit" style="display: none;"></i> </a>
-                    </li> <!--end::Fullscreen Toggle--> <!--begin::User Menu Dropdown-->
-                    <li class="nav-item dropdown user-menu"> <a href="#" class="nav-link dropdown-toggle"
-                            data-bs-toggle="dropdown"> <img src="{% static './images/user2-160x160.jpg' %}"
-                                class="user-image rounded-circle shadow" alt="User Image"> <span
-                                class="d-none d-md-inline">{{ request.session.user_email }}</span> </a>
-                        <ul class="dropdown-menu dropdown-menu-lg dropdown-menu-end"> <!--begin::User Image-->
-                            <li class="user-header text-bg-secondary"> <img src="{% static './images/user2-160x160.jpg' %}"
-                                    class="rounded-circle shadow" alt="User Image">
-                                <p>
-                                    {{ request.session.user_email }} - Admin
-                                    <!-- <small>Since Nov. 2023</small> -->
-                                </p>
-                            </li> <!--end::User Image--> <!--begin::Menu Body-->
-
-                            <li class="user-footer"> <a href="#" class="btn btn-default btn-flat">Profile</a> <a
-                                    href="{% url 'logout' %}" class="btn btn-default btn-flat float-end">Sign out</a> </li>
-                            <!--end::Menu Footer-->
-                        </ul>
-                    </li> <!--end::User Menu Dropdown-->
-                </ul> <!--end::End Navbar Links-->
-            </div> <!--end::Container-->
-        </nav> <!--end::Header--> <!--begin::Sidebar-->
-        <aside class="app-sidebar shadow"> <!--begin::Sidebar Brand-->
-            <div class="sidebar-brand"> <!--begin::Brand Link--> <a href="{% url 'file-upload' %}" class="brand-link">
-                    <!--begin::Brand Image--> <img src="{% static './images/logo-mini.png' %}" alt="Lumina Datamatics"
-                        class="brand-image logo-mini"> <!--end::Brand Image--> <!--begin::Brand Text--> <span
-                        class="brand-text fw-light" style="position:relative; left: -40px;"><img
-                            src="{% static './images/logo.png' %}" alt="Lumina Datamatics" class="brand-image"></span>
-                    <!--end::Brand Text--> </a>
-                <!--end::Brand Link-->
-            </div> <!--end::Sidebar Brand--> <!--begin::Sidebar Wrapper-->
-            <div class="sidebar-wrapper">
-                <nav class="mt-2"> <!--begin::Sidebar Menu-->
-                    <ul class="nav sidebar-menu flex-column" data-lte-toggle="treeview" role="menu"
-                        data-accordion="false">
-                        <li class="nav-item"> <a href="{% url 'file-upload' %}" class="nav-link active"> <i
-                                    class="nav-icon bi bi-upload"></i>
-                                <p>Upload</p>
-                            </a> </li>
-                        <li class="nav-item"> <a href="{% url 'tool-check' %}" class="nav-link"> <i
-                                    class="nav-icon bi bi-house"></i>
-                                <p>Home</p>
-                            </a> </li>    
-                        
-                    </ul> <!--end::Sidebar Menu-->
-                </nav>
-            </div> <!--end::Sidebar Wrapper-->
-        </aside> <!--end::Sidebar--> <!--begin::App Main-->
+        {% include 'header.html' %}
+        {% include 'sidebar.html' %}
         <main class="app-main"> <!--begin::App Content Header-->
             <div class="app-content-header"> <!--begin::Container-->
                 <div class="container-fluid"> <!--begin::Row-->
                     <div class="row">
                         <div class="col-sm-6">
-                            <h3 class="mb-0">Upload</h3>
+                            <h3 class="mb-0">📂 File Upload</h3>
                         </div>
                         <div class="col-sm-6">
                             <ol class="breadcrumb float-sm-end">
                                 <li class="breadcrumb-item"><a href="{% url 'file-upload' %}">Home</a></li>
                                 <li class="breadcrumb-item active" aria-current="page"><a href="{% url 'tool-check' %}"></a>
-                                    Upload</a>
+                                   📂 File Upload</a>
                                 </li>
                             </ol>
                         </div>
@@ -224,17 +134,9 @@
                 </div> <!--end::Container-->
             </div> <!--end::App Content-->
         </main> <!--end::App Main--> <!--begin::Footer-->
-        <footer class="app-footer"> <!--begin::To the end-->
-            <!-- <div class="float-end d-none d-sm-inline">Anything you want</div>--> <!--end::To the end-->
-            <!--begin::Copyright--> <strong>
-                Copyright &copy; 2014-2025&nbsp;
-                <a href="https://www.luminadatamatics.com/" target="_blank" class="text-decoration-none">Lumina
-                    Datamatics LTD</a>.
-            </strong>
-            All rights reserved.
-            <!--end::Copyright-->
-        </footer> <!--end::Footer-->
-    </div> <!--end::App Wrapper--> <!--begin::Script--> <!--begin::Third Party Plugin(OverlayScrollbars)-->
+        {% include 'footer.html' %}
+
+    </div> 
     <script src="https://code.jquery.com/jquery-3.7.1.min.js"
         integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
     <script src="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/browser/overlayscrollbars.browser.es6.min.js"
@@ -277,110 +179,30 @@
                 placeholder: 'Select Competitors'
             });
         });
-
-        //$('#myForm').on('submit', function (e) {
-        //    e.preventDefault(); //stop submit
-
-        //    if ($('#file').val() != "") {
-        //        //Check if checkbox is checked then show modal
-        //        $('#myModal').modal('show');
-        //    }
-        //});
     </script> <!--end::OverlayScrollbars Configure--> <!-- OPTIONAL SCRIPTS --> <!-- apexcharts -->
     <script src="https://cdn.jsdelivr.net/npm/apexcharts@3.37.1/dist/apexcharts.min.js"
         integrity="sha256-+vh8GkaU7C9/wbSLIcwq82tQ2wTf44aOHA8HlBMwRI8=" crossorigin="anonymous"></script>
-    
-    <!-- <script>
-    function enableSubmitButton() {
+    <script>
+        document.addEventListener('DOMContentLoaded', function () {
+        const form = document.getElementById('uploadForm');
         const fileInput = document.getElementById('fileInput');
         const submitBtn = document.getElementById('submitBtn');
+        const responseDiv = document.getElementById('responseMessage');
 
-        if (fileInput.files.length > 0) {
-            submitBtn.disabled = false;
-        } else {
-            submitBtn.disabled = true;
-        }
-    }
-    </script> -->
-
-    <script>
-document.addEventListener('DOMContentLoaded', function () {
-    const form = document.getElementById('uploadForm');
-    const fileInput = document.getElementById('fileInput');
-    const submitBtn = document.getElementById('submitBtn');
-    const responseDiv = document.getElementById('responseMessage');
-
-    // Enable submit button when file is selected
-    fileInput.addEventListener('change', function () {
-        submitBtn.disabled = fileInput.files.length === 0;
-    });
-
-    // Handle form submission
-    form.addEventListener('submit', function (e) {
-        e.preventDefault();
-        $('#full-page-loader').show();
-        // Disable button during upload
-        submitBtn.disabled = true;
-        submitBtn.textContent = 'Uploading...';
-
-        const formData = new FormData(form);
-
-        fetch('/core/api/upload-rules/', {
-            method: 'POST',
-            body: formData,
-            headers: {
-                'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
-            }
-        })
-        .then(response => response.json())
-        .then(data => {
-            if (data.success) {
-                responseDiv.innerHTML = `<div class="alert alert-success">✅ ${data.message}</div>`;
-                fileInput.value = ''; // Clear file input
-                submitBtn.disabled = true; // Keep disabled until new file selected
-                $('#full-page-loader').hide();
-            } else {
-                responseDiv.innerHTML = `<div class="alert alert-danger">❌ ${data.error}</div>`;
-                submitBtn.disabled = false;
-                $('#full-page-loader').hide();
-            }
-        })
-        .catch(error => {
-            responseDiv.innerHTML = `<div class="alert alert-danger">❌ Upload failed: ${error}</div>`;
-            submitBtn.disabled = false;
-            $('#full-page-loader').hide();
-        })
-        .finally(() => {
-            submitBtn.textContent = 'Upload';
-            $('#full-page-loader').hide();
-
-            // Remove message after 5 seconds
-            setTimeout(() => {
-                responseDiv.innerHTML = '';
-                window.location.href = "/tool-check";
-            }, 3000);
-        });
-    });
-});
-</script>
-
-    <!-- <script>
-        function enableSubmitButton() {
-            const fileInput = document.getElementById('fileInput');
-            const submitBtn = document.getElementById('submitBtn');
+        // Enable submit button when file is selected
+        fileInput.addEventListener('change', function () {
             submitBtn.disabled = fileInput.files.length === 0;
-        }
-        
-        document.addEventListener('DOMContentLoaded', function () {
-            
-            const form = document.getElementById('uploadForm');
+        });
 
-            form.addEventListener('submit', function(e) {
-            e.preventDefault(); // prevent default form submission
+        // Handle form submission
+        form.addEventListener('submit', function (e) {
+            e.preventDefault();
+            $('#full-page-loader').show();
+            // Disable button during upload
+            submitBtn.disabled = true;
+            submitBtn.textContent = 'Uploading...';
 
-            const form = e.target;
             const formData = new FormData(form);
-            const responseDiv = document.getElementById('responseMessage');
 
             fetch('/core/api/upload-rules/', {
                 method: 'POST',
@@ -393,18 +215,33 @@ document.addEventListener('DOMContentLoaded', function () {
             .then(data => {
                 if (data.success) {
                     responseDiv.innerHTML = `<div class="alert alert-success">✅ ${data.message}</div>`;
+                    fileInput.value = ''; // Clear file input
+                    submitBtn.disabled = true; // Keep disabled until new file selected
+                    $('#full-page-loader').hide();
                 } else {
                     responseDiv.innerHTML = `<div class="alert alert-danger">❌ ${data.error}</div>`;
+                    submitBtn.disabled = false;
+                    $('#full-page-loader').hide();
                 }
             })
             .catch(error => {
                 responseDiv.innerHTML = `<div class="alert alert-danger">❌ Upload failed: ${error}</div>`;
+                submitBtn.disabled = false;
+                $('#full-page-loader').hide();
+            })
+            .finally(() => {
+                submitBtn.textContent = 'Upload';
+                $('#full-page-loader').hide();
+
+                // Remove message after 5 seconds
+                setTimeout(() => {
+                    responseDiv.innerHTML = '';
+                    window.location.href = "/tool-check";
+                }, 3000);
             });
         });
-                
-        });
-
-        </script> -->
+    });
+</script>
 
 </body><!--end::Body-->
 

+ 37 - 0
content_quality_tool_public/templates/sidebar.html

@@ -0,0 +1,37 @@
+{% load static %}
+<aside class="app-sidebar shadow"> <!--begin::Sidebar Brand-->
+    <div class="sidebar-brand"> <!--begin::Brand Link--> <a href="{% url 'file-upload' %}" class="brand-link">
+            <!--begin::Brand Image--> <img src="{% static './images/logo-mini.png' %}" alt="Lumina Datamatics"
+                class="brand-image logo-mini"> <!--end::Brand Image--> <!--begin::Brand Text--> <span
+                class="brand-text fw-light"><img style="position:relative; left: -40px;"
+                    src="{% static './images/logo.png' %}" alt="Lumina Datamatics" class="brand-image"></span>
+            <!--end::Brand Text--> </a>
+    </div> <!--end::Sidebar Brand--> <!--begin::Sidebar Wrapper-->
+    <div class="sidebar-wrapper">
+        <nav class="mt-2"> <!--begin::Sidebar Menu-->
+            <ul class="nav sidebar-menu flex-column" data-lte-toggle="treeview" role="menu"
+                data-accordion="false">
+                <li class="nav-item"> <a href="{% url 'file-upload' %}" class="nav-link {% if request.path == '/home/'  %}active{% endif %}"> <i
+                            class="nav-icon bi bi-upload"></i>
+                        <p>Upload</p>
+                    </a> </li>
+                <li class="nav-item"> <a href="{% url 'tool-check' %}" class="nav-link {% if request.path == '/tool-check/' %}active{% endif %}"> <i
+                            class="nav-icon bi bi-house"></i>
+                        <p>Home</p>
+                    </a> </li>  
+                <li class="nav-item"> <a href="{% url 'generate-video' %}" class="nav-link {% if request.path == '/video/generate-video/' %}active{% endif %}"> <i
+                            class="nav-icon bi bi-cpu"></i>
+                        <p>Generate Video</p>
+                    </a> </li>    
+                
+                <li class="nav-item"> <a href="{% url 'attribute-extraction' %}" class="nav-link {% if request.path == '/attribute-extraction/' %}active{% endif %}"> <i
+                            class="nav-icon bi bi-scissors"></i>
+                        <p>Attribute Extraction</p>
+                    </a> </li>        
+                
+
+            </ul> <!--end::Sidebar Menu-->
+        </nav>
+    </div> <!--end::Sidebar Wrapper-->
+</aside> <!--end::Sidebar--> <!--begin::App Main-->
+        

+ 1 - 8
content_quality_tool_public/urls.py

@@ -10,14 +10,7 @@ urlpatterns = [
     path('login/', views.logout_view, name='logout'),
     path('home/', views.upload, name='file-upload'),
     path('tool-check/', views.getData, name='tool-check'),
-    
-    # path('upload-rules/', views.as_view(), name='upload-rules'),
-
-
-    
-
-    #Management URLS
-    # path('file-upload/', views.get_company_list, name='fileUpload'),
+    path('attribute-extraction/', views.getAttributeExtraction, name='attribute-extraction'),
 ]
 
 if settings.DEBUG:

+ 6 - 1
content_quality_tool_public/views.py

@@ -96,4 +96,9 @@ def upload(request):
 # get-data page
 @login_required
 def getData(request):
-    return render(request, 'get-data.html')
+    return render(request, 'get-data.html')
+
+# get-data page
+@login_required
+def getAttributeExtraction(request):
+    return render(request, 'attr-extraction.html')

+ 7 - 5
core/services/gemini_service.py

@@ -495,15 +495,17 @@ class GeminiAttributeService:
                     genai.types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: genai.types.HarmBlockThreshold.BLOCK_NONE
                 }
             )
-        except genai.types.GenerationError as e:
-            # Handle specific generation errors
-            print("Generation error:", str(e))
-            return {"error": "Content generation failed", "details": str(e)}
+        # except genai.types.GenerationError as e:
+        #     # Handle specific generation errors
+        #     print("Generation error:", str(e))
+        #     return None
+        #     # return {"error": "Content generation failed", "details": str(e)}
 
         except Exception as e:
             # Catch-all for any other unexpected errors
             print("Unexpected error:", str(e))
-            return {"error": "Unexpected error occurred", "details": str(e)}
+            return None
+            # return {"error": "Unexpected error occurred", "details": str(e)}
         
     def generate_comprehensive_suggestions(
         self,

BIN
media/uploads/wallet.jpg


BIN
media/uploads/wallet_DJy17YO.jpg


BIN
media/uploads/wallet_OnM0xil.jpg


BIN
media/uploads/wallet_Uk6Hy0q.jpg


BIN
media/uploads/wallet_k4xV4eu.jpg


BIN
media/uploads/wallet_oqDJD4Z.jpg


BIN
media/uploads/wallet_pdWTLhA.jpg


BIN
media/uploads/wallet_sz9Dovt.jpg


+ 0 - 0
video_generator/__init__.py


+ 3 - 0
video_generator/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
video_generator/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class VideoGeneratorConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'video_generator'

+ 10 - 0
video_generator/decorators.py

@@ -0,0 +1,10 @@
+from functools import wraps
+from django.shortcuts import redirect
+
+def login_required(view_func):
+    @wraps(view_func)
+    def _wrapped_view(request, *args, **kwargs):
+        if 'user_email' not in request.session:
+            return redirect('login_view')  # Redirect to your login URL name
+        return view_func(request, *args, **kwargs)
+    return _wrapped_view

+ 0 - 0
video_generator/migrations/__init__.py


+ 3 - 0
video_generator/models.py

@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.

+ 378 - 0
video_generator/static/css/upload.css

@@ -0,0 +1,378 @@
+/* * { margin: 0; padding: 0; box-sizing: border-box; } */
+
+.video-generator {
+    font-family: 'Inter', 'Segoe UI', sans-serif;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    min-height: 100vh;
+    overflow-x: hidden;
+    position: relative;
+}
+
+.video-generator::before {
+    content: '';
+    position: absolute;
+    width: 500px;
+    height: 500px;
+    background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
+    border-radius: 50%;
+    top: -200px;
+    right: -200px;
+    animation: float 20s ease-in-out infinite;
+}
+
+.video-generator::after {
+    content: '';
+    position: absolute;
+    width: 400px;
+    height: 400px;
+    background: radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%);
+    border-radius: 50%;
+    bottom: -150px;
+    left: -150px;
+    animation: float 15s ease-in-out infinite reverse;
+}
+
+@keyframes float {
+    0%, 100% { transform: translate(0, 0) rotate(0deg); }
+    33% { transform: translate(30px, -30px) rotate(5deg); }
+    66% { transform: translate(-20px, 20px) rotate(-5deg); }
+}
+
+.container {
+    position: relative;
+    z-index: 1;
+}
+
+.hero-section {
+    text-align: center;
+    padding: 60px 20px 40px;
+    animation: fadeInDown 0.8s ease-out;
+}
+
+@keyframes fadeInDown {
+    from {
+    opacity: 0;
+    transform: translateY(-30px);
+    }
+    to {
+    opacity: 1;
+    transform: translateY(0);
+    }
+}
+
+.hero-title {
+    font-size: 3.5rem;
+    font-weight: 800;
+    background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%);
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+    background-clip: text;
+    margin-bottom: 15px;
+    letter-spacing: -1px;
+}
+
+.hero-subtitle {
+    color: rgba(255,255,255,0.9);
+    font-size: 1.2rem;
+    font-weight: 300;
+    max-width: 600px;
+    margin: 0 auto;
+}
+
+.upload-container {
+    display: flex;
+    justify-content: center;
+    gap: 40px;
+    margin: 50px auto;
+    flex-wrap: wrap;
+    animation: fadeInUp 0.8s ease-out 0.2s both;
+}
+
+@keyframes fadeInUp {
+    from {
+    opacity: 0;
+    transform: translateY(30px);
+    }
+    to {
+    opacity: 1;
+    transform: translateY(0);
+    }
+}
+
+.upload-card {
+    background: rgba(255, 255, 255, 0.1);
+    backdrop-filter: blur(20px);
+    border: 2px solid rgba(255, 255, 255, 0.2);
+    border-radius: 24px;
+    width: 280px;
+    height: 320px;
+    position: relative;
+    cursor: pointer;
+    transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+    overflow: hidden;
+    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+}
+
+.upload-card::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
+    opacity: 0;
+    transition: opacity 0.4s ease;
+}
+
+.upload-card:hover {
+    transform: translateY(-10px) scale(1.02);
+    border-color: rgba(255, 255, 255, 0.4);
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.upload-card:hover::before {
+    opacity: 1;
+}
+
+.upload-card.active {
+    border-color: rgba(255, 255, 255, 0.6);
+    background: rgba(255, 255, 255, 0.15);
+}
+
+.upload-content {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    padding: 20px;
+    position: relative;
+    z-index: 2;
+}
+
+.upload-icon {
+    font-size: 64px;
+    margin-bottom: 20px;
+    animation: pulse 2s ease-in-out infinite;
+    filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2));
+}
+
+@keyframes pulse {
+    0%, 100% { transform: scale(1); }
+    50% { transform: scale(1.05); }
+}
+
+.upload-label {
+    color: white;
+    font-size: 1.4rem;
+    font-weight: 600;
+    margin-bottom: 10px;
+    text-shadow: 0 2px 4px rgba(0,0,0,0.2);
+}
+
+.upload-hint {
+    color: rgba(255,255,255,0.7);
+    font-size: 0.9rem;
+    text-align: center;
+}
+
+input[type="file"] {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    opacity: 0;
+    cursor: pointer;
+    top: 0;
+    left: 0;
+    z-index: 3;
+}
+
+.preview-container {
+    display: none;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    animation: zoomIn 0.4s ease-out;
+}
+
+@keyframes zoomIn {
+    from {
+    opacity: 0;
+    transform: scale(0.8);
+    }
+    to {
+    opacity: 1;
+    transform: scale(1);
+    }
+}
+
+.preview-container.show {
+    display: flex;
+}
+
+.preview-img {
+    width: 200px;
+    height: 200px;
+    object-fit: cover;
+    border-radius: 16px;
+    border: 3px solid rgba(255,255,255,0.3);
+    box-shadow: 0 8px 24px rgba(0,0,0,0.2);
+    margin-bottom: 15px;
+}
+
+.file-name {
+    color: white;
+    font-size: 0.85rem;
+    max-width: 90%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    text-align: center;
+    background: rgba(0,0,0,0.3);
+    padding: 8px 16px;
+    border-radius: 20px;
+    backdrop-filter: blur(10px);
+}
+
+.arrow-container {
+    animation: fadeInUp 0.8s ease-out 0.4s both;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin: 0 20px;
+}
+
+.arrow {
+    font-size: 3rem;
+    color: rgba(255,255,255,0.6);
+    animation: slideRight 1.5s ease-in-out infinite;
+}
+
+@keyframes slideRight {
+    0%, 100% { transform: translateX(0); }
+    50% { transform: translateX(10px); }
+}
+
+.action-section {
+    text-align: center;
+    animation: fadeInUp 0.8s ease-out 0.6s both;
+}
+
+.generate-btn {
+    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+    border: none;
+    color: white;
+    font-size: 1.3rem;
+    font-weight: 700;
+    padding: 18px 60px;
+    border-radius: 50px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    box-shadow: 0 10px 40px rgba(245, 87, 108, 0.4);
+    position: relative;
+    overflow: hidden;
+}
+
+.generate-btn::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: -100%;
+    width: 100%;
+    height: 100%;
+    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
+    transition: left 0.5s ease;
+}
+
+.generate-btn:hover:not(:disabled) {
+    transform: translateY(-3px);
+    box-shadow: 0 15px 50px rgba(245, 87, 108, 0.6);
+}
+
+.generate-btn:hover:not(:disabled)::before {
+    left: 100%;
+}
+
+.generate-btn:active:not(:disabled) {
+    transform: translateY(-1px);
+}
+
+.generate-btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+    box-shadow: 0 5px 20px rgba(245, 87, 108, 0.2);
+}
+
+.generate-btn.processing {
+    background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
+    animation: processingPulse 1.5s ease-in-out infinite;
+}
+
+@keyframes processingPulse {
+    0%, 100% { transform: scale(1); }
+    50% { transform: scale(1.05); }
+}
+
+.modal-content {
+    background: rgba(255, 255, 255, 0.95);
+    backdrop-filter: blur(20px);
+    border: none;
+    border-radius: 24px;
+    overflow: hidden;
+    box-shadow: 0 20px 80px rgba(0,0,0,0.3);
+}
+
+.modal-header {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    border: none;
+    padding: 20px 30px;
+}
+
+.modal-title {
+    font-weight: 700;
+    font-size: 1.5rem;
+}
+
+.btn-close {
+    filter: brightness(0) invert(1);
+    opacity: 0.8;
+}
+
+.btn-close:hover {
+    opacity: 1;
+}
+
+.modal-body {
+    padding: 30px;
+}
+
+#outputVideo {
+    border-radius: 16px;
+    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
+}
+
+.spinner {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    border: 3px solid rgba(255,255,255,0.3);
+    border-radius: 50%;
+    border-top-color: white;
+    animation: spin 0.8s linear infinite;
+    margin-left: 10px;
+    vertical-align: middle;
+}
+
+@keyframes spin {
+    to { transform: rotate(360deg); }
+}
+
+@media (max-width: 768px) {
+    .hero-title { font-size: 2.5rem; }
+    .upload-container { gap: 30px; }
+    .upload-card { width: 240px; height: 280px; }
+    .arrow { display: none; }
+}

BIN
video_generator/static/fonts/aptos-font/Aptos.eot


BIN
video_generator/static/fonts/aptos-font/Aptos.woff


BIN
video_generator/static/fonts/aptos-font/Aptos.woff2


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-black-italic.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-black.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-bold.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-extrabold-italic 2.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-extrabold-italic.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-extrabold.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-italic.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-light-italic.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-light.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos-semibold.ttf


BIN
video_generator/static/fonts/aptos-font/__MACOSX/._aptos.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-black-italic.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-black.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-bold.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-extrabold-italic 2.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-extrabold-italic.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-extrabold.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-italic.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-light-italic.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-light.ttf


BIN
video_generator/static/fonts/aptos-font/aptos-semibold.ttf


BIN
video_generator/static/fonts/aptos-font/aptos.ttf


BIN
video_generator/static/images/45.jpg


BIN
video_generator/static/images/left.png


BIN
video_generator/static/images/logo-mini.png


BIN
video_generator/static/images/logo.png


BIN
video_generator/static/images/user2-160x160.jpg


+ 62 - 0
video_generator/static/js/upload.js

@@ -0,0 +1,62 @@
+let startFile = null;
+let endFile = null;
+const processBtn = document.getElementById("processBtn");
+
+document.getElementById("startFrame").addEventListener("change", (e) => handleFile(e, "start"));
+document.getElementById("endFrame").addEventListener("change", (e) => handleFile(e, "end"));
+
+function handleFile(e, type) {
+    const file = e.target.files[0];
+    if (!file) return;
+    
+    const preview = document.getElementById(type + "Preview");
+    const fileName = document.getElementById(type + "FileName");
+    const previewContainer = document.getElementById(type + "PreviewContainer");
+    const card = document.getElementById(type + "Card");
+    const uploadContent = card.querySelector('.upload-content');
+    
+    preview.src = URL.createObjectURL(file);
+    fileName.textContent = file.name;
+    
+    uploadContent.style.display = 'none';
+    previewContainer.classList.add('show');
+    card.classList.add('active');
+    
+    if (type === "start") startFile = file;
+    else endFile = file;
+    
+    processBtn.disabled = !(startFile && endFile);
+}
+
+processBtn.addEventListener("click", async () => {
+    const formData = new FormData();
+    formData.append("first_frame", startFile);
+    formData.append("last_frame", endFile);
+
+    processBtn.innerHTML = '⏳ Processing<span class="spinner"></span>';
+    processBtn.classList.add('processing');
+    processBtn.disabled = true;
+
+    try {
+    const response = await fetch("/video/video-generator/", { method: "POST", body: formData });
+    const data = await response.json();
+
+    processBtn.innerHTML = "🚀 Generate Video";
+    processBtn.classList.remove('processing');
+    processBtn.disabled = false;
+
+    if (data.video_url) {
+        const video = document.getElementById("outputVideo");
+        video.src = data.video_url;
+        new bootstrap.Modal(document.getElementById("videoModal")).show();
+    } else {
+        alert("No video URL in response. Check backend output.");
+        console.log("Response:", data);
+    }
+    } catch (err) {
+    alert("Error: " + err.message);
+    processBtn.innerHTML = "🚀 Generate Video";
+    processBtn.classList.remove('processing');
+    processBtn.disabled = false;
+    }
+});

+ 159 - 0
video_generator/templates/img-upload.html

@@ -0,0 +1,159 @@
+{% load static %}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>AI Image to Video Generator</title>
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3@5.0.12/index.css"
+        integrity="sha256-tXJfXfp6Ewt1ilPzLDtQnJV4hclT9XuaZUKyUvmyr+Q=" crossorigin="anonymous">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/styles/overlayscrollbars.min.css"
+        integrity="sha256-dSokZseQNT08wYEWiz5iLI8QPlKxG+TswNRD8k35cpg=" crossorigin="anonymous">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.min.css"
+        integrity="sha256-Qsx5lrStHZyR9REqhUF8iQt73X06c8LGIUPzpOhwRrI=" crossorigin="anonymous">
+  <link rel="stylesheet" href="{% static './css/adminlte.css' %}">
+  <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+  <link rel="stylesheet" href="{% static './css/select2-bootstrap4.min.css' %}">
+  <link rel="stylesheet" href="{% static 'css/custom.css' %}">
+
+  <link rel="stylesheet" href="{% static 'css/upload.css' %}">
+</head>
+<body class="layout-fixed sidebar-expand-lg sidebar-mini app-loaded sidebar-collapse">
+  <div class="app-wrapper"> <!--begin::Header-->
+    {% include 'header.html' %}
+    {% include 'sidebar.html' %}
+    <main class="app-main"> <!--begin::App Content Header-->
+      <div class="app-content-header"> <!--begin::Container-->
+          <div class="container-fluid"> <!--begin::Row-->
+              <div class="row">
+                  <div class="col-sm-6">
+                      <h3 class="mb-0">✨ AI Video Magic</h3>
+                  </div>
+                  <div class="col-sm-6">
+                      <ol class="breadcrumb float-sm-end">
+                          <li class="breadcrumb-item"><a href="{% url 'file-upload' %}">Home</a></li>
+                          <li class="breadcrumb-item active" aria-current="page"><a href="{% url 'generate-video' %}"></a>
+                              ✨ AI Video Magic</a>
+                          </li>
+                      </ol>
+                  </div>
+              </div> <!--end::Row-->
+          </div> <!--end::Container-->
+      </div>
+      <div class="app-content-header"> <!--begin::Container-->
+          <div class="container-fluid "> <!--begin::Row-->
+            <div class=" video-generator" >
+              <div class="hero-section">
+                <h1 class="hero-title">✨ AI Video Magic</h1>
+                <p class="hero-subtitle">Transform your images into stunning transition videos with cutting-edge AI technology</p>
+              </div>
+              
+              <div class="upload-container">
+                <!-- Start Frame Card -->
+                <div class="upload-card" id="startCard">
+                  <div class="upload-content">
+                    <div class="upload-icon">🎬</div>
+                    <div class="upload-label">Start Frame</div>
+                    <div class="upload-hint">Click or drag to upload</div>
+                  </div>
+                  <div class="preview-container" id="startPreviewContainer">
+                    <img id="startPreview" class="preview-img" alt="Start Preview">
+                    <div id="startFileName" class="file-name"></div>
+                  </div>
+                  <input type="file" id="startFrame" accept="image/*">
+                </div>
+                
+                <div class="arrow-container">
+                  <div class="arrow">→</div>
+                </div>
+                
+                <!-- End Frame Card -->
+                <div class="upload-card" id="endCard">
+                  <div class="upload-content">
+                    <div class="upload-icon">🎞️</div>
+                    <div class="upload-label">End Frame</div>
+                    <div class="upload-hint">Click or drag to upload</div>
+                  </div>
+                  <div class="preview-container" id="endPreviewContainer">
+                    <img id="endPreview" class="preview-img" alt="End Preview">
+                    <div id="endFileName" class="file-name"></div>
+                  </div>
+                  <input type="file" id="endFrame" accept="image/*">
+                </div>
+              </div>
+              
+              <div class="action-section">
+                <button class="generate-btn" id="processBtn" disabled>
+                  🚀 Generate Video
+                </button>
+              </div>
+            </div>
+          </div>
+      </div>
+    </main>
+
+    <!-- Video Modal -->
+    <div class="modal fade" id="videoModal" tabindex="-1" aria-hidden="true">
+      <div class="modal-dialog modal-dialog-centered modal-lg">
+        <div class="modal-content">
+          <div class="modal-header">
+            <h5 class="modal-title">🎉 Your Video is Ready!</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+          </div>
+          <div class="modal-body">
+            <video id="outputVideo" class="w-100" controls></video>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    {% include 'footer.html' %}
+  </div>
+
+  <script src="{% static 'js/upload.js' %}"></script>
+  <script src="https://code.jquery.com/jquery-3.7.1.min.js"
+        integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
+    <script src="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/browser/overlayscrollbars.browser.es6.min.js"
+        integrity="sha256-H2VM7BKda+v2Z4+DRy69uknwxjyDRhszjXFhsL4gD3w=" crossorigin="anonymous"></script>
+    <!--end::Third Party Plugin(OverlayScrollbars)--><!--begin::Required Plugin(popperjs for Bootstrap 5)-->
+    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
+        integrity="sha256-whL0tQWoY1Ku1iskqPFvmZ+CHsvmRWx/PIoEvIeWh4I=" crossorigin="anonymous"></script>
+    <!--end::Required Plugin(popperjs for Bootstrap 5)--><!--begin::Required Plugin(Bootstrap 5)-->
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"
+        integrity="sha256-YMa+wAM6QkVyz999odX7lPRxkoYAan8suedu4k2Zur8=" crossorigin="anonymous"></script>
+    <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
+    <!--end::Required Plugin(Bootstrap 5)--><!--begin::Required Plugin(AdminLTE)-->
+    <script src="{% static './js/adminlte.js' %}"></script>
+    <script>
+        const SELECTOR_SIDEBAR_WRAPPER = ".sidebar-wrapper";
+        const Default = {
+            scrollbarTheme: "os-theme-light",
+            scrollbarAutoHide: "leave",
+            scrollbarClickScroll: true,
+        };
+        document.addEventListener("DOMContentLoaded", function () {
+            const sidebarWrapper = document.querySelector(SELECTOR_SIDEBAR_WRAPPER);
+            if (
+                sidebarWrapper &&
+                typeof OverlayScrollbarsGlobal?.OverlayScrollbars !== "undefined"
+            ) {
+                OverlayScrollbarsGlobal.OverlayScrollbars(sidebarWrapper, {
+                    scrollbars: {
+                        theme: Default.scrollbarTheme,
+                        autoHide: Default.scrollbarAutoHide,
+                        clickScroll: Default.scrollbarClickScroll,
+                    },
+                });
+            }
+        });
+        $(document).ready(function () {
+            $('.select2').select2({
+                theme: 'bootstrap4',
+                placeholder: 'Select Competitors'
+            });
+        });
+    </script> 
+    
+</body>
+</html>

+ 3 - 0
video_generator/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 13 - 0
video_generator/urls.py

@@ -0,0 +1,13 @@
+from django.urls import path
+from django.conf import settings
+from . import views
+from django.conf.urls.static import static
+
+
+urlpatterns = [
+    path('generate-video/', views.img_process, name='generate-video'),
+    path('video-generator/', views.process_images, name='video-generator'),
+]
+
+if settings.DEBUG:
+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 92 - 0
video_generator/views.py

@@ -0,0 +1,92 @@
+# # Standard Library Imports
+import os
+import time
+
+# # Third-Party Library Imports
+import requests
+
+# # Django Imports
+from django.shortcuts import render
+from django.conf import settings
+from django.http import JsonResponse
+from django.views.decorators.csrf import csrf_exempt
+from django.core.files.storage import default_storage
+from django.core.files.base import ContentFile
+from .decorators import login_required
+
+@login_required
+def img_process(request):
+    return render(request, 'img-upload.html')
+
+MINIMAX_API_KEY = settings.MINIMAX_API_KEY
+VIDEO_GEN_URL = "https://api.minimax.io/v1/video_generation"
+QUERY_URL = "https://api.minimax.io/v1/query/video_generation"
+FILES_URL = "https://api.minimax.io/v1/files/retrieve"
+POLL_INTERVAL = 5  # seconds
+
+
+@csrf_exempt
+def process_images(request):
+    if request.method != "POST":
+        return JsonResponse({"error": "Invalid request method."}, status=405)
+
+    first_frame = request.FILES.get("first_frame")
+    last_frame = request.FILES.get("last_frame")
+
+    if not first_frame or not last_frame:
+        return JsonResponse({"error": "Both first_frame and last_frame are required."}, status=400)
+
+    try:
+        # --- Save uploaded images locally ---
+        first_path = default_storage.save(f"uploads/{first_frame.name}", first_frame)
+        last_path = default_storage.save(f"uploads/{last_frame.name}", last_frame)
+
+        first_filename = os.path.basename(first_path)
+        last_filename = os.path.basename(last_path)
+
+        # --- Build public URLs using ngrok ---
+        # Replace this with your current ngrok URL
+        NGROK_URL = "https://postcartilaginous-kyler-nonrun.ngrok-free.dev"
+        first_url = f"{NGROK_URL}{settings.MEDIA_URL}{first_filename}"
+        last_url = f"{NGROK_URL}{settings.MEDIA_URL}{last_filename}"
+
+        # --- Call MiniMax API ---
+        headers = {"Authorization": f"Bearer {MINIMAX_API_KEY}"}
+        payload = {
+            "prompt": "Generate a smooth transition between two frames.",
+            "first_frame_image": first_url,
+            "last_frame_image": last_url,
+            "model": "MiniMax-Hailuo-02",
+            "duration": 6,
+            "resolution": "1080P"
+        }
+
+        response = requests.post(VIDEO_GEN_URL, headers=headers, json=payload)
+        response.raise_for_status()
+        print("Response", response.json())
+        task_id = response.json().get("task_id")
+        if not task_id:
+            return JsonResponse({"error": "Failed to get task_id from MiniMax API."}, status=500)
+
+        # --- Poll MiniMax until task completes ---
+        file_id = None
+        while not file_id:
+            time.sleep(POLL_INTERVAL)
+            status_resp = requests.get(QUERY_URL, headers=headers, params={"task_id": task_id})
+            status_resp.raise_for_status()
+            status_data = status_resp.json()
+            status = status_data.get("status")
+            if status == "Success":
+                file_id = status_data.get("file_id")
+            elif status == "Fail":
+                return JsonResponse({"error": status_data.get("error_message", "Video generation failed")}, status=500)
+
+        # --- Get final video download URL ---
+        download_resp = requests.get(FILES_URL, headers=headers, params={"file_id": file_id})
+        download_resp.raise_for_status()
+        download_url = download_resp.json()["file"]["download_url"]
+
+        return JsonResponse({"video_url": download_url})
+
+    except Exception as e:
+        return JsonResponse({"error": str(e)}, status=500)