Ga naar inhoud

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 als failed.
  • 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 zonder werkafspraken.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 als failed.

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.edit voor 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 media ready is.
  • 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, WorkAgreementPendingUpload met 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 met LLMService (script) en SpeechService (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.py
  • backend/features/werkafspraken/services.py
  • backend/features/werkafspraken/podcast_services.py
  • backend/features/werkafspraken/podcast_generation.py
  • backend/features/werkafspraken/tasks.py

API & Communicatie (Swagger)