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() return re.sub(r"\s+", " ", str(text)).strip()
def _tags_from(topic: str, spec_id: str = "") -> list[str]: def _spec_text(spec: dict) -> str:
"""Etiquetas del tema, sin repetir las de base y sin pasarse de los 500 """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. caracteres que YouTube cuenta sumando toda la lista.
El tema entero va primero como una sola etiqueta: partido en palabras deja **Salen del spec, no del `topic`.** El tema es la consulta de investigación
cosas como "New" y "Mexico" sueltas, que no buscan igual que "Socorro New y en la mitad de las sesiones está en español: el Short de Trans-en-Provence
Mexico 1964". Las palabras sueltas van detrás igualmente, que cuestan poco. 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} seen = {t.casefold() for t in BASE_TAGS}
tags = list(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: if phrase and phrase.casefold() not in seen:
seen.add(phrase.casefold()) seen.add(phrase.casefold())
tags.append(phrase) 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: for word in words:
low = word.casefold() low = word.casefold()
if low in seen or low in _STOPWORDS or len(word) < 3: 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": { "snippet": {
"title": title, "title": title,
"description": description, "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), "categoryId": str(category_id or settings.youtube_category_id),
"defaultLanguage": "en", "defaultLanguage": "en",
"defaultAudioLanguage": "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"] assert topic not in meta["snippet"]["description"]
for palabra in ("análisis", "suelo", "evidencia"): for palabra in ("análisis", "suelo", "evidencia"):
assert palabra not in meta["snippet"]["description"] 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(): 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(): def test_tags_drop_stopwords_and_duplicates_and_respect_the_limit():
tags = build_metadata(SPEC, "The Landing of the UFO in Socorro New Mexico" tags = build_metadata(SPEC, "The Radars of the FAA in Alaska")["snippet"]["tags"]
)["snippet"]["tags"]
lowered = [t.casefold() for t in tags] lowered = [t.casefold() for t in tags]
assert "the" not in lowered and "of" not in lowered and "in" not in lowered assert "the" not in lowered and "of" not in lowered and "in" not in lowered
assert len(lowered) == len(set(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 assert sum(len(t) + 1 for t in tags) <= yt.MAX_TAGS_CHARS
def test_the_whole_topic_is_one_tag(): def test_tags_never_carry_the_spanish_that_only_lived_in_the_search_query():
"""Partido en palabras deja "New" y "Mexico" sueltas, que no buscan igual.""" """El caso real: el Short de Trans-en-Provence, canal en inglés.
tags = build_metadata(SPEC, "Socorro New Mexico 1964")["snippet"]["tags"]
assert "Socorro New Mexico 1964" in tags Se etiquetó con `análisis`, `suelo`, `evidencia` y `física` porque las
assert "Socorro" in tags, "las sueltas también, que cuestan poco" 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(): def test_tags_stay_under_the_limit_with_an_absurd_topic():