nvti commited on
Commit
5349353
·
1 Parent(s): 097ec6e

docs: update default coach-summary model to build-small-hackathon/pozify-coach-summary1 and clarify fallback behavior in documentation

Browse files
README.md CHANGED
@@ -46,14 +46,13 @@ The current codebase supports:
46
  - verifier and conservative fallback summaries
47
  - Modal training pipelines for both the exercise router and coach-summary model
48
 
49
- The current safest cloud runtime for coach summary is:
50
 
51
- - `Qwen/Qwen3-14B`
52
 
53
- The current fine-tuned coach-summary Hugging Face repo can be trained, merged, and published, but
54
- it is not yet cleanly usable through the current Hugging Face serverless `chat_completion` path.
55
- If you want to use that fine-tuned model today, the simplest path is local merged-model inference
56
- through `POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR`.
57
 
58
  ## Product Flow
59
 
@@ -82,25 +81,25 @@ Supported router labels:
82
 
83
  ## Model Stack
84
 
85
- | Component | Model or method | Trained here? | Runtime |
86
- | --- | --- | --- | --- |
87
- | Pose extraction | MediaPipe Pose Landmarker Lite | No | CPU / MediaPipe delegate |
88
- | Exercise router | PyTorch BiLSTM over 30-frame pose windows | Yes | Torch |
89
- | Router baseline | scikit-learn `HistGradientBoostingClassifier` | Yes | CPU fallback/reference |
90
- | Rep counting | Exercise-specific state machines | No ML | CPU |
91
- | Issue markers | Transparent rules over per-rep metrics | No ML | CPU |
92
- | Coach summary | `Qwen/Qwen3-14B` by default | Base model only | HF Inference, local Transformers, or llama.cpp |
93
- | Coach-summary fine-tune | LoRA / merged checkpoint pipeline on Modal | Yes | Local merged-model path recommended |
94
- | Verifier | Deterministic safety and grounding checks | No ML | CPU |
95
 
96
  The trained router is intentionally tiny:
97
 
98
- | Artifact | Count |
99
- | --- | ---: |
100
- | BiLSTM router trainable params | 182,796 |
101
- | Router input features per frame | 237 |
102
- | Window length | 30 frames |
103
- | Output classes | 4 |
104
 
105
  ## Run The App Locally
106
 
@@ -146,17 +145,18 @@ uv run python app.py
146
 
147
  ## Coach Summary Runtime Options
148
 
149
- ### 1. Simplest cloud path
150
 
151
- Use the default supported cloud model:
152
 
153
  ```bash
154
- export POZIFY_COACH_SUMMARY_MODEL=Qwen/Qwen3-14B
155
  uv run python app.py
156
  ```
157
 
158
- This is the easiest path if you want the UI to avoid immediate fallback caused by unsupported custom
159
- HF serverless routing.
 
160
 
161
  ### 2. Use the fine-tuned merged model locally
162
 
@@ -172,7 +172,16 @@ uv run python app.py
172
  This is the simplest way to use `build-small-hackathon/pozify-coach-summary1` today without adding a
173
  dedicated inference endpoint.
174
 
175
- ### 3. llama.cpp
 
 
 
 
 
 
 
 
 
176
 
177
  Pozify can send the coach-summary prompt to a local `llama-server` that exposes the
178
  OpenAI-compatible `/v1/chat/completions` endpoint.
@@ -200,14 +209,14 @@ uv run python app.py
200
 
201
  ### Useful environment variables
202
 
203
- | Variable | Purpose |
204
- | --- | --- |
205
- | `POZIFY_ROUTER_DEVICE` | Override router device, for example `cpu` or `cuda`. |
206
- | `POZIFY_SPACES_GPU_DURATION` | `spaces.GPU` duration in seconds, default `120`. |
207
- | `POZIFY_COACH_SUMMARY_PROVIDER` | `hf_inference`, `local_transformers`, or `llama_cpp`. |
208
- | `POZIFY_COACH_SUMMARY_MODEL` | Coach model id or llama.cpp model alias. |
209
  | `POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR` | Prefer a local merged/model directory for coach summary. |
