deepctl-cmd-listen 0.0.2__tar.gz → 0.0.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-cmd-listen
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Listen (live speech-to-text) command for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -15,10 +15,12 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  Requires-Dist: deepctl-core>=0.1.10
18
+ Requires-Dist: deepctl-shared-utils>=0.1.10
18
19
  Requires-Dist: click>=8.0.0
19
20
  Requires-Dist: rich>=13.0.0
20
21
  Requires-Dist: pydantic>=2.0.0
21
22
  Requires-Dist: websockets>=11.0
23
+ Requires-Dist: deepgram-captions>=2.0.0
22
24
  Provides-Extra: mic
23
25
  Requires-Dist: sounddevice>=0.4.6; extra == "mic"
24
26
  Requires-Dist: numpy>=1.24.0; extra == "mic"
@@ -62,6 +64,7 @@ pipx run deepctl --help
62
64
  - `rich>=13.0.0`
63
65
  - `pydantic>=2.0.0`
64
66
  - `websockets>=11.0`
67
+ - `deepgram-captions>=1.0.0`
65
68
 
66
69
  ## License
67
70
 
@@ -37,6 +37,7 @@ pipx run deepctl --help
37
37
  - `rich>=13.0.0`
38
38
  - `pydantic>=2.0.0`
39
39
  - `websockets>=11.0`
40
+ - `deepgram-captions>=1.0.0`
40
41
 
41
42
  ## License
42
43
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepctl-cmd-listen"
7
- version = "0.0.2" # x-release-please-version
7
+ version = "0.0.4" # x-release-please-version
8
8
  description = "Listen (live speech-to-text) command for deepctl"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -22,10 +22,12 @@ keywords = ["deepgram", "cli", "stt", "live", "streaming", "listen"]
22
22
  requires-python = ">=3.10"
23
23
  dependencies = [
24
24
  "deepctl-core>=0.1.10",
25
+ "deepctl-shared-utils>=0.1.10",
25
26
  "click>=8.0.0",
26
27
  "rich>=13.0.0",
27
28
  "pydantic>=2.0.0",
28
29
  "websockets>=11.0",
30
+ "deepgram-captions>=2.0.0",
29
31
  ]
30
32
 
31
33
  [project.optional-dependencies]
@@ -0,0 +1,169 @@
1
+ """Caption generation for the listen command.
2
+
3
+ Strategy:
4
+ - Pre-recorded responses: pass full API result directly to deepgram-captions
5
+ library (DeepgramConverter), which handles utterances, chunking, and speaker
6
+ labels according to the WebVTT/SRT spec.
7
+
8
+ - Live streaming (real-time): StreamingCaptionWriter outputs one caption
9
+ block per final utterance as it arrives, using word-level timestamps from
10
+ the WebSocket Results message. Suitable for piping to a live player or
11
+ progressive file writes.
12
+
13
+ - Live streaming (end-of-stream batch): captions_from_words() accumulates
14
+ all word objects from the stream, builds a synthetic prerecorded response
15
+ dict, and passes it through the library — so chunking and formatting is
16
+ identical to prerecorded output.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ from deepgram_captions import DeepgramConverter
24
+ from deepgram_captions import srt as _srt
25
+ from deepgram_captions import webvtt as _webvtt
26
+
27
+ # ── Pre-recorded ───────────────────────────────────────────────────────────────
28
+
29
+
30
+ def captions_from_prerecorded(api_result: dict[str, Any], fmt: str) -> str:
31
+ """Generate SRT or WebVTT from a complete Deepgram prerecorded API response.
32
+
33
+ Args:
34
+ api_result: Raw dict from the Deepgram REST API (the full JSON response).
35
+ fmt: "webvtt" or "srt".
36
+
37
+ Returns:
38
+ Formatted caption string.
39
+ """
40
+ converter = DeepgramConverter(api_result, use_exception=False)
41
+ return str(_webvtt(converter) if fmt == "webvtt" else _srt(converter))
42
+
43
+
44
+ # ── Live streaming — batch (end-of-stream) ─────────────────────────────────────
45
+
46
+
47
+ def captions_from_words(words: list[dict[str, Any]], fmt: str) -> str:
48
+ """Generate SRT or WebVTT from accumulated live-stream word objects.
49
+
50
+ Builds a synthetic prerecorded response so the library's chunking and
51
+ formatting logic is applied identically to both prerecorded and live output.
52
+
53
+ Args:
54
+ words: List of word dicts collected from streaming Results messages.
55
+ Each must have at minimum: word, start (float), end (float).
56
+ speaker (int) is used for diarization if present.
57
+ fmt: "webvtt" or "srt".
58
+
59
+ Returns:
60
+ Formatted caption string, or a bare header if no words.
61
+ """
62
+ if not words:
63
+ return "WEBVTT\n" if fmt == "webvtt" else ""
64
+
65
+ transcript = " ".join(w.get("punctuated_word") or w.get("word", "") for w in words)
66
+ synthetic: dict[str, Any] = {
67
+ "results": {
68
+ "channels": [
69
+ {
70
+ "alternatives": [
71
+ {
72
+ "transcript": transcript,
73
+ "words": words,
74
+ }
75
+ ]
76
+ }
77
+ ]
78
+ }
79
+ }
80
+ return captions_from_prerecorded(synthetic, fmt)
81
+
82
+
83
+ # ── Live streaming — real-time ─────────────────────────────────────────────────
84
+
85
+
86
+ class StreamingCaptionWriter:
87
+ """Writes one caption block per final streaming utterance to stdout.
88
+
89
+ Designed for live display or real-time pipe targets. Each call to
90
+ write_entry() immediately prints a complete, properly-formatted caption
91
+ block. The accumulated words are available at the end for batch output
92
+ (e.g. saving a clean .vtt/.srt file via captions_from_words()).
93
+
94
+ Supports WebVTT <v Speaker N> voice tags and SRT [Speaker N] labels when
95
+ word-level speaker data is present.
96
+ """
97
+
98
+ def __init__(self, fmt: str) -> None:
99
+ self.fmt = fmt # "webvtt" | "srt"
100
+ self._count = 0
101
+ self._all_words: list[dict[str, Any]] = []
102
+
103
+ def print_header(self) -> None:
104
+ """Print the format header. Call once before any entries."""
105
+ if self.fmt == "webvtt":
106
+ print("WEBVTT")
107
+ print()
108
+
109
+ def write_entry(
110
+ self,
111
+ words: list[dict[str, Any]],
112
+ start: float,
113
+ end: float,
114
+ ) -> None:
115
+ """Print one caption block immediately.
116
+
117
+ Args:
118
+ words: Word objects for this utterance (need 'word'/'punctuated_word').
119
+ start: Caption start time in seconds.
120
+ end: Caption end time in seconds.
121
+ """
122
+ if not words:
123
+ return
124
+
125
+ self._count += 1
126
+ self._all_words.extend(words)
127
+
128
+ text = " ".join(w.get("punctuated_word") or w.get("word", "") for w in words)
129
+ first_speaker: int | None = words[0].get("speaker") if words else None
130
+
131
+ if self.fmt == "webvtt":
132
+ print(f"{_fmt_webvtt(start)} --> {_fmt_webvtt(end)}")
133
+ prefix = f"<v Speaker {first_speaker}>" if first_speaker is not None else ""
134
+ print(f"{prefix}{text}")
135
+ print()
136
+ else:
137
+ print(self._count)
138
+ print(f"{_fmt_srt(start)} --> {_fmt_srt(end)}")
139
+ if first_speaker is not None:
140
+ print(f"[Speaker {first_speaker}]")
141
+ print(text)
142
+ print()
143
+
144
+ @property
145
+ def accumulated_words(self) -> list[dict[str, Any]]:
146
+ """All words received so far — use with captions_from_words() for batch output."""
147
+ return self._all_words
148
+
149
+
150
+ # ── Timestamp helpers ──────────────────────────────────────────────────────────
151
+
152
+
153
+ def _fmt_webvtt(seconds: float) -> str:
154
+ """Format seconds as a WebVTT timestamp: HH:MM:SS.mmm"""
155
+ seconds = max(0.0, seconds)
156
+ h = int(seconds // 3600)
157
+ m = int((seconds % 3600) // 60)
158
+ s = seconds % 60
159
+ return f"{h:02d}:{m:02d}:{s:06.3f}"
160
+
161
+
162
+ def _fmt_srt(seconds: float) -> str:
163
+ """Format seconds as an SRT timestamp: HH:MM:SS,mmm"""
164
+ seconds = max(0.0, seconds)
165
+ h = int(seconds // 3600)
166
+ m = int((seconds % 3600) // 60)
167
+ s = int(seconds % 60)
168
+ ms = round((seconds % 1) * 1000)
169
+ return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"