@danhachuel/leadmaster 0.1.0 → 0.1.2

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/scripts/cli.mjs CHANGED
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Comandos:
7
7
  * install Prepara a pasta de dados (home) e instala as dependências Python.
8
- * start Sobe o backend FastAPI (serving da UI construída) e abre o navegador.
8
+ * start Sobe o backend FastAPI (serving da UI construída) e imprime a URL
9
+ * no terminal — NUNCA abre o navegador; quem abre é o usuário.
9
10
  * `npx @danhachuel/leadmaster` sem argumentos executa este comando.
10
11
  * doctor Diagnóstico rápido: Node, Python, dependências, pasta de dados.
11
12
  * help Ajuda.
@@ -240,16 +241,6 @@ async function waitForHealth(port, timeoutMs) {
240
241
 
241
242
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
242
243
 
243
- function openBrowser(url) {
244
- try {
245
- if (platform() === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
246
- else if (platform() === 'darwin') spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
247
- else spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
248
- } catch {
249
- console.log(`Abra manualmente: ${url}`);
250
- }
251
- }
252
-
253
244
  function portaLivre(port) {
254
245
  return new Promise((resolve) => {
255
246
  const server = net.createServer();
@@ -301,7 +292,7 @@ function cmdInstall(args) {
301
292
  console.log('');
302
293
  ensureHome(home);
303
294
  console.log(`✓ Pasta de dados pronta: ${home}`);
304
- console.log(` (leads, campanhas, pipeline e configuração ficam em "${path.join(home, 'storage')}")`);
295
+ console.log(` (leads, campanhas e configuração ficam em "${path.join(home, 'storage')}")`);
305
296
 
306
297
  const python = detectPython();
307
298
  if (!python) {
@@ -349,8 +340,7 @@ function cmdStart(args) {
349
340
  }
350
341
  const url = `http://127.0.0.1:${porta}`;
351
342
  if (jaRodando) {
352
- console.log(`ℹ LeadMaster já está rodando em ${url} — abrindo o navegador.`);
353
- openBrowser(url);
343
+ console.log(`ℹ LeadMaster já está rodando em ${url} — abra o endereço no navegador.`);
354
344
  process.exit(0);
355
345
  return;
356
346
  }
@@ -383,7 +373,7 @@ function cmdStart(args) {
383
373
  console.warn('⚠ Interface construída não encontrada (frontend/dist).');
384
374
  }
385
375
 
386
- const child = spawn(python.cmd, [...python.args, '-m', 'uvicorn', 'main:app', '--host', '127.0.0.1', '--port', String(porta)], {
376
+ const child = spawn(python.cmd, [...python.args, '-m', 'uvicorn', 'main:app', '--host', '127.0.0.1', '--port', String(porta), '--no-access-log'], {
387
377
  cwd: path.join(PKG_ROOT, 'backend'),
388
378
  env: {
389
379
  ...process.env,
@@ -432,7 +422,8 @@ function cmdStart(args) {
432
422
  process.exit(1);
433
423
  }
434
424
  console.log(`✓ LeadMaster no ar em ${url} (dados: ${home})`);
435
- openBrowser(url);
425
+ console.log('');
426
+ console.log(`ℹ Abra ${url} no navegador quando quiser — o LeadMaster nunca abre janelas sozinho.`);
436
427
  console.log('');
437
428
  console.log('Pressione Ctrl+C para encerrar.');
438
429
  })().catch((err) => {
@@ -473,7 +464,7 @@ function cmdHelp() {
473
464
  console.log('');
474
465
  console.log('Comandos:');
475
466
  console.log(' install Prepara a pasta de dados e instala dependências Python.');
476
- console.log(' start Sobe o servidor local e abre o navegador (padrão).');
467
+ console.log(' start Sobe o servidor local e imprime a URL no terminal (você abre o navegador).');
477
468
  console.log(' doctor Diagnóstico do ambiente.');
478
469
  console.log(' version Mostra a versão.');
479
470
  console.log(' help Esta ajuda.');
@@ -1,102 +0,0 @@
1
- """Integração com o MoneyPrinterTurbo (produção de vídeos wide 16:9).
2
-
3
- API esperada (padrão do projeto): POST http://localhost:8080/api/v1/videos
4
- Cria a tarefa e consulta o progresso até o vídeo ficar pronto.
5
- """
6
-
7
- import time
8
-
9
- import httpx
10
-
11
- from . import storage
12
-
13
-
14
- class ServicoIndisponivel(Exception):
15
- """MoneyPrinterTurbo não está rodando — mensagem acionável."""
16
-
17
-
18
- class ProducaoErro(Exception):
19
- """O serviço respondeu, mas a produção falhou."""
20
-
21
-
22
- def _conf() -> dict:
23
- return storage.get_config()["integracoes"]["moneyprinter"]
24
-
25
-
26
- def _url() -> str:
27
- return _conf()["url"].rstrip("/")
28
-
29
-
30
- def verificar() -> bool:
31
- """True se o serviço responde (qualquer código HTTP conta como 'vivo')."""
32
- try:
33
- httpx.get(_url() + "/docs", timeout=2)
34
- return True
35
- except httpx.HTTPError:
36
- try:
37
- httpx.get(_url(), timeout=2)
38
- return True
39
- except httpx.HTTPError:
40
- return False
41
-
42
-
43
- def criar_video_wide(
44
- subject: str,
45
- script: str,
46
- terms: str = "",
47
- duracao_segundos: int = 60,
48
- voz: str = "pt-BR-Standard",
49
- ) -> str:
50
- """Cria a tarefa de vídeo wide (16:9). Retorna o task_id."""
51
- payload = {
52
- "video_subject": subject,
53
- "video_script": script,
54
- "video_terms": terms or subject,
55
- "video_aspect": "16:9",
56
- "video_clip_duration": min(duracao_segundos, 20),
57
- "video_source_provider": "pexels",
58
- "voice_volume": 1.0,
59
- "voice_rate": 1.0,
60
- }
61
- try:
62
- r = httpx.post(_url() + "/api/v1/videos", json=payload, timeout=120)
63
- except httpx.HTTPError as e:
64
- raise ServicoIndisponivel(
65
- f"MoneyPrinterTurbo não respondeu em {_url()}. "
66
- "Inicie o serviço (uvicorn app.server:app) e clique em Repetir."
67
- ) from e
68
- if r.status_code >= 400:
69
- raise ProducaoErro(f"MoneyPrinterTurbo rejeitou a tarefa (HTTP {r.status_code}): {r.text[:300]}")
70
- task_id = (r.json().get("data") or {}).get("task_id")
71
- if not task_id:
72
- raise ProducaoErro(f"Resposta sem task_id: {r.text[:300]}")
73
- return task_id
74
-
75
-
76
- def aguardar_video(task_id: str, timeout_min: int = 20, a_cada_progresso=None) -> dict:
77
- """Sonda a tarefa até concluir. Retorna {progresso, videos, saidas}."""
78
- limite = time.time() + timeout_min * 60
79
- while time.time() < limite:
80
- try:
81
- r = httpx.get(_url() + "/api/v1/tasks", params={"task_id": task_id}, timeout=15)
82
- except httpx.HTTPError as e:
83
- raise ServicoIndisponivel(
84
- f"MoneyPrinterTurbo sumiu durante a produção: {e}. Clique em Repetir com o serviço ativo."
85
- ) from e
86
- if r.status_code >= 500:
87
- time.sleep(5)
88
- continue
89
- data = (r.json() or {}).get("data") or {}
90
- task = data
91
- if isinstance(data.get("tasks"), list) and data["tasks"]:
92
- task = data["tasks"][0]
93
- progresso = task.get("progress") or 0
94
- if a_cada_progresso:
95
- a_cada_progresso(progresso)
96
- if task.get("videos"):
97
- return {"progresso": 100, "videos": task["videos"], "saidas": task.get("progress_details", {})}
98
- time.sleep(5)
99
- raise ProducaoErro(
100
- f"A tarefa {task_id} não ficou pronta em {timeout_min} minutos. "
101
- "Verifique o MoneyPrinterTurbo e use Repetir."
102
- )
@@ -1,62 +0,0 @@
1
- """Integração com o OpenMontage (CLI local, fallback de produção de vídeos).
2
-
3
- O caminho do executável é configurável em Ferramentas → OpenMontage.
4
- Como as flags do CLI podem variar entre versões, usamos um template de
5
- argumentos configurável com os placeholders {roteiro} e {saida}.
6
- """
7
-
8
- import subprocess
9
- from pathlib import Path
10
-
11
- from . import storage
12
-
13
-
14
- class IntegracaoNaoConfigurada(Exception):
15
- """Caminho do CLI não configurado — mensagem acionável."""
16
-
17
-
18
- class ExecucaoErro(Exception):
19
- """O CLI falhou; stderr vai na mensagem."""
20
-
21
-
22
- def _conf() -> dict:
23
- return storage.get_config()["integracoes"]["openmontage"]
24
-
25
-
26
- def disponivel() -> bool:
27
- cli = _conf().get("cli_path", "")
28
- return bool(cli) and Path(cli).exists()
29
-
30
-
31
- def produzir(job_id: str, roteiro_path: str) -> dict:
32
- """Executa o OpenMontage com o roteiro e devolve {video_path}."""
33
- conf = _conf()
34
- cli = conf.get("cli_path", "")
35
- if not cli or not Path(cli).exists():
36
- raise IntegracaoNaoConfigurada(
37
- "OpenMontage não configurado. Informe o caminho do executável em "
38
- "Ferramentas → OpenMontage (https://github.com/openmotional/OpenMontage) "
39
- "ou mantenha o MoneyPrinterTurbo ativo."
40
- )
41
- saida = storage.videos_dir() / job_id
42
- saida.mkdir(parents=True, exist_ok=True)
43
- try:
44
- resultado = subprocess.run(
45
- [cli, roteiro_path, "--output", str(saida)],
46
- capture_output=True,
47
- text=True,
48
- timeout=1800,
49
- shell=False,
50
- )
51
- except FileNotFoundError as e:
52
- raise IntegracaoNaoConfigurada(f"Não foi possível executar o CLI do OpenMontage: {e}") from e
53
- except subprocess.TimeoutExpired as e:
54
- raise ExecucaoErro("OpenMontage passou de 30 minutos sem concluir.") from e
55
- if resultado.returncode != 0:
56
- raise ExecucaoErro(f"OpenMontage terminou com código {resultado.returncode}: {resultado.stderr[-400:]}")
57
- mp4 = sorted(saida.glob("*.mp4"))
58
- return {
59
- "video_path": str(mp4[-1]) if mp4 else None,
60
- "pasta_saida": str(saida),
61
- "observacao": "" if mp4 else "Execução OK, mas nenhum .mp4 encontrado na pasta de saída.",
62
- }