primocode 8.42.2 → 9.0.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.
Files changed (102) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +31 -192
  7. package/lib/catalogo.js +60 -60
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +545 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +606 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +13 -1
  23. package/lib/marca.js +4 -1
  24. package/lib/marketplace.js +189 -0
  25. package/lib/permissao.js +137 -0
  26. package/lib/registro.js +229 -0
  27. package/lib/skills.js +329 -0
  28. package/lib/tools.js +51 -181
  29. package/lib/ui.js +7 -3
  30. package/lib/voz.js +239 -0
  31. package/package.json +12 -3
  32. package/skills/acessibilidade/SKILL.md +28 -0
  33. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  34. package/skills/depurar-de-verdade/SKILL.md +29 -0
  35. package/skills/editar-meu-video/SKILL.md +59 -0
  36. package/skills/escrever-em-portugues/SKILL.md +28 -0
  37. package/skills/git-limpo/SKILL.md +28 -0
  38. package/skills/motion-de-primeira/SKILL.md +62 -0
  39. package/skills/pagina-que-converte/SKILL.md +28 -0
  40. package/skills/performance-web/SKILL.md +28 -0
  41. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  42. package/skills/slides-que-prendem/SKILL.md +40 -0
  43. package/studio/edge.py +16 -66
  44. package/studio/narrar.py +274 -0
  45. package/studio/pessoa.py +167 -0
  46. package/voz/primo_voz/__init__.py +0 -0
  47. package/voz/primo_voz/__main__.py +325 -0
  48. package/voz/primo_voz/escuta.py +235 -0
  49. package/voz/primo_voz/orbe.py +216 -0
  50. package/lib/audio-livre.js +0 -177
  51. package/lib/chaves.js +0 -149
  52. package/lib/claude-engine.js +0 -631
  53. package/lib/codex-engine.js +0 -208
  54. package/lib/remotion.js +0 -663
  55. package/lib/studio.js +0 -960
  56. package/studio/PRIMOCODE.md +0 -58
  57. package/studio/README.md +0 -154
  58. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  59. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  60. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  61. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  62. package/studio/ferramentas/00-protocolo.md +0 -125
  63. package/studio/ferramentas/01-modelos.md +0 -270
  64. package/studio/ferramentas/README.md +0 -32
  65. package/studio/ferramentas/corte.md +0 -919
  66. package/studio/ferramentas/grade.md +0 -92
  67. package/studio/ferramentas/prisma.md +0 -107
  68. package/studio/ferramentas/prosa.md +0 -86
  69. package/studio/ferramentas/tela.md +0 -93
  70. package/studio/ferramentas/traco.md +0 -79
  71. package/studio/server.py +0 -2254
  72. package/studio/studio.log +0 -57
  73. package/studio/voz.py +0 -388
  74. package/studio/web/analise.html +0 -52
  75. package/studio/web/core.css +0 -1167
  76. package/studio/web/fontes/bebas.woff2 +0 -0
  77. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  78. package/studio/web/fontes/inter-400.woff2 +0 -0
  79. package/studio/web/fontes/inter-700.woff2 +0 -0
  80. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  81. package/studio/web/fontes/playfair-700.woff2 +0 -0
  82. package/studio/web/fontes.css +0 -23
  83. package/studio/web/home.html +0 -217
  84. package/studio/web/js/analise.js +0 -510
  85. package/studio/web/js/audio.js +0 -607
  86. package/studio/web/js/editor.js +0 -1802
  87. package/studio/web/js/exportar.js +0 -222
  88. package/studio/web/js/ferramentas.js +0 -108
  89. package/studio/web/js/icones.js +0 -120
  90. package/studio/web/js/midia.js +0 -157
  91. package/studio/web/js/narracao.js +0 -325
  92. package/studio/web/js/player.js +0 -903
  93. package/studio/web/js/render-grade.js +0 -243
  94. package/studio/web/js/render-prosa.js +0 -104
  95. package/studio/web/js/render.js +0 -1852
  96. package/studio/web/js/sfx.js +0 -233
  97. package/studio/web/js/video.js +0 -464
  98. package/studio/web/js/voz-neural.js +0 -152
  99. package/studio/web/render.css +0 -1352
  100. package/studio/web/tf.html +0 -15
  101. package/studio/web/tool.html +0 -78
  102. package/studio/web/view.html +0 -27
