added self sign up and email verification
This commit is contained in:
594
main.py
Normal file
594
main.py
Normal file
@@ -0,0 +1,594 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import smtplib
|
||||
import subprocess
|
||||
import uuid
|
||||
import warnings
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ["TORCH_CODEC_ENABLED"] = "0"
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="pyannote.audio")
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="torchaudio")
|
||||
|
||||
from fastapi import FastAPI, UploadFile, File, Form, Body, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
import whisperx
|
||||
import torch
|
||||
|
||||
MAX_FILE_SIZE = 25 * 1024 * 1024
|
||||
ALLOWED_CONTENT_TYPES = {
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"audio/webm",
|
||||
"audio/ogg",
|
||||
"audio/wav",
|
||||
"video/webm",
|
||||
}
|
||||
|
||||
DATA_DIR = Path("data")
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
OUTPUT_DIR = Path("output")
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
compute_type = "float32" if device == "cpu" else "float16"
|
||||
|
||||
_whisper_model = None
|
||||
_align_model = None
|
||||
_align_metadata = None
|
||||
|
||||
|
||||
# ── Model loading ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_whisper_model():
|
||||
global _whisper_model
|
||||
if _whisper_model is None:
|
||||
print(f"Lade WhisperX Modell large-v3 auf {device} ({compute_type})...")
|
||||
_whisper_model = whisperx.load_model(
|
||||
"large-v3", device=device, compute_type=compute_type
|
||||
)
|
||||
print("WhisperX Modell geladen.")
|
||||
return _whisper_model
|
||||
|
||||
|
||||
def get_align_model():
|
||||
global _align_model, _align_metadata
|
||||
if _align_model is None:
|
||||
print("Lade Alignment Modell (de)...")
|
||||
_align_model, _align_metadata = whisperx.load_align_model(
|
||||
language_code="de", device=device
|
||||
)
|
||||
print("Alignment Modell geladen.")
|
||||
return _align_model, _align_metadata
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
print(f"Server startet – lade Modelle auf {device}...")
|
||||
get_whisper_model()
|
||||
get_align_model()
|
||||
print("Alle Modelle geladen – Server bereit!")
|
||||
yield
|
||||
print("Server fährt herunter.")
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
# ── Audio helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def convert_to_mp3(input_path: Path) -> Path:
|
||||
output_path = input_path.with_suffix(".converted.mp3")
|
||||
r = subprocess.run(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-i",
|
||||
str(input_path),
|
||||
"-acodec",
|
||||
"libmp3lame",
|
||||
"-q:a",
|
||||
"2",
|
||||
str(output_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"ffmpeg fehlgeschlagen:\n{r.stderr}")
|
||||
return output_path
|
||||
|
||||
|
||||
def detect_audio_format(data: bytes) -> str:
|
||||
if data[:3] == b"ID3" or data[:2] in (b"\xff\xfb", b"\xff\xf3"):
|
||||
return "mp3"
|
||||
if data[:4] == b"fLaC":
|
||||
return "flac"
|
||||
if data[:4] == b"OggS":
|
||||
return "ogg"
|
||||
if data[:4] == b"RIFF":
|
||||
return "wav"
|
||||
if data[:4] == b"\x1aE\xdf\xa3":
|
||||
return "webm"
|
||||
return "unknown"
|
||||
|
||||
|
||||
# ── Transcription ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def transcribe_audio(file_path: Path) -> str:
|
||||
whisper_model = get_whisper_model()
|
||||
align_model, align_metadata = get_align_model()
|
||||
|
||||
fmt = detect_audio_format(file_path.read_bytes())
|
||||
needs_conversion = (
|
||||
fmt in ("webm", "ogg", "flac") or file_path.suffix.lower() != ".mp3"
|
||||
)
|
||||
audio_path = convert_to_mp3(file_path) if needs_conversion else file_path
|
||||
audio_str = str(audio_path)
|
||||
|
||||
print("Transkription...")
|
||||
result = whisper_model.transcribe(audio_str, language="de")
|
||||
print("Alignment...")
|
||||
result = whisperx.align(
|
||||
result["segments"], align_model, align_metadata, audio_str, device=device
|
||||
)
|
||||
|
||||
if os.getenv("ENABLE_SPEAKER_DIARIZATION", "true").lower() == "true":
|
||||
try:
|
||||
print("Diarization...")
|
||||
from whisperx.diarize import DiarizationPipeline
|
||||
|
||||
pipeline = DiarizationPipeline(token=os.getenv("HF_TOKEN"), device=device)
|
||||
diarize_segments = pipeline(audio_str)
|
||||
result = whisperx.assign_word_speakers(diarize_segments, result)
|
||||
print("Diarization fertig.")
|
||||
except Exception as e:
|
||||
print(f"Diarization fehlgeschlagen: {e}")
|
||||
|
||||
if needs_conversion:
|
||||
audio_path.unlink(missing_ok=True)
|
||||
|
||||
return "\n".join(
|
||||
f"{seg.get('speaker', 'SPRECHER')}: {seg['text'].strip()}"
|
||||
for seg in result["segments"]
|
||||
if seg["text"].strip()
|
||||
)
|
||||
|
||||
|
||||
# ── AI helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _openai_client():
|
||||
from openai import OpenAI
|
||||
|
||||
key = os.getenv("OPENAI_API_KEY")
|
||||
if not key:
|
||||
raise RuntimeError("OPENAI_API_KEY nicht gesetzt")
|
||||
return OpenAI(api_key=key)
|
||||
|
||||
|
||||
def process_with_ai(text: str) -> str:
|
||||
"""Grammatik- und Interpunktionskorrektur, Sprecher-Labels bleiben."""
|
||||
client = _openai_client()
|
||||
r = client.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Du bist ein Assistent zur Nachbearbeitung von Sprachtranskripten. "
|
||||
"Verbessere Grammatik, Rechtschreibung und Interpunktion. "
|
||||
"Behalte die Sprecher-Labels (z.B. 'Max Mustermann:') exakt bei. "
|
||||
"Verändere die inhaltliche Bedeutung NICHT. "
|
||||
"Format: Sprecher: Text"
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
)
|
||||
return r.choices[0].message.content or ""
|
||||
|
||||
|
||||
def summarize_with_ai(transcript: str, title: str) -> str:
|
||||
"""Erstellt eine strukturierte Meeting-Zusammenfassung ohne Markdown."""
|
||||
client = _openai_client()
|
||||
r = client.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Du bist ein Assistent für Meeting-Protokolle. "
|
||||
"Erstelle eine strukturierte Zusammenfassung des Meeting-Transkripts. "
|
||||
"Gliedere immer in genau diese drei Abschnitte, auch wenn das Transkript kurz oder unvollständig ist – "
|
||||
"fasse dann zusammen was vorhanden ist:\n\n"
|
||||
"Kernthemen:\n[Was wurde besprochen]\n\n"
|
||||
"Entscheidungen:\n[Getroffene Entscheidungen, falls keine: 'Keine Entscheidungen getroffen.']\n\n"
|
||||
"Nächste Schritte:\n[Offene Punkte und Aufgaben, falls keine: 'Keine Folgeaufgaben definiert.']\n\n"
|
||||
"Wichtige Regeln:\n"
|
||||
"- Kein Markdown, keine Sterne, keine Rauten, keine Aufzählungszeichen mit Bindestrich\n"
|
||||
"- Nur Fließtext und Zeilenumbrüche\n"
|
||||
"- Professionelles Deutsch\n"
|
||||
"- Immer alle drei Abschnitte ausgeben, egal wie kurz das Transkript ist"
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Meeting: {title}\n\nTranskript:\n{transcript}",
|
||||
},
|
||||
],
|
||||
)
|
||||
return r.choices[0].message.content or ""
|
||||
|
||||
|
||||
# ── PDF generation ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _safe_filename(title: str) -> str:
|
||||
"""Erstellt einen sicheren Dateinamen aus dem Meeting-Titel."""
|
||||
safe = re.sub(r"[^\w\-äöüÄÖÜß ]", "", title).strip()
|
||||
safe = re.sub(r"\s+", "_", safe)
|
||||
return safe or "Transkript"
|
||||
|
||||
|
||||
def build_pdf(title: str, summary: str, transcript: str) -> bytes:
|
||||
"""Professionelles PDF: heller Hintergrund, Noto Sans, saubere Typografie."""
|
||||
from fpdf import FPDF
|
||||
|
||||
# Font paths
|
||||
FONT_DIR = Path("/usr/share/fonts/truetype/noto")
|
||||
F_REG = str(FONT_DIR / "NotoSans-Regular.ttf")
|
||||
F_BOLD = str(FONT_DIR / "NotoSans-Bold.ttf")
|
||||
F_ITAL = str(FONT_DIR / "NotoSans-Italic.ttf")
|
||||
|
||||
# Colour palette (fully print-friendly, dark on white)
|
||||
C_HEADER_BG = (248, 248, 250) # very light grey header band
|
||||
C_HEADER_FG = (20, 20, 30) # near-black title
|
||||
C_HEADER_SUB = (120, 120, 135) # label / date in header
|
||||
C_HEADER_LINE = (200, 200, 210) # bottom border of header band
|
||||
C_LABEL = (130, 130, 145) # section labels
|
||||
C_RULE = (220, 220, 228) # horizontal rules
|
||||
C_BODY = (30, 30, 40) # main body text
|
||||
C_SUMMARY = (55, 55, 70) # summary text
|
||||
C_FOOTER = (160, 160, 175)
|
||||
|
||||
# Speaker accent colours (left border + name) — muted, print-safe
|
||||
SPEAKER_PALETTE = [
|
||||
(51, 102, 187), # blue
|
||||
(180, 60, 60), # red
|
||||
(40, 140, 80), # green
|
||||
(140, 70, 170), # purple
|
||||
(190, 120, 20), # amber
|
||||
(20, 150, 160), # teal
|
||||
(190, 80, 120), # rose
|
||||
(80, 100, 180), # indigo
|
||||
]
|
||||
|
||||
date_str = datetime.now().strftime("%d. %B %Y")
|
||||
|
||||
class PDF(FPDF):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_font("Noto", "", F_REG, uni=True)
|
||||
self.add_font("Noto", "B", F_BOLD, uni=True)
|
||||
self.add_font("Noto", "I", F_ITAL, uni=True)
|
||||
|
||||
def header(self):
|
||||
if self.page_no() == 1:
|
||||
return # cover drawn manually
|
||||
# Running header on pages 2+
|
||||
self.set_font("Noto", "", 8)
|
||||
self.set_text_color(*C_LABEL)
|
||||
self.cell(0, 6, title, align="L")
|
||||
self.cell(0, 6, date_str, align="R", new_x="LMARGIN", new_y="NEXT")
|
||||
self.set_draw_color(*C_RULE)
|
||||
self.set_line_width(0.3)
|
||||
self.line(self.l_margin, self.get_y(), self.w - self.r_margin, self.get_y())
|
||||
self.ln(5)
|
||||
|
||||
def footer(self):
|
||||
self.set_y(-14)
|
||||
self.set_font("Noto", "", 8)
|
||||
self.set_text_color(*C_FOOTER)
|
||||
self.cell(0, 8, f"{self.page_no()}", align="C")
|
||||
|
||||
pdf = PDF()
|
||||
pdf.set_margins(22, 28, 22)
|
||||
pdf.set_auto_page_break(auto=True, margin=20)
|
||||
|
||||
# ── PAGE 1: Cover block ───────────────────────────────────────────────────
|
||||
pdf.add_page()
|
||||
|
||||
# Light header band
|
||||
pdf.set_fill_color(*C_HEADER_BG)
|
||||
pdf.rect(0, 0, pdf.w, 52, style="F")
|
||||
# Bottom border line
|
||||
pdf.set_draw_color(*C_HEADER_LINE)
|
||||
pdf.set_line_width(0.4)
|
||||
pdf.line(0, 52, pdf.w, 52)
|
||||
|
||||
# "MEETING-PROTOKOLL" label
|
||||
pdf.set_xy(22, 13)
|
||||
pdf.set_font("Noto", "", 8)
|
||||
pdf.set_text_color(*C_HEADER_SUB)
|
||||
pdf.cell(0, 5, "MEETING-PROTOKOLL")
|
||||
|
||||
# Title
|
||||
pdf.set_xy(22, 21)
|
||||
pdf.set_font("Noto", "B", 20)
|
||||
pdf.set_text_color(*C_HEADER_FG)
|
||||
pdf.cell(0, 10, title)
|
||||
|
||||
# Date
|
||||
pdf.set_xy(22, 38)
|
||||
pdf.set_font("Noto", "", 9)
|
||||
pdf.set_text_color(*C_HEADER_SUB)
|
||||
pdf.cell(0, 5, date_str)
|
||||
|
||||
pdf.set_y(62) # below header band
|
||||
|
||||
# ── SUMMARY SECTION ───────────────────────────────────────────────────────
|
||||
if summary:
|
||||
# Section label
|
||||
pdf.set_font("Noto", "B", 8)
|
||||
pdf.set_text_color(*C_LABEL)
|
||||
pdf.set_x(pdf.l_margin)
|
||||
pdf.cell(0, 5, "ZUSAMMENFASSUNG", new_x="LMARGIN", new_y="NEXT")
|
||||
# Rule
|
||||
pdf.set_draw_color(*C_RULE)
|
||||
pdf.set_line_width(0.3)
|
||||
pdf.line(pdf.l_margin, pdf.get_y() + 1, pdf.w - pdf.r_margin, pdf.get_y() + 1)
|
||||
pdf.ln(5)
|
||||
|
||||
# Summary text — render each paragraph
|
||||
body_w = pdf.w - pdf.l_margin - pdf.r_margin
|
||||
pdf.set_font("Noto", "", 10)
|
||||
pdf.set_text_color(*C_SUMMARY)
|
||||
for para in summary.strip().split("\n"):
|
||||
para = para.strip()
|
||||
if not para:
|
||||
pdf.ln(3)
|
||||
continue
|
||||
pdf.set_x(pdf.l_margin)
|
||||
# Section headers inside summary (e.g. "Kernthemen:")
|
||||
if para.endswith(":") and len(para) < 50:
|
||||
pdf.ln(2)
|
||||
pdf.set_font("Noto", "B", 10)
|
||||
pdf.set_text_color(*C_BODY)
|
||||
pdf.multi_cell(body_w, 6, para)
|
||||
pdf.set_font("Noto", "", 10)
|
||||
pdf.set_text_color(*C_SUMMARY)
|
||||
else:
|
||||
pdf.multi_cell(body_w, 6, para)
|
||||
pdf.ln(10)
|
||||
|
||||
# ── TRANSCRIPT SECTION ────────────────────────────────────────────────────
|
||||
pdf.set_font("Noto", "B", 8)
|
||||
pdf.set_text_color(*C_LABEL)
|
||||
pdf.set_x(pdf.l_margin)
|
||||
pdf.cell(0, 5, "TRANSKRIPT", new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_draw_color(*C_RULE)
|
||||
pdf.set_line_width(0.3)
|
||||
pdf.line(pdf.l_margin, pdf.get_y() + 1, pdf.w - pdf.r_margin, pdf.get_y() + 1)
|
||||
pdf.ln(6)
|
||||
|
||||
for line in transcript.split("\n"):
|
||||
if not line.strip():
|
||||
pdf.ln(2)
|
||||
continue
|
||||
|
||||
colon = line.find(":")
|
||||
if colon == -1:
|
||||
pdf.set_font("Noto", "", 10)
|
||||
pdf.set_text_color(*C_BODY)
|
||||
pdf.set_x(pdf.l_margin)
|
||||
pdf.multi_cell(pdf.w - pdf.l_margin - pdf.r_margin, 6, line.strip())
|
||||
pdf.ln(1)
|
||||
continue
|
||||
|
||||
speaker = line[:colon].strip()
|
||||
text = line[colon + 1 :].strip()
|
||||
if not text:
|
||||
continue
|
||||
|
||||
# Speaker name bold, then text inline — no colour, no background
|
||||
pdf.set_x(pdf.l_margin)
|
||||
pdf.set_font("Noto", "B", 10)
|
||||
pdf.set_text_color(*C_BODY)
|
||||
name_label = speaker + ": "
|
||||
name_w = pdf.get_string_width(name_label)
|
||||
|
||||
y = pdf.get_y()
|
||||
pdf.set_xy(pdf.l_margin, y)
|
||||
pdf.cell(name_w, 5, name_label)
|
||||
|
||||
text_x = pdf.l_margin + name_w
|
||||
text_w = pdf.w - pdf.r_margin - text_x
|
||||
pdf.set_font("Noto", "", 10)
|
||||
pdf.set_xy(text_x, y)
|
||||
pdf.multi_cell(text_w, 5, text)
|
||||
|
||||
pdf.ln(0.5)
|
||||
|
||||
return bytes(pdf.output())
|
||||
|
||||
|
||||
# ── Email / file output ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def deliver(
|
||||
to_email: str,
|
||||
title: str,
|
||||
summary: str,
|
||||
transcript: str,
|
||||
) -> bool:
|
||||
"""Erstellt PDF und sendet es per E-Mail oder speichert es als Datei."""
|
||||
pdf_bytes = build_pdf(title, summary, transcript)
|
||||
safe_title = _safe_filename(title)
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
pdf_name = f"{date_str}_{safe_title}.pdf"
|
||||
|
||||
smtp_host = os.getenv("SMTP_HOST")
|
||||
smtp_port = int(os.getenv("SMTP_PORT", 587))
|
||||
smtp_user = os.getenv("SMTP_USER")
|
||||
smtp_password = os.getenv("SMTP_PASSWORD")
|
||||
# SMTP_FROM kann ein Anzeigename sein — wir stellen sicher dass eine
|
||||
# gültige Absenderadresse verwendet wird: "Name <email>" oder nur die E-Mail
|
||||
smtp_from_raw = os.getenv("SMTP_FROM", "").strip()
|
||||
if smtp_from_raw and "@" not in smtp_from_raw and smtp_user:
|
||||
smtp_from = f"{smtp_from_raw} <{smtp_user}>"
|
||||
elif smtp_from_raw:
|
||||
smtp_from = smtp_from_raw
|
||||
else:
|
||||
smtp_from = smtp_user or ""
|
||||
|
||||
if not all([smtp_host, smtp_user, smtp_password]):
|
||||
# Fallback: Datei speichern
|
||||
out = OUTPUT_DIR / pdf_name
|
||||
out.write_bytes(pdf_bytes)
|
||||
print(f"SMTP nicht konfiguriert – PDF gespeichert: {out}")
|
||||
return True
|
||||
|
||||
body = (
|
||||
f"Meeting: {title}\n"
|
||||
f"Datum: {datetime.now().strftime('%d.%m.%Y')}\n\n"
|
||||
f"--- Zusammenfassung ---\n\n{summary}\n\n"
|
||||
f"Das vollständige Transkript ist als PDF angehängt."
|
||||
)
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg["Subject"] = f"Meeting-Protokoll: {title}"
|
||||
msg["From"] = smtp_from
|
||||
msg["To"] = to_email
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
|
||||
attachment = MIMEApplication(pdf_bytes, _subtype="pdf")
|
||||
attachment.add_header("Content-Disposition", "attachment", filename=pdf_name)
|
||||
msg.attach(attachment)
|
||||
|
||||
with smtplib.SMTP(str(smtp_host), smtp_port) as server:
|
||||
server.starttls()
|
||||
server.login(str(smtp_user), str(smtp_password))
|
||||
server.send_message(msg)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# ── API Endpoints ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def get_index():
|
||||
return Path(__file__).parent.joinpath("index.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@app.post("/upload")
|
||||
async def upload_audio(file: UploadFile = File(...)):
|
||||
content_type = file.content_type or ""
|
||||
if content_type not in ALLOWED_CONTENT_TYPES and not content_type.startswith(
|
||||
"audio/"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=415, detail=f"Nicht unterstützter Typ: {content_type}"
|
||||
)
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"Datei zu groß ({len(content) // 1024 // 1024} MB). Max 25 MB",
|
||||
)
|
||||
if len(content) == 0:
|
||||
raise HTTPException(status_code=400, detail="Leere Datei")
|
||||
|
||||
ext_map = {
|
||||
"audio/mpeg": ".mp3",
|
||||
"audio/mp3": ".mp3",
|
||||
"audio/webm": ".webm",
|
||||
"video/webm": ".webm",
|
||||
"audio/ogg": ".ogg",
|
||||
"audio/wav": ".wav",
|
||||
"audio/flac": ".flac",
|
||||
}
|
||||
ext = ext_map.get(content_type, ".audio")
|
||||
file_id = str(uuid.uuid4())
|
||||
file_path = DATA_DIR / f"{file_id}{ext}"
|
||||
file_path.write_bytes(content)
|
||||
return {"file_id": file_id, "filename": file.filename, "size": len(content)}
|
||||
|
||||
|
||||
@app.post("/transcribe")
|
||||
async def transcribe(body: dict = Body(...)):
|
||||
file_id = body.get("file_id")
|
||||
if not file_id:
|
||||
raise HTTPException(status_code=400, detail="file_id required")
|
||||
|
||||
matches = [m for m in DATA_DIR.glob(f"{file_id}.*") if ".converted." not in m.name]
|
||||
if not matches:
|
||||
raise HTTPException(status_code=404, detail="Datei nicht gefunden")
|
||||
|
||||
try:
|
||||
text = transcribe_audio(matches[0])
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Transkription fehlgeschlagen: {e}"
|
||||
)
|
||||
|
||||
return {"transcription": text}
|
||||
|
||||
|
||||
@app.post("/process")
|
||||
async def process_text(text: str = Form(...)):
|
||||
if not text.strip():
|
||||
raise HTTPException(status_code=400, detail="Kein Text")
|
||||
try:
|
||||
return {"processed": process_with_ai(text)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/summarize")
|
||||
async def summarize(transcript: str = Form(...), title: str = Form(...)):
|
||||
"""Erstellt eine KI-Zusammenfassung des finalen Transkripts."""
|
||||
if not transcript.strip():
|
||||
raise HTTPException(status_code=400, detail="Kein Transkript")
|
||||
try:
|
||||
return {"summary": summarize_with_ai(transcript, title or "Meeting")}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/send")
|
||||
async def send(
|
||||
email: str = Form(...),
|
||||
title: str = Form(...),
|
||||
summary: str = Form(...),
|
||||
transcript: str = Form(...),
|
||||
):
|
||||
"""Generiert PDF und sendet/speichert es."""
|
||||
if not email or "@" not in email:
|
||||
raise HTTPException(status_code=400, detail="Ungültige E-Mail")
|
||||
try:
|
||||
deliver(email, title or "Meeting", summary, transcript)
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Reference in New Issue
Block a user