Un usuario sube un archivo llamado vacaciones.png
Un usuario añade un archivo llamado vacaciones.png al chat de tu aplicación. No puedes saber qué hay dentro. Puede ser una foto o un programa con nombre de imagen.
Enviar una imagen a un modelo con seguridad es comprobarla antes de que salga de tu servidor. Esta lección enseña las comprobaciones.
Al final de esta lección
Nota
Puedo construir una petición que lleve una imagen y rechazar un archivo que incumple una regla antes de enviarlo.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Base64 | una forma de escribir bytes con letras y cifras de texto | |
| Tipo de medio | una etiqueta que dice qué clase de archivo es | image/png |
| Primeros bytes | el comienzo de un archivo, que dice lo que es de verdad | |
| Carga | los datos que lleva una petición | |
| Límite | lo máximo que acepta el servicio |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una imagen viaja como texto
Una petición suele ser texto, pero una imagen son bytes. Base64 convierte cada 3 bytes en 4 caracteres normales, y así la petición sigue siendo texto.
Tiene un precio. El texto es un tercio más largo que los bytes. Por eso un límite sobre los bytes no es un límite sobre la petición.
Cómo base64 agranda un archivo
| Bytes | Caracteres | Por qué |
|---|---|---|
| 3 | 4 | Un grupo de 3 pasa a 4 |
| 4 | 8 | Se empieza un segundo grupo |
| 28 | 40 | Diez grupos de 4 |
| 3000 | 4000 | Un tercio más, más o menos |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Fíate de los primeros bytes, no del nombre
Un nombre es solo lo que alguien escribió. Los primeros bytes de un archivo los fija su formato. Un PNG empieza siempre con los mismos 8 bytes, y un JPEG con 3.
Por eso el tipo de medio se decide leyendo los primeros bytes. Si el nombre no coincide, rechaza el archivo o al menos avisa.
Un error frecuente
Advertencia
Mucha gente se fía del nombre del archivo y del tipo que informa el navegador. Los escribe quien envía. Comprueba los primeros bytes en tu propio servidor, porque un programa llamado vacaciones.png pasaría una comprobación del nombre.
Primero se comprueba, luego se envía
Haz todas las comprobaciones antes de que salga un solo byte. Reúne todos los problemas en una lista, en un orden fijo, para que la persona los arregle de una vez. Solo un archivo sin problemas se codifica y entra en la carga.
Una llamada rechazada también cuesta tiempo, y el modelo nunca ve un archivo que no comprobaste.
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"estado \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de comprobación, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const head = [137, 80, 78, 71, 13, 10, 26, 10];' 'const png = Buffer.concat([Buffer.from(head), Buffer.alloc(40, 7)]);' 'const fake = Buffer.concat([Buffer.from(\"MZ\"), Buffer.alloc(40, 7)]);' 'const huge = Buffer.concat([Buffer.from(head), Buffer.alloc(142, 7)]);' 'const LIMIT = 100;' > send2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de comprobación, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function sniff(b) {' ' if (b.length >= 8 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) return \"png\";' ' if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) return \"jpeg\";' ' return \"desconocido\";' '}' 'function check(b) {' ' const problems = [];' ' if (b.length > LIMIT) problems.push(\"demasiado grande\");' ' if (sniff(b) === \"desconocido\") problems.push(\"tipo no permitido\");' ' return problems;' '}' >> send2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de comprobación, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const list = [[\"gato.png\", png], [\"apuntes.png\", fake], [\"enorme.png\", huge]];' ' for (const item of list) {' ' const b = item[1];' ' console.log(item[0] + \": \" + sniff(b) + \", \" + b.length + \" bytes, base64 \" + b.toString(\"base64\").length + \" caracteres\");' ' console.log(\" problemas: \" + JSON.stringify(check(b)));' ' }' >> send2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de comprobación, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const part = { type: \"image\", media_type: \"image/png\", data: png.toString(\"base64\").slice(0, 12) + \"...\" };' ' console.log(JSON.stringify({ messages: [{ role: \"user\", content: [{ type: \"text\", text: \"Descríbela.\" }, part] }] }));' ' const q = \"Escribe una frase corta y amable que diga a un usuario que su imagen era demasiado grande para enviarla.\";' ' const reply = await ask([{ role: \"user\", content: q }], \"Esa imagen es demasiado grande para enviarla. Elige una más pequeña, por favor.\", 40);' ' console.log(\"respuesta: \" + reply.text);' '}' 'main();' >> send2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Comprueba tres archivos
docker exec -w /home/node/project agent node send2.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Las líneas que imprime
| Línea | Qué muestra |
|---|---|
| gato.png: png, 48 bytes, base64 64 caracteres | Bytes de un PNG de verdad, y 48 bytes pasaron a 64 caracteres |
| apuntes.png: desconocido ... tipo no permitido | El nombre dice png, los bytes no |
| enorme.png: png, 150 bytes ... demasiado grande | Tipo correcto, pero sobre el límite de 100 |
| {"messages":[...]} | El cuerpo de la petición, con los datos recortados |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.