Un utilisateur envoie vacances.png
Un utilisateur ajoute un fichier nommé vacances.png dans votre zone de discussion. Vous ne savez pas ce qu’il contient. Ce peut être une photo, ou un programme qui porte un nom d’image.
Envoyer une image à un modèle sans danger, c’est la vérifier avant qu’elle quitte votre serveur. Cette leçon montre les contrôles.
À la fin de cette leçon
Remarque
Je peux construire une requête qui porte une image, et refuser un fichier qui enfreint une règle avant de l’envoyer.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Base64 | une façon d’écrire des octets avec des lettres et des chiffres | |
| Type de média | une étiquette qui dit quelle sorte de fichier c’est | image/png |
| Premiers octets | le début d’un fichier, qui dit ce qu’il est vraiment | |
| Charge utile | les données que porte une requête | |
| Limite | le maximum que le service accepte |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une image voyage sous forme de texte
Une requête est en général du texte, mais une image est faite d’octets. Le base64 transforme chaque groupe de 3 octets en 4 caractères simples. La requête reste ainsi du texte.
Cela a un prix. Le texte est d’un tiers plus long que les octets. Une limite sur les octets n’est donc pas une limite sur la requête.
Comment le base64 agrandit un fichier
| Octets | Caractères | Pourquoi |
|---|---|---|
| 3 | 4 | Un groupe de 3 devient 4 |
| 4 | 8 | Un deuxième groupe est entamé |
| 28 | 40 | Dix groupes de 4 |
| 3 000 | 4 000 | Environ un tiers de plus |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Fiez-vous aux premiers octets, pas au nom
Un nom n’est que ce que quelqu’un a tapé. Les premiers octets d’un fichier sont fixés par son format. Un PNG commence toujours par les mêmes 8 octets, un JPEG par 3.
Décidez donc du type de média en lisant les premiers octets. Si le nom n’est pas d’accord, refusez le fichier ou au moins signalez-le.
Une erreur courante
Attention
Beaucoup se fient au nom du fichier et au type que signale le navigateur. Les deux sont tapés par l’expéditeur. Contrôlez les premiers octets sur votre propre serveur : un programme nommé vacances.png passerait un contrôle fait sur le nom.
Contrôler d’abord, envoyer ensuite
Faites tous les contrôles avant qu’un seul octet parte. Rassemblez tous les problèmes dans une liste, dans un ordre fixe, pour que l’utilisateur corrige tout d’un coup. Seul un fichier sans problème est encodé et mis dans la charge utile.
Un appel refusé fait perdre du temps, et le modèle ne voit jamais un fichier que vous n’avez pas contrôlé.
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, 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(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de contrôle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const entete = [137, 80, 78, 71, 13, 10, 26, 10];' 'const png = Buffer.concat([Buffer.from(entete), Buffer.alloc(40, 7)]);' 'const faux = Buffer.concat([Buffer.from(\"MZ\"), Buffer.alloc(40, 7)]);' 'const enorme = Buffer.concat([Buffer.from(entete), Buffer.alloc(142, 7)]);' 'const LIMITE = 100;' > envoi2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de contrôle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function flairer(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 \"inconnu\";' '}' 'function verifier(b) {' ' const problemes = [];' ' if (b.length > LIMITE) problemes.push(\"trop gros\");' ' if (flairer(b) === \"inconnu\") problemes.push(\"type refusé\");' ' return problemes;' '}' >> envoi2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de contrôle, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function afficher(liste) {' ' for (const element of liste) {' ' const b = element[1];' ' console.log(element[0] + \" : \" + flairer(b) + \", \" + b.length + \" octets, base64 \" + b.toString(\"base64\").length + \" caractères\");' ' console.log(\" problèmes : \" + JSON.stringify(verifier(b)));' ' }' ' const partie = { type: \"image\", media_type: \"image/png\", data: png.toString(\"base64\").slice(0, 12) + \"...\" };' ' console.log(JSON.stringify({ messages: [{ role: \"user\", content: [{ type: \"text\", text: \"Décrivez-la.\" }, partie] }] }));' '}' >> envoi2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de contrôle, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' afficher([[\"chaton.png\", png], [\"devoirs.png\", faux], [\"enorme.png\", enorme]]);' ' const q = \"Écrivez une phrase courte et aimable qui dit à un utilisateur que son image est trop grosse pour être envoyée.\";' ' const reponse = await demander([{ role: \"user\", content: q }], \"Cette image est trop grosse pour être envoyée, merci d’en choisir une plus petite.\", 40);' ' console.log(\"réponse : \" + reponse.texte);' '}' 'main();' >> envoi2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Contrôler trois fichiers
docker exec -w /home/node/project agent node envoi2.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Les lignes affichées
| Ligne | Ce qu’elle montre |
|---|---|
| chaton.png : png, 48 octets, base64 64 caractères | De vrais octets PNG : 48 octets sont devenus 64 caractères |
| devoirs.png : inconnu ... type refusé | Le nom dit png, les octets non |
| enorme.png : png, 150 octets ... trop gros | Le bon type, mais au-dessus de la limite de 100 |
| {"messages":[...]} | Le corps de la requête, avec les données coupées |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.