station-announcer/gen_photo_alt_text/routes.py
2025-11-15 10:54:32 +00:00

166 lines
4.9 KiB
Python

"""Flask routes for the alt text generator."""
from __future__ import annotations
import base64
from datetime import datetime
from flask import (
Blueprint,
Response,
abort,
current_app,
flash,
redirect,
render_template,
request,
url_for,
)
from .immich import ImmichError
from .openai_client import AltTextGenerationError
bp = Blueprint("main", __name__)
def _parse_timestamp(raw: str | None) -> datetime | None:
if not raw:
return None
try:
normalized = raw.replace("Z", "+00:00")
return datetime.fromisoformat(normalized)
except ValueError:
return None
def _humanize_timestamp(raw: str | None) -> str | None:
dt = _parse_timestamp(raw)
if not dt:
return raw
return dt.strftime("%d %b %Y / %H:%M")
def _timestamp_with_timezone(raw: str | None) -> str | None:
dt = _parse_timestamp(raw)
if not dt:
return raw
label = dt.strftime("%d %b %Y %H:%M")
tzname = dt.tzname()
if tzname:
return f"{label} {tzname}"
offset = dt.utcoffset()
if offset is not None:
total_minutes = int(offset.total_seconds() // 60)
hours, minutes = divmod(abs(total_minutes), 60)
sign = "+" if total_minutes >= 0 else "-"
return f"{label} UTC{sign}{hours:02d}:{minutes:02d}"
return f"{label} (timezone unknown)"
@bp.route("/")
def index():
immich_client = current_app.immich_client
alt_cache = current_app.alt_text_cache
days = current_app.config.get("RECENT_DAYS", 3)
assets = []
error_message = None
try:
for asset in immich_client.get_recent_assets(days=days):
assets.append(
{
"id": asset.id,
"file_name": asset.file_name,
"captured_at": asset.captured_at,
"captured_display": _humanize_timestamp(asset.captured_at),
"thumbnail_url": asset.thumbnail_url,
"alt_text": alt_cache.get(asset.id),
}
)
except ImmichError as exc:
error_message = str(exc)
return render_template("index.html", assets=assets, error_message=error_message)
@bp.route("/assets/<asset_id>", methods=["GET", "POST"])
def asset_detail(asset_id: str):
immich_client = current_app.immich_client
alt_cache = current_app.alt_text_cache
generator = current_app.alt_text_generator
error_message = None
notes = ""
try:
asset = immich_client.get_asset(asset_id)
except ImmichError as exc:
return render_template(
"detail.html",
asset=None,
alt_text=None,
error_message=str(exc),
notes=notes,
)
alt_text = alt_cache.get(asset_id)
if request.method == "POST":
notes = request.form.get("notes", "")
try:
content, mime_type = immich_client.fetch_asset_content(asset_id, "preview")
data_url = "data:{};base64,{}".format(
mime_type, base64.b64encode(content).decode("ascii")
)
location_text = asset.location_label
coordinates_text = None
if asset.latitude is not None and asset.longitude is not None:
coordinates_text = f"{asset.latitude:.5f}, {asset.longitude:.5f}"
captured_text = _timestamp_with_timezone(asset.captured_at)
generated = generator.generate_alt_text(
data_url,
notes,
captured_at=captured_text,
location=location_text,
coordinates=coordinates_text,
)
alt_cache.set(asset_id, generated)
flash("Alt text generated.")
return redirect(url_for("main.asset_detail", asset_id=asset_id))
except ImmichError as exc:
error_message = f"Failed to fetch image: {exc}"
except AltTextGenerationError as exc:
error_message = str(exc)
formatted_asset = {
"id": asset.id,
"file_name": asset.file_name,
"captured_at": asset.captured_at,
"captured_display": _humanize_timestamp(asset.captured_at),
"thumbnail_url": asset.thumbnail_url,
"preview_url": asset.preview_url,
"original_url": asset.original_url,
"web_url": asset.web_url,
"latitude": asset.latitude,
"longitude": asset.longitude,
"location": asset.location_label,
}
return render_template(
"detail.html",
asset=formatted_asset,
alt_text=alt_cache.get(asset_id),
error_message=error_message,
notes=notes,
)
@bp.route("/proxy/assets/<asset_id>/<variant>")
def asset_proxy(asset_id: str, variant: str):
immich_client = current_app.immich_client
try:
content, mimetype = immich_client.fetch_asset_content(asset_id, variant)
except ImmichError as exc:
abort(404, description=str(exc))
return Response(content, mimetype=mimetype)