diff --git a/.gitattributes b/.gitattributes
index a468f0f350d53317767c7a0bd17c2078e8f26241..96d67ad45a492f201984f0cf701b0cb05009be15 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -35,3 +35,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
fonts/TikTok-Bold.ttf filter=lfs diff=lfs merge=lfs -text
gradio_demo.png filter=lfs diff=lfs merge=lfs -text
+assets/edu_note.wav filter=lfs diff=lfs merge=lfs -text
+assets/fun_fact.wav filter=lfs diff=lfs merge=lfs -text
+assets/thanks.wav filter=lfs diff=lfs merge=lfs -text
diff --git a/assets/edu_note.wav b/assets/edu_note.wav
new file mode 100644
index 0000000000000000000000000000000000000000..e5bbd527fba7db4a0bbfa3bf4116699c6eefb3c3
--- /dev/null
+++ b/assets/edu_note.wav
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:880362273fe0348d8e73e9cf99cbe58573ccaac1e51628e92d4a362c8199d399
+size 416444
diff --git a/assets/fun_fact.wav b/assets/fun_fact.wav
new file mode 100644
index 0000000000000000000000000000000000000000..58949399a97d7511b438fbce1855f1888e4d63bd
--- /dev/null
+++ b/assets/fun_fact.wav
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:245b8db3fc90671b02baa477ce0c2d08754b225ae95a853687191ad126de562e
+size 496844
diff --git a/assets/thanks.wav b/assets/thanks.wav
new file mode 100644
index 0000000000000000000000000000000000000000..8ff68850b72654f7a9187fff3bfb6f66dd07bc5d
--- /dev/null
+++ b/assets/thanks.wav
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d04843d86bff241ff659cf6c6931641797411dbf3b1532eee83914058bbc5a64
+size 288044
diff --git a/auth/database.py b/auth/database.py
new file mode 100644
index 0000000000000000000000000000000000000000..af5f6cfffae64f17eeba670d3a812d76cb064887
--- /dev/null
+++ b/auth/database.py
@@ -0,0 +1,88 @@
+import os
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker, declarative_base
+from sqlalchemy.exc import OperationalError
+
+# ==========================================================
+# DATABASE URL
+# ==========================================================
+DATABASE_URL = os.getenv("DATABASE_URL")
+
+if not DATABASE_URL:
+ raise RuntimeError(
+ "DATABASE_URL environment variable is not set"
+ )
+
+# ==========================================================
+# SUPABASE ENTERPRISE ENGINE CONFIG
+# ==========================================================
+# Optimized for:
+# - Supabase Pooler
+# - FastAPI async workload
+# - Background workers
+# - Long-running autonomous services
+
+engine = create_engine(
+ DATABASE_URL,
+
+ # --- Pool Stability ---
+ pool_pre_ping=True, # validates dead connections
+ pool_recycle=300, # refresh connections
+ pool_size=5, # safe baseline
+ max_overflow=10, # burst capacity
+
+ # --- Reliability ---
+ echo=False,
+ future=True,
+
+ # --- Supabase Requirement ---
+ connect_args={
+ "sslmode": "require",
+ "connect_timeout": 10,
+ },
+)
+
+# ==========================================================
+# SESSION FACTORY
+# ==========================================================
+SessionLocal = sessionmaker(
+ autocommit=False,
+ autoflush=False,
+ bind=engine,
+)
+
+# ==========================================================
+# BASE MODEL
+# ==========================================================
+Base = declarative_base()
+
+# ==========================================================
+# DEPENDENCY (FASTAPI)
+# ==========================================================
+def get_db():
+ """
+ FastAPI dependency injection session.
+ Ensures connection cleanup even on crash.
+ """
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+
+# ==========================================================
+# CONNECTION TEST (STARTUP SAFE)
+# ==========================================================
+def verify_database_connection():
+ """
+ Validates database connectivity during startup.
+ Prevents silent runtime failures.
+ """
+ try:
+ with engine.connect() as conn:
+ conn.execute("SELECT 1")
+ except OperationalError as e:
+ raise RuntimeError(
+ f"Database connection failed: {str(e)}"
+ )
\ No newline at end of file
diff --git a/auth/models.py b/auth/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9eb23d5d0dce83b21d7f8983cd329ba371fea68
--- /dev/null
+++ b/auth/models.py
@@ -0,0 +1,96 @@
+import uuid
+from sqlalchemy import Column, String, Boolean, DateTime, func, Index
+from sqlalchemy.dialects.postgresql import UUID
+
+from auth.database import Base
+
+
+# =========================================================
+# USERS TABLE (CORE AUTH ENTITY)
+# =========================================================
+class User(Base):
+ __tablename__ = "users"
+
+ # -----------------------------
+ # Primary Key (UUID for Supabase compatibility)
+ # -----------------------------
+ id = Column(
+ UUID(as_uuid=True),
+ primary_key=True,
+ default=uuid.uuid4,
+ nullable=False,
+ )
+
+ # -----------------------------
+ # Identity Fields
+ # -----------------------------
+ email = Column(String(255), unique=True, nullable=False, index=True)
+ username = Column(String(100), unique=True, nullable=True, index=True)
+
+ # -----------------------------
+ # Security Fields
+ # NOTE: stores hashed password only (never plaintext)
+ # -----------------------------
+ hashed_password = Column(String(255), nullable=False)
+
+ # -----------------------------
+ # Account State
+ # -----------------------------
+ is_active = Column(Boolean, default=True, nullable=False)
+ is_verified = Column(Boolean, default=False, nullable=False)
+
+ # -----------------------------
+ # Audit Fields
+ # -----------------------------
+ created_at = Column(
+ DateTime(timezone=True),
+ server_default=func.now(),
+ nullable=False,
+ )
+
+ updated_at = Column(
+ DateTime(timezone=True),
+ server_default=func.now(),
+ onupdate=func.now(),
+ nullable=False,
+ )
+
+
+# =========================================================
+# OPTIONAL: API KEY TABLE (FOR AUTOMATION / N8N / WORKFLOWS)
+# =========================================================
+class ApiKey(Base):
+ __tablename__ = "api_keys"
+
+ id = Column(
+ UUID(as_uuid=True),
+ primary_key=True,
+ default=uuid.uuid4,
+ nullable=False,
+ )
+
+ user_id = Column(
+ UUID(as_uuid=True),
+ nullable=False,
+ index=True,
+ )
+
+ key_hash = Column(String(255), nullable=False, unique=True)
+
+ name = Column(String(120), nullable=True)
+
+ is_active = Column(Boolean, default=True, nullable=False)
+
+ created_at = Column(
+ DateTime(timezone=True),
+ server_default=func.now(),
+ nullable=False,
+ )
+
+
+# =========================================================
+# INDEXES (PERFORMANCE OPTIMIZATION)
+# =========================================================
+Index("idx_users_email", User.email)
+Index("idx_users_username", User.username)
+Index("idx_api_keys_user_id", ApiKey.user_id)
\ No newline at end of file
diff --git a/auth/routes.py b/auth/routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2fe1c1448f7da86a66adb17d13ee76ffa523b4c
--- /dev/null
+++ b/auth/routes.py
@@ -0,0 +1,340 @@
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from sqlalchemy.exc import IntegrityError
+from datetime import datetime
+
+from auth.database import get_db
+from auth.models import User
+from auth.schemas import (
+ SignupSchema,
+ LoginSchema,
+ TokenSchema,
+)
+from auth.security import (
+ hash_password,
+ verify_password,
+ create_access_token,
+ decode_token,
+)
+
+router = APIRouter(
+ prefix="/api/auth",
+ tags=["Authentication"],
+)
+
+
+# =========================================================
+# SIGNUP
+# =========================================================
+
+@router.post("/signup", status_code=201)
+def signup(
+ data: SignupSchema,
+ db: Session = Depends(get_db),
+):
+
+ try:
+
+ # =================================================
+ # NORMALIZATION
+ # =================================================
+
+ email = data.email.strip().lower()
+ username = data.username.strip()
+
+ # =================================================
+ # EXISTING EMAIL CHECK
+ # =================================================
+
+ existing_email = (
+ db.query(User)
+ .filter(User.email == email)
+ .first()
+ )
+
+ if existing_email:
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail="Email already registered",
+ )
+
+ # =================================================
+ # EXISTING USERNAME CHECK
+ # =================================================
+
+ existing_username = (
+ db.query(User)
+ .filter(User.username == username)
+ .first()
+ )
+
+ if existing_username:
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail="Username already taken",
+ )
+
+ # =================================================
+ # HASH PASSWORD
+ # =================================================
+
+ password_hash = hash_password(data.password)
+
+ # =================================================
+ # CREATE USER
+ # =================================================
+
+ user = User(
+ email=email,
+ username=username,
+ password_hash=password_hash,
+ created_at=datetime.utcnow(),
+ )
+
+ db.add(user)
+ db.commit()
+ db.refresh(user)
+
+ # =================================================
+ # CREATE JWT
+ # =================================================
+
+ token = create_access_token(
+ {
+ "sub": str(user.id),
+ "email": user.email,
+ }
+ )
+
+ return {
+ "status": "success",
+ "message": "Account created successfully",
+ "token": token,
+ "user": {
+ "id": str(user.id),
+ "email": user.email,
+ "username": user.username,
+ "created_at": (
+ user.created_at.isoformat()
+ if user.created_at
+ else None
+ ),
+ },
+ }
+
+ except HTTPException:
+ raise
+
+ except IntegrityError as e:
+
+ db.rollback()
+
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail="User already exists",
+ )
+
+ except ValueError as e:
+
+ db.rollback()
+
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=str(e),
+ )
+
+ except Exception as e:
+
+ db.rollback()
+
+ print("SIGNUP ERROR:", str(e))
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Signup failed: {str(e)}",
+ )
+
+
+# =========================================================
+# LOGIN
+# =========================================================
+
+@router.post("/login")
+def login(
+ data: LoginSchema,
+ db: Session = Depends(get_db),
+):
+
+ try:
+
+ email = data.email.strip().lower()
+
+ user = (
+ db.query(User)
+ .filter(User.email == email)
+ .first()
+ )
+
+ if not user:
+
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid email or password",
+ )
+
+ if not verify_password(
+ data.password,
+ user.password_hash,
+ ):
+
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid email or password",
+ )
+
+ token = create_access_token(
+ {
+ "sub": str(user.id),
+ "email": user.email,
+ }
+ )
+
+ return {
+ "status": "success",
+ "message": "Login successful",
+ "token": token,
+ "user": {
+ "id": str(user.id),
+ "email": user.email,
+ "username": user.username,
+ "created_at": (
+ user.created_at.isoformat()
+ if user.created_at
+ else None
+ ),
+ },
+ }
+
+ except HTTPException:
+ raise
+
+ except Exception as e:
+
+ print("LOGIN ERROR:", str(e))
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Login failed: {str(e)}",
+ )
+
+
+# =========================================================
+# VERIFY TOKEN
+# =========================================================
+
+@router.post("/verify")
+def verify_token(
+ data: TokenSchema,
+):
+
+ try:
+
+ payload = decode_token(data.token)
+
+ if not payload:
+
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid token",
+ )
+
+ return {
+ "valid": True,
+ "payload": payload,
+ }
+
+ except HTTPException:
+ raise
+
+ except Exception as e:
+
+ print("VERIFY ERROR:", str(e))
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Token verification failed: {str(e)}",
+ )
+
+
+# =========================================================
+# REFRESH TOKEN
+# =========================================================
+
+@router.post("/refresh")
+def refresh_token(
+ data: TokenSchema,
+ db: Session = Depends(get_db),
+):
+
+ try:
+
+ payload = decode_token(data.token)
+
+ user_id = payload.get("sub")
+
+ if not user_id:
+
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid token payload",
+ )
+
+ user = (
+ db.query(User)
+ .filter(User.id == user_id)
+ .first()
+ )
+
+ if not user:
+
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="User not found",
+ )
+
+ new_token = create_access_token(
+ {
+ "sub": str(user.id),
+ "email": user.email,
+ }
+ )
+
+ return {
+ "status": "success",
+ "token": new_token,
+ }
+
+ except HTTPException:
+ raise
+
+ except Exception as e:
+
+ print("REFRESH ERROR:", str(e))
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Refresh failed: {str(e)}",
+ )
+
+
+# =========================================================
+# LOGOUT
+# =========================================================
+
+@router.post("/logout")
+def logout():
+
+ return {
+ "status": "success",
+ "message": "Logout successful",
+ }
\ No newline at end of file
diff --git a/auth/schemas.py b/auth/schemas.py
new file mode 100644
index 0000000000000000000000000000000000000000..01472622737511de3ef76a09b389f50ac39971cc
--- /dev/null
+++ b/auth/schemas.py
@@ -0,0 +1,59 @@
+from pydantic import BaseModel, EmailStr, Field, ConfigDict
+
+
+# =========================================================
+# BASE CONFIG (STRICT MODE → PREVENTS SILENT DATA COERCION)
+# =========================================================
+class StrictSchema(BaseModel):
+ model_config = ConfigDict(
+ strict=True,
+ extra="forbid",
+ validate_assignment=True,
+ )
+
+
+# =========================================================
+# SIGNUP SCHEMA
+# =========================================================
+class SignupSchema(StrictSchema):
+ email: EmailStr
+ username: str = Field(
+ min_length=3,
+ max_length=32,
+ pattern=r"^[a-zA-Z0-9_]+$"
+ )
+ password: str = Field(
+ min_length=8,
+ max_length=72,
+ )
+
+
+# =========================================================
+# LOGIN SCHEMA
+# =========================================================
+class LoginSchema(StrictSchema):
+ email: EmailStr
+ password: str = Field(
+ min_length=1,
+ max_length=72,
+ )
+
+
+# =========================================================
+# TOKEN REQUEST SCHEMA
+# =========================================================
+class TokenSchema(StrictSchema):
+ token: str = Field(
+ min_length=10,
+ max_length=2048
+ )
+
+
+# =========================================================
+# USER RESPONSE SCHEMA (SAFE OUTPUT MODEL)
+# =========================================================
+class UserOutSchema(StrictSchema):
+ id: int
+ email: EmailStr
+ username: str
+ created_at: str
\ No newline at end of file
diff --git a/auth/security.py b/auth/security.py
new file mode 100644
index 0000000000000000000000000000000000000000..248b4b97de7017df7f15c14588ad43444965901b
--- /dev/null
+++ b/auth/security.py
@@ -0,0 +1,137 @@
+import os
+from datetime import datetime, timedelta, timezone
+from typing import Optional, Dict, Any
+
+from jose import jwt, JWTError
+from passlib.context import CryptContext
+from fastapi import HTTPException, status, Depends
+from fastapi.security import OAuth2PasswordBearer
+
+
+# =========================================================
+# ENV CONFIG
+# =========================================================
+
+SECRET_KEY = os.getenv("SECRET_KEY")
+
+if not SECRET_KEY:
+ raise RuntimeError("SECRET_KEY environment variable missing")
+
+ALGORITHM = os.getenv("ALGORITHM", "HS256")
+
+ACCESS_TOKEN_EXPIRE_MINUTES = int(
+ os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "1440")
+)
+
+
+# =========================================================
+# PASSWORD HASHING
+# =========================================================
+# IMPORTANT:
+# pbkdf2_sha256 avoids:
+# - bcrypt crashes
+# - bcrypt native dependency issues
+# - 72-byte limits
+# - passlib backend bugs
+# =========================================================
+
+pwd_context = CryptContext(
+ schemes=["pbkdf2_sha256"],
+ deprecated="auto",
+)
+
+
+def hash_password(password: str) -> str:
+
+ if not password:
+ raise ValueError("Password required")
+
+ if len(password) < 8:
+ raise ValueError("Password too short")
+
+ return pwd_context.hash(password)
+
+
+def verify_password(
+ plain_password: str,
+ hashed_password: str,
+) -> bool:
+
+ try:
+ return pwd_context.verify(
+ plain_password,
+ hashed_password,
+ )
+ except Exception:
+ return False
+
+
+# =========================================================
+# JWT
+# =========================================================
+
+def create_access_token(
+ data: Dict[str, Any],
+ expires_delta: Optional[timedelta] = None,
+) -> str:
+
+ to_encode = data.copy()
+
+ expire = datetime.now(timezone.utc) + (
+ expires_delta
+ if expires_delta
+ else timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ )
+
+ to_encode.update({"exp": expire})
+
+ return jwt.encode(
+ to_encode,
+ SECRET_KEY,
+ algorithm=ALGORITHM,
+ )
+
+
+def decode_token(token: str):
+
+ try:
+ return jwt.decode(
+ token,
+ SECRET_KEY,
+ algorithms=[ALGORITHM],
+ )
+
+ except JWTError:
+
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid token",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+
+# =========================================================
+# AUTH DEPENDENCY
+# =========================================================
+
+oauth2_scheme = OAuth2PasswordBearer(
+ tokenUrl="/api/auth/login"
+)
+
+
+def get_current_user(
+ token: str = Depends(oauth2_scheme)
+):
+
+ payload = decode_token(token)
+
+ user_id = payload.get("sub")
+
+ if not user_id:
+
+ raise HTTPException(
+ status_code=401,
+ detail="Invalid authentication",
+ )
+
+ return payload
\ No newline at end of file
diff --git a/core/builders/api_builder.py b/core/builders/api_builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2b911003465c5c10d4d2151d24c7194e51b8bbf
--- /dev/null
+++ b/core/builders/api_builder.py
@@ -0,0 +1,142 @@
+from fastapi import APIRouter, UploadFile, File, Form, HTTPException
+from fastapi.responses import JSONResponse, FileResponse
+from typing import Optional
+import tempfile
+import shutil
+import os
+
+from core.registry.loader import get_tasks
+from core.execution.executor import execute_task
+
+
+# =====================================================
+# ROUTER BUILDER
+# =====================================================
+
+def build_api_router() -> APIRouter:
+ """
+ Dynamically builds API routes from TASK REGISTRY.
+
+ Generated endpoints:
+
+ POST /execute/{task}
+ GET /tasks
+ GET /health
+ """
+
+ router = APIRouter(tags=["API"])
+
+ # =================================================
+ # EXECUTE TASK
+ # =================================================
+ @router.post("/execute/{task_name}")
+ async def execute(
+ task_name: str,
+ file: Optional[UploadFile] = File(None),
+ url_input: Optional[str] = Form(None),
+ ):
+ """
+ Universal execution endpoint.
+ Accepts:
+ - file upload
+ - url_input
+ """
+
+ tasks = get_tasks()
+
+ if task_name not in tasks:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Task '{task_name}' not found",
+ )
+
+ temp_path = None
+
+ try:
+ # -----------------------------------------
+ # SAVE UPLOADED FILE
+ # -----------------------------------------
+ if file:
+ suffix = os.path.splitext(file.filename)[1]
+
+ with tempfile.NamedTemporaryFile(
+ delete=False,
+ suffix=suffix,
+ ) as tmp:
+ shutil.copyfileobj(file.file, tmp)
+ temp_path = tmp.name
+
+ # -----------------------------------------
+ # BUILD INPUT PAYLOAD
+ # -----------------------------------------
+ payload = {
+ "file_path": temp_path,
+ "url_input": url_input,
+ }
+
+ # -----------------------------------------
+ # EXECUTE TASK
+ # -----------------------------------------
+ result = await execute_task(task_name, payload)
+
+ # -----------------------------------------
+ # FILE RESPONSE
+ # -----------------------------------------
+ if isinstance(result, dict) and result.get("file"):
+ output_file = result["file"]
+
+ if os.path.exists(output_file):
+ return FileResponse(
+ output_file,
+ filename=os.path.basename(output_file),
+ )
+
+ # -----------------------------------------
+ # JSON RESPONSE
+ # -----------------------------------------
+ return JSONResponse(result)
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=str(e),
+ )
+
+ finally:
+ # -----------------------------------------
+ # CLEANUP TEMP FILE
+ # -----------------------------------------
+ if temp_path and os.path.exists(temp_path):
+ os.unlink(temp_path)
+
+ # =================================================
+ # TASK LIST
+ # =================================================
+ @router.get("/tasks")
+ async def list_tasks():
+ """
+ Returns registry tasks.
+ Used by UI and Docs builder.
+ """
+
+ tasks = get_tasks()
+
+ return {
+ name: {
+ "category": getattr(t, "category", "general"),
+ "description": getattr(t, "description", ""),
+ }
+ for name, t in tasks.items()
+ }
+
+ # =================================================
+ # HEALTH CHECK
+ # =================================================
+ @router.get("/health")
+ async def health():
+ return {
+ "status": "ok",
+ "tasks_loaded": len(get_tasks()),
+ }
+
+ return router
\ No newline at end of file
diff --git a/core/builders/docs_builder.py b/core/builders/docs_builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..86cda92f82a022e7f7c7318fbeca2e9c0624bbb1
--- /dev/null
+++ b/core/builders/docs_builder.py
@@ -0,0 +1,89 @@
+from fastapi import APIRouter
+from fastapi.responses import JSONResponse
+from typing import Dict, Any, List
+
+from core.registry.tasks_registry import TASKS
+
+
+# =====================================================
+# INTERNAL HELPERS
+# =====================================================
+
+def _serialize_task(task) -> Dict[str, Any]:
+ """
+ Convert TaskDefinition → API-safe metadata.
+ Must NOT import task modules.
+ """
+
+ return {
+ "name": getattr(task, "name", None),
+ "description": getattr(task, "description", ""),
+ "category": getattr(task, "category", "general"),
+ "module": getattr(task, "module", None),
+ "callable": getattr(task, "callable_name", "run"),
+ "async": getattr(task, "async_task", False),
+ "enabled": getattr(task, "enabled", True),
+ }
+
+
+def _group_tasks(tasks: List[Any]) -> Dict[str, List[Dict[str, Any]]]:
+ """
+ Groups tasks by category for UI rendering.
+ """
+
+ grouped: Dict[str, List[Dict[str, Any]]] = {}
+
+ for task in tasks:
+ category = getattr(task, "category", "general")
+ grouped.setdefault(category, []).append(_serialize_task(task))
+
+ return grouped
+
+
+# =====================================================
+# DOCS ROUTER BUILDER (V11)
+# =====================================================
+
+def build_docs_router() -> APIRouter:
+ """
+ Builds dynamic documentation endpoints.
+
+ Provides:
+ /docs/tasks → flat task list
+ /docs/catalog → grouped tasks
+ /docs/health → docs status
+ """
+
+ router = APIRouter(
+ prefix="/docs",
+ tags=["Documentation"],
+ )
+
+ # -------------------------------------------------
+ # List all tasks
+ # -------------------------------------------------
+ @router.get("/tasks")
+ async def list_tasks():
+ return JSONResponse(
+ [_serialize_task(task) for task in TASKS]
+ )
+
+ # -------------------------------------------------
+ # Categorized task catalog
+ # -------------------------------------------------
+ @router.get("/catalog")
+ async def task_catalog():
+ return JSONResponse(_group_tasks(TASKS))
+
+ # -------------------------------------------------
+ # Docs health endpoint
+ # -------------------------------------------------
+ @router.get("/health")
+ async def docs_health():
+ return {
+ "status": "ok",
+ "service": "docs",
+ "tasks_registered": len(TASKS),
+ }
+
+ return router
\ No newline at end of file
diff --git a/core/builders/ui_builder.py b/core/builders/ui_builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9d3f29e53d0e3e198f4622e150aa865427aa98b
--- /dev/null
+++ b/core/builders/ui_builder.py
@@ -0,0 +1,107 @@
+from fastapi import APIRouter
+from fastapi.responses import HTMLResponse, FileResponse
+from pathlib import Path
+from typing import Dict, List
+
+from core.registry.tasks_registry import TASKS
+
+
+# =====================================================
+# CONFIG
+# =====================================================
+
+BASE_DIR = Path(__file__).resolve().parents[2]
+UI_DIR = BASE_DIR / "ui"
+INDEX_FILE = UI_DIR / "index.html"
+
+
+# =====================================================
+# TASK GROUPING
+# =====================================================
+
+def group_tasks() -> Dict[str, List[dict]]:
+ """
+ Converts registry → categorized UI structure.
+ """
+
+ categories: Dict[str, List[dict]] = {}
+
+ for task in TASKS:
+ if not getattr(task, "enabled", True):
+ continue
+
+ category = getattr(task, "category", "general")
+
+ categories.setdefault(category, []).append(
+ {
+ "name": task.name,
+ "description": getattr(task, "description", ""),
+ }
+ )
+
+ return categories
+
+
+# =====================================================
+# ROUTER BUILDER
+# =====================================================
+
+def build_ui_router() -> APIRouter:
+ """
+ Serves UI + dynamic UI metadata.
+
+ Endpoints:
+ /
+ /ui
+ /ui/tasks
+ /ui/health
+ """
+
+ router = APIRouter(tags=["UI"])
+
+ # -------------------------------------------------
+ # MAIN UI
+ # -------------------------------------------------
+ @router.get("/", response_class=HTMLResponse)
+ async def serve_root():
+ """
+ Serves index.html
+ """
+
+ if not INDEX_FILE.exists():
+ return HTMLResponse(
+ "
+
+
+
+
+ AUTONOMOUS CONTENT OPERATOR
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..55c26a4edbbaf5f1d4f9790fd8eac05f2934c102
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,338 @@
+/* =========================================================
+ BASYX V11 UI SYSTEM
+ Industrial Dark OS Theme
+========================================================= */
+
+:root {
+ --void: #080808;
+ --surface: #0f0f0f;
+ --panel: #161616;
+ --panel-2: #121212;
+
+ --border: #232323;
+ --border-2: #2c2c2c;
+
+ --accent: #e8ff47;
+ --accent-dim: #b8cc30;
+
+ --text: #e8e8e8;
+ --text-dim: #888888;
+
+ --muted: #555555;
+ --danger: #ef4444;
+ --warn: #f59e0b;
+ --ok: #22c55e;
+
+ --radius: 6px;
+
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono";
+ --font-display: Bebas Neue, sans-serif;
+ --font-body: system-ui, -apple-system, Segoe UI, Roboto;
+}
+
+/* =========================================================
+ BASE RESET
+========================================================= */
+
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ background: var(--void);
+ color: var(--text);
+ font-family: var(--font-body);
+ height: 100%;
+}
+
+h1, h2, h3 {
+ margin: 0;
+}
+
+/* =========================================================
+ SIDEBAR
+========================================================= */
+
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 220px;
+ height: 100vh;
+ background: #0b0b0b;
+ border-right: 1px solid var(--border);
+ padding: 16px;
+ overflow-y: auto;
+ transition: transform 0.25s ease;
+}
+
+.brand {
+ font-family: var(--font-display);
+ font-size: 28px;
+ color: var(--accent);
+ margin-bottom: 18px;
+ letter-spacing: 1px;
+}
+
+.nav-group-title {
+ font-size: 11px;
+ color: var(--text-dim);
+ margin: 16px 0 6px;
+ letter-spacing: 1px;
+}
+
+.nav-item {
+ padding: 10px 12px;
+ cursor: pointer;
+ color: var(--text-dim);
+ border-left: 2px solid transparent;
+ transition: all 0.2s ease;
+ border-radius: 4px;
+}
+
+.nav-item:hover {
+ color: var(--text);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.nav-item.active {
+ border-left: 2px solid var(--accent);
+ color: var(--accent);
+ background: rgba(232, 255, 71, 0.06);
+}
+
+/* =========================================================
+ MAIN LAYOUT
+========================================================= */
+
+.main {
+ margin-left: 220px;
+ padding: 24px;
+}
+
+/* =========================================================
+ SECTIONS
+========================================================= */
+
+.section {
+ display: none;
+ animation: fadeIn 0.25s ease;
+}
+
+.section.active {
+ display: block;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* =========================================================
+ PANELS
+========================================================= */
+
+.panel {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ padding: 18px;
+ border-radius: var(--radius);
+ margin-bottom: 16px;
+}
+
+.panel h2 {
+ font-family: var(--font-display);
+ letter-spacing: 1px;
+ color: var(--accent);
+ margin-bottom: 12px;
+}
+
+/* =========================================================
+ INPUT SYSTEM
+========================================================= */
+
+input, textarea, select {
+ width: 100%;
+ padding: 10px 12px;
+ margin-top: 8px;
+ background: #111;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text);
+ outline: none;
+}
+
+input:focus {
+ border-color: var(--accent);
+}
+
+/* =========================================================
+ BUTTONS
+========================================================= */
+
+button {
+ padding: 12px 14px;
+ border: none;
+ cursor: pointer;
+ background: var(--accent);
+ color: #000;
+ font-weight: 700;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ margin-top: 10px;
+}
+
+button:hover {
+ filter: brightness(1.05);
+}
+
+button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* =========================================================
+ PRE / OUTPUT
+========================================================= */
+
+pre {
+ background: #0b0b0b;
+ border: 1px solid var(--border);
+ padding: 12px;
+ border-radius: var(--radius);
+ overflow: auto;
+ color: var(--text);
+ font-family: var(--font-mono);
+}
+
+/* =========================================================
+ VIDEO OUTPUT
+========================================================= */
+
+video {
+ width: 100%;
+ border-radius: var(--radius);
+ border: 1px solid var(--border);
+ background: #000;
+}
+
+/* =========================================================
+ CARDS (future UI expansion)
+========================================================= */
+
+.card-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+}
+
+.card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ padding: 14px;
+ border-radius: var(--radius);
+ cursor: pointer;
+ transition: 0.2s;
+}
+
+.card:hover {
+ border-color: var(--accent);
+ transform: translateY(-2px);
+}
+
+/* =========================================================
+ STATUS BADGES
+========================================================= */
+
+.badge {
+ display: inline-block;
+ padding: 4px 8px;
+ font-size: 12px;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+}
+
+.badge.ok {
+ color: var(--ok);
+ border-color: var(--ok);
+}
+
+.badge.warn {
+ color: var(--warn);
+ border-color: var(--warn);
+}
+
+.badge.error {
+ color: var(--danger);
+ border-color: var(--danger);
+}
+
+/* =========================================================
+ STAT CARDS
+========================================================= */
+
+.stat-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+ margin: 12px 0;
+}
+
+.stat-card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ padding: 14px;
+ border-radius: var(--radius);
+}
+
+.stat-label {
+ font-size: 12px;
+ color: var(--text-dim);
+}
+
+.stat-value {
+ font-size: 34px;
+ font-family: var(--font-display);
+ color: var(--accent);
+}
+
+/* =========================================================
+ LOADING STATE
+========================================================= */
+
+.loading {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+/* =========================================================
+ MOBILE
+========================================================= */
+
+@media (max-width: 768px) {
+
+ .sidebar {
+ transform: translateX(-100%);
+ position: fixed;
+ z-index: 50;
+ }
+
+ .sidebar.open {
+ transform: translateX(0);
+ }
+
+ .main {
+ margin-left: 0;
+ padding: 16px;
+ }
+
+ .card-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .stat-grid {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40c75472b2df38dce59c6250b63167298c0e06a2
--- /dev/null
+++ b/utils/__init__.py
@@ -0,0 +1 @@
+# Initialize package
diff --git a/utils/ab_generator.py b/utils/ab_generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe35d963dcc587d1e64fba02b674ccf2db3b8416
--- /dev/null
+++ b/utils/ab_generator.py
@@ -0,0 +1,11 @@
+from .strategist import rewrite_hook
+
+def generate_variants(segment):
+
+ text = " ".join([w["text"] for w in segment])
+
+ return [
+ rewrite_hook(text),
+ f"🔥 {text}",
+ f"Did you know? {text}",
+ ]
\ No newline at end of file
diff --git a/utils/auto_editor.py b/utils/auto_editor.py
new file mode 100644
index 0000000000000000000000000000000000000000..88fed30e92620253ff2c03663103d7c0aff7bbbb
--- /dev/null
+++ b/utils/auto_editor.py
@@ -0,0 +1,112 @@
+"""
+auto_editor.py
+---------------------------------------
+Automatic video pacing editor
+
+Features:
+- Removes silence automatically
+- Speeds up slow segments
+- Keeps speech natural
+- CPU optimized (HuggingFace FREE tier safe)
+
+Input:
+ input.mp4
+
+Output:
+ edited.mp4
+"""
+
+import subprocess
+import os
+import sys
+from pathlib import Path
+
+INPUT_VIDEO = "input.mp4"
+OUTPUT_VIDEO = "edited.mp4"
+
+
+def run_command(cmd):
+ """Run shell command safely"""
+ try:
+ subprocess.run(
+ cmd,
+ shell=True,
+ check=True
+ )
+ except subprocess.CalledProcessError as e:
+ print("Command failed:", e)
+ sys.exit(1)
+
+
+def check_input():
+ if not os.path.exists(INPUT_VIDEO):
+ print(f"❌ Missing file: {INPUT_VIDEO}")
+ sys.exit(1)
+
+
+def install_auto_editor():
+ """
+ Ensures auto-editor exists.
+ Required because HF containers reset.
+ """
+ print("Installing auto-editor...")
+ run_command("pip install --no-cache-dir auto-editor")
+
+
+def auto_edit():
+ """
+ Main editing step.
+ Removes silence + improves pacing.
+ """
+
+ cmd = f"""
+ auto-editor "{INPUT_VIDEO}"
+ --margin 0.2s
+ --silent-speed 99999
+ --video-speed 1
+ --audio-normalize peak
+ --export mp4
+ --output "{OUTPUT_VIDEO}"
+ """
+
+ print("Running auto-editor...")
+ run_command(cmd)
+
+
+def optimize_output():
+ """
+ Re-encode for social media compatibility.
+ """
+
+ temp = "optimized.mp4"
+
+ cmd = f"""
+ ffmpeg -y -i "{OUTPUT_VIDEO}"
+ -vf scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2
+ -c:v libx264
+ -preset veryfast
+ -crf 23
+ -c:a aac
+ -b:a 128k
+ "{temp}"
+ """
+
+ print("Optimizing output...")
+ run_command(cmd)
+
+ os.replace(temp, OUTPUT_VIDEO)
+
+
+def main():
+ print("===== AUTO EDITOR START =====")
+
+ check_input()
+ install_auto_editor()
+ auto_edit()
+ optimize_output()
+
+ print("✅ Editing complete:", OUTPUT_VIDEO)
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/utils/autonomous_engine.py b/utils/autonomous_engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..00dca39c9898a87b1f0506556474dcc71d8d5c95
--- /dev/null
+++ b/utils/autonomous_engine.py
@@ -0,0 +1,97 @@
+"""
+V8 Autonomous Viral Engine
+--------------------------
+Fully automated pipeline for viral video generation.
+"""
+
+import os
+from utils.transcription import transcribe_video
+from utils.highlights import detect_highlights
+from utils.clipper import create_clips
+from utils.srt import generate_srt
+from utils.render import render_subtitles
+from utils.variations import generate_hooks
+from utils.pacing import pacing_engine
+from utils.viral_scorer import score_clip
+
+
+# =====================================================
+# CORE AUTONOMOUS PIPELINE
+# =====================================================
+
+def run_autonomous_engine(video_path):
+ """
+ One-call system → full viral pipeline
+ """
+
+ print("[V8 ENGINE] Starting autonomous pipeline...")
+
+ # 1. TRANSCRIBE
+ words = transcribe_video(video_path)
+
+ # 2. DETECT HIGHLIGHTS
+ highlights = detect_highlights(words)
+
+ if not highlights:
+ return {
+ "status": "failed",
+ "reason": "No highlights detected"
+ }
+
+ # 3. AUTO CLIP GENERATION
+ clips = create_clips(video_path, highlights)
+
+ if not clips:
+ return {
+ "status": "failed",
+ "reason": "Clip generation failed"
+ }
+
+ outputs = []
+
+ # 4. PROCESS EACH CLIP AUTONOMOUSLY
+ for i, clip in enumerate(clips):
+
+ try:
+ clip_words = transcribe_video(clip)
+
+ # 5. CAPTIONS
+ srt = generate_srt(clip_words)
+
+ # 6. PACING OPTIMIZATION (NEW V8)
+ paced_clip = pacing_engine(clip, clip_words)
+
+ # 7. HOOK VARIATIONS
+ hooks = generate_hooks(clip_words)
+
+ # 8. RENDER OUTPUT
+ output_path = clip.replace(".mp4", f"_v8_{i}.mp4")
+
+ final_video = render_subtitles(
+ paced_clip,
+ srt,
+ output_path
+ )
+
+ # 9. VIRAL SCORING
+ score = score_clip(clip_words)
+
+ outputs.append({
+ "clip": final_video,
+ "score": score,
+ "hooks": hooks[:3],
+ "index": i
+ })
+
+ except Exception as e:
+ print(f"[V8 ENGINE] Clip {i} failed:", str(e))
+ continue
+
+ # 10. SORT BY VIRAL SCORE
+ outputs = sorted(outputs, key=lambda x: x["score"], reverse=True)
+
+ return {
+ "status": "completed",
+ "best_clip": outputs[0] if outputs else None,
+ "all_variants": outputs
+ }
\ No newline at end of file
diff --git a/utils/batch_queue.py b/utils/batch_queue.py
new file mode 100644
index 0000000000000000000000000000000000000000..4155e344dfc28f95be640273a7ed1b301cdec644
--- /dev/null
+++ b/utils/batch_queue.py
@@ -0,0 +1,90 @@
+import threading
+from queue import Queue
+import uuid
+import time
+
+from .job_queue import jobs, update, notify_webhook
+from .transcription import transcribe_video
+from .render import render_video
+from .highlights import detect_highlights
+
+
+batch_queue = Queue()
+
+
+def create_batch_job(video_path, webhook=None):
+
+ job_id = str(uuid.uuid4())
+
+ jobs[job_id] = {
+ "id": job_id,
+ "status": "queued",
+ "progress": 0,
+ "clips": [],
+ "video": video_path,
+ "webhook": webhook,
+ }
+
+ batch_queue.put(job_id)
+
+ return job_id
+
+
+def worker():
+
+ while True:
+
+ job_id = batch_queue.get()
+ job = jobs[job_id]
+
+ try:
+
+ update(job_id, status="processing", progress=5)
+
+ # 1. TRANSCRIBE
+ words = transcribe_video(job["video"])
+
+ update(job_id, progress=30)
+
+ # 2. DETECT HIGHLIGHTS
+ highlights = detect_highlights(words)
+
+ update(job_id, progress=50)
+
+ outputs = []
+
+ # 3. RENDER MULTIPLE CLIPS
+ for i, segment in enumerate(highlights):
+
+ start = segment[0]["start"]
+ end = segment[-1]["end"]
+
+ clip_path = render_video(job["video"], words)
+
+ outputs.append(clip_path)
+
+ update(job_id, progress=50 + int((i+1)/len(highlights)*40))
+
+ # 4. FINALIZE
+ update(job_id,
+ status="completed",
+ progress=100,
+ clips=outputs)
+
+ notify_webhook(job_id)
+
+ except Exception as e:
+
+ update(job_id,
+ status="failed",
+ error=str(e))
+
+ notify_webhook(job_id)
+
+ batch_queue.task_done()
+
+
+def start_batch_worker():
+
+ t = threading.Thread(target=worker, daemon=True)
+ t.start()
\ No newline at end of file
diff --git a/utils/broll.py b/utils/broll.py
new file mode 100644
index 0000000000000000000000000000000000000000..377300581b6a5e525f9a67bcfa3e37bc778819ab
--- /dev/null
+++ b/utils/broll.py
@@ -0,0 +1,221 @@
+"""
+broll.py
+---------------------------------------
+AI B-Roll Injection System (V8)
+
+Purpose:
+- Detect topics in transcript
+- Map topics → generic stock B-roll assets
+- Overlay or replace segments
+- Improve retention & visual variety
+
+Works in CPU-only environments.
+No external API dependency required.
+"""
+
+import os
+import random
+import subprocess
+
+
+# =====================================================
+# STOCK B-ROLL LIBRARY (LOCAL FALLBACK)
+# =====================================================
+
+DEFAULT_BROLL = {
+ "money": "assets/broll/money.mp4",
+ "success": "assets/broll/success.mp4",
+ "business": "assets/broll/business.mp4",
+ "phone": "assets/broll/phone.mp4",
+ "tech": "assets/broll/tech.mp4",
+ "people": "assets/broll/people.mp4",
+ "talking": "assets/broll/talking.mp4",
+ "default": "assets/broll/default.mp4",
+}
+
+
+# =====================================================
+# TOPIC DETECTION
+# =====================================================
+
+def detect_topic(text):
+ """
+ Simple keyword-based topic classifier.
+ Lightweight (no ML dependency).
+ """
+
+ text = text.lower()
+
+ if any(w in text for w in ["money", "rich", "income", "profit"]):
+ return "money"
+
+ if any(w in text for w in ["business", "startup", "company"]):
+ return "business"
+
+ if any(w in text for w in ["phone", "mobile", "iphone", "android"]):
+ return "phone"
+
+ if any(w in text for w in ["tech", "ai", "software", "computer"]):
+ return "tech"
+
+ if any(w in text for w in ["success", "win", "achieve"]):
+ return "success"
+
+ if any(w in text for w in ["people", "person", "man", "woman"]):
+ return "people"
+
+ if any(w in text for w in ["talk", "speak", "say"]):
+ return "talking"
+
+ return "default"
+
+
+# =====================================================
+# SEGMENT ANALYZER
+# =====================================================
+
+def extract_segments(words, segment_length=8):
+ """
+ Converts transcript words into grouped segments.
+ """
+
+ segments = []
+ buffer = []
+
+ for w in words:
+ buffer.append(w)
+
+ if len(buffer) >= segment_length:
+ segments.append(buffer)
+ buffer = []
+
+ if buffer:
+ segments.append(buffer)
+
+ return segments
+
+
+# =====================================================
+# B-ROLL MATCHING ENGINE
+# =====================================================
+
+def match_broll(segment):
+ """
+ Map transcript segment → B-roll video
+ """
+
+ text = " ".join([w["word"] for w in segment])
+ topic = detect_topic(text)
+
+ return DEFAULT_BROLL.get(topic, DEFAULT_BROLL["default"])
+
+
+# =====================================================
+# B-ROLL INSERTION (FFMPEG OVERLAY STRATEGY)
+# =====================================================
+
+def overlay_broll(base_video, broll_video, output_path, start_time, duration):
+ """
+ Overlays B-roll using ffmpeg.
+ Lightweight crossfade approach.
+ """
+
+ cmd = [
+ "ffmpeg", "-y",
+ "-i", base_video,
+ "-i", broll_video,
+ "-filter_complex",
+ f"[1:v]scale=1080:1920,format=rgba[ov];"
+ f"[0:v][ov]overlay=enable='between(t,{start_time},{start_time+duration})'",
+ "-c:v", "libx264",
+ "-preset", "ultrafast",
+ "-c:a", "copy",
+ output_path
+ ]
+
+ subprocess.run(cmd, check=True)
+
+
+# =====================================================
+# MAIN PIPELINE
+# =====================================================
+
+def insert_broll(video_path, words=None):
+ """
+ Full B-roll injection pipeline
+ """
+
+ if not words:
+ # fallback: return original video
+ return video_path
+
+ segments = extract_segments(words)
+
+ current_video = video_path
+ outputs = []
+
+ for i, segment in enumerate(segments):
+
+ broll = match_broll(segment)
+
+ output_file = f"broll_output_{i}.mp4"
+
+ start_time = segment[0]["start"]
+ duration = segment[-1]["end"] - start_time
+
+ try:
+ overlay_broll(
+ current_video,
+ broll,
+ output_file,
+ start_time,
+ duration
+ )
+
+ current_video = output_file
+ outputs.append(output_file)
+
+ except Exception as e:
+ print(f"[BROLL ERROR] Segment {i}: {e}")
+ continue
+
+ return outputs[-1] if outputs else video_path
+
+
+# =====================================================
+# ADVANCED VERSION (V8 EXTENSION)
+# =====================================================
+
+def smart_broll_engine(words, hook_boost=True):
+ """
+ Enhanced version:
+ - prioritizes hook segments
+ - increases emotional pacing
+ """
+
+ segments = extract_segments(words)
+
+ prioritized = []
+
+ for seg in segments:
+
+ text = " ".join([w["word"] for w in seg]).lower()
+
+ score = 0
+
+ if any(k in text for k in ["you", "this", "stop", "now"]):
+ score += 2
+
+ if hook_boost and len(seg) < 5:
+ score += 1
+
+ prioritized.append((score, seg))
+
+ prioritized.sort(reverse=True, key=lambda x: x[0])
+
+ final_video = None
+
+ for _, seg in prioritized:
+ final_video = insert_broll(final_video or "input.mp4", seg)
+
+ return final_video
\ No newline at end of file
diff --git a/utils/caption_director.py b/utils/caption_director.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc7aab54eaa246c93c8dd1afa8c30c1d39f7b86d
--- /dev/null
+++ b/utils/caption_director.py
@@ -0,0 +1,231 @@
+"""
+caption_director.py
+---------------------------------------
+AI Caption Intelligence System
+
+Responsibilities:
+- Convert transcript → styled caption segments
+- Decide emphasis words
+- Break text into readable chunks
+- Optimize for TikTok/Reels retention
+- Support hook-style captions
+
+INPUT:
+ words = [
+ {"word": "hello", "start": 0.2, "end": 0.5},
+ ...
+ ]
+
+OUTPUT:
+ caption blocks:
+ [
+ {
+ "text": "THIS IS CRAZY",
+ "start": 0.2,
+ "end": 2.1,
+ "style": "hook"
+ }
+ ]
+"""
+
+import re
+
+
+# -----------------------------
+# CONFIG
+# -----------------------------
+
+MAX_WORDS_PER_CAPTION = 6
+HOOK_KEYWORDS = [
+ "listen", "wait", "you", "this", "crazy",
+ "insane", "important", "stop", "secret"
+]
+
+
+# -----------------------------
+# UTIL: CLEAN TEXT
+# -----------------------------
+
+def clean_word(word):
+ return re.sub(r"[^a-zA-Z0-9']", "", word).lower()
+
+
+# -----------------------------
+# DETECT EMPHASIS
+# -----------------------------
+
+def is_emphasis(word):
+ w = clean_word(word)
+ return w in HOOK_KEYWORDS or len(word) > 8
+
+
+# -----------------------------
+# GROUP WORDS INTO CAPTIONS
+# -----------------------------
+
+def group_words(words):
+ captions = []
+ buffer = []
+
+ for w in words:
+ buffer.append(w)
+
+ if len(buffer) >= MAX_WORDS_PER_CAPTION:
+ captions.append(buffer)
+ buffer = []
+
+ if buffer:
+ captions.append(buffer)
+
+ return captions
+
+
+# -----------------------------
+# BUILD CAPTION BLOCK
+# -----------------------------
+
+def build_caption_block(group):
+ text = []
+ start = group[0]["start"]
+ end = group[-1]["end"]
+
+ emphasis_count = 0
+
+ for w in group:
+ word = w["word"]
+
+ if is_emphasis(word):
+ text.append(word.upper())
+ emphasis_count += 1
+ else:
+ text.append(word)
+
+ caption_text = " ".join(text)
+
+ style = "hook" if emphasis_count > 0 else "normal"
+
+ return {
+ "text": caption_text,
+ "start": start,
+ "end": end,
+ "style": style
+ }
+
+
+# -----------------------------
+# MAIN DIRECTOR
+# -----------------------------
+
+def caption_director(words):
+ """
+ Main caption intelligence engine
+ """
+
+ if not words:
+ return []
+
+ grouped = group_words(words)
+
+ captions = []
+
+ for group in grouped:
+ captions.append(build_caption_block(group))
+
+ return captions
+
+
+# -----------------------------
+# HOOK CAPTION GENERATOR
+# -----------------------------
+
+def generate_hook_caption(words):
+ """
+ Extracts first high-impact caption
+ """
+
+ for w in words[:20]:
+ if is_emphasis(w["word"]):
+ return {
+ "text": w["word"].upper(),
+ "start": w["start"],
+ "end": w["end"],
+ "style": "hook"
+ }
+
+ return None
+
+
+# -----------------------------
+# AUTO CAPTION PIPELINE
+# -----------------------------
+
+def auto_captions(words):
+ """
+ Full pipeline:
+ - detect hook
+ - generate captions
+ """
+
+ captions = caption_director(words)
+
+ hook = generate_hook_caption(words)
+
+ if hook:
+ captions.insert(0, hook)
+
+ return captions
+
+
+# -----------------------------
+# STYLE DECISION ENGINE
+# -----------------------------
+
+def decide_style(caption):
+ text = caption["text"]
+
+ if caption["style"] == "hook":
+ return "large_bold_center"
+
+ if len(text) > 40:
+ return "small_multi_line"
+
+ if text.isupper():
+ return "emphasis"
+
+ return "standard"
+
+
+# -----------------------------
+# EXPORT HELPERS
+# -----------------------------
+
+def format_for_render(captions):
+ """
+ Converts captions into render-friendly format
+ """
+
+ formatted = []
+
+ for c in captions:
+ formatted.append({
+ "text": c["text"],
+ "start": c["start"],
+ "end": c["end"],
+ "style": decide_style(c)
+ })
+
+ return formatted
+
+
+# -----------------------------
+# PUBLIC API
+# -----------------------------
+
+def process_captions(words):
+ """
+ Full external API
+ """
+
+ captions = auto_captions(words)
+
+ return format_for_render(captions)
\ No newline at end of file
diff --git a/utils/caption_seo.py b/utils/caption_seo.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee9ae85129368464a6b2e90e0e852fe61d70a80d
--- /dev/null
+++ b/utils/caption_seo.py
@@ -0,0 +1,7 @@
+def generate_caption(words):
+
+ keywords = list(set([w["text"].lower() for w in words[:10]]))
+
+ tags = " ".join([f"#{k}" for k in keywords[:5]])
+
+ return f"{' '.join(keywords[:8]).capitalize()}...\n\n{tags}"
\ No newline at end of file
diff --git a/utils/clipper.py b/utils/clipper.py
new file mode 100644
index 0000000000000000000000000000000000000000..554bce316083dc5461ff086d781f84440c02cda9
--- /dev/null
+++ b/utils/clipper.py
@@ -0,0 +1,130 @@
+from moviepy.editor import VideoFileClip
+import os
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+# =====================================================
+# SEGMENT NORMALIZER (CRITICAL V8 FIX)
+# =====================================================
+
+def normalize_segments(segments):
+ """
+ Accepts:
+ - dict segments: {"start": x, "end": y}
+ - list segments: [{"start":x,"end":y}, ...]
+ - tuple/list segments: [(start,end), ...]
+
+ Returns:
+ - clean list of dicts
+ """
+
+ if not segments:
+ return []
+
+ normalized = []
+
+ # Case 1: single dict
+ if isinstance(segments, dict):
+ segments = [segments]
+
+ for s in segments:
+
+ # dict format (preferred)
+ if isinstance(s, dict):
+ if "start" in s and "end" in s:
+ normalized.append({
+ "start": float(s["start"]),
+ "end": float(s["end"])
+ })
+ continue
+
+ # list/tuple format
+ if isinstance(s, (list, tuple)) and len(s) >= 2:
+ try:
+ normalized.append({
+ "start": float(s[0]),
+ "end": float(s[1])
+ })
+ except Exception:
+ continue
+
+ return normalized
+
+
+# =====================================================
+# CORE CLIP GENERATOR (SAFE VERSION)
+# =====================================================
+
+def create_clip(video_path, start, end, index):
+ """
+ Creates a single clip safely with validation
+ """
+
+ try:
+ start = float(start)
+ end = float(end)
+
+ if end <= start:
+ logger.warning(f"Invalid segment skipped: {start}-{end}")
+ return None
+
+ clip = VideoFileClip(video_path).subclip(start, end)
+
+ output = video_path.replace(
+ ".mp4",
+ f"_clip_{index}.mp4"
+ )
+
+ clip.write_videofile(
+ output,
+ codec="libx264",
+ audio_codec="aac",
+ preset="ultrafast",
+ threads=2,
+ logger=None # prevents HF log spam
+ )
+
+ return output
+
+ except Exception as e:
+ logger.error(f"Clip creation failed: {str(e)}")
+ return None
+
+
+# =====================================================
+# BATCH CLIP ENGINE (V8 AUTOCUT CORE FIX)
+# =====================================================
+
+def create_clips(video_path, segments):
+ """
+ Main entry used by V8 Highlights / AutoClip engine
+ """
+
+ segments = normalize_segments(segments)
+
+ if not segments:
+ logger.warning("No valid segments found")
+ return []
+
+ outputs = []
+
+ for i, seg in enumerate(segments):
+
+ try:
+ out = create_clip(
+ video_path,
+ seg["start"],
+ seg["end"],
+ i
+ )
+
+ if out:
+ outputs.append(out)
+
+ except Exception as e:
+ logger.error(f"Segment {i} failed: {str(e)}")
+ continue
+
+ return outputs
\ No newline at end of file
diff --git a/utils/config.py b/utils/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..8622d3ccdb090ea288a4cab5babbc8139b29de13
--- /dev/null
+++ b/utils/config.py
@@ -0,0 +1,11 @@
+from pathlib import Path
+
+
+BASE_DIR = Path(__file__).resolve().parents[1]
+
+MODEL_SIZE = "base"
+
+FONT_PATH = str(BASE_DIR / "fonts" / "TikTok-Bold.ttf")
+
+OUTPUT_DIR = str(BASE_DIR / "outputs")
+TEMP_DIR = str(BASE_DIR / "temp")
diff --git a/utils/director.py b/utils/director.py
new file mode 100644
index 0000000000000000000000000000000000000000..b476af4d3dad578ca9fd7f2c84b8823d1a314580
--- /dev/null
+++ b/utils/director.py
@@ -0,0 +1,26 @@
+def rewrite_script(words):
+ """
+ Converts raw transcript → structured viral script
+ """
+
+ text = " ".join([w["text"] for w in words])
+
+ return {
+ "hook": f"Wait—{text[:60]}...",
+ "summary": text[:200],
+ "emotion": "high" if "!" in text else "medium"
+ }
+
+
+def viral_score(engagement_curve):
+ """
+ Evaluates full video potential
+ """
+
+ if not engagement_curve:
+ return 0
+
+ peak = max(engagement_curve)
+ avg = sum(engagement_curve) / len(engagement_curve)
+
+ return (peak * 0.7) + (avg * 0.3)
\ No newline at end of file
diff --git a/utils/emotion.py b/utils/emotion.py
new file mode 100644
index 0000000000000000000000000000000000000000..eed357ef3f50ffc05c3e75f5d97ec6221997ed34
--- /dev/null
+++ b/utils/emotion.py
@@ -0,0 +1,14 @@
+def emotion_score(words):
+
+ strong_words = [
+ "amazing", "crazy", "insane", "love", "hate",
+ "never", "always", "big", "huge", "shocking"
+ ]
+
+ score = 0
+
+ for w in words:
+ if w["text"].lower() in strong_words:
+ score += 2
+
+ return min(score, 100)
\ No newline at end of file
diff --git a/utils/engagement.py b/utils/engagement.py
new file mode 100644
index 0000000000000000000000000000000000000000..8063db1a370f66b09b95ce148aae9dbc416c9a6a
--- /dev/null
+++ b/utils/engagement.py
@@ -0,0 +1,17 @@
+def simulate_retention(words):
+
+ timeline = []
+ score = 100
+
+ for i, w in enumerate(words):
+
+ # drop-off logic
+ if i > len(words) * 0.7:
+ score -= 2
+
+ if w["text"].lower() in ["boring", "slow"]:
+ score -= 10
+
+ timeline.append(max(score, 0))
+
+ return timeline
\ No newline at end of file
diff --git a/utils/ffmpeg.py b/utils/ffmpeg.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0778e12bd3cce6574a9c7d91502d979920e0b75
--- /dev/null
+++ b/utils/ffmpeg.py
@@ -0,0 +1,14 @@
+import subprocess
+
+def extract_audio(video, audio):
+ cmd = [
+ "ffmpeg",
+ "-y",
+ "-i", video,
+ "-vn",
+ "-acodec", "pcm_s16le",
+ "-ar", "16000",
+ "-ac", "1",
+ audio
+ ]
+ subprocess.run(cmd, check=True)
\ No newline at end of file
diff --git a/utils/highlights.py b/utils/highlights.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c17cf99b6b690a6d9941f8f97597124eaf56b5e
--- /dev/null
+++ b/utils/highlights.py
@@ -0,0 +1,35 @@
+def score_word(word):
+ """
+ Simple heuristic scoring:
+ - longer words slightly more important
+ - punctuation emphasis
+ """
+
+ score = len(word["text"]) * 0.1
+
+ if any(p in word["text"] for p in ["!", "?", "."]):
+ score += 1
+
+ return score
+
+
+def detect_highlights(words, threshold=0.8):
+ """
+ Groups words into highlight segments
+ """
+
+ highlights = []
+ buffer = []
+
+ for w in words:
+ if score_word(w) > threshold:
+ buffer.append(w)
+ else:
+ if buffer:
+ highlights.append(buffer)
+ buffer = []
+
+ if buffer:
+ highlights.append(buffer)
+
+ return highlights
\ No newline at end of file
diff --git a/utils/hook_detector.py b/utils/hook_detector.py
new file mode 100644
index 0000000000000000000000000000000000000000..254fa9bed943c861db78c4f03258b6c766ec66ef
--- /dev/null
+++ b/utils/hook_detector.py
@@ -0,0 +1,6 @@
+def detect_hook(words):
+ """
+ Finds strongest opening segment (first 3–7 seconds)
+ """
+
+ return [w for w in words if w["start"] <= 5]
\ No newline at end of file
diff --git a/utils/input_resolver.py b/utils/input_resolver.py
new file mode 100644
index 0000000000000000000000000000000000000000..97d809adb6d079861065c759e477facbacc34f43
--- /dev/null
+++ b/utils/input_resolver.py
@@ -0,0 +1,112 @@
+import os
+import uuid
+import requests
+import subprocess
+from urllib.parse import urlparse
+
+from utils.logger import logger
+
+DOWNLOAD_DIR = "jobs"
+os.makedirs(DOWNLOAD_DIR, exist_ok=True)
+
+
+# ---------------------------------------------------
+# URL DETECTION
+# ---------------------------------------------------
+
+def is_url(value: str):
+ try:
+ result = urlparse(value)
+ return result.scheme in ("http", "https")
+ except Exception:
+ return False
+
+
+def is_social_url(url: str):
+ domains = [
+ "youtube.com",
+ "youtu.be",
+ "tiktok.com",
+ "instagram.com",
+ "facebook.com",
+ "fb.watch",
+ "twitter.com",
+ "x.com"
+ ]
+ return any(d in url.lower() for d in domains)
+
+
+# ---------------------------------------------------
+# DIRECT FILE DOWNLOAD
+# ---------------------------------------------------
+
+def download_direct(url: str) -> str:
+ filename = f"{uuid.uuid4()}.mp4"
+ output = os.path.join(DOWNLOAD_DIR, filename)
+
+ logger.info(f"[INPUT] Direct download → {url}")
+
+ with requests.get(url, stream=True, timeout=120) as r:
+ r.raise_for_status()
+ with open(output, "wb") as f:
+ for chunk in r.iter_content(chunk_size=8192):
+ if chunk:
+ f.write(chunk)
+
+ logger.info(f"[INPUT] Saved → {output}")
+ return output
+
+
+# ---------------------------------------------------
+# SOCIAL MEDIA DOWNLOAD (yt-dlp)
+# ---------------------------------------------------
+
+def download_social(url: str) -> str:
+
+ filename = f"{uuid.uuid4()}.mp4"
+ output = os.path.join(DOWNLOAD_DIR, filename)
+
+ logger.info(f"[INPUT] Social download → {url}")
+
+ cmd = [
+ "yt-dlp",
+ "-f", "bestvideo+bestaudio/best",
+ "--merge-output-format", "mp4",
+ "-o", output,
+ url,
+ ]
+
+ subprocess.run(cmd, check=True)
+
+ if not os.path.exists(output):
+ raise Exception("yt-dlp download failed")
+
+ logger.info(f"[INPUT] Saved → {output}")
+ return output
+
+
+# ---------------------------------------------------
+# UNIVERSAL RESOLVER
+# ---------------------------------------------------
+
+def resolve_input(input_value):
+ """
+ Accepts:
+ - Upload path
+ - Direct URL
+ - YouTube/TikTok/Instagram/Facebook link
+ """
+
+ # Already local
+ if isinstance(input_value, str) and os.path.exists(input_value):
+ return input_value
+
+ # URL input
+ if isinstance(input_value, str) and is_url(input_value):
+
+ if is_social_url(input_value):
+ return download_social(input_value)
+
+ return download_direct(input_value)
+
+ raise Exception("Unsupported input type")
\ No newline at end of file
diff --git a/utils/job_queue.py b/utils/job_queue.py
new file mode 100644
index 0000000000000000000000000000000000000000..5135118af5985bc0003967a81633563359e47d13
--- /dev/null
+++ b/utils/job_queue.py
@@ -0,0 +1,194 @@
+import threading
+import uuid
+from queue import Queue
+import traceback
+import time
+
+from .logger import logger
+from .validators import validate_video
+from .transcription import transcribe_video
+from .director import rewrite_script, viral_score
+from .engagement import simulate_retention
+from .platform import adapt_platform
+from .persona import predict_audience
+from .clipper import create_clip
+
+
+# =====================================================
+# GLOBAL STATE
+# =====================================================
+
+jobs = {}
+queue = Queue()
+
+WORKER_STARTED = False
+
+
+# =====================================================
+# CREATE DIRECTOR JOB
+# =====================================================
+
+def create_job(video_path: str, webhook: str | None = None):
+
+ job_id = str(uuid.uuid4())
+
+ jobs[job_id] = {
+ "id": job_id,
+ "status": "queued",
+ "stage": "waiting",
+ "progress": 0,
+
+ "video": video_path,
+
+ # V7 INTELLIGENCE OUTPUTS
+ "viral_score": None,
+ "persona": None,
+ "hook": None,
+ "platforms": [],
+ "strategy_summary": None,
+
+ "clips": [],
+ "webhook": webhook,
+ "error": None,
+ "created_at": time.time(),
+ }
+
+ queue.put(job_id)
+
+ logger.info(f"[V7] Director job queued: {job_id}")
+
+ return job_id
+
+
+# =====================================================
+# GET JOB
+# =====================================================
+
+def get_job(job_id: str):
+ return jobs.get(job_id)
+
+
+# =====================================================
+# UPDATE HELPERS
+# =====================================================
+
+def update(job_id, **kwargs):
+ if job_id in jobs:
+ jobs[job_id].update(kwargs)
+
+
+# =====================================================
+# V7 AUTONOMOUS DIRECTOR WORKER
+# =====================================================
+
+def worker():
+
+ logger.info("[V7] Autonomous Viral Director started")
+
+ while True:
+
+ job_id = queue.get()
+ job = jobs[job_id]
+
+ try:
+
+ # -----------------------------
+ # 1. TRANSCRIPTION
+ # -----------------------------
+ update(job_id, status="processing", stage="transcribing", progress=10)
+ words = transcribe_video(job["video"])
+
+ # -----------------------------
+ # 2. SCRIPT RECONSTRUCTION
+ # -----------------------------
+ update(job_id, stage="rewriting narrative", progress=25)
+ script = rewrite_script(words)
+
+ # -----------------------------
+ # 3. AUDIENCE MODELING
+ # -----------------------------
+ persona = predict_audience(words)
+
+ # -----------------------------
+ # 4. ENGAGEMENT SIMULATION
+ # -----------------------------
+ update(job_id, stage="simulating audience", progress=45)
+ curve = simulate_retention(words)
+
+ v_score = viral_score(curve)
+
+ # -----------------------------
+ # 5. PLATFORM STRATEGY
+ # -----------------------------
+ update(job_id, stage="platform adaptation", progress=65)
+
+ tiktok = adapt_platform(script, "tiktok")
+ reels = adapt_platform(script, "reels")
+
+ platforms = ["tiktok", "reels"]
+
+ # -----------------------------
+ # 6. SINGLE BEST OUTPUT (DIRECTOR DECISION)
+ # -----------------------------
+ update(job_id, stage="rendering final cut", progress=85)
+
+ clip = create_clip(
+ job["video"],
+ words[0]["start"],
+ words[-1]["end"],
+ 0
+ )
+
+ # -----------------------------
+ # 7. FINAL DIRECTOR OUTPUT
+ # -----------------------------
+ update(job_id,
+ status="completed",
+ stage="director finished",
+ progress=100,
+
+ viral_score=v_score,
+ persona=persona,
+ hook=script["hook"],
+ platforms=platforms,
+
+ strategy_summary={
+ "curve_peak": max(curve),
+ "avg_curve": sum(curve) / len(curve),
+ "decision": "auto-selected best full narrative cut"
+ },
+
+ clips=[clip])
+
+ logger.info(f"[V7] Director output complete: {job_id}")
+
+ except Exception as e:
+
+ logger.error(traceback.format_exc())
+
+ update(job_id,
+ status="failed",
+ stage="error",
+ error=str(e))
+
+ finally:
+ queue.task_done()
+
+
+# =====================================================
+# START WORKER (SINGLETON SAFE)
+# =====================================================
+
+def start_worker():
+
+ global WORKER_STARTED
+
+ if WORKER_STARTED:
+ return
+
+ WORKER_STARTED = True
+
+ t = threading.Thread(target=worker, daemon=True)
+ t.start()
+
+ logger.info("[V7] Worker initialized")
\ No newline at end of file
diff --git a/utils/jumpcut.py b/utils/jumpcut.py
new file mode 100644
index 0000000000000000000000000000000000000000..baa61f4e20c247688c350535d3bec560afac0c73
--- /dev/null
+++ b/utils/jumpcut.py
@@ -0,0 +1,226 @@
+"""
+jumpcut.py
+---------------------------------------
+Smart Jump Cut Engine (V8)
+
+Purpose:
+- Remove silence and filler pauses
+- Improve pacing for short-form video
+- Optimize retention curve
+- Create TikTok/Reels-style fast cuts
+
+Works fully on CPU (FFmpeg-based).
+No GPU required.
+"""
+
+import subprocess
+import os
+
+
+# =====================================================
+# CONFIG
+# =====================================================
+
+TEMP_SILENCE_FILE = "silence_detect.txt"
+OUTPUT_FILE = "jumpcut_output.mp4"
+
+
+# =====================================================
+# SILENCE DETECTION
+# =====================================================
+
+def detect_silence(video_path):
+ """
+ Uses ffmpeg silencedetect to find pauses.
+ """
+
+ cmd = [
+ "ffmpeg",
+ "-i", video_path,
+ "-af", "silencedetect=noise=-30dB:d=0.4",
+ "-f", "null",
+ "-"
+ ]
+
+ result = subprocess.run(cmd, stderr=subprocess.PIPE, text=True)
+
+ return result.stderr
+
+
+# =====================================================
+# PARSE SILENCE TIMESTAMPS
+# =====================================================
+
+def parse_silence(log):
+ """
+ Extract silence start/end timestamps
+ """
+
+ silences = []
+
+ start = None
+
+ for line in log.split("\n"):
+
+ if "silence_start" in line:
+ try:
+ start = float(line.split("silence_start:")[1].strip())
+ except:
+ continue
+
+ if "silence_end" in line and start is not None:
+ try:
+ end = float(line.split("silence_end:")[1].split("|")[0].strip())
+ silences.append((start, end))
+ start = None
+ except:
+ continue
+
+ return silences
+
+
+# =====================================================
+# BUILD FILTER (JUMP CUT LOGIC)
+# =====================================================
+
+def build_filter(silences, duration):
+ """
+ Converts silence ranges into ffmpeg trim filter
+ """
+
+ if not silences:
+ return None
+
+ segments = []
+ last_end = 0
+
+ for start, end in silences:
+
+ if start > last_end:
+ segments.append((last_end, start))
+
+ last_end = end
+
+ if last_end < duration:
+ segments.append((last_end, duration))
+
+ filters = []
+
+ for i, (start, end) in enumerate(segments):
+ filters.append(
+ f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS[v{i}];"
+ f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[a{i}]"
+ )
+
+ video_concat = "".join([f"[v{i}]" for i in range(len(segments))])
+ audio_concat = "".join([f"[a{i}]" for i in range(len(segments))])
+
+ filters.append(
+ f"{video_concat}{audio_concat}concat=n={len(segments)}:v=1:a=1[outv][outa]"
+ )
+
+ return ";".join(filters)
+
+
+# =====================================================
+# CORE ENGINE
+# =====================================================
+
+def smart_jumpcut(video_path):
+ """
+ Main jump cut engine
+ """
+
+ print("[JUMPCUT] Analyzing video...")
+
+ # Step 1: detect silence
+ log = detect_silence(video_path)
+
+ silences = parse_silence(log)
+
+ print(f"[JUMPCUT] Detected silences: {len(silences)}")
+
+ # Step 2: get duration
+ probe_cmd = [
+ "ffprobe",
+ "-v", "error",
+ "-show_entries",
+ "format=duration",
+ "-of",
+ "default=noprint_wrappers=1:nokey=1",
+ video_path
+ ]
+
+ duration = float(subprocess.check_output(probe_cmd).decode().strip())
+
+ # Step 3: build filter
+ filter_complex = build_filter(silences, duration)
+
+ if not filter_complex:
+ print("[JUMPCUT] No silences found, returning original")
+ return video_path
+
+ # Step 4: render output
+ output_path = OUTPUT_FILE
+
+ cmd = [
+ "ffmpeg", "-y",
+ "-i", video_path,
+ "-filter_complex", filter_complex,
+ "-map", "[outv]",
+ "-map", "[outa]",
+ "-c:v", "libx264",
+ "-preset", "ultrafast",
+ "-c:a", "aac",
+ output_path
+ ]
+
+ print("[JUMPCUT] Rendering optimized video...")
+
+ subprocess.run(cmd, check=True)
+
+ print("[JUMPCUT] Done:", output_path)
+
+ return output_path
+
+
+# =====================================================
+# SIMPLE FAST MODE (FALLBACK)
+# =====================================================
+
+def fast_jumpcut(video_path):
+ """
+ Lightweight fallback:
+ removes only large pauses quickly
+ """
+
+ output = "fast_jumpcut.mp4"
+
+ cmd = [
+ "ffmpeg", "-y",
+ "-i", video_path,
+ "-af", "silenceremove=start_periods=1:start_threshold=-30dB:stop_periods=-1",
+ "-c:v", "libx264",
+ "-preset", "ultrafast",
+ "-c:a", "aac",
+ output
+ ]
+
+ subprocess.run(cmd, check=True)
+
+ return output
+
+
+# =====================================================
+# PUBLIC API
+# =====================================================
+
+def smart_jumpcut_engine(video_path, mode="smart"):
+ """
+ Entry point used by main.py
+ """
+
+ if mode == "fast":
+ return fast_jumpcut(video_path)
+
+ return smart_jumpcut(video_path)
\ No newline at end of file
diff --git a/utils/logger.py b/utils/logger.py
new file mode 100644
index 0000000000000000000000000000000000000000..74b7676e5b3c7a193a52cd987c617815d267460b
--- /dev/null
+++ b/utils/logger.py
@@ -0,0 +1,8 @@
+import logging
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s"
+)
+
+logger = logging.getLogger("fast_whisper_api")
\ No newline at end of file
diff --git a/utils/music.py b/utils/music.py
new file mode 100644
index 0000000000000000000000000000000000000000..f209e89f4dd8708e07b6303e468421e15b254b3d
--- /dev/null
+++ b/utils/music.py
@@ -0,0 +1,22 @@
+from moviepy.editor import AudioFileClip, CompositeAudioClip, VideoFileClip
+
+
+def add_music(video_path, music_path):
+
+ video = VideoFileClip(video_path)
+
+ music = (
+ AudioFileClip(music_path)
+ .volumex(0.15)
+ .set_duration(video.duration)
+ )
+
+ final_audio = CompositeAudioClip([video.audio, music])
+
+ video = video.set_audio(final_audio)
+
+ output = video_path.replace(".mp4", "_music.mp4")
+
+ video.write_videofile(output, codec="libx264")
+
+ return output
\ No newline at end of file
diff --git a/utils/pacing.py b/utils/pacing.py
new file mode 100644
index 0000000000000000000000000000000000000000..057160b2dedc1467144477cdfe76a60b6ffe37ad
--- /dev/null
+++ b/utils/pacing.py
@@ -0,0 +1,241 @@
+"""
+pacing.py
+---------------------------------------
+Retention & Pacing Optimization Engine (V8)
+
+Purpose:
+- Adjust video pacing for maximum retention
+- Compress slow segments
+- Emphasize high-value moments
+- Create TikTok / Reels optimized flow
+
+Works in CPU-only environments (FFmpeg-based).
+"""
+
+import subprocess
+import os
+
+
+# =====================================================
+# CONFIG
+# =====================================================
+
+OUTPUT_FILE = "pacing_optimized.mp4"
+
+SLOW_THRESHOLD = 1.25 # speed multiplier for slow segments
+FAST_THRESHOLD = 1.75 # speed multiplier for filler segments
+
+
+# =====================================================
+# BASIC SEGMENT ESTIMATION (NO ML DEPENDENCY)
+# =====================================================
+
+def estimate_segment_value(text):
+ """
+ Heuristic scoring system:
+ determines importance of spoken segment.
+ """
+
+ text = text.lower()
+
+ high_value_keywords = [
+ "you", "secret", "important", "stop",
+ "crazy", "insane", "listen", "this",
+ "money", "success", "life", "truth"
+ ]
+
+ filler_keywords = [
+ "um", "uh", "like", "you know", "so",
+ "actually", "basically"
+ ]
+
+ score = 1.0
+
+ # boost high value words
+ for w in high_value_keywords:
+ if w in text:
+ score += 0.6
+
+ # penalize filler speech
+ for w in filler_keywords:
+ if w in text:
+ score -= 0.4
+
+ return max(0.5, min(score, 2.0))
+
+
+# =====================================================
+# SPEED MAP GENERATOR
+# =====================================================
+
+def build_speed_map(words):
+ """
+ Converts transcript into pacing instructions
+ """
+
+ segments = []
+ buffer = []
+
+ for w in words:
+ buffer.append(w)
+
+ # group into micro segments
+ if len(buffer) >= 6:
+ segments.append(buffer)
+ buffer = []
+
+ if buffer:
+ segments.append(buffer)
+
+ speed_map = []
+
+ for seg in segments:
+
+ text = " ".join([w["word"] for w in seg])
+ score = estimate_segment_value(text)
+
+ start = seg[0]["start"]
+ end = seg[-1]["end"]
+
+ # decide speed
+ if score > 1.4:
+ speed = 1.0 # keep normal (important content)
+ elif score > 1.0:
+ speed = 1.15 # slight compression
+ else:
+ speed = FAST_THRESHOLD # aggressive speed-up
+
+ speed_map.append({
+ "start": start,
+ "end": end,
+ "speed": speed
+ })
+
+ return speed_map
+
+
+# =====================================================
+# FFMEG FILTER BUILDER
+# =====================================================
+
+def build_filter(speed_map):
+ """
+ Creates FFmpeg atempo + setpts filter chain
+ """
+
+ filters = []
+
+ for i, seg in enumerate(speed_map):
+
+ start = seg["start"]
+ end = seg["end"]
+ speed = seg["speed"]
+
+ # video speed
+ filters.append(
+ f"[0:v]trim=start={start}:end={end},setpts=PTS/{speed}[v{i}]"
+ )
+
+ # audio speed
+ filters.append(
+ f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS,"
+ f"atempo={speed}[a{i}]"
+ )
+
+ v_streams = "".join([f"[v{i}]" for i in range(len(speed_map))])
+ a_streams = "".join([f"[a{i}]" for i in range(len(speed_map))])
+
+ filters.append(
+ f"{v_streams}{a_streams}concat=n={len(speed_map)}:v=1:a=1[outv][outa]"
+ )
+
+ return ";".join(filters)
+
+
+# =====================================================
+# MAIN ENGINE
+# =====================================================
+
+def optimize_pacing(video_path, words=None):
+ """
+ Main entry point for V8 pacing system
+ """
+
+ print("[PACING] Starting optimization...")
+
+ if not words:
+ print("[PACING] No transcript provided — returning original video")
+ return video_path
+
+ # Step 1: build speed map
+ speed_map = build_speed_map(words)
+
+ print(f"[PACING] Segments: {len(speed_map)}")
+
+ # Step 2: build ffmpeg filter
+ filter_complex = build_filter(speed_map)
+
+ output_path = OUTPUT_FILE
+
+ # Step 3: render optimized video
+ cmd = [
+ "ffmpeg", "-y",
+ "-i", video_path,
+ "-filter_complex", filter_complex,
+ "-map", "[outv]",
+ "-map", "[outa]",
+ "-c:v", "libx264",
+ "-preset", "ultrafast",
+ "-c:a", "aac",
+ output_path
+ ]
+
+ subprocess.run(cmd, check=True)
+
+ print("[PACING] Done:", output_path)
+
+ return output_path
+
+
+# =====================================================
+# LIGHTWEIGHT MODE (FAST FALLBACK)
+# =====================================================
+
+def fast_pacing(video_path):
+ """
+ Simple fallback: global speed-up only
+ """
+
+ output = "fast_pacing.mp4"
+
+ cmd = [
+ "ffmpeg", "-y",
+ "-i", video_path,
+ "-filter_complex",
+ "[0:v]setpts=0.92*PTS[v];[0:a]atempo=1.08[a]",
+ "-map", "[v]",
+ "-map", "[a]",
+ "-c:v", "libx264",
+ "-preset", "ultrafast",
+ "-c:a", "aac",
+ output
+ ]
+
+ subprocess.run(cmd, check=True)
+
+ return output
+
+
+# =====================================================
+# PUBLIC API
+# =====================================================
+
+def pacing_engine(video_path, words=None, mode="smart"):
+ """
+ Entry point used by main.py
+ """
+
+ if mode == "fast":
+ return fast_pacing(video_path)
+
+ return optimize_pacing(video_path, words)
\ No newline at end of file
diff --git a/utils/persona.py b/utils/persona.py
new file mode 100644
index 0000000000000000000000000000000000000000..08f49ff9f60214e601591690baa6e7dec3031548
--- /dev/null
+++ b/utils/persona.py
@@ -0,0 +1,9 @@
+def predict_audience(words):
+
+ if len(words) < 50:
+ return "short_attention_gen_z"
+
+ if any(w["text"].lower() in ["money", "success"] for w in words):
+ return "motivated_entrepreneur"
+
+ return "general"
\ No newline at end of file
diff --git a/utils/platform.py b/utils/platform.py
new file mode 100644
index 0000000000000000000000000000000000000000..5817bff53e26afd231e8c8e7c7879bfaf3ef48ce
--- /dev/null
+++ b/utils/platform.py
@@ -0,0 +1,15 @@
+def adapt_platform(script, platform="tiktok"):
+
+ if platform == "tiktok":
+ return {
+ "hook": "🔥 " + script["hook"],
+ "style": "fast_cut"
+ }
+
+ if platform == "reels":
+ return {
+ "hook": script["hook"],
+ "style": "cinematic"
+ }
+
+ return script
\ No newline at end of file
diff --git a/utils/render.py b/utils/render.py
new file mode 100644
index 0000000000000000000000000000000000000000..97673c4c7f73e1787d67a4b096e16dc2efd0bc46
--- /dev/null
+++ b/utils/render.py
@@ -0,0 +1,73 @@
+import os
+import subprocess
+import uuid
+from utils.logger import logger
+
+OUTPUT_DIR = "jobs"
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+
+
+def render_subtitles(
+ video_path: str,
+ srt_text: str,
+ output_path: str | None = None,
+):
+ """
+ Universal subtitle renderer for V7.
+
+ Supports:
+ - API render
+ - UI render
+ - Batch jobs
+ - Worker queue
+ """
+
+ if output_path is None:
+ output_path = os.path.join(
+ OUTPUT_DIR,
+ f"{uuid.uuid4()}_render.mp4"
+ )
+
+ # --------------------------------------------------
+ # Write SRT file
+ # --------------------------------------------------
+
+ srt_path = output_path.replace(".mp4", ".srt")
+
+ with open(srt_path, "w", encoding="utf-8") as f:
+ f.write(srt_text)
+
+ logger.info(f"[RENDER] SRT saved → {srt_path}")
+
+ # --------------------------------------------------
+ # FFmpeg Subtitle Burn
+ # --------------------------------------------------
+
+ cmd = [
+ "ffmpeg",
+ "-y",
+ "-i", video_path,
+ "-vf", f"subtitles={srt_path}",
+ "-c:a", "copy",
+ output_path,
+ ]
+
+ logger.info("[RENDER] Running ffmpeg render")
+
+ process = subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+
+ if process.returncode != 0:
+ logger.error(process.stderr)
+ raise Exception("FFmpeg render failed")
+
+ if not os.path.exists(output_path):
+ raise Exception("Rendered file missing")
+
+ logger.info(f"[RENDER] Output → {output_path}")
+
+ return output_path
\ No newline at end of file
diff --git a/utils/retention.py b/utils/retention.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e05d16a4a5130ab400fc351103bdca8c10054ae
--- /dev/null
+++ b/utils/retention.py
@@ -0,0 +1,22 @@
+def predict_retention(words):
+
+ if not words:
+ return 0
+
+ duration = words[-1]["end"] - words[0]["start"]
+
+ score = 100
+
+ # too long → drop-off risk
+ if duration > 30:
+ score -= 30
+
+ # too short → no engagement
+ if duration < 6:
+ score -= 20
+
+ # weak opening signal
+ if "..." in words[0]["text"]:
+ score -= 10
+
+ return max(0, score)
\ No newline at end of file
diff --git a/utils/silence.py b/utils/silence.py
new file mode 100644
index 0000000000000000000000000000000000000000..f13c4c36bcf82c1f3484f0671f466976769b1d20
--- /dev/null
+++ b/utils/silence.py
@@ -0,0 +1,17 @@
+import subprocess
+
+
+def remove_silence(input_video, output_video):
+
+ cmd = [
+ "ffmpeg",
+ "-y",
+ "-i", input_video,
+ "-af",
+ "silenceremove=start_periods=1:start_threshold=-40dB",
+ output_video
+ ]
+
+ subprocess.run(cmd, check=True)
+
+ return output_video
\ No newline at end of file
diff --git a/utils/srt.py b/utils/srt.py
new file mode 100644
index 0000000000000000000000000000000000000000..44bc931df4c4bb09dccef9fd7b713dccc55ff007
--- /dev/null
+++ b/utils/srt.py
@@ -0,0 +1,188 @@
+from typing import List, Dict, Any, Union
+
+
+# =====================================================
+# PUBLIC API (USED BY MAIN.PY)
+# =====================================================
+
+def generate_srt(data: List[Dict[str, Any]]) -> str:
+ """
+ Universal SRT generator for:
+ - Whisper word output (V1–V7)
+ - Highlight segments (start/end grouped words)
+ - Mixed/partial structures
+
+ Expected input formats:
+ 1. Word-level:
+ {"text": "...", "start": float, "end": float}
+
+ 2. Segment-level:
+ [{"start": float, "end": float, "text": "..."}]
+
+ Returns:
+ SRT formatted string
+ """
+
+ if not data:
+ return ""
+
+ normalized = _normalize_input(data)
+ return _build_srt(normalized)
+
+
+# =====================================================
+# NORMALIZATION LAYER (CRITICAL FOR V1–V7 COMPATIBILITY)
+# =====================================================
+
+def _normalize_input(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """
+ Converts any supported structure into unified subtitle blocks
+ """
+
+ normalized = []
+
+ # CASE 1: Already segment-based
+ if isinstance(data[0], dict) and "start" in data[0] and "end" in data[0] and "text" in data[0]:
+ for item in data:
+ normalized.append({
+ "start": float(item.get("start", 0)),
+ "end": float(item.get("end", 0)),
+ "text": str(item.get("text", "")).strip()
+ })
+ return normalized
+
+ # CASE 2: Whisper word-level output
+ buffer = []
+ current_start = None
+
+ for w in data:
+
+ if not isinstance(w, dict):
+ continue
+
+ text = str(w.get("text", "")).strip()
+ start = w.get("start", None)
+ end = w.get("end", None)
+
+ if start is None or end is None:
+ continue
+
+ if current_start is None:
+ current_start = start
+
+ buffer.append(text)
+
+ # Chunking strategy: group every ~8–12 words
+ if len(buffer) >= 10:
+
+ normalized.append({
+ "start": current_start,
+ "end": end,
+ "text": " ".join(buffer)
+ })
+
+ buffer = []
+ current_start = None
+
+ # flush remaining buffer
+ if buffer:
+ normalized.append({
+ "start": current_start or 0,
+ "end": data[-1].get("end", 0),
+ "text": " ".join(buffer)
+ })
+
+ return normalized
+
+
+# =====================================================
+# SRT BUILDER
+# =====================================================
+
+def _build_srt(items: List[Dict[str, Any]]) -> str:
+ """
+ Converts normalized subtitle blocks → SRT format
+ """
+
+ output = []
+ index = 1
+
+ for item in items:
+
+ start = _format_time(item["start"])
+ end = _format_time(item["end"])
+ text = _clean_text(item["text"])
+
+ if not text:
+ continue
+
+ output.append(f"{index}")
+ output.append(f"{start} --> {end}")
+ output.append(f"{text}")
+ output.append("") # blank line separator
+
+ index += 1
+
+ return "\n".join(output).strip()
+
+
+# =====================================================
+# TIME FORMATTER
+# =====================================================
+
+def _format_time(seconds: Union[int, float]) -> str:
+ """
+ Converts seconds → SRT timestamp format
+ HH:MM:SS,mmm
+ """
+
+ try:
+ seconds = float(seconds)
+ except:
+ seconds = 0.0
+
+ hrs = int(seconds // 3600)
+ mins = int((seconds % 3600) // 60)
+ secs = int(seconds % 60)
+ ms = int((seconds - int(seconds)) * 1000)
+
+ return f"{hrs:02}:{mins:02}:{secs:02},{ms:03}"
+
+
+# =====================================================
+# TEXT CLEANER (IMPORTANT FOR VIDEO RENDERING STABILITY)
+# =====================================================
+
+def _clean_text(text: str) -> str:
+ """
+ Sanitizes subtitle text for rendering engines
+ """
+
+ if not text:
+ return ""
+
+ text = text.replace("\n", " ")
+ text = text.replace("\r", " ")
+
+ # remove excessive spacing
+ text = " ".join(text.split())
+
+ return text.strip()
+
+
+# =====================================================
+# OPTIONAL DEBUG HELPER (SAFE IN PRODUCTION)
+# =====================================================
+
+def debug_srt(data: List[Dict[str, Any]]) -> dict:
+ """
+ Returns structured preview for debugging pipelines
+ """
+
+ normalized = _normalize_input(data)
+
+ return {
+ "blocks": len(normalized),
+ "sample": normalized[:3],
+ "duration": normalized[-1]["end"] if normalized else 0
+ }
\ No newline at end of file
diff --git a/utils/storage.py b/utils/storage.py
new file mode 100644
index 0000000000000000000000000000000000000000..289ca0522e6bd19fd2f30840d868253030ed6a62
--- /dev/null
+++ b/utils/storage.py
@@ -0,0 +1,18 @@
+import os
+import time
+
+MAX_AGE = 60 * 60 # 1 hour
+
+
+def cleanup(folder="jobs"):
+
+ now = time.time()
+
+ for f in os.listdir(folder):
+
+ path = os.path.join(folder, f)
+
+ if os.path.isfile(path):
+
+ if now - os.path.getmtime(path) > MAX_AGE:
+ os.remove(path)
\ No newline at end of file
diff --git a/utils/strategist.py b/utils/strategist.py
new file mode 100644
index 0000000000000000000000000000000000000000..38b107f11fe951ccf77c4be1f213f931931ca891
--- /dev/null
+++ b/utils/strategist.py
@@ -0,0 +1,29 @@
+def rewrite_hook(text):
+ """
+ Simulates GPT-style hook optimization
+ """
+
+ if not text:
+ return "You won’t believe this..."
+
+ return f"Wait—{text.strip().capitalize()}"
+
+
+def strategy_score(words):
+ """
+ Combines virality + structure + hook strength
+ """
+
+ base_score = len(words)
+
+ text = " ".join([w["text"] for w in words]).lower()
+
+ # Hook boost
+ if any(k in text for k in ["you", "stop", "wait", "imagine"]):
+ base_score += 30
+
+ # Emotional intensity
+ if "!" in text:
+ base_score += 15
+
+ return min(100, base_score)
\ No newline at end of file
diff --git a/utils/subtitle_engine.py b/utils/subtitle_engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..20c0cbc7214423856d77f7cd7bccbe7b7cec12ae
--- /dev/null
+++ b/utils/subtitle_engine.py
@@ -0,0 +1,24 @@
+from moviepy.editor import TextClip
+from .config import FONT_PATH
+
+
+def word_clip(word):
+
+ duration = word["end"] - word["start"]
+
+ clip = TextClip(
+ word["text"],
+ font=FONT_PATH,
+ fontsize=80,
+ color="white",
+ stroke_color="black",
+ stroke_width=4,
+ method="caption",
+ size=(900, None),
+ )
+
+ return (
+ clip.set_start(word["start"])
+ .set_duration(duration)
+ .set_position(("center", "center"))
+ )
\ No newline at end of file
diff --git a/utils/transcription.py b/utils/transcription.py
new file mode 100644
index 0000000000000000000000000000000000000000..dcba5a598e453b27d8bdd2f73a7337054a072497
--- /dev/null
+++ b/utils/transcription.py
@@ -0,0 +1,44 @@
+from faster_whisper import WhisperModel
+from .config import MODEL_SIZE, TEMP_DIR
+from .ffmpeg import extract_audio
+import os
+
+model = None
+
+os.makedirs(TEMP_DIR, exist_ok=True)
+
+
+def get_model():
+ global model
+
+ if model is None:
+ model = WhisperModel(
+ MODEL_SIZE,
+ device="cpu",
+ compute_type="int8"
+ )
+
+ return model
+
+
+def transcribe_video(video):
+
+ audio = f"{TEMP_DIR}/audio.wav"
+ extract_audio(video, audio)
+
+ segments, _ = get_model().transcribe(
+ audio,
+ word_timestamps=True
+ )
+
+ words = []
+
+ for seg in segments:
+ for w in seg.words:
+ words.append({
+ "start": w.start,
+ "end": w.end,
+ "text": w.word.strip()
+ })
+
+ return words
diff --git a/utils/validators.py b/utils/validators.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1c5b8039b01af8bc8a3203e93a70953cb1a535a
--- /dev/null
+++ b/utils/validators.py
@@ -0,0 +1,14 @@
+import os
+
+ALLOWED = [".mp4", ".mov", ".mkv"]
+
+
+def validate_video(path):
+
+ if not os.path.exists(path):
+ raise Exception("File not found")
+
+ ext = os.path.splitext(path)[1].lower()
+
+ if ext not in ALLOWED:
+ raise Exception("Unsupported video format")
\ No newline at end of file
diff --git a/utils/variations.py b/utils/variations.py
new file mode 100644
index 0000000000000000000000000000000000000000..06d03abc6845a969006f3db1191ea403eea3dbc5
--- /dev/null
+++ b/utils/variations.py
@@ -0,0 +1,238 @@
+"""
+variations.py
+---------------------------------------
+Hook & Script Variation Engine (V8)
+
+Purpose:
+- Generate multiple viral hooks
+- Create alternative script directions
+- Support A/B testing of edits
+- Enable Multi-Version Render pipeline
+
+Works fully CPU-only.
+No external API required.
+"""
+
+import random
+import hashlib
+
+
+# =====================================================
+# HOOK TEMPLATES (VIRAL PATTERNS)
+# =====================================================
+
+HOOK_PATTERNS = [
+ "You are not going to believe this...",
+ "This is what nobody tells you about {}",
+ "Stop scrolling if you want to understand {}",
+ "The truth about {} will shock you",
+ "Most people get {} wrong",
+ "If you understand this, your {} changes forever",
+ "I wish I knew this before about {}",
+ "This is how you actually win at {}",
+ "Everyone is lying about {}",
+ "Watch this before it's too late..."
+]
+
+
+# =====================================================
+# TEXT CLEANER
+# =====================================================
+
+def extract_keywords(words):
+ """
+ Extract simple keyword candidates from transcript
+ """
+
+ freq = {}
+
+ for w in words:
+ word = w["word"].lower().strip()
+ if len(word) < 3:
+ continue
+ freq[word] = freq.get(word, 0) + 1
+
+ sorted_words = sorted(freq.items(), key=lambda x: x[1], reverse=True)
+
+ return [w[0] for w in sorted_words[:5]]
+
+
+# =====================================================
+# HOOK GENERATION
+# =====================================================
+
+def generate_hooks(words, count=5):
+ """
+ Generate multiple viral hooks from transcript
+ """
+
+ keywords = extract_keywords(words)
+
+ hooks = []
+
+ for i in range(count):
+
+ template = random.choice(HOOK_PATTERNS)
+
+ keyword = random.choice(keywords) if keywords else "this"
+
+ try:
+ hook = template.format(keyword)
+ except:
+ hook = template
+
+ hooks.append(hook)
+
+ return hooks
+
+
+# =====================================================
+# SCRIPT VARIATION ENGINE
+# =====================================================
+
+def generate_script_variations(words):
+ """
+ Creates alternative narrative directions
+ """
+
+ base_text = " ".join([w["word"] for w in words])
+
+ variations = []
+
+ variations.append({
+ "style": "direct",
+ "script": base_text
+ })
+
+ variations.append({
+ "style": "emotional",
+ "script": "Imagine this... " + base_text
+ })
+
+ variations.append({
+ "style": "urgent",
+ "script": "You need to hear this: " + base_text
+ })
+
+ variations.append({
+ "style": "story",
+ "script": "Let me tell you something important. " + base_text
+ })
+
+ return variations
+
+
+# =====================================================
+# CAPTION VARIATION ENGINE
+# =====================================================
+
+def generate_caption_variations(captions):
+ """
+ Creates multiple caption styles for rendering
+ """
+
+ styles = []
+
+ for c in captions:
+
+ styles.append({
+ "style": "bold_center",
+ "text": c["text"].upper()
+ })
+
+ styles.append({
+ "style": "minimal",
+ "text": c["text"]
+ })
+
+ styles.append({
+ "style": "emphasis_words",
+ "text": highlight_keywords(c["text"])
+ })
+
+ return styles
+
+
+# =====================================================
+# KEYWORD HIGHLIGHTER
+# =====================================================
+
+def highlight_keywords(text):
+ """
+ Emphasizes strong words in captions
+ """
+
+ keywords = ["you", "this", "stop", "now", "secret", "important"]
+
+ words = text.split()
+
+ output = []
+
+ for w in words:
+ if w.lower() in keywords:
+ output.append(w.upper())
+ else:
+ output.append(w)
+
+ return " ".join(output)
+
+
+# =====================================================
+# MULTI VERSION RENDER ENGINE
+# =====================================================
+
+def generate_render_variations(video_path, hooks=None):
+ """
+ Creates multiple render variants metadata
+ (actual rendering happens in render.py)
+ """
+
+ if not hooks:
+ hooks = ["Hook 1", "Hook 2", "Hook 3"]
+
+ outputs = []
+
+ for i, hook in enumerate(hooks):
+
+ outputs.append({
+ "version": i + 1,
+ "hook": hook,
+ "output_file": f"render_variant_{i+1}.mp4"
+ })
+
+ return outputs
+
+
+# =====================================================
+# DETERMINISTIC VIRAL HASH
+# =====================================================
+
+def viral_signature(text):
+ """
+ Creates deterministic ID for A/B testing consistency
+ """
+
+ return hashlib.md5(text.encode()).hexdigest()[:10]
+
+
+# =====================================================
+# PUBLIC API
+# =====================================================
+
+def generate_hooks_only(words):
+ return generate_hooks(words)
+
+
+def generate_full_variations(words):
+ """
+ Full pipeline for V8 Multi-Version system
+ """
+
+ hooks = generate_hooks(words)
+
+ scripts = generate_script_variations(words)
+
+ return {
+ "hooks": hooks,
+ "scripts": scripts
+ }
\ No newline at end of file
diff --git a/utils/vertical.py b/utils/vertical.py
new file mode 100644
index 0000000000000000000000000000000000000000..87374164e2c3e96991a5728e40f8d8d1b83d6a42
--- /dev/null
+++ b/utils/vertical.py
@@ -0,0 +1,25 @@
+from moviepy.editor import VideoFileClip
+
+
+def vertical_crop(video_path):
+
+ clip = VideoFileClip(video_path)
+
+ w, h = clip.size
+ target_ratio = 9 / 16
+
+ new_width = int(h * target_ratio)
+
+ x_center = w / 2
+
+ cropped = clip.crop(
+ x_center=x_center,
+ width=new_width,
+ height=h
+ )
+
+ output = video_path.replace(".mp4", "_vertical.mp4")
+
+ cropped.write_videofile(output, codec="libx264")
+
+ return output
\ No newline at end of file
diff --git a/utils/viral_scorer.py b/utils/viral_scorer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c39ce00a1cd16301765bab739a3ff697a289eca1
--- /dev/null
+++ b/utils/viral_scorer.py
@@ -0,0 +1,46 @@
+def score_clip(words):
+ """
+ Returns virality score (0–100)
+ based on speech + structure signals
+ """
+
+ if not words:
+ return 0
+
+ text = " ".join([w["text"] for w in words]).lower()
+
+ score = 0
+
+ # -----------------------------
+ # HOOK SIGNAL (first words)
+ # -----------------------------
+ hook_words = ["you", "imagine", "stop", "listen", "this", "never", "why"]
+
+ if any(h in text[:50] for h in hook_words):
+ score += 25
+
+ # -----------------------------
+ # EMOTION SIGNAL
+ # -----------------------------
+ exclamations = sum(1 for w in words if "!" in w["text"])
+ score += min(exclamations * 5, 20)
+
+ # -----------------------------
+ # LENGTH OPTIMIZATION
+ # -----------------------------
+ duration = words[-1]["end"] - words[0]["start"]
+
+ if 6 <= duration <= 25:
+ score += 25
+ elif duration < 6:
+ score -= 10
+ else:
+ score -= 5
+
+ # -----------------------------
+ # WORD DENSITY
+ # -----------------------------
+ score += min(len(words) / 2, 20)
+
+ # Clamp
+ return max(0, min(100, score))
\ No newline at end of file
diff --git a/utils/zoom_tracker.py b/utils/zoom_tracker.py
new file mode 100644
index 0000000000000000000000000000000000000000..08525418ed13d724362e6fa49db1508ecc6627c8
--- /dev/null
+++ b/utils/zoom_tracker.py
@@ -0,0 +1,11 @@
+def zoom_intensity(score):
+ """
+ Maps viral score to zoom level
+ """
+
+ if score > 80:
+ return 1.2 # aggressive zoom
+ elif score > 50:
+ return 1.1
+ else:
+ return 1.0
\ No newline at end of file
diff --git a/voices/af.pt b/voices/af.pt
new file mode 100644
index 0000000000000000000000000000000000000000..a67cad519413efaf099c768ed3ba6ed7bac6bfb4
--- /dev/null
+++ b/voices/af.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:03d6457d8d31306c7c4e7afc5040b446e6cb556b40fd80d07fbdbc8dc75d300c
+size 131
diff --git a/voices/af_bella.pt b/voices/af_bella.pt
new file mode 100644
index 0000000000000000000000000000000000000000..9221cbb248feafe5b575b3a4c2297f7da9f10a23
--- /dev/null
+++ b/voices/af_bella.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:04339167efe7fe6dedf2eedfcdd357bbe3f4451619fcb67c0feea0708da0bb31
+size 131
diff --git a/voices/af_nicole.pt b/voices/af_nicole.pt
new file mode 100644
index 0000000000000000000000000000000000000000..3a2cc59923702171df2e4fe7720930b7e08fe93b
--- /dev/null
+++ b/voices/af_nicole.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ca02cd41d325f5445b14805a3ec6985cae55a21cec91635f8c2038a5b288383
+size 131
diff --git a/voices/af_sarah.pt b/voices/af_sarah.pt
new file mode 100644
index 0000000000000000000000000000000000000000..01586c4caa5ca7db52b54c0cb721452d7d32f66e
--- /dev/null
+++ b/voices/af_sarah.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2a6f3c43e3645896c7f0d57968f23d5ef5860b764bd4abb6d75076a1e7fda2e9
+size 131
diff --git a/voices/af_sky.pt b/voices/af_sky.pt
new file mode 100644
index 0000000000000000000000000000000000000000..7c39e9bcd1590004f8761adc0bb103e0df338051
--- /dev/null
+++ b/voices/af_sky.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab87dbbf5b86022f6c01aed43cb4bcd221fcb95241c1d1e67e27dde2978a59c5
+size 131
diff --git a/voices/am_adam.pt b/voices/am_adam.pt
new file mode 100644
index 0000000000000000000000000000000000000000..b3300d7399123b5bc5f16e44bbba64840a6ba4cf
--- /dev/null
+++ b/voices/am_adam.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fd49625a3f7fba1b2ca0cd84b817bc0531fdc94bd256953c3636e8afb09df2f4
+size 131
diff --git a/voices/am_michael.pt b/voices/am_michael.pt
new file mode 100644
index 0000000000000000000000000000000000000000..cf80006e6e345586272f5881908a5f2972e4e664
--- /dev/null
+++ b/voices/am_michael.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bba4bc7701cefde36070eaf65d20a689dae85282efc79c8d5770933a56c2ff76
+size 131
diff --git a/voices/bf_emma.pt b/voices/bf_emma.pt
new file mode 100644
index 0000000000000000000000000000000000000000..3379731d55b1234c1f4594b8065ac398894d7775
--- /dev/null
+++ b/voices/bf_emma.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d5c84df49c195eb467a7a2e3f5f5e25624808f751db021fd5776495e1b9bb585
+size 131
diff --git a/voices/bf_isabella.pt b/voices/bf_isabella.pt
new file mode 100644
index 0000000000000000000000000000000000000000..861e4bbf104fdee879c39bdd952d3ba24ccc6617
--- /dev/null
+++ b/voices/bf_isabella.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cc23980551bb88178169bbdcfe3f10d69fc8b8ded9fdb0ec1123a3294b95266d
+size 131
diff --git a/voices/bm_george.pt b/voices/bm_george.pt
new file mode 100644
index 0000000000000000000000000000000000000000..9b2bc7a75b3ddbb8cd7e11aec17f6358dbee8239
--- /dev/null
+++ b/voices/bm_george.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27dff07c3aaf98f311475d2d3212f377325ab49b0ff566100424a3301252bbb2
+size 131
diff --git a/voices/bm_lewis.pt b/voices/bm_lewis.pt
new file mode 100644
index 0000000000000000000000000000000000000000..d2e59aa976fd20aae5bcee04ea0081198a849599
--- /dev/null
+++ b/voices/bm_lewis.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c9d076bfb89ada363657fea4d40d67f4174929dc771fdc245547397714fd33dc
+size 131
diff --git a/weights/.gitkeep b/weights/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/weights/kokoro-quant.onnx b/weights/kokoro-quant.onnx
new file mode 100644
index 0000000000000000000000000000000000000000..a34a4bec1c44b37756b2ef7622da8ce8ddc53380
--- /dev/null
+++ b/weights/kokoro-quant.onnx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6fa3c2d4e9dabfb2929b555c425c1c56f8333d46c47de8668de365a3d158de83
+size 134
diff --git a/weights/kokoro-v0_19.onnx b/weights/kokoro-v0_19.onnx
new file mode 100644
index 0000000000000000000000000000000000000000..f17da6d7150a4560e9b0b8e1cff29947448908dc
--- /dev/null
+++ b/weights/kokoro-v0_19.onnx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d6bd36e46e7e6e1f118d2ae1a8ff807a24c06cdae309f3c4f34230ad9eb37652
+size 134