@danhachuel/prodmaker 0.1.13 → 0.1.14

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
@@ -3,6 +3,26 @@
3
3
  Todas as versões seguem a etiqueta visual `MAJOR.MINOR.PATCH` com patch de dois dígitos
4
4
  (o registry NPM normaliza `0.1.00` → `0.1.0`).
5
5
 
6
+ ## 0.1.14 — 2026-10-31
7
+
8
+ - **Fase 10 — REVISÃO (fechamento das lacunas apontadas na criação de produto/PDF).**
9
+ - **`POST /api/builder/generate-product`:** gera o **conteúdo completo** do produto em
10
+ Markdown conforme o agente low-ticket — 1. Capa (≤6 palavras); 2. Diagnóstico (5 motivos);
11
+ 3. Solução (5–7 passos); 4. Exemplos (3 casos); 5. Checklist; 6. Bônus.
12
+ - **`POST /api/builder/generate-assets`:** 3 Order Bumps (checkout +R$21,90 · upsell +R$197 ·
13
+ parceiro 50/50) + **Quiz com IA** (5 perguntas) — rota própria conforme imagem 4.
14
+ - **`POST /api/builder/generate-landing`:** landing page de vendas com as **12 seções
15
+ validadas**, salva em `paginas` com preview em `/pages/{id}`.
16
+ - **Wizard com 4 passos:** (1) mineração + parceiro; (2) **seleção de formato** (E-book/
17
+ Planilha/Mini-curso/Desafio/Workshop) + estrutura + assets + preço R$27–97; (3) conteúdo
18
+ completo + **PDF** + landing page; (4) **12 anúncios + 10 pins** + **Publicar no GitHub +
19
+ Cloudflare** (reaproveita o motor da Fase 4).
20
+ - `generate-ads` agora devolve também os 10 pins; `generate-pdf` usa o conteúdo completo
21
+ quando disponível (antes renderia só o esqueleto).
22
+ - **Salvamento completo:** `salvar` grava anúncios + pins e **cria o registro em `produtos`**
23
+ (status aprovado, vinculado via `infoprodutos.produto_id`) — AdsPages e AdsCreator passam
24
+ a consumir o produto do builder automaticamente.
25
+
6
26
  ## 0.1.13 — 2026-10-31
7
27
 
8
28
  - **Fase 10 — Product Builder Wizard (motor de criação de ofertas).**
@@ -17,6 +17,17 @@ class EstruturaBody(BaseModel):
17
17
  oferta: dict
18
18
 
19
19
 
20
+ class ProdutoBody(BaseModel):
21
+ oferta: dict
22
+ estrutura: dict = {}
23
+ formato: str = ""
24
+
25
+
26
+ class AssetsBody(BaseModel):
27
+ oferta: dict
28
+ preco: float = 47
29
+
30
+
20
31
  class AnunciosBody(BaseModel):
21
32
  produto: str
22
33
  promessa: str = ""
@@ -24,17 +35,25 @@ class AnunciosBody(BaseModel):
24
35
  link: str = ""
25
36
 
26
37
 
38
+ class LandingBody(BaseModel):
39
+ oferta: dict
40
+ estrutura: dict = {}
41
+ produto_md: str = ""
42
+
43
+
27
44
  class PdfBody(BaseModel):
28
45
  oferta: dict
29
- estrutura: dict
46
+ estrutura: dict = {}
30
47
  preco: float
31
48
  parceiro: str = ""
32
49
  modo: str = "rapido"
50
+ produto_md: str = ""
33
51
 
34
52
 
35
53
  class SalvarBody(BaseModel):
36
54
  record_id: int
37
- anuncios: list
55
+ anuncios: list = []
56
+ pins: list = []
38
57
 
39
58
 
40
59
  def _erro(exc: LLMError) -> HTTPException:
@@ -57,6 +76,33 @@ def generate_structure(body: EstruturaBody):
57
76
  raise _erro(exc)
58
77
 
59
78
 
79
+ @router.post("/generate-product")
80
+ def generate_product(body: ProdutoBody):
81
+ """Conteúdo completo do produto em Markdown (agente low-ticket)."""
82
+ try:
83
+ return builder.gerar_produto(body.oferta, body.estrutura, body.formato)
84
+ except LLMError as exc:
85
+ raise _erro(exc)
86
+
87
+
88
+ @router.post("/generate-assets")
89
+ def generate_assets(body: AssetsBody):
90
+ """3 Order Bumps (checkout/upsell/parceiro) + Quiz com IA."""
91
+ try:
92
+ return builder.gerar_assets(body.oferta, body.preco)
93
+ except LLMError as exc:
94
+ raise _erro(exc)
95
+
96
+
97
+ @router.post("/generate-landing")
98
+ def generate_landing(body: LandingBody):
99
+ """Landing page de vendas (12 seções) para o produto do wizard."""
100
+ try:
101
+ return builder.gerar_landing(body.oferta, body.estrutura, body.produto_md)
102
+ except LLMError as exc:
103
+ raise _erro(exc)
104
+
105
+
60
106
  @router.post("/generate-ads")
61
107
  def generate_ads(body: AnunciosBody):
62
108
  try:
@@ -76,6 +122,6 @@ def generate_pdf(body: PdfBody):
76
122
  @router.post("/salvar")
77
123
  def salvar(body: SalvarBody):
78
124
  try:
79
- return builder.salvar(body.anuncios, body.record_id)
125
+ return builder.salvar(body.anuncios, body.record_id, body.pins)
80
126
  except (LLMError, ValueError) as exc:
81
127
  raise HTTPException(status_code=400, detail=str(exc))
package/app/main.py CHANGED
@@ -15,7 +15,7 @@ from .services.database import ensure_db, get_db, now_str, row_to_dict
15
15
  from .services.llm import LLMError, analyze_product, list_providers
16
16
  from .services.store import PRODMAKER_HOME, STORAGE, STATE, ensure_state_dir, get_master_code, get_settings, save_settings
17
17
 
18
- APP_VERSION = "0.1.13"
18
+ APP_VERSION = "0.1.14"
19
19
  COOKIE_NAME = "prodmaker_session"
20
20
  SESSION_DAYS = 30
21
21
  MASTER_ALIAS = "admin"
@@ -264,7 +264,7 @@ def health():
264
264
 
265
265
  @app.get("/api/version")
266
266
  def version():
267
- return {"version": APP_VERSION, "canonical": "0.1.13"}
267
+ return {"version": APP_VERSION, "canonical": "0.1.14"}
268
268
 
269
269
 
270
270
  @app.get("/api/settings")
@@ -94,6 +94,56 @@ def gerar_estrutura(oferta: dict) -> dict:
94
94
  return {"estrutura": estrutura, "provider": result["provider"]}
95
95
 
96
96
 
97
+ def gerar_produto(oferta: dict, estrutura: dict, formato: str = "") -> dict:
98
+ """Gera o CONTEÚDO COMPLETO do produto em Markdown (estrutura obrigatória do agente .md)."""
99
+ if not oferta or not (oferta.get("promessa") or "").strip():
100
+ raise LLMError("Selecione uma oferta antes de gerar o conteúdo do produto.")
101
+ formato = (formato or oferta.get("formato") or "E-book").strip()
102
+ prompt = _fill(
103
+ _template("builder-produto.md", "Gere o conteudo completo do produto {OFERTA} em Markdown."),
104
+ {
105
+ "OFERTA": oferta.get("nome") or "",
106
+ "FORMATO": formato,
107
+ "PROMESSA": oferta.get("promessa") or "",
108
+ "NICHO": oferta.get("publico") or (oferta.get("dores") or [""])[0],
109
+ "AVATAR": oferta.get("publico") or "",
110
+ "PRECO": oferta.get("preco_sugerido") or 47,
111
+ },
112
+ )
113
+ result = generate(
114
+ prompt,
115
+ system="Você é um especialista em produtos digitais práticos. Escreva o produto completo em Markdown, sem comentários.",
116
+ max_tokens=12000,
117
+ )
118
+ md_text = (result["text"] or "").strip()
119
+ if len(md_text) < 400 or "##" not in md_text:
120
+ raise LLMError("O LLM não retornou o conteúdo completo do produto — tente novamente.")
121
+ return {"markdown": md_text, "provider": result["provider"]}
122
+
123
+
124
+ def gerar_assets(oferta: dict, preco: float = 47) -> dict:
125
+ """Gera 3 Order Bumps (checkout/upsell/parceiro 50-50) + Quiz com IA."""
126
+ if not oferta or not (oferta.get("promessa") or "").strip():
127
+ raise LLMError("Selecione uma oferta antes de gerar os assets.")
128
+ prompt = _fill(
129
+ _template("builder-assets.md", "Gere order bumps e quiz para {OFERTA} em JSON."),
130
+ {
131
+ "OFERTA": oferta.get("nome") or "",
132
+ "PROMESSA": oferta.get("promessa") or "",
133
+ "PRECO": preco or oferta.get("preco_sugerido") or 47,
134
+ },
135
+ )
136
+ result = generate(prompt, system="Você é um estrategista de monetização. Responda SOMENTE JSON válido.", max_tokens=5000)
137
+ try:
138
+ parsed = _parse_json(result["text"])
139
+ except json.JSONDecodeError as exc:
140
+ raise LLMError(f"JSON do LLM inválido: {exc}") from exc
141
+ bumps = parsed.get("order_bumps") or []
142
+ if len(bumps) < 3:
143
+ raise LLMError(f"O LLM retornou {len(bumps)} order bumps (esperado 3) — tente novamente.")
144
+ return {"order_bumps": bumps[:3], "quiz": parsed.get("quiz") or {}, "provider": result["provider"]}
145
+
146
+
97
147
  def gerar_anuncios(produto: str, promessa: str, nicho: str = "", link: str = "") -> dict:
98
148
  if not produto.strip():
99
149
  raise LLMError("Informe o nome do produto para gerar os anúncios.")
@@ -101,15 +151,16 @@ def gerar_anuncios(produto: str, promessa: str, nicho: str = "", link: str = "")
101
151
  _template("builder-anuncios.md", "Gere 12 anuncios para {PRODUTO} em JSON com chave 'anuncios'."),
102
152
  {"PRODUTO": produto, "PROMESSA": promessa, "NICHO": nicho, "LINK": link},
103
153
  )
104
- result = generate(prompt, system="Você é um copywriter de direct response. Responda SOMENTE JSON válido.", max_tokens=5000)
154
+ result = generate(prompt, system="Você é um copywriter de direct response e especialista em Pinterest. Responda SOMENTE JSON válido.", max_tokens=6000)
105
155
  try:
106
156
  parsed = _parse_json(result["text"])
107
157
  except json.JSONDecodeError as exc:
108
158
  raise LLMError(f"JSON do LLM inválido: {exc}") from exc
109
159
  anuncios = [item for item in (parsed.get("anuncios") or []) if isinstance(item, dict)]
160
+ pins = [item for item in (parsed.get("pins") or []) if isinstance(item, dict)]
110
161
  if len(anuncios) < 12:
111
162
  raise LLMError(f"O LLM retornou {len(anuncios)} anúncios (esperado 12) — tente novamente.")
112
- return {"anuncios": anuncios[:12], "provider": result["provider"]}
163
+ return {"anuncios": anuncios[:12], "pins": pins[:10], "provider": result["provider"]}
113
164
 
114
165
 
115
166
  def _imagem_prompts(oferta: dict, estrutura: dict) -> list[str]:
@@ -154,14 +205,48 @@ def _ensure_columns() -> None:
154
205
  add_column_if_missing(conn, "infoprodutos", "oferta_json TEXT NOT NULL DEFAULT ''", "oferta_json")
155
206
  add_column_if_missing(conn, "infoprodutos", "estrutura_json TEXT NOT NULL DEFAULT ''", "estrutura_json")
156
207
  add_column_if_missing(conn, "infoprodutos", "anuncios_json TEXT NOT NULL DEFAULT ''", "anuncios_json")
208
+ add_column_if_missing(conn, "infoprodutos", "pins_json TEXT NOT NULL DEFAULT ''", "pins_json")
157
209
  add_column_if_missing(conn, "infoprodutos", "preco REAL NOT NULL DEFAULT 0", "preco")
158
210
  add_column_if_missing(conn, "infoprodutos", "parceiro TEXT NOT NULL DEFAULT ''", "parceiro")
211
+ add_column_if_missing(conn, "infoprodutos", "produto_id INTEGER", "produto_id")
212
+
213
+
214
+ def gerar_landing(oferta: dict, estrutura: dict, produto_md: str = "") -> dict:
215
+ """Gera a landing page de vendas (12 seções) via motor de páginas e salva em paginas."""
216
+ from . import pages
217
+
218
+ form = {
219
+ "titulo": oferta.get("nome") or "Produto",
220
+ "avatar": oferta.get("publico") or "",
221
+ "resultado": oferta.get("promessa") or "",
222
+ "dor": (oferta.get("dores") or [""])[0],
223
+ "afiliado_link": oferta.get("checkout_link") or "",
224
+ "checkout_link": oferta.get("checkout_link") or "",
225
+ "prompt": (produto_md or "")[:3000],
226
+ }
227
+ produto_ctx = {"id": None, "nome": oferta.get("nome") or "Produto", "link_afiliado": oferta.get("checkout_link") or ""}
228
+ resultado = pages.gerar_pagina("vendas", produto_ctx, form)
229
+ timestamp = now_str()
230
+ with get_db() as conn:
231
+ cursor = conn.execute(
232
+ "INSERT INTO paginas (titulo, tipo, produto_id, idioma, html_path, status, created_at, updated_at) VALUES (?, 'vendas', NULL, ?, '', 'rascunho', ?, ?)",
233
+ ((oferta.get("nome") or "Produto")[:120], oferta.get("idioma") or "Portugues", timestamp, timestamp),
234
+ )
235
+ page_id = cursor.lastrowid
236
+ pages.salvar_html(page_id, resultado["html"])
237
+ with get_db() as conn:
238
+ conn.execute("UPDATE paginas SET html_path = ? WHERE id = ?", (f"pages/{page_id}/index.html", page_id))
239
+ row = conn.execute("SELECT * FROM paginas WHERE id = ?", (page_id,)).fetchone()
240
+ record = row_to_dict(row)
241
+ record["preview_url"] = f"/pages/{page_id}"
242
+ record["provider"] = resultado["provider"]
243
+ return record
159
244
 
160
245
 
161
- def gerar_pdf(oferta: dict, estrutura: dict, preco: float, parceiro: str = "", modo: str = "rapido") -> dict:
162
- """Gera o PDF do produto + prompts de imagem (Gamma/Canva). Salva em infoprodutos."""
246
+ def gerar_pdf(oferta: dict, estrutura: dict, preco: float, parceiro: str = "", modo: str = "rapido", produto_md: str = "") -> dict:
247
+ """Gera o PDF do produto (conteúdo completo quando disponível) + prompts de imagem."""
163
248
  _ensure_columns()
164
- md_text = _markdown_produto(oferta, estrutura, preco, parceiro)
249
+ md_text = (produto_md or "").strip() or _markdown_produto(oferta, estrutura, preco, parceiro)
165
250
  prompt_imagens = _imagem_prompts(oferta, estrutura)
166
251
  timestamp = now_str()
167
252
  with get_db() as conn:
@@ -204,14 +289,45 @@ def gerar_pdf(oferta: dict, estrutura: dict, preco: float, parceiro: str = "", m
204
289
  return record
205
290
 
206
291
 
207
- def salvar(anuncios: list, record_id: int) -> dict:
292
+ def _criar_produto_para_consumo(record_id: int) -> dict:
293
+ """Cria (ou reusa) o registro em `produtos` para AdsPages/AdsCreator consumirem."""
294
+ row = _get_infoproduto(record_id)
295
+ oferta = json.loads(row.get("oferta_json") or "{}")
296
+ nome = (oferta.get("nome") or row["titulo"] or "Produto")[:160]
297
+ with get_db() as conn:
298
+ existente = None
299
+ if row.get("produto_id"):
300
+ existente = conn.execute("SELECT id FROM produtos WHERE id = ?", (row["produto_id"],)).fetchone()
301
+ if not existente:
302
+ mesmo_nome = conn.execute("SELECT id FROM produtos WHERE nome = ? ORDER BY id DESC LIMIT 1", (nome,)).fetchone()
303
+ if mesmo_nome:
304
+ produto_id = mesmo_nome["id"]
305
+ else:
306
+ cursor = conn.execute(
307
+ "INSERT INTO produtos (nome, plataforma, link_afiliado, comissao, preco, nicho, status, created_at, updated_at) VALUES (?, 'Outro', '', 0, ?, ?, 'aprovado', ?, ?)",
308
+ (nome, float(row.get("preco") or oferta.get("preco_sugerido") or 47), (oferta.get("publico") or "")[:120], now_str(), now_str()),
309
+ )
310
+ produto_id = cursor.lastrowid
311
+ conn.execute("UPDATE infoprodutos SET produto_id = ? WHERE id = ?", (produto_id, record_id))
312
+ return {"produto_id": produto_id, "nome": nome}
313
+
314
+
315
+ def _get_infoproduto(record_id: int) -> dict:
316
+ with get_db() as conn:
317
+ row = conn.execute("SELECT * FROM infoprodutos WHERE id = ?", (record_id,)).fetchone()
318
+ if not row:
319
+ raise LLMError("Infoproduto não encontrado.")
320
+ return row_to_dict(row)
321
+
322
+
323
+ def salvar(anuncios: list, record_id: int, pins: list | None = None) -> dict:
208
324
  _ensure_columns()
209
325
  with get_db() as conn:
210
326
  conn.execute(
211
- "UPDATE infoprodutos SET anuncios_json = ? WHERE id = ?",
212
- (json.dumps(anuncios[:12], ensure_ascii=False), record_id),
327
+ "UPDATE infoprodutos SET anuncios_json = ?, pins_json = ? WHERE id = ?",
328
+ (json.dumps((anuncios or [])[:12], ensure_ascii=False), json.dumps((pins or [])[:10], ensure_ascii=False), record_id),
213
329
  )
214
- row = conn.execute("SELECT * FROM infoprodutos WHERE id = ?", (record_id,)).fetchone()
215
- record = row_to_dict(row)
330
+ produto = _criar_produto_para_consumo(record_id)
331
+ record = _get_infoproduto(record_id)
216
332
  oferta = json.loads(record.get("oferta_json") or "{}")
217
- return {**record, "oferta": oferta}
333
+ return {**record, "oferta": oferta, "produto": produto}