livepilot 1.12.2 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +3 -3
  3. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  4. package/m4l_device/livepilot_bridge.js +1 -1
  5. package/mcp_server/__init__.py +1 -1
  6. package/mcp_server/branches/__init__.py +32 -0
  7. package/mcp_server/branches/types.py +230 -0
  8. package/mcp_server/composer/__init__.py +10 -1
  9. package/mcp_server/composer/branch_producer.py +229 -0
  10. package/mcp_server/evaluation/policy.py +129 -2
  11. package/mcp_server/experiment/engine.py +47 -11
  12. package/mcp_server/experiment/models.py +72 -7
  13. package/mcp_server/experiment/tools.py +231 -35
  14. package/mcp_server/memory/taste_graph.py +84 -11
  15. package/mcp_server/persistence/taste_store.py +21 -5
  16. package/mcp_server/runtime/session_kernel.py +46 -0
  17. package/mcp_server/runtime/tools.py +29 -3
  18. package/mcp_server/synthesis_brain/__init__.py +53 -0
  19. package/mcp_server/synthesis_brain/adapters/__init__.py +34 -0
  20. package/mcp_server/synthesis_brain/adapters/analog.py +167 -0
  21. package/mcp_server/synthesis_brain/adapters/base.py +86 -0
  22. package/mcp_server/synthesis_brain/adapters/drift.py +166 -0
  23. package/mcp_server/synthesis_brain/adapters/meld.py +151 -0
  24. package/mcp_server/synthesis_brain/adapters/operator.py +169 -0
  25. package/mcp_server/synthesis_brain/adapters/wavetable.py +228 -0
  26. package/mcp_server/synthesis_brain/engine.py +91 -0
  27. package/mcp_server/synthesis_brain/models.py +121 -0
  28. package/mcp_server/synthesis_brain/timbre.py +194 -0
  29. package/mcp_server/tools/_conductor.py +144 -0
  30. package/mcp_server/wonder_mode/engine.py +324 -0
  31. package/mcp_server/wonder_mode/tools.py +153 -1
  32. package/package.json +2 -2
  33. package/remote_script/LivePilot/__init__.py +1 -1
  34. package/server.json +2 -2
@@ -7,6 +7,8 @@ Tools:
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from typing import Optional
11
+
10
12
  from fastmcp import Context
11
13
 
12
14
  from ..server import mcp
@@ -79,6 +81,10 @@ def get_session_kernel(
79
81
  request_text: str = "",
80
82
  mode: str = "improve",
81
83
  aggression: float = 0.5,
84
+ freshness: float = 0.5,
85
+ creativity_profile: str = "",
86
+ sacred_elements: Optional[list] = None,
87
+ synth_hints: Optional[dict] = None,
82
88
  ) -> dict:
83
89
  """Build the unified turn snapshot for V2 orchestration.
84
90
 
@@ -86,11 +92,27 @@ def get_session_kernel(
86
92
  Assembles: session info, capability state, action ledger, taste profile,
87
93
  anti-preferences, and session memory into one canonical snapshot.
88
94
 
89
- mode: observe | improve | explore | finish | diagnose
90
- aggression: 0.0 (subtle) to 1.0 (bold)
95
+ Core params:
96
+ mode: observe | improve | explore | finish | diagnose
97
+ aggression: 0.0 (subtle) to 1.0 (bold) — execution boldness.
98
+
99
+ Creative controls (PR2 — branch-native migration, optional):
100
+ freshness: 0.0 (don't surprise me) to 1.0 (surprise me). Read by
101
+ producers (Wonder, synthesis_brain, composer) to bias branch
102
+ generation. Distinct from aggression, which is about applying
103
+ a single move boldly; freshness is about how far to roam.
104
+ creativity_profile: shorthand producer philosophy tag. Known values
105
+ include "surgeon" (targeted), "alchemist" (transformative),
106
+ "sculptor" (synthesis-focused). Empty ⇒ producer picks a default.
107
+ sacred_elements: caller-asserted list of sacred elements that
108
+ override or augment what song_brain infers. Shape matches
109
+ song_brain entries: {element_type, description, salience}.
110
+ synth_hints: focus hints for synthesis_brain; shape is open in PR2
111
+ and firms up in PR9. Typical keys: track_indices, device_paths,
112
+ target_timbre, preferred_devices.
91
113
 
92
114
  Returns: SessionKernel dict with kernel_id, session topology, capabilities,
93
- memory context, and routing hints.
115
+ memory context, routing hints, and (if provided) creative controls.
94
116
  """
