@aiuby-ia/cli 0.3.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
@@ -2,24 +2,26 @@
2
2
 
3
3
  Linha de comando e templates de partida do **AIUBY Compute**.
4
4
 
5
- > **Estado: prévia (0.3.0).** A CLI autentica, copia templates, cadastra aplicações e
5
+ > **Estado: prévia (0.4.0).** A CLI autentica, copia templates, cadastra aplicações e
6
6
  > revisões a partir do `aiuby.toml`, lê o catálogo de hardware e mostra o plano de
7
7
  > implantação. **Implantar funciona só em organizações com a prévia do runtime habilitada
8
8
  > pela AIUBY**: CPU no Google Cloud, até 1 vCPU e 1 GB, 1 réplica, endereço público e prazo
9
- > (a aplicação para sozinha). Nas demais, o pedido é recusado com os motivos. Logs, escala,
10
- > volumes, segredos e GPU ainda não existem.
9
+ > (a aplicação para sozinha), com `aiuby logs` para ver a saída. Nas demais, o pedido é
10
+ > recusado com os motivos. Escala, volumes, segredos e GPU ainda não existem.
11
11
 
12
12
  ## Instalação
13
13
 
14
- Precisa de Node.js 20 ou mais novo. O pacote é distribuído pelo site da AIUBY:
14
+ Precisa de Node.js 20 ou mais novo.
15
15
 
16
16
  ```bash
17
- npm install -g https://aiuby.com/cli/aiuby-cli-latest.tgz
17
+ npm install -g @aiuby-ia/cli
18
18
  aiuby --version
19
19
  ```
20
20
 
21
- Para fixar uma versão, troque `latest` pelo número (por exemplo `aiuby-cli-0.3.0.tgz`).
22
- Para atualizar, rode o mesmo comando de novo. Para remover: `npm uninstall -g @aiuby-ia/cli`.
21
+ Sem acesso ao registro do npm, o mesmo pacote está no site da AIUBY:
22
+ `npm install -g https://aiuby.com/cli/aiuby-cli-latest.tgz`.
23
+
24
+ Para atualizar, rode o comando de instalação de novo. Para remover: `npm uninstall -g @aiuby-ia/cli`.
23
25
 
24
26
  ## Primeiros passos
25
27
 
@@ -51,6 +53,7 @@ aiuby apps create # cadastra; não implanta
51
53
  aiuby apps update # depois de mudar o aiuby.toml: nova revisão
52
54
  aiuby deploy --dry-run minha-api
53
55
  aiuby deploy minha-api # só com a prévia do runtime; pede o escopo "Implantar no Compute"
56
+ aiuby logs minha-api
54
57
  aiuby stop minha-api
