@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 +12 -7
- package/dist/aiuby.mjs +57 -6
- package/docs/api.md +16 -0
- package/docs/cli.md +6 -3
- 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
|
@@ -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.
|
|
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 é
|
|
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.
|
|
14
|
+
Precisa de Node.js 20 ou mais novo.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npm install -g
|
|
17
|
+
npm install -g @aiuby-ia/cli
|
|
18
18
|
aiuby --version
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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.
|
|
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 (
|
|
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,
|
|
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", "
|
|
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
|
+
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
|
|
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`, `
|
|
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
|
@@ -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}
|