feat(peertube): Add web interface for video analysis

- Create standalone web UI at /peertube-analyse/
- Add CGI backend (peertube-analyse, peertube-analyse-status)
- Add RPCD methods: analyse, analyse_status
- Update portal with Intelligence & Analyse section
- Expose via analyse.gk2.secubox.in with SSL

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-21 18:34:26 +01:00
parent 389e4a58e0
commit f76dfe8a67
8 changed files with 1193 additions and 2 deletions

View File

@ -2992,3 +2992,35 @@ git checkout HEAD -- index.html
- Files:
- `secubox-app-peertube/files/usr/sbin/peertube-analyse` (778 lines)
- `secubox-app-peertube/Makefile` (updated)
64. **PeerTube Analyse Web Interface & Portal (2026-02-21)**
- Created standalone web interface for PeerTube video analysis.
- **URL**: https://analyse.gk2.secubox.in/peertube-analyse/
- **Web Interface Features**:
- Cyberpunk-themed design matching SecuBox portal
- Video URL input with example presets
- Options: Force Whisper, No AI Analysis, Model/Language selection
- Progress status bar with live polling
- Tabbed results: Analysis (Markdown), Transcript, Metadata
- Copy to clipboard functionality
- **CGI Backend**:
- `/cgi-bin/peertube-analyse` — Start analysis (POST)
- `/cgi-bin/peertube-analyse-status` — Poll job status (GET)
- Async job system with background processing
- JSON API with job_id for polling
- **RPCD Integration**:
- Added `analyse` and `analyse_status` methods to `luci.peertube`
- ACL permissions updated for read/write access
- **Portal Integration**:
- New "Intelligence & Analyse" section in SecuBox portal
- Added PeerTube Analyse and Radio Stream services
- **HAProxy/SSL**:
- Domain: analyse.gk2.secubox.in
- Let's Encrypt certificate auto-provisioned
- Routing via uhttpd backend (static content)
- Files:
- `secubox-app-peertube/files/www/peertube-analyse/index.html`
- `secubox-app-peertube/files/www/cgi-bin/peertube-analyse`
- `secubox-app-peertube/files/www/cgi-bin/peertube-analyse-status`
- `luci-app-peertube/root/usr/libexec/rpcd/luci.peertube` (updated)
- `luci-app-secubox-portal/root/www/gk2-hub/portal.html` (updated)

View File

