# SonoScribe — DICOM Conformance Statement **Product:** SonoScribe ultrasound reporting **Version:** 0.1.0 · **Date:** 2026-07-31 **Scope:** the SonoScribe Edge Connector (`connector/sonoscribe_connector.py`) and the DICOM services it provides. The reporting engine itself is not a DICOM device. This statement follows the structure of PS3.2. Every capability listed here is implemented and covered by automated tests; anything not implemented is stated as such rather than omitted. If a behaviour you need is missing, it is missing — please ask rather than infer. --- ## 0. Read this first — what this product is, in DICOM terms SonoScribe generates the **text of a radiology report**. It does not analyse pixels, does not compute measurements from image geometry, and is not a diagnostic image-processing device. Three consequences shape everything below: 1. **Pixel data is discarded on arrival.** We accept image SOP classes so a modality configured to "send everything" does not error, but the pixel data is stripped at the receiver before anything else touches the dataset — including pixels nested in sequences such as `IconImageSequence`. Nothing is persisted and nothing is forwarded. If your workflow requires us to store images, we are not the right product. 2. **We retrieve metadata, never bulk data.** Over DICOMweb we use `WADO-RS /metadata` and never request `/frames` or rendered images. 3. **Every report is reviewed and signed by a radiologist before it is emitted.** An unsigned draft is never sent to a PACS or RIS, and where one is (in a preview workflow) it is explicitly marked unverified — see §4.3. --- ## 1. Implementation model ### 1.1 Application data flow ``` [ modality / PACS ] INSIDE THE HOSPITAL │ C-STORE (push) ┌──────────────────────────────┐ ├──────────────────►│ │ │ │ SonoScribe Edge Connector │──── outbound HTTPS ───► cloud │ QIDO/WADO (pull) │ • strips pixel data │ (TLS 1.2+) └──────────────────►│ • de-identifies locally │ └──────────────────────────────┘ [ RIS ] ◄──── HL7 ORU^R01 over MLLP ──────────┘ [ PACS ] ◄─── C-STORE / STOW-RS of a DICOM SR ┘ ``` The connector runs **inside the hospital network** and reaches us **outbound only**. No inbound firewall rule is required. The connector contains no clinical logic, no prompts and no model credentials — it is a de-identifying pipe. ### 1.2 Functional definition of AEs | AE | Role | Purpose | |---|---|---| | `SONOSCRIBE` (configurable) | **SCP** | Verification (C-ECHO) and Storage (C-STORE) — receives studies and structured reports pushed by a modality or PACS | | `SONOSCRIBE` (configurable) | **SCU** | Storage (C-STORE) — sends the finished report back as a DICOM SR | Both AE titles are set per site via `dicom_ae_title`. ### 1.3 Sequencing The receiver is stateless per association. Each C-STORE is handled independently and is handed to the processing queue **only after the instance is completely received and parsed**. An association torn mid-transfer delivers nothing — verified by test, including a 24 MB instance aborted 5 ms in. --- ## 2. AE specifications ### 2.1 Storage SCP (receiving) **Presentation contexts accepted:** | SOP Class UID | Name | Notes | |---|---|---| | `1.2.840.10008.1.1` | Verification | C-ECHO. Supported because every PACS engineer tests connectivity with it first | | `1.2.840.10008.5.1.4.1.1.88.11` | Basic Text SR Storage | | | `1.2.840.10008.5.1.4.1.1.88.22` | Enhanced SR Storage | | | `1.2.840.10008.5.1.4.1.1.88.33` | Comprehensive SR Storage | | | `1.2.840.10008.5.1.4.1.1.88.34` | Comprehensive 3D SR Storage | | | `1.2.840.10008.5.1.4.1.1.2` | CT Image Storage | **metadata only — pixels discarded** | | `1.2.840.10008.5.1.4.1.1.4` | MR Image Storage | **metadata only — pixels discarded** | | `1.2.840.10008.5.1.4.1.1.6.1` | Ultrasound Image Storage | **metadata only — pixels discarded** | | `1.2.840.10008.5.1.4.1.1.3.1` | Ultrasound Multi-frame Image Storage | **metadata only — pixels discarded** | **Transfer syntaxes:** all 45 that pynetdicom supports, including Implicit VR Little Endian (`1.2.840.10008.1.2`), Explicit VR Little Endian (`1.2.840.10008.1.2.1`), Explicit VR Big Endian, Deflated, and the JPEG / JPEG-LS / JPEG 2000 / RLE families. Because pixel data is discarded, a compressed transfer syntax costs us nothing and is never decoded. **Role:** SCP only. We do not negotiate SCU/SCP role reversal. **Maximum PDU length:** 16 384 bytes (configurable). **Association policy:** | Condition | Response | |---|---| | Calling AE title on the site's allowlist | Association accepted | | Calling AE title **not** on the allowlist | **A-ASSOCIATE-RJ**, result 1 (rejected-permanent), source 1 (service-user), reason **3 — calling AE title not recognised** | | Allowlist empty | **Every** association rejected. An empty allowlist means deny-all; it is never read as allow-all | | Malformed AE title (contains NUL or backslash) | A-ABORT, issued by the DICOM library during PDU validation. **See §6.1 — this case is not visible in our logs** | **Status codes returned on C-STORE:** | Status | Meaning | |---|---| | `0x0000` | Success — instance received, parsed and queued | | `0xC000` | Cannot understand — the instance carries no `StudyInstanceUID`, so nothing downstream can file it. We refuse rather than invent one | | `0xA700` | Out of resources — an internal error while handling the instance | ### 2.2 Storage SCU (sending the report back) We send the finished report as a **Comprehensive 3D SR** (`1.2.840.10008.5.1.4.1.1.88.34`) using Explicit VR Little Endian. Delivery outcome is recorded per instance; a failure is retried by the outbox with backoff rather than dropped. The same SR can be delivered over **STOW-RS** instead — see §3.2. --- ## 3. DICOMweb ### 3.1 Consumed (we are the client) | Service | Use | |---|---| | **QIDO-RS** `/studies` | Find new studies, and find PRIOR studies by PatientID | | **WADO-RS** `/studies/{uid}/metadata` | Study and series metadata | | **WADO-RS** `/studies/{uid}/series` | Series list | We never call `/instances`, `/frames`, `/rendered` or `/thumbnail`. `Accept: application/dicom+json` throughout. A `204` from QIDO is treated as "no matches", not an error. ### 3.2 Produced (we are the client, pushing) **STOW-RS** `POST /studies/{StudyInstanceUID}` with `multipart/related; type="application/dicom"`, each part a complete Part-10 object. Response handling, stated explicitly because it is where implementations usually go wrong: | Server response | Our interpretation | |---|---| | `200` with `ReferencedSOPSequence` | Stored | | `202` with `FailedSOPSequence` | **Partial failure.** Reported as a failure, with the SOP Instance UID and DICOM failure reason of every failed instance | | `202` with an empty body | **Not treated as success.** PS3.18 requires a 202 to carry `FailedSOPSequence`; a 202 with nothing in it is unverifiable | | `2xx` with an unparseable body (XML, plain text, an error document) | **Not treated as success** | | Fewer instances acknowledged than sent | **Not treated as success** | | `4xx` / `5xx` | Failure, with the server's own message preserved | ### 3.3 Not implemented - **QIDO/WADO/STOW *serving*.** We consume these; we do not host them. - **Modality Worklist (MWL) SCP.** - **C-FIND / C-MOVE / C-GET** as SCU or SCP. Study retrieval is DICOMweb or C-STORE push only. - **Storage Commitment.** --- ## 4. The Structured Report we produce ### 4.1 Identity | Attribute | Value | |---|---| | SOP Class | Comprehensive 3D SR — `1.2.840.10008.5.1.4.1.1.88.34` | | `StudyInstanceUID` | **Inherited verbatim from the source study.** This is deliberate: it is what makes the report file into the same study and open in the same viewer as the images. We refuse to build an SR without one rather than mint an orphan study | | `SeriesInstanceUID` / `SOPInstanceUID` | Newly generated, unique per instance | | `SeriesNumber` | `9001` — deliberately high so the report sorts after the image series | | `Modality` | `SR` | | `SpecificCharacterSet` | `ISO_IR 192` (UTF-8). Indian names and clinical symbols round-trip byte-exact | ### 4.2 Content template **TID 2000 (Basic Diagnostic Imaging Report)**, root concept `(18748-4, LN, "Diagnostic Imaging Report")`. We deliberately do **not** claim TID 1500 (Measurement Report). This is a narrative report: its children are Findings / Impression / Recommendation, and it contains none of TID 1500's mandatory rows such as the Image Library container. Declaring a template we do not satisfy would be worse than declaring the one we do. | Content item | Concept | |---|---| | Findings container | `(121070, DCM, "Findings")`, one `TEXT` child per report region using the **clinic's own section heading** | | Impression | `(121073, DCM, "Impression")` | | Recommendation | `(121075, DCM, "Recommendation")` | | Flagged findings | `(121106, DCM, "Comment")` — its own concept, never sharing the impression's | | Language | `(121049, DCM, …)` = `eng` | | Observer | `(121005/121008, DCM)` — the signing radiologist | | Procedure reported | `(121058, DCM)` → DCM modality code `CT`, `MR` or `US` (CID 29) | **No numeric measurement groups are emitted.** Our measurements come from the radiologist's dictation, not from image geometry, so there is no `ImageRegion` or referenced frame to anchor them to. An unanchored measurement in a slot viewers expect to be geometric would be misleading. ### 4.3 Draft versus signed — the attribute pair that matters | Report state | `CompletionFlag` | `VerificationFlag` | `VerifyingObserverSequence` | |---|---|---|---| | Signed by a radiologist | `COMPLETE` | `VERIFIED` | present, naming the signer | | Finalized without an attributable signer | `COMPLETE` | `UNVERIFIED` | absent | | Draft / preview | `PARTIAL` | `UNVERIFIED` | absent | `VERIFIED` is asserted **only** when the report is both finalized and attributable to a named radiologist. A viewer can therefore never present an unsigned draft as though it had been signed. All four combinations are covered by automated tests. --- ## 5. Security and privacy - **Plain DIMSE is unencrypted and its AE title is self-declared.** The AE allowlist is an access control, not authentication. The listener is therefore intended to run on the edge connector inside your network, or across a VPN. Binding it to a non-loopback interface without TLS is **refused by the software** unless explicitly overridden for an isolated network. - **De-identification happens locally, before egress.** Direct identifiers are replaced with an opaque, site-scoped pseudonym computed with a secret that never leaves your building. We receive a stable key we can bill and de-duplicate on — never the patient's identity. - **Outbound HTTPS only**, TLS verified by default. - **Study UIDs are validated** before being used to build any URL against your PACS (digits and dots, ≤ 64 characters, per PS3.5 Table 6.2-1). --- ## 6. Known limitations — stated, not omitted **6.1 A malformed calling AE title fails silently on our side.** An AE title containing a NUL or a backslash is rejected by the DICOM library while decoding the association request, before our software sees it. No association object exists, so we cannot log it. From our side such a modality is indistinguishable from one that never connected. **If a modality cannot associate and our logs show nothing at all, check the AE title for padding characters first.** **6.2 No Modality Worklist.** We do not provide an MWL SCP, so we cannot be a worklist source. **6.3 Character sets other than UTF-8.** We declare `ISO_IR 192` on what we produce. Inbound instances in other character sets are read as-is by the DICOM library; we do not transcode. **6.4 No Storage Commitment.** A successful C-STORE status is our acknowledgement; we do not implement the Storage Commitment Push Model. --- ## 7. Configuration a site must provide ```yaml dicom_listen_port: 11112 # the port your modality is configured to send to dicom_ae_title: SONOSCRIBE # our called AE title dicom_calling_aes: # EVERY sending AE must be listed — empty means deny-all - CT_SCANNER_01 - PACS_MAIN dicomweb_base_url: https://pacs.internal/dicomweb # optional, for the pull path dicomweb_modalities: [CT, MR] ``` --- ## 8. Testing your integration 1. **C-ECHO** to the configured AE/port. A success proves network, port and AE title in one step. 2. **C-STORE** one study. Our log records `dimse_store_received` with the study UID and calling AE. 3. If the association is refused, the reject reason distinguishes the cause: reason **3** means your calling AE title is not on our allowlist (not that our called AE title is wrong). Contact your SonoScribe integration engineer with the AE titles, IP and port you intend to use, and we will confirm the allowlist entry before you test.