fix(youtube): las etiquetas salen del spec, no de la consulta de investigación
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s

El Short de Trans-en-Provence subió al canal en inglés etiquetado `análisis`,
`suelo`, `evidencia` y `física`; el de Cash-Landrum con `quemaduras`,
`radiación`, `demanda` y `gobierno`. Salían del `topic`, que es la consulta de
investigación y en la mitad de las sesiones está en español.

No se arregla con una lista de palabras en español ni con un detector de idioma,
que serían el parche sin fin — y además el criterio correcto no es el idioma:
`BASE AÉREA TALAVERA` es el nombre de la base y está DIBUJADO en pantalla, así
que etiquetarlo es correcto. Al revés, `Zimbabwe`, `Brazil` y `Texas` sólo
estaban en la consulta y son justo lo que se busca.

Así que el tema no se tira, se criba: llega a etiqueta si el vídeo lo dice. El
spec es inglés por construcción (título e id los escribe el modelo, los props
son lo dibujado), y sirve de criba sin que haya que saber de idiomas.

Y un segundo fallo que salió al medir: la etiqueta de frase se cortaba a media
palabra en CINCO de los ocho Shorts generados ("...GEPAN CNES análisis suelo
evi", "...demanda gobiern"). Eran etiquetas muertas, y era justamente la
etiqueta que la función existía para poner. Ahora sale del `meta.id`, que ya
venía en inglés y es la frase que se busca de verdad — "socorro 1964 zamora",
"ariel school 1994" — y el recorte, si hace falta, corta por palabra.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EHakatofHeJAzdXL26Q5bq
This commit is contained in:
ChemaVX
2026-09-02 08:25:55 +00:00
co-authored by Claude Opus 5
parent 068b7feefa
commit bf51fb30c0
2 changed files with 112 additions and 18 deletions
+58 -8
View File
@@ -148,23 +148,73 @@ def _clean(text: str) -> str:
return re.sub(r"\s+", " ", str(text)).strip()
def _tags_from(topic: str, spec_id: str = "") -> list[str]:
"""Etiquetas del tema, sin repetir las de base y sin pasarse de los 500
def _spec_text(spec: dict) -> str:
"""Todo el texto que el vídeo enseña o dice, que es inglés por construcción.
El título y el `id` los escribe el modelo en inglés, y los props son lo que
se dibuja en pantalla. Sirve de criba: una palabra que no está aquí no está
en el vídeo.
"""
meta = spec.get("meta") or {}
parts = [str(meta.get("title") or ""), str(meta.get("id") or "").replace("_", " ")]
def walk(node: Any) -> None:
if isinstance(node, dict):
for value in node.values():
walk(value)
elif isinstance(node, list):
for value in node:
walk(value)
elif isinstance(node, str):
parts.append(node)
walk(spec.get("shots"))
return " ".join(parts)
def _words(text: str) -> list[str]:
return re.findall(r"[\w'-]+", text)
def _tags_from(spec: dict, topic: str) -> list[str]:
"""Etiquetas del vídeo, sin repetir las de base y sin pasarse de los 500
caracteres que YouTube cuenta sumando toda la lista.
El tema entero va primero como una sola etiqueta: partido en palabras deja
cosas como "New" y "Mexico" sueltas, que no buscan igual que "Socorro New
Mexico 1964". Las palabras sueltas van detrás igualmente, que cuestan poco.
**Salen del spec, no del `topic`.** El tema es la consulta de investigación
y en la mitad de las sesiones está en español: el Short de Trans-en-Provence
se subió al canal en inglés etiquetado `análisis`, `suelo`, `evidencia` y
`física`, y el de Cash-Landrum con `quemaduras`, `radiación` y `gobierno`.
El tema no se tira, se **criba**: una palabra suya llega a etiqueta sólo si
el vídeo la dice. Así sobreviven los nombres propios que sólo estaban en la
consulta —Zimbabwe, Brazil, Texas, New Mexico— y se caen las palabras que
nunca salieron de la caja de búsqueda. Y no hace falta lista de palabras en
español ni detector de idioma, que serían el parche sin fin: el criterio no
es el idioma, es si el vídeo lo dice. Por eso `BASE AÉREA TALAVERA` sí
etiqueta — es el nombre de la base, y está dibujado en pantalla.
La etiqueta de frase sale del `meta.id`: es la que buscan de verdad
("socorro 1964 zamora", "ariel school 1994"), y ya venía en inglés. Antes
era el tema entero recortado a 60 caracteres, que en cinco de los ocho
Shorts generados lo partía a media palabra —"...GEPAN CNES análisis suelo
evi"— y una frase partida no la busca nadie.
"""
seen = {t.casefold() for t in BASE_TAGS}
tags = list(BASE_TAGS)
meta = spec.get("meta") or {}
phrase = _clean(topic)[:MAX_TAG]
phrase = _clean(str(meta.get("id") or "").replace("_", " "))
if len(phrase) > MAX_TAG:
# Cortada por palabra: media palabra no la busca nadie.
phrase = phrase[:MAX_TAG].rsplit(" ", 1)[0]
if phrase and phrase.casefold() not in seen:
seen.add(phrase.casefold())
tags.append(phrase)
words = re.findall(r"[\w'-]+", f"{topic} {spec_id.replace('_', ' ')}")
spoken = _spec_text(spec).casefold()
words = (_words(str(meta.get("id") or "").replace("_", " "))
+ _words(str(meta.get("title") or ""))
+ [w for w in _words(topic) if w.casefold() in spoken])
for word in words:
low = word.casefold()
if low in seen or low in _STOPWORDS or len(word) < 3:
@@ -244,7 +294,7 @@ def build_metadata(spec: dict, topic: str, article_url: Optional[str] = None,
"snippet": {
"title": title,
"description": description,
"tags": _tags_from(topic, str(meta.get("id") or "")),
"tags": _tags_from(spec, topic),
"categoryId": str(category_id or settings.youtube_category_id),
"defaultLanguage": "en",
"defaultAudioLanguage": "en",
+54 -10
View File
@@ -326,8 +326,6 @@ def test_the_research_topic_never_reaches_the_description():
assert topic not in meta["snippet"]["description"]
for palabra in ("análisis", "suelo", "evidencia"):
assert palabra not in meta["snippet"]["description"]
# Pero sigue alimentando las etiquetas, donde una frase de búsqueda sí vale.
assert any(topic.startswith(tag[:20]) for tag in meta["snippet"]["tags"])
def test_the_description_survives_a_spec_with_nothing_to_cite():
@@ -348,21 +346,67 @@ def test_title_falls_back_to_the_topic():
def test_tags_drop_stopwords_and_duplicates_and_respect_the_limit():
tags = build_metadata(SPEC, "The Landing of the UFO in Socorro New Mexico"
)["snippet"]["tags"]
tags = build_metadata(SPEC, "The Radars of the FAA in Alaska")["snippet"]["tags"]
lowered = [t.casefold() for t in tags]
assert "the" not in lowered and "of" not in lowered and "in" not in lowered
assert len(lowered) == len(set(lowered))
assert "socorro" in lowered
assert "radars" in lowered, "el vídeo lo dice en el título"
assert "faa" in lowered, "el vídeo lo dibuja en un plano"
assert sum(len(t) + 1 for t in tags) <= yt.MAX_TAGS_CHARS
def test_the_whole_topic_is_one_tag():
"""Partido en palabras deja "New" y "Mexico" sueltas, que no buscan igual."""
tags = build_metadata(SPEC, "Socorro New Mexico 1964")["snippet"]["tags"]
assert "Socorro New Mexico 1964" in tags
assert "Socorro" in tags, "las sueltas también, que cuestan poco"
def test_tags_never_carry_the_spanish_that_only_lived_in_the_search_query():
"""El caso real: el Short de Trans-en-Provence, canal en inglés.
Se etiquetó con `análisis`, `suelo`, `evidencia` y `física` porque las
etiquetas salían del `topic`, que es la consulta de investigación. No se
arregla con una lista de palabras en español eso es el parche sin fin
sino cambiando de fuente: ahora salen del spec, que es inglés por
construcción, y del tema sólo lo que el vídeo de verdad dice.
"""
spec = {"meta": {"id": "trans_en_provence_1981",
"title": "Trans-en-Provence: Ground Trace, Lab Analysis"},
"shots": [{"template": "signal_strips",
"props": {"headline": "GEPAN ANALYSIS · SOIL"}}]}
topic = "Trans-en-Provence Francia 1981 GEPAN CNES análisis suelo evidencia física"
tags = build_metadata(spec, topic)["snippet"]["tags"]
lowered = {t.casefold() for t in tags}
for basura in ("análisis", "suelo", "evidencia", "física", "francia", "cnes"):
assert basura not in lowered, f"{basura} sólo estaba en la consulta"
# Y lo que el vídeo sí dice sobrevive.
assert "gepan" in lowered
assert "trans-en-provence" in lowered
def test_a_topic_word_survives_if_the_video_says_it():
"""La criba no es por idioma, es por si el vídeo lo dice.
Zimbabwe, Brazil y Texas sólo estaban en la consulta y son justo lo que se
busca; `BASE AÉREA TALAVERA` está en español y también, porque es el nombre
de la base y sale dibujado. Un filtro por idioma habría tirado los dos.
"""
spec = {"meta": {"id": "talavera_1976", "title": "Green Humanoid at the Air Base"},
"shots": [{"template": "data_card",
"props": {"card_title": "BASE AÉREA TALAVERA"}}]}
tags = build_metadata(spec, "Talavera 1976 OVNI humanoide Base Aérea")["snippet"]["tags"]
lowered = {t.casefold() for t in tags}
assert "aérea" in lowered, "está dibujado en pantalla"
assert "ovni" not in lowered and "humanoide" not in lowered
def test_the_phrase_tag_is_never_cut_mid_word():
"""En cinco de los ocho Shorts generados la frase salía partida —"...GEPAN
CNES análisis suelo evi"— y una frase partida no la busca nadie."""
largo = "_".join(["palabra"] * 12) # muy por encima de MAX_TAG
spec = {"meta": {"id": largo, "title": "T"}, "shots": []}
frase = build_metadata(spec, "x")["snippet"]["tags"][len(yt.BASE_TAGS)]
assert len(frase) <= yt.MAX_TAG
assert not frase.endswith("palabr"), "cortada a media palabra"
assert frase.split()[-1] == "palabra"
def test_tags_stay_under_the_limit_with_an_absurd_topic():