Albert Arnó

For my Catalan friends and Alexa · Pels meus companys catalans i Alexa

Alexa no té cap resum de notícies en català. Amb mitja hora i zero euros, te’n pots fer un amb el butlletí horari de 3CatInfo.

A note in Catalan for Catalan speakers: how to make an Amazon Echo play the hourly news bulletin from 3CatInfo (Catalunya Ràdio) when you say “Alexa, pon las noticias”, using a private Flash Briefing skill and a free AWS Lambda. The code below works for anyone.

A casa tenim Alexa configurada en castellà, perquè en català no n’hi ha. Cada matí li demanava «Alexa, pon las noticias» i em posava notícies en castellà d’arreu. Volia sentir 3CatInfo, i al catàleg de notícies d’Alexa no hi ha ni Catalunya Ràdio ni 3CatInfo.

La bona notícia és que no cal esperar que ningú ho publiqui. Alexa deixa que qualsevol persona es faci el seu propi resum de notícies (un Flash Briefing) en mode desenvolupament, sense publicar-lo, i 3Cat penja cada hora l’emissió de 3CatInfo en MP3. Només cal una peça petita que les uneixi. Tot plegat porta uns 30 minuts i costa 0 €.

Per què no surt Catalunya Ràdio a les notícies d’Alexa

El resum de notícies d’Alexa només ofereix les skills Flash Briefing publicades per al mercat i l’idioma del teu Echo. A amazon.es, en castellà, la CCMA no en té cap. I Alexa no parla català, així que tampoc hi ha un mercat català on buscar-la. Des de l’app no ho arreglaràs.

Com funciona la solució

  1. Dius «Alexa, pon las noticias».
  2. Alexa consulta la teva skill Flash Briefing privada.
  3. La skill llegeix un petit feed JSON que serveix una funció AWS Lambda.
  4. La Lambda pregunta a l’API pública de 3Cat quina és l’última hora de 3CatInfo publicada (el programa 710, «Notícies 3CatInfo – hora a hora»).
  5. Alexa reprodueix els primers ~4 minuts d’aquella hora: el butlletí d’inici d’hora.

Alguns detalls:

  • Frescor. 3Cat publica l’hora sencera en MP3 uns 10–15 minuts després d’acabar. Les notícies que sentiràs tenen entre 15 minuts i unes 2 hores.
  • El retall. L’MP3 dura una hora. La Lambda en demana només els primers 4 MB amb una petició de rang (128 kbps = 16 KB/s, uns 250 segons).
  • Per què no els «Titulars». 3Cat té també un àudio curt de titulars de Catalunya Informació, però el seu MP3 no es renova de manera fiable. Les emissions horàries sí.
  • Seguretat. La ruta d’àudio només accepta fitxers MP3 de 3Cat, perquè la Lambda no es converteixi en un proxy obert.

Què necessites

QuèPer quèNota
Compte d’Amazon dels Echo (amazon.es)Entrar al developer console i a l’app AlexaHa de ser el mateix compte en tots dos llocs
Compte d’AWSAllotjar la LambdaCapa gratuïta: 1 milió de peticions al mes
Una imatge de 512 × 512 (PNG o JPG)Icona del feedQualsevol imatge serveix
AWS CLI (opcional)Fer-ho tot des del terminalTambé es pot fer des de la consola web

Regió d’AWS recomanada: eu-west-1 (Irlanda), la més propera als servidors europeus d’Alexa.

Pas 1 · Crear la Lambda

Una funció Python 3.13, 256 MB de memòria i 30 segons de timeout. No necessita cap llibreria externa.

Des de la consola web: AWS Console → Lambda → regió eu-west-1Create functionAuthor from scratch:

  1. Nom: flash_briefing_3catinfo.
  2. Runtime: Python 3.13.
  3. Permisos: Create a new role with basic Lambda permissions.
  4. Enganxa el codi a lambda_function.py i prem Deploy.
  5. A Configuration → General configuration, posa 256 MB i 30 s.