210
- | `POZIFY_COACH_SUMMARY_BYPASS_VERIFIER` | Keep model output even when verifier fails. |
211
 
212
  ## Exercise Router Training
213
 
@@ -262,10 +271,10 @@ uv run modal run scripts/coach_summary_modal.py --stage publish-merged --repo-id
262
 
263
  Important runtime note:
264
 
265
- - publishing the merged repo does not automatically make it usable through HF serverless
266
- `chat_completion`
267
- - if you want immediate working inference, use `Qwen/Qwen3-14B`
268
- - if you want to use the fine-tuned model, use `POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR`
269
 
270
  ## Generated Artifacts
271
 
 
46
  - verifier and conservative fallback summaries
47
  - Modal training pipelines for both the exercise router and coach-summary model
48
 
49
+ The current default coach-summary model is:
50
 
51
+ - `build-small-hackathon/pozify-coach-summary1`
52
 
53
+ Pozify first tries Hugging Face `chat_completion`, then falls back to Hugging Face
54
+ `text_generation` for non-chat model repos. If hosted inference still rejects the repo or returns an
55
+ invalid schema, the app keeps the conservative fallback summary.
 
56
 
57
  ## Product Flow
58
 
 
81
 
82
  ## Model Stack
83
 
84
+ | Component | Model or method | Trained here? | Runtime |
85
+ | ----------------------- | -------------------------------------------------------- | ----------------------- | ---------------------------------------------- |
86
+ | Pose extraction | MediaPipe Pose Landmarker Lite | No | CPU / MediaPipe delegate |
87
+ | Exercise router | PyTorch BiLSTM over 30-frame pose windows | Yes | Torch |
88
+ | Router baseline | scikit-learn `HistGradientBoostingClassifier` | Yes | CPU fallback/reference |
89
+ | Rep counting | Exercise-specific state machines | No ML | CPU |
90
+ | Issue markers | Transparent rules over per-rep metrics | No ML | CPU |
91
+ | Coach summary | `build-small-hackathon/pozify-coach-summary1` by default | Fine-tuned merged model | HF Inference, local Transformers, or llama.cpp |
92
+ | Coach-summary fine-tune | LoRA / merged checkpoint pipeline on Modal | Yes | Local merged-model path recommended |
93
+ | Verifier | Deterministic safety and grounding checks | No ML | CPU |
94
 
95
  The trained router is intentionally tiny:
96
 
97
+ | Artifact | Count |
98
+ | ------------------------------- | --------: |
99
+ | BiLSTM router trainable params | 182,796 |
100
+ | Router input features per frame | 237 |
101
+ | Window length | 30 frames |
102
+ | Output classes | 4 |
103
 
104
  ## Run The App Locally
105
 
 
145
 
146
  ## Coach Summary Runtime Options
147
 
148
+ ### 1. Fine-tuned coach model
149
 
150
+ The app defaults to the fine-tuned coach-summary model:
151
 
152
  ```bash
153
+ export POZIFY_COACH_SUMMARY_MODEL=build-small-hackathon/pozify-coach-summary1
154
  uv run python app.py
155
  ```
156
 
157
+ Pozify tries `chat_completion` first and falls back to `text_generation` when Hugging Face reports
158
+ that the repo is not a chat model. The deterministic fallback summary remains enabled if hosted
159
+ inference is unavailable or the model output fails validation.
160
 
161
  ### 2. Use the fine-tuned merged model locally
162
 
 
172
  This is the simplest way to use `build-small-hackathon/pozify-coach-summary1` today without adding a
173
  dedicated inference endpoint.
174
 
175
+ ### 3. Base cloud model override
176
+
177
+ If you need the previous base-model runtime:
178
+
179
+ ```bash
180
+ export POZIFY_COACH_SUMMARY_MODEL=Qwen/Qwen3-14B
181
+ uv run python app.py
182
+ ```
183
+
184
+ ### 4. llama.cpp
185
 
186
  Pozify can send the coach-summary prompt to a local `llama-server` that exposes the
187
  OpenAI-compatible `/v1/chat/completions` endpoint.
 
209
 
210
  ### Useful environment variables