55
58
  ```
56
59
 
@@ -70,6 +73,8 @@ Em automação, defina `AIUBY_TOKEN` no ambiente em vez de usar `login`.
70
73
  | [`templates/fastapi-http`](templates/fastapi-http) | API HTTP em Python com FastAPI, em CPU |
71
74
  | [`templates/vllm-openai`](templates/vllm-openai) | Inferência com vLLM e API compatível com OpenAI, em GPU |
72
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 |
73
78
 
74
79
  São pontos de partida de código. Ainda não foram executados num runtime da AIUBY.
75
80
 
package/dist/aiuby.mjs CHANGED
@@ -20482,6 +20482,9 @@ var APP_SLUG = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
20482
20482
  var MAX_REPLICAS = 100;
20483
20483
  var CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
20484
20484
  var noControl = (v) => !CONTROL_CHARS.test(v);
20485
+ var ENV_NAME = /^[A-Z_][A-Z0-9_]{0,63}$/;
20486
+ var RESERVED_ENV = /* @__PURE__ */ new Set(["PORT", "K_SERVICE", "K_REVISION", "K_CONFIGURATION"]);
20487
+ var MAX_ENV_VARS = 50;
20485
20488
  var IMAGE_REF = /^(?!.*\/\/)[A-Za-z0-9][A-Za-z0-9._\-/:@]*$/;
20486
20489
  var HardwareRequest = external_exports.object({
20487
20490
  /** `any`: o planejador escolhe entre os provedores habilitados. */
@@ -20497,6 +20500,11 @@ var AppConfig = external_exports.object({
20497
20500
  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),
20498
20501
  entrypoint: external_exports.string().min(1).max(300).refine(noControl, "entrypoint n\xE3o aceita caracteres de controle").nullable().default(null),
20499
20502
  port: external_exports.number().int().min(1).max(65535).default(8080),
20503
+ /**
20504
+ * Variáveis de ambiente da aplicação. Ficam na configuração, visíveis a quem vê a
20505
+ * aplicação: servem para ajuste, NÃO para segredo (senha, chave, token).
20506
+ */
20507
+ env: external_exports.record(external_exports.string().regex(ENV_NAME, "nome de vari\xE1vel: mai\xFAsculas, d\xEDgitos e sublinhado").refine((name) => !RESERVED_ENV.has(name), "nome reservado pela plataforma"), external_exports.string().max(2e3).refine(noControl, "valor n\xE3o aceita caracteres de controle")).refine((env) => Object.keys(env).length <= MAX_ENV_VARS, `no m\xE1ximo ${MAX_ENV_VARS} vari\xE1veis`).default({}),
20500
20508
  hardware: HardwareRequest,
20501
20509
  scaling: external_exports.object({
20502
20510
  minReplicas: external_exports.number().int().min(0).max(MAX_REPLICAS).default(0),
@@ -20620,17 +20628,20 @@ async function appsStatus(io, args) {
20620
20628
  io.out(`Hardware: ${app.hardwareLabel ?? "-"}`);
20621
20629
  io.out(`Regi\xE3o: ${app.region ?? "a definir"}`);
20622
20630
  io.out("");
20623
- 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));
20624
20633
  for (const r of plan.reasons) io.out(` - ${r.message}`);
20625
20634
  });
20626
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` : ""}.`;
20627
20638
  var fetchDeployment = (io, stored, slug) => apiGet(stored, `/api/platform/compute/apps/${encodeURIComponent(slug)}/deployments`, io.fetcher);
20628
20639
  var when = (iso) => new Date(iso).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" });
20629
20640
  function printDeployment(io, d) {
20630
20641
  io.out(`Implanta\xE7\xE3o: ${DEPLOYMENT_STATUS_LABEL[d.status].toLowerCase()}, revis\xE3o r${d.revision}, ${d.region}`);
20631
20642
  if (d.url) io.out(`Endere\xE7o: ${d.url}`);
20632
20643
  if (d.error) io.out(`Detalhe: ${d.error}`);
20633
- 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)}`);
20634
20645
  }
20635
20646
  var DEPLOY_HINT = 'A chave precisa do escopo "Implantar no Compute".';
20636
20647
  var WAIT_STEP_MS = 3e3;
