From bf51fb30c0f6913bd1017261b9240e46a4c665c5 Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Wed, 2 Sep 2026 08:25:55 +0000 Subject: [PATCH] =?UTF-8?q?fix(youtube):=20las=20etiquetas=20salen=20del?= =?UTF-8?q?=20spec,=20no=20de=20la=20consulta=20de=20investigaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01EHakatofHeJAzdXL26Q5bq --- src/generator/youtube.py | 66 +++++++++++++++++++++++++++++++++++----- tests/test_youtube.py | 64 ++++++++++++++++++++++++++++++++------ 2 files changed, 112 insertions(+), 18 deletions(-) diff --git a/src/generator/youtube.py b/src/generator/youtube.py index 27b5599..7d1931f 100644 --- a/src/generator/youtube.py +++ b/src/generator/youtube.py @@ -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", diff --git a/tests/test_youtube.py b/tests/test_youtube.py index 5d56a1e..a188746 100644 --- a/tests/test_youtube.py +++ b/tests/test_youtube.py @@ -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():