From e27b08d1bd24d205298aa704a435a74067252616 Mon Sep 17 00:00:00 2001 From: Shay Palachy Date: Tue, 12 May 2026 23:59:29 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20usability=20pass=20=E2=80=94=20gotchas?= =?UTF-8?q?=20page,=20glossary,=20labelled=20waveform,=20less=20jargon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganises the docsite for a tired-volunteer reading lens. Major changes: - New "Common mistakes" page consolidates scattered pitfalls (has_violence derivation, hardcoded speaker dirs, peak ~= 0.79, UPPERCASE/lowercase, NEG-isn't-violent, split: train, quality_flags semantics) so each one costs one read instead of recurring across pages. - New Glossary page maps AGG/VIC/SW/BEN, F0, SSML, IR, ISM, dBFS, RMS, prosody cap, dirty file, weak vs strong labels, voice IDs. - Home page rewritten: leads with a real labelled waveform of an SV clip (PNG generated from corpus data + .jsonl event boundaries), then a 4-line load snippet, then side-by-side team cards instead of tabbed product picker. Toy-corpus warning and "what's not here" callout moved below the fold. - Schema reference rewritten as a single annotated JSON example with click-to-expand explanations, ordered by frequency-of-use rather than by Pydantic object hierarchy. Tables retained only for EventLabel, manifest columns, and the .txt transcript format. - Audio format leads with consumer facts (peak ~= 0.79, padding included in timestamps, two-peak-fields convention); pipeline internals (M3a per-turn RMS, Stage 1/2/3 normalization, target rationale) collapsed into optional admonitions. - Taxonomy adds an explicit intensity-vs-typology coupling table and flags that scripts are LLM-generated, not human-written. - She-Proves / Elephant pages reframed as the differential vs the shared reference, with cleaner speaker tables (split speaker_id / voice columns) and full clip listings collapsed. - Critical ??? collapsed admonitions opened to !!! visible ones (peak-is-0.79, ACOU vs DIST, background event types, Tier A meaning, NEG-trap, casing convention). ??? reserved for skippable detail (per-turn RMS rationale, peak-target rationale). - Operator jargon stripped — milestone codes (M3a/M8a/M10a), "wet test", "spec validation", "pipeline bootstrapping". SSML/F0/IR/ISM/ Whisper defined on first use via Glossary cross-links. - Custom CSS adds status pills and team cards. Logo, palette, search, nav structure unchanged. Co-Authored-By: Claude Opus 4.7 --- docs/assets/extra.css | 60 ++++ docs/assets/sp_sv_a_0001_00_waveform.png | Bin 0 -> 83446 bytes docs/audio-format.md | 131 +++----- docs/deliveries.md | 109 ++++--- docs/elephant.md | 213 ++++++------- docs/getting-started.md | 224 ++++++------- docs/glossary.md | 110 +++++++ docs/gotchas.md | 136 ++++++++ docs/index.md | 147 +++------ docs/schema.md | 389 ++++++++++++----------- docs/she-proves.md | 166 +++++----- docs/taxonomy.md | 167 +++++----- mkdocs.yml | 13 +- 13 files changed, 1041 insertions(+), 824 deletions(-) create mode 100644 docs/assets/extra.css create mode 100644 docs/assets/sp_sv_a_0001_00_waveform.png create mode 100644 docs/glossary.md create mode 100644 docs/gotchas.md diff --git a/docs/assets/extra.css b/docs/assets/extra.css new file mode 100644 index 0000000..a6e2c4c --- /dev/null +++ b/docs/assets/extra.css @@ -0,0 +1,60 @@ +/* Status pill used in headers and front-page hero */ +.status-pill { + display: inline-block; + padding: 0.15rem 0.55rem; + border-radius: 0.4rem; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.03em; + text-transform: uppercase; + vertical-align: middle; + margin: 0 0.25rem; +} +.status-pill.provisional { background: #FFB300; color: #3E2723; } +.status-pill.approved { background: #43A047; color: white; } +.status-pill.superseded { background: #BDBDBD; color: #424242; } + +/* Cards used on the home page to replace tabbed "What is this?" widget */ +.team-cards { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin: 1.25rem 0 1.5rem; +} +@media (max-width: 720px) { + .team-cards { grid-template-columns: 1fr; } +} +.team-card { + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 0.45rem; + padding: 1rem 1.1rem; + background: var(--md-default-bg-color); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} +.team-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 18px rgba(0,0,0,0.06); +} +.team-card h3 { + margin: 0 0 0.35rem; + font-size: 1rem; + color: var(--md-primary-fg-color); +} +.team-card .tagline { + font-size: 0.78rem; + color: var(--md-default-fg-color--light); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; +} +.team-card p { margin: 0.4rem 0; font-size: 0.92rem; } +.team-card a.card-link { + display: inline-block; + margin-top: 0.5rem; + font-weight: 600; + font-size: 0.9rem; +} + +/* Tighter table look for reference pages */ +.md-typeset table:not([class]) { font-size: 0.78rem; } +.md-typeset table:not([class]) code { font-size: 0.78rem; } diff --git a/docs/assets/sp_sv_a_0001_00_waveform.png b/docs/assets/sp_sv_a_0001_00_waveform.png new file mode 100644 index 0000000000000000000000000000000000000000..f7bf4fe0e80be23771e8cf946927e0cbe3057c3e GIT binary patch literal 83446 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 LTRQK?o%{a-jA%e@ literal 0 HcmV?d00001 diff --git a/docs/audio-format.md b/docs/audio-format.md index b0173a9..61912f8 100644 --- a/docs/audio-format.md +++ b/docs/audio-format.md @@ -1,110 +1,75 @@ # Audio Format -All clips in the corpus conform to the following hard constraints. Clips that fail these checks are rejected at generation time and will not appear in the corpus. +The three facts you need to use the data, then optional detail on how it gets that way. --- -## Format requirements +## What you need to know -| Property | Value | -|----------|-------| -| Sample rate | 16 000 Hz | -| Channels | 1 (mono) | -| Bit depth | 16-bit PCM | -| Peak level | ≤ –1.0 dBFS (safety ceiling) | -| Duration | ≥ 3.0 s | -| Encoding | WAV (no lossy formats) | +| Fact | Value | Why it matters | +|------|-------|----------------| +| **Sample rate** | 16 000 Hz | Always. Resample your features for this. | +| **Channels / depth** | mono / 16-bit PCM WAV | `wav.ndim == 1`. No lossy formats anywhere. | +| **Peak level** | ≤ –1.0 dBFS (target –2.0 dBFS) | `np.abs(wav).max() ≈ 0.79`, **not** 1.0. | +| **Silence pad** | ≥ 0.5 s at head and tail | Onset/offset timestamps **already account for it** — no shift needed. | +| **Duration** | ≥ 3.0 s | Hard minimum; clips below it are rejected. | ```python -import soundfile as sf -import numpy as np +import soundfile as sf, numpy as np wav, sr = sf.read("data/he/agg_m_30-45_001/sp_sv_a_0001_00.wav") assert sr == 16000 -assert wav.ndim == 1 # mono -assert wav.dtype == np.float64 # soundfile returns float64 by default -assert np.abs(wav).max() <= 1.0 # -1.0 dBFS ≈ linear amplitude 1.0 - -# Check format info -info = sf.info("data/he/agg_m_30-45_001/sp_sv_a_0001_00.wav") -print(info.subtype) # PCM_16 +assert wav.ndim == 1 +assert wav.dtype == np.float64 # soundfile default +assert np.abs(wav).max() <= 1.0 # safety ceiling at -1.0 dBFS +print(sf.info("data/he/agg_m_30-45_001/sp_sv_a_0001_00.wav").subtype) # PCM_16 ``` --- -## Normalization pipeline - -Each clip passes through two normalization steps: - -``` -TTS render (float32, arbitrary loudness) - ↓ -[1] Per-turn RMS gain (M3a) — preserves inter-turn contrast - ↓ -[2] Single global peak gain — lands absolute peak at target_peak_dbfs - ↓ -[3] Safety limiter — clips at ≤ –1.0 dBFS (guaranteed no-op for target ≥ –12.0) - ↓ -Tier B only: room IR + device → renormalize to same target - ↓ -Output WAV -``` - -### Step 1 — Per-turn RMS gain (M3a) - -Each dialogue turn is gain-adjusted so its RMS matches a per-intensity target. This preserves the acoustic contrast between calm and escalated turns — a whispered turn at I1 stays quieter than a shouted turn at I5 — while giving the subsequent global normalization a stable peak-to-RMS ratio to work with. - -??? info "Why per-turn RMS matters" - Without per-turn normalization, the TTS engine produces flat RMS across intensities regardless of the requested prosody. The raw Azure and Google outputs are nearly constant-loudness even when the SSML requests "shout" style. Per-turn RMS gain is the mechanism that creates the acoustic loudness gradient you expect to see in the data. - -### Step 2 — Single global peak gain - -A single gain is applied to the whole mix so the clip's absolute peak lands at `loudness_target_peak_dbfs` (default: –2.0 dBFS). Because it's a single gain, all per-turn RMS *ratios* survive unchanged — the contrast from Step 1 is preserved. +## Two peak fields, two meanings -The configured target is recorded in `generation_metadata.loudness_target_peak_dbfs`. -The measured output peak is recorded in `preprocessing_applied.normalized_dbfs`. +Every clip records two related loudness values: -### Step 3 — Safety limiter +| Field | Set by | What it is | +|-------|--------|------------| +| `generation_metadata.loudness_target_peak_dbfs` | The pipeline config | **Configured** peak target (default –2.0 dBFS) | +| `preprocessing_applied.normalized_dbfs` | Measurement at write time | **Measured** post-preprocess peak of the actual WAV | -A hard ceiling at –1.0 dBFS. For in-spec targets (range: [–12.0, –1.5] dBFS), this is a guaranteed no-op. It exists as a safety rail against misconfiguration. +If those two disagree by more than a fraction of a dB, something is wrong with normalization. Useful as a diagnostic check. --- -## Silence padding +## Known audio quirks -Every clip has at least 0.5 s of ambient silence at the head and tail. This is applied by `preprocess()` and logged in `preprocessing_applied.silence_padded: true`. +### `vic_f0_high` on the 2 Google clips -Onset/offset timestamps in the `.txt` transcript and `.jsonl` events are already shifted to account for the leading pad — they refer to positions in the final processed WAV, not the raw TTS output. +`sp_sv_a_0003_00` and `sp_it_a_0003_00` use the Google Chirp 3 HD female voice (`he-IL-Chirp3-HD-Achernar`). Its F0 baseline runs measurably higher than the Azure reference voice (`he-IL-HilaNeural`), against which the QA F0 thresholds were calibrated. ---- +**What to do about it:** nothing. The flag fires correctly; the audio is fine. If you compute F0-derived features, calibrate per backend (`generation_metadata.tts_backend`) — or just use spectral features that aren't sensitive to baseline F0. Don't exclude these two clips: they're the only backend diversity you have in this delivery. -## Dirty files +### `quality_flags: ["emotion_downgrade"]` -`preprocessing_applied` records the processing that was applied. The **pre-preprocessing WAV** is retained as `{clip_id}_dirty.wav` under `assets/speech/dirty/`. These are the raw TTS-mixer outputs before normalization, padding, or denoising. +The pipeline detected that the TTS engine produced slightly less intense prosody than the SSML asked for at high-intensity turns. The audio is still valid; the prosody is just a touch tamer than the scene intended. About 15 of 20 clips in delivery-003 carry this flag — it's not a defect signal. -The `dirty_file_path` field in ClipMetadata gives the repo-relative path: -``` -"dirty_file_path": "assets/speech/dirty/sp_sv_a_0001_00_dirty.wav" -``` +### Dirty files -Dirty files are useful for: -- Diagnosing normalization issues (compare dirty peak vs. `normalized_dbfs`) -- Checking raw TTS prosody before processing -- Re-running preprocessing with different parameters +The pre-preprocessing WAV is retained at `assets/speech/dirty/{clip_id}_dirty.wav`. Its path is recorded in `dirty_file_path`. These files are the raw TTS-mixer outputs before normalization, padding, or denoising — useful for diagnosing the pipeline, not for training. -!!! warning "Do not modify dirty files" - The `assets/` directory is managed by SynthBanshee. Manual edits to `.wav` files under `assets/speech/` will break SHA-256 cache lookups. +!!! warning "Don't modify files under `assets/`" + `assets/speech/` is the SynthBanshee SHA-256 SSML cache. Renaming or editing any file there will break cache lookups and force a paid re-synthesis on next run. --- ## TTS backends -| Backend | Voices | Clips in delivery-003 | -|---------|--------|----------------------| +| Backend | Voices in delivery-003 | Clips | +|---------|-----------------------|------:| | Azure Cognitive Services | `he-IL-AvriNeural` (M), `he-IL-HilaNeural` (F) | 18 | | Google Cloud TTS Chirp 3 HD | `he-IL-Chirp3-HD-Achird` (M), `he-IL-Chirp3-HD-Achernar` (F) | 2 | -The backend per speaker is recorded in `generation_metadata.tts_backend`: +Per-speaker backend is in `generation_metadata.tts_backend`: + ```json "tts_backend": { "AGG_M_30-45_002": "google", @@ -112,21 +77,29 @@ The backend per speaker is recorded in `generation_metadata.tts_backend`: } ``` -??? info "Azure SSML cache" - SynthBanshee caches per-utterance WAVs under `assets/speech/` keyed by SHA-256 of the full rendered SSML string. Re-running generation with the same SSML is **free** for Azure clips — the file is returned directly from cache without an API call. Google Chirp HD does not use the same cache: it produces slightly different audio on each synthesis (minor bit-level variation at the same parameters). +Azure is deterministic — re-rendering the same SSML returns byte-identical WAVs (via the SHA-256 cache). Google Chirp 3 HD is not — it produces minor bit-level variation on each synthesis at the same parameters. If you need byte-stable reproducibility for an experiment, you may see the Google clips re-render slightly differently between fresh generations even though peak / RMS / duration stay within tolerance. --- -## Known audio quirks +## How the normalization actually works -### `vic_f0_high` — Google Chirp HD female F0 baseline +You don't need this to consume the data. Open the section below if you're debugging loudness drift, building a comparable pipeline, or just curious. -The two Google Chirp 3 HD clips (`sp_sv_a_0003_00`, `sp_it_a_0003_00`) use the female voice `he-IL-Chirp3-HD-Achernar`. This voice's F0 baseline runs measurably higher than `he-IL-HilaNeural` (Azure), against which the corpus QA M10a thresholds were calibrated. +??? info "The normalization pipeline (3 stages)" + ``` + TTS render → per-turn RMS gain → single global peak gain → safety limiter → Tier B: room IR + device + noise → renormalize → output WAV + ``` -Both clips are flagged `vic_f0_high` in the QA report. This is expected and tracked — it reflects a real backend difference, not a synthesis failure. **Do not exclude these clips** on the basis of this flag; calibrate your model's F0 features against the correct baseline per backend. + **Stage 1: per-turn RMS gain.** Each dialogue turn is gain-adjusted so its RMS matches a per-intensity target. This creates the calm-to-loud gradient you'd expect — a whispered I1 turn stays quieter than a shouted I5 turn. Without this step, raw Azure and Google output is nearly constant-loudness regardless of the requested prosody style. -### `quality_flags: ["emotion_downgrade"]` + **Stage 2: single global peak gain.** A single multiplicative gain lands the clip's absolute peak at `loudness_target_peak_dbfs` (default –2.0 dBFS). Because it's one gain applied to the whole mix, every per-turn RMS ratio from Stage 1 survives unchanged. + + **Stage 3: safety limiter.** A hard ceiling at –1.0 dBFS. For in-spec targets in `[-12.0, -1.5]` dBFS, this is always a no-op. It exists as a safety rail against config drift. + + **Tier B post-processing.** Room IR convolution, device frequency response (e.g. `pi_budget_mic`), and background-noise injection happen after Stage 3. Then the same `peak_normalize_to_target` helper renormalises so every tier exits at the same absolute peak — Tier A and Tier B are comparable on the loudness dimension. -Several clips carry an `emotion_downgrade` quality flag. This means the TTS engine produced a less emotionally intense output than requested by the SSML prosody hints — the pipeline detected the downgrade and flagged it. Audio quality is still acceptable; the prosody is slightly less extreme than the scene specification intended. +??? info "Why per-turn RMS gain matters" + Without it, the TTS engine produces flat RMS across turns regardless of the requested prosody. The raw Azure and Google outputs are nearly constant-loudness even when the SSML requests a "shout" style or sets `prosody volume="+50%"`. Per-turn RMS gain is the mechanism that creates the acoustic loudness gradient between calm and escalated turns — without it, your model has nothing to learn loudness escalation from. -In delivery-003: 15 clips carry at least one quality flag, mostly from prosody cap activations at I3+. +??? info "Why peak normalize to –2.0 dBFS instead of 0 dBFS" + The 2 dB of headroom buys safety against any later processing step that might add 1–2 dB of gain (room IR convolution can do this). Peak at 0 dBFS would clip; peak at –1.0 dBFS leaves no headroom for the limiter. –2.0 is the conservative middle. diff --git a/docs/deliveries.md b/docs/deliveries.md index 71dac3b..12c9e2a 100644 --- a/docs/deliveries.md +++ b/docs/deliveries.md @@ -1,16 +1,14 @@ # Deliveries -All data deliveries are logged here. Each entry links to per-delivery notes with clip counts, QA findings, known limitations, and the SynthBanshee commit that produced the batch. +What's currently in the corpus, what's missing, and what changed in the latest batch. One row per data delivery in the log at the bottom. --- -## Delivery 003 — multi-project, multi-voice +## Current delivery — 003 -**Date:** 2026-05-12 · **Status:** provisional · **PR:** [#5](https://github.com/DataHackIL/avdp-synth-corpus/pull/5) +provisional · 2026-05-12 [`#5`](https://github.com/DataHackIL/avdp-synth-corpus/pull/5) · slug: `multi-project-multi-voice` · supersedes delivery-002. -This is the current working delivery. It replaces delivery-002. - -### At a glance +### What's in it | | | |---|---| @@ -19,53 +17,74 @@ This is the current working delivery. It replaces delivery-002. | Projects | `she_proves` (12) + `elephant_in_the_room` (8) | | Tiers | A (12 clean) + B (8 room-augmented) | | TTS backends | Azure (18) + Google Chirp 3 HD (2) | +| Unique speaker personas | 6 (4 in She-Proves, 2 in Elephant) | | Validation failures | 0 / 20 | | Pipeline | SynthBanshee `0.1.0` @ [`1ea48f3`](https://github.com/DataHackIL/SynthBanshee/commit/1ea48f3) | -[Full notes](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/003-multi-project-multi-voice/notes.md) · [QA report](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/003-multi-project-multi-voice/qa-report.json) +Authoritative records: [`metadata.yaml`](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/003-multi-project-multi-voice/metadata.yaml) · [`notes.md`](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/003-multi-project-multi-voice/notes.md) · [`qa-report.json`](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/003-multi-project-multi-voice/qa-report.json). -### QA findings — closed (vs. delivery-002) +### Known limitations -| Finding | Delivery-002 | Delivery-003 | -|---------|:---:|:---:| -| `agg_no_escalation` | 3 clips | **0** — AGG RMS now escalates with intensity | -| `warn_no_overlap` | 4 clips | **0** — overlap_ratio 100% on I4+ clips | -| `warn_emotion_downgrade` | 4 clips | **0** — emotion_downgrade_ratio 0% | -| `generation_metadata` absent | 0 of 8 clips | **20 of 20** carry the full block | -| `dirty_file_path` null | 7 of 8 clips | **20 of 20** retain dirty files | -| `normalized_dbfs` hardcoded `-1.0` | all 8 clips | **fixed** — now the measured peak | +- **All clips are `split: train`.** Only 4 unique speaker personas across 20 clips — speaker-disjoint partitioning isn't feasible at this scale. +- **One room type for Elephant.** All 8 Tier-B clips use `clinic_office`. `welfare_office` and `open_office` are in the pipeline but not exercised yet. +- **One device profile for She-Proves.** No `phone_in_pocket` etc. augmentation applied yet — Tier-A clips are clean, not phone-captured. +- **Voice diversity is low.** 2 voice families per gender; the QA threshold for "diverse" is ≥3. +- **Toy-batch scale.** 20 clips is enough to wire up consumer plumbing. Not enough to train a production model. -Additional findings closed by the 2026-05-12 schema-shift regen (PRs [#110](https://github.com/DataHackIL/SynthBanshee/pull/110)/[#111](https://github.com/DataHackIL/SynthBanshee/pull/111)/[#112](https://github.com/DataHackIL/SynthBanshee/pull/112)): +### Open QA flags -| Finding | Resolution | -|---------|-----------| -| `single_backend` false positive | `qa.py` now derives backend diversity from `generation_metadata.tts_backend.values()`; reports `clips_by_tts_backend: {azure: 18, google: 2}` | -| Absolute paths in clip JSON | `dirty_file_path` and `transcript_path` are now repo-relative POSIX strings | -| Leaked pytest tmp_path on `sp_neu_a_0001_00` | Regen overwrote with canonical path; autouse env-var strip fixture prevents future leaks | +| Flag | Detail | What to do about it | +|------|--------|---------------------| +| `low_voice_diversity_male` | 2 male voice families across the corpus (threshold ≥3) | Track per-voice eval separately; expect feature overfit to AvriNeural until more voices land | +| `low_voice_diversity_female` | Same, for female voices | Same | +| `vic_f0_high` (per-clip × 2) | `sp_sv_a_0003_00`, `sp_it_a_0003_00` — Google Chirp HD female F0 above Azure baseline | **Nothing.** Don't exclude the clips. Calibrate F0 features per backend if you compute them. See [Audio Format](audio-format.md#vic_f0_high-on-the-2-google-clips). | +| `quality_flagged_clips: 15` | Mostly `emotion_downgrade` from prosody cap activations at I3+ | Don't reflexively filter these out — they pass validation. See [Common mistakes #7](gotchas.md#7-quality_flags-doesnt-mean-broken). | -### QA findings — open +### Distribution -| Finding | Detail | -|---------|--------| -| `low_voice_diversity_male` | 2 voice families per gender; threshold ≥ 3 | -| `low_voice_diversity_female` | 2 voice families per gender; threshold ≥ 3 | -| `vic_f0_high` (2 clips) | `sp_sv_a_0003_00` and `sp_it_a_0003_00` — Google Chirp HD female F0 runs higher than Azure Hila reference | -| `quality_flagged_clips: 15` | Mostly from prosody cap activations at I3+; expected behaviour | +| Typology | Tier A (She-Proves) | Tier B (Elephant) | Total | +|----------|:--:|:--:|:--:| +| `SV` | 3 | 2 | 5 | +| `IT` | 3 | 2 | 5 | +| `NEG` | 3 | 2 | 5 | +| `NEU` | 3 | 2 | 5 | -### Known limitations +`max_intensity` across the 20 clips: I5 = 10 clips · I3 = 4 clips · I2 = 6 clips. + +--- + +## What this delivery exercises + +Use these to check your consumer code on the schema features the delivery was designed to cover: + +1. Full `ClipMetadata` schema — including the `generation_metadata` block and (for Tier B) populated `acoustic_scene`. +2. Per-surface casing rules — UPPERCASE `speaker_id`, lowercase paths and clip IDs. +3. `has_violence` derivation from events — NEG clips correctly `false` even at `max_intensity ≥ 3`. +4. Multi-project layout under a single `data/he/` root. +5. Multi-backend provenance — `generation_metadata.tts_backend` differs per speaker. + +--- + +## What changed vs delivery-002 -- **Speaker-disjoint splits not feasible.** 4 unique speaker personas across 20 clips; all clips are `split: train`. -- **Two speaker directories only.** `agg_m_30-45_002/` and `ben_m_40-55_003/` are first appearances — code hardcoding `agg_m_30-45_001/` will miss them. -- **One room type.** All 8 Elephant Tier B clips use `clinic_office`. Future deliveries will add `welfare_office` and `open_office`. -- **Toy corpus only.** 20 clips is not sufficient for training production models. +??? abstract "Closed QA findings (vs. delivery-002)" + | Finding | Delivery-002 | Delivery-003 | + |---------|:---:|:---:| + | `agg_no_escalation` | 3 clips | **0** — AGG RMS now escalates with intensity | + | `warn_no_overlap` | 4 clips | **0** — turn-overlap fires on I4+ clips | + | `warn_emotion_downgrade` | 4 clips | **0** | + | `generation_metadata` absent | 0 of 8 clips had it | **20 of 20** carry the full block | + | `dirty_file_path` null | 7 of 8 clips | **20 of 20** retain dirty files | + | `normalized_dbfs` hardcoded `-1.0` | all 8 clips | Records the measured peak | -### What this delivery exercises +??? abstract "Closed by the 2026-05-12 schema-shift regen" + Three SynthBanshee PRs landed alongside the regen ([#110](https://github.com/DataHackIL/SynthBanshee/pull/110) / [#111](https://github.com/DataHackIL/SynthBanshee/pull/111) / [#112](https://github.com/DataHackIL/SynthBanshee/pull/112)): -1. Full `ClipMetadata` schema including `generation_metadata`, `voice_family`, and (for Tier B) the populated `acoustic_scene` block -2. Per-surface casing rules: UPPERCASE `speaker_id`, lowercase paths and clip IDs -3. `has_violence` derivation from events: NEG clips are correctly `false` even at `max_intensity ≥ 3` -4. Multi-project layout under a single `data/he/` root -5. Multi-backend provenance: `generation_metadata.tts_backend` per speaker + | Finding | Resolution | + |---------|-----------| + | `single_backend` false positive | `qa.py` derives backend diversity from `generation_metadata.tts_backend.values()`; reports `clips_by_tts_backend: {azure: 18, google: 2}` | + | Absolute paths in clip JSON | `dirty_file_path` and `transcript_path` are now repo-relative POSIX | + | Leaked pytest tmp_path on `sp_neu_a_0001_00` | Regen overwrote with canonical path; autouse env-var strip fixture prevents future leaks | --- @@ -73,14 +92,14 @@ Additional findings closed by the 2026-05-12 schema-shift regen (PRs [#110](http | # | Date | Slug | Project | Tier | Clips | Duration | Status | |---|------|------|---------|------|------:|------:|--------| -| [003](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/003-multi-project-multi-voice/notes.md) | 2026-05-12 | multi-project-multi-voice | she_proves + elephant | A + B | 20 | ~42m | provisional | -| [002](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/002-m2a-wettest/notes.md) | 2026-04-15 | m2a-wettest | she_proves | A | 8 | ~17m | superseded | -| [001](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/001-debug-run-1/notes.md) | 2026-04-15 | debug-run-1 | she_proves | A | 1 | 2m 36s | superseded | +| [003](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/003-multi-project-multi-voice/notes.md) | 2026-05-12 | multi-project-multi-voice | she_proves + elephant | A + B | 20 | ~42m | provisional | +| [002](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/002-m2a-wettest/notes.md) | 2026-04-15 | m2a-wettest | she_proves | A | 8 | ~17m | superseded | +| [001](https://github.com/DataHackIL/avdp-synth-corpus/blob/main/deliveries/001-debug-run-1/notes.md) | 2026-04-15 | debug-run-1 | she_proves | A | 1 | 2m 36s | superseded | ## Status definitions | Status | Meaning | |--------|---------| -| `provisional` | Wet-test batch; not yet approved for model training | +| `provisional` | Preview batch; consumer-integration only, not approved for training | | `approved` | QA passed; cleared for training use | -| `superseded` | Replaced by a later delivery with the same scenes at higher quality | +| `superseded` | Replaced by a later delivery covering the same scenes at higher quality | diff --git a/docs/elephant.md b/docs/elephant.md index fa62e35..2409795 100644 --- a/docs/elephant.md +++ b/docs/elephant.md @@ -1,178 +1,155 @@ # Elephant in the Room Guide -**Elephant in the Room (הפיל שבחדר)** is a Raspberry Pi–class device placed in clinic and welfare offices that alerts security when a social worker is under threat. +Elephant in the Room (הפיל שבחדר) is a Raspberry Pi–class device placed in clinic and welfare offices that alerts security when a social worker is under threat. **Optimisation target: high precision** — false alarms erode trust with the security team and the workers they protect. -**Optimization target: high precision.** False alarms erode trust with security staff and social workers alike. +This page is the *differential* between Elephant clips and the rest of the corpus. For shared concepts (schema, labels, audio format) follow the cross-links. --- -## Scene structure +## Scene profile -| Property | Value | -|----------|-------| -| Duration | 1–4 minutes | -| Tier | B (room IR + device + noise augmentation) | -| Alert window | Final 40% of the clip | -| Device profile | `pi_budget_mic` | -| Room types | `clinic_office`, `welfare_office`, `open_office` | -| Language | Hebrew (`he`) | +| | | +|---|---| +| Project code | `elephant_in_the_room` (clip-id prefix `el_*`) | +| Tier | B — room IR + device profile + background noise applied | +| Duration | 1–4 min | +| Alert window | Final 40% of the clip — violence events concentrate here | +| Device | `pi_budget_mic` | +| Room types | `clinic_office`, `welfare_office`, `open_office` (only `clinic_office` in delivery-003) | -The alert-in-final-40% constraint reflects real-world deployment: the device picks up normal consultation audio before a client becomes threatening. The model must recognize genuine escalation from a baseline of professional interaction. - -??? info "Tier B acoustic augmentation pipeline" - Tier B clips go through three augmentation steps after TTS rendering and preprocessing: - - 1. **Room impulse response (IR)** — the clean speech is convolved with a synthetic room IR (generated by `pyroomacoustics` image-source method) to simulate the acoustic of the target room type. - 2. **Device frequency response** — the `pi_budget_mic` profile applies the frequency response of a budget Raspberry Pi microphone capsule. - 3. **Background noise injection** — ambient noise events (HVAC hum, equipment sounds) are mixed in at specified SNR levels. - - After augmentation, the clip is renormalized to the same peak target (–2.0 dBFS) via the shared `peak_normalize_to_target` helper — so all tiers exit at the same absolute peak level. +The alert-in-final-40% constraint mirrors real-world deployment: the device picks up normal consultation audio for most of the session before any threat emerges. The model must distinguish escalation from a baseline of routine professional interaction. --- -## Speaker pair +## What Tier B adds (and why) -Delivery-003 has one Elephant speaker pair. +Tier B clips run through three augmentation stages after preprocessing. This is what separates them from She-Proves (Tier A) clips. -| Speaker dir | Male speaker | Female speaker | Backend | -|-------------|--------------|----------------|---------| -| `ben_m_40-55_003/` | `BEN_M_40-55_003` → `he-IL-AvriNeural` | `SW_F_30-45_001` → `he-IL-HilaNeural` | Azure | +| Stage | What it adds | Where to find it in metadata | +|-------|--------------|------------------------------| +| Room IR convolution | Reverb of a real-sounding room | `acoustic_scene.room_type`, `ir_source` | +| Device profile | Frequency response of a budget Pi microphone | `acoustic_scene.device` | +| Background noise injection | HVAC hum + occasional `ACOU_*` events | `acoustic_scene.background_events` | -The roles are **BEN (beneficiary/client, male) + SW (social worker, female)** — matching the most common demographic in Israeli welfare/clinic settings. +After augmentation the clip is renormalised to the same peak target (–2.0 dBFS) as Tier A, so the two tiers are comparable on the loudness dimension. -!!! note "`ben_m_40-55_003/` is a new speaker directory in delivery-003" - Downstream code that hardcoded `agg_m_30-45_001/` for She-Proves will not find these clips. Use `manifest.csv` or filter by `meta["project"] == "elephant_in_the_room"`. +!!! info "What `pyroomacoustics_ism` is" + The image-source method (ISM) synthesises a room impulse response by simulating a virtual point source reflecting off the walls of a modelled room. [`pyroomacoustics`](https://pyroomacoustics.readthedocs.io/) is the Python library that implements it. The resulting IR, when convolved with clean speech, makes the speech sound like it was recorded in the modelled room — without needing a real recording. --- -## The `acoustic_scene` block +## The `acoustic_scene` field -This is the key difference between Tier A and Tier B metadata. For Elephant clips, `acoustic_scene` is fully populated: +For Tier A clips this is all `null` / empty. For Elephant clips it's fully populated: ```json "acoustic_scene": { - "room_type": "clinic_office", - "device": "pi_budget_mic", - "ir_source": "pyroomacoustics_ism", - "snr_db_actual": 11.2, - "speaker_distance_meters": 1.2, - "background_events": [ - {"type": "hvac_hum", "onset": 0.0, "offset": 147.0, "level_db": -37.4}, - {"type": "ACOU_SLAM", "onset": 72.164, "offset": 72.476, "level_db": 9.9}, - {"type": "ACOU_FALL", "onset": 97.57, "offset": 98.473, "level_db": 9.6} - ] + "room_type": "clinic_office", + "device": "pi_budget_mic", + "ir_source": "pyroomacoustics_ism", + "snr_db_actual": 11.2, + "speaker_distance_meters": 1.2, + "background_events": [ + {"type": "hvac_hum", "onset": 0.000, "offset": 147.031, "level_db": -37.4}, + {"type": "ACOU_SLAM", "onset": 72.164, "offset": 72.476, "level_db": 9.9}, + {"type": "ACOU_FALL", "onset": 97.570, "offset": 98.473, "level_db": 9.6} + ] } ``` -| Field | Meaning | -|-------|---------| -| `room_type` | Simulated room environment | -| `device` | Microphone/device profile applied | -| `ir_source` | Method used to generate room IR | -| `snr_db_actual` | Measured speech-to-noise ratio after mixing | -| `speaker_distance_meters` | Simulated speaker-to-mic distance | -| `background_events` | Non-speech acoustic events: type, timestamps, level | +| Field | What it tells you | +|-------|-------------------| +| `room_type` | Modelled room (`clinic_office` / `welfare_office` / `open_office`) | +| `device` | Microphone profile applied (`pi_budget_mic`) | +| `ir_source` | How the room IR was generated (currently always `pyroomacoustics_ism`) | +| `snr_db_actual` | Measured speech-to-noise ratio in dB **after** mixing — your ground truth for SNR-stratified eval | +| `speaker_distance_meters` | Simulated distance from speaker to microphone | +| `background_events` | List of non-speech acoustic events: `hvac_hum` (constant low-level), `ACOU_SLAM` / `ACOU_FALL` (brief, high-level) | -??? info "What is `pyroomacoustics_ism`?" - The image-source method (ISM) is an algorithm for computing room impulse responses by reflecting a virtual point source off the room's walls. `pyroomacoustics` is a Python library that implements it. +!!! info "`ACOU_*` events are double-recorded" + Each `ACOU_SLAM` / `ACOU_FALL` event lives in **both** `acoustic_scene.background_events` (with `level_db` mixing metadata) **and** the `.jsonl` strong-label file (as a regular `EventLabel` with `tier1_category: "ACOU"`). The two views are deliberate — the first carries audio-level provenance, the second is the supervision target. If you train an event detector, use the `.jsonl` view. + +--- + +## Speaker pair - The resulting IR simulates how sound travels from a speaker to a microphone in a room of specified dimensions and surface absorption coefficients — giving the audio the characteristic reverb of the target room type without recording in a real room. +One pair in delivery-003. Roles match Israeli welfare/clinic demographics: BEN (client/service-user, male) + SW (social worker, female). -??? info "Background event types" - | Type | Description | - |------|-------------| - | `hvac_hum` | Constant HVAC/ventilation hum (low level, full duration) | - | `ACOU_SLAM` | Door slam or hard object impact (brief, high level) | - | `ACOU_FALL` | Object falling or being thrown (brief, high level) | +Speaker directory: `data/he/ben_m_40-55_003/` - `ACOU_*` events are also tagged as `EventLabel` entries in the `.jsonl` strong labels with `tier1_category: "ACOU"`. This means they contribute to `weak_label.violence_categories` even in SV/IT clips where the primary violence is verbal or physical. +| Role | speaker_id | TTS voice | +|------|-----------|-----------| +| BEN | `BEN_M_40-55_003` | `he-IL-AvriNeural` | +| SW | `SW_F_30-45_001` | `he-IL-HilaNeural` | + +Both speakers use the Azure backend. See [Glossary — Speaker roles](glossary.md#speaker-roles) if `BEN` and `SW` are new abbreviations. --- ## Clips in delivery-003 -`data/he/ben_m_40-55_003/` +**8 clips · ~17 min · 4 violent (SV + IT), 4 non-violent (NEG + NEU) · all `room_type: clinic_office`, all `device: pi_budget_mic`, SNR ~11 dB** -| Clip ID | Typology | `has_violence` | Duration | SNR (dB) | -|---------|----------|:---:|------:|:---:| -| `el_sv_b_0001_00` | SV | ✓ | 2m 27.0s | ~11 | -| `el_sv_b_0002_00` | SV | ✓ | 2m 18.5s | ~11 | -| `el_it_b_0001_00` | IT | ✓ | 2m 30.0s | ~11 | -| `el_it_b_0002_00` | IT | ✓ | 2m 31.6s | ~11 | -| `el_neg_b_0001_00` | NEG | — | 1m 53.8s | ~11 | -| `el_neg_b_0002_00` | NEG | — | 2m 54.6s | ~11 | -| `el_neu_b_0001_00` | NEU | — | 1m 56.9s | ~11 | -| `el_neu_b_0002_00` | NEU | — | 1m 19.7s | ~11 | +??? abstract "Full clip listing" + All in `data/he/ben_m_40-55_003/`: -All 8 clips are Tier B with `device: pi_budget_mic` and `room_type: clinic_office`. + | Clip ID | Typology | violent | Duration | + |---------|----------|:---:|---------:| + | `el_sv_b_0001_00` | SV | ✓ | 2m 27.0s | + | `el_sv_b_0002_00` | SV | ✓ | 2m 18.5s | + | `el_it_b_0001_00` | IT | ✓ | 2m 30.0s | + | `el_it_b_0002_00` | IT | ✓ | 2m 31.6s | + | `el_neg_b_0001_00` | NEG | — | 1m 53.8s | + | `el_neg_b_0002_00` | NEG | — | 2m 54.6s | + | `el_neu_b_0001_00` | NEU | — | 1m 56.9s | + | `el_neu_b_0002_00` | NEU | — | 1m 19.7s | --- -## Loading Elephant clips +## Loading and inspecting an Elephant clip ```python -import json -import soundfile as sf -import numpy as np -import pandas as pd +import pandas as pd, soundfile as sf, json from pathlib import Path root = Path(".") df = pd.read_csv("data/he/manifest.csv") -el_clips = df[df["project"] == "elephant_in_the_room"] +el = df[df["project"] == "elephant_in_the_room"] # 8 rows -# Load audio + metadata for a Tier B clip -clip_id = "el_sv_b_0001_00" -wav, sr = sf.read(root / f"data/he/ben_m_40-55_003/{clip_id}.wav") -meta = json.loads((root / f"data/he/ben_m_40-55_003/{clip_id}.json").read_text()) +# Pick one clip +row = el.iloc[0] +wav, sr = sf.read(root / row.wav_path) +meta = json.loads((root / row.wav_path).with_suffix(".json").read_text()) -# Inspect acoustic scene +# Acoustic scene scene = meta["acoustic_scene"] -print(f"Room: {scene['room_type']} Device: {scene['device']} SNR: {scene['snr_db_actual']} dB") -# Room: clinic_office Device: pi_budget_mic SNR: 11.2 dB +print(f"{scene['room_type']} {scene['device']} SNR {scene['snr_db_actual']} dB " + f"dist {scene['speaker_distance_meters']} m") -# Find background acoustic events +# Background acoustic events for evt in scene["background_events"]: - print(f"{evt['type']}: {evt['onset']:.1f}s – {evt['offset']:.1f}s @ {evt['level_db']} dB") -# hvac_hum: 0.0s – 147.0s @ -37.4 dB -# ACOU_SLAM: 72.2s – 72.5s @ 9.9 dB -# ACOU_FALL: 97.6s – 98.5s @ 9.6 dB + print(f" {evt['type']:10s} {evt['onset']:6.1f}s – {evt['offset']:6.1f}s @ {evt['level_db']:+5.1f} dB") -# Get alert window (final 40%) +# Alert window (final 40%) — for sliding-window evaluation duration = meta["duration_seconds"] -alert_start = duration * 0.60 -print(f"Alert window: {alert_start:.1f}s – {duration:.1f}s") +alert_start = 0.60 * duration -# Filter strong labels to alert window only -events = [json.loads(l) for l in - (root / f"data/he/ben_m_40-55_003/{clip_id}.jsonl").read_text().splitlines()] +events = [json.loads(l) for l in (root / row.strong_labels_path).read_text().splitlines() if l.strip()] alert_events = [e for e in events if e["onset"] >= alert_start] +print(f"alert window: {alert_start:.1f}s – {duration:.1f}s " + f"{len(alert_events)} events fire in window " + f"(of {len(events)} total)") ``` --- -## Guidance for model training - -!!! warning "This is a toy corpus — not for production training" - 8 Elephant clips from 1 speaker pair in 1 room type is insufficient for training. This delivery exists to bootstrap your data pipeline and acoustic-scene parsing code. - -**High-precision orientation:** - -- **NEG clips are essential.** Your precision target means you must not fire on `el_neg_b_*` clips — intense speech in a clinic room with background noise, but no violence. Train hard against these. -- **The alert-in-final-40% window** is where violence events concentrate. Consider a sliding-window detector that scores the final portion of each clip more aggressively than the opening. -- **SNR is ~11 dB.** This is a realistic but challenging condition for acoustic feature extraction. Verify that your features (MFCCs, log-mel, etc.) are robust at this SNR before comparing with She-Proves Tier A results. - -**Tier B–specific features:** - -- `acoustic_scene.snr_db_actual` gives you the ground-truth SNR per clip — useful for SNR-conditioned training or evaluation stratification. -- `background_events` timestamps let you train event detectors separately from the speech violence detector. -- `acoustic_scene.room_type` will diversify across room types at scale (`clinic_office`, `welfare_office`, `open_office`). Future deliveries will include all three. - -**What delivery-003 doesn't cover:** +## Training-time notes (specific to this project) -- Only `clinic_office` room type (all 8 clips) -- Only one speaker pair (BEN_M_40-55_003 + SW_F_30-45_001) -- No test/val split (4 unique speakers total; all are `split: train`) -- SNR variation (all ~11 dB) +- **NEG clips are essential for precision.** `el_neg_b_*` is intense speech in a clinic room with background noise but no violence. If your detector fires on these, security stops trusting it. Train hard against these. +- **The alert-in-final-40% structure is exploitable.** Consider a sliding-window detector that biases toward the back half of each clip — or use the window structure as a positional feature. Don't reward early firing. +- **SNR ~11 dB is challenging.** Verify your features (MFCCs, log-mel, etc.) are robust here before comparing with She-Proves Tier A results. SNR is recorded per clip (`acoustic_scene.snr_db_actual`) — use it for SNR-stratified eval. +- **`ACOU_*` events double as strong labels.** You can train an event detector on `ACOU_SLAM` / `ACOU_FALL` separately from the speech-violence detector and ensemble them. +- **What delivery-003 *doesn't* cover:** only `clinic_office`, only one speaker pair (BEN+SW), only Azure backend, SNR essentially constant at ~11 dB. Plan for room diversity, SNR stratification, and speaker-disjoint splits when scaling. -Plan for room-type diversity, SNR stratification, and speaker disjoint splits at scale. +!!! warning "Still a small test batch" + 8 clips, 1 room type, 1 speaker pair, 1 SNR is enough to wire up data loaders and acoustic-scene parsing. It is not enough to train a production model. Build the plumbing; wait for the real batch. diff --git a/docs/getting-started.md b/docs/getting-started.md index 0310028..a7f9cbb 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,183 +1,163 @@ -# Getting Started +# Start here -This guide walks through loading and using clips from the corpus in Python. All paths are relative to the repository root. +Your first 10 minutes with the corpus. By the end you'll have cloned it, verified the clone, loaded one clip with its labels, and seen what's in a transcript. -## Prerequisites +--- + +## 1. Clone ```bash -pip install soundfile numpy pandas pydantic +git clone https://github.com/DataHackIL/avdp-synth-corpus.git +cd avdp-synth-corpus ``` -??? note "Optional: full SynthBanshee schema" - If you want strict Pydantic validation against the full `ClipMetadata` schema: - ```bash - git clone https://github.com/DataHackIL/SynthBanshee - cd SynthBanshee && pip install -e . - ``` - This gives you `from synthbanshee.labels.schema import ClipMetadata` and `validate_clip()`. - For most DS workflows, plain `json.loads()` is sufficient. +No Git LFS. Total size is a few hundred megabytes for delivery-003 — the audio lives in `data/he/`, the SSML caches live in `assets/`. -## Clone the corpus +--- + +## 2. Verify the clone ```bash -git clone https://github.com/DataHackIL/avdp-synth-corpus.git -cd avdp-synth-corpus +find data/he -name "*.wav" | wc -l # expect 20 +wc -l data/he/manifest.csv # expect 21 (header + 20 rows) ``` -The repository contains the audio files directly (no LFS). Total size is moderate — `data/he/` is roughly a few hundred MB for delivery-003. +If those numbers don't match, the clone is incomplete — `git lfs pull` is not the answer (we don't use LFS). Re-clone. --- -## Load a single clip +## 3. Install the minimal Python deps + +```bash +pip install soundfile numpy pandas +``` + +That's enough for everything on this page. `pydantic` is only needed if you want strict schema validation; `jsonlines` only if you prefer it to the one-liner that reads `.jsonl` directly. + +??? note "When you'd want the full SynthBanshee install" + If you want `from synthbanshee.labels.schema import ClipMetadata` for strict Pydantic validation, or `synthbanshee qa-report` to re-run QA over the data directory: + ```bash + git clone https://github.com/DataHackIL/SynthBanshee + cd SynthBanshee && pip install -e . + ``` + For consuming the corpus, `json.loads()` is fine and is what the examples below use. + +--- + +## 4. Load one clip end-to-end + +The path on disk is **lowercase** even though the speaker ID in JSON is **UPPERCASE** — that's a [Gotcha #4](gotchas.md#4-uppercase-in-json-lowercase-on-disk). ```python import json from pathlib import Path import soundfile as sf -import numpy as np - -root = Path(".") # run from repo root +root = Path(".") # repo root +clip_dir = root / "data/he/agg_m_30-45_001" clip_id = "sp_sv_a_0001_00" -speaker_dir = root / "data/he/agg_m_30-45_001" -# --- Audio --- -wav, sr = sf.read(speaker_dir / f"{clip_id}.wav") -# wav: float64 array, shape (N,). sr: always 16000. +# Audio +wav, sr = sf.read(clip_dir / f"{clip_id}.wav") +assert sr == 16000 and wav.ndim == 1 # always 16 kHz mono -print(f"Duration: {len(wav)/sr:.1f}s Sample rate: {sr} Peak: {np.abs(wav).max():.4f}") -# Duration: 110.5s Sample rate: 16000 Peak: 0.7943 - -# --- Weak labels (ClipMetadata) --- -meta = json.loads((speaker_dir / f"{clip_id}.json").read_text()) -wl = meta["weak_label"] -print(f"Typology: {meta['violence_typology']} has_violence: {wl['has_violence']} " - f"max_intensity: {wl['max_intensity']}") -# Typology: SV has_violence: True max_intensity: 5 - -# --- Transcript --- -transcript = (speaker_dir / f"{clip_id}.txt").read_text(encoding="utf-8") -print(transcript[:200]) # Hebrew turns with timestamps +print(f"duration={len(wav)/sr:.1f}s peak={abs(wav).max():.3f}") +# duration=110.5s peak=0.794 ``` -??? info "Why is the peak ~0.79 (–2.0 dBFS) not 1.0?" - All clips are peak-normalized to a **–2.0 dBFS target** (not –1.0 dBFS = 1.0 linear). - This gives 2 dB of headroom above the safety limiter ceiling (–1.0 dBFS). - `preprocessing_applied.normalized_dbfs` in the JSON records the measured peak. - See [Audio Format](audio-format.md) for the full normalization pipeline. - ---- +!!! info "Why is the peak 0.794 and not 1.0?" + Clips are normalized to a **–2.0 dBFS peak target**, which is roughly 0.79 linear amplitude. Use `generation_metadata.loudness_target_peak_dbfs` to read the configured target and `preprocessing_applied.normalized_dbfs` to read the measured output peak. Full detail: [Audio Format](audio-format.md). -## Load strong-label events +Clip-level labels (weak labels): ```python -import jsonlines # pip install jsonlines +meta = json.loads((clip_dir / f"{clip_id}.json").read_text()) +wl = meta["weak_label"] +print(f"typology={meta['violence_typology']} has_violence={wl['has_violence']} " + f"intensity_max={wl['max_intensity']} categories={wl['violence_categories']}") +# typology=SV has_violence=True intensity_max=5 categories=['DIST', 'PHYS', 'VERB'] +``` -events = [] -with jsonlines.open(speaker_dir / f"{clip_id}.jsonl") as reader: - for evt in reader: - events.append(evt) +Event-level labels (strong labels): -# Or without jsonlines: +```python events = [ json.loads(line) - for line in (speaker_dir / f"{clip_id}.jsonl").read_text().splitlines() + for line in (clip_dir / f"{clip_id}.jsonl").read_text().splitlines() if line.strip() ] for evt in events[:3]: - print(f"[{evt['onset']:.1f}s – {evt['offset']:.1f}s] " - f"{evt['tier1_category']}/{evt['tier2_subtype']} I{evt['intensity']}") -# [0.8s – 10.1s] VERB/VERB_SHOUT I2 -# [10.5s – 18.7s] VERB/VERB_SHOUT I2 -# [18.3s – 29.7s] VERB/VERB_THREAT I3 + print(f"[{evt['onset']:5.1f}s – {evt['offset']:5.1f}s] " + f"{evt['speaker_role']} {evt['tier1_category']}/{evt['tier2_subtype']} " + f"I{evt['intensity']}") +# [ 0.8s – 10.1s] AGG VERB/VERB_SHOUT I2 +# [ 10.5s – 18.7s] VIC VERB/VERB_SHOUT I2 +# [ 18.3s – 29.7s] AGG VERB/VERB_THREAT I3 ``` -??? info "What are tier1_category and tier2_subtype?" - Strong labels follow a three-level taxonomy: +The full 14-event escalation arc for this clip is the one visualised on the [home page](index.md#see-it-first) — verbal → distress → physical → settle. + +--- + +## 5. Read a transcript - **Typology** (clip-level): `SV` · `IT` · `NEG` · `NEU` +`.txt` files are turn-major with a small header block per turn. They use UTF-8 Hebrew and are intended both for human reading and as ASR reference. - **Tier 1 category** (event-level): `VERB` · `DIST` · `PHYS` · `EMOT` · `ACOU` · `NONE` +``` +[CLIP_ID: sp_sv_a_0001_00] +[SPEAKER: AGG_M_30-45_001 | ROLE: AGG | ONSET: 0.76 | OFFSET: 10.07] +מה זה הארוחה הזאת? שאלתי אותך דבר אחד פשוט, לעשות ארוחת ערב נורמלית. +[ACTION: VERB_SHOUT | INTENSITY: 2] +[SPEAKER: VIC_F_25-40_002 | ROLE: VIC | ONSET: 10.49 | OFFSET: 18.74] +עבדתי עד שש היום. עשיתי מה שהספקתי... +``` - **Tier 2 subtype** (event-level): e.g. `VERB_SHOUT`, `VERB_THREAT`, `DIST_SCREAM`, `PHYS_HARD`, `ACOU_SLAM` +!!! note "Hebrew is right-to-left; some terminals mis-render it" + macOS Terminal.app handles it correctly; older Windows consoles don't. If transcripts look reversed or garbled, view the `.txt` in an editor (VS Code, BBEdit) rather than `cat`. - See [Label Taxonomy](taxonomy.md) for the full table and has_violence derivation rule. +Timestamps in the header are already relative to the **final processed WAV** — they include the 0.5 s silence pad at the head. No shift needed. --- -## Work with the manifest +## 6. Work from the manifest, not from hardcoded paths -`data/he/manifest.csv` is a flat summary of all clips. It's the fastest entry point for filtering and dataset construction. +`data/he/manifest.csv` is one row per clip. It's the fastest entry point for filtering and the safest way to find files (because [hardcoded speaker directories will miss two-thirds of the clips](gotchas.md#2-dont-hardcode-speaker-directory-paths)). ```python -import pandas as pd +import pandas as pd, soundfile as sf df = pd.read_csv("data/he/manifest.csv") -print(df.columns.tolist()) +df.columns.tolist() # ['clip_id', 'project', 'violence_typology', 'tier', 'duration_seconds', # 'speaker_ids', 'voice_families', 'has_violence', 'max_intensity', # 'quality_flags', 'split', 'wav_path', 'strong_labels_path'] -# Filter by project -she_proves_clips = df[df["project"] == "she_proves"] +# Filter +violent = df[df["has_violence"]] # 10 clips +elephant = df[df["project"] == "elephant_in_the_room"] # 8 clips +sv_high = df[(df["violence_typology"] == "SV") & (df["max_intensity"] >= 4)] -# Filter by typology -sv_clips = df[df["violence_typology"] == "SV"] - -# High-intensity violent clips only -high_intensity = df[(df["has_violence"]) & (df["max_intensity"] >= 4)] - -# Load audio for a manifest row +# Load audio for any manifest row — wav_path is already repo-relative POSIX row = df.iloc[0] -wav, sr = sf.read(row["wav_path"]) # paths are repo-relative POSIX strings -``` - -!!! warning "`speaker_ids` and `voice_families` are pipe-delimited" - These columns contain multiple values joined by `|`: - ```python - speakers = row["speaker_ids"].split("|") - # ['AGG_M_30-45_001', 'VIC_F_25-40_002'] - ``` - -!!! note "All clips are `split: train` in delivery-003" - The corpus has only 4 unique speaker personas across 20 clips — speaker-disjoint splits are not feasible at this scale. When the corpus scales, speaker-disjoint train/val/test splits will be assigned by SynthBanshee. Until then, treat this as an unpartitioned pool. - ---- - -## Find a clip's speaker directory - -Clip IDs follow the pattern `{project_prefix}_{typology}_{tier}_{scene_num}_{take}`. The on-disk directory is the **lowercase** form of the first speaker ID listed in `speakers[]`: - -```python -def clip_dir(root: Path, clip_id: str, meta: dict) -> Path: - first_speaker = meta["speakers"][0]["speaker_id"] - return root / "data" / meta["language"] / first_speaker.lower() +wav, sr = sf.read(row["wav_path"]) +speakers = row["speaker_ids"].split("|") # pipe-delimited! +voices = row["voice_families"].split("|") # same order as speaker_ids ``` -| clip_id | speaker_dir | -|---------|-------------| -| `sp_sv_a_0001_00` | `data/he/agg_m_30-45_001/` | -| `sp_sv_a_0003_00` | `data/he/agg_m_30-45_002/` | -| `el_sv_b_0001_00` | `data/he/ben_m_40-55_003/` | - -Or use `manifest.csv` directly — `wav_path` already contains the full repo-relative path. +!!! warning "`speaker_ids` and `voice_families` are pipe-delimited strings" + They are not CSV-nested lists. Split on `|`. --- -## Validate a clip - -If you have SynthBanshee installed: - -```bash -synthbanshee validate data/he/agg_m_30-45_001/sp_sv_a_0001_00.wav -``` - -This checks: all four files present, WAV format (16 kHz mono), peak ≤ –1.0 dBFS, duration ≥ 3 s, JSON parses as `ClipMetadata`. - -To run QA over the entire language directory: - -```bash -synthbanshee qa-report data/he/ -synthbanshee qa-report data/he/ --run-summary # adds corpus-level aggregates -``` +## 7. Where to go next + +| You're about to… | Read | +|------------------|------| +| Write data-loading code | [Common mistakes](gotchas.md) (2 min read; saves debugging) | +| Look up a field in `.json` | [Schema Reference](schema.md) | +| Understand `has_violence` semantics | [Label Taxonomy](taxonomy.md) | +| Look up a term (F0, SSML, IR, BEN…) | [Glossary](glossary.md) | +| Work specifically with phone-app data | [She-Proves guide](she-proves.md) | +| Work specifically with Tier B / room-augmented audio | [Elephant in the Room guide](elephant.md) | +| Verify a clip is spec-compliant | `synthbanshee validate ` (requires SynthBanshee installed) | diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000..7a18b7e --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,110 @@ +# Glossary + +Abbreviations and jargon that show up across the corpus and on this site, in one place. + +--- + +## Speaker roles + +The role of each speaker is encoded in the speaker_id prefix and in `speakers[].role`. + +| Code | Stands for | Used in | +|------|-----------|---------| +| `AGG` | **Aggressor** — the perpetrator in a domestic-violence scene | She-Proves clips (`AGG_M_30-45_*`) | +| `VIC` | **Victim** — the target of violence in a domestic-violence scene | She-Proves clips (`VIC_F_25-40_*`) | +| `BEN` | **Beneficiary / client** — a service-user in a welfare or clinic setting (the threatening party in Elephant scenes) | Elephant clips (`BEN_M_40-55_*`) | +| `SW` | **Social Worker** — the threatened professional in Elephant scenes | Elephant clips (`SW_F_30-45_*`) | + +The role determines the prosody profile, scene position, and which `tier1_category` events the speaker can produce. + +--- + +## Project codes + +| Code | Project | Clip ID prefix | +|------|---------|----------------| +| `she_proves` | She-Proves smartphone app | `sp_*` | +| `elephant_in_the_room` | Elephant in the Room (clinic/welfare device) | `el_*` | + +--- + +## Violence typology + +The clip-level `violence_typology` field — not an ordered scale. See [Label Taxonomy](taxonomy.md) for details. + +| Code | Stands for | +|------|------------| +| `SV` | Severe Violence | +| `IT` | Intimate Terrorism | +| `NEG` | Negative confusor (sounds intense, no violence) | +| `NEU` | Neutral | + +--- + +## Tier 1 event category + +The event-level `tier1_category` field on each `EventLabel`. + +| Code | Stands for | +|------|------------| +| `VERB` | Verbal violence (shouting, threats, insults) | +| `DIST` | Distress vocalisations (screaming, crying under duress) | +| `PHYS` | Physical violence cues (impact sounds, struggle) | +| `EMOT` | Emotional manipulation (gaslighting, guilt-tripping) | +| `ACOU` | Acoustic non-vocal events (slams, falls) | +| `NONE` | Ambient / neutral / no violence cue | + +--- + +## Tier codes + +| Code | Meaning | +|------|---------| +| `A` | Clean audio — no room IR, no device profile, no background noise | +| `B` | Room IR + device profile + background noise injection | + +--- + +## Audio jargon + +| Term | Meaning | +|------|---------| +| **F0** | Fundamental frequency — the lowest frequency of a periodic signal; for voice, the pitch. Reported per speaker in some QA outputs. | +| **dBFS** | Decibels relative to full scale — 0 dBFS is the maximum amplitude representable by the format; –2 dBFS is ~80% of full amplitude. | +| **Peak normalization** | Applying a single gain to the whole signal so its absolute maximum matches a target level. | +| **RMS** | Root-mean-square — a measure of average signal energy. SynthBanshee uses per-turn RMS gain to enforce the loudness gradient between calm and escalated turns. | +| **SNR** | Signal-to-noise ratio — speech level minus background-noise level, in dB. Recorded in `acoustic_scene.snr_db_actual` for Tier B clips. | +| **IR** | Impulse response — a recording of how a room (or microphone, or speaker) responds to an idealised pulse. Convolving clean speech with a room IR makes it sound like it was recorded in that room. | +| **ISM** | Image-source method — an algorithm for synthetically generating room IRs by reflecting virtual sound sources off room walls. Implemented by `pyroomacoustics`. | +| **SSML** | Speech Synthesis Markup Language — an XML dialect that controls TTS output (pitch, rate, emphasis, breaks, voice). Azure and Google both accept SSML. | +| **TTS** | Text-to-speech — the generation of audio from a text prompt. | +| **Prosody** | The patterns of stress, intonation, pitch, and rate that make speech expressive (vs. flat). | +| **Prosody cap** | A safety clamp applied by SynthBanshee to LLM-suggested prosody values to prevent unnatural extremes (pitch ≤ +2 st, rate ∈ [0.85, 1.20]). | +| **Whisper** | OpenAI's open-weight ASR model, used internally as a sanity check that synthesised audio is still transcribable. | + +--- + +## Pipeline / corpus jargon + +| Term | Meaning | +|------|---------| +| **Dirty file** | The pre-preprocessing WAV (raw TTS-mixer output, before normalization and padding). Retained under `assets/speech/dirty/{clip_id}_dirty.wav`. | +| **Generation metadata** | The `generation_metadata` field — pipeline provenance: which TTS backend was used, which voice family, what mix mode, etc. | +| **Manifest** | The flat CSV summary at `data/he/manifest.csv` — one row per clip, columns for filtering. | +| **Strong labels** | Event-level labels in `.jsonl` files — one `EventLabel` object per labelled event, with onset/offset/category. | +| **Weak labels** | Clip-level summary labels in `.json` — `has_violence`, `max_intensity`, `violence_typology`, `violence_categories`. | +| **Quality flag** | A soft warning in `quality_flags` (e.g. `emotion_downgrade`). Doesn't fail validation; flags audio worth a second look. | +| **Delivery** | A merged data batch under `deliveries/{slug}/`. Each delivery records its SynthBanshee commit, metadata, and per-batch QA notes. | + +--- + +## Hebrew TTS voice IDs + +The four voices used in delivery-003: + +| Voice ID | Gender | Backend | +|----------|:---:|---------| +| `he-IL-AvriNeural` | M | Azure | +| `he-IL-HilaNeural` | F | Azure | +| `he-IL-Chirp3-HD-Achird` | M | Google Chirp 3 HD | +| `he-IL-Chirp3-HD-Achernar` | F | Google Chirp 3 HD | diff --git a/docs/gotchas.md b/docs/gotchas.md new file mode 100644 index 0000000..816c90b --- /dev/null +++ b/docs/gotchas.md @@ -0,0 +1,136 @@ +# Common mistakes + +Read this once before you write code against the corpus. Two minutes here saves a debugging session later. + +--- + +## 1. Don't derive `has_violence` from typology + +This will misclassify every `NEG` clip: + +```python +# WRONG — NEG clips will look violent because of their max_intensity +has_violence = typology in ("SV", "IT") + +# CORRECT — uses the event-level ground truth +has_violence = any(e["tier1_category"] != "NONE" for e in events) +``` + +`has_violence` in `weak_label` is **derived from strong-label events**, not from typology. NEG clips can have `max_intensity = 3` (raised voices, distress) and still be `has_violence: false` because every one of their events lands `tier1_category: "NONE"` by design. That's the whole point of NEG: hard negatives that sound intense but aren't violent. + +--- + +## 2. Don't hardcode speaker directory paths + +There's already more than one. Delivery-003 has three speaker directories under `data/he/`: + +``` +data/he/agg_m_30-45_001/ # She-Proves, Azure pair +data/he/agg_m_30-45_002/ # She-Proves, Google Chirp HD pair (new in delivery-003) +data/he/ben_m_40-55_003/ # Elephant in the Room, Azure pair (new in delivery-003) +``` + +Code that hardcodes `data/he/agg_m_30-45_001/` will miss two-thirds of the clips. Use `manifest.csv` (the `wav_path` column is repo-relative POSIX), or derive the directory from the first entry in `speakers[]`: + +```python +speaker_dir = root / "data" / meta["language"] / meta["speakers"][0]["speaker_id"].lower() +``` + +--- + +## 3. Audio peak is ~0.79, not 1.0 + +Clips are normalized to a **–2.0 dBFS peak target** (not –1.0 dBFS = linear 1.0). Loading a clip and expecting full-range float values will surprise you: + +```python +wav, sr = sf.read("data/he/agg_m_30-45_001/sp_sv_a_0001_00.wav") +print(np.abs(wav).max()) # ~0.7943, not 1.0 +``` + +The –2 dBFS target leaves 2 dB of headroom above the safety limiter at –1.0 dBFS. The configured target is recorded in `generation_metadata.loudness_target_peak_dbfs`; the measured peak is recorded in `preprocessing_applied.normalized_dbfs`. + +--- + +## 4. UPPERCASE in JSON, lowercase on disk + +The same speaker has two surface forms: + +| Surface | Form | Example | +|---------|------|---------| +| JSON field (`speaker_id`, `speakers[].speaker_id`) | **UPPERCASE** | `AGG_M_30-45_001` | +| Filesystem directory | **lowercase** | `agg_m_30-45_001/` | +| `clip_id` (everywhere) | **lowercase** | `sp_sv_a_0001_00` | + +If you build a dict keyed on speaker IDs from JSON and then try to look up paths with the same string, you'll get a `FileNotFoundError`. Always `.lower()` when converting from JSON to a path. + +--- + +## 5. NEG is not "violent at low intensity" + +The four violence typologies are **not** an ordered scale. + +| | | +|---|---| +| `SV` | Severe Violence — physical attacks, life-threatening | +| `IT` | Intimate Terrorism — sustained coercive control, repeated abuse | +| `NEG` | **Negative confusor** — sounds intense, no violence (hard negative) | +| `NEU` | Neutral — mundane conversation | + +A NEG clip is **not** "a milder SV." It is acoustic distress that a naive model would mistake for violence. Treating NEG as a positive class will tank your precision. + +--- + +## 6. All clips are `split: train` in delivery-003 + +The `split` column exists in `manifest.csv`, but there are only 4 unique speaker personas across all 20 clips. Speaker-disjoint train/val/test partitioning isn't feasible at this scale — every clip is therefore assigned `split: train`. **Don't trust the `split` column as a usable partition.** Treat the whole corpus as an unpartitioned pool for now. SynthBanshee will assign meaningful splits once the speaker pool grows. + +--- + +## 7. `quality_flags` doesn't mean "broken" + +About 15 of 20 clips in delivery-003 carry at least one `quality_flags` entry — usually `emotion_downgrade` (the TTS produced slightly less intense prosody than the SSML asked for at high-intensity turns). These clips are still validated and spec-compliant; the flag is a soft hint, not a failure. Don't filter them out reflexively. + +The hard line is `synthbanshee validate` — a clip either passes or doesn't. If it's in the corpus, it passed. + +--- + +## 8. The 2 Google clips have a `vic_f0_high` flag — that's expected + +`sp_sv_a_0003_00` and `sp_it_a_0003_00` use the Google Chirp 3 HD female voice (`he-IL-Chirp3-HD-Achernar`), whose fundamental-frequency baseline runs higher than the Azure reference voice the QA thresholds were calibrated against. The flag is fired correctly; the audio is fine. **Don't exclude these clips on the basis of this flag** — your model needs the backend diversity. If you compute F0-derived features, calibrate per backend. + +--- + +## 9. Timestamps already account for silence padding + +Every clip has ≥0.5 s of silence at head and tail. **Onset/offset timestamps in `.txt` and `.jsonl` are already shifted** to refer to positions in the final processed WAV. You don't need to add the pad — read the timestamp, slice the WAV, done. + +--- + +## 10. The `.json` and `.jsonl` files aren't the same thing + +| File | Contains | When to load | +|------|----------|--------------| +| `{clip_id}.json` | `ClipMetadata` — one object per clip: weak labels, speakers, provenance, acoustic scene | Always | +| `{clip_id}.jsonl` | `EventLabel` records — one JSON object per **line**, one per labelled event in the clip | When you need per-event strong labels (onset/offset/category) | + +If you `json.loads()` the `.jsonl` you'll get an error. Read line by line. + +--- + +## Quick verification + +Use these snippets to confirm a fresh clone is intact: + +```bash +find data/he -name "*.wav" | wc -l # expect 20 +wc -l data/he/manifest.csv # expect 21 (header + 20 rows) +``` + +```python +import pandas as pd +df = pd.read_csv("data/he/manifest.csv") +assert len(df) == 20 +assert set(df["tier"]) == {"A", "B"} +assert set(df["violence_typology"]) == {"SV", "IT", "NEG", "NEU"} +assert df["has_violence"].sum() == 10 # 5 SV + 5 IT +``` diff --git a/docs/index.md b/docs/index.md index 7b5b4d2..aa280c8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,127 +1,82 @@ -# avdp-synth-corpus +# AVDP Synthetic Corpus -**Synthetic Hebrew audio corpus for the Audio Violence Detection Pipeline (AVDP)** +**Synthetic Hebrew audio clips for the Audio Violence Detection Pipeline.** +Hebrew (he-IL) · 16 kHz mono 16-bit PCM · generated by [SynthBanshee](https://github.com/DataHackIL/SynthBanshee). -Generated by [SynthBanshee](https://github.com/DataHackIL/SynthBanshee) · Hebrew (he-IL) · 16 kHz mono 16-bit PCM +delivery 003 · 2026-05-12 · provisional +20 clips · ~41.6 min · `she_proves` (12) + `elephant_in_the_room` (8) · Azure (18) + Google (2) · 0 validation failures. --- -!!! warning "Toy corpus — not approved for model training" - All current deliveries are provisional wet-test batches for spec validation and pipeline bootstrapping. - The `split` field in `manifest.csv` is informational only. **Do not train production models on this data.** - See [Deliveries](deliveries.md) for the full status of each batch. +## See it first ---- - -## What is this? - -This repository contains **synthetic Hebrew audio clips** representing domestic-violence and threat scenarios, produced by a text-to-speech pipeline with automatic prosody modelling and acoustic augmentation. +A real clip from the corpus — Severe Violence scene, two speakers, with strong-label events overlaid on the waveform: -Two downstream products consume this data: +![Waveform of sp_sv_a_0001_00 with event boundaries](assets/sp_sv_a_0001_00_waveform.png) -=== "She-Proves" +You can read the typical escalation arc directly: an argument starts as verbal (`VERB`, blue), peaks into distress vocalisations (`DIST`, orange) around 36s, then into physical-violence cues (`PHYS`, red) around 71s. Intensity badges (`I2` → `I5`) follow the same curve. - A smartphone app that passively monitors audio for domestic violence incidents and preserves evidence for legal use. High-recall orientation — better to flag and review than to miss. - - → [She-Proves team guide](she-proves.md) +--- -=== "Elephant in the Room" +## Load a clip in 4 lines - A Raspberry Pi–class device placed in clinic and welfare offices that alerts security when a social worker is under threat. High-precision orientation — false alarms erode trust. +```python +import json, soundfile as sf +wav, sr = sf.read("data/he/agg_m_30-45_001/sp_sv_a_0001_00.wav") +meta = json.loads(open("data/he/agg_m_30-45_001/sp_sv_a_0001_00.json").read()) +print(f"{len(wav)/sr:.1f}s has_violence={meta['weak_label']['has_violence']} " + f"intensity_max={meta['weak_label']['max_intensity']}") +# 110.5s has_violence=True intensity_max=5 +``` - → [Elephant in the Room team guide](elephant.md) +For everything else: [Start here →](getting-started.md) --- -## Current delivery at a glance +## Two consumer teams -**Delivery 003 — multi-project, multi-voice** · 2026-05-12 · provisional +
-| Dimension | Value | -|-----------|-------| -| Clips | 20 | -| Total duration | ~41.6 min | -| Projects | `she_proves` (12 clips) + `elephant_in_the_room` (8 clips) | -| Tiers | A — clean (12) + B — room-augmented (8) | -| TTS backends | Azure (18 clips) + Google Chirp 3 HD (2 clips) | -| Validation failures | 0 / 20 | -| Pipeline | SynthBanshee `0.1.0` @ [`1ea48f3`](https://github.com/DataHackIL/SynthBanshee/commit/1ea48f3) | +
+
smartphone app
+### She-Proves +Passively monitors a phone for domestic-violence incidents and preserves audio evidence for legal use. **High-recall** orientation — better to flag and review than to miss. -Full breakdown: [Deliveries](deliveries.md) · [She-Proves clips](she-proves.md#clips-in-delivery-003) · [Elephant clips](elephant.md#clips-in-delivery-003) +12 clips · Tier A (clean audio) · scenes 3–6 min · phone-pocket device profile. ---- +[She-Proves guide →](she-proves.md){ .card-link } +
-## Repository layout +
+
raspberry pi · clinic / welfare office
+### Elephant in the Room +A Pi-class device that alerts security when a social worker is under threat. **High-precision** orientation — false alarms erode trust. -``` -data/ - he/ # ISO 639-1 language code - {speaker_dir}/ # e.g. agg_m_30-45_001/ (lowercase of first speaker ID) - {clip_id}.wav # 16 kHz mono 16-bit PCM - {clip_id}.txt # per-turn transcript with onset/offset markers - {clip_id}.json # ClipMetadata (weak labels, provenance, speaker info) - {clip_id}.jsonl # EventLabel records — one JSON object per line - manifest.csv # flat summary of all clips under data/he/ - -assets/ - speech/ # SHA-256-keyed per-utterance WAV cache (do not modify) - dirty/ # pre-preprocessing WAVs, retained per spec - scripts/ # SHA-256-keyed LLM script cache (do not modify) - -deliveries/ - {slug}/ - metadata.yaml # structured delivery record - notes.md # narrative QA notes and known limitations - qa-report.json # synthbanshee qa-report output -``` +8 clips · Tier B (room IR + budget mic + noise) · scenes 1–4 min · alert in final 40%. -??? info "Why are there four files per clip?" - - **`.wav`** — the audio, spec-compliant (normalized, padded, validated) - - **`.txt`** — the transcript with turn-level onset/offset markers, used as ASR reference - - **`.json`** — `ClipMetadata`: weak labels (`has_violence`, `max_intensity`), speaker list, acoustic scene, provenance (`generation_metadata`) - - **`.jsonl`** — `EventLabel` records: one line per strong-label event with category, subtype, onset, offset, intensity, emotional state +[Elephant guide →](elephant.md){ .card-link } +
- You only need `.wav` + `.json` for most training pipelines. Add `.jsonl` when you need per-event strong labels or onset/offset supervision. +
--- -## Where to start +## Where to go -| I want to… | Go to | -|------------|-------| -| Load my first clip in Python | [Getting Started → Load a clip](getting-started.md#load-a-single-clip) | -| Understand what the labels mean | [Label Taxonomy](taxonomy.md) | -| Parse `ClipMetadata` with Pydantic | [Schema Reference](schema.md) | -| Work with She-Proves scenes | [She-Proves guide](she-proves.md) | -| Work with Elephant Tier B audio | [Elephant in the Room guide](elephant.md) | -| Understand the audio normalization | [Audio Format](audio-format.md) | -| Check current quality status | [Deliveries](deliveries.md) | +| | | +|---|---| +| **First time here** | [Start here](getting-started.md) — clone, load one clip, read its labels | +| **About to write code** | [Common mistakes](gotchas.md) — read this once; it'll save you a few | +| **Decoding a label** | [Label Taxonomy](taxonomy.md) — typologies, categories, `has_violence` rule | +| **Decoding a JSON field** | [Schema Reference](schema.md) — annotated `ClipMetadata` example | +| **Working with team data** | [She-Proves](she-proves.md) · [Elephant](elephant.md) | +| **Looking up a term** | [Glossary](glossary.md) — F0, SSML, IR, AGG/VIC/SW/BEN, etc. | +| **Checking what's current** | [Deliveries](deliveries.md) — current batch, known gaps | --- -## Quick snippet +!!! warning "This is a small test batch, not training data" + All current deliveries are preview batches for verifying that downstream data-loading code works before the full dataset arrives. The `split` column in `manifest.csv` is informational only — all 20 clips are `split: train` because there aren't enough unique speakers for a disjoint partition at this scale. **Do not train production models on this corpus.** -```python -import json -from pathlib import Path -import soundfile as sf - -root = Path(".") # repo root - -# Load a clip -wav, sr = sf.read(root / "data/he/agg_m_30-45_001/sp_sv_a_0001_00.wav") -meta = json.loads((root / "data/he/agg_m_30-45_001/sp_sv_a_0001_00.json").read_text()) - -print(f"Duration: {len(wav)/sr:.1f}s has_violence: {meta['weak_label']['has_violence']}") -# Duration: 110.5s has_violence: True -``` - -For manifest-level operations: - -```python -import pandas as pd - -df = pd.read_csv("data/he/manifest.csv") -violent = df[df["has_violence"] == True] -print(violent[["clip_id", "project", "violence_typology", "duration_seconds"]].to_string()) -``` +!!! info "What's *not* in this corpus" + No real human recordings (synthetic TTS only) · no Arabic or English (Hebrew only) · no inter-annotator agreement metrics (labels are auto-generated by SynthBanshee) · no demographic detail beyond `gender` + `age_range`. Scripts are LLM-generated in Hebrew, not human-written. See [Glossary](glossary.md) for what each abbreviation means. diff --git a/docs/schema.md b/docs/schema.md index 98a10fd..dccbf1e 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -1,219 +1,226 @@ # Schema Reference -Every clip's `.json` file contains a `ClipMetadata` object. The authoritative Pydantic model is in [SynthBanshee `synthbanshee/labels/schema.py`](https://github.com/DataHackIL/SynthBanshee/blob/main/synthbanshee/labels/schema.py). +A real `ClipMetadata` JSON, fully annotated. Click the `+` markers to jump to a field's explanation. Fields are ordered by how often you'll actually use them: top-level → labels → speakers → augmentation (Tier B only) → provenance (diagnostic, usually skip). ---- - -## Loading with Pydantic - -```python -from synthbanshee.labels.schema import ClipMetadata # requires SynthBanshee installed -from pathlib import Path - -meta = ClipMetadata.model_validate_json( - Path("data/he/agg_m_30-45_001/sp_sv_a_0001_00.json").read_text() -) -print(meta.clip_id, meta.violence_typology, meta.weak_label.has_violence) -# sp_sv_a_0001_00 SV True -``` - -Plain JSON (no SynthBanshee required): - -```python -import json -from pathlib import Path - -meta = json.loads(Path("data/he/agg_m_30-45_001/sp_sv_a_0001_00.json").read_text()) -``` +The authoritative Pydantic model lives in [SynthBanshee `synthbanshee/labels/schema.py`](https://github.com/DataHackIL/SynthBanshee/blob/main/synthbanshee/labels/schema.py). For day-to-day consumer work, `json.loads()` is fine. --- -## Top-level `ClipMetadata` fields - -| Field | Type | Description | -|-------|------|-------------| -| `clip_id` | `str` | Lowercase ASCII clip identifier, e.g. `sp_sv_a_0001_00` | -| `project` | `str` | `she_proves` or `elephant_in_the_room` | -| `language` | `str` | ISO 639-1, always `"he"` | -| `violence_typology` | `str` | `SV` / `IT` / `NEG` / `NEU` — see [taxonomy](taxonomy.md) | -| `tier` | `str` | `"A"` (clean) or `"B"` (room-augmented) | -| `duration_seconds` | `float` | Duration of the processed WAV | -| `sample_rate` | `int` | Always `16000` | -| `channels` | `int` | Always `1` | -| `is_synthetic` | `bool` | Always `true` in this corpus | -| `generator_version` | `str` | SynthBanshee semver, e.g. `"0.1.0"` | -| `generation_date` | `str` | ISO 8601 date of generation | -| `random_seed` | `int` | Scene-level RNG seed for reproducibility | -| `scene_config` | `str` | Relative path to the scene YAML in SynthBanshee | -| `transcript_path` | `str` | Repo-relative POSIX path to the `.txt` transcript | -| `dirty_file_path` | `str` | Repo-relative POSIX path to the pre-preprocessing WAV | -| `speakers` | `list[SpeakerInfo]` | Speaker metadata — see below | -| `weak_label` | `WeakLabel` | Clip-level summary labels | -| `generation_metadata` | `GenerationMetadata \| null` | Pipeline provenance — see below | -| `preprocessing_applied` | `PreprocessingApplied` | What preprocessing steps ran | -| `acoustic_scene` | `AcousticScene` | Room/device augmentation (Tier B) | -| `quality_flags` | `list[str]` | QA flags, e.g. `["emotion_downgrade"]` | -| `snr_db_estimated` | `float \| null` | Estimated SNR (not always populated) | -| `annotator_confidence` | `float` | Auto-label confidence, 0–1 (auto-generated: always `1.0`) | -| `iaa_reviewed` | `bool` | Whether inter-annotator agreement review was done | -| `she_proves_meta` | `null` | Reserved for She-Proves–specific metadata (future) | -| `elephant_meta` | `null` | Reserved for Elephant–specific metadata (future) | - ---- - -## `SpeakerInfo` - -One entry per speaker in `speakers[]`. - -| Field | Type | Description | -|-------|------|-------------| -| `speaker_id` | `str` | UPPERCASE persona ID, e.g. `AGG_M_30-45_001` | -| `role` | `str` | `AGG` (aggressor), `VIC` (victim), `SW` (social worker), `BEN` (beneficiary/client) | -| `gender` | `str` | `"male"` or `"female"` | -| `age_range` | `str` | e.g. `"30-45"` | -| `tts_voice_id` | `str` | TTS voice identifier, e.g. `"he-IL-AvriNeural"` | -| `voice_family` | `str` | Same as `tts_voice_id` (may diverge in future) | - -??? info "Speaker ID casing convention" - The `speaker_id` field in JSON is always **UPPERCASE**: `AGG_M_30-45_001`. - The on-disk directory is **lowercase**: `agg_m_30-45_001/`. - This is a deliberate per-surface casing rule — see [SynthBanshee spec §2.5](https://github.com/DataHackIL/SynthBanshee/blob/main/docs/spec.md#25-filename-constraints). - ---- - -## `WeakLabel` - -| Field | Type | Description | -|-------|------|-------------| -| `has_violence` | `bool` | `any(e.tier1_category != "NONE" for e in events)` — see [taxonomy](taxonomy.md#has_violence-the-correct-derivation) | -| `violence_typology` | `str` | Mirrors top-level `violence_typology` | -| `max_intensity` | `int` | Highest per-turn intensity across the clip (1–5) | -| `violence_categories` | `list[str]` | Distinct `tier1_category` values observed in events | - ---- +## Annotated example + +```json +{ + "clip_id": "sp_sv_a_0001_00", // (1)! + "project": "she_proves", // (2)! + "language": "he", // (3)! + "violence_typology": "SV", // (4)! + "tier": "A", // (5)! + "duration_seconds": 110.46, // (6)! + "sample_rate": 16000, // (7)! + "channels": 1, + "is_synthetic": true, // (8)! + + "weak_label": { // (9)! + "has_violence": true, + "violence_typology": "SV", + "max_intensity": 5, + "violence_categories": ["DIST", "PHYS", "VERB"] + }, + + "speakers": [ // (10)! + { + "speaker_id": "AGG_M_30-45_001", + "role": "AGG", + "gender": "male", + "age_range": "30-45", + "tts_voice_id": "he-IL-AvriNeural", + "voice_family": "he-IL-AvriNeural" + }, + { + "speaker_id": "VIC_F_25-40_002", + "role": "VIC", + "gender": "female", + "age_range": "25-40", + "tts_voice_id": "he-IL-HilaNeural", + "voice_family": "he-IL-HilaNeural" + } + ], + + "transcript_path": "data/he/agg_m_30-45_001/sp_sv_a_0001_00.txt", // (11)! + "dirty_file_path": "assets/speech/dirty/sp_sv_a_0001_00_dirty.wav", // (12)! + + "quality_flags": ["emotion_downgrade"], // (13)! + + "acoustic_scene": { // (14)! + "room_type": null, + "device": null, + "ir_source": null, + "snr_db_actual": null, + "speaker_distance_meters": null, + "background_events": [] + }, + + "preprocessing_applied": { // (15)! + "resampled_to_16k": true, + "downmixed_to_mono": true, + "normalized_dbfs": -2.0000002, + "silence_padded": true, + "denoised": true, + "spectral_filtered": true + }, + + "generation_metadata": { /* ...see below... */ }, // (16)! + + "generator_version": "0.1.0", // (17)! + "generation_date": "2026-05-12", + "random_seed": 1201, + "scene_config": "configs/scenes/she_proves/sp_sv_a_0001.yaml", + "snr_db_estimated": null, // (18)! + "annotator_confidence": 1.0, // (19)! + "iaa_reviewed": false, + "she_proves_meta": null, // (20)! + "elephant_meta": null +} +``` -## `GenerationMetadata` - -Present on all delivery-003 clips; may be `null` on older clips. - -| Field | Type | Description | -|-------|------|-------------| -| `pipeline_version` | `str` | SynthBanshee semver | -| `tts_backend` | `dict[str, str]` | Speaker ID → `"azure"` or `"google"` | -| `voice_family` | `dict[str, str]` | Speaker ID → voice family string | -| `mix_mode_used` | `str` | `"sequential"` (turns in order) or `"overlapping"` | -| `normalization_strategy` | `str` | `"per_turn_rms_v2_target_peak"` | -| `loudness_target_peak_dbfs` | `float` | Configured peak target, e.g. `-2.0` | -| `breathiness_applied` | `bool` | Whether breathiness augmentation was applied | -| `effective_prosody_caps` | `list[ProsodyCap]` | Per-turn cap activations at I3–I5 | -| `speaker_state_serialized` | `dict[str, SpeakerState]` | Final prosody state per speaker | -| `prosody_controller_version` | `str \| null` | Version of the prosody controller | -| `text_normalization_version` | `str \| null` | Version of text normalization | -| `timing_controller_version` | `str \| null` | Version of timing controller | - -### `ProsodyCap` (entry in `effective_prosody_caps`) - -| Field | Description | -|-------|-------------| -| `turn_index` | Zero-based turn index | -| `intensity` | Intensity score for that turn | -| `dim` | `"pitch"` or `"rate"` | -| `pre_cap` | Prosody value before capping (semitones for pitch, ratio for rate) | -| `post_cap` | Prosody value after capping | - -### `SpeakerState` (entry in `speaker_state_serialized`) - -| Field | Description | -|-------|-------------| -| `pitch_offset_st` | Final pitch offset in semitones | -| `rate_offset` | Final speaking rate multiplier | -| `volume_offset_db` | Final volume offset in dB | -| `breathiness_level` | Breathiness level 0–1 | +1. Lowercase ASCII clip identifier. Pattern: `{project_prefix}_{typology}_{tier}_{scene_num}_{take}`. +2. `she_proves` or `elephant_in_the_room`. Determines clip-id prefix (`sp_*` / `el_*`) and which `*_meta` field is non-null. +3. ISO 639-1 — always `"he"` in this corpus. +4. `SV` · `IT` · `NEG` · `NEU`. **Not** an ordered scale — see [Label Taxonomy](taxonomy.md). `NEG` is the hard-negative class (sounds intense, not violent). +5. `"A"` (clean, TTS only) or `"B"` (room IR + device profile + background noise applied). Determines whether `acoustic_scene` is populated. +6. Duration of the final processed WAV, **including** the 0.5 s silence pad on each end. +7. Always 16000. Channels always 1. Format always 16-bit PCM WAV. +8. Always `true` in this corpus. The field exists because future real-recording deliveries will set it `false`. +9. Clip-level summary labels. `has_violence` is derived from events: `any(e.tier1_category != "NONE")`. Don't derive it from typology — see [Gotcha #1](gotchas.md#1-dont-derive-has_violence-from-typology). +10. One entry per speaker. The on-disk directory is **`speakers[0].speaker_id.lower()`** — UPPERCASE in JSON, lowercase on disk ([Gotcha #4](gotchas.md#4-uppercase-in-json-lowercase-on-disk)). +11. Repo-relative POSIX path to the `.txt` transcript. +12. Repo-relative POSIX path to the pre-preprocessing ("dirty") WAV, retained per spec. Useful for diagnosing normalization issues. **Don't modify** — `assets/` is managed by SynthBanshee ([Gotcha #7](gotchas.md#7-quality_flags-doesnt-mean-broken)). +13. Soft warnings. Don't filter on these reflexively — they don't fail validation. Most common: `emotion_downgrade` (TTS produced slightly less intense prosody than requested), `vic_f0_high` (Google female F0 above Azure baseline; expected on the 2 Google clips). +14. Populated for Tier B (Elephant) clips; all `null` / empty for Tier A. See [Elephant guide](elephant.md#the-acoustic_scene-field). +15. Records *what* preprocessing ran. `normalized_dbfs` is the **measured** post-preprocess peak — pair with `generation_metadata.loudness_target_peak_dbfs` (the configured target) to diagnose loudness drift. +16. Pipeline provenance. Always present on delivery-003 clips; may be `null` on older clips. Expanded below. +17. SynthBanshee version that produced this clip. Combined with `random_seed` + `scene_config`, scenes are reproducible. +18. Estimated SNR — not populated for any current delivery. Use `acoustic_scene.snr_db_actual` for Tier B. +19. Auto-label confidence; always `1.0` because labels are generated by the pipeline (not human-annotated). `iaa_reviewed` is always `false` for the same reason. +20. Reserved for per-project metadata. Always `null` in current deliveries. --- -## `PreprocessingApplied` +## `generation_metadata` — pipeline provenance + +Expanded view of field (16). Use this block for diagnostics, not for filtering training data. + +```json +{ + "pipeline_version": "0.1.0", + "tts_backend": {"AGG_M_30-45_001": "azure", "VIC_F_25-40_002": "azure"}, + "voice_family": {"AGG_M_30-45_001": "he-IL-AvriNeural", "VIC_F_25-40_002": "he-IL-HilaNeural"}, + "mix_mode_used": "sequential", + "normalization_strategy": "per_turn_rms_v2_target_peak", // internal version string; informational + "loudness_target_peak_dbfs": -2.0, + "breathiness_applied": false, + "effective_prosody_caps": [ // per-turn cap activations at I3+ + {"turn_index": 1, "intensity": 2, "dim": "rate", "pre_cap": 0.912, "post_cap": 0.95}, + {"turn_index": 4, "intensity": 4, "dim": "pitch", "pre_cap": 2.348, "post_cap": 2.0} + ], + "speaker_state_serialized": { + "AGG_M_30-45_001": {"pitch_offset_st": 1.40, "rate_offset": 1.14, "volume_offset_db": 3.80, "breathiness_level": 0.0}, + "VIC_F_25-40_002": {"pitch_offset_st": 0.56, "rate_offset": 0.89, "volume_offset_db": -2.58, "breathiness_level": 0.0} + } +} +``` -| Field | Type | Description | -|-------|------|-------------| -| `resampled_to_16k` | `bool` | Whether sample rate conversion ran | -| `downmixed_to_mono` | `bool` | Whether channel downmix ran | -| `normalized_dbfs` | `float` | **Measured** peak dBFS of the output WAV (not the target) | -| `silence_padded` | `bool` | Whether silence padding was applied | -| `denoised` | `bool` | Whether denoising ran | -| `spectral_filtered` | `bool` | Whether spectral filtering ran | +| Field | What it tells you | +|-------|-------------------| +| `tts_backend` | Per-speaker dict mapping speaker_id → `"azure"` or `"google"`. The corpus-level backend distribution is derived from this — don't look for a top-level `tts_engine` field, it was removed. | +| `voice_family` | Per-speaker dict mapping speaker_id → voice ID. Currently identical to `speakers[].tts_voice_id`. | +| `mix_mode_used` | `"sequential"` (turns in order) or `"overlapping"` (turns can overlap at I4+). All delivery-003 violent clips use `"overlapping"` at high intensity; calm clips use `"sequential"`. | +| `loudness_target_peak_dbfs` | The **configured** peak target (–2.0 dBFS by default). Pair with `preprocessing_applied.normalized_dbfs` (the measured peak) to detect drift. | +| `effective_prosody_caps` | Per-turn list of cap activations — when the LLM-suggested pitch or rate exceeded the safety cap. Common at I3+ in this delivery. Recording them lets you compute the "uncapped" prosody the LLM intended. | +| `speaker_state_serialized` | Final per-speaker prosody offset. Used for reproducing a scene with the same speaker drift. | -!!! note "`normalized_dbfs` is the measured peak, not the target" - Use `generation_metadata.loudness_target_peak_dbfs` for the configured target. - Use `preprocessing_applied.normalized_dbfs` to verify the actual output peak. - On delivery-003, both should be very close to `–2.0` (within floating-point precision). +??? info "Internal version-string fields" + `normalization_strategy`, `prosody_controller_version`, `text_normalization_version`, `timing_controller_version` are internal version strings. They're recorded for provenance but you won't filter on them as a consumer. --- -## `AcousticScene` - -Populated for Tier B clips. Null fields indicate Tier A (no augmentation). - -| Field | Type | Description | -|-------|------|-------------| -| `room_type` | `str \| null` | e.g. `"clinic_office"`, `"welfare_office"`, `"open_office"` | -| `device` | `str \| null` | e.g. `"pi_budget_mic"` | -| `ir_source` | `str \| null` | Room impulse response source, e.g. `"pyroomacoustics_ism"` | -| `snr_db_actual` | `float \| null` | Actual SNR after augmentation (dB) | -| `speaker_distance_meters` | `float \| null` | Simulated speaker distance from microphone | -| `background_events` | `list[BackgroundEvent]` | Non-speech acoustic events added | - -### `BackgroundEvent` - -| Field | Description | -|-------|-------------| -| `type` | `"hvac_hum"`, `"ACOU_SLAM"`, `"ACOU_FALL"`, etc. | -| `onset` | Start time in seconds | -| `offset` | End time in seconds | -| `level_db` | Relative level of the event (dB) | - ---- +## `EventLabel` — `.jsonl` rows + +One JSON object per line. One line per labelled event. Read line-by-line — `json.loads()` on the whole file errors. + +```json +{ + "event_id": "sp_sv_a_0001_00_EVT_004", + "clip_id": "sp_sv_a_0001_00", + "onset": 36.736, + "offset": 46.552, + "tier1_category": "DIST", + "tier2_subtype": "DIST_SCREAM", + "intensity": 4, + "speaker_id": "AGG_M_30-45_001", + "speaker_role": "AGG", + "emotional_state": "anger", + "confidence": 1.0, + "label_source": "auto", + "iaa_reviewed": false, + "truncated": false, + "notes": null +} +``` -## `EventLabel` (`.jsonl` rows) - -One JSON object per line. Each represents a single labelled event within the clip. - -| Field | Type | Description | -|-------|------|-------------| -| `event_id` | `str` | `{clip_id}_EVT_{index:03d}` | -| `clip_id` | `str` | Parent clip ID | -| `onset` | `float` | Event start time in seconds (in the processed WAV) | -| `offset` | `float` | Event end time in seconds | -| `tier1_category` | `str` | `VERB` / `DIST` / `PHYS` / `EMOT` / `ACOU` / `NONE` | -| `tier2_subtype` | `str` | e.g. `VERB_SHOUT`, `PHYS_HARD` | -| `intensity` | `int` | Turn intensity 1–5 | -| `speaker_id` | `str` | UPPERCASE speaker persona ID | -| `speaker_role` | `str` | `AGG`, `VIC`, `SW`, `BEN` | -| `emotional_state` | `str` | e.g. `"anger"`, `"fear"`, `"desperation"`, `"neutral"` | -| `confidence` | `float` | Auto-label confidence (always `1.0` for auto-generated) | -| `label_source` | `str` | `"auto"` for all current clips | -| `iaa_reviewed` | `bool` | Always `false` in current deliveries | -| `truncated` | `bool` | Whether the event was cut short by a turn boundary | -| `notes` | `str \| null` | Annotator notes | +| Field | Notes | +|-------|-------| +| `onset` / `offset` | Seconds in the **final processed WAV**. Already shifted to account for the 0.5 s leading silence pad. | +| `tier1_category` | `VERB` · `DIST` · `PHYS` · `EMOT` · `ACOU` · `NONE`. See [Label Taxonomy](taxonomy.md). | +| `tier2_subtype` | e.g. `VERB_SHOUT`, `DIST_SCREAM`, `PHYS_HARD`, `ACOU_SLAM`. | +| `intensity` | The intensity of the turn the event belongs to (1–5). | +| `speaker_id` | UPPERCASE. Matches one of `ClipMetadata.speakers[].speaker_id`. | +| `speaker_role` | `AGG` · `VIC` · `SW` · `BEN`. See [Glossary](glossary.md#speaker-roles). | +| `emotional_state` | Free-text label of speaker emotion at this turn (e.g. `"anger"`, `"fear"`, `"desperation"`, `"neutral"`). | +| `confidence` | Always `1.0` (labels are auto-generated). | +| `label_source` | Always `"auto"`. | +| `iaa_reviewed` | Always `false` in current deliveries — no human inter-annotator agreement review yet. | +| `truncated` | `true` if the event was cut short by a turn boundary. | --- ## Manifest CSV columns -`data/he/manifest.csv` — one row per clip. +`data/he/manifest.csv` — one row per clip, the fastest entry point for filtering. | Column | Type | Notes | |--------|------|-------| -| `clip_id` | str | Matches JSON `clip_id` | +| `clip_id` | str | Matches `ClipMetadata.clip_id` | | `project` | str | `she_proves` / `elephant_in_the_room` | | `violence_typology` | str | `SV` / `IT` / `NEG` / `NEU` | | `tier` | str | `A` / `B` | -| `duration_seconds` | float | | -| `speaker_ids` | str | Pipe-delimited, e.g. `AGG_M_30-45_001\|VIC_F_25-40_002` | -| `voice_families` | str | Pipe-delimited, matches `speaker_ids` order | -| `has_violence` | bool | See [taxonomy](taxonomy.md#has_violence-the-correct-derivation) | +| `duration_seconds` | float | Final WAV duration including pads | +| `speaker_ids` | str | **Pipe-delimited.** `AGG_M_30-45_001\|VIC_F_25-40_002` | +| `voice_families` | str | **Pipe-delimited**, same order as `speaker_ids` | +| `has_violence` | bool | Derived from events — see [Gotcha #1](gotchas.md#1-dont-derive-has_violence-from-typology) | | `max_intensity` | int | 1–5 | -| `quality_flags` | str | Comma-delimited flag list | -| `split` | str | `train` / `val` / `test` — all `train` in delivery-003 | -| `wav_path` | str | Repo-relative POSIX path | -| `strong_labels_path` | str | Repo-relative POSIX path to `.jsonl` | +| `quality_flags` | str | Comma-delimited soft warnings | +| `split` | str | `train` / `val` / `test` — **all `train`** in delivery-003 ([Gotcha #6](gotchas.md#6-all-clips-are-split-train-in-delivery-003)) | +| `wav_path` | str | Repo-relative POSIX path to the `.wav` | +| `strong_labels_path` | str | Repo-relative POSIX path to the `.jsonl` | + +--- + +## Transcript file format (`.txt`) + +Plain UTF-8. One turn = one header line + one or more text lines + one action line. Hebrew text only; no Latin script in the body. + +``` +[CLIP_ID: sp_sv_a_0001_00] +[SPEAKER: AGG_M_30-45_001 | ROLE: AGG | ONSET: 0.76 | OFFSET: 10.07] +מה זה הארוחה הזאת? שאלתי אותך דבר אחד פשוט, לעשות ארוחת ערב נורמלית. +[ACTION: VERB_SHOUT | INTENSITY: 2] +[SPEAKER: VIC_F_25-40_002 | ROLE: VIC | ONSET: 10.49 | OFFSET: 18.74] +עבדתי עד שש היום. עשיתי מה שהספקתי... +[ACTION: VERB_SHOUT | INTENSITY: 2] +``` + +- The first line is a single `[CLIP_ID: ...]` header. +- Each subsequent turn is a `[SPEAKER: ... | ROLE: ... | ONSET: ... | OFFSET: ...]` line, the Hebrew text, then `[ACTION: | INTENSITY: 1–5]`. +- `ONSET` / `OFFSET` are in seconds, relative to the final processed WAV (already include the leading pad). +- The `.jsonl` strong labels are the canonical source for events; the transcript is for human reading and as an ASR reference. diff --git a/docs/she-proves.md b/docs/she-proves.md index fef7983..6be24c7 100644 --- a/docs/she-proves.md +++ b/docs/she-proves.md @@ -1,133 +1,117 @@ -# She-Proves Team Guide +# She-Proves Guide -She-Proves is a smartphone app that **passively monitors audio for domestic violence incidents** and preserves evidence for legal use. +She-Proves is a smartphone app that passively monitors audio for domestic-violence incidents and preserves evidence for legal use. **Optimisation target: high recall** — better to flag for review than to miss. -**Optimization target: high recall.** It is better to flag an incident for review than to miss one. +This page is the *differential* between She-Proves clips and the rest of the corpus. For shared concepts (schema, labels, audio format) follow the cross-links. --- -## Scene structure +## Scene profile -| Property | Value | -|----------|-------| -| Duration | 3–6 minutes | -| Tier | A (clean — no room processing) | -| Pre-incident window | ≥ 60% of clip duration before the first violence event | -| Device profile | `phone_in_pocket`, `phone_on_table`, `phone_in_hand` | -| Room types | apartment rooms (living room, bedroom, kitchen) | -| Language | Hebrew (`he`) | +| | | +|---|---| +| Project code | `she_proves` (clip-id prefix `sp_*`) | +| Tier | A — clean audio, no room/device augmentation | +| Duration | 3–6 min | +| Pre-incident window | ≥ 60% of clip is normal speech before the first violence event | +| Device | `phone_in_pocket`, `phone_on_table`, `phone_in_hand` (planned; not active in delivery-003) | +| Room | Apartment (living room, bedroom, kitchen) — planned; not active in delivery-003 | -The long pre-incident window reflects real-world deployment: the app is always listening, and incidents are rare. Models trained on this data should handle extended periods of mundane speech before a rapid escalation. +The long pre-incident window is intentional. In deployment the app is always listening; incidents are rare. A model trained only on escalation segments will miss the gradual-buildup signal that precedes most domestic-violence events. -??? info "Tier A — what does 'clean' mean?" - Tier A clips have **no acoustic augmentation** — no room impulse response convolution, no device frequency response, no background noise injection. The audio is the direct TTS-mixer output after preprocessing: peak-normalized, silence-padded, 16 kHz mono 16-bit PCM. +!!! note "What 'Tier A' means here" + Tier A audio is the direct TTS-mixer output after preprocessing — peak-normalised, silence-padded, 16 kHz mono PCM. No room IR, no microphone profile, no background noise. `acoustic_scene.room_type`, `device`, `ir_source`, and `snr_db_actual` are all `null` for every Tier A clip. - For Tier A, `acoustic_scene.room_type`, `device`, `ir_source`, and `snr_db_actual` are all `null`. - - Tier B (used by Elephant) adds all of the above. See [Elephant in the Room](elephant.md) for details. + Delivery-003 has no Tier-A device augmentation yet (the `phone_in_pocket` etc. profiles exist in the pipeline but aren't applied at this stage). When that's added in a future delivery, the `acoustic_scene` block will start carrying `device` while keeping `room_type` null. --- ## Speaker pairs -Delivery-003 has two She-Proves speaker pairs — one per TTS backend. +Two pairs in delivery-003, one per TTS backend. Both pairs play the **AGG (aggressor, male) + VIC (victim, female)** roles. + +=== "Azure pair (10 clips)" + Speaker directory: `data/he/agg_m_30-45_001/` + + | Role | speaker_id | TTS voice | + |------|-----------|-----------| + | AGG | `AGG_M_30-45_001` | `he-IL-AvriNeural` | + | VIC | `VIC_F_25-40_002` | `he-IL-HilaNeural` | + +=== "Google Chirp HD pair (2 clips)" + Speaker directory: `data/he/agg_m_30-45_002/` -| Pair | Speaker dir | Male speaker | Female speaker | Backend | -|------|-------------|--------------|----------------|---------| -| Azure | `agg_m_30-45_001/` | `AGG_M_30-45_001` → `he-IL-AvriNeural` | `VIC_F_25-40_002` → `he-IL-HilaNeural` | Azure | -| Google Chirp HD | `agg_m_30-45_002/` | `AGG_M_30-45_002` → `he-IL-Chirp3-HD-Achird` | `VIC_F_25-40_003` → `he-IL-Chirp3-HD-Achernar` | Google | + | Role | speaker_id | TTS voice | + |------|-----------|-----------| + | AGG | `AGG_M_30-45_002` | `he-IL-Chirp3-HD-Achird` | + | VIC | `VIC_F_25-40_003` | `he-IL-Chirp3-HD-Achernar` | -Both pairs play **AGG (aggressor, male) + VIC (victim, female)** roles. The Google pair was added in delivery-003 specifically to introduce backend diversity. + The Google pair was added in delivery-003 specifically to introduce backend diversity. Both clips carry a `vic_f0_high` flag — see [Audio Format](audio-format.md#vic_f0_high-on-the-2-google-clips). -!!! note "Two speaker directories" - Clips from the Azure pair live under `data/he/agg_m_30-45_001/`. - Clips from the Google pair live under `data/he/agg_m_30-45_002/`. - Downstream code that hardcodes `agg_m_30-45_001/` will miss the Google clips. - Use `manifest.csv` or filter `meta["generation_metadata"]["tts_backend"]` to find both. +[Gotcha #2: don't hardcode `agg_m_30-45_001/`](gotchas.md#2-dont-hardcode-speaker-directory-paths) — three speaker directories exist now, including one for Elephant. Filter on `manifest.csv["project"] == "she_proves"` or on `meta["project"]`. --- ## Clips in delivery-003 -### Azure pair — 10 clips +**12 clips · ~20 min · 6 violent (`SV` + `IT`), 6 non-violent (`NEG` + `NEU`)** -`data/he/agg_m_30-45_001/` +??? abstract "Full clip listing" + Azure pair, `data/he/agg_m_30-45_001/` — 10 clips: -| Clip ID | Typology | `has_violence` | Duration | -|---------|----------|:---:|------:| -| `sp_sv_a_0001_00` | SV | ✓ | 1m 50.5s | -| `sp_sv_a_0002_00` | SV | ✓ | 1m 32.1s | -| `sp_it_a_0001_00` | IT | ✓ | 2m 23.8s | -| `sp_it_a_0002_00` | IT | ✓ | 2m 19.7s | -| `sp_neg_a_0001_00` | NEG | — | 1m 58.8s | -| `sp_neg_a_0002_00` | NEG | — | 1m 47.8s | -| `sp_neg_a_0003_00` | NEG | — | 2m 26.3s | -| `sp_neu_a_0001_00` | NEU | — | 1m 59.2s | -| `sp_neu_a_0002_00` | NEU | — | 2m 09.0s | -| `sp_neu_a_0003_00` | NEU | — | 1m 45.1s | + | Clip ID | Typology | violent | Duration | + |---------|----------|:---:|---------:| + | `sp_sv_a_0001_00` | SV | ✓ | 1m 50.5s | + | `sp_sv_a_0002_00` | SV | ✓ | 1m 32.1s | + | `sp_it_a_0001_00` | IT | ✓ | 2m 23.8s | + | `sp_it_a_0002_00` | IT | ✓ | 2m 19.7s | + | `sp_neg_a_0001_00` | NEG | — | 1m 58.8s | + | `sp_neg_a_0002_00` | NEG | — | 1m 47.8s | + | `sp_neg_a_0003_00` | NEG | — | 2m 26.3s | + | `sp_neu_a_0001_00` | NEU | — | 1m 59.2s | + | `sp_neu_a_0002_00` | NEU | — | 2m 09.0s | + | `sp_neu_a_0003_00` | NEU | — | 1m 45.1s | -### Google Chirp HD pair — 2 clips + Google Chirp HD pair, `data/he/agg_m_30-45_002/` — 2 clips: -`data/he/agg_m_30-45_002/` + | Clip ID | Typology | violent | Duration | Flags | + |---------|----------|:---:|---------:|-------| + | `sp_sv_a_0003_00` | SV | ✓ | 1m 42.8s | `vic_f0_high` | + | `sp_it_a_0003_00` | IT | ✓ | 1m 53.9s | `vic_f0_high` | -| Clip ID | Typology | `has_violence` | Duration | Note | -|---------|----------|:---:|------:|------| -| `sp_sv_a_0003_00` | SV | ✓ | 1m 42.8s | `vic_f0_high` flag | -| `sp_it_a_0003_00` | IT | ✓ | 1m 53.9s | `vic_f0_high` flag | - -The `vic_f0_high` flag on the Google clips indicates the female voice (`he-IL-Chirp3-HD-Achernar`) has a higher F0 baseline than the Azure Hila reference. See [Audio Format → vic_f0_high](audio-format.md#vic_f0_high-google-chirp-hd-female-f0-baseline). +The waveform on the [home page](index.md#see-it-first) is `sp_sv_a_0001_00` — a worked example of an SV escalation arc in this project's data. --- -## Loading She-Proves clips +## Loading just the She-Proves clips ```python -import json -import soundfile as sf -import pandas as pd +import pandas as pd, soundfile as sf, json from pathlib import Path root = Path(".") - -# Via manifest — easiest df = pd.read_csv("data/he/manifest.csv") -sp_clips = df[df["project"] == "she_proves"] - -# Load all She-Proves audio -wavs = {} -for _, row in sp_clips.iterrows(): - wav, sr = sf.read(root / row["wav_path"]) - wavs[row["clip_id"]] = wav - -# Filter to violent She-Proves clips only -sp_violent = sp_clips[sp_clips["has_violence"] == True] - -# Get per-backend split -sp_clips["backend"] = sp_clips["voice_families"].apply( - lambda v: "google" if "Chirp" in v else "azure" -) -print(sp_clips.groupby("backend")["clip_id"].count()) -# azure 10 -# google 2 -``` +sp = df[df["project"] == "she_proves"] # 12 rows ---- - -## Guidance for model training - -!!! warning "This is a toy corpus — not for production training" - 12 She-Proves clips (10 Azure + 2 Google) are not enough for training a production model. Use this delivery to validate your data pipeline and schema parsing. Full-scale data follows. +# Tag backend per row (Google clips have "Chirp" in voice_families) +sp = sp.assign(backend=sp["voice_families"].str.contains("Chirp").map({True: "google", False: "azure"})) +print(sp.groupby("backend")["clip_id"].count()) +# azure 10 +# google 2 -**High-recall orientation:** - -- **NEG clips are your hardest negatives.** They contain intense speech (raised voices, arguments, crying) with `has_violence: false`. Your recall model must not fire on them. -- **The pre-incident window** (first 60% of the clip) will look like NEU/low-intensity speech. Include it in your training windows — models that only see escalated segments will miss early warning signals. -- **Per-turn intensity** in the `.jsonl` events gives you fine-grained supervision beyond binary `has_violence`. Consider training an intensity regressor as an auxiliary objective. +# Load audio for each row +audio = {row.clip_id: sf.read(root / row.wav_path) for row in sp.itertuples()} +``` -**Backend diversity:** +--- -The 2 Google Chirp HD clips expose your feature extractor to a different F0 baseline and spectral profile. At small scale, they're useful for checking that your features don't overfit to Azure voice characteristics. +## Training-time notes (specific to this project) -**Speaker splits:** +- **NEG clips are your hardest negatives.** `sp_neg_a_*` clips have raised voices, distress, arguments — and `has_violence: false`. Recall metrics that fire on these will tank precision. See [Gotcha #1](gotchas.md#1-dont-derive-has_violence-from-typology) and [Gotcha #5](gotchas.md#5-neg-is-not-violent-at-low-intensity). +- **Use the pre-incident window.** The first 60% of each violent clip looks like NEU-grade speech. Train across the full clip, not only on escalation segments — early-warning signal lives in the buildup. +- **Per-turn intensity is a useful auxiliary objective.** `EventLabel.intensity` gives turn-level supervision beyond binary `has_violence`. An intensity regressor trained alongside the classifier often boosts the latter. +- **Only 2 voice families per gender in this delivery** (`low_voice_diversity_*` is flagged at the corpus level). Expect your acoustic features to over-fit to AvriNeural and HilaNeural — track per-voice eval separately when the corpus grows. +- **No device/room augmentation yet on She-Proves clips.** When the `phone_in_pocket` profile activates in a future delivery, your model will see substantially more high-frequency roll-off and handling noise than what's in delivery-003. -All 12 clips share 2 unique speaker personas (4 if you count Azure+Google pairs separately). There are not enough speakers for a speaker-disjoint split in this delivery. Re-evaluate when the corpus scales to 100+ speakers. +!!! warning "Still a small test batch" + 12 clips and 4 voices is enough to wire up your data loaders, label parsers, and evaluation harness. It is not enough to train a production model. Build the plumbing; wait for the real batch. diff --git a/docs/taxonomy.md b/docs/taxonomy.md index 8154a06..973f5cd 100644 --- a/docs/taxonomy.md +++ b/docs/taxonomy.md @@ -1,126 +1,137 @@ # Label Taxonomy -Labels follow a three-level hierarchy. The **source of truth** is `taxonomy.yaml` in the [SynthBanshee](https://github.com/DataHackIL/SynthBanshee) repo. Never derive labels from field names alone — always read from the actual data. +Three levels: clip-level **typology** → event-level **tier 1 category** → event-level **tier 2 subtype**. Plus a per-turn **intensity** (1–5) that drives prosody. The source of truth is `taxonomy.yaml` in [SynthBanshee](https://github.com/DataHackIL/SynthBanshee). --- -## Violence typologies (clip-level) +## Violence typology (clip-level) -The `violence_typology` field classifies the overall scenario of the clip. +The `violence_typology` field. **Not** an ordered scale. -| Typology | Full name | Description | -|----------|-----------|-------------| -| `SV` | Severe Violence | Physical violence, life-threatening escalation | -| `IT` | Intimate Terrorism | Systematic coercive control, repeated verbal/emotional abuse | -| `NEG` | Negative / Confusor | Acoustically intense but non-violent — anger, argument, distress, crying | -| `NEU` | Neutral | Calm or mundane conversation with no violence markers | +| Code | Name | What it sounds like | +|------|------|---------------------| +| `SV` | Severe Violence | Physical violence, life-threatening escalation. `tier1_category` includes `PHYS`, `DIST`, often `VERB`. | +| `IT` | Intimate Terrorism | Sustained coercive control, repeated verbal/emotional abuse — typically without physical attack. Heavy on `VERB` and `EMOT`. | +| `NEG` | Negative confusor | Acoustically intense but non-violent — anger, argument, distress, crying. **Hard negative class.** All events are `tier1_category: "NONE"`. | +| `NEU` | Neutral | Calm or mundane conversation. No violence markers. | -??? info "Why NEG is not the same as non-violent IT/SV" - NEG clips are designed as **hard negatives** — they sound intense and may have raised voices, crying, or confrontational tone, but no actual violence occurs. Their purpose is to train models to distinguish acoustic distress from violence. - - Models that rely only on loudness or emotional tone will misclassify NEG clips. This is by design. +!!! danger "NEG is the trap" + A NEG clip can have raised voices, crying, and `max_intensity: 3`. It will *sound* like violence to a model that only listens for loudness or emotional tone. But it is by definition `has_violence: false` — its purpose is to teach your model the difference between distress and violence. Training NEG as a positive class will collapse your precision. See [Gotcha #5](gotchas.md#5-neg-is-not-violent-at-low-intensity). --- -## `has_violence` — the correct derivation - -`has_violence` is a **derived convenience field** computed from the strong-label events, not from typology: +## `has_violence` — derived from events ```python has_violence = any(e["tier1_category"] != "NONE" for e in events) ``` -This means: +That's the rule. Two consequences worth knowing: -- `NEG` clips are **always** `has_violence: false`, regardless of `max_intensity` — by definition, every event in a NEG clip lands `tier1_category: "NONE"`. -- A `NEU` clip with even one stray non-NONE event would be `has_violence: true` (shouldn't happen in a well-labelled corpus, but the rule is defensive). +- **NEG clips are always `has_violence: false`** — every event in a NEG clip has `tier1_category: "NONE"` by construction, even when `max_intensity` is high. +- **NEU clips are always `has_violence: false`** for the same reason. +- A `SV` or `IT` clip is `has_violence: true` because at least one event has a non-NONE category. -!!! danger "Do not re-derive `has_violence` from typology + intensity" +!!! danger "Don't derive `has_violence` from typology or intensity" ```python - # WRONG — will misclassify every NEG clip - has_violence = typology in ("SV", "IT") - - # CORRECT - has_violence = any(e["tier1_category"] != "NONE" for e in events) + has_violence = typology in ("SV", "IT") # WRONG — works on this corpus but fragile + has_violence = max_intensity >= 3 # VERY WRONG — fires on every NEG clip ``` - The taxonomy columns are the ground truth. `has_violence` exists only for fast filtering and baseline modelling — never use it as the sole training label. + The event-level taxonomy is the ground truth. `weak_label.has_violence` exists for fast filtering and baseline modelling only — never as the sole training label. Train on the strong-label events when you can. --- ## Tier 1 categories (event-level) -Each `EventLabel` in the `.jsonl` file has a `tier1_category`: +The `tier1_category` field on each `EventLabel`. Six values. -| Category | Description | Example contexts | -|----------|-------------|-----------------| -| `VERB` | Verbal violence — threats, shouting, demeaning language | Arguments, intimidation | -| `DIST` | Distress vocalisations — screaming, crying under duress | Peak escalation turns | -| `PHYS` | Physical violence cues — impact sounds, struggle | Severe violence scenes | -| `EMOT` | Emotional manipulation — guilt-tripping, gaslighting | IT/coercive control | -| `ACOU` | Acoustic events — object impacts, slams, falls | Background events in Tier B | -| `NONE` | No violence — ambient speech, neutral turns | All NEU/NEG events | +| Category | What it covers | Where it shows up | +|----------|----------------|-------------------| +| `VERB` | Verbal violence — threats, shouting, demeaning language | Most violent clips, all intensity levels | +| `DIST` | Distress vocalisations — screaming, crying under duress | I3+ turns in SV/IT, peak escalation | +| `PHYS` | Physical violence cues — impact sounds, struggle | I4+ turns in SV clips | +| `EMOT` | Emotional manipulation — gaslighting, guilt-tripping | IT clips, coercive control turns | +| `ACOU` | Acoustic non-vocal events — slams, falls | Tier B clips, recorded in `acoustic_scene.background_events` | +| `NONE` | Ambient speech / neutral turn | All NEU clips, all NEG clips, calm turns in SV/IT | -??? info "ACOU vs DIST" - `ACOU` captures **non-vocal acoustic cues** — a door slam, an object falling, an impact sound. These appear in Tier B clips as `background_events` in the `acoustic_scene` block. +!!! info "ACOU vs DIST" + `ACOU` is **non-vocal** acoustic — a door slam, an object hitting the floor. `DIST` is **vocal distress** — a scream, crying. A scene where someone throws a glass and the victim screams will have an `ACOU_SLAM` event for the glass and a `DIST_SCREAM` event for the scream. - `DIST` captures **vocal distress** — screams, panic vocalisations, crying under coercion. + Tier B Elephant clips inject `ACOU_*` events as part of room augmentation; they show up both in `acoustic_scene.background_events` (with audio-level metadata) and in `.jsonl` strong labels (as labelled events). Tier A She-Proves clips can't produce ACOU events — there's no room-augmentation stage to add them. --- ## Tier 2 subtypes (event-level) -| Tier 1 | Tier 2 subtype | Description | -|--------|----------------|-------------| -| VERB | `VERB_SHOUT` | Raised or shouted speech | -| VERB | `VERB_THREAT` | Direct verbal threats | -| VERB | `VERB_INSULT` | Demeaning or insulting language | -| DIST | `DIST_SCREAM` | Distress scream or panic vocalisation | -| DIST | `DIST_CRY` | Crying or sobbing under duress | -| PHYS | `PHYS_HARD` | Hard physical impact cue | -| PHYS | `PHYS_SOFT` | Softer physical contact cue | -| EMOT | `EMOT_GASLIGHT` | Gaslighting or reality-denial | -| EMOT | `EMOT_GUILT` | Guilt-tripping or emotional coercion | -| ACOU | `ACOU_SLAM` | Object slam or door slam | -| ACOU | `ACOU_FALL` | Object falling or thrown | -| NONE | `NONE_AMBIENT` | Regular ambient speech or neutral turn | +| Tier 1 | Tier 2 | Description | +|--------|--------|-------------| +| `VERB` | `VERB_SHOUT` | Raised or shouted speech | +| `VERB` | `VERB_THREAT` | Direct verbal threats | +| `VERB` | `VERB_INSULT` | Demeaning or insulting language | +| `DIST` | `DIST_SCREAM` | Distress scream or panic vocalisation | +| `DIST` | `DIST_CRY` | Crying or sobbing under duress | +| `PHYS` | `PHYS_HARD` | Hard physical impact cue | +| `PHYS` | `PHYS_SOFT` | Softer physical contact cue | +| `EMOT` | `EMOT_GASLIGHT` | Gaslighting or reality-denial | +| `EMOT` | `EMOT_GUILT` | Guilt-tripping or emotional coercion | +| `ACOU` | `ACOU_SLAM` | Object slam or door slam | +| `ACOU` | `ACOU_FALL` | Object falling or thrown | +| `NONE` | `NONE_AMBIENT` | Regular ambient speech or neutral turn | --- ## Intensity scale (turn-level) -Intensity is scored 1–5 per dialogue turn. It controls prosody generation (pitch, rate, volume) and determines which tier1/tier2 labels are applied. +Each turn has an `intensity` in `[1, 5]`. It controls prosody generation (pitch, rate, volume) and the LLM script tone. + +| Score | Label | What's happening | +|-------|-------|------------------| +| 1 | Low tension | Calm conversation, mild undercurrent | +| 2 | Moderate tension | Noticeable friction, raised voices | +| 3 | Active conflict | Clear verbal aggression or intimidation | +| 4 | Escalated violence | Physical or high-intensity verbal violence | +| 5 | Extreme | Severe physical violence, panic, imminent danger | + +### How intensity and typology relate + +They are correlated but not the same. + +| Typology | Typical `max_intensity` range | Why | +|----------|:-----------------------------:|-----| +| `NEU` | 1–2 | Mundane conversation by definition | +| `NEG` | 2–3 | Distressed but non-violent; intensity rises with shouting/crying, but no PHYS/DIST events fire | +| `IT` | 3–5 | Sustained verbal/emotional aggression; can hit I5 on threats without physical violence | +| `SV` | 4–5 | Physical escalation requires I4+ turns | -| Score | Label | Description | Prosody profile | -|-------|-------|-------------|----------------| -| 1 | Low tension | Calm conversation, mild undercurrent | Near-neutral | -| 2 | Moderate tension | Noticeable friction, raised voices | Slightly raised pitch/rate | -| 3 | Active conflict | Clear verbal aggression or intimidation | Elevated pitch, faster rate | -| 4 | Escalated violence | Physical or high-intensity verbal violence | High pitch, fast rate, volume up | -| 5 | Extreme / life-threatening | Severe physical violence, panic | Maximally expressive (capped) | +In delivery-003 the actual distribution is `max_intensity` 5 = 10 clips, 3 = 4 clips, 2 = 6 clips. Useful for designing stratified eval splits: if you want a balanced eval set across intensity *and* typology, you'll need to upsample (or wait for more data). -??? info "The prosody cap at I4–I5" - At intensity 4–5, the LLM-generated prosody values are capped before SSML rendering to prevent Whisper transcription failures and maintain naturalness. The cap values are: +??? info "What is the prosody cap?" + At I3+, the LLM-suggested prosody values are clamped before SSML rendering to keep speech natural and transcribable by Whisper: - - **Pitch:** max +2.0 semitones (post-cap) - - **Rate:** range [0.85, 1.20] (post-cap) + - **Pitch:** capped at +2.0 semitones (post-cap) + - **Rate:** clamped to [0.85, 1.20] - Any cap activation is recorded in `generation_metadata.effective_prosody_caps` per turn. You'll see many activations at I4–I5 in delivery-003 — this is expected. The cap was calibrated in a listening test in May 2026 (SynthBanshee PR #87). + When clamping fires, the pre- and post-cap values are recorded per turn in `generation_metadata.effective_prosody_caps`. You'll see many activations at I4–I5 in delivery-003 — that's the intended behaviour, calibrated by listening test in May 2026 (SynthBanshee PR #87). --- -## Distribution in delivery-003 +## Where the labels come from + +- **Strong labels (`.jsonl`)** are generated by SynthBanshee from the LLM-authored script — the LLM produces turn-level intensity and an action tag (`VERB_SHOUT`, `DIST_SCREAM`, …), SynthBanshee converts them into `EventLabel` records. +- **Weak labels (`.json` → `weak_label`)** are derived from the strong labels by aggregation. +- **No human annotation has happened.** `confidence` is always `1.0`; `label_source` is always `"auto"`; `iaa_reviewed` is always `false`. Future deliveries may introduce human review on a subset — they'll set `iaa_reviewed: true` per clip when that happens. + +The scripts themselves are LLM-generated Hebrew dialogue, conditioned on the scene YAML and persona definitions in SynthBanshee. They are **not** transcripts of real conversations. -| Typology | Clips | Projects | Tiers | -|----------|------:|---------|-------| -| SV | 5 | she_proves (3) + elephant (2) | A (3) + B (2) | -| IT | 5 | she_proves (3) + elephant (2) | A (3) + B (2) | -| NEG | 5 | she_proves (3) + elephant (2) | A (3) + B (2) | -| NEU | 5 | she_proves (3) + elephant (2) | A (3) + B (2) | +--- + +## Distribution in delivery-003 -Intensity distribution across all 20 clips: +| Typology | Clips | Tier A (she_proves) | Tier B (elephant) | +|----------|------:|:-------------------:|:------------------:| +| `SV` | 5 | 3 | 2 | +| `IT` | 5 | 3 | 2 | +| `NEG` | 5 | 3 | 2 | +| `NEU` | 5 | 3 | 2 | -| Max intensity | Clips | -|:---:|:---:| -| 5 | 10 | -| 3 | 4 | -| 2 | 6 | +Balanced across typology and across project. Not balanced across speakers — see [Deliveries](deliveries.md#known-limitations). diff --git a/mkdocs.yml b/mkdocs.yml index 11dbc73..d6b6d04 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ -site_name: avdp-synth-corpus -site_description: Synthetic Hebrew audio corpus for the Audio Violence Detection Pipeline — consumer guide for She-Proves and Elephant in the Room teams +site_name: AVDP Synthetic Corpus +site_description: Synthetic Hebrew audio corpus for the Audio Violence Detection Pipeline — consumer guide for the She-Proves and Elephant in the Room teams site_url: https://datahackil.github.io/avdp-synth-corpus/ repo_url: https://github.com/DataHackIL/avdp-synth-corpus repo_name: DataHackIL/avdp-synth-corpus @@ -26,7 +26,6 @@ theme: - navigation.tabs - navigation.tabs.sticky - navigation.sections - - navigation.expand - navigation.indexes - navigation.top - toc.follow @@ -38,6 +37,9 @@ theme: - content.tabs.link - announce.dismiss +extra_css: + - assets/extra.css + markdown_extensions: - admonition - pymdownx.details @@ -63,6 +65,7 @@ markdown_extensions: - toc: permalink: true - def_list + - abbr plugins: - search: @@ -70,7 +73,8 @@ plugins: nav: - Home: index.md - - Getting Started: getting-started.md + - Start here: getting-started.md + - Common mistakes: gotchas.md - Team Guides: - She-Proves: she-proves.md - Elephant in the Room: elephant.md @@ -78,6 +82,7 @@ nav: - Label Taxonomy: taxonomy.md - Schema Reference: schema.md - Audio Format: audio-format.md + - Glossary: glossary.md - Deliveries: deliveries.md extra: