File size: 2,268 Bytes
414dc55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""The suspect dossier (immutable ground truth) and anchored lies.

A suspect never improvises a lie at runtime. Every lie is authored here as a fixed
``claimed`` string with a known set of clues that break it, so a small model only
has to *deliver* a known lie, never invent and track one.
"""

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field

from .timeline import StatedAlibi, WhereaboutsSegment
from .visual import VisualDescriptor


class PhysicalCapability(BaseModel):
    model_config = ConfigDict(frozen=True)

    strength: bool = True
    mobility: bool = True


class PersonalityAxes(BaseModel):
    model_config = ConfigDict(frozen=True)

    composure: float = Field(default=0.5, ge=0.0, le=1.0)
    aggression: float = Field(default=0.5, ge=0.0, le=1.0)
    evasiveness: float = Field(default=0.5, ge=0.0, le=1.0)


class VoiceAssignment(BaseModel):
    """Deterministic TTS voice binding, assigned after generation."""

    model_config = ConfigDict(frozen=True)

    engine: str
    speaker_id: int
    length_scale: float = 1.0
    noise_w: float = 0.8


class AnchoredLie(BaseModel):
    model_config = ConfigDict(frozen=True)

    lie_id: str
    topic: str
    claimed: str
    truth_ref: str
    breaks_on: tuple[str, ...] = ()
    fallback: str = ""


class Suspect(BaseModel):
    model_config = ConfigDict(frozen=True)

    sus_id: str
    name: str
    role: str
    persona_summary: str
    # How they behave under questioning (confident, frightened, hostile, ...). Prompt-only:
    # it shapes the actor LLM's voice but is NEVER shown to the player (not in PlayerCaseView).
    demeanour: str = ""

    # Director-only. NEVER placed in the actor prompt (see projections.suspect_brief).
    is_culprit: bool = False

    physical_capability: PhysicalCapability = PhysicalCapability()
    personality: PersonalityAxes = PersonalityAxes()
    tells: tuple[str, ...] = ()

    knows_facts: tuple[str, ...] = ()
    secrets: tuple[str, ...] = ()
    true_whereabouts: tuple[WhereaboutsSegment, ...] = ()
    stated_alibi: StatedAlibi
    must_lie_about: tuple[str, ...] = ()
    anchored_lies: tuple[AnchoredLie, ...] = ()

    voice: VoiceAssignment | None = None
    visual: VisualDescriptor | None = None