Files
novi-lessons/Les18-AI-Toolbox-Next/Les18-Lesopdracht.md
2026-06-07 13:35:02 +02:00

283 lines
6.9 KiB
Markdown

# Les 18 — Lesopdracht
## Kies één modaliteit, bouw een mini-demo
**Duur:** 30 min in-class
---
## Doel
Pak één van de modaliteiten uit deze les en bouw daar een minimale werkende demo van. Voice, vision, image gen, of local LLM — jouw keuze.
---
## Opties
### Optie 1 — Voice transcriptie
Bouw een mic-button + transcribe. Werkende voice → text in een Next.js page.
```typescript
// app/voice/page.tsx
"use client";
import { useState, useRef } from "react";
export default function VoicePage() {
const [text, setText] = useState("");
const [recording, setRecording] = useState(false);
const recorder = useRef<MediaRecorder | null>(null);
const chunks = useRef<Blob[]>([]);
async function start() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
recorder.current = new MediaRecorder(stream);
recorder.current.ondataavailable = (e) => chunks.current.push(e.data);
recorder.current.onstop = async () => {
const blob = new Blob(chunks.current, { type: "audio/webm" });
chunks.current = [];
const fd = new FormData();
fd.append("audio", blob);
const res = await fetch("/api/transcribe", { method: "POST", body: fd });
const { text } = await res.json();
setText(text);
};
recorder.current.start();
setRecording(true);
}
function stop() {
recorder.current?.stop();
setRecording(false);
}
return (
<main className="p-8">
<button onClick={recording ? stop : start} className="bg-blue-600 text-white px-6 py-3 rounded-full">
{recording ? "Stop" : "Start opnemen"}
</button>
<p className="mt-6">{text}</p>
</main>
);
}
```
`app/api/transcribe/route.ts`:
```typescript
import { experimental_transcribe as transcribe } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const fd = await req.formData();
const audio = fd.get("audio") as Blob;
const result = await transcribe({
model: openai.transcription("whisper-1"),
audio: new Uint8Array(await audio.arrayBuffer()),
});
return Response.json({ text: result.text });
}
```
---
### Optie 2 — Vision foto-analyse
Bouw een upload-form + analyse. User upload foto → AI beschrijft.
```typescript
// app/vision/page.tsx
"use client";
import { useState } from "react";
export default function VisionPage() {
const [desc, setDesc] = useState("");
const [loading, setLoading] = useState(false);
async function analyze(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setLoading(true);
const fd = new FormData();
fd.append("image", file);
const res = await fetch("/api/analyze", { method: "POST", body: fd });
const { description } = await res.json();
setDesc(description);
setLoading(false);
}
return (
<main className="p-8">
<input type="file" accept="image/*" onChange={analyze} />
{loading && <p>Bezig...</p>}
<p className="mt-6">{desc}</p>
</main>
);
}
```
`app/api/analyze/route.ts`:
```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const fd = await req.formData();
const image = fd.get("image") as File;
const buffer = await image.arrayBuffer();
const result = await generateText({
model: openai("gpt-4o"),
messages: [{
role: "user",
content: [
{ type: "text", text: "Beschrijf wat er op deze foto staat in 3 zinnen." },
{ type: "image", image: new Uint8Array(buffer) },
],
}],
});
return Response.json({ description: result.text });
}
```
---
### Optie 3 — Image generation
Bouw een prompt-input → generate → preview.
```typescript
// app/generate/page.tsx
"use client";
import { useState } from "react";
export default function GeneratePage() {
const [prompt, setPrompt] = useState("");
const [imageUrl, setImageUrl] = useState("");
const [loading, setLoading] = useState(false);
async function generate() {
setLoading(true);
const res = await fetch("/api/generate", {
method: "POST",
body: JSON.stringify({ prompt }),
});
const { image } = await res.json();
setImageUrl(`data:image/png;base64,${image}`);
setLoading(false);
}
return (
<main className="p-8 max-w-2xl mx-auto">
<textarea value={prompt} onChange={(e) => setPrompt(e.target.value)}
placeholder="Beschrijf je afbeelding..." rows={3}
className="w-full border p-2 rounded" />
<button onClick={generate} disabled={loading}
className="mt-3 bg-blue-600 text-white px-4 py-2 rounded">
{loading ? "..." : "Generate"}
</button>
{imageUrl && <img src={imageUrl} alt="" className="mt-6 rounded" />}
</main>
);
}
```
`app/api/generate/route.ts`:
```typescript
import { experimental_generateImage as generateImage } from "ai";
import { fal } from "@ai-sdk/fal";
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = await generateImage({
model: fal.image("fal-ai/flux/schnell"),
prompt,
size: "1024x1024",
});
return Response.json({ image: result.image.base64 });
}
```
`pnpm add @ai-sdk/fal` — gratis tier op fal.ai.
---
### Optie 4 — Local LLM met Ollama
Run llama lokaal, chat ermee via je app.
```bash
brew install ollama
ollama pull llama4:8b
ollama serve
```
```bash
pnpm add ollama-ai-provider
```
```typescript
// app/local/page.tsx + /api/local-chat
import { createOllama } from "ollama-ai-provider";
import { streamText } from "ai";
const ollama = createOllama();
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: ollama("llama4:8b"),
messages,
});
return result.toUIMessageStreamResponse();
}
```
Frontend kan dezelfde `useChat` gebruiken als in Les 12.
---
## Eisen
Kies één optie. Aan het eind:
- [ ] Demo werkt lokaal
- [ ] Eén feature compleet (transcribe, analyze, generate, of chat)
- [ ] Geen errors in console
- [ ] Optioneel: gedeeld in Teams met screenshot
---
## Tijdsindeling (30 min)
| Stap | Tijd |
|------|------|
| 1 — Project setup | 5 min |
| 2 — Backend route | 10 min |
| 3 — Frontend page | 10 min |
| 4 — Test + debug | 5 min |
---
## Veelvoorkomende problemen
| Symptoom | Oplossing |
|----------|-----------|
| `MediaRecorder undefined` | Browser support — Chrome werkt, Safari iets anders |
| Fal API key | Sign up + create key — gratis |
| Ollama "connection refused" | `ollama serve` runnen + check `:11434` |
| Vision error | Image te groot? Resize to <4MB |
| Transcribe error op localhost | Gebruik buffer (Uint8Array) niet Blob direct |
---
## Klaar?
Dat was de laatste lesopdracht van de leerlijn. Voor je eindopdracht — wat je nu kunt:
- Multi-modal features toevoegen (voice/vision)
- Lokale modellen voor privacy
- Image generation voor branding
- Edge AI voor performance
Veel succes!