fix(youtube): las etiquetas salen del spec, no de la consulta de investigación
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
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:
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user