Óticas Paris

Provador Virtual

Acesso exclusivo. Digite o código que você recebeu.

build 12ago-1

Requisitos mínimos

O que é preciso para testar agora e para levar a ferramenta à loja.

Para a equipe testar (este ambiente)

Para integrar na loja

Para hospedar o modelo completo no servidor de vocês

O que a THM entrega e opera

Óticas ParisProvador Virtual
THM-Glass-provadorvirtualprova virtual de óculos

Endpoint

# Teste rápido de terminal — prove a integração antes de escrever código
curl -X POST https://provador.paristeste.cloud/provar \
  -H "content-type: application/json" \
  -d '{
    "images": ["<data URI da foto do cliente>",
               "<data URI ou URL da foto do produto>"],
    "tipo": "grau"
  }'
# → {"status":"ok","image":"data:image/webp;base64,...","seed":123,"qc":{...}}
// Navegador (front da loja) — igual ao que esta página usa
const r = await fetch("https://provador.paristeste.cloud/provar", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ images: [fotoCliente, fotoProduto], tipo: "grau" })
});
const d = await r.json();
if (d.status === "revisar") mostrarOrientacao(d.aviso, d.recomendacao);
else if (d.image) exibirProva(d.image, d.seed);
// Node.js 18+ (backend da loja) — esconde o código de acesso do navegador
const resp = await fetch("https://provador.paristeste.cloud/provar", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    images: [fotoClienteB64, fotoProdutoB64],
    tipo: "sol",
  }),
  signal: AbortSignal.timeout(100_000),
});
const prova = await resp.json();
console.log(prova.status, prova.seed, prova.qc);
# Python 3 — ERP, automação ou backend
import requests

r = requests.post("https://provador.paristeste.cloud/provar", json={
    "images": [foto_cliente_b64, foto_produto_b64],
    "tipo": "grau",
}, timeout=100)
d = r.json()
if d["status"] == "ok":
    salvar_log(seed=d["seed"], nota=d["qc"]["nota"], segundos=d["segundos"])
<?php // PHP 8 — lojas em WooCommerce/Magento
$ch = curl_init("https://provador.paristeste.cloud/provar");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => ["content-type: application/json"],
  CURLOPT_POSTFIELDS => json_encode([
    "images" => [$fotoCliente, $fotoProduto],
    "tipo"   => "grau",
  ]),
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 100,
]);
$prova = json_decode(curl_exec($ch), true);
// C# / .NET 8
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(100) };
var corpo = JsonSerializer.Serialize(new {
    images = new[] { fotoCliente, fotoProduto },
    tipo = "grau",
});
var resp = await http.PostAsync("https://provador.paristeste.cloud/provar",
    new StringContent(corpo, Encoding.UTF8, "application/json"));
var prova = await resp.Content.ReadFromJsonAsync<JsonElement>();
// Java 17+ (HttpClient nativo)
var corpo = """
  {"images":["%s","%s"],"tipo":"grau"}
  """.formatted(fotoCliente, fotoProduto);
var req = HttpRequest.newBuilder(URI.create("https://provador.paristeste.cloud/provar"))
    .header("content-type", "application/json")
    .timeout(Duration.ofSeconds(100))
    .POST(HttpRequest.BodyPublishers.ofString(corpo)).build();
var resp = HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofString());
# Ruby — Shopify apps e afins
require "net/http"; require "json"

uri = URI("https://provador.paristeste.cloud/provar")
req = Net::HTTP::Post.new(uri, "content-type" => "application/json")
req.body = { images: [foto_cliente, foto_produto], tipo: "grau" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true,
                       read_timeout: 100) { |h| h.request(req) }
prova = JSON.parse(resp.body)
// Go
corpo, _ := json.Marshal(map[string]any{
    "images": []string{fotoCliente, fotoProduto},
    "tipo":   "grau",
})
cli := &http.Client{Timeout: 100 * time.Second}
resp, err := cli.Post("https://provador.paristeste.cloud/provar",
    "application/json", bytes.NewReader(corpo))
// decodifique resp.Body e trate status "ok" | "revisar"
// React — hook pronto para a PDP
function useProva() {
  const [estado, setEstado] = useState({ carregando: false });
  const provar = async (fotoCliente, fotoProduto, tipo) => {
    setEstado({ carregando: true });
    try {
      const r = await fetch("https://provador.paristeste.cloud/provar", {
        method: "POST", headers: { "content-type": "application/json" },
        body: JSON.stringify({ images: [fotoCliente, fotoProduto], tipo }),
        signal: AbortSignal.timeout(100_000),
      });
      setEstado({ carregando: false, ...(await r.json()) });
    } catch { setEstado({ carregando: false, erroRede: true }); }
  };
  return [estado, provar];
}
<!-- Provador Virtual THM — widget de SANDBOX para a página de produto.
     Cole antes do </body> do template da PDP:
     botão → foto do cliente → prova no nosso servidor → resultado na tela.
     A imagem do produto é capturada da própria página (og:image). -->
<script>
(function () {
  var API   = "https://provador.paristeste.cloud/provar";

  // 1. imagem do produto: og:image existe em toda PDP VTEX e não depende de tema
  var meta = document.querySelector('meta[property="og:image"]');
  var fotoProduto = meta ? meta.content : null;
  if (!fotoProduto) return;              // não é página de produto: não faz nada

  // 2. o botão — posicione onde preferir; aqui, depois da área de compra
  var alvo = document.querySelector(".vtex-flex-layout-0-x-flexRow--product-main") || document.body;
  var btn = document.createElement("button");
  btn.textContent = "Provar virtual";
  btn.style.cssText = "margin:12px 0;padding:12px 22px;border:0;border-radius:8px;" +
    "background:#b07a4f;color:#fff;font-size:15px;cursor:pointer";
  alvo.appendChild(btn);

  // 3. entrada da foto (galeria ou câmera do celular)
  var input = document.createElement("input");
  input.type = "file"; input.accept = "image/*"; input.hidden = true;
  document.body.appendChild(input);
  btn.onclick = function () { input.click(); };

  // 4. reduz a 1024px ANTES de subir — o motor trabalha nesse tamanho
  input.onchange = function () {
    var f = input.files[0]; if (!f) return;
    var img = new Image();
    img.onload = function () {
      var M = 1024, e = Math.min(1, M / Math.max(img.width, img.height));
      var c = document.createElement("canvas");
      c.width = img.width * e; c.height = img.height * e;
      c.getContext("2d").drawImage(img, 0, 0, c.width, c.height);
      provar(c.toDataURL("image/jpeg", 0.85));
    };
    var r = new FileReader();
    r.onload = function () { img.src = r.result; };
    r.readAsDataURL(f);
  };

  // 5. chamada + resultado (um retry, timeout 100 s)
  function provar(fotoCliente, retentou) {
    btn.disabled = true; btn.textContent = "Gerando sua prova…";
    var ac = new AbortController();
    var t = setTimeout(function () { ac.abort(); }, 100000);
    fetch(API, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ images: [fotoCliente, fotoProduto],
                             tipo: /sol/i.test(document.title) ? "sol" : "grau" }),
      signal: ac.signal
    })
    .then(function (r) { return r.json(); })
    .then(function (d) {
      clearTimeout(t);
      if (d.status === "revisar") alert(d.aviso + "\n" + (d.recomendacao || ""));
      else if (d.image) mostrar(d.image);
      else alert("Não foi possível gerar agora. Tente novamente.");
    })
    .catch(function () {
      clearTimeout(t);
      if (!retentou) return provar(fotoCliente, true);   // rede oscilou: 1 retry
      alert("Conexão instável — verifique o sinal e tente de novo.");
    })
    .finally(function () { btn.disabled = false; btn.textContent = "Provar virtual"; });
  }

  // 6. resultado num overlay simples (troque pelo modal do tema de vocês)
  function mostrar(uri) {
    var o = document.createElement("div");
    o.style.cssText = "position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.7);" +
      "display:grid;place-items:center;padding:20px";
    o.innerHTML = '<img src="' + uri + '" style="max-height:85vh;max-width:95vw;border-radius:12px">';
    o.onclick = function () { o.remove(); };
    document.body.appendChild(o);
  }
})();
</script>
// VTEX — na PDP, usando a imagem que a página já exibe
const fotoProduto = document.querySelector(".vtex-store-components-3-x-productImageTag")?.src;
// converta a foto do cliente (input file/câmera) para data URI e chame:
const prova = await fetch("https://provador.paristeste.cloud/provar", {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({
    images: [fotoClienteDataURI, fotoProduto],   // URL do produto também vale
    tipo: skuEhSol ? "sol" : "grau",
  }),
}).then(r => r.json());

Códigos de resposta — o que fazer com cada um

okExiba image (WebP, vertical). Registre seed, qc.nota e segundos nos seus logs — com a seed qualquer prova é reproduzível.
revisar · sem_rostoNão é erro: oriente com a mensagem de recomendacao (foto de frente, bem iluminada).
revisar · foto_dificilO motor não conseguiu preservar a foto exatamente como é — peça uma foto mais afastada, rosto inteiro.
revisar · arma_na_fotoObjeto bloqueado por segurança. Exiba a recomendação.
HTTP 422Corpo malformado — confira o JSON e os dois itens de images.
HTTP 503Motor reiniciando — repita em ~1 minuto.

Boas práticas de integração

fotoReduza para 1024 px no lado maior, JPEG qualidade 0,85, antes de enviar. O motor trabalha nesse tamanho — acima disso é upload pago à toa, e upload é o elo fraco no 4G.
redeTimeout de 100 s + 1 retry automático. A geração leva 5–12 s; em horário de pico a fila pode chegar a ~1 min.
filaO motor processa uma prova por vez (GPU dedicada). Desabilite o botão durante a chamada e não dispare em paralelo para o mesmo usuário.
conversarevisar é orientação, não erro: sempre exiba aviso e recomendacao ao cliente.
privacidadeA foto do cliente é processada em memória e não é armazenada pelo motor. Na loja, só persista com consentimento (LGPD).

Operação e monitoramento

GET /healthVivacidade + modelo + versão + GPU. Monitore a cada minuto; é o mesmo endpoint que valida um deploy.
sinaissegundos subindo = fila crescendo · taxa de revisar subindo = fotos ruins chegando (reforce o guia da foto) · qc.nota < 3 recorrente = acione a THM.
reproduçãoQualquer prova questionada: mande sku + seed para a THM — a mesma prova é regenerada bit a bit para análise.
atividadeO painel à direita mostra as chamadas desta sessão em tempo real, com tamanhos, tempos e seeds.

Comportamento

entradafoto do usuário + imagem da armação (base64)
saídasempre vertical; a foto do usuário é preservada — só a região dos óculos muda
seedopcional — mesma seed reproduz o resultado

Segurança

conteudo_imprópriostatus "revisar" — bloqueia nudez/violência antes de gerar
demais fotosa prova é sempre tentada; se não ficar boa, o usuário refaz

Atividade ao vivo

As chamadas aparecem aqui conforme você usa a ferramenta.

Experimente as armações sem sair de casa

Escolha um modelo, use sua foto e veja como fica no seu rosto em segundos.

Sua foto

rosto de frente, bem iluminado, sem óculos · leva poucos segundos

A foto ideal

✓De frente, rosto inteiroDe frente, rosto inteiro
✓Ambiente claroAmbiente claro
✓Um pouco afastada, ombros à vistaUm pouco afastada, ombros à vista
✕Muito perto / rosto cortadoMuito perto / rosto cortado
✕De baixo ou de ladoDe baixo ou de lado
✕Escura ou borradaEscura ou borrada
Tire ou envie sua foto e clique em gerar.