@@ -20647,6 +20658,7 @@ async function deploy(io, args) {
20647
20658
  const plan = await fetchPlan(io, stored, slug);
20648
20659
  emit(io, plan, () => {
20649
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));
20650
20662
  for (const r of plan.reasons) io.out(` - ${r.message}`);
20651
20663
  io.out("");
20652
20664
  io.out("Simula\xE7\xE3o: nada foi criado nem cobrado.");
@@ -20672,6 +20684,39 @@ async function stop(io, args) {
20672
20684
  await apiPost(requireLogin(), `/api/platform/compute/apps/${encodeURIComponent(slug)}/deployments/stop`, void 0, io.fetcher).catch(scopeHint(DEPLOY_HINT));
20673
20685
  emit(io, { ok: true, slug }, () => io.out(`${slug} parada: o servi\xE7o foi apagado e o endere\xE7o n\xE3o responde mais.`));
20674
20686
  }
20687
+ async function logs(io, args) {
20688
+ const slug = args.find((a, i) => !a.startsWith("--") && args[i - 1] !== "--limit");
20689
+ if (!slug) throw new CliError("Uso: aiuby logs [--limit N] <identificador>", EXIT.usage);
20690
+ const flag = args.indexOf("--limit");
20691
+ const limit = flag >= 0 ? Number(args[flag + 1]) : 100;
20692
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new CliError("--limit precisa ser um n\xFAmero de 1 a 200.", EXIT.usage);
20693
+ const data = await apiGet(requireLogin(), `/api/platform/compute/apps/${encodeURIComponent(slug)}/logs?limit=${limit}`, io.fetcher);
20694
+ emit(io, data, () => {
20695
+ if (data.lines.length === 0) return io.out(`Nenhuma linha de log desde a implanta\xE7\xE3o (revis\xE3o r${data.deployment.revision}).`);
20696
+ for (const line of [...data.lines].reverse()) io.out(`${line.time.replace("T", " ").slice(0, 19)} ${line.severity.padEnd(7)} ${line.text}`);
20697
+ });
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
+ }
20675
20720
  async function capacityList(io, args) {
20676
20721
  const flag = args.indexOf("--provider");
20677
20722
  const provider = flag >= 0 ? args[flag + 1] : void 0;
@@ -20791,7 +20836,7 @@ var notYet = (command) => async () => {
20791
20836
  };
20792
20837
 
20793
20838
  // src/main.ts
20794
- var VERSION = "0.3.0";
20839
+ var VERSION = "0.5.0";
20795
20840
  var HELP = `aiuby ${VERSION} \u2014 linha de comando do AIUBY Compute
20796
20841
 
20797
20842
  Uso: aiuby <comando> [op\xE7\xF5es]
@@ -20809,12 +20854,15 @@ Dispon\xEDveis hoje:
20809
20854
  capacity list [--provider gcp|aws] Cat\xE1logo de hardware (refer\xEAncia, n\xE3o verificado)
20810
20855
  config validate [arquivo] Valida um aiuby.toml local
20811
20856
  deploy --dry-run <identificador> Mostra o plano, sem criar nada
20812
- 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)
20813
20858
  stop <identificador> Para a implanta\xE7\xE3o e apaga o servi\xE7o
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
20814
20862
  doctor Confere ambiente, credencial e API
20815
20863
 
20816
20864
  Ainda n\xE3o dispon\xEDveis:
20817
- run, logs, scale, rollback, volumes, secrets, domains
20865
+ run, scale, rollback, volumes, secrets, domains
20818
20866
 
20819
20867
  Op\xE7\xF5es gerais:
20820
20868
  --json Sa\xEDda em JSON
@@ -20823,7 +20871,7 @@ Op\xE7\xF5es gerais:
20823
20871
 
20824
20872
  Ambiente: AIUBY_TOKEN, AIUBY_API_URL, AIUBY_CONFIG_DIR
20825
20873
  C\xF3digos de sa\xEDda: 0 ok \xB7 1 erro \xB7 2 uso incorreto \xB7 3 n\xE3o autenticado \xB7 4 recusado \xB7 5 configura\xE7\xE3o inv\xE1lida`;
20826
- var PLANNED = ["run", "logs", "scale", "rollback", "volumes", "secrets", "domains"];
20874
+ var PLANNED = ["run", "scale", "rollback", "volumes", "secrets", "domains"];
20827
20875
  var ROUTES = {
20828
20876
  login,
20829
20877
  logout,
@@ -20838,6 +20886,9 @@ var ROUTES = {
20838
20886
  "config validate": configValidate,
20839
20887
  deploy,
20840
20888
  stop,
20889
+ logs,
20890
+ billing,
20891
+ usage,
20841
20892
  doctor
20842
20893
  };
20843
20894
  async function run(argv, io) {
package/docs/api.md CHANGED
@@ -74,10 +74,15 @@ Cadastra a aplicação com a primeira revisão. Nada é implantado. Responde `20
74
74
  ```json
75
75
  { "name": "Minha API", "slug": "minha-api",
76
76
  "config": { "runtime": "container", "image": "REGISTRO/minha-api:1", "port": 8080,
77
+ "env": { "LOG_LEVEL": "info" },
77
78
  "hardware": { "provider": "any", "cpu": 1, "memoryMb": 512 },
78
79
  "scaling": { "minReplicas": 0, "maxReplicas": 2 } } }
79
80
  ```
80
81
 
82
+ `config.env` são variáveis de ambiente (até 50; nomes em maiúsculas, dígitos e sublinhado;
83
+ `PORT` e os nomes `K_*` são da plataforma). Elas ficam visíveis a quem vê a aplicação: não
84
+ use para senha, chave ou token.
85
+
81
86
  Regras: `slug` com 3 a 40 caracteres (minúsculas, dígitos e hífen, começando por letra),
82
87
  único no workspace; `runtime` `container` exige `image`; campos desconhecidos são recusados.
83
88
  Limites: 50 aplicações por workspace, 200 revisões por aplicação.
@@ -102,6 +107,17 @@ Leitura. A implantação mais recente, já conferida com a conta de nuvem, ou `n
102
107
 
103
108
  `status`: `deploying`, `running`, `failed` ou `stopped`.
104
109
 
110
+ ### `GET /apps/{ref}/logs`
111
+
112
+ Leitura. Últimas linhas de log da implantação mais recente, da mais nova para a mais antiga
113
+ (`?limit=`, padrão 100, até 200). Vale também para implantação que falhou ou já parou. Só em
114
+ organização com a prévia do runtime; sem implantação, responde `409`. Limite: 20 leituras por minuto.
115
+
116
+ ```json
117
+ { "lines": [ { "time": "2026-11-12T10:05:00Z", "severity": "INFO", "text": "servidor no ar" } ],
118
+ "deployment": { "status": "running", "revision": 2 } }
119
+ ```
120
+
105
121
  ### `POST /apps/{ref}/deployments`
106
122
 
107
123
  Sessão de quem gerencia o workspace, ou chave com `compute:deploy`. Implanta a revisão atual.
package/docs/cli.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Referência da CLI `aiuby`
2
2
 
3
- Versão 0.3.0. A CLI fala só com a API da AIUBY: nunca pede nem usa credenciais da AWS ou
3
+ Versão 0.4.0. A CLI fala só com a API da AIUBY: nunca pede nem usa credenciais da AWS ou
4
4
  do Google Cloud.
5
5
 
6
- Instalação: `npm install -g https://aiuby.com/cli/aiuby-cli-latest.tgz` (Node.js 20 ou mais novo).
6
+ Instalação: `npm install -g @aiuby-ia/cli` (Node.js 20 ou mais novo).
7
7
 
8
8
  ## Autenticação
9
9
 
@@ -53,6 +53,9 @@ 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) |
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 |
56
59
 
57
60
  Limites da prévia: só CPU, até 1 vCPU e 1 GB, 1 réplica (escala a zero quando ociosa),
58
61
  endereço público sem autenticação, prazo de algumas horas. Sem cobrança nesta fase.
@@ -67,7 +70,7 @@ endereço público sem autenticação, prazo de algumas horas. Sem cobrança nes
67
70
 
68
71
  ## Ainda não disponíveis
69
72
 
70
- `run`, `logs`, `scale`, `rollback`, `volumes`, `secrets`
73
+ `run`, `scale`, `rollback`, `volumes`, `secrets`
71
74
  e `domains` respondem que ainda não estão disponíveis e saem com código 4. Nenhum deles chama a API.
72
75
 
73
76
  ## Opções gerais
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiuby-ia/cli",
3
- "version": "0.3.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