Spaces:
Sleeping
Sleeping
Yuvvan Talreja commited on
Commit ·
12089c6
1
Parent(s): 39f82ec
Initial Deployment
Browse files- Dockerfile +23 -0
- app.py +7 -0
- app_fastapi.py +1 -0
- requirements.txt +17 -0
- static/script.js +2173 -0
- static/styles.css +1889 -0
- templates/index.html +315 -0
- topic_segmenter.py +519 -0
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies including FFmpeg
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
ffmpeg \
|
| 8 |
+
build-essential \
|
| 9 |
+
&& apt-get clean \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Copy requirements first for better caching
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Copy the rest of the application
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
# Expose the port
|
| 20 |
+
EXPOSE 5000
|
| 21 |
+
|
| 22 |
+
# Run using Gunicorn
|
| 23 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
app.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app_fastapi import app
|
| 2 |
+
|
| 3 |
+
# This file is used for Hugging Face Spaces deployment
|
| 4 |
+
# Spaces will automatically detect this app.py file and serve the FastAPI app
|
| 5 |
+
|
| 6 |
+
# For local development, you can run:
|
| 7 |
+
# uvicorn app_fastapi:app --host 0.0.0.0 --port 8000
|
app_fastapi.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask>=2.0.0
|
| 2 |
+
gunicorn>=20.0.0
|
| 3 |
+
numpy>=1.19.0
|
| 4 |
+
scikit-learn>=1.0.0
|
| 5 |
+
nltk>=3.6.0
|
| 6 |
+
torch>=1.9.0
|
| 7 |
+
transformers>=4.0.0
|
| 8 |
+
keybert>=0.5.0
|
| 9 |
+
spacy>=3.0.0
|
| 10 |
+
librosa>=0.8.0
|
| 11 |
+
soundfile>=0.10.0
|
| 12 |
+
thinc>=8.0.0
|
| 13 |
+
fastapi>=0.68.0
|
| 14 |
+
uvicorn>=0.15.0
|
| 15 |
+
jinja2>=3.0.0
|
| 16 |
+
python-multipart>=0.0.5
|
| 17 |
+
pydantic>=1.8.2
|
static/script.js
ADDED
|
@@ -0,0 +1,2173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { pipeline, env } from 'https://unpkg.com/@xenova/transformers@2.16.0/dist/transformers.min.js';
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
env.allowLocalModels = false;
|
| 5 |
+
env.useBrowserCache = true;
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
const videoPlayer = document.getElementById('videoPlayer');
|
| 9 |
+
const fileInput = document.getElementById('fileInput');
|
| 10 |
+
const transcribeButton = document.getElementById('transcribeButton');
|
| 11 |
+
const analyzeButton = document.getElementById('analyzeButton');
|
| 12 |
+
const transcriptionDiv = document.getElementById('transcription');
|
| 13 |
+
const topicResultsDiv = document.getElementById('topicResults');
|
| 14 |
+
const statusDiv = document.getElementById('status');
|
| 15 |
+
const modelSelector = document.getElementById('modelSelector');
|
| 16 |
+
const modelStatus = document.getElementById('modelStatus');
|
| 17 |
+
|
| 18 |
+
let whisperPipeline = null;
|
| 19 |
+
let transcriptText = '';
|
| 20 |
+
let mindmap;
|
| 21 |
+
|
| 22 |
+
// Original transcribeAudio function definition
|
| 23 |
+
async function transcribeAudio(audioData) {
|
| 24 |
+
try {
|
| 25 |
+
statusDiv.textContent = 'Transcribing audio...';
|
| 26 |
+
transcriptionDiv.textContent = '';
|
| 27 |
+
|
| 28 |
+
// Make sure we're using the correct progress element
|
| 29 |
+
const progressBarFill = document.querySelector('.progress-bar-fill');
|
| 30 |
+
if (progressBarFill) {
|
| 31 |
+
progressBarFill.style.width = '0%';
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Check if server-side transcription is possible
|
| 35 |
+
const selectedModel = modelSelector.value;
|
| 36 |
+
if (selectedModel !== 'browser') {
|
| 37 |
+
// Convert audio data to base64
|
| 38 |
+
const base64Audio = arrayBufferToBase64(audioData.buffer);
|
| 39 |
+
|
| 40 |
+
// Send to server as JSON
|
| 41 |
+
const response = await fetch('/transcribe', {
|
| 42 |
+
method: 'POST',
|
| 43 |
+
headers: {
|
| 44 |
+
'Content-Type': 'application/json'
|
| 45 |
+
},
|
| 46 |
+
body: JSON.stringify({
|
| 47 |
+
audio: base64Audio,
|
| 48 |
+
model: selectedModel
|
| 49 |
+
})
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
if (!response.ok) {
|
| 53 |
+
throw new Error('Server transcription failed');
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const result = await response.json();
|
| 57 |
+
transcriptText = result.transcript || result.text || '';
|
| 58 |
+
|
| 59 |
+
// Process the transcript to create segments for display
|
| 60 |
+
if (transcriptText) {
|
| 61 |
+
const segments = [];
|
| 62 |
+
// Extract timestamps and text using regex
|
| 63 |
+
const timestampPattern = /\[(\d+)\](.*?)(?=\[\d+\]|$)/g;
|
| 64 |
+
let match;
|
| 65 |
+
|
| 66 |
+
while ((match = timestampPattern.exec(transcriptText)) !== null) {
|
| 67 |
+
segments.push({
|
| 68 |
+
start: parseInt(match[1]),
|
| 69 |
+
text: match[2].trim()
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// If no timestamps were found, fallback to displaying the whole text
|
| 74 |
+
if (segments.length === 0 && transcriptText.trim()) {
|
| 75 |
+
segments.push({
|
| 76 |
+
start: 0,
|
| 77 |
+
text: transcriptText.trim()
|
| 78 |
+
});
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Display with timestamps
|
| 82 |
+
console.log("Displaying transcription with segments:", segments);
|
| 83 |
+
displayTranscription(segments);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
statusDiv.textContent = 'Transcription complete!';
|
| 87 |
+
analyzeButton.disabled = false;
|
| 88 |
+
if (progressBarFill) {
|
| 89 |
+
progressBarFill.style.width = '100%';
|
| 90 |
+
}
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Browser-side transcription using Whisper
|
| 95 |
+
if (!whisperPipeline) {
|
| 96 |
+
statusDiv.textContent = 'Loading Whisper model...';
|
| 97 |
+
|
| 98 |
+
// Initialize the pipeline
|
| 99 |
+
whisperPipeline = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en');
|
| 100 |
+
statusDiv.textContent = 'Model loaded. Transcribing...';
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Processing with progress updates
|
| 104 |
+
const transcriptionResult = await whisperPipeline(audioData, {
|
| 105 |
+
chunk_length_s: 30,
|
| 106 |
+
stride_length_s: 5,
|
| 107 |
+
language: 'english',
|
| 108 |
+
task: 'transcribe',
|
| 109 |
+
return_timestamps: true,
|
| 110 |
+
callback_function: function(progressData) {
|
| 111 |
+
const percentage = Math.round(progressData.progress * 100);
|
| 112 |
+
statusDiv.textContent = `Transcribing: ${percentage}%`;
|
| 113 |
+
|
| 114 |
+
const progressBarFill = document.querySelector('.progress-bar-fill');
|
| 115 |
+
if (progressBarFill) {
|
| 116 |
+
progressBarFill.style.width = `${percentage}%`;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
// Save and display transcription
|
| 122 |
+
transcriptText = transcriptionResult.text;
|
| 123 |
+
displayTranscription(transcriptionResult.chunks || []);
|
| 124 |
+
|
| 125 |
+
statusDiv.textContent = 'Transcription complete!';
|
| 126 |
+
analyzeButton.disabled = false;
|
| 127 |
+
if (progressBarFill) {
|
| 128 |
+
progressBarFill.style.width = '100%';
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
} catch (error) {
|
| 132 |
+
console.error('Transcription error:', error);
|
| 133 |
+
statusDiv.textContent = `Error: ${error.message}`;
|
| 134 |
+
throw error;
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Helper function to display transcription with timestamps
|
| 139 |
+
function displayTranscription(segments) {
|
| 140 |
+
console.log("displayTranscription called with segments:", segments);
|
| 141 |
+
|
| 142 |
+
// Clear the transcription div of all content, including the empty state
|
| 143 |
+
if (transcriptionDiv) {
|
| 144 |
+
transcriptionDiv.innerHTML = '';
|
| 145 |
+
} else {
|
| 146 |
+
console.error("transcriptionDiv element not found");
|
| 147 |
+
return;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if (!segments || segments.length === 0) {
|
| 151 |
+
const emptyState = document.createElement('div');
|
| 152 |
+
emptyState.className = 'empty-state';
|
| 153 |
+
emptyState.innerHTML = `
|
| 154 |
+
<i class="fas fa-file-alt"></i>
|
| 155 |
+
<h3 class="empty-state-title">No transcription yet</h3>
|
| 156 |
+
<p class="empty-state-description">Upload a video and click "Transcribe Video" to see the transcription here</p>
|
| 157 |
+
`;
|
| 158 |
+
transcriptionDiv.appendChild(emptyState);
|
| 159 |
+
return;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// Create a container for the transcription content
|
| 163 |
+
const transcriptionContent = document.createElement('div');
|
| 164 |
+
transcriptionContent.className = 'transcription-content';
|
| 165 |
+
|
| 166 |
+
segments.forEach(segment => {
|
| 167 |
+
const paragraphElement = document.createElement('p');
|
| 168 |
+
|
| 169 |
+
// Format and add timestamp prefix
|
| 170 |
+
let timestamp = '?';
|
| 171 |
+
let text = '';
|
| 172 |
+
|
| 173 |
+
if (Array.isArray(segment) && segment.length >= 2) {
|
| 174 |
+
// For array format [start, end, text]
|
| 175 |
+
timestamp = Math.floor(segment[0]);
|
| 176 |
+
text = segment[2] || '';
|
| 177 |
+
} else if (typeof segment === 'object' && segment.start !== undefined) {
|
| 178 |
+
// For object format {start, end, text}
|
| 179 |
+
timestamp = Math.floor(segment.start);
|
| 180 |
+
text = segment.text || '';
|
| 181 |
+
} else {
|
| 182 |
+
// Fallback
|
| 183 |
+
text = segment.toString();
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
paragraphElement.textContent = `[${timestamp}] ${text}`;
|
| 187 |
+
|
| 188 |
+
// Add click handler to jump to timestamp
|
| 189 |
+
paragraphElement.addEventListener('click', () => {
|
| 190 |
+
videoPlayer.currentTime = timestamp;
|
| 191 |
+
videoPlayer.play().catch(e => console.error('Could not play video:', e));
|
| 192 |
+
});
|
| 193 |
+
|
| 194 |
+
paragraphElement.style.cursor = 'pointer';
|
| 195 |
+
transcriptionContent.appendChild(paragraphElement);
|
| 196 |
+
});
|
| 197 |
+
|
| 198 |
+
transcriptionDiv.appendChild(transcriptionContent);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// Store original function references
|
| 202 |
+
const originalTranscribeAudio = transcribeAudio;
|
| 203 |
+
const originalAnalyzeTopics = analyzeTopics;
|
| 204 |
+
|
| 205 |
+
// Override with enhanced versions
|
| 206 |
+
transcribeAudio = async function(audioData) {
|
| 207 |
+
// Set progress step - use the global function safely
|
| 208 |
+
try {
|
| 209 |
+
if (typeof window.setActiveStep === 'function') {
|
| 210 |
+
window.setActiveStep('transcribe');
|
| 211 |
+
}
|
| 212 |
+
} catch (err) {
|
| 213 |
+
console.warn('Could not update progress steps:', err);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
try {
|
| 217 |
+
await originalTranscribeAudio(audioData);
|
| 218 |
+
// If successful, make analyze button fancy with pulse animation
|
| 219 |
+
analyzeButton.classList.add('animate-pulse');
|
| 220 |
+
setTimeout(() => analyzeButton.classList.remove('animate-pulse'), 3000);
|
| 221 |
+
} catch (error) {
|
| 222 |
+
console.error('Transcription error:', error);
|
| 223 |
+
throw error;
|
| 224 |
+
}
|
| 225 |
+
};
|
| 226 |
+
|
| 227 |
+
analyzeTopics = async function() {
|
| 228 |
+
// Set progress step - use the global function safely
|
| 229 |
+
try {
|
| 230 |
+
if (typeof window.setActiveStep === 'function') {
|
| 231 |
+
window.setActiveStep('analyze');
|
| 232 |
+
}
|
| 233 |
+
} catch (err) {
|
| 234 |
+
console.warn('Could not update progress steps:', err);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// Remove pulse animation if still there
|
| 238 |
+
analyzeButton.classList.remove('animate-pulse');
|
| 239 |
+
|
| 240 |
+
try {
|
| 241 |
+
await originalAnalyzeTopics();
|
| 242 |
+
// If successful, we move to visualization
|
| 243 |
+
try {
|
| 244 |
+
if (typeof window.setActiveStep === 'function') {
|
| 245 |
+
window.setActiveStep('visualize');
|
| 246 |
+
}
|
| 247 |
+
} catch (err) {
|
| 248 |
+
console.warn('Could not update progress steps:', err);
|
| 249 |
+
}
|
| 250 |
+
} catch (error) {
|
| 251 |
+
console.error('Analysis error:', error);
|
| 252 |
+
throw error;
|
| 253 |
+
}
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
fileInput.addEventListener('change', (e) => {
|
| 257 |
+
const file = e.target.files[0];
|
| 258 |
+
if (file) {
|
| 259 |
+
const videoURL = URL.createObjectURL(file);
|
| 260 |
+
videoPlayer.src = videoURL;
|
| 261 |
+
transcribeButton.disabled = false;
|
| 262 |
+
analyzeButton.disabled = true;
|
| 263 |
+
transcriptionDiv.textContent = '';
|
| 264 |
+
topicResultsDiv.textContent = '';
|
| 265 |
+
transcriptText = '';
|
| 266 |
+
statusDiv.textContent = 'Video loaded. Ready to transcribe.';
|
| 267 |
+
|
| 268 |
+
const progressBarFill = document.querySelector('.progress-bar-fill');
|
| 269 |
+
if (progressBarFill) {
|
| 270 |
+
progressBarFill.style.width = '0%';
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
});
|
| 274 |
+
|
| 275 |
+
// Audio Processing Functions
|
| 276 |
+
function convertAudioBuffer(audioBuffer) {
|
| 277 |
+
const audioData = audioBuffer.getChannelData(0);
|
| 278 |
+
const targetSampleRate = 16000;
|
| 279 |
+
const resamplingRatio = targetSampleRate / audioBuffer.sampleRate;
|
| 280 |
+
const resampledLength = Math.floor(audioData.length * resamplingRatio);
|
| 281 |
+
const resampledData = new Float32Array(resampledLength);
|
| 282 |
+
|
| 283 |
+
for (let i = 0; i < resampledLength; i++) {
|
| 284 |
+
const originalIndex = Math.floor(i / resamplingRatio);
|
| 285 |
+
resampledData[i] = audioData[originalIndex];
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
return resampledData;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
async function extractAudio(videoFile) {
|
| 292 |
+
try {
|
| 293 |
+
console.log('Starting audio extraction');
|
| 294 |
+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 295 |
+
const arrayBuffer = await videoFile.arrayBuffer();
|
| 296 |
+
console.log('Video file loaded into buffer');
|
| 297 |
+
|
| 298 |
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
| 299 |
+
console.log('Audio decoded successfully');
|
| 300 |
+
|
| 301 |
+
return convertAudioBuffer(audioBuffer);
|
| 302 |
+
} catch (error) {
|
| 303 |
+
console.error('Error extracting audio:', error);
|
| 304 |
+
throw new Error(`Failed to extract audio: ${error.message}`);
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// Define setActiveStep globally before any other functions use it
|
| 309 |
+
window.setActiveStep = function(step) {
|
| 310 |
+
// Will be initialized properly later, but create empty function to prevent errors
|
| 311 |
+
console.log(`Progress step: ${step}`);
|
| 312 |
+
};
|
| 313 |
+
|
| 314 |
+
// Helper function to convert ArrayBuffer to base64
|
| 315 |
+
function arrayBufferToBase64(buffer) {
|
| 316 |
+
let binary = '';
|
| 317 |
+
const bytes = new Uint8Array(buffer);
|
| 318 |
+
const len = bytes.byteLength;
|
| 319 |
+
for (let i = 0; i < len; i++) {
|
| 320 |
+
binary += String.fromCharCode(bytes[i]);
|
| 321 |
+
}
|
| 322 |
+
return window.btoa(binary);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
function formatTimestamp(seconds) {
|
| 326 |
+
// Round down to nearest second and return as integer
|
| 327 |
+
return `[${Math.floor(seconds)}]`;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
async function analyzeTopics() {
|
| 331 |
+
try {
|
| 332 |
+
statusDiv.textContent = 'Analyzing topics...';
|
| 333 |
+
|
| 334 |
+
// Make sure we're using the correct progress element
|
| 335 |
+
const progressBarFill = document.querySelector('.progress-bar-fill');
|
| 336 |
+
if (progressBarFill) {
|
| 337 |
+
progressBarFill.style.width = '0%';
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
const response = await fetch('/analyze', {
|
| 341 |
+
method: 'POST',
|
| 342 |
+
headers: {
|
| 343 |
+
'Content-Type': 'application/json'
|
| 344 |
+
},
|
| 345 |
+
body: JSON.stringify({ transcript: transcriptText })
|
| 346 |
+
});
|
| 347 |
+
|
| 348 |
+
if (!response.ok) {
|
| 349 |
+
throw new Error('Failed to analyze topics');
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
const data = await response.json();
|
| 353 |
+
displayTopicResults(data.segments);
|
| 354 |
+
statusDiv.textContent = 'Topic analysis complete!';
|
| 355 |
+
|
| 356 |
+
if (progressBarFill) {
|
| 357 |
+
progressBarFill.style.width = '100%';
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
} catch (error) {
|
| 361 |
+
console.error('Topic analysis error:', error);
|
| 362 |
+
statusDiv.textContent = `Error: ${error.message}`;
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
function displayTopicResults(segments) {
|
| 367 |
+
// Clear previous results
|
| 368 |
+
topicResultsDiv.innerHTML = '';
|
| 369 |
+
|
| 370 |
+
// Display topics in the sidebar
|
| 371 |
+
segments.forEach(segment => {
|
| 372 |
+
const topicElement = document.createElement('div');
|
| 373 |
+
topicElement.className = 'topic';
|
| 374 |
+
|
| 375 |
+
const headerElement = document.createElement('div');
|
| 376 |
+
headerElement.className = 'topic-header';
|
| 377 |
+
headerElement.textContent = segment.topic_name;
|
| 378 |
+
|
| 379 |
+
const contentElement = document.createElement('div');
|
| 380 |
+
contentElement.className = 'topic-content';
|
| 381 |
+
|
| 382 |
+
// Handle content that could be either an array or a string
|
| 383 |
+
if (Array.isArray(segment.content)) {
|
| 384 |
+
contentElement.textContent = segment.content.join('\n');
|
| 385 |
+
} else {
|
| 386 |
+
contentElement.textContent = segment.content;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
topicElement.appendChild(headerElement);
|
| 390 |
+
topicElement.appendChild(contentElement);
|
| 391 |
+
|
| 392 |
+
// If this is a parent topic with children, display them as nested elements
|
| 393 |
+
if (segment.is_parent && segment.children && segment.children.length > 0) {
|
| 394 |
+
topicElement.classList.add('parent-topic');
|
| 395 |
+
|
| 396 |
+
const childrenContainer = document.createElement('div');
|
| 397 |
+
childrenContainer.className = 'subtopics';
|
| 398 |
+
|
| 399 |
+
segment.children.forEach(child => {
|
| 400 |
+
const childElement = document.createElement('div');
|
| 401 |
+
childElement.className = 'subtopic';
|
| 402 |
+
|
| 403 |
+
const childHeader = document.createElement('div');
|
| 404 |
+
childHeader.className = 'subtopic-header';
|
| 405 |
+
childHeader.textContent = child.topic_name;
|
| 406 |
+
|
| 407 |
+
const childContent = document.createElement('div');
|
| 408 |
+
childContent.className = 'subtopic-content';
|
| 409 |
+
|
| 410 |
+
// Handle content that could be either an array or a string
|
| 411 |
+
if (Array.isArray(child.content)) {
|
| 412 |
+
childContent.textContent = child.content.join('\n');
|
| 413 |
+
} else {
|
| 414 |
+
childContent.textContent = child.content;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
childElement.appendChild(childHeader);
|
| 418 |
+
childElement.appendChild(childContent);
|
| 419 |
+
childrenContainer.appendChild(childElement);
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
topicElement.appendChild(childrenContainer);
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
topicResultsDiv.appendChild(topicElement);
|
| 426 |
+
});
|
| 427 |
+
|
| 428 |
+
// Update mindmap and show fullscreen overlay
|
| 429 |
+
mindmap.update(segments);
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// Event Listeners
|
| 433 |
+
transcribeButton.addEventListener('click', async () => {
|
| 434 |
+
try {
|
| 435 |
+
transcribeButton.disabled = true;
|
| 436 |
+
modelSelector.disabled = true;
|
| 437 |
+
const videoFile = fileInput.files[0];
|
| 438 |
+
|
| 439 |
+
if (!videoFile) {
|
| 440 |
+
throw new Error('No video file selected');
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
const videoPreview = document.querySelector('.video-preview');
|
| 444 |
+
const videoHeight = videoPlayer.offsetHeight;
|
| 445 |
+
if (videoHeight > 0) {
|
| 446 |
+
videoPreview.style.height = `${videoHeight}px`;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
statusDiv.textContent = 'Extracting audio...';
|
| 450 |
+
const audioData = await extractAudio(videoFile);
|
| 451 |
+
await transcribeAudio(audioData);
|
| 452 |
+
modelSelector.disabled = false;
|
| 453 |
+
|
| 454 |
+
} catch (error) {
|
| 455 |
+
console.error('Processing error:', error);
|
| 456 |
+
statusDiv.textContent = `Error: ${error.message}`;
|
| 457 |
+
transcribeButton.disabled = false;
|
| 458 |
+
modelSelector.disabled = false;
|
| 459 |
+
}
|
| 460 |
+
});
|
| 461 |
+
|
| 462 |
+
analyzeButton.addEventListener('click', analyzeTopics);
|
| 463 |
+
|
| 464 |
+
modelSelector.addEventListener('change', async (e) => {
|
| 465 |
+
const selectedModel = e.target.value;
|
| 466 |
+
transcribeButton.disabled = false;
|
| 467 |
+
analyzeButton.disabled = true;
|
| 468 |
+
await initWhisper(selectedModel);
|
| 469 |
+
if (fileInput.files.length > 0) {
|
| 470 |
+
transcribeButton.disabled = false;
|
| 471 |
+
}
|
| 472 |
+
});
|
| 473 |
+
|
| 474 |
+
// Initialize with the selected model when the page loads
|
| 475 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 476 |
+
// Initialize all UI components
|
| 477 |
+
initThemeToggle();
|
| 478 |
+
initFAB();
|
| 479 |
+
initGallery();
|
| 480 |
+
initCustomVideoPlayer();
|
| 481 |
+
initUploadParticles();
|
| 482 |
+
initProgressSteps();
|
| 483 |
+
|
| 484 |
+
// Original initialization
|
| 485 |
+
mindmap = new TopicMindmap();
|
| 486 |
+
initWhisper(modelSelector.value).catch(error => {
|
| 487 |
+
console.error('Failed to initialize Whisper:', error);
|
| 488 |
+
statusDiv.textContent = 'Failed to initialize the transcription model';
|
| 489 |
+
});
|
| 490 |
+
});
|
| 491 |
+
|
| 492 |
+
// Theme toggle functionality
|
| 493 |
+
function initThemeToggle() {
|
| 494 |
+
const themeToggle = document.getElementById('themeToggle');
|
| 495 |
+
const htmlElement = document.documentElement;
|
| 496 |
+
|
| 497 |
+
// Check for saved theme preference
|
| 498 |
+
const savedTheme = localStorage.getItem('theme');
|
| 499 |
+
if (savedTheme) {
|
| 500 |
+
htmlElement.setAttribute('data-theme', savedTheme);
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
themeToggle.addEventListener('click', () => {
|
| 504 |
+
const currentTheme = htmlElement.getAttribute('data-theme');
|
| 505 |
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
| 506 |
+
|
| 507 |
+
htmlElement.setAttribute('data-theme', newTheme);
|
| 508 |
+
localStorage.setItem('theme', newTheme);
|
| 509 |
+
});
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
// FAB functionality
|
| 513 |
+
function initFAB() {
|
| 514 |
+
const fabContainer = document.getElementById('fabContainer');
|
| 515 |
+
const mainFab = document.getElementById('mainFab');
|
| 516 |
+
const exportPdfBtn = document.getElementById('exportPdfBtn');
|
| 517 |
+
const exportPngBtn = document.getElementById('exportPngBtn');
|
| 518 |
+
const exportSvgBtn = document.getElementById('exportSvgBtn');
|
| 519 |
+
const shareMindmapBtn = document.getElementById('shareMindmapBtn');
|
| 520 |
+
|
| 521 |
+
// Toggle FAB options
|
| 522 |
+
mainFab.addEventListener('click', () => {
|
| 523 |
+
fabContainer.classList.toggle('active');
|
| 524 |
+
});
|
| 525 |
+
|
| 526 |
+
// Close FAB when clicking outside
|
| 527 |
+
document.addEventListener('click', (e) => {
|
| 528 |
+
if (!fabContainer.contains(e.target) && fabContainer.classList.contains('active')) {
|
| 529 |
+
fabContainer.classList.remove('active');
|
| 530 |
+
}
|
| 531 |
+
});
|
| 532 |
+
|
| 533 |
+
// Export as PDF
|
| 534 |
+
exportPdfBtn.addEventListener('click', () => {
|
| 535 |
+
exportMindmap('pdf');
|
| 536 |
+
});
|
| 537 |
+
|
| 538 |
+
// Export as PNG
|
| 539 |
+
exportPngBtn.addEventListener('click', () => {
|
| 540 |
+
exportMindmap('png');
|
| 541 |
+
});
|
| 542 |
+
|
| 543 |
+
// Export as SVG
|
| 544 |
+
exportSvgBtn.addEventListener('click', () => {
|
| 545 |
+
exportMindmap('svg');
|
| 546 |
+
});
|
| 547 |
+
|
| 548 |
+
// Share mindmap
|
| 549 |
+
shareMindmapBtn.addEventListener('click', () => {
|
| 550 |
+
shareMindmap();
|
| 551 |
+
});
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
// Gallery functionality
|
| 555 |
+
function initGallery() {
|
| 556 |
+
const galleryContainer = document.getElementById('resultsGallery');
|
| 557 |
+
const galleryClose = document.getElementById('galleryClose');
|
| 558 |
+
const galleryContent = document.getElementById('galleryContent');
|
| 559 |
+
|
| 560 |
+
// Add "Recent Mindmaps" option to FAB
|
| 561 |
+
const fabContainer = document.getElementById('fabContainer');
|
| 562 |
+
const recentMindmapsBtn = document.createElement('button');
|
| 563 |
+
recentMindmapsBtn.className = 'fab-option';
|
| 564 |
+
recentMindmapsBtn.setAttribute('title', 'Recent Mindmaps');
|
| 565 |
+
recentMindmapsBtn.innerHTML = '<i class="fas fa-history"></i>';
|
| 566 |
+
fabContainer.querySelector('.fab-options').appendChild(recentMindmapsBtn);
|
| 567 |
+
|
| 568 |
+
// Show gallery
|
| 569 |
+
recentMindmapsBtn.addEventListener('click', () => {
|
| 570 |
+
showGallery();
|
| 571 |
+
fabContainer.classList.remove('active');
|
| 572 |
+
});
|
| 573 |
+
|
| 574 |
+
// Close gallery
|
| 575 |
+
galleryClose.addEventListener('click', () => {
|
| 576 |
+
galleryContainer.classList.remove('active');
|
| 577 |
+
});
|
| 578 |
+
|
| 579 |
+
// Close gallery with escape key
|
| 580 |
+
document.addEventListener('keydown', (e) => {
|
| 581 |
+
if (e.key === 'Escape' && galleryContainer.classList.contains('active')) {
|
| 582 |
+
galleryContainer.classList.remove('active');
|
| 583 |
+
}
|
| 584 |
+
});
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
// Show gallery and populate with saved mindmaps
|
| 588 |
+
function showGallery() {
|
| 589 |
+
const galleryContainer = document.getElementById('resultsGallery');
|
| 590 |
+
const galleryContent = document.getElementById('galleryContent');
|
| 591 |
+
|
| 592 |
+
// Clear existing content
|
| 593 |
+
galleryContent.innerHTML = '';
|
| 594 |
+
|
| 595 |
+
// Get saved mindmaps from localStorage
|
| 596 |
+
const savedMindmaps = getSavedMindmaps();
|
| 597 |
+
|
| 598 |
+
if (savedMindmaps.length === 0) {
|
| 599 |
+
// Show empty state
|
| 600 |
+
galleryContent.innerHTML = `
|
| 601 |
+
<div class="gallery-empty">
|
| 602 |
+
<i class="fas fa-file-alt" style="font-size: 3rem; color: var(--text-secondary); margin-bottom: 1rem;"></i>
|
| 603 |
+
<p>No saved mindmaps yet</p>
|
| 604 |
+
<p>Your analyzed videos will appear here</p>
|
| 605 |
+
</div>
|
| 606 |
+
`;
|
| 607 |
+
} else {
|
| 608 |
+
// Create gallery items for each saved mindmap
|
| 609 |
+
savedMindmaps.forEach(mindmap => {
|
| 610 |
+
createGalleryItem(mindmap);
|
| 611 |
+
});
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
// Show gallery
|
| 615 |
+
galleryContainer.classList.add('active');
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
// Create a gallery item for a saved mindmap
|
| 619 |
+
function createGalleryItem(mindmapData) {
|
| 620 |
+
const galleryContent = document.getElementById('galleryContent');
|
| 621 |
+
|
| 622 |
+
const item = document.createElement('div');
|
| 623 |
+
item.className = 'gallery-item';
|
| 624 |
+
|
| 625 |
+
// Use a random color from our palette for preview
|
| 626 |
+
const colors = ['#3498db', '#2ecc71', '#e74c3c', '#9b59b6', '#e67e22', '#f1c40f', '#1abc9c'];
|
| 627 |
+
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
| 628 |
+
|
| 629 |
+
item.innerHTML = `
|
| 630 |
+
<div class="gallery-item-preview" style="background-color: ${randomColor};">
|
| 631 |
+
<div style="height: 100%; display: flex; align-items: center; justify-content: center; color: white;">
|
| 632 |
+
<i class="fas fa-project-diagram" style="font-size: 2rem;"></i>
|
| 633 |
+
</div>
|
| 634 |
+
</div>
|
| 635 |
+
<div class="gallery-item-info">
|
| 636 |
+
<div class="gallery-item-title">${mindmapData.title || 'Untitled Mindmap'}</div>
|
| 637 |
+
<div class="gallery-item-date">${new Date(mindmapData.date).toLocaleDateString()}</div>
|
| 638 |
+
</div>
|
| 639 |
+
`;
|
| 640 |
+
|
| 641 |
+
// Load mindmap when clicked
|
| 642 |
+
item.addEventListener('click', () => {
|
| 643 |
+
loadMindmap(mindmapData);
|
| 644 |
+
document.getElementById('resultsGallery').classList.remove('active');
|
| 645 |
+
});
|
| 646 |
+
|
| 647 |
+
galleryContent.appendChild(item);
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
// Export mindmap in various formats
|
| 651 |
+
function exportMindmap(format) {
|
| 652 |
+
if (!mindmap || !mindmap.mindmap) {
|
| 653 |
+
alert('No mindmap to export. Please analyze a video first.');
|
| 654 |
+
return;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
try {
|
| 658 |
+
switch (format) {
|
| 659 |
+
case 'pdf':
|
| 660 |
+
exportAsPDF();
|
| 661 |
+
break;
|
| 662 |
+
case 'png':
|
| 663 |
+
exportAsPNG();
|
| 664 |
+
break;
|
| 665 |
+
case 'svg':
|
| 666 |
+
exportAsSVG();
|
| 667 |
+
break;
|
| 668 |
+
}
|
| 669 |
+
} catch (error) {
|
| 670 |
+
console.error(`Error exporting as ${format}:`, error);
|
| 671 |
+
alert(`Failed to export mindmap as ${format.toUpperCase()}`);
|
| 672 |
+
}
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
// Export as PDF using html2pdf
|
| 676 |
+
function exportAsPDF() {
|
| 677 |
+
// We'll use html2pdf.js for PDF export
|
| 678 |
+
const script = document.createElement('script');
|
| 679 |
+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js';
|
| 680 |
+
document.head.appendChild(script);
|
| 681 |
+
|
| 682 |
+
script.onload = () => {
|
| 683 |
+
const element = document.getElementById('mindmap');
|
| 684 |
+
const opt = {
|
| 685 |
+
margin: 1,
|
| 686 |
+
filename: 'flowify-mindmap.pdf',
|
| 687 |
+
image: { type: 'jpeg', quality: 0.98 },
|
| 688 |
+
html2canvas: { scale: 2 },
|
| 689 |
+
jsPDF: { unit: 'in', format: 'letter', orientation: 'landscape' }
|
| 690 |
+
};
|
| 691 |
+
|
| 692 |
+
// Use html2pdf library to generate PDF
|
| 693 |
+
html2pdf().set(opt).from(element).save();
|
| 694 |
+
};
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
// Export as PNG
|
| 698 |
+
function exportAsPNG() {
|
| 699 |
+
// We'll use html2canvas for PNG export
|
| 700 |
+
const script = document.createElement('script');
|
| 701 |
+
script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
|
| 702 |
+
document.head.appendChild(script);
|
| 703 |
+
|
| 704 |
+
script.onload = () => {
|
| 705 |
+
html2canvas(document.getElementById('mindmap'), {
|
| 706 |
+
scale: 2,
|
| 707 |
+
backgroundColor: null
|
| 708 |
+
}).then(canvas => {
|
| 709 |
+
const link = document.createElement('a');
|
| 710 |
+
link.download = 'flowify-mindmap.png';
|
| 711 |
+
link.href = canvas.toDataURL('image/png');
|
| 712 |
+
link.click();
|
| 713 |
+
});
|
| 714 |
+
};
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
// Export as SVG
|
| 718 |
+
function exportAsSVG() {
|
| 719 |
+
alert('SVG export functionality is under development. This feature will be available in a future update.');
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
// Share mindmap (placeholder)
|
| 723 |
+
function shareMindmap() {
|
| 724 |
+
// Save current mindmap to localStorage
|
| 725 |
+
if (mindmap && mindmap.mindmap) {
|
| 726 |
+
const data = mindmap.mindmap.get_data();
|
| 727 |
+
const mindmapData = {
|
| 728 |
+
id: Date.now(),
|
| 729 |
+
title: data.data.topic || 'Video Topics',
|
| 730 |
+
data: data,
|
| 731 |
+
date: new Date().toISOString()
|
| 732 |
+
};
|
| 733 |
+
|
| 734 |
+
saveMindmap(mindmapData);
|
| 735 |
+
|
| 736 |
+
alert('Mindmap saved! You can access it from the Recent Mindmaps gallery.');
|
| 737 |
+
} else {
|
| 738 |
+
alert('No mindmap to share. Please analyze a video first.');
|
| 739 |
+
}
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
// Save mindmap to localStorage
|
| 743 |
+
function saveMindmap(mindmapData) {
|
| 744 |
+
let savedMindmaps = getSavedMindmaps();
|
| 745 |
+
savedMindmaps.push(mindmapData);
|
| 746 |
+
|
| 747 |
+
// Keep only the most recent 20 mindmaps
|
| 748 |
+
if (savedMindmaps.length > 20) {
|
| 749 |
+
savedMindmaps = savedMindmaps.slice(-20);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
localStorage.setItem('flowify-mindmaps', JSON.stringify(savedMindmaps));
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
// Get saved mindmaps from localStorage
|
| 756 |
+
function getSavedMindmaps() {
|
| 757 |
+
const savedData = localStorage.getItem('flowify-mindmaps');
|
| 758 |
+
return savedData ? JSON.parse(savedData) : [];
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
// Load a mindmap from saved data
|
| 762 |
+
function loadMindmap(mindmapData) {
|
| 763 |
+
if (mindmap && mindmap.mindmap && mindmapData.data) {
|
| 764 |
+
mindmap.mindmap.show(mindmapData.data);
|
| 765 |
+
mindmap.mindmapOverlay.classList.add('active');
|
| 766 |
+
}
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
class TopicMindmap {
|
| 770 |
+
constructor() {
|
| 771 |
+
this.mindmap = null;
|
| 772 |
+
this.container = 'mindmap';
|
| 773 |
+
this.zoomScale = 1;
|
| 774 |
+
this.isPanning = false;
|
| 775 |
+
this.panX = 0;
|
| 776 |
+
this.panY = 0;
|
| 777 |
+
this.isEditing = false;
|
| 778 |
+
this.segmentData = new Map();
|
| 779 |
+
|
| 780 |
+
// Get DOM elements
|
| 781 |
+
this.mainVideo = document.getElementById('videoPlayer');
|
| 782 |
+
this.overlayVideo = document.getElementById('overlayVideo');
|
| 783 |
+
this.videoSection = document.getElementById('videoSection');
|
| 784 |
+
this.videoInfo = document.getElementById('videoInfo');
|
| 785 |
+
this.toolbar = document.querySelector('.mindmap-toolbar');
|
| 786 |
+
this.mindmapOverlay = document.getElementById('mindmapOverlay');
|
| 787 |
+
this.videoCloseButton = document.getElementById('videoCloseButton');
|
| 788 |
+
|
| 789 |
+
// Context menu elements
|
| 790 |
+
this.contextMenu = document.getElementById('nodeContextMenu');
|
| 791 |
+
this.colorPicker = document.getElementById('colorPicker');
|
| 792 |
+
this.currentNodeId = null;
|
| 793 |
+
|
| 794 |
+
// Initially hide video section
|
| 795 |
+
if (this.videoSection) {
|
| 796 |
+
this.videoSection.style.display = 'none';
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
this.initialize();
|
| 800 |
+
this.setupToolbar();
|
| 801 |
+
this.setupNodeInteraction();
|
| 802 |
+
this.setupVideoCloseButton();
|
| 803 |
+
this.setupContextMenu();
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
initialize() {
|
| 807 |
+
const options = {
|
| 808 |
+
container: this.container,
|
| 809 |
+
theme: 'primary',
|
| 810 |
+
editable: true,
|
| 811 |
+
support_html: true,
|
| 812 |
+
view: {
|
| 813 |
+
draggable: true,
|
| 814 |
+
hmargin: 300,
|
| 815 |
+
vmargin: 200,
|
| 816 |
+
line_width: 2,
|
| 817 |
+
line_color: '#95a5a6',
|
| 818 |
+
node_spacing: 100
|
| 819 |
+
},
|
| 820 |
+
layout: {
|
| 821 |
+
hspace: 150,
|
| 822 |
+
vspace: 80,
|
| 823 |
+
pspace: 30
|
| 824 |
+
}
|
| 825 |
+
};
|
| 826 |
+
|
| 827 |
+
try {
|
| 828 |
+
// Add a style element for immediate expander fixes
|
| 829 |
+
this.addExpanderFixStyles();
|
| 830 |
+
|
| 831 |
+
this.mindmap = new jsMind(options);
|
| 832 |
+
const emptyMind = this.createEmptyMindMap();
|
| 833 |
+
this.mindmap.show(emptyMind);
|
| 834 |
+
|
| 835 |
+
const container = document.querySelector('.fullscreen-mindmap');
|
| 836 |
+
container.style.transformOrigin = '0 0';
|
| 837 |
+
container.style.position = 'absolute';
|
| 838 |
+
container.style.width = '100%';
|
| 839 |
+
container.style.height = '100%';
|
| 840 |
+
|
| 841 |
+
const mindmapWrapper = document.getElementById('mindmap');
|
| 842 |
+
mindmapWrapper.style.position = 'absolute';
|
| 843 |
+
mindmapWrapper.style.width = '100%';
|
| 844 |
+
mindmapWrapper.style.height = '100%';
|
| 845 |
+
mindmapWrapper.style.overflow = 'visible';
|
| 846 |
+
|
| 847 |
+
// Apply custom classes to expanders after rendering
|
| 848 |
+
this.applyCustomExpanderClasses();
|
| 849 |
+
|
| 850 |
+
this.setupNodeInteraction();
|
| 851 |
+
this.setupPanning();
|
| 852 |
+
} catch (error) {
|
| 853 |
+
console.error('Error initializing mindmap:', error);
|
| 854 |
+
}
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
addExpanderFixStyles() {
|
| 858 |
+
// Add a style element to ensure expanders don't show any text content
|
| 859 |
+
const style = document.createElement('style');
|
| 860 |
+
style.textContent = `
|
| 861 |
+
jmexpander * {
|
| 862 |
+
display: none !important;
|
| 863 |
+
}
|
| 864 |
+
jmexpander::before, jmexpander::after {
|
| 865 |
+
display: none !important;
|
| 866 |
+
}
|
| 867 |
+
`;
|
| 868 |
+
document.head.appendChild(style);
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
setupNodeInteraction() {
|
| 872 |
+
// Remove any existing event listeners
|
| 873 |
+
const container = document.getElementById(this.container);
|
| 874 |
+
if (!container) return;
|
| 875 |
+
|
| 876 |
+
container.addEventListener('click', (e) => {
|
| 877 |
+
const target = e.target;
|
| 878 |
+
if (target.tagName.toLowerCase() === 'jmnode') {
|
| 879 |
+
const nodeId = target.getAttribute('nodeid');
|
| 880 |
+
if (nodeId) {
|
| 881 |
+
this.handleNodeClick(nodeId);
|
| 882 |
+
}
|
| 883 |
+
}
|
| 884 |
+
});
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
handleNodeClick(nodeId) {
|
| 888 |
+
console.log('Node clicked:', nodeId);
|
| 889 |
+
const segmentInfo = this.segmentData.get(nodeId);
|
| 890 |
+
console.log('Segment info:', segmentInfo);
|
| 891 |
+
|
| 892 |
+
if (segmentInfo && segmentInfo.timestamp !== undefined) {
|
| 893 |
+
// Show video section
|
| 894 |
+
if (this.videoSection) {
|
| 895 |
+
this.videoSection.style.display = 'block';
|
| 896 |
+
console.log('Video section displayed');
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
// Adjust layout
|
| 900 |
+
if (this.toolbar) {
|
| 901 |
+
this.toolbar.classList.add('with-video');
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
// Setup video
|
| 905 |
+
if (this.mainVideo && this.mainVideo.src && this.overlayVideo) {
|
| 906 |
+
console.log('Setting up video playback at time:', segmentInfo.timestamp);
|
| 907 |
+
|
| 908 |
+
// Set video source if needed
|
| 909 |
+
if (this.overlayVideo.src !== this.mainVideo.src) {
|
| 910 |
+
this.overlayVideo.src = this.mainVideo.src;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
// Get the timestamp as a number
|
| 914 |
+
let timeInSeconds = segmentInfo.timestamp;
|
| 915 |
+
|
| 916 |
+
// If it's still a string (with brackets), parse it
|
| 917 |
+
if (typeof timeInSeconds === 'string') {
|
| 918 |
+
if (timeInSeconds.startsWith('[') && timeInSeconds.endsWith(']')) {
|
| 919 |
+
timeInSeconds = parseFloat(timeInSeconds.slice(1, -1));
|
| 920 |
+
} else {
|
| 921 |
+
timeInSeconds = parseFloat(timeInSeconds);
|
| 922 |
+
}
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
// Ensure we have a valid number
|
| 926 |
+
if (!isNaN(timeInSeconds)) {
|
| 927 |
+
console.log('Seeking to time:', timeInSeconds);
|
| 928 |
+
this.overlayVideo.currentTime = timeInSeconds;
|
| 929 |
+
this.overlayVideo.play().catch(error => {
|
| 930 |
+
console.error('Error playing video:', error);
|
| 931 |
+
});
|
| 932 |
+
|
| 933 |
+
// Update info
|
| 934 |
+
if (this.videoInfo) {
|
| 935 |
+
const timestamp = this.formatTimestamp(timeInSeconds);
|
| 936 |
+
const content = segmentInfo.content.replace(/\[\d+(?:\.\d+)?\]\s*/, '');
|
| 937 |
+
this.videoInfo.textContent = `${timestamp}: ${content}`;
|
| 938 |
+
}
|
| 939 |
+
} else {
|
| 940 |
+
console.error('Invalid timestamp:', segmentInfo.timestamp);
|
| 941 |
+
}
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
// Update layout
|
| 945 |
+
this.updateLayout();
|
| 946 |
+
}
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
updateLayout() {
|
| 950 |
+
// Adjust mindmap section width when video is shown
|
| 951 |
+
const mindmapSection = document.querySelector('.mindmap-section');
|
| 952 |
+
const videoWidth = '40%';
|
| 953 |
+
const mindmapWidth = '60%';
|
| 954 |
+
|
| 955 |
+
if (this.videoSection && this.videoSection.style.display === 'block') {
|
| 956 |
+
this.videoSection.style.width = videoWidth;
|
| 957 |
+
if (mindmapSection) {
|
| 958 |
+
mindmapSection.style.width = mindmapWidth;
|
| 959 |
+
}
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
// Force mindmap refresh
|
| 963 |
+
if (this.mindmap) {
|
| 964 |
+
this.mindmap.resize();
|
| 965 |
+
}
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
setupNodeEditing() {
|
| 969 |
+
// Add double-click event listener for editing nodes
|
| 970 |
+
const container = document.getElementById(this.container);
|
| 971 |
+
container.addEventListener('dblclick', (e) => {
|
| 972 |
+
const target = e.target;
|
| 973 |
+
if (target.tagName.toLowerCase() === 'jmnode') {
|
| 974 |
+
this.editNode(target);
|
| 975 |
+
}
|
| 976 |
+
});
|
| 977 |
+
|
| 978 |
+
// Add node drag and drop functionality
|
| 979 |
+
container.addEventListener('mousedown', (e) => {
|
| 980 |
+
if (!this.isPanning && e.target.tagName.toLowerCase() === 'jmnode') {
|
| 981 |
+
this.startNodeDrag(e);
|
| 982 |
+
}
|
| 983 |
+
});
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
editNode(nodeElement) {
|
| 987 |
+
if (this.isPanning) return;
|
| 988 |
+
|
| 989 |
+
const nodeId = nodeElement.getAttribute('nodeid');
|
| 990 |
+
const currentText = this.mindmap.get_node(nodeId).topic;
|
| 991 |
+
|
| 992 |
+
// Create and style the input element
|
| 993 |
+
const input = document.createElement('input');
|
| 994 |
+
input.type = 'text';
|
| 995 |
+
input.value = currentText;
|
| 996 |
+
input.style.width = Math.max(nodeElement.offsetWidth, 100) + 'px';
|
| 997 |
+
input.style.height = nodeElement.offsetHeight + 'px';
|
| 998 |
+
input.style.position = 'absolute';
|
| 999 |
+
input.style.left = nodeElement.offsetLeft + 'px';
|
| 1000 |
+
input.style.top = nodeElement.offsetTop + 'px';
|
| 1001 |
+
input.style.zIndex = '1000';
|
| 1002 |
+
input.style.padding = '4px';
|
| 1003 |
+
input.style.border = '2px solid #3498db';
|
| 1004 |
+
input.style.borderRadius = '4px';
|
| 1005 |
+
input.style.backgroundColor = 'white';
|
| 1006 |
+
|
| 1007 |
+
// Replace node with input
|
| 1008 |
+
nodeElement.style.visibility = 'hidden';
|
| 1009 |
+
nodeElement.parentNode.appendChild(input);
|
| 1010 |
+
input.focus();
|
| 1011 |
+
input.select();
|
| 1012 |
+
|
| 1013 |
+
const finishEditing = () => {
|
| 1014 |
+
const newText = input.value.trim();
|
| 1015 |
+
if (newText) {
|
| 1016 |
+
this.mindmap.update_node(nodeId, newText);
|
| 1017 |
+
}
|
| 1018 |
+
nodeElement.style.visibility = 'visible';
|
| 1019 |
+
input.remove();
|
| 1020 |
+
};
|
| 1021 |
+
|
| 1022 |
+
input.addEventListener('blur', finishEditing);
|
| 1023 |
+
input.addEventListener('keydown', (e) => {
|
| 1024 |
+
if (e.key === 'Enter') {
|
| 1025 |
+
finishEditing();
|
| 1026 |
+
} else if (e.key === 'Escape') {
|
| 1027 |
+
nodeElement.style.visibility = 'visible';
|
| 1028 |
+
input.remove();
|
| 1029 |
+
}
|
| 1030 |
+
});
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
+
startNodeDrag(e) {
|
| 1034 |
+
if (this.isPanning) return;
|
| 1035 |
+
|
| 1036 |
+
const nodeElement = e.target;
|
| 1037 |
+
const nodeId = nodeElement.getAttribute('nodeid');
|
| 1038 |
+
if (!nodeId) return;
|
| 1039 |
+
|
| 1040 |
+
const startX = e.clientX;
|
| 1041 |
+
const startY = e.clientY;
|
| 1042 |
+
const originalLeft = nodeElement.offsetLeft;
|
| 1043 |
+
const originalTop = nodeElement.offsetTop;
|
| 1044 |
+
|
| 1045 |
+
const mouseMoveHandler = (e) => {
|
| 1046 |
+
const dx = (e.clientX - startX) / this.zoomScale;
|
| 1047 |
+
const dy = (e.clientY - startY) / this.zoomScale;
|
| 1048 |
+
|
| 1049 |
+
nodeElement.style.left = (originalLeft + dx) / this.zoomScale + 'px';
|
| 1050 |
+
nodeElement.style.top = (originalTop + dy) / this.zoomScale + 'px';
|
| 1051 |
+
|
| 1052 |
+
// Update connected lines
|
| 1053 |
+
this.mindmap.layout.layout();
|
| 1054 |
+
};
|
| 1055 |
+
|
| 1056 |
+
const mouseUpHandler = () => {
|
| 1057 |
+
document.removeEventListener('mousemove', mouseMoveHandler);
|
| 1058 |
+
document.removeEventListener('mouseup', mouseUpHandler);
|
| 1059 |
+
|
| 1060 |
+
// Ensure the mindmap updates its internal positions
|
| 1061 |
+
this.mindmap.layout.layout();
|
| 1062 |
+
};
|
| 1063 |
+
|
| 1064 |
+
document.addEventListener('mousemove', mouseMoveHandler);
|
| 1065 |
+
document.addEventListener('mouseup', mouseUpHandler);
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
setupPanning() {
|
| 1069 |
+
const container = document.querySelector('.fullscreen-mindmap');
|
| 1070 |
+
const mindmapWrapper = document.getElementById('mindmap');
|
| 1071 |
+
const panButton = document.getElementById('panButton');
|
| 1072 |
+
|
| 1073 |
+
panButton.addEventListener('click', () => {
|
| 1074 |
+
this.isPanning = !this.isPanning;
|
| 1075 |
+
panButton.classList.toggle('active');
|
| 1076 |
+
container.style.cursor = this.isPanning ? 'grab' : 'default';
|
| 1077 |
+
});
|
| 1078 |
+
|
| 1079 |
+
container.addEventListener('wheel', (e) => {
|
| 1080 |
+
if (e.ctrlKey) {
|
| 1081 |
+
e.preventDefault();
|
| 1082 |
+
const delta = e.deltaY * -0.01;
|
| 1083 |
+
const newScale = Math.min(Math.max(this.zoomScale + delta, 0.1), 3);
|
| 1084 |
+
|
| 1085 |
+
// Get mouse position relative to container
|
| 1086 |
+
const rect = container.getBoundingClientRect();
|
| 1087 |
+
const x = (e.clientX - rect.left) / this.zoomScale;
|
| 1088 |
+
const y = (e.clientY - rect.top) / this.zoomScale;
|
| 1089 |
+
|
| 1090 |
+
// Calculate new pan position to zoom towards mouse
|
| 1091 |
+
const scale = newScale / this.zoomScale;
|
| 1092 |
+
this.panX = x - (x - this.panX) * scale;
|
| 1093 |
+
this.panY = y - (y - this.panY) * scale;
|
| 1094 |
+
|
| 1095 |
+
this.zoomScale = newScale;
|
| 1096 |
+
this.applyTransform();
|
| 1097 |
+
}
|
| 1098 |
+
});
|
| 1099 |
+
|
| 1100 |
+
container.addEventListener('mousedown', (e) => {
|
| 1101 |
+
if (!this.isPanning) return;
|
| 1102 |
+
|
| 1103 |
+
container.style.cursor = 'grabbing';
|
| 1104 |
+
const startX = e.clientX - this.panX * this.zoomScale;
|
| 1105 |
+
const startY = e.clientY - this.panY * this.zoomScale;
|
| 1106 |
+
|
| 1107 |
+
const mouseMoveHandler = (e) => {
|
| 1108 |
+
if (!this.isPanning) return;
|
| 1109 |
+
|
| 1110 |
+
this.panX = (e.clientX - startX) / this.zoomScale;
|
| 1111 |
+
this.panY = (e.clientY - startY) / this.zoomScale;
|
| 1112 |
+
this.applyTransform();
|
| 1113 |
+
};
|
| 1114 |
+
|
| 1115 |
+
const mouseUpHandler = () => {
|
| 1116 |
+
container.style.cursor = this.isPanning ? 'grab' : 'default';
|
| 1117 |
+
document.removeEventListener('mousemove', mouseMoveHandler);
|
| 1118 |
+
document.removeEventListener('mouseup', mouseUpHandler);
|
| 1119 |
+
};
|
| 1120 |
+
|
| 1121 |
+
document.addEventListener('mousemove', mouseMoveHandler);
|
| 1122 |
+
document.addEventListener('mouseup', mouseUpHandler);
|
| 1123 |
+
});
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
applyTransform() {
|
| 1127 |
+
const container = document.querySelector('.fullscreen-mindmap');
|
| 1128 |
+
const matrix = `matrix(${this.zoomScale}, 0, 0, ${this.zoomScale}, ${this.panX * this.zoomScale}, ${this.panY * this.zoomScale})`;
|
| 1129 |
+
container.style.transform = matrix;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
resetView() {
|
| 1133 |
+
this.zoomScale = 1;
|
| 1134 |
+
this.panX = 0;
|
| 1135 |
+
this.panY = 0;
|
| 1136 |
+
this.applyTransform();
|
| 1137 |
+
|
| 1138 |
+
const panButton = document.getElementById('panButton');
|
| 1139 |
+
this.isPanning = false;
|
| 1140 |
+
panButton.classList.remove('active');
|
| 1141 |
+
document.querySelector('.fullscreen-mindmap').style.cursor = 'default';
|
| 1142 |
+
|
| 1143 |
+
if (this.mindmap) {
|
| 1144 |
+
this.mindmap.resize();
|
| 1145 |
+
|
| 1146 |
+
// Center the mindmap after reset
|
| 1147 |
+
const container = document.querySelector('.fullscreen-mindmap');
|
| 1148 |
+
const mindmapEl = document.getElementById(this.container);
|
| 1149 |
+
if (container && mindmapEl) {
|
| 1150 |
+
const containerRect = container.getBoundingClientRect();
|
| 1151 |
+
const mindmapRect = mindmapEl.getBoundingClientRect();
|
| 1152 |
+
|
| 1153 |
+
this.panX = (containerRect.width - mindmapRect.width) / 2;
|
| 1154 |
+
this.panY = (containerRect.height - mindmapRect.height) / 2;
|
| 1155 |
+
this.applyTransform();
|
| 1156 |
+
}
|
| 1157 |
+
}
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
setupToolbar() {
|
| 1161 |
+
const backButton = document.getElementById('backButton');
|
| 1162 |
+
const editButton = document.getElementById('editButton');
|
| 1163 |
+
const zoomInButton = document.getElementById('zoomInButton');
|
| 1164 |
+
const zoomOutButton = document.getElementById('zoomOutButton');
|
| 1165 |
+
const resetButton = document.getElementById('resetButton');
|
| 1166 |
+
const panButton = document.getElementById('panButton');
|
| 1167 |
+
|
| 1168 |
+
if (backButton) {
|
| 1169 |
+
backButton.addEventListener('click', () => {
|
| 1170 |
+
// Hide video section when closing
|
| 1171 |
+
this.videoSection.classList.remove('active');
|
| 1172 |
+
this.toolbar.classList.remove('with-video');
|
| 1173 |
+
|
| 1174 |
+
// Stop video playback
|
| 1175 |
+
if (this.overlayVideo.src) {
|
| 1176 |
+
this.overlayVideo.pause();
|
| 1177 |
+
}
|
| 1178 |
+
|
| 1179 |
+
document.getElementById('mindmapOverlay').classList.remove('active');
|
| 1180 |
+
});
|
| 1181 |
+
|
| 1182 |
+
}
|
| 1183 |
+
|
| 1184 |
+
if (editButton) {
|
| 1185 |
+
editButton.addEventListener('click', () => {
|
| 1186 |
+
this.isEditing = !this.isEditing;
|
| 1187 |
+
editButton.classList.toggle('active');
|
| 1188 |
+
if (this.isEditing) {
|
| 1189 |
+
this.mindmap.enable_edit();
|
| 1190 |
+
editButton.innerHTML = '<i class="fas fa-edit"></i> Done';
|
| 1191 |
+
} else {
|
| 1192 |
+
this.mindmap.disable_edit();
|
| 1193 |
+
editButton.innerHTML = '<i class="fas fa-edit"></i> Edit';
|
| 1194 |
+
}
|
| 1195 |
+
});
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
if (zoomInButton) {
|
| 1199 |
+
zoomInButton.addEventListener('click', () => this.zoomIn());
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
if (zoomOutButton) {
|
| 1203 |
+
zoomOutButton.addEventListener('click', () => this.zoomOut());
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
if (resetButton) {
|
| 1207 |
+
resetButton.addEventListener('click', () => this.resetView());
|
| 1208 |
+
}
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
createEmptyMindMap() {
|
| 1212 |
+
return {
|
| 1213 |
+
meta: {
|
| 1214 |
+
name: 'Topics',
|
| 1215 |
+
author: 'Video Analyzer',
|
| 1216 |
+
version: '1.0'
|
| 1217 |
+
},
|
| 1218 |
+
format: 'node_tree',
|
| 1219 |
+
data: {
|
| 1220 |
+
id: 'root',
|
| 1221 |
+
topic: 'Video Topics',
|
| 1222 |
+
direction: 'center',
|
| 1223 |
+
children: []
|
| 1224 |
+
}
|
| 1225 |
+
};
|
| 1226 |
+
}
|
| 1227 |
+
|
| 1228 |
+
zoomIn() {
|
| 1229 |
+
this.zoomScale = Math.min(this.zoomScale + 0.1, 2);
|
| 1230 |
+
this.applyZoom();
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
zoomOut() {
|
| 1234 |
+
this.zoomScale = Math.max(this.zoomScale - 0.1, 0.5);
|
| 1235 |
+
this.applyZoom();
|
| 1236 |
+
}
|
| 1237 |
+
|
| 1238 |
+
resetView() {
|
| 1239 |
+
this.zoomScale = 1;
|
| 1240 |
+
this.applyZoom();
|
| 1241 |
+
}
|
| 1242 |
+
|
| 1243 |
+
applyZoom() {
|
| 1244 |
+
const container = document.querySelector('.fullscreen-mindmap');
|
| 1245 |
+
if (container) {
|
| 1246 |
+
container.style.transform = `scale(${this.zoomScale})`;
|
| 1247 |
+
container.style.transformOrigin = 'center center';
|
| 1248 |
+
}
|
| 1249 |
+
}
|
| 1250 |
+
|
| 1251 |
+
createMindMapData(segments) {
|
| 1252 |
+
if (!segments || segments.length === 0) {
|
| 1253 |
+
return this.createEmptyMindMap();
|
| 1254 |
+
}
|
| 1255 |
+
|
| 1256 |
+
const mind = {
|
| 1257 |
+
meta: {
|
| 1258 |
+
name: 'Topics',
|
| 1259 |
+
author: 'Video Analyzer',
|
| 1260 |
+
version: '1.0'
|
| 1261 |
+
},
|
| 1262 |
+
format: 'node_tree',
|
| 1263 |
+
data: {
|
| 1264 |
+
id: 'root',
|
| 1265 |
+
topic: 'Video Topics',
|
| 1266 |
+
direction: 'center',
|
| 1267 |
+
expanded: true,
|
| 1268 |
+
children: []
|
| 1269 |
+
}
|
| 1270 |
+
};
|
| 1271 |
+
|
| 1272 |
+
// Clear previous segment data
|
| 1273 |
+
this.segmentData.clear();
|
| 1274 |
+
|
| 1275 |
+
// Process each segment
|
| 1276 |
+
let nodeId = 1;
|
| 1277 |
+
|
| 1278 |
+
segments.forEach(segment => {
|
| 1279 |
+
// Extract a cleaner topic name without repeating terms
|
| 1280 |
+
const topicName = segment.topic_name.split(':')[1]?.trim() || segment.topic_name.trim();
|
| 1281 |
+
|
| 1282 |
+
// Create the main topic node
|
| 1283 |
+
const topicNode = {
|
| 1284 |
+
id: `topic_${nodeId}`,
|
| 1285 |
+
topic: topicName,
|
| 1286 |
+
direction: this.getDirection(nodeId, segments.length),
|
| 1287 |
+
expanded: false,
|
| 1288 |
+
children: []
|
| 1289 |
+
};
|
| 1290 |
+
|
| 1291 |
+
// Store the timestamp for the main node
|
| 1292 |
+
this.segmentData.set(`topic_${nodeId}`, {
|
| 1293 |
+
timestamp: segment.timestamp || 0,
|
| 1294 |
+
content: Array.isArray(segment.content) ? segment.content.join('\n') : segment.content
|
| 1295 |
+
});
|
| 1296 |
+
|
| 1297 |
+
// If this is a parent topic with children, add them as subnodes
|
| 1298 |
+
if (segment.is_parent && segment.children && segment.children.length > 0) {
|
| 1299 |
+
segment.children.forEach((child, childIndex) => {
|
| 1300 |
+
const childName = child.topic_name.split(':')[1]?.trim() || child.topic_name.trim();
|
| 1301 |
+
const childContent = Array.isArray(child.content) ? child.content.join('\n') : child.content;
|
| 1302 |
+
|
| 1303 |
+
// Extract a meaningful snippet from the content
|
| 1304 |
+
let contentSnippet = '';
|
| 1305 |
+
const cleanContent = childContent.replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
|
| 1306 |
+
contentSnippet = cleanContent.split('.')[0];
|
| 1307 |
+
if (contentSnippet.length > 50) {
|
| 1308 |
+
contentSnippet = contentSnippet.slice(0, 50) + '...';
|
| 1309 |
+
} else {
|
| 1310 |
+
contentSnippet += '...';
|
| 1311 |
+
}
|
| 1312 |
+
|
| 1313 |
+
const childNodeId = `content_${nodeId}_${childIndex}`;
|
| 1314 |
+
|
| 1315 |
+
topicNode.children.push({
|
| 1316 |
+
id: childNodeId,
|
| 1317 |
+
topic: childName,
|
| 1318 |
+
direction: topicNode.direction,
|
| 1319 |
+
expanded: false
|
| 1320 |
+
});
|
| 1321 |
+
|
| 1322 |
+
// Store the timestamp and content for this child node
|
| 1323 |
+
this.segmentData.set(childNodeId, {
|
| 1324 |
+
timestamp: child.timestamp || 0,
|
| 1325 |
+
content: childContent
|
| 1326 |
+
});
|
| 1327 |
+
});
|
| 1328 |
+
}
|
| 1329 |
+
// For regular segments or parents without children, add their content as subnodes
|
| 1330 |
+
else {
|
| 1331 |
+
// Get the content as an array
|
| 1332 |
+
const contentArray = Array.isArray(segment.content) ? segment.content : [segment.content];
|
| 1333 |
+
|
| 1334 |
+
// If there's a lot of content, create content summary nodes
|
| 1335 |
+
if (contentArray.length > 5) {
|
| 1336 |
+
// Create just one summary node for all content
|
| 1337 |
+
const contentNodeId = `content_${nodeId}_0`;
|
| 1338 |
+
|
| 1339 |
+
// Create a content summary
|
| 1340 |
+
const cleanContent = contentArray.join(' ').replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
|
| 1341 |
+
let contentSnippet = cleanContent.split('.')[0];
|
| 1342 |
+
if (contentSnippet.length > 50) {
|
| 1343 |
+
contentSnippet = contentSnippet.slice(0, 50) + '...';
|
| 1344 |
+
} else {
|
| 1345 |
+
contentSnippet += '...';
|
| 1346 |
+
}
|
| 1347 |
+
|
| 1348 |
+
topicNode.children.push({
|
| 1349 |
+
id: contentNodeId,
|
| 1350 |
+
topic: contentSnippet,
|
| 1351 |
+
direction: topicNode.direction,
|
| 1352 |
+
expanded: false
|
| 1353 |
+
});
|
| 1354 |
+
|
| 1355 |
+
// Store the timestamp and full content
|
| 1356 |
+
this.segmentData.set(contentNodeId, {
|
| 1357 |
+
timestamp: segment.timestamp || 0,
|
| 1358 |
+
content: contentArray.join('\n')
|
| 1359 |
+
});
|
| 1360 |
+
}
|
| 1361 |
+
// For small segments, include separate nodes for each part
|
| 1362 |
+
else {
|
| 1363 |
+
contentArray.forEach((content, index) => {
|
| 1364 |
+
if (typeof content === 'string' && content.trim()) {
|
| 1365 |
+
const contentNodeId = `content_${nodeId}_${index}`;
|
| 1366 |
+
|
| 1367 |
+
// Clean up the content for display
|
| 1368 |
+
const cleanContent = content.replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
|
| 1369 |
+
let contentSnippet = cleanContent.split('.')[0];
|
| 1370 |
+
if (contentSnippet.length > 50) {
|
| 1371 |
+
contentSnippet = contentSnippet.slice(0, 50) + '...';
|
| 1372 |
+
} else {
|
| 1373 |
+
contentSnippet += '...';
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
topicNode.children.push({
|
| 1377 |
+
id: contentNodeId,
|
| 1378 |
+
topic: contentSnippet,
|
| 1379 |
+
direction: topicNode.direction,
|
| 1380 |
+
expanded: false
|
| 1381 |
+
});
|
| 1382 |
+
|
| 1383 |
+
// Store the timestamp and full content
|
| 1384 |
+
this.segmentData.set(contentNodeId, {
|
| 1385 |
+
timestamp: segment.timestamp || 0,
|
| 1386 |
+
content: content
|
| 1387 |
+
});
|
| 1388 |
+
}
|
| 1389 |
+
});
|
| 1390 |
+
}
|
| 1391 |
+
}
|
| 1392 |
+
|
| 1393 |
+
mind.data.children.push(topicNode);
|
| 1394 |
+
nodeId++;
|
| 1395 |
+
});
|
| 1396 |
+
|
| 1397 |
+
return mind;
|
| 1398 |
+
}
|
| 1399 |
+
|
| 1400 |
+
extractTimestamp(text) {
|
| 1401 |
+
// Look for timestamps in the format [X.X] or [X]
|
| 1402 |
+
const matches = text.match(/\[(\d+(?:\.\d+)?)\]/g);
|
| 1403 |
+
if (matches && matches.length > 0) {
|
| 1404 |
+
// Extract the first timestamp found
|
| 1405 |
+
const firstTimestamp = matches[0].replace('[', '').replace(']', '');
|
| 1406 |
+
// Round down to the nearest second (floor)
|
| 1407 |
+
const seconds = Math.floor(parseFloat(firstTimestamp));
|
| 1408 |
+
console.log('Extracted timestamp:', seconds, 'from text:', text);
|
| 1409 |
+
return seconds;
|
| 1410 |
+
}
|
| 1411 |
+
return 0;
|
| 1412 |
+
}
|
| 1413 |
+
|
| 1414 |
+
getDirection(index, total) {
|
| 1415 |
+
const halfTotal = Math.ceil(total / 2);
|
| 1416 |
+
if (index <= halfTotal) {
|
| 1417 |
+
return 'right';
|
| 1418 |
+
} else {
|
| 1419 |
+
return 'left';
|
| 1420 |
+
}
|
| 1421 |
+
}
|
| 1422 |
+
|
| 1423 |
+
update(segments) {
|
| 1424 |
+
if (!this.mindmap) {
|
| 1425 |
+
console.error('JSMind not initialized');
|
| 1426 |
+
return;
|
| 1427 |
+
}
|
| 1428 |
+
|
| 1429 |
+
try {
|
| 1430 |
+
// Store any current node colors before updating
|
| 1431 |
+
const nodeColors = this.saveNodeColors();
|
| 1432 |
+
|
| 1433 |
+
// Clear previous data
|
| 1434 |
+
this.segmentData.clear();
|
| 1435 |
+
|
| 1436 |
+
// Create and show new mindmap
|
| 1437 |
+
const mindData = this.createMindMapData(segments);
|
| 1438 |
+
this.mindmap.show(mindData);
|
| 1439 |
+
|
| 1440 |
+
// Save the mindmap to gallery
|
| 1441 |
+
this.saveMindmapToGallery(mindData);
|
| 1442 |
+
|
| 1443 |
+
// Restore node colors
|
| 1444 |
+
this.restoreNodeColors(nodeColors);
|
| 1445 |
+
|
| 1446 |
+
// Apply custom classes to expanders
|
| 1447 |
+
this.applyCustomExpanderClasses();
|
| 1448 |
+
|
| 1449 |
+
// Reset view and show overlay
|
| 1450 |
+
this.resetView();
|
| 1451 |
+
if (this.mindmapOverlay) {
|
| 1452 |
+
this.mindmapOverlay.classList.add('active');
|
| 1453 |
+
}
|
| 1454 |
+
|
| 1455 |
+
// Hide video section initially
|
| 1456 |
+
if (this.videoSection) {
|
| 1457 |
+
this.videoSection.style.display = 'none';
|
| 1458 |
+
}
|
| 1459 |
+
|
| 1460 |
+
// Reset mindmap section to full width
|
| 1461 |
+
const mindmapSection = document.querySelector('.mindmap-section');
|
| 1462 |
+
if (mindmapSection) {
|
| 1463 |
+
mindmapSection.style.width = '100%';
|
| 1464 |
+
}
|
| 1465 |
+
|
| 1466 |
+
// Remove video-related classes
|
| 1467 |
+
if (this.toolbar) {
|
| 1468 |
+
this.toolbar.classList.remove('with-video');
|
| 1469 |
+
}
|
| 1470 |
+
|
| 1471 |
+
// Reinitialize node interaction
|
| 1472 |
+
this.setupNodeInteraction();
|
| 1473 |
+
|
| 1474 |
+
} catch (error) {
|
| 1475 |
+
console.error('Error updating mindmap:', error);
|
| 1476 |
+
}
|
| 1477 |
+
}
|
| 1478 |
+
|
| 1479 |
+
saveMindmapToGallery(mindData) {
|
| 1480 |
+
// Create mindmap data object
|
| 1481 |
+
const mindmapData = {
|
| 1482 |
+
id: Date.now(),
|
| 1483 |
+
title: mindData.data.topic || 'Video Topics',
|
| 1484 |
+
data: mindData,
|
| 1485 |
+
date: new Date().toISOString()
|
| 1486 |
+
};
|
| 1487 |
+
|
| 1488 |
+
// Save to localStorage
|
| 1489 |
+
saveMindmap(mindmapData);
|
| 1490 |
+
}
|
| 1491 |
+
|
| 1492 |
+
saveNodeColors() {
|
| 1493 |
+
// Save current node colors
|
| 1494 |
+
const nodeColors = {};
|
| 1495 |
+
if (this.mindmap) {
|
| 1496 |
+
const nodes = document.querySelectorAll('jmnode');
|
| 1497 |
+
nodes.forEach(node => {
|
| 1498 |
+
const nodeId = node.getAttribute('nodeid');
|
| 1499 |
+
if (nodeId) {
|
| 1500 |
+
const colorClasses = [
|
| 1501 |
+
'node-blue', 'node-green', 'node-red', 'node-purple',
|
| 1502 |
+
'node-orange', 'node-yellow', 'node-teal', 'node-gray'
|
| 1503 |
+
];
|
| 1504 |
+
|
| 1505 |
+
colorClasses.forEach(cls => {
|
| 1506 |
+
if (node.classList.contains(cls)) {
|
| 1507 |
+
nodeColors[nodeId] = cls.replace('node-', '');
|
| 1508 |
+
}
|
| 1509 |
+
});
|
| 1510 |
+
}
|
| 1511 |
+
});
|
| 1512 |
+
}
|
| 1513 |
+
return nodeColors;
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
restoreNodeColors(nodeColors) {
|
| 1517 |
+
// Restore saved node colors
|
| 1518 |
+
if (nodeColors && Object.keys(nodeColors).length > 0) {
|
| 1519 |
+
setTimeout(() => {
|
| 1520 |
+
for (const [nodeId, color] of Object.entries(nodeColors)) {
|
| 1521 |
+
this.changeNodeColor(nodeId, color);
|
| 1522 |
+
}
|
| 1523 |
+
}, 100);
|
| 1524 |
+
}
|
| 1525 |
+
}
|
| 1526 |
+
|
| 1527 |
+
setupVideoCloseButton() {
|
| 1528 |
+
if (this.videoCloseButton) {
|
| 1529 |
+
this.videoCloseButton.addEventListener('click', () => {
|
| 1530 |
+
this.closeVideoOverlay();
|
| 1531 |
+
});
|
| 1532 |
+
}
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
closeVideoOverlay() {
|
| 1536 |
+
// Hide video section
|
| 1537 |
+
if (this.videoSection) {
|
| 1538 |
+
this.videoSection.style.display = 'none';
|
| 1539 |
+
|
| 1540 |
+
// Stop video playback
|
| 1541 |
+
if (this.overlayVideo && this.overlayVideo.src) {
|
| 1542 |
+
this.overlayVideo.pause();
|
| 1543 |
+
}
|
| 1544 |
+
}
|
| 1545 |
+
|
| 1546 |
+
// Remove active class from toolbar
|
| 1547 |
+
if (this.toolbar) {
|
| 1548 |
+
this.toolbar.classList.remove('with-video');
|
| 1549 |
+
}
|
| 1550 |
+
|
| 1551 |
+
// Reset mindmap section to full width
|
| 1552 |
+
const mindmapSection = document.querySelector('.mindmap-section');
|
| 1553 |
+
if (mindmapSection) {
|
| 1554 |
+
mindmapSection.style.width = '100%';
|
| 1555 |
+
}
|
| 1556 |
+
|
| 1557 |
+
// Force mindmap refresh
|
| 1558 |
+
if (this.mindmap) {
|
| 1559 |
+
this.mindmap.resize();
|
| 1560 |
+
}
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
applyCustomExpanderClasses() {
|
| 1564 |
+
// Add custom classes to expand/collapse buttons
|
| 1565 |
+
setTimeout(() => {
|
| 1566 |
+
this.processExpanders();
|
| 1567 |
+
|
| 1568 |
+
// Set up a mutation observer to handle future changes
|
| 1569 |
+
const observer = new MutationObserver(mutations => {
|
| 1570 |
+
mutations.forEach(mutation => {
|
| 1571 |
+
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
| 1572 |
+
this.processExpanders();
|
| 1573 |
+
}
|
| 1574 |
+
});
|
| 1575 |
+
});
|
| 1576 |
+
|
| 1577 |
+
observer.observe(document.getElementById(this.container), {
|
| 1578 |
+
childList: true,
|
| 1579 |
+
subtree: true,
|
| 1580 |
+
characterData: true
|
| 1581 |
+
});
|
| 1582 |
+
}, 100);
|
| 1583 |
+
}
|
| 1584 |
+
|
| 1585 |
+
processExpanders() {
|
| 1586 |
+
const expanders = document.querySelectorAll('jmexpander');
|
| 1587 |
+
expanders.forEach(expander => {
|
| 1588 |
+
// Check if it's an expand or collapse button
|
| 1589 |
+
if (expander.textContent.includes('+')) {
|
| 1590 |
+
expander.classList.add('expand');
|
| 1591 |
+
// Clear content more aggressively
|
| 1592 |
+
expander.textContent = '';
|
| 1593 |
+
expander.innerText = '';
|
| 1594 |
+
expander.innerHTML = '';
|
| 1595 |
+
} else if (expander.textContent.includes('-')) {
|
| 1596 |
+
expander.classList.add('collapse');
|
| 1597 |
+
// Clear content more aggressively
|
| 1598 |
+
expander.textContent = '';
|
| 1599 |
+
expander.innerText = '';
|
| 1600 |
+
expander.innerHTML = '';
|
| 1601 |
+
}
|
| 1602 |
+
});
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
+
setupContextMenu() {
|
| 1606 |
+
// Hide context menu when clicking elsewhere
|
| 1607 |
+
document.addEventListener('click', (e) => {
|
| 1608 |
+
if (!this.contextMenu.contains(e.target)) {
|
| 1609 |
+
this.hideContextMenu();
|
| 1610 |
+
}
|
| 1611 |
+
});
|
| 1612 |
+
|
| 1613 |
+
// Hide context menu on escape key
|
| 1614 |
+
document.addEventListener('keydown', (e) => {
|
| 1615 |
+
if (e.key === 'Escape') {
|
| 1616 |
+
this.hideContextMenu();
|
| 1617 |
+
}
|
| 1618 |
+
});
|
| 1619 |
+
|
| 1620 |
+
// Hide color picker by default and setup action handlers
|
| 1621 |
+
this.colorPicker.style.display = 'none';
|
| 1622 |
+
this.setupContextMenuActions();
|
| 1623 |
+
|
| 1624 |
+
// Add event listeners for right-click context menu
|
| 1625 |
+
const container = document.getElementById(this.container);
|
| 1626 |
+
if (!container) return;
|
| 1627 |
+
|
| 1628 |
+
container.addEventListener('contextmenu', (e) => {
|
| 1629 |
+
const target = e.target;
|
| 1630 |
+
if (target.tagName.toLowerCase() === 'jmnode') {
|
| 1631 |
+
e.preventDefault();
|
| 1632 |
+
const nodeId = target.getAttribute('nodeid');
|
| 1633 |
+
if (nodeId) {
|
| 1634 |
+
this.currentNodeId = nodeId;
|
| 1635 |
+
this.showContextMenu(e.clientX, e.clientY);
|
| 1636 |
+
|
| 1637 |
+
// Update expand/collapse text based on node state
|
| 1638 |
+
const expandAction = document.getElementById('expandCollapseAction');
|
| 1639 |
+
const node = this.mindmap.get_node(nodeId);
|
| 1640 |
+
const isExpanded = this.mindmap.get_node_expanded(node);
|
| 1641 |
+
|
| 1642 |
+
if (expandAction) {
|
| 1643 |
+
if (isExpanded) {
|
| 1644 |
+
expandAction.innerHTML = '<i class="fas fa-angle-double-up"></i> Collapse all children';
|
| 1645 |
+
} else {
|
| 1646 |
+
expandAction.innerHTML = '<i class="fas fa-angle-double-down"></i> Expand all children';
|
| 1647 |
+
}
|
| 1648 |
+
}
|
| 1649 |
+
|
| 1650 |
+
// Disable parent addition for root node
|
| 1651 |
+
const addParentAction = document.getElementById('addParentAction');
|
| 1652 |
+
if (addParentAction) {
|
| 1653 |
+
if (nodeId === 'root') {
|
| 1654 |
+
addParentAction.classList.add('disabled');
|
| 1655 |
+
addParentAction.style.opacity = '0.5';
|
| 1656 |
+
addParentAction.style.cursor = 'not-allowed';
|
| 1657 |
+
} else {
|
| 1658 |
+
addParentAction.classList.remove('disabled');
|
| 1659 |
+
addParentAction.style.opacity = '1';
|
| 1660 |
+
addParentAction.style.cursor = 'pointer';
|
| 1661 |
+
}
|
| 1662 |
+
}
|
| 1663 |
+
}
|
| 1664 |
+
}
|
| 1665 |
+
});
|
| 1666 |
+
}
|
| 1667 |
+
|
| 1668 |
+
setupContextMenuActions() {
|
| 1669 |
+
// Edit node action
|
| 1670 |
+
document.getElementById('editNodeAction').addEventListener('click', () => {
|
| 1671 |
+
this.editNodeText(this.currentNodeId);
|
| 1672 |
+
this.hideContextMenu();
|
| 1673 |
+
});
|
| 1674 |
+
|
| 1675 |
+
// Add child node action
|
| 1676 |
+
document.getElementById('addChildAction').addEventListener('click', () => {
|
| 1677 |
+
this.addChildNode(this.currentNodeId);
|
| 1678 |
+
this.hideContextMenu();
|
| 1679 |
+
});
|
| 1680 |
+
|
| 1681 |
+
// Add parent node action
|
| 1682 |
+
document.getElementById('addParentAction').addEventListener('click', () => {
|
| 1683 |
+
if (this.currentNodeId !== 'root') {
|
| 1684 |
+
this.addParentNode(this.currentNodeId);
|
| 1685 |
+
}
|
| 1686 |
+
this.hideContextMenu();
|
| 1687 |
+
});
|
| 1688 |
+
|
| 1689 |
+
// Focus on branch action
|
| 1690 |
+
document.getElementById('focusNodeAction').addEventListener('click', () => {
|
| 1691 |
+
this.focusOnNode(this.currentNodeId);
|
| 1692 |
+
this.hideContextMenu();
|
| 1693 |
+
});
|
| 1694 |
+
|
| 1695 |
+
// Expand/collapse all children action
|
| 1696 |
+
document.getElementById('expandCollapseAction').addEventListener('click', () => {
|
| 1697 |
+
this.toggleExpandCollapseAll(this.currentNodeId);
|
| 1698 |
+
this.hideContextMenu();
|
| 1699 |
+
});
|
| 1700 |
+
|
| 1701 |
+
// Open color picker
|
| 1702 |
+
document.getElementById('nodeColorAction').addEventListener('click', (e) => {
|
| 1703 |
+
e.stopPropagation();
|
| 1704 |
+
this.toggleColorPicker();
|
| 1705 |
+
});
|
| 1706 |
+
|
| 1707 |
+
// Delete node action
|
| 1708 |
+
document.getElementById('deleteNodeAction').addEventListener('click', () => {
|
| 1709 |
+
this.deleteNode(this.currentNodeId);
|
| 1710 |
+
this.hideContextMenu();
|
| 1711 |
+
});
|
| 1712 |
+
|
| 1713 |
+
// Color picker options
|
| 1714 |
+
const colorOptions = document.querySelectorAll('.color-option');
|
| 1715 |
+
colorOptions.forEach(option => {
|
| 1716 |
+
option.addEventListener('click', (e) => {
|
| 1717 |
+
const color = e.target.getAttribute('data-color');
|
| 1718 |
+
this.changeNodeColor(this.currentNodeId, color);
|
| 1719 |
+
this.hideContextMenu();
|
| 1720 |
+
});
|
| 1721 |
+
});
|
| 1722 |
+
}
|
| 1723 |
+
|
| 1724 |
+
showContextMenu(x, y) {
|
| 1725 |
+
// Reset color picker visibility
|
| 1726 |
+
this.colorPicker.style.display = 'none';
|
| 1727 |
+
|
| 1728 |
+
// Position the context menu
|
| 1729 |
+
this.contextMenu.style.left = `${x}px`;
|
| 1730 |
+
this.contextMenu.style.top = `${y}px`;
|
| 1731 |
+
|
| 1732 |
+
// Show the context menu
|
| 1733 |
+
this.contextMenu.classList.add('visible');
|
| 1734 |
+
}
|
| 1735 |
+
|
| 1736 |
+
hideContextMenu() {
|
| 1737 |
+
// Hide the context menu
|
| 1738 |
+
this.contextMenu.classList.remove('visible');
|
| 1739 |
+
this.colorPicker.style.display = 'none';
|
| 1740 |
+
}
|
| 1741 |
+
|
| 1742 |
+
toggleColorPicker() {
|
| 1743 |
+
// Toggle color picker visibility
|
| 1744 |
+
this.colorPicker.style.display = this.colorPicker.style.display === 'none' ? 'block' : 'none';
|
| 1745 |
+
}
|
| 1746 |
+
|
| 1747 |
+
editNodeText(nodeId) {
|
| 1748 |
+
// Get the node element and its position
|
| 1749 |
+
const node = this.mindmap.get_node(nodeId);
|
| 1750 |
+
const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
|
| 1751 |
+
|
| 1752 |
+
if (!node || !nodeElement) return;
|
| 1753 |
+
|
| 1754 |
+
// Get node position
|
| 1755 |
+
const rect = nodeElement.getBoundingClientRect();
|
| 1756 |
+
|
| 1757 |
+
// Create input element
|
| 1758 |
+
const input = document.createElement('input');
|
| 1759 |
+
input.type = 'text';
|
| 1760 |
+
input.className = 'node-edit-input';
|
| 1761 |
+
input.value = node.topic;
|
| 1762 |
+
|
| 1763 |
+
// Position input over the node
|
| 1764 |
+
input.style.left = `${rect.left}px`;
|
| 1765 |
+
input.style.top = `${rect.top}px`;
|
| 1766 |
+
input.style.width = `${Math.max(rect.width, 150)}px`;
|
| 1767 |
+
|
| 1768 |
+
// Add input to DOM
|
| 1769 |
+
document.body.appendChild(input);
|
| 1770 |
+
|
| 1771 |
+
// Focus and select text
|
| 1772 |
+
input.focus();
|
| 1773 |
+
input.select();
|
| 1774 |
+
|
| 1775 |
+
// Handle completion of editing
|
| 1776 |
+
const completeEdit = () => {
|
| 1777 |
+
const newText = input.value.trim();
|
| 1778 |
+
if (newText && newText !== node.topic) {
|
| 1779 |
+
this.mindmap.update_node(nodeId, newText);
|
| 1780 |
+
}
|
| 1781 |
+
document.body.removeChild(input);
|
| 1782 |
+
};
|
| 1783 |
+
|
| 1784 |
+
// Event listeners for input
|
| 1785 |
+
input.addEventListener('blur', completeEdit);
|
| 1786 |
+
input.addEventListener('keydown', (e) => {
|
| 1787 |
+
if (e.key === 'Enter') {
|
| 1788 |
+
completeEdit();
|
| 1789 |
+
} else if (e.key === 'Escape') {
|
| 1790 |
+
document.body.removeChild(input);
|
| 1791 |
+
}
|
| 1792 |
+
});
|
| 1793 |
+
}
|
| 1794 |
+
|
| 1795 |
+
addChildNode(parentId) {
|
| 1796 |
+
// Generate a unique ID for the new node
|
| 1797 |
+
const newNodeId = `node_${Date.now()}`;
|
| 1798 |
+
|
| 1799 |
+
// Add a new child node
|
| 1800 |
+
this.mindmap.add_node(parentId, newNodeId, 'New Node');
|
| 1801 |
+
|
| 1802 |
+
// Ensure the parent is expanded
|
| 1803 |
+
this.mindmap.expand_node(parentId);
|
| 1804 |
+
|
| 1805 |
+
// After a small delay, edit the new node
|
| 1806 |
+
setTimeout(() => {
|
| 1807 |
+
this.editNodeText(newNodeId);
|
| 1808 |
+
}, 100);
|
| 1809 |
+
}
|
| 1810 |
+
|
| 1811 |
+
addParentNode(childId) {
|
| 1812 |
+
// This is a more complex operation as jsMind doesn't have direct support
|
| 1813 |
+
const child = this.mindmap.get_node(childId);
|
| 1814 |
+
if (!child || childId === 'root') return;
|
| 1815 |
+
|
| 1816 |
+
const originalParentId = child.parent.id;
|
| 1817 |
+
|
| 1818 |
+
// Generate a unique ID for the new parent node
|
| 1819 |
+
const newParentId = `node_${Date.now()}`;
|
| 1820 |
+
|
| 1821 |
+
// Add the new parent to the original parent
|
| 1822 |
+
this.mindmap.add_node(originalParentId, newParentId, 'New Parent');
|
| 1823 |
+
|
| 1824 |
+
// Move the child node to be under the new parent
|
| 1825 |
+
this.mindmap.move_node(childId, newParentId);
|
| 1826 |
+
|
| 1827 |
+
// Ensure nodes are expanded
|
| 1828 |
+
this.mindmap.expand_node(originalParentId);
|
| 1829 |
+
this.mindmap.expand_node(newParentId);
|
| 1830 |
+
|
| 1831 |
+
// After a small delay, edit the new node
|
| 1832 |
+
setTimeout(() => {
|
| 1833 |
+
this.editNodeText(newParentId);
|
| 1834 |
+
}, 100);
|
| 1835 |
+
}
|
| 1836 |
+
|
| 1837 |
+
focusOnNode(nodeId) {
|
| 1838 |
+
// First, ensure the node and all its ancestors are expanded
|
| 1839 |
+
let node = this.mindmap.get_node(nodeId);
|
| 1840 |
+
while (node && node.id !== 'root') {
|
| 1841 |
+
this.mindmap.expand_node(node.parent.id);
|
| 1842 |
+
node = node.parent;
|
| 1843 |
+
}
|
| 1844 |
+
|
| 1845 |
+
// Get the node element
|
| 1846 |
+
const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
|
| 1847 |
+
if (!nodeElement) return;
|
| 1848 |
+
|
| 1849 |
+
// Get node position and center the view on it
|
| 1850 |
+
const rect = nodeElement.getBoundingClientRect();
|
| 1851 |
+
const containerRect = document.querySelector('.fullscreen-mindmap').getBoundingClientRect();
|
| 1852 |
+
|
| 1853 |
+
// Calculate the center position
|
| 1854 |
+
const centerX = containerRect.width / 2;
|
| 1855 |
+
const centerY = containerRect.height / 2;
|
| 1856 |
+
|
| 1857 |
+
// Calculate the target position to center the node
|
| 1858 |
+
this.panX = centerX - rect.left - rect.width / 2;
|
| 1859 |
+
this.panY = centerY - rect.top - rect.height / 2;
|
| 1860 |
+
|
| 1861 |
+
// Apply the transformation
|
| 1862 |
+
this.applyTransform();
|
| 1863 |
+
}
|
| 1864 |
+
|
| 1865 |
+
toggleExpandCollapseAll(nodeId) {
|
| 1866 |
+
const node = this.mindmap.get_node(nodeId);
|
| 1867 |
+
if (!node) return;
|
| 1868 |
+
|
| 1869 |
+
const isExpanded = this.mindmap.get_node_expanded(node);
|
| 1870 |
+
|
| 1871 |
+
if (isExpanded) {
|
| 1872 |
+
// Collapse all descendants
|
| 1873 |
+
this.collapseAllChildren(nodeId);
|
| 1874 |
+
} else {
|
| 1875 |
+
// Expand all descendants
|
| 1876 |
+
this.expandAllChildren(nodeId);
|
| 1877 |
+
}
|
| 1878 |
+
}
|
| 1879 |
+
|
| 1880 |
+
expandAllChildren(nodeId) {
|
| 1881 |
+
// Recursively expand all nodes
|
| 1882 |
+
const expandRecursively = (id) => {
|
| 1883 |
+
const node = this.mindmap.get_node(id);
|
| 1884 |
+
if (!node) return;
|
| 1885 |
+
|
| 1886 |
+
this.mindmap.expand_node(id);
|
| 1887 |
+
|
| 1888 |
+
if (node.children && node.children.length > 0) {
|
| 1889 |
+
node.children.forEach(child => {
|
| 1890 |
+
expandRecursively(child.id);
|
| 1891 |
+
});
|
| 1892 |
+
}
|
| 1893 |
+
};
|
| 1894 |
+
|
| 1895 |
+
expandRecursively(nodeId);
|
| 1896 |
+
}
|
| 1897 |
+
|
| 1898 |
+
collapseAllChildren(nodeId) {
|
| 1899 |
+
// Recursively collapse all nodes
|
| 1900 |
+
const collapseRecursively = (id) => {
|
| 1901 |
+
const node = this.mindmap.get_node(id);
|
| 1902 |
+
if (!node) return;
|
| 1903 |
+
|
| 1904 |
+
if (node.children && node.children.length > 0) {
|
| 1905 |
+
node.children.forEach(child => {
|
| 1906 |
+
collapseRecursively(child.id);
|
| 1907 |
+
});
|
| 1908 |
+
this.mindmap.collapse_node(id);
|
| 1909 |
+
}
|
| 1910 |
+
};
|
| 1911 |
+
|
| 1912 |
+
// We don't want to collapse the starting node itself
|
| 1913 |
+
const node = this.mindmap.get_node(nodeId);
|
| 1914 |
+
if (node && node.children && node.children.length > 0) {
|
| 1915 |
+
node.children.forEach(child => {
|
| 1916 |
+
collapseRecursively(child.id);
|
| 1917 |
+
});
|
| 1918 |
+
}
|
| 1919 |
+
}
|
| 1920 |
+
|
| 1921 |
+
changeNodeColor(nodeId, color) {
|
| 1922 |
+
if (!nodeId || !color) return;
|
| 1923 |
+
|
| 1924 |
+
// Get the node element
|
| 1925 |
+
const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
|
| 1926 |
+
if (!nodeElement) return;
|
| 1927 |
+
|
| 1928 |
+
// Remove existing color classes
|
| 1929 |
+
const colorClasses = [
|
| 1930 |
+
'node-blue', 'node-green', 'node-red', 'node-purple',
|
| 1931 |
+
'node-orange', 'node-yellow', 'node-teal', 'node-gray'
|
| 1932 |
+
];
|
| 1933 |
+
|
| 1934 |
+
colorClasses.forEach(cls => {
|
| 1935 |
+
nodeElement.classList.remove(cls);
|
| 1936 |
+
});
|
| 1937 |
+
|
| 1938 |
+
// Add the new color class
|
| 1939 |
+
nodeElement.classList.add(`node-${color}`);
|
| 1940 |
+
|
| 1941 |
+
// Store the color in the node data (if needed for persistence)
|
| 1942 |
+
const node = this.mindmap.get_node(nodeId);
|
| 1943 |
+
if (node) {
|
| 1944 |
+
node.data = node.data || {};
|
| 1945 |
+
node.data.color = color;
|
| 1946 |
+
}
|
| 1947 |
+
}
|
| 1948 |
+
|
| 1949 |
+
deleteNode(nodeId) {
|
| 1950 |
+
// Can't delete the root node
|
| 1951 |
+
if (nodeId === 'root') return;
|
| 1952 |
+
|
| 1953 |
+
// Confirm deletion
|
| 1954 |
+
if (confirm('Are you sure you want to delete this node and all its children?')) {
|
| 1955 |
+
this.mindmap.remove_node(nodeId);
|
| 1956 |
+
}
|
| 1957 |
+
}
|
| 1958 |
+
}
|
| 1959 |
+
|
| 1960 |
+
// Custom Video Player
|
| 1961 |
+
function initCustomVideoPlayer() {
|
| 1962 |
+
const videoPlayer = document.getElementById('videoPlayer');
|
| 1963 |
+
const videoControls = document.querySelector('.video-controls');
|
| 1964 |
+
const playPauseBtn = document.getElementById('playPauseBtn');
|
| 1965 |
+
const progressBar = document.querySelector('.video-progress-bar');
|
| 1966 |
+
const progress = document.querySelector('.video-progress');
|
| 1967 |
+
const currentTimeDisplay = document.getElementById('currentTime');
|
| 1968 |
+
const durationDisplay = document.getElementById('duration');
|
| 1969 |
+
const muteBtn = document.getElementById('muteBtn');
|
| 1970 |
+
const fullscreenBtn = document.getElementById('fullscreenBtn');
|
| 1971 |
+
const videoContainer = document.querySelector('.video-preview');
|
| 1972 |
+
|
| 1973 |
+
if (!videoPlayer || !videoControls) return;
|
| 1974 |
+
|
| 1975 |
+
// Hide default controls (but keep them accessible for mobile/compatibility)
|
| 1976 |
+
videoPlayer.controls = false;
|
| 1977 |
+
|
| 1978 |
+
// Play/Pause
|
| 1979 |
+
playPauseBtn.addEventListener('click', () => {
|
| 1980 |
+
if (videoPlayer.paused) {
|
| 1981 |
+
videoPlayer.play();
|
| 1982 |
+
videoContainer.classList.add('playing');
|
| 1983 |
+
} else {
|
| 1984 |
+
videoPlayer.pause();
|
| 1985 |
+
videoContainer.classList.remove('playing');
|
| 1986 |
+
}
|
| 1987 |
+
});
|
| 1988 |
+
|
| 1989 |
+
// Update progress bar
|
| 1990 |
+
videoPlayer.addEventListener('timeupdate', () => {
|
| 1991 |
+
const percent = (videoPlayer.currentTime / videoPlayer.duration) * 100;
|
| 1992 |
+
progressBar.style.width = `${percent}%`;
|
| 1993 |
+
|
| 1994 |
+
// Update time display
|
| 1995 |
+
currentTimeDisplay.textContent = formatTime(videoPlayer.currentTime);
|
| 1996 |
+
});
|
| 1997 |
+
|
| 1998 |
+
// Click on progress bar to seek
|
| 1999 |
+
progress.addEventListener('click', (e) => {
|
| 2000 |
+
const rect = progress.getBoundingClientRect();
|
| 2001 |
+
const pos = (e.clientX - rect.left) / rect.width;
|
| 2002 |
+
videoPlayer.currentTime = pos * videoPlayer.duration;
|
| 2003 |
+
});
|
| 2004 |
+
|
| 2005 |
+
// Load metadata (duration, etc.)
|
| 2006 |
+
videoPlayer.addEventListener('loadedmetadata', () => {
|
| 2007 |
+
durationDisplay.textContent = formatTime(videoPlayer.duration);
|
| 2008 |
+
});
|
| 2009 |
+
|
| 2010 |
+
// Mute/Unmute
|
| 2011 |
+
muteBtn.addEventListener('click', () => {
|
| 2012 |
+
videoPlayer.muted = !videoPlayer.muted;
|
| 2013 |
+
muteBtn.innerHTML = videoPlayer.muted ?
|
| 2014 |
+
'<i class="fas fa-volume-mute"></i>' :
|
| 2015 |
+
'<i class="fas fa-volume-up"></i>';
|
| 2016 |
+
});
|
| 2017 |
+
|
| 2018 |
+
// Fullscreen
|
| 2019 |
+
fullscreenBtn.addEventListener('click', () => {
|
| 2020 |
+
if (videoContainer.requestFullscreen) {
|
| 2021 |
+
videoContainer.requestFullscreen();
|
| 2022 |
+
} else if (videoContainer.webkitRequestFullscreen) {
|
| 2023 |
+
videoContainer.webkitRequestFullscreen();
|
| 2024 |
+
} else if (videoContainer.msRequestFullscreen) {
|
| 2025 |
+
videoContainer.msRequestFullscreen();
|
| 2026 |
+
}
|
| 2027 |
+
});
|
| 2028 |
+
|
| 2029 |
+
// Update UI on play/pause
|
| 2030 |
+
videoPlayer.addEventListener('play', () => {
|
| 2031 |
+
videoContainer.classList.add('playing');
|
| 2032 |
+
});
|
| 2033 |
+
|
| 2034 |
+
videoPlayer.addEventListener('pause', () => {
|
| 2035 |
+
videoContainer.classList.remove('playing');
|
| 2036 |
+
});
|
| 2037 |
+
|
| 2038 |
+
// Format time to MM:SS
|
| 2039 |
+
function formatTime(seconds) {
|
| 2040 |
+
const minutes = Math.floor(seconds / 60);
|
| 2041 |
+
seconds = Math.floor(seconds % 60);
|
| 2042 |
+
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
| 2043 |
+
}
|
| 2044 |
+
}
|
| 2045 |
+
|
| 2046 |
+
// Upload Particles Animation
|
| 2047 |
+
function initUploadParticles() {
|
| 2048 |
+
const uploadContainer = document.querySelector('.upload-container');
|
| 2049 |
+
const particles = document.querySelector('.particles');
|
| 2050 |
+
|
| 2051 |
+
if (!uploadContainer || !particles) return;
|
| 2052 |
+
|
| 2053 |
+
// Create particles
|
| 2054 |
+
for (let i = 0; i < 15; i++) {
|
| 2055 |
+
const particle = document.createElement('div');
|
| 2056 |
+
particle.className = 'particle';
|
| 2057 |
+
|
| 2058 |
+
// Random position
|
| 2059 |
+
const posX = Math.random() * 100;
|
| 2060 |
+
particle.style.left = `${posX}%`;
|
| 2061 |
+
particle.style.bottom = '0';
|
| 2062 |
+
|
| 2063 |
+
// Random size
|
| 2064 |
+
const size = Math.random() * 4 + 2;
|
| 2065 |
+
particle.style.width = `${size}px`;
|
| 2066 |
+
particle.style.height = `${size}px`;
|
| 2067 |
+
|
| 2068 |
+
// Random animation delay and duration
|
| 2069 |
+
const delay = Math.random() * 2;
|
| 2070 |
+
const duration = Math.random() * 2 + 1;
|
| 2071 |
+
particle.style.animation = `particleAnimation ${duration}s ${delay}s infinite ease-out`;
|
| 2072 |
+
|
| 2073 |
+
// Add to container
|
| 2074 |
+
particles.appendChild(particle);
|
| 2075 |
+
}
|
| 2076 |
+
|
| 2077 |
+
// Show upload progress when uploading
|
| 2078 |
+
const fileInput = document.getElementById('fileInput');
|
| 2079 |
+
|
| 2080 |
+
uploadContainer.addEventListener('click', () => {
|
| 2081 |
+
fileInput.click();
|
| 2082 |
+
});
|
| 2083 |
+
|
| 2084 |
+
// Drag and drop functionality with improved feedback
|
| 2085 |
+
uploadContainer.addEventListener('dragover', (e) => {
|
| 2086 |
+
e.preventDefault();
|
| 2087 |
+
uploadContainer.classList.add('active');
|
| 2088 |
+
});
|
| 2089 |
+
|
| 2090 |
+
uploadContainer.addEventListener('dragleave', () => {
|
| 2091 |
+
uploadContainer.classList.remove('active');
|
| 2092 |
+
});
|
| 2093 |
+
|
| 2094 |
+
uploadContainer.addEventListener('drop', (e) => {
|
| 2095 |
+
e.preventDefault();
|
| 2096 |
+
uploadContainer.classList.remove('active');
|
| 2097 |
+
|
| 2098 |
+
if (e.dataTransfer.files.length) {
|
| 2099 |
+
fileInput.files = e.dataTransfer.files;
|
| 2100 |
+
fileInput.dispatchEvent(new Event('change'));
|
| 2101 |
+
}
|
| 2102 |
+
});
|
| 2103 |
+
}
|
| 2104 |
+
|
| 2105 |
+
// Progress Steps Visualization
|
| 2106 |
+
function initProgressSteps() {
|
| 2107 |
+
const uploadStep = document.getElementById('uploadStep');
|
| 2108 |
+
const transcribeStep = document.getElementById('transcribeStep');
|
| 2109 |
+
const analyzeStep = document.getElementById('analyzeStep');
|
| 2110 |
+
const visualizeStep = document.getElementById('visualizeStep');
|
| 2111 |
+
const progressBarFill = document.querySelector('.progress-bar-fill');
|
| 2112 |
+
|
| 2113 |
+
if (!uploadStep || !progressBarFill) return;
|
| 2114 |
+
|
| 2115 |
+
// Set initial state
|
| 2116 |
+
setActiveStep('upload');
|
| 2117 |
+
|
| 2118 |
+
// Update steps based on current operation
|
| 2119 |
+
fileInput.addEventListener('change', () => {
|
| 2120 |
+
setActiveStep('upload');
|
| 2121 |
+
});
|
| 2122 |
+
|
| 2123 |
+
transcribeButton.addEventListener('click', () => {
|
| 2124 |
+
setTimeout(() => setActiveStep('transcribe'), 100);
|
| 2125 |
+
});
|
| 2126 |
+
|
| 2127 |
+
analyzeButton.addEventListener('click', () => {
|
| 2128 |
+
setTimeout(() => setActiveStep('analyze'), 100);
|
| 2129 |
+
});
|
| 2130 |
+
|
| 2131 |
+
// Redefine the global setActiveStep function with the proper implementation
|
| 2132 |
+
window.setActiveStep = function(step) {
|
| 2133 |
+
// Skip if elements aren't available yet
|
| 2134 |
+
if (!uploadStep || !progressBarFill) return;
|
| 2135 |
+
|
| 2136 |
+
// Reset all steps
|
| 2137 |
+
uploadStep.className = 'progress-step';
|
| 2138 |
+
transcribeStep.className = 'progress-step';
|
| 2139 |
+
analyzeStep.className = 'progress-step';
|
| 2140 |
+
visualizeStep.className = 'progress-step';
|
| 2141 |
+
|
| 2142 |
+
// Set progress level
|
| 2143 |
+
let progressWidth = '0%';
|
| 2144 |
+
|
| 2145 |
+
switch(step) {
|
| 2146 |
+
case 'upload':
|
| 2147 |
+
uploadStep.className = 'progress-step active';
|
| 2148 |
+
progressWidth = '25%';
|
| 2149 |
+
break;
|
| 2150 |
+
case 'transcribe':
|
| 2151 |
+
uploadStep.className = 'progress-step completed';
|
| 2152 |
+
transcribeStep.className = 'progress-step active';
|
| 2153 |
+
progressWidth = '50%';
|
| 2154 |
+
break;
|
| 2155 |
+
case 'analyze':
|
| 2156 |
+
uploadStep.className = 'progress-step completed';
|
| 2157 |
+
transcribeStep.className = 'progress-step completed';
|
| 2158 |
+
analyzeStep.className = 'progress-step active';
|
| 2159 |
+
progressWidth = '75%';
|
| 2160 |
+
break;
|
| 2161 |
+
case 'visualize':
|
| 2162 |
+
uploadStep.className = 'progress-step completed';
|
| 2163 |
+
transcribeStep.className = 'progress-step completed';
|
| 2164 |
+
analyzeStep.className = 'progress-step completed';
|
| 2165 |
+
visualizeStep.className = 'progress-step active';
|
| 2166 |
+
progressWidth = '100%';
|
| 2167 |
+
break;
|
| 2168 |
+
}
|
| 2169 |
+
|
| 2170 |
+
// Animate progress bar
|
| 2171 |
+
progressBarFill.style.width = progressWidth;
|
| 2172 |
+
};
|
| 2173 |
+
}
|
static/styles.css
ADDED
|
@@ -0,0 +1,1889 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Root Variables */
|
| 2 |
+
:root {
|
| 3 |
+
/* Light Mode Colors */
|
| 4 |
+
--primary-color: #3498db;
|
| 5 |
+
--primary-dark: #2980b9;
|
| 6 |
+
--secondary-color: #2ecc71;
|
| 7 |
+
--secondary-dark: #27ae60;
|
| 8 |
+
--accent-color: #9b59b6;
|
| 9 |
+
--accent-dark: #8e44ad;
|
| 10 |
+
--background-color: #f8f9fa;
|
| 11 |
+
--surface-color: #ffffff;
|
| 12 |
+
--text-primary: #2c3e50;
|
| 13 |
+
--text-secondary: #7f8c8d;
|
| 14 |
+
--border-color: #e0e0e0;
|
| 15 |
+
--shadow-sm: 0 2px 4px rgba(0,0,0,0.05);
|
| 16 |
+
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
|
| 17 |
+
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
|
| 18 |
+
--radius-sm: 4px;
|
| 19 |
+
--radius-md: 8px;
|
| 20 |
+
--radius-lg: 12px;
|
| 21 |
+
|
| 22 |
+
/* Enhanced Typography */
|
| 23 |
+
--font-heading: 'Montserrat', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
| 24 |
+
--font-body: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
| 25 |
+
--font-mono: 'JetBrains Mono', 'Courier New', monospace;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* Dark Mode Colors */
|
| 29 |
+
[data-theme="dark"] {
|
| 30 |
+
--primary-color: #60a5fa;
|
| 31 |
+
--primary-dark: #3b82f6;
|
| 32 |
+
--secondary-color: #10b981;
|
| 33 |
+
--secondary-dark: #059669;
|
| 34 |
+
--accent-color: #c084fc;
|
| 35 |
+
--accent-dark: #a855f7;
|
| 36 |
+
--background-color: #111827;
|
| 37 |
+
--surface-color: #1f2937;
|
| 38 |
+
--text-primary: #f9fafb;
|
| 39 |
+
--text-secondary: #e5e7eb;
|
| 40 |
+
--border-color: #374151;
|
| 41 |
+
--shadow-sm: 0 2px 4px rgba(0,0,0,0.3);
|
| 42 |
+
--shadow-md: 0 4px 6px rgba(0,0,0,0.4);
|
| 43 |
+
--shadow-lg: 0 10px 15px rgba(0,0,0,0.5);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* Reset & Base Styles */
|
| 47 |
+
* {
|
| 48 |
+
margin: 0;
|
| 49 |
+
padding: 0;
|
| 50 |
+
box-sizing: border-box;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* Font imports */
|
| 54 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Montserrat:wght@500;600;700&display=swap');
|
| 55 |
+
|
| 56 |
+
body {
|
| 57 |
+
font-family: var(--font-body);
|
| 58 |
+
background-color: var(--background-color);
|
| 59 |
+
color: var(--text-primary);
|
| 60 |
+
line-height: 1.6;
|
| 61 |
+
transition: background-color 0.3s ease, color 0.3s ease;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
h1, h2, h3, h4, h5, h6 {
|
| 65 |
+
font-family: var(--font-heading);
|
| 66 |
+
font-weight: 600;
|
| 67 |
+
line-height: 1.3;
|
| 68 |
+
margin-bottom: 0.5em;
|
| 69 |
+
color: var(--text-primary);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
code, pre {
|
| 73 |
+
font-family: var(--font-mono);
|
| 74 |
+
font-size: 0.9em;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Theme Toggle */
|
| 78 |
+
.theme-toggle {
|
| 79 |
+
position: fixed;
|
| 80 |
+
top: 1rem;
|
| 81 |
+
right: 1rem;
|
| 82 |
+
z-index: 1001;
|
| 83 |
+
display: flex;
|
| 84 |
+
align-items: center;
|
| 85 |
+
gap: 0.5rem;
|
| 86 |
+
background-color: var(--surface-color);
|
| 87 |
+
border-radius: 2rem;
|
| 88 |
+
padding: 0.35rem;
|
| 89 |
+
box-shadow: var(--shadow-md);
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
border: 1px solid var(--border-color);
|
| 92 |
+
transition: all 0.2s ease;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.theme-toggle:hover {
|
| 96 |
+
transform: translateY(-2px);
|
| 97 |
+
box-shadow: var(--shadow-lg);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.theme-toggle-icon {
|
| 101 |
+
display: flex;
|
| 102 |
+
align-items: center;
|
| 103 |
+
justify-content: center;
|
| 104 |
+
width: 2rem;
|
| 105 |
+
height: 2rem;
|
| 106 |
+
font-size: 1.2rem;
|
| 107 |
+
border-radius: 50%;
|
| 108 |
+
color: var(--text-primary);
|
| 109 |
+
transition: all 0.3s ease;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.theme-toggle-icon.light-icon {
|
| 113 |
+
background-color: var(--primary-color);
|
| 114 |
+
color: white;
|
| 115 |
+
transform: var(--theme-light-transform, scale(1));
|
| 116 |
+
opacity: var(--theme-light-opacity, 1);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.theme-toggle-icon.dark-icon {
|
| 120 |
+
background-color: transparent;
|
| 121 |
+
transform: var(--theme-dark-transform, scale(0.8));
|
| 122 |
+
opacity: var(--theme-dark-opacity, 0.5);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
[data-theme="dark"] .theme-toggle-icon.light-icon {
|
| 126 |
+
background-color: transparent;
|
| 127 |
+
transform: scale(0.8);
|
| 128 |
+
opacity: 0.5;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
[data-theme="dark"] .theme-toggle-icon.dark-icon {
|
| 132 |
+
background-color: var(--accent-color);
|
| 133 |
+
color: white;
|
| 134 |
+
transform: scale(1);
|
| 135 |
+
opacity: 1;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/* Layout */
|
| 139 |
+
.app-container {
|
| 140 |
+
min-height: 100vh;
|
| 141 |
+
display: flex;
|
| 142 |
+
flex-direction: column;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.app-header {
|
| 146 |
+
background-color: var(--surface-color);
|
| 147 |
+
padding: 1rem 2rem;
|
| 148 |
+
box-shadow: var(--shadow-sm);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.logo-container {
|
| 152 |
+
display: flex;
|
| 153 |
+
align-items: center;
|
| 154 |
+
gap: 1rem;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.logo-icon {
|
| 158 |
+
font-size: 2rem;
|
| 159 |
+
color: var(--primary-color);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.main-content {
|
| 163 |
+
flex: 1;
|
| 164 |
+
max-width: 1400px;
|
| 165 |
+
margin: 2rem auto;
|
| 166 |
+
padding: 0 2rem;
|
| 167 |
+
display: grid;
|
| 168 |
+
gap: 2rem;
|
| 169 |
+
grid-template-rows: auto minmax(0, 1fr);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/* Upload Panel */
|
| 173 |
+
.upload-panel {
|
| 174 |
+
background-color: var(--surface-color);
|
| 175 |
+
border-radius: var(--radius-lg);
|
| 176 |
+
padding: 2rem;
|
| 177 |
+
box-shadow: var(--shadow-md);
|
| 178 |
+
max-height: fit-content;
|
| 179 |
+
overflow: hidden;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.upload-section {
|
| 183 |
+
display: grid;
|
| 184 |
+
grid-template-columns: 2fr 1fr;
|
| 185 |
+
gap: 2rem;
|
| 186 |
+
margin-bottom: 2rem;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.upload-container {
|
| 190 |
+
border: 2px dashed rgba(52, 152, 219, 0.3);
|
| 191 |
+
border-radius: var(--radius-lg);
|
| 192 |
+
padding: 2.5rem;
|
| 193 |
+
text-align: center;
|
| 194 |
+
transition: all 0.3s ease;
|
| 195 |
+
cursor: pointer;
|
| 196 |
+
background: linear-gradient(135deg, rgba(52, 152, 219, 0.02) 0%, rgba(52, 152, 219, 0.08) 100%);
|
| 197 |
+
position: relative;
|
| 198 |
+
overflow: hidden;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.upload-container::before {
|
| 202 |
+
content: "";
|
| 203 |
+
position: absolute;
|
| 204 |
+
top: 0;
|
| 205 |
+
left: 0;
|
| 206 |
+
right: 0;
|
| 207 |
+
bottom: 0;
|
| 208 |
+
background: radial-gradient(circle at center, rgba(52, 152, 219, 0.1) 0%, transparent 70%);
|
| 209 |
+
opacity: 0;
|
| 210 |
+
transition: opacity 0.3s ease;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.upload-container:hover {
|
| 214 |
+
border-color: var(--primary-color);
|
| 215 |
+
transform: translateY(-5px);
|
| 216 |
+
box-shadow: 0 15px 30px -10px rgba(52, 152, 219, 0.15);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.upload-container:hover::before {
|
| 220 |
+
opacity: 1;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.upload-icon-container {
|
| 224 |
+
width: 80px;
|
| 225 |
+
height: 80px;
|
| 226 |
+
border-radius: 50%;
|
| 227 |
+
background-color: rgba(52, 152, 219, 0.1);
|
| 228 |
+
margin: 0 auto 1.5rem;
|
| 229 |
+
display: flex;
|
| 230 |
+
align-items: center;
|
| 231 |
+
justify-content: center;
|
| 232 |
+
transition: all 0.3s ease;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.upload-container:hover .upload-icon-container {
|
| 236 |
+
background-color: rgba(52, 152, 219, 0.2);
|
| 237 |
+
transform: scale(1.1);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.upload-icon {
|
| 241 |
+
font-size: 2.5rem;
|
| 242 |
+
color: var(--primary-color);
|
| 243 |
+
transition: all 0.3s ease;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.upload-container:hover .upload-icon {
|
| 247 |
+
transform: translateY(-5px);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.upload-text {
|
| 251 |
+
font-family: var(--font-heading);
|
| 252 |
+
font-size: 1.25rem;
|
| 253 |
+
font-weight: 600;
|
| 254 |
+
color: var(--text-primary);
|
| 255 |
+
margin-bottom: 0.5rem;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.upload-subtext {
|
| 259 |
+
font-size: 0.95rem;
|
| 260 |
+
color: var(--text-secondary);
|
| 261 |
+
max-width: 80%;
|
| 262 |
+
margin: 0 auto;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
/* Upload file particles */
|
| 266 |
+
.particles {
|
| 267 |
+
position: absolute;
|
| 268 |
+
top: 0;
|
| 269 |
+
left: 0;
|
| 270 |
+
width: 100%;
|
| 271 |
+
height: 100%;
|
| 272 |
+
pointer-events: none;
|
| 273 |
+
opacity: 0;
|
| 274 |
+
transition: opacity 0.3s ease;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.particle {
|
| 278 |
+
position: absolute;
|
| 279 |
+
width: 6px;
|
| 280 |
+
height: 6px;
|
| 281 |
+
border-radius: 50%;
|
| 282 |
+
background-color: var(--primary-color);
|
| 283 |
+
opacity: 0;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.upload-container:hover .particles {
|
| 287 |
+
opacity: 1;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
@keyframes particleAnimation {
|
| 291 |
+
0% { transform: translateY(0) scale(0); opacity: 0; }
|
| 292 |
+
50% { opacity: 0.8; }
|
| 293 |
+
100% { transform: translateY(-100px) scale(1); opacity: 0; }
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/* Upload Progress Animations */
|
| 297 |
+
.upload-progress {
|
| 298 |
+
width: 100%;
|
| 299 |
+
height: 4px;
|
| 300 |
+
margin-top: 1.5rem;
|
| 301 |
+
background-color: rgba(52, 152, 219, 0.1);
|
| 302 |
+
border-radius: 2px;
|
| 303 |
+
overflow: hidden;
|
| 304 |
+
opacity: 0;
|
| 305 |
+
transition: opacity 0.3s ease;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.upload-progress-bar {
|
| 309 |
+
height: 100%;
|
| 310 |
+
width: 0;
|
| 311 |
+
background: linear-gradient(90deg, var(--primary-color), var(--primary-dark));
|
| 312 |
+
border-radius: 2px;
|
| 313 |
+
transition: width 0.3s ease;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.upload-container.uploading .upload-progress {
|
| 317 |
+
opacity: 1;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/* Dark Mode Adjustments */
|
| 321 |
+
[data-theme="dark"] .upload-container {
|
| 322 |
+
border-color: rgba(96, 165, 250, 0.3);
|
| 323 |
+
background: linear-gradient(135deg, rgba(30, 41, 59, 0.5) 0%, rgba(30, 41, 59, 0.8) 100%);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
[data-theme="dark"] .upload-container::before {
|
| 327 |
+
background: radial-gradient(circle at center, rgba(96, 165, 250, 0.15) 0%, transparent 70%);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
[data-theme="dark"] .upload-icon-container {
|
| 331 |
+
background-color: rgba(96, 165, 250, 0.15);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/* Model Selector */
|
| 335 |
+
.model-selector {
|
| 336 |
+
display: flex;
|
| 337 |
+
flex-direction: column;
|
| 338 |
+
gap: 0.5rem;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.model-label {
|
| 342 |
+
font-weight: 500;
|
| 343 |
+
color: var(--text-primary);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.select-wrapper {
|
| 347 |
+
position: relative;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.select-wrapper select {
|
| 351 |
+
width: 100%;
|
| 352 |
+
padding: 0.75rem;
|
| 353 |
+
border: 1px solid var(--border-color);
|
| 354 |
+
border-radius: var(--radius-sm);
|
| 355 |
+
appearance: none;
|
| 356 |
+
background-color: var(--surface-color);
|
| 357 |
+
font-size: 1rem;
|
| 358 |
+
color: var(--text-primary);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.select-icon {
|
| 362 |
+
position: absolute;
|
| 363 |
+
right: 1rem;
|
| 364 |
+
top: 50%;
|
| 365 |
+
transform: translateY(-50%);
|
| 366 |
+
color: var(--text-secondary);
|
| 367 |
+
pointer-events: none;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.model-status {
|
| 371 |
+
font-size: 0.9rem;
|
| 372 |
+
color: var(--text-secondary);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
/* Video Preview */
|
| 376 |
+
.video-preview {
|
| 377 |
+
margin: 2rem 0;
|
| 378 |
+
border-radius: var(--radius-md);
|
| 379 |
+
overflow: hidden;
|
| 380 |
+
background-color: #000;
|
| 381 |
+
position: relative;
|
| 382 |
+
box-shadow: 0 15px 25px -5px rgba(0, 0, 0, 0.15),
|
| 383 |
+
0 10px 10px -5px rgba(0, 0, 0, 0.05);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.video-preview video {
|
| 387 |
+
width: 100%;
|
| 388 |
+
object-fit: contain;
|
| 389 |
+
display: block;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.video-preview::after {
|
| 393 |
+
content: '';
|
| 394 |
+
position: absolute;
|
| 395 |
+
bottom: 0;
|
| 396 |
+
left: 0;
|
| 397 |
+
right: 0;
|
| 398 |
+
height: 90px;
|
| 399 |
+
background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%);
|
| 400 |
+
pointer-events: none;
|
| 401 |
+
opacity: 0;
|
| 402 |
+
transition: opacity 0.3s ease;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.video-preview:hover::after {
|
| 406 |
+
opacity: 1;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.video-controls {
|
| 410 |
+
position: absolute;
|
| 411 |
+
bottom: 0;
|
| 412 |
+
left: 0;
|
| 413 |
+
width: 100%;
|
| 414 |
+
padding: 1rem;
|
| 415 |
+
display: flex;
|
| 416 |
+
align-items: center;
|
| 417 |
+
gap: 1rem;
|
| 418 |
+
z-index: 10;
|
| 419 |
+
opacity: 0;
|
| 420 |
+
transform: translateY(10px);
|
| 421 |
+
transition: all 0.3s ease;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.video-preview:hover .video-controls {
|
| 425 |
+
opacity: 1;
|
| 426 |
+
transform: translateY(0);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.video-progress {
|
| 430 |
+
flex: 1;
|
| 431 |
+
height: 4px;
|
| 432 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 433 |
+
border-radius: 2px;
|
| 434 |
+
overflow: hidden;
|
| 435 |
+
cursor: pointer;
|
| 436 |
+
position: relative;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.video-progress-bar {
|
| 440 |
+
height: 100%;
|
| 441 |
+
width: 0;
|
| 442 |
+
background-color: var(--primary-color);
|
| 443 |
+
border-radius: 2px;
|
| 444 |
+
position: relative;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.video-progress-handle {
|
| 448 |
+
position: absolute;
|
| 449 |
+
right: -6px;
|
| 450 |
+
top: 50%;
|
| 451 |
+
transform: translateY(-50%);
|
| 452 |
+
width: 12px;
|
| 453 |
+
height: 12px;
|
| 454 |
+
border-radius: 50%;
|
| 455 |
+
background-color: white;
|
| 456 |
+
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
|
| 457 |
+
opacity: 0;
|
| 458 |
+
transition: opacity 0.2s ease;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.video-progress:hover .video-progress-handle {
|
| 462 |
+
opacity: 1;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.video-control-button {
|
| 466 |
+
background: transparent;
|
| 467 |
+
border: none;
|
| 468 |
+
color: white;
|
| 469 |
+
cursor: pointer;
|
| 470 |
+
width: 36px;
|
| 471 |
+
height: 36px;
|
| 472 |
+
display: flex;
|
| 473 |
+
align-items: center;
|
| 474 |
+
justify-content: center;
|
| 475 |
+
border-radius: 50%;
|
| 476 |
+
transition: all 0.2s ease;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
.video-control-button:hover {
|
| 480 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.video-time {
|
| 484 |
+
color: white;
|
| 485 |
+
font-family: var(--font-mono);
|
| 486 |
+
font-size: 0.85rem;
|
| 487 |
+
min-width: 50px;
|
| 488 |
+
text-align: center;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.video-preview.playing .video-play-icon {
|
| 492 |
+
display: none;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.video-preview:not(.playing) .video-pause-icon {
|
| 496 |
+
display: none;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
/* Action Buttons */
|
| 500 |
+
.action-buttons {
|
| 501 |
+
display: flex;
|
| 502 |
+
gap: 1rem;
|
| 503 |
+
margin-bottom: 1.5rem;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.primary-button, .secondary-button {
|
| 507 |
+
padding: 0.75rem 1.5rem;
|
| 508 |
+
border: none;
|
| 509 |
+
border-radius: var(--radius-sm);
|
| 510 |
+
font-size: 1rem;
|
| 511 |
+
font-weight: 500;
|
| 512 |
+
cursor: pointer;
|
| 513 |
+
display: flex;
|
| 514 |
+
align-items: center;
|
| 515 |
+
gap: 0.5rem;
|
| 516 |
+
transition: all 0.2s ease;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.primary-button {
|
| 520 |
+
background-color: var(--primary-color);
|
| 521 |
+
color: white;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.primary-button:hover {
|
| 525 |
+
background-color: var(--primary-dark);
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.secondary-button {
|
| 529 |
+
background-color: var(--secondary-color);
|
| 530 |
+
color: white;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.secondary-button:hover {
|
| 534 |
+
background-color: var(--secondary-dark);
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.primary-button:disabled, .secondary-button:disabled {
|
| 538 |
+
background-color: var(--text-secondary);
|
| 539 |
+
cursor: not-allowed;
|
| 540 |
+
opacity: 0.7;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
/* Progress Bar */
|
| 544 |
+
.progress-container {
|
| 545 |
+
background-color: var(--background-color);
|
| 546 |
+
padding: 1.5rem;
|
| 547 |
+
border-radius: var(--radius-md);
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.status-text {
|
| 551 |
+
margin-bottom: 0.75rem;
|
| 552 |
+
color: var(--text-secondary);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.progress-bar {
|
| 556 |
+
height: 0.5rem;
|
| 557 |
+
background-color: var(--border-color);
|
| 558 |
+
border-radius: 999px;
|
| 559 |
+
overflow: hidden;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
.progress {
|
| 563 |
+
height: 100%;
|
| 564 |
+
background-color: var(--primary-color);
|
| 565 |
+
transition: width 0.3s ease;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
/* Results Panel */
|
| 569 |
+
.results-panel {
|
| 570 |
+
display: grid;
|
| 571 |
+
grid-template-columns: 1fr 1fr;
|
| 572 |
+
gap: 2rem;
|
| 573 |
+
max-height: calc(100vh - 600px);
|
| 574 |
+
min-height: 500px;
|
| 575 |
+
overflow: hidden;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.transcription-container, .topics-container {
|
| 579 |
+
background-color: var(--surface-color);
|
| 580 |
+
border-radius: var(--radius-lg);
|
| 581 |
+
padding: 1.5rem;
|
| 582 |
+
box-shadow: var(--shadow-md);
|
| 583 |
+
display: flex;
|
| 584 |
+
flex-direction: column;
|
| 585 |
+
max-height: 100%;
|
| 586 |
+
overflow: hidden;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
h2 {
|
| 590 |
+
display: flex;
|
| 591 |
+
align-items: center;
|
| 592 |
+
gap: 0.5rem;
|
| 593 |
+
margin-bottom: 1rem;
|
| 594 |
+
padding-bottom: 0.75rem;
|
| 595 |
+
border-bottom: 2px solid var(--border-color);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.transcription-content, .topics-content {
|
| 599 |
+
max-height: 500px;
|
| 600 |
+
overflow-y: auto;
|
| 601 |
+
padding: 1rem;
|
| 602 |
+
background-color: var(--background-color);
|
| 603 |
+
border-radius: var(--radius-md);
|
| 604 |
+
font-size: 0.95rem;
|
| 605 |
+
line-height: 1.7;
|
| 606 |
+
height: 500px;
|
| 607 |
+
overflow-y: auto;
|
| 608 |
+
overflow-x: hidden;
|
| 609 |
+
word-wrap: break-word;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
/* Topic Styles */
|
| 613 |
+
.topic {
|
| 614 |
+
margin-bottom: 1.5rem;
|
| 615 |
+
padding: 1rem;
|
| 616 |
+
background-color: var(--surface-color);
|
| 617 |
+
border-left: 4px solid var(--primary-color);
|
| 618 |
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
| 619 |
+
box-shadow: var(--shadow-sm);
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
.topic-header {
|
| 623 |
+
font-weight: 600;
|
| 624 |
+
color: var(--text-primary);
|
| 625 |
+
margin-bottom: 0.5rem;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
.topic-content {
|
| 629 |
+
color: var(--text-secondary);
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
/* Mindmap Overlay */
|
| 633 |
+
.mindmap-overlay {
|
| 634 |
+
position: fixed;
|
| 635 |
+
top: 0;
|
| 636 |
+
left: 0;
|
| 637 |
+
width: 100vw;
|
| 638 |
+
height: 100vh;
|
| 639 |
+
background-color: var(--background-color);
|
| 640 |
+
z-index: 1000;
|
| 641 |
+
display: none;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
.mindmap-overlay.active {
|
| 645 |
+
display: block;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mindmap-content {
|
| 649 |
+
display: flex;
|
| 650 |
+
height: 100vh;
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
/* Video Section */
|
| 654 |
+
.video-section {
|
| 655 |
+
width: 40%;
|
| 656 |
+
padding: 2rem;
|
| 657 |
+
background-color: var(--surface-color);
|
| 658 |
+
border-right: 1px solid var(--border-color);
|
| 659 |
+
display: none;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
.video-section.active {
|
| 663 |
+
display: block;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
.video-player-container {
|
| 667 |
+
border-radius: var(--radius-md);
|
| 668 |
+
overflow: hidden;
|
| 669 |
+
background-color: #000;
|
| 670 |
+
margin-bottom: 1rem;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.video-player-container video {
|
| 674 |
+
width: 100%;
|
| 675 |
+
object-fit: contain;
|
| 676 |
+
display: block;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.video-info {
|
| 680 |
+
padding: 1rem;
|
| 681 |
+
background-color: var(--background-color);
|
| 682 |
+
border-radius: var(--radius-md);
|
| 683 |
+
font-size: 0.9rem;
|
| 684 |
+
color: var(--text-secondary);
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
/* Mindmap Section */
|
| 688 |
+
.mindmap-section {
|
| 689 |
+
flex: 1;
|
| 690 |
+
position: relative;
|
| 691 |
+
overflow: hidden;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.mindmap-toolbar {
|
| 695 |
+
position: absolute;
|
| 696 |
+
top: 1rem;
|
| 697 |
+
left: 1rem;
|
| 698 |
+
z-index: 100;
|
| 699 |
+
display: flex;
|
| 700 |
+
gap: 0.5rem;
|
| 701 |
+
background-color: var(--surface-color);
|
| 702 |
+
padding: 0.5rem;
|
| 703 |
+
border-radius: var(--radius-md);
|
| 704 |
+
box-shadow: var(--shadow-md);
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.tool-button {
|
| 708 |
+
padding: 0.5rem 1rem;
|
| 709 |
+
border: none;
|
| 710 |
+
border-radius: var(--radius-sm);
|
| 711 |
+
background-color: var(--surface-color);
|
| 712 |
+
color: var(--text-primary);
|
| 713 |
+
cursor: pointer;
|
| 714 |
+
display: flex;
|
| 715 |
+
align-items: center;
|
| 716 |
+
gap: 0.5rem;
|
| 717 |
+
transition: all 0.2s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.tool-button:hover {
|
| 721 |
+
background-color: var(--background-color);
|
| 722 |
+
color: var(--primary-color);
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.tool-button.active {
|
| 726 |
+
background-color: var(--primary-color);
|
| 727 |
+
color: white;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.fullscreen-mindmap {
|
| 731 |
+
width: 100%;
|
| 732 |
+
height: calc(100vh - 4rem);
|
| 733 |
+
position: relative;
|
| 734 |
+
overflow: hidden;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
/* Responsive Design */
|
| 738 |
+
@media (max-width: 1200px) {
|
| 739 |
+
.main-content {
|
| 740 |
+
padding: 0 1rem;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
.results-panel {
|
| 744 |
+
grid-template-columns: 1fr;
|
| 745 |
+
}
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
@media (max-width: 768px) {
|
| 749 |
+
.upload-section {
|
| 750 |
+
grid-template-columns: 1fr;
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
.mindmap-content {
|
| 754 |
+
flex-direction: column;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.video-section {
|
| 758 |
+
width: 100%;
|
| 759 |
+
border-right: none;
|
| 760 |
+
border-bottom: 1px solid var(--border-color);
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
.tool-button {
|
| 764 |
+
padding: 0.5rem;
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
.tool-button span {
|
| 768 |
+
display: none;
|
| 769 |
+
}
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
@media (max-width: 480px) {
|
| 773 |
+
.action-buttons {
|
| 774 |
+
flex-direction: column;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
.primary-button, .secondary-button {
|
| 778 |
+
width: 100%;
|
| 779 |
+
justify-content: center;
|
| 780 |
+
}
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
/* Custom Mindmap Node Styles */
|
| 784 |
+
jmnode {
|
| 785 |
+
box-shadow: var(--shadow-sm);
|
| 786 |
+
border-radius: var(--radius-md);
|
| 787 |
+
padding: 8px 12px !important;
|
| 788 |
+
transition: all 0.2s ease;
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
jmnode:hover {
|
| 792 |
+
box-shadow: var(--shadow-md);
|
| 793 |
+
transform: translateY(-1px);
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
/* Improved Expand/Collapse Buttons */
|
| 797 |
+
jmexpander {
|
| 798 |
+
width: 20px !important;
|
| 799 |
+
height: 20px !important;
|
| 800 |
+
border-radius: 50% !important;
|
| 801 |
+
background-color: var(--surface-color) !important;
|
| 802 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
| 803 |
+
display: flex !important;
|
| 804 |
+
align-items: center;
|
| 805 |
+
justify-content: center;
|
| 806 |
+
font-size: 16px !important;
|
| 807 |
+
font-weight: bold;
|
| 808 |
+
color: var(--primary-color) !important;
|
| 809 |
+
transition: all 0.2s ease;
|
| 810 |
+
text-indent: -9999px !important;
|
| 811 |
+
overflow: hidden !important;
|
| 812 |
+
font-size: 0 !important;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
jmexpander:hover {
|
| 816 |
+
background-color: var(--primary-color) !important;
|
| 817 |
+
color: white !important;
|
| 818 |
+
transform: scale(1.1);
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
/* Custom icons for expander instead of +/- */
|
| 822 |
+
jmexpander.expand {
|
| 823 |
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%233498db" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>') !important;
|
| 824 |
+
background-position: center !important;
|
| 825 |
+
background-repeat: no-repeat !important;
|
| 826 |
+
background-size: 12px !important;
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
jmexpander.collapse {
|
| 830 |
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%233498db" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13H5v-2h14v2z"/></svg>') !important;
|
| 831 |
+
background-position: center !important;
|
| 832 |
+
background-repeat: no-repeat !important;
|
| 833 |
+
background-size: 12px !important;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
jmexpander.expand:hover, jmexpander.collapse:hover {
|
| 837 |
+
background-color: var(--primary-color) !important;
|
| 838 |
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>') !important;
|
| 839 |
+
background-size: 12px !important;
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
jmexpander.collapse:hover {
|
| 843 |
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13H5v-2h14v2z"/></svg>') !important;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
/* Video Close Button */
|
| 847 |
+
.video-close-button {
|
| 848 |
+
position: absolute;
|
| 849 |
+
top: 1rem;
|
| 850 |
+
right: 1rem;
|
| 851 |
+
width: 36px;
|
| 852 |
+
height: 36px;
|
| 853 |
+
border-radius: 50%;
|
| 854 |
+
background-color: var(--surface-color);
|
| 855 |
+
color: var(--text-primary);
|
| 856 |
+
border: none;
|
| 857 |
+
display: flex;
|
| 858 |
+
align-items: center;
|
| 859 |
+
justify-content: center;
|
| 860 |
+
cursor: pointer;
|
| 861 |
+
box-shadow: var(--shadow-sm);
|
| 862 |
+
transition: all 0.2s ease;
|
| 863 |
+
z-index: 10;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
.video-close-button:hover {
|
| 867 |
+
background-color: #f8f9fa;
|
| 868 |
+
color: var(--primary-color);
|
| 869 |
+
box-shadow: var(--shadow-md);
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
/* Line style improvements */
|
| 873 |
+
jmnode.root {
|
| 874 |
+
background-color: var(--primary-color) !important;
|
| 875 |
+
color: white !important;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
jmnode.root jmexpander {
|
| 879 |
+
background-color: white !important;
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
/* Connection lines styling */
|
| 883 |
+
jmexpander::before {
|
| 884 |
+
border-color: var(--primary-color) !important;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
/* Node Context Menu */
|
| 888 |
+
.node-context-menu {
|
| 889 |
+
position: absolute;
|
| 890 |
+
background-color: var(--surface-color);
|
| 891 |
+
border-radius: var(--radius-md);
|
| 892 |
+
box-shadow: var(--shadow-lg);
|
| 893 |
+
padding: 0.5rem 0;
|
| 894 |
+
z-index: 2000;
|
| 895 |
+
min-width: 180px;
|
| 896 |
+
font-size: 0.9rem;
|
| 897 |
+
opacity: 0;
|
| 898 |
+
transform: scale(0.95);
|
| 899 |
+
transform-origin: top left;
|
| 900 |
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
| 901 |
+
pointer-events: none;
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
.node-context-menu.visible {
|
| 905 |
+
opacity: 1;
|
| 906 |
+
transform: scale(1);
|
| 907 |
+
pointer-events: auto;
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
.context-menu-item {
|
| 911 |
+
padding: 0.6rem 1rem;
|
| 912 |
+
display: flex;
|
| 913 |
+
align-items: center;
|
| 914 |
+
gap: 0.75rem;
|
| 915 |
+
cursor: pointer;
|
| 916 |
+
transition: all 0.15s ease;
|
| 917 |
+
color: var(--text-primary);
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
.context-menu-item:hover {
|
| 921 |
+
background-color: rgba(52, 152, 219, 0.1);
|
| 922 |
+
color: var(--primary-color);
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
.context-menu-item i {
|
| 926 |
+
width: 16px;
|
| 927 |
+
text-align: center;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
.context-menu-separator {
|
| 931 |
+
height: 1px;
|
| 932 |
+
background-color: var(--border-color);
|
| 933 |
+
margin: 0.5rem 0;
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
/* Node color picker */
|
| 937 |
+
.color-picker-container {
|
| 938 |
+
padding: 0.5rem 1rem;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
.color-picker-label {
|
| 942 |
+
display: block;
|
| 943 |
+
margin-bottom: 0.5rem;
|
| 944 |
+
font-size: 0.9rem;
|
| 945 |
+
color: var(--text-secondary);
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
.color-options {
|
| 949 |
+
display: flex;
|
| 950 |
+
flex-wrap: wrap;
|
| 951 |
+
gap: 0.5rem;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.color-option {
|
| 955 |
+
width: 24px;
|
| 956 |
+
height: 24px;
|
| 957 |
+
border-radius: 50%;
|
| 958 |
+
cursor: pointer;
|
| 959 |
+
position: relative;
|
| 960 |
+
transition: transform 0.2s ease;
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
.color-option:hover {
|
| 964 |
+
transform: scale(1.15);
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
.color-option.selected::after {
|
| 968 |
+
content: '';
|
| 969 |
+
position: absolute;
|
| 970 |
+
top: 50%;
|
| 971 |
+
left: 50%;
|
| 972 |
+
transform: translate(-50%, -50%);
|
| 973 |
+
width: 10px;
|
| 974 |
+
height: 10px;
|
| 975 |
+
border-radius: 50%;
|
| 976 |
+
background-color: white;
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
/* Node edit input */
|
| 980 |
+
.node-edit-input {
|
| 981 |
+
position: absolute;
|
| 982 |
+
z-index: 2000;
|
| 983 |
+
padding: 8px 12px;
|
| 984 |
+
border: 2px solid var(--primary-color);
|
| 985 |
+
border-radius: var(--radius-md);
|
| 986 |
+
box-shadow: var(--shadow-md);
|
| 987 |
+
font-family: inherit;
|
| 988 |
+
font-size: 14px;
|
| 989 |
+
background-color: var(--surface-color);
|
| 990 |
+
width: auto;
|
| 991 |
+
min-width: 150px;
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
/* Custom node colors */
|
| 995 |
+
jmnode.node-blue {
|
| 996 |
+
background-color: #3498db !important;
|
| 997 |
+
color: white !important;
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
jmnode.node-green {
|
| 1001 |
+
background-color: #2ecc71 !important;
|
| 1002 |
+
color: white !important;
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
jmnode.node-red {
|
| 1006 |
+
background-color: #e74c3c !important;
|
| 1007 |
+
color: white !important;
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
jmnode.node-purple {
|
| 1011 |
+
background-color: #9b59b6 !important;
|
| 1012 |
+
color: white !important;
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
jmnode.node-orange {
|
| 1016 |
+
background-color: #e67e22 !important;
|
| 1017 |
+
color: white !important;
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
+
jmnode.node-yellow {
|
| 1021 |
+
background-color: #f1c40f !important;
|
| 1022 |
+
color: #34495e !important;
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
jmnode.node-teal {
|
| 1026 |
+
background-color: #1abc9c !important;
|
| 1027 |
+
color: white !important;
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
jmnode.node-gray {
|
| 1031 |
+
background-color: #95a5a6 !important;
|
| 1032 |
+
color: white !important;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
/* Node hover states with color indication */
|
| 1036 |
+
jmnode.node-blue:hover, jmnode.node-green:hover, jmnode.node-red:hover,
|
| 1037 |
+
jmnode.node-purple:hover, jmnode.node-orange:hover, jmnode.node-yellow:hover,
|
| 1038 |
+
jmnode.node-teal:hover, jmnode.node-gray:hover {
|
| 1039 |
+
filter: brightness(1.1);
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
/* Floating Action Button (FAB) */
|
| 1043 |
+
.fab-container {
|
| 1044 |
+
position: fixed;
|
| 1045 |
+
bottom: 2rem;
|
| 1046 |
+
right: 2rem;
|
| 1047 |
+
z-index: 999;
|
| 1048 |
+
display: flex;
|
| 1049 |
+
flex-direction: column-reverse;
|
| 1050 |
+
align-items: center;
|
| 1051 |
+
gap: 0.75rem;
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
.fab-button {
|
| 1055 |
+
width: 3.5rem;
|
| 1056 |
+
height: 3.5rem;
|
| 1057 |
+
border-radius: 50%;
|
| 1058 |
+
background-color: var(--primary-color);
|
| 1059 |
+
color: white;
|
| 1060 |
+
border: none;
|
| 1061 |
+
box-shadow: var(--shadow-lg);
|
| 1062 |
+
display: flex;
|
| 1063 |
+
align-items: center;
|
| 1064 |
+
justify-content: center;
|
| 1065 |
+
font-size: 1.5rem;
|
| 1066 |
+
cursor: pointer;
|
| 1067 |
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
.fab-button:hover {
|
| 1071 |
+
transform: scale(1.1);
|
| 1072 |
+
background-color: var(--primary-dark);
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
.fab-button:active {
|
| 1076 |
+
transform: scale(0.95);
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
.fab-options {
|
| 1080 |
+
display: flex;
|
| 1081 |
+
flex-direction: column;
|
| 1082 |
+
gap: 0.75rem;
|
| 1083 |
+
opacity: 0;
|
| 1084 |
+
pointer-events: none;
|
| 1085 |
+
transform: translateY(1rem);
|
| 1086 |
+
transition: all 0.3s ease;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
.fab-container.active .fab-options {
|
| 1090 |
+
opacity: 1;
|
| 1091 |
+
pointer-events: auto;
|
| 1092 |
+
transform: translateY(0);
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
.fab-container.active .fab-button {
|
| 1096 |
+
transform: rotate(45deg);
|
| 1097 |
+
background-color: var(--accent-color);
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
.fab-option {
|
| 1101 |
+
width: 2.5rem;
|
| 1102 |
+
height: 2.5rem;
|
| 1103 |
+
border-radius: 50%;
|
| 1104 |
+
background-color: var(--surface-color);
|
| 1105 |
+
color: var(--text-primary);
|
| 1106 |
+
border: none;
|
| 1107 |
+
box-shadow: var(--shadow-md);
|
| 1108 |
+
display: flex;
|
| 1109 |
+
align-items: center;
|
| 1110 |
+
justify-content: center;
|
| 1111 |
+
font-size: 1rem;
|
| 1112 |
+
cursor: pointer;
|
| 1113 |
+
transition: all 0.2s ease;
|
| 1114 |
+
position: relative;
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
.fab-option:hover {
|
| 1118 |
+
transform: scale(1.1);
|
| 1119 |
+
color: var(--primary-color);
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
.fab-option:hover::after {
|
| 1123 |
+
content: attr(title);
|
| 1124 |
+
position: absolute;
|
| 1125 |
+
right: calc(100% + 0.75rem);
|
| 1126 |
+
background-color: var(--surface-color);
|
| 1127 |
+
color: var(--text-primary);
|
| 1128 |
+
padding: 0.25rem 0.5rem;
|
| 1129 |
+
border-radius: var(--radius-sm);
|
| 1130 |
+
font-size: 0.8rem;
|
| 1131 |
+
white-space: nowrap;
|
| 1132 |
+
box-shadow: var(--shadow-sm);
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
/* Results Gallery */
|
| 1136 |
+
.gallery-container {
|
| 1137 |
+
position: fixed;
|
| 1138 |
+
top: 0;
|
| 1139 |
+
left: 0;
|
| 1140 |
+
width: 100%;
|
| 1141 |
+
height: 100%;
|
| 1142 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 1143 |
+
z-index: 2000;
|
| 1144 |
+
display: flex;
|
| 1145 |
+
align-items: center;
|
| 1146 |
+
justify-content: center;
|
| 1147 |
+
opacity: 0;
|
| 1148 |
+
pointer-events: none;
|
| 1149 |
+
transition: opacity 0.3s ease;
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
.gallery-container.active {
|
| 1153 |
+
opacity: 1;
|
| 1154 |
+
pointer-events: auto;
|
| 1155 |
+
}
|
| 1156 |
+
|
| 1157 |
+
.gallery-content {
|
| 1158 |
+
width: 90%;
|
| 1159 |
+
max-width: 1200px;
|
| 1160 |
+
height: 80%;
|
| 1161 |
+
background-color: var(--surface-color);
|
| 1162 |
+
border-radius: var(--radius-lg);
|
| 1163 |
+
box-shadow: var(--shadow-lg);
|
| 1164 |
+
overflow: hidden;
|
| 1165 |
+
display: flex;
|
| 1166 |
+
flex-direction: column;
|
| 1167 |
+
transform: scale(0.95);
|
| 1168 |
+
transition: transform 0.3s ease;
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
.gallery-container.active .gallery-content {
|
| 1172 |
+
transform: scale(1);
|
| 1173 |
+
}
|
| 1174 |
+
|
| 1175 |
+
.gallery-header {
|
| 1176 |
+
display: flex;
|
| 1177 |
+
align-items: center;
|
| 1178 |
+
justify-content: space-between;
|
| 1179 |
+
padding: 1rem 1.5rem;
|
| 1180 |
+
border-bottom: 1px solid var(--border-color);
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
.gallery-header h2 {
|
| 1184 |
+
margin: 0;
|
| 1185 |
+
font-size: 1.5rem;
|
| 1186 |
+
color: var(--text-primary);
|
| 1187 |
+
}
|
| 1188 |
+
|
| 1189 |
+
.gallery-close {
|
| 1190 |
+
width: 2rem;
|
| 1191 |
+
height: 2rem;
|
| 1192 |
+
border-radius: 50%;
|
| 1193 |
+
background-color: transparent;
|
| 1194 |
+
color: var(--text-primary);
|
| 1195 |
+
border: none;
|
| 1196 |
+
display: flex;
|
| 1197 |
+
align-items: center;
|
| 1198 |
+
justify-content: center;
|
| 1199 |
+
font-size: 1.2rem;
|
| 1200 |
+
cursor: pointer;
|
| 1201 |
+
transition: all 0.2s ease;
|
| 1202 |
+
}
|
| 1203 |
+
|
| 1204 |
+
.gallery-close:hover {
|
| 1205 |
+
background-color: rgba(0, 0, 0, 0.1);
|
| 1206 |
+
color: var(--primary-color);
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
.gallery-content {
|
| 1210 |
+
padding: 1.5rem;
|
| 1211 |
+
overflow-y: auto;
|
| 1212 |
+
display: grid;
|
| 1213 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 1214 |
+
gap: 1.5rem;
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
.gallery-item {
|
| 1218 |
+
border-radius: var(--radius-md);
|
| 1219 |
+
overflow: hidden;
|
| 1220 |
+
box-shadow: var(--shadow-md);
|
| 1221 |
+
background-color: var(--background-color);
|
| 1222 |
+
transition: all 0.2s ease;
|
| 1223 |
+
cursor: pointer;
|
| 1224 |
+
aspect-ratio: 16/9;
|
| 1225 |
+
position: relative;
|
| 1226 |
+
}
|
| 1227 |
+
|
| 1228 |
+
.gallery-item:hover {
|
| 1229 |
+
transform: translateY(-0.5rem);
|
| 1230 |
+
box-shadow: var(--shadow-lg);
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
.gallery-item-preview {
|
| 1234 |
+
width: 100%;
|
| 1235 |
+
height: 75%;
|
| 1236 |
+
object-fit: cover;
|
| 1237 |
+
background-color: var(--primary-color);
|
| 1238 |
+
opacity: 0.8;
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
.gallery-item-info {
|
| 1242 |
+
padding: 0.75rem;
|
| 1243 |
+
display: flex;
|
| 1244 |
+
flex-direction: column;
|
| 1245 |
+
}
|
| 1246 |
+
|
| 1247 |
+
.gallery-item-title {
|
| 1248 |
+
font-weight: 600;
|
| 1249 |
+
font-size: 1rem;
|
| 1250 |
+
color: var(--text-primary);
|
| 1251 |
+
margin-bottom: 0.25rem;
|
| 1252 |
+
display: -webkit-box;
|
| 1253 |
+
-webkit-line-clamp: 1;
|
| 1254 |
+
-webkit-box-orient: vertical;
|
| 1255 |
+
overflow: hidden;
|
| 1256 |
+
}
|
| 1257 |
+
|
| 1258 |
+
.gallery-item-date {
|
| 1259 |
+
font-size: 0.8rem;
|
| 1260 |
+
color: var(--text-secondary);
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
/* Dark Mode Adjustments for existing components */
|
| 1264 |
+
[data-theme="dark"] .app-header {
|
| 1265 |
+
background-color: var(--surface-color);
|
| 1266 |
+
border-bottom: 1px solid var(--border-color);
|
| 1267 |
+
}
|
| 1268 |
+
|
| 1269 |
+
[data-theme="dark"] .logo-icon {
|
| 1270 |
+
color: var(--primary-color);
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
[data-theme="dark"] .transcription-content,
|
| 1274 |
+
[data-theme="dark"] .topics-content {
|
| 1275 |
+
background-color: #2d3748;
|
| 1276 |
+
border: 1px solid var(--border-color);
|
| 1277 |
+
}
|
| 1278 |
+
|
| 1279 |
+
[data-theme="dark"] .progress-container {
|
| 1280 |
+
background-color: #2d3748;
|
| 1281 |
+
}
|
| 1282 |
+
|
| 1283 |
+
[data-theme="dark"] .topic {
|
| 1284 |
+
background-color: #2d3748;
|
| 1285 |
+
border-left: 4px solid var(--primary-color);
|
| 1286 |
+
}
|
| 1287 |
+
|
| 1288 |
+
[data-theme="dark"] jmnode {
|
| 1289 |
+
background-color: #2d3748 !important;
|
| 1290 |
+
color: var(--text-primary) !important;
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
[data-theme="dark"] jmnode.root {
|
| 1294 |
+
background-color: var(--primary-color) !important;
|
| 1295 |
+
color: white !important;
|
| 1296 |
+
}
|
| 1297 |
+
|
| 1298 |
+
[data-theme="dark"] jmexpander {
|
| 1299 |
+
background-color: #4a5568 !important;
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
[data-theme="dark"] .node-context-menu {
|
| 1303 |
+
background-color: #2d3748;
|
| 1304 |
+
border: 1px solid var(--border-color);
|
| 1305 |
+
}
|
| 1306 |
+
|
| 1307 |
+
[data-theme="dark"] .context-menu-separator {
|
| 1308 |
+
background-color: var(--border-color);
|
| 1309 |
+
}
|
| 1310 |
+
|
| 1311 |
+
[data-theme="dark"] .context-menu-item:hover {
|
| 1312 |
+
background-color: #4a5568;
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
/* 1. Elevated Card Design System */
|
| 1316 |
+
.card {
|
| 1317 |
+
background-color: var(--surface-color);
|
| 1318 |
+
border-radius: var(--radius-lg);
|
| 1319 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05),
|
| 1320 |
+
0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
| 1321 |
+
overflow: hidden;
|
| 1322 |
+
border: 1px solid rgba(0, 0, 0, 0.04);
|
| 1323 |
+
transition: all 0.3s ease;
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
.card:hover {
|
| 1327 |
+
box-shadow: 0 15px 30px -5px rgba(0, 0, 0, 0.07),
|
| 1328 |
+
0 10px 15px -3px rgba(0, 0, 0, 0.04);
|
| 1329 |
+
transform: translateY(-2px);
|
| 1330 |
+
}
|
| 1331 |
+
|
| 1332 |
+
.card-header {
|
| 1333 |
+
padding: 1.25rem 1.5rem;
|
| 1334 |
+
border-bottom: 1px solid var(--border-color);
|
| 1335 |
+
display: flex;
|
| 1336 |
+
align-items: center;
|
| 1337 |
+
justify-content: space-between;
|
| 1338 |
+
}
|
| 1339 |
+
|
| 1340 |
+
.card-title {
|
| 1341 |
+
font-family: var(--font-heading);
|
| 1342 |
+
font-weight: 600;
|
| 1343 |
+
font-size: 1.2rem;
|
| 1344 |
+
margin: 0;
|
| 1345 |
+
display: flex;
|
| 1346 |
+
align-items: center;
|
| 1347 |
+
gap: 0.75rem;
|
| 1348 |
+
color: var(--text-primary);
|
| 1349 |
+
}
|
| 1350 |
+
|
| 1351 |
+
.card-title i {
|
| 1352 |
+
color: var(--primary-color);
|
| 1353 |
+
font-size: 1.3rem;
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
.card-body {
|
| 1357 |
+
padding: 1.5rem;
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
.card-footer {
|
| 1361 |
+
padding: 1.25rem 1.5rem;
|
| 1362 |
+
border-top: 1px solid var(--border-color);
|
| 1363 |
+
background-color: rgba(0, 0, 0, 0.01);
|
| 1364 |
+
}
|
| 1365 |
+
|
| 1366 |
+
/* 2. Micro-Interactions & Transitions */
|
| 1367 |
+
@keyframes fadeIn {
|
| 1368 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 1369 |
+
to { opacity: 1; transform: translateY(0); }
|
| 1370 |
+
}
|
| 1371 |
+
|
| 1372 |
+
@keyframes pulse {
|
| 1373 |
+
0% { transform: scale(1); }
|
| 1374 |
+
50% { transform: scale(1.05); }
|
| 1375 |
+
100% { transform: scale(1); }
|
| 1376 |
+
}
|
| 1377 |
+
|
| 1378 |
+
@keyframes shimmer {
|
| 1379 |
+
0% { background-position: -1000px 0; }
|
| 1380 |
+
100% { background-position: 1000px 0; }
|
| 1381 |
+
}
|
| 1382 |
+
|
| 1383 |
+
.animate-fade-in {
|
| 1384 |
+
animation: fadeIn 0.4s ease forwards;
|
| 1385 |
+
}
|
| 1386 |
+
|
| 1387 |
+
.animate-pulse {
|
| 1388 |
+
animation: pulse 2s infinite ease-in-out;
|
| 1389 |
+
}
|
| 1390 |
+
|
| 1391 |
+
/* 4. Custom Video Player Controls */
|
| 1392 |
+
.video-preview {
|
| 1393 |
+
margin: 2rem 0;
|
| 1394 |
+
border-radius: var(--radius-md);
|
| 1395 |
+
overflow: hidden;
|
| 1396 |
+
background-color: #000;
|
| 1397 |
+
position: relative;
|
| 1398 |
+
box-shadow: 0 15px 25px -5px rgba(0, 0, 0, 0.15),
|
| 1399 |
+
0 10px 10px -5px rgba(0, 0, 0, 0.05);
|
| 1400 |
+
}
|
| 1401 |
+
|
| 1402 |
+
.video-preview::after {
|
| 1403 |
+
content: '';
|
| 1404 |
+
position: absolute;
|
| 1405 |
+
bottom: 0;
|
| 1406 |
+
left: 0;
|
| 1407 |
+
right: 0;
|
| 1408 |
+
height: 90px;
|
| 1409 |
+
background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%);
|
| 1410 |
+
pointer-events: none;
|
| 1411 |
+
opacity: 0;
|
| 1412 |
+
transition: opacity 0.3s ease;
|
| 1413 |
+
}
|
| 1414 |
+
|
| 1415 |
+
.video-preview:hover::after {
|
| 1416 |
+
opacity: 1;
|
| 1417 |
+
}
|
| 1418 |
+
|
| 1419 |
+
.video-controls {
|
| 1420 |
+
position: absolute;
|
| 1421 |
+
bottom: 0;
|
| 1422 |
+
left: 0;
|
| 1423 |
+
width: 100%;
|
| 1424 |
+
padding: 1rem;
|
| 1425 |
+
display: flex;
|
| 1426 |
+
align-items: center;
|
| 1427 |
+
gap: 1rem;
|
| 1428 |
+
z-index: 10;
|
| 1429 |
+
opacity: 0;
|
| 1430 |
+
transform: translateY(10px);
|
| 1431 |
+
transition: all 0.3s ease;
|
| 1432 |
+
}
|
| 1433 |
+
|
| 1434 |
+
.video-preview:hover .video-controls {
|
| 1435 |
+
opacity: 1;
|
| 1436 |
+
transform: translateY(0);
|
| 1437 |
+
}
|
| 1438 |
+
|
| 1439 |
+
.video-progress {
|
| 1440 |
+
flex: 1;
|
| 1441 |
+
height: 4px;
|
| 1442 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 1443 |
+
border-radius: 2px;
|
| 1444 |
+
overflow: hidden;
|
| 1445 |
+
cursor: pointer;
|
| 1446 |
+
position: relative;
|
| 1447 |
+
}
|
| 1448 |
+
|
| 1449 |
+
.video-progress-bar {
|
| 1450 |
+
height: 100%;
|
| 1451 |
+
width: 0;
|
| 1452 |
+
background-color: var(--primary-color);
|
| 1453 |
+
border-radius: 2px;
|
| 1454 |
+
position: relative;
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
.video-progress-handle {
|
| 1458 |
+
position: absolute;
|
| 1459 |
+
right: -6px;
|
| 1460 |
+
top: 50%;
|
| 1461 |
+
transform: translateY(-50%);
|
| 1462 |
+
width: 12px;
|
| 1463 |
+
height: 12px;
|
| 1464 |
+
border-radius: 50%;
|
| 1465 |
+
background-color: white;
|
| 1466 |
+
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
|
| 1467 |
+
opacity: 0;
|
| 1468 |
+
transition: opacity 0.2s ease;
|
| 1469 |
+
}
|
| 1470 |
+
|
| 1471 |
+
.video-progress:hover .video-progress-handle {
|
| 1472 |
+
opacity: 1;
|
| 1473 |
+
}
|
| 1474 |
+
|
| 1475 |
+
.video-control-button {
|
| 1476 |
+
background: transparent;
|
| 1477 |
+
border: none;
|
| 1478 |
+
color: white;
|
| 1479 |
+
cursor: pointer;
|
| 1480 |
+
width: 36px;
|
| 1481 |
+
height: 36px;
|
| 1482 |
+
display: flex;
|
| 1483 |
+
align-items: center;
|
| 1484 |
+
justify-content: center;
|
| 1485 |
+
border-radius: 50%;
|
| 1486 |
+
transition: all 0.2s ease;
|
| 1487 |
+
}
|
| 1488 |
+
|
| 1489 |
+
.video-control-button:hover {
|
| 1490 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
.video-time {
|
| 1494 |
+
color: white;
|
| 1495 |
+
font-family: var(--font-mono);
|
| 1496 |
+
font-size: 0.85rem;
|
| 1497 |
+
min-width: 50px;
|
| 1498 |
+
text-align: center;
|
| 1499 |
+
}
|
| 1500 |
+
|
| 1501 |
+
.video-preview.playing .video-play-icon {
|
| 1502 |
+
display: none;
|
| 1503 |
+
}
|
| 1504 |
+
|
| 1505 |
+
.video-preview:not(.playing) .video-pause-icon {
|
| 1506 |
+
display: none;
|
| 1507 |
+
}
|
| 1508 |
+
|
| 1509 |
+
/* 5. Streamlined Action Buttons */
|
| 1510 |
+
.action-button {
|
| 1511 |
+
padding: 0.9rem 1.8rem;
|
| 1512 |
+
border: none;
|
| 1513 |
+
border-radius: var(--radius-md);
|
| 1514 |
+
font-size: 1rem;
|
| 1515 |
+
font-weight: 600;
|
| 1516 |
+
font-family: var(--font-heading);
|
| 1517 |
+
cursor: pointer;
|
| 1518 |
+
display: flex;
|
| 1519 |
+
align-items: center;
|
| 1520 |
+
justify-content: center;
|
| 1521 |
+
gap: 0.75rem;
|
| 1522 |
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 1523 |
+
position: relative;
|
| 1524 |
+
overflow: hidden;
|
| 1525 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
| 1526 |
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
| 1527 |
+
}
|
| 1528 |
+
|
| 1529 |
+
.action-button::before {
|
| 1530 |
+
content: '';
|
| 1531 |
+
position: absolute;
|
| 1532 |
+
top: 0;
|
| 1533 |
+
left: 0;
|
| 1534 |
+
width: 100%;
|
| 1535 |
+
height: 100%;
|
| 1536 |
+
background: linear-gradient(120deg, rgba(255,255,255,0) 30%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0) 70%);
|
| 1537 |
+
transform: translateX(-100%);
|
| 1538 |
+
transition: all 0.6s ease;
|
| 1539 |
+
}
|
| 1540 |
+
|
| 1541 |
+
.action-button:hover::before {
|
| 1542 |
+
transform: translateX(100%);
|
| 1543 |
+
}
|
| 1544 |
+
|
| 1545 |
+
.action-button i {
|
| 1546 |
+
font-size: 1.1rem;
|
| 1547 |
+
transition: transform 0.3s ease;
|
| 1548 |
+
}
|
| 1549 |
+
|
| 1550 |
+
.action-button:hover i {
|
| 1551 |
+
transform: scale(1.2);
|
| 1552 |
+
}
|
| 1553 |
+
|
| 1554 |
+
.action-button:active {
|
| 1555 |
+
transform: translateY(2px);
|
| 1556 |
+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 1557 |
+
}
|
| 1558 |
+
|
| 1559 |
+
.action-button.primary {
|
| 1560 |
+
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
| 1561 |
+
color: white;
|
| 1562 |
+
}
|
| 1563 |
+
|
| 1564 |
+
.action-button.primary:hover {
|
| 1565 |
+
background: linear-gradient(135deg, var(--primary-dark), var(--primary-color));
|
| 1566 |
+
box-shadow: 0 10px 15px -3px rgba(52, 152, 219, 0.3),
|
| 1567 |
+
0 4px 6px -2px rgba(52, 152, 219, 0.15);
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
.action-button.secondary {
|
| 1571 |
+
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-dark));
|
| 1572 |
+
color: white;
|
| 1573 |
+
}
|
| 1574 |
+
|
| 1575 |
+
.action-button.secondary:hover {
|
| 1576 |
+
background: linear-gradient(135deg, var(--secondary-dark), var(--secondary-color));
|
| 1577 |
+
box-shadow: 0 10px 15px -3px rgba(46, 204, 113, 0.3),
|
| 1578 |
+
0 4px 6px -2px rgba(46, 204, 113, 0.15);
|
| 1579 |
+
}
|
| 1580 |
+
|
| 1581 |
+
.action-button.disabled {
|
| 1582 |
+
opacity: 0.7;
|
| 1583 |
+
cursor: not-allowed;
|
| 1584 |
+
background: #95a5a6;
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
.action-button.disabled::before {
|
| 1588 |
+
display: none;
|
| 1589 |
+
}
|
| 1590 |
+
|
| 1591 |
+
.action-button.processing {
|
| 1592 |
+
position: relative;
|
| 1593 |
+
overflow: hidden;
|
| 1594 |
+
}
|
| 1595 |
+
|
| 1596 |
+
.action-button.processing::after {
|
| 1597 |
+
content: "";
|
| 1598 |
+
position: absolute;
|
| 1599 |
+
top: 0;
|
| 1600 |
+
left: 0;
|
| 1601 |
+
width: 100%;
|
| 1602 |
+
height: 100%;
|
| 1603 |
+
background: linear-gradient(90deg,
|
| 1604 |
+
transparent 25%,
|
| 1605 |
+
rgba(255, 255, 255, 0.3) 50%,
|
| 1606 |
+
transparent 75%);
|
| 1607 |
+
background-size: 200% 100%;
|
| 1608 |
+
animation: shimmer 1.5s infinite linear;
|
| 1609 |
+
}
|
| 1610 |
+
|
| 1611 |
+
/* 6. Content Container Improvements */
|
| 1612 |
+
.content-container {
|
| 1613 |
+
background-color: var(--surface-color);
|
| 1614 |
+
border-radius: var(--radius-lg);
|
| 1615 |
+
overflow: hidden;
|
| 1616 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05),
|
| 1617 |
+
0 8px 10px -6px rgba(0, 0, 0, 0.02);
|
| 1618 |
+
display: flex;
|
| 1619 |
+
flex-direction: column;
|
| 1620 |
+
height: 100%;
|
| 1621 |
+
transition: all 0.3s ease;
|
| 1622 |
+
border: 1px solid var(--border-color);
|
| 1623 |
+
}
|
| 1624 |
+
|
| 1625 |
+
.content-header {
|
| 1626 |
+
padding: 1.25rem 1.5rem;
|
| 1627 |
+
border-bottom: 1px solid var(--border-color);
|
| 1628 |
+
display: flex;
|
| 1629 |
+
align-items: center;
|
| 1630 |
+
gap: 0.75rem;
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
.content-title {
|
| 1634 |
+
font-family: var(--font-heading);
|
| 1635 |
+
font-weight: 600;
|
| 1636 |
+
font-size: 1.2rem;
|
| 1637 |
+
margin: 0;
|
| 1638 |
+
color: var(--text-primary);
|
| 1639 |
+
display: flex;
|
| 1640 |
+
align-items: center;
|
| 1641 |
+
gap: 0.75rem;
|
| 1642 |
+
}
|
| 1643 |
+
|
| 1644 |
+
.content-title i {
|
| 1645 |
+
color: var(--primary-color);
|
| 1646 |
+
font-size: 1.3rem;
|
| 1647 |
+
}
|
| 1648 |
+
|
| 1649 |
+
.content-body {
|
| 1650 |
+
flex: 1;
|
| 1651 |
+
overflow: hidden;
|
| 1652 |
+
position: relative;
|
| 1653 |
+
}
|
| 1654 |
+
|
| 1655 |
+
.content-scroll {
|
| 1656 |
+
height: 100%;
|
| 1657 |
+
overflow-y: auto;
|
| 1658 |
+
padding: 1.5rem;
|
| 1659 |
+
scrollbar-width: thin;
|
| 1660 |
+
scrollbar-color: var(--border-color) transparent;
|
| 1661 |
+
}
|
| 1662 |
+
|
| 1663 |
+
.content-scroll::-webkit-scrollbar {
|
| 1664 |
+
width: 6px;
|
| 1665 |
+
}
|
| 1666 |
+
|
| 1667 |
+
.content-scroll::-webkit-scrollbar-track {
|
| 1668 |
+
background: transparent;
|
| 1669 |
+
}
|
| 1670 |
+
|
| 1671 |
+
.content-scroll::-webkit-scrollbar-thumb {
|
| 1672 |
+
background-color: var(--border-color);
|
| 1673 |
+
border-radius: 20px;
|
| 1674 |
+
}
|
| 1675 |
+
|
| 1676 |
+
.empty-state {
|
| 1677 |
+
display: flex;
|
| 1678 |
+
flex-direction: column;
|
| 1679 |
+
align-items: center;
|
| 1680 |
+
justify-content: center;
|
| 1681 |
+
padding: 3rem 1.5rem;
|
| 1682 |
+
color: var(--text-secondary);
|
| 1683 |
+
text-align: center;
|
| 1684 |
+
}
|
| 1685 |
+
|
| 1686 |
+
.empty-state i {
|
| 1687 |
+
font-size: 3rem;
|
| 1688 |
+
margin-bottom: 1rem;
|
| 1689 |
+
opacity: 0.5;
|
| 1690 |
+
}
|
| 1691 |
+
|
| 1692 |
+
.empty-state-title {
|
| 1693 |
+
font-weight: 600;
|
| 1694 |
+
margin-bottom: 0.5rem;
|
| 1695 |
+
color: var(--text-primary);
|
| 1696 |
+
}
|
| 1697 |
+
|
| 1698 |
+
.empty-state-description {
|
| 1699 |
+
max-width: 300px;
|
| 1700 |
+
margin: 0 auto;
|
| 1701 |
+
font-size: 0.95rem;
|
| 1702 |
+
}
|
| 1703 |
+
|
| 1704 |
+
/* 7. Status/Progress Visualization */
|
| 1705 |
+
.status-progress {
|
| 1706 |
+
margin-top: 1.5rem;
|
| 1707 |
+
display: flex;
|
| 1708 |
+
flex-direction: column;
|
| 1709 |
+
gap: 0.75rem;
|
| 1710 |
+
}
|
| 1711 |
+
|
| 1712 |
+
.status-text {
|
| 1713 |
+
display: flex;
|
| 1714 |
+
align-items: center;
|
| 1715 |
+
gap: 0.5rem;
|
| 1716 |
+
color: var(--text-secondary);
|
| 1717 |
+
font-size: 0.95rem;
|
| 1718 |
+
}
|
| 1719 |
+
|
| 1720 |
+
.status-text i {
|
| 1721 |
+
color: var(--primary-color);
|
| 1722 |
+
}
|
| 1723 |
+
|
| 1724 |
+
.progress-container {
|
| 1725 |
+
background-color: var(--background-color);
|
| 1726 |
+
padding: 1.5rem;
|
| 1727 |
+
border-radius: var(--radius-md);
|
| 1728 |
+
overflow: hidden;
|
| 1729 |
+
}
|
| 1730 |
+
|
| 1731 |
+
.progress-steps {
|
| 1732 |
+
display: flex;
|
| 1733 |
+
margin-bottom: 1rem;
|
| 1734 |
+
position: relative;
|
| 1735 |
+
}
|
| 1736 |
+
|
| 1737 |
+
.progress-step {
|
| 1738 |
+
flex: 1;
|
| 1739 |
+
text-align: center;
|
| 1740 |
+
position: relative;
|
| 1741 |
+
padding-bottom: 1.5rem;
|
| 1742 |
+
}
|
| 1743 |
+
|
| 1744 |
+
.progress-step-indicator {
|
| 1745 |
+
width: 24px;
|
| 1746 |
+
height: 24px;
|
| 1747 |
+
border-radius: 50%;
|
| 1748 |
+
background-color: var(--surface-color);
|
| 1749 |
+
border: 2px solid var(--border-color);
|
| 1750 |
+
margin: 0 auto 0.5rem;
|
| 1751 |
+
display: flex;
|
| 1752 |
+
align-items: center;
|
| 1753 |
+
justify-content: center;
|
| 1754 |
+
position: relative;
|
| 1755 |
+
z-index: 2;
|
| 1756 |
+
transition: all 0.3s ease;
|
| 1757 |
+
}
|
| 1758 |
+
|
| 1759 |
+
.progress-step-line {
|
| 1760 |
+
position: absolute;
|
| 1761 |
+
top: 12px;
|
| 1762 |
+
left: 0;
|
| 1763 |
+
right: 0;
|
| 1764 |
+
height: 2px;
|
| 1765 |
+
background-color: var(--border-color);
|
| 1766 |
+
z-index: 1;
|
| 1767 |
+
}
|
| 1768 |
+
|
| 1769 |
+
.progress-step-icon {
|
| 1770 |
+
color: var(--text-secondary);
|
| 1771 |
+
font-size: 0.8rem;
|
| 1772 |
+
opacity: 0;
|
| 1773 |
+
transition: all 0.3s ease;
|
| 1774 |
+
}
|
| 1775 |
+
|
| 1776 |
+
.progress-step-label {
|
| 1777 |
+
font-size: 0.8rem;
|
| 1778 |
+
font-weight: 500;
|
| 1779 |
+
color: var(--text-secondary);
|
| 1780 |
+
transition: all 0.3s ease;
|
| 1781 |
+
}
|
| 1782 |
+
|
| 1783 |
+
.progress-step.active .progress-step-indicator {
|
| 1784 |
+
background-color: var(--primary-color);
|
| 1785 |
+
border-color: var(--primary-color);
|
| 1786 |
+
transform: scale(1.1);
|
| 1787 |
+
box-shadow: 0 0 0 4px rgba(52, 152, 219, 0.2);
|
| 1788 |
+
}
|
| 1789 |
+
|
| 1790 |
+
.progress-step.active .progress-step-icon {
|
| 1791 |
+
opacity: 1;
|
| 1792 |
+
color: white;
|
| 1793 |
+
}
|
| 1794 |
+
|
| 1795 |
+
.progress-step.active .progress-step-label {
|
| 1796 |
+
color: var(--primary-color);
|
| 1797 |
+
font-weight: 600;
|
| 1798 |
+
}
|
| 1799 |
+
|
| 1800 |
+
.progress-step.completed .progress-step-indicator {
|
| 1801 |
+
background-color: var(--primary-color);
|
| 1802 |
+
border-color: var(--primary-color);
|
| 1803 |
+
}
|
| 1804 |
+
|
| 1805 |
+
.progress-step.completed .progress-step-icon {
|
| 1806 |
+
opacity: 1;
|
| 1807 |
+
color: white;
|
| 1808 |
+
}
|
| 1809 |
+
|
| 1810 |
+
.progress-step.completed + .progress-step .progress-step-line,
|
| 1811 |
+
.progress-step.active + .progress-step .progress-step-line {
|
| 1812 |
+
background: linear-gradient(to right, var(--primary-color) 0%, var(--border-color) 100%);
|
| 1813 |
+
}
|
| 1814 |
+
|
| 1815 |
+
.progress-step.completed .progress-step-line {
|
| 1816 |
+
background-color: var(--primary-color);
|
| 1817 |
+
}
|
| 1818 |
+
|
| 1819 |
+
.progress-bar {
|
| 1820 |
+
height: 6px;
|
| 1821 |
+
background-color: var(--border-color);
|
| 1822 |
+
border-radius: 3px;
|
| 1823 |
+
overflow: hidden;
|
| 1824 |
+
margin: 0.5rem 0;
|
| 1825 |
+
}
|
| 1826 |
+
|
| 1827 |
+
.progress-bar-fill {
|
| 1828 |
+
height: 100%;
|
| 1829 |
+
background: linear-gradient(90deg, var(--primary-color), var(--primary-dark));
|
| 1830 |
+
border-radius: 3px;
|
| 1831 |
+
transition: width 0.3s ease;
|
| 1832 |
+
position: relative;
|
| 1833 |
+
overflow: hidden;
|
| 1834 |
+
}
|
| 1835 |
+
|
| 1836 |
+
.progress-bar-fill::after {
|
| 1837 |
+
content: "";
|
| 1838 |
+
position: absolute;
|
| 1839 |
+
top: 0;
|
| 1840 |
+
left: 0;
|
| 1841 |
+
right: 0;
|
| 1842 |
+
bottom: 0;
|
| 1843 |
+
background: linear-gradient(
|
| 1844 |
+
90deg,
|
| 1845 |
+
rgba(255, 255, 255, 0) 0%,
|
| 1846 |
+
rgba(255, 255, 255, 0.4) 50%,
|
| 1847 |
+
rgba(255, 255, 255, 0) 100%
|
| 1848 |
+
);
|
| 1849 |
+
animation: shimmer 2s infinite linear;
|
| 1850 |
+
transform: translateX(-100%);
|
| 1851 |
+
}
|
| 1852 |
+
|
| 1853 |
+
/* Dark mode adjustments */
|
| 1854 |
+
[data-theme="dark"] .content-container {
|
| 1855 |
+
border-color: var(--border-color);
|
| 1856 |
+
}
|
| 1857 |
+
|
| 1858 |
+
[data-theme="dark"] .progress-container {
|
| 1859 |
+
background-color: #2d3748;
|
| 1860 |
+
}
|
| 1861 |
+
|
| 1862 |
+
[data-theme="dark"] .progress-step-indicator {
|
| 1863 |
+
background-color: #4a5568;
|
| 1864 |
+
border-color: #4a5568;
|
| 1865 |
+
}
|
| 1866 |
+
|
| 1867 |
+
[data-theme="dark"] .progress-step-line {
|
| 1868 |
+
background-color: #4a5568;
|
| 1869 |
+
}
|
| 1870 |
+
|
| 1871 |
+
[data-theme="dark"] .progress-bar {
|
| 1872 |
+
background-color: #4a5568;
|
| 1873 |
+
}
|
| 1874 |
+
|
| 1875 |
+
.transcription-content {
|
| 1876 |
+
width: 100%;
|
| 1877 |
+
line-height: 1.6;
|
| 1878 |
+
}
|
| 1879 |
+
|
| 1880 |
+
.transcription-content p {
|
| 1881 |
+
margin-bottom: 8px;
|
| 1882 |
+
padding: 8px;
|
| 1883 |
+
border-radius: var(--radius-sm);
|
| 1884 |
+
transition: background-color 0.2s ease;
|
| 1885 |
+
}
|
| 1886 |
+
|
| 1887 |
+
.transcription-content p:hover {
|
| 1888 |
+
background-color: rgba(52, 152, 219, 0.1);
|
| 1889 |
+
}
|
templates/index.html
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Flowify</title>
|
| 7 |
+
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
| 9 |
+
<link type="text/css" rel="stylesheet" href="https://unpkg.com/jsmind@0.8.6/style/jsmind.css" />
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<div class="app-container">
|
| 14 |
+
<header class="app-header">
|
| 15 |
+
<div class="logo-container">
|
| 16 |
+
<i class="fas fa-project-diagram logo-icon"></i>
|
| 17 |
+
<h1>Flowify</h1>
|
| 18 |
+
</div>
|
| 19 |
+
</header>
|
| 20 |
+
|
| 21 |
+
<!-- Theme Toggle Button -->
|
| 22 |
+
<div class="theme-toggle" id="themeToggle">
|
| 23 |
+
<div class="theme-toggle-icon light-icon">
|
| 24 |
+
<i class="fas fa-sun"></i>
|
| 25 |
+
</div>
|
| 26 |
+
<div class="theme-toggle-icon dark-icon">
|
| 27 |
+
<i class="fas fa-moon"></i>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<main class="main-content">
|
| 32 |
+
<div class="upload-panel card">
|
| 33 |
+
<div class="card-body">
|
| 34 |
+
<div class="upload-section">
|
| 35 |
+
<div class="upload-container" id="dropZone">
|
| 36 |
+
<div class="particles">
|
| 37 |
+
<!-- Particles will be added dynamically via JS -->
|
| 38 |
+
</div>
|
| 39 |
+
<div class="upload-icon-container">
|
| 40 |
+
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
| 41 |
+
</div>
|
| 42 |
+
<h3 class="upload-text">Drop video file here or click to upload</h3>
|
| 43 |
+
<p class="upload-subtext">Supports MP4, WebM, MOV formats</p>
|
| 44 |
+
<input type="file" id="fileInput" accept="video/*" style="display:none;">
|
| 45 |
+
<div class="upload-progress">
|
| 46 |
+
<div class="upload-progress-bar" style="width: 0%"></div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<div class="model-selector card">
|
| 51 |
+
<div class="card-header">
|
| 52 |
+
<h3 class="card-title"><i class="fas fa-cog"></i> Processing Model</h3>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="card-body">
|
| 55 |
+
<div class="select-wrapper">
|
| 56 |
+
<select id="modelSelector">
|
| 57 |
+
<option value="Xenova/whisper-tiny.en">Whisper Tiny (Fast)</option>
|
| 58 |
+
<option value="Xenova/whisper-base.en">Whisper Base</option>
|
| 59 |
+
<option value="Xenova/whisper-small.en" selected>Whisper Small</option>
|
| 60 |
+
<option value="Xenova/whisper-medium.en">Whisper Medium</option>
|
| 61 |
+
</select>
|
| 62 |
+
<i class="fas fa-chevron-down select-icon"></i>
|
| 63 |
+
</div>
|
| 64 |
+
<div id="modelStatus" class="model-status">
|
| 65 |
+
<i class="fas fa-check-circle"></i> Model available
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<div class="video-preview">
|
| 72 |
+
<video id="videoPlayer" controls></video>
|
| 73 |
+
|
| 74 |
+
<!-- Custom Video Controls -->
|
| 75 |
+
<div class="video-controls">
|
| 76 |
+
<button class="video-control-button" id="playPauseBtn">
|
| 77 |
+
<i class="fas fa-play video-play-icon"></i>
|
| 78 |
+
<i class="fas fa-pause video-pause-icon"></i>
|
| 79 |
+
</button>
|
| 80 |
+
<div class="video-time" id="currentTime">0:00</div>
|
| 81 |
+
<div class="video-progress">
|
| 82 |
+
<div class="video-progress-bar">
|
| 83 |
+
<div class="video-progress-handle"></div>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
<div class="video-time" id="duration">0:00</div>
|
| 87 |
+
<button class="video-control-button" id="muteBtn">
|
| 88 |
+
<i class="fas fa-volume-up"></i>
|
| 89 |
+
</button>
|
| 90 |
+
<button class="video-control-button" id="fullscreenBtn">
|
| 91 |
+
<i class="fas fa-expand"></i>
|
| 92 |
+
</button>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<div class="action-buttons">
|
| 97 |
+
<button id="transcribeButton" class="action-button primary" disabled>
|
| 98 |
+
<i class="fas fa-microphone"></i>
|
| 99 |
+
Transcribe Video
|
| 100 |
+
</button>
|
| 101 |
+
<button id="analyzeButton" class="action-button secondary" disabled>
|
| 102 |
+
<i class="fas fa-brain"></i>
|
| 103 |
+
Analyze Topics
|
| 104 |
+
</button>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<!-- New Progress Visualization -->
|
| 108 |
+
<div class="status-progress animate-fade-in">
|
| 109 |
+
<div class="status-text" id="status">
|
| 110 |
+
<i class="fas fa-info-circle"></i>
|
| 111 |
+
<span>Ready to transcribe</span>
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<div class="progress-container">
|
| 115 |
+
<div class="progress-steps">
|
| 116 |
+
<div class="progress-step" id="uploadStep">
|
| 117 |
+
<div class="progress-step-line"></div>
|
| 118 |
+
<div class="progress-step-indicator">
|
| 119 |
+
<i class="fas fa-upload progress-step-icon"></i>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="progress-step-label">Upload</div>
|
| 122 |
+
</div>
|
| 123 |
+
<div class="progress-step" id="transcribeStep">
|
| 124 |
+
<div class="progress-step-line"></div>
|
| 125 |
+
<div class="progress-step-indicator">
|
| 126 |
+
<i class="fas fa-microphone progress-step-icon"></i>
|
| 127 |
+
</div>
|
| 128 |
+
<div class="progress-step-label">Transcribe</div>
|
| 129 |
+
</div>
|
| 130 |
+
<div class="progress-step" id="analyzeStep">
|
| 131 |
+
<div class="progress-step-line"></div>
|
| 132 |
+
<div class="progress-step-indicator">
|
| 133 |
+
<i class="fas fa-brain progress-step-icon"></i>
|
| 134 |
+
</div>
|
| 135 |
+
<div class="progress-step-label">Analyze</div>
|
| 136 |
+
</div>
|
| 137 |
+
<div class="progress-step" id="visualizeStep">
|
| 138 |
+
<div class="progress-step-indicator">
|
| 139 |
+
<i class="fas fa-project-diagram progress-step-icon"></i>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="progress-step-label">Visualize</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div class="progress-bar">
|
| 146 |
+
<div class="progress-bar-fill" style="width: 0%"></div>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<div class="results-panel">
|
| 154 |
+
<div class="content-container animate-fade-in">
|
| 155 |
+
<div class="content-header">
|
| 156 |
+
<h2 class="content-title"><i class="fas fa-file-alt"></i> Transcription</h2>
|
| 157 |
+
</div>
|
| 158 |
+
<div class="content-body">
|
| 159 |
+
<div id="transcription" class="content-scroll">
|
| 160 |
+
<div class="empty-state">
|
| 161 |
+
<i class="fas fa-file-alt"></i>
|
| 162 |
+
<h3 class="empty-state-title">No transcription yet</h3>
|
| 163 |
+
<p class="empty-state-description">Upload a video and click "Transcribe Video" to see the transcription here</p>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div class="content-container animate-fade-in">
|
| 170 |
+
<div class="content-header">
|
| 171 |
+
<h2 class="content-title"><i class="fas fa-list"></i> Topics</h2>
|
| 172 |
+
</div>
|
| 173 |
+
<div class="content-body">
|
| 174 |
+
<div id="topicResults" class="content-scroll">
|
| 175 |
+
<div class="empty-state">
|
| 176 |
+
<i class="fas fa-brain"></i>
|
| 177 |
+
<h3 class="empty-state-title">No topics analyzed</h3>
|
| 178 |
+
<p class="empty-state-description">After transcribing, click "Analyze Topics" to identify key topics in your video</p>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</main>
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
<!-- Mindmap Overlay -->
|
| 188 |
+
<div id="mindmapOverlay" class="mindmap-overlay">
|
| 189 |
+
<div class="mindmap-content">
|
| 190 |
+
<div id="videoSection" class="video-section">
|
| 191 |
+
<div class="video-player-container">
|
| 192 |
+
<video id="overlayVideo" controls>
|
| 193 |
+
Your browser does not support the video element.
|
| 194 |
+
</video>
|
| 195 |
+
<button id="videoCloseButton" class="video-close-button">
|
| 196 |
+
<i class="fas fa-times"></i>
|
| 197 |
+
</button>
|
| 198 |
+
</div>
|
| 199 |
+
<div id="videoInfo" class="video-info"></div>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<div class="mindmap-section">
|
| 203 |
+
<div class="mindmap-toolbar">
|
| 204 |
+
<button id="backButton" class="tool-button">
|
| 205 |
+
<i class="fas fa-arrow-left"></i> Back
|
| 206 |
+
</button>
|
| 207 |
+
<button id="editButton" class="tool-button">
|
| 208 |
+
<i class="fas fa-edit"></i> Edit
|
| 209 |
+
</button>
|
| 210 |
+
<button id="zoomInButton" class="tool-button">
|
| 211 |
+
<i class="fas fa-search-plus"></i> Zoom In
|
| 212 |
+
</button>
|
| 213 |
+
<button id="zoomOutButton" class="tool-button">
|
| 214 |
+
<i class="fas fa-search-minus"></i> Zoom Out
|
| 215 |
+
</button>
|
| 216 |
+
<button id="panButton" class="tool-button">
|
| 217 |
+
<i class="fas fa-hand-paper"></i> Pan
|
| 218 |
+
</button>
|
| 219 |
+
<button id="resetButton" class="tool-button">
|
| 220 |
+
<i class="fas fa-sync"></i> Reset
|
| 221 |
+
</button>
|
| 222 |
+
</div>
|
| 223 |
+
<div id="mindmap" class="fullscreen-mindmap"></div>
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
|
| 228 |
+
<!-- Node Context Menu -->
|
| 229 |
+
<div id="nodeContextMenu" class="node-context-menu">
|
| 230 |
+
<div class="context-menu-item" id="editNodeAction">
|
| 231 |
+
<i class="fas fa-edit"></i>
|
| 232 |
+
Edit node
|
| 233 |
+
</div>
|
| 234 |
+
<div class="context-menu-item" id="addChildAction">
|
| 235 |
+
<i class="fas fa-plus-circle"></i>
|
| 236 |
+
Add child node
|
| 237 |
+
</div>
|
| 238 |
+
<div class="context-menu-item" id="addParentAction">
|
| 239 |
+
<i class="fas fa-level-up-alt"></i>
|
| 240 |
+
Add parent node
|
| 241 |
+
</div>
|
| 242 |
+
<div class="context-menu-separator"></div>
|
| 243 |
+
<div class="context-menu-item" id="focusNodeAction">
|
| 244 |
+
<i class="fas fa-search-plus"></i>
|
| 245 |
+
Focus on this branch
|
| 246 |
+
</div>
|
| 247 |
+
<div class="context-menu-item" id="expandCollapseAction">
|
| 248 |
+
<i class="fas fa-angle-double-down"></i>
|
| 249 |
+
Expand all children
|
| 250 |
+
</div>
|
| 251 |
+
<div class="context-menu-separator"></div>
|
| 252 |
+
<div class="context-menu-item" id="nodeColorAction">
|
| 253 |
+
<i class="fas fa-palette"></i>
|
| 254 |
+
Change color
|
| 255 |
+
</div>
|
| 256 |
+
<div class="color-picker-container" id="colorPicker" style="display: none;">
|
| 257 |
+
<div class="color-picker-label">Select a color:</div>
|
| 258 |
+
<div class="color-options">
|
| 259 |
+
<div class="color-option" style="background-color: #3498db;" data-color="blue"></div>
|
| 260 |
+
<div class="color-option" style="background-color: #2ecc71;" data-color="green"></div>
|
| 261 |
+
<div class="color-option" style="background-color: #e74c3c;" data-color="red"></div>
|
| 262 |
+
<div class="color-option" style="background-color: #9b59b6;" data-color="purple"></div>
|
| 263 |
+
<div class="color-option" style="background-color: #e67e22;" data-color="orange"></div>
|
| 264 |
+
<div class="color-option" style="background-color: #f1c40f;" data-color="yellow"></div>
|
| 265 |
+
<div class="color-option" style="background-color: #1abc9c;" data-color="teal"></div>
|
| 266 |
+
<div class="color-option" style="background-color: #95a5a6;" data-color="gray"></div>
|
| 267 |
+
</div>
|
| 268 |
+
</div>
|
| 269 |
+
<div class="context-menu-separator"></div>
|
| 270 |
+
<div class="context-menu-item" id="deleteNodeAction">
|
| 271 |
+
<i class="fas fa-trash-alt"></i>
|
| 272 |
+
Delete node
|
| 273 |
+
</div>
|
| 274 |
+
</div>
|
| 275 |
+
|
| 276 |
+
<!-- Floating Action Button (FAB) -->
|
| 277 |
+
<div class="fab-container" id="fabContainer">
|
| 278 |
+
<button class="fab-button" id="mainFab">
|
| 279 |
+
<i class="fas fa-plus"></i>
|
| 280 |
+
</button>
|
| 281 |
+
<div class="fab-options">
|
| 282 |
+
<button class="fab-option" id="exportPdfBtn" title="Export as PDF">
|
| 283 |
+
<i class="fas fa-file-pdf"></i>
|
| 284 |
+
</button>
|
| 285 |
+
<button class="fab-option" id="exportPngBtn" title="Export as PNG">
|
| 286 |
+
<i class="fas fa-file-image"></i>
|
| 287 |
+
</button>
|
| 288 |
+
<button class="fab-option" id="exportSvgBtn" title="Export as SVG">
|
| 289 |
+
<i class="fas fa-file-code"></i>
|
| 290 |
+
</button>
|
| 291 |
+
<button class="fab-option" id="shareMindmapBtn" title="Share Mindmap">
|
| 292 |
+
<i class="fas fa-share-alt"></i>
|
| 293 |
+
</button>
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
|
| 297 |
+
<!-- Results Gallery -->
|
| 298 |
+
<div class="gallery-container" id="resultsGallery">
|
| 299 |
+
<div class="gallery-header">
|
| 300 |
+
<h2>Recent Mindmaps</h2>
|
| 301 |
+
<button class="gallery-close" id="galleryClose">
|
| 302 |
+
<i class="fas fa-times"></i>
|
| 303 |
+
</button>
|
| 304 |
+
</div>
|
| 305 |
+
<div class="gallery-content" id="galleryContent">
|
| 306 |
+
<!-- Gallery items will be populated by JavaScript -->
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<script src="https://cdn.jsdelivr.net/npm/@xenova/transformers@2.16.0"></script>
|
| 311 |
+
<script type="text/javascript" src="https://unpkg.com/jsmind@0.8.6/es6/jsmind.js"></script>
|
| 312 |
+
<script>window.jsMind = jsMind;</script>
|
| 313 |
+
<script type="module" src="{{ url_for('static', filename='script.js') }}"></script>
|
| 314 |
+
</body>
|
| 315 |
+
</html>
|
topic_segmenter.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 3 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 4 |
+
from nltk.tokenize import sent_tokenize, word_tokenize
|
| 5 |
+
from nltk.stem import WordNetLemmatizer
|
| 6 |
+
from keybert import KeyBERT
|
| 7 |
+
import re
|
| 8 |
+
from collections import defaultdict
|
| 9 |
+
from sklearn.cluster import AgglomerativeClustering
|
| 10 |
+
|
| 11 |
+
class TopicSegmenter:
|
| 12 |
+
def __init__(self, window_size=3, similarity_threshold=0.2, context_size=2, min_segment_size=2, topic_similarity_threshold=0.3,
|
| 13 |
+
max_topics=8, hierarchical_threshold=0.6):
|
| 14 |
+
self.window_size = window_size
|
| 15 |
+
self.similarity_threshold = similarity_threshold
|
| 16 |
+
self.context_size = context_size
|
| 17 |
+
self.min_segment_size = min_segment_size
|
| 18 |
+
self.topic_similarity_threshold = topic_similarity_threshold
|
| 19 |
+
self.max_topics = max_topics # Maximum number of main topics
|
| 20 |
+
self.hierarchical_threshold = hierarchical_threshold # Threshold for hierarchical clustering
|
| 21 |
+
self.keyword_extractor = KeyBERT()
|
| 22 |
+
|
| 23 |
+
self.vectorizer = TfidfVectorizer(
|
| 24 |
+
stop_words='english',
|
| 25 |
+
min_df=1,
|
| 26 |
+
max_df=0.95,
|
| 27 |
+
ngram_range=(1, 2)
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
self.segment_vectorizer = TfidfVectorizer(
|
| 31 |
+
stop_words='english',
|
| 32 |
+
min_df=1,
|
| 33 |
+
max_df=1.0,
|
| 34 |
+
ngram_range=(1, 2)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
self.lemmatizer = WordNetLemmatizer()
|
| 38 |
+
self.topic_history = []
|
| 39 |
+
self.topic_hierarchy = {} # Store parent-child relationships
|
| 40 |
+
self.fitted_vectorizer = None
|
| 41 |
+
|
| 42 |
+
def preprocess_text(self, text):
|
| 43 |
+
# First, split the text by timestamps
|
| 44 |
+
timestamp_chunks = re.findall(r'\[(\d+)\](.*?)(?=\[\d+\]|$)', text, re.DOTALL)
|
| 45 |
+
|
| 46 |
+
all_sentences = []
|
| 47 |
+
original_sentences = []
|
| 48 |
+
timestamp_mapping = [] # To track which timestamp each sentence belongs to
|
| 49 |
+
|
| 50 |
+
for timestamp, chunk_text in timestamp_chunks:
|
| 51 |
+
# Split each chunk into sentences
|
| 52 |
+
chunk_sentences = sent_tokenize(chunk_text.strip())
|
| 53 |
+
for sentence in chunk_sentences:
|
| 54 |
+
if sentence.strip():
|
| 55 |
+
# Keep track of the original sentence
|
| 56 |
+
original_sentences.append(sentence.strip())
|
| 57 |
+
|
| 58 |
+
# Clean and process the sentence for analysis
|
| 59 |
+
clean_sentence = re.sub(r'^[^:]+:', '', sentence).strip()
|
| 60 |
+
clean_sentence = re.sub(r'[^\w\s]', '', clean_sentence.lower())
|
| 61 |
+
words = word_tokenize(clean_sentence)
|
| 62 |
+
lemmatized = [self.lemmatizer.lemmatize(word) for word in words]
|
| 63 |
+
processed_sentence = ' '.join(lemmatized)
|
| 64 |
+
|
| 65 |
+
all_sentences.append(processed_sentence)
|
| 66 |
+
timestamp_mapping.append(int(timestamp))
|
| 67 |
+
|
| 68 |
+
return all_sentences, original_sentences, timestamp_mapping
|
| 69 |
+
|
| 70 |
+
def get_topic_fingerprint(self, segment_text):
|
| 71 |
+
cleaned_text = ' '.join([re.sub(r'^[^:]+:', '', sent).strip() for sent in segment_text])
|
| 72 |
+
|
| 73 |
+
if self.fitted_vectorizer is None:
|
| 74 |
+
self.fitted_vectorizer = self.segment_vectorizer.fit([cleaned_text])
|
| 75 |
+
tfidf_matrix = self.fitted_vectorizer.transform([cleaned_text])
|
| 76 |
+
else:
|
| 77 |
+
tfidf_matrix = self.fitted_vectorizer.transform([cleaned_text])
|
| 78 |
+
|
| 79 |
+
return tfidf_matrix.toarray()[0]
|
| 80 |
+
|
| 81 |
+
def compare_with_previous_topics(self, current_segment):
|
| 82 |
+
if not self.topic_history:
|
| 83 |
+
return None, 0.0
|
| 84 |
+
|
| 85 |
+
current_fingerprint = self.get_topic_fingerprint(current_segment)
|
| 86 |
+
max_similarity = 0.0
|
| 87 |
+
best_match_idx = None
|
| 88 |
+
|
| 89 |
+
for idx, (topic_fingerprint, _, _, _, _) in enumerate(self.topic_history):
|
| 90 |
+
if len(topic_fingerprint) != len(current_fingerprint):
|
| 91 |
+
continue
|
| 92 |
+
|
| 93 |
+
similarity = cosine_similarity([topic_fingerprint], [current_fingerprint])[0][0]
|
| 94 |
+
if similarity > max_similarity:
|
| 95 |
+
max_similarity = similarity
|
| 96 |
+
best_match_idx = idx
|
| 97 |
+
|
| 98 |
+
return best_match_idx, max_similarity
|
| 99 |
+
|
| 100 |
+
def extract_keywords(self, sentences, top_n=3):
|
| 101 |
+
if isinstance(sentences, list):
|
| 102 |
+
text = ' '.join(sentences)
|
| 103 |
+
else:
|
| 104 |
+
text = sentences
|
| 105 |
+
|
| 106 |
+
keywords = self.keyword_extractor.extract_keywords(
|
| 107 |
+
text,
|
| 108 |
+
keyphrase_ngram_range=(1, 2), # Allow for 1-2 word keyphrases
|
| 109 |
+
stop_words='english',
|
| 110 |
+
top_n=top_n
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
sorted_keywords = sorted(keywords, key=lambda x: x[1], reverse=True)
|
| 114 |
+
return [kw[0] for kw in sorted_keywords[:top_n]]
|
| 115 |
+
|
| 116 |
+
def detect_speaker_changes(self, text):
|
| 117 |
+
"""
|
| 118 |
+
Detect points in the transcript where speakers change.
|
| 119 |
+
Returns a list of indices where speaker changes occur.
|
| 120 |
+
"""
|
| 121 |
+
speaker_pattern = re.compile(r'(?:^|\s)(?:I\'m|And I\'m|Hey I\'m|I am)\s+([A-Z][a-z]+)', re.MULTILINE)
|
| 122 |
+
matches = list(speaker_pattern.finditer(text))
|
| 123 |
+
|
| 124 |
+
speaker_boundaries = []
|
| 125 |
+
for match in matches:
|
| 126 |
+
# Find the sentence containing this speaker introduction
|
| 127 |
+
sentences = sent_tokenize(text[:match.end()])
|
| 128 |
+
if sentences:
|
| 129 |
+
speaker_boundaries.append(len(sentences) - 1)
|
| 130 |
+
|
| 131 |
+
return speaker_boundaries
|
| 132 |
+
|
| 133 |
+
def find_closest_timestamp(self, sentence, transcript):
|
| 134 |
+
"""
|
| 135 |
+
Find the timestamp after the most recent timestamp before the sentence.
|
| 136 |
+
"""
|
| 137 |
+
chunks = re.findall(r'\[(\d+)\](.*?)(?=\[\d+\]|$)', transcript, re.DOTALL)
|
| 138 |
+
sentence = sentence.strip().lower()
|
| 139 |
+
|
| 140 |
+
previous_timestamp = 0
|
| 141 |
+
current_timestamp = 0
|
| 142 |
+
|
| 143 |
+
for i, (timestamp, text) in enumerate(chunks):
|
| 144 |
+
current_timestamp = int(timestamp)
|
| 145 |
+
text = text.strip().lower()
|
| 146 |
+
|
| 147 |
+
if sentence in text or text in sentence:
|
| 148 |
+
if i > 0 and i < len(chunks):
|
| 149 |
+
return current_timestamp
|
| 150 |
+
return previous_timestamp
|
| 151 |
+
|
| 152 |
+
previous_timestamp = current_timestamp
|
| 153 |
+
|
| 154 |
+
return current_timestamp
|
| 155 |
+
|
| 156 |
+
def calculate_topic_importance(self, segment, total_segments):
|
| 157 |
+
"""
|
| 158 |
+
Calculate importance score for a topic based on:
|
| 159 |
+
1. Length of the segment
|
| 160 |
+
2. Position in the transcript
|
| 161 |
+
3. Keyword significance
|
| 162 |
+
"""
|
| 163 |
+
# Length factor - longer segments likely more important
|
| 164 |
+
length_score = min(1.0, len(segment) / 10) # Cap at 1.0 for segments of 10+ sentences
|
| 165 |
+
|
| 166 |
+
# Position factor - beginning and end often contain important information
|
| 167 |
+
position_in_doc = self.topic_history.index(segment) / total_segments
|
| 168 |
+
position_score = 1.0 - min(abs(position_in_doc - 0.0), abs(position_in_doc - 1.0))
|
| 169 |
+
|
| 170 |
+
# Keyword significance - look for key terms indicating importance
|
| 171 |
+
importance_indicators = ['important', 'key', 'main', 'primary', 'critical', 'essential',
|
| 172 |
+
'significant', 'revolutionary', 'breakthrough', 'innovative']
|
| 173 |
+
|
| 174 |
+
text = ' '.join(segment)
|
| 175 |
+
significance_score = 0.0
|
| 176 |
+
for indicator in importance_indicators:
|
| 177 |
+
if indicator in text.lower():
|
| 178 |
+
significance_score += 0.2 # Add 0.2 for each indicator found
|
| 179 |
+
significance_score = min(1.0, significance_score) # Cap at 1.0
|
| 180 |
+
|
| 181 |
+
# Combined score, weighted
|
| 182 |
+
combined_score = (0.4 * length_score) + (0.3 * position_score) + (0.3 * significance_score)
|
| 183 |
+
return combined_score
|
| 184 |
+
|
| 185 |
+
def cluster_topics(self, segments):
|
| 186 |
+
"""
|
| 187 |
+
Cluster similar topics together to form a hierarchy
|
| 188 |
+
"""
|
| 189 |
+
if len(segments) <= 1:
|
| 190 |
+
return segments, []
|
| 191 |
+
|
| 192 |
+
# Extract features for each segment
|
| 193 |
+
feature_vectors = []
|
| 194 |
+
for segment_idx, segment in enumerate(segments):
|
| 195 |
+
feature_vectors.append(self.get_topic_fingerprint(segment))
|
| 196 |
+
|
| 197 |
+
feature_matrix = np.array(feature_vectors)
|
| 198 |
+
|
| 199 |
+
# Use hierarchical clustering with compatible parameters
|
| 200 |
+
num_clusters = min(self.max_topics, len(segments))
|
| 201 |
+
|
| 202 |
+
# Try different parameter combinations based on scikit-learn version compatibility
|
| 203 |
+
try:
|
| 204 |
+
# First attempt with basic parameters
|
| 205 |
+
clustering = AgglomerativeClustering(
|
| 206 |
+
n_clusters=num_clusters
|
| 207 |
+
)
|
| 208 |
+
labels = clustering.fit_predict(feature_matrix)
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f"Clustering with default parameters failed: {str(e)}")
|
| 211 |
+
# Fallback to manual clustering based on similarity
|
| 212 |
+
labels = self.manual_clustering(feature_matrix, num_clusters)
|
| 213 |
+
|
| 214 |
+
# Group segments by cluster
|
| 215 |
+
clusters = defaultdict(list)
|
| 216 |
+
for segment_idx, cluster_id in enumerate(labels):
|
| 217 |
+
clusters[cluster_id].append(segment_idx)
|
| 218 |
+
|
| 219 |
+
# Create parent-child relationships
|
| 220 |
+
parent_child_map = {}
|
| 221 |
+
|
| 222 |
+
for cluster_id, segment_indices in clusters.items():
|
| 223 |
+
# If cluster has multiple segments, create a parent topic
|
| 224 |
+
if len(segment_indices) > 1:
|
| 225 |
+
# Combine segments to create a parent topic
|
| 226 |
+
all_content = []
|
| 227 |
+
for idx in segment_indices:
|
| 228 |
+
all_content.extend(segments[idx])
|
| 229 |
+
|
| 230 |
+
# Extract the most representative keywords
|
| 231 |
+
keywords = self.extract_keywords(all_content, top_n=3)
|
| 232 |
+
parent_topic = f"{', '.join(keywords)}"
|
| 233 |
+
|
| 234 |
+
# Add parent-child relationships
|
| 235 |
+
for idx in segment_indices:
|
| 236 |
+
parent_child_map[idx] = parent_topic
|
| 237 |
+
|
| 238 |
+
return labels, parent_child_map
|
| 239 |
+
|
| 240 |
+
def manual_clustering(self, feature_matrix, num_clusters):
|
| 241 |
+
"""
|
| 242 |
+
Implement a simple similarity-based clustering as fallback
|
| 243 |
+
"""
|
| 244 |
+
n_samples = feature_matrix.shape[0]
|
| 245 |
+
|
| 246 |
+
# If we have very few samples, each gets its own cluster
|
| 247 |
+
if n_samples <= num_clusters:
|
| 248 |
+
return np.arange(n_samples)
|
| 249 |
+
|
| 250 |
+
# Calculate similarity matrix
|
| 251 |
+
similarity = cosine_similarity(feature_matrix)
|
| 252 |
+
|
| 253 |
+
labels = np.arange(n_samples)
|
| 254 |
+
current_num_clusters = n_samples
|
| 255 |
+
|
| 256 |
+
while current_num_clusters > num_clusters:
|
| 257 |
+
max_similarity = -1
|
| 258 |
+
merge_i, merge_j = 0, 0
|
| 259 |
+
|
| 260 |
+
for i in range(n_samples):
|
| 261 |
+
for j in range(i+1, n_samples):
|
| 262 |
+
|
| 263 |
+
if labels[i] == labels[j]:
|
| 264 |
+
continue
|
| 265 |
+
|
| 266 |
+
if similarity[i, j] > max_similarity:
|
| 267 |
+
max_similarity = similarity[i, j]
|
| 268 |
+
merge_i, merge_j = i, j
|
| 269 |
+
|
| 270 |
+
old_label = labels[merge_j]
|
| 271 |
+
new_label = labels[merge_i]
|
| 272 |
+
|
| 273 |
+
for i in range(n_samples):
|
| 274 |
+
if labels[i] == old_label:
|
| 275 |
+
labels[i] = new_label
|
| 276 |
+
|
| 277 |
+
unique_labels = np.unique(labels)
|
| 278 |
+
mapping = {old: new for new, old in enumerate(unique_labels)}
|
| 279 |
+
labels = np.array([mapping[label] for label in labels])
|
| 280 |
+
|
| 281 |
+
current_num_clusters -= 1
|
| 282 |
+
|
| 283 |
+
return labels
|
| 284 |
+
|
| 285 |
+
def segment_transcript(self, text):
|
| 286 |
+
cleaned_sentences, original_sentences, timestamp_mapping = self.preprocess_text(text)
|
| 287 |
+
|
| 288 |
+
if len(cleaned_sentences) == 0:
|
| 289 |
+
return [], [], [], {}
|
| 290 |
+
|
| 291 |
+
similarity_matrix = self.calculate_similarity_matrix(cleaned_sentences)
|
| 292 |
+
|
| 293 |
+
content_boundaries = self.detect_topic_boundaries(similarity_matrix)
|
| 294 |
+
|
| 295 |
+
speaker_boundaries = self.detect_speaker_changes(text)
|
| 296 |
+
|
| 297 |
+
timestamp_boundaries = []
|
| 298 |
+
for i in range(1, len(timestamp_mapping)):
|
| 299 |
+
if timestamp_mapping[i] - timestamp_mapping[i-1] > 5: # If there's a gap of more than 5 seconds
|
| 300 |
+
timestamp_boundaries.append(i)
|
| 301 |
+
|
| 302 |
+
all_boundaries = sorted(set(content_boundaries + speaker_boundaries + timestamp_boundaries))
|
| 303 |
+
|
| 304 |
+
initial_segments = []
|
| 305 |
+
initial_segment_texts = []
|
| 306 |
+
current_topic_id = 0
|
| 307 |
+
self.topic_history = [] # Reset topic history
|
| 308 |
+
|
| 309 |
+
start_idx = 0
|
| 310 |
+
for boundary in all_boundaries + [len(original_sentences)]:
|
| 311 |
+
if boundary - start_idx < self.min_segment_size:
|
| 312 |
+
continue
|
| 313 |
+
|
| 314 |
+
current_segment = original_sentences[start_idx:boundary]
|
| 315 |
+
segment_timestamps = timestamp_mapping[start_idx:boundary]
|
| 316 |
+
|
| 317 |
+
closest_timestamp = segment_timestamps[0] if segment_timestamps else 0
|
| 318 |
+
|
| 319 |
+
# Extract keywords directly from content without predefined categories
|
| 320 |
+
keywords = self.extract_keywords(current_segment, top_n=4)
|
| 321 |
+
|
| 322 |
+
# Create topic name directly from the top keywords - completely dynamic
|
| 323 |
+
topic_name = f"{', '.join(keywords[:2])}"
|
| 324 |
+
|
| 325 |
+
# Calculate importance score
|
| 326 |
+
topic_importance = len(current_segment) / 3 # Simple approach: longer segments are more important
|
| 327 |
+
|
| 328 |
+
# Create a topic fingerprint
|
| 329 |
+
topic_fingerprint = self.get_topic_fingerprint(current_segment)
|
| 330 |
+
|
| 331 |
+
# Store topic information
|
| 332 |
+
self.topic_history.append((topic_fingerprint, topic_name, [current_segment], closest_timestamp, topic_importance))
|
| 333 |
+
initial_segments.append(current_segment)
|
| 334 |
+
initial_segment_texts.append(topic_name)
|
| 335 |
+
|
| 336 |
+
start_idx = boundary
|
| 337 |
+
|
| 338 |
+
# Cluster segments to form hierarchical topics
|
| 339 |
+
if len(initial_segments) > 1:
|
| 340 |
+
cluster_labels, parent_child_map = self.cluster_topics(initial_segments)
|
| 341 |
+
else:
|
| 342 |
+
cluster_labels = [0] if initial_segments else []
|
| 343 |
+
parent_child_map = {}
|
| 344 |
+
|
| 345 |
+
# Organize segments into hierarchical structure
|
| 346 |
+
final_segments = []
|
| 347 |
+
topic_mappings = []
|
| 348 |
+
topic_hierarchies = {}
|
| 349 |
+
|
| 350 |
+
# Group by parent topics
|
| 351 |
+
parent_topic_groups = defaultdict(list)
|
| 352 |
+
|
| 353 |
+
for i, segment in enumerate(initial_segments):
|
| 354 |
+
topic_id = i
|
| 355 |
+
|
| 356 |
+
# Check if this segment belongs to a parent topic
|
| 357 |
+
if i in parent_child_map:
|
| 358 |
+
parent_topic = parent_child_map[i]
|
| 359 |
+
parent_id = len(self.topic_history)
|
| 360 |
+
|
| 361 |
+
# If this is the first time seeing this parent topic
|
| 362 |
+
if parent_topic not in parent_topic_groups:
|
| 363 |
+
# Add the parent topic to our history
|
| 364 |
+
all_content = []
|
| 365 |
+
for j, seg in enumerate(initial_segments):
|
| 366 |
+
if j in parent_child_map and parent_child_map[j] == parent_topic:
|
| 367 |
+
all_content.extend(seg)
|
| 368 |
+
|
| 369 |
+
parent_fingerprint = self.get_topic_fingerprint(all_content)
|
| 370 |
+
parent_timestamp = self.topic_history[i][3] # Use timestamp of first segment
|
| 371 |
+
parent_importance = 1.0 # Parent topics are most important
|
| 372 |
+
|
| 373 |
+
self.topic_history.append((parent_fingerprint, parent_topic, [all_content], parent_timestamp, parent_importance))
|
| 374 |
+
topic_hierarchies[parent_id] = [j for j, seg in enumerate(initial_segments)
|
| 375 |
+
if j in parent_child_map and parent_child_map[j] == parent_topic]
|
| 376 |
+
|
| 377 |
+
# Find the parent ID
|
| 378 |
+
for j, (_, name, _, _, _) in enumerate(self.topic_history):
|
| 379 |
+
if name == parent_topic:
|
| 380 |
+
parent_id = j
|
| 381 |
+
break
|
| 382 |
+
|
| 383 |
+
# Add to parent topic group
|
| 384 |
+
parent_topic_groups[parent_topic].append((segment, topic_id))
|
| 385 |
+
else:
|
| 386 |
+
# This is already a main topic
|
| 387 |
+
final_segments.append(segment)
|
| 388 |
+
topic_mappings.append(topic_id)
|
| 389 |
+
|
| 390 |
+
# Add grouped segments under their parent topics
|
| 391 |
+
for parent_topic, segments_and_ids in parent_topic_groups.items():
|
| 392 |
+
# Find the parent ID
|
| 393 |
+
parent_id = None
|
| 394 |
+
for i, (_, name, _, _, _) in enumerate(self.topic_history):
|
| 395 |
+
if name == parent_topic:
|
| 396 |
+
parent_id = i
|
| 397 |
+
break
|
| 398 |
+
|
| 399 |
+
if parent_id is not None:
|
| 400 |
+
# Add parent as a segment
|
| 401 |
+
parent_content = []
|
| 402 |
+
for segment, _ in segments_and_ids:
|
| 403 |
+
parent_content.extend(segment)
|
| 404 |
+
|
| 405 |
+
final_segments.append(parent_content)
|
| 406 |
+
topic_mappings.append(parent_id)
|
| 407 |
+
|
| 408 |
+
# Add children segments
|
| 409 |
+
for segment, child_id in segments_and_ids:
|
| 410 |
+
final_segments.append(segment)
|
| 411 |
+
topic_mappings.append(child_id)
|
| 412 |
+
|
| 413 |
+
# For topics that don't have many segments, merge them if they're similar
|
| 414 |
+
if len(final_segments) > self.max_topics * 2:
|
| 415 |
+
merged_segments = []
|
| 416 |
+
merged_mappings = []
|
| 417 |
+
i = 0
|
| 418 |
+
|
| 419 |
+
while i < len(final_segments):
|
| 420 |
+
current_segment = final_segments[i]
|
| 421 |
+
current_mapping = topic_mappings[i]
|
| 422 |
+
|
| 423 |
+
# If this is a short segment, try to merge with next
|
| 424 |
+
if len(current_segment) <= 2 and i + 1 < len(final_segments):
|
| 425 |
+
next_segment = final_segments[i+1]
|
| 426 |
+
combined = current_segment + next_segment
|
| 427 |
+
|
| 428 |
+
# Use keywords to see if they're related
|
| 429 |
+
current_keywords = self.extract_keywords(current_segment)
|
| 430 |
+
next_keywords = self.extract_keywords(next_segment)
|
| 431 |
+
|
| 432 |
+
# Check for keyword overlap
|
| 433 |
+
overlap = any(kw in next_keywords for kw in current_keywords)
|
| 434 |
+
|
| 435 |
+
if overlap:
|
| 436 |
+
# Merge the segments
|
| 437 |
+
merged_segments.append(combined)
|
| 438 |
+
merged_mappings.append(current_mapping) # Keep the first mapping
|
| 439 |
+
i += 2 # Skip both segments
|
| 440 |
+
continue
|
| 441 |
+
|
| 442 |
+
# If no merge, keep as is
|
| 443 |
+
merged_segments.append(current_segment)
|
| 444 |
+
merged_mappings.append(current_mapping)
|
| 445 |
+
i += 1
|
| 446 |
+
|
| 447 |
+
final_segments = merged_segments
|
| 448 |
+
topic_mappings = merged_mappings
|
| 449 |
+
|
| 450 |
+
return final_segments, topic_mappings, self.topic_history, topic_hierarchies
|
| 451 |
+
|
| 452 |
+
def calculate_similarity_matrix(self, sentences):
|
| 453 |
+
if not sentences:
|
| 454 |
+
return np.array([[]])
|
| 455 |
+
tfidf_matrix = self.vectorizer.fit_transform(sentences)
|
| 456 |
+
return cosine_similarity(tfidf_matrix)
|
| 457 |
+
|
| 458 |
+
def detect_topic_boundaries(self, similarity_matrix):
|
| 459 |
+
boundaries = []
|
| 460 |
+
n_sentences = len(similarity_matrix)
|
| 461 |
+
|
| 462 |
+
if n_sentences <= 2 * self.window_size:
|
| 463 |
+
return []
|
| 464 |
+
|
| 465 |
+
for i in range(self.window_size, n_sentences - self.window_size):
|
| 466 |
+
prev_window = similarity_matrix[i-self.window_size:i, i-self.window_size:i]
|
| 467 |
+
prev_similarity = np.mean(prev_window)
|
| 468 |
+
|
| 469 |
+
next_window = similarity_matrix[i:i+self.window_size, i:i+self.window_size]
|
| 470 |
+
next_similarity = np.mean(next_window)
|
| 471 |
+
|
| 472 |
+
cross_window = similarity_matrix[i-self.window_size:i, i:i+self.window_size]
|
| 473 |
+
cross_similarity = np.mean(cross_window)
|
| 474 |
+
|
| 475 |
+
# Stricter conditions for detecting boundaries
|
| 476 |
+
if (cross_similarity < self.similarity_threshold and
|
| 477 |
+
cross_similarity < prev_similarity * 0.7 and # Increased constraint
|
| 478 |
+
cross_similarity < next_similarity * 0.7 and # Increased constraint
|
| 479 |
+
(len(boundaries) == 0 or i - boundaries[-1] >= self.min_segment_size)):
|
| 480 |
+
boundaries.append(i)
|
| 481 |
+
|
| 482 |
+
return boundaries
|
| 483 |
+
|
| 484 |
+
if __name__ == "__main__":
|
| 485 |
+
transcript = """
|
| 486 |
+
[0] Hey I'm Stanley, I'm a Stanford CS major. I did fun at Engineering at Facebook. [4] Hey I'm Andy, I'm also a Stanford CS major and I did platform engineering at Facebook. [8] I'm Evan, I was on the founding team of Vivo, the music video service. [12] And I'm Tony, I was a product editor at Square. [15] And the four of us came together about six months ago to work on software for small business owners. [20] But we didn't have a need at first so we just went out and talked to all the small business owners we could find. [24] [6] After over a hundred interviews, we came across a really interesting problem with small business restaurants in an area like this. [30] Yeah, so it turns out restaurants in Palo Alto don't deliver even though they really [34] want to, but they can't afford it. [36] But their consumers are craving for it, but the places that the consumers love just can't [41] deliver. [42] And we also found out about these delivery drivers who had a ton of spare time and they [45] all wanted an extra cash during that downtime. [48] Right, so that's when we built an initial product, PaloAltodelivery.com, and how it [53] worked is the customer goes to the website and places it all in order that gets automatically [59] [32] since the restaurant [60] And then we as the dispatchers with some pretty neat routing and badging algorithms were able to send the drivers efficiently to [66] Get the orders to the customers at a really fast time. Yeah, and the four of us actually started off as delivery drivers and [74] Over time we hired more as we grew and in our first month of launch with not much marketing [80] We got over 150 paying customers in the Palo Alto area, which was really awesome [84] And from that we generated over $10,000 in sales
|
| 487 |
+
"""
|
| 488 |
+
|
| 489 |
+
segmenter = TopicSegmenter(
|
| 490 |
+
window_size=2, # Smaller window for more granular segmentation
|
| 491 |
+
similarity_threshold=0.15, # Lower threshold to detect more subtle changes
|
| 492 |
+
context_size=1,
|
| 493 |
+
min_segment_size=2, # Smaller minimum size for segments
|
| 494 |
+
topic_similarity_threshold=0.25, # Lower threshold to better distinguish topics
|
| 495 |
+
max_topics=5, # Target number of main topics
|
| 496 |
+
hierarchical_threshold=0.6 # Threshold for hierarchical clustering
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
segments, topic_mappings, topic_history, topic_hierarchies = segmenter.segment_transcript(transcript)
|
| 500 |
+
|
| 501 |
+
print("Topic Segmentation Analysis:\n")
|
| 502 |
+
print(f"Found {len(set(topic_mappings))} distinct topics")
|
| 503 |
+
|
| 504 |
+
# Print hierarchical topic structure
|
| 505 |
+
print("\nTopic Hierarchy:")
|
| 506 |
+
for parent_id, child_ids in topic_hierarchies.items():
|
| 507 |
+
parent_name = topic_history[parent_id][1]
|
| 508 |
+
print(f"Main Topic: {parent_name}")
|
| 509 |
+
for child_id in child_ids:
|
| 510 |
+
child_name = topic_history[child_id][1]
|
| 511 |
+
print(f" - Subtopic: {child_name}")
|
| 512 |
+
|
| 513 |
+
print("\nDetailed Segments:")
|
| 514 |
+
for i, (segment, topic_id) in enumerate(zip(segments, topic_mappings)):
|
| 515 |
+
print(f"Segment {i+1} (Part of {topic_history[topic_id][1]}):")
|
| 516 |
+
print(f"Closest Timestamp: {topic_history[topic_id][3]} seconds")
|
| 517 |
+
print("-" * 50)
|
| 518 |
+
print("\n".join(segment[:2]) + "..." if len(segment) > 2 else "\n".join(segment))
|
| 519 |
+
print("-" * 50 + "\n")
|