@aiuby-ia/cli 0.4.0 → 0.5.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/README.md +2 -0
- package/dist/aiuby.mjs +39 -5
- package/docs/cli.md +2 -0
- package/package.json +1 -1
- package/templates/hf-stable-diffusion/Dockerfile +13 -0
- package/templates/hf-stable-diffusion/aiuby.toml +21 -0
- package/templates/hf-stable-diffusion/main.py +44 -0
- package/templates/hf-stable-diffusion/requirements.txt +6 -0
- package/templates/vllm-openai/Dockerfile +3 -2
- package/templates/vllm-openai/aiuby.toml +7 -2
- package/templates/yolo-detect/Dockerfile +10 -0
- package/templates/yolo-detect/aiuby.toml +21 -0
- package/templates/yolo-detect/main.py +36 -0
- package/templates/yolo-detect/requirements.txt +3 -0
package/README.md
CHANGED
|
@@ -73,6 +73,8 @@ Em automação, defina `AIUBY_TOKEN` no ambiente em vez de usar `login`.
|
|
|
73
73
|
| [`templates/fastapi-http`](templates/fastapi-http) | API HTTP em Python com FastAPI, em CPU |
|
|
74
74
|
| [`templates/vllm-openai`](templates/vllm-openai) | Inferência com vLLM e API compatível com OpenAI, em GPU |
|
|
75
75
|
| [`templates/langgraph-agent`](templates/langgraph-agent) | Agente com LangGraph atrás de uma API HTTP, em CPU |
|
|
76
|
+
| [`templates/hf-stable-diffusion`](templates/hf-stable-diffusion) | Geração de imagem com um modelo de difusão do Hugging Face, em GPU |
|
|
77
|
+
| [`templates/yolo-detect`](templates/yolo-detect) | Detecção de objetos com YOLO (Ultralytics, AGPL-3.0), em GPU |
|
|
76
78
|
|
|
77
79
|
São pontos de partida de código. Ainda não foram executados num runtime da AIUBY.
|
|
78
80
|
|
package/dist/aiuby.mjs
CHANGED
|
@@ -20485,6 +20485,7 @@ var noControl = (v) => !CONTROL_CHARS.test(v);
|
|
|
20485
20485
|
var ENV_NAME = /^[A-Z_][A-Z0-9_]{0,63}$/;
|
|
20486
20486
|
var RESERVED_ENV = /* @__PURE__ */ new Set(["PORT", "K_SERVICE", "K_REVISION", "K_CONFIGURATION"]);
|
|
20487
20487
|
var MAX_ENV_VARS = 50;
|
|
20488
|
+
var MAX_ARGS = 30;
|
|
20488
20489
|
var IMAGE_REF = /^(?!.*\/\/)[A-Za-z0-9][A-Za-z0-9._\-/:@]*$/;
|
|
20489
20490
|
var HardwareRequest = external_exports.object({
|
|
20490
20491
|
/** `any`: o planejador escolhe entre os provedores habilitados. */
|
|
@@ -20499,6 +20500,8 @@ var AppConfig = external_exports.object({
|
|
|
20499
20500
|
runtime: external_exports.enum(["container", "python"]),
|
|
20500
20501
|
image: external_exports.string().min(1).max(300).regex(IMAGE_REF, "image precisa ser uma refer\xEAncia de imagem de container, sem espa\xE7os").nullable().default(null),
|
|
20501
20502
|
entrypoint: external_exports.string().min(1).max(300).refine(noControl, "entrypoint n\xE3o aceita caracteres de controle").nullable().default(null),
|
|
20503
|
+
/** Argumentos do processo da imagem (substituem o `CMD`). Vazio: vale o que a imagem define. */
|
|
20504
|
+
args: external_exports.array(external_exports.string().min(1).max(300).refine(noControl, "args n\xE3o aceita caracteres de controle")).max(MAX_ARGS, `no m\xE1ximo ${MAX_ARGS} argumentos`).default([]),
|
|
20502
20505
|
port: external_exports.number().int().min(1).max(65535).default(8080),
|
|
20503
20506
|
/**
|
|
20504
20507
|
* Variáveis de ambiente da aplicação. Ficam na configuração, visíveis a quem vê a
|
|
@@ -20628,17 +20631,20 @@ async function appsStatus(io, args) {
|
|
|
20628
20631
|
io.out(`Hardware: ${app.hardwareLabel ?? "-"}`);
|
|
20629
20632
|
io.out(`Regi\xE3o: ${app.region ?? "a definir"}`);
|
|
20630
20633
|
io.out("");
|
|
20631
|
-
io.out(plan.deployable ? `Plano: implant\xE1vel${plan.preview ? " (pr\xE9via do runtime
|
|
20634
|
+
io.out(plan.deployable ? `Plano: implant\xE1vel${plan.preview ? " (pr\xE9via do runtime, com prazo, sem cobran\xE7a)" : ""}.` : "Plano: n\xE3o implant\xE1vel hoje.");
|
|
20635
|
+
if (plan.paid) io.out(paidLine(plan.paid));
|
|
20632
20636
|
for (const r of plan.reasons) io.out(` - ${r.message}`);
|
|
20633
20637
|
});
|
|
20634
20638
|
}
|
|
20639
|
+
var brl = (microBrl) => (microBrl / 1e6).toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
|
|
20640
|
+
var paidLine = (paid) => `Tamanho ${paid.sizeName}${paid.priceMicroBrlPerHour !== null ? `: ${brl(paid.priceMicroBrlPerHour)} por hora em uso` : ""}.`;
|
|
20635
20641
|
var fetchDeployment = (io, stored, slug) => apiGet(stored, `/api/platform/compute/apps/${encodeURIComponent(slug)}/deployments`, io.fetcher);
|
|
20636
20642
|
var when = (iso) => new Date(iso).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" });
|
|
20637
20643
|
function printDeployment(io, d) {
|
|
20638
20644
|
io.out(`Implanta\xE7\xE3o: ${DEPLOYMENT_STATUS_LABEL[d.status].toLowerCase()}, revis\xE3o r${d.revision}, ${d.region}`);
|
|
20639
20645
|
if (d.url) io.out(`Endere\xE7o: ${d.url}`);
|
|
20640
20646
|
if (d.error) io.out(`Detalhe: ${d.error}`);
|
|
20641
|
-
if (d.status === "running" || d.status === "deploying") io.out(`Prazo: para sozinha em ${when(d.expiresAt)}`);
|
|
20647
|
+
if (d.status === "running" || d.status === "deploying") io.out(d.billing === "paid" ? "Cobran\xE7a: do saldo pr\xE9-pago, pelo tempo em uso. Fica no ar at\xE9 parar, o saldo acabar ou o teto do workspace ser atingido." : `Prazo: para sozinha em ${when(d.expiresAt)}`);
|
|
20642
20648
|
}
|
|
20643
20649
|
var DEPLOY_HINT = 'A chave precisa do escopo "Implantar no Compute".';
|
|
20644
20650
|
var WAIT_STEP_MS = 3e3;
|
|
@@ -20655,6 +20661,7 @@ async function deploy(io, args) {
|
|
|
20655
20661
|
const plan = await fetchPlan(io, stored, slug);
|
|
20656
20662
|
emit(io, plan, () => {
|
|
20657
20663
|
io.out(plan.deployable ? `Plano de ${slug}: implant\xE1vel.` : `Plano de ${slug}: n\xE3o implant\xE1vel hoje.`);
|
|
20664
|
+
if (plan.paid) io.out(paidLine(plan.paid));
|
|
20658
20665
|
for (const r of plan.reasons) io.out(` - ${r.message}`);
|
|
20659
20666
|
io.out("");
|
|
20660
20667
|
io.out("Simula\xE7\xE3o: nada foi criado nem cobrado.");
|
|
@@ -20692,6 +20699,27 @@ async function logs(io, args) {
|
|
|
20692
20699
|
for (const line of [...data.lines].reverse()) io.out(`${line.time.replace("T", " ").slice(0, 19)} ${line.severity.padEnd(7)} ${line.text}`);
|
|
20693
20700
|
});
|
|
20694
20701
|
}
|
|
20702
|
+
async function billing(io) {
|
|
20703
|
+
const data = await apiGet(requireLogin(), "/api/platform/compute/billing", io.fetcher);
|
|
20704
|
+
emit(io, data, () => {
|
|
20705
|
+
if (!data.enabled) io.out("A implanta\xE7\xE3o paga n\xE3o est\xE1 habilitada nesta organiza\xE7\xE3o.");
|
|
20706
|
+
io.out(`Saldo pr\xE9-pago: ${data.mode === "prepaid" && data.availableMicroBrl !== null ? brl(data.availableMicroBrl) : "-"}`);
|
|
20707
|
+
io.out(`Compute no m\xEAs: ${brl(data.monthSpendMicroBrl)}`);
|
|
20708
|
+
io.out(`Teto do workspace: ${data.budgetMicroBrl === null ? "n\xE3o definido (dono ou admin define na tela do Compute)" : brl(data.budgetMicroBrl)}`);
|
|
20709
|
+
io.out("");
|
|
20710
|
+
table(io, ["TAMANHO", "VCPU", "MEM\xD3RIA", "PRE\xC7O POR HORA EM USO"], data.sizes.map((s) => [s.name, String(s.cpu), `${s.memoryMb} MB`, s.priceMicroBrlPerHour === null ? "-" : brl(s.priceMicroBrlPerHour)]));
|
|
20711
|
+
});
|
|
20712
|
+
}
|
|
20713
|
+
async function usage(io, args) {
|
|
20714
|
+
const flag = args.indexOf("--limit");
|
|
20715
|
+
const limit = flag >= 0 ? Number(args[flag + 1]) : 50;
|
|
20716
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new CliError("--limit precisa ser um n\xFAmero de 1 a 200.", EXIT.usage);
|
|
20717
|
+
const data = await apiGet(requireLogin(), `/api/platform/compute/usage?limit=${limit}`, io.fetcher);
|
|
20718
|
+
emit(io, data, () => {
|
|
20719
|
+
if (data.usage.length === 0) return io.out("Nenhum uso cobrado neste workspace.");
|
|
20720
|
+
table(io, ["APLICA\xC7\xC3O", "AT\xC9", "EM USO", "SA\xCDDA", "VALOR"], data.usage.map((u) => [u.appSlug, when(u.periodEnd), `${Math.round(u.billableSeconds / 60)} min`, `${(u.egressBytes / 1024 ** 2).toFixed(1)} MB`, brl(u.billedMicroBrl)]));
|
|
20721
|
+
});
|
|
20722
|
+
}
|
|
20695
20723
|
async function capacityList(io, args) {
|
|
20696
20724
|
const flag = args.indexOf("--provider");
|
|
20697
20725
|
const provider = flag >= 0 ? args[flag + 1] : void 0;
|
|
@@ -20746,7 +20774,9 @@ async function appsUpdate(io, args) {
|
|
|
20746
20774
|
const app = readAppFile(io, file2);
|
|
20747
20775
|
const stored = requireLogin();
|
|
20748
20776
|
const current = await apiGet(stored, `/api/platform/compute/apps/${encodeURIComponent(app.slug)}`, io.fetcher);
|
|
20749
|
-
const
|
|
20777
|
+
const stored_ = current.revisions.find((r) => r.revision === current.currentRevision)?.config;
|
|
20778
|
+
const normalized = stored_ ? AppConfig.safeParse(stored_) : null;
|
|
20779
|
+
const latest = normalized?.success ? normalized.data : stored_;
|
|
20750
20780
|
if (latest && JSON.stringify(sortKeys(latest)) === JSON.stringify(sortKeys(app.config))) {
|
|
20751
20781
|
emit(io, { id: current.id, slug: current.slug, revision: current.currentRevision, changed: false }, () => io.out(`Sem mudan\xE7a: ${current.slug} j\xE1 est\xE1 com essa configura\xE7\xE3o (r${current.currentRevision}).`));
|
|
20752
20782
|
return;
|
|
@@ -20811,7 +20841,7 @@ var notYet = (command) => async () => {
|
|
|
20811
20841
|
};
|
|
20812
20842
|
|
|
20813
20843
|
// src/main.ts
|
|
20814
|
-
var VERSION = "0.
|
|
20844
|
+
var VERSION = "0.5.1";
|
|
20815
20845
|
var HELP = `aiuby ${VERSION} \u2014 linha de comando do AIUBY Compute
|
|
20816
20846
|
|
|
20817
20847
|
Uso: aiuby <comando> [op\xE7\xF5es]
|
|
@@ -20829,9 +20859,11 @@ Dispon\xEDveis hoje:
|
|
|
20829
20859
|
capacity list [--provider gcp|aws] Cat\xE1logo de hardware (refer\xEAncia, n\xE3o verificado)
|
|
20830
20860
|
config validate [arquivo] Valida um aiuby.toml local
|
|
20831
20861
|
deploy --dry-run <identificador> Mostra o plano, sem criar nada
|
|
20832
|
-
deploy [--no-wait] <identificador> Implanta a revis\xE3o atual (
|
|
20862
|
+
deploy [--no-wait] <identificador> Implanta a revis\xE3o atual (o plano diz se a organiza\xE7\xE3o pode)
|
|
20833
20863
|
stop <identificador> Para a implanta\xE7\xE3o e apaga o servi\xE7o
|
|
20834
20864
|
logs [--limit N] <identificador> \xDAltimas linhas de log da implanta\xE7\xE3o mais recente
|
|
20865
|
+
billing Saldo, teto e gasto do m\xEAs de Compute, e os tamanhos com pre\xE7o
|
|
20866
|
+
usage [--limit N] Extrato do Compute no workspace
|
|
20835
20867
|
doctor Confere ambiente, credencial e API
|
|
20836
20868
|
|
|
20837
20869
|
Ainda n\xE3o dispon\xEDveis:
|
|
@@ -20860,6 +20892,8 @@ var ROUTES = {
|
|
|
20860
20892
|
deploy,
|
|
20861
20893
|
stop,
|
|
20862
20894
|
logs,
|
|
20895
|
+
billing,
|
|
20896
|
+
usage,
|
|
20863
20897
|
doctor
|
|
20864
20898
|
};
|
|
20865
20899
|
async function run(argv, io) {
|
package/docs/cli.md
CHANGED
|
@@ -53,6 +53,8 @@ runtime habilitada pela AIUBY. Fora dela, a API recusa com os motivos (código d
|
|
|
53
53
|
| --- | --- |
|
|
54
54
|
| `aiuby deploy [--no-wait] <identificador>` | Implanta a revisão atual no Google Cloud e acompanha até ficar em execução (até 3 minutos). Mostra o endereço público e o prazo. Reimplantar troca a revisão no mesmo endereço |
|
|
55
55
|
| `aiuby stop <identificador>` | Para a implantação: o serviço é apagado e o endereço deixa de responder |
|
|
56
|
+
| `aiuby billing` | Saldo pré-pago, teto mensal e gasto do mês de Compute no workspace, e os tamanhos com o preço por hora em uso |
|
|
57
|
+
| `aiuby usage [--limit N]` | Extrato do Compute: períodos medidos e cobrados (padrão 50, até 200) |
|
|
56
58
|
| `aiuby logs [--limit N] <identificador>` | Últimas linhas de log da implantação mais recente (padrão 100, até 200). Vale também para uma que falhou ou já parou |
|
|
57
59
|
|
|
58
60
|
Limites da prévia: só CPU, até 1 vCPU e 1 GB, 1 réplica (escala a zero quando ociosa),
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Geração de imagem com um modelo de difusão do Hugging Face. O modelo é baixado na primeira
|
|
2
|
+
# subida: escolha um cuja licença permita o seu uso e que caiba na memória da GPU pedida.
|
|
3
|
+
FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY requirements.txt .
|
|
6
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
7
|
+
COPY main.py .
|
|
8
|
+
# Usuário sem privilégio: o runtime não deve precisar de root. O cache do modelo fica em /tmp.
|
|
9
|
+
RUN useradd --create-home app
|
|
10
|
+
USER app
|
|
11
|
+
ENV MODEL_ID="stable-diffusion-v1-5/stable-diffusion-v1-5" HF_HOME="/tmp/hf"
|
|
12
|
+
EXPOSE 8080
|
|
13
|
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Geração de imagem em GPU. Valide com: aiuby config validate
|
|
2
|
+
name = "Geração de imagem"
|
|
3
|
+
slug = "geracao-de-imagem"
|
|
4
|
+
|
|
5
|
+
[config]
|
|
6
|
+
runtime = "container"
|
|
7
|
+
image = "REGISTRO/geracao-de-imagem:1"
|
|
8
|
+
port = 8080
|
|
9
|
+
|
|
10
|
+
[config.hardware]
|
|
11
|
+
provider = "gcp"
|
|
12
|
+
cpu = 4
|
|
13
|
+
memoryMb = 16384
|
|
14
|
+
|
|
15
|
+
[config.hardware.gpu]
|
|
16
|
+
sku = "gcp-cloudrun-l4"
|
|
17
|
+
count = 1
|
|
18
|
+
|
|
19
|
+
[config.scaling]
|
|
20
|
+
minReplicas = 0
|
|
21
|
+
maxReplicas = 1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Geração de imagem com um modelo de difusão do Hugging Face, exposta por HTTP.
|
|
2
|
+
O modelo é carregado uma vez, na subida; a primeira subida também faz o download."""
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from diffusers import AutoPipelineForText2Image
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi.responses import Response
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
MODEL_ID = os.environ.get("MODEL_ID", "stable-diffusion-v1-5/stable-diffusion-v1-5")
|
|
14
|
+
estado: dict = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@asynccontextmanager
|
|
18
|
+
async def ciclo(_: FastAPI):
|
|
19
|
+
gpu = torch.cuda.is_available()
|
|
20
|
+
pipe = AutoPipelineForText2Image.from_pretrained(MODEL_ID, torch_dtype=torch.float16 if gpu else torch.float32)
|
|
21
|
+
estado["pipe"] = pipe.to("cuda" if gpu else "cpu")
|
|
22
|
+
yield
|
|
23
|
+
estado.clear()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
app = FastAPI(title="geracao-de-imagem", lifespan=ciclo)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Pedido(BaseModel):
|
|
30
|
+
prompt: str = Field(min_length=1, max_length=500)
|
|
31
|
+
passos: int = Field(default=25, ge=1, le=50)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.get("/health")
|
|
35
|
+
def health() -> dict[str, str]:
|
|
36
|
+
return {"status": "ok", "modelo": MODEL_ID}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.post("/gerar")
|
|
40
|
+
def gerar(pedido: Pedido) -> Response:
|
|
41
|
+
imagem = estado["pipe"](pedido.prompt, num_inference_steps=pedido.passos).images[0]
|
|
42
|
+
saida = io.BytesIO()
|
|
43
|
+
imagem.save(saida, format="PNG")
|
|
44
|
+
return Response(content=saida.getvalue(), media_type="image/png")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Opcional: o aiuby.toml já usa a imagem oficial do vLLM com o modelo nos argumentos. Use este
|
|
2
|
+
# Dockerfile só se quiser uma imagem própria (por exemplo, com o modelo fixo). O modelo é baixado na subida:
|
|
2
3
|
# escolha um que caiba na memória da GPU pedida e cuja licença permita o seu uso.
|
|
3
4
|
FROM vllm/vllm-openai:latest
|
|
4
|
-
ENV MODEL_ID="
|
|
5
|
+
ENV MODEL_ID="microsoft/Phi-3-mini-4k-instruct" HF_HOME="/tmp/hf"
|
|
5
6
|
EXPOSE 8080
|
|
6
7
|
ENTRYPOINT ["/bin/sh", "-c", "python3 -m vllm.entrypoints.openai.api_server --model \"$MODEL_ID\" --host 0.0.0.0 --port 8080"]
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
# Inferência compatível com OpenAI em GPU
|
|
1
|
+
# Inferência compatível com OpenAI em GPU, com a imagem oficial do vLLM e o Phi-3 mini.
|
|
2
|
+
# Não precisa construir imagem: os argumentos escolhem o modelo. Valide com: aiuby config validate
|
|
2
3
|
name = "Inferência vLLM"
|
|
3
4
|
slug = "inferencia-vllm"
|
|
4
5
|
|
|
5
6
|
[config]
|
|
6
7
|
runtime = "container"
|
|
7
|
-
image = "
|
|
8
|
+
image = "docker.io/vllm/vllm-openai:latest"
|
|
9
|
+
args = ["--model", "microsoft/Phi-3-mini-4k-instruct", "--host", "0.0.0.0", "--port", "8080", "--max-model-len", "4096"]
|
|
8
10
|
port = 8080
|
|
9
11
|
|
|
12
|
+
[config.env]
|
|
13
|
+
HF_HOME = "/tmp/hf"
|
|
14
|
+
|
|
10
15
|
[config.hardware]
|
|
11
16
|
provider = "gcp"
|
|
12
17
|
cpu = 4
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Detecção de objetos com um modelo YOLO da Ultralytics. Os pesos são baixados na primeira
|
|
2
|
+
# subida. Atenção à licença: a Ultralytics é AGPL-3.0; uso comercial fechado exige licença deles.
|
|
3
|
+
FROM ultralytics/ultralytics:latest
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY requirements.txt .
|
|
6
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
7
|
+
COPY main.py .
|
|
8
|
+
ENV MODEL_ID="yolov10x.pt" YOLO_CONFIG_DIR="/tmp/ultralytics"
|
|
9
|
+
EXPOSE 8080
|
|
10
|
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Detecção de objetos em GPU. Valide com: aiuby config validate
|
|
2
|
+
name = "Detecção de objetos"
|
|
3
|
+
slug = "deteccao-de-objetos"
|
|
4
|
+
|
|
5
|
+
[config]
|
|
6
|
+
runtime = "container"
|
|
7
|
+
image = "REGISTRO/deteccao-de-objetos:1"
|
|
8
|
+
port = 8080
|
|
9
|
+
|
|
10
|
+
[config.hardware]
|
|
11
|
+
provider = "gcp"
|
|
12
|
+
cpu = 4
|
|
13
|
+
memoryMb = 16384
|
|
14
|
+
|
|
15
|
+
[config.hardware.gpu]
|
|
16
|
+
sku = "gcp-cloudrun-l4"
|
|
17
|
+
count = 1
|
|
18
|
+
|
|
19
|
+
[config.scaling]
|
|
20
|
+
minReplicas = 0
|
|
21
|
+
maxReplicas = 1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Detecção de objetos com YOLO (Ultralytics) exposta por HTTP: recebe uma imagem e devolve
|
|
2
|
+
os objetos encontrados, com caixa e confiança."""
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, File, HTTPException, UploadFile
|
|
7
|
+
from PIL import Image
|
|
8
|
+
from ultralytics import YOLO
|
|
9
|
+
|
|
10
|
+
MODEL_ID = os.environ.get("MODEL_ID", "yolov10x.pt")
|
|
11
|
+
LIMITE_BYTES = 10 * 1024 * 1024
|
|
12
|
+
modelo = YOLO(MODEL_ID)
|
|
13
|
+
|
|
14
|
+
app = FastAPI(title="deteccao-de-objetos")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.get("/health")
|
|
18
|
+
def health() -> dict[str, str]:
|
|
19
|
+
return {"status": "ok", "modelo": MODEL_ID}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.post("/detectar")
|
|
23
|
+
async def detectar(arquivo: UploadFile = File(...)) -> dict:
|
|
24
|
+
dados = await arquivo.read(LIMITE_BYTES + 1)
|
|
25
|
+
if len(dados) > LIMITE_BYTES:
|
|
26
|
+
raise HTTPException(status_code=413, detail="Imagem acima de 10 MB.")
|
|
27
|
+
try:
|
|
28
|
+
imagem = Image.open(io.BytesIO(dados)).convert("RGB")
|
|
29
|
+
except Exception as erro: # arquivo que não é imagem
|
|
30
|
+
raise HTTPException(status_code=400, detail="O arquivo enviado não é uma imagem legível.") from erro
|
|
31
|
+
resultado = modelo(imagem, verbose=False)[0]
|
|
32
|
+
objetos = [
|
|
33
|
+
{"classe": resultado.names[int(caixa.cls)], "confianca": round(float(caixa.conf), 4), "caixa": [round(v, 1) for v in caixa.xyxy[0].tolist()]}
|
|
34
|
+
for caixa in resultado.boxes
|
|
35
|
+
]
|
|
36
|
+
return {"objetos": objetos}
|