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.
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/PKG-INFO +4 -1
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/README.md +1 -0
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/pyproject.toml +3 -1
- deepctl_cmd_listen-0.0.4/src/deepctl_cmd_listen/captions.py +169 -0
- deepctl_cmd_listen-0.0.4/src/deepctl_cmd_listen/command.py +1156 -0
- deepctl_cmd_listen-0.0.4/src/deepctl_cmd_listen/formatters.py +98 -0
- deepctl_cmd_listen-0.0.4/src/deepctl_cmd_listen/models.py +32 -0
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/src/deepctl_cmd_listen.egg-info/PKG-INFO +4 -1
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/src/deepctl_cmd_listen.egg-info/SOURCES.txt +2 -0
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/src/deepctl_cmd_listen.egg-info/requires.txt +2 -0
- deepctl_cmd_listen-0.0.2/src/deepctl_cmd_listen/command.py +0 -401
- deepctl_cmd_listen-0.0.2/src/deepctl_cmd_listen/models.py +0 -11
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/setup.cfg +0 -0
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/src/deepctl_cmd_listen/__init__.py +0 -0
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/src/deepctl_cmd_listen.egg-info/dependency_links.txt +0 -0
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/src/deepctl_cmd_listen.egg-info/entry_points.txt +0 -0
- {deepctl_cmd_listen-0.0.2 → deepctl_cmd_listen-0.0.4}/src/deepctl_cmd_listen.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepctl-cmd-listen
|
|
3
|
-
Version: 0.0.
|
|
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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deepctl-cmd-listen"
|
|
7
|
-
version = "0.0.
|
|
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}"
|