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:
parent
389e4a58e0
commit
f76dfe8a67
@ -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)
|
||||
|
||||
@ -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"}'
|
||||
;;
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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\"}"
|
||||
@ -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
|
||||
@ -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 — <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>
|
||||
Loading…
Reference in New Issue
Block a user