Werkafspraken (Backend)
De backend zorgt voor de opslag van werkafspraken, de transcodering van video's en PDF's, en de optionele generatie van een audio-podcast via een AI-pijplijn op de achtergrond.
Technisch Ontwerp
De module deelt het mediasysteem met de nieuwsmodule: afbeeldingen worden omgezet naar WebP, video's via ffmpeg naar H.264/AAC MP4 met thumbnail, en PDF's gerasterd naar pagina-previews plus een opgeschoonde download-versie. Zware taken lopen asynchroon via Celery. Bovenop het mediasysteem kan een werkafspraak een podcast krijgen: een korte audio-samenvatting die uit de tekst of de PDF wordt gegenereerd.
Podcast-generatie
flowchart TD
Create["create/update_work_agreement_item"] --> Eval["evaluate_podcast_candidate"]
Eval --> Elig{Geschikt? tekst >= 280 tekens}
Elig -- nee --> Stop[podcast_status blijft none + reden]
Elig -- ja --> Queue["queue_podcast_generation (limiet 5/week)"]
Queue --> Task["generate_work_agreement_podcast_task"]
Task --> Reval[Her-evalueer eligibility]
Reval --> Script["generate_podcast_script: LLMService Gemini Flash"]
Script --> TTS["synthesize_podcast_audio: GCP Chirp 3 HD"]
TTS --> Upload["MP3 naar S3 + ffprobe duur"]
Upload --> Save["podcast_status = ready + metadata"]
Task -- fout --> Failed["podcast_status = failed + podcast_error"]
Geschiktheid: foto en video zijn altijd ongeschikt. Een PDF is geschikt als er minstens 280 tekens leesbare tekst uit te halen is (pdfplumber). Een tekstbericht is geschikt als de platte tekst minstens 280 tekens telt (MIN_PODCAST_TEXT_LENGTH). Het script gaat via LLMService.generate (scope werkafspraken_podcast_script_v1, Gemini Flash, max_tokens=900, temperature=0.7). De audio gaat via SpeechService.synthesize (scope werkafspraken_podcast_v1, GCP Cloud Text-to-Speech Chirp 3 HD, standaardstem nl-NL-Chirp3-HD-Charon, MP3). De generatie is gelimiteerd op 5 per week per tenant. SpeechService weigert invoer met BSN- of telefoonpatronen; werkafspraken bevatten geen patientdata. Een podcast_source_hash (SHA256 van de brontekst) voorkomt dubbele generatie en detecteert of de bron is gewijzigd.
Datamodel (ERD)
erDiagram
WorkAgreementCategory ||--o{ WorkAgreementItem : category
User ||--o| WorkAgreementItem : created_by
WorkAgreementCategory {
uuid id PK
string name
string color
datetime created_at
datetime updated_at
}
WorkAgreementItem {
uuid id PK
uuid category_id FK
string title
string subtitle
text content
string version
string media_type
string media_status
string media_s3_key
string media_raw_s3_key
string media_thumbnail_s3_key
string media_download_s3_key
string media_file_name
json media_pages
string media_color
string media_icon_name
string media_icon_color
string media_error
string podcast_status
string podcast_s3_key
int podcast_duration_seconds
datetime podcast_generated_at
string podcast_source_hash
string podcast_error
bool podcast_is_eligible
string podcast_ineligibility_reason
bigint created_by_id FK
datetime created_at
datetime updated_at
}
WorkAgreementPendingUpload {
uuid id PK
string s3_key
string status
datetime created_at
}
media_type is photo, video, pdf of none; media_status en podcast_status volgen de levenscyclus (none/queued/processing/ready/failed). CHECK-constraints bewaken de media- en podcast-invarianten (bij none geen S3-keys; bij podcast_status=ready een podcast_s3_key). De User-FK gebruikt db_constraint=False; created_by is een integer-PK. Tijdens podcast-generatie wordt de tenant bepaald via connection.schema_name.
API & Communicatie
Schrijfacties vereisen werkafspraken.edit; alle GET-endpoints vragen alleen authenticatie. Media-bestanden worden eerst geupload en verwerkt; voor de frontend worden tijdelijke signed URLs gegenereerd.
Categorieen
GET /api/werkafspraken/categories/- alle categorieen.POST /api/werkafspraken/categories/- aanmaken (werkafspraken.edit).PATCH /api/werkafspraken/categories/{id}/- bijwerken (werkafspraken.edit).DELETE /api/werkafspraken/categories/{id}/- verwijderen (werkafspraken.edit); kan niet als er nog items aan hangen.
Items
GET /api/werkafspraken/items/- lijst met paginatie (50 per pagina), zoeken (titel, subtitel, inhoud, versie, categorie) en sortering (datum of A-Z).POST /api/werkafspraken/items/- aanmaken (werkafspraken.edit).GET /api/werkafspraken/items/{id}/- detail.PATCH /api/werkafspraken/items/{id}/- bijwerken (werkafspraken.edit).DELETE /api/werkafspraken/items/{id}/- verwijderen (werkafspraken.edit).POST /api/werkafspraken/items/{id}/retry-transcode/- mislukte video/PDF-verwerking opnieuw starten (werkafspraken.edit).
Media-upload
POST /api/werkafspraken/media/- multipart-upload voor afbeeldingen (werkafspraken.edit).POST /api/werkafspraken/media/upload-url/- presigned POST voor directe S3-upload van video/PDF (werkafspraken.edit).POST /api/werkafspraken/media/complete/- directe upload verifieren en verwerking inplannen (werkafspraken.edit).
Overig
GET /api/werkafspraken/ping/- heartbeat.
Achtergrondtaken
generate_work_agreement_podcast_task- genereert script en audio en slaat de metadata op.transcode_work_agreement_video_task- transcodeert video naar MP4 plus thumbnail.rasterize_work_agreement_pdf_task- rastert PDF naar pagina's en download-versie en her-evalueert de podcast-geschiktheid.reconcile_stuck_work_agreement_videos- markeert vastgelopen media alsfailed.cleanup_work_agreement_pending_uploads- ruimt verlaten directe uploads op.
Foutafhandeling & Statuscodes
400- foute invoer of bestand buiten de limieten: afbeelding 10 MB, PDF 25 MB / 100 pagina's, video 750 MB / 600 s, inhoud 50000 tekens.401- geen geldig token.403- mutatie zonderwerkafspraken.edit.404- item of categorie bestaat niet (meer) of valt buiten de tenant.500- fout in de ffmpeg-conversie of de AI-pijplijn; de taak markeert het item daarna alsfailed.
Autorisatie & Beveiliging
- Data is strikt gescheiden per apotheek (tenant isolation); een apotheek ziet nooit afspraken van een andere organisatie.
- Er is een enkel rolrecht:
werkafspraken.editvoor het aanmaken, wijzigen of verwijderen van afspraken en categorieen en voor media-upload, retry en podcast-generatie. Er is geen apart leesrecht; GET-endpoints vragen alleen authenticatie. - Media-keys worden gevalideerd op trusted hosts en het tenant-prefix (
storage_key_from_url) zodat cross-tenant verwijzingen worden geweigerd. Signed URLs worden alleen meegegeven zodra de mediareadyis. - De podcast-pijplijn weigert brontekst met patientpatronen (BSN, telefoonnummer) en is gelimiteerd op 5 generaties per week per tenant.
Bestandsstructuur & Verantwoordelijkheden
models.py-WorkAgreementCategory,WorkAgreementItem,WorkAgreementPendingUploadmet de media- en podcast-invarianten.services.py- CRUD, zoeken, sortering, beeldverwerking, video-transcodering, PDF-rasterisatie en het inplannen van de taken.podcast_services.py- geschiktheidsbepaling, tekstextractie, het in de wachtrij zetten en de generatie aansturen.podcast_generation.py- de koppeling metLLMService(script) enSpeechService(audio).serializers.py- in- en output-serializers met de media- en podcast-payload en de sanering.views.py- de API-endpoints met de in-view rolrechtcontrole.tasks.py- de Celery-taken voor podcast, transcodering, rasterisatie en reconciliatie.
Belangrijke bestanden
backend/features/werkafspraken/models.pybackend/features/werkafspraken/services.pybackend/features/werkafspraken/podcast_services.pybackend/features/werkafspraken/podcast_generation.pybackend/features/werkafspraken/tasks.py