@danhachuel/thunderbolt 0.3.88 → 0.3.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/main.py
CHANGED
|
@@ -64,7 +64,7 @@ from hermes_ui.thumbnails import (
|
|
|
64
64
|
upload_thumbnail_image,
|
|
65
65
|
)
|
|
66
66
|
from hermes_ui.draft_video import DRAFT_SETTING_SECTIONS, missing_content_fields, missing_setting_sections, normalise_saved_script, setting_widget_suffixes
|
|
67
|
-
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_thumbnail_prompt, generate_title_and_keywords, generate_topic_for_channel, generate_video_keywords
|
|
67
|
+
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_thumbnail_prompt, generate_title_and_keywords, generate_topic_for_channel, generate_video_description, generate_video_keywords
|
|
68
68
|
from hermes_ui.media_generation import MediaGenerationError, generate_image_for_card, generate_video_for_card
|
|
69
69
|
from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
|
|
70
70
|
from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
|
|
@@ -4701,8 +4701,39 @@ def render_upload_conventional():
|
|
|
4701
4701
|
channel = selected_youtube_channel or channel_map.get(str(task.get("channel_id")), {})
|
|
4702
4702
|
account = direct_accounts.get(str(channel.get("google_account_id", "")))
|
|
4703
4703
|
if "YouTube" in destination:
|
|
4704
|
-
|
|
4705
|
-
|
|
4704
|
+
detected_language = normalize_video_language(task.get("language") or channel.get("language") or "pt")
|
|
4705
|
+
language_state_key = f"yt_language_{task['id']}"
|
|
4706
|
+
language_source_key = f"yt_language_source_{task['id']}"
|
|
4707
|
+
if st.session_state.get(language_source_key) != detected_language:
|
|
4708
|
+
st.session_state[language_state_key] = detected_language
|
|
4709
|
+
st.session_state[language_source_key] = detected_language
|
|
4710
|
+
title_state_key = f"yt_title_{task['id']}"
|
|
4711
|
+
title = st.text_input("Título", value=task.get("title") or task.get("topic", "Vídeo Thunderbolt"), key=title_state_key)
|
|
4712
|
+
description_state_key = f"yt_description_{task['id']}"
|
|
4713
|
+
description_error_key = f"yt_description_ai_error_{task['id']}"
|
|
4714
|
+
|
|
4715
|
+
def generate_upload_description_callback() -> None:
|
|
4716
|
+
current_title = str(st.session_state.get(title_state_key) or title).strip()
|
|
4717
|
+
raw_tags = st.session_state.get(f"yt_tags_{task['id']}", task.get("tags", ""))
|
|
4718
|
+
current_tags = raw_tags if isinstance(raw_tags, list) else [item.strip() for item in str(raw_tags or "").split(",") if item.strip()]
|
|
4719
|
+
try:
|
|
4720
|
+
st.session_state[description_state_key] = generate_video_description(
|
|
4721
|
+
settings,
|
|
4722
|
+
channel,
|
|
4723
|
+
str(task.get("topic") or current_title),
|
|
4724
|
+
title=current_title,
|
|
4725
|
+
tags=current_tags,
|
|
4726
|
+
language=detected_language,
|
|
4727
|
+
)
|
|
4728
|
+
except CreativeGenerationError as exc:
|
|
4729
|
+
st.session_state[description_error_key] = str(exc)
|
|
4730
|
+
else:
|
|
4731
|
+
st.session_state.pop(description_error_key, None)
|
|
4732
|
+
|
|
4733
|
+
description = st.text_area("Descrição", value=task.get("description", ""), key=description_state_key, height=100)
|
|
4734
|
+
st.button("Gerar descrição com IA", key=f"yt_description_ai_{task['id']}", use_container_width=False, on_click=generate_upload_description_callback)
|
|
4735
|
+
if description_error := str(st.session_state.get(description_error_key) or "").strip():
|
|
4736
|
+
st.error(f"Não foi possível gerar a descrição: {description_error}")
|
|
4706
4737
|
tags_raw = st.text_input("Tags separadas por vírgula", value=task.get("tags", "") if isinstance(task.get("tags", ""), str) else ", ".join(task.get("tags", [])), key=f"yt_tags_{task['id']}")
|
|
4707
4738
|
yt_cols = st.columns(3)
|
|
4708
4739
|
with yt_cols[0]:
|
|
@@ -4711,7 +4742,13 @@ def render_upload_conventional():
|
|
|
4711
4742
|
with yt_cols[1]:
|
|
4712
4743
|
category_id = st.text_input("Category ID", value="22", key=f"yt_category_{task['id']}")
|
|
4713
4744
|
with yt_cols[2]:
|
|
4714
|
-
language = st.
|
|
4745
|
+
language = st.selectbox(
|
|
4746
|
+
"Idioma",
|
|
4747
|
+
VIDEO_LANGUAGE_SELECTION_OPTIONS,
|
|
4748
|
+
index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(detected_language) if detected_language in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0,
|
|
4749
|
+
format_func=video_language_label,
|
|
4750
|
+
key=language_state_key,
|
|
4751
|
+
)
|
|
4715
4752
|
quota_count = official_upload_count(channel, account)
|
|
4716
4753
|
st.caption(f"API Oficial hoje: {quota_count}/{OFFICIAL_DAILY_LIMIT} envios nesta conta Gmail.")
|
|
4717
4754
|
if not selected_youtube_channel:
|
|
@@ -6366,6 +6403,22 @@ def render_supabase_tutorial():
|
|
|
6366
6403
|
st.markdown(tutorial_content, unsafe_allow_html=True)
|
|
6367
6404
|
|
|
6368
6405
|
|
|
6406
|
+
def render_youtube_frontend_upload_tutorial():
|
|
6407
|
+
"""Render the safe operational guide for the YouTube direct-upload workflow."""
|
|
6408
|
+
tutorial_path = ROOT / "seed" / "references" / "youtube-video-upload-frontend.md"
|
|
6409
|
+
st.title("Tutorial YouTube Video-Upload Frontend")
|
|
6410
|
+
st.caption("Guia prático e seguro para rever metadados, sessão e envio directo de vídeos concluídos.")
|
|
6411
|
+
st.markdown("[Abrir referência técnica no GitHub](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api)")
|
|
6412
|
+
try:
|
|
6413
|
+
tutorial_content = tutorial_path.read_text(encoding="utf-8").strip()
|
|
6414
|
+
except OSError:
|
|
6415
|
+
tutorial_content = ""
|
|
6416
|
+
if not tutorial_content:
|
|
6417
|
+
st.error("O conteúdo local do tutorial não está disponível. Consulte a referência técnica no GitHub.")
|
|
6418
|
+
return
|
|
6419
|
+
st.markdown(tutorial_content, unsafe_allow_html=False)
|
|
6420
|
+
|
|
6421
|
+
|
|
6369
6422
|
def render_mcp():
|
|
6370
6423
|
st.title("MCP")
|
|
6371
6424
|
st.caption("Clientes externos, servidor MCP do Thunderbolt e a skill local ficam separados para evitar confundir funções diferentes.")
|
|
@@ -6671,6 +6724,7 @@ def main():
|
|
|
6671
6724
|
("Tutorial Supabase", ":material/storage:", "Tutorial Supabase"),
|
|
6672
6725
|
("Tutorial Kaggle", ":material/menu_book:", "Tutorial Kaggle"),
|
|
6673
6726
|
("Tutorial Apify", ":material/menu_book:", "Tutorial Apify"),
|
|
6727
|
+
("Tutorial YouTube Video-Upload Frontend", ":material/video_library:", "Tutorial YouTube Video-Upload Frontend"),
|
|
6674
6728
|
]
|
|
6675
6729
|
settings_items = [
|
|
6676
6730
|
("MCP", ":material/hub:", "MCP"),
|
|
@@ -6809,6 +6863,7 @@ def main():
|
|
|
6809
6863
|
"Documentação": lambda: render_edit_placeholder("Documentação", "Seleccione um tutorial no menu expansível."),
|
|
6810
6864
|
"Tutorial Meta": render_models_ai_tutorial,
|
|
6811
6865
|
"Tutorial Supabase": render_supabase_tutorial,
|
|
6866
|
+
"Tutorial YouTube Video-Upload Frontend": render_youtube_frontend_upload_tutorial,
|
|
6812
6867
|
"Configurações": lambda: render_edit_placeholder("Configurações", "Seleccione uma opção no menu expansível."),
|
|
6813
6868
|
"MCP": render_mcp,
|
|
6814
6869
|
"Contas Google": render_google_accounts,
|
|
@@ -121,6 +121,46 @@ def channel_context(channel: dict[str, Any], blueprint: dict[str, Any] | None =
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def generate_video_description(
|
|
125
|
+
settings: dict[str, Any],
|
|
126
|
+
channel: dict[str, Any],
|
|
127
|
+
topic: str,
|
|
128
|
+
*,
|
|
129
|
+
title: str = "",
|
|
130
|
+
tags: list[str] | None = None,
|
|
131
|
+
language: str = "",
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Generate a concise YouTube description through the configured text LLM pool."""
|
|
134
|
+
topic = str(topic or "").strip()
|
|
135
|
+
title = str(title or "").strip()
|
|
136
|
+
if not topic and not title:
|
|
137
|
+
raise CreativeGenerationError("É necessário um tópico ou título antes de gerar a descrição do vídeo.")
|
|
138
|
+
context = channel_context(channel)
|
|
139
|
+
normalized_tags = [str(item).strip() for item in (tags or []) if str(item).strip()][:15]
|
|
140
|
+
system = (
|
|
141
|
+
"És um editor de metadados de YouTube. Cria uma descrição útil, clara e envolvente para o vídeo, "
|
|
142
|
+
"sem alegações não verificadas, sem clickbait falso e sem mencionar que foi gerada por IA. "
|
|
143
|
+
"Usa dois parágrafos curtos, inclui um convite natural para subscrever quando apropriado e devolve "
|
|
144
|
+
"apenas JSON válido com a chave description."
|
|
145
|
+
)
|
|
146
|
+
user = json.dumps(
|
|
147
|
+
{
|
|
148
|
+
"channel": context,
|
|
149
|
+
"language": language or context["language"],
|
|
150
|
+
"topic": topic,
|
|
151
|
+
"title": title or topic,
|
|
152
|
+
"tags": normalized_tags,
|
|
153
|
+
"requirements": {"paragraphs": 2, "max_characters": 1800, "no_unverified_claims": True},
|
|
154
|
+
},
|
|
155
|
+
ensure_ascii=False,
|
|
156
|
+
)
|
|
157
|
+
result = _chat_json(settings, system, user)
|
|
158
|
+
description = str(result.get("description") or "").strip()
|
|
159
|
+
if not description:
|
|
160
|
+
raise CreativeGenerationError("O provider LLM não devolveu uma descrição válida para o vídeo.")
|
|
161
|
+
return description[:1800]
|
|
162
|
+
|
|
163
|
+
|
|
124
164
|
def generate_topic_for_channel(
|
|
125
165
|
settings: dict[str, Any],
|
|
126
166
|
channel: dict[str, Any],
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Tutorial YouTube Video-Upload Frontend
|
|
2
|
+
|
|
3
|
+
Este tutorial explica como utilizar, de forma responsável, o fluxo **Direct upload** do Thunderbolt para vídeos já concluídos. A rota é uma integração técnica baseada em comportamentos do frontend do YouTube e pode mudar sem aviso. Use-a somente em contas e canais que controla e respeite as políticas aplicáveis do YouTube.
|
|
4
|
+
|
|
5
|
+
> **Segurança primeiro:** dados de sessão, cookies de autenticação, identificadores de canal e material de credencial são confidenciais. Nunca os copie para conversas, capturas de ecrã, issues, ficheiros versionados ou campos de texto comuns. Configure-os exclusivamente nos campos protegidos da aplicação local.
|
|
6
|
+
|
|
7
|
+
## 1. Antes de começar
|
|
8
|
+
|
|
9
|
+
Confirme que o vídeo foi concluído no **Video Backlog** e que o respectivo canal YouTube está seleccionado no Thunderbolt. No formulário de Upload, confirme título, descrição, tags, privacidade, categoria e idioma antes de autorizar qualquer envio.
|
|
10
|
+
|
|
11
|
+
O Thunderbolt oferece dois percursos distintos: o fluxo oficial, que utiliza a integração de API autorizada, e o fluxo directo, destinado a uma sessão válida e a uma conta controlada pelo utilizador. Estes percursos não devem ser usados para contornar políticas, limites da plataforma ou permissões de canal.
|
|
12
|
+
|
|
13
|
+
| Elemento | Finalidade | Boa prática |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| Vídeo | Ficheiro final da tarefa concluída | Verifique duração, áudio, miniatura e legendas antes do envio. |
|
|
16
|
+
| Metadados | Título, descrição, tags, idioma e privacidade | Reveja manualmente antes de publicar. A descrição por IA pode ser editada. |
|
|
17
|
+
| Canal | Destino associado à conta autorizada | Confirme que é o canal correcto, sobretudo em contas com vários canais. |
|
|
18
|
+
| Sessão | Autorização temporária de upload directo | Mantenha-a apenas em armazenamento local protegido e actualize-a quando expirar. |
|
|
19
|
+
|
|
20
|
+
## 2. Preparar a autenticação de forma segura
|
|
21
|
+
|
|
22
|
+
No Thunderbolt, abra **Upload > Direct upload** e utilize os controlos de autorização fornecidos pela interface. Complete o procedimento somente no seu navegador e conta YouTube. Não partilhe dados de sessão, cookies, tokens temporários, IDs de canal ou chaves de integração com terceiros.
|
|
23
|
+
|
|
24
|
+
Se a verificação de saúde indicar sessão expirada, interrompa o envio e renove a autorização pela interface. O sistema deve apresentar o diagnóstico sem mostrar os valores protegidos. Nunca use valores de exemplo ou chaves encontradas em documentos públicos.
|
|
25
|
+
|
|
26
|
+
## 3. Envio em partes e memória
|
|
27
|
+
|
|
28
|
+
O envio directo pode transferir o ficheiro em partes, em vez de carregar todo o vídeo para a memória de uma só vez. Esta estratégia é adequada para ficheiros grandes e reduz a pressão de memória no computador. O Thunderbolt mantém o vídeo no armazenamento local e regista apenas o estado e o resultado do processo.
|
|
29
|
+
|
|
30
|
+
## 4. Executar o envio
|
|
31
|
+
|
|
32
|
+
Abra **Upload > Conventional upload** para rever os metadados. Utilize **Gerar descrição com IA** apenas quando quiser uma sugestão editorial; o resultado permanece editável. O campo **Idioma** é detectado da tarefa ou canal e é mostrado numa lista suspensa para evitar códigos escritos incorrectamente.
|
|
33
|
+
|
|
34
|
+
Depois de confirmar o canal, a privacidade e os metadados, seleccione o fluxo de envio apropriado. Um envio publica ou cria conteúdo numa plataforma externa; por isso, o Thunderbolt deve exigir a confirmação explícita do utilizador antes da acção.
|
|
35
|
+
|
|
36
|
+
## 5. Diagnóstico e manutenção
|
|
37
|
+
|
|
38
|
+
| Situação | Acção recomendada |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| Sessão expirada ou não autorizada | Renove a autorização na interface e volte a executar a verificação de saúde. |
|
|
41
|
+
| Canal incorrecto | Cancele antes do envio e seleccione o canal certo. |
|
|
42
|
+
| Falha de rede | Não repita automaticamente sem confirmar se o vídeo foi criado no YouTube Studio. |
|
|
43
|
+
| Metadados incompletos | Corrija título, descrição, tags, idioma ou privacidade no formulário antes de enviar. |
|
|
44
|
+
| Limite ou política da plataforma | Pare o processo e siga as políticas e limites actualmente aplicáveis do YouTube. |
|
|
45
|
+
|
|
46
|
+
## Referência técnica
|
|
47
|
+
|
|
48
|
+
O material fornecido pelo utilizador cita o repositório [Nojus10/YouTube-Video-Upload-Frontend-Api](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api). O Thunderbolt apresenta este tutorial como orientação de integração segura; não replica segredos, valores de autenticação nem instruções de extracção de credenciais.
|