95
117
  from .session_kernel import build_session_kernel
96
118
 
@@ -179,6 +201,10 @@ def get_session_kernel(
179
201
  session_memory=session_mem,
180
202
  taste_graph=taste_graph,
181
203
  anti_preferences=anti_prefs,
204
+ freshness=freshness,
205
+ creativity_profile=creativity_profile,
206
+ sacred_elements=sacred_elements,
207
+ synth_hints=synth_hints,
182
208
  )
183
209
 
184
210
  # Populate routing hints from conductor when request context is available
@@ -0,0 +1,53 @@
1
+ """Synthesis brain — native-synth-aware branch production.
2
+
3
+ Parallel subsystem to mcp_server.sound_design. Where sound_design reasons
4
+ at the block-type level (oscillator / filter / envelope / ...), the
5
+ synthesis brain reasons at the native-device level with per-adapter
6
+ knowledge of Wavetable / Operator / Analog / Drift / Meld parameter
7
+ spaces, modulation graphs, and articulation profiles.
8
+
9
+ Each adapter implements the SynthAdapter protocol:
10
+ - device_name: the Ableton device name it claims
11
+ - extract_profile(device_parameters): read a SynthProfile from live params
12
+ - propose_branches(profile, target, kernel): emit BranchSeed objects plus
13
+ pre-compiled plans (set_device_parameter / batch_set_parameters steps)
14
+
15
+ PR9 ships Wavetable and Operator adapters. PR10 adds Analog, Drift, Meld
16
+ and render-based timbre extraction on top of ``capture_audio``.
17
+
18
+ No MCP @tool() decorators in this PR — the subsystem is callable from
19
+ Python (Wonder's generate_branch_seeds, composer, etc.). PR12 wires
20
+ dedicated MCP tools and does the tool-count metadata sweep in one pass.
21
+ """
22
+
23
+ from .models import (
24
+ SynthProfile,
25
+ TimbralFingerprint,
26
+ ModulationGraph,
27
+ ArticulationProfile,
28
+ OPAQUE,
29
+ NATIVE,
30
+ )
31
+ from .adapters import get_adapter, SynthAdapter
32
+ from .engine import (
33
+ analyze_synth_patch,
34
+ propose_synth_branches,
35
+ supported_devices,
36
+ )
37
+ from .timbre import extract_timbre_fingerprint, diff_fingerprint
38
+
39
+ __all__ = [
40
+ "SynthProfile",
41
+ "TimbralFingerprint",
42
+ "ModulationGraph",
43
+ "ArticulationProfile",
44
+ "OPAQUE",
45
+ "NATIVE",
46
+ "SynthAdapter",
47
+ "get_adapter",
48
+ "analyze_synth_patch",
49
+ "propose_synth_branches",
50
+ "supported_devices",
51
+ "extract_timbre_fingerprint",
52
+ "diff_fingerprint",
53
+ ]
@@ -0,0 +1,34 @@
1
+ """Synth adapter registry.
2
+
3
+ Each adapter is a Python class implementing the SynthAdapter protocol.
4
+ Registration is explicit — adapters add themselves to ``_REGISTRY`` at
5
+ module import time. ``get_adapter(device_name)`` returns the registered
6
+ adapter or None.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .base import SynthAdapter, _REGISTRY, register_adapter
12
+ from . import wavetable as _wavetable # noqa: F401 — import for registration
13
+ from . import operator as _operator # noqa: F401
14
+ from . import analog as _analog # noqa: F401
15
+ from . import drift as _drift # noqa: F401
16
+ from . import meld as _meld # noqa: F401
17
+
18
+
19
+ def get_adapter(device_name: str) -> SynthAdapter | None:
20
+ """Return the adapter for a given Ableton device name, or None."""
21
+ return _REGISTRY.get(device_name)
22
+
23
+
24
+ def registered_devices() -> list[str]:
25
+ """List device names this package has an adapter for."""
26
+ return sorted(_REGISTRY.keys())
27
+
28
+
29
+ __all__ = [
30
+ "SynthAdapter",
31
+ "get_adapter",
32
+ "register_adapter",
33
+ "registered_devices",
34
+ ]
@@ -0,0 +1,167 @@
1
+ """Analog adapter — Ableton's classic two-oscillator subtractive synth.
2
+
3
+ PR10 ships one canned proposer: filter_envelope_variant — pushes Filter
4
+ Envelope Amount while shortening the Filter Decay, producing the
5
+ characteristic "plucked" attack that Analog excels at. Later PRs add
6
+ detune/unison variants and dual-filter variants.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ from typing import Optional
13
+
14
+ from ...branches import BranchSeed, freeform_seed
15
+ from ..models import (
16
+ SynthProfile,
17
+ TimbralFingerprint,
18
+ ModulationGraph,
19
+ ArticulationProfile,
20
+ NATIVE,
21
+ )
22
+ from .base import register_adapter
23
+
24
+
25
+ _KNOWN_PARAMS = {
26
+ "Osc1 Shape",
27
+ "Osc2 Shape",
28
+ "Osc1 Tune",
29
+ "Osc2 Tune",
30
+ "F1 Freq",
31
+ "F1 Reso",
32
+ "F1 Env Amount",
33
+ "F1 Env A",
34
+ "F1 Env D",
35
+ "F1 Env S",
36
+ "F1 Env R",
37
+ "A1 Attack",
38
+ "A1 Decay",
39
+ "A1 Sustain",
40
+ "A1 Release",
41
+ "Glide Mode",
42
+ "Glide Time",
43
+ }
44
+
45
+
46
+ @register_adapter
47
+ class AnalogAdapter:
48
+ device_name: str = "Analog"
49
+
50
+ def extract_profile(
51
+ self,
52
+ track_index: int,
53
+ device_index: int,
54
+ parameter_state: dict,
55
+ display_values: Optional[dict] = None,
56
+ role_hint: str = "",
57
+ ) -> SynthProfile:
58
+ notes: list[str] = []
59
+
60
+ # Filter-env coupling summary
61
+ env_amount = parameter_state.get("F1 Env Amount", 0.0)
62
+ env_decay = parameter_state.get("F1 Env D", 0.0)
63
+ if env_amount and abs(env_amount) > 0.3 and env_decay and env_decay < 0.3:
64
+ notes.append(
65
+ f"Already plucky: F1 Env Amount={env_amount:.2f}, Decay={env_decay:.2f}"
66
+ )
67
+
68
+ articulation = ArticulationProfile(
69
+ attack_ms=float(parameter_state.get("A1 Attack", 0.0) or 0.0),
70
+ release_ms=float(parameter_state.get("A1 Release", 0.0) or 0.0),
71
+ )
72
+
73
+ mod = ModulationGraph()
74
+ if env_amount and abs(env_amount) > 0.01:
75
+ mod.routes.append({
76
+ "source": "Filter Env",
77
+ "target": "F1 Freq",
78
+ "amount": env_amount,
79
+ "range": None,
80
+ })
81
+
82
+ focused_state = {k: v for k, v in parameter_state.items() if k in _KNOWN_PARAMS}
83
+ focused_display = (
84
+ {k: v for k, v in (display_values or {}).items() if k in _KNOWN_PARAMS}
85
+ if display_values else {}
86
+ )
87
+
88
+ return SynthProfile(
89
+ device_name=self.device_name,
90
+ opacity=NATIVE,
91
+ track_index=track_index,
92
+ device_index=device_index,
93
+ parameter_state=focused_state,
94
+ display_values=focused_display,
95
+ role_hint=role_hint,
96
+ modulation=mod,
97
+ articulation=articulation,
98
+ notes=notes,
99
+ )
100
+
101
+ def propose_branches(
102
+ self,
103
+ profile: SynthProfile,
104
+ target: TimbralFingerprint,
105
+ kernel: Optional[dict] = None,
106
+ ) -> list[tuple[BranchSeed, dict]]:
107
+ kernel = kernel or {}
108
+ freshness = float(kernel.get("freshness", 0.5) or 0.5)
109
+ track = profile.track_index
110
+ device = profile.device_index
111
+
112
+ # Skip proposal if already plucky — avoids doubling-down on the
113
+ # same treatment.
114
+ if any("Already plucky" in n for n in profile.notes):
115
+ return []
116
+
117
+ current_env = float(profile.parameter_state.get("F1 Env Amount", 0.0) or 0.0)
118
+ # Target filter-env amount scales with freshness.
119
+ new_env = min(1.0, max(current_env, 0.45 if freshness < 0.5 else 0.65))
120
+ current_decay = float(profile.parameter_state.get("F1 Env D", 0.5) or 0.5)
121
+ new_decay = min(current_decay, 0.25 if freshness < 0.5 else 0.15)
122
+
123
+ seed = freeform_seed(
124
+ seed_id=_short_id("an_plk", f"{track}:{device}:{new_env:.2f}:{new_decay:.2f}"),
125
+ hypothesis=(
126
+ f"Analog filter-pluck: Env Amount → {new_env:.2f}, "
127
+ f"Decay → {new_decay:.2f} for attack character"
128
+ ),
129
+ source="synthesis",
130
+ novelty_label="strong" if freshness < 0.7 else "unexpected",
131
+ risk_label="low",
132
+ affected_scope={
133
+ "track_indices": [track],
134
+ "device_paths": [f"track/{track}/device/{device}"],
135
+ },
136
+ distinctness_reason="only Analog seed that couples Filter Env + Decay",
137
+ )
138
+ plan = {
139
+ "steps": [
140
+ {
141
+ "tool": "set_device_parameter",
142
+ "params": {
143
+ "track_index": track,
144
+ "device_index": device,
145
+ "parameter_name": "F1 Env Amount",
146
+ "value": round(new_env, 3),
147
+ },
148
+ },
149
+ {
150
+ "tool": "set_device_parameter",
151
+ "params": {
152
+ "track_index": track,
153
+ "device_index": device,
154
+ "parameter_name": "F1 Env D",
155
+ "value": round(new_decay, 3),
156
+ },
157
+ },
158
+ ],
159
+ "step_count": 2,
160
+ "summary": f"F1 Env Amount → {new_env:.2f}, F1 Env D → {new_decay:.2f}",
161
+ }
162
+ return [(seed, plan)]
163
+
164
+
165
+ def _short_id(prefix: str, key: str) -> str:
166
+ h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
167
+ return f"{prefix}_{h}"
@@ -0,0 +1,86 @@
1
+ """SynthAdapter base protocol + registry infrastructure.
2
+
3
+ Adapters are plain classes that expose:
4
+ - device_name (class attribute)
5
+ - extract_profile(track_index, device_index, parameter_state, display_values,
6
+ role_hint) -> SynthProfile
7
+ - propose_branches(profile, target, kernel) -> list[tuple[BranchSeed, dict]]
8
+ (each tuple: (seed, compiled_plan_dict))
9
+
10
+ Adapters register themselves via ``register_adapter(cls)`` as a class
11
+ decorator at module-import time, so the registry is populated when
12
+ the adapters package is imported.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Protocol, runtime_checkable, Optional
18
+
19
+ from ...branches import BranchSeed
20
+ from ..models import SynthProfile, TimbralFingerprint
21
+
22
+
23
+ # Adapter registry — populated by register_adapter at import time.
24
+ _REGISTRY: dict[str, "SynthAdapter"] = {}
25
+
26
+
27
+ @runtime_checkable
28
+ class SynthAdapter(Protocol):
29
+ """Contract every native-synth adapter must satisfy."""
30
+
31
+ device_name: str
32
+
33
+ def extract_profile(
34
+ self,
35
+ track_index: int,
36
+ device_index: int,
37
+ parameter_state: dict,
38
+ display_values: Optional[dict] = None,
39
+ role_hint: str = "",
40
+ ) -> SynthProfile:
41
+ """Build a SynthProfile from raw device parameter state.
42
+
43
+ Must be pure — no I/O. The caller has already fetched parameters
44
+ via get_device_parameters / get_display_values and hands them here.
45
+ """
46
+ ...
47
+
48
+ def propose_branches(
49
+ self,
50
+ profile: SynthProfile,
51
+ target: TimbralFingerprint,
52
+ kernel: Optional[dict] = None,
53
+ ) -> list[tuple[BranchSeed, dict]]:
54
+ """Emit seed + pre-compiled plan pairs.
55
+
56
+ Each pair: (BranchSeed with source="synthesis", compiled_plan dict
57
+ ready for execution_router.execute_plan_steps_async).
58
+
59
+ The kernel dict may carry freshness / creativity_profile / synth_hints
60
+ (see SessionKernel PR2 additions); adapters read these to bias
61
+ proposals. Pre-PR10, adapters ship canned proposers; later PRs
62
+ extend with render-based verification.
63
+ """
64
+ ...
65
+
66
+
67
+ def register_adapter(cls):
68
+ """Class decorator — register an adapter under its device_name.
69
+
70
+ Raises ValueError if the class lacks device_name or duplicates an
71
+ existing registration.
72
+ """
73
+ device_name = getattr(cls, "device_name", None)
74
+ if not device_name:
75
+ raise ValueError(
76
+ f"Adapter {cls.__name__} must define a class attribute "
77
+ f"'device_name' to register"
78
+ )
79
+ if device_name in _REGISTRY:
80
+ raise ValueError(
81
+ f"Duplicate synth adapter registration for device '{device_name}' "
82
+ f"(existing: {type(_REGISTRY[device_name]).__name__}, "
83
+ f"new: {cls.__name__})"
84
+ )
85
+ _REGISTRY[device_name] = cls()
86
+ return cls
@@ -0,0 +1,166 @@
1
+ """Drift adapter — Ableton 12's modern subtractive synth.
2
+
3
+ Drift has a cleaner parameter set than Analog and pairs oscillator
4
+ shapes with a character wave + sub + noise blend. PR10 ships one
5
+ canned proposer: character_blend — shifts the oscillator wave + sub
6
+ balance to change core tone without touching the filter. Later PRs
7
+ add tuning-table variants and LFO-routing variants.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ from typing import Optional
14
+
15
+ from ...branches import BranchSeed, freeform_seed
16
+ from ..models import (
17
+ SynthProfile,
18
+ TimbralFingerprint,
19
+ ModulationGraph,
20
+ ArticulationProfile,
21
+ NATIVE,
22
+ )
23
+ from .base import register_adapter
24
+
25
+
26
+ _KNOWN_PARAMS = {
27
+ "Wave",
28
+ "Character",
29
+ "Tune",
30
+ "Sub Level",
31
+ "Sub Tone",
32
+ "Noise Level",
33
+ "Noise Color",
34
+ "Filter Freq",
35
+ "Filter Res",
36
+ "Filter Env",
37
+ "LFO Rate",
38
+ "LFO Amount",
39
+ "Amp Env A",
40
+ "Amp Env D",
41
+ "Amp Env S",
42
+ "Amp Env R",
43
+ }
44
+
45
+
46
+ @register_adapter
47
+ class DriftAdapter:
48
+ device_name: str = "Drift"
49
+
50
+ def extract_profile(
51
+ self,
52
+ track_index: int,
53
+ device_index: int,
54
+ parameter_state: dict,
55
+ display_values: Optional[dict] = None,
56
+ role_hint: str = "",
57
+ ) -> SynthProfile:
58
+ notes: list[str] = []
59
+
60
+ sub = float(parameter_state.get("Sub Level", 0.0) or 0.0)
61
+ noise = float(parameter_state.get("Noise Level", 0.0) or 0.0)
62
+ if sub > 0.5 and role_hint in ("lead", "stab"):
63
+ notes.append(f"Sub Level {sub:.2f} is high for a {role_hint} — check bass clash")
64
+ if noise > 0.5:
65
+ notes.append(f"Noise Level {noise:.2f} — significant noise content")
66
+
67
+ articulation = ArticulationProfile(
68
+ attack_ms=float(parameter_state.get("Amp Env A", 0.0) or 0.0),
69
+ release_ms=float(parameter_state.get("Amp Env R", 0.0) or 0.0),
70
+ )
71
+
72
+ mod = ModulationGraph()
73
+ lfo_amount = parameter_state.get("LFO Amount", 0.0)
74
+ if lfo_amount and abs(lfo_amount) > 0.01:
75
+ mod.routes.append({
76
+ "source": "LFO",
77
+ "target": "(inferred)",
78
+ "amount": lfo_amount,
79
+ "range": None,
80
+ })
81
+
82
+ focused_state = {k: v for k, v in parameter_state.items() if k in _KNOWN_PARAMS}
83
+ focused_display = (
84
+ {k: v for k, v in (display_values or {}).items() if k in _KNOWN_PARAMS}
85
+ if display_values else {}
86
+ )
87
+
88
+ return SynthProfile(
89
+ device_name=self.device_name,
90
+ opacity=NATIVE,
91
+ track_index=track_index,
92
+ device_index=device_index,
93
+ parameter_state=focused_state,
94
+ display_values=focused_display,
95
+ role_hint=role_hint,
96
+ modulation=mod,
97
+ articulation=articulation,
98
+ notes=notes,
99
+ )
100
+
101
+ def propose_branches(
102
+ self,
103
+ profile: SynthProfile,
104
+ target: TimbralFingerprint,
105
+ kernel: Optional[dict] = None,
106
+ ) -> list[tuple[BranchSeed, dict]]:
107
+ kernel = kernel or {}
108
+ freshness = float(kernel.get("freshness", 0.5) or 0.5)
109
+ track = profile.track_index
110
+ device = profile.device_index
111
+
112
+ current_char = float(profile.parameter_state.get("Character", 0.0) or 0.0)
113
+ current_sub = float(profile.parameter_state.get("Sub Level", 0.0) or 0.0)
114
+
115
+ # Toggle character in the opposite direction to create contrast.
116
+ new_char = min(1.0, max(current_char + 0.3, 0.3)) if current_char <= 0.5 else max(0.0, current_char - 0.3)
117
+ # Move sub slightly toward 0.3 if we're starting outside 0.2-0.4
118
+ target_sub = 0.3
119
+ new_sub = current_sub + (target_sub - current_sub) * (0.5 if freshness < 0.5 else 0.8)
120
+ new_sub = round(max(0.0, min(1.0, new_sub)), 3)
121
+
122
+ seed = freeform_seed(
123
+ seed_id=_short_id("dr_chr", f"{track}:{device}:{new_char:.2f}:{new_sub:.2f}"),
124
+ hypothesis=(
125
+ f"Drift character blend: Character → {new_char:.2f}, "
126
+ f"Sub Level → {new_sub:.2f} for a different core tone"
127
+ ),
128
+ source="synthesis",
129
+ novelty_label="strong",
130
+ risk_label="low",
131
+ affected_scope={
132
+ "track_indices": [track],
133
+ "device_paths": [f"track/{track}/device/{device}"],
134
+ },
135
+ distinctness_reason="only Drift seed that shifts Character + Sub balance",
136
+ )
137
+ plan = {
138
+ "steps": [
139
+ {
140
+ "tool": "set_device_parameter",
141
+ "params": {
142
+ "track_index": track,
143
+ "device_index": device,
144
+ "parameter_name": "Character",
145
+ "value": round(new_char, 3),
146
+ },
147
+ },
148
+ {
149
+ "tool": "set_device_parameter",
150
+ "params": {
151
+ "track_index": track,
152
+ "device_index": device,
153
+ "parameter_name": "Sub Level",
154
+ "value": new_sub,
155
+ },
156
+ },
157
+ ],
158
+ "step_count": 2,
159
+ "summary": f"Character → {new_char:.2f}, Sub Level → {new_sub:.2f}",
160
+ }
161
+ return [(seed, plan)]
162
+
163
+
164
+ def _short_id(prefix: str, key: str) -> str:
165
+ h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
166
+ return f"{prefix}_{h}"