> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ai-coustics.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Batch call analysis with Tyto

> Analyze local recordings with the SDK and write scores for review.

Use [Tyto](/models/audio-insight/tyto) to rank recordings by audio conditions and inspect their score dimensions. This tutorial uses Python SDK 3.3.0/core 0.25.0 and `tyto-1.1-l-16khz`. File analysis runs locally in the SDK.

The script uses [`FileAnalyzer`](/reference/sdk/api/python/analysis#fileanalyzer) and adapts the released [Python file-analysis example](https://github.com/ai-coustics/aic-sdk-py/blob/3.3.0/examples/analyze_file.py). For live input, use [real-time analysis](/models/audio-insight/real-time-analysis).

## Set up

Use Python 3.12 or newer, an SDK key in `AIC_SDK_LICENSE` and a folder of WAV or FLAC recordings. Select the human/user channel before scoring. The example rejects multi-channel recordings so it cannot silently mix two sides of a conversation.

```bash theme={null}
python -m venv .venv
source .venv/bin/activate
python -m pip install 'aic-sdk==3.3.0' 'numpy==2.5.3' 'soundfile==0.13.1'
```

Set the SDK key through your normal secret mechanism. See [authentication](/models/get-started/authenticate-apps) and [telemetry](/reference/concepts/sdk-telemetry) for network requirements.

## Create the script

Save as `analyze_calls.py`. The example analyzes full 5 s windows at 1 s steps for an overlapping review timeline. Each window requires a full model pass. Use larger steps for lower-cost batch triage when your viewer supports the changed cadence.

```python theme={null}
import argparse
import json
import os
from pathlib import Path

import aic_sdk as aic
import numpy as np
import soundfile as sf

MODEL = "tyto-1.1-l-16khz"
WINDOW_SECONDS = 5
STEP_SECONDS = 1
DIMENSIONS = (
    "risk_score", "speaker_reverb", "speaker_loudness", "interfering_speech",
    "noise", "packet_loss", "codec_degradation",
)


def analyze_file(analyzer, path):
    samples, rate = sf.read(path, dtype="float32")
    if samples.ndim != 1:
        raise ValueError("Select the user channel and save it as mono first")
    if not np.isfinite(samples).all() or not np.any(samples):
        raise ValueError("Use finite, non-silent audio")
    if not 8000 <= rate <= 192000:
        raise ValueError("Sample rate must be between 8000 and 192000 Hz")
    if len(samples) < WINDOW_SECONDS * rate:
        return None  # This example excludes partly padded windows.
    results = analyzer.analyze(np.ascontiguousarray(samples), rate, rate * STEP_SECONDS)
    expected = 1 + (len(samples) - WINDOW_SECONDS * rate) // (rate * STEP_SECONDS)
    if len(results) != expected:
        raise RuntimeError("Unexpected analysis window count")
    frames = {name: [float(getattr(result, name)) for result in results] for name in DIMENSIONS}
    if any(not np.isfinite(values).all() for values in frames.values()):
        raise RuntimeError("Non-finite analysis result")
    return {"file": path.name, "duration_sec": len(samples) / rate, "frames": frames}


def main(folder, output):
    key = os.environ["AIC_SDK_LICENSE"]
    files = sorted(path for path in folder.iterdir() if path.is_file() and path.suffix.lower() in {".wav", ".flac"})
    if not files:
        raise ValueError("No WAV or FLAC files found")
    model = aic.Model.from_file(aic.Model.download(MODEL, "./models"))
    analyzer = aic.FileAnalyzer(model, key)
    calls = []
    try:
        for path in files:
            call = analyze_file(analyzer, path)
            if call is None:
                print(f"Skipped {path.name}: shorter than this example's 5 s window")
                continue
            calls.append(call)
            values = call["frames"]["risk_score"]
            print(f"{path.name}: {len(values)} window(s), mean risk {np.mean(values):.3f}")
    finally:
        # FileAnalyzer owns its pair; release it outside a real-time callback.
        del analyzer
    if not calls:
        raise RuntimeError("No complete recordings were analyzed; no results written")
    output.write_text(json.dumps({"model": "Tyto", "calls": calls}, indent=2), encoding="utf-8")
    print(f"Wrote {len(calls)} call(s) to {output}; step {STEP_SECONDS} s")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("folder", type=Path)
    parser.add_argument("output", type=Path, nargs="?", default=Path("analysis.json"))
    args = parser.parse_args()
    main(args.folder, args.output)
```

## Run it on your recordings

```bash theme={null}
python analyze_calls.py recordings/ analysis.json
```

Expect one progress line per analyzed file and a JSON file with seven equally sized score arrays per call. A 12 s recording with 1 s steps produces eight complete windows, starting at 0 s through 7 s. A partial trailing window is excluded.

The first run downloads the model to `./models`; provision it separately for controlled deployments. The decoder supplies mono `float32` samples at their actual rate. The collector handles supported host rates internally, so Tyto's native 16 kHz rate does not require external resampling.

<Note>
  In this release, `FileAnalyzer.analyze()` returns one silence-padded result for input at or below 5 s, including very short input. This tutorial explicitly skips recordings shorter than 5 s to keep its comparisons on complete windows. That is an example policy, not an SDK minimum-length error.
</Note>

For longer files, the SDK resets between independent windows and evaluates complete windows on the requested step grid. Smaller steps add overlap and model passes, not independent evidence.

## Review the results

Start with the JSON and the [Tyto dimension reference](/models/audio-insight/tyto#tyto-dimensions). Inspect the highest-risk calls, listen to representative recordings and compare with actual downstream failures. `speaker_loudness` is a level indicator; high values are not automatically degradation.

Use means, percentiles and the fraction of flagged windows for triage. Document your threshold and step size. The score is an audio indicator, not a guaranteed STT error rate or a diagnosis of the network component that caused an artifact.

### Upload to the dashboard

The [call-analysis dashboard](https://call-analysis.ai-coustics.com/) provides a visual review path for `analysis.json`. Use **Load data** and the **Analysis JSON** input. Only provide recordings through its audio input if your organization's data policy permits it, and check the dashboard's current format/cadence requirements before interpreting its timeline.

Keep the JSON, model ID, SDK version and `STEP_SECONDS` together. The JSON stores arrays rather than explicit per-window timestamps, so a viewer must use the same step size to place scores correctly. The local SDK script does not require a dashboard upload to complete analysis.

## Troubleshooting

| Failure | Recovery |
| :- | :- |
| Missing `AIC_SDK_LICENSE` or authorization error | Supply a valid key to this process and follow [authentication recovery](/production/troubleshooting#credentials-are-missing-invalid-or-expired). SDK failures stop the script. |
| Multi-channel file | Export the user channel as mono. Downmix only when the channels represent one recording rather than independent speakers. |
| Unsupported/undecodable file | Convert to a supported WAV or FLAC file, then verify the decoded rate and samples. The SDK does not decode media codecs. |
| Every recording is short | Use longer recordings for this comparison or deliberately design a separate padded-window evaluation. |
| Unexpected score | Check channel selection, silence, window boundaries and deployment calibration before changing an intervention threshold. |

For a broader comparison of enhancement, speech-to-text and voice-agent behavior, use the [evaluation guide](/guides/evaluate-audio).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.