# kcode-speech-classification 0.1.0
This crate classifies caller-supplied 24-feature speech rows. It performs no audio analysis and stores no audio bytes.
## Library
Open a classifier with:
`SpeechClassifier::open(path: impl AsRef<Path>) -> Result<SpeechClassifier, Error>`
The database uses SQLite WAL mode, synchronous `FULL`, an append-only `ADD`/`DELETE` event log, and a transactionally maintained current view. Opening version 1 storage replays the authoritative event log and rebuilds the current view. Other schema versions are rejected. A classifier may be shared across threads.
The three domain operations are:
- `identify(&self, key: ObservationKey, cohort: Cohort, row: FeatureRow, threshold: f64) -> Result<IdentifyOutcome, Error>`
- `train(&self, key: ObservationKey, cohort: Cohort, row: FeatureRow, speaker_id: String) -> Result<TrainOutcome, Error>`
- `delete(&self, key: ObservationKey) -> Result<DeleteOutcome, Error>`
`ObservationKey` contains only `object_id` and `piece_index`. `Cohort` contains `provider`, `model`, `prompt_version`, `schema_version`, and `primary_language`. Every cohort component is matched exactly; `primary_language` must be a lowercase ISO 639-3 code.
`FeatureRow` serializes these fields in order:
1. `accent_variety` (string)
2. `perceived_age` (number)
3. `vocal_gender_presentation` (number)
4. `median_f0_hz` (number)
5. `formant_dispersion_hz` (number)
6. `vai` (number)
7. `hypernasality` (number)
8. `creaky_phonation_percent` (number)
9. `rhotic_realization` (string)
10. `word_initial_stressed_prevocalic_t_vot_ms` (number)
11. `breathiness` (number)
12. `roughness` (number)
13. `f0_pitch_span_semitones` (number)
14. `articulation_rate_syllables_per_second` (number)
15. `npvi_v` (number)
16. `cefr` (`"A1"`, `"A2"`, `"B1"`, `"B2"`, `"C1"`, or `"C2"`)
17. `foreign_accentedness` (number)
18. `unstressed_vowel_reduction_percent` (number)
19. `lateral_realization` (string)
20. `filled_pauses_per_100_words` (number)
21. `s_realization` (string)
22. `lexical_stress_accuracy_percent` (number)
23. `monophthongization_percent` (number)
24. `consonant_cluster_reduction_percent` (number)
Strings and identities must be nonempty. Numbers must be finite. Percentages, gender presentation, breathiness, and roughness are bounded by 0 through 100, hypernasality by 0 through 4, and foreign accentedness by 1 through 9. Age and acoustic/rate values must be positive; `npvi_v` and filled-pause rate must be nonnegative. CEFR is scored as the ordered levels A1 through C2 while retaining its canonical serialized strings. Values are stored exactly as supplied.
`identify` scores profiles only in the exact cohort. Its optional `evidence` reports the lowest-cost candidate, optional runner-up, background population cost, absolute gap, optional runner-up gap, and raw `confidence_score`. Costs are unweighted Gaussian/Laplace negative-log-likelihood costs; gaps and confidence are raw log-likelihood evidence, not percentages. `speaker_id` is present only when confidence is at least the finite caller threshold. An accepted row is added atomically and immediately trains that speaker. Unknown outcomes retain nothing. Repeating an identical accepted call adds no event. A conflicting existing key returns `Error::Conflict`; use `train` to correct it or `delete` to remove it.
`train` atomically returns `Added`, `Unchanged`, or `Corrected`. Correction appends `DELETE` then `ADD`. `delete` is idempotent and returns `Deleted` or `NotFound`. `speaker_id` is caller-owned; unknown speakers are never stored.
## Unix service
The `kcode-speech-classification` binary accepts exactly:
`kcode-speech-classification --database <path> --socket <path>`
It binds an owner-only `0600` Unix socket and exchanges one JSON object per newline. Requests are `ProtocolRequest`, tagged by `"operation"` as `"identify"`, `"train"`, or `"delete"`. Responses are `ProtocolResponse`, tagged by `"status"` as `"success"` with an operation result or `"error"` with a stable code and message. The service bounds request lines and concurrent client threads. It refuses to remove a non-socket or live socket path; a stale socket is removed before binding. Database and socket lifecycle remain the operator's responsibility.