211
 
212
+ | Variable | Purpose |
213
+ | -------------------------------------- | -------------------------------------------------------- |
214
+ | `POZIFY_ROUTER_DEVICE` | Override router device, for example `cpu` or `cuda`. |
215
+ | `POZIFY_SPACES_GPU_DURATION` | `spaces.GPU` duration in seconds, default `120`. |
216
+ | `POZIFY_COACH_SUMMARY_PROVIDER` | `hf_inference`, `local_transformers`, or `llama_cpp`. |
217
+ | `POZIFY_COACH_SUMMARY_MODEL` | Coach model id or llama.cpp model alias. |
218
  | `POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR` | Prefer a local merged/model directory for coach summary. |
219
+ | `POZIFY_COACH_SUMMARY_BYPASS_VERIFIER` | Keep model output even when verifier fails. |
220
 
221
  ## Exercise Router Training
222
 
 
271
 
272
  Important runtime note:
273
 
274
+ - the default coach model is `build-small-hackathon/pozify-coach-summary1`
275
+ - Hugging Face hosted inference may still reject a repo or produce invalid JSON, so the
276
+ conservative fallback summary stays enabled
277
+ - for the most predictable fine-tuned inference path, use `POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR`
278
 
279
  ## Generated Artifacts
280
 
docs/01-docs-index.md CHANGED
@@ -20,7 +20,7 @@ Read in this order if you want to understand and run the current project:
20
 
21
  ## Notes
22
 
23
- - Current coach-summary cloud runtime default: `Qwen/Qwen3-14B`
24
- - Current fine-tuned coach-summary Hugging Face repo is best used through local merged-model
25
- inference, not HF serverless `chat_completion`
26
  - Current app entrypoint: `uv run python app.py`
 
20
 
21
  ## Notes
22
 
23
+ - Current coach-summary runtime default: `build-small-hackathon/pozify-coach-summary1`
24
+ - The Hugging Face provider tries `chat_completion` first, then `text_generation` for non-chat
25
+ model repos; local merged-model inference remains the most predictable fine-tuned path
26
  - Current app entrypoint: `uv run python app.py`
docs/10-overview-build-small-hackathon-report.md CHANGED
@@ -3,7 +3,7 @@
3
  Status note:
4
 
5
  - This report is kept as the hackathon narrative document.
6
- - Current runtime defaults have moved to `Qwen/Qwen3-14B`.
7
  - Current coach-summary training now includes a LoRA/merge/publish pipeline on Modal.
8
  - For the current operational commands, prefer [../README.md](../README.md) and
9
  [30-coach-modal-training.md](30-coach-modal-training.md).
@@ -57,7 +57,7 @@ pretending every video is one of the supported movements.
57
  | Pose extractor | MediaPipe Pose Landmarker Lite | Fast, practical feature extractor for a Gradio Space. |
58
  | Exercise router | Custom PyTorch BiLSTM | Tiny trainable temporal model over pose windows. |
59
  | Baseline router | scikit-learn HistGradientBoostingClassifier | Strong baseline over engineered vectors and fallback artifact. |
60
- | Coach summary | Qwen/Qwen3-14B | Current default cloud runtime for structured JSON explanation. |
61
  | llama.cpp path | Qwen3-14B Instruct GGUF via `llama-server` | Local-first/off-grid coach summary path with GPU offload. |
62
 
63
  The original hackathon build trained the exercise router first and used Qwen as a grounded
@@ -212,7 +212,7 @@ logic stay outside the GPU worker. Useful settings:
212
 
213
  ```bash
214
  POZIFY_COACH_SUMMARY_PROVIDER=local_transformers
215
- POZIFY_COACH_SUMMARY_MODEL=Qwen/Qwen3-14B
216
  POZIFY_SPACES_GPU_DURATION=300
217
  ```
218
 
 
3
  Status note:
4
 
5
  - This report is kept as the hackathon narrative document.
6
+ - Current runtime defaults use `build-small-hackathon/pozify-coach-summary1`.
7
  - Current coach-summary training now includes a LoRA/merge/publish pipeline on Modal.
