@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 +28 -0
- package/README.md +11 -21
- package/backend/main.py +5 -62
- package/backend/services/llm.py +5 -125
- package/backend/services/storage.py +48 -68
- package/frontend/dist/assets/{index-eswbuT3S.css → index-CFQHp6yf.css} +1 -1
- package/frontend/dist/assets/index-CPq53JWx.js +90 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +4 -6
- package/scripts/cli.mjs +1 -1
- package/backend/services/moneyprinter.py +0 -102
- package/backend/services/openmontage.py +0 -62
- package/backend/services/pipeline.py +0 -515
- package/backend/services/short_video_maker.py +0 -72
- package/backend/services/youtube_upload.py +0 -163
- package/frontend/dist/assets/index-CANBU7DH.js +0 -93
package/frontend/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>LeadMaster — Painel</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CPq53JWx.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CFQHp6yf.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-gray-100 min-h-screen">
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danhachuel/leadmaster",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "SaaS local de prospecção e automação
|
|
3
|
+
"version": "0.1.01",
|
|
4
|
+
"description": "SaaS local de prospecção e automação: CRM, extratores de leads e agentes de IA — 100% local.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Dan Hachuel",
|
|
7
7
|
"type": "module",
|
|
@@ -24,16 +24,14 @@
|
|
|
24
24
|
"install:local": "node scripts/cli.mjs install",
|
|
25
25
|
"doctor": "node scripts/cli.mjs doctor",
|
|
26
26
|
"dev": "concurrently -n api,web -c blue,magenta \"npm:dev:api\" \"npm:dev:web\"",
|
|
27
|
-
"dev:api": "cd backend && python -m uvicorn main:app --reload --host 127.0.0.1 --port
|
|
27
|
+
"dev:api": "cd backend && python -m uvicorn main:app --reload --host 127.0.0.1 --port 8010",
|
|
28
28
|
"dev:web": "npm --prefix frontend run dev",
|
|
29
29
|
"build": "npm --prefix frontend run build"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"lead-generation",
|
|
33
33
|
"crm",
|
|
34
|
-
"
|
|
35
|
-
"youtube",
|
|
36
|
-
"shorts",
|
|
34
|
+
"prospection",
|
|
37
35
|
"automation",
|
|
38
36
|
"local-first"
|
|
39
37
|
],
|
package/scripts/cli.mjs
CHANGED
|
@@ -301,7 +301,7 @@ function cmdInstall(args) {
|
|
|
301
301
|
console.log('');
|
|
302
302
|
ensureHome(home);
|
|
303
303
|
console.log(`✓ Pasta de dados pronta: ${home}`);
|
|
304
|
-
console.log(` (leads, campanhas
|
|
304
|
+
console.log(` (leads, campanhas e configuração ficam em "${path.join(home, 'storage')}")`);
|
|
305
305
|
|
|
306
306
|
const python = detectPython();
|
|
307
307
|
if (!python) {
|
|
@@ -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
|
-
}
|