finchvox 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,63 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Traces - Finchvox</title>
7
+ <link rel="stylesheet" href="/css/app.css">
8
+ <script src="/js/time-utils.js"></script>
9
+ <script defer src="/lib/alpine.min.js"></script>
10
+ </head>
11
+ <body x-data="tracesListApp()" x-init="init()">
12
+ <div class="page-header">
13
+ <img src="/images/finchvox-logo.png" alt="FinchVox Logo" class="logo">
14
+ <span class="secondary-text">Traces</span> <span>/</span> <span class="trace-count" x-text="`${traces.length} traces`"></span>
15
+ </div>
16
+ <main class="traces-list-view">
17
+ <div x-show="traces.length === 0" class="error" style="margin:100px 2rem 1rem 2rem;">
18
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="error-icon">
19
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
20
+ </svg>
21
+ <span>
22
+ Is this thing on? No traces have been found in the data directory. See the <a href="https://github.com/itsderek23/finchvox/README.md#troubleshooting" target="_blank">troubleshooting guide</a>.
23
+ <div x-show="dataDir && dataDir.length > 0" class="secondary-text" style="display:block; margin-top:0.5em;">
24
+ Data directory: <span class="mono" x-text="dataDir"></span>
25
+ </div>
26
+ </span>
27
+ </div>
28
+
29
+ <table class="traces-table" x-show="traces.length > 0">
30
+ <thead>
31
+ <tr>
32
+ <th>Service Name</th>
33
+ <th>Trace ID</th>
34
+ <th>Start Time</th>
35
+ <th>End Time</th>
36
+ <th>Duration</th>
37
+ <th>Spans</th>
38
+ <th></th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ <template x-for="trace in traces" :key="trace.trace_id">
43
+ <tr class="trace-row">
44
+ <td x-text="trace.service_name || '-'"></td>
45
+ <td class="trace-id">
46
+ <a class="mono" :href="`/traces/${trace.trace_id}`" x-text="trace.trace_id"></a>
47
+ </td>
48
+ <td x-text="formatDate(trace.start_time)"></td>
49
+ <td x-text="formatDate(trace.end_time)"></td>
50
+ <td x-text="formatDuration(trace.duration_ms)"></td>
51
+ <td x-text="trace.span_count"></td>
52
+ <td>
53
+
54
+ </td>
55
+ </tr>
56
+ </template>
57
+ </tbody>
58
+ </table>
59
+ </main>
60
+
61
+ <script src="/js/traces_list.js"></script>
62
+ </body>
63
+ </html>
finchvox/ui_routes.py ADDED
@@ -0,0 +1,362 @@
1
+ """
2
+ UI routes for FinchVox Trace Viewer.
3
+
4
+ Serves the web UI and provides REST APIs for trace data.
5
+ """
6
+
7
+ import json
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
12
+ from fastapi.responses import FileResponse, JSONResponse
13
+ from fastapi.staticfiles import StaticFiles
14
+ from loguru import logger
15
+
16
+ from finchvox.audio_utils import find_chunks, combine_chunks
17
+ from finchvox.trace import Trace
18
+ from finchvox.collector.config import (
19
+ get_traces_base_dir,
20
+ get_trace_dir,
21
+ get_trace_logs_dir,
22
+ get_trace_audio_dir,
23
+ get_trace_exceptions_dir,
24
+ get_default_data_dir
25
+ )
26
+
27
+
28
+ # UI directory - check package location first (when installed via pip/uv),
29
+ # then fall back to development location
30
+ UI_DIR = Path(__file__).parent / "ui"
31
+ if not UI_DIR.exists():
32
+ # Fall back to development location (ui/ at project root)
33
+ PROJECT_ROOT = Path(__file__).parent.parent.parent
34
+ UI_DIR = PROJECT_ROOT / "ui"
35
+
36
+
37
+ def register_ui_routes(app: FastAPI, data_dir: Path = None):
38
+ """
39
+ Register UI routes and static file serving on an existing FastAPI app.
40
+
41
+ Args:
42
+ app: Existing FastAPI application to register routes on
43
+ data_dir: Base data directory (default: ~/.finchvox)
44
+ """
45
+ if data_dir is None:
46
+ data_dir = get_default_data_dir()
47
+
48
+ traces_base_dir = get_traces_base_dir(data_dir)
49
+ # Mount static files FIRST (must be before route handlers)
50
+ app.mount("/css", StaticFiles(directory=str(UI_DIR / "css")), name="css")
51
+ app.mount("/js", StaticFiles(directory=str(UI_DIR / "js")), name="js")
52
+ app.mount("/lib", StaticFiles(directory=str(UI_DIR / "lib")), name="lib")
53
+ app.mount("/images", StaticFiles(directory=str(UI_DIR / "images")), name="images")
54
+
55
+ @app.get("/favicon.ico")
56
+ async def favicon():
57
+ """Serve the favicon."""
58
+ return FileResponse(str(UI_DIR / "images" / "favicon.ico"))
59
+
60
+ @app.get("/")
61
+ async def index():
62
+ """Serve the traces list page."""
63
+ return FileResponse(str(UI_DIR / "traces_list.html"))
64
+
65
+ @app.get("/traces/{trace_id}")
66
+ async def trace_detail_page(trace_id: str):
67
+ """Serve the trace detail page."""
68
+ return FileResponse(str(UI_DIR / "trace_detail.html"))
69
+
70
+ @app.get("/api/traces")
71
+ async def list_traces() -> JSONResponse:
72
+ """
73
+ List all available traces.
74
+
75
+ Returns:
76
+ List of trace metadata including trace_id, service_name, span_count,
77
+ start_time, end_time, and duration_ms
78
+ """
79
+ traces = []
80
+
81
+ if not traces_base_dir.exists():
82
+ return JSONResponse({"traces": [], "data_dir": str(traces_base_dir)})
83
+
84
+ # Scan trace directories (each directory is a trace)
85
+ for trace_dir in traces_base_dir.iterdir():
86
+ if not trace_dir.is_dir():
87
+ continue
88
+
89
+ trace_id = trace_dir.name
90
+ trace_file = trace_dir / f"trace_{trace_id}.jsonl"
91
+
92
+ if not trace_file.exists():
93
+ continue
94
+
95
+ try:
96
+ # Use Trace class to load metadata
97
+ trace = Trace(trace_file)
98
+ traces.append(trace.to_dict())
99
+ except Exception as e:
100
+ print(f"Error reading trace file {trace_file}: {e}")
101
+ continue
102
+
103
+ # Sort by start_time descending (most recent first)
104
+ traces.sort(key=lambda t: t.get("start_time") or 0, reverse=True)
105
+
106
+ return JSONResponse({"traces": traces, "data_dir": str(traces_base_dir)})
107
+
108
+ @app.get("/api/trace/{trace_id}")
109
+ async def get_trace(trace_id: str) -> JSONResponse:
110
+ """
111
+ Get all spans for a specific trace.
112
+
113
+ Args:
114
+ trace_id: The trace ID
115
+
116
+ Returns:
117
+ JSON with spans array
118
+ """
119
+ trace_dir = get_trace_dir(data_dir, trace_id)
120
+ trace_file = trace_dir / f"trace_{trace_id}.jsonl"
121
+
122
+ if not trace_file.exists():
123
+ raise HTTPException(status_code=404, detail=f"Trace {trace_id} not found")
124
+
125
+ spans = []
126
+ last_span_time = None
127
+ try:
128
+ with open(trace_file, 'r') as f:
129
+ for line in f:
130
+ if line.strip():
131
+ span = json.loads(line)
132
+ spans.append(span)
133
+ # Track the last span's end time for abandonment detection
134
+ if "end_time_unix_nano" in span:
135
+ last_span_time = span["end_time_unix_nano"]
136
+ except Exception as e:
137
+ raise HTTPException(status_code=500, detail=f"Error reading trace: {str(e)}")
138
+
139
+ return JSONResponse({
140
+ "spans": spans,
141
+ "last_span_time": last_span_time
142
+ })
143
+
144
+ @app.get("/api/trace/{trace_id}/raw")
145
+ async def get_trace_raw(trace_id: str) -> JSONResponse:
146
+ """
147
+ Get raw trace data as a formatted JSON array.
148
+
149
+ Reads the JSONL file and returns all spans as a single JSON array
150
+ with indentation for easy reading in browser.
151
+
152
+ Args:
153
+ trace_id: The trace ID
154
+
155
+ Returns:
156
+ JSON array of all spans with formatting
157
+ """
158
+ trace_dir = get_trace_dir(data_dir, trace_id)
159
+ trace_file = trace_dir / f"trace_{trace_id}.jsonl"
160
+
161
+ if not trace_file.exists():
162
+ raise HTTPException(status_code=404, detail=f"Trace {trace_id} not found")
163
+
164
+ spans = []
165
+ try:
166
+ with open(trace_file, 'r') as f:
167
+ for line in f:
168
+ if line.strip():
169
+ span = json.loads(line)
170
+ spans.append(span)
171
+ except Exception as e:
172
+ raise HTTPException(status_code=500, detail=f"Error reading trace: {str(e)}")
173
+
174
+ # Return as formatted JSON with indentation
175
+ return JSONResponse(
176
+ content=spans,
177
+ media_type="application/json",
178
+ headers={
179
+ "Content-Type": "application/json; charset=utf-8"
180
+ }
181
+ )
182
+
183
+ @app.get("/api/logs/{trace_id}")
184
+ async def get_logs(trace_id: str) -> JSONResponse:
185
+ """
186
+ Get all logs for a specific trace.
187
+
188
+ Args:
189
+ trace_id: The trace ID
190
+
191
+ Returns:
192
+ JSON with logs array
193
+ """
194
+ logs_dir = get_trace_logs_dir(data_dir, trace_id)
195
+ log_file = logs_dir / f"log_{trace_id}.jsonl"
196
+
197
+ if not log_file.exists():
198
+ return JSONResponse({"logs": []})
199
+
200
+ logs = []
201
+ try:
202
+ with open(log_file, 'r') as f:
203
+ for line in f:
204
+ if line.strip():
205
+ log = json.loads(line)
206
+ logs.append(log)
207
+ except Exception as e:
208
+ raise HTTPException(status_code=500, detail=f"Error reading logs: {str(e)}")
209
+
210
+ return JSONResponse({"logs": logs})
211
+
212
+ @app.get("/api/exceptions/{trace_id}")
213
+ async def get_exceptions(trace_id: str) -> JSONResponse:
214
+ """
215
+ Get all exceptions for a specific trace.
216
+
217
+ Args:
218
+ trace_id: The trace ID
219
+
220
+ Returns:
221
+ JSON with exceptions array
222
+ """
223
+ exceptions_dir = get_trace_exceptions_dir(data_dir, trace_id)
224
+ exceptions_file = exceptions_dir / f"exceptions_{trace_id}.jsonl"
225
+
226
+ if not exceptions_file.exists():
227
+ return JSONResponse({"exceptions": []})
228
+
229
+ exceptions = []
230
+ try:
231
+ with open(exceptions_file, 'r') as f:
232
+ for line in f:
233
+ if line.strip():
234
+ exception = json.loads(line)
235
+ exceptions.append(exception)
236
+ except Exception as e:
237
+ raise HTTPException(status_code=500, detail=f"Error reading exceptions: {str(e)}")
238
+
239
+ return JSONResponse({"exceptions": exceptions})
240
+
241
+ @app.get("/api/audio/{trace_id}")
242
+ async def get_audio(trace_id: str, background_tasks: BackgroundTasks):
243
+ """
244
+ Get combined audio for a specific trace.
245
+
246
+ Combines all audio chunks into a single WAV file on-demand.
247
+
248
+ Args:
249
+ trace_id: The trace ID
250
+ background_tasks: FastAPI background tasks for cleanup
251
+
252
+ Returns:
253
+ Combined WAV file with all audio chunks
254
+ """
255
+ audio_dir = get_trace_audio_dir(data_dir, trace_id)
256
+
257
+ if not audio_dir.exists():
258
+ raise HTTPException(status_code=404, detail=f"Audio for trace {trace_id} not found")
259
+
260
+ # Find all chunks for this trace
261
+ logger.info(f"Finding audio chunks for trace {trace_id}")
262
+ # Pass the parent of audio_dir (trace dir) since find_chunks expects base and appends trace_id
263
+ chunks = find_chunks(get_traces_base_dir(data_dir), trace_id)
264
+
265
+ if not chunks:
266
+ raise HTTPException(status_code=404, detail=f"No audio chunks found for trace {trace_id}")
267
+
268
+ logger.info(f"Found {len(chunks)} chunks for trace {trace_id}")
269
+
270
+ # Generate combined WAV in temp file
271
+ with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp:
272
+ tmp_path = Path(tmp.name)
273
+
274
+ combine_chunks(chunks, tmp_path)
275
+
276
+ # Schedule cleanup after response sent
277
+ background_tasks.add_task(tmp_path.unlink)
278
+
279
+ return FileResponse(
280
+ str(tmp_path),
281
+ media_type="audio/wav",
282
+ headers={
283
+ "Content-Disposition": f"inline; filename=trace_{trace_id}.wav"
284
+ }
285
+ )
286
+
287
+ @app.get("/api/audio/{trace_id}/download")
288
+ async def download_audio(trace_id: str, background_tasks: BackgroundTasks):
289
+ """
290
+ Download combined audio for a specific trace.
291
+
292
+ Same as get_audio but with Content-Disposition: attachment to trigger download.
293
+
294
+ Args:
295
+ trace_id: The trace ID
296
+ background_tasks: FastAPI background tasks for cleanup
297
+
298
+ Returns:
299
+ Combined WAV file with all audio chunks (as download)
300
+ """
301
+ audio_dir = get_trace_audio_dir(data_dir, trace_id)
302
+
303
+ if not audio_dir.exists():
304
+ raise HTTPException(status_code=404, detail=f"Audio for trace {trace_id} not found")
305
+
306
+ # Find all chunks for this trace
307
+ logger.info(f"Finding audio chunks for trace {trace_id}")
308
+ chunks = find_chunks(get_traces_base_dir(data_dir), trace_id)
309
+
310
+ if not chunks:
311
+ raise HTTPException(status_code=404, detail=f"No audio chunks found for trace {trace_id}")
312
+
313
+ logger.info(f"Found {len(chunks)} chunks for trace {trace_id}")
314
+
315
+ # Generate combined WAV in temp file
316
+ with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp:
317
+ tmp_path = Path(tmp.name)
318
+
319
+ combine_chunks(chunks, tmp_path)
320
+
321
+ # Schedule cleanup after response sent
322
+ background_tasks.add_task(tmp_path.unlink)
323
+
324
+ return FileResponse(
325
+ str(tmp_path),
326
+ media_type="audio/wav",
327
+ headers={
328
+ "Content-Disposition": f"attachment; filename=trace_{trace_id}.wav"
329
+ }
330
+ )
331
+
332
+ @app.get("/api/audio/{trace_id}/status")
333
+ async def get_audio_status(trace_id: str) -> JSONResponse:
334
+ """
335
+ Get audio metadata without combining chunks.
336
+
337
+ Returns chunk count and last modification time for detecting
338
+ when new audio has been added to a trace.
339
+
340
+ Args:
341
+ trace_id: The trace ID
342
+
343
+ Returns:
344
+ JSON with chunk_count and last_modified timestamp
345
+ """
346
+ audio_dir = get_trace_audio_dir(data_dir, trace_id)
347
+
348
+ if not audio_dir.exists():
349
+ return JSONResponse({"chunk_count": 0, "last_modified": None})
350
+
351
+ # Find all chunks for this trace
352
+ chunks = find_chunks(get_traces_base_dir(data_dir), trace_id)
353
+
354
+ last_modified = None
355
+ if chunks:
356
+ # Get most recent modification time
357
+ last_modified = max(Path(c).stat().st_mtime for c in chunks)
358
+
359
+ return JSONResponse({
360
+ "chunk_count": len(chunks),
361
+ "last_modified": last_modified
362
+ })
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: finchvox
3
+ Version: 0.0.1
4
+ Summary: Voice AI observability dev tool for Pipecat
5
+ License: Finchvox Source Available License
6
+
7
+ Copyright (c) 2026 Finchvox
8
+
9
+ Permission is granted, free of charge, to any person obtaining a copy of this
10
+ software and associated documentation files (the "Software"), to use, copy,
11
+ modify, and distribute the Software for internal or private use.
12
+
13
+ You may:
14
+ - Run the Software locally or within your organization
15
+ - Modify the Software for your own use
16
+ - Deploy the Software within your own infrastructure
17
+ - Deploy the Software on behalf of a customer, provided the customer operates
18
+ the Software themselves
19
+
20
+ You may NOT:
21
+ - Offer the Software as a hosted service, managed service, or SaaS
22
+ - Provide access to the Software to third parties as a service
23
+ - Resell, rebrand, or commercially exploit the Software as a service
24
+
25
+ This license does not grant you the right to use the Finchvox name, logo, or
26
+ trademarks.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
29
+ License-File: LICENSE
30
+ Requires-Python: >=3.10
31
+ Requires-Dist: aiofiles>=24.1.0
32
+ Requires-Dist: aiohttp>=3.9.0
33
+ Requires-Dist: fastapi>=0.115.0
34
+ Requires-Dist: grpcio>=1.60.0
35
+ Requires-Dist: loguru>=0.7.0
36
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.27.0
37
+ Requires-Dist: opentelemetry-proto>=1.27.0
38
+ Requires-Dist: protobuf<6.0.0,>=4.25.0
39
+ Requires-Dist: python-multipart>=0.0.9
40
+ Requires-Dist: ruff>=0.14.10
41
+ Requires-Dist: uvicorn[standard]>=0.32.0
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
44
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # <img src="ui/images/finchvox-logo.png" height=24 /> Finchvox - elevated debuggability for Voice AI apps
48
+
49
+ Do your eyes bleed like a Vecna victim watching Pipecat logs fly by? Do OpenTelemetry traces look impressive … yet explain nothing? If so, meet Finchvox, a local debuggability tool purpose-built for Voice AI apps.
50
+
51
+ Finchvox unifies conversation audio and traces in a single UI, highlighting voice-specific problems like interruptions and high user <-> bot latency. Good luck convincing DataDog to add that!
52
+
53
+ <a href="./docs/demo.gif"><img src="./docs/screenshot.png" /></a>
54
+
55
+ ## Table of Contents
56
+
57
+ - [Prerequisites](#prerequisites)
58
+ - [Installation](#installation)
59
+ - [Usage](#usage---finchvox-server)
60
+ - [Setup](#setup)
61
+ - [✨ AI Install](#-let-your-ai-coding-agent-do-the-work)
62
+ - [Step 1 – Enable Tracing in Your Pipecat Application](#step-1---enable-tracing-in-your-pipecat-application)
63
+ - [Step 2 – Enable Audio Recording](#step-2---enable-audio-recording)
64
+ - [Troubleshooting](#troubleshooting)
65
+
66
+ ## Prerequisites
67
+
68
+ - Python 3.10 or higher
69
+ - A Pipecat Voice AI application
70
+
71
+ ## Installation
72
+
73
+ ```bash
74
+ # uv
75
+ uv add finchvox "pipecat-ai[tracing]"
76
+
77
+ # Or with pip
78
+ pip install finchvox "pipecat-ai[tracing]"
79
+ ```
80
+
81
+ ## Usage - Finchvox server
82
+
83
+ ```bash
84
+ uv run finchvox start
85
+ ```
86
+
87
+ For the list of available options, run:
88
+
89
+ ```bash
90
+ uv run finchvox --help
91
+ ```
92
+
93
+ ## Setup
94
+
95
+ ### ✨ Let your AI coding agent do the work!
96
+
97
+ Try this starter prompt:
98
+
99
+ ```
100
+ Follow the "Setup" instructions at https://github.com/itsderek23/finchvox/blob/main/README.md to setup tracing and audio recording for finchvox.
101
+ ```
102
+
103
+ ### Step 1 - Enable Tracing in Your Pipecat Application
104
+
105
+ ```python
106
+ import os
107
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
108
+ from pipecat.utils.tracing.setup import setup_tracing
109
+ ```
110
+
111
+ ```python
112
+ # Step 1: Initialize OpenTelemetry with your chosen exporter
113
+ exporter = OTLPSpanExporter(
114
+ endpoint="http://localhost:4317",
115
+ insecure=True,
116
+ )
117
+
118
+ setup_tracing(
119
+ service_name="my-voice-app",
120
+ exporter=exporter,
121
+ )
122
+
123
+ # Step 2: Enable tracing in your PipelineTask
124
+ task = PipelineTask(
125
+ pipeline,
126
+ params=PipelineParams(
127
+ enable_metrics=True
128
+ ),
129
+ enable_tracing=True,
130
+ enable_turn_tracking=True
131
+ )
132
+ ```
133
+
134
+ For the full list of OpenTelemetry setup options, see the [Pipecat OpenTelemetry docs](https://docs.pipecat.ai/server/utilities/opentelemetry#overview).
135
+
136
+ ### Step 2 - Enable Audio Recording
137
+
138
+ Import the audio recorder and add it to your pipeline:
139
+
140
+ ```python
141
+ from finchvox.audio_recorder import ConversationAudioRecorder
142
+ ```
143
+
144
+ ```python
145
+ audio_recorder = ConversationAudioRecorder()
146
+
147
+ pipeline = Pipeline(
148
+ [
149
+ # Other processors, like STT, LLM, TTS, etc.
150
+ audio_recorder.get_processor(),
151
+ # context_aggregator.assistant(),
152
+ ]
153
+ )
154
+ ```
155
+
156
+ Start and stop recording on client connect/disconnect events:
157
+
158
+ ```python
159
+ @transport.event_handler("on_client_connected")
160
+ async def on_client_connected(transport, client):
161
+ await audio_recorder.start_recording()
162
+
163
+ # Other initialization logic...
164
+
165
+ @transport.event_handler("on_client_disconnected")
166
+ async def on_client_disconnected(transport, client):
167
+ await audio_recorder.stop_recording()
168
+
169
+ # Other cleanup logic...
170
+ ```
171
+
172
+ ## Troubleshooting
173
+
174
+ ### Port already in use
175
+
176
+ If port 4317 is already occupied:
177
+
178
+ ```bash
179
+ # Find process using port
180
+ lsof -i :4317
181
+
182
+ # Kill the process
183
+ kill -9 <PID>
184
+ ```
185
+
186
+ ### No spans being written
187
+
188
+ 1. Check collector is running: Look for "OTLP collector listening on port 4317" log message
189
+ 2. Verify client endpoint: Ensure Pipecat is configured to send to `http://localhost:4317`
@@ -0,0 +1,31 @@
1
+ finchvox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ finchvox/__main__.py,sha256=qKFAbLd3VP4KXyzdF-ENX6Lw4DO3ntywSwKCFbViED0,2376
3
+ finchvox/audio_recorder.py,sha256=QyKj25ooKX2N1oF-mL1BGE2qVldyodeXVpo__S4pCCg,10427
4
+ finchvox/audio_utils.py,sha256=k9L4enss9Ckq-rPgrkah0CMpwpOM0Ty_KAhICSoABOs,4500
5
+ finchvox/cli.py,sha256=ScWNcPb3bfX-jA_CfNbxTdHYL3MKxZHDTYwwWHdfAJo,3583
6
+ finchvox/server.py,sha256=BP7zROwvAvdoP8177_jAl0GCCknuCd54AMaJon-3QDs,6794
7
+ finchvox/trace.py,sha256=7bjaQDQvoFWgrmCxLhn1kBe7TIl26Tg7rcXxLi0XCAE,4048
8
+ finchvox/ui_routes.py,sha256=Ibxfu45mKw9GKmBMDXJdPCZvdfXtXMEBcW6g42Y-syM,12090
9
+ finchvox/collector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ finchvox/collector/__main__.py,sha256=aZE5_CkO9G3U1TeKJWec9Cpx2R3HNfmu90zM-6MePPQ,575
11
+ finchvox/collector/audio_handler.py,sha256=VfZnwsO4aGIN7TTHow4kE37KS_5CV7HtVDalM1RnfJk,4395
12
+ finchvox/collector/collector_routes.py,sha256=ysh8PNYH5gsXGRcMcLozGdyjZP5mhYgz-gModFOUP5w,6671
13
+ finchvox/collector/config.py,sha256=9yj2PQsI8DPSzpKphSqpLrBcInxkouVw4w37XTAFDNg,1896
14
+ finchvox/collector/server.py,sha256=2rNH1pmKQEyaE7vrld5sdWClqXgSwxq5VNYKe-tzmt8,4239
15
+ finchvox/collector/service.py,sha256=t4sceC-kwYQ1yoNrjgM3WHGWthNhAB1u7JzcSvDMRfc,1711
16
+ finchvox/collector/writer.py,sha256=1fjtxuBd3NcwR4Y-jFUJfJdKznHyH37JShSOHuw_nhM,3207
17
+ finchvox/ui/trace_detail.html,sha256=ouAC1Gn_ELqjPK0rqdXOou2KJYlj6X3QbYyJOdBhBDU,19201
18
+ finchvox/ui/traces_list.html,sha256=XjBOydvDfGM4fWvSxQ5GwHeIwXGNGWGK4zKjK47nOks,3000
19
+ finchvox/ui/css/app.css,sha256=rgiEWz7q-XaNsVMlq2JHD6SMfm4yBoItfY6fjPOpJ3M,14361
20
+ finchvox/ui/images/favicon.ico,sha256=j0O9rVzMW78TFsiA8zYelMIHQhN8GcTzbAf9OvjXC5M,15406
21
+ finchvox/ui/images/finchvox-logo.png,sha256=_D7isfCweOAsWoLW0UhGvjZF53MC8OX1CS6AKWecamQ,37048
22
+ finchvox/ui/js/time-utils.js,sha256=jb_vLsMvagTrXMnRCx12WHyh4KjeaZZZFUgz6zCsHIY,3478
23
+ finchvox/ui/js/trace_detail.js,sha256=HDMH4oVaBeyrhBY0DLChvBtImr9y6-qL3MU-jZO9YWc,56247
24
+ finchvox/ui/js/traces_list.js,sha256=FdL_1s-9HttFjom3QiBx_ww3N-6dC58NcBYCYdoaA5w,711
25
+ finchvox/ui/lib/alpine.min.js,sha256=e2nmRsTW_W5F0yF1XHx48Hdf-vCgsat5O3q4YPaizUQ,45764
26
+ finchvox/ui/lib/wavesurfer.min.js,sha256=MHeB40E-2QV3fR_LifZI3x5zpcFQNGuqbGZgMgsWCOM,40117
27
+ finchvox-0.0.1.dist-info/METADATA,sha256=hQsN1WV21j4Z0uJFZQ4EbOPI2WRMmGTa4eraPJ49_uU,5431
28
+ finchvox-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
29
+ finchvox-0.0.1.dist-info/entry_points.txt,sha256=sgwiFNVxRoX7_N2E3mF-gNPJfaeIlejZkrboVPXY0zc,47
30
+ finchvox-0.0.1.dist-info/licenses/LICENSE,sha256=FhYQcbAV4VeqmDcdLwjJeEu6CqIpsKaGXGCUXQYkPmg,912
31
+ finchvox-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ finchvox = finchvox.cli:main
@@ -0,0 +1,24 @@
1
+ Finchvox Source Available License
2
+
3
+ Copyright (c) 2026 Finchvox
4
+
5
+ Permission is granted, free of charge, to any person obtaining a copy of this
6
+ software and associated documentation files (the "Software"), to use, copy,
7
+ modify, and distribute the Software for internal or private use.
8
+
9
+ You may:
10
+ - Run the Software locally or within your organization
11
+ - Modify the Software for your own use
12
+ - Deploy the Software within your own infrastructure
13
+ - Deploy the Software on behalf of a customer, provided the customer operates
14
+ the Software themselves
15
+
16
+ You may NOT:
17
+ - Offer the Software as a hosted service, managed service, or SaaS
18
+ - Provide access to the Software to third parties as a service
19
+ - Resell, rebrand, or commercially exploit the Software as a service
20
+
21
+ This license does not grant you the right to use the Finchvox name, logo, or
22
+ trademarks.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.