@ -530,6 +530,151 @@ method_import_job_status() {
json_dump
}
# Method: analyse - Start video transcript analysis
method_analyse() {
read -r input
json_load "$input"
json_get_var url url ""
json_get_var force_whisper force_whisper "0"
json_get_var no_analyse no_analyse "0"
json_get_var model model "medium"
json_get_var lang lang "fr"
if [ -z "$url" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "URL is required"
json_dump
return
fi
# Check if peertube-analyse exists
if [ ! -x "/usr/sbin/peertube-analyse" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "peertube-analyse not installed"
json_dump
return
fi
# Generate job ID
local job_id="analyse_$(date +%s)_$$"
local output_dir="/tmp/peertube-analyse/${job_id}"
local statusfile="/tmp/peertube-analyse-${job_id}.status"
local resultfile="/tmp/peertube-analyse-${job_id}.json"
mkdir -p "$output_dir"
echo "starting" > "$statusfile"
# Build command args
local args=""
[ "$force_whisper" = "1" ] && args="$args --force-whisper"
[ "$no_analyse" = "1" ] && args="$args --no-analyse"
[ -n "$model" ] && args="$args --model $model"
[ -n "$lang" ] && args="$args --lang $lang"
args="$args --output $output_dir"
# Run in background
(
echo "extracting" > "$statusfile"
# Run the analysis
OUTPUT_BASE="$output_dir" /usr/sbin/peertube-analyse $args "$url" > "/tmp/peertube-analyse-${job_id}.log" 2>&1
local rc=$?
if [ $rc -eq 0 ]; then
echo "completed" > "$statusfile"
# Build result JSON
local meta_file=$(find "$output_dir" -name "*.meta.json" -type f 2>/dev/null | head -1)
local transcript_file=$(find "$output_dir" -name "*.transcript.txt" -type f 2>/dev/null | head -1)
local analysis_file=$(find "$output_dir" -name "*.analyse.md" -type f 2>/dev/null | head -1)
# Create result JSON manually (avoid jshn for large content)
{
echo '{'
echo '"success": true,'
echo '"job_id": "'"$job_id"'",'
# Metadata (use jq if available, otherwise jsonfilter)
if [ -f "$meta_file" ]; then
echo '"metadata": '
cat "$meta_file"
echo ','
else
echo '"metadata": null,'
fi
# Transcript
if [ -f "$transcript_file" ]; then
printf '"transcript": '
cat "$transcript_file" | jq -Rs '.'
echo ','
else
echo '"transcript": null,'
fi
# Analysis
if [ -f "$analysis_file" ]; then
printf '"analysis": '
cat "$analysis_file" | jq -Rs '.'
else
echo '"analysis": null'
fi
echo '}'
} > "$resultfile"
else
echo "failed" > "$statusfile"
echo '{"success": false, "error": "Analysis failed", "job_id": "'"$job_id"'"}' > "$resultfile"
fi
) &
json_init
json_add_boolean "success" 1
json_add_string "message" "Analysis started"
json_add_string "job_id" "$job_id"
json_dump
}
# Method: analyse_status - Get analysis job status/results
method_analyse_status() {
read -r input
json_load "$input"
json_get_var job_id job_id ""
if [ -z "$job_id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "job_id is required"
json_dump
return
fi
local statusfile="/tmp/peertube-analyse-${job_id}.status"
local resultfile="/tmp/peertube-analyse-${job_id}.json"
local logfile="/tmp/peertube-analyse-${job_id}.log"
local status="unknown"
[ -f "$statusfile" ] && status=$(cat "$statusfile")
# If completed, return the full result
if [ "$status" = "completed" ] && [ -f "$resultfile" ]; then
cat "$resultfile"
return
fi
# Otherwise return status
local logs=""
[ -f "$logfile" ] && logs=$(tail -10 "$logfile")
json_init
json_add_string "status" "$status"
json_add_string "job_id" "$job_id"
json_add_string "logs" "$logs"
json_dump
}
# Method: import_status - Check import progress
method_import_status() {
local import_dir="/var/lib/peertube/storage/tmp/import"
@ -589,6 +734,16 @@ list_methods() {
json_add_object "import_job_status"
json_add_string "job_id" ""
json_close_object
json_add_object "analyse"
json_add_string "url" ""
json_add_string "force_whisper" "0"
json_add_string "no_analyse" "0"
json_add_string "model" "medium"
json_add_string "lang" "fr"
json_close_object
json_add_object "analyse_status"
json_add_string "job_id" ""
json_close_object
json_dump
}
@ -644,6 +799,12 @@ case "$1" in
import_job_status)
method_import_job_status
;;
analyse)
method_analyse
;;
analyse_status)
method_analyse_status
;;
*)
echo '{"error":"Method not found"}'
;;

View File

@ -3,13 +3,13 @@
"description": "Grant access to PeerTube management",
"read": {
"ubus": {
"luci.peertube": ["status", "logs", "import_status", "import_job_status"]
"luci.peertube": ["status", "logs", "import_status", "import_job_status", "analyse_status"]
},
"uci": ["peertube"]
},
"write": {
"ubus": {
"luci.peertube": ["start", "stop", "install", "uninstall", "update", "emancipate", "live_enable", "live_disable", "configure_haproxy", "import_video"]
"luci.peertube": ["start", "stop", "install", "uninstall", "update", "emancipate", "live_enable", "live_disable", "configure_haproxy", "import_video", "analyse"]
},
"uci": ["peertube"]
}

View File

