@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.
- package/MANUAL-INSTALACAO.md +7 -7
- package/README.md +9 -11
- package/app/main.py +33 -65
- package/app/modules/niche_finder/__init__.py +1 -3
- package/app/modules/niche_finder/data_loader.py +11 -45
- package/hermes_ui/storage.py +1 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +0 -1
- package/scripts/install.mjs +0 -1
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 |
|
|
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
|
+
## 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.
|
|
113
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
114
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 já 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/ #
|
|
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,
|
|
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
|
|
663
|
-
st.info("
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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
|
-
"""
|
|
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("
|
|
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
|
|
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
|
|
94
|
+
"""Validate the internal dataset schema and return a normalised copy."""
|
|
124
95
|
if frame is None or frame.empty:
|
|
125
|
-
raise DatasetError("
|
|
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("
|
|
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("
|
|
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
|
|
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"))
|
package/hermes_ui/storage.py
CHANGED
|
@@ -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
|
|
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
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");
|
package/scripts/install.mjs
CHANGED
|
@@ -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);
|