File size: 4,024 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
"""The playground's task presets: prompts, the JSON schema each can be constrained to, and
whether the schema is on by default.

  Emotion   the seven-label emotion prompt, no system prompt of our own; free output by default
  Combined  the Inter-2 Audio system prompt + one instruction covering engagement, social
            signals, emotion and delivery; constrained by default, since free output can skip
            sections

When constrained, every label is limited to the values listed in the prompt and every
rationale is capped at 300 characters.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

PROMPTS = Path(__file__).parent / "prompts"
EMOTIONS = ["angry", "disgusted", "fearful", "happy", "neutral", "sad", "surprised"]
CATALOG_EMOTIONS = ["Anger", "Disgust", "Fear", "Happiness", "Neutral", "Sadness", "Surprise"]
SIGNALS = ["Hesitation", "Agreement", "Uncertainty", "Frustration", "Interest", "Confidence"]
ENGAGEMENT = ["engaged", "neutral", "disengaged"]
DELIVERY = ["Normal", "Crying", "Laughing", "Mumbling", "Shouting", "Singing", "Whispering"]
LEVELS = ["high", "medium", "low"]
RATIONALE_MAX_CHARS = 300  # keeps the longest constrained answer well inside 768 new tokens


def _prompt(name: str) -> str:
    return (PROMPTS / name).read_text(encoding="utf-8").strip()


def emotion_schema(confidence_steps: int = 4, evidence_max_chars: int = 0) -> dict:
    """{primary_emotion, confidence, evidence}: the label limited to the 7 emotions.

    confidence is limited to i/confidence_steps (a power of 2, so xgrammar keeps
    the short float form) and evidence is capped at evidence_max_chars (0 forces
    it empty): the label is what matters for this task, and an unbounded number
    can loop on digits. Properties are generated in this order,
    so the label comes first and neither cap can change it under greedy decoding.
    """
    if confidence_steps < 1 or confidence_steps & (confidence_steps - 1):
        raise ValueError(f"confidence_steps must be a power of 2, got {confidence_steps}")
    return {
        "type": "object",
        "properties": {
            "primary_emotion": {"type": "string", "enum": EMOTIONS},
            "confidence": {"enum": [i / confidence_steps for i in range(confidence_steps + 1)]},
            "evidence": {"type": "string", "maxLength": evidence_max_chars},
        },
        "required": ["primary_emotion", "confidence", "evidence"],
    }


def _labelled_list(names: list[str], min_items: int, max_items: int, rationale: bool = True) -> dict:
    fields = {
        "name": {"type": "string", "enum": names},
        "probability": {"type": "string", "enum": LEVELS},
    }
    if rationale:
        fields["rationale"] = {"type": "string", "maxLength": RATIONALE_MAX_CHARS}
    return {
        "type": "array",
        "minItems": min_items,
        "maxItems": max_items,
        "items": {
            "type": "object",
            "properties": fields,
            "required": list(fields),
        },
    }


def combined_schema() -> dict:
    """{engagement_status, signals, emotions, delivery}, in that order (the instruction's order)."""
    return {
        "type": "object",
        "properties": {
            "engagement_status": {"type": "string", "enum": ENGAGEMENT},
            "signals": _labelled_list(SIGNALS, 0, len(SIGNALS)),
            "emotions": _labelled_list(CATALOG_EMOTIONS, 1, 1),
            "delivery": {"type": "string", "enum": DELIVERY},
        },
        "required": ["engagement_status", "signals", "emotions", "delivery"],
    }


@dataclass(frozen=True)
class Task:
    system_prompt: str
    user_prompt: str
    schema: dict
    constrain: bool  # the schema checkbox's default for this task


TASKS: dict[str, Task] = {
    "Emotion": Task("", _prompt("emotion_user.txt"), emotion_schema(), constrain=False),
    "Combined": Task(_prompt("shared_system.txt"), _prompt("combined_user.txt"), combined_schema(), constrain=True),
}
DEFAULT_TASK = "Emotion"