Commit ·
7632cf2
1
Parent(s): 7b1d1ea
wow
Browse files- .gitattributes +35 -0
- Dockerfile +86 -0
- README.md +8 -0
- main.go +60 -0
- requirements.txt +23 -0
- src/app.py +808 -0
- src/factuality_logic.py +143 -0
- src/inference_logic.py +305 -0
- src/labeling_logic.py +145 -0
- src/my_vision_process.py +17 -0
- src/toon_parser.py +220 -0
- src/transcription.py +53 -0
- start.sh +23 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# Stage 1: Build Frontend (React/TS/Vite)
|
| 3 |
+
# ==========================================
|
| 4 |
+
FROM node:20-slim AS frontend-builder
|
| 5 |
+
WORKDIR /app/frontend
|
| 6 |
+
|
| 7 |
+
# Copy frontend definitions
|
| 8 |
+
COPY frontend/package.json frontend/package-lock.json* ./
|
| 9 |
+
RUN npm install
|
| 10 |
+
|
| 11 |
+
# Copy source and build
|
| 12 |
+
COPY frontend/ ./
|
| 13 |
+
RUN npm run build
|
| 14 |
+
|
| 15 |
+
# ==========================================
|
| 16 |
+
# Stage 2: Build Backend (Golang)
|
| 17 |
+
# ==========================================
|
| 18 |
+
FROM golang:1.23 AS backend-builder
|
| 19 |
+
WORKDIR /app/backend
|
| 20 |
+
|
| 21 |
+
# Copy Go source
|
| 22 |
+
COPY main.go .
|
| 23 |
+
|
| 24 |
+
# Build static binary
|
| 25 |
+
RUN go mod init vchat-server && \
|
| 26 |
+
go mod tidy && \
|
| 27 |
+
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o vchat-server main.go
|
| 28 |
+
|
| 29 |
+
# ==========================================
|
| 30 |
+
# Stage 3: Final Runtime (Hugging Face Space - API Lite)
|
| 31 |
+
# ==========================================
|
| 32 |
+
FROM python:3.11-slim
|
| 33 |
+
|
| 34 |
+
# Default to LITE_MODE=true for HF Spaces (API Only)
|
| 35 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 36 |
+
DEBIAN_FRONTEND=noninteractive \
|
| 37 |
+
LITE_MODE=true \
|
| 38 |
+
PATH="/home/user/.local/bin:$PATH" \
|
| 39 |
+
PIP_NO_CACHE_DIR=1
|
| 40 |
+
|
| 41 |
+
# Create a non-root user (Required for HF Spaces)
|
| 42 |
+
RUN useradd -m -u 1000 user
|
| 43 |
+
|
| 44 |
+
WORKDIR /app
|
| 45 |
+
|
| 46 |
+
# 1. Install System Dependencies (FFmpeg required for yt-dlp)
|
| 47 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 48 |
+
ffmpeg \
|
| 49 |
+
git \
|
| 50 |
+
curl \
|
| 51 |
+
gnupg \
|
| 52 |
+
ca-certificates \
|
| 53 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 54 |
+
|
| 55 |
+
# 2. Install Python Dependencies
|
| 56 |
+
RUN pip install uv
|
| 57 |
+
COPY requirements.txt ./
|
| 58 |
+
RUN uv pip install --system -r requirements.txt
|
| 59 |
+
# Explicitly force latest yt-dlp to handle Twitter/X API changes
|
| 60 |
+
RUN uv pip install --system --upgrade "yt-dlp[default]"
|
| 61 |
+
|
| 62 |
+
# 3. Copy Python Application Code
|
| 63 |
+
COPY --chown=user src/ ./src/
|
| 64 |
+
|
| 65 |
+
# 4. Install Built Artifacts
|
| 66 |
+
COPY --from=backend-builder --chown=user /app/backend/vchat-server /app/vchat-server
|
| 67 |
+
RUN mkdir -p /app/static
|
| 68 |
+
COPY --from=frontend-builder --chown=user /app/frontend/dist /app/static
|
| 69 |
+
|
| 70 |
+
# 5. Setup Directories and Permissions
|
| 71 |
+
RUN mkdir -p /app/data /app/data/videos /app/data/labels /app/data/prompts /app/data/responses /app/metadata \
|
| 72 |
+
&& chown -R user:user /app/data /app/metadata
|
| 73 |
+
|
| 74 |
+
# 6. Setup Entrypoint
|
| 75 |
+
COPY --chown=user start.sh /app/start.sh
|
| 76 |
+
RUN sed -i 's/\r$//' /app/start.sh && \
|
| 77 |
+
chmod +x /app/start.sh
|
| 78 |
+
|
| 79 |
+
# Switch to non-root user
|
| 80 |
+
USER user
|
| 81 |
+
|
| 82 |
+
# Expose the HF Space port
|
| 83 |
+
EXPOSE 7860
|
| 84 |
+
|
| 85 |
+
# Run the Orchestrator
|
| 86 |
+
CMD ["/app/start.sh"]
|
README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: VFacts
|
| 3 |
+
emoji: 😻
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
main.go
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"net/http"
|
| 6 |
+
"net/http/httputil"
|
| 7 |
+
"net/url"
|
| 8 |
+
"os"
|
| 9 |
+
"strings"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func main() {
|
| 13 |
+
// Target Python FastAPI server (running locally in the container)
|
| 14 |
+
pythonTarget := "http://127.0.0.1:8001"
|
| 15 |
+
pythonURL, err := url.Parse(pythonTarget)
|
| 16 |
+
if err != nil {
|
| 17 |
+
log.Fatalf("Invalid Python target URL: %v", err)
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Create Reverse Proxy
|
| 21 |
+
proxy := httputil.NewSingleHostReverseProxy(pythonURL)
|
| 22 |
+
|
| 23 |
+
// HF Spaces: Files are copied to /app/static in Dockerfile
|
| 24 |
+
staticPath := "/app/static"
|
| 25 |
+
fs := http.FileServer(http.Dir(staticPath))
|
| 26 |
+
|
| 27 |
+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
| 28 |
+
// Proxy API requests to Python
|
| 29 |
+
if strings.HasPrefix(r.URL.Path, "/process") ||
|
| 30 |
+
strings.HasPrefix(r.URL.Path, "/label_video") ||
|
| 31 |
+
strings.HasPrefix(r.URL.Path, "/batch_label") ||
|
| 32 |
+
strings.HasPrefix(r.URL.Path, "/model-architecture") ||
|
| 33 |
+
strings.HasPrefix(r.URL.Path, "/download-dataset") ||
|
| 34 |
+
strings.HasPrefix(r.URL.Path, "/extension") ||
|
| 35 |
+
strings.HasPrefix(r.URL.Path, "/manage") ||
|
| 36 |
+
strings.HasPrefix(r.URL.Path, "/queue") {
|
| 37 |
+
|
| 38 |
+
log.Printf("Proxying %s to Python Backend...", r.URL.Path)
|
| 39 |
+
proxy.ServeHTTP(w, r)
|
| 40 |
+
return
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Check if file exists in static dir, otherwise serve index.html (SPA Routing)
|
| 44 |
+
path := staticPath + r.URL.Path
|
| 45 |
+
if _, err := os.Stat(path); os.IsNotExist(err) {
|
| 46 |
+
http.ServeFile(w, r, staticPath+"/index.html")
|
| 47 |
+
return
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
fs.ServeHTTP(w, r)
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
// HF Spaces requires listening on port 7860
|
| 54 |
+
port := "7860"
|
| 55 |
+
log.Printf("vChat HF Server listening on port %s", port)
|
| 56 |
+
log.Printf("Serving static files from %s", staticPath)
|
| 57 |
+
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
| 58 |
+
log.Fatal(err)
|
| 59 |
+
}
|
| 60 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- Core Server ---
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn[standard]
|
| 4 |
+
python-multipart
|
| 5 |
+
requests
|
| 6 |
+
aiofiles
|
| 7 |
+
jinja2
|
| 8 |
+
python-dotenv
|
| 9 |
+
|
| 10 |
+
# --- Data & Vision Utils ---
|
| 11 |
+
Pillow
|
| 12 |
+
packaging
|
| 13 |
+
numpy
|
| 14 |
+
|
| 15 |
+
# --- Google Cloud & APIs ---
|
| 16 |
+
google-generativeai>=0.4.0
|
| 17 |
+
google-cloud-aiplatform
|
| 18 |
+
google-genai
|
| 19 |
+
mlcroissant
|
| 20 |
+
|
| 21 |
+
# --- Audio & Video Fetching ---
|
| 22 |
+
yt-dlp
|
| 23 |
+
ffmpeg-python
|
src/app.py
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import asyncio
|
| 4 |
+
import subprocess
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import logging
|
| 7 |
+
import csv
|
| 8 |
+
import io
|
| 9 |
+
import datetime
|
| 10 |
+
import json
|
| 11 |
+
import hashlib
|
| 12 |
+
import re
|
| 13 |
+
import glob
|
| 14 |
+
import shutil
|
| 15 |
+
import time
|
| 16 |
+
from fastapi import FastAPI, Request, Form, UploadFile, File, Body, HTTPException
|
| 17 |
+
from fastapi.responses import HTMLResponse, StreamingResponse, PlainTextResponse, Response, FileResponse, JSONResponse
|
| 18 |
+
from fastapi.templating import Jinja2Templates
|
| 19 |
+
from fastapi.staticfiles import StaticFiles
|
| 20 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 21 |
+
import yt_dlp
|
| 22 |
+
import inference_logic
|
| 23 |
+
import factuality_logic
|
| 24 |
+
import transcription
|
| 25 |
+
from factuality_logic import parse_vtt
|
| 26 |
+
from toon_parser import parse_veracity_toon
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
import mlcroissant as mlc
|
| 30 |
+
CROISSANT_AVAILABLE = True
|
| 31 |
+
except ImportError:
|
| 32 |
+
try:
|
| 33 |
+
import croissant as mlc
|
| 34 |
+
CROISSANT_AVAILABLE = True
|
| 35 |
+
except ImportError:
|
| 36 |
+
mlc = None
|
| 37 |
+
CROISSANT_AVAILABLE = False
|
| 38 |
+
|
| 39 |
+
# Configure Logging with High Verbosity
|
| 40 |
+
logging.basicConfig(
|
| 41 |
+
level=logging.INFO,
|
| 42 |
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
| 43 |
+
handlers=[logging.StreamHandler(sys.stdout)]
|
| 44 |
+
)
|
| 45 |
+
logger = logging.getLogger("vChat")
|
| 46 |
+
|
| 47 |
+
LITE_MODE = os.getenv("LITE_MODE", "false").lower() == "true"
|
| 48 |
+
|
| 49 |
+
app = FastAPI()
|
| 50 |
+
|
| 51 |
+
app.add_middleware(
|
| 52 |
+
CORSMiddleware,
|
| 53 |
+
allow_origins=["*"],
|
| 54 |
+
allow_credentials=True,
|
| 55 |
+
allow_methods=["*"],
|
| 56 |
+
allow_headers=["*"],
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# HF Spaces specific path
|
| 60 |
+
STATIC_DIR = "/app/static"
|
| 61 |
+
if not os.path.isdir(STATIC_DIR):
|
| 62 |
+
# Fallback if running locally
|
| 63 |
+
STATIC_DIR = "static"
|
| 64 |
+
os.makedirs(STATIC_DIR, exist_ok=True)
|
| 65 |
+
|
| 66 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 67 |
+
templates = Jinja2Templates(directory=STATIC_DIR)
|
| 68 |
+
|
| 69 |
+
# Ensure data directories exist (HF Spaces writable locations)
|
| 70 |
+
os.makedirs("data/videos", exist_ok=True)
|
| 71 |
+
os.makedirs("data", exist_ok=True)
|
| 72 |
+
os.makedirs("data/labels", exist_ok=True)
|
| 73 |
+
os.makedirs("data/prompts", exist_ok=True)
|
| 74 |
+
os.makedirs("data/responses", exist_ok=True)
|
| 75 |
+
os.makedirs("metadata", exist_ok=True)
|
| 76 |
+
|
| 77 |
+
STOP_QUEUE_SIGNAL = False
|
| 78 |
+
|
| 79 |
+
@app.on_event("startup")
|
| 80 |
+
async def startup_event():
|
| 81 |
+
logger.info("Application starting up...")
|
| 82 |
+
try:
|
| 83 |
+
transcription.load_model()
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logger.warning(f"Could not load Whisper model: {e}")
|
| 86 |
+
|
| 87 |
+
if not LITE_MODE:
|
| 88 |
+
try:
|
| 89 |
+
inference_logic.load_models()
|
| 90 |
+
except Exception as e:
|
| 91 |
+
logger.fatal(f"Could not load local inference models. Error: {e}", exc_info=True)
|
| 92 |
+
else:
|
| 93 |
+
logger.info("Running in LITE mode (API Only).")
|
| 94 |
+
|
| 95 |
+
@app.get("/", response_class=HTMLResponse)
|
| 96 |
+
async def read_root(request: Request):
|
| 97 |
+
custom_model_available = False
|
| 98 |
+
if not LITE_MODE:
|
| 99 |
+
custom_model_available = inference_logic.peft_model is not None
|
| 100 |
+
if not (Path(STATIC_DIR) / "index.html").exists():
|
| 101 |
+
return HTMLResponse(content="Frontend not found.", status_code=404)
|
| 102 |
+
return templates.TemplateResponse("index.html", {
|
| 103 |
+
"request": request,
|
| 104 |
+
"custom_model_available": custom_model_available,
|
| 105 |
+
"lite_mode": LITE_MODE
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
@app.get("/model-architecture", response_class=PlainTextResponse)
|
| 109 |
+
async def get_model_architecture():
|
| 110 |
+
if LITE_MODE: return "Running in LITE mode."
|
| 111 |
+
if inference_logic.base_model: return str(inference_logic.base_model)
|
| 112 |
+
return "Model not loaded."
|
| 113 |
+
|
| 114 |
+
@app.get("/download-dataset")
|
| 115 |
+
async def download_dataset():
|
| 116 |
+
file_path = Path("data/dataset.csv")
|
| 117 |
+
if file_path.exists():
|
| 118 |
+
return FileResponse(path=file_path, filename="dataset.csv", media_type='text/csv')
|
| 119 |
+
return Response("Dataset not found.", status_code=404)
|
| 120 |
+
|
| 121 |
+
progress_message = ""
|
| 122 |
+
def progress_hook(d):
|
| 123 |
+
global progress_message
|
| 124 |
+
if d['status'] == 'downloading':
|
| 125 |
+
progress_message = f"Downloading: {d.get('_percent_str', 'N/A')} at {d.get('_speed_str', 'N/A')}\r"
|
| 126 |
+
elif d['status'] == 'finished':
|
| 127 |
+
progress_message = f"\nDownload finished. Preparing video assets...\n"
|
| 128 |
+
|
| 129 |
+
def get_cookies_path():
|
| 130 |
+
"""Look for cookies file in known locations for better yt-dlp support."""
|
| 131 |
+
candidates = ["cookies.txt", "data/cookies.txt", "/app/cookies.txt"]
|
| 132 |
+
for c in candidates:
|
| 133 |
+
if os.path.exists(c):
|
| 134 |
+
return os.path.abspath(c)
|
| 135 |
+
return None
|
| 136 |
+
|
| 137 |
+
async def run_subprocess_async(command: list[str]):
|
| 138 |
+
cmd_str = ' '.join(command)
|
| 139 |
+
logger.info(f"[Subprocess] Running: {cmd_str}")
|
| 140 |
+
process = await asyncio.create_subprocess_exec(*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 141 |
+
stdout, stderr = await process.communicate()
|
| 142 |
+
|
| 143 |
+
if process.returncode != 0:
|
| 144 |
+
err_msg = stderr.decode()
|
| 145 |
+
logger.error(f"[Subprocess] Failed ({process.returncode}): {err_msg}")
|
| 146 |
+
raise RuntimeError(f"Command failed: {err_msg}")
|
| 147 |
+
logger.info(f"[Subprocess] Success.")
|
| 148 |
+
return stdout.decode()
|
| 149 |
+
|
| 150 |
+
def extract_tweet_id(url: str) -> str | None:
|
| 151 |
+
match = re.search(r"(?:twitter|x)\.com/[^/]+/status/(\d+)", url)
|
| 152 |
+
if match: return match.group(1)
|
| 153 |
+
return None
|
| 154 |
+
|
| 155 |
+
def check_if_processed(link: str) -> bool:
|
| 156 |
+
target_id = extract_tweet_id(link)
|
| 157 |
+
link_clean = link.split('?')[0].strip().rstrip('/')
|
| 158 |
+
|
| 159 |
+
for filename in ["data/dataset.csv", "data/manual_dataset.csv"]:
|
| 160 |
+
path = Path(filename)
|
| 161 |
+
if not path.exists(): continue
|
| 162 |
+
try:
|
| 163 |
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 164 |
+
sample = f.read(2048)
|
| 165 |
+
f.seek(0)
|
| 166 |
+
try: has_header = csv.Sniffer().has_header(sample)
|
| 167 |
+
except: has_header = True
|
| 168 |
+
|
| 169 |
+
if has_header:
|
| 170 |
+
reader = csv.DictReader(f)
|
| 171 |
+
for row in reader:
|
| 172 |
+
row_link = row.get('link', '').split('?')[0].strip().rstrip('/')
|
| 173 |
+
if row_link == link_clean: return True
|
| 174 |
+
row_id = row.get('id', '')
|
| 175 |
+
if target_id and row_id == target_id: return True
|
| 176 |
+
else:
|
| 177 |
+
reader = csv.reader(f)
|
| 178 |
+
for row in reader:
|
| 179 |
+
if not row: continue
|
| 180 |
+
if link_clean in row: return True
|
| 181 |
+
if target_id and target_id in row: return True
|
| 182 |
+
except Exception:
|
| 183 |
+
continue
|
| 184 |
+
return False
|
| 185 |
+
|
| 186 |
+
async def prepare_video_assets_async(url: str) -> dict:
|
| 187 |
+
global progress_message
|
| 188 |
+
loop = asyncio.get_event_loop()
|
| 189 |
+
is_local = not (url.startswith("http://") or url.startswith("https://"))
|
| 190 |
+
video_id = "unknown"
|
| 191 |
+
transcript_path = None
|
| 192 |
+
|
| 193 |
+
logger.info(f"Preparing assets for URL: {url}")
|
| 194 |
+
|
| 195 |
+
if is_local:
|
| 196 |
+
original_path = Path(url)
|
| 197 |
+
if not original_path.exists(): raise FileNotFoundError(f"File not found: {url}")
|
| 198 |
+
video_id = hashlib.md5(str(url).encode('utf-8')).hexdigest()[:16]
|
| 199 |
+
metadata = {"id": video_id, "link": url, "caption": original_path.stem}
|
| 200 |
+
else:
|
| 201 |
+
tweet_id = extract_tweet_id(url)
|
| 202 |
+
video_id = tweet_id if tweet_id else hashlib.md5(url.encode('utf-8')).hexdigest()[:16]
|
| 203 |
+
sanitized_check = Path(f"data/videos/{video_id}_fixed.mp4")
|
| 204 |
+
|
| 205 |
+
cookies_path = get_cookies_path()
|
| 206 |
+
ydl_opts = {
|
| 207 |
+
'format': 'best[ext=mp4]/best',
|
| 208 |
+
'outtmpl': 'data/videos/%(id)s.%(ext)s',
|
| 209 |
+
'progress_hooks': [progress_hook],
|
| 210 |
+
'quiet': False,
|
| 211 |
+
'no_warnings': False,
|
| 212 |
+
'noplaylist': True,
|
| 213 |
+
'no_overwrites': True,
|
| 214 |
+
'writesubtitles': True,
|
| 215 |
+
'writeautomaticsub': True,
|
| 216 |
+
'subtitleslangs': ['en'],
|
| 217 |
+
'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
if cookies_path:
|
| 221 |
+
ydl_opts['cookiefile'] = cookies_path
|
| 222 |
+
logger.info(f"Using cookies from {cookies_path}")
|
| 223 |
+
|
| 224 |
+
if sanitized_check.exists():
|
| 225 |
+
logger.info(f"Video {video_id} already cached at {sanitized_check}")
|
| 226 |
+
original_path = sanitized_check
|
| 227 |
+
metadata = {"id": video_id, "link": url, "caption": "Cached Video"}
|
| 228 |
+
else:
|
| 229 |
+
try:
|
| 230 |
+
logger.info(f"Starting yt-dlp download for {video_id}...")
|
| 231 |
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
| 232 |
+
info = await loop.run_in_executor(None, lambda: ydl.extract_info(url, download=True))
|
| 233 |
+
original_path = Path(ydl.prepare_filename(info))
|
| 234 |
+
metadata = {
|
| 235 |
+
"id": info.get("id", video_id), "link": info.get("webpage_url", url),
|
| 236 |
+
"caption": info.get("description", info.get("title", "N/A")).encode('ascii', 'ignore').decode('ascii').strip()[:500],
|
| 237 |
+
"postdatetime": info.get("upload_date", "N/A")
|
| 238 |
+
}
|
| 239 |
+
video_id = info.get("id", video_id)
|
| 240 |
+
logger.info("yt-dlp download successful.")
|
| 241 |
+
except yt_dlp.utils.DownloadError as e:
|
| 242 |
+
logger.error(f"yt-dlp download error: {e}")
|
| 243 |
+
if "No video could be found" in str(e):
|
| 244 |
+
raise ValueError(f"No video content found at {url}")
|
| 245 |
+
raise RuntimeError(f"Download failed: {str(e)}")
|
| 246 |
+
except Exception as e:
|
| 247 |
+
logger.error(f"Unexpected yt-dlp error: {e}")
|
| 248 |
+
raise RuntimeError(f"Download failed: {str(e)}")
|
| 249 |
+
|
| 250 |
+
transcript_path = next(Path("data/videos").glob(f"{video_id}*.en.vtt"), None)
|
| 251 |
+
if not transcript_path: transcript_path = next(Path("data/videos").glob(f"{video_id}*.vtt"), None)
|
| 252 |
+
|
| 253 |
+
sanitized_path = Path(f"data/videos/{video_id}_fixed.mp4")
|
| 254 |
+
|
| 255 |
+
# --- FFmpeg Sanitization Logic with Robust Fallback ---
|
| 256 |
+
if not sanitized_path.exists() and original_path.exists():
|
| 257 |
+
logger.info(f"Sanitizing video {video_id} (Original: {original_path})...")
|
| 258 |
+
ffmpeg_bin = shutil.which('ffmpeg')
|
| 259 |
+
if not ffmpeg_bin: raise RuntimeError("FFmpeg binary not found in system path!")
|
| 260 |
+
|
| 261 |
+
try:
|
| 262 |
+
await run_subprocess_async([ffmpeg_bin, "-i", str(original_path), "-c:v", "libx264", "-c:a", "aac", "-pix_fmt", "yuv420p", "-y", str(sanitized_path)])
|
| 263 |
+
logger.info("Sanitization (re-encode) successful.")
|
| 264 |
+
except Exception as e:
|
| 265 |
+
logger.warning(f"Re-encode failed ({e}). Attempting Stream Copy...")
|
| 266 |
+
try:
|
| 267 |
+
await run_subprocess_async([ffmpeg_bin, "-i", str(original_path), "-c", "copy", "-y", str(sanitized_path)])
|
| 268 |
+
logger.info("Sanitization (copy) successful.")
|
| 269 |
+
except Exception as e2:
|
| 270 |
+
logger.error(f"Sanitization failed completely: {e2}")
|
| 271 |
+
if original_path.suffix == '.mp4':
|
| 272 |
+
logger.warning("Using original file as sanitized file.")
|
| 273 |
+
shutil.copy(original_path, sanitized_path)
|
| 274 |
+
else:
|
| 275 |
+
raise RuntimeError("Could not produce a valid MP4 file.")
|
| 276 |
+
|
| 277 |
+
# --- Audio Extraction ---
|
| 278 |
+
audio_path = sanitized_path.with_suffix('.wav')
|
| 279 |
+
if not audio_path.exists() and sanitized_path.exists():
|
| 280 |
+
logger.info(f"Extracting audio to {audio_path}...")
|
| 281 |
+
try:
|
| 282 |
+
await run_subprocess_async(["ffmpeg", "-i", str(sanitized_path), "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", "-y", str(audio_path)])
|
| 283 |
+
logger.info("Audio extraction successful.")
|
| 284 |
+
except Exception as e:
|
| 285 |
+
logger.error(f"Audio extraction failed: {e}")
|
| 286 |
+
|
| 287 |
+
# --- Transcription ---
|
| 288 |
+
if not transcript_path and audio_path.exists() and transcription.transcription_model is not None:
|
| 289 |
+
logger.info("Generating transcript via Whisper...")
|
| 290 |
+
transcript_path = await loop.run_in_executor(None, transcription.generate_transcript, str(audio_path))
|
| 291 |
+
elif not transcript_path:
|
| 292 |
+
logger.info("Skipping local transcription (Whisper not loaded or audio missing).")
|
| 293 |
+
|
| 294 |
+
return {"video": str(sanitized_path), "transcript": str(transcript_path) if transcript_path else None, "metadata": metadata}
|
| 295 |
+
|
| 296 |
+
def safe_int(value):
|
| 297 |
+
try:
|
| 298 |
+
clean = re.sub(r'[^\d]', '', str(value))
|
| 299 |
+
return int(clean) if clean else 0
|
| 300 |
+
except Exception:
|
| 301 |
+
return 0
|
| 302 |
+
|
| 303 |
+
async def generate_and_save_croissant_metadata(row_data: dict) -> str:
|
| 304 |
+
try:
|
| 305 |
+
sanitized_data = {
|
| 306 |
+
"id": str(row_data.get("id", "")),
|
| 307 |
+
"link": str(row_data.get("link", "")),
|
| 308 |
+
"visual_integrity_score": safe_int(row_data.get("visual_integrity_score")),
|
| 309 |
+
"final_veracity_score": safe_int(row_data.get("final_veracity_score"))
|
| 310 |
+
}
|
| 311 |
+
video_id = sanitized_data["id"]
|
| 312 |
+
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
| 313 |
+
croissant_json = {
|
| 314 |
+
"@context": "https://schema.org/",
|
| 315 |
+
"@type": "Dataset",
|
| 316 |
+
"name": f"vchat-label-{video_id}",
|
| 317 |
+
"description": f"Veracity analysis labels for video {video_id}",
|
| 318 |
+
"url": sanitized_data["link"],
|
| 319 |
+
"variableMeasured": sanitized_data
|
| 320 |
+
}
|
| 321 |
+
path = Path("metadata") / f"{video_id}_{timestamp}.json"
|
| 322 |
+
path.write_text(json.dumps(croissant_json, indent=2))
|
| 323 |
+
return str(path)
|
| 324 |
+
except Exception:
|
| 325 |
+
return "N/A (Error)"
|
| 326 |
+
|
| 327 |
+
async def get_labels_for_link(video_url: str, gemini_config: dict, vertex_config: dict, model_selection: str, include_comments: bool, reasoning_method: str = "cot"):
|
| 328 |
+
try:
|
| 329 |
+
yield f"Downloading assets for {video_url}..."
|
| 330 |
+
|
| 331 |
+
try:
|
| 332 |
+
paths = await prepare_video_assets_async(video_url)
|
| 333 |
+
except ValueError as ve:
|
| 334 |
+
yield f"Skipped: {str(ve)}"
|
| 335 |
+
logger.warning(f"Skipping {video_url}: {ve}")
|
| 336 |
+
return
|
| 337 |
+
except Exception as e:
|
| 338 |
+
yield f"Error preparing assets: {str(e)}"
|
| 339 |
+
logger.error(f"Asset prep failed for {video_url}: {e}")
|
| 340 |
+
return
|
| 341 |
+
|
| 342 |
+
video_path = paths["video"]
|
| 343 |
+
transcript_text = parse_vtt(paths["transcript"]) if paths["transcript"] else "No transcript (Audio/Video Analysis only)."
|
| 344 |
+
caption = paths["metadata"].get("caption", "")
|
| 345 |
+
|
| 346 |
+
yield f"Assets ready. Running inference ({model_selection}, {reasoning_method.upper()})..."
|
| 347 |
+
logger.info(f"Starting inference pipeline for {video_url} (Transcript len: {len(transcript_text)})")
|
| 348 |
+
|
| 349 |
+
final_labels = None
|
| 350 |
+
raw_toon = ""
|
| 351 |
+
prompt_used = ""
|
| 352 |
+
|
| 353 |
+
pipeline = inference_logic.run_gemini_labeling_pipeline if model_selection == 'gemini' else inference_logic.run_vertex_labeling_pipeline
|
| 354 |
+
config = gemini_config if model_selection == 'gemini' else vertex_config
|
| 355 |
+
|
| 356 |
+
# Add timeout protection for inference
|
| 357 |
+
try:
|
| 358 |
+
async for msg in pipeline(video_path, caption, transcript_text, config, include_comments, reasoning_method):
|
| 359 |
+
if isinstance(msg, dict) and "parsed_data" in msg:
|
| 360 |
+
final_labels = msg["parsed_data"]
|
| 361 |
+
raw_toon = msg.get("raw_toon", "")
|
| 362 |
+
prompt_used = msg.get("prompt_used", "")
|
| 363 |
+
logger.info("Inference successful. Data parsed.")
|
| 364 |
+
elif isinstance(msg, str):
|
| 365 |
+
yield msg
|
| 366 |
+
elif isinstance(msg, dict) and "error" in msg:
|
| 367 |
+
yield f"API Error: {msg['error']}"
|
| 368 |
+
except Exception as pipe_err:
|
| 369 |
+
logger.error(f"Pipeline crashed: {pipe_err}")
|
| 370 |
+
yield f"Critical Pipeline Failure: {pipe_err}"
|
| 371 |
+
return
|
| 372 |
+
|
| 373 |
+
if not final_labels:
|
| 374 |
+
logger.error(f"Inference pipeline completed but returned no labels for {video_url}")
|
| 375 |
+
yield "No labels generated. Check logs."
|
| 376 |
+
return
|
| 377 |
+
|
| 378 |
+
final_labels["meta_info"] = {
|
| 379 |
+
"prompt_used": prompt_used,
|
| 380 |
+
"model_selection": model_selection,
|
| 381 |
+
"reasoning_method": reasoning_method
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
vec = final_labels.get("veracity_vectors", {})
|
| 385 |
+
mod = final_labels.get("modalities", {})
|
| 386 |
+
fin = final_labels.get("final_assessment", {})
|
| 387 |
+
|
| 388 |
+
row = {
|
| 389 |
+
"id": paths["metadata"]["id"],
|
| 390 |
+
"link": paths["metadata"]["link"],
|
| 391 |
+
"caption": caption,
|
| 392 |
+
"postdatetime": paths["metadata"].get("postdatetime", ""),
|
| 393 |
+
"collecttime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 394 |
+
"videotranscriptionpath": paths["transcript"] or "",
|
| 395 |
+
"visual_integrity_score": vec.get("visual_integrity_score", "0"),
|
| 396 |
+
"audio_integrity_score": vec.get("audio_integrity_score", "0"),
|
| 397 |
+
"source_credibility_score": vec.get("source_credibility_score", "0"),
|
| 398 |
+
"logical_consistency_score": vec.get("logical_consistency_score", "0"),
|
| 399 |
+
"emotional_manipulation_score": vec.get("emotional_manipulation_score", "0"),
|
| 400 |
+
"video_audio_score": mod.get("video_audio_score", "0"),
|
| 401 |
+
"video_caption_score": mod.get("video_caption_score", "0"),
|
| 402 |
+
"audio_caption_score": mod.get("audio_caption_score", "0"),
|
| 403 |
+
"final_veracity_score": fin.get("veracity_score_total", "0"),
|
| 404 |
+
"final_reasoning": fin.get("reasoning", "")
|
| 405 |
+
}
|
| 406 |
+
yield {"csv_row": row, "full_json": final_labels, "raw_toon": raw_toon}
|
| 407 |
+
|
| 408 |
+
except Exception as e:
|
| 409 |
+
logger.error(f"Fatal error in get_labels_for_link: {e}", exc_info=True)
|
| 410 |
+
yield {"error": str(e)}
|
| 411 |
+
|
| 412 |
+
@app.get("/queue/list")
|
| 413 |
+
async def get_queue_list():
|
| 414 |
+
queue_path = Path("data/batch_queue.csv")
|
| 415 |
+
if not queue_path.exists(): return []
|
| 416 |
+
items = []
|
| 417 |
+
with open(queue_path, 'r', encoding='utf-8') as f:
|
| 418 |
+
reader = csv.reader(f)
|
| 419 |
+
try: next(reader)
|
| 420 |
+
except: pass
|
| 421 |
+
for row in reader:
|
| 422 |
+
if len(row) > 0:
|
| 423 |
+
link = row[0]
|
| 424 |
+
status = "Processed" if check_if_processed(link) else "Pending"
|
| 425 |
+
items.append({
|
| 426 |
+
"link": link,
|
| 427 |
+
"timestamp": row[1] if len(row) > 1 else "",
|
| 428 |
+
"status": status
|
| 429 |
+
})
|
| 430 |
+
return items
|
| 431 |
+
|
| 432 |
+
@app.delete("/queue/delete")
|
| 433 |
+
async def delete_queue_item(link: str):
|
| 434 |
+
queue_path = Path("data/batch_queue.csv")
|
| 435 |
+
if not queue_path.exists():
|
| 436 |
+
return {"status": "error", "message": "Queue file not found"}
|
| 437 |
+
|
| 438 |
+
rows = []
|
| 439 |
+
deleted = False
|
| 440 |
+
try:
|
| 441 |
+
with open(queue_path, 'r', encoding='utf-8') as f:
|
| 442 |
+
reader = csv.reader(f)
|
| 443 |
+
rows = list(reader)
|
| 444 |
+
|
| 445 |
+
new_rows = []
|
| 446 |
+
if rows and len(rows) > 0 and rows[0][0] == "link":
|
| 447 |
+
new_rows.append(rows[0])
|
| 448 |
+
rows = rows[1:]
|
| 449 |
+
|
| 450 |
+
for row in rows:
|
| 451 |
+
if not row: continue
|
| 452 |
+
if row[0] == link:
|
| 453 |
+
deleted = True
|
| 454 |
+
else:
|
| 455 |
+
new_rows.append(row)
|
| 456 |
+
|
| 457 |
+
with open(queue_path, 'w', newline='', encoding='utf-8') as f:
|
| 458 |
+
writer = csv.writer(f)
|
| 459 |
+
writer.writerows(new_rows)
|
| 460 |
+
|
| 461 |
+
if deleted:
|
| 462 |
+
return {"status": "success", "link": link}
|
| 463 |
+
else:
|
| 464 |
+
return {"status": "not_found", "message": "Link not found in queue"}
|
| 465 |
+
|
| 466 |
+
except Exception as e:
|
| 467 |
+
return {"status": "error", "message": str(e)}
|
| 468 |
+
|
| 469 |
+
@app.post("/queue/stop")
|
| 470 |
+
async def stop_queue_processing():
|
| 471 |
+
global STOP_QUEUE_SIGNAL
|
| 472 |
+
logger.info("Received Stop Signal from User.")
|
| 473 |
+
STOP_QUEUE_SIGNAL = True
|
| 474 |
+
return {"status": "stopping"}
|
| 475 |
+
|
| 476 |
+
@app.post("/queue/upload_csv")
|
| 477 |
+
async def upload_csv_to_queue(file: UploadFile = File(...)):
|
| 478 |
+
try:
|
| 479 |
+
content = await file.read()
|
| 480 |
+
try:
|
| 481 |
+
decoded = content.decode('utf-8').splitlines()
|
| 482 |
+
except UnicodeDecodeError:
|
| 483 |
+
decoded = content.decode('latin-1').splitlines()
|
| 484 |
+
|
| 485 |
+
reader = csv.reader(decoded)
|
| 486 |
+
links_to_add = []
|
| 487 |
+
header = next(reader, None)
|
| 488 |
+
if not header: return {"status": "empty file"}
|
| 489 |
+
|
| 490 |
+
link_idx = 0
|
| 491 |
+
header_lower = [h.lower() for h in header]
|
| 492 |
+
|
| 493 |
+
if "link" in header_lower: link_idx = header_lower.index("link")
|
| 494 |
+
elif "url" in header_lower: link_idx = header_lower.index("url")
|
| 495 |
+
elif len(header) > 0 and header[0].strip().startswith("http"):
|
| 496 |
+
links_to_add.append(header[0])
|
| 497 |
+
link_idx = 0
|
| 498 |
+
|
| 499 |
+
for row in reader:
|
| 500 |
+
if len(row) > link_idx and row[link_idx].strip():
|
| 501 |
+
links_to_add.append(row[link_idx].strip())
|
| 502 |
+
|
| 503 |
+
queue_path = Path("data/batch_queue.csv")
|
| 504 |
+
existing_links = set()
|
| 505 |
+
if queue_path.exists():
|
| 506 |
+
with open(queue_path, 'r', encoding='utf-8') as f:
|
| 507 |
+
existing_links = set(f.read().splitlines())
|
| 508 |
+
|
| 509 |
+
added_count = 0
|
| 510 |
+
with open(queue_path, 'a', newline='', encoding='utf-8') as f:
|
| 511 |
+
writer = csv.writer(f)
|
| 512 |
+
if not queue_path.exists() or queue_path.stat().st_size == 0:
|
| 513 |
+
writer.writerow(["link", "ingest_timestamp"])
|
| 514 |
+
|
| 515 |
+
for link in links_to_add:
|
| 516 |
+
duplicate = False
|
| 517 |
+
for line in existing_links:
|
| 518 |
+
if link in line:
|
| 519 |
+
duplicate = True
|
| 520 |
+
break
|
| 521 |
+
if duplicate: continue
|
| 522 |
+
|
| 523 |
+
writer.writerow([link, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")])
|
| 524 |
+
added_count += 1
|
| 525 |
+
|
| 526 |
+
return {"status": "success", "added": added_count}
|
| 527 |
+
except Exception as e:
|
| 528 |
+
logging.error(f"Upload CSV error: {e}")
|
| 529 |
+
return JSONResponse(status_code=400, content={"error": str(e), "status": "failed"})
|
| 530 |
+
|
| 531 |
+
@app.post("/queue/run")
|
| 532 |
+
async def run_queue_processing(
|
| 533 |
+
model_selection: str = Form(...),
|
| 534 |
+
gemini_api_key: str = Form(""), gemini_model_name: str = Form(""),
|
| 535 |
+
vertex_project_id: str = Form(""), vertex_location: str = Form(""), vertex_model_name: str = Form(""), vertex_api_key: str = Form(""),
|
| 536 |
+
include_comments: bool = Form(False),
|
| 537 |
+
reasoning_method: str = Form("cot")
|
| 538 |
+
):
|
| 539 |
+
global STOP_QUEUE_SIGNAL
|
| 540 |
+
STOP_QUEUE_SIGNAL = False
|
| 541 |
+
gemini_config = {"api_key": gemini_api_key, "model_name": gemini_model_name}
|
| 542 |
+
vertex_config = {"project_id": vertex_project_id, "location": vertex_location, "model_name": vertex_model_name, "api_key": vertex_api_key}
|
| 543 |
+
|
| 544 |
+
async def queue_stream():
|
| 545 |
+
queue_path = Path("data/batch_queue.csv")
|
| 546 |
+
if not queue_path.exists():
|
| 547 |
+
yield "data: Queue empty.\n\n"
|
| 548 |
+
return
|
| 549 |
+
|
| 550 |
+
items = []
|
| 551 |
+
with open(queue_path, 'r', encoding='utf-8') as f:
|
| 552 |
+
reader = csv.reader(f)
|
| 553 |
+
try: next(reader)
|
| 554 |
+
except: pass
|
| 555 |
+
for row in reader:
|
| 556 |
+
if row: items.append(row[0])
|
| 557 |
+
|
| 558 |
+
processed_count = 0
|
| 559 |
+
total = len(items)
|
| 560 |
+
|
| 561 |
+
logger.info(f"Starting batch queue processing for {total} items.")
|
| 562 |
+
|
| 563 |
+
for i, link in enumerate(items):
|
| 564 |
+
if STOP_QUEUE_SIGNAL:
|
| 565 |
+
yield "data: [SYSTEM] Stopped by user.\n\n"
|
| 566 |
+
logger.info("Stopping queue loop.")
|
| 567 |
+
break
|
| 568 |
+
|
| 569 |
+
if check_if_processed(link):
|
| 570 |
+
yield f"data: [SKIP] {link} processed.\n\n"
|
| 571 |
+
continue
|
| 572 |
+
|
| 573 |
+
yield f"data: [START] {i+1}/{total}: {link}\n\n"
|
| 574 |
+
final_data = None
|
| 575 |
+
|
| 576 |
+
# Streaming results from pipeline
|
| 577 |
+
async for res in get_labels_for_link(link, gemini_config, vertex_config, model_selection, include_comments, reasoning_method):
|
| 578 |
+
if isinstance(res, str):
|
| 579 |
+
msg = res.replace('\n', ' ')
|
| 580 |
+
yield f"data: {msg}\n\n"
|
| 581 |
+
if isinstance(res, dict):
|
| 582 |
+
if "error" in res:
|
| 583 |
+
yield f"data: [ERROR DETAIL] {res['error']}\n\n"
|
| 584 |
+
if "csv_row" in res:
|
| 585 |
+
final_data = res
|
| 586 |
+
|
| 587 |
+
if final_data:
|
| 588 |
+
row = final_data["csv_row"]
|
| 589 |
+
vid_id = row["id"]
|
| 590 |
+
ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
| 591 |
+
|
| 592 |
+
# Save artifacts
|
| 593 |
+
json_path = f"data/labels/{vid_id}_{ts}_labels.json"
|
| 594 |
+
with open(json_path, 'w') as f: json.dump(final_data["full_json"], f, indent=2)
|
| 595 |
+
with open(f"data/labels/{vid_id}_{ts}.toon", 'w') as f: f.write(final_data["raw_toon"])
|
| 596 |
+
|
| 597 |
+
prompt_content = final_data.get("full_json", {}).get("meta_info", {}).get("prompt_used", "")
|
| 598 |
+
if prompt_content:
|
| 599 |
+
with open(f"data/prompts/{vid_id}_{ts}_prompt.txt", 'w', encoding='utf-8') as f:
|
| 600 |
+
f.write(prompt_content)
|
| 601 |
+
|
| 602 |
+
raw_response = final_data.get("raw_toon", "")
|
| 603 |
+
if raw_response:
|
| 604 |
+
with open(f"data/responses/{vid_id}.txt", 'w', encoding='utf-8') as f:
|
| 605 |
+
f.write(raw_response)
|
| 606 |
+
|
| 607 |
+
row["metadatapath"] = await generate_and_save_croissant_metadata(row)
|
| 608 |
+
row["json_path"] = json_path
|
| 609 |
+
|
| 610 |
+
dpath = Path("data/dataset.csv")
|
| 611 |
+
exists = dpath.exists()
|
| 612 |
+
with open(dpath, 'a', newline='', encoding='utf-8') as f:
|
| 613 |
+
writer = csv.DictWriter(f, fieldnames=list(row.keys()), extrasaction='ignore')
|
| 614 |
+
if not exists: writer.writeheader()
|
| 615 |
+
writer.writerow(row)
|
| 616 |
+
|
| 617 |
+
processed_count += 1
|
| 618 |
+
yield f"data: [SUCCESS] Labeled.\n\n"
|
| 619 |
+
else:
|
| 620 |
+
yield f"data: [FAIL] Failed to label. Check logs.\n\n"
|
| 621 |
+
|
| 622 |
+
yield f"data: Batch Complete. +{processed_count} videos labeled.\n\n"
|
| 623 |
+
yield "event: close\ndata: Done\n\n"
|
| 624 |
+
|
| 625 |
+
return StreamingResponse(queue_stream(), media_type="text/event-stream")
|
| 626 |
+
|
| 627 |
+
@app.post("/extension/ingest")
|
| 628 |
+
async def extension_ingest(request: Request):
|
| 629 |
+
try:
|
| 630 |
+
data = await request.json()
|
| 631 |
+
link = data.get("link")
|
| 632 |
+
if not link: raise HTTPException(status_code=400, detail="No link")
|
| 633 |
+
queue_path = Path("data/batch_queue.csv")
|
| 634 |
+
file_exists = queue_path.exists()
|
| 635 |
+
|
| 636 |
+
if file_exists:
|
| 637 |
+
with open(queue_path, 'r', encoding='utf-8') as f:
|
| 638 |
+
if link in f.read():
|
| 639 |
+
return {"status": "queued", "msg": "Duplicate"}
|
| 640 |
+
|
| 641 |
+
with open(queue_path, 'a', newline='', encoding='utf-8') as f:
|
| 642 |
+
writer = csv.writer(f)
|
| 643 |
+
if not file_exists: writer.writerow(["link", "ingest_timestamp"])
|
| 644 |
+
writer.writerow([link, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")])
|
| 645 |
+
|
| 646 |
+
return {"status": "queued", "link": link}
|
| 647 |
+
except Exception as e:
|
| 648 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 649 |
+
|
| 650 |
+
@app.post("/extension/save_comments")
|
| 651 |
+
async def extension_save_comments(request: Request):
|
| 652 |
+
try:
|
| 653 |
+
data = await request.json()
|
| 654 |
+
link = data.get("link")
|
| 655 |
+
comments = data.get("comments", [])
|
| 656 |
+
if not link or not comments: raise HTTPException(status_code=400, detail="Missing data")
|
| 657 |
+
|
| 658 |
+
csv_path = Path("data/comments.csv")
|
| 659 |
+
exists = csv_path.exists()
|
| 660 |
+
fieldnames = ["link", "author", "comment_text", "timestamp"]
|
| 661 |
+
|
| 662 |
+
with open(csv_path, 'a', newline='', encoding='utf-8') as f:
|
| 663 |
+
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
|
| 664 |
+
if not exists: writer.writeheader()
|
| 665 |
+
|
| 666 |
+
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 667 |
+
for c in comments:
|
| 668 |
+
row = {"link": link, "timestamp": ts}
|
| 669 |
+
if isinstance(c, dict):
|
| 670 |
+
row["author"] = c.get("author", "Unknown")
|
| 671 |
+
row["comment_text"] = c.get("text", "").strip()
|
| 672 |
+
else:
|
| 673 |
+
row["author"] = "Unknown"
|
| 674 |
+
row["comment_text"] = str(c).strip()
|
| 675 |
+
|
| 676 |
+
if row["comment_text"]:
|
| 677 |
+
writer.writerow(row)
|
| 678 |
+
|
| 679 |
+
return {"status": "saved", "count": len(comments)}
|
| 680 |
+
except Exception as e:
|
| 681 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 682 |
+
|
| 683 |
+
@app.post("/extension/save_manual")
|
| 684 |
+
async def extension_save_manual(request: Request):
|
| 685 |
+
try:
|
| 686 |
+
data = await request.json()
|
| 687 |
+
link = data.get("link")
|
| 688 |
+
labels = data.get("labels", {})
|
| 689 |
+
stats = data.get("stats", {})
|
| 690 |
+
if not link: raise HTTPException(status_code=400, detail="No link")
|
| 691 |
+
|
| 692 |
+
video_id = extract_tweet_id(link) or hashlib.md5(link.encode()).hexdigest()[:16]
|
| 693 |
+
|
| 694 |
+
row_data = {
|
| 695 |
+
"id": video_id,
|
| 696 |
+
"link": link,
|
| 697 |
+
"caption": data.get("caption", ""),
|
| 698 |
+
"collecttime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 699 |
+
"source": "manual_extension",
|
| 700 |
+
"visual_integrity_score": labels.get("visual_integrity_score", 0),
|
| 701 |
+
"audio_integrity_score": labels.get("audio_integrity_score", 0),
|
| 702 |
+
"source_credibility_score": labels.get("source_credibility_score", 0),
|
| 703 |
+
"logical_consistency_score": labels.get("logical_consistency_score", 0),
|
| 704 |
+
"emotional_manipulation_score": labels.get("emotional_manipulation_score", 0),
|
| 705 |
+
"video_audio_score": labels.get("video_audio_score", 0),
|
| 706 |
+
"video_caption_score": labels.get("video_caption_score", 0),
|
| 707 |
+
"audio_caption_score": labels.get("audio_caption_score", 0),
|
| 708 |
+
"final_veracity_score": labels.get("final_veracity_score", 0),
|
| 709 |
+
"final_reasoning": labels.get("reasoning", ""),
|
| 710 |
+
"stats_likes": stats.get("likes", 0),
|
| 711 |
+
"stats_shares": stats.get("shares", 0),
|
| 712 |
+
"stats_comments": stats.get("comments", 0),
|
| 713 |
+
"stats_platform": stats.get("platform", "unknown")
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
dpath = Path("data/manual_dataset.csv")
|
| 717 |
+
exists = dpath.exists()
|
| 718 |
+
fieldnames = list(row_data.keys())
|
| 719 |
+
|
| 720 |
+
with open(dpath, 'a', newline='', encoding='utf-8') as f:
|
| 721 |
+
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
|
| 722 |
+
if not exists: writer.writeheader()
|
| 723 |
+
writer.writerow(row_data)
|
| 724 |
+
|
| 725 |
+
return {"status": "saved"}
|
| 726 |
+
except Exception as e:
|
| 727 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 728 |
+
|
| 729 |
+
@app.get("/manage/list")
|
| 730 |
+
async def list_data():
|
| 731 |
+
data = []
|
| 732 |
+
def read_csv(path, source_type):
|
| 733 |
+
if not path.exists(): return
|
| 734 |
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 735 |
+
reader = csv.DictReader(f)
|
| 736 |
+
for row in reader:
|
| 737 |
+
if not row.get('id') or row['id'].strip() == "":
|
| 738 |
+
link = row.get('link', '')
|
| 739 |
+
tid = extract_tweet_id(link)
|
| 740 |
+
row['id'] = tid if tid else hashlib.md5(link.encode()).hexdigest()[:16]
|
| 741 |
+
|
| 742 |
+
json_content = None
|
| 743 |
+
if row.get('json_path') and os.path.exists(row['json_path']):
|
| 744 |
+
try:
|
| 745 |
+
with open(row['json_path'], 'r') as jf: json_content = json.load(jf)
|
| 746 |
+
except: pass
|
| 747 |
+
|
| 748 |
+
row['source_type'] = source_type
|
| 749 |
+
row['json_data'] = json_content
|
| 750 |
+
data.append(row)
|
| 751 |
+
|
| 752 |
+
read_csv(Path("data/dataset.csv"), "auto")
|
| 753 |
+
read_csv(Path("data/manual_dataset.csv"), "manual")
|
| 754 |
+
data.sort(key=lambda x: x.get('collecttime', ''), reverse=True)
|
| 755 |
+
return data
|
| 756 |
+
|
| 757 |
+
@app.delete("/manage/delete")
|
| 758 |
+
async def delete_data(id: str = "", link: str = ""):
|
| 759 |
+
if not id and not link: raise HTTPException(status_code=400, detail="Must provide ID or Link")
|
| 760 |
+
deleted_count = 0
|
| 761 |
+
target_id = id
|
| 762 |
+
|
| 763 |
+
def remove_from_csv(path):
|
| 764 |
+
nonlocal deleted_count, target_id
|
| 765 |
+
if not path.exists(): return
|
| 766 |
+
rows = []
|
| 767 |
+
found_in_file = False
|
| 768 |
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 769 |
+
reader = csv.DictReader(f)
|
| 770 |
+
fieldnames = reader.fieldnames
|
| 771 |
+
for row in reader:
|
| 772 |
+
is_match = False
|
| 773 |
+
if id and row.get('id') == id: is_match = True
|
| 774 |
+
elif link and row.get('link') == link: is_match = True
|
| 775 |
+
if is_match:
|
| 776 |
+
found_in_file = True
|
| 777 |
+
deleted_count += 1
|
| 778 |
+
if not target_id: target_id = row.get('id')
|
| 779 |
+
else: rows.append(row)
|
| 780 |
+
if found_in_file:
|
| 781 |
+
with open(path, 'w', newline='', encoding='utf-8') as f:
|
| 782 |
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
| 783 |
+
writer.writeheader()
|
| 784 |
+
writer.writerows(rows)
|
| 785 |
+
|
| 786 |
+
remove_from_csv(Path("data/dataset.csv"))
|
| 787 |
+
remove_from_csv(Path("data/manual_dataset.csv"))
|
| 788 |
+
if target_id:
|
| 789 |
+
for p in Path("data/labels").glob(f"{target_id}_*"): p.unlink(missing_ok=True)
|
| 790 |
+
for p in Path("metadata").glob(f"{target_id}_*"): p.unlink(missing_ok=True)
|
| 791 |
+
return {"status": "deleted", "count": deleted_count}
|
| 792 |
+
|
| 793 |
+
@app.post("/label_video")
|
| 794 |
+
async def label_video_endpoint(
|
| 795 |
+
video_url: str = Form(...), model_selection: str = Form(...),
|
| 796 |
+
gemini_api_key: str = Form(""), gemini_model_name: str = Form(""),
|
| 797 |
+
vertex_project_id: str = Form(""), vertex_location: str = Form(""), vertex_model_name: str = Form(""), vertex_api_key: str = Form(""),
|
| 798 |
+
include_comments: bool = Form(False),
|
| 799 |
+
reasoning_method: str = Form("cot")
|
| 800 |
+
):
|
| 801 |
+
gemini_config = {"api_key": gemini_api_key, "model_name": gemini_model_name}
|
| 802 |
+
vertex_config = {"project_id": vertex_project_id, "location": vertex_location, "model_name": vertex_model_name, "api_key": vertex_api_key}
|
| 803 |
+
async def stream():
|
| 804 |
+
async for msg in get_labels_for_link(video_url, gemini_config, vertex_config, model_selection, include_comments, reasoning_method):
|
| 805 |
+
if isinstance(msg, str): yield f"data: {msg}\n\n"
|
| 806 |
+
if isinstance(msg, dict) and "csv_row" in msg: yield "data: Done. Labels generated.\n\n"
|
| 807 |
+
yield "event: close\ndata: Done.\n\n"
|
| 808 |
+
return StreamingResponse(stream(), media_type="text/event-stream")
|
src/factuality_logic.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# factuality_logic.py
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import asyncio
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import inference_logic
|
| 9 |
+
from toon_parser import parse_toon_line
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
PROMPT_VISUAL_ARTIFACTS = (
|
| 14 |
+
"Analyze the video for visual manipulation (Deepfakes, editing anomalies).\n"
|
| 15 |
+
"Steps inside <thinking>: 1. Scan for artifacts. 2. Check cuts.\n"
|
| 16 |
+
"Output TOON format:\n"
|
| 17 |
+
"visual_analysis: result[2]{score,justification}:\n"
|
| 18 |
+
"Score(1-10),\"Justification text\""
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
PROMPT_CONTENT_ANALYSIS = (
|
| 22 |
+
"Analyze the content for accuracy and logic.\n"
|
| 23 |
+
"Steps inside <thinking>: 1. Identify claims. 2. Check fallacies. 3. Assess emotion.\n"
|
| 24 |
+
"**Transcript:**\n{transcript}\n"
|
| 25 |
+
"Output TOON format:\n"
|
| 26 |
+
"content_analysis: result[2]{score,justification}:\n"
|
| 27 |
+
"Score(1-10),\"Justification text\""
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
PROMPT_AUDIO_ANALYSIS = (
|
| 31 |
+
"Analyze audio for synthesis or manipulation.\n"
|
| 32 |
+
"Steps inside <thinking>: 1. Listen for robotic inflections. 2. Check lip-sync.\n"
|
| 33 |
+
"**Transcript:**\n{transcript}\n"
|
| 34 |
+
"Output TOON format:\n"
|
| 35 |
+
"audio_analysis: result[2]{score,justification}:\n"
|
| 36 |
+
"Score(1-10),\"Justification text\""
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def parse_vtt(file_path: str) -> str:
|
| 41 |
+
try:
|
| 42 |
+
if not os.path.exists(file_path):
|
| 43 |
+
return "Transcript file not found."
|
| 44 |
+
|
| 45 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 46 |
+
lines = f.readlines()
|
| 47 |
+
|
| 48 |
+
text_lines = []
|
| 49 |
+
for line in lines:
|
| 50 |
+
line = line.strip()
|
| 51 |
+
if line and not line.startswith('WEBVTT') and not '-->' in line and not line.isdigit():
|
| 52 |
+
clean_line = re.sub(r'<[^>]+>', '', line)
|
| 53 |
+
if clean_line and (not text_lines or clean_line != text_lines[-1]):
|
| 54 |
+
text_lines.append(clean_line)
|
| 55 |
+
|
| 56 |
+
return "\n".join(text_lines) if text_lines else "No speech found in transcript."
|
| 57 |
+
except Exception as e:
|
| 58 |
+
logger.error(f"Error parsing VTT file {file_path}: {e}")
|
| 59 |
+
return f"Error reading transcript: {e}"
|
| 60 |
+
|
| 61 |
+
async def run_factuality_pipeline(paths: dict, checks: dict, generation_config: dict):
|
| 62 |
+
video_path = paths.get("video")
|
| 63 |
+
transcript_path = paths.get("transcript")
|
| 64 |
+
|
| 65 |
+
if not video_path:
|
| 66 |
+
yield "ERROR: Video path not found. Cannot start analysis.\n\n"
|
| 67 |
+
return
|
| 68 |
+
|
| 69 |
+
yield "Step 1: Processing Transcript...\n"
|
| 70 |
+
await asyncio.sleep(0.1)
|
| 71 |
+
transcript = "No transcript was downloaded for this video."
|
| 72 |
+
if transcript_path and os.path.exists(transcript_path):
|
| 73 |
+
transcript = parse_vtt(transcript_path)
|
| 74 |
+
yield f" - Transcript file found and processed.\n"
|
| 75 |
+
else:
|
| 76 |
+
yield f" - No transcript file was found.\n"
|
| 77 |
+
|
| 78 |
+
yield f"\n--- Extracted Transcript ---\n{transcript}\n--------------------------\n\n"
|
| 79 |
+
await asyncio.sleep(0.1)
|
| 80 |
+
|
| 81 |
+
analysis_steps = []
|
| 82 |
+
if checks.get("visuals"):
|
| 83 |
+
analysis_steps.append(("Visual Integrity", PROMPT_VISUAL_ARTIFACTS))
|
| 84 |
+
if checks.get("content"):
|
| 85 |
+
analysis_steps.append(("Content Veracity", PROMPT_CONTENT_ANALYSIS.format(transcript=transcript)))
|
| 86 |
+
if checks.get("audio"):
|
| 87 |
+
analysis_steps.append(("Audio Forensics", PROMPT_AUDIO_ANALYSIS.format(transcript=transcript)))
|
| 88 |
+
|
| 89 |
+
for i, (title, prompt) in enumerate(analysis_steps):
|
| 90 |
+
yield f"--- Step {i + 2}: Running '{title}' Analysis ---\n"
|
| 91 |
+
yield "(Model is generating TOON analysis with scores...)\n\n"
|
| 92 |
+
await asyncio.sleep(0.1)
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
current_gen_config = generation_config.copy()
|
| 96 |
+
sampling_fps = current_gen_config.pop("sampling_fps", 2.0)
|
| 97 |
+
current_gen_config.pop("num_perceptions", None)
|
| 98 |
+
|
| 99 |
+
current_gen_config["temperature"] = 0.1
|
| 100 |
+
current_gen_config["do_sample"] = True
|
| 101 |
+
|
| 102 |
+
ans = inference_logic.inference_step(
|
| 103 |
+
video_path=video_path,
|
| 104 |
+
prompt=prompt,
|
| 105 |
+
generation_kwargs=current_gen_config,
|
| 106 |
+
sampling_fps=sampling_fps,
|
| 107 |
+
pred_glue=None
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
yield f" - Analysis Complete for '{title}'. Parsing TOON...\n\n"
|
| 111 |
+
|
| 112 |
+
parsed_result = {}
|
| 113 |
+
match = re.search(r'(\w+_analysis): result\[2\]\{score,justification\}:\s*\n(.+)', ans, re.MULTILINE)
|
| 114 |
+
|
| 115 |
+
thinking = "No thinking block found."
|
| 116 |
+
think_match = re.search(r'<thinking>(.*?)</thinking>', ans, re.DOTALL)
|
| 117 |
+
if think_match:
|
| 118 |
+
thinking = think_match.group(1).strip()
|
| 119 |
+
|
| 120 |
+
if match:
|
| 121 |
+
key, value_line = match.groups()
|
| 122 |
+
parsed_result = parse_toon_line({'key': key, 'headers': ['score', 'justification']}, value_line.strip())
|
| 123 |
+
else:
|
| 124 |
+
logger.warning(f"Could not parse TOON for '{title}'. Raw: {ans}")
|
| 125 |
+
yield f"Warning: Model did not return valid TOON. Raw output:\n{ans}\n"
|
| 126 |
+
continue
|
| 127 |
+
|
| 128 |
+
score = parsed_result.get('score', 'N/A')
|
| 129 |
+
justification = parsed_result.get('justification', 'No justification provided.')
|
| 130 |
+
|
| 131 |
+
yield f"===== ANALYSIS RESULT: {title.upper()} =====\n"
|
| 132 |
+
yield f"SCORE: {score}/10\n"
|
| 133 |
+
yield f"Reasoning (Step-by-Step): {thinking}\n"
|
| 134 |
+
yield f"Final Justification: {justification}\n\n"
|
| 135 |
+
yield f"========================================\n\n"
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
error_message = f"An error occurred during the '{title}' analysis step: {e}"
|
| 139 |
+
logger.error(error_message, exc_info=True)
|
| 140 |
+
yield f"ERROR: {error_message}\n\n"
|
| 141 |
+
break
|
| 142 |
+
|
| 143 |
+
yield "Factuality Analysis Pipeline Finished.\n"
|
src/inference_logic.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
import time
|
| 5 |
+
import logging
|
| 6 |
+
import asyncio
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
# Safe imports for Lite Mode (API only)
|
| 10 |
+
try:
|
| 11 |
+
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
| 12 |
+
from peft import PeftModel
|
| 13 |
+
except ImportError:
|
| 14 |
+
Qwen3VLForConditionalGeneration = None
|
| 15 |
+
AutoProcessor = None
|
| 16 |
+
PeftModel = None
|
| 17 |
+
|
| 18 |
+
from labeling_logic import (
|
| 19 |
+
LABELING_PROMPT_TEMPLATE, SCORE_INSTRUCTIONS_SIMPLE, SCORE_INSTRUCTIONS_REASONING,
|
| 20 |
+
SCHEMA_SIMPLE, SCHEMA_REASONING,
|
| 21 |
+
FCOT_MACRO_PROMPT, FCOT_MESO_PROMPT, FCOT_SYNTHESIS_PROMPT
|
| 22 |
+
)
|
| 23 |
+
from toon_parser import parse_veracity_toon
|
| 24 |
+
|
| 25 |
+
# Optional local imports
|
| 26 |
+
try:
|
| 27 |
+
from my_vision_process import process_vision_info, client
|
| 28 |
+
except ImportError:
|
| 29 |
+
process_vision_info = None
|
| 30 |
+
client = None
|
| 31 |
+
|
| 32 |
+
# Google GenAI Imports
|
| 33 |
+
try:
|
| 34 |
+
import google.generativeai as genai_legacy
|
| 35 |
+
from google.generativeai.types import generation_types, HarmCategory, HarmBlockThreshold
|
| 36 |
+
except ImportError:
|
| 37 |
+
genai_legacy = None
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
# Modern Google GenAI SDK (v1)
|
| 41 |
+
from google import genai
|
| 42 |
+
from google.genai.types import (
|
| 43 |
+
GenerateContentConfig,
|
| 44 |
+
HttpOptions,
|
| 45 |
+
Retrieval,
|
| 46 |
+
Tool,
|
| 47 |
+
VertexAISearch,
|
| 48 |
+
GoogleSearch,
|
| 49 |
+
Part,
|
| 50 |
+
SafetySetting
|
| 51 |
+
)
|
| 52 |
+
import vertexai
|
| 53 |
+
except ImportError:
|
| 54 |
+
genai = None
|
| 55 |
+
vertexai = None
|
| 56 |
+
|
| 57 |
+
LITE_MODE = os.getenv("LITE_MODE", "true").lower() == "true"
|
| 58 |
+
processor = None
|
| 59 |
+
base_model = None
|
| 60 |
+
peft_model = None
|
| 61 |
+
active_model = None
|
| 62 |
+
logger = logging.getLogger(__name__)
|
| 63 |
+
|
| 64 |
+
def load_models():
|
| 65 |
+
pass
|
| 66 |
+
|
| 67 |
+
async def attempt_toon_repair(original_text: str, schema: str, client, model_type: str, config: dict):
|
| 68 |
+
logger.info("Attempting TOON Repair...")
|
| 69 |
+
repair_prompt = f"SYSTEM: Reformat the following text into strict TOON schema. Infer missing scores as 0.\n\nSCHEMA:\n{schema}\n\nINPUT:\n{original_text}\n"
|
| 70 |
+
try:
|
| 71 |
+
loop = asyncio.get_event_loop()
|
| 72 |
+
repaired_text = ""
|
| 73 |
+
if model_type == 'gemini':
|
| 74 |
+
model = genai_legacy.GenerativeModel("models/gemini-2.0-flash-exp")
|
| 75 |
+
response = await loop.run_in_executor(None, lambda: model.generate_content(repair_prompt))
|
| 76 |
+
repaired_text = response.text
|
| 77 |
+
elif model_type == 'vertex':
|
| 78 |
+
cl = client if client else genai.Client(vertexai=True, project=config['project_id'], location=config['location'])
|
| 79 |
+
response = await loop.run_in_executor(None, lambda: cl.models.generate_content(model=config['model_name'], contents=repair_prompt))
|
| 80 |
+
repaired_text = response.text
|
| 81 |
+
return repaired_text
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"Repair failed: {e}")
|
| 84 |
+
return original_text
|
| 85 |
+
|
| 86 |
+
async def run_gemini_labeling_pipeline(video_path: str, caption: str, transcript: str, gemini_config: dict, include_comments: bool, reasoning_method: str = "cot"):
|
| 87 |
+
if genai_legacy is None:
|
| 88 |
+
yield "ERROR: Legacy SDK missing.\n"
|
| 89 |
+
return
|
| 90 |
+
|
| 91 |
+
api_key = gemini_config.get("api_key")
|
| 92 |
+
if not api_key:
|
| 93 |
+
yield "ERROR: No Gemini API Key provided."
|
| 94 |
+
return
|
| 95 |
+
|
| 96 |
+
logger.info(f"[Gemini] Initializing with model {gemini_config.get('model_name')}")
|
| 97 |
+
|
| 98 |
+
safety_settings = [
|
| 99 |
+
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
| 100 |
+
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
| 101 |
+
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
|
| 102 |
+
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
|
| 103 |
+
]
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
genai_legacy.configure(api_key=api_key)
|
| 107 |
+
loop = asyncio.get_event_loop()
|
| 108 |
+
|
| 109 |
+
# 1. Upload File
|
| 110 |
+
logger.info(f"[Gemini] Uploading video file: {video_path}...")
|
| 111 |
+
yield f"Uploading video to Gemini..."
|
| 112 |
+
|
| 113 |
+
uploaded_file = await loop.run_in_executor(None, lambda: genai_legacy.upload_file(path=video_path, mime_type="video/mp4"))
|
| 114 |
+
logger.info(f"[Gemini] Upload complete. URI: {uploaded_file.uri} | State: {uploaded_file.state.name}")
|
| 115 |
+
|
| 116 |
+
# 2. Wait for Processing (Fix: Refresh state in loop)
|
| 117 |
+
wait_start = time.time()
|
| 118 |
+
while True:
|
| 119 |
+
# Refresh file status
|
| 120 |
+
uploaded_file = await loop.run_in_executor(None, lambda: genai_legacy.get_file(uploaded_file.name))
|
| 121 |
+
state_name = uploaded_file.state.name
|
| 122 |
+
|
| 123 |
+
if state_name == "ACTIVE":
|
| 124 |
+
logger.info("[Gemini] Video processing complete. Ready for inference.")
|
| 125 |
+
break
|
| 126 |
+
elif state_name == "FAILED":
|
| 127 |
+
logger.error(f"[Gemini] Video processing failed on server side.")
|
| 128 |
+
yield "ERROR: Google failed to process video."
|
| 129 |
+
return
|
| 130 |
+
|
| 131 |
+
if time.time() - wait_start > 300: # 5 minute timeout
|
| 132 |
+
logger.error("[Gemini] Video processing timed out.")
|
| 133 |
+
yield "ERROR: Video processing timed out."
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
logger.info(f"[Gemini] Processing video... (State: {state_name})")
|
| 137 |
+
yield "Processing video on Google servers..."
|
| 138 |
+
await asyncio.sleep(5)
|
| 139 |
+
|
| 140 |
+
# 3. Prepare Inference
|
| 141 |
+
model_name = gemini_config.get("model_name") or "models/gemini-2.0-flash-exp"
|
| 142 |
+
model = genai_legacy.GenerativeModel(model_name)
|
| 143 |
+
toon_schema = SCHEMA_REASONING if include_comments else SCHEMA_SIMPLE
|
| 144 |
+
score_instructions = SCORE_INSTRUCTIONS_REASONING if include_comments else SCORE_INSTRUCTIONS_SIMPLE
|
| 145 |
+
|
| 146 |
+
raw_text = ""
|
| 147 |
+
prompt_used = ""
|
| 148 |
+
gen_config = {"temperature": 0.1}
|
| 149 |
+
|
| 150 |
+
logger.info(f"[Gemini] Starting inference with method: {reasoning_method}")
|
| 151 |
+
|
| 152 |
+
if reasoning_method == "fcot":
|
| 153 |
+
yield "Starting FCoT (Gemini)..."
|
| 154 |
+
chat = model.start_chat(history=[])
|
| 155 |
+
|
| 156 |
+
macro_prompt = FCOT_MACRO_PROMPT.format(caption=caption, transcript=transcript)
|
| 157 |
+
logger.info("[Gemini] Sending Macro Prompt...")
|
| 158 |
+
res1 = await loop.run_in_executor(None, lambda: chat.send_message([uploaded_file, macro_prompt], safety_settings=safety_settings))
|
| 159 |
+
macro_hypothesis = res1.text
|
| 160 |
+
yield f"Hypothesis: {macro_hypothesis[:100]}...\n"
|
| 161 |
+
|
| 162 |
+
meso_prompt = FCOT_MESO_PROMPT.format(macro_hypothesis=macro_hypothesis)
|
| 163 |
+
logger.info("[Gemini] Sending Meso Prompt...")
|
| 164 |
+
res2 = await loop.run_in_executor(None, lambda: chat.send_message(meso_prompt, safety_settings=safety_settings))
|
| 165 |
+
|
| 166 |
+
synthesis_prompt = FCOT_SYNTHESIS_PROMPT.format(toon_schema=toon_schema, score_instructions=score_instructions)
|
| 167 |
+
logger.info("[Gemini] Sending Synthesis Prompt...")
|
| 168 |
+
res3 = await loop.run_in_executor(None, lambda: chat.send_message(synthesis_prompt, safety_settings=safety_settings))
|
| 169 |
+
|
| 170 |
+
raw_text = res3.text
|
| 171 |
+
prompt_used = f"FCoT:\n{macro_prompt}\n..."
|
| 172 |
+
else:
|
| 173 |
+
prompt_text = LABELING_PROMPT_TEMPLATE.format(caption=caption, transcript=transcript, toon_schema=toon_schema, score_instructions=score_instructions)
|
| 174 |
+
prompt_used = prompt_text
|
| 175 |
+
yield f"Generating Labels ({model_name})..."
|
| 176 |
+
logger.info("[Gemini] Sending standard generation request...")
|
| 177 |
+
response = await loop.run_in_executor(
|
| 178 |
+
None,
|
| 179 |
+
lambda: model.generate_content([prompt_text, uploaded_file], generation_config=gen_config, safety_settings=safety_settings)
|
| 180 |
+
)
|
| 181 |
+
raw_text = response.text
|
| 182 |
+
|
| 183 |
+
# Log response info
|
| 184 |
+
logger.info(f"[Gemini] Response received. Length: {len(raw_text)}")
|
| 185 |
+
if not raw_text:
|
| 186 |
+
yield "Model returned empty response (Check API quota or safety)."
|
| 187 |
+
yield {"error": "Empty Response - likely safety block"}
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
parsed_data = parse_veracity_toon(raw_text)
|
| 191 |
+
if parsed_data['veracity_vectors']['visual_integrity_score'] == '0':
|
| 192 |
+
yield "Auto-Repairing output..."
|
| 193 |
+
raw_text = await attempt_toon_repair(raw_text, toon_schema, None, 'gemini', gemini_config)
|
| 194 |
+
parsed_data = parse_veracity_toon(raw_text)
|
| 195 |
+
|
| 196 |
+
yield {"raw_toon": raw_text, "parsed_data": parsed_data, "prompt_used": prompt_used}
|
| 197 |
+
|
| 198 |
+
# Cleanup
|
| 199 |
+
try:
|
| 200 |
+
logger.info(f"[Gemini] Deleting remote file {uploaded_file.name}")
|
| 201 |
+
await loop.run_in_executor(None, lambda: genai_legacy.delete_file(name=uploaded_file.name))
|
| 202 |
+
except Exception as cleanup_err:
|
| 203 |
+
logger.warning(f"Failed to cleanup file: {cleanup_err}")
|
| 204 |
+
|
| 205 |
+
except Exception as e:
|
| 206 |
+
logger.error(f"Gemini Pipeline Error: {e}", exc_info=True)
|
| 207 |
+
yield f"ERROR (Gemini): {e}"
|
| 208 |
+
|
| 209 |
+
async def run_vertex_labeling_pipeline(video_path: str, caption: str, transcript: str, vertex_config: dict, include_comments: bool, reasoning_method: str = "cot"):
|
| 210 |
+
if genai is None:
|
| 211 |
+
yield "ERROR: 'google-genai' not installed.\n"
|
| 212 |
+
return
|
| 213 |
+
|
| 214 |
+
project_id = vertex_config.get("project_id")
|
| 215 |
+
if not project_id:
|
| 216 |
+
yield "ERROR: No Vertex Project ID."
|
| 217 |
+
return
|
| 218 |
+
|
| 219 |
+
logger.info(f"[Vertex] Initializing for project {project_id}")
|
| 220 |
+
|
| 221 |
+
safety_settings = [
|
| 222 |
+
SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="BLOCK_ONLY_HIGH"),
|
| 223 |
+
SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_ONLY_HIGH"),
|
| 224 |
+
SafetySetting(category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="BLOCK_ONLY_HIGH"),
|
| 225 |
+
SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_ONLY_HIGH"),
|
| 226 |
+
]
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
client = genai.Client(vertexai=True, project=project_id, location=vertex_config.get("location", "us-central1"))
|
| 230 |
+
|
| 231 |
+
logger.info(f"[Vertex] Reading local video file: {video_path}")
|
| 232 |
+
with open(video_path, 'rb') as f: video_bytes = f.read()
|
| 233 |
+
video_part = Part.from_bytes(data=video_bytes, mime_type="video/mp4")
|
| 234 |
+
|
| 235 |
+
toon_schema = SCHEMA_REASONING if include_comments else SCHEMA_SIMPLE
|
| 236 |
+
score_instructions = SCORE_INSTRUCTIONS_REASONING if include_comments else SCORE_INSTRUCTIONS_SIMPLE
|
| 237 |
+
model_name = vertex_config.get("model_name", "gemini-2.5-flash-lite")
|
| 238 |
+
|
| 239 |
+
raw_text = ""
|
| 240 |
+
prompt_used = ""
|
| 241 |
+
loop = asyncio.get_event_loop()
|
| 242 |
+
config = GenerateContentConfig(
|
| 243 |
+
temperature=0.1,
|
| 244 |
+
response_mime_type="text/plain",
|
| 245 |
+
tools=[Tool(google_search=GoogleSearch())],
|
| 246 |
+
safety_settings=safety_settings
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
logger.info(f"[Vertex] Starting inference with {model_name}")
|
| 250 |
+
|
| 251 |
+
if reasoning_method == "fcot":
|
| 252 |
+
yield "Starting FCoT (Vertex)..."
|
| 253 |
+
chat = client.chats.create(model=model_name, config=config)
|
| 254 |
+
|
| 255 |
+
macro_prompt = FCOT_MACRO_PROMPT.format(caption=caption, transcript=transcript)
|
| 256 |
+
logger.info("[Vertex] Sending Macro Prompt...")
|
| 257 |
+
res1 = await loop.run_in_executor(None, lambda: chat.send_message([video_part, macro_prompt]))
|
| 258 |
+
macro_hypothesis = res1.text
|
| 259 |
+
yield f"Hypothesis: {macro_hypothesis[:80]}...\n"
|
| 260 |
+
|
| 261 |
+
meso_prompt = FCOT_MESO_PROMPT.format(macro_hypothesis=macro_hypothesis)
|
| 262 |
+
logger.info("[Vertex] Sending Meso Prompt...")
|
| 263 |
+
res2 = await loop.run_in_executor(None, lambda: chat.send_message(meso_prompt))
|
| 264 |
+
|
| 265 |
+
synthesis_prompt = FCOT_SYNTHESIS_PROMPT.format(toon_schema=toon_schema, score_instructions=score_instructions)
|
| 266 |
+
logger.info("[Vertex] Sending Synthesis Prompt...")
|
| 267 |
+
res3 = await loop.run_in_executor(None, lambda: chat.send_message(synthesis_prompt))
|
| 268 |
+
|
| 269 |
+
raw_text = res3.text
|
| 270 |
+
prompt_used = f"FCoT (Vertex):\n{macro_prompt}..."
|
| 271 |
+
|
| 272 |
+
else:
|
| 273 |
+
prompt_text = LABELING_PROMPT_TEMPLATE.format(caption=caption, transcript=transcript, toon_schema=toon_schema, score_instructions=score_instructions)
|
| 274 |
+
prompt_used = prompt_text
|
| 275 |
+
yield f"Generating Labels ({model_name})..."
|
| 276 |
+
logger.info("[Vertex] Sending standard generation request...")
|
| 277 |
+
response = await loop.run_in_executor(
|
| 278 |
+
None,
|
| 279 |
+
lambda: client.models.generate_content(model=model_name, contents=[video_part, prompt_text], config=config)
|
| 280 |
+
)
|
| 281 |
+
raw_text = response.text
|
| 282 |
+
|
| 283 |
+
logger.info(f"[Vertex] Response Length: {len(raw_text)}")
|
| 284 |
+
if not raw_text:
|
| 285 |
+
yield "Model returned empty response."
|
| 286 |
+
yield {"error": "Empty Response"}
|
| 287 |
+
return
|
| 288 |
+
|
| 289 |
+
parsed_data = parse_veracity_toon(raw_text)
|
| 290 |
+
if parsed_data['veracity_vectors']['visual_integrity_score'] == '0':
|
| 291 |
+
yield "Auto-Repairing output..."
|
| 292 |
+
raw_text = await attempt_toon_repair(raw_text, toon_schema, client, 'vertex', vertex_config)
|
| 293 |
+
parsed_data = parse_veracity_toon(raw_text)
|
| 294 |
+
|
| 295 |
+
yield {"raw_toon": raw_text, "parsed_data": parsed_data, "prompt_used": prompt_used}
|
| 296 |
+
|
| 297 |
+
except Exception as e:
|
| 298 |
+
yield f"ERROR (Vertex): {e}"
|
| 299 |
+
logger.error("Vertex Labeling Error", exc_info=True)
|
| 300 |
+
|
| 301 |
+
async def run_gemini_pipeline(video_path, question, checks, gemini_config, generation_config=None):
|
| 302 |
+
yield "Legacy pipeline not fully supported in HF Space."
|
| 303 |
+
|
| 304 |
+
async def run_vertex_pipeline(video_path, question, checks, vertex_config, generation_config=None):
|
| 305 |
+
yield "Legacy pipeline not fully supported in HF Space."
|
src/labeling_logic.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# labeling_logic.py
|
| 2 |
+
|
| 3 |
+
LABELING_PROMPT_TEMPLATE = """
|
| 4 |
+
You are an AI Factuality Assessment Agent operating under the "Ali Arsanjani Factuality Factors" framework.
|
| 5 |
+
Your goal is to mass-label video content, quantifying "Veracity Vectors" and "Modality Alignment".
|
| 6 |
+
|
| 7 |
+
**INPUT DATA:**
|
| 8 |
+
- **User Caption:** "{caption}"
|
| 9 |
+
- **Audio Transcript:** "{transcript}"
|
| 10 |
+
- **Visuals:** (Provided in video context)
|
| 11 |
+
|
| 12 |
+
**INSTRUCTIONS:**
|
| 13 |
+
1. **Grounding:** Cross-reference claims in the transcript with your internal knowledge base (and tools if active).
|
| 14 |
+
2. **Chain of Thought (<thinking>):** You MUST think step-by-step inside a `<thinking>` block before generating output.
|
| 15 |
+
* Analyze *Visual Integrity* (Artifacts, edits).
|
| 16 |
+
* Analyze *Audio Integrity* (Voice cloning, sync).
|
| 17 |
+
* Analyze *Modality Alignment* (Does video match audio? Does caption match content? Does audio match caption?).
|
| 18 |
+
* Analyze *Logic* (Fallacies, gaps).
|
| 19 |
+
* Determine *Disinformation* classification.
|
| 20 |
+
3. **Output Format:** Output strictly in **TOON** format (Token-Oriented Object Notation) as defined below.
|
| 21 |
+
|
| 22 |
+
**CRITICAL CONSTRAINTS:**
|
| 23 |
+
- Do NOT repeat the input data.
|
| 24 |
+
- START your response IMMEDIATELY with the `<thinking>` tag.
|
| 25 |
+
- **DO NOT use Markdown code blocks.** (Output plain text only).
|
| 26 |
+
- Use strict `Key : Type [ Count ] {{ Headers }} :` format followed by data lines.
|
| 27 |
+
- Strings containing commas MUST be quoted.
|
| 28 |
+
- ALL scores must be filled (use 0 if unsure, do not leave blank).
|
| 29 |
+
- **MODALITY SCORING:** You must provide 3 distinct alignment scores: Video-Audio, Video-Caption, and Audio-Caption.
|
| 30 |
+
|
| 31 |
+
**TOON SCHEMA:**
|
| 32 |
+
{toon_schema}
|
| 33 |
+
|
| 34 |
+
{score_instructions}
|
| 35 |
+
|
| 36 |
+
**RESPONSE:**
|
| 37 |
+
<thinking>
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
SCORE_INSTRUCTIONS_REASONING = """
|
| 41 |
+
**Constraints:**
|
| 42 |
+
1. Provide specific reasoning for EACH score in the `vectors` and `modalities` tables.
|
| 43 |
+
2. Ensure strings are properly quoted.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
SCORE_INSTRUCTIONS_SIMPLE = """
|
| 47 |
+
**Constraint:** Focus on objective measurements. Keep text concise.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
SCHEMA_SIMPLE = """summary: text[1]{text}:
|
| 51 |
+
"Brief neutral summary of the video events"
|
| 52 |
+
|
| 53 |
+
vectors: scores[1]{visual,audio,source,logic,emotion}:
|
| 54 |
+
(Int 1-10),(Int 1-10),(Int 1-10),(Int 1-10),(Int 1-10)
|
| 55 |
+
*Scale: 1=Fake/Malicious, 10=Authentic/Neutral*
|
| 56 |
+
|
| 57 |
+
modalities: scores[1]{video_audio_score,video_caption_score,audio_caption_score}:
|
| 58 |
+
(Int 1-10),(Int 1-10),(Int 1-10)
|
| 59 |
+
*Scale: 1=Mismatch, 10=Perfect Match*
|
| 60 |
+
|
| 61 |
+
factuality: factors[1]{accuracy,gap,grounding}:
|
| 62 |
+
(Verified/Misleading/False),"Missing evidence description","Grounding check results"
|
| 63 |
+
|
| 64 |
+
disinfo: analysis[1]{class,intent,threat}:
|
| 65 |
+
(None/Misinfo/Disinfo/Satire),(Political/Commercial/None),(Deepfake/Recontextualization/None)
|
| 66 |
+
|
| 67 |
+
final: assessment[1]{score,reasoning}:
|
| 68 |
+
(Int 1-100),"Final synthesis of why this score was given"
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
SCHEMA_REASONING = """
|
| 72 |
+
summary: text[1]{text}:
|
| 73 |
+
"Brief neutral summary of the video events"
|
| 74 |
+
|
| 75 |
+
vectors: details[5]{category,score,reasoning}:
|
| 76 |
+
Visual,(Int 1-10),"Reasoning for visual score"
|
| 77 |
+
Audio,(Int 1-10),"Reasoning for audio score"
|
| 78 |
+
Source,(Int 1-10),"Reasoning for source credibility"
|
| 79 |
+
Logic,(Int 1-10),"Reasoning for logical consistency"
|
| 80 |
+
Emotion,(Int 1-10),"Reasoning for emotional manipulation"
|
| 81 |
+
|
| 82 |
+
modalities: details[3]{category,score,reasoning}:
|
| 83 |
+
VideoAudio,(Int 1-10),"Reasoning for video-to-audio alignment"
|
| 84 |
+
VideoCaption,(Int 1-10),"Reasoning for video-to-caption alignment"
|
| 85 |
+
AudioCaption,(Int 1-10),"Reasoning for audio-to-caption alignment"
|
| 86 |
+
|
| 87 |
+
factuality: factors[1]{accuracy,gap,grounding}:
|
| 88 |
+
(Verified/Misleading/False),"Missing evidence description","Grounding check results"
|
| 89 |
+
|
| 90 |
+
disinfo: analysis[1]{class,intent,threat}:
|
| 91 |
+
(None/Misinfo/Disinfo/Satire),(Political/Commercial/None),(Deepfake/Recontextualization/None)
|
| 92 |
+
|
| 93 |
+
final: assessment[1]{score,reasoning}:
|
| 94 |
+
(Int 1-100),"Final synthesis of why this score was given"
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
FCOT_MACRO_PROMPT = """
|
| 98 |
+
**Fractal Chain of Thought - Stage 1: Macro-Scale Hypothesis (Wide Aperture)**
|
| 99 |
+
|
| 100 |
+
You are analyzing a video for factuality.
|
| 101 |
+
**Context:** Caption: "{caption}" | Transcript: "{transcript}"
|
| 102 |
+
|
| 103 |
+
1. **Global Scan**: Observe the video, audio, and caption as a whole entity.
|
| 104 |
+
2. **Context Aperture**: Wide. Assess the overall intent (Humor, Information, Political, Social) and the setting.
|
| 105 |
+
3. **Macro Hypothesis**: Formulate a high-level hypothesis about the veracity. (e.g., "The video is likely authentic but the caption misrepresents the location" or "The audio quality suggests synthetic generation").
|
| 106 |
+
|
| 107 |
+
**Objective**: Maximize **Coverage** (broadly explore potential angles of manipulation).
|
| 108 |
+
|
| 109 |
+
**Output**: A concise paragraph summarizing the "Macro Hypothesis".
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
FCOT_MESO_PROMPT = """
|
| 113 |
+
**Fractal Chain of Thought - Stage 2: Meso-Scale Expansion (Recursive Verification)**
|
| 114 |
+
|
| 115 |
+
**Current Macro Hypothesis**: "{macro_hypothesis}"
|
| 116 |
+
|
| 117 |
+
**Action**: Zoom In. Decompose the hypothesis into specific verification branches.
|
| 118 |
+
Perform the following checks recursively:
|
| 119 |
+
|
| 120 |
+
1. **Visual Branch**: Look for specific artifacts, lighting inconsistencies, cuts, or deepfake signs.
|
| 121 |
+
2. **Audio Branch**: Analyze lip-sync, background noise consistency, and voice tonality.
|
| 122 |
+
3. **Logical Branch**: Does the visual evidence strictly support the caption's claim? Are there logical fallacies?
|
| 123 |
+
|
| 124 |
+
**Dual-Objective Self-Correction**:
|
| 125 |
+
- **Faithfulness**: Do not hallucinate details not present in the video.
|
| 126 |
+
- **Coverage**: Did you miss any subtle cues?
|
| 127 |
+
|
| 128 |
+
**Output**: Detailed "Micro-Observations" for each branch. If you find contradictions to the Macro Hypothesis, note them explicitly as **"Self-Correction"**.
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
FCOT_SYNTHESIS_PROMPT = """
|
| 132 |
+
**Fractal Chain of Thought - Stage 3: Inter-Scale Consensus & Synthesis**
|
| 133 |
+
|
| 134 |
+
**Action**: Integrate your Macro Hypothesis and Micro-Observations.
|
| 135 |
+
- **Consensus Check**: If Micro-Observations contradict the Macro Hypothesis, prioritize the Micro evidence (Self-Correction).
|
| 136 |
+
- **Compression**: Synthesize the findings into the final structured format.
|
| 137 |
+
|
| 138 |
+
**Output Format**:
|
| 139 |
+
Strictly fill out the following TOON schema based on the consensus. Do not include markdown code blocks.
|
| 140 |
+
|
| 141 |
+
**TOON SCHEMA**:
|
| 142 |
+
{toon_schema}
|
| 143 |
+
|
| 144 |
+
{score_instructions}
|
| 145 |
+
"""
|
src/my_vision_process.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# my_vision_process.py (Stub for HF Spaces / Lite Mode)
|
| 2 |
+
import logging
|
| 3 |
+
|
| 4 |
+
logger = logging.getLogger(__name__)
|
| 5 |
+
|
| 6 |
+
# Dummy client
|
| 7 |
+
client = None
|
| 8 |
+
|
| 9 |
+
def process_vision_info(messages, return_video_kwargs=False, client=None):
|
| 10 |
+
"""
|
| 11 |
+
Stub function to prevent ImportErrors in API-only mode.
|
| 12 |
+
If this is called, it means LITE_MODE logic failed or was bypassed.
|
| 13 |
+
"""
|
| 14 |
+
logger.warning("process_vision_info called in LITE/API environment. Returning empty placeholders.")
|
| 15 |
+
if return_video_kwargs:
|
| 16 |
+
return None, None, {"fps": [0]}
|
| 17 |
+
return None, None
|
src/toon_parser.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# toon_parser.py
|
| 2 |
+
import re
|
| 3 |
+
import logging
|
| 4 |
+
import csv
|
| 5 |
+
from io import StringIO
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
def parse_toon_line(line_def, data_line):
|
| 10 |
+
if not data_line or data_line.isspace():
|
| 11 |
+
return {}
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
reader = csv.reader(StringIO(data_line), skipinitialspace=True)
|
| 15 |
+
try:
|
| 16 |
+
values = next(reader)
|
| 17 |
+
except StopIteration:
|
| 18 |
+
values = []
|
| 19 |
+
|
| 20 |
+
cleaned_values = []
|
| 21 |
+
for v in values:
|
| 22 |
+
v_str = v.strip()
|
| 23 |
+
v_str = v_str.replace('(', '').replace(')', '')
|
| 24 |
+
if '/' in v_str and any(c.isdigit() for c in v_str):
|
| 25 |
+
parts = v_str.split('/')
|
| 26 |
+
if parts[0].strip().isdigit():
|
| 27 |
+
v_str = parts[0].strip()
|
| 28 |
+
cleaned_values.append(v_str)
|
| 29 |
+
|
| 30 |
+
headers = line_def.get('headers', [])
|
| 31 |
+
|
| 32 |
+
if len(cleaned_values) < len(headers):
|
| 33 |
+
cleaned_values += [""] * (len(headers) - len(cleaned_values))
|
| 34 |
+
elif len(cleaned_values) > len(headers):
|
| 35 |
+
cleaned_values = cleaned_values[:len(headers)]
|
| 36 |
+
|
| 37 |
+
return dict(zip(headers, cleaned_values))
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"Error parsing TOON line '{data_line}': {e}")
|
| 40 |
+
return {}
|
| 41 |
+
|
| 42 |
+
def fuzzy_extract_scores(text: str) -> dict:
|
| 43 |
+
scores = {
|
| 44 |
+
'visual': '0', 'audio': '0', 'source': '0', 'logic': '0', 'emotion': '0',
|
| 45 |
+
'video_audio': '0', 'video_caption': '0', 'audio_caption': '0'
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
mappings = [
|
| 49 |
+
('visual', 'visual'),
|
| 50 |
+
('visual.*?integrity', 'visual'),
|
| 51 |
+
('accuracy', 'visual'),
|
| 52 |
+
('audio', 'audio'),
|
| 53 |
+
('source', 'source'),
|
| 54 |
+
('logic', 'logic'),
|
| 55 |
+
('emotion', 'emotion'),
|
| 56 |
+
(r'video.*?audio', 'video_audio'),
|
| 57 |
+
(r'video.*?caption', 'video_caption'),
|
| 58 |
+
(r'audio.*?caption', 'audio_caption')
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
for pattern_str, key in mappings:
|
| 62 |
+
pattern = re.compile(fr'(?i){pattern_str}.*?[:=\-\s\(]+(\b10\b|\b\d\b)(?:/10)?')
|
| 63 |
+
match = pattern.search(text)
|
| 64 |
+
if match:
|
| 65 |
+
if scores[key] == '0':
|
| 66 |
+
scores[key] = match.group(1)
|
| 67 |
+
|
| 68 |
+
return scores
|
| 69 |
+
|
| 70 |
+
def parse_veracity_toon(text: str) -> dict:
|
| 71 |
+
if not text:
|
| 72 |
+
return {}
|
| 73 |
+
|
| 74 |
+
text = re.sub(r'```\w*', '', text)
|
| 75 |
+
text = re.sub(r'```', '', text)
|
| 76 |
+
text = text.strip()
|
| 77 |
+
|
| 78 |
+
parsed_sections = {}
|
| 79 |
+
|
| 80 |
+
block_pattern = re.compile(
|
| 81 |
+
r'([a-zA-Z0-9_]+)\s*:\s*(?:\w+\s*)?(?:\[\s*(\d+)\s*\])?\s*\{\s*(.*?)\s*\}\s*:\s*',
|
| 82 |
+
re.MULTILINE
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
matches = list(block_pattern.finditer(text))
|
| 86 |
+
|
| 87 |
+
for i, match in enumerate(matches):
|
| 88 |
+
key = match.group(1).lower()
|
| 89 |
+
count = int(match.group(2)) if match.group(2) else 1
|
| 90 |
+
headers_str = match.group(3)
|
| 91 |
+
headers = [h.strip().lower() for h in headers_str.split(',')]
|
| 92 |
+
|
| 93 |
+
start_idx = match.end()
|
| 94 |
+
end_idx = matches[i+1].start() if i + 1 < len(matches) else len(text)
|
| 95 |
+
block_content = text[start_idx:end_idx].strip()
|
| 96 |
+
|
| 97 |
+
lines = [line.strip() for line in block_content.splitlines() if line.strip()]
|
| 98 |
+
|
| 99 |
+
data_items = []
|
| 100 |
+
valid_lines = [l for l in lines if len(l) > 1]
|
| 101 |
+
|
| 102 |
+
for line in valid_lines[:count]:
|
| 103 |
+
item = parse_toon_line({'key': key, 'headers': headers}, line)
|
| 104 |
+
data_items.append(item)
|
| 105 |
+
|
| 106 |
+
if count == 1 and data_items:
|
| 107 |
+
parsed_sections[key] = data_items[0]
|
| 108 |
+
else:
|
| 109 |
+
parsed_sections[key] = data_items
|
| 110 |
+
|
| 111 |
+
flat_result = {
|
| 112 |
+
'veracity_vectors': {
|
| 113 |
+
'visual_integrity_score': '0',
|
| 114 |
+
'audio_integrity_score': '0',
|
| 115 |
+
'source_credibility_score': '0',
|
| 116 |
+
'logical_consistency_score': '0',
|
| 117 |
+
'emotional_manipulation_score': '0'
|
| 118 |
+
},
|
| 119 |
+
'modalities': {
|
| 120 |
+
'video_audio_score': '0',
|
| 121 |
+
'video_caption_score': '0',
|
| 122 |
+
'audio_caption_score': '0'
|
| 123 |
+
},
|
| 124 |
+
'video_context_summary': '',
|
| 125 |
+
'factuality_factors': {},
|
| 126 |
+
'disinformation_analysis': {},
|
| 127 |
+
'final_assessment': {}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
got_vectors = False
|
| 131 |
+
got_modalities = False
|
| 132 |
+
|
| 133 |
+
vectors_data = parsed_sections.get('vectors', [])
|
| 134 |
+
if isinstance(vectors_data, dict):
|
| 135 |
+
v = vectors_data
|
| 136 |
+
if any(val and val != '0' for val in v.values()):
|
| 137 |
+
if 'visual' in v: flat_result['veracity_vectors']['visual_integrity_score'] = v['visual']
|
| 138 |
+
if 'audio' in v: flat_result['veracity_vectors']['audio_integrity_score'] = v['audio']
|
| 139 |
+
if 'source' in v: flat_result['veracity_vectors']['source_credibility_score'] = v['source']
|
| 140 |
+
if 'logic' in v: flat_result['veracity_vectors']['logical_consistency_score'] = v['logic']
|
| 141 |
+
if 'emotion' in v: flat_result['veracity_vectors']['emotional_manipulation_score'] = v['emotion']
|
| 142 |
+
got_vectors = True
|
| 143 |
+
|
| 144 |
+
elif isinstance(vectors_data, list):
|
| 145 |
+
for item in vectors_data:
|
| 146 |
+
cat = item.get('category', '').lower()
|
| 147 |
+
score = item.get('score', '0')
|
| 148 |
+
if score and score != '0':
|
| 149 |
+
got_vectors = True
|
| 150 |
+
if 'visual' in cat: flat_result['veracity_vectors']['visual_integrity_score'] = score
|
| 151 |
+
elif 'audio' in cat: flat_result['veracity_vectors']['audio_integrity_score'] = score
|
| 152 |
+
elif 'source' in cat: flat_result['veracity_vectors']['source_credibility_score'] = score
|
| 153 |
+
elif 'logic' in cat: flat_result['veracity_vectors']['logical_consistency_score'] = score
|
| 154 |
+
elif 'emotion' in cat: flat_result['veracity_vectors']['emotional_manipulation_score'] = score
|
| 155 |
+
|
| 156 |
+
modalities_data = parsed_sections.get('modalities', [])
|
| 157 |
+
if isinstance(modalities_data, dict):
|
| 158 |
+
m = modalities_data
|
| 159 |
+
for k, v in m.items():
|
| 160 |
+
k_clean = k.lower().replace(' ', '').replace('-', '').replace('_', '')
|
| 161 |
+
if 'videoaudio' in k_clean: flat_result['modalities']['video_audio_score'] = v
|
| 162 |
+
elif 'videocaption' in k_clean: flat_result['modalities']['video_caption_score'] = v
|
| 163 |
+
elif 'audiocaption' in k_clean: flat_result['modalities']['audio_caption_score'] = v
|
| 164 |
+
if v and v != '0': got_modalities = True
|
| 165 |
+
|
| 166 |
+
elif isinstance(modalities_data, list):
|
| 167 |
+
for item in modalities_data:
|
| 168 |
+
cat = item.get('category', '').lower().replace(' ', '').replace('-', '').replace('_', '')
|
| 169 |
+
score = item.get('score', '0')
|
| 170 |
+
if score and score != '0':
|
| 171 |
+
got_modalities = True
|
| 172 |
+
if 'videoaudio' in cat: flat_result['modalities']['video_audio_score'] = score
|
| 173 |
+
elif 'videocaption' in cat: flat_result['modalities']['video_caption_score'] = score
|
| 174 |
+
elif 'audiocaption' in cat: flat_result['modalities']['audio_caption_score'] = score
|
| 175 |
+
|
| 176 |
+
if not got_vectors or not got_modalities:
|
| 177 |
+
fuzzy_scores = fuzzy_extract_scores(text)
|
| 178 |
+
|
| 179 |
+
if not got_vectors:
|
| 180 |
+
flat_result['veracity_vectors']['visual_integrity_score'] = fuzzy_scores['visual']
|
| 181 |
+
flat_result['veracity_vectors']['audio_integrity_score'] = fuzzy_scores['audio']
|
| 182 |
+
flat_result['veracity_vectors']['source_credibility_score'] = fuzzy_scores['source']
|
| 183 |
+
flat_result['veracity_vectors']['logical_consistency_score'] = fuzzy_scores['logic']
|
| 184 |
+
flat_result['veracity_vectors']['emotional_manipulation_score'] = fuzzy_scores['emotion']
|
| 185 |
+
|
| 186 |
+
if not got_modalities:
|
| 187 |
+
flat_result['modalities']['video_audio_score'] = fuzzy_scores['video_audio']
|
| 188 |
+
flat_result['modalities']['video_caption_score'] = fuzzy_scores['video_caption']
|
| 189 |
+
flat_result['modalities']['audio_caption_score'] = fuzzy_scores['audio_caption']
|
| 190 |
+
|
| 191 |
+
f = parsed_sections.get('factuality', {})
|
| 192 |
+
if isinstance(f, list): f = f[0] if f else {}
|
| 193 |
+
flat_result['factuality_factors'] = {
|
| 194 |
+
'claim_accuracy': f.get('accuracy', 'Unverifiable'),
|
| 195 |
+
'evidence_gap': f.get('gap', ''),
|
| 196 |
+
'grounding_check': f.get('grounding', '')
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
d = parsed_sections.get('disinfo', {})
|
| 200 |
+
if isinstance(d, list): d = d[0] if d else {}
|
| 201 |
+
flat_result['disinformation_analysis'] = {
|
| 202 |
+
'classification': d.get('class', 'None'),
|
| 203 |
+
'intent': d.get('intent', 'None'),
|
| 204 |
+
'threat_vector': d.get('threat', 'None')
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
fn = parsed_sections.get('final', {})
|
| 208 |
+
if isinstance(fn, list): fn = fn[0] if fn else {}
|
| 209 |
+
flat_result['final_assessment'] = {
|
| 210 |
+
'veracity_score_total': fn.get('score', '0'),
|
| 211 |
+
'reasoning': fn.get('reasoning', '')
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
s = parsed_sections.get('summary', {})
|
| 215 |
+
if isinstance(s, list): s = s[0] if s else {}
|
| 216 |
+
flat_result['video_context_summary'] = s.get('text', '')
|
| 217 |
+
|
| 218 |
+
flat_result['raw_parsed_structure'] = parsed_sections
|
| 219 |
+
|
| 220 |
+
return flat_result
|
src/transcription.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
LITE_MODE = os.getenv("LITE_MODE", "true").lower() == "true"
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
import whisper
|
| 10 |
+
WHISPER_AVAILABLE = True
|
| 11 |
+
except ImportError:
|
| 12 |
+
WHISPER_AVAILABLE = False
|
| 13 |
+
|
| 14 |
+
transcription_model = None
|
| 15 |
+
|
| 16 |
+
def load_model():
|
| 17 |
+
if LITE_MODE or not WHISPER_AVAILABLE:
|
| 18 |
+
logger.info("LITE_MODE is enabled or Whisper is uninstalled. Skipping Whisper model loading.")
|
| 19 |
+
return
|
| 20 |
+
|
| 21 |
+
global transcription_model
|
| 22 |
+
if transcription_model is None:
|
| 23 |
+
try:
|
| 24 |
+
logger.info("Loading 'base.en' Whisper model for transcription...")
|
| 25 |
+
transcription_model = whisper.load_model("base.en")
|
| 26 |
+
logger.info("Whisper model loaded successfully.")
|
| 27 |
+
except Exception as e:
|
| 28 |
+
logger.error(f"Failed to load Whisper model: {e}", exc_info=True)
|
| 29 |
+
transcription_model = None
|
| 30 |
+
|
| 31 |
+
def generate_transcript(audio_path_str: str) -> str:
|
| 32 |
+
if transcription_model is None:
|
| 33 |
+
logger.warning("Transcription model is not available (API-Lite Mode). Bypassing local transcription.")
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
audio_path = Path(audio_path_str)
|
| 38 |
+
logger.info(f"Starting transcription for: {audio_path.name}")
|
| 39 |
+
|
| 40 |
+
result = transcription_model.transcribe(audio_path_str, verbose=False)
|
| 41 |
+
|
| 42 |
+
vtt_path = audio_path.with_suffix('.vtt')
|
| 43 |
+
|
| 44 |
+
from whisper.utils import get_writer
|
| 45 |
+
writer = get_writer("vtt", str(vtt_path.parent))
|
| 46 |
+
writer(result, str(audio_path.name))
|
| 47 |
+
|
| 48 |
+
logger.info(f"Transcription complete. VTT file saved to: {vtt_path}")
|
| 49 |
+
return str(vtt_path)
|
| 50 |
+
|
| 51 |
+
except Exception as e:
|
| 52 |
+
logger.error(f"An error occurred during transcription for {audio_path_str}: {e}", exc_info=True)
|
| 53 |
+
return None
|
start.sh
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# 1. Start Python FastAPI in the background (Internal Port 8001)
|
| 4 |
+
echo "Starting Python Inference Engine..."
|
| 5 |
+
export PYTHONPATH=$PYTHONPATH:/app/src
|
| 6 |
+
# Use --log-level info to see startup issues
|
| 7 |
+
python -m uvicorn src.app:app --host 127.0.0.1 --port 8001 --log-level info &
|
| 8 |
+
|
| 9 |
+
# Wait longer for Python to initialize, or until port is open
|
| 10 |
+
echo "Waiting for Python backend to initialize..."
|
| 11 |
+
timeout=30
|
| 12 |
+
while ! curl -s http://127.0.0.1:8001/ > /dev/null; do
|
| 13 |
+
sleep 2
|
| 14 |
+
timeout=$((timeout-2))
|
| 15 |
+
if [ $timeout -le 0 ]; then
|
| 16 |
+
echo "Python backend failed to start on time. Logs might show why."
|
| 17 |
+
break
|
| 18 |
+
fi
|
| 19 |
+
done
|
| 20 |
+
|
| 21 |
+
# 2. Start Golang Web Server (Public Port 7860)
|
| 22 |
+
echo "Starting Go Web Server..."
|
| 23 |
+
/app/vchat-server
|