@aiuby-ia/cli 0.4.0 → 0.5.0
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 +33 -4
- 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/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
|
@@ -20628,17 +20628,20 @@ async function appsStatus(io, args) {
|
|
|
20628
20628
|
io.out(`Hardware: ${app.hardwareLabel ?? "-"}`);
|
|
20629
20629
|
io.out(`Regi\xE3o: ${app.region ?? "a definir"}`);
|
|
20630
20630
|
io.out("");
|
|
20631
|
-
io.out(plan.deployable ? `Plano: implant\xE1vel${plan.preview ? " (pr\xE9via do runtime
|
|
20631
|
+
io.out(plan.deployable ? `Plano: implant\xE1vel${plan.preview ? " (pr\xE9via do runtime, com prazo, sem cobran\xE7a)" : ""}.` : "Plano: n\xE3o implant\xE1vel hoje.");
|
|
20632
|
+
if (plan.paid) io.out(paidLine(plan.paid));
|
|
20632
20633
|
for (const r of plan.reasons) io.out(` - ${r.message}`);
|
|
20633
20634
|
});
|
|
20634
20635
|
}
|
|
20636
|
+
var brl = (microBrl) => (microBrl / 1e6).toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
|
|
20637
|
+
var paidLine = (paid) => `Tamanho ${paid.sizeName}${paid.priceMicroBrlPerHour !== null ? `: ${brl(paid.priceMicroBrlPerHour)} por hora em uso` : ""}.`;
|
|
20635
20638
|
var fetchDeployment = (io, stored, slug) => apiGet(stored, `/api/platform/compute/apps/${encodeURIComponent(slug)}/deployments`, io.fetcher);
|
|
20636
20639
|
var when = (iso) => new Date(iso).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" });
|
|
20637
20640
|
function printDeployment(io, d) {
|
|
20638
20641
|
io.out(`Implanta\xE7\xE3o: ${DEPLOYMENT_STATUS_LABEL[d.status].toLowerCase()}, revis\xE3o r${d.revision}, ${d.region}`);
|
|
20639
20642
|
if (d.url) io.out(`Endere\xE7o: ${d.url}`);
|
|
20640
20643
|
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)}`);
|
|
20644
|
+
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
20645
|
}
|
|
20643
20646
|
var DEPLOY_HINT = 'A chave precisa do escopo "Implantar no Compute".';
|
|
20644
20647
|
var WAIT_STEP_MS = 3e3;
|
|
@@ -20655,6 +20658,7 @@ async function deploy(io, args) {
|
|
|
20655
20658
|
const plan = await fetchPlan(io, stored, slug);
|
|
20656
20659
|
emit(io, plan, () => {
|
|
20657
20660
|
io.out(plan.deployable ? `Plano de ${slug}: implant\xE1vel.` : `Plano de ${slug}: n\xE3o implant\xE1vel hoje.`);
|
|
20661
|
+
if (plan.paid) io.out(paidLine(plan.paid));
|
|
20658
20662
|
for (const r of plan.reasons) io.out(` - ${r.message}`);
|
|
20659
20663
|
io.out("");
|
|
20660
20664
|
io.out("Simula\xE7\xE3o: nada foi criado nem cobrado.");
|
|
@@ -20692,6 +20696,27 @@ async function logs(io, args) {
|
|
|
20692
20696
|
for (const line of [...data.lines].reverse()) io.out(`${line.time.replace("T", " ").slice(0, 19)} ${line.severity.padEnd(7)} ${line.text}`);
|
|
20693
20697
|
});
|
|
20694
20698
|
}
|
|
20699
|
+
async function billing(io) {
|
|
20700
|
+
const data = await apiGet(requireLogin(), "/api/platform/compute/billing", io.fetcher);
|
|
20701
|
+
emit(io, data, () => {
|
|
20702
|
+
if (!data.enabled) io.out("A implanta\xE7\xE3o paga n\xE3o est\xE1 habilitada nesta organiza\xE7\xE3o.");
|
|
20703
|
+
io.out(`Saldo pr\xE9-pago: ${data.mode === "prepaid" && data.availableMicroBrl !== null ? brl(data.availableMicroBrl) : "-"}`);
|
|
20704
|
+
io.out(`Compute no m\xEAs: ${brl(data.monthSpendMicroBrl)}`);
|
|
20705
|
+
io.out(`Teto do workspace: ${data.budgetMicroBrl === null ? "n\xE3o definido (dono ou admin define na tela do Compute)" : brl(data.budgetMicroBrl)}`);
|
|
20706
|
+
io.out("");
|
|
20707
|
+
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)]));
|
|
20708
|
+
});
|
|
20709
|
+
}
|
|
20710
|
+
async function usage(io, args) {
|
|
20711
|
+
const flag = args.indexOf("--limit");
|
|
20712
|
+
const limit = flag >= 0 ? Number(args[flag + 1]) : 50;
|
|
20713
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new CliError("--limit precisa ser um n\xFAmero de 1 a 200.", EXIT.usage);
|
|
20714
|
+
const data = await apiGet(requireLogin(), `/api/platform/compute/usage?limit=${limit}`, io.fetcher);
|
|
20715
|
+
emit(io, data, () => {
|
|
20716
|
+
if (data.usage.length === 0) return io.out("Nenhum uso cobrado neste workspace.");
|
|
20717
|
+
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)]));
|
|
20718
|
+
});
|
|
20719
|
+
}
|
|
20695
20720
|
async function capacityList(io, args) {
|
|
20696
20721
|
const flag = args.indexOf("--provider");
|
|
20697
20722
|
const provider = flag >= 0 ? args[flag + 1] : void 0;
|
|
@@ -20811,7 +20836,7 @@ var notYet = (command) => async () => {
|
|
|
20811
20836
|
};
|
|
20812
20837
|
|
|
20813
20838
|
// src/main.ts
|
|
20814
|
-
var VERSION = "0.
|
|
20839
|
+
var VERSION = "0.5.0";
|
|
20815
20840
|
var HELP = `aiuby ${VERSION} \u2014 linha de comando do AIUBY Compute
|
|
20816
20841
|
|
|
20817
20842
|
Uso: aiuby <comando> [op\xE7\xF5es]
|
|
@@ -20829,9 +20854,11 @@ Dispon\xEDveis hoje:
|
|
|
20829
20854
|
capacity list [--provider gcp|aws] Cat\xE1logo de hardware (refer\xEAncia, n\xE3o verificado)
|
|
20830
20855
|
config validate [arquivo] Valida um aiuby.toml local
|
|
20831
20856
|
deploy --dry-run <identificador> Mostra o plano, sem criar nada
|
|
20832
|
-
deploy [--no-wait] <identificador> Implanta a revis\xE3o atual (
|
|
20857
|
+
deploy [--no-wait] <identificador> Implanta a revis\xE3o atual (o plano diz se a organiza\xE7\xE3o pode)
|
|
20833
20858
|
stop <identificador> Para a implanta\xE7\xE3o e apaga o servi\xE7o
|
|
20834
20859
|
logs [--limit N] <identificador> \xDAltimas linhas de log da implanta\xE7\xE3o mais recente
|
|
20860
|
+
billing Saldo, teto e gasto do m\xEAs de Compute, e os tamanhos com pre\xE7o
|
|
20861
|
+
usage [--limit N] Extrato do Compute no workspace
|
|
20835
20862
|
doctor Confere ambiente, credencial e API
|
|
20836
20863
|
|
|
20837
20864
|
Ainda n\xE3o dispon\xEDveis:
|
|
@@ -20860,6 +20887,8 @@ var ROUTES = {
|
|
|
20860
20887
|
deploy,
|
|
20861
20888
|
stop,
|
|
20862
20889
|
logs,
|
|
20890
|
+
billing,
|
|
20891
|
+
usage,
|
|
20863
20892
|
doctor
|
|
20864
20893
|
};
|
|
20865
20894
|
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")
|
|
@@ -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}
|