# Inspect saved Hume and Oruk responses locally

Download the [standalone comparator](https://oruk.ai/examples/compare-legacy-hume.py) and optional [provenance manifest example](https://oruk.ai/examples/hume-comparison-provenance.example.json). Python 3.10+ and its standard library are sufficient. The comparator reads only the files you select. It makes no network request, executes no response content, needs no credentials, and does not submit or decode audio.

It accepts a saved **legacy Hume batch predictions export** and an Oruk `speech.result` with an `analysis`, `affect`, or `emotions` task and native `emotions`/`segments` arrays. It inspects returned schemas; this is not a compatibility promise for every model. Current Hume Tagger/Prosody, EVI, streaming messages and dedicated Resonance-2 responses have different contracts. See the [measurement migration guide](https://oruk.ai/compare/hume-expression-measurement-alternative) for those distinctions.

## Existing saved-response command

Keep this command for responses that you have separately checked use the same recording and time origin:

```sh
python3 compare-legacy-hume.py \
  --hume hume-predictions.json --oruk analysis.json \
  --output private-migration-review.json
```

Without a manifest, both origins are assumed to be zero and `time_alignment.basis` is `unverified_zero_origin_assumption`. The tool hashes the exact input JSON bytes, but it has not checked any recording. Multiple Hume sources require an explicit `--source-index`; multiple files in the chosen source require `--file-index`. Indices start at zero. No entry is silently substituted for an absent selection.

## Bind local bytes and declare time origins

Save the manifest example as `provenance.json`. Its zero hashes and one-byte size are placeholders, **not a usable fixture**. Replace them with the SHA-256 hashes of your recording and both saved JSON files, the recording's exact byte count, the Hume source/file indices, and both response time origins. Keep this association and the underlying permission records in your own controlled storage.

All manifest fields are required; extra fields are rejected. Hashes use 64 lowercase hexadecimal characters. `file_index` is normally an integer; `null` is accepted only when the selected Hume source actually has no file, such as a failed source. Each origin is a finite, nonnegative number of seconds. Do not assume a missing origin is zero.

```sh
python3 compare-legacy-hume.py \
  --hume hume-predictions.json --oruk analysis.json \
  --audio recording.wav --manifest provenance.json \
  --output private-migration-review-with-provenance.json
```

`--audio` and `--manifest` must be supplied together. The comparator streams the actual recording bytes through SHA-256 without decoding them. Each response is hashed and parsed from the same bounded read. A wrong hash, byte count, missing selection or contradictory CLI index stops the run before an output file is created. A manifest can select a nondefault source/file without repeating the indices on the command line.

The exact coordinate rule is:

```text
recording_seconds = response_seconds + time_origin_seconds
```

For example, Hume's interval `[0, 2]` with origin `4` and Oruk's interval `[0, 2]` with origin `5` overlap for one second in the **declared recording coordinates**. Zero or negative overlap does not link intervals. Nonfinite derived times or offsets that erase a positive interval through floating-point precision loss are rejected. Original response timestamps remain unchanged in the saved native objects.

`provenance.local_bytes_verified: true` means the selected local files match the declaration. It does **not** prove either provider processed that recording, used the declared crop/origin, or returned an authentic export. `provider_association_verified`, `time_alignment.provider_verified` and the compatibility field `audio_identity_verified` remain `false`. The manifest's association is explicitly `customer_declared_same_recording`; overlap is not independently verified alignment or a speaker match.

## Interpret the report

Report schema **2** keeps the selected `hume_source`, complete `oruk` object, input hashes, original interval coordinates, source/group/prediction indices, overlap links and unmatched indices. Parsed native values are preserved; whitespace, key ordering and original JSON serialization are not reproduced. Input hashes identify the original JSON bytes. Derived intervals refer to `group_index` rather than repeating a potentially large native group ID; that ID remains in `hume_source`.

Every encountered emotion/style name is listed under `unmapped_dimensions`, including names shared by both providers. They are **observed labels only**, not either model's complete taxonomy or a conversion/calibration table. Additional returned Oruk `scores`, `axes`, `unipolar` and `labels` fields are preserved and inventoried as unhandled native signals. Other top-level Oruk fields are listed separately as `unhandled_oruk_fields`; neither list claims semantic support for those fields.

`unsupported_hume_modalities` inventories model keys other than prosody in the **selected source/file**. A `null` entry means that key had no returned value; a present value is retained without evaluation. This does not audit unselected export entries or mean the provider itself lacks that modality. Missing labels, missing scores, nulls, empty arrays and numeric zero remain distinct. Scores are never filled, averaged, normalized, thresholded or mapped. A failed source or absent prosody stays visible. An unobserved label is not declared an unsupported model class.

## Bounds and local storage

The comparator accepts regular local files, rejects final-path symlinks and special files, and checks file identity before opening and after reading. Optional operating-system no-follow/nonblocking flags strengthen those checks where available. Use a directory you control; these checks are not an operating-system sandbox against another process modifying your filesystem.

| Local tool bound | Limit |
| --- | --- |
| Each response JSON | 32 MiB |
| Manifest | 16 KiB |
| Recording bytes, streamed without decoding | 512 MiB |
| JSON nesting / visited values | 64 levels / 250,000 values |
| Validated arrays / Hume intervals | 20,000 each |
| Interval comparisons / overlap links | 2,000,000 / 100,000 |
| Observed dimension name | 256 UTF-8 bytes |
| Serialized report | 96 MiB |

These are local inspection bounds, not hosted API upload or model limits. Duplicate JSON keys, nonfinite numbers, malformed timestamps/emotion scores and clear segment/duration contradictions fail. A one-microsecond tolerance accommodates accumulated floating-point segment timing; native times are not clamped.

Existing output files are never overwritten. On POSIX the new output requests mode `0600`; on Windows access follows the directory/file ACL, so choose an appropriately restricted directory. The implementation retains a portable Python path when optional Unix flags are absent; native Windows execution has not been validated in this revision.

The report can contain transcripts, scores and private response metadata. Store it with the recording's access restrictions and retention policy. Do not put credentials in files or arguments and do not attach a report publicly. Exit `0` means a local report was saved, `1` means data or file validation failed, and `2` means invalid CLI arguments. None establishes permission, provider provenance, expression accuracy, successful cutover, billing reconciliation or customer acceptance.