@ -200,6 +200,20 @@
</a>
</div>
<h2 class="section-title">Intelligence & Analyse</h2>
<div class="services-grid">
<a href="https://analyse.gk2.secubox.in/" class="service-card" target="_blank">
<div class="service-icon">🎬</div>
<div class="service-name">PeerTube Analyse</div>
<div class="service-url">analyse.gk2.secubox.in</div>
</a>
<a href="https://stream.gk2.secubox.in/" class="service-card" target="_blank">
<div class="service-icon">📻</div>
<div class="service-name">Radio Stream</div>
<div class="service-url">stream.gk2.secubox.in</div>
</a>
</div>
<h2 class="section-title">Administration</h2>
<div class="services-grid">
<a href="https://admin.gk2.secubox.in/" class="service-card" target="_blank">

View File

@ -42,6 +42,13 @@ define Package/secubox-app-peertube/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/peertubectl $(1)/usr/sbin/peertubectl
$(INSTALL_BIN) ./files/usr/sbin/peertube-analyse $(1)/usr/sbin/peertube-analyse
$(INSTALL_DIR) $(1)/www/peertube-analyse
$(INSTALL_DATA) ./files/www/peertube-analyse/index.html $(1)/www/peertube-analyse/
$(INSTALL_DIR) $(1)/www/cgi-bin
$(INSTALL_BIN) ./files/www/cgi-bin/peertube-analyse $(1)/www/cgi-bin/
$(INSTALL_BIN) ./files/www/cgi-bin/peertube-analyse-status $(1)/www/cgi-bin/
endef
$(eval $(call BuildPackage,secubox-app-peertube))

View File

