@danhachuel/leadmaster 0.1.0 → 0.1.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,34 @@ O formato é baseado no [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.
6
6
  A etiqueta visual de versão usa patch de dois dígitos (ex.: `0.1.00`); o registry NPM
7
7
  normaliza para a forma canônica (ex.: `0.1.0`).
8
8
 
9
+ ## [0.1.01] — 2026-10-23
10
+
11
+ ### Removido
12
+
13
+ - **Pipeline de vídeos faceless** — o LeadMaster agora é 100% focado em prospecção
14
+ (CRM, extratores de leads e agentes de IA). Removidos:
15
+ - Aba lateral "Pipeline de Vídeos", página e rota `/pipeline`.
16
+ - Integrações com MoneyPrinterTurbo, Short-Video-Maker, OpenMontage e upload para
17
+ YouTube: serviços backend, endpoints `/api/pipeline`, cards e testes em
18
+ Ferramentas, menções no Painel, tutorial do header, FAQ do Suporte e
19
+ diagnóstico.
20
+ - Motor do pipeline, agendador de publicação e contadores diários de upload.
21
+ - `pipeline.json` do armazenamento local — removido automaticamente das
22
+ instalações existentes, junto com as chaves de vídeo do `config.json`
23
+ (`integracoes.moneyprinter`, `short_video_maker`, `openmontage`,
24
+ `youtube_upload`, `uploads`) e o evento de webhook `video.publicado`.
25
+
26
+ ### Adicionado
27
+
28
+ - Migração automática de configuração: chaves de vídeo de versões antigas são
29
+ limpas na primeira execução da nova versão.
30
+ - Card "Status da Conexão" do Painel passa a mostrar LLM e OpenOutreach.
31
+
32
+ ### Alterado
33
+
34
+ - Backend de desenvolvimento (uvicorn) agora usa a porta **8010** — a 8000 costuma
35
+ ser usada por outros serviços locais (ex.: PandaNews), o que causava conflito.
36
+
9
37
  ## [0.1.00] — 2026-10-23
10
38
 
11
39
  ### Adicionado
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # LeadMaster
2
2
 
3
- **SaaS local de prospecção e automação de conteúdo.** CRM, extratores de leads, agentes de
4
- IA e um pipeline completo de geração de vídeos faceless (YouTube e Shorts) — instalável via
3
+ **SaaS local de prospecção.** CRM, extratores de leads e agentes de IA — instalável via
5
4
  `npx`, 100% local, sem nuvem e sem banco de dados externo.
6
5
 
7
6
  ![Stack](https://img.shields.io/badge/React%2018-Vite%20%2B%20TS-61dafb) ![Backend](https://img.shields.io/badge/FastAPI-Python%203.11%2B-009688) ![Storage](https://img.shields.io/badge/storage-JSON%20local-orange)
@@ -32,19 +31,19 @@ npx --yes @danhachuel/leadmaster@0.1.0
32
31
  `doctor` (diagnóstico), `help`.
33
32
 
34
33
  A pasta de dados padrão é `Documents\REPOSITORIO\LeadMaster` (sobrescreva com
35
- `--home` ou `LEADMASTER_HOME`). Todos os dados — leads, campanhas, pipeline e
34
+ `--home` ou `LEADMASTER_HOME`). Todos os dados — leads, campanhas e
36
35
  configuração — ficam em `<home>\storage` como arquivos JSON.
37
36
 
38
37
  ### Modo desenvolvimento (a partir do repositório)
39
38
 
40
39
  ```bash
41
40
  npm install # dependências da raiz (concurrently)
42
- npm run dev # sobe backend (uvicorn :8000, reload) + frontend (Vite :5173)
41
+ npm run dev # sobe backend (uvicorn :8010, reload) + frontend (Vite :5055)
43
42
  ```
44
43
 
45
44
  O Vite dev server sobe a UI de desenvolvimento em `http://localhost:5055` (com
46
45
  auto-incremento para 5056, 5057... se a porta estiver ocupada) e faz proxy de `/api`
47
- para `http://127.0.0.1:8000` (backend de desenvolvimento).
46
+ para `http://127.0.0.1:8010` (backend de desenvolvimento).
48
47
 
49
48
  ---
50
49
 
@@ -56,7 +55,6 @@ para `http://127.0.0.1:8000` (backend de desenvolvimento).
56
55
  | **Leads** | CRUD completo, importação em massa a partir dos extratores, barra de progresso de preenchimento, enriquecimento por IA. |
57
56
  | **CRM Kanban** | Drag-and-drop (@dnd-kit) nas colunas *Leads → Em contato → Proposta → Fechado*; o agente de IA sugere movimentações e enriquece os cards. |
58
57
  | **Extratores** | **Google Maps** (palavra-chave + localização, via `gosom/google-maps-scraper`), **Instagram** (URL do perfil, via `lucasoal/igmapper`) e **CNPJ** (BrasilAPI + busca por CNAE/UF + `D4Vinci/Scrapling`). Toda extração gera pré-visualização antes de importar. |
59
- | **Pipeline de vídeos** | Fluxo de 5 etapas com Iniciar/Pausar/Parar/Repetir: **Roteiro** (7–9k caracteres via LLM) → **Thumbnail/Título** → **Produção Wide 16:9** (MoneyPrinterTurbo/OpenMontage) → **Produção Shorts 9:16** (Short-Video-Maker) → **Upload** (não listado → aguarda 1h → público; até 50/dia, com fallback `youtube-uploader-mcp`). |
60
58
  | **Disparos** | Campanhas de disparo (texto/áudio) com templates configuráveis e execução via `eracle/OpenOutreach`. |
61
59
  | **Agentes** | Agente de disparo (prompts/templates) e agente de atendimento (texto via LLM ou voz via `pipecat-ai/pipecat`). |
62
60
  | **Ferramentas** | Chaves de API, URLs das integrações locais, testes de conexão — tudo salvo em `config.json` local. |
@@ -69,12 +67,7 @@ para `http://127.0.0.1:8000` (backend de desenvolvimento).
69
67
  npx @danhachuel/leadmaster
70
68
  └─ scripts/cli.mjs # instala deps Python, sobe uvicorn, abre navegador
71
69
  └─ backend/main.py # FastAPI: API + serving da UI construída (frontend/dist)
72
- ├─ services/storage.py # JSON local (leads, campanhas, pipeline, config)
73
- ├─ services/pipeline.py # motor das 5 etapas (threads + checkpoints)
74
- ├─ services/moneyprinter.py # POST localhost:8080/api/v1/videos
75
- ├─ services/short_video_maker.py # POST localhost:3123/api/short-video
76
- ├─ services/openmontage.py # CLI local
77
- ├─ services/youtube_upload.py # upload + agendamento não listado → público
70
+ ├─ services/storage.py # JSON local (leads, campanhas, config)
78
71
  ├─ services/extractors.py # Maps / Instagram / CNPJ
79
72
  ├─ services/crm_agent.py # sugestões + enriquecimento
80
73
  ├─ services/agents.py # disparo (OpenOutreach) + atendimento
@@ -82,17 +75,14 @@ npx @danhachuel/leadmaster
82
75
  ```
83
76
 
84
77
  - **Portas**: LeadMaster UI/API `:5055` (auto-incrementa: 5056, 5057, ... se ocupada) ·
85
- backend de desenvolvimento `:8000` · MoneyPrinterTurbo `:8080` · Short-Video-Maker `:3123`.
86
- - **Degradación graciosa**: se um serviço externo não estiver rodando, a etapa falha com
87
- mensagem acionável e um botão **Repetir** — nada de falsos sucessos.
78
+ backend de desenvolvimento `:8000`.
79
+ - **Degradación graciosa**: se um serviço externo não estiver configurado/rodando, a ação
80
+ falha com mensagem acionável — nada de falsos sucessos.
88
81
 
89
82
  ### Serviços externos (opcionais, todos locais)
90
83
 
91
84
  | Serviço | Como usar no LeadMaster |
92
85
  |---|---|
93
- | [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo) | Suba em `http://localhost:8080` e configure em *Ferramentas*. |
94
- | [Short-Video-Maker](https://github.com/lucasoal/short-video-maker) | Suba em `http://localhost:3123`. |
95
- | [OpenMontage](https://github.com/openmotional/OpenMontage) | Informe o caminho do CLI em *Ferramentas*. |
96
86
  | [gosom/google-maps-scraper](https://github.com/gosom/google-maps-scraper) | Informe o caminho do binário em *Ferramentas → Extratores*. |
97
87
  | [lucasoal/igmapper](https://github.com/lucasoal/igmapper) | Informe o caminho em *Ferramentas → Extratores*. |
98
88
  | BrasilAPI / OpenCNPJ / Scrapling | Consulta por CNPJ e busca por CNAE/UF (configuráveis). |
@@ -106,11 +96,11 @@ npx @danhachuel/leadmaster
106
96
  A interface replica o protótipo `LeadMaster v1.0` (sidebar escura `#1e1b4b`, cards
107
97
  brancos, gradientes indigo/purple) e aplica as **7 Leis de Usabilidade**:
108
98
 
109
- 1. **Fitts** — CTAs grandes ("Iniciar Novo Vídeo", "Salvar").
99
+ 1. **Fitts** — CTAs grandes ("Nova Campanha", "Salvar").
110
100
  2. **Hick** — configurações avançadas em accordions.
111
- 3. **Zeigarnik** — barras de progresso (pipeline e preenchimento de leads).
101
+ 3. **Zeigarnik** — barras de progresso (preenchimento de leads e campanhas).
112
102
  4. **Jakob** — consistência com o modelo mental do protótipo (menu lateral fixo).
113
- 5. **Gradiente de Meta** — "Faltam 2 etapas para o upload 🚀".
103
+ 5. **Gradiente de Meta** — "Faltam 2 campos para o lead ficar completo".
114
104
  6. **von Restorff** — botão principal com gradiente sobre fundo neutro.
115
105
  7. **Miller** — métricas agrupadas em cards (chunking).
116
106
 
package/backend/main.py CHANGED
@@ -18,7 +18,7 @@ from fastapi.responses import FileResponse
18
18
  from fastapi.staticfiles import StaticFiles
19
19
  from pydantic import BaseModel, Field
20
20
 
21
- from services import agents, crm_agent, extractors, llm, moneyprinter, pipeline, short_video_maker, storage, youtube_upload
21
+ from services import agents, crm_agent, extractors, llm, storage
22
22
 
23
23
  REPO_ROOT = Path(__file__).resolve().parent.parent
24
24
  FRONTEND_DIST = REPO_ROOT / "frontend" / "dist"
@@ -35,14 +35,11 @@ def _versao() -> str:
35
35
 
36
36
 
37
37
  VERSION = _versao()
38
- engine = pipeline.PipelineEngine()
39
38
 
40
39
 
41
40
  @asynccontextmanager
42
41
  async def lifespan(_app: FastAPI):
43
42
  storage.ensure_seeds()
44
- engine.recuperar()
45
- engine.iniciar_agendador()
46
43
  yield
47
44
 
48
45
 
@@ -89,19 +86,6 @@ class CampaignIn(BaseModel):
89
86
  lead_ids: list[str] = []
90
87
 
91
88
 
92
- class JobIn(BaseModel):
93
- tema: str = Field(min_length=1)
94
- titulo: str = ""
95
- tom: str = "informativo"
96
- duracao_min: int = 8
97
- keyword_seo: str = ""
98
- formatos: list[str] = ["wide", "shorts"]
99
-
100
-
101
- class AcaoIn(BaseModel):
102
- acao: str
103
-
104
-
105
89
  class GoogleMapsIn(BaseModel):
106
90
  palavra_chave: str = Field(min_length=2)
107
91
  localizacao: str = Field(min_length=2)
@@ -156,12 +140,9 @@ def health():
156
140
  @app.get("/api/doctor")
157
141
  def doctor():
158
142
  integracoes = {
159
- "moneyprinter": moneyprinter.verificar(),
160
- "short_video_maker": short_video_maker.verificar(),
161
- "uploader_youtube": youtube_upload.verificar(),
162
143
  "llm_configurado": llm.configurado(),
163
- "openmontage": bool(storage.get_config()["integracoes"]["openmontage"].get("cli_path")),
164
144
  "openoutreach": agents.disparo_configurado(),
145
+ "pipecat": bool(storage.get_config()["integracoes"]["pipecat"].get("url")),
165
146
  }
166
147
  return {
167
148
  "ok": True,
@@ -170,7 +151,6 @@ def doctor():
170
151
  "sistema": platform.platform(),
171
152
  "dist_frontend": FRONTEND_DIST.exists(),
172
153
  "storage": str(storage.storage_dir()),
173
- "uploads_hoje": youtube_upload.contagem_hoje(),
174
154
  "integracoes": integracoes,
175
155
  }
176
156
 
@@ -192,14 +172,10 @@ def metrics():
192
172
  enviadas = sum(int(c.get("enviadas", 0)) for c in campaigns)
193
173
  erros = sum(int(c.get("erros", 0)) for c in campaigns)
194
174
  total_envios = enviadas + erros
195
- jobs = storage.list_jobs()
196
175
 
197
176
  return {
198
177
  "conexao": {
199
178
  "geral": True, # o backend local responde => conectado
200
- "moneyprinter": moneyprinter.verificar(),
201
- "short_video_maker": short_video_maker.verificar(),
202
- "uploader_youtube": youtube_upload.verificar(),
203
179
  "llm": llm.configurado(),
204
180
  "openoutreach": agents.disparo_configurado(),
205
181
  },
@@ -213,10 +189,6 @@ def metrics():
213
189
  for etapa in ("leads", "em_contato", "proposta", "fechado")
214
190
  },
215
191
  "campanhas": {"enviadas": enviadas, "erros": erros, "total": len(campaigns)},
216
- "pipeline": {
217
- "ativos": sum(1 for j in jobs if j.get("status") in ("em_andamento", "pausado")),
218
- "concluidos": sum(1 for j in jobs if j.get("status") == "concluido"),
219
- },
220
192
  "perfil": storage.get_config().get("perfil", {}),
221
193
  }
222
194
 
@@ -345,34 +317,6 @@ def remover_campanha(campaign_id: str):
345
317
  return {"ok": True}
346
318
 
347
319
 
348
- # ---------------------------------------------------------------------------
349
- # Pipeline de vídeos
350
- # ---------------------------------------------------------------------------
351
-
352
- @app.get("/api/pipeline")
353
- def listar_pipeline():
354
- return engine.listar()
355
-
356
-
357
- @app.post("/api/pipeline", status_code=201)
358
- def criar_job(job_in: JobIn):
359
- try:
360
- return engine.criar_job(job_in.model_dump())
361
- except ValueError as e:
362
- raise HTTPException(400, str(e))
363
-
364
-
365
- @app.post("/api/pipeline/{job_id}/actions")
366
- def agir_pipeline(job_id: str, acao_in: AcaoIn):
367
- try:
368
- resultado = engine.agir(job_id, acao_in.acao)
369
- except ValueError as e:
370
- raise HTTPException(400, str(e))
371
- if not resultado.get("ok"):
372
- raise HTTPException(400, resultado.get("mensagem", "Ação não permitida."))
373
- return resultado
374
-
375
-
376
320
  # ---------------------------------------------------------------------------
377
321
  # Extratores
378
322
  # ---------------------------------------------------------------------------
@@ -447,11 +391,7 @@ def testar_integracao(dados: IntegracaoTestIn):
447
391
  @app.get("/api/integrations/status")
448
392
  def status_integracoes():
449
393
  return {
450
- "moneyprinter": moneyprinter.verificar(),
451
- "short_video_maker": short_video_maker.verificar(),
452
- "uploader_youtube": youtube_upload.verificar(),
453
394
  "llm": llm.configurado(),
454
- "openmontage": bool(storage.get_config()["integracoes"]["openmontage"].get("cli_path")),
455
395
  "openoutreach": agents.disparo_configurado(),
456
396
  "pipecat": bool(storage.get_config()["integracoes"]["pipecat"].get("url")),
457
397
  }
@@ -517,6 +457,9 @@ if FRONTEND_DIST.exists():
517
457
 
518
458
  @app.get("/{full_path:path}", include_in_schema=False)
519
459
  def spa(full_path: str):
460
+ # Rotas de API inexistentes devolvem 404 JSON (não a página do SPA).
461
+ if full_path.startswith("api/") or full_path == "api":
462
+ raise HTTPException(404, f"Rota /{full_path} não existe.")
520
463
  alvo = FRONTEND_DIST / full_path
521
464
  if full_path and alvo.is_file():
522
465
  return FileResponse(alvo)
@@ -1,9 +1,9 @@
1
- """Cliente LLM (API OpenAI-compatível) com fallback local para roteiros.
1
+ """Cliente LLM (API OpenAI-compatível) para os agentes do LeadMaster.
2
2
 
3
3
  A configuração fica em `config.json → llm` (provedor, base_url, api_key, modelo).
4
- Se o LLM não estiver configurado, os roteiros são gerados por um modelo local
5
- (template estruturado) e a origem é sempre informada ao usuário — nunca fingimos
6
- uma geração por IA.
4
+ Usado pelo agente de CRM (enriquecimento/sugestões) e pelo agente de atendimento.
5
+ Se o LLM não estiver configurado, as heurísticas locais assumem e a mensagem
6
+ sempre informa o que fazer — nunca fingimos uma resposta de IA.
7
7
  """
8
8
 
9
9
  import httpx
@@ -28,7 +28,7 @@ def configurado() -> bool:
28
28
  return bool(c.get("api_key") and c.get("base_url"))
29
29
 
30
30
 
31
- def chat(mensagens: list, temperature: float = 0.7, max_tokens: int = 4000) -> str:
31
+ def chat(mensagens: list, temperature: float = 0.7, max_tokens: int = 2000) -> str:
32
32
  c = _conf()
33
33
  if not configurado():
34
34
  raise LLMNaoConfigurado(
@@ -67,123 +67,3 @@ def testar_conexao() -> dict:
67
67
  return {"ok": False, "mensagem": str(e)}
68
68
  except LLMErro as e:
69
69
  return {"ok": False, "mensagem": str(e)}
70
-
71
-
72
- # ---------------------------------------------------------------------------
73
- # Roteiro (7k–9k caracteres)
74
- # ---------------------------------------------------------------------------
75
-
76
- def gerar_roteiro(tema: str, titulo: str = "", tom: str = "informativo", duracao_min: int = 8) -> dict:
77
- """Gera o roteiro do vídeo faceless. Retorna {roteiro, origem, aviso}."""
78
- if configurado():
79
- sistema = (
80
- "Você é um roteirista profissional de vídeos faceless para YouTube, "
81
- "especialista em retenção. Escreva em português do Brasil."
82
- )
83
- usuario = (
84
- f"Escreva um roteiro completo para um vídeo faceless de YouTube sobre: {tema}.\n"
85
- f"Título de trabalho: {titulo or '(crie um título)'}\n"
86
- f"Tom: {tom}. Duração estimada: {duracao_min} minutos.\n"
87
- "REQUISITOS:\n"
88
- "- Entre 7000 e 9000 caracteres de texto corrido (sem marcadores de cena).\n"
89
- "- Comece com um gancho de 2 frases que prende na primeira.\n"
90
- "- Use seções lógicas, exemplos concretos e números.\n"
91
- "- Termine com CTA para inscrever-se no canal.\n"
92
- "- Apenas o texto do roteiro, sem títulos em markdown."
93
- )
94
- try:
95
- texto = chat(
96
- [{"role": "system", "content": sistema}, {"role": "user", "content": usuario}],
97
- temperature=0.8,
98
- max_tokens=6000,
99
- )
100
- return {"roteiro": texto.strip(), "origem": "llm", "aviso": ""}
101
- except LLMErro as e:
102
- raise # configurado mas falhou: etapa deve falhar com mensagem acionável
103
- return {
104
- "roteiro": _roteiro_local(tema, titulo, tom, duracao_min),
105
- "origem": "local",
106
- "aviso": "LLM não configurado — roteiro gerado por modelo local (rascunho). Configure o LLM em Ferramentas para roteiros completos de 7k–9k caracteres.",
107
- }
108
-
109
-
110
- def _roteiro_local(tema: str, titulo: str, tom: str, duracao_min: int) -> str:
111
- """Rascunho estruturado, honesto sobre sua origem local."""
112
- titulo = titulo or f"{tema}: o guia definitivo"
113
- blocos = max(3, duracao_min)
114
- linhas = [
115
- f"[GANCHO] {titulo}. Nos próximos {duracao_min} minutos você vai entender exatamente "
116
- f"como {tema.lower()} funciona na prática — e por que quase todo mundo erra nisso. Fica até o final.",
117
- "",
118
- "[INTRODUÇÃO]",
119
- f"Antes de qualquer coisa: o que é {tema.lower()}? É a diferença entre agir no escuro "
120
- "e agir com método. Neste vídeo eu vou te mostrar o passo a passo completo.",
121
- "",
122
- ]
123
- for i in range(1, blocos + 1):
124
- linhas += [
125
- f"[PARTE {i} — PONTO-CHAVE {i}]",
126
- f"Primeiro, o contexto: a maior parte das pessoas que tenta {tema.lower()} falha porque "
127
- "pula a etapa de preparação. Não faça isso. Comece pelo diagnóstico simples: onde você "
128
- "está hoje e qual é o resultado mínimo que já mudaria seu jogo.",
129
- f"Segundo, a execução: escolha UMA estratégia de {tema.lower()} e execute-a por 14 dias "
130
- "sem mudar de rota. Registre os números. O que não é medido não melhora.",
131
- f"Terceiro, o erro comum da parte {i}: fazer mais do mesmo esperando resultado diferente. "
132
- "Ajuste uma variável por vez e observe.",
133
- "",
134
- ]
135
- linhas += [
136
- "[ENCERRAMENTO E CTA]",
137
- "Se esse vídeo te ajudou, se inscreve no canal e ativa o sininho — toda semana tem "
138
- f"conteúdo novo sobre {tema.lower()}. Comenta aqui qual etapa você vai aplicar primeiro. Até o próximo!",
139
- "",
140
- "(Rascunho gerado localmente pelo LeadMaster, sem LLM. Configure seu LLM em "
141
- "Ferramentas para gerar roteiros completos de 7.000 a 9.000 caracteres.)",
142
- ]
143
- return "\n".join(linhas)
144
-
145
-
146
- def gerar_titulo_e_thumbnail(tema: str, roteiro: str) -> dict:
147
- """Gera título otimizado + prompt de thumbnail. Retorna {titulo, titulo_alternativo, thumbnail_prompt, origem}."""
148
- if configurado():
149
- try:
150
- resposta = chat(
151
- [
152
- {
153
- "role": "system",
154
- "content": (
155
- "Você é especialista em SEO de YouTube. Responda APENAS com JSON válido, "
156
- 'sem markdown, no formato: {"titulo": "...", "titulo_alternativo": "...", '
157
- '"thumbnail_prompt": "..."}. Títulos com até 60 caracteres, thumbnail_prompt '
158
- "em português, descrevendo imagem sem texto legível."
159
- ),
160
- },
161
- {
162
- "role": "user",
163
- "content": f"Tema do vídeo: {tema}\n\nRoteiro (início):\n{roteiro[:1500]}",
164
- },
165
- ],
166
- temperature=0.6,
167
- max_tokens=500,
168
- )
169
- import json as _json
170
-
171
- inicio = resposta.find("{")
172
- dados = _json.loads(resposta[inicio:] if inicio >= 0 else resposta)
173
- return {
174
- "titulo": dados.get("titulo", ""),
175
- "titulo_alternativo": dados.get("titulo_alternativo", ""),
176
- "thumbnail_prompt": dados.get("thumbnail_prompt", ""),
177
- "origem": "llm",
178
- }
179
- except (LLMErro, ValueError, KeyError):
180
- pass # cai no fallback local abaixo
181
- return {
182
- "titulo": f"{tema}: o guia definitivo (passo a passo)",
183
- "titulo_alternativo": f"Como dominar {tema} em pouco tempo",
184
- "thumbnail_prompt": (
185
- f"Imagem 16:9 vibrante sobre {tema}, estilo minimalista de alto contraste, "
186
- "pessoa em silhueta olhando para resultado crescente, cores indigo e roxo, sem texto."
187
- ),
188
- "origem": "local",
189
- }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Toda a escrita é atômica (arquivo temporário + replace) e protegida por um lock
4
4
  global, para permitir o uso concorrente pelas rotas da API e pelos workers em
5
- background (pipeline, campanhas, agendador de publicação).
5
+ background (campanhas de disparo).
6
6
  """
7
7
 
8
8
  import json
@@ -46,18 +46,6 @@ def storage_dir() -> Path:
46
46
  return d
47
47
 
48
48
 
49
- def roteiros_dir() -> Path:
50
- d = get_home() / "roteiros"
51
- d.mkdir(parents=True, exist_ok=True)
52
- return d
53
-
54
-
55
- def videos_dir() -> Path:
56
- d = get_home() / "videos"
57
- d.mkdir(parents=True, exist_ok=True)
58
- return d
59
-
60
-
61
49
  def now_iso() -> str:
62
50
  return datetime.now().astimezone().isoformat(timespec="seconds")
63
51
 
@@ -116,20 +104,7 @@ DEFAULT_CONFIG = {
116
104
  "modelo": "gpt-4o-mini",
117
105
  },
118
106
  "integracoes": {
119
- "moneyprinter": {"url": "http://localhost:8080"},
120
- "short_video_maker": {"url": "http://localhost:3123"},
121
- "openmontage": {"cli_path": ""},
122
- "youtube_upload": {
123
- "url": "",
124
- "api_key": "",
125
- "upload_path": "/api/upload",
126
- "publicar_path": "/api/videos/{video_id}/publicar",
127
- "modo": "nao_listado_1h_publico",
128
- "limite_diario": 50,
129
- "espera_publicacao_horas": 1,
130
- "uploader_mcp_path": "",
131
- },
132
- "openoutreach": {"url": "", "send_path": "/api/send"},
107
+ "openoutreach": {"url": "", "enabled": False, "send_path": "/api/send"},
133
108
  "pipecat": {"url": ""},
134
109
  },
135
110
  "extratores": {
@@ -147,7 +122,7 @@ DEFAULT_CONFIG = {
147
122
  "templates": {
148
123
  "texto": (
149
124
  "Olá {{primeiro_nome}}! Sou da {{minha_empresa}} e ajudo empresas como "
150
- "{{empresa}} a crescerem com conteúdo automatizado no YouTube. "
125
+ "{{empresa}} a crescerem com prospecção automatizada. "
151
126
  "Posso te enviar uma proposta rápida?"
152
127
  ),
153
128
  "audio": "Olá {{primeiro_nome}}, tudo bem? Falo em nome da {{minha_empresa}}...",
@@ -159,11 +134,20 @@ DEFAULT_CONFIG = {
159
134
  },
160
135
  "webhook": {
161
136
  "url": "",
162
- "eventos": ["lead.criado", "lead.etapa", "campanha.concluida", "video.publicado"],
137
+ "eventos": ["lead.criado", "lead.etapa", "campanha.concluida"],
163
138
  },
164
- "uploads": {"data": "", "contador": 0},
165
139
  }
166
140
 
141
+ # Chaves de versões antigas (o pipeline de vídeos foi retirado do produto) —
142
+ # são removidas automaticamente da configuração local na primeira leitura.
143
+ _CAMINHOS_REMOVIDOS = [
144
+ ("integracoes", "moneyprinter"),
145
+ ("integracoes", "short_video_maker"),
146
+ ("integracoes", "openmontage"),
147
+ ("integracoes", "youtube_upload"),
148
+ ("uploads",),
149
+ ]
150
+
167
151
 
168
152
  def _deep_merge(base: dict, override: dict) -> dict:
169
153
  out = dict(base)
@@ -188,8 +172,41 @@ def _deep_merge_preserve_secrets(base: dict, override: dict) -> dict:
188
172
  return out
189
173
 
190
174
 
175
+ def _limpar_deprecated(config: dict) -> dict:
176
+ """Migração: remove chaves de versões antigas (pipeline de vídeos)."""
177
+ mudou = False
178
+ for caminho in _CAMINHOS_REMOVIDOS:
179
+ secao = config
180
+ for chave in caminho[:-1]:
181
+ secao = secao.get(chave) if isinstance(secao, dict) else None
182
+ if not isinstance(secao, dict):
183
+ break
184
+ else:
185
+ ultimo = caminho[-1]
186
+ if isinstance(secao, dict) and ultimo in secao:
187
+ secao.pop(ultimo)
188
+ mudou = True
189
+ # Evento de webhook do recurso removido.
190
+ webhook = config.get("webhook")
191
+ if isinstance(webhook, dict) and isinstance(webhook.get("eventos"), list):
192
+ eventos = [e for e in webhook["eventos"] if e != "video.publicado"]
193
+ if eventos != webhook["eventos"]:
194
+ webhook["eventos"] = eventos
195
+ mudou = True
196
+ # Dados órfãos do pipeline de vídeos: o arquivo só existia para esse recurso.
197
+ pipeline_path = storage_dir() / PIPELINE_FILE
198
+ if pipeline_path.exists():
199
+ try:
200
+ pipeline_path.unlink()
201
+ except OSError:
202
+ pass
203
+ if mudou:
204
+ _write(CONFIG_FILE, config)
205
+ return config
206
+
207
+
191
208
  def get_config() -> dict:
192
- return _deep_merge(DEFAULT_CONFIG, _read(CONFIG_FILE, {}))
209
+ return _limpar_deprecated(_deep_merge(DEFAULT_CONFIG, _read(CONFIG_FILE, {})))
193
210
 
194
211
 
195
212
  def update_config(patch: dict) -> dict:
@@ -332,41 +349,6 @@ def delete_campaign(campaign_id: str) -> bool:
332
349
  return True
333
350
 
334
351
 
335
- # ---------------------------------------------------------------------------
336
- # Pipeline
337
- # ---------------------------------------------------------------------------
338
-
339
- def list_jobs() -> list:
340
- data = _read(PIPELINE_FILE, {"jobs": []})
341
- if isinstance(data, list): # tolerância a formato antigo
342
- data = {"jobs": data}
343
- return data.get("jobs", [])
344
-
345
-
346
- def save_jobs(jobs: list) -> None:
347
- _write(PIPELINE_FILE, {"jobs": jobs})
348
-
349
-
350
- def get_job(job_id: str) -> dict | None:
351
- for j in list_jobs():
352
- if j.get("id") == job_id:
353
- return j
354
- return None
355
-
356
-
357
- def upsert_job(job: dict) -> dict:
358
- with LOCK:
359
- jobs = list_jobs()
360
- for i, j in enumerate(jobs):
361
- if j.get("id") == job["id"]:
362
- jobs[i] = job
363
- break
364
- else:
365
- jobs.append(job)
366
- save_jobs(jobs)
367
- return job
368
-
369
-
370
352
  # ---------------------------------------------------------------------------
371
353
  # Seeds (primeira execução)
372
354
  # ---------------------------------------------------------------------------
@@ -446,8 +428,6 @@ def ensure_seeds() -> None:
446
428
  _write(LEADS_FILE, _seed_leads())
447
429
  if not (d / CAMPAIGNS_FILE).exists():
448
430
  _write(CAMPAIGNS_FILE, _seed_campaigns())
449
- if not (d / PIPELINE_FILE).exists():
450
- _write(PIPELINE_FILE, {"jobs": []})
451
431
  if not (d / CONFIG_FILE).exists():
452
432
  _write(CONFIG_FILE, DEFAULT_CONFIG)
453
433