All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 48s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
Phase A: 8 new IT-Security training modules (SEC-PWD, SEC-DESK, SEC-KIAI, SEC-BYOD, SEC-VIDEO, SEC-USB, SEC-INC, SEC-HOME) with CTM entries. Bulk content and quiz generation endpoints for all 28 modules. Phase B: Piper TTS service (Python/FastAPI) for local German speech synthesis. training_media table, TTSClient in Go backend, audio generation endpoints, AudioPlayer component in frontend. MinIO storage integration. Phase C: FFmpeg presentation video pipeline — LLM generates slide scripts, ImageMagick renders 1920x1080 slides, FFmpeg combines with audio to MP4. VideoPlayer and ScriptPreview components in frontend. New files: 15 created, 9 modified - compliance-tts-service/ (Dockerfile, main.py, tts_engine.py, storage.py, slide_renderer.py, video_generator.py) - migrations 014-016 (training engine, IT-security modules, media table) - training package (models, store, content_generator, media, handlers) - frontend (AudioPlayer, VideoPlayer, ScriptPreview, api, types, page) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
133 lines
3.6 KiB
Python
133 lines
3.6 KiB
Python
"""ImageMagick slide renderer for presentation videos."""
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import textwrap
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Slide dimensions
|
|
WIDTH = 1920
|
|
HEIGHT = 1080
|
|
HEADER_HEIGHT = 120
|
|
FOOTER_HEIGHT = 60
|
|
FONT = "DejaVu-Sans"
|
|
FONT_BOLD = "DejaVu-Sans-Bold"
|
|
|
|
|
|
def render_slide(
|
|
heading: str,
|
|
text: str,
|
|
bullet_points: list[str],
|
|
slide_number: int,
|
|
total_slides: int,
|
|
module_code: str,
|
|
output_path: str,
|
|
) -> None:
|
|
"""Render a single slide as PNG using ImageMagick."""
|
|
cmd = [
|
|
"convert",
|
|
"-size", f"{WIDTH}x{HEIGHT}",
|
|
"xc:white",
|
|
# Blue header bar
|
|
"-fill", "#1e3a5f",
|
|
"-draw", f"rectangle 0,0 {WIDTH},{HEADER_HEIGHT}",
|
|
# Header text
|
|
"-fill", "white",
|
|
"-font", FONT_BOLD,
|
|
"-pointsize", "42",
|
|
"-gravity", "NorthWest",
|
|
"-annotate", f"+60+{(HEADER_HEIGHT - 42) // 2}", heading[:80],
|
|
]
|
|
|
|
y_pos = HEADER_HEIGHT + 40
|
|
|
|
# Main text
|
|
if text:
|
|
wrapped = textwrap.fill(text, width=80)
|
|
for line in wrapped.split("\n")[:6]:
|
|
cmd.extend([
|
|
"-fill", "#333333",
|
|
"-font", FONT,
|
|
"-pointsize", "28",
|
|
"-gravity", "NorthWest",
|
|
"-annotate", f"+60+{y_pos}", line,
|
|
])
|
|
y_pos += 38
|
|
|
|
y_pos += 20
|
|
|
|
# Bullet points
|
|
for bp in bullet_points[:8]:
|
|
wrapped_bp = textwrap.fill(bp, width=75)
|
|
first_line = True
|
|
for line in wrapped_bp.split("\n"):
|
|
prefix = " • " if first_line else " "
|
|
cmd.extend([
|
|
"-fill", "#444444",
|
|
"-font", FONT,
|
|
"-pointsize", "26",
|
|
"-gravity", "NorthWest",
|
|
"-annotate", f"+60+{y_pos}", f"{prefix}{line}",
|
|
])
|
|
y_pos += 34
|
|
first_line = False
|
|
y_pos += 8
|
|
|
|
# Footer bar
|
|
cmd.extend([
|
|
"-fill", "#f0f0f0",
|
|
"-draw", f"rectangle 0,{HEIGHT - FOOTER_HEIGHT} {WIDTH},{HEIGHT}",
|
|
"-fill", "#888888",
|
|
"-font", FONT,
|
|
"-pointsize", "20",
|
|
"-gravity", "SouthWest",
|
|
"-annotate", f"+60+{(FOOTER_HEIGHT - 20) // 2}", f"{module_code}",
|
|
"-gravity", "SouthEast",
|
|
"-annotate", f"+60+{(FOOTER_HEIGHT - 20) // 2}", f"Folie {slide_number}/{total_slides}",
|
|
])
|
|
|
|
cmd.append(output_path)
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"ImageMagick failed: {result.stderr}")
|
|
|
|
|
|
def render_title_slide(
|
|
title: str,
|
|
subtitle: str,
|
|
output_path: str,
|
|
) -> None:
|
|
"""Render a title slide."""
|
|
cmd = [
|
|
"convert",
|
|
"-size", f"{WIDTH}x{HEIGHT}",
|
|
"xc:white",
|
|
# Full blue background
|
|
"-fill", "#1e3a5f",
|
|
"-draw", f"rectangle 0,0 {WIDTH},{HEIGHT}",
|
|
# Title
|
|
"-fill", "white",
|
|
"-font", FONT_BOLD,
|
|
"-pointsize", "56",
|
|
"-gravity", "Center",
|
|
"-annotate", "+0-60", title[:60],
|
|
# Subtitle
|
|
"-fill", "#b0c4de",
|
|
"-font", FONT,
|
|
"-pointsize", "32",
|
|
"-gravity", "Center",
|
|
"-annotate", "+0+40", subtitle[:80],
|
|
# Footer
|
|
"-fill", "#6688aa",
|
|
"-pointsize", "22",
|
|
"-gravity", "South",
|
|
"-annotate", "+0+30", "BreakPilot Compliance Training",
|
|
output_path,
|
|
]
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"ImageMagick title slide failed: {result.stderr}")
|