Commit ·
2cdd0e3
1
Parent(s): e25dfc1
Add annotation app
Browse files- .python-version +1 -0
- Dockerfile +27 -0
- app.py +252 -0
- evaluation_dataset.jsonl +0 -0
- index.html +613 -0
- pyproject.toml +9 -0
- uv.lock +161 -0
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Based on https://github.com/astral-sh/uv-docker-example/blob/main/README.md
|
| 2 |
+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
| 3 |
+
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 9 |
+
ENV DATASET_PATH="/app/evaluation_dataset.jsonl"
|
| 10 |
+
ENV DB_PATH="/data/annotations.db"
|
| 11 |
+
|
| 12 |
+
# Copy dependency files first (better layer caching)
|
| 13 |
+
COPY --chown=user pyproject.toml .python-version uv.lock ./
|
| 14 |
+
RUN uv sync --locked
|
| 15 |
+
|
| 16 |
+
COPY --chown=user app.py .
|
| 17 |
+
COPY --chown=user index.html .
|
| 18 |
+
COPY --chown=user evaluation_dataset.jsonl .
|
| 19 |
+
|
| 20 |
+
# Persistent storage for annotations (enable in HF Space settings)
|
| 21 |
+
RUN mkdir -p /data && chown user:user /data
|
| 22 |
+
|
| 23 |
+
USER user
|
| 24 |
+
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Flask annotation app for blind summary evaluation.
|
| 2 |
+
|
| 3 |
+
Serves a web UI where annotators evaluate AI-generated summaries.
|
| 4 |
+
No accounts required -- annotators identify themselves by name.
|
| 5 |
+
Annotations are persisted in a SQLite database.
|
| 6 |
+
|
| 7 |
+
Optional password protection: set APP_PASSWORD as an environment variable.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import sqlite3
|
| 13 |
+
from functools import cache
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from flask import Flask, Response, jsonify, redirect, request, send_file, session
|
| 17 |
+
|
| 18 |
+
DATASET_PATH = Path(os.environ.get("DATASET_PATH", "evaluation_dataset.jsonl"))
|
| 19 |
+
DB_PATH = Path(os.environ.get("DB_PATH", "annotations.db"))
|
| 20 |
+
|
| 21 |
+
ANNOTATION_FIELDS = (
|
| 22 |
+
"bewertung", "korrekt", "relevant", "vollstaendig", "kohaerenz", "anmerkungen",
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
APP_PASSWORD = os.environ.get("APP_PASSWORD", "")
|
| 26 |
+
|
| 27 |
+
app = Flask(__name__)
|
| 28 |
+
app.secret_key = os.environ.get("SECRET_KEY", os.urandom(24).hex())
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ---------------------------------------------------------------------------
|
| 32 |
+
# Database
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
|
| 35 |
+
def get_db() -> sqlite3.Connection:
|
| 36 |
+
"""Open a SQLite connection with row factory and WAL mode."""
|
| 37 |
+
db = sqlite3.connect(str(DB_PATH))
|
| 38 |
+
db.row_factory = sqlite3.Row
|
| 39 |
+
db.execute("PRAGMA journal_mode=WAL")
|
| 40 |
+
db.execute("""
|
| 41 |
+
CREATE TABLE IF NOT EXISTS annotations (
|
| 42 |
+
annotator TEXT NOT NULL,
|
| 43 |
+
eval_id TEXT NOT NULL,
|
| 44 |
+
bewertung TEXT,
|
| 45 |
+
korrekt TEXT,
|
| 46 |
+
relevant TEXT,
|
| 47 |
+
vollstaendig TEXT,
|
| 48 |
+
kohaerenz TEXT,
|
| 49 |
+
anmerkungen TEXT,
|
| 50 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 51 |
+
PRIMARY KEY (annotator, eval_id)
|
| 52 |
+
)
|
| 53 |
+
""")
|
| 54 |
+
return db
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
# Dataset (immutable, cached)
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
|
| 61 |
+
@cache
|
| 62 |
+
def load_dataset() -> tuple[dict, ...]:
|
| 63 |
+
"""Load evaluation items from JSONL. Cached because the dataset never changes."""
|
| 64 |
+
items = []
|
| 65 |
+
with open(DATASET_PATH, encoding="utf-8") as f:
|
| 66 |
+
for line in f:
|
| 67 |
+
if line.strip():
|
| 68 |
+
row = json.loads(line)
|
| 69 |
+
has_prior = bool(row.get("bewertung"))
|
| 70 |
+
row["has_prior_judgement"] = has_prior
|
| 71 |
+
if has_prior:
|
| 72 |
+
for field in ANNOTATION_FIELDS:
|
| 73 |
+
row[f"prior_{field}"] = row.get(field)
|
| 74 |
+
items.append(row)
|
| 75 |
+
items.sort(key=lambda x: x["eval_id"])
|
| 76 |
+
return tuple(items)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def fetch_annotations(db: sqlite3.Connection, annotator: str) -> dict[str, dict]:
|
| 80 |
+
"""Fetch all annotations for a given annotator, keyed by eval_id."""
|
| 81 |
+
rows = db.execute(
|
| 82 |
+
"SELECT * FROM annotations WHERE annotator = ?", (annotator,),
|
| 83 |
+
).fetchall()
|
| 84 |
+
return {row["eval_id"]: dict(row) for row in rows}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def merge_items_with_annotations(
|
| 88 |
+
items: tuple[dict, ...],
|
| 89 |
+
annotations: dict[str, dict],
|
| 90 |
+
) -> list[dict]:
|
| 91 |
+
"""Return items with annotation values merged in (does not mutate originals)."""
|
| 92 |
+
merged = []
|
| 93 |
+
for item in items:
|
| 94 |
+
entry = {**item}
|
| 95 |
+
ann = annotations.get(item["eval_id"])
|
| 96 |
+
if ann:
|
| 97 |
+
for field in ANNOTATION_FIELDS:
|
| 98 |
+
entry[field] = ann.get(field)
|
| 99 |
+
merged.append(entry)
|
| 100 |
+
return merged
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# ---------------------------------------------------------------------------
|
| 104 |
+
# Optional password protection
|
| 105 |
+
# ---------------------------------------------------------------------------
|
| 106 |
+
|
| 107 |
+
LOGIN_HTML = """<!DOCTYPE html>
|
| 108 |
+
<html lang="de"><head><meta charset="UTF-8">
|
| 109 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 110 |
+
<title>Anmeldung</title>
|
| 111 |
+
<style>
|
| 112 |
+
body { font-family: system-ui, sans-serif; background: #f5f5f5; }
|
| 113 |
+
.box { max-width: 320px; margin: 120px auto; background: #fff;
|
| 114 |
+
padding: 32px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1);
|
| 115 |
+
text-align: center; }
|
| 116 |
+
h2 { margin-bottom: 16px; font-size: 20px; }
|
| 117 |
+
input { padding: 10px; width: 100%%; border: 1px solid #ccc; border-radius: 4px;
|
| 118 |
+
font-size: 15px; margin-bottom: 12px; box-sizing: border-box; }
|
| 119 |
+
button { padding: 10px 24px; border: none; border-radius: 4px;
|
| 120 |
+
background: #2563eb; color: #fff; font-size: 15px; cursor: pointer; }
|
| 121 |
+
button:hover { background: #1d4ed8; }
|
| 122 |
+
.err { color: #dc2626; font-size: 13px; margin-bottom: 8px; }
|
| 123 |
+
</style></head><body>
|
| 124 |
+
<div class="box">
|
| 125 |
+
<h2>Zusammenfassungs-Evaluation</h2>
|
| 126 |
+
%(error)s
|
| 127 |
+
<form method="post">
|
| 128 |
+
<input type="password" name="password" placeholder="Passwort" autofocus>
|
| 129 |
+
<button type="submit">Weiter</button>
|
| 130 |
+
</form>
|
| 131 |
+
</div></body></html>"""
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
@app.before_request
|
| 135 |
+
def check_auth():
|
| 136 |
+
if not APP_PASSWORD:
|
| 137 |
+
return None
|
| 138 |
+
if request.path == "/login":
|
| 139 |
+
return None
|
| 140 |
+
if session.get("authenticated"):
|
| 141 |
+
return None
|
| 142 |
+
if request.path.startswith("/api/"):
|
| 143 |
+
return jsonify({"error": "unauthorized"}), 401
|
| 144 |
+
return redirect("/login")
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@app.route("/login", methods=["GET", "POST"])
|
| 148 |
+
def login():
|
| 149 |
+
if request.method == "POST":
|
| 150 |
+
if request.form.get("password") == APP_PASSWORD:
|
| 151 |
+
session["authenticated"] = True
|
| 152 |
+
return redirect("/")
|
| 153 |
+
return LOGIN_HTML % {"error": '<p class="err">Falsches Passwort.</p>'}, 401
|
| 154 |
+
return LOGIN_HTML % {"error": ""}
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# ---------------------------------------------------------------------------
|
| 158 |
+
# Routes
|
| 159 |
+
# ---------------------------------------------------------------------------
|
| 160 |
+
|
| 161 |
+
@app.route("/")
|
| 162 |
+
def index():
|
| 163 |
+
return send_file("index.html")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@app.route("/api/entries")
|
| 167 |
+
def get_entries():
|
| 168 |
+
"""Return all evaluation items, optionally with annotations merged for an annotator."""
|
| 169 |
+
annotator = request.args.get("annotator", "").strip()
|
| 170 |
+
items = load_dataset()
|
| 171 |
+
if annotator:
|
| 172 |
+
db = get_db()
|
| 173 |
+
annotations = fetch_annotations(db, annotator)
|
| 174 |
+
db.close()
|
| 175 |
+
return jsonify(merge_items_with_annotations(items, annotations))
|
| 176 |
+
return jsonify(list(items))
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@app.route("/api/annotate", methods=["POST"])
|
| 180 |
+
def annotate():
|
| 181 |
+
"""Save or update an annotation."""
|
| 182 |
+
data = request.get_json()
|
| 183 |
+
db = get_db()
|
| 184 |
+
db.execute(
|
| 185 |
+
"""INSERT OR REPLACE INTO annotations
|
| 186 |
+
(annotator, eval_id, bewertung, korrekt, relevant, vollstaendig, kohaerenz, anmerkungen)
|
| 187 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
| 188 |
+
(
|
| 189 |
+
data["annotator"].strip(),
|
| 190 |
+
data["eval_id"],
|
| 191 |
+
data.get("bewertung"),
|
| 192 |
+
data.get("korrekt"),
|
| 193 |
+
data.get("relevant"),
|
| 194 |
+
data.get("vollstaendig"),
|
| 195 |
+
data.get("kohaerenz"),
|
| 196 |
+
data.get("anmerkungen"),
|
| 197 |
+
),
|
| 198 |
+
)
|
| 199 |
+
db.commit()
|
| 200 |
+
db.close()
|
| 201 |
+
return jsonify({"status": "ok"})
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
@app.route("/api/progress")
|
| 205 |
+
def progress():
|
| 206 |
+
"""Return annotation progress for an annotator."""
|
| 207 |
+
annotator = request.args.get("annotator", "").strip()
|
| 208 |
+
total = len(load_dataset())
|
| 209 |
+
if not annotator:
|
| 210 |
+
return jsonify({"total": total, "annotated": 0})
|
| 211 |
+
db = get_db()
|
| 212 |
+
count = db.execute(
|
| 213 |
+
"SELECT COUNT(*) FROM annotations WHERE annotator = ?", (annotator,),
|
| 214 |
+
).fetchone()[0]
|
| 215 |
+
db.close()
|
| 216 |
+
return jsonify({"total": total, "annotated": count})
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
@app.route("/api/export")
|
| 220 |
+
def export_annotations():
|
| 221 |
+
"""Export all annotations as downloadable JSONL."""
|
| 222 |
+
db = get_db()
|
| 223 |
+
rows = db.execute(
|
| 224 |
+
"SELECT * FROM annotations ORDER BY annotator, eval_id",
|
| 225 |
+
).fetchall()
|
| 226 |
+
db.close()
|
| 227 |
+
lines = [json.dumps(dict(row), ensure_ascii=False) for row in rows]
|
| 228 |
+
return Response(
|
| 229 |
+
"\n".join(lines) + "\n",
|
| 230 |
+
mimetype="application/jsonl",
|
| 231 |
+
headers={"Content-Disposition": "attachment; filename=annotations.jsonl"},
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
@app.route("/api/stats")
|
| 236 |
+
def stats():
|
| 237 |
+
"""Return overall annotation statistics (for the admin)."""
|
| 238 |
+
total = len(load_dataset())
|
| 239 |
+
db = get_db()
|
| 240 |
+
annotators = db.execute(
|
| 241 |
+
"SELECT annotator, COUNT(*) as count FROM annotations GROUP BY annotator ORDER BY annotator",
|
| 242 |
+
).fetchall()
|
| 243 |
+
db.close()
|
| 244 |
+
return jsonify({
|
| 245 |
+
"total": total,
|
| 246 |
+
"annotators": [{"name": r["annotator"], "count": r["count"]} for r in annotators],
|
| 247 |
+
})
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
if __name__ == "__main__":
|
| 251 |
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 252 |
+
app.run(host="0.0.0.0", port=7860)
|
evaluation_dataset.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
index.html
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="de">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Zusammenfassungs-Evaluation</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
+
body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; color: #222; }
|
| 10 |
+
|
| 11 |
+
header {
|
| 12 |
+
background: #1a1a2e; color: #fff; padding: 12px 24px;
|
| 13 |
+
display: flex; align-items: center; gap: 24px; flex-wrap: wrap;
|
| 14 |
+
position: sticky; top: 0; z-index: 10;
|
| 15 |
+
}
|
| 16 |
+
header h1 { font-size: 17px; font-weight: 600; white-space: nowrap; }
|
| 17 |
+
.progress-box { margin-left: auto; font-size: 13px; opacity: .85; display: flex; align-items: center; gap: 10px; }
|
| 18 |
+
.progress-bar { width: 120px; height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
|
| 19 |
+
.progress-fill { height: 100%; background: #22c55e; transition: width .3s; }
|
| 20 |
+
|
| 21 |
+
.nav {
|
| 22 |
+
display: flex; align-items: center; gap: 12px;
|
| 23 |
+
padding: 10px 24px; background: #fff; border-bottom: 1px solid #ddd;
|
| 24 |
+
position: sticky; top: 52px; z-index: 9; flex-wrap: wrap;
|
| 25 |
+
}
|
| 26 |
+
.nav button {
|
| 27 |
+
padding: 6px 16px; border: 1px solid #ccc; border-radius: 4px;
|
| 28 |
+
background: #fff; cursor: pointer; font-size: 14px;
|
| 29 |
+
}
|
| 30 |
+
.nav button:hover { background: #eee; }
|
| 31 |
+
.nav button:disabled { opacity: .4; cursor: default; }
|
| 32 |
+
.nav .counter { font-size: 14px; font-weight: 600; min-width: 80px; text-align: center; }
|
| 33 |
+
.filters { display: flex; align-items: center; gap: 8px; margin-left: auto; }
|
| 34 |
+
.filters label { font-size: 12px; font-weight: 600; color: #666; }
|
| 35 |
+
.filters select { padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; }
|
| 36 |
+
|
| 37 |
+
.card {
|
| 38 |
+
max-width: 960px; margin: 20px auto; background: #fff;
|
| 39 |
+
border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1); overflow: hidden;
|
| 40 |
+
}
|
| 41 |
+
.card-header {
|
| 42 |
+
padding: 14px 20px; border-bottom: 1px solid #eee;
|
| 43 |
+
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
| 44 |
+
}
|
| 45 |
+
.card-header h2 { font-size: 15px; font-weight: 600; }
|
| 46 |
+
.card-header .meta { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
| 47 |
+
.card-header .id { font-size: 12px; color: #888; font-family: monospace; }
|
| 48 |
+
|
| 49 |
+
.badge {
|
| 50 |
+
padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600;
|
| 51 |
+
}
|
| 52 |
+
.badge-DRA { background: #ecfdf5; color: #065f46; }
|
| 53 |
+
.badge-DW { background: #eff6ff; color: #1e40af; }
|
| 54 |
+
|
| 55 |
+
.section { padding: 14px 20px; border-bottom: 1px solid #f0f0f0; }
|
| 56 |
+
.section-label {
|
| 57 |
+
font-size: 11px; font-weight: 700; color: #666;
|
| 58 |
+
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px;
|
| 59 |
+
}
|
| 60 |
+
.section-content { font-size: 14px; line-height: 1.65; white-space: pre-wrap; }
|
| 61 |
+
|
| 62 |
+
.summary-section {
|
| 63 |
+
background: #eff6ff; border-left: 4px solid #2563eb;
|
| 64 |
+
}
|
| 65 |
+
.summary-section .section-content { font-size: 15px; font-weight: 500; }
|
| 66 |
+
|
| 67 |
+
.transcript-section .section-content {
|
| 68 |
+
max-height: 260px; overflow-y: auto; font-size: 13px; color: #444;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.original-toggle {
|
| 72 |
+
padding: 8px 20px; background: #f8f8f8; border-bottom: 1px solid #f0f0f0;
|
| 73 |
+
cursor: pointer; display: flex; align-items: center; gap: 8px;
|
| 74 |
+
font-size: 12px; font-weight: 600; color: #666; user-select: none;
|
| 75 |
+
}
|
| 76 |
+
.original-toggle:hover { background: #f0f0f0; }
|
| 77 |
+
.original-toggle .arrow { transition: transform .2s; display: inline-block; }
|
| 78 |
+
.original-toggle .arrow.open { transform: rotate(90deg); }
|
| 79 |
+
.original-transcript {
|
| 80 |
+
display: none; padding: 14px 20px; border-bottom: 1px solid #f0f0f0;
|
| 81 |
+
}
|
| 82 |
+
.original-transcript.open { display: block; }
|
| 83 |
+
.original-transcript .section-content {
|
| 84 |
+
max-height: 260px; overflow-y: auto; font-size: 13px; color: #444;
|
| 85 |
+
white-space: pre-wrap; line-height: 1.65;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* --- Evaluation controls --- */
|
| 89 |
+
.eval-section {
|
| 90 |
+
padding: 20px; border-bottom: 1px solid #f0f0f0;
|
| 91 |
+
border: 2px solid #2563eb; border-radius: 0 0 0 0;
|
| 92 |
+
margin: 0; background: #fafbff;
|
| 93 |
+
}
|
| 94 |
+
.eval-section .section-label { font-size: 13px; margin-bottom: 12px; color: #1e40af; }
|
| 95 |
+
|
| 96 |
+
.rating-row { margin-bottom: 16px; }
|
| 97 |
+
.rating-row label { font-size: 13px; font-weight: 600; color: #444; display: block; margin-bottom: 4px; }
|
| 98 |
+
.rating-row select { padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
|
| 99 |
+
|
| 100 |
+
.criteria { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 16px; }
|
| 101 |
+
.criterion { display: flex; flex-direction: column; gap: 4px; min-width: 120px; }
|
| 102 |
+
.criterion label { font-size: 12px; font-weight: 700; color: #555; text-transform: uppercase; }
|
| 103 |
+
.criterion select {
|
| 104 |
+
padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;
|
| 105 |
+
}
|
| 106 |
+
.criterion select.val-ja { border-color: #22c55e; background: #f0fdf4; }
|
| 107 |
+
.criterion select.val-aus { border-color: #f59e0b; background: #fffbeb; }
|
| 108 |
+
.criterion select.val-nein { border-color: #ef4444; background: #fef2f2; }
|
| 109 |
+
.criterion select.val-none { border-color: #ccc; background: #f9fafb; }
|
| 110 |
+
|
| 111 |
+
.criterion-desc {
|
| 112 |
+
font-size: 11px; color: #777; font-weight: 400; line-height: 1.4; margin-bottom: 2px;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/* Collapsible guidelines */
|
| 116 |
+
.guidelines-toggle {
|
| 117 |
+
padding: 10px 20px; background: #f0f4ff; border-bottom: 1px solid #d0d8f0;
|
| 118 |
+
cursor: pointer; display: flex; align-items: center; gap: 8px;
|
| 119 |
+
font-size: 13px; font-weight: 600; color: #1e40af; user-select: none;
|
| 120 |
+
}
|
| 121 |
+
.guidelines-toggle:hover { background: #e0e8ff; }
|
| 122 |
+
.guidelines-toggle .arrow { transition: transform .2s; display: inline-block; }
|
| 123 |
+
.guidelines-toggle .arrow.open { transform: rotate(90deg); }
|
| 124 |
+
.guidelines-body {
|
| 125 |
+
display: none; padding: 14px 20px; background: #f8f9ff; border-bottom: 1px solid #d0d8f0;
|
| 126 |
+
font-size: 13px; line-height: 1.7; color: #444;
|
| 127 |
+
}
|
| 128 |
+
.guidelines-body.open { display: block; }
|
| 129 |
+
.guidelines-body h3 { font-size: 13px; font-weight: 700; color: #1e40af; margin: 12px 0 4px 0; }
|
| 130 |
+
.guidelines-body h3:first-child { margin-top: 0; }
|
| 131 |
+
.guidelines-body ul { margin: 4px 0 8px 18px; }
|
| 132 |
+
.guidelines-body li { margin-bottom: 2px; }
|
| 133 |
+
.guidelines-body .scale-label { font-weight: 600; }
|
| 134 |
+
|
| 135 |
+
.comments-row { margin-top: 4px; }
|
| 136 |
+
.comments-row label { font-size: 12px; font-weight: 700; color: #555; text-transform: uppercase; display: block; margin-bottom: 4px; }
|
| 137 |
+
.comments-row textarea {
|
| 138 |
+
width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px;
|
| 139 |
+
font-family: inherit; font-size: 14px; line-height: 1.5; resize: vertical;
|
| 140 |
+
}
|
| 141 |
+
.comments-row textarea:focus { outline: 2px solid #2563eb; border-color: #2563eb; }
|
| 142 |
+
|
| 143 |
+
.actions { padding: 14px 20px; display: flex; justify-content: flex-end; gap: 12px; align-items: center; }
|
| 144 |
+
.save-hint { font-size: 12px; color: #888; margin-right: auto; }
|
| 145 |
+
.btn-save {
|
| 146 |
+
padding: 8px 24px; border: none; border-radius: 4px;
|
| 147 |
+
background: #2563eb; color: #fff; font-size: 14px; cursor: pointer; font-weight: 500;
|
| 148 |
+
}
|
| 149 |
+
.btn-save:hover { background: #1d4ed8; }
|
| 150 |
+
.btn-save:disabled { opacity: .4; cursor: default; }
|
| 151 |
+
|
| 152 |
+
.toast {
|
| 153 |
+
position: fixed; bottom: 24px; right: 24px; padding: 12px 20px;
|
| 154 |
+
border-radius: 6px; color: #fff; font-size: 14px;
|
| 155 |
+
opacity: 0; transition: opacity .3s; pointer-events: none;
|
| 156 |
+
}
|
| 157 |
+
.toast.show { opacity: 1; }
|
| 158 |
+
.toast.success { background: #22c55e; }
|
| 159 |
+
.toast.error { background: #ef4444; }
|
| 160 |
+
|
| 161 |
+
.empty-state {
|
| 162 |
+
text-align: center; padding: 60px 20px; color: #666; font-size: 15px;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.badge-promptd {
|
| 166 |
+
background: #fef3c7; color: #92400e; padding: 3px 10px;
|
| 167 |
+
border-radius: 12px; font-size: 12px; font-weight: 600;
|
| 168 |
+
}
|
| 169 |
+
.reference-banner {
|
| 170 |
+
padding: 10px 20px; background: #fef9c3; border-bottom: 1px solid #fde68a;
|
| 171 |
+
font-size: 13px; color: #92400e; font-weight: 500;
|
| 172 |
+
}
|
| 173 |
+
.reference-banner strong { font-weight: 700; }
|
| 174 |
+
.prior-judgement { padding: 14px 20px; border-bottom: 1px solid #f0f0f0; background: #fffbeb; }
|
| 175 |
+
.prior-judgement .section-label { color: #92400e; }
|
| 176 |
+
.prior-judgement .pj-grid {
|
| 177 |
+
display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; margin-top: 6px;
|
| 178 |
+
}
|
| 179 |
+
.prior-judgement .pj-item { display: flex; flex-direction: column; gap: 2px; }
|
| 180 |
+
.prior-judgement .pj-label { font-size: 11px; font-weight: 700; color: #92400e; text-transform: uppercase; }
|
| 181 |
+
.prior-judgement .pj-value { font-weight: 600; }
|
| 182 |
+
</style>
|
| 183 |
+
</head>
|
| 184 |
+
<body>
|
| 185 |
+
|
| 186 |
+
<header>
|
| 187 |
+
<h1>Zusammenfassungs-Evaluation</h1>
|
| 188 |
+
<div class="progress-box">
|
| 189 |
+
<span id="progress-text">-</span>
|
| 190 |
+
<div class="progress-bar"><div class="progress-fill" id="progress-fill" style="width:0"></div></div>
|
| 191 |
+
</div>
|
| 192 |
+
</header>
|
| 193 |
+
|
| 194 |
+
<div class="nav">
|
| 195 |
+
<button id="btn-prev" onclick="navigate(-1)">Prev</button>
|
| 196 |
+
<span class="counter" id="counter">-</span>
|
| 197 |
+
<button id="btn-next" onclick="navigate(1)">Next</button>
|
| 198 |
+
<div class="filters">
|
| 199 |
+
<label>Bearbeitungsstatus:</label>
|
| 200 |
+
<select id="filter-status" onchange="applyFilters()">
|
| 201 |
+
<option value="">alle</option>
|
| 202 |
+
<option value="pending">offen</option>
|
| 203 |
+
<option value="done">bewertet</option>
|
| 204 |
+
</select>
|
| 205 |
+
<label>LRA:</label>
|
| 206 |
+
<select id="filter-rfa" onchange="applyFilters()">
|
| 207 |
+
<option value="">alle</option>
|
| 208 |
+
<option value="DW">DW</option>
|
| 209 |
+
<option value="DRA">DRA</option>
|
| 210 |
+
</select>
|
| 211 |
+
<label>Bewertungsrunde:</label>
|
| 212 |
+
<select id="filter-type" onchange="applyFilters()">
|
| 213 |
+
<option value="">alle</option>
|
| 214 |
+
<option value="new" selected>Neu (zu bewerten)</option>
|
| 215 |
+
<option value="promptd">Alter Prompt (bereits evaluiert)</option>
|
| 216 |
+
</select>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
<div class="card" id="card">
|
| 221 |
+
<div class="card-header">
|
| 222 |
+
<h2 id="title">-</h2>
|
| 223 |
+
<div class="meta">
|
| 224 |
+
<span class="badge" id="badge-rfa"></span>
|
| 225 |
+
<span class="badge-promptd" id="badge-promptd" style="display:none">Alter Prompt</span>
|
| 226 |
+
<span class="id" id="eval-id">-</span>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<div class="reference-banner" id="reference-banner" style="display:none">
|
| 231 |
+
<strong>Alter Prompt:</strong> Dieser Eintrag wurde bereits in einer frueheren Evaluation bewertet. Die bestehende Bewertung wird unten angezeigt.
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<div class="prior-judgement" id="prior-judgement" style="display:none">
|
| 235 |
+
<div class="section-label">Bestehende Bewertung (Alter Prompt)</div>
|
| 236 |
+
<div class="pj-grid" id="pj-grid"></div>
|
| 237 |
+
<div id="pj-anmerkungen" style="margin-top:8px; font-size:13px; color:#444;"></div>
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<div class="section summary-section">
|
| 241 |
+
<div class="section-label">Zusammenfassung</div>
|
| 242 |
+
<div class="section-content" id="summary">-</div>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<div class="section">
|
| 246 |
+
<div class="section-label">Referenz</div>
|
| 247 |
+
<div class="section-content" id="referenz">-</div>
|
| 248 |
+
</div>
|
| 249 |
+
|
| 250 |
+
<div class="section transcript-section">
|
| 251 |
+
<div class="section-label" id="transcript-label">Transkript</div>
|
| 252 |
+
<div class="section-content" id="transcript">-</div>
|
| 253 |
+
</div>
|
| 254 |
+
|
| 255 |
+
<div class="original-toggle" id="original-toggle" style="display:none" onclick="toggleOriginal()">
|
| 256 |
+
<span class="arrow" id="original-arrow">▶</span>
|
| 257 |
+
Original-Transkript (Fremdsprache) anzeigen
|
| 258 |
+
</div>
|
| 259 |
+
<div class="original-transcript" id="original-transcript">
|
| 260 |
+
<div class="section-label">Original-Transkript</div>
|
| 261 |
+
<div class="section-content" id="original-text">-</div>
|
| 262 |
+
</div>
|
| 263 |
+
|
| 264 |
+
<div class="guidelines-toggle" onclick="toggleGuidelines()">
|
| 265 |
+
<span class="arrow" id="guidelines-arrow">▶</span>
|
| 266 |
+
Bewertungsrichtlinien anzeigen
|
| 267 |
+
</div>
|
| 268 |
+
<div class="guidelines-body" id="guidelines-body">
|
| 269 |
+
<h3>Vorgehensweise</h3>
|
| 270 |
+
<p>Lies das Transkript vollstaendig (oder ueberfliege es gruendlich). Lies dann die Zusammenfassung und waehle fuer jedes Kriterium eine Stufe (ja / ausreichend / nein). Vergib abschliessend eine Gesamtbewertung als Schulnote.</p>
|
| 271 |
+
|
| 272 |
+
<h3>Stufen</h3>
|
| 273 |
+
<ul>
|
| 274 |
+
<li><span class="scale-label">Ja</span> -- Perfekt oder nahezu perfekt, keine nennenswerten Schwaechen</li>
|
| 275 |
+
<li><span class="scale-label">Ausreichend</span> -- Mit Schwaechen, aber noch brauchbar</li>
|
| 276 |
+
<li><span class="scale-label">Nein</span> -- Inakzeptabel, voellig unbrauchbar</li>
|
| 277 |
+
</ul>
|
| 278 |
+
|
| 279 |
+
<h3>1. Korrekt</h3>
|
| 280 |
+
<p>Faktische Korrektheit: Werden Fakten, Namen und Orte korrekt wiedergegeben? Gibt es Halluzinationen oder Informationen, die nicht im Transkript vorkommen?</p>
|
| 281 |
+
<ul>
|
| 282 |
+
<li><span class="scale-label">Ja:</span> Alle Aussagen faktisch korrekt und durch das Transkript gestuetzt. Keine Halluzinationen.</li>
|
| 283 |
+
<li><span class="scale-label">Ausreichend:</span> Einige Ungenauigkeiten, Kernaussagen stimmen groesstenteils.</li>
|
| 284 |
+
<li><span class="scale-label">Nein:</span> Faktische Fehler und Halluzinationen. Ueberwiegend falsche Informationen.</li>
|
| 285 |
+
</ul>
|
| 286 |
+
|
| 287 |
+
<h3>2. Relevant</h3>
|
| 288 |
+
<p>Auswahl der bedeutsamen Inhalte: Enthaelt die Zusammenfassung nur wichtige Informationen? Gibt es unnoetige Wiederholungen?</p>
|
| 289 |
+
<ul>
|
| 290 |
+
<li><span class="scale-label">Ja:</span> Konzentriert sich ausschliesslich auf die wichtigsten Informationen. Keine Redundanzen.</li>
|
| 291 |
+
<li><span class="scale-label">Ausreichend:</span> Enthaelt auch weniger relevante Informationen. Einige unwichtige Details.</li>
|
| 292 |
+
<li><span class="scale-label">Nein:</span> Konzentriert sich auf nebensaechliche Aspekte statt Kernthemen.</li>
|
| 293 |
+
</ul>
|
| 294 |
+
|
| 295 |
+
<h3>3. Vollstaendig</h3>
|
| 296 |
+
<p>Sind alle wesentlichen Fakten, Kernaussagen und relevanten Entitaeten enthalten?</p>
|
| 297 |
+
<ul>
|
| 298 |
+
<li><span class="scale-label">Ja:</span> Alle wesentlichen Informationen und Kernaussagen enthalten. Wichtige Entitaeten vollstaendig erfasst.</li>
|
| 299 |
+
<li><span class="scale-label">Ausreichend:</span> Grundlegender Ueberblick, aber einzelne bedeutsame Aspekte fehlen.</li>
|
| 300 |
+
<li><span class="scale-label">Nein:</span> Wichtige Kernaussagen oder zentrale Entitaeten nicht erfasst. Zu lueckenhaft.</li>
|
| 301 |
+
</ul>
|
| 302 |
+
|
| 303 |
+
<h3>4. Kohaerenz</h3>
|
| 304 |
+
<p>Ist die Zusammenfassung gut strukturiert und logisch nachvollziehbar? Bilden die Saetze einen zusammenhaengenden Text?</p>
|
| 305 |
+
<ul>
|
| 306 |
+
<li><span class="scale-label">Ja:</span> Strukturiert und organisiert. Saetze bauen logisch aufeinander auf.</li>
|
| 307 |
+
<li><span class="scale-label">Ausreichend:</span> Grundsaetzlich verstaendlich, aber stellenweise sprunghaft.</li>
|
| 308 |
+
<li><span class="scale-label">Nein:</span> Unstrukturiert, Zusammenhang nur schwer nachvollziehbar.</li>
|
| 309 |
+
</ul>
|
| 310 |
+
|
| 311 |
+
<h3>Gesamtbewertung</h3>
|
| 312 |
+
<p>Schulnote 1 (sehr gut) bis 6 (ungenuegend). Bezieht sich auf den generellen Eindruck, wie gut das Transkript zusammengefasst wurde. Muss kein Durchschnitt der Kriterien sein -- z.B. kann fehlende Korrektheit staerker wiegen als fehlende Kohaerenz.</p>
|
| 313 |
+
|
| 314 |
+
<h3>Anmerkungen</h3>
|
| 315 |
+
<p>Optional: Besondere Fehler, Staerken, Halluzinationen, Begruendungen oder konkrete Verbesserungsvorschlaege fuer den Prompt.</p>
|
| 316 |
+
</div>
|
| 317 |
+
|
| 318 |
+
<div class="eval-section">
|
| 319 |
+
<div class="section-label">Bewertung</div>
|
| 320 |
+
|
| 321 |
+
<div class="rating-row">
|
| 322 |
+
<label>Gesamtbewertung (1 = sehr gut, 6 = ungenuegend)</label>
|
| 323 |
+
<select id="bewertung" onchange="markDirty()">
|
| 324 |
+
<option value="">(bitte waehlen)</option>
|
| 325 |
+
<option value="1">1 - sehr gut</option>
|
| 326 |
+
<option value="2">2 - gut</option>
|
| 327 |
+
<option value="3">3 - befriedigend</option>
|
| 328 |
+
<option value="4">4 - ausreichend</option>
|
| 329 |
+
<option value="5">5 - mangelhaft</option>
|
| 330 |
+
<option value="6">6 - ungenuegend</option>
|
| 331 |
+
</select>
|
| 332 |
+
</div>
|
| 333 |
+
|
| 334 |
+
<div class="criteria" id="criteria"></div>
|
| 335 |
+
|
| 336 |
+
<div class="comments-row">
|
| 337 |
+
<label for="anmerkungen">Anmerkungen</label>
|
| 338 |
+
<textarea id="anmerkungen" rows="3" placeholder="Optionale Anmerkungen..." oninput="markDirty()"></textarea>
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
|
| 342 |
+
<div class="actions">
|
| 343 |
+
<span class="save-hint" id="save-hint"></span>
|
| 344 |
+
<button class="btn-save" id="btn-save" onclick="saveAnnotation()" disabled>Speichern</button>
|
| 345 |
+
</div>
|
| 346 |
+
</div>
|
| 347 |
+
|
| 348 |
+
<div class="toast" id="toast"></div>
|
| 349 |
+
|
| 350 |
+
<script>
|
| 351 |
+
const CRITERIA = ["korrekt", "relevant", "vollstaendig", "kohaerenz"];
|
| 352 |
+
const CRITERIA_DESC = {
|
| 353 |
+
korrekt: "Faktisch korrekt? Keine Halluzinationen?",
|
| 354 |
+
relevant: "Nur wichtige Informationen? Keine Redundanzen?",
|
| 355 |
+
vollstaendig: "Alle wesentlichen Fakten und Entitaeten enthalten?",
|
| 356 |
+
kohaerenz: "Gut strukturiert und logisch nachvollziehbar?",
|
| 357 |
+
};
|
| 358 |
+
|
| 359 |
+
let allRows = [];
|
| 360 |
+
let filteredRows = [];
|
| 361 |
+
let currentIdx = 0;
|
| 362 |
+
let dirty = false;
|
| 363 |
+
|
| 364 |
+
/* ---------- Silent session ID (one per browser, prevents overwrites) ---------- */
|
| 365 |
+
|
| 366 |
+
function getSessionId() {
|
| 367 |
+
let id = localStorage.getItem("eval_session_id");
|
| 368 |
+
if (!id) {
|
| 369 |
+
id = crypto.randomUUID();
|
| 370 |
+
localStorage.setItem("eval_session_id", id);
|
| 371 |
+
}
|
| 372 |
+
return id;
|
| 373 |
+
}
|
| 374 |
+
const SESSION_ID = getSessionId();
|
| 375 |
+
|
| 376 |
+
/* ---------- Data loading ---------- */
|
| 377 |
+
|
| 378 |
+
async function loadEntries() {
|
| 379 |
+
const res = await fetch("/api/entries?annotator=" + encodeURIComponent(SESSION_ID));
|
| 380 |
+
if (!res.ok) { allRows = []; applyFilters(); return; }
|
| 381 |
+
allRows = await res.json();
|
| 382 |
+
applyFilters();
|
| 383 |
+
refreshProgress();
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
function applyFilters() {
|
| 387 |
+
if (dirty && !confirm("Ungespeicherte Aenderungen verwerfen?")) return;
|
| 388 |
+
dirty = false;
|
| 389 |
+
const status = document.getElementById("filter-status").value;
|
| 390 |
+
const rfa = document.getElementById("filter-rfa").value;
|
| 391 |
+
filteredRows = [...allRows];
|
| 392 |
+
if (status === "pending") {
|
| 393 |
+
filteredRows = filteredRows.filter(r => !r.bewertung);
|
| 394 |
+
} else if (status === "done") {
|
| 395 |
+
filteredRows = filteredRows.filter(r => r.bewertung);
|
| 396 |
+
}
|
| 397 |
+
if (rfa) {
|
| 398 |
+
filteredRows = filteredRows.filter(r => r.rfa === rfa);
|
| 399 |
+
}
|
| 400 |
+
const typeFilter = document.getElementById("filter-type").value;
|
| 401 |
+
if (typeFilter === "promptd") {
|
| 402 |
+
filteredRows = filteredRows.filter(r => r.has_prior_judgement);
|
| 403 |
+
} else if (typeFilter === "new") {
|
| 404 |
+
filteredRows = filteredRows.filter(r => !r.has_prior_judgement);
|
| 405 |
+
}
|
| 406 |
+
currentIdx = 0;
|
| 407 |
+
render();
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
async function refreshProgress() {
|
| 411 |
+
const res = await fetch("/api/progress?annotator=" + encodeURIComponent(SESSION_ID));
|
| 412 |
+
if (!res.ok) return;
|
| 413 |
+
const p = await res.json();
|
| 414 |
+
document.getElementById("progress-text").textContent = p.annotated + " / " + p.total;
|
| 415 |
+
const pct = p.total ? Math.round(100 * p.annotated / p.total) : 0;
|
| 416 |
+
document.getElementById("progress-fill").style.width = pct + "%";
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
/* ---------- Rendering ---------- */
|
| 420 |
+
|
| 421 |
+
function render() {
|
| 422 |
+
const card = document.getElementById("card");
|
| 423 |
+
if (filteredRows.length === 0) {
|
| 424 |
+
card.innerHTML = '<div class="empty-state">Keine Eintraege gefunden.</div>';
|
| 425 |
+
document.getElementById("counter").textContent = "0 / 0";
|
| 426 |
+
return;
|
| 427 |
+
}
|
| 428 |
+
// Ensure card structure is present (may have been replaced by empty state)
|
| 429 |
+
if (!document.getElementById("title")) { location.reload(); return; }
|
| 430 |
+
|
| 431 |
+
const row = filteredRows[currentIdx];
|
| 432 |
+
document.getElementById("counter").textContent = (currentIdx + 1) + " / " + filteredRows.length;
|
| 433 |
+
document.getElementById("btn-prev").disabled = currentIdx === 0;
|
| 434 |
+
document.getElementById("btn-next").disabled = currentIdx === filteredRows.length - 1;
|
| 435 |
+
|
| 436 |
+
document.getElementById("eval-id").textContent = row.eval_id;
|
| 437 |
+
document.getElementById("title").textContent =
|
| 438 |
+
(row.sende_haupttitel || "") + " \u2013 " + (row.beitragstitel || "");
|
| 439 |
+
|
| 440 |
+
const badge = document.getElementById("badge-rfa");
|
| 441 |
+
badge.textContent = row.rfa;
|
| 442 |
+
badge.className = "badge badge-" + row.rfa;
|
| 443 |
+
|
| 444 |
+
const isPromptD = row.has_prior_judgement;
|
| 445 |
+
document.getElementById("badge-promptd").style.display = isPromptD ? "" : "none";
|
| 446 |
+
document.getElementById("reference-banner").style.display = isPromptD ? "" : "none";
|
| 447 |
+
|
| 448 |
+
const pjEl = document.getElementById("prior-judgement");
|
| 449 |
+
if (isPromptD) {
|
| 450 |
+
pjEl.style.display = "";
|
| 451 |
+
const grid = document.getElementById("pj-grid");
|
| 452 |
+
grid.innerHTML =
|
| 453 |
+
'<div class="pj-item"><span class="pj-label">Gesamt</span><span class="pj-value">' + (row.prior_bewertung || "-") + '</span></div>' +
|
| 454 |
+
CRITERIA.map(c =>
|
| 455 |
+
'<div class="pj-item"><span class="pj-label">' + c.charAt(0).toUpperCase() + c.slice(1) +
|
| 456 |
+
'</span><span class="pj-value">' + (row["prior_" + c] || "-") + '</span></div>'
|
| 457 |
+
).join("");
|
| 458 |
+
const pjAnm = document.getElementById("pj-anmerkungen");
|
| 459 |
+
pjAnm.textContent = row.prior_anmerkungen ? "Anmerkungen: " + row.prior_anmerkungen : "";
|
| 460 |
+
} else {
|
| 461 |
+
pjEl.style.display = "none";
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
document.getElementById("summary").textContent = row.summary || "-";
|
| 465 |
+
document.getElementById("referenz").textContent = row.referenz || "-";
|
| 466 |
+
|
| 467 |
+
const hasTranslation = row.rfa === "DW" && row.translation;
|
| 468 |
+
if (hasTranslation) {
|
| 469 |
+
document.getElementById("transcript-label").textContent = "Transkript (Uebersetzung)";
|
| 470 |
+
document.getElementById("transcript").textContent = row.translation;
|
| 471 |
+
document.getElementById("original-toggle").style.display = "";
|
| 472 |
+
document.getElementById("original-text").textContent = row.transkript || "-";
|
| 473 |
+
document.getElementById("original-transcript").classList.remove("open");
|
| 474 |
+
document.getElementById("original-arrow").classList.remove("open");
|
| 475 |
+
} else {
|
| 476 |
+
document.getElementById("transcript-label").textContent = "Transkript";
|
| 477 |
+
document.getElementById("transcript").textContent = row.transkript || "-";
|
| 478 |
+
document.getElementById("original-toggle").style.display = "none";
|
| 479 |
+
document.getElementById("original-transcript").classList.remove("open");
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
// Bewertung dropdown
|
| 483 |
+
document.getElementById("bewertung").value = row.bewertung || "";
|
| 484 |
+
|
| 485 |
+
// Criteria
|
| 486 |
+
const criteriaEl = document.getElementById("criteria");
|
| 487 |
+
criteriaEl.innerHTML = "";
|
| 488 |
+
for (const c of CRITERIA) {
|
| 489 |
+
const val = (row[c] || "").toLowerCase();
|
| 490 |
+
const div = document.createElement("div");
|
| 491 |
+
div.className = "criterion";
|
| 492 |
+
div.innerHTML =
|
| 493 |
+
'<label>' + c.charAt(0).toUpperCase() + c.slice(1) + '</label>' +
|
| 494 |
+
'<span class="criterion-desc">' + CRITERIA_DESC[c] + '</span>' +
|
| 495 |
+
'<select data-criterion="' + c + '" onchange="onCriterionChange(this)">' +
|
| 496 |
+
'<option value=""' + (!val ? ' selected' : '') + '>(bitte waehlen)</option>' +
|
| 497 |
+
'<option value="ja"' + (val === 'ja' ? ' selected' : '') + '>ja</option>' +
|
| 498 |
+
'<option value="ausreichend"' + (val === 'ausreichend' ? ' selected' : '') + '>ausreichend</option>' +
|
| 499 |
+
'<option value="nein"' + (val === 'nein' ? ' selected' : '') + '>nein</option>' +
|
| 500 |
+
'</select>';
|
| 501 |
+
const sel = div.querySelector("select");
|
| 502 |
+
applyCriterionStyle(sel);
|
| 503 |
+
criteriaEl.appendChild(div);
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
// Anmerkungen
|
| 507 |
+
document.getElementById("anmerkungen").value = row.anmerkungen || "";
|
| 508 |
+
|
| 509 |
+
// Save hint
|
| 510 |
+
document.getElementById("save-hint").textContent = row.bewertung ? "Bereits bewertet" : "";
|
| 511 |
+
|
| 512 |
+
dirty = false;
|
| 513 |
+
document.getElementById("btn-save").disabled = true;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
function applyCriterionStyle(sel) {
|
| 517 |
+
const v = sel.value;
|
| 518 |
+
sel.className = v === "ja" ? "val-ja" : v === "ausreichend" ? "val-aus" : v === "nein" ? "val-nein" : "val-none";
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/* ---------- Interaction ---------- */
|
| 522 |
+
|
| 523 |
+
function markDirty() {
|
| 524 |
+
dirty = true;
|
| 525 |
+
document.getElementById("btn-save").disabled = false;
|
| 526 |
+
document.getElementById("save-hint").textContent = "";
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
function onCriterionChange(sel) {
|
| 530 |
+
applyCriterionStyle(sel);
|
| 531 |
+
markDirty();
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
function navigate(dir) {
|
| 535 |
+
if (dirty && !confirm("Ungespeicherte Aenderungen verwerfen?")) return;
|
| 536 |
+
dirty = false;
|
| 537 |
+
currentIdx = Math.max(0, Math.min(filteredRows.length - 1, currentIdx + dir));
|
| 538 |
+
render();
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
async function saveAnnotation() {
|
| 542 |
+
const row = filteredRows[currentIdx];
|
| 543 |
+
const data = {
|
| 544 |
+
annotator: SESSION_ID,
|
| 545 |
+
eval_id: row.eval_id,
|
| 546 |
+
bewertung: document.getElementById("bewertung").value || null,
|
| 547 |
+
anmerkungen: document.getElementById("anmerkungen").value || null,
|
| 548 |
+
};
|
| 549 |
+
document.querySelectorAll("#criteria select").forEach(sel => {
|
| 550 |
+
data[sel.dataset.criterion] = sel.value || null;
|
| 551 |
+
});
|
| 552 |
+
|
| 553 |
+
const res = await fetch("/api/annotate", {
|
| 554 |
+
method: "POST",
|
| 555 |
+
headers: { "Content-Type": "application/json" },
|
| 556 |
+
body: JSON.stringify(data),
|
| 557 |
+
});
|
| 558 |
+
|
| 559 |
+
if (res.ok) {
|
| 560 |
+
// Update local row data
|
| 561 |
+
for (const key of ["bewertung", "korrekt", "relevant", "vollstaendig", "kohaerenz", "anmerkungen"]) {
|
| 562 |
+
row[key] = data[key];
|
| 563 |
+
}
|
| 564 |
+
// Also update in allRows
|
| 565 |
+
const src = allRows.find(r => r.eval_id === row.eval_id);
|
| 566 |
+
if (src) Object.assign(src, data);
|
| 567 |
+
|
| 568 |
+
dirty = false;
|
| 569 |
+
document.getElementById("btn-save").disabled = true;
|
| 570 |
+
document.getElementById("save-hint").textContent = "Gespeichert";
|
| 571 |
+
showToast("Gespeichert", "success");
|
| 572 |
+
refreshProgress();
|
| 573 |
+
} else {
|
| 574 |
+
showToast("Fehler beim Speichern", "error");
|
| 575 |
+
}
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
/* ---------- Utilities ---------- */
|
| 579 |
+
|
| 580 |
+
function showToast(msg, type) {
|
| 581 |
+
const el = document.getElementById("toast");
|
| 582 |
+
el.textContent = msg;
|
| 583 |
+
el.className = "toast " + type + " show";
|
| 584 |
+
setTimeout(() => el.classList.remove("show"), 2000);
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
document.addEventListener("keydown", e => {
|
| 588 |
+
if (e.target.tagName === "SELECT" || e.target.tagName === "TEXTAREA") return;
|
| 589 |
+
if (e.key === "ArrowLeft") navigate(-1);
|
| 590 |
+
if (e.key === "ArrowRight") navigate(1);
|
| 591 |
+
});
|
| 592 |
+
|
| 593 |
+
/* ---------- Guidelines toggle ---------- */
|
| 594 |
+
|
| 595 |
+
function toggleGuidelines() {
|
| 596 |
+
const body = document.getElementById("guidelines-body");
|
| 597 |
+
const arrow = document.getElementById("guidelines-arrow");
|
| 598 |
+
body.classList.toggle("open");
|
| 599 |
+
arrow.classList.toggle("open");
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
function toggleOriginal() {
|
| 603 |
+
const body = document.getElementById("original-transcript");
|
| 604 |
+
const arrow = document.getElementById("original-arrow");
|
| 605 |
+
body.classList.toggle("open");
|
| 606 |
+
arrow.classList.toggle("open");
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
/* ---------- Init ---------- */
|
| 610 |
+
loadEntries();
|
| 611 |
+
</script>
|
| 612 |
+
</body>
|
| 613 |
+
</html>
|
pyproject.toml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "annotation-app"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"flask==3.1.3",
|
| 9 |
+
]
|
uv.lock
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version = 1
|
| 2 |
+
revision = 3
|
| 3 |
+
requires-python = ">=3.12"
|
| 4 |
+
|
| 5 |
+
[options]
|
| 6 |
+
exclude-newer = "2026-04-09T08:47:56.7359646Z"
|
| 7 |
+
exclude-newer-span = "P7D"
|
| 8 |
+
|
| 9 |
+
[[package]]
|
| 10 |
+
name = "annotation-app"
|
| 11 |
+
version = "0.1.0"
|
| 12 |
+
source = { virtual = "." }
|
| 13 |
+
dependencies = [
|
| 14 |
+
{ name = "flask" },
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
[package.metadata]
|
| 18 |
+
requires-dist = [{ name = "flask", specifier = "==3.1.3" }]
|
| 19 |
+
|
| 20 |
+
[[package]]
|
| 21 |
+
name = "blinker"
|
| 22 |
+
version = "1.9.0"
|
| 23 |
+
source = { registry = "https://pypi.org/simple" }
|
| 24 |
+
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
| 25 |
+
wheels = [
|
| 26 |
+
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
[[package]]
|
| 30 |
+
name = "click"
|
| 31 |
+
version = "8.3.2"
|
| 32 |
+
source = { registry = "https://pypi.org/simple" }
|
| 33 |
+
dependencies = [
|
| 34 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 35 |
+
]
|
| 36 |
+
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
|
| 37 |
+
wheels = [
|
| 38 |
+
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
[[package]]
|
| 42 |
+
name = "colorama"
|
| 43 |
+
version = "0.4.6"
|
| 44 |
+
source = { registry = "https://pypi.org/simple" }
|
| 45 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
| 46 |
+
wheels = [
|
| 47 |
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
[[package]]
|
| 51 |
+
name = "flask"
|
| 52 |
+
version = "3.1.3"
|
| 53 |
+
source = { registry = "https://pypi.org/simple" }
|
| 54 |
+
dependencies = [
|
| 55 |
+
{ name = "blinker" },
|
| 56 |
+
{ name = "click" },
|
| 57 |
+
{ name = "itsdangerous" },
|
| 58 |
+
{ name = "jinja2" },
|
| 59 |
+
{ name = "markupsafe" },
|
| 60 |
+
{ name = "werkzeug" },
|
| 61 |
+
]
|
| 62 |
+
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
| 63 |
+
wheels = [
|
| 64 |
+
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
[[package]]
|
| 68 |
+
name = "itsdangerous"
|
| 69 |
+
version = "2.2.0"
|
| 70 |
+
source = { registry = "https://pypi.org/simple" }
|
| 71 |
+
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
| 72 |
+
wheels = [
|
| 73 |
+
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
[[package]]
|
| 77 |
+
name = "jinja2"
|
| 78 |
+
version = "3.1.6"
|
| 79 |
+
source = { registry = "https://pypi.org/simple" }
|
| 80 |
+
dependencies = [
|
| 81 |
+
{ name = "markupsafe" },
|
| 82 |
+
]
|
| 83 |
+
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
| 84 |
+
wheels = [
|
| 85 |
+
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
[[package]]
|
| 89 |
+
name = "markupsafe"
|
| 90 |
+
version = "3.0.3"
|
| 91 |
+
source = { registry = "https://pypi.org/simple" }
|
| 92 |
+
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
| 93 |
+
wheels = [
|
| 94 |
+
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
| 95 |
+
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
| 96 |
+
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
| 97 |
+
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
| 98 |
+
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
| 99 |
+
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
| 100 |
+
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
| 101 |
+
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
| 102 |
+
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
| 103 |
+
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
| 104 |
+
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
| 105 |
+
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
| 106 |
+
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
| 107 |
+
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
| 108 |
+
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
| 109 |
+
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
| 110 |
+
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
| 111 |
+
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
| 112 |
+
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
| 113 |
+
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
| 114 |
+
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
| 115 |
+
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
| 116 |
+
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
| 117 |
+
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
| 118 |
+
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
| 119 |
+
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
| 120 |
+
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
| 121 |
+
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
| 122 |
+
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
| 123 |
+
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
| 124 |
+
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
| 125 |
+
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
| 126 |
+
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
| 127 |
+
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
| 128 |
+
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
| 129 |
+
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
| 130 |
+
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
| 131 |
+
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
| 132 |
+
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
| 133 |
+
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
| 134 |
+
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
| 135 |
+
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
| 136 |
+
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
| 137 |
+
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
| 138 |
+
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
| 139 |
+
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
| 140 |
+
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
| 141 |
+
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
| 142 |
+
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
| 143 |
+
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
| 144 |
+
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
| 145 |
+
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
| 146 |
+
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
| 147 |
+
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
| 148 |
+
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
| 149 |
+
]
|
| 150 |
+
|
| 151 |
+
[[package]]
|
| 152 |
+
name = "werkzeug"
|
| 153 |
+
version = "3.1.8"
|
| 154 |
+
source = { registry = "https://pypi.org/simple" }
|
| 155 |
+
dependencies = [
|
| 156 |
+
{ name = "markupsafe" },
|
| 157 |
+
]
|
| 158 |
+
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
|
| 159 |
+
wheels = [
|
| 160 |
+
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
|
| 161 |
+
]
|