From 8bd87d883cfa93dfe56fa6fef64a0cdd1198c528 Mon Sep 17 00:00:00 2001 From: dimitris_sgou Date: Fri, 2 Oct 2026 11:13:44 +0300 Subject: [PATCH 1/7] ALTO-5590: list the 24 behavioral outputs in the README, fix task list in models.py Add a Behavioral outputs table linking to the new Definition of behaviors docs page. The ResultItem.task description listed politeness, which the API does not return, and missed engagement, intensity and generator. Co-Authored-By: Claude Opus 5.5 --- README.md | 20 ++++++++++++++++++++ src/behavioralsignals/models.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c91e63..4a247a0 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,26 @@ process = client.behavioral.upload_audio( ) ``` +### Behavioral outputs + +The Behavioral API returns **24 signals** across 8 dimensions: 18 behavioral signals and 6 speaker signals. Each label is its own signal with its own probability. +All eight dimensions are scored together on every utterance longer than 1 second, so one utterance can be *sad + weak + negative + slow + hesitating + withdrawn*, from a female speaker aged 31 - 45. + +| Task | What it measures | Labels | +|---|---|---| +| `emotion` | Basic emotion in the voice | `happy`, `angry`, `sad`, `neutral` | +| `strength` | Arousal: energy in the voice | `strong`, `weak`, `neutral` | +| `positivity` | Sentiment of the tone | `positive`, `negative`, `neutral` | +| `speaking_rate` | How fast the speaker talks | `fast`, `slow`, `normal` | +| `hesitation` | Signs of hesitation | `yes`, `no` | +| `engagement` | Involved or detached tone | `engaged`, `withdrawn`, `neutral` | +| `gender` | Sex of the speaker | `female`, `male` | +| `age` | Age range of the speaker | `18 - 22`, `23 - 30`, `31 - 45`, `46 - 65` | +| `intensity` | Intensity of the emotion | none: a score between 0 and 1 | + +`neutral` means something different in each task, so always read a label together with its task. +See [Definition of behaviors](https://behavioralsignals.readme.io/docs/definition-of-behaviors) for what each signal means. + ## Deepfake Detection ### Audio diff --git a/src/behavioralsignals/models.py b/src/behavioralsignals/models.py index f941cd4..835652e 100644 --- a/src/behavioralsignals/models.py +++ b/src/behavioralsignals/models.py @@ -237,7 +237,7 @@ class ResultItem(_SerializableModel): ) task: str | None = Field( None, - description="The behavioral attribute. Can be one of diarization, deepfake, visual_deepfake, asr, gender, age, language, features, emotion, strength, positivity, speaking_rate, hesitation, politeness. " + description="The behavioral attribute. Can be one of diarization, deepfake, visual_deepfake, generator, asr, gender, age, language, features, emotion, strength, positivity, speaking_rate, hesitation, engagement, intensity. " "Consider visiting the guides in behavioralsignals.readme.io for the latest examples.", examples=["emotion"], ) From 533277f51935c4b404e1d7b0bdbe43badc04909e Mon Sep 17 00:00:00 2001 From: dimitris_sgou Date: Fri, 2 Oct 2026 11:50:13 +0300 Subject: [PATCH 2/7] ALTO-5590: explain probabilities as scores, show all of them in MCP rows README explains that posteriors are scores and how to derive valence and arousal from positivity and strength. MCP get_result rows now carry every label's probability (2 decimals) instead of a single confidence. print_results showed the top label's probability next to finalLabel; it now shows the probability of finalLabel. Co-Authored-By: Claude Opus 5.5 --- README.md | 24 ++++++++++++++++++++++-- src/behavioralsignals/mcp_server.py | 16 +++++++++++++--- src/behavioralsignals/models.py | 8 ++++++-- src/behavioralsignals/utils.py | 5 +++-- tests/test_mcp_server.py | 22 +++++++++++++++++----- tests/test_utils.py | 6 ++++++ 6 files changed, 67 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4a247a0..2f0fe4b 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ print_results(result.results) `upload_audio()` returns a process with a unique process ID (`pid`). `wait_for_result()` polls until processing completes and returns the analysis result. -`print_results()` prints one line per result: the start and end time in seconds, the task, and the top label with its probability. +`print_results()` prints one line per result: the start and end time in seconds, the task, and the final label with its probability. Continuous tasks such as `intensity` have no label, only a score, and `asr` and `diarization` have a label without a probability. For a 10-second clip of one speaker, the output starts like this: @@ -140,6 +140,26 @@ All eight dimensions are scored together on every utterance longer than 1 second | `intensity` | Intensity of the emotion | none: a score between 0 and 1 | `neutral` means something different in each task, so always read a label together with its task. + +`finalLabel` gives one answer per task. It is picked with tuned thresholds, so it is not always the label with the highest probability. +When you need a score instead of a label, use the probabilities in `prediction`. They are strings, so convert them with `float()`. +For each task in the table they add up to 1, so each label's probability is a score from 0 to 1 that you can track over time, average over a call, or compare against your own threshold. + +For a scale from -1 to 1, subtract the probabilities of two opposite labels: `positivity` gives valence and `strength` gives arousal. + +```python +for item in result.results: + if item.task in ("positivity", "strength"): + scores = {p.label: float(p.posterior) for p in item.prediction} + if item.task == "positivity": + print(item.st, "valence", scores["positive"] - scores["negative"]) + else: + print(item.st, "arousal", scores["strong"] - scores["weak"]) +``` + +The API does not predict valence or arousal itself; they are derived from the probabilities. +All probabilities are model estimates: use them to compare utterances and follow trends, not as exact measurements. + See [Definition of behaviors](https://behavioralsignals.readme.io/docs/definition-of-behaviors) for what each signal means. ## Deepfake Detection @@ -404,7 +424,7 @@ The server exposes four tools: |---|---| | `analyze_behavior` | Uploads an audio file or S3 presigned URL for behavioral analysis and returns the results | | `detect_deepfake` | Uploads an audio or video file, or an S3 presigned URL, for deepfake detection and returns the results | -| `get_result` | Returns the results of a process, in pages, optionally filtered to specific tasks | +| `get_result` | Returns the results of a process, in pages, optionally filtered to specific tasks. Each row has the final label and the probability of every label | | `list_processes` | Lists processes, newest first, including the failure reason when available | Notes: diff --git a/src/behavioralsignals/mcp_server.py b/src/behavioralsignals/mcp_server.py index 5b87b1c..3cf21fa 100644 --- a/src/behavioralsignals/mcp_server.py +++ b/src/behavioralsignals/mcp_server.py @@ -36,7 +36,7 @@ DEFAULT_WAIT = 45 DEFAULT_LIMIT = 300 -RESULT_COLUMNS = ["start", "end", "task", "label", "confidence"] +RESULT_COLUMNS = ["start", "end", "task", "label", "probabilities"] PROCESS_COLUMNS = ["pid", "name", "status", "duration", "created", "reason"] READ_ONLY = ToolAnnotations(read_only_hint=True) @@ -100,6 +100,9 @@ def get_result( `analysis` is the kind of job: "behavioral" (analyze_behavior), "deepfake_audio" or "deepfake_video" (detect_deepfake). Results come as tab-separated rows, `limit` rows from `offset`. `tasks` keeps only those tasks, e.g. ["emotion"]; the header lists the task names. + Each row has the final label and the probability of every label of the task. Use the + probabilities as scores, e.g. valence = P(positive) - P(negative) from positivity, and + arousal = P(strong) - P(weak) from strength. """ with _tool_errors(), _api(analysis) as api: return _result_text(api, analysis, pid, wait_seconds, tasks, offset, limit) @@ -238,8 +241,15 @@ def _result_row(item: ResultItem) -> list[str] | None: label = item.finalLabel or (top.score if top else None) if not label: return None - confidence = top.posterior if top else None - return [_cell(value) for value in (item.startTime, item.endTime, item.task, label, confidence)] + values = (item.startTime, item.endTime, item.task, label, _probabilities(item)) + return [_cell(value) for value in values] + + +def _probabilities(item: ResultItem) -> str: + """Returns every label of the item with its probability, e.g. "sad=0.70, neutral=0.30".""" + return ", ".join( + f"{p.label}={float(p.posterior):.2f}" for p in item.prediction or [] if p.posterior + ) def _format_result( diff --git a/src/behavioralsignals/models.py b/src/behavioralsignals/models.py index 835652e..873efb4 100644 --- a/src/behavioralsignals/models.py +++ b/src/behavioralsignals/models.py @@ -215,7 +215,9 @@ def model_dump_json(self, **kwargs): class ModelPredictions(_SerializableModel): label: str | None = Field(None, description="The name of the class", examples=["happy"]) posterior: str | None = Field( - None, description="The probability of this class being present", examples=["0.754"] + None, + description="The probability of this class, as a string. The posteriors of one result add up to 1; use float(posterior) as a score when finalLabel is not enough", + examples=["0.754"], ) score: str | None = Field( None, @@ -243,7 +245,9 @@ class ResultItem(_SerializableModel): ) prediction: list[ModelPredictions] | None = None finalLabel: str | None = Field( - None, description="The dominant value of the behavioral attribute", examples=["happy"] + None, + description="The label the API picks for this task, using tuned thresholds: it may not be the label with the highest posterior", + examples=["happy"], ) level: str | None = Field( None, diff --git a/src/behavioralsignals/utils.py b/src/behavioralsignals/utils.py index 18054cb..ee0c232 100644 --- a/src/behavioralsignals/utils.py +++ b/src/behavioralsignals/utils.py @@ -27,7 +27,7 @@ def make_audio_stream(file_path: str, chunk_size: float = 0.25) -> tuple[Iterato def print_results(items: Iterable[ResultItem] | None) -> None: - """Print one line per result: start time, end time, task, top label and its probability. + """Print one line per result: start time, end time, task, final label and its probability. Continuous tasks (e.g. intensity) have no label, so their score is printed instead. Rows without a label or score (e.g. the features row, which carries embeddings) are skipped. @@ -40,5 +40,6 @@ def print_results(items: Iterable[ResultItem] | None) -> None: label = item.finalLabel or (top.score if top else None) if not label: continue - confidence = f" ({float(top.posterior):.1%})" if top and top.posterior else "" + posterior = next((p.posterior for p in item.prediction or [] if p.label == label), None) + confidence = f" ({float(posterior):.1%})" if posterior else "" print(f"{item.st} {item.et} {item.task} {label.strip()}{confidence}") diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 5686e91..66a1365 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -111,13 +111,25 @@ async def test_formats_labelled_rows_and_drops_the_rest(api): assert not is_error assert text == ( "pid 7 completed. Rows 1-3 of 3. Tasks: asr, emotion, intensity\n" - "start\tend\ttask\tlabel\tconfidence\n" + "start\tend\ttask\tlabel\tprobabilities\n" "0.5\t1.5\tasr\tHello there.\t\n" - "0.5\t1.5\temotion\tsad\t0.706\n" + "0.5\t1.5\temotion\tsad\tsad=0.71\n" "0.5\t1.5\tintensity\t0.62\t" ) +async def test_rows_show_the_probability_of_every_label(api): + prediction = [ + ModelPredictions(label="sad", posterior="0.5"), + ModelPredictions(label="neutral", posterior="0.4"), + ] + api.responses["wait_for_result"] = ResultResponse( + pid=7, results=[ResultItem(task="emotion", finalLabel="neutral", prediction=prediction)] + ) + _, text = await call("get_result", pid=7, analysis="behavioral") + assert text.endswith("\temotion\tneutral\tsad=0.50, neutral=0.40") + + async def test_tabs_and_newlines_in_a_cell_become_spaces(api): api.responses["wait_for_result"] = ResultResponse( pid=7, results=[item("asr", "Hi\tthere.\nBye.")] @@ -134,9 +146,9 @@ async def test_video_rows_have_a_track_column(api): ) _, text = await call("get_result", pid=7, analysis="deepfake_video") assert text.splitlines()[1:] == [ - "track\tstart\tend\ttask\tlabel\tconfidence", - "audio\t0.5\t1.5\tdeepfake\tbonafide\t0.9", - "video\t0.5\t1.5\tvisual_deepfake\tspoofed\t0.8", + "track\tstart\tend\ttask\tlabel\tprobabilities", + "audio\t0.5\t1.5\tdeepfake\tbonafide\tbonafide=0.90", + "video\t0.5\t1.5\tvisual_deepfake\tspoofed\tspoofed=0.80", ] diff --git a/tests/test_utils.py b/tests/test_utils.py index bae75ec..ab44139 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -59,6 +59,12 @@ def test_print_results_prints_one_line_per_result(capsys): ) +def test_print_results_shows_the_probability_of_the_final_label(capsys): + prediction = [{"label": "sad", "posterior": "0.5"}, {"label": "neutral", "posterior": "0.4"}] + print_results([_item("emotion", prediction, "neutral")]) + assert capsys.readouterr().out == "0.487 3.001 emotion neutral (40.0%)\n" + + def test_print_results_accepts_none(capsys): print_results(None) assert capsys.readouterr().out == "" From 09b57f430079a4ba7a50ce73ddea4a109c575985 Mon Sep 17 00:00:00 2001 From: dimitris_sgou Date: Fri, 2 Oct 2026 12:51:56 +0300 Subject: [PATCH 3/7] ALTO-5590: valence = positivity, clearer behavioral outputs for agents Positivity is valence and strength is arousal. The README table gets per-task counts, the 5,184 profiles, the per-task meaning of neutral, the short-utterance rule and the global speaking rate scale. ResultItem.task groups tasks by kind. Checked with blind agent Q&A runs. Co-Authored-By: Claude Opus 5.5 --- README.md | 37 ++++++++++++++++------------- src/behavioralsignals/mcp_server.py | 4 ++-- src/behavioralsignals/models.py | 2 +- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2f0fe4b..d15cf36 100644 --- a/README.md +++ b/README.md @@ -125,27 +125,31 @@ process = client.behavioral.upload_audio( ### Behavioral outputs The Behavioral API returns **24 signals** across 8 dimensions: 18 behavioral signals and 6 speaker signals. Each label is its own signal with its own probability. -All eight dimensions are scored together on every utterance longer than 1 second, so one utterance can be *sad + weak + negative + slow + hesitating + withdrawn*, from a female speaker aged 31 - 45. - -| Task | What it measures | Labels | -|---|---|---| -| `emotion` | Basic emotion in the voice | `happy`, `angry`, `sad`, `neutral` | -| `strength` | Arousal: energy in the voice | `strong`, `weak`, `neutral` | -| `positivity` | Sentiment of the tone | `positive`, `negative`, `neutral` | -| `speaking_rate` | How fast the speaker talks | `fast`, `slow`, `normal` | -| `hesitation` | Signs of hesitation | `yes`, `no` | -| `engagement` | Involved or detached tone | `engaged`, `withdrawn`, `neutral` | -| `gender` | Sex of the speaker | `female`, `male` | -| `age` | Age range of the speaker | `18 - 22`, `23 - 30`, `31 - 45`, `46 - 65` | -| `intensity` | Intensity of the emotion | none: a score between 0 and 1 | - -`neutral` means something different in each task, so always read a label together with its task. +All eight dimensions are scored together on every utterance longer than 1 second, so one utterance can be *sad + weak + negative + slow + hesitating + withdrawn*, from a female speaker aged 31 - 45. That gives 4 × 3 × 3 × 3 × 2 × 3 × 2 × 4 = **5,184 possible profiles** per utterance. + +| Task | What it measures | Labels | # | +|---|---|---|---| +| `emotion` | Basic emotion in the voice | `happy`, `angry`, `sad`, `neutral` | 4 | +| `strength` | Arousal: energy in the voice | `strong`, `weak`, `neutral` | 3 | +| `positivity` | Valence: sentiment of the tone | `positive`, `negative`, `neutral` | 3 | +| `speaking_rate` | How fast the speaker talks, compared to speakers in general, not to their own pace | `fast`, `slow`, `normal` | 3 | +| `hesitation` | Signs of hesitation | `yes`, `no` | 2 | +| `engagement` | Involved or detached tone | `engaged`, `withdrawn`, `neutral` | 3 | +| `gender` | Sex of the speaker | `female`, `male` | 2 | +| `age` | Age range of the speaker | `18 - 22`, `23 - 30`, `31 - 45`, `46 - 65` | 4 | +| **Total** | | | **24** | + +Plus `intensity`: how intense the emotion is, whichever emotion it is, as a score between 0 and 1. It has no labels and is not counted in the 24. +Utterances of 1 second or less return only `diarization`, `asr`, `gender` and `language`. + +`neutral` means something different in each task, so always read a label together with its task: `emotion: neutral` means no clear emotion, `strength: neutral` normal energy, `positivity: neutral` neither positive nor negative, and `engagement: neutral` neither engaged nor withdrawn. `finalLabel` gives one answer per task. It is picked with tuned thresholds, so it is not always the label with the highest probability. When you need a score instead of a label, use the probabilities in `prediction`. They are strings, so convert them with `float()`. For each task in the table they add up to 1, so each label's probability is a score from 0 to 1 that you can track over time, average over a call, or compare against your own threshold. +For example, the `angry` probability of `emotion` is a score for anger. `intensity` is not: it does not say which emotion is intense. -For a scale from -1 to 1, subtract the probabilities of two opposite labels: `positivity` gives valence and `strength` gives arousal. +`positivity` is valence and `strength` is arousal, the two dimensions often used in emotion research. The API gives them as probabilities from 0 to 1. To put them on the usual scale from -1 to 1, subtract the probabilities of the two opposite labels: ```python for item in result.results: @@ -157,7 +161,6 @@ for item in result.results: print(item.st, "arousal", scores["strong"] - scores["weak"]) ``` -The API does not predict valence or arousal itself; they are derived from the probabilities. All probabilities are model estimates: use them to compare utterances and follow trends, not as exact measurements. See [Definition of behaviors](https://behavioralsignals.readme.io/docs/definition-of-behaviors) for what each signal means. diff --git a/src/behavioralsignals/mcp_server.py b/src/behavioralsignals/mcp_server.py index 3cf21fa..ad82f14 100644 --- a/src/behavioralsignals/mcp_server.py +++ b/src/behavioralsignals/mcp_server.py @@ -101,8 +101,8 @@ def get_result( "deepfake_video" (detect_deepfake). Results come as tab-separated rows, `limit` rows from `offset`. `tasks` keeps only those tasks, e.g. ["emotion"]; the header lists the task names. Each row has the final label and the probability of every label of the task. Use the - probabilities as scores, e.g. valence = P(positive) - P(negative) from positivity, and - arousal = P(strong) - P(weak) from strength. + probabilities as scores. positivity is valence and strength is arousal; for a scale from -1 + to 1, use P(positive) - P(negative) and P(strong) - P(weak). """ with _tool_errors(), _api(analysis) as api: return _result_text(api, analysis, pid, wait_seconds, tasks, offset, limit) diff --git a/src/behavioralsignals/models.py b/src/behavioralsignals/models.py index 873efb4..a44c7b0 100644 --- a/src/behavioralsignals/models.py +++ b/src/behavioralsignals/models.py @@ -239,7 +239,7 @@ class ResultItem(_SerializableModel): ) task: str | None = Field( None, - description="The behavioral attribute. Can be one of diarization, deepfake, visual_deepfake, generator, asr, gender, age, language, features, emotion, strength, positivity, speaking_rate, hesitation, engagement, intensity. " + description="The task. Behavioral: emotion, strength, positivity, speaking_rate, hesitation, engagement, intensity. Speaker: gender, age. Other: diarization, asr, language, features. Deepfakes API: deepfake, visual_deepfake, generator. " "Consider visiting the guides in behavioralsignals.readme.io for the latest examples.", examples=["emotion"], ) From 6b4fd852f859be187394f2b4eefbf09777c076f6 Mon Sep 17 00:00:00 2001 From: dimitris_sgou Date: Fri, 2 Oct 2026 13:52:38 +0300 Subject: [PATCH 4/7] ALTO-5590: correct finalLabel, per-speaker gender/age and short-utterance outputs Checked against modeling_api and live API runs: - finalLabel is the top label for behavioral tasks - gender and age are per speaker; utterances of 1 s or less get only diarization, asr and language - open-ended outputs, grouping by utterance and speaker, intensity wording - models.py: embedding sizes (192 speaker, 768 behavioral), level, dominantInSegments Co-Authored-By: Claude Opus 5.5 --- README.md | 9 ++++++--- src/behavioralsignals/models.py | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d15cf36..8ab744e 100644 --- a/README.md +++ b/README.md @@ -139,12 +139,15 @@ All eight dimensions are scored together on every utterance longer than 1 second | `age` | Age range of the speaker | `18 - 22`, `23 - 30`, `31 - 45`, `46 - 65` | 4 | | **Total** | | | **24** | -Plus `intensity`: how intense the emotion is, whichever emotion it is, as a score between 0 and 1. It has no labels and is not counted in the 24. -Utterances of 1 second or less return only `diarization`, `asr`, `gender` and `language`. +Plus `intensity`: how intense the emotion is, as one score between 0 and 1. It is not a score for any one emotion, even when `emotion` is `angry`. It has no labels and is not counted in the 24. +Utterances of 1 second or less return only `diarization`, `asr` and `language`. +`gender` and `age` are given per speaker: every utterance of a speaker longer than 1 second gets the same values, averaged over the file. +The other tasks have open-ended values, not fixed labels: `diarization` (a speaker ID such as `SPEAKER_00`), `asr` (the transcript) and `language`. With `embeddings=True`, `features` holds the behavioral embedding. +All results of one utterance share its `id`, `startTime` and `endTime`. To split results by speaker, use the `diarization` label of each utterance. Speaker IDs are generic: the API does not say who is the agent and who is the customer. `neutral` means something different in each task, so always read a label together with its task: `emotion: neutral` means no clear emotion, `strength: neutral` normal energy, `positivity: neutral` neither positive nor negative, and `engagement: neutral` neither engaged nor withdrawn. -`finalLabel` gives one answer per task. It is picked with tuned thresholds, so it is not always the label with the highest probability. +`finalLabel` gives one answer per task. For behavioral tasks it is the label with the highest probability. When you need a score instead of a label, use the probabilities in `prediction`. They are strings, so convert them with `float()`. For each task in the table they add up to 1, so each label's probability is a score from 0 to 1 that you can track over time, average over a call, or compare against your own threshold. For example, the `angry` probability of `emotion` is a score for anger. `intensity` is not: it does not say which emotion is intense. diff --git a/src/behavioralsignals/models.py b/src/behavioralsignals/models.py index a44c7b0..dafbde9 100644 --- a/src/behavioralsignals/models.py +++ b/src/behavioralsignals/models.py @@ -225,7 +225,7 @@ class ModelPredictions(_SerializableModel): examples=["0.62"], ) dominantInSegments: list[int] | None = Field( - None, description="The segments in which this class is dominant" + None, description="Internal field, usually empty. You can ignore it." ) @@ -246,17 +246,17 @@ class ResultItem(_SerializableModel): prediction: list[ModelPredictions] | None = None finalLabel: str | None = Field( None, - description="The label the API picks for this task, using tuned thresholds: it may not be the label with the highest posterior", + description="The label the API picks for this task. For behavioral tasks it is the label with the highest posterior.", examples=["happy"], ) level: str | None = Field( None, - description="Whether this result corresponds to a segment/utterance", + description="'utterance' in batch results. Streaming can also return 'segment'.", examples=["utterance"], ) embedding: str | None = Field( None, - description="The corresponding embedding (present in diarization or features). It's a stringified array of length 728.", + description="The embedding as a stringified array: the speaker embedding (192 values) on the diarization row of a speaker's first utterance, or the behavioral embedding (768 values) on the features row.", examples=["[11.614513397216797, -15.228992462158203, -4.92175817489624, ...]"], ) From f2620b8368d76d4bc727aaf10a9cfe6eafd692ba Mon Sep 17 00:00:00 2001 From: dimitris_sgou Date: Fri, 2 Oct 2026 14:07:55 +0300 Subject: [PATCH 5/7] ALTO-5590: align wording with the brand kit claims rules - outputs are estimates from the voice, not facts about the speaker - "estimate behavioral signals" instead of "analyze human behavior" - mark video deepfake detection as experimental in the MCP tool Co-Authored-By: Claude Opus 5.5 --- README.md | 12 ++++++------ src/behavioralsignals/mcp_server.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8ab744e..5e3f095 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Official Python SDK for the [Behavioral Signals API](https://behavioralsignals.readme.io/). -Analyze human behavior and detect deepfake speech using batch and real-time audio APIs. Experimental video deepfake detection is also available in batch mode. +Estimate behavioral signals from speech and detect deepfake speech using batch and real-time audio APIs. Experimental video deepfake detection is also available in batch mode. [Python SDK Documentation](https://behavioralsignals.readme.io/docs/behavioral-signals-python-sdk) · [Examples](examples/) · @@ -34,7 +34,7 @@ Analyze human behavior and detect deepfake speech using batch and real-time audi ## Features -- **Behavioral Analysis** — analyze human behavior from speech in batch and real-time streaming modes +- **Behavioral Analysis** — estimate behavioral signals from speech in batch and real-time streaming modes - **Deepfake Detection** — detect synthetic or manipulated speech in batch and real-time streaming modes - **Video Deepfake Detection (Experimental, Batch Only)** — analyze both the video frames and audio track of supported video files - **Core Speech Attributes (Batch Only)** — automatic speech recognition (ASR), speaker diarization, and language identification @@ -125,7 +125,7 @@ process = client.behavioral.upload_audio( ### Behavioral outputs The Behavioral API returns **24 signals** across 8 dimensions: 18 behavioral signals and 6 speaker signals. Each label is its own signal with its own probability. -All eight dimensions are scored together on every utterance longer than 1 second, so one utterance can be *sad + weak + negative + slow + hesitating + withdrawn*, from a female speaker aged 31 - 45. That gives 4 × 3 × 3 × 3 × 2 × 3 × 2 × 4 = **5,184 possible profiles** per utterance. +All eight dimensions are scored together on every utterance longer than 1 second, so one utterance can be *sad + weak + negative + slow + hesitating + withdrawn*, with a voice estimated as female and aged 31 - 45. That gives 4 × 3 × 3 × 3 × 2 × 3 × 2 × 4 = **5,184 possible profiles** per utterance. | Task | What it measures | Labels | # | |---|---|---|---| @@ -135,8 +135,8 @@ All eight dimensions are scored together on every utterance longer than 1 second | `speaking_rate` | How fast the speaker talks, compared to speakers in general, not to their own pace | `fast`, `slow`, `normal` | 3 | | `hesitation` | Signs of hesitation | `yes`, `no` | 2 | | `engagement` | Involved or detached tone | `engaged`, `withdrawn`, `neutral` | 3 | -| `gender` | Sex of the speaker | `female`, `male` | 2 | -| `age` | Age range of the speaker | `18 - 22`, `23 - 30`, `31 - 45`, `46 - 65` | 4 | +| `gender` | Sex of the speaker, estimated from the voice | `female`, `male` | 2 | +| `age` | Age range of the speaker, estimated from the voice | `18 - 22`, `23 - 30`, `31 - 45`, `46 - 65` | 4 | | **Total** | | | **24** | Plus `intensity`: how intense the emotion is, as one score between 0 and 1. It is not a score for any one emotion, even when `emotion` is `angry`. It has no labels and is not counted in the 24. @@ -429,7 +429,7 @@ The server exposes four tools: | Tool | What it does | |---|---| | `analyze_behavior` | Uploads an audio file or S3 presigned URL for behavioral analysis and returns the results | -| `detect_deepfake` | Uploads an audio or video file, or an S3 presigned URL, for deepfake detection and returns the results | +| `detect_deepfake` | Uploads an audio or video file, or an S3 presigned URL, for deepfake detection (video is experimental) and returns the results | | `get_result` | Returns the results of a process, in pages, optionally filtered to specific tasks. Each row has the final label and the probability of every label | | `list_processes` | Lists processes, newest first, including the failure reason when available | diff --git a/src/behavioralsignals/mcp_server.py b/src/behavioralsignals/mcp_server.py index ad82f14..8894a88 100644 --- a/src/behavioralsignals/mcp_server.py +++ b/src/behavioralsignals/mcp_server.py @@ -71,7 +71,7 @@ def detect_deepfake( generator_detection: bool = False, wait_seconds: WaitSeconds = DEFAULT_WAIT, ) -> str: - """Detects whether speech (media="audio") or a video (media="video") is a deepfake. + """Detects whether speech (media="audio") or a video (media="video", experimental) is a deepfake. `source` is an absolute path to a local file, or an S3 presigned URL. With generator_detection=True, the result also names the likely generator (experimental). Each From e278d4032a7289d9d4cfb73d3a9c774abff229c5 Mon Sep 17 00:00:00 2001 From: dimitris_sgou Date: Mon, 5 Oct 2026 16:29:03 +0300 Subject: [PATCH 6/7] ALTO-5590: posteriors add up to about 1, except language (top 3 only) Co-Authored-By: Claude Opus 5.5 --- src/behavioralsignals/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/behavioralsignals/models.py b/src/behavioralsignals/models.py index dafbde9..e6b07ea 100644 --- a/src/behavioralsignals/models.py +++ b/src/behavioralsignals/models.py @@ -216,7 +216,7 @@ class ModelPredictions(_SerializableModel): label: str | None = Field(None, description="The name of the class", examples=["happy"]) posterior: str | None = Field( None, - description="The probability of this class, as a string. The posteriors of one result add up to 1; use float(posterior) as a score when finalLabel is not enough", + description="The probability of this class, as a string. The posteriors of one result add up to about 1, except for language, which lists only the top 3 languages. Use float(posterior) as a score when finalLabel is not enough", examples=["0.754"], ) score: str | None = Field( From e173d7b2bb79c487f9eb98509f957d64f3e682bf Mon Sep 17 00:00:00 2001 From: dimitris_sgou Date: Mon, 5 Oct 2026 16:57:05 +0300 Subject: [PATCH 7/7] ALTO-5590: 1-second and per-speaker rules apply to batch results only Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e3f095..8484629 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ All eight dimensions are scored together on every utterance longer than 1 second | **Total** | | | **24** | Plus `intensity`: how intense the emotion is, as one score between 0 and 1. It is not a score for any one emotion, even when `emotion` is `angry`. It has no labels and is not counted in the 24. -Utterances of 1 second or less return only `diarization`, `asr` and `language`. -`gender` and `age` are given per speaker: every utterance of a speaker longer than 1 second gets the same values, averaged over the file. +In batch results, utterances of 1 second or less return only `diarization`, `asr` and `language`. When streaming, every segment gets all 24 signals and `intensity`, however short it is. +In batch results, `gender` and `age` are given per speaker: every utterance of a speaker longer than 1 second gets the same values, averaged over the file. When streaming, `gender` and `age` are estimated on each segment. The other tasks have open-ended values, not fixed labels: `diarization` (a speaker ID such as `SPEAKER_00`), `asr` (the transcript) and `language`. With `embeddings=True`, `features` holds the behavioral embedding. All results of one utterance share its `id`, `startTime` and `endTime`. To split results by speaker, use the `diarization` label of each utterance. Speaker IDs are generic: the API does not say who is the agent and who is the customer.