Skip to content

Commit 3097785

Browse files
author
safenestdev
committed
feat: add voice streaming, credits_used field, and documentation
1 parent 543d5ed commit 3097785

6 files changed

Lines changed: 460 additions & 2 deletions

File tree

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,62 @@ print(f"Risk: {report.risk_level}")
187187
print(f"Next Steps: {report.recommended_next_steps}")
188188
```
189189

190+
### Voice Streaming
191+
192+
Real-time voice streaming with live safety analysis over WebSocket. Requires `websockets`:
193+
194+
```bash
195+
pip install tuteliq[voice]
196+
```
197+
198+
```python
199+
from tuteliq import VoiceStreamConfig, VoiceStreamHandlers
200+
201+
session = client.voice_stream(
202+
config=VoiceStreamConfig(
203+
interval_seconds=10,
204+
analysis_types=["bullying", "unsafe"],
205+
),
206+
handlers=VoiceStreamHandlers(
207+
on_transcription=lambda e: print(f"Transcript: {e.text}"),
208+
on_alert=lambda e: print(f"Alert: {e.category} ({e.severity})"),
209+
),
210+
)
211+
212+
await session.connect()
213+
214+
# Send audio chunks as they arrive
215+
await session.send_audio(audio_bytes)
216+
217+
# End session and get summary
218+
summary = await session.end()
219+
print(f"Risk: {summary.overall_risk}")
220+
print(f"Score: {summary.overall_risk_score}")
221+
print(f"Full transcript: {summary.transcript}")
222+
```
223+
224+
---
225+
226+
## Credits Tracking
227+
228+
Each response includes the number of credits consumed:
229+
230+
```python
231+
result = await client.detect_bullying("Test message")
232+
print(f"Credits used: {result.credits_used}") # 1
233+
```
234+
235+
| Method | Credits | Notes |
236+
|--------|---------|-------|
237+
| `detect_bullying()` | 1 | Single text analysis |
238+
| `detect_unsafe()` | 1 | Single text analysis |
239+
| `detect_grooming()` | 1 per 10 msgs | `ceil(messages / 10)`, min 1 |
240+
| `analyze_emotions()` | 1 per 10 msgs | `ceil(messages / 10)`, min 1 |
241+
| `get_action_plan()` | 2 | Longer generation |
242+
| `generate_report()` | 3 | Structured output |
243+
| `analyze_voice()` | 5 | Transcription + analysis |
244+
| `analyze_image()` | 3 | Vision + OCR + analysis |
245+
190246
---
191247

192248
## Tracking Fields

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "tuteliq"
7-
version = "1.2.0"
7+
version = "2.2.0"
88
description = "Official Python SDK for Tuteliq - AI-powered child safety API"
99
readme = "README.md"
1010
license = "MIT"
@@ -41,6 +41,9 @@ dependencies = [
4141
]
4242

4343
[project.optional-dependencies]
44+
voice = [
45+
"websockets>=12.0",
46+
]
4447
dev = [
4548
"pytest>=7.0.0",
4649
"pytest-asyncio>=0.21.0",

tuteliq/__init__.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@
8181
AuditLogEntry,
8282
AuditLogsResult,
8383
)
84+
from tuteliq.voice_stream import (
85+
VoiceStreamConfig,
86+
VoiceStreamHandlers,
87+
VoiceStreamSession,
88+
VoiceReadyEvent,
89+
VoiceTranscriptionEvent,
90+
VoiceTranscriptionSegment,
91+
VoiceAlertEvent,
92+
VoiceSessionSummaryEvent,
93+
VoiceConfigUpdatedEvent,
94+
VoiceErrorEvent,
95+
)
8496
from tuteliq.errors import (
8597
TuteliqError,
8698
AuthenticationError,
@@ -94,7 +106,7 @@
94106
TierAccessError,
95107
)
96108

97-
__version__ = "2.1.0"
109+
__version__ = "2.2.0"
98110
__all__ = [
99111
# Client
100112
"Tuteliq",
@@ -166,6 +178,17 @@
166178
"RectifyDataResult",
167179
"AuditLogEntry",
168180
"AuditLogsResult",
181+
# Voice streaming types
182+
"VoiceStreamConfig",
183+
"VoiceStreamHandlers",
184+
"VoiceStreamSession",
185+
"VoiceReadyEvent",
186+
"VoiceTranscriptionEvent",
187+
"VoiceTranscriptionSegment",
188+
"VoiceAlertEvent",
189+
"VoiceSessionSummaryEvent",
190+
"VoiceConfigUpdatedEvent",
191+
"VoiceErrorEvent",
169192
# Errors
170193
"TuteliqError",
171194
"AuthenticationError",

tuteliq/client.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,52 @@ async def analyze_image(
824824
data = await self._multipart_request("/api/v1/safety/image", fields, files)
825825
return ImageAnalysisResult.from_dict(data)
826826

827+
# =========================================================================
828+
# Voice Streaming
829+
# =========================================================================
830+
831+
def voice_stream(
832+
self,
833+
config: Optional["VoiceStreamConfig"] = None,
834+
handlers: Optional["VoiceStreamHandlers"] = None,
835+
) -> "VoiceStreamSession":
836+
"""Create a voice streaming session over WebSocket.
837+
838+
Requires the ``websockets`` package::
839+
840+
pip install websockets
841+
842+
Args:
843+
config: Optional session configuration (interval, analysis types).
844+
handlers: Optional event handler callbacks.
845+
846+
Returns:
847+
A VoiceStreamSession. Call ``await session.connect()`` to start.
848+
849+
Example::
850+
851+
session = client.voice_stream(
852+
config=VoiceStreamConfig(
853+
interval_seconds=10,
854+
analysis_types=["bullying", "unsafe"],
855+
),
856+
handlers=VoiceStreamHandlers(
857+
on_transcription=lambda e: print("Transcript:", e.text),
858+
on_alert=lambda e: print("Alert:", e.category, e.severity),
859+
),
860+
)
861+
await session.connect()
862+
await session.send_audio(audio_bytes)
863+
summary = await session.end()
864+
"""
865+
from tuteliq.voice_stream import VoiceStreamSession
866+
867+
return VoiceStreamSession(
868+
api_key=self._api_key,
869+
config=config,
870+
handlers=handlers,
871+
)
872+
827873
# =========================================================================
828874
# Webhooks
829875
# =========================================================================

tuteliq/models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class BullyingResult:
123123
rationale: str
124124
recommended_action: str
125125
risk_score: float
126+
credits_used: Optional[int] = None
126127
external_id: Optional[str] = None
127128
metadata: Optional[dict[str, Any]] = None
128129

@@ -137,6 +138,7 @@ def from_dict(cls, data: dict[str, Any]) -> "BullyingResult":
137138
rationale=data["rationale"],
138139
recommended_action=data["recommended_action"],
139140
risk_score=data["risk_score"],
141+
credits_used=data.get("credits_used"),
140142
external_id=data.get("external_id"),
141143
metadata=data.get("metadata"),
142144
)
@@ -177,6 +179,7 @@ class GroomingResult:
177179
rationale: str
178180
risk_score: float
179181
recommended_action: str
182+
credits_used: Optional[int] = None
180183
external_id: Optional[str] = None
181184
metadata: Optional[dict[str, Any]] = None
182185

@@ -190,6 +193,7 @@ def from_dict(cls, data: dict[str, Any]) -> "GroomingResult":
190193
rationale=data["rationale"],
191194
risk_score=data["risk_score"],
192195
recommended_action=data["recommended_action"],
196+
credits_used=data.get("credits_used"),
193197
external_id=data.get("external_id"),
194198
metadata=data.get("metadata"),
195199
)
@@ -221,6 +225,7 @@ class UnsafeResult:
221225
risk_score: float
222226
rationale: str
223227
recommended_action: str
228+
credits_used: Optional[int] = None
224229
external_id: Optional[str] = None
225230
metadata: Optional[dict[str, Any]] = None
226231

@@ -235,6 +240,7 @@ def from_dict(cls, data: dict[str, Any]) -> "UnsafeResult":
235240
risk_score=data["risk_score"],
236241
rationale=data["rationale"],
237242
recommended_action=data["recommended_action"],
243+
credits_used=data.get("credits_used"),
238244
external_id=data.get("external_id"),
239245
metadata=data.get("metadata"),
240246
)
@@ -266,6 +272,7 @@ class AnalyzeResult:
266272
bullying: Optional[BullyingResult] = None
267273
unsafe: Optional[UnsafeResult] = None
268274
recommended_action: str = "none"
275+
credits_used: Optional[int] = None
269276
external_id: Optional[str] = None
270277
metadata: Optional[dict[str, Any]] = None
271278

@@ -304,6 +311,7 @@ class EmotionsResult:
304311
trend: EmotionTrend
305312
summary: str
306313
recommended_followup: str
314+
credits_used: Optional[int] = None
307315
external_id: Optional[str] = None
308316
metadata: Optional[dict[str, Any]] = None
309317

@@ -316,6 +324,7 @@ def from_dict(cls, data: dict[str, Any]) -> "EmotionsResult":
316324
trend=EmotionTrend(data["trend"]),
317325
summary=data["summary"],
318326
recommended_followup=data["recommended_followup"],
327+
credits_used=data.get("credits_used"),
319328
external_id=data.get("external_id"),
320329
metadata=data.get("metadata"),
321330
)
@@ -346,6 +355,7 @@ class ActionPlanResult:
346355
steps: list[str]
347356
tone: str
348357
reading_level: Optional[str] = None
358+
credits_used: Optional[int] = None
349359
external_id: Optional[str] = None
350360
metadata: Optional[dict[str, Any]] = None
351361

@@ -357,6 +367,7 @@ def from_dict(cls, data: dict[str, Any]) -> "ActionPlanResult":
357367
steps=data["steps"],
358368
tone=data["tone"],
359369
reading_level=data.get("approx_reading_level"),
370+
credits_used=data.get("credits_used"),
360371
external_id=data.get("external_id"),
361372
metadata=data.get("metadata"),
362373
)
@@ -397,6 +408,7 @@ class ReportResult:
397408
risk_level: RiskLevel
398409
categories: list[str]
399410
recommended_next_steps: list[str]
411+
credits_used: Optional[int] = None
400412
external_id: Optional[str] = None
401413
metadata: Optional[dict[str, Any]] = None
402414

@@ -408,6 +420,7 @@ def from_dict(cls, data: dict[str, Any]) -> "ReportResult":
408420
risk_level=RiskLevel(data["risk_level"]),
409421
categories=data["categories"],
410422
recommended_next_steps=data["recommended_next_steps"],
423+
credits_used=data.get("credits_used"),
411424
external_id=data.get("external_id"),
412425
metadata=data.get("metadata"),
413426
)
@@ -802,6 +815,7 @@ class VoiceAnalysisResult:
802815
analysis: Optional[dict[str, Any]] = None
803816
overall_risk_score: Optional[float] = None
804817
overall_severity: Optional[str] = None
818+
credits_used: Optional[int] = None
805819
external_id: Optional[str] = None
806820
customer_id: Optional[str] = None
807821
metadata: Optional[dict[str, Any]] = None
@@ -818,6 +832,7 @@ def from_dict(cls, data: dict[str, Any]) -> "VoiceAnalysisResult":
818832
analysis=data.get("analysis"),
819833
overall_risk_score=data.get("overall_risk_score"),
820834
overall_severity=data.get("overall_severity"),
835+
credits_used=data.get("credits_used"),
821836
external_id=data.get("external_id"),
822837
customer_id=data.get("customer_id"),
823838
metadata=data.get("metadata"),
@@ -864,6 +879,7 @@ class ImageAnalysisResult:
864879
text_analysis: Optional[dict[str, Any]] = None
865880
overall_risk_score: Optional[float] = None
866881
overall_severity: Optional[str] = None
882+
credits_used: Optional[int] = None
867883
external_id: Optional[str] = None
868884
customer_id: Optional[str] = None
869885
metadata: Optional[dict[str, Any]] = None
@@ -880,6 +896,7 @@ def from_dict(cls, data: dict[str, Any]) -> "ImageAnalysisResult":
880896
text_analysis=data.get("text_analysis"),
881897
overall_risk_score=data.get("overall_risk_score"),
882898
overall_severity=data.get("overall_severity"),
899+
credits_used=data.get("credits_used"),
883900
external_id=data.get("external_id"),
884901
customer_id=data.get("customer_id"),
885902
metadata=data.get("metadata"),

0 commit comments

Comments
 (0)