@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/CHANGELOG.md +52 -0
- package/README.md +15 -24
- package/backend/main.py +26 -62
- package/backend/services/llm.py +5 -125
- package/backend/services/storage.py +48 -68
- package/frontend/dist/assets/index-0xtgAs3t.js +90 -0
- package/frontend/dist/assets/{index-eswbuT3S.css → index-pSbjfRli.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +4 -6
- package/scripts/cli.mjs +8 -17
- 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/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
|
|
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
|
|
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} —
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|