@ -0,0 +1,151 @@
#!/bin/sh
# CGI endpoint for PeerTube video analysis
# Returns JSON response
# Set headers
printf "Content-Type: application/json\r\n"
printf "Access-Control-Allow-Origin: *\r\n"
printf "Access-Control-Allow-Methods: POST, OPTIONS\r\n"
printf "Access-Control-Allow-Headers: Content-Type\r\n"
printf "\r\n"
# Handle OPTIONS (CORS preflight)
if [ "$REQUEST_METHOD" = "OPTIONS" ]; then
exit 0
fi
# Only allow POST
if [ "$REQUEST_METHOD" != "POST" ]; then
echo '{"error": "Method not allowed"}'
exit 0
fi
# Read input
INPUT=$(cat)
# Parse JSON (use jq if available, else jsonfilter)
if command -v jq >/dev/null 2>&1; then
URL=$(echo "$INPUT" | jq -r '.url // empty')
FORCE_WHISPER=$(echo "$INPUT" | jq -r '.force_whisper // "0"')
NO_ANALYSE=$(echo "$INPUT" | jq -r '.no_analyse // "0"')
MODEL=$(echo "$INPUT" | jq -r '.model // "medium"')
LANG=$(echo "$INPUT" | jq -r '.lang // "fr"')
else
URL=$(echo "$INPUT" | jsonfilter -e '@.url' 2>/dev/null)
FORCE_WHISPER=$(echo "$INPUT" | jsonfilter -e '@.force_whisper' 2>/dev/null)
NO_ANALYSE=$(echo "$INPUT" | jsonfilter -e '@.no_analyse' 2>/dev/null)
MODEL=$(echo "$INPUT" | jsonfilter -e '@.model' 2>/dev/null)
LANG=$(echo "$INPUT" | jsonfilter -e '@.lang' 2>/dev/null)
fi
# Validate URL
if [ -z "$URL" ]; then
echo '{"error": "URL is required"}'
exit 0
fi
# Sanitize URL (basic security check)
case "$URL" in
http://*|https://*)
# Valid URL prefix
;;
*)
echo '{"error": "Invalid URL format"}'
exit 0
;;
esac
# Set defaults
[ -z "$MODEL" ] && MODEL="medium"
[ -z "$LANG" ] && LANG="fr"
# Generate job ID
JOB_ID="analyse_$(date +%s)_$$"
OUTPUT_DIR="/tmp/peertube-analyse/${JOB_ID}"
STATUS_FILE="/tmp/peertube-analyse-${JOB_ID}.status"
RESULT_FILE="/tmp/peertube-analyse-${JOB_ID}.json"
LOG_FILE="/tmp/peertube-analyse-${JOB_ID}.log"
mkdir -p "$OUTPUT_DIR"
# Check for existing analysis script
if [ ! -x "/usr/sbin/peertube-analyse" ]; then
echo '{"error": "peertube-analyse not installed"}'
exit 0
fi
# Build command arguments
ARGS=""
[ "$FORCE_WHISPER" = "1" ] || [ "$FORCE_WHISPER" = "true" ] && ARGS="$ARGS --force-whisper"
[ "$NO_ANALYSE" = "1" ] || [ "$NO_ANALYSE" = "true" ] && ARGS="$ARGS --no-analyse"
[ -n "$MODEL" ] && ARGS="$ARGS --model $MODEL"
[ -n "$LANG" ] && ARGS="$ARGS --lang $LANG"
ARGS="$ARGS --output $OUTPUT_DIR"
# Start analysis in background
echo "starting" > "$STATUS_FILE"
(
echo "extracting" > "$STATUS_FILE"
# Run the analysis
OUTPUT_BASE="$OUTPUT_DIR" /usr/sbin/peertube-analyse $ARGS "$URL" > "$LOG_FILE" 2>&1
RC=$?
if [ $RC -eq 0 ]; then
echo "completed" > "$STATUS_FILE"
# Find output files
META_FILE=$(find "$OUTPUT_DIR" -name "*.meta.json" -type f 2>/dev/null | head -1)
TRANSCRIPT_FILE=$(find "$OUTPUT_DIR" -name "*.transcript.txt" -type f 2>/dev/null | head -1)
ANALYSIS_FILE=$(find "$OUTPUT_DIR" -name "*.analyse.md" -type f 2>/dev/null | head -1)
# Build result JSON
{
echo '{'
echo '"success": true,'
echo '"job_id": "'"$JOB_ID"'",'
# Metadata
if [ -f "$META_FILE" ]; then
echo '"metadata": '
cat "$META_FILE"
echo ','
else
echo '"metadata": null,'
fi
# Transcript
if [ -f "$TRANSCRIPT_FILE" ]; then
printf '"transcript": '
if command -v jq >/dev/null 2>&1; then
cat "$TRANSCRIPT_FILE" | jq -Rs '.'
else
printf '"%s"' "$(cat "$TRANSCRIPT_FILE" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/' | tr -d '\n')"
fi
echo ','
else
echo '"transcript": null,'
fi
# Analysis
if [ -f "$ANALYSIS_FILE" ]; then
printf '"analysis": '
if command -v jq >/dev/null 2>&1; then
cat "$ANALYSIS_FILE" | jq -Rs '.'
else
printf '"%s"' "$(cat "$ANALYSIS_FILE" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/' | tr -d '\n')"
fi
else
echo '"analysis": null'
fi
echo '}'
} > "$RESULT_FILE"
else
echo "failed" > "$STATUS_FILE"
echo '{"success": false, "error": "Analysis failed", "job_id": "'"$JOB_ID"'"}' > "$RESULT_FILE"
fi
) &
# Return job ID for polling
echo "{\"success\": true, \"message\": \"Analysis started\", \"job_id\": \"$JOB_ID\"}"

View File

