-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtextual_ui.py
More file actions
217 lines (182 loc) · 6.67 KB
/
textual_ui.py
File metadata and controls
217 lines (182 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import asyncio
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, RichLog, Static
from textual.containers import Container, Vertical, ScrollableContainer
from textual.reactive import reactive
from stream_audio_file import stream_audio
from print_transcript import DisplayConfig, StreamingFormatter
class TranscriptionApp(App):
"""A Textual app for real-time audio transcription."""
CSS = """
Screen {
align: center middle;
}
#main-container {
width: 100%;
height: 100%;
border: solid $primary;
}
#info-panel {
height: auto;
background: $surface;
padding: 1;
border-bottom: solid $primary;
}
#transcript-container {
height: 1fr;
border: solid $accent;
padding: 1;
}
#status {
height: 3;
background: $surface;
padding: 1;
border-top: solid $primary;
}
.info-line {
margin: 0 1;
}
"""
TITLE = "Real-Time Transcription"
status_text = reactive("Ready")
def __init__(
self,
audio_file: str | None,
url: str,
encoding: str | None,
channels: int | None,
sample_rate: int | None,
verbose: int,
config: DisplayConfig,
play_audio: bool = False,
):
super().__init__()
self.audio_file = audio_file
self.url = url
self.encoding = encoding
self.channels = channels
self.sample_rate = sample_rate
self.verbose = verbose
self.config = config
self.play_audio = play_audio
self.formatter: StreamingFormatter | None = None
self.stream_task: asyncio.Task | None = None
def compose(self) -> ComposeResult:
"""Create child widgets for the app."""
yield Header()
with Vertical(id="main-container"):
with Container(id="info-panel"):
yield Static("🎤 Audio Source:", classes="info-line")
yield Static(
f" {'Microphone (live)' if self.audio_file is None else self.audio_file}",
classes="info-line",
)
if self.config.print_speakers:
yield Static("👤 Showing speakers", classes="info-line")
if self.config.print_channels:
yield Static("📢 Showing channels", classes="info-line")
if self.config.print_interim:
yield Static("⏱️ Showing interim results", classes="info-line")
if self.play_audio:
yield Static("🔊 Playing audio through speaker", classes="info-line")
with ScrollableContainer(id="transcript-container"):
yield RichLog(id="transcript", wrap=True, highlight=True, markup=True)
with Container(id="status"):
yield Static(self.status_text, id="status-display")
yield Footer()
async def on_mount(self) -> None:
"""Start streaming when the app mounts."""
self.transcript_log = self.query_one("#transcript", RichLog)
self.status_display = self.query_one("#status-display", Static)
# Initialize formatter with Rich markup enabled
self.formatter = StreamingFormatter(self.config, use_rich_markup=True)
# Start streaming
self.update_status("Connecting to Deepgram...")
self.stream_task = asyncio.create_task(self.start_streaming())
def update_status(self, text: str):
"""Update the status display."""
self.status_text = text
self.status_display.update(text)
async def message_callback(self, message: dict):
"""Called for each message received from Deepgram."""
if self.formatter is None:
return
# Format the message (with Rich markup if colorize is enabled)
formatted = self.formatter.format_message(message)
# Update status based on message type
msg_type = message.get("type")
if msg_type == "OpenStream" or msg_type == "Connected":
self.update_status("🟢 Connected - Streaming audio...")
elif msg_type == "Results":
if message.get("speech_final"):
self.update_status("✅ Processing audio...")
else:
self.update_status("🎙️ Listening...")
elif msg_type == "Metadata":
self.update_status("✅ Transcription complete")
# Display the formatted message (Rich markup is automatically rendered)
if formatted:
self.transcript_log.write(formatted)
async def start_streaming(self):
"""Start the audio streaming process."""
try:
# Use a temporary output file (won't be used in UI mode)
temp_output = "/tmp/transcript_temp.json"
await stream_audio(
output_filename=temp_output,
filename=self.audio_file,
url=self.url,
encoding=self.encoding,
channels=self.channels,
sample_rate=self.sample_rate,
verbose=self.verbose,
message_callback=self.message_callback,
play_audio=self.play_audio,
)
self.update_status("✅ Complete - Press Ctrl+C to exit")
except Exception as e:
self.update_status(f"❌ Error: {str(e)}")
self.transcript_log.write(f"\n[bold red]Error:[/bold red] {str(e)}")
async def action_quit(self):
"""Quit the application."""
if self.stream_task:
self.stream_task.cancel()
await super().action_quit()
async def launch_ui(
audio_file: str | None,
url: str,
encoding: str | None,
channels: int | None,
sample_rate: int | None,
verbose: int,
print_channels: bool = False,
print_speakers: bool = True,
print_interim: bool = True,
print_received: bool = True,
print_latency: bool = True,
print_entities: bool = False,
colorize: bool = True,
play_audio: bool = False,
):
"""Launch the Textual UI with the given configuration."""
config = DisplayConfig(
print_channels=print_channels,
print_speakers=print_speakers,
print_interim=print_interim,
print_received=print_received,
print_latency=print_latency,
print_entities=print_entities,
colorize=colorize,
only_transcript=False,
)
app = TranscriptionApp(
audio_file=audio_file,
url=url,
encoding=encoding,
channels=channels,
sample_rate=sample_rate,
verbose=verbose,
config=config,
play_audio=play_audio,
)
await app.run_async()