@danhachuel/thunderbolt 0.2.29 → 0.2.30

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Este manual descreve a instalação local da UI Thunderbolt, baseada no MoneyPrinterTurbo, utilizando o pacote npm `@danhachuel/thunderbolt`. O fluxo recomendado instala automaticamente o ambiente Python, as dependências da aplicação, as dependências do MoneyPrinterTurbo, o Streamlit e o suporte FFmpeg através de `imageio-ffmpeg`.
4
4
 
5
- > **Versão deste manual:** 0.2.29
5
+ > **Versão deste manual:** 0.2.30
6
6
  > **Pacote npm:** `@danhachuel/thunderbolt`
7
7
  > **Porta padrão da UI:** `localhost:3030`
8
8
  > **Repositório:** [github.com/DanHachuel/thunderbolt](https://github.com/DanHachuel/thunderbolt)
@@ -100,13 +100,13 @@ Execute:
100
100
  Windows PowerShell ou MobaXterm:
101
101
 
102
102
  ```powershell
103
- npx.cmd --yes @danhachuel/thunderbolt@0.2.29 install
103
+ npx.cmd --yes @danhachuel/thunderbolt@0.2.30 install
104
104
  ```
105
105
 
106
106
  Linux/macOS:
107
107
 
108
108
  ```bash
109
- npx --yes @danhachuel/thunderbolt@0.2.29 install
109
+ npx --yes @danhachuel/thunderbolt@0.2.30 install
110
110
  ```
111
111
 
112
112
  A instalação normal é **segura para actualizações**: preserva `storage`, Blueprints, Brandings, configurações e artefactos do utilizador. Remove apenas `.venv`, o clone técnico do MoneyPrinterTurbo e dependências que serão recriadas. Uma pasta antiga sem dados do utilizador, como `C:\Users\<utilizador>\AppData\Local\hermes` da tentativa incompleta, pode ser removida; uma pasta antiga que contenha Blueprints, Brandings ou storage é preservada e apenas avisada no terminal. Feche processos Python, Node, Streamlit e MobaXterm que estejam a usar as pastas antes de executar.
@@ -362,7 +362,7 @@ Após iniciar a aplicação, valide o seguinte percurso:
362
362
  2. **Blueprints:** coloque um JSON em `~/.thunderbolt/storage/blueprints/importados/` (Windows: `%LOCALAPPDATA%\\THUNDERBOLT\storage\blueprints\importados\`) ou use o carregador da interface.
363
363
  3. **Brandings:** abra a subaba **Brandings** e confirme a listagem dos ficheiros JSON.
364
364
  4. **Canais:** em **Importar do YouTube**, use o método **Página pública — sem API Key** com um URL `/channel/UC...`, um handle ou uma subpágina `/videos`; o parser resolve o ID, consulta a página pública e tenta o RSS quando necessário. Confirme que o resultado abre o formulário de revisão sem Data API Key. Se o canal não existir ou não fornecer metadados, confirme a mensagem clara e que o formulário de uma pesquisa anterior desaparece. A Data API é opcional e fica separada; em **Cadastro manual**, preencha os dados sem qualquer consulta externa.
365
- 5. **Niche Finder:** abra a página na sidebar, clique em **Baixar Dataset Kaggle** ou carregue um CSV próprio e confirme o ficheiro em `storage/data/niches/`. Com um CSV que contenha `title`, `publish_date`, `country`, `view_count`, `like_count` e `comment_count`, clique em **Usar CSV carregado**, escolha 3 clusters e suporte 0,05 e execute **Analisar Nichos**. Confirme as métricas, a tabela de clusters, o gráfico Plotly, as regras de associação e os dados filtrados. Altere país, engagement, datas e tags e confirme que a análise é recalculada.
365
+ 5. **Niche Finder:** abra a página na sidebar e aguarde a preparação automática. Confirme que não aparece upload de CSV, botão de download de dataset ou selector de ficheiros. A análise inicial deve carregar directamente métricas, clusters, gráfico Plotly, regras de associação e dados analisados. Altere país, engagement, datas e tags e confirme que **Analisar Nichos** recalcula os resultados.
366
366
  6. **Pipeline > Criação de Vídeos:** teste primeiro o modo **Canal específico** e depois os modos de lote.
367
367
  7. **Criação de Vídeos > Vídeos:** verifique o estado `to_do` e os botões **Iniciar** e **Parar** dentro da subaba. Confirme também que **Criação de Músicas** mostra o mesmo fluxo com título próprio.
368
368
  8. **Upload:** configure o OAuth Client ID e Secret, autorize primeiro o **youtube-automation-agent** na própria aba, confirme o estado **pronto para publicar**, preencha título/descrição/tags e publique um MP4 real. O botão **Autorizar fallback OAuth** existe apenas para redundância; a Data API Key, se configurada, é exclusivamente para consultas oficiais públicas e nunca substitui OAuth.
@@ -380,11 +380,11 @@ Após iniciar a aplicação, valide o seguinte percurso:
380
380
 
381
381
  A página **Niche Finder** integra a lógica adaptada do projecto open source [johanfortus/Niche-Finder](https://github.com/johanfortus/Niche-Finder), cujo projecto original usa K-Means e FP-Growth sobre o dataset público [Trending Youtube Video Statistics (113 Countries)](https://www.kaggle.com/datasets/asaniczka/trending-youtube-videos-113-countries). No Thunderbolt não existe Flask, rota HTTP adicional, template HTML, JavaScript D3 ou segundo processo; toda a análise é síncrona no Streamlit.
382
382
 
383
- O botão **Baixar Dataset Kaggle** guarda a cópia em `storage/data/niches/`. O carregador usa KaggleHub para obter o dataset público; não existem credenciais hardcoded. Também é possível carregar um CSV próprio e clicar em **Usar CSV carregado**. O esquema mínimo é `title`, `publish_date`, `country`, `view_count`, `like_count` e `comment_count`; `video_tags` é opcional para obter itemsets e regras de associação. A cache utiliza `st.cache_data` para evitar leituras repetidas do mesmo ficheiro.
383
+ Ao abrir a página, o Thunderbolt prepara automaticamente os dados públicos através do KaggleHub e inicia a análise. O utilizador não precisa de baixar datasets, carregar CSVs, preparar planilhas ou escolher ficheiros. A interface apresenta directamente os parâmetros da busca e os resultados.
384
384
 
385
- Os parâmetros da UI são número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, engagement, intervalo de datas e tags. O núcleo normaliza os dados, calcula engagement, aplica filtros, faz transformação logarítmica e standardização, executa K-Means e calcula itemsets/regras com FP-Growth. Os resultados são DataFrames de clusters, itemsets frequentes, regras de associação e dados filtrados; o gráfico de dispersão é criado nativamente com Plotly. A cache de dados e os CSVs locais são ignorados pelo Git.
385
+ Os parâmetros da UI são número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, engagement, intervalo de datas e tags. O núcleo normaliza os dados, calcula engagement, aplica filtros, faz transformação logarítmica e standardização, executa K-Means e calcula itemsets/regras com FP-Growth. A análise inicial é automática; o botão **Analisar Nichos** recalcula os resultados depois de alterar os filtros. Os resultados são DataFrames de clusters, itemsets frequentes, regras de associação e dados analisados; o gráfico de dispersão é criado nativamente com Plotly.
386
386
 
387
- As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.29 install`; o instalador detecta e reutiliza o que já estiver válido.
387
+ As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.30 install`; o instalador detecta e reutiliza o que já estiver válido.
388
388
 
389
389
  ## Pipeline: Criação de Vídeos, Criação de Músicas e Automação
390
390
 
package/README.md CHANGED
@@ -17,7 +17,7 @@ A primeira versão implementa a camada UI independente com:
17
17
  | Canais | Subabas de importação pública sem API Key, Data API opcional e cadastro manual independente |
18
18
  | Criação de Vídeos / Criação de Músicas | Subabas Criar vídeo e Vídeos; lotes; 51 rótulos de idioma; Pexels/Pixabay, full IA com Estilo IA e Apenas Música com agente musical; a segunda página reutiliza o mesmo fluxo com título próprio |
19
19
  | Automação | Lista de vídeos e canais, selectores editáveis de Blueprint/voz padrão, Automação ON e horário diário HH:MM; UI configurável sem worker em segundo plano |
20
- | Niche Finder | Dataset Kaggle ou CSV próprio, cache local, filtros, K-Means, FP-Growth, regras de associação, clusters e gráfico Plotly nativo |
20
+ | Niche Finder | Preparação e análise automáticas, filtros, K-Means, FP-Growth, regras de associação, clusters e gráfico Plotly nativo |
21
21
  | Upload | YouTube via `youtube-automation-agent` adaptado internamente, OAuth directo de redundância, Upload directo experimental, TikTok, Instagram e Facebook Pages no front end |
22
22
  | MCP | Catálogo local opcional de Short Video Maker, AutoVio, OpenMontage e OpenCut, com portas editáveis e activação |
23
23
  | Limpador de Metadados | Upload isolado de vídeos terceiros, limpeza FFmpeg, edição de título/descrição/tags e manifesto JSON |
@@ -26,7 +26,7 @@ A primeira versão implementa a camada UI independente com:
26
26
 
27
27
  Os adaptadores do MoneyPrinterTurbo e de publicação nas plataformas são ligados pelas configurações locais e pelos pontos de integração em `integrations/`. A UI não inventa dados quando um serviço externo ou credencial não está disponível.
28
28
 
29
- ## Navegação da UI 0.2.29
29
+ ## Navegação da UI 0.2.30
30
30
 
31
31
  A barra lateral mantém apenas os cinco níveis principais, nesta ordem: **Início**, **Pipeline**, **Automação**, **Niche Finder** e **Configurações**. **Pipeline** é expansível e contém **Criação de Vídeos**, **Criação de Músicas**, **Upload** e **Limpador de Metadados**. **Configurações** é expansível e contém **Canais**, **Blueprints**, **MCP** e **Configurações Técnicas**. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
32
32
 
@@ -109,9 +109,9 @@ thunderbolt
109
109
  No Windows PowerShell, se `npx` for bloqueado por `npx.ps1`, use directamente `npx.cmd`:
110
110
 
111
111
  ```powershell
112
- npx.cmd --yes @danhachuel/thunderbolt@0.2.29 install
113
- npx.cmd --yes @danhachuel/thunderbolt@0.2.29 doctor
114
- npx.cmd --yes @danhachuel/thunderbolt@0.2.29
112
+ npx.cmd --yes @danhachuel/thunderbolt@0.2.30 install
113
+ npx.cmd --yes @danhachuel/thunderbolt@0.2.30 doctor
114
+ npx.cmd --yes @danhachuel/thunderbolt@0.2.30
115
115
  ```
116
116
 
117
117
  Como alternativa, pode permitir scripts para o seu utilizador:
@@ -131,13 +131,11 @@ Após `install`, o launcher usa o ambiente virtual instalado em `~/.thunderbolt/
131
131
 
132
132
  ## Niche Finder
133
133
 
134
- A página **Niche Finder** integra a lógica do projecto open source [johanfortus/Niche-Finder](https://github.com/johanfortus/Niche-Finder) directamente no processo Streamlit. O Thunderbolt não inicia Flask, não abre uma porta adicional e não copia templates HTML ou D3. A análise usa o dataset público [Trending Youtube Video Statistics (113 Countries)](https://www.kaggle.com/datasets/asaniczka/trending-youtube-videos-113-countries), mas também aceita um CSV próprio.
134
+ A página **Niche Finder** integra a lógica do projecto open source [johanfortus/Niche-Finder](https://github.com/johanfortus/Niche-Finder) directamente no processo Streamlit. O Thunderbolt não inicia Flask, não abre uma porta adicional e não copia templates HTML ou D3. Ao abrir a página, o próprio Thunderbolt prepara automaticamente os dados públicos necessários através do KaggleHub, guarda a cache localmente e executa a análise; o utilizador não precisa de baixar datasets, carregar CSVs ou preparar planilhas.
135
135
 
136
- Na sidebar da página, use **Baixar Dataset Kaggle** para descarregar e guardar o dataset em `storage/data/niches/`, ou carregue um CSV e clique em **Usar CSV carregado**. A cache é mantida localmente e o carregador valida as colunas mínimas `title`, `publish_date`, `country`, `view_count`, `like_count` e `comment_count`. Não são necessárias credenciais Kaggle hardcoded: o KaggleHub obtém o dataset público e guarda o resultado na cache local.
136
+ A interface apresenta directamente os parâmetros da busca: número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, categoria de engagement, intervalo de datas e tags. A análise inicial é executada automaticamente e **Analisar Nichos** permite recalcular os resultados depois de alterar os filtros. O núcleo aplica normalização, filtros, `log1p`, `StandardScaler`, K-Means e FP-Growth. Os resultados aparecem em DataFrames para clusters, itemsets frequentes, regras de associação e dados analisados, acompanhados por uma visualização Plotly nativa e pesquisa de palavras nos clusters.
137
137
 
138
- O painel permite seleccionar número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, categoria de engagement, intervalo de datas e tags. **Analisar Nichos** aplica normalização, filtros, `log1p`, `StandardScaler`, K-Means e FP-Growth. Os resultados aparecem em DataFrames para clusters, itemsets frequentes, regras de associação e dados filtrados, acompanhados por uma visualização Plotly nativa e pesquisa de palavras nos clusters. O download/cache dos dados é feito de forma lazy e os CSVs não são versionados pelo Git.
139
-
140
- As dependências adicionais são instaladas pelo fluxo normal do pacote: `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.29 install`; o instalador detecta e reutiliza componentes já válidos.
138
+ As dependências adicionais são instaladas pelo fluxo normal do pacote: `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.30 install`; o instalador detecta e reutiliza componentes válidos.
141
139
 
142
140
  ## Armazenamento local
143
141
 
@@ -158,7 +156,7 @@ storage/
158
156
  ├── music/ # músicas locais para o modo Apenas Música
159
157
  ├── voice_previews/ # amostras isoladas do teste de vozes
160
158
  ├── data/
161
- │ └── niches/ # dataset Kaggle cacheado e CSVs enviados ao Niche Finder
159
+ │ └── niches/ # dados automáticos cacheados para o Niche Finder
162
160
  ├── skills/ # skill MoneyPrinterTurbo guardada pelo utilizador
163
161
  ├── metadata_cleaner/
164
162
  │ ├── originals/ # cópias dos vídeos terceiros enviados
package/app/main.py CHANGED
@@ -20,7 +20,7 @@ except (OSError, json.JSONDecodeError):
20
20
  from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, pipeline_summary, set_channel_defaults, transition_task, update_channel
21
21
  from hermes_ui.storage import BLUEPRINTS, ensure_storage, list_blueprint_files, load_blueprint_file, now, read_json, write_json
22
22
  from app.modules.niche_finder.core import NicheAnalysisError, run_niche_analysis
23
- from app.modules.niche_finder.data_loader import DatasetError, download_kaggle_dataset, list_cached_datasets, load_dataframe, save_uploaded_csv
23
+ from app.modules.niche_finder.data_loader import DatasetError, download_kaggle_dataset, load_dataframe
24
24
  from hermes_ui.blueprints import create_blueprint_from_link, list_branding_files, save_generated_blueprint
25
25
  from hermes_ui.metadata_cleaner import build_description, clean_video_metadata, list_edit_records, metadata_manifest, normalize_tags, save_edit_record, store_external_video
26
26
  from hermes_ui.mcp import detect_local_service, install_skill_locally, load_integrations, load_server_config, read_packaged_skill, save_server_config, update_integration
@@ -639,6 +639,12 @@ def _cached_niche_download():
639
639
  return str(download_kaggle_dataset())
640
640
 
641
641
 
642
+ def _load_automatic_niche_dataset():
643
+ dataset_path = _cached_niche_download()
644
+ path = Path(dataset_path)
645
+ return dataset_path, _cached_niche_dataset(dataset_path, path.stat().st_mtime_ns)
646
+
647
+
642
648
  def _niche_tag_options(frame) -> list[str]:
643
649
  tags: set[str] = set()
644
650
  for value in frame.get("video_tags", []):
@@ -659,62 +665,28 @@ def _niche_date_bounds(frame):
659
665
 
660
666
  def render_niche_finder():
661
667
  st.title("Niche Finder")
662
- st.caption("Busca de nichos baseada no projecto open source Niche-Finder, integrada no processo Streamlit do Thunderbolt.")
663
- st.info("A análise é executada localmente, sem Flask ou segundo servidor. O dataset padrão é guardado em `storage/data/niches/` e pode ser substituído por um CSV próprio.")
664
-
665
- with st.sidebar:
666
- st.subheader("Fonte de dados")
667
- uploaded_file = st.file_uploader("Carregar CSV próprio", type=["csv"], key="niche_uploaded_csv")
668
- if st.button("Baixar Dataset Kaggle", use_container_width=True, key="niche_download_dataset"):
669
- try:
670
- downloaded = _cached_niche_download()
671
- st.session_state["niche_dataset_path"] = downloaded
672
- st.success("Dataset Kaggle guardado no storage local.")
673
- st.rerun()
674
- except DatasetError as exc:
675
- st.error(str(exc))
676
- cached = list_cached_datasets()
677
- if cached:
678
- labels = {str(path): path.relative_to(path.parents[2]).as_posix() if len(path.parents) > 2 else path.name for path in cached}
679
- current_path = st.session_state.get("niche_dataset_path", str(cached[0]))
680
- if current_path not in labels:
681
- current_path = str(cached[0])
682
- selected_path = st.selectbox("Dataset local", list(labels), index=list(labels).index(current_path), format_func=lambda item: labels[item], key="niche_dataset_selector")
683
- if selected_path != st.session_state.get("niche_dataset_path"):
684
- st.session_state["niche_dataset_path"] = selected_path
685
- st.session_state.pop("niche_results", None)
686
- st.rerun()
687
- if uploaded_file is not None and st.button("Usar CSV carregado", use_container_width=True, key="niche_use_uploaded"):
688
- try:
689
- uploaded_path = save_uploaded_csv(uploaded_file.getvalue(), uploaded_file.name)
690
- st.session_state["niche_dataset_path"] = str(uploaded_path)
691
- st.session_state.pop("niche_results", None)
692
- st.success(f"CSV guardado em `{uploaded_path}`.")
693
- st.rerun()
694
- except DatasetError as exc:
695
- st.error(str(exc))
668
+ st.caption("Busca automática de padrões, nichos e tags para orientar canais faceless.")
669
+ st.info("O Thunderbolt prepara e analisa automaticamente os dados necessários. Não é necessária configuração manual.")
696
670
 
697
- dataset_path = st.session_state.get("niche_dataset_path")
698
- frame = None
699
- if dataset_path:
700
- path = Path(dataset_path)
701
- if path.exists():
702
- try:
703
- frame = _cached_niche_dataset(str(path), path.stat().st_mtime_ns)
704
- except (DatasetError, OSError) as exc:
705
- st.error(str(exc))
706
- else:
707
- st.warning("O dataset seleccionado já não existe no storage local. Baixe-o novamente ou carregue um CSV.")
708
- if frame is None:
709
- st.subheader("Começar uma análise")
710
- st.write("Baixe o dataset público do Kaggle ou carregue um CSV com as colunas `title`, `publish_date`, `country`, `view_count`, `like_count` e `comment_count`.")
671
+ try:
672
+ with st.spinner("A preparar automaticamente o Niche Finder…"):
673
+ dataset_path, frame = _load_automatic_niche_dataset()
674
+ except (DatasetError, OSError) as exc:
675
+ st.error("Não foi possível preparar automaticamente o Niche Finder neste momento. Verifique a ligação à Internet e tente novamente.")
676
+ with st.expander("Detalhes técnicos"):
677
+ st.caption(str(exc))
711
678
  return
712
679
 
680
+ signature = f"{dataset_path}:{Path(dataset_path).stat().st_mtime_ns}"
681
+ if st.session_state.get("niche_results_signature") != signature:
682
+ st.session_state.pop("niche_results", None)
683
+ st.session_state["niche_results_signature"] = signature
684
+
713
685
  min_date, max_date = _niche_date_bounds(frame)
714
686
  countries = ["Todos"] + sorted(str(value) for value in frame["country"].dropna().unique())
715
687
  tag_options = _niche_tag_options(frame)
716
688
  with st.sidebar:
717
- st.subheader("Parâmetros")
689
+ st.subheader("Parâmetros da busca")
718
690
  n_clusters = st.slider("Número de Clusters", 2, 10, 5, key="niche_n_clusters")
719
691
  min_support = st.slider("Suporte Mínimo", 0.01, 0.5, 0.05, 0.01, format="%.2f", key="niche_min_support")
720
692
  country = st.selectbox("País", countries, key="niche_country")
@@ -724,11 +696,12 @@ def render_niche_finder():
724
696
  selected_tags = st.multiselect("Tags opcionais", tag_options, key="niche_tags")
725
697
  analyse = st.button("Analisar Nichos", type="primary", use_container_width=True, key="niche_analyse")
726
698
 
727
- if analyse:
699
+ results = st.session_state.get("niche_results")
700
+ if results is None or analyse:
728
701
  try:
729
702
  if start_date and end_date and start_date > end_date:
730
703
  raise NicheAnalysisError("A data inicial não pode ser posterior à data final.")
731
- with st.spinner("A analisar clusters e associações…"):
704
+ with st.spinner("A analisar nichos automaticamente…"):
732
705
  results = run_niche_analysis(
733
706
  str(dataset_path),
734
707
  n_clusters=n_clusters,
@@ -741,18 +714,14 @@ def render_niche_finder():
741
714
  )
742
715
  st.session_state["niche_results"] = results
743
716
  except (NicheAnalysisError, DatasetError, OSError) as exc:
744
- st.error(str(exc))
745
-
746
- results = st.session_state.get("niche_results")
747
- if not results:
748
- st.subheader("Dataset carregado")
749
- st.dataframe(frame.head(100), use_container_width=True, hide_index=True)
750
- st.caption(f"{len(frame):,} registos válidos carregados. Ajuste os parâmetros na barra lateral e clique em Analisar Nichos.")
751
- return
717
+ st.error("Não foi possível concluir a análise automática com os filtros actuais.")
718
+ with st.expander("Detalhes técnicos"):
719
+ st.caption(str(exc))
720
+ return
752
721
 
753
722
  summary = results.get("summary", {})
754
723
  metric_cols = st.columns(4)
755
- for col, (label, value) in zip(metric_cols, [("Registos filtrados", summary.get("rows_filtered", 0)), ("Clusters", summary.get("cluster_count", 0)), ("Itemsets frequentes", summary.get("frequent_item_count", 0)), ("Regras", summary.get("association_rule_count", 0))]):
724
+ for col, (label, value) in zip(metric_cols, [("Registos analisados", summary.get("rows_filtered", 0)), ("Clusters", summary.get("cluster_count", 0)), ("Itemsets frequentes", summary.get("frequent_item_count", 0)), ("Regras de associação", summary.get("association_rule_count", 0))]):
756
725
  with col:
757
726
  card(label, value)
758
727
 
@@ -763,7 +732,7 @@ def render_niche_finder():
763
732
  keyword = st.text_input("Filtrar palavras-chave nos clusters", key="niche_cluster_keyword", placeholder="Ex.: música, gaming, receitas")
764
733
  if keyword.strip():
765
734
  cluster_table = cluster_table[cluster_table["palavras"].str.contains(keyword.strip(), case=False, na=False)]
766
- tab_clusters, tab_rules, tab_data = st.tabs(["Clusters encontrados", "Regras de associação", "Dados filtrados"])
735
+ tab_clusters, tab_rules, tab_data = st.tabs(["Clusters encontrados", "Regras de associação", "Dados analisados"])
767
736
  with tab_clusters:
768
737
  st.dataframe(cluster_table, use_container_width=True, hide_index=True)
769
738
  if not points.empty:
@@ -775,10 +744,10 @@ def render_niche_finder():
775
744
  figure.update_layout(legend_title_text="Cluster")
776
745
  st.plotly_chart(figure, use_container_width=True)
777
746
  except ImportError:
778
- st.warning("A visualização Plotly não está instalada. Execute a instalação incremental do Thunderbolt.")
747
+ st.error("A visualização da análise não está disponível nesta instalação.")
779
748
  with tab_rules:
780
749
  if rules_table.empty:
781
- st.info("Não foram encontradas regras com o suporte e lift actuais. Reduza o suporte mínimo ou escolha outro filtro.")
750
+ st.info("Não foram encontradas regras com os filtros actuais. Reduza o suporte mínimo ou escolha outro filtro.")
782
751
  else:
783
752
  st.dataframe(rules_table, use_container_width=True, hide_index=True)
784
753
  if not items_table.empty:
@@ -788,7 +757,6 @@ def render_niche_finder():
788
757
  st.dataframe(results["raw_data"], use_container_width=True, hide_index=True)
789
758
 
790
759
 
791
-
792
760
  def render_videos():
793
761
  st.subheader("Vídeos e backlog")
794
762
  st.caption("Acompanhamento dos vídeos criados, estados da pipeline e controlos de execução.")
@@ -1,14 +1,12 @@
1
1
  """Streamlit-native Niche Finder integration for Thunderbolt."""
2
2
 
3
3
  from .core import NicheAnalysisError, run_niche_analysis
4
- from .data_loader import DATA_DIR, DatasetError, download_kaggle_dataset, list_cached_datasets, save_uploaded_csv
4
+ from .data_loader import DATA_DIR, DatasetError, download_kaggle_dataset
5
5
 
6
6
  __all__ = [
7
7
  "DATA_DIR",
8
8
  "DatasetError",
9
9
  "NicheAnalysisError",
10
10
  "download_kaggle_dataset",
11
- "list_cached_datasets",
12
11
  "run_niche_analysis",
13
- "save_uploaded_csv",
14
12
  ]
@@ -1,10 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import io
4
- import re
5
3
  import shutil
6
4
  from pathlib import Path
7
- from typing import BinaryIO
8
5
 
9
6
  import pandas as pd
10
7
 
@@ -39,24 +36,14 @@ OPTIONAL_COLUMNS = (
39
36
 
40
37
 
41
38
  class DatasetError(RuntimeError):
42
- """Raised when a Niche Finder dataset cannot be read or validated."""
39
+ """Raised when the automatic Niche Finder dataset cannot be read or validated."""
43
40
 
44
41
 
45
42
  def ensure_data_dir() -> Path:
46
43
  DATA_DIR.mkdir(parents=True, exist_ok=True)
47
- (DATA_DIR / "uploads").mkdir(parents=True, exist_ok=True)
48
44
  return DATA_DIR
49
45
 
50
46
 
51
- def _safe_filename(name: str) -> str:
52
- stem = re.sub(r"[^A-Za-z0-9._-]+", "-", Path(name or "dataset.csv").name).strip(".-")
53
- if not stem:
54
- stem = "dataset.csv"
55
- if not stem.lower().endswith(".csv"):
56
- stem += ".csv"
57
- return stem
58
-
59
-
60
47
  def _find_csv(root: Path) -> Path | None:
61
48
  candidates = sorted(root.rglob("*.csv"))
62
49
  if not candidates:
@@ -83,51 +70,35 @@ def _download_with_kagglehub() -> Path | None:
83
70
  except TypeError:
84
71
  downloaded = kagglehub.dataset_download(DEFAULT_DATASET_SLUG)
85
72
  except Exception as exc: # pragma: no cover - depends on network/provider state
86
- raise DatasetError(f"O KaggleHub não conseguiu descarregar o dataset: {exc}") from exc
73
+ raise DatasetError(f"O Thunderbolt não conseguiu preparar os dados automáticos: {exc}") from exc
87
74
  source = _find_csv(Path(downloaded))
88
75
  if source is None:
89
- raise DatasetError("O download KaggleHub terminou sem encontrar um ficheiro CSV.")
76
+ raise DatasetError("A preparação automática terminou sem encontrar dados compatíveis.")
90
77
  return _copy_csv(source, DEFAULT_DATASET_PATH)
91
78
 
92
79
 
93
80
  def download_kaggle_dataset() -> Path:
94
- """Download and cache the public Niche-Finder dataset in Thunderbolt storage."""
81
+ """Prepare and cache the public Niche-Finder dataset without user interaction."""
95
82
  ensure_data_dir()
96
83
  if DEFAULT_DATASET_PATH.exists() and DEFAULT_DATASET_PATH.stat().st_size > 0:
97
84
  return DEFAULT_DATASET_PATH
98
85
  if kagglehub is None:
99
- raise DatasetError("A dependência kagglehub não está instalada. Execute novamente a instalação do Thunderbolt.")
86
+ raise DatasetError("O componente automático de dados não está disponível nesta instalação.")
100
87
  downloaded = _download_with_kagglehub()
101
88
  if downloaded is None:
102
- raise DatasetError("O KaggleHub não devolveu um caminho de dataset válido.")
89
+ raise DatasetError("O componente automático de dados não devolveu um caminho válido.")
103
90
  return downloaded
104
91
 
105
92
 
106
- def save_uploaded_csv(payload: bytes | BinaryIO, filename: str) -> Path:
107
- """Validate and persist an uploaded CSV under the user's local data directory."""
108
- ensure_data_dir()
109
- raw = payload.read() if hasattr(payload, "read") else payload
110
- if not isinstance(raw, (bytes, bytearray)) or not raw:
111
- raise DatasetError("O ficheiro CSV enviado está vazio.")
112
- try:
113
- frame = pd.read_csv(io.BytesIO(raw))
114
- validate_dataset(frame)
115
- except (OSError, UnicodeDecodeError, pd.errors.ParserError, pd.errors.EmptyDataError) as exc:
116
- raise DatasetError(f"Não foi possível ler o CSV enviado: {exc}") from exc
117
- destination = DATA_DIR / "uploads" / _safe_filename(filename)
118
- destination.write_bytes(raw)
119
- return destination
120
-
121
-
122
93
  def validate_dataset(frame: pd.DataFrame) -> pd.DataFrame:
123
- """Validate the minimal schema and return a normalised copy."""
94
+ """Validate the internal dataset schema and return a normalised copy."""
124
95
  if frame is None or frame.empty:
125
- raise DatasetError("O dataset não contém linhas.")
96
+ raise DatasetError("A fonte automática de dados não contém registos.")
126
97
  cleaned = frame.copy()
127
98
  cleaned.columns = [str(column).strip() for column in cleaned.columns]
128
99
  missing = [column for column in REQUIRED_COLUMNS if column not in cleaned.columns]
129
100
  if missing:
130
- raise DatasetError("O CSV precisa das colunas: " + ", ".join(missing) + ".")
101
+ raise DatasetError("A fonte automática não contém as colunas necessárias: " + ", ".join(missing) + ".")
131
102
  if "video_tags" not in cleaned.columns:
132
103
  cleaned["video_tags"] = ""
133
104
  for column in ("view_count", "like_count", "comment_count"):
@@ -138,7 +109,7 @@ def validate_dataset(frame: pd.DataFrame) -> pd.DataFrame:
138
109
  cleaned = cleaned.dropna(subset=["publish_date", "view_count", "like_count", "comment_count"])
139
110
  cleaned = cleaned[cleaned["title"] != ""].copy()
140
111
  if cleaned.empty:
141
- raise DatasetError("O dataset não contém linhas válidas depois da normalização.")
112
+ raise DatasetError("A fonte automática não contém registos válidos depois da normalização.")
142
113
  return cleaned.reset_index(drop=True)
143
114
 
144
115
 
@@ -146,10 +117,5 @@ def load_dataframe(path: str | Path) -> pd.DataFrame:
146
117
  try:
147
118
  frame = pd.read_csv(path)
148
119
  except (OSError, UnicodeDecodeError, pd.errors.ParserError, pd.errors.EmptyDataError) as exc:
149
- raise DatasetError(f"Não foi possível ler o dataset: {exc}") from exc
120
+ raise DatasetError(f"Não foi possível ler a fonte automática de dados: {exc}") from exc
150
121
  return validate_dataset(frame)
151
-
152
-
153
- def list_cached_datasets() -> list[Path]:
154
- ensure_data_dir()
155
- return sorted(DATA_DIR.glob("**/*.csv"))
@@ -219,7 +219,7 @@ def seed_blueprints() -> None:
219
219
 
220
220
 
221
221
  def ensure_storage() -> None:
222
- for path in [STATE, BLUEPRINTS / "canais", BLUEPRINTS / "nichos", BLUEPRINTS / "importados", BLUEPRINTS / "brandings", STORAGE / "brand", STORAGE / "scripts", STORAGE / "thumbnails", STORAGE / "videos", STORAGE / "artifacts", STORAGE / "skills", STORAGE / "metadata_cleaner", STORAGE / "metadata_cleaner" / "outputs", STORAGE / "music", STORAGE / "voice_previews", NICHES_DATA, NICHES_DATA / "uploads"]:
222
+ for path in [STATE, BLUEPRINTS / "canais", BLUEPRINTS / "nichos", BLUEPRINTS / "importados", BLUEPRINTS / "brandings", STORAGE / "brand", STORAGE / "scripts", STORAGE / "thumbnails", STORAGE / "videos", STORAGE / "artifacts", STORAGE / "skills", STORAGE / "metadata_cleaner", STORAGE / "metadata_cleaner" / "outputs", STORAGE / "music", STORAGE / "voice_previews", NICHES_DATA]:
223
223
  path.mkdir(parents=True, exist_ok=True)
224
224
  seed_blueprints()
225
225
  for filename, default in DEFAULTS.items():
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "main": "scripts/cli.mjs",
6
6
  "type": "module",
package/scripts/cli.mjs CHANGED
@@ -81,7 +81,6 @@ function ensureRuntimeStorage() {
81
81
  join(storageRoot, "voice_previews"),
82
82
  join(storageRoot, "data"),
83
83
  join(storageRoot, "data", "niches"),
84
- join(storageRoot, "data", "niches", "uploads"),
85
84
  ];
86
85
  for (const directory of directories) mkdirSync(directory, { recursive: true });
87
86
  const seedRoot = resolve(root, "seed", "blueprints");
@@ -112,7 +112,6 @@ function ensureDirs() {
112
112
  join(storageRoot, "voice_previews"),
113
113
  join(storageRoot, "data"),
114
114
  join(storageRoot, "data", "niches"),
115
- join(storageRoot, "data", "niches", "uploads"),
116
115
  ];
117
116
  for (const directory of directories) mkdirSync(directory, { recursive: true });
118
117
  copySeedBlueprints(storageRoot);