@ -0,0 +1,64 @@
#!/bin/sh
# CGI endpoint for checking PeerTube analysis status
# Returns JSON response
# Set headers
printf "Content-Type: application/json\r\n"
printf "Access-Control-Allow-Origin: *\r\n"
printf "\r\n"
# Get job_id from query string or POST body
JOB_ID=""
if [ -n "$QUERY_STRING" ]; then
JOB_ID=$(echo "$QUERY_STRING" | sed -n 's/.*job_id=\([^&]*\).*/\1/p')
fi
if [ -z "$JOB_ID" ] && [ "$REQUEST_METHOD" = "POST" ]; then
INPUT=$(cat)
if command -v jq >/dev/null 2>&1; then
JOB_ID=$(echo "$INPUT" | jq -r '.job_id // empty')
else
JOB_ID=$(echo "$INPUT" | jsonfilter -e '@.job_id' 2>/dev/null)
fi
fi
if [ -z "$JOB_ID" ]; then
echo '{"error": "job_id is required"}'
exit 0
fi
# Sanitize job_id (only allow alphanumeric and underscore)
JOB_ID=$(echo "$JOB_ID" | tr -cd 'a-zA-Z0-9_')
STATUS_FILE="/tmp/peertube-analyse-${JOB_ID}.status"
RESULT_FILE="/tmp/peertube-analyse-${JOB_ID}.json"
LOG_FILE="/tmp/peertube-analyse-${JOB_ID}.log"
# Check if job exists
if [ ! -f "$STATUS_FILE" ]; then
echo '{"error": "Job not found", "job_id": "'"$JOB_ID"'"}'
exit 0
fi
STATUS=$(cat "$STATUS_FILE" 2>/dev/null || echo "unknown")
# If completed, return full result
if [ "$STATUS" = "completed" ] && [ -f "$RESULT_FILE" ]; then
cat "$RESULT_FILE"
exit 0
fi
# Otherwise return status with logs
LOGS=""
if [ -f "$LOG_FILE" ]; then
LOGS=$(tail -5 "$LOG_FILE" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g')
fi
cat << EOF
{
"status": "$STATUS",
"job_id": "$JOB_ID",
"logs": "$LOGS"
}
EOF

View File

@ -0,0 +1,762 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PeerTube Analyse — SecuBox Intelligence</title>
<style>
:root {
--bg: #0a0a0f;
--surface: #12121a;
--surface2: #1a1a2e;
--border: #252535;
--accent: #6366f1;
--accent2: #8b5cf6;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--text: #e0e0e0;
--muted: #888;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", system-ui, sans-serif;
min-height: 100vh;
line-height: 1.6;
}
body::before {
content: "";
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(99,102,241,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(99,102,241,0.02) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
position: relative;
z-index: 1;
}
.header {
text-align: center;
padding: 40px 0 30px;
border-bottom: 1px solid var(--border);
margin-bottom: 30px;
}
.header-icon {
font-size: 4rem;
margin-bottom: 15px;
display: block;
}
.header h1 {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #fff, var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
.header p {
color: var(--muted);
font-size: 1rem;
}
.input-section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 25px;
margin-bottom: 25px;
}
.input-group {
display: flex;
gap: 12px;
margin-bottom: 15px;
}
.url-input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 18px;
color: var(--text);
font-size: 1rem;
font-family: monospace;
outline: none;
transition: border-color 0.2s;
}
.url-input:focus {
border-color: var(--accent);
}
.url-input::placeholder {
color: var(--muted);
}
.analyse-btn {
background: linear-gradient(135deg, var(--accent), var(--accent2));
border: none;
border-radius: 10px;
padding: 14px 28px;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: transform 0.2s, box-shadow 0.2s;
}
.analyse-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99,102,241,0.4);
}
.analyse-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.options {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.option {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
color: var(--muted);
}
.option input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--accent);
}
.option select {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
color: var(--text);
font-size: 0.85rem;
}
.status-bar {
display: none;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
}
.status-bar.active { display: block; }
.status-bar.loading { border-color: var(--accent); }
.status-bar.success { border-color: var(--success); }
.status-bar.error { border-color: var(--error); }
.status-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.status-icon {
font-size: 1.5rem;
}
.status-title {
font-weight: 600;
font-size: 1.1rem;
}
.status-message {
color: var(--muted);
font-size: 0.9rem;
}
.progress-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
margin-top: 15px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent2));
border-radius: 2px;
width: 0%;
transition: width 0.3s;
}
.results {
display: none;
}
.results.active { display: block; }
.result-tabs {
display: flex;
gap: 5px;
margin-bottom: 20px;
border-bottom: 1px solid var(--border);
padding-bottom: 5px;
}
.tab-btn {
background: transparent;
border: none;
padding: 12px 20px;
color: var(--muted);
font-size: 0.95rem;
cursor: pointer;
border-radius: 8px 8px 0 0;
transition: all 0.2s;
}
.tab-btn:hover {
color: var(--text);
background: var(--surface);
}
.tab-btn.active {
color: var(--accent);
background: var(--surface);
border-bottom: 2px solid var(--accent);
}
.tab-content {
display: none;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 25px;
min-height: 400px;
}
.tab-content.active { display: block; }
.metadata-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.meta-card {
background: var(--bg);
border-radius: 10px;
padding: 15px;
}
.meta-label {
font-size: 0.75rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 5px;
}
.meta-value {
font-size: 1rem;
font-weight: 500;
word-break: break-word;
}
.transcript-box {
background: var(--bg);
border-radius: 10px;
padding: 20px;
max-height: 500px;
overflow-y: auto;
font-size: 0.95rem;
line-height: 1.8;
white-space: pre-wrap;
}
.analysis-content {
line-height: 1.8;
}
.analysis-content h1, .analysis-content h2, .analysis-content h3 {
color: var(--accent);
margin: 20px 0 10px;
}
.analysis-content h1 { font-size: 1.5rem; }
.analysis-content h2 { font-size: 1.25rem; }
.analysis-content h3 { font-size: 1.1rem; }
.analysis-content p { margin-bottom: 15px; }
.analysis-content ul, .analysis-content ol {
margin: 10px 0 15px 25px;
}
.analysis-content li { margin-bottom: 8px; }
.analysis-content strong { color: #fff; }
.analysis-content blockquote {
border-left: 3px solid var(--accent);
padding-left: 15px;
color: var(--muted);
margin: 15px 0;
}
.copy-btn {
position: absolute;
top: 15px;
right: 15px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
color: var(--muted);
font-size: 0.8rem;
cursor: pointer;
}
.copy-btn:hover {
color: var(--text);
border-color: var(--accent);
}
.tab-panel {
position: relative;
}
.footer {
text-align: center;
padding: 40px 0;
color: var(--muted);
font-size: 0.85rem;
}
.footer a {
color: var(--accent);
text-decoration: none;
}
.examples {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--border);
}
.examples-title {
font-size: 0.8rem;
color: var(--muted);
margin-bottom: 8px;
}
.example-link {
display: inline-block;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 12px;
margin: 3px;
font-size: 0.8rem;
color: var(--accent);
text-decoration: none;
cursor: pointer;
}
.example-link:hover {
border-color: var(--accent);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@media (max-width: 600px) {
.input-group { flex-direction: column; }
.analyse-btn { width: 100%; justify-content: center; }
.options { flex-direction: column; gap: 12px; }
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<span class="header-icon">🎬</span>
<h1>PeerTube Analyse</h1>
<p>Extraction de transcript et analyse IA — SecuBox Intelligence</p>
</header>
<section class="input-section">
<div class="input-group">
<input type="text" id="videoUrl" class="url-input"
placeholder="https://tube.gk2.secubox.in/w/..."
autocomplete="off">
<button id="analyseBtn" class="analyse-btn">
<span>Analyser</span>
</button>
</div>
<div class="options">
<label class="option">
<input type="checkbox" id="forceWhisper">
<span>Forcer Whisper</span>
</label>
<label class="option">
<input type="checkbox" id="noAnalyse">
<span>Sans analyse IA</span>
</label>
<label class="option">
<span>Modele:</span>
<select id="whisperModel">
<option value="tiny">tiny (rapide)</option>
<option value="base">base</option>
<option value="small">small</option>
<option value="medium" selected>medium</option>
<option value="large-v3">large-v3 (precis)</option>
</select>
</label>
<label class="option">
<span>Langue:</span>
<select id="whisperLang">
<option value="fr" selected>Francais</option>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="es">Espanol</option>
<option value="auto">Auto</option>
</select>
</label>
</div>
<div class="examples">
<div class="examples-title">Exemples:</div>
<span class="example-link" data-url="https://tube.gk2.secubox.in/w/">Derniere video</span>
</div>
</section>
<section id="statusBar" class="status-bar">
<div class="status-header">
<span id="statusIcon" class="status-icon"></span>
<span id="statusTitle" class="status-title"></span>
</div>
<div id="statusMessage" class="status-message"></div>
<div class="progress-bar">
<div id="progressFill" class="progress-fill"></div>
</div>
</section>
<section id="results" class="results">
<div class="result-tabs">
<button class="tab-btn active" data-tab="analysis">Analyse</button>
<button class="tab-btn" data-tab="transcript">Transcript</button>
<button class="tab-btn" data-tab="metadata">Metadonnees</button>
</div>
<div id="tab-analysis" class="tab-content active tab-panel">
<button class="copy-btn" onclick="copyContent('analysis')">Copier</button>
<div id="analysisContent" class="analysis-content"></div>
</div>
<div id="tab-transcript" class="tab-content tab-panel">
<button class="copy-btn" onclick="copyContent('transcript')">Copier</button>
<div id="transcriptContent" class="transcript-box"></div>
</div>
<div id="tab-metadata" class="tab-content tab-panel">
<button class="copy-btn" onclick="copyContent('metadata')">Copier JSON</button>
<div id="metadataContent" class="metadata-grid"></div>
</div>
</section>
<footer class="footer">
<p>SecuBox Intelligence Module &mdash; <a href="/admin/secubox/dashboard">Dashboard</a></p>
</footer>
</div>
<script>
let currentData = {};
// DOM elements
const videoUrl = document.getElementById('videoUrl');
const analyseBtn = document.getElementById('analyseBtn');
const statusBar = document.getElementById('statusBar');
const statusIcon = document.getElementById('statusIcon');
const statusTitle = document.getElementById('statusTitle');
const statusMessage = document.getElementById('statusMessage');
const progressFill = document.getElementById('progressFill');
const results = document.getElementById('results');
// Tab handling
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
// Example links
document.querySelectorAll('.example-link').forEach(link => {
link.addEventListener('click', () => {
videoUrl.value = link.dataset.url;
videoUrl.focus();
});
});
// Status updates
function setStatus(type, icon, title, message, progress = 0) {
statusBar.className = 'status-bar active ' + type;
statusIcon.textContent = icon;
statusTitle.textContent = title;
statusMessage.textContent = message;
progressFill.style.width = progress + '%';
}
// API base path (same origin CGI)
const API_BASE = '/cgi-bin';
// Markdown to HTML (simple)
function markdownToHtml(md) {
return md
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/^\- (.*$)/gm, '<li>$1</li>')
.replace(/^\d+\. (.*$)/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
.replace(/\n\n/g, '</p><p>')
.replace(/^(?!<[hulo])/gm, '<p>')
.replace(/(?<![>])$/gm, '</p>')
.replace(/<p><\/p>/g, '')
.replace(/---/g, '<hr>');
}
// Copy to clipboard
function copyContent(type) {
let text = '';
if (type === 'analysis') {
text = currentData.analysis || '';
} else if (type === 'transcript') {
text = currentData.transcript || '';
} else if (type === 'metadata') {
text = JSON.stringify(currentData.metadata, null, 2);
}
navigator.clipboard.writeText(text).then(() => {
alert('Copie dans le presse-papiers!');
});
}
// Display results
function displayResults(data) {
currentData = data;
results.classList.add('active');
// Analysis
if (data.analysis) {
document.getElementById('analysisContent').innerHTML = markdownToHtml(data.analysis);
} else {
document.getElementById('analysisContent').innerHTML = '<p style="color:var(--muted)">Analyse non disponible</p>';
}
// Transcript
if (data.transcript) {
document.getElementById('transcriptContent').textContent = data.transcript;
} else {
document.getElementById('transcriptContent').textContent = 'Transcript non disponible';
}
// Metadata
if (data.metadata) {
const meta = data.metadata;
const grid = document.getElementById('metadataContent');
grid.innerHTML = `
<div class="meta-card">
<div class="meta-label">Titre</div>
<div class="meta-value">${meta.title || 'N/A'}</div>
</div>
<div class="meta-card">
<div class="meta-label">Duree</div>
<div class="meta-value">${meta.duration_string || meta.duration || 'N/A'}</div>
</div>
<div class="meta-card">
<div class="meta-label">Auteur</div>
<div class="meta-value">${meta.uploader || meta.channel || 'N/A'}</div>
</div>
<div class="meta-card">
<div class="meta-label">Date</div>
<div class="meta-value">${meta.upload_date || 'N/A'}</div>
</div>
<div class="meta-card">
<div class="meta-label">Vues</div>
<div class="meta-value">${meta.view_count || 'N/A'}</div>
</div>
<div class="meta-card">
<div class="meta-label">Tags</div>
<div class="meta-value">${(meta.tags || []).join(', ') || 'Aucun'}</div>
</div>
`;
}
}
// Poll for analysis completion
async function pollAnalysisStatus(jobId, maxAttempts = 180) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000)); // Poll every 2 seconds
const progress = Math.min(20 + (i * 60 / maxAttempts), 90);
if (i < 5) {
setStatus('loading', '🔍', 'Extraction des metadonnees...', 'Connexion a PeerTube', progress);
} else if (i < 30) {
setStatus('loading', '📝', 'Transcription...', 'Whisper traite l\'audio', progress);
} else {
setStatus('loading', '🤖', 'Analyse IA...', 'Claude analyse le contenu', progress);
}
try {
const response = await fetch(`${API_BASE}/peertube-analyse-status?job_id=${encodeURIComponent(jobId)}`);
const result = await response.json();
if (result.error) {
throw new Error(result.error);
}
if (result.status === 'completed' && result.success) {
return result;
} else if (result.status === 'failed') {
throw new Error(result.error || 'Analyse echouee');
}
// Continue polling for 'starting', 'extracting', etc.
} catch (e) {
if (e.message.includes('not found')) throw e;
// Ignore transient network errors
console.log('Poll error (retrying):', e.message);
}
}
throw new Error('Timeout: analyse trop longue (6 minutes max)');
}
// Main analyse function
async function analyse() {
const url = videoUrl.value.trim();
if (!url) {
alert('Veuillez entrer une URL PeerTube');
return;
}
const params = {
url: url,
force_whisper: document.getElementById('forceWhisper').checked,
no_analyse: document.getElementById('noAnalyse').checked,
model: document.getElementById('whisperModel').value,
lang: document.getElementById('whisperLang').value
};
analyseBtn.disabled = true;
analyseBtn.innerHTML = '<span class="spinner"></span> Analyse...';
results.classList.remove('active');
try {
// Step 1: Start analysis
setStatus('loading', '🚀', 'Demarrage...', 'Initialisation de l\'analyse', 5);
const response = await fetch(`${API_BASE}/peertube-analyse`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
});
const startResult = await response.json();
if (!startResult.success) {
throw new Error(startResult.error || 'Erreur au demarrage');
}
const jobId = startResult.job_id;
setStatus('loading', '🔍', 'Extraction...', 'Job ID: ' + jobId, 10);
// Step 2: Poll for completion
const data = await pollAnalysisStatus(jobId);
setStatus('success', '✅', 'Analyse terminee!', 'Resultats disponibles', 100);
displayResults(data);
} catch (error) {
setStatus('error', '❌', 'Erreur', error.message, 0);
console.error(error);
} finally {
analyseBtn.disabled = false;
analyseBtn.innerHTML = '<span>Analyser</span>';
}
}
// Event listeners
analyseBtn.addEventListener('click', analyse);
videoUrl.addEventListener('keypress', (e) => {
if (e.key === 'Enter') analyse();
});
// Demo mode for testing without backend
function demoMode() {
const demoData = {
metadata: {
title: "Demo: Analyse de securite reseau",
duration_string: "15:42",
uploader: "SecuBox",
upload_date: "2026-02-21",
view_count: 1234,
tags: ["securite", "reseau", "cybersecurite"]
},
transcript: "Ceci est un exemple de transcript. Dans cette video, nous allons analyser les differentes techniques de securite reseau...",
analysis: "# Analyse: Demo securite reseau\n\n## Resume executif\nCette video presente une introduction aux concepts de securite reseau.\n\n## Themes principaux\n- Securite perimetre\n- Detection d'intrusion\n- Analyse de trafic\n\n## Points cles\n1. Importance du monitoring continu\n2. Configuration des firewalls\n3. Segmentation reseau"
};
displayResults(demoData);
setStatus('success', '✅', 'Mode demo', 'Donnees de demonstration', 100);
}
// Ready
console.log('PeerTube Analyse ready');
</script>
</body>
</html>