@@ -0,0 +1,343 @@
1
+ /**
2
+ * assets.js — de onde vêm os ícones, as imagens, o som e as fontes.
3
+ *
4
+ * "Ícones: trocar a fonte de ícones atual, que está genérica demais.
5
+ * Integrar uma biblioteca de ícones boa e autêntica. Imagens: buscar
6
+ * imagens e PNGs na internet, remover fundo quando necessário. Trilha e
7
+ * efeitos sonoros buscados na internet, não apenas os que já existem."
8
+ *
9
+ * ── O QUE HAVIA ──────────────────────────────────────────────────────────
10
+ * Um `icones.js` de 120 linhas com uns poucos desenhos feitos à mão, e nada
11
+ * mais: sem imagem, sem som, sem fonte além das seis embutidas no pacote. O
12
+ * resultado é o que foi descrito — genérico, porque o repertório inteiro
13
+ * cabia num arquivo de 120 linhas.
14
+ *
15
+ * ── POR QUE ESTAS FONTES, E NÃO PEXELS/FREESOUND ─────────────────────────
16
+ * As três usadas aqui funcionam SEM CHAVE NENHUMA, e isso não é detalhe: o
17
+ * produto promete que a pessoa instala e usa. Um estúdio que só busca imagem
18
+ * depois de a pessoa criar conta em dois serviços é um estúdio que não busca
19
+ * imagem.
20
+ *
21
+ * · Iconify — mais de 200 mil ícones de 150 coleções (Lucide, Phosphor,
22
+ * Material, Simple Icons com as marcas). Um SVG por requisição, sem
23
+ * cadastro. É o fim do pacote genérico.
24
+ * · Openverse — imagem e áudio com licença aberta, de Flickr, Wikimedia,
25
+ * Freesound e outros, com a atribuição junto. Sem cadastro.
26
+ * · Google Fonts — o leque de fontes, sem cadastro.
27
+ *
28
+ * Pexels e Freesound entram por cima QUANDO houver chave (PEXELS_API_KEY,
29
+ * FREESOUND_API_KEY): a qualidade de foto do Pexels é melhor. Sem a chave, o
30
+ * Openverse atende — em vez de o estúdio não atender.
31
+ *
32
+ * ── A LICENÇA VAI JUNTO, SEMPRE ──────────────────────────────────────────
33
+ * Toda mídia baixada guarda um `.licenca.json` ao lado, com autor, licença e
34
+ * de onde veio. Não é burocracia: material aberto quase sempre exige crédito,
35
+ * e descobrir isso depois de publicar o vídeo é tarde. Com o arquivo ao lado,
36
+ * o agente consegue montar os créditos sem adivinhar.
37
+ *
38
+ * ── NADA DISSO ENTRA NO RENDER PELA REDE ─────────────────────────────────
39
+ * Tudo é BAIXADO para a pasta do projeto antes. Render que busca imagem na
40
+ * internet falha no meio, depois de vinte minutos de trabalho — e falha de
41
+ * novo amanhã, quando o link morrer.
42
+ */
43
+
44
+ 'use strict';
45
+
46
+ const fs = require('fs');
47
+ const path = require('path');
48
+ const https = require('https');
49
+ const { URL } = require('url');
50
+
51
+ const PRAZO = 20000;
52
+
53
+ function pegar(url, { json = false, binario = false, cabecalhos = {} } = {}) {
54
+ return new Promise((resolve, reject) => {
55
+ const u = new URL(url);
56
+ const req = https.get({
57
+ hostname: u.hostname,
58
+ path: u.pathname + u.search,
59
+ headers: {
60
+ // O Openverse recusa quem não se identifica, e com razão.
61
+ 'User-Agent': 'PrimoCode-Studio/1.0 (+https://github.com/ConectaPrimoAI/primocode)',
62
+ Accept: json ? 'application/json' : '*/*',
63
+ ...cabecalhos,
64
+ },
65
+ }, (res) => {
66
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
67
+ res.resume();
68
+ return resolve(pegar(new URL(res.headers.location, url).toString(), { json, binario, cabecalhos }));
69
+ }
70
+ if (res.statusCode >= 400) {
71
+ res.resume();
72
+ return reject(new Error(`HTTP ${res.statusCode} em ${u.hostname}`));
73
+ }
74
+ const pedacos = [];
75
+ res.on('data', (d) => pedacos.push(d));
76
+ res.on('end', () => {
77
+ const corpo = Buffer.concat(pedacos);
78
+ if (binario) return resolve(corpo);
79
+ const texto = corpo.toString('utf8');
80
+ if (!json) return resolve(texto);
81
+ try { resolve(JSON.parse(texto)); }
82
+ catch { reject(new Error('resposta não era JSON')); }
83
+ });
84
+ });
85
+ const relogio = setTimeout(() => req.destroy(new Error('a busca demorou demais')), PRAZO);
86
+ if (relogio.unref) relogio.unref();
87
+ req.on('close', () => clearTimeout(relogio));
88
+ req.on('error', reject);
89
+ });
90
+ }
91
+
92
+ function guardar(arquivo, dados, licenca) {
93
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
94
+ fs.writeFileSync(arquivo, dados);
95
+ if (licenca) {
96
+ fs.writeFileSync(arquivo + '.licenca.json', JSON.stringify(licenca, null, 2), 'utf8');
97
+ }
98
+ return arquivo;
99
+ }
100
+
101
+ // ── ícones ───────────────────────────────────────────────────────────────
102
+
103
+ /* As coleções que valem a pena, na ordem em que costumam servir. Não é uma
104
+ lista fechada: qualquer coleção do Iconify funciona, isto é só o que o
105
+ agente vê quando não sabe o que pedir. */
106
+ const COLECOES = [
107
+ ['lucide', 'traço limpo e moderno, o padrão para interface'],
108
+ ['phosphor', 'traço com peso ajustável, boa para destaque'],
109
+ ['solar', 'cheios e arredondados, bons em tamanho grande'],
110
+ ['tabler', 'traço fino e técnico'],
111
+ ['material-symbols', 'os do Google, para produto Android/web'],
112
+ ['simple-icons', 'as MARCAS — pix, whatsapp, spotify, github'],
113
+ ['fluent-emoji-flat', 'emoji desenhado, quando o assunto pede leveza'],
114
+ ['game-icons', 'ilustrativos, para assunto fora do corporativo'],
115
+ ];
116
+
117
+ /**
118
+ * Baixa um ícone e devolve o SVG.
119
+ *
120
+ * O SVG vem com `currentColor` no traço para a cor ser decidida na cena — um
121
+ * ícone com cor cravada no arquivo obriga a baixar de novo para mudar de cor.
122
+ */
123
+ async function icone(nome, { pasta = null } = {}) {
124
+ const m = String(nome || '').match(/^([a-z0-9-]+):([a-z0-9-]+)$/i);
125
+ if (!m) return { ok: false, error: 'o ícone é "coleção:nome", como "lucide:zap"' };
126
+ const url = `https://api.iconify.design/${m[1]}/${m[2]}.svg?color=currentColor`;
127
+ try {
128
+ const svg = await pegar(url);
129
+ if (!/^<svg/i.test(svg.trim())) {
130
+ return { ok: false, error: `"${nome}" não existe. Procure com estudio_icones.` };
131
+ }
132
+ if (pasta) guardar(path.join(pasta, 'midia', 'icones', `${m[1]}-${m[2]}.svg`), svg);
133
+ return { ok: true, nome, svg };
134
+ } catch (e) {
135
+ return { ok: false, error: `não deu para baixar o ícone: ${e.message}` };
136
+ }
137
+ }
138
+
139
+ /** Procura ícones por palavra, em todas as coleções. */
140
+ async function procurarIcones(termo, { limite = 24 } = {}) {
141
+ try {
142
+ const r = await pegar(
143
+ `https://api.iconify.design/search?query=${encodeURIComponent(termo)}&limit=${limite}`,
144
+ { json: true });
145
+ return { ok: true, icones: r.icons || [], colecoes: COLECOES };
146
+ } catch (e) {
147
+ return { ok: false, error: e.message, colecoes: COLECOES };
148
+ }
149
+ }
150
+
151
+ /** Baixa todos os ícones que o plano usa, de uma vez. */
152
+ async function iconesDoPlano(plano, pasta) {
153
+ const nomes = new Set();
154
+ for (const cena of plano.cenas || []) {
155
+ for (const el of cena.elementos || []) if (el.tipo === 'icone' && el.nome) nomes.add(el.nome);
156
+ }
157
+ const mapa = {};
158
+ const faltaram = [];
159
+ for (const nome of nomes) {
160
+ const r = await icone(nome, { pasta });
161
+ if (r.ok) mapa[nome] = r.svg;
162
+ else faltaram.push({ nome, motivo: r.error });
163
+ }
164
+ return { mapa, faltaram };
165
+ }
166
+
167
+ // ── imagens ──────────────────────────────────────────────────────────────
168
+
169
+ function extensaoDe(url, padrao) {
170
+ const m = String(url).split('?')[0].match(/\.([a-z0-9]{2,5})$/i);
171
+ return m ? '.' + m[1].toLowerCase() : padrao;
172
+ }
173
+
174
+ /**
175
+ * A extensão pelos PRIMEIROS BYTES, não pela URL.
176
+ *
177
+ * O Jamendo entrega mp3 numa URL sem extensão nenhuma ("?trackid=…&
178
+ * format=mp32"), e o Openverse repassa essa URL como está. Pela URL o arquivo
179
+ * era salvo como `.jpg` — e um `.jpg` que na verdade é um mp3 entra no plano,
180
+ * passa pelo validador (o arquivo existe!) e só quebra lá na frente, dentro
181
+ * do Remotion, com um erro que não fala de download nenhum.
182
+ *
183
+ * Os bytes não mentem. A URL é o palpite de reserva, para o formato que não
184
+ * tiver assinatura conhecida aqui.
185
+ */
186
+ function extensaoPorConteudo(bytes) {
187
+ if (!bytes || bytes.length < 12) return null;
188
+ const c = bytes.subarray(0, 12);
189
+ const quatro = c.subarray(0, 4).toString('latin1');
190
+ if (c.subarray(0, 3).toString('latin1') === 'ID3' || (c[0] === 0xff && (c[1] & 0xe0) === 0xe0)) return '.mp3';
191
+ if (quatro === 'OggS') return '.ogg';
192
+ if (quatro === 'fLaC') return '.flac';
193
+ // RIFF é container: WAVE é som, WEBP é imagem — os bytes 8..11 dizem qual.
194
+ if (quatro === 'RIFF') {
195
+ const tipo = c.subarray(8, 12).toString('latin1');
196
+ return tipo === 'WEBP' ? '.webp' : tipo === 'WAVE' ? '.wav' : null;
197
+ }
198
+ if (c[0] === 0x89 && quatro.slice(1) === 'PNG') return '.png';
199
+ if (c[0] === 0xff && c[1] === 0xd8) return '.jpg';
200
+ if (c.subarray(0, 3).toString('latin1') === 'GIF') return '.gif';
201
+ if (c[0] === 0x00 && c[1] === 0x00 && c[2] === 0x01 && c[3] === 0x00) return '.ico';
202
+ // ftyp no offset 4: MP4 e M4A dividem o container; o `brand` separa.
203
+ if (c.subarray(4, 8).toString('latin1') === 'ftyp') {
204
+ return /^(M4A|mp42|isom|iso2|mp41)/.test(c.subarray(8, 12).toString('latin1')) ? '.mp4' : '.mp4';
205
+ }
206
+ const inicio = bytes.subarray(0, 400).toString('utf8').trimStart();
207
+ if (/^<(\?xml|svg)/i.test(inicio)) return '.svg';
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * Procura imagem. Pexels na frente quando há chave; Openverse sempre.
213
+ *
214
+ * @returns {{ok, imagens: Array<{url, titulo, autor, licenca, creditos, fonte}>}}
215
+ */
216
+ async function procurarImagens(termo, { quantas = 8, transparente = false } = {}) {
217
+ const chave = process.env.PEXELS_API_KEY;
218
+ if (chave && !transparente) {
219
+ try {
220
+ const r = await pegar(
221
+ `https://api.pexels.com/v1/search?query=${encodeURIComponent(termo)}&per_page=${quantas}`,
222
+ { json: true, cabecalhos: { Authorization: chave } });
223
+ const imagens = (r.photos || []).map((p) => ({
224
+ url: p.src.original, titulo: p.alt || termo, autor: p.photographer,
225
+ licenca: 'Pexels', creditos: `Foto de ${p.photographer} (Pexels)`, fonte: 'pexels',
226
+ largura: p.width, altura: p.height,
227
+ }));
228
+ if (imagens.length) return { ok: true, imagens };
229
+ } catch { /* cai para o Openverse */ }
230
+ }
231
+
232
+ try {
233
+ // `license_type=commercial` corta o que não pode ser usado em peça de
234
+ // marca — que é o caso de quase todo vídeo feito aqui.
235
+ const r = await pegar(
236
+ `https://api.openverse.org/v1/images/?q=${encodeURIComponent(termo)}`
237
+ + `&page_size=${quantas}&license_type=commercial`
238
+ + (transparente ? '&extension=png' : ''),
239
+ { json: true });
240
+ const imagens = (r.results || []).map((x) => ({
241
+ url: x.url, titulo: x.title || termo, autor: x.creator,
242
+ licenca: x.license, creditos: x.attribution, fonte: 'openverse',
243
+ largura: x.width, altura: x.height,
244
+ }));
245
+ return { ok: true, imagens };
246
+ } catch (e) {
247
+ return { ok: false, error: `não deu para procurar imagem: ${e.message}`, imagens: [] };
248
+ }
249
+ }
250
+
251
+ // ── som ──────────────────────────────────────────────────────────────────
252
+
253
+ /**
254
+ * Procura música ou efeito sonoro.
255
+ *
256
+ * `duracaoMin` existe para separar as duas coisas sem o usuário dizer: um
257
+ * "whoosh" tem menos de dois segundos, uma trilha tem mais de trinta.
258
+ */
259
+ async function procurarSom(termo, { quantas = 8, duracaoMin = 0, duracaoMax = 0 } = {}) {
260
+ try {
261
+ let url = `https://api.openverse.org/v1/audio/?q=${encodeURIComponent(termo)}`
262
+ + `&page_size=${quantas}&license_type=commercial`;
263
+ if (duracaoMin) url += `&length=${duracaoMin < 30 ? 'short' : 'medium'}`;
264
+ const r = await pegar(url, { json: true });
265
+ const sons = (r.results || [])
266
+ .filter((x) => !duracaoMax || !x.duration || x.duration / 1000 <= duracaoMax)
267
+ .map((x) => ({
268
+ url: x.url, titulo: x.title || termo, autor: x.creator,
269
+ licenca: x.license, creditos: x.attribution, fonte: 'openverse',
270
+ segundos: x.duration ? Math.round(x.duration / 1000) : null,
271
+ }));
272
+ return { ok: true, sons };
273
+ } catch (e) {
274
+ return { ok: false, error: `não deu para procurar som: ${e.message}`, sons: [] };
275
+ }
276
+ }
277
+
278
+ // ── baixar ───────────────────────────────────────────────────────────────
279
+
280
+ /**
281
+ * Traz a mídia para a pasta do projeto.
282
+ *
283
+ * O `relativo` que volta é o que entra no plano — sempre relativo à pasta do
284
+ * projeto, nunca absoluto: caminho absoluto quebra quando o projeto é movido
285
+ * ou aberto em outra máquina.
286
+ */
287
+ async function baixar(item, { pasta, nome = null, sub = 'midia' } = {}) {
288
+ if (!item || !item.url) return { ok: false, error: 'sem endereço para baixar' };
289
+ try {
290
+ const dados = await pegar(item.url, { binario: true });
291
+ const ext = extensaoPorConteudo(dados) || extensaoDe(item.url, '.jpg');
292
+ const limpo = (nome || String(item.titulo || 'midia'))
293
+ .normalize('NFD').replace(/[̀-ͯ]/g, '')
294
+ .replace(/[^a-z0-9]+/gi, '-').toLowerCase().slice(0, 40);
295
+ const destino = path.join(pasta, sub, `${limpo || 'midia'}-${Date.now().toString(36)}${ext}`);
296
+ guardar(destino, dados, {
297
+ titulo: item.titulo, autor: item.autor, licenca: item.licenca,
298
+ creditos: item.creditos, origem: item.url, fonte: item.fonte, quando: new Date().toISOString(),
299
+ });
300
+ return {
301
+ ok: true,
302
+ arquivo: destino,
303
+ relativo: path.relative(pasta, destino).replace(/\\/g, '/'),
304
+ bytes: dados.length,
305
+ creditos: item.creditos || null,
306
+ };
307
+ } catch (e) {
308
+ return { ok: false, error: `não deu para baixar: ${e.message}` };
309
+ }
310
+ }
311
+
312
+ // ── fontes ───────────────────────────────────────────────────────────────
313
+
314
+ /* O leque. Antes eram seis arquivos .woff2 no pacote; agora é o Google Fonts
315
+ inteiro. Esta lista não LIMITA nada — qualquer família do Google Fonts
316
+ funciona no plano. Ela existe porque um modelo que não conhece o catálogo
317
+ escreve "Inter" em tudo, e o resultado é o que se reclamou: tudo igual. */
318
+ const FONTES = {
319
+ display: ['Anton', 'Bebas Neue', 'Archivo Black', 'Oswald', 'Teko', 'Alfa Slab One',
320
+ 'Bungee', 'Righteous', 'Bowlby One', 'Passion One', 'Ultra', 'Rubik Mono One'],
321
+ grotesca: ['Inter', 'Manrope', 'Plus Jakarta Sans', 'Outfit', 'Sora', 'Space Grotesk',
322
+ 'DM Sans', 'Figtree', 'Onest', 'Geist', 'Satoshi', 'General Sans'],
323
+ serifa: ['Playfair Display', 'Fraunces', 'Instrument Serif', 'DM Serif Display',
324
+ 'Libre Baskerville', 'Lora', 'Cormorant Garamond', 'Spectral', 'Newsreader'],
325
+ tecnica: ['JetBrains Mono', 'IBM Plex Mono', 'Space Mono', 'Azeret Mono', 'Martian Mono'],
326
+ manuscrita: ['Caveat', 'Kalam', 'Permanent Marker', 'Shadows Into Light', 'Gloria Hallelujah'],
327
+ condensada: ['Barlow Condensed', 'Archivo Narrow', 'Saira Condensed', 'Fjalla One', 'Roboto Condensed'],
328
+ };
329
+
330
+ /** A fonte existe no Google Fonts? Erra-se o nome com facilidade. */
331
+ async function conferirFonte(familia) {
332
+ const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(familia).replace(/%20/g, '+')}`;
333
+ try { await pegar(url); return { ok: true, familia }; }
334
+ catch { return { ok: false, error: `"${familia}" não existe no Google Fonts.` }; }
335
+ }
336
+
337
+ module.exports = {
338
+ extensaoPorConteudo,
339
+ pegar, guardar,
340
+ icone, procurarIcones, iconesDoPlano, COLECOES,
341
+ procurarImagens, procurarSom, baixar,
342
+ FONTES, conferirFonte,
343
+ };