8
  - For the current operational commands, prefer [../README.md](../README.md) and
9
  [30-coach-modal-training.md](30-coach-modal-training.md).
 
57
  | Pose extractor | MediaPipe Pose Landmarker Lite | Fast, practical feature extractor for a Gradio Space. |
58
  | Exercise router | Custom PyTorch BiLSTM | Tiny trainable temporal model over pose windows. |
59
  | Baseline router | scikit-learn HistGradientBoostingClassifier | Strong baseline over engineered vectors and fallback artifact. |
60
+ | Coach summary | build-small-hackathon/pozify-coach-summary1 | Current default fine-tuned runtime for structured JSON explanation. |
61
  | llama.cpp path | Qwen3-14B Instruct GGUF via `llama-server` | Local-first/off-grid coach summary path with GPU offload. |
62
 
63
  The original hackathon build trained the exercise router first and used Qwen as a grounded
 
212
 
213
  ```bash
214
  POZIFY_COACH_SUMMARY_PROVIDER=local_transformers
215
+ POZIFY_COACH_SUMMARY_MODEL=build-small-hackathon/pozify-coach-summary1
216
  POZIFY_SPACES_GPU_DURATION=300
217
  ```
218
 
docs/30-coach-modal-training.md CHANGED
@@ -34,8 +34,9 @@ Important limitation:
34
 
35
  - Publishing a merged model repo to Hugging Face does **not** currently guarantee that the repo can
36
  be used through Hugging Face serverless `chat_completion`.
37
- - In the current codebase, the safest cloud runtime remains `Qwen/Qwen3-14B`.
38
- - The simplest way to use the fine-tuned merged model today is local inference through
 
39
  `POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR`.
40
 
41
  ## Requirements
@@ -142,10 +143,10 @@ The Modal model volume stores:
142
 
143
  ## Current Runtime Usage
144
 
145
- ### Safest cloud runtime now
146
 
147
  ```bash
148
- export POZIFY_COACH_SUMMARY_MODEL=Qwen/Qwen3-14B
149
  uv run python app.py
150
  ```
151
 
@@ -160,15 +161,16 @@ export POZIFY_COACH_SUMMARY_ADAPTER_ID=build-small-hackathon/pozify-coach-summar
160
  uv run python app.py
161
  ```
162
 
163
- ### Not recommended right now
164
 
165
- Pointing app runtime directly at the merged Hugging Face repo through:
166
 
167
  ```bash
168
- export POZIFY_COACH_SUMMARY_MODEL=build-small-hackathon/pozify-coach-summary1
169
  ```
170
 
171
- This may still fall back because HF serverless currently rejects that repo as “not a chat model”.
 
172
 
173
  ## Evaluation Meaning
174
 
 
34
 
35
  - Publishing a merged model repo to Hugging Face does **not** currently guarantee that the repo can
36
  be used through Hugging Face serverless `chat_completion`.
37
+ - The current codebase defaults to `build-small-hackathon/pozify-coach-summary1` and falls back
38
+ from `chat_completion` to `text_generation` for non-chat model repos.
39
+ - The most predictable fine-tuned path remains local inference through
40
  `POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR`.
41
 
42
  ## Requirements
 
143
 
144
  ## Current Runtime Usage
145
 
146
+ ### Default fine-tuned runtime
147
 
148
  ```bash
149
+ export POZIFY_COACH_SUMMARY_MODEL=build-small-hackathon/pozify-coach-summary1
150
  uv run python app.py
151
  ```
152
 
 
161
  uv run python app.py
162
  ```
163
 
164
+ ### Previous base-model runtime
165
 
166
+ If you need to force the previous base model:
167
 
168
  ```bash
169
+ export POZIFY_COACH_SUMMARY_MODEL=Qwen/Qwen3-14B
170
  ```
171
 
172
+ The fine-tuned model may still fall back if hosted inference is unavailable or returns invalid JSON,
173
+ so the deterministic fallback summary remains part of the runtime path.
174
 
175
  ## Evaluation Meaning
176
 
