Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +28 -0
- README.md +26 -7
- app.py +23 -0
- backend/__init__.py +0 -0
- backend/audio_utils.py +13 -0
- backend/capabilities.py +64 -0
- backend/easy_turn.py +7 -0
- backend/encoders.py +79 -0
- backend/fusion.py +21 -0
- backend/gate.py +67 -0
- backend/heads.py +56 -0
- backend/orchestrator.py +117 -0
- backend/pooling.py +26 -0
- backend/presets.py +46 -0
- backend/semantic.py +97 -0
- backend/smart_turn_onnx.py +50 -0
- backend/streaming_semantic.py +59 -0
- backend/types.py +65 -0
- config.py +33 -0
- database/audio/sample_clips/english_fleurs_1511.wav +3 -0
- database/audio/sample_clips/english_fleurs_1512.wav +3 -0
- database/audio/sample_clips/english_fleurs_1545.wav +3 -0
- database/audio/sample_clips/english_fleurs_1546.wav +3 -0
- database/audio/sample_clips/english_fleurs_1554.wav +0 -0
- database/audio/sample_clips/english_fleurs_1578.wav +3 -0
- database/audio/sample_clips/english_fleurs_1595.wav +3 -0
- database/audio/sample_clips/english_fleurs_1637.wav +3 -0
- database/audio/sample_clips/english_fleurs_1639.wav +3 -0
- database/audio/sample_clips/english_fleurs_1645.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1524.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1526.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1540.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1549.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1560.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1581.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1609.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1620.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1641.wav +3 -0
- database/audio/sample_clips/hindi_fleurs_1645.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD09072.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD13003.wav +0 -0
- database/audio/sample_clips/hinglish_hiacc_AD22092.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD23025.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD36007.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD36064.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD40049.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD40103.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD40158.wav +3 -0
- database/audio/sample_clips/hinglish_hiacc_AD60096.wav +3 -0
- database/audio/sample_clips/manifest.json +232 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,31 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
database/audio/sample_clips/english_fleurs_1511.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
database/audio/sample_clips/english_fleurs_1512.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
database/audio/sample_clips/english_fleurs_1545.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
database/audio/sample_clips/english_fleurs_1546.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
database/audio/sample_clips/english_fleurs_1578.wav filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
database/audio/sample_clips/english_fleurs_1595.wav filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
database/audio/sample_clips/english_fleurs_1637.wav filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
database/audio/sample_clips/english_fleurs_1639.wav filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
database/audio/sample_clips/english_fleurs_1645.wav filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
database/audio/sample_clips/hindi_fleurs_1524.wav filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
database/audio/sample_clips/hindi_fleurs_1526.wav filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
database/audio/sample_clips/hindi_fleurs_1540.wav filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
database/audio/sample_clips/hindi_fleurs_1549.wav filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
database/audio/sample_clips/hindi_fleurs_1560.wav filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
database/audio/sample_clips/hindi_fleurs_1581.wav filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
database/audio/sample_clips/hindi_fleurs_1609.wav filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
database/audio/sample_clips/hindi_fleurs_1620.wav filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
database/audio/sample_clips/hindi_fleurs_1641.wav filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
database/audio/sample_clips/hindi_fleurs_1645.wav filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
database/audio/sample_clips/hinglish_hiacc_AD09072.wav filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
database/audio/sample_clips/hinglish_hiacc_AD22092.wav filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
database/audio/sample_clips/hinglish_hiacc_AD23025.wav filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
database/audio/sample_clips/hinglish_hiacc_AD36007.wav filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
database/audio/sample_clips/hinglish_hiacc_AD36064.wav filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
database/audio/sample_clips/hinglish_hiacc_AD40049.wav filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
database/audio/sample_clips/hinglish_hiacc_AD40103.wav filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
database/audio/sample_clips/hinglish_hiacc_AD40158.wav filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
database/audio/sample_clips/hinglish_hiacc_AD60096.wav filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,13 +1,32 @@
|
|
| 1 |
---
|
| 2 |
-
title: Turn Detection
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Turn Detection Live Dashboard
|
| 3 |
+
emoji: 🎙️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.25.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Turn Detection — Live Dashboard
|
| 14 |
+
|
| 15 |
+
A real-time dashboard for audio turn detection: deciding whether a speaker is
|
| 16 |
+
finished talking or just pausing. Speak into the mic or replay a bundled clip
|
| 17 |
+
and watch several models score the decision live, plotted against the
|
| 18 |
+
waveform.
|
| 19 |
+
|
| 20 |
+
## Models
|
| 21 |
+
Smart Turn v3.2 (zero-shot), Whisper with trained heads, a Qwen-based semantic
|
| 22 |
+
judge, the LiveKit end-of-turn detector, acoustic + semantic fusion, and a
|
| 23 |
+
streaming semantic branch.
|
| 24 |
+
|
| 25 |
+
## How to use
|
| 26 |
+
1. Select one or more models.
|
| 27 |
+
2. Record from the mic, or replay a sample clip / upload a recording.
|
| 28 |
+
3. Each model's "turn complete" probability is drawn against the waveform; the
|
| 29 |
+
dashed line is the decision threshold.
|
| 30 |
+
|
| 31 |
+
Trained head checkpoints and demo clips are bundled. Other model weights
|
| 32 |
+
download on first use.
|
app.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import traceback
|
| 2 |
+
import numpy as np
|
| 3 |
+
from backend.orchestrator import run_pipeline
|
| 4 |
+
from backend.presets import list_presets
|
| 5 |
+
from frontend.layout import build_app
|
| 6 |
+
|
| 7 |
+
def _warmup() -> None:
|
| 8 |
+
sr = 16000
|
| 9 |
+
dummy = np.zeros(sr, dtype=np.float32)
|
| 10 |
+
for preset in list_presets():
|
| 11 |
+
if not preset.get('available'):
|
| 12 |
+
continue
|
| 13 |
+
try:
|
| 14 |
+
run_pipeline(preset['config'], dummy, sample_rate=sr, preset_label=preset['label'])
|
| 15 |
+
print(f"[warmup] ready: {preset['label']}")
|
| 16 |
+
except Exception as exc:
|
| 17 |
+
print(f"[warmup] skipped {preset['label']}: {exc}")
|
| 18 |
+
traceback.print_exc()
|
| 19 |
+
print('[warmup] done')
|
| 20 |
+
if __name__ == '__main__':
|
| 21 |
+
_warmup()
|
| 22 |
+
demo = build_app()
|
| 23 |
+
demo.launch(share=False)
|
backend/__init__.py
ADDED
|
File without changes
|
backend/audio_utils.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import numpy as np
|
| 3 |
+
import config
|
| 4 |
+
|
| 5 |
+
def truncate_or_left_pad(audio: np.ndarray, n_seconds: int=config.SMART_TURN_WINDOW_SECONDS, sample_rate: int=config.SAMPLE_RATE) -> np.ndarray:
|
| 6 |
+
audio = np.asarray(audio, dtype=np.float32)
|
| 7 |
+
max_samples = n_seconds * sample_rate
|
| 8 |
+
if len(audio) > max_samples:
|
| 9 |
+
return audio[-max_samples:]
|
| 10 |
+
if len(audio) < max_samples:
|
| 11 |
+
padding = max_samples - len(audio)
|
| 12 |
+
return np.pad(audio, (padding, 0), mode='constant', constant_values=0)
|
| 13 |
+
return audio
|
backend/capabilities.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import json
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Optional
|
| 5 |
+
import config
|
| 6 |
+
STATIC_CAPABILITIES: dict[str, dict] = {'gate.webrtcvad': {'available': True, 'reason': None, 'provenance': 'real_checkpoint'}, 'gate.silero_vad': {'available': True, 'reason': None, 'provenance': 'real_checkpoint'}, 'gate.none': {'available': True, 'reason': None, 'provenance': 'rule'}, 'acoustic.smart_turn_onnx': {'available': True, 'reason': None, 'provenance': 'real_checkpoint'}, 'semantic.qwen_local': {'available': True, 'reason': None, 'provenance': 'real_checkpoint'}, 'semantic.livekit_eou': {'available': True, 'reason': None, 'provenance': 'real_checkpoint'}, 'semantic.qwen_local_streaming': {'available': True, 'reason': None, 'provenance': 'architecture_reimplemented'}, 'semantic.groq_api': {'available': False, 'reason': 'no API key configured - set GROQ_API_KEY to enable (see docs/decision-log.md #17)', 'provenance': 'unavailable'}, 'semantic.openrouter_api': {'available': False, 'reason': 'no API key configured - set OPENROUTER_API_KEY to enable (see docs/decision-log.md #17)', 'provenance': 'unavailable'}, 'fusion.weighted_vote': {'available': True, 'reason': None, 'provenance': 'rule'}, 'fusion.easy_turn': {'available': False, 'reason': "Easy Turn's linguistic branch needs its own ASR component we haven't wired yet - checkpoint downloaded for reference only (see docs/decision-log.md #19)", 'provenance': 'unavailable'}, 'mode.full_duplex_bypass.moshi': {'available': False, 'reason': 'needs GPU VRAM not available on this machine; API routing deferred (see docs/decision-log.md #6, #17)', 'provenance': 'unavailable'}, 'mode.full_duplex_bypass.human1': {'available': False, 'reason': 'needs GPU VRAM not available on this machine; API routing deferred (see docs/decision-log.md #6, #17)', 'provenance': 'unavailable'}}
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class CapabilityInfo:
|
| 10 |
+
key: str
|
| 11 |
+
available: bool
|
| 12 |
+
reason: Optional[str]
|
| 13 |
+
provenance: Optional[str]
|
| 14 |
+
|
| 15 |
+
def _head_checkpoint_path(encoder: str, pooling: str, head: str) -> tuple:
|
| 16 |
+
stem = f'{encoder}_{pooling}_{head}'
|
| 17 |
+
ckpt = config.CHECKPOINTS_DIR / f'{stem}.pt'
|
| 18 |
+
meta = config.CHECKPOINTS_DIR / f'{stem}.metadata.json'
|
| 19 |
+
return (ckpt, meta)
|
| 20 |
+
|
| 21 |
+
def head_capability_key(encoder: str, pooling: str, head: str) -> str:
|
| 22 |
+
return f'acoustic.head.{encoder}.{pooling}.{head}'
|
| 23 |
+
|
| 24 |
+
def _check_trained_head(encoder: str, pooling: str, head: str) -> CapabilityInfo:
|
| 25 |
+
key = head_capability_key(encoder, pooling, head)
|
| 26 |
+
ckpt, meta = _head_checkpoint_path(encoder, pooling, head)
|
| 27 |
+
if not (ckpt.exists() and meta.exists()):
|
| 28 |
+
return CapabilityInfo(key=key, available=False, reason=f'not trained yet - run experiments/train_head.py to produce database/checkpoints/{encoder}_{pooling}_{head}.pt', provenance='unavailable')
|
| 29 |
+
try:
|
| 30 |
+
metadata = json.loads(meta.read_text())
|
| 31 |
+
except (OSError, json.JSONDecodeError) as exc:
|
| 32 |
+
return CapabilityInfo(key=key, available=False, reason=f'checkpoint metadata unreadable ({exc}) - retrain via experiments/train_head.py', provenance='unavailable')
|
| 33 |
+
if metadata.get('encoder') != encoder or metadata.get('pooling') != pooling or metadata.get('head') != head:
|
| 34 |
+
return CapabilityInfo(key=key, available=False, reason='checkpoint metadata does not match the requested (encoder, pooling, head) combination', provenance='unavailable')
|
| 35 |
+
return CapabilityInfo(key=key, available=True, reason=None, provenance='trained_by_us')
|
| 36 |
+
|
| 37 |
+
def get(key: str) -> CapabilityInfo:
|
| 38 |
+
if key.startswith('acoustic.head.'):
|
| 39 |
+
_, _, encoder, pooling, head = key.split('.')
|
| 40 |
+
return _check_trained_head(encoder, pooling, head)
|
| 41 |
+
if key in STATIC_CAPABILITIES:
|
| 42 |
+
entry = STATIC_CAPABILITIES[key]
|
| 43 |
+
return CapabilityInfo(key=key, **entry)
|
| 44 |
+
raise KeyError(f'unknown capability key: {key!r}')
|
| 45 |
+
|
| 46 |
+
def is_available(key: str) -> bool:
|
| 47 |
+
return get(key).available
|
| 48 |
+
|
| 49 |
+
def list_trained_heads() -> list[CapabilityInfo]:
|
| 50 |
+
found = []
|
| 51 |
+
if not config.CHECKPOINTS_DIR.exists():
|
| 52 |
+
return found
|
| 53 |
+
for meta_path in sorted(config.CHECKPOINTS_DIR.glob('*.metadata.json')):
|
| 54 |
+
try:
|
| 55 |
+
metadata = json.loads(meta_path.read_text())
|
| 56 |
+
except (OSError, json.JSONDecodeError):
|
| 57 |
+
continue
|
| 58 |
+
encoder, pooling, head = (metadata.get('encoder'), metadata.get('pooling'), metadata.get('head'))
|
| 59 |
+
if not all([encoder, pooling, head]):
|
| 60 |
+
continue
|
| 61 |
+
info = _check_trained_head(encoder, pooling, head)
|
| 62 |
+
if info.available:
|
| 63 |
+
found.append(info)
|
| 64 |
+
return found
|
backend/easy_turn.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from backend.capabilities import get
|
| 3 |
+
from backend.types import StageResult
|
| 4 |
+
|
| 5 |
+
def run(*_args, **_kwargs) -> StageResult:
|
| 6 |
+
info = get('fusion.easy_turn')
|
| 7 |
+
return StageResult(stage='fusion.easy_turn', timing_ms=0.0, output=None, available=False, reason=info.reason, provenance='unavailable')
|
backend/encoders.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import math
|
| 3 |
+
import time
|
| 4 |
+
from typing import Literal
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import config
|
| 8 |
+
from backend.audio_utils import truncate_or_left_pad
|
| 9 |
+
from backend.types import StageResult
|
| 10 |
+
EncoderName = Literal['whisper_tiny', 'whisper_base', 'wav2vec2']
|
| 11 |
+
ENCODER_WINDOW_SECONDS = {'whisper_tiny': 30, 'whisper_base': 30, 'wav2vec2': config.SMART_TURN_WINDOW_SECONDS}
|
| 12 |
+
_ENCODER_OUTPUT_HOP_SAMPLES = 320
|
| 13 |
+
_device = 'mps' if torch.backends.mps.is_available() else 'cpu'
|
| 14 |
+
_whisper_models: dict[str, tuple] = {}
|
| 15 |
+
_wav2vec2_model = None
|
| 16 |
+
_wav2vec2_processor = None
|
| 17 |
+
|
| 18 |
+
def _get_whisper(model_id: str):
|
| 19 |
+
if model_id not in _whisper_models:
|
| 20 |
+
from transformers import WhisperFeatureExtractor, WhisperModel
|
| 21 |
+
feature_extractor = WhisperFeatureExtractor(chunk_length=30)
|
| 22 |
+
model = WhisperModel.from_pretrained(model_id).to(_device).eval()
|
| 23 |
+
_whisper_models[model_id] = (feature_extractor, model)
|
| 24 |
+
return _whisper_models[model_id]
|
| 25 |
+
|
| 26 |
+
def _get_wav2vec2():
|
| 27 |
+
global _wav2vec2_model, _wav2vec2_processor
|
| 28 |
+
if _wav2vec2_model is None:
|
| 29 |
+
from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2Model
|
| 30 |
+
_wav2vec2_processor = Wav2Vec2FeatureExtractor.from_pretrained(config.WAV2VEC2_ID)
|
| 31 |
+
_wav2vec2_model = Wav2Vec2Model.from_pretrained(config.WAV2VEC2_ID).to(_device).eval()
|
| 32 |
+
return (_wav2vec2_processor, _wav2vec2_model)
|
| 33 |
+
|
| 34 |
+
def _valid_output_frames(original_num_samples: int, window_seconds: int, sample_rate: int) -> int:
|
| 35 |
+
real_samples = min(original_num_samples, window_seconds * sample_rate)
|
| 36 |
+
return max(1, math.ceil(real_samples / _ENCODER_OUTPUT_HOP_SAMPLES))
|
| 37 |
+
|
| 38 |
+
@torch.inference_mode()
|
| 39 |
+
def _encode_whisper(model_id: str, audio: np.ndarray, sample_rate: int) -> tuple[np.ndarray, int]:
|
| 40 |
+
feature_extractor, model = _get_whisper(model_id)
|
| 41 |
+
window_seconds = ENCODER_WINDOW_SECONDS['whisper_tiny']
|
| 42 |
+
window_samples = window_seconds * sample_rate
|
| 43 |
+
valid_frames = _valid_output_frames(len(audio), window_seconds, sample_rate)
|
| 44 |
+
padded = truncate_or_left_pad(audio, window_seconds, sample_rate)
|
| 45 |
+
inputs = feature_extractor(padded, sampling_rate=sample_rate, return_tensors='pt', padding='max_length', max_length=window_samples, truncation=True, do_normalize=True)
|
| 46 |
+
input_features = inputs.input_features.to(_device)
|
| 47 |
+
encoder = model.get_encoder()
|
| 48 |
+
hidden_states = encoder(input_features).last_hidden_state
|
| 49 |
+
hidden_states = hidden_states.squeeze(0).float().cpu().numpy()
|
| 50 |
+
valid_frames = min(valid_frames, hidden_states.shape[0])
|
| 51 |
+
return (hidden_states, valid_frames)
|
| 52 |
+
|
| 53 |
+
@torch.inference_mode()
|
| 54 |
+
def _encode_wav2vec2(audio: np.ndarray, sample_rate: int) -> tuple[np.ndarray, int]:
|
| 55 |
+
processor, model = _get_wav2vec2()
|
| 56 |
+
window_seconds = ENCODER_WINDOW_SECONDS['wav2vec2']
|
| 57 |
+
valid_frames = _valid_output_frames(len(audio), window_seconds, sample_rate)
|
| 58 |
+
padded = truncate_or_left_pad(audio, window_seconds, sample_rate)
|
| 59 |
+
inputs = processor(padded, sampling_rate=sample_rate, return_tensors='pt')
|
| 60 |
+
input_values = inputs.input_values.to(_device)
|
| 61 |
+
hidden_states = model(input_values).last_hidden_state
|
| 62 |
+
hidden_states = hidden_states.squeeze(0).float().cpu().numpy()
|
| 63 |
+
valid_frames = min(valid_frames, hidden_states.shape[0])
|
| 64 |
+
return (hidden_states, valid_frames)
|
| 65 |
+
|
| 66 |
+
def extract_hidden_states(encoder: EncoderName, audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE) -> tuple[np.ndarray, int]:
|
| 67 |
+
if encoder == 'whisper_tiny':
|
| 68 |
+
return _encode_whisper(config.WHISPER_TINY_ID, audio, sample_rate)
|
| 69 |
+
if encoder == 'whisper_base':
|
| 70 |
+
return _encode_whisper(config.WHISPER_BASE_ID, audio, sample_rate)
|
| 71 |
+
if encoder == 'wav2vec2':
|
| 72 |
+
return _encode_wav2vec2(audio, sample_rate)
|
| 73 |
+
raise ValueError(f'unknown encoder: {encoder!r}')
|
| 74 |
+
|
| 75 |
+
def run(encoder: EncoderName, audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE) -> StageResult:
|
| 76 |
+
start = time.perf_counter()
|
| 77 |
+
hidden_states, valid_length = extract_hidden_states(encoder, audio, sample_rate)
|
| 78 |
+
timing_ms = (time.perf_counter() - start) * 1000
|
| 79 |
+
return StageResult(stage=f'encoder.{encoder}', timing_ms=timing_ms, output={'hidden_states': hidden_states, 'valid_length': valid_length}, available=True, provenance='real_checkpoint')
|
backend/fusion.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import time
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from backend.types import StageResult
|
| 5 |
+
|
| 6 |
+
def weighted_vote(acoustic_probability: float, semantic_verdict: Optional[str]=None, semantic_probability: Optional[float]=None, acoustic_weight: float=0.6) -> dict:
|
| 7 |
+
if semantic_verdict == 'wait':
|
| 8 |
+
return {'decision': 'incomplete', 'probability': min(acoustic_probability, 0.2), 'rule': "semantic 'wait' verdict overrides acoustic signal"}
|
| 9 |
+
if semantic_probability is not None:
|
| 10 |
+
semantic_score = semantic_probability
|
| 11 |
+
else:
|
| 12 |
+
semantic_score = 1.0 if semantic_verdict == 'complete' else 0.0
|
| 13 |
+
fused_probability = acoustic_weight * acoustic_probability + (1 - acoustic_weight) * semantic_score
|
| 14 |
+
decision = 'complete' if fused_probability > 0.5 else 'incomplete'
|
| 15 |
+
return {'decision': decision, 'probability': fused_probability, 'rule': f'weighted_vote(acoustic_weight={acoustic_weight})'}
|
| 16 |
+
|
| 17 |
+
def run(acoustic_probability: float, semantic_verdict: Optional[str]=None, semantic_probability: Optional[float]=None, acoustic_weight: float=0.6) -> StageResult:
|
| 18 |
+
start = time.perf_counter()
|
| 19 |
+
result = weighted_vote(acoustic_probability, semantic_verdict, semantic_probability, acoustic_weight)
|
| 20 |
+
timing_ms = (time.perf_counter() - start) * 1000
|
| 21 |
+
return StageResult(stage='fusion.weighted_vote', timing_ms=timing_ms, output=result, available=True, provenance='rule')
|
backend/gate.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import time
|
| 3 |
+
import numpy as np
|
| 4 |
+
import webrtcvad
|
| 5 |
+
import config
|
| 6 |
+
from backend.types import StageResult
|
| 7 |
+
_silero_model = None
|
| 8 |
+
_silero_utils = None
|
| 9 |
+
|
| 10 |
+
def _get_silero():
|
| 11 |
+
global _silero_model, _silero_utils
|
| 12 |
+
if _silero_model is None:
|
| 13 |
+
import torch
|
| 14 |
+
_silero_model, _silero_utils = torch.hub.load(repo_or_dir='snakers4/silero-vad', model='silero_vad', trust_repo=True)
|
| 15 |
+
return (_silero_model, _silero_utils)
|
| 16 |
+
|
| 17 |
+
def _float_audio(audio: np.ndarray) -> np.ndarray:
|
| 18 |
+
audio = np.asarray(audio)
|
| 19 |
+
if audio.dtype.kind == 'i':
|
| 20 |
+
return audio.astype(np.float32) / 32768.0
|
| 21 |
+
return audio.astype(np.float32)
|
| 22 |
+
|
| 23 |
+
def _pcm16_bytes(audio_f32: np.ndarray) -> bytes:
|
| 24 |
+
clipped = np.clip(audio_f32, -1.0, 1.0)
|
| 25 |
+
return (clipped * 32767.0).astype(np.int16).tobytes()
|
| 26 |
+
|
| 27 |
+
def run_webrtcvad(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE, aggressiveness: int=2) -> StageResult:
|
| 28 |
+
start = time.perf_counter()
|
| 29 |
+
audio_f32 = _float_audio(audio)
|
| 30 |
+
pcm = _pcm16_bytes(audio_f32)
|
| 31 |
+
frame_ms = 30
|
| 32 |
+
frame_bytes = int(sample_rate * (frame_ms / 1000.0)) * 2
|
| 33 |
+
vad = webrtcvad.Vad(aggressiveness)
|
| 34 |
+
speech_frames = 0
|
| 35 |
+
total_frames = 0
|
| 36 |
+
for offset in range(0, len(pcm) - frame_bytes + 1, frame_bytes):
|
| 37 |
+
frame = pcm[offset:offset + frame_bytes]
|
| 38 |
+
total_frames += 1
|
| 39 |
+
if vad.is_speech(frame, sample_rate):
|
| 40 |
+
speech_frames += 1
|
| 41 |
+
speech_detected = total_frames > 0 and speech_frames / total_frames > 0.1
|
| 42 |
+
timing_ms = (time.perf_counter() - start) * 1000
|
| 43 |
+
return StageResult(stage='gate.webrtcvad', timing_ms=timing_ms, output={'speech_detected': speech_detected, 'speech_frame_ratio': speech_frames / total_frames if total_frames else 0.0}, available=True, provenance='real_checkpoint')
|
| 44 |
+
|
| 45 |
+
def run_silero(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE, threshold: float=0.5) -> StageResult:
|
| 46 |
+
import torch
|
| 47 |
+
start = time.perf_counter()
|
| 48 |
+
model, utils = _get_silero()
|
| 49 |
+
get_speech_timestamps = utils[0]
|
| 50 |
+
audio_f32 = _float_audio(audio)
|
| 51 |
+
tensor = torch.from_numpy(audio_f32)
|
| 52 |
+
timestamps = get_speech_timestamps(tensor, model, sampling_rate=sample_rate, threshold=threshold)
|
| 53 |
+
speech_detected = len(timestamps) > 0
|
| 54 |
+
timing_ms = (time.perf_counter() - start) * 1000
|
| 55 |
+
return StageResult(stage='gate.silero_vad', timing_ms=timing_ms, output={'speech_detected': speech_detected, 'speech_segments': timestamps}, available=True, provenance='real_checkpoint')
|
| 56 |
+
|
| 57 |
+
def run_none(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE) -> StageResult:
|
| 58 |
+
return StageResult(stage='gate.none', timing_ms=0.0, output={'speech_detected': True}, available=True, provenance='rule')
|
| 59 |
+
|
| 60 |
+
def run(gate: str, audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE, **kwargs) -> StageResult:
|
| 61 |
+
if gate == 'webrtcvad':
|
| 62 |
+
return run_webrtcvad(audio, sample_rate, aggressiveness=kwargs.get('vad_aggressiveness', 2))
|
| 63 |
+
if gate == 'silero_vad':
|
| 64 |
+
return run_silero(audio, sample_rate)
|
| 65 |
+
if gate == 'none':
|
| 66 |
+
return run_none(audio, sample_rate)
|
| 67 |
+
raise ValueError(f'unknown gate: {gate!r}')
|
backend/heads.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import config
|
| 8 |
+
|
| 9 |
+
class LinearHead(nn.Module):
|
| 10 |
+
|
| 11 |
+
def __init__(self, input_dim: int):
|
| 12 |
+
super().__init__()
|
| 13 |
+
self.linear = nn.Linear(input_dim, 1)
|
| 14 |
+
|
| 15 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 16 |
+
return self.linear(x).squeeze(-1)
|
| 17 |
+
|
| 18 |
+
class MLPHead(nn.Module):
|
| 19 |
+
|
| 20 |
+
def __init__(self, input_dim: int, hidden_dim: int=128):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.net = nn.Sequential(nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1))
|
| 23 |
+
|
| 24 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 25 |
+
return self.net(x).squeeze(-1)
|
| 26 |
+
|
| 27 |
+
def build_head(head_type: str, input_dim: int) -> nn.Module:
|
| 28 |
+
if head_type == 'linear':
|
| 29 |
+
return LinearHead(input_dim)
|
| 30 |
+
if head_type == 'mlp':
|
| 31 |
+
return MLPHead(input_dim)
|
| 32 |
+
raise ValueError(f'unknown head_type: {head_type!r}')
|
| 33 |
+
|
| 34 |
+
def checkpoint_paths(encoder: str, pooling: str, head: str) -> tuple[Path, Path]:
|
| 35 |
+
stem = f'{encoder}_{pooling}_{head}'
|
| 36 |
+
return (config.CHECKPOINTS_DIR / f'{stem}.pt', config.CHECKPOINTS_DIR / f'{stem}.metadata.json')
|
| 37 |
+
|
| 38 |
+
def save(encoder: str, pooling: str, head_type: str, input_dim: int, head_state_dict: dict, metrics: dict, pooling_state_dict: Optional[dict]=None, trained_on: Optional[str]=None) -> None:
|
| 39 |
+
config.CHECKPOINTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 40 |
+
ckpt_path, meta_path = checkpoint_paths(encoder, pooling, head_type)
|
| 41 |
+
torch.save({'head_state_dict': head_state_dict, 'pooling_state_dict': pooling_state_dict, 'input_dim': input_dim}, ckpt_path)
|
| 42 |
+
metadata = {'encoder': encoder, 'pooling': pooling, 'head': head_type, 'input_dim': input_dim, 'metrics': metrics}
|
| 43 |
+
if trained_on is not None:
|
| 44 |
+
metadata['trained_on'] = trained_on
|
| 45 |
+
meta_path.write_text(json.dumps(metadata, indent=2))
|
| 46 |
+
|
| 47 |
+
def load(encoder: str, pooling: str, head_type: str) -> tuple[nn.Module, Optional[dict], dict]:
|
| 48 |
+
ckpt_path, meta_path = checkpoint_paths(encoder, pooling, head_type)
|
| 49 |
+
if not (ckpt_path.exists() and meta_path.exists()):
|
| 50 |
+
raise FileNotFoundError(f'no trained checkpoint at {ckpt_path} - run experiments/train_head.py first (see backend/capabilities.py for the exact command).')
|
| 51 |
+
metadata = json.loads(meta_path.read_text())
|
| 52 |
+
blob = torch.load(ckpt_path, map_location='cpu', weights_only=True)
|
| 53 |
+
head = build_head(head_type, blob['input_dim'])
|
| 54 |
+
head.load_state_dict(blob['head_state_dict'])
|
| 55 |
+
head.eval()
|
| 56 |
+
return (head, blob.get('pooling_state_dict'), metadata)
|
backend/orchestrator.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import time
|
| 3 |
+
from typing import Optional
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import config as cfg
|
| 7 |
+
from backend import encoders, fusion, gate, heads, pooling, semantic, smart_turn_onnx, streaming_semantic
|
| 8 |
+
from backend.capabilities import get as get_capability
|
| 9 |
+
from backend.capabilities import head_capability_key
|
| 10 |
+
from backend.types import PipelineConfig, PipelineResult, StageResult, StageUnavailableError
|
| 11 |
+
|
| 12 |
+
def _require(capability_key: str) -> None:
|
| 13 |
+
info = get_capability(capability_key)
|
| 14 |
+
if not info.available:
|
| 15 |
+
raise StageUnavailableError(f'{capability_key} is not available: {info.reason}')
|
| 16 |
+
|
| 17 |
+
def _run_custom_acoustic_branch(pc: PipelineConfig, audio: np.ndarray, sample_rate: int) -> tuple[StageResult, StageResult]:
|
| 18 |
+
head_key = head_capability_key(pc.encoder, pc.pooling, pc.head)
|
| 19 |
+
_require(head_key)
|
| 20 |
+
encoder_result = encoders.run(pc.encoder, audio, sample_rate)
|
| 21 |
+
hidden_states = encoder_result.output['hidden_states']
|
| 22 |
+
valid_length = encoder_result.output['valid_length']
|
| 23 |
+
head_module, pooling_state_dict, metadata = heads.load(pc.encoder, pc.pooling, pc.head)
|
| 24 |
+
pool_start = time.perf_counter()
|
| 25 |
+
with torch.inference_mode():
|
| 26 |
+
if pc.pooling == 'mean':
|
| 27 |
+
pooled = pooling.mean_pool(hidden_states, valid_length)
|
| 28 |
+
pooled_tensor = torch.from_numpy(pooled).float().unsqueeze(0)
|
| 29 |
+
elif pc.pooling == 'cross_attention':
|
| 30 |
+
attn_pool = pooling.CrossAttentionPool(hidden_dim=hidden_states.shape[-1])
|
| 31 |
+
attn_pool.load_state_dict(pooling_state_dict)
|
| 32 |
+
attn_pool.eval()
|
| 33 |
+
hs_tensor = torch.from_numpy(hidden_states).float().unsqueeze(0)
|
| 34 |
+
vl_tensor = torch.tensor([valid_length])
|
| 35 |
+
pooled_tensor = attn_pool(hs_tensor, vl_tensor)
|
| 36 |
+
else:
|
| 37 |
+
raise ValueError(f'unknown pooling: {pc.pooling!r}')
|
| 38 |
+
logit = head_module(pooled_tensor)
|
| 39 |
+
probability = torch.sigmoid(logit).item()
|
| 40 |
+
pool_head_ms = (time.perf_counter() - pool_start) * 1000
|
| 41 |
+
decision = 'complete' if probability > 0.5 else 'incomplete'
|
| 42 |
+
head_result = StageResult(stage=f'acoustic.custom.{pc.encoder}.{pc.pooling}.{pc.head}', timing_ms=pool_head_ms, output={'decision': decision, 'probability': probability, 'metrics_at_train_time': metadata.get('metrics')}, available=True, provenance='trained_by_us')
|
| 43 |
+
return (encoder_result, head_result)
|
| 44 |
+
|
| 45 |
+
def run_pipeline(pc: PipelineConfig, audio: np.ndarray, sample_rate: int=cfg.SAMPLE_RATE, preset_label: Optional[str]=None) -> PipelineResult:
|
| 46 |
+
if pc.full_duplex != 'off':
|
| 47 |
+
_require(f'mode.full_duplex_bypass.{pc.full_duplex}')
|
| 48 |
+
raise StageUnavailableError('full-duplex bypass is not implemented in this pass')
|
| 49 |
+
stage_results: list[StageResult] = []
|
| 50 |
+
start = time.perf_counter()
|
| 51 |
+
_require(f'gate.{pc.gate}')
|
| 52 |
+
gate_result = gate.run(pc.gate, audio, sample_rate, vad_aggressiveness=pc.vad_aggressiveness)
|
| 53 |
+
stage_results.append(gate_result)
|
| 54 |
+
acoustic_result = None
|
| 55 |
+
acoustic_probability = None
|
| 56 |
+
acoustic_provenance = None
|
| 57 |
+
if pc.encoder == 'none':
|
| 58 |
+
pass
|
| 59 |
+
elif pc.encoder == 'smart_turn_onnx':
|
| 60 |
+
_require('acoustic.smart_turn_onnx')
|
| 61 |
+
acoustic_result = smart_turn_onnx.run(audio, sample_rate)
|
| 62 |
+
stage_results.append(acoustic_result)
|
| 63 |
+
acoustic_probability = acoustic_result.output['probability']
|
| 64 |
+
acoustic_provenance = 'real_checkpoint'
|
| 65 |
+
else:
|
| 66 |
+
encoder_result, head_result = _run_custom_acoustic_branch(pc, audio, sample_rate)
|
| 67 |
+
stage_results.extend([encoder_result, head_result])
|
| 68 |
+
acoustic_result = head_result
|
| 69 |
+
acoustic_probability = head_result.output['probability']
|
| 70 |
+
acoustic_provenance = 'trained_by_us'
|
| 71 |
+
transcript = None
|
| 72 |
+
semantic_verdict = None
|
| 73 |
+
semantic_probability = None
|
| 74 |
+
if pc.semantic != 'off':
|
| 75 |
+
_require(f'semantic.{pc.semantic}')
|
| 76 |
+
if pc.semantic == 'qwen_local':
|
| 77 |
+
semantic_result = semantic.run(audio, sample_rate, temperature=pc.semantic_temperature)
|
| 78 |
+
stage_results.append(semantic_result)
|
| 79 |
+
transcript = semantic_result.output['transcript']
|
| 80 |
+
semantic_verdict = semantic_result.output['verdict']
|
| 81 |
+
elif pc.semantic == 'qwen_local_streaming':
|
| 82 |
+
semantic_result = streaming_semantic.run(audio, sample_rate, temperature=pc.semantic_temperature)
|
| 83 |
+
stage_results.append(semantic_result)
|
| 84 |
+
transcript = semantic_result.output['transcript']
|
| 85 |
+
semantic_verdict = semantic_result.output['verdict']
|
| 86 |
+
elif pc.semantic == 'livekit_eou':
|
| 87 |
+
semantic_result = semantic.run_livekit(audio, sample_rate)
|
| 88 |
+
stage_results.append(semantic_result)
|
| 89 |
+
transcript = semantic_result.output['transcript']
|
| 90 |
+
semantic_probability = semantic_result.output['probability']
|
| 91 |
+
else:
|
| 92 |
+
raise StageUnavailableError(f'semantic provider {pc.semantic!r} is not implemented yet')
|
| 93 |
+
if pc.fusion == 'weighted_vote':
|
| 94 |
+
_require('fusion.weighted_vote')
|
| 95 |
+
fusion_result = fusion.run(acoustic_probability, semantic_verdict, semantic_probability, pc.acoustic_weight)
|
| 96 |
+
stage_results.append(fusion_result)
|
| 97 |
+
decision = fusion_result.output['decision']
|
| 98 |
+
probability = fusion_result.output['probability']
|
| 99 |
+
result_provenance = 'rule'
|
| 100 |
+
elif pc.fusion == 'easy_turn':
|
| 101 |
+
_require('fusion.easy_turn')
|
| 102 |
+
raise StageUnavailableError('unreachable')
|
| 103 |
+
elif pc.encoder == 'none':
|
| 104 |
+
if semantic_probability is not None:
|
| 105 |
+
decision = 'complete' if semantic_probability > 0.5 else 'incomplete'
|
| 106 |
+
probability = semantic_probability
|
| 107 |
+
result_provenance = 'real_checkpoint'
|
| 108 |
+
else:
|
| 109 |
+
decision = 'complete' if semantic_verdict == 'complete' else 'incomplete'
|
| 110 |
+
probability = None
|
| 111 |
+
result_provenance = 'architecture_reimplemented' if pc.semantic == 'qwen_local_streaming' else 'real_checkpoint'
|
| 112 |
+
else:
|
| 113 |
+
decision = acoustic_result.output['decision']
|
| 114 |
+
probability = acoustic_probability
|
| 115 |
+
result_provenance = acoustic_provenance
|
| 116 |
+
total_latency_ms = (time.perf_counter() - start) * 1000
|
| 117 |
+
return PipelineResult(config=pc, decision=decision, probability=probability, transcript=transcript, semantic_verdict=semantic_verdict, stage_results=stage_results, total_latency_ms=total_latency_ms, provenance=result_provenance, preset_label=preset_label)
|
backend/pooling.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
|
| 6 |
+
def mean_pool(hidden_states: np.ndarray, valid_length: int) -> np.ndarray:
|
| 7 |
+
valid_length = max(1, min(valid_length, hidden_states.shape[0]))
|
| 8 |
+
return hidden_states[-valid_length:].mean(axis=0)
|
| 9 |
+
|
| 10 |
+
class CrossAttentionPool(nn.Module):
|
| 11 |
+
|
| 12 |
+
def __init__(self, hidden_dim: int):
|
| 13 |
+
super().__init__()
|
| 14 |
+
self.hidden_dim = hidden_dim
|
| 15 |
+
self.query = nn.Parameter(torch.randn(hidden_dim) * hidden_dim ** (-0.5))
|
| 16 |
+
|
| 17 |
+
def forward(self, hidden_states: torch.Tensor, valid_lengths: torch.Tensor) -> torch.Tensor:
|
| 18 |
+
batch, seq_len, dim = hidden_states.shape
|
| 19 |
+
positions = torch.arange(seq_len, device=hidden_states.device).unsqueeze(0)
|
| 20 |
+
start_idx = (seq_len - valid_lengths).unsqueeze(1)
|
| 21 |
+
mask = positions >= start_idx
|
| 22 |
+
scores = hidden_states @ self.query / dim ** 0.5
|
| 23 |
+
scores = scores.masked_fill(~mask, float('-inf'))
|
| 24 |
+
weights = torch.softmax(scores, dim=-1)
|
| 25 |
+
pooled = torch.einsum('bt,btd->bd', weights, hidden_states)
|
| 26 |
+
return pooled
|
backend/presets.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from backend.capabilities import get as get_capability
|
| 4 |
+
from backend.capabilities import head_capability_key
|
| 5 |
+
from backend.types import PipelineConfig, Provenance
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class Preset:
|
| 9 |
+
label: str
|
| 10 |
+
config: PipelineConfig
|
| 11 |
+
approximates: str
|
| 12 |
+
static_provenance: Provenance
|
| 13 |
+
|
| 14 |
+
def _required_capability_keys(pc: PipelineConfig) -> list[str]:
|
| 15 |
+
keys = [f'gate.{pc.gate}']
|
| 16 |
+
if pc.encoder == 'smart_turn_onnx':
|
| 17 |
+
keys.append('acoustic.smart_turn_onnx')
|
| 18 |
+
elif pc.encoder != 'none':
|
| 19 |
+
keys.append(head_capability_key(pc.encoder, pc.pooling, pc.head))
|
| 20 |
+
if pc.semantic != 'off':
|
| 21 |
+
keys.append(f'semantic.{pc.semantic}')
|
| 22 |
+
if pc.fusion != 'off':
|
| 23 |
+
keys.append(f'fusion.{pc.fusion}')
|
| 24 |
+
return keys
|
| 25 |
+
_PRESET_DEFS: list[Preset] = [Preset(label='Smart Turn v3.2 (zero-shot)', config=PipelineConfig(gate='silero_vad', encoder='smart_turn_onnx', semantic='off', fusion='off'), approximates='Smart Turn v3.2 (Pipecat)', static_provenance='real_checkpoint'), Preset(label='Whisper-Tiny + Mean-Pool + Linear (trained)', config=PipelineConfig(gate='silero_vad', encoder='whisper_tiny', pooling='mean', head='linear', semantic='off', fusion='off'), approximates="Smart Turn v2's recipe (mean pool + linear), retrained by us", static_provenance='trained_by_us'), Preset(label='Whisper-Tiny + Mean-Pool + MLP (trained)', config=PipelineConfig(gate='silero_vad', encoder='whisper_tiny', pooling='mean', head='mlp', semantic='off', fusion='off'), approximates='', static_provenance='trained_by_us'), Preset(label='Whisper-Tiny + Cross-Attn + Linear (trained)', config=PipelineConfig(gate='silero_vad', encoder='whisper_tiny', pooling='cross_attention', head='linear', semantic='off', fusion='off'), approximates="Smart Turn v3.2's recipe (cross-attn pool + linear), retrained by us on real Hindi/Hinglish audio (HiACC + FLEURS) after the same architecture scored worst OOD (1/5) trained on the challenge's synthetic TTS data - real data fixed it outright (5/5). See docs/decision-log.md #38, experiments/results/dataset_comparison.md.", static_provenance='trained_by_us'), Preset(label='TEN Turn Detection (prompted)', config=PipelineConfig(gate='silero_vad', encoder='none', semantic='qwen_local', fusion='off'), approximates='TEN Turn Detection - approximated with local Qwen2.5-0.5B instead of their 7B (docs/decision-log.md #11)', static_provenance='real_checkpoint'), Preset(label='LiveKit End-of-Turn Detector (zero-shot)', config=PipelineConfig(gate='silero_vad', encoder='none', semantic='livekit_eou', fusion='off'), approximates='livekit/turn-detector - a real checkpoint, not an approximation (docs/decision-log.md #33)', static_provenance='real_checkpoint'), Preset(label='Acoustic + Semantic Fusion (rule-based)', config=PipelineConfig(gate='silero_vad', encoder='whisper_tiny', pooling='mean', head='linear', semantic='livekit_eou', fusion='weighted_vote'), approximates="A zero-training combination of our trained acoustic head with LiveKit's real EOU probability (docs/decision-log.md #33) - NOT Easy Turn's actual jointly-trained fusion, and no 'wait' handling since LiveKit's model has no such class (traded away when this preset moved off qwen_local)", static_provenance='rule'), Preset(label='FastTurn Streaming (reimplemented)', config=PipelineConfig(gate='silero_vad', encoder='smart_turn_onnx', semantic='qwen_local_streaming', fusion='weighted_vote'), approximates="FastTurn's described streaming-fusion mechanism, reimplemented by us for Hindi/Hinglish with a CTC checkpoint (theainerd/Wav2Vec2-large-xlsr-hindi) - NOT their code or reported numbers, which were never released (docs/decision-log.md #23)", static_provenance='architecture_reimplemented'), Preset(label='Easy Turn (unavailable)', config=PipelineConfig(gate='silero_vad', encoder='whisper_tiny', pooling='mean', head='linear', semantic='qwen_local', fusion='easy_turn'), approximates="Easy Turn's real joint acoustic+linguistic fusion - NOT wired up this pass", static_provenance='unavailable')]
|
| 26 |
+
|
| 27 |
+
def list_presets() -> list[dict]:
|
| 28 |
+
out = []
|
| 29 |
+
for preset in _PRESET_DEFS:
|
| 30 |
+
try:
|
| 31 |
+
infos = [get_capability(key) for key in _required_capability_keys(preset.config)]
|
| 32 |
+
failing = [i for i in infos if not i.available]
|
| 33 |
+
if failing:
|
| 34 |
+
available = False
|
| 35 |
+
reason = '; '.join((f'{i.key}: {i.reason}' for i in failing))
|
| 36 |
+
provenance = 'unavailable'
|
| 37 |
+
else:
|
| 38 |
+
available = True
|
| 39 |
+
reason = None
|
| 40 |
+
provenance = preset.static_provenance
|
| 41 |
+
except (KeyError, ValueError) as exc:
|
| 42 |
+
available = False
|
| 43 |
+
reason = f'preset is not runnable as configured: {exc}'
|
| 44 |
+
provenance = 'unavailable'
|
| 45 |
+
out.append({'label': preset.label, 'config': preset.config, 'approximates': preset.approximates, 'available': available, 'reason': reason, 'provenance': provenance})
|
| 46 |
+
return out
|
backend/semantic.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import re
|
| 3 |
+
import time
|
| 4 |
+
import unicodedata
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import config
|
| 8 |
+
from backend.types import StageResult
|
| 9 |
+
_device = 'mps' if torch.backends.mps.is_available() else 'cpu'
|
| 10 |
+
_asr_pipeline = None
|
| 11 |
+
_qwen_model = None
|
| 12 |
+
_qwen_tokenizer = None
|
| 13 |
+
_livekit_model = None
|
| 14 |
+
_livekit_tokenizer = None
|
| 15 |
+
_livekit_im_end_id = None
|
| 16 |
+
PROMPT_TEMPLATE = 'You are analyzing a snippet of transcribed speech from a Hindi-English (Hinglish) conversation with a voice assistant. Code-switching between Hindi and English mid-sentence is normal and not a sign of incompleteness. Filler words like "matlab", "toh", "haan", "wo kya bolte hain", "um", "uh" indicate the speaker is still thinking and has NOT completed their turn.\n\nTranscript: "{transcript}"\n\nClassify whether the speaker\'s turn is:\n- complete: the utterance is a complete thought, the speaker is done\n- incomplete: the utterance is grammatically or semantically incomplete, more is coming\n- wait: the speaker is explicitly asking for a pause (e.g. "ek second", "hold on", "wait")\n\nRespond with exactly one word: complete, incomplete, or wait.'
|
| 17 |
+
_VALID_LABELS = ('complete', 'incomplete', 'wait')
|
| 18 |
+
|
| 19 |
+
def _get_asr_pipeline():
|
| 20 |
+
global _asr_pipeline
|
| 21 |
+
if _asr_pipeline is None:
|
| 22 |
+
from transformers import pipeline
|
| 23 |
+
_asr_pipeline = pipeline('automatic-speech-recognition', model=config.WHISPER_TINY_ID, device=_device if _device != 'mps' else -1)
|
| 24 |
+
return _asr_pipeline
|
| 25 |
+
|
| 26 |
+
def _get_qwen():
|
| 27 |
+
global _qwen_model, _qwen_tokenizer
|
| 28 |
+
if _qwen_model is None:
|
| 29 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 30 |
+
_qwen_tokenizer = AutoTokenizer.from_pretrained(config.QWEN_LOCAL_ID)
|
| 31 |
+
_qwen_model = AutoModelForCausalLM.from_pretrained(config.QWEN_LOCAL_ID).to(_device).eval()
|
| 32 |
+
return (_qwen_model, _qwen_tokenizer)
|
| 33 |
+
|
| 34 |
+
def _get_livekit():
|
| 35 |
+
global _livekit_model, _livekit_tokenizer, _livekit_im_end_id
|
| 36 |
+
if _livekit_model is None:
|
| 37 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 38 |
+
_livekit_tokenizer = AutoTokenizer.from_pretrained(config.LIVEKIT_TURN_DETECTOR_ID)
|
| 39 |
+
_livekit_model = AutoModelForCausalLM.from_pretrained(config.LIVEKIT_TURN_DETECTOR_ID).to(_device).eval()
|
| 40 |
+
_livekit_im_end_id = _livekit_tokenizer.convert_tokens_to_ids('<|im_end|>')
|
| 41 |
+
return (_livekit_model, _livekit_tokenizer, _livekit_im_end_id)
|
| 42 |
+
_PUNCTUATION_RE = re.compile("[^\\w\\s'-]", re.UNICODE)
|
| 43 |
+
_WHITESPACE_RE = re.compile('\\s+')
|
| 44 |
+
|
| 45 |
+
def _normalize_for_livekit(transcript: str) -> str:
|
| 46 |
+
text = unicodedata.normalize('NFKC', transcript).lower()
|
| 47 |
+
text = _PUNCTUATION_RE.sub(' ', text)
|
| 48 |
+
return _WHITESPACE_RE.sub(' ', text).strip()
|
| 49 |
+
|
| 50 |
+
def transcribe(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE) -> str:
|
| 51 |
+
asr = _get_asr_pipeline()
|
| 52 |
+
result = asr({'raw': np.asarray(audio, dtype=np.float32), 'sampling_rate': sample_rate})
|
| 53 |
+
return result['text'].strip()
|
| 54 |
+
|
| 55 |
+
def _parse_label(raw_text: str) -> str:
|
| 56 |
+
lowered = raw_text.lower()
|
| 57 |
+
for label in _VALID_LABELS:
|
| 58 |
+
if re.search(f'\\b{label}\\b', lowered):
|
| 59 |
+
return label
|
| 60 |
+
return 'incomplete'
|
| 61 |
+
|
| 62 |
+
@torch.inference_mode()
|
| 63 |
+
def classify_transcript(transcript: str, temperature: float=0.2) -> dict:
|
| 64 |
+
model, tokenizer = _get_qwen()
|
| 65 |
+
messages = [{'role': 'user', 'content': PROMPT_TEMPLATE.format(transcript=transcript)}]
|
| 66 |
+
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 67 |
+
inputs = tokenizer(prompt, return_tensors='pt').to(_device)
|
| 68 |
+
do_sample = temperature > 0
|
| 69 |
+
output_ids = model.generate(**inputs, max_new_tokens=8, do_sample=do_sample, temperature=temperature if do_sample else None, pad_token_id=tokenizer.eos_token_id)
|
| 70 |
+
generated = output_ids[0][inputs['input_ids'].shape[1]:]
|
| 71 |
+
raw_text = tokenizer.decode(generated, skip_special_tokens=True)
|
| 72 |
+
label = _parse_label(raw_text)
|
| 73 |
+
return {'verdict': label, 'raw_response': raw_text.strip()}
|
| 74 |
+
|
| 75 |
+
def run(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE, temperature: float=0.2) -> StageResult:
|
| 76 |
+
start = time.perf_counter()
|
| 77 |
+
transcript = transcribe(audio, sample_rate)
|
| 78 |
+
result = classify_transcript(transcript, temperature)
|
| 79 |
+
timing_ms = (time.perf_counter() - start) * 1000
|
| 80 |
+
return StageResult(stage='semantic.qwen_local', timing_ms=timing_ms, output={'transcript': transcript, 'verdict': result['verdict'], 'raw_response': result['raw_response']}, available=True, provenance='real_checkpoint')
|
| 81 |
+
|
| 82 |
+
@torch.inference_mode()
|
| 83 |
+
def classify_transcript_livekit(transcript: str) -> float:
|
| 84 |
+
model, tokenizer, im_end_id = _get_livekit()
|
| 85 |
+
normalized = _normalize_for_livekit(transcript)
|
| 86 |
+
prompt = f'<|im_start|><|user|>{normalized}'
|
| 87 |
+
inputs = tokenizer(prompt, return_tensors='pt').to(_device)
|
| 88 |
+
logits = model(**inputs).logits[0, -1, :]
|
| 89 |
+
probs = torch.softmax(logits, dim=-1)
|
| 90 |
+
return probs[im_end_id].item()
|
| 91 |
+
|
| 92 |
+
def run_livekit(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE) -> StageResult:
|
| 93 |
+
start = time.perf_counter()
|
| 94 |
+
transcript = transcribe(audio, sample_rate)
|
| 95 |
+
probability = classify_transcript_livekit(transcript)
|
| 96 |
+
timing_ms = (time.perf_counter() - start) * 1000
|
| 97 |
+
return StageResult(stage='semantic.livekit_eou', timing_ms=timing_ms, output={'transcript': transcript, 'probability': probability}, available=True, provenance='real_checkpoint')
|
backend/smart_turn_onnx.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import time
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import onnxruntime as ort
|
| 6 |
+
from transformers import WhisperFeatureExtractor
|
| 7 |
+
import config
|
| 8 |
+
from backend.audio_utils import truncate_or_left_pad
|
| 9 |
+
from backend.types import StageResult
|
| 10 |
+
_session: ort.InferenceSession | None = None
|
| 11 |
+
_feature_extractor: WhisperFeatureExtractor | None = None
|
| 12 |
+
|
| 13 |
+
def _onnx_path() -> Path:
|
| 14 |
+
return config.MODELS_DIR / config.SMART_TURN_ONNX_FILENAME
|
| 15 |
+
|
| 16 |
+
def _get_session() -> ort.InferenceSession:
|
| 17 |
+
global _session
|
| 18 |
+
if _session is None:
|
| 19 |
+
path = _onnx_path()
|
| 20 |
+
if not path.exists():
|
| 21 |
+
from huggingface_hub import hf_hub_download
|
| 22 |
+
config.MODELS_DIR.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
hf_hub_download(repo_id=config.SMART_TURN_REPO, filename=config.SMART_TURN_ONNX_FILENAME, local_dir=config.MODELS_DIR)
|
| 24 |
+
_session = ort.InferenceSession(str(path))
|
| 25 |
+
return _session
|
| 26 |
+
|
| 27 |
+
def _get_feature_extractor() -> WhisperFeatureExtractor:
|
| 28 |
+
global _feature_extractor
|
| 29 |
+
if _feature_extractor is None:
|
| 30 |
+
_feature_extractor = WhisperFeatureExtractor(chunk_length=config.SMART_TURN_WINDOW_SECONDS)
|
| 31 |
+
return _feature_extractor
|
| 32 |
+
|
| 33 |
+
def preprocess(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE) -> np.ndarray:
|
| 34 |
+
if sample_rate != config.SAMPLE_RATE:
|
| 35 |
+
raise ValueError(f'Smart Turn expects {config.SAMPLE_RATE}Hz mono PCM, got sample_rate={sample_rate}. Resample before calling this function.')
|
| 36 |
+
audio = truncate_or_left_pad(audio, config.SMART_TURN_WINDOW_SECONDS, sample_rate)
|
| 37 |
+
fe = _get_feature_extractor()
|
| 38 |
+
inputs = fe(audio, sampling_rate=sample_rate, return_tensors='np', padding='max_length', max_length=config.SMART_TURN_WINDOW_SAMPLES, truncation=True, do_normalize=True)
|
| 39 |
+
input_features = inputs.input_features.squeeze(0).astype(np.float32)
|
| 40 |
+
return np.expand_dims(input_features, axis=0)
|
| 41 |
+
|
| 42 |
+
def run(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE) -> StageResult:
|
| 43 |
+
start = time.perf_counter()
|
| 44 |
+
session = _get_session()
|
| 45 |
+
input_features = preprocess(audio, sample_rate)
|
| 46 |
+
outputs = session.run(None, {'input_features': input_features})
|
| 47 |
+
probability = float(outputs[0].flatten()[0])
|
| 48 |
+
prediction = 'complete' if probability > 0.5 else 'incomplete'
|
| 49 |
+
timing_ms = (time.perf_counter() - start) * 1000
|
| 50 |
+
return StageResult(stage='acoustic.smart_turn_onnx', timing_ms=timing_ms, output={'decision': prediction, 'probability': probability}, available=True, provenance='real_checkpoint')
|
backend/streaming_semantic.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import math
|
| 3 |
+
import time
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import config
|
| 7 |
+
from backend.semantic import classify_transcript
|
| 8 |
+
from backend.types import StageResult
|
| 9 |
+
_device = 'mps' if torch.backends.mps.is_available() else 'cpu'
|
| 10 |
+
_ctc_model = None
|
| 11 |
+
_ctc_processor = None
|
| 12 |
+
DEFAULT_CHUNK_MS = 1000
|
| 13 |
+
MAX_STEPS = 10
|
| 14 |
+
|
| 15 |
+
def _get_ctc():
|
| 16 |
+
global _ctc_model, _ctc_processor
|
| 17 |
+
if _ctc_model is None:
|
| 18 |
+
from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor
|
| 19 |
+
_ctc_processor = Wav2Vec2Processor.from_pretrained(config.HINDI_CTC_ID)
|
| 20 |
+
_ctc_model = Wav2Vec2ForCTC.from_pretrained(config.HINDI_CTC_ID).to(_device).eval()
|
| 21 |
+
return (_ctc_processor, _ctc_model)
|
| 22 |
+
|
| 23 |
+
@torch.inference_mode()
|
| 24 |
+
def transcribe_ctc_chunk(audio_prefix: np.ndarray, sample_rate: int=config.SAMPLE_RATE) -> str:
|
| 25 |
+
processor, model = _get_ctc()
|
| 26 |
+
inputs = processor(audio_prefix, sampling_rate=sample_rate, return_tensors='pt', padding=True)
|
| 27 |
+
input_values = inputs.input_values.to(_device)
|
| 28 |
+
logits = model(input_values).logits
|
| 29 |
+
predicted_ids = torch.argmax(logits, dim=-1)
|
| 30 |
+
text = processor.batch_decode(predicted_ids)[0]
|
| 31 |
+
return text.strip()
|
| 32 |
+
|
| 33 |
+
def run(audio: np.ndarray, sample_rate: int=config.SAMPLE_RATE, chunk_ms: int=DEFAULT_CHUNK_MS, temperature: float=0.2) -> StageResult:
|
| 34 |
+
start = time.perf_counter()
|
| 35 |
+
audio = np.asarray(audio, dtype=np.float32)
|
| 36 |
+
total_audio_ms = len(audio) / sample_rate * 1000
|
| 37 |
+
chunk_samples = int(sample_rate * chunk_ms / 1000)
|
| 38 |
+
history: list[dict] = []
|
| 39 |
+
first_decisive_audio_ms = None
|
| 40 |
+
num_steps = min(MAX_STEPS, max(1, len(audio) // chunk_samples))
|
| 41 |
+
stride = max(chunk_samples, math.ceil(len(audio) / num_steps))
|
| 42 |
+
for step in range(1, num_steps + 1):
|
| 43 |
+
prefix_len = min(len(audio), step * stride)
|
| 44 |
+
prefix = audio[:prefix_len]
|
| 45 |
+
elapsed_audio_ms = prefix_len / sample_rate * 1000
|
| 46 |
+
step_start = time.perf_counter()
|
| 47 |
+
transcript_so_far = transcribe_ctc_chunk(prefix, sample_rate)
|
| 48 |
+
verdict = 'incomplete'
|
| 49 |
+
if transcript_so_far:
|
| 50 |
+
verdict = classify_transcript(transcript_so_far, temperature)['verdict']
|
| 51 |
+
step_ms = (time.perf_counter() - step_start) * 1000
|
| 52 |
+
history.append({'elapsed_audio_ms': round(elapsed_audio_ms, 1), 'transcript_so_far': transcript_so_far, 'verdict': verdict, 'step_processing_ms': round(step_ms, 1)})
|
| 53 |
+
if first_decisive_audio_ms is None and verdict != 'incomplete':
|
| 54 |
+
first_decisive_audio_ms = elapsed_audio_ms
|
| 55 |
+
if prefix_len >= len(audio):
|
| 56 |
+
break
|
| 57 |
+
final = history[-1]
|
| 58 |
+
timing_ms = (time.perf_counter() - start) * 1000
|
| 59 |
+
return StageResult(stage='semantic.qwen_local_streaming', timing_ms=timing_ms, output={'transcript': final['transcript_so_far'], 'verdict': final['verdict'], 'history': history, 'total_audio_ms': round(total_audio_ms, 1), 'first_decisive_audio_ms': first_decisive_audio_ms, 'eou_delay_saved_ms': round(total_audio_ms - first_decisive_audio_ms, 1) if first_decisive_audio_ms is not None else None}, available=True, provenance='architecture_reimplemented')
|
backend/types.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from dataclasses import dataclass, field
|
| 3 |
+
from typing import Any, Literal, Optional
|
| 4 |
+
Provenance = Literal['real_checkpoint', 'trained_by_us', 'rule', 'architecture_reimplemented', 'unavailable']
|
| 5 |
+
GateChoice = Literal['webrtcvad', 'silero_vad', 'none']
|
| 6 |
+
EncoderChoice = Literal['whisper_tiny', 'whisper_base', 'wav2vec2', 'smart_turn_onnx', 'none']
|
| 7 |
+
PoolingChoice = Literal['mean', 'cross_attention']
|
| 8 |
+
HeadChoice = Literal['linear', 'mlp']
|
| 9 |
+
SemanticChoice = Literal['off', 'qwen_local', 'qwen_local_streaming', 'livekit_eou', 'groq_api', 'openrouter_api']
|
| 10 |
+
FusionChoice = Literal['off', 'weighted_vote', 'easy_turn']
|
| 11 |
+
OutputClasses = Literal['binary', '3class', '4class']
|
| 12 |
+
FullDuplexChoice = Literal['off', 'moshi', 'human1']
|
| 13 |
+
|
| 14 |
+
class StageUnavailableError(RuntimeError):
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class StageResult:
|
| 19 |
+
stage: str
|
| 20 |
+
timing_ms: float
|
| 21 |
+
output: Any = None
|
| 22 |
+
available: bool = True
|
| 23 |
+
reason: Optional[str] = None
|
| 24 |
+
provenance: Optional[Provenance] = None
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class PipelineConfig:
|
| 28 |
+
gate: GateChoice = 'silero_vad'
|
| 29 |
+
encoder: EncoderChoice = 'whisper_tiny'
|
| 30 |
+
pooling: PoolingChoice = 'mean'
|
| 31 |
+
head: HeadChoice = 'linear'
|
| 32 |
+
semantic: SemanticChoice = 'off'
|
| 33 |
+
fusion: FusionChoice = 'off'
|
| 34 |
+
output_classes: OutputClasses = 'binary'
|
| 35 |
+
full_duplex: FullDuplexChoice = 'off'
|
| 36 |
+
vad_aggressiveness: int = 2
|
| 37 |
+
silence_trigger_ms: int = 400
|
| 38 |
+
semantic_temperature: float = 0.2
|
| 39 |
+
acoustic_weight: float = 0.6
|
| 40 |
+
|
| 41 |
+
def __post_init__(self) -> None:
|
| 42 |
+
if self.encoder == 'none':
|
| 43 |
+
if self.semantic == 'off':
|
| 44 |
+
raise ValueError("encoder='none' requires the semantic branch to be on (nothing would decide).")
|
| 45 |
+
if self.fusion != 'off':
|
| 46 |
+
raise ValueError("encoder='none' has no acoustic score to fuse with - set fusion='off'.")
|
| 47 |
+
if self.encoder == 'smart_turn_onnx':
|
| 48 |
+
if self.pooling != 'mean' or self.head != 'linear':
|
| 49 |
+
raise ValueError("encoder='smart_turn_onnx' is a single opaque preset and cannot be combined with a separate pooling/head choice (see docs/decision-log.md #18).")
|
| 50 |
+
if self.fusion != 'off' and self.semantic == 'off':
|
| 51 |
+
raise ValueError('fusion requires the semantic branch to be on.')
|
| 52 |
+
if self.full_duplex != 'off':
|
| 53 |
+
pass
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class PipelineResult:
|
| 57 |
+
config: PipelineConfig
|
| 58 |
+
decision: Optional[str] = None
|
| 59 |
+
probability: Optional[float] = None
|
| 60 |
+
transcript: Optional[str] = None
|
| 61 |
+
semantic_verdict: Optional[str] = None
|
| 62 |
+
stage_results: list[StageResult] = field(default_factory=list)
|
| 63 |
+
total_latency_ms: float = 0.0
|
| 64 |
+
provenance: Optional[Provenance] = None
|
| 65 |
+
preset_label: Optional[str] = None
|
config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
ROOT = Path(__file__).resolve().parent
|
| 3 |
+
DATABASE_DIR = ROOT / 'database'
|
| 4 |
+
AUDIO_DIR = DATABASE_DIR / 'audio'
|
| 5 |
+
SAMPLE_CLIPS_DIR = AUDIO_DIR / 'sample_clips'
|
| 6 |
+
EVAL_DIR = AUDIO_DIR / 'eval'
|
| 7 |
+
OOD_TEST_CLIPS_DIR = EVAL_DIR / 'ood_test_clips'
|
| 8 |
+
TRUNCATION_PILOT_DIR = EVAL_DIR / 'truncation_pilot'
|
| 9 |
+
LIVE_RUNS_DIR = AUDIO_DIR / 'live_runs'
|
| 10 |
+
CHECKPOINTS_DIR = DATABASE_DIR / 'checkpoints'
|
| 11 |
+
MODELS_DIR = DATABASE_DIR / 'models'
|
| 12 |
+
EASY_TURN_DIR = MODELS_DIR / 'easy_turn'
|
| 13 |
+
CACHE_DIR = DATABASE_DIR / 'cache'
|
| 14 |
+
DATA_CACHE_DIR = CACHE_DIR / 'dataset'
|
| 15 |
+
OOD_CACHE_DIR = CACHE_DIR / 'ood_test'
|
| 16 |
+
EXPERIMENTS_DIR = ROOT / 'experiments'
|
| 17 |
+
EMBEDDINGS_CACHE_DIR = EXPERIMENTS_DIR / 'embeddings_cache'
|
| 18 |
+
RESULTS_DIR = EXPERIMENTS_DIR / 'results'
|
| 19 |
+
SAMPLE_RATE = 16000
|
| 20 |
+
SMART_TURN_WINDOW_SECONDS = 8
|
| 21 |
+
SMART_TURN_WINDOW_SAMPLES = SMART_TURN_WINDOW_SECONDS * SAMPLE_RATE
|
| 22 |
+
WHISPER_TINY_ID = 'openai/whisper-tiny'
|
| 23 |
+
WHISPER_BASE_ID = 'openai/whisper-base'
|
| 24 |
+
WAV2VEC2_ID = 'facebook/wav2vec2-base'
|
| 25 |
+
QWEN_LOCAL_ID = 'Qwen/Qwen2.5-0.5B-Instruct'
|
| 26 |
+
HINDI_CTC_ID = 'theainerd/Wav2Vec2-large-xlsr-hindi'
|
| 27 |
+
LIVEKIT_TURN_DETECTOR_ID = 'livekit/turn-detector'
|
| 28 |
+
SMART_TURN_REPO = 'pipecat-ai/smart-turn-v3'
|
| 29 |
+
SMART_TURN_ONNX_FILENAME = 'smart-turn-v3.2-cpu.onnx'
|
| 30 |
+
DATASET_REPO = 'pipecat-ai/smart-turn-data-v3.2-train'
|
| 31 |
+
DATASET_NUM_SHARDS_TOTAL = 83
|
| 32 |
+
EASY_TURN_REPO = 'ASLP-lab/Easy-Turn'
|
| 33 |
+
ENCODER_IDS = {'whisper_tiny': WHISPER_TINY_ID, 'whisper_base': WHISPER_BASE_ID, 'wav2vec2': WAV2VEC2_ID}
|
database/audio/sample_clips/english_fleurs_1511.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a9dde919c8a85ece3116cf42fa3f2d052a9174319a4062c975631c848594961d
|
| 3 |
+
size 180524
|
database/audio/sample_clips/english_fleurs_1512.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c30bddf019ff7abf8f81f0d6a0fc018b186613a9b2d67c3f3df24ae255e0b6b1
|
| 3 |
+
size 345644
|
database/audio/sample_clips/english_fleurs_1545.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee23300897063f00999fb4a5f3bfd7521152b596ad91c6bf4758be54a855c2ba
|
| 3 |
+
size 245164
|
database/audio/sample_clips/english_fleurs_1546.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:be0fb2731a5012de1902223605b2a5110828482b65aa48b869eebaf319d2cdaf
|
| 3 |
+
size 391724
|
database/audio/sample_clips/english_fleurs_1554.wav
ADDED
|
Binary file (86.4 kB). View file
|
|
|
database/audio/sample_clips/english_fleurs_1578.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:933a4569cc229f58e56cf680fa4cd3c7660b1e73cf6ef734ed906699bb81b1f9
|
| 3 |
+
size 265644
|
database/audio/sample_clips/english_fleurs_1595.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d736aa4815a6de4ecc1f2864c4e6f9c67906b8eb37e8df103a4107d95d257377
|
| 3 |
+
size 320684
|
database/audio/sample_clips/english_fleurs_1637.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8bad0d5ef9019d1c9cbbc4aac7abe0ea49f5b4fd7956f92a369f70bc4946ae5f
|
| 3 |
+
size 1015724
|
database/audio/sample_clips/english_fleurs_1639.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:78af7de3a38af8b7a368096b7047650fc387491601bfe48221806cf46d94ec92
|
| 3 |
+
size 291884
|
database/audio/sample_clips/english_fleurs_1645.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:15a992f941349e2a03fafca3d96ea1a2d86ffb6aadf8833369095b21423e6042
|
| 3 |
+
size 218924
|
database/audio/sample_clips/hindi_fleurs_1524.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e00d95d250c2ba7a3f2186b26f82a126e80b61bb667bd0ee2af712287c620bef
|
| 3 |
+
size 382124
|
database/audio/sample_clips/hindi_fleurs_1526.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f1b686404e5dadd0cdadc91c20737c9d5846b0ceb981f533576f1c1ec1c49cd1
|
| 3 |
+
size 453164
|
database/audio/sample_clips/hindi_fleurs_1540.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e2053febb046fab7e62066d77ecefa597f80db86e02843517400d4192d2df270
|
| 3 |
+
size 276524
|
database/audio/sample_clips/hindi_fleurs_1549.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:855c89aa564662385d32b05d35e28edc60a6d05974735320886f913c87c493ee
|
| 3 |
+
size 506924
|
database/audio/sample_clips/hindi_fleurs_1560.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6c24145d731abae2b8e1323ff08edd944f8a2a1ed05035ebf43770f8190f8f9f
|
| 3 |
+
size 192044
|
database/audio/sample_clips/hindi_fleurs_1581.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7c58925c65f89f1ddeba6055d59b27e37ae49bf1ca0233ae0a1d1747f60a3168
|
| 3 |
+
size 224684
|
database/audio/sample_clips/hindi_fleurs_1609.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4871a032b62c48f1d286d0b977b8218856c1205360359f96a273304870007e7c
|
| 3 |
+
size 303404
|
database/audio/sample_clips/hindi_fleurs_1620.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:41801b96eb739e2f6a0850c7f9cc75adb36468a9a81613922996276ac4bd4f38
|
| 3 |
+
size 610604
|
database/audio/sample_clips/hindi_fleurs_1641.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1c81049dd7cb390aa3267410ac4e1e2e95622d6085366fe3ff088471324cd8a5
|
| 3 |
+
size 361004
|
database/audio/sample_clips/hindi_fleurs_1645.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7f1aee1a6596889197682524a5ad105141d38b54318ed03e1bf3580b64f36c39
|
| 3 |
+
size 169004
|
database/audio/sample_clips/hinglish_hiacc_AD09072.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cd452e54e2e1432586f68f05d2cd25d0411d83dafc8f4dac000ff4cb59bc9597
|
| 3 |
+
size 512084
|
database/audio/sample_clips/hinglish_hiacc_AD13003.wav
ADDED
|
Binary file (96.8 kB). View file
|
|
|
database/audio/sample_clips/hinglish_hiacc_AD22092.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:77a8459e9696d11be084b1671b8bca53d0c5275f52f4eb2a1c834d94992a94fd
|
| 3 |
+
size 227710
|
database/audio/sample_clips/hinglish_hiacc_AD23025.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:599e5b5ccd3c1dbf321963d158eda124e3e6ca2c582b03e1786474732da7ddcb
|
| 3 |
+
size 104730
|
database/audio/sample_clips/hinglish_hiacc_AD36007.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c8223b79e5de802db3adb45d4bf724c9eadaf79701d20c357b1a56ea189dccc0
|
| 3 |
+
size 137886
|
database/audio/sample_clips/hinglish_hiacc_AD36064.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f90d8b109474071f39fd8767c50d4afb5d7b22dd83e91f0ece9c3edb707426d7
|
| 3 |
+
size 121006
|
database/audio/sample_clips/hinglish_hiacc_AD40049.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5234def68e6e0823873e2dc63f2586a3986f2ddf4ecb53a2d6d31945198ee34e
|
| 3 |
+
size 289802
|
database/audio/sample_clips/hinglish_hiacc_AD40103.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1dc4b1146f75f1540b9348c02b4277d205189c41ebf1a65c8ba3ce939fefa607
|
| 3 |
+
size 193346
|
database/audio/sample_clips/hinglish_hiacc_AD40158.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:96eeca3c3bd867f0b4f953843004127cc1d6f486dcc6d637a6740d4cdd29a0a9
|
| 3 |
+
size 173452
|
database/audio/sample_clips/hinglish_hiacc_AD60096.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:778f8fe8a0fa53585b93e61d94270f34cc681297ca5da4d2f0c9fec53d8a6a3f
|
| 3 |
+
size 153124
|
database/audio/sample_clips/manifest.json
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"filename": "english_fleurs_1554.wav",
|
| 4 |
+
"bucket": "english",
|
| 5 |
+
"source": "fleurs_en_us",
|
| 6 |
+
"duration_seconds": 2.7,
|
| 7 |
+
"transcript": "he built a wifi door bell he said"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"filename": "english_fleurs_1511.wav",
|
| 11 |
+
"bucket": "english",
|
| 12 |
+
"source": "fleurs_en_us",
|
| 13 |
+
"duration_seconds": 5.64,
|
| 14 |
+
"transcript": "soon officers equipped with riot gear entered the yard and cornered the inmates with tear gas"
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"filename": "english_fleurs_1645.wav",
|
| 18 |
+
"bucket": "english",
|
| 19 |
+
"source": "fleurs_en_us",
|
| 20 |
+
"duration_seconds": 6.84,
|
| 21 |
+
"transcript": "its all-pervading power affected everyone from king to commoner"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"filename": "english_fleurs_1545.wav",
|
| 25 |
+
"bucket": "english",
|
| 26 |
+
"source": "fleurs_en_us",
|
| 27 |
+
"duration_seconds": 7.66,
|
| 28 |
+
"transcript": "a curry can be either dry or wet depending on the amount of liquid"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"filename": "english_fleurs_1578.wav",
|
| 32 |
+
"bucket": "english",
|
| 33 |
+
"source": "fleurs_en_us",
|
| 34 |
+
"duration_seconds": 8.3,
|
| 35 |
+
"transcript": "then lakkha singh took the lead in singing the bhajans"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"filename": "english_fleurs_1639.wav",
|
| 39 |
+
"bucket": "english",
|
| 40 |
+
"source": "fleurs_en_us",
|
| 41 |
+
"duration_seconds": 9.12,
|
| 42 |
+
"transcript": "since students are often the most critical audience the blog writer begins to strive to improve writing to avoid criticism"
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"filename": "english_fleurs_1595.wav",
|
| 46 |
+
"bucket": "english",
|
| 47 |
+
"source": "fleurs_en_us",
|
| 48 |
+
"duration_seconds": 10.02,
|
| 49 |
+
"transcript": "the tibetan buddhism is based on the teachings of buddha but were extended by the mahayana path of love and by a lot of techniques from indian yoga"
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"filename": "english_fleurs_1512.wav",
|
| 53 |
+
"bucket": "english",
|
| 54 |
+
"source": "fleurs_en_us",
|
| 55 |
+
"duration_seconds": 10.8,
|
| 56 |
+
"transcript": "this new environment has different resources and different competitors so the new population will need different features or adaptations to be a strong competitor than what they had needed before"
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"filename": "english_fleurs_1546.wav",
|
| 60 |
+
"bucket": "english",
|
| 61 |
+
"source": "fleurs_en_us",
|
| 62 |
+
"duration_seconds": 12.24,
|
| 63 |
+
"transcript": "the feathers' structure suggests that they were not used in flight but rather for temperature regulation or display the researchers suggested that even though this is the tail of a young dinosaur the sample shows adult plumage and not a chick's down"
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"filename": "english_fleurs_1637.wav",
|
| 67 |
+
"bucket": "english",
|
| 68 |
+
"source": "fleurs_en_us",
|
| 69 |
+
"duration_seconds": 31.74,
|
| 70 |
+
"transcript": "the terrified king louis xvi queen marie antoinette their two young children 11 year old marie therese and four year old louis-charles and the king's sister madam elizabeth on the 6th october 1789 were forced back to paris from versailles by a mob of market women"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"filename": "hindi_fleurs_1645.wav",
|
| 74 |
+
"bucket": "hindi",
|
| 75 |
+
"source": "fleurs_hi_in",
|
| 76 |
+
"duration_seconds": 5.28,
|
| 77 |
+
"transcript": "\u0907\u0938\u0915\u0940 \u0938\u0930\u094d\u0935\u0935\u094d\u092f\u093e\u092a\u0940 \u0936\u0915\u094d\u0924\u093f \u0928\u0947 \u0930\u093e\u091c\u093e \u0938\u0947 \u0932\u0947\u0915\u0930 \u0906\u092e \u0924\u0915 \u0938\u092d\u0940 \u0915\u094b \u092a\u094d\u0930\u092d\u093e\u0935\u093f\u0924 \u0915\u093f\u092f\u093e"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"filename": "hindi_fleurs_1560.wav",
|
| 81 |
+
"bucket": "hindi",
|
| 82 |
+
"source": "fleurs_hi_in",
|
| 83 |
+
"duration_seconds": 6.0,
|
| 84 |
+
"transcript": "\u091c\u0948\u0938\u0947 \u0939\u0940 \u0906\u092a \u0927\u093e\u0930\u093e \u0938\u0947 \u0928\u093f\u0915\u0932 \u0906\u0924\u0947 \u0939\u0948\u0902 \u092a\u0940\u091b\u0947 \u0915\u0940 \u0913\u0930 \u0924\u0948\u0930\u0928\u0947 \u092e\u0947\u0902 \u0915\u094b\u0908 \u0916\u093e\u0938 \u092e\u0941\u0936\u094d\u0915\u093f\u0932 \u0928\u0939\u0940\u0902 \u0939\u094b\u0924\u0940 \u0939\u0948"
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"filename": "hindi_fleurs_1581.wav",
|
| 88 |
+
"bucket": "hindi",
|
| 89 |
+
"source": "fleurs_hi_in",
|
| 90 |
+
"duration_seconds": 7.02,
|
| 91 |
+
"transcript": "\u0905\u092e\u0947\u091c\u093c\u0928 \u0927\u0930\u0924\u0940 \u0915\u0940 \u0938\u092c\u0938\u0947 \u091a\u094c\u0921\u093c\u0940 \u0928\u0926\u0940 \u092d\u0940 \u0939\u0948 \u091c\u094b \u0915\u092d\u0940-\u0915\u092d\u0940 \u091b\u0939 \u092e\u0940\u0932 \u091c\u093f\u0924\u0928\u0940 \u091a\u094c\u0921\u093c\u0940 \u0939\u094b \u091c\u093e\u0924\u0940 \u0939\u0948"
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"filename": "hindi_fleurs_1540.wav",
|
| 95 |
+
"bucket": "hindi",
|
| 96 |
+
"source": "fleurs_hi_in",
|
| 97 |
+
"duration_seconds": 8.64,
|
| 98 |
+
"transcript": "\u0938\u0948\u0926\u094d\u0927\u093e\u0902\u0924\u093f\u0915 \u0930\u0942\u092a \u0938\u0947 \u0924\u093f\u092c\u094d\u092c\u0924\u0940 \u092c\u094c\u0926\u094d\u0927 \u0927\u0930\u094d\u092e \u092c\u0939\u0941\u0924 \u0938\u0930\u0932 \u0939\u0948 \u0907\u0938\u092e\u0947\u0902 \u0915\u0941\u0902\u0921\u0932\u093f\u0928\u0940 \u092f\u094b\u0917 \u0927\u094d\u092f\u093e\u0928 \u0914\u0930 \u0938\u092d\u0940 \u0915\u094b \u092a\u094d\u0930\u0947\u092e \u0938\u0947 \u0917\u0932\u0947 \u0932\u0917\u093e\u0928\u0947 \u0935\u093e\u0932\u093e \u092e\u093e\u0930\u094d\u0917 \u0936\u093e\u092e\u093f\u0932 \u0939\u0948"
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"filename": "hindi_fleurs_1609.wav",
|
| 102 |
+
"bucket": "hindi",
|
| 103 |
+
"source": "fleurs_hi_in",
|
| 104 |
+
"duration_seconds": 9.48,
|
| 105 |
+
"transcript": "\u0930\u0949\u092c\u093f\u0928 \u0909\u0925\u092a\u094d\u092a\u093e \u0928\u0947 \u0915\u0947\u0935\u0932 41 \u0917\u0947\u0902\u0926\u094b\u0902 \u092e\u0947\u0902 11 \u091a\u094c\u0915\u094b\u0902 \u0914\u0930 2 \u091b\u0915\u094d\u0915\u094b\u0902 \u0915\u0940 \u092e\u0926\u0926 \u0938\u0947 70 \u0930\u0928 \u092c\u0928\u093e \u0915\u0930 \u092a\u093e\u0930\u0940 \u0915\u093e \u0938\u0930\u094d\u0935\u094b\u091a\u094d\u091a \u0938\u094d\u0915\u094b\u0930 \u092c\u0928\u093e\u092f\u093e"
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"filename": "hindi_fleurs_1641.wav",
|
| 109 |
+
"bucket": "hindi",
|
| 110 |
+
"source": "fleurs_hi_in",
|
| 111 |
+
"duration_seconds": 11.28,
|
| 112 |
+
"transcript": "\u0939\u093e\u0932\u093e\u0902\u0915\u093f \u091c\u0902\u0917\u0932 \u0915\u093e \u0905\u0930\u094d\u0925 \u0915\u0947\u0935\u0932 \u092e\u0948\u0902\u0917\u094d\u0930\u094b\u0935 \u0926\u0932\u0926\u0932 \u0928\u0939\u0940\u0902 \u0939\u0948\u0902 \u0935\u0947 \u0915\u0941\u091b \u0905\u0902\u0924\u093f\u092e \u0936\u0915\u094d\u0924\u093f\u0936\u093e\u0932\u0940 \u091c\u0902\u0917\u0932\u094b\u0902 \u092e\u0947\u0902 \u0938\u0947 \u0939\u0948\u0902 \u091c\u094b \u0915\u092d\u0940 \u0917\u0902\u0917\u093e \u092a\u0920\u093e\u0930 \u0915\u094b \u0915\u0935\u0930 \u0915\u0930\u0924\u0947 \u0925\u0947"
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"filename": "hindi_fleurs_1524.wav",
|
| 116 |
+
"bucket": "hindi",
|
| 117 |
+
"source": "fleurs_hi_in",
|
| 118 |
+
"duration_seconds": 11.94,
|
| 119 |
+
"transcript": "\u092e\u0940\u091f\u094d\u0930\u093f\u0915 \u092a\u094d\u0930\u0923\u093e\u0932\u0940 \u0915\u0947 \u0907\u0938\u094d\u0924\u0947\u092e\u093e\u0932 \u0928\u093f\u0930\u092a\u0947\u0915\u094d\u0937\u0935\u093e\u0926 \u0938\u0947 \u0917\u0923\u0924\u0902\u0924\u094d\u0930\u0935\u093e\u0926 \u0930\u093e\u0937\u094d\u091f\u094d\u0930\u0935\u093e\u0926 \u092e\u0947\u0902 \u092c\u0926\u0932\u093e\u0935 \u0914\u0930 \u0926\u0947\u0936 \u0915\u093e \u092d\u0930\u094b\u0938\u093e \u0932\u094b\u0917\u094b\u0902 \u0938\u0947 \u0939\u0948 \u0915\u093f\u0938\u0940 \u090f\u0915 \u0936\u093e\u0938\u0915 \u0938\u0947 \u0928\u0939\u0940\u0902 \u0910\u0938\u0940 \u092c\u093e\u0924\u094b\u0902 \u0915\u093e \u092c\u0939\u0941\u0924 \u091c\u093c\u094d\u092f\u093e\u0926\u093e \u0938\u093e\u092e\u093e\u091c\u093f\u0915 \u0914\u0930 \u0930\u093e\u091c\u0928\u0948\u0924\u093f\u0915 \u0905\u0938\u0930 \u0939\u094b\u0924\u093e \u0939\u0948"
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"filename": "hindi_fleurs_1526.wav",
|
| 123 |
+
"bucket": "hindi",
|
| 124 |
+
"source": "fleurs_hi_in",
|
| 125 |
+
"duration_seconds": 14.16,
|
| 126 |
+
"transcript": "\u092f\u0926\u093f \u0906\u092a \u0938\u0930\u094d\u0926\u093f\u092f\u094b\u0902 \u092e\u0947\u0902 \u0906\u0930\u094d\u0915\u091f\u093f\u0915 \u092f\u093e \u0905\u0902\u091f\u093e\u0930\u094d\u0915\u091f\u093f\u0915 \u0915\u094d\u0937\u0947\u0924\u094d\u0930\u094b\u0902 \u092e\u0947\u0902 \u091c\u093e\u0924\u0947 \u0939\u0948\u0902 \u0924\u094b \u0906\u092a \u0927\u094d\u0930\u0941\u0935\u0940\u092f \u0930\u093e\u0924 \u0915\u093e \u0905\u0928\u0941\u092d\u0935 \u0915\u0930\u0947\u0902\u0917\u0947 \u091c\u093f\u0938\u0915\u093e \u0905\u0930\u094d\u0925 \u0939\u0948 \u0915\u093f \u0938\u0942\u0930\u091c \u0915\u094d\u0937\u093f\u0924\u093f\u091c \u0938\u0947 \u090a\u092a\u0930 \u0928\u0939\u0940\u0902 \u0909\u0917\u0924\u093e \u0939\u0948"
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"filename": "hindi_fleurs_1549.wav",
|
| 130 |
+
"bucket": "hindi",
|
| 131 |
+
"source": "fleurs_hi_in",
|
| 132 |
+
"duration_seconds": 15.84,
|
| 133 |
+
"transcript": "\u0907\u0928 \u0938\u093f\u0926\u094d\u0927\u093e\u0902\u0924\u094b\u0902 \u0938\u0947 \u0938\u0902\u0915\u0947\u0924 \u092e\u093f\u0932\u0924\u093e \u0939\u0948 \u0915\u093f \u0932\u094b\u0917\u094b\u0902 \u0915\u0940 \u0915\u0941\u091b \u0910\u0938\u0940 \u091c\u093c\u0930\u0942\u0930\u0924\u0947\u0902 \u0914\u0930/\u092f\u093e \u0907\u091a\u094d\u091b\u093e\u090f\u0901 \u0925\u0940\u0902 \u091c\u094b \u0909\u0928\u0915\u0947 \u0935\u092f\u0938\u094d\u0915 \u0939\u094b\u0924\u0947-\u0939\u094b\u0924\u0947 \u0909\u0928\u0915\u0947 \u092d\u0940\u0924\u0930 \u0928\u093f\u0939\u093f\u0924 \u0939\u094b \u0917\u092f\u0940"
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"filename": "hindi_fleurs_1620.wav",
|
| 137 |
+
"bucket": "hindi",
|
| 138 |
+
"source": "fleurs_hi_in",
|
| 139 |
+
"duration_seconds": 19.08,
|
| 140 |
+
"transcript": "\u091c\u093e\u0935\u093e \u0915\u0947 \u0935\u094d\u092f\u0902\u091c\u0928 \u0905\u092c \u092a\u0942\u0930\u0947 \u0926\u094d\u0935\u0940\u092a\u0938\u092e\u0942\u0939 \u092e\u0947\u0902 \u0935\u094d\u092f\u093e\u092a\u0915 \u0930\u0942\u092a \u0938\u0947 \u0909\u092a\u0932\u092c\u094d\u0927 \u0939\u0948\u0902 \u091c\u093f\u0938\u092e\u0947\u0902 \u092a\u093e\u0930\u0902\u092a\u0930\u093f\u0915 \u0935\u094d\u092f\u0902\u091c\u0928\u094b\u0902 \u0915\u0940 \u090f\u0915 \u0935\u093f\u0936\u0947\u0937\u0924\u093e \u0939\u0948 \u091c\u093e\u0935\u093e \u0915\u0947 \u0935\u094d\u092f\u0902\u091c\u0928\u094b\u0902 \u092e\u0947\u0902 \u092a\u094d\u0930\u092e\u0941\u0916 \u0938\u094d\u0935\u093e\u0926 \u092e\u0942\u0902\u0917\u092b\u0932\u0940 \u092e\u093f\u0930\u094d\u091a \u091a\u0940\u0928\u0940 \u0935\u093f\u0936\u0947\u0937 \u0930\u0942\u092a \u0938\u0947 \u091c\u093e\u0935\u093e \u0915\u0940 \u0928\u093e\u0930\u093f\u092f\u0932 \u091a\u0940\u0928\u0940 \u0914\u0930 \u0935\u093f\u092d\u093f\u0928\u094d\u0928 \u0938\u0941\u0917\u0902\u0927\u093f\u0924 \u092e\u0938\u093e\u0932\u094b\u0902 \u0915\u093e \u0939\u094b\u0924\u093e \u0939\u0948\u0964"
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"filename": "hinglish_hiacc_AD13003.wav",
|
| 144 |
+
"bucket": "hinglish",
|
| 145 |
+
"source": "hiacc_adult_test",
|
| 146 |
+
"duration_seconds": 3.02,
|
| 147 |
+
"transcript": "So \u092e\u0947\u0930\u093e favourite festival \u0939\u0948 Diwali",
|
| 148 |
+
"code_mixing_index": 33.33,
|
| 149 |
+
"code_switch_count": 4
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"filename": "hinglish_hiacc_AD23025.wav",
|
| 153 |
+
"bucket": "hinglish",
|
| 154 |
+
"source": "hiacc_adult_test",
|
| 155 |
+
"duration_seconds": 3.27,
|
| 156 |
+
"transcript": "\u0914\u0930 \u0909\u0928\u0915\u0940 friendship \u092c\u093f\u0932\u094d\u0915\u0941\u0932 \u091a\u0941\u091f\u0915\u093f\u092f\u094b\u0902 \u0915\u0940 \u0924\u0930\u0939 \u0925\u0940",
|
| 157 |
+
"code_mixing_index": 12.5,
|
| 158 |
+
"code_switch_count": 2
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"filename": "hinglish_hiacc_AD36064.wav",
|
| 162 |
+
"bucket": "hinglish",
|
| 163 |
+
"source": "hiacc_adult_test",
|
| 164 |
+
"duration_seconds": 3.78,
|
| 165 |
+
"transcript": "\u0935\u094b as a memorial \u092c\u0928\u093e \u0939\u0948 those people who lost",
|
| 166 |
+
"code_mixing_index": 30.0,
|
| 167 |
+
"code_switch_count": 3
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"filename": "hinglish_hiacc_AD36007.wav",
|
| 171 |
+
"bucket": "hinglish",
|
| 172 |
+
"source": "hiacc_adult_test",
|
| 173 |
+
"duration_seconds": 4.31,
|
| 174 |
+
"transcript": "Major reason is because \u092e\u0941\u091d\u0947 lights \u092c\u0939\u0941\u0924 \u092a\u0938\u0902\u0926 \u0939\u0948 I love all these \u0926\u093f\u092f\u093e",
|
| 175 |
+
"code_mixing_index": 35.71,
|
| 176 |
+
"code_switch_count": 5
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"filename": "hinglish_hiacc_AD60096.wav",
|
| 180 |
+
"bucket": "hinglish",
|
| 181 |
+
"source": "hiacc_adult_test",
|
| 182 |
+
"duration_seconds": 4.78,
|
| 183 |
+
"transcript": "\u0907\u0938\u0947 \u0938\u0941\u0928\u0915\u0947 \u0926\u094b\u0928\u094b\u0902 \u0928\u0947 \u0938\u094b\u091a\u093e \u0915\u093f \u092f\u0947 \u0909\u0928\u0915\u093e next adventure spot \u0939\u094b \u0938\u0915\u0924\u093e \u0939\u0948\u0964",
|
| 184 |
+
"code_mixing_index": 21.43,
|
| 185 |
+
"code_switch_count": 2
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"filename": "hinglish_hiacc_AD40158.wav",
|
| 189 |
+
"bucket": "hinglish",
|
| 190 |
+
"source": "hiacc_adult_test",
|
| 191 |
+
"duration_seconds": 5.42,
|
| 192 |
+
"transcript": "\u0910\u0938\u093e \u0932\u0917 \u0930\u0939\u093e \u0939\u0948 \u091c\u0948\u0938\u0947 \u0915\u0940 superman \u092c\u0928\u0928\u0947 \u0915\u0940 \u0915\u094b\u0936\u093f\u0936 \u0915\u0930 \u0930\u0939\u093e \u0939\u0948 \u0924\u094b \u0907\u0938 image \u0938\u0947 \u092f\u0947 \u092d\u0940 \u0915\u0939\u093e \u091c\u093e \u0938\u0915\u0924\u093e \u0939\u0948 \u0915\u093f",
|
| 193 |
+
"code_mixing_index": 8.33,
|
| 194 |
+
"code_switch_count": 4
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"filename": "hinglish_hiacc_AD40103.wav",
|
| 198 |
+
"bucket": "hinglish",
|
| 199 |
+
"source": "hiacc_adult_test",
|
| 200 |
+
"duration_seconds": 6.04,
|
| 201 |
+
"transcript": "\u091a\u093f\u0902\u091f\u0942 \u0914\u0930 \u0930\u093e\u091c\u0942 \u0928\u0947 guardian \u0915\u0947 \u0926\u093f\u090f \u0917\u090f task complete \u0915\u0930 \u0932\u093f\u090f \u0914\u0930 \u0939\u0930 \u090f\u0915 task \u0915\u0947 \u092c\u093e\u0926",
|
| 202 |
+
"code_mixing_index": 22.22,
|
| 203 |
+
"code_switch_count": 6
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"filename": "hinglish_hiacc_AD22092.wav",
|
| 207 |
+
"bucket": "hinglish",
|
| 208 |
+
"source": "hiacc_adult_test",
|
| 209 |
+
"duration_seconds": 7.11,
|
| 210 |
+
"transcript": "\u0905\u092a\u0928\u093e \u0916\u093e\u0928\u093e \u0926\u0947\u0916 \u0915\u0947 \u0915\u094d\u092f\u0942\u0901\u0915\u093f \u090f\u0915 \u0915\u094b \u0915\u092e \u092e\u093f\u0932\u093e \u090f\u0915 \u0915\u094b \u091c\u093e\u0926\u093e \u092e\u093f\u0932\u093e \u0924\u092c \u092d\u0940 \u0935\u094b unsatisfied \u0939\u0948 \u0909\u0938\u0915\u0947 dressing sense \u0914\u0930 \u0935\u094b \u0938\u092c \u091a\u0940\u091c\u0947 \u092d\u0940 \u0905\u091a\u094d\u091b\u0940 \u0939\u0948\u0964",
|
| 211 |
+
"code_mixing_index": 10.71,
|
| 212 |
+
"code_switch_count": 4
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"filename": "hinglish_hiacc_AD40049.wav",
|
| 216 |
+
"bucket": "hinglish",
|
| 217 |
+
"source": "hiacc_adult_test",
|
| 218 |
+
"duration_seconds": 9.05,
|
| 219 |
+
"transcript": "\u092a\u0939\u093e\u0921\u0940 \u092f\u093e \u092b\u093f\u0930 \u0915\u093f\u0938\u0940 outstation \u091c\u0948\u0938\u0947 \u0936\u0939\u0930 \u092e\u0947\u0902 \u0939\u094b \u0938\u0915\u0924\u093e \u0939\u0948 \u0915\u094d\u092f\u0942\u0915\u093f \u0935\u0939\u093e\u0901 \u092a\u0947 \u092c\u0939\u0941\u0924 different \u091c\u0917\u0939 \u0915\u0947 \u0932\u094b\u0917 \u0939\u094b\u0924\u0947 \u0939\u0948\u0902\u0964",
|
| 220 |
+
"code_mixing_index": 9.52,
|
| 221 |
+
"code_switch_count": 4
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
"filename": "hinglish_hiacc_AD09072.wav",
|
| 225 |
+
"bucket": "hinglish",
|
| 226 |
+
"source": "hiacc_adult_test",
|
| 227 |
+
"duration_seconds": 16.0,
|
| 228 |
+
"transcript": "that's a barren land Barren \u092a\u0921\u093c\u0940 \u0939\u0948 \u0910\u0938\u093e \u0932\u0917\u0924\u093e \u0939\u0948 \u0915\u093f \u092f\u0947 desert \u0939\u0948 \u090f\u0915 \u0914\u0930 there are small small Mountains are there \u091c\u094b mountains \u0939\u0948 and that are short heighted mountain \u091c\u093f\u0938\u0938\u0947 \u0915\u0940 \u0938\u093e\u092b \u0928\u091c\u0930 \u0906\u0924\u093e \u0939\u0948 \u0915\u093f \u092f\u0947 \u090f\u0915 desert \u0915\u093e part \u0939\u0948",
|
| 229 |
+
"code_mixing_index": 48.89,
|
| 230 |
+
"code_switch_count": 13
|
| 231 |
+
}
|
| 232 |
+
]
|