个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Scanner de cartas Pokémon TCG pela câmera do celular, direto no navegador. Aponte a câmera para a carta e o site diz qual é, com preço de referência e links de busca para a Liga Pokémon e a MYP Cards. Pokémon TCG card scanner for the phone browser. Point the camera at a card to identify it.查看完整 README ↗
Scanner de cartas Pokémon TCG pela câmera do celular, direto no navegador. Aponte a câmera para a carta e o site diz qual é, com preço de referência e links de busca para a Liga Pokémon e a MYP Cards.
Pokémon TCG card scanner for the phone browser. Point the camera at a card to identify it.
Cartas em japonês ou coreano são identificadas pela arte e caem na versão em inglês equivalente. O servidor publicado também tem algumas coleções japonesas inteiras (ex.: Storm Emeralda), com a imagem da carta japonesa.
câmera → retícula viva (scanic, segue os 4 cantos da carta)
→ foto nítida → YOLO acha os 4 cantos → corte com correção de perspectiva
→ POST /recognize (servidor)
DINOv2-S @336 → similaridade de cosseno contra ~20 mil cartas
→ menos a penalidade das cartas "ímã" (as que parecem com qualquer foto)
→ desempate por pHash quando dois prints têm a mesma arte
→ só quando o match é ambíguo, OCR (RapidOCR): o nome no topo da carta e, se ainda faltar, o número no rodapé em alta resolução
→ resultado + preço de referência + links de busca
O reconhecimento roda num servidor, não no celular. Rodar o modelo no navegador travava a tela e levava segundos por carta. No servidor quente, um scan leva ~0,5s.
O site publicado usa o nosso servidor. Para usar o reconhecimento no seu projeto, veja Usar o reconhecimento no seu projeto.
Decisões de arquitetura e o porquê de cada uma: docs/arquitetura.md.
npm install
cp .env.example .env # ajuste VITE_RECOGNIZE_URL se usar outro servidor
npm run dev # http://localhost:5173
A câmera exige HTTPS, mas localhost é exceção. Para testar no celular, publique o build (veja abaixo) ou use um túnel HTTPS.
npm run typecheck
npm run build # gera dist/, um site estático
O dist/ é estático e roda em qualquer host (Vercel, Netlify, GitHub Pages, Cloudflare Pages). Configure as variáveis no build:
| Variável | Uso |
|---|---|
VITE_RECOGNIZE_URL | URL do servidor de reconhecimento. Vazio = o servidor publicado (https://cartinhas-recognize.fly.dev). |
VITE_FEEDBACK_URL | Opcional. Link do botão "Mandar feedback". |
VITE_TELEMETRY_URL | Opcional. Endpoint que recebe a telemetria anônima (POST, corpo JSON em texto puro). Vazio = sem telemetria. |
Não existe um modelo treinado por nós. O modelo é o DINOv2-small público (Meta, Apache-2.0), sem ajuste. O que faz o reconhecimento funcionar é o que está em volta dele:
Tudo isso está no código de server/, com os scripts para gerar o modelo e o índice. Há duas formas de usar:
Siga o server/README.md. O atalho é baixar da última release o modelo, o índice e a penalidade prontos, e pular direto para rodar. Do zero:
VITE_RECOGNIZE_URL.Diferenças para o servidor publicado:
Um build sem VITE_RECOGNIZE_URL já usa o nosso servidor (https://cartinhas-recognize.fly.dev), que aceita chamadas de qualquer site. Serve para testar e para uso pessoal, com estes limites:
Para um site com tráfego de verdade, rode o seu servidor (opção 1).
POST {VITE_RECOGNIZE_URL}/recognize, multipart/form-data:
file: JPEG da carta (lado maior até 900px).pre: 1 quando a imagem já é só a carta recortada. Sem ele, o servidor detecta a carta na foto.footer (opcional, só com pre=1): JPEG dos 28% de baixo da mesma carta, em resolução cheia (até 1200px de largura). O servidor lê o número da carta nesta imagem. No file reduzido, o número tem ~12px e o OCR não lê.Resposta:
{
"card": { "api_id": "sv07-158", "name": "Lapras ex", "number": "158", "set_id": "sv07",
"set_name": "Stellar Crown", "printed_total": 142,
"image_url": "https://assets.tcgdex.net/en/sv/sv07/158/low.webp", "cos": 0.91 },
"confident": true,
"not_card": false,
"ocr": { "number": "158", "total": "142", "match": true, "name": "Lapras ex 220", "name_match": true },
"candidates": [ "… até 5 cartas no mesmo formato de card …" ]
}
confident: true quando o número lido confere, quando a semelhança é alta com folga sobre a 2ª candidata, ou quando o nome lido confere e a semelhança é alta com folga sobre as outras cartas desse nome. Com false, mostre as candidatas para a pessoa escolher.not_card: true quando a foto não parece carta nenhuma (mão, mesa, tecido). O card continua vindo, mas não deve ser mostrado.candidates: a carta escolhida primeiro, depois a ordem do ranking do servidor, que desconta as cartas "ímã" e põe na frente as cartas do nome lido. Por isso o cos nem sempre aparece em ordem decrescente.ocr: o que o servidor leu na carta. name é o texto lido no topo e name_match diz se ele casou com o nome de alguma carta. Os dois vêm vazios quando o OCR não rodou (match decisivo ou quadro sem carta).400 quando o file não é uma imagem; 413 quando a imagem tem mais de 16 milhões de pixels.api_id é o id do TCGdex, na maioria das cartas: https://api.tcgdex.net/v2/en/cards/{api_id} traz o resto dos dados. As exceções são as coleções que só o servidor publicado tem (ex.: m6-jp-87).GET /health acorda o servidor. O site chama essa rota ao abrir, porque a máquina suspende quando fica ociosa e o primeiro scan depois disso pode levar ~20s.
Uma foto da carta, de preferência só a carta (sem pre, o servidor procura a carta na foto). Troque SERVER pelo seu servidor (veja Usar o reconhecimento no seu projeto).
curl
curl -F "[email protected]" https://cartinhas-recognize.fly.dev/recognize
JavaScript (navegador ou Node 18+)
const SERVER = "https://cartinhas-recognize.fly.dev";
// `foto` é um Blob/File: um <input type="file">, ou canvas.toBlob(cb, "image/jpeg", 0.8).
async function reconhecer(foto) {
const form = new FormData();
form.append("file", foto, "carta.jpg");
const res = await fetch(`${SERVER}/recognize`, { method: "POST", body: form });
const r = await res.json();
if (r.not_card) return null;
return { carta: r.card, certeza: r.confident, candidatas: r.candidates };
}
Python
import requests
SERVER = "https://cartinhas-recognize.fly.dev"
with open("carta.jpg", "rb") as f:
r = requests.post(f"{SERVER}/recognize", files={"file": ("carta.jpg", f, "image/jpeg")}, timeout=60).json()
if r["not_card"]:
print("Não é uma carta")
else:
c = r["card"]
print(c["name"], c["set_name"], c["number"], "(certeza)" if r["confident"] else "(confira)")
for alt in r["candidates"]:
print(" -", alt["api_id"], alt["cos"])
Saída com a foto do Altaria de Surging Sparks:
Altaria Surging Sparks 134 (certeza)
- sv08-134 0.916
- swsh3.5-49 0.732
- swsh12-143 0.733
- swsh7-106 0.716
- sv03-160 0.746
A primeira chamada com a máquina dormindo leva até ~20s. Use um timeout de pelo menos 40s, ou chame GET /health antes.
A foto da carta vai para o servidor só para ser identificada. Ela fica no máximo ~6 horas em disco para depuração e depois é apagada. O site não tem conta nem cookie. A lista de cartas fica só no seu aparelho.
A versão publicada coleta telemetria anônima para medir o acerto do scanner: um ID aleatório por aparelho (gerado no localStorage), o país (vindo da CDN), a origem do acesso, os eventos de scan (cartas candidatas, confiança, tempo, "não é essa") e os dados técnicos da câmera que o navegador informa (nome da lente, modo e distância de foco, resolução). Não vai foto, IP nem nada que identifique a pessoa. O código está em src/telemetry.ts. Ele só envia quando o build define VITE_TELEMETRY_URL, então um build seu não manda nada.