"""Feed Flash Briefing d'Alexa amb el butlletí horari de 3CatInfo.

Lambda amb Function URL pública (auth NONE), dos camins:

  GET /        -> JSON Flash Briefing amb l'hora més recent de 3CatInfo
  GET /audio   -> primers ~4 min d'aquell MP3 (butlletí d'inici d'hora)
"""

import base64
import json
import os
import re
import urllib.request
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

API_URL = (
    "https://api.3cat.cat/audios?_format=json&programaradio_id=710"
    "&items_pagina=6&ordre=-data_publicacio&version=2.0"
)
MP3_BASE = "https://audios.3catvideos.cat/multimedia/"
# Només rutes d'MP3 de 3Cat: evita que /audio sigui un proxy obert.
MP3_PATH_RE = re.compile(r"^mp3/\d/\d/\d+\.mp3$")
# Emissions horàries ("3CatInfo, de 18 a 19 h - 22/09/2026").
HOURLY_TITLE_RE = re.compile(r"^3CatInfo, d.+ h - \d{2}/\d{2}/\d{4}$")
CLIP_BYTES = int(os.environ.get("CLIP_BYTES", "4000000"))  # ~250 s
MADRID = ZoneInfo("Europe/Madrid")
USER_AGENT = "alexa-flash-briefing/1.0"


def _get(url, headers=None, timeout=10):
    req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, **(headers or {})})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return resp.read()


def _latest_hour():
    items = json.loads(_get(API_URL))["resposta"]["items"]["item"]
    for item in items:
        audios = item.get("audios") or []
        if HOURLY_TITLE_RE.match(item.get("titol", "")) and audios:
            path = audios[0]["text"]
            if MP3_PATH_RE.match(path):
                return item, path
    raise RuntimeError("No s'ha trobat cap emissió horària de 3CatInfo")


def _feed(event):
    item, path = _latest_hour()
    start = datetime.strptime(item["data_publicacio"], "%d/%m/%Y %H:%M:%S").replace(tzinfo=MADRID)
    host = event["requestContext"]["domainName"]
    body = {
        "uid": f"3catinfo-{item['id']}",
        "updateDate": start.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.0Z"),
        "titleText": item["titol"],
        "mainText": "",
        "streamUrl": f"https://{host}/audio?src={path}",
        "redirectionUrl": "https://www.3cat.cat/3cat/directes/3catinfo-radio/",
    }
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json; charset=utf-8", "Cache-Control": "max-age=300"},
        "body": json.dumps(body, ensure_ascii=False),
    }


def _audio(event):
    path = (event.get("queryStringParameters") or {}).get("src", "")
    if not MP3_PATH_RE.match(path):
        return {"statusCode": 400, "body": "src invàlid"}
    data = _get(MP3_BASE + path, headers={"Range": f"bytes=0-{CLIP_BYTES - 1}"}, timeout=20)[:CLIP_BYTES]
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "audio/mpeg", "Cache-Control": "max-age=86400"},
        "isBase64Encoded": True,
        "body": base64.b64encode(data).decode("ascii"),
    }


def lambda_handler(event, context):
    route = event.get("rawPath", "/")
    try:
        if route == "/audio":
            return _audio(event)
        return _feed(event)
    except Exception as exc:
        print(f"ERROR {route}: {exc!r}")
        return {"statusCode": 502, "body": "3Cat no disponible"}

Des del terminal (alternativa), amb el codi desat a lambda_function.py i un rol que tingui la política AWSLambdaBasicExecutionRole:

zip -j fb.zip lambda_function.py
aws lambda create-function --region eu-west-1 \
  --function-name flash_briefing_3catinfo \
  --runtime python3.13 --handler lambda_function.lambda_handler \
  --role arn:aws:iam::<EL_TEU_COMPTE>:role/<EL_TEU_ROL> \
  --zip-file fileb://fb.zip --memory-size 256 --timeout 30

Pas 2 · Donar-li una URL pública i provar-la

Alexa ha de poder llegir el feed sense autenticació, així que la Lambda necessita una Function URL amb autenticació NONE.

Consola web: a la funció → Configuration → Function URLCreate function URL → Auth type NONESave. La consola ja hi afegeix els permisos públics.

Terminal:

F=flash_briefing_3catinfo; R=eu-west-1
aws lambda create-function-url-config --region $R --function-name $F --auth-type NONE
aws lambda add-permission --region $R --function-name $F --statement-id public-url \
  --action lambda:InvokeFunctionUrl --principal '*' --function-url-auth-type NONE
aws lambda add-permission --region $R --function-name $F --statement-id public-url-invoke \
  --action lambda:InvokeFunction --principal '*' --invoked-via-function-url

Obtindràs una URL del tipus https://<id>.lambda-url.eu-west-1.on.aws/. Prova-la:

curl https://<id>.lambda-url.eu-west-1.on.aws/

Ha de tornar un JSON amb un titleText com «3CatInfo, de 18 a 19 h – …» i un streamUrl. Obre el streamUrl al navegador: hauries de sentir uns 4 minuts de 3CatInfo.

Pas 3 · Crear la skill Flash Briefing

La skill es queda en mode desenvolupament: no l’has d’enviar a certificació ni publicar-la, i funciona als Echo del teu compte.

  1. Entra a developer.amazon.com/alexa/console/ask amb el compte d’amazon.es dels teus Echo.
  2. Create Skill → nom Noticies3CatInfo → idioma Spanish (ES) → tipus Flash Briefing.
  3. Custom error message: No he podido cargar las noticias de 3CatInfo.
  4. Add new feed, amb aquests valors:
CampValor
PreambleDe 3CatInfo (en castellà ha de començar per «En» o «De»)
Name3CatInfo
Content update frequencyHourly
Content typeAudio
Content genreNews
FeedLa URL de la Lambda del pas 2
Feed iconLa teva imatge de 512 × 512
  1. Save. En aquest moment Amazon crida el feed per validar-lo.
  2. Pestanya TestSkill testing is enabled in: Development. Al simulador, amb Spanish (ES), escriu pon las noticias: hauries de sentir el butlletí.

A la pestanya Test la skill no surt llistada enlloc, i el missatge «Skill I/O is available only…» és normal en un Flash Briefing.

Pas 4 · Activar-la a l’app Alexa

  1. App Alexa → Settings → Preferences → News (en castellà: Configuración → Preferencias → Noticias).
  2. Add content → busca Noticies3CatInfo i activa-la.
  3. Posa-la la primera, o treu les altres fonts si només vols 3CatInfo.
  4. Digues «Alexa, pon las noticias».

Si no trobes l’apartat News, escriu «news settings» al quadre Ask Alexa about settings de la mateixa pantalla, o busca la skill a More → Skills & Games → Your Skills → Dev.

Problemes habituals

ProblemaSolució
El Preamble no s’acceptaEn castellà ha de començar per «En» o «De»: De 3CatInfo
La skill no surt a l’appL’app i el developer console han d’estar amb el mateix compte d’Amazon; espera uns minuts que es propagui
Alexa+ respon amb el seu propi resumProva «Alexa, pon mi resumen de noticias» o «Alexa, ¿qué hay de nuevo?»
Les notícies semblen d’una hora enrereÉs normal: 3Cat publica l’hora sencera 10–15 minuts després d’acabar, i Alexa guarda el feed uns minuts
El butlletí es talla massa aviat o tardCanvia la variable d’entorn CLIP_BYTES (16.000 bytes per segon; màxim uns 4.400.000, pel límit de 6 MB de Lambda)
Error «3Cat no disponible»3Cat ha canviat l’API o els títols: mira els logs de la Lambda a CloudWatch

L’àudio és de 3Cat (CCMA) i només l’escoltes tu, als teus dispositius. Aquesta guia no redistribueix cap contingut: la Lambda només retalla, per a ús privat, l’MP3 públic de l’última hora.

Si ho muntes i t’hi trobes algun problema, escriu-me. I si a la CCMA hi ha algú llegint això: una skill oficial de Flash Briefing de 3CatInfo per a Alexa faria feliç molta gent.