src/pozify/slm/providers.py CHANGED
@@ -24,7 +24,7 @@ LLAMA_CPP_BASE_URL_ENV = "POZIFY_LLAMA_CPP_BASE_URL"
24
  LLAMA_CPP_TIMEOUT_ENV = "POZIFY_LLAMA_CPP_TIMEOUT"
25
 
26
  DEFAULT_PROVIDER = "hf_inference"
27
- DEFAULT_MODEL = "Qwen/Qwen3-14B"
28
  DEFAULT_LLAMA_CPP_BASE_URL = "http://127.0.0.1:8080"
29
  LOCAL_TRANSFORMERS_PROVIDER = "local_transformers"
30
  LOCAL_TRANSFORMERS_ALIASES = {LOCAL_TRANSFORMERS_PROVIDER, "local", "transformers"}
@@ -97,52 +97,83 @@ class HFInferenceCoachSummaryModel:
97
  self._client = InferenceClient(api_key=self.token)
98
  return self._client
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  def generate_summary(self, prompt: str) -> CoachSummaryGeneration:
101
  client = self._client_instance()
 
102
  if hasattr(client, "chat_completion"):
103
- response = client.chat_completion(
104
- model=self.model,
105
- messages=[
106
- {
107
- "role": "system",
108
- "content": "Return JSON only.",
109
- },
110
- {
111
- "role": "user",
112
- "content": prompt,
113
- },
114
- ],
115
- temperature=self.temperature,
116
- max_tokens=self.max_tokens,
117
- )
118
- choices = getattr(response, "choices", None) or []
119
- if not choices:
120
- raise RuntimeError("Hugging Face inference returned no choices")
121
- message = getattr(choices[0], "message", None)
122
- text = getattr(message, "content", None)
123
- if not isinstance(text, str) or not text.strip():
124
- raise RuntimeError("Hugging Face inference returned an empty message")
125
- return CoachSummaryGeneration(
126
- text=text,
127
- provider="hf_inference",
128
- model=self.model,
129
- )
130
 
131
  if hasattr(client, "text_generation"):
132
- text = client.text_generation(
133
- prompt,
134
- model=self.model,
135
- max_new_tokens=self.max_tokens,
136
- temperature=self.temperature,
137
- return_full_text=False,
138
- )
139
- if not isinstance(text, str) or not text.strip():
140
- raise RuntimeError("Hugging Face text generation returned empty output")
141
- return CoachSummaryGeneration(
142
- text=text,
143
- provider="hf_inference",
144
- model=self.model,
145
- )
146
 
147
  raise RuntimeError("No supported Hugging Face inference method is available")
148
 
 
24
  LLAMA_CPP_TIMEOUT_ENV = "POZIFY_LLAMA_CPP_TIMEOUT"
25
 
26
  DEFAULT_PROVIDER = "hf_inference"
27
+ DEFAULT_MODEL = "build-small-hackathon/pozify-coach-summary1"
28
  DEFAULT_LLAMA_CPP_BASE_URL = "http://127.0.0.1:8080"
29
  LOCAL_TRANSFORMERS_PROVIDER = "local_transformers"
30
  LOCAL_TRANSFORMERS_ALIASES = {LOCAL_TRANSFORMERS_PROVIDER, "local", "transformers"}
 
97
  self._client = InferenceClient(api_key=self.token)
98
  return self._client
99
 
100
+ def _generate_with_chat_completion(
101
+ self,
102
+ client: Any,
103
+ prompt: str,
104
+ ) -> CoachSummaryGeneration:
105
+ response = client.chat_completion(
106
+ model=self.model,
107
+ messages=[
108
+ {
109
+ "role": "system",
110
+ "content": "Return JSON only.",
111
+ },
112
+ {
113
+ "role": "user",
114
+ "content": prompt,
115
+ },
116
+ ],
117
+ temperature=self.temperature,
118
+ max_tokens=self.max_tokens,
119
+ )
120
+ choices = getattr(response, "choices", None) or []
121
+ if not choices:
122
+ raise RuntimeError("Hugging Face inference returned no choices")
123
+ message = getattr(choices[0], "message", None)
124
+ text = getattr(message, "content", None)
125
+ if not isinstance(text, str) or not text.strip():
126
+ raise RuntimeError("Hugging Face inference returned an empty message")
127
+ return CoachSummaryGeneration(
128
+ text=text,
129
+ provider="hf_inference",
130
+ model=self.model,
131
+ )
132
+
133
+ def _generate_with_text_generation(
134
+ self,
135
+ client: Any,
136
+ prompt: str,
137
+ ) -> CoachSummaryGeneration:
138
+ text = client.text_generation(
139
+ prompt,
140
+ model=self.model,
141
+ max_new_tokens=self.max_tokens,
142
+ temperature=self.temperature,
143
+ return_full_text=False,
144
+ )
145
+ if not isinstance(text, str) or not text.strip():
146
+ raise RuntimeError("Hugging Face text generation returned empty output")
147
+ return CoachSummaryGeneration(
148
+ text=text,
149
+ provider="hf_inference",
150
+ model=self.model,
151
+ )
152
+
153
  def generate_summary(self, prompt: str) -> CoachSummaryGeneration:
154
  client = self._client_instance()
155
+ chat_error: Exception | None = None
156
  if hasattr(client, "chat_completion"):
157
+ try:
158
+ return self._generate_with_chat_completion(client, prompt)
159
+ except Exception as exc:
160
+ chat_error = exc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  if hasattr(client, "text_generation"):
163
+ try:
164
+ return self._generate_with_text_generation(client, prompt)
165
+ except Exception as exc:
166
+ if chat_error is None:
167
+ raise
168
+ raise RuntimeError(
169
+ "Hugging Face chat completion and text generation both failed: "
170
+ f"chat={chat_error}; text_generation={exc}"
171
+ ) from exc
172
+
173
+ if chat_error is not None:
174
+ raise RuntimeError(
175
+ f"Hugging Face chat completion failed and text generation is unavailable: {chat_error}"
176
+ ) from chat_error
177
 
178
  raise RuntimeError("No supported Hugging Face inference method is available")
179
 
tests/test_slm_providers.py CHANGED
@@ -67,7 +67,43 @@ class SlmProviderTests(unittest.TestCase):
67
  model = get_coach_summary_model()
68
 
69
  self.assertIsInstance(model, HFInferenceCoachSummaryModel)
70
- self.assertEqual(model.model, "Qwen/Qwen3-14B")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  if __name__ == "__main__":
 
67
  model = get_coach_summary_model()
68
 
69
  self.assertIsInstance(model, HFInferenceCoachSummaryModel)
70
+ self.assertEqual(model.model, "build-small-hackathon/pozify-coach-summary1")
71
+
72
+ def test_hf_inference_falls_back_to_text_generation_for_non_chat_model(self) -> None:
73
+ class _TextGenerationClient:
74
+ def __init__(self) -> None:
75
+ self.text_generation_kwargs = None
76
+
77
+ def chat_completion(self, **_kwargs):
78
+ raise RuntimeError("not a chat model")
79
+
80
+ def text_generation(self, prompt: str, **kwargs):
81
+ self.text_generation_kwargs = {"prompt": prompt, **kwargs}
82
+ return '{"summary":"ok"}'
83
+
84
+ client = _TextGenerationClient()
85
+ model = HFInferenceCoachSummaryModel(
86
+ model="build-small-hackathon/pozify-coach-summary1",
87
+ max_tokens=123,
88
+ temperature=0.2,
89
+ )
90
+ model._client = client
91
+
92
+ generation = model.generate_summary("coach prompt")
93
+
94
+ self.assertEqual(generation.provider, "hf_inference")
95
+ self.assertEqual(generation.model, "build-small-hackathon/pozify-coach-summary1")
96
+ self.assertEqual(generation.text, '{"summary":"ok"}')
97
+ self.assertEqual(
98
+ client.text_generation_kwargs,
99
+ {
100
+ "prompt": "coach prompt",
101
+ "model": "build-small-hackathon/pozify-coach-summary1",
102
+ "max_new_tokens": 123,
103
+ "temperature": 0.2,
104
+ "return_full_text": False,
105
+ },
106
+ )
107
 
108
 
109
  if __name__ == "__main__":