Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/GustavoNightmare/InformacionMuseo/llms.txt

Use this file to discover all available pages before exploring further.

BioScan Museo’s audio narration system uses a separate servertts microservice to pre-generate MP3 files for each species. The integration is push-based: Flask calls the TTS service whenever a species record is created, edited, or has its QR style updated — the TTS service never pulls data from Flask. There is no /api/public route registered in Flask; all species data flows outward from Flask to the TTS sidecar via its /internal/species/sync and /internal/species/delete endpoints.

How the push pipeline works

When an admin saves a species through the admin panel, Flask performs the following steps after committing the database change:
  1. Builds a structured content payload with build_species_tts_payload().
  2. Calls sync_species_to_tts(), which POSTs that payload to the TTS sidecar’s /internal/species/sync endpoint, authenticated with the MUSEO_TTS_SHARED_KEY shared secret.
  3. The TTS service stores the payload locally and pre-generates audio files in one or more narration styles.
When a species is deleted, Flask calls delete_species_from_tts() instead, which POSTs to the TTS sidecar’s /internal/species/delete endpoint so the sidecar can purge the corresponding audio files. The two environment variables that govern the integration are:
VariablePurpose
MUSEO_TTS_INTERNAL_BASE_URLBase URL of the TTS sidecar reachable from Flask (internal network). Falls back to MUSEO_TTS_PUBLIC_BASE_URL if unset. If both are empty, sync calls are silently skipped.
MUSEO_TTS_SHARED_KEYShared secret sent as the X-API-Key header on every request Flask makes to the TTS sidecar. A missing or empty key causes sync_species_to_tts() and delete_species_from_tts() to raise RuntimeError.

build_species_tts_payload()

Defined in app.py, this function constructs the JSON object that Flask sends to the TTS sidecar. It reads directly from the Species model and applies no transformations other than replacing None with safe defaults.
def build_species_tts_payload(species: Species) -> dict:
    return {
        "species_id": species.id,
        "qr_id": species.qr_id or species.id,
        "common_name": species.nombre_comun or "",
        "scientific_name": species.nombre_cientifico or "",
        "description": species.descripcion or "",
        "habitat": species.habitat or "",
        "diet": species.dieta or "",
        "curiosities": species.curiosidades or [],
    }

Payload fields

species_id
string
The internal primary-key ID of the species (Species.id).
qr_id
string
The QR slug. Falls back to species_id when Species.qr_id is not set.
common_name
string
Common display name (nombre_comun). Empty string if not set.
scientific_name
string
Scientific name (nombre_cientifico). Empty string if not set.
description
string
Free-text species description (descripcion). Empty string if not set.
habitat
string
Habitat description (habitat). Empty string if not set.
diet
string
Diet description (dieta). Empty string if not set.
curiosities
array
List of curiosity strings, deserialised from curiosidades_json. Empty array if none are stored.

Example payload

{
  "species_id": "condor-001",
  "qr_id": "condor-001",
  "common_name": "Cóndor Andino",
  "scientific_name": "Vultur gryphus",
  "description": "Ave carroñera emblemática de los Andes colombianos.",
  "habitat": "Zonas montañosas andinas por encima de los 3 000 m.",
  "diet": "Carroña de mamíferos grandes como llamas, vacas y caballos.",
  "curiosities": [
    "Planea largas distancias sin batir las alas aprovechando corrientes térmicas.",
    "Puede pasar varios días sin comer y luego consumir hasta 5 kg de carroña de una sola vez."
  ]
}

sync_species_to_tts()

Called automatically after every successful species create or edit (including QR style updates). Posts the build_species_tts_payload() result to POST {MUSEO_TTS_INTERNAL_BASE_URL}/internal/species/sync with a 180-second timeout. If the TTS service returns HTTP 400 or higher, a RuntimeError is raised and Flask shows a flash warning — the species record itself has already been saved.
def sync_species_to_tts(species: Species) -> None:
    if not MUSEO_TTS_INTERNAL_BASE_URL:
        return  # TTS integration disabled — no URL configured

    shared_key = (os.getenv("MUSEO_TTS_SHARED_KEY") or "").strip()
    if not shared_key:
        raise RuntimeError("MUSEO_TTS_SHARED_KEY no está configurado")

    url = f"{MUSEO_TTS_INTERNAL_BASE_URL}/internal/species/sync"
    response = requests.post(
        url,
        json=build_species_tts_payload(species),
        headers={"X-API-Key": shared_key},
        timeout=180,
    )
    if response.status_code >= 400:
        raise RuntimeError(
            f"TTS sync HTTP {response.status_code}: {response.text[:300]}"
        )

When it is called

Admin actionTriggers sync?
Create new species (POST /admin/especies/nueva)✅ Yes
Edit species (POST /admin/especies/<id>/editar)✅ Yes
Save QR style (POST /admin/qr/<id>/personalizar)✅ Yes
Delete species (POST /admin/especies/<id>/eliminar)❌ No — calls delete_species_from_tts() instead
Reindex RAG (POST /admin/especies/<id>/reindex)❌ No

delete_species_from_tts()

Called immediately after a species is deleted from the database. Posts a minimal JSON body to POST {MUSEO_TTS_INTERNAL_BASE_URL}/internal/species/delete so the TTS sidecar can purge the corresponding audio files. Uses a 60-second timeout. Errors are silently swallowed at the call site to prevent a TTS failure from blocking the admin UI.
def delete_species_from_tts(species_id: str, qr_id: str | None = None) -> None:
    if not MUSEO_TTS_INTERNAL_BASE_URL:
        return

    shared_key = (os.getenv("MUSEO_TTS_SHARED_KEY") or "").strip()
    if not shared_key:
        raise RuntimeError("MUSEO_TTS_SHARED_KEY no está configurado")

    payload = {"species_id": species_id}
    if qr_id:
        payload["qr_id"] = qr_id

    url = f"{MUSEO_TTS_INTERNAL_BASE_URL}/internal/species/delete"
    response = requests.post(
        url,
        json=payload,
        headers={"X-API-Key": shared_key},
        timeout=60,
    )
    if response.status_code >= 400:
        raise RuntimeError(
            f"TTS delete HTTP {response.status_code}: {response.text[:300]}"
        )

Delete request body

FieldTypeDescription
species_idstringInternal species ID (Species.id). Always present.
qr_idstringQR slug captured before the delete. Included when available so the TTS sidecar can locate audio files by slug.

Environment variable reference

VariableRequiredDescription
MUSEO_TTS_INTERNAL_BASE_URLRecommendedInternal base URL of the TTS sidecar (e.g., http://servertts:8010). If empty, sync and delete calls are skipped silently. Takes precedence over MUSEO_TTS_PUBLIC_BASE_URL.
MUSEO_TTS_PUBLIC_BASE_URLFallbackPublic base URL used when MUSEO_TTS_INTERNAL_BASE_URL is not set.
MUSEO_TTS_SHARED_KEYRequired when TTS URL is setShared secret sent as the X-API-Key header. Both Flask and the TTS sidecar must be configured with the same value.
There is no Flask endpoint that the TTS sidecar calls to retrieve species data. The data flow is one-way: Flask → TTS sidecar. Kiosk apps and visitor-facing clients that need audio narration should request MP3 files directly from the TTS service (e.g., GET /tts/by-qr/<qr_id>?key=<TTS_API_KEY>), not from Flask.

Build docs developers (and LLMs) love