@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 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: CPU, 1 r\xE9plica, com prazo)" : ""}.` : "Plano: n\xE3o implant\xE1vel hoje.");
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.4.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 (s\xF3 em organiza\xE7\xE3o com a pr\xE9via do runtime)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiuby-ia/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Linha de comando do AIUBY Compute",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,6 @@
1
+ fastapi>=0.115
2
+ uvicorn>=0.30
3
+ diffusers>=0.31
4
+ transformers>=4.46
5
+ accelerate>=1.0
6
+ safetensors>=0.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}
@@ -0,0 +1,3 @@
1
+ fastapi>=0.115
2
+ uvicorn>=0.30
3
+ python-multipart>=0.0.9