File size: 6,370 Bytes
f821a4a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""Pieces of the Inter-2-Audio playground: audio, chat layout, parsing, the result summary.

Kept apart from app.py (which loads the model and the UI) so they can be tested
without a GPU. The task presets (prompts + JSON schemas) live in tasks.py.
Decoding defaults: audio before text, greedy, repetition penalty 1.0.
"""

from __future__ import annotations

import ast
import html
import json
import re
from typing import Any

import numpy as np

TARGET_SR = 16_000
ENCODER_WINDOW_S = 30.0
DEFAULT_REPETITION_PENALTY = 1.0
DEFAULT_MAX_NEW_TOKENS = 768


def build_messages(user_prompt: str, system_prompt: str = "", audio_first: bool = True) -> list[dict[str, Any]]:
    """One user turn holding the audio placeholder and the prompt, plus a system turn only if given."""
    text = {"type": "text", "text": user_prompt}
    audio = {"type": "audio", "audio": "clip"}  # placeholder; the waveform goes to the processor separately
    messages = [{"role": "system", "content": [{"type": "text", "text": system_prompt}]}] if system_prompt else []
    return messages + [{"role": "user", "content": [audio, text] if audio_first else [text, audio]}]


class AudioInputError(ValueError):
    """A problem with the uploaded audio; its message is safe to show to the user."""


def load_audio(path: str, truncate: bool = True) -> tuple[np.ndarray, float]:
    """Mono float32 at 16 kHz, cut to the encoder window. Returns (waveform, original duration in s)."""
    import librosa

    try:
        wav, _ = librosa.load(path, sr=TARGET_SR, mono=True)
    except Exception as exc:
        raise AudioInputError("Could not read this audio file. Try a WAV, MP3 or FLAC file.") from exc
    duration = len(wav) / TARGET_SR
    if duration == 0:
        raise AudioInputError("The audio file is empty.")
    if duration > ENCODER_WINDOW_S + 1e-3:
        if not truncate:
            raise AudioInputError(f"The clip is {duration:.1f}s; the model hears at most {ENCODER_WINDOW_S:.0f}s.")
        wav = wav[: int(ENCODER_WINDOW_S * TARGET_SR)]
    return wav.astype(np.float32), duration


def parse_json(text: str) -> Any | None:
    """The model's JSON answer, tolerating a code fence, surrounding prose or Python-style literals."""
    text = text.strip()
    fence = re.match(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL)
    if fence:
        text = fence.group(1).strip()
    match = re.search(r"[\[{].*[\]}]", text, re.DOTALL)
    for candidate in (text, match.group(0) if match else None):
        if candidate is None:
            continue
        try:
            return json.loads(candidate)
        except json.JSONDecodeError:
            pass
        try:
            return ast.literal_eval(candidate)
        except (ValueError, SyntaxError):
            pass
    return None


def _esc(value: Any) -> str:
    return html.escape(str(value))


def _badge(level: Any) -> str:
    if not level:
        return ""
    return f'<span class="ih-badge ih-badge-{_esc(level)}">{_esc(level)}</span>'


def _tile(caption: str, value: Any, level: Any = None) -> str:
    return (
        f'<div class="ih-tile"><div class="ih-caption">{caption}</div>'
        f'<div class="ih-value">{_esc(value)}{_badge(level)}</div></div>'
    )


def _primary_emotion(payload: dict) -> str:
    confidence = payload.get("confidence")
    meta = f"Confidence {float(confidence):.2f}" if isinstance(confidence, (int, float)) else ""
    evidence = f'<p class="ih-note">{_esc(payload["evidence"])}</p>' if payload.get("evidence") else ""
    meta_html = f'<div class="ih-meta">{meta}</div>' if meta else ""
    return (
        f'<div class="ih-caption">Primary emotion</div>'
        f'<div class="ih-headline">{_esc(payload["primary_emotion"])}</div>{meta_html}{evidence}'
    )


def _combined(payload: dict) -> str:
    """Tiles for the single-label fields, the emotion's rationale, then one row per social signal."""
    emotion = next((e for e in payload.get("emotions") or [] if isinstance(e, dict)), None)
    tiles = []
    if emotion:
        tiles.append(_tile("Emotion", emotion.get("name", "?"), emotion.get("probability")))
    if "engagement_status" in payload:
        tiles.append(_tile("Engagement", payload["engagement_status"]))
    if "delivery" in payload:
        tiles.append(_tile("Delivery", payload["delivery"]))
    out = f'<div class="ih-tiles">{"".join(tiles)}</div>' if tiles else ""
    if emotion and emotion.get("rationale"):
        out += f'<p class="ih-note">{_esc(emotion["rationale"])}</p>'
    if "signals" in payload:
        rows = [
            f'<div class="ih-signal"><div class="ih-signal-head">{_esc(sig.get("name", "?"))}'
            f"{_badge(sig.get('probability'))}</div>"
            + (f'<div class="ih-note">{_esc(sig["rationale"])}</div>' if sig.get("rationale") else "")
            + "</div>"
            for sig in payload["signals"] or []
            if isinstance(sig, dict)
        ]
        body = "".join(rows) or '<div class="ih-note">None evidenced.</div>'
        out += f'<div class="ih-caption ih-section">Social signals</div>{body}'
    return out


def format_summary(payload: Any, truncated_from: float | None, hit_token_limit: bool = False) -> str:
    """The result card as HTML: the known fields laid out, otherwise the top-level keys of whatever came back.
    Everything from the model is HTML-escaped."""
    notes = []
    if truncated_from is not None:
        notes.append(f"Clip was {truncated_from:.1f}s; only the first {ENCODER_WINDOW_S:.0f}s were used.")
    if hit_token_limit:
        notes.append("Hit the max-new-tokens limit; the output may be cut off.")
    if payload is None:
        body = '<p class="ih-note">Could not parse JSON from the output. See the raw text.</p>'
    elif isinstance(payload, dict) and "primary_emotion" in payload:
        body = _primary_emotion(payload)
    elif isinstance(payload, dict) and payload.keys() & {"emotions", "engagement_status", "signals", "delivery"}:
        body = _combined(payload)
    elif isinstance(payload, dict):
        body = '<p class="ih-note">Parsed JSON with keys: ' + ", ".join(_esc(k) for k in payload) + "</p>"
    else:
        body = f'<p class="ih-note">Parsed JSON ({type(payload).__name__}).</p>'
    notes_html = "".join(f'<p class="ih-warning">{n}</p>' for n in notes)
    return f'<div class="ih-card">{notes_html}{body}</div>'