Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions authors/honanevan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# honanevan

Author: honanevan

GitHub: [honanevan](https://github.com/honanevan)

Description: Contributor of the accompanying Sapat Deepgram integration and
transcription guide. The contribution was prepared with AI assistance and
validated with local automated tests.
21 changes: 21 additions & 0 deletions definitions/20260907_definition_http_contract_test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: 'HTTP Contract Test'
description: 'A test that checks the request and response agreement at an HTTP integration boundary.'
---

# HTTP Contract Test

## Definition

An HTTP contract test checks whether an application constructs the expected
request and correctly interprets responses at an HTTP interface. It can verify
headers, parameters, body encoding, required fields, and error handling.

A controlled server or HTTP-client substitute can make these checks repeatable
without contacting a paid service. Such a test verifies the application's
handling of the modeled interface. It does not establish that a live service is
available, that a credential works, or that a model's output is accurate.

For example, a transcription provider test can check that audio bytes are sent
as a binary body and that a missing transcript raises an error. Assessing the
recognized words still requires an actual recording and a reference transcript.
333 changes: 333 additions & 0 deletions guides/20260907_transcribe_video_with_sapat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
---
title: 'Transcribe Video with Sapat and Deepgram'
description: 'Build a reproducible transcription workflow, add a Deepgram provider, and test the HTTP boundary without making paid API requests.'
date: 2026-09-07
author: 'honanevan'
tags: ['python', 'audio', 'testing']
---

# Transcribe Video with Sapat and Deepgram

A video transcript looks like a small text file, but producing it reliably
involves several separate operations: reading the media container, converting
audio, sending a request, interpreting the service response, and saving the
result. When all of those operations live in one script, an authentication
failure can look like an empty transcript, and a packaging mistake can prevent
the command from starting on another machine.

This guide uses [Sapat](https://github.com/nibzard/sapat) to make those boundaries
visible. We add a Deepgram provider, exercise its request and response handling
without sending audio to a paid service, and prepare a reproducible environment
for a real transcription. The implementation is available in
[the companion branch](https://github.com/honanevan/sapat/tree/feat/deepgram-provider)
and [upstream PR #79](https://github.com/nibzard/sapat/pull/79).

> Validation status: the provider tests and wheel installation described here
> were executed locally on Windows, in Linux CI with Python 3.10 and 3.12,
> and inside a Daytona sandbox with Python 3.14.4. All 190 tests passed in the
> Daytona run. A live Deepgram transcription also passed locally on Windows
> using free account credit. The Daytona live request failed with a connection
> reset; its network access remains unresolved. These are separate results.
> Offline tests do not measure speech recognition accuracy.

## What the pipeline does

![Sapat transcription pipeline](assets/20260907_transcribe_video_with_sapat_img1.png)

Sapat's current command selects a provider and asks that provider for its
preferred audio format. The processing layer converts the input when necessary,
splits files above the configured size threshold, and passes the resulting audio
to the provider. A successful response becomes a `TranscriptionResult`; the
processing layer writes its `text` field beside the input with a `.txt` suffix.

That separation matters when extending the application. A provider should know
how its service authenticates and represents a transcript. It should not also
need to manage the command-line parser, video conversion, or output filenames.
Keeping those responsibilities separate makes an HTTP contract test useful:
the test can verify the network boundary without running an encoder or paying
for an inference request.

The current source also has limitations to account for. Directory processing
selects `.mp4` files rather than every possible media extension. The processing
layer removes the converted audio after success, even when that file already
existed. Work in a disposable directory containing copies, and keep original
recordings elsewhere. Review the current processing code before adapting this
workflow to an archival collection.

## Understand Whisper before choosing a provider

Whisper is a speech recognition model family, not the name of every transcription
API. It can transcribe speech, and its supported tasks include speech translation.
The [Whisper repository](https://github.com/openai/whisper) documents local model
installation and command-line usage. A hosted provider can expose a Whisper model
without requiring the client computer to download its weights or run inference.

In this guide, FFmpeg performs media conversion; it does not recognize words.
Sapat coordinates the workflow; it does not itself implement a speech model.
Deepgram handles the recognition request, with `nova-3` selected explicitly in
the new provider. Nova-3 is not Whisper. This distinction lets you compare
services without confusing the application, the transport API, and the model.

Changing a provider does not guarantee the same text. Punctuation, language
support, timestamps, formatting, and treatment of silence can differ. Evaluate
a small representative recording before processing a collection. For meetings,
include overlapping speech and names; for lectures, include technical terms.
Keep the same source audio when comparing results so that conversion changes do
not become an uncontrolled part of the comparison.

## Prepare the project

You need Git, Python, FFmpeg for media conversion, and a terminal. The new
provider uses the project's existing `requests` dependency rather than another
SDK. The offline tests require no service account. Real transcription requires
a Deepgram account and key, and service usage may be billed separately from
the environment running Sapat.

Clone the companion branch and record its revision:

```bash
git clone --branch feat/deepgram-provider https://github.com/honanevan/sapat.git
cd sapat
git rev-parse HEAD
python -m venv .venv
```

Activate the environment on Linux or macOS:

```bash
source .venv/bin/activate
```

On Windows PowerShell, use the environment's interpreter directly if script
activation is disabled:

```powershell
.\.venv\Scripts\python.exe -m pip install '.[dev]' build
.\.venv\Scripts\python.exe -m pytest tests/test_deepgram.py -q
```

With an activated environment, the equivalent commands are:

```bash
python -m pip install '.[dev]' build
python -m pytest tests/test_deepgram.py -q
```

The implementation revision used for the local checks is
`7a9bdfb707566224f7681e397bbb64013d4d4dc6`. Record the revision you actually
run rather than assuming a branch will remain unchanged. Keep the environment
and dependency information with your results when reporting a problem.

## Bring the checks into Daytona

Daytona provides an isolated environment for running the project. Its current
[quickstart](https://www.daytona.io/docs/en/) describes account setup, API keys,
and sandbox creation. Follow that documentation for your installation; older
Sapat README examples use a different generation of the Daytona CLI.

In the Daytona dashboard, open **Sandboxes**, choose **Create Sandbox**, and
give it a descriptive name such as `sapat-guide-validation`. The validation
run used the default `daytonaio/sandbox:0.8.0` snapshot in the EU region with
one vCPU, 1 GiB RAM, and 3 GiB disk. Keep HTTP previews private and set an
auto-stop interval; this run used ten minutes, with auto-archive also set to
ten minutes. Check your wallet before creating resources. Our account showed
free credits and no payment card; that is an observation, not a guarantee of
the current signup offer for every reader.

Open the new sandbox's **Terminal** tab. Its **Open in new tab** link provides
a larger terminal. Create a disposable working directory, then run the same
clone, virtual-environment, installation, and test commands above:

```bash
mkdir -p /tmp/sapat-validation
cd /tmp/sapat-validation
python --version
git --version
ffmpeg -version
```

The 8 September 2026 run used Python 3.14.4 and Git 2.53.0; FFmpeg was already
available at `/usr/bin/ffmpeg`. The provider suite passed all 14 tests and the
full suite passed all 190 tests. If FFmpeg is absent from your image, install
it through the image's package manager. Python package installation alone does
not install that executable.

Start with the offline tests. They establish whether the Python package imports
and whether the provider constructs the expected requests. Only after those
checks pass should you add a real service credential through your environment's
secret configuration. Do not put the credential in the repository, a screenshot,
or a command copied into a public issue.

For a repeatable demonstration, capture the sandbox's Python version, the Git
revision, the test summary, and the final transcript path. Keep recordings and
transcripts out of a public demo unless you have permission to publish them.
Export the results you need before stopping or deleting the environment.

## Inspect the Deepgram request

The [Deepgram prerecorded-audio guide](https://developers.deepgram.com/docs/pre-recorded-audio)
shows a binary upload to `/v1/listen`, authenticated with a `Token` header.
The new provider sends the audio stream as the request body and supplies the
model and language as query parameters. It does not use the multipart upload
format used by some other transcription services.

An explicit model avoids silently inheriting an API default. Smart formatting
is enabled. Passing an empty language requests detection; otherwise the supplied
language is forwarded. The shared Sapat prompt and temperature options do not
map directly to this integration, so the provider warns when they are supplied.
It also does not advertise LLM transcript correction.

The response parser requires a transcript string in the first channel's first
alternative. An empty string is a valid response for audio with no recognized
speech. A missing channel or transcript is different: the provider raises an
error instead of converting malformed data into an apparently successful empty
file. This is a small distinction with a large effect on unattended processing.

## Run the contract tests

A [contract test](../definitions/20260907_definition_http_contract_test.md)
checks the agreement between the application and an external
interface. Here, a controlled replacement for `requests.post` receives the
actual arguments constructed by the provider and supplies a representative
response. No recognition model runs during these checks.

```bash
python -m pytest tests/test_deepgram.py -q
```

The local run produced 14 passing tests. They cover the following boundaries:

| Boundary | What the test establishes |
| --- | --- |
| Upload | Audio bytes form the request body; authentication and MIME type match |
| Options | Model, explicit language, and automatic detection are represented correctly |
| Response | Text, detected language, and duration reach the result object |
| Silence | An empty transcript remains a valid empty string |
| Invalid data | Missing fields and invalid JSON raise a clear error |
| Authentication | A missing key fails before a network request |
| Service errors | HTTP failures are surfaced without echoing response bodies |
| Timeout | The audio file is closed when the request raises an exception |
| Discovery | A fresh Python process discovers the configured provider |

Run the complete suite as well:

```bash
python -m pytest -q
```

On the local Windows environment, this produced 189 passes and one failure in
the existing WhisperX executable-path handling. That test also fails when run
alone: POSIX-style argument splitting removes Windows path backslashes. Report
that distinction when reviewing the contribution; do not replace the result
with a claim that the entire suite passed.

## Check the wheel, not only the checkout

Editable installations can hide packaging mistakes because imports resolve to
the source tree. Sapat's package-discovery configuration originally included
only `sapat`, omitting `sapat.providers`. The companion change includes both
the top-level package and its subpackages.

Build a wheel:

```bash
python -m build --wheel --outdir validation-wheel
```

The repository contains archived wheels in `dist`, so use the new output
directory and avoid installing every wheel from `dist/*.whl`.

Inside Daytona, the new wheel was also installed into a second virtual
environment at `/tmp/sapat-installed-wheel`. From `/tmp`, provider discovery
with a dummy `DEEPGRAM_API_KEY=test-only-key` returned `DeepgramProvider`, and
`sapat --help` displayed the CLI options. This checks the installed package
without relying on imports from the source directory. FFmpeg 7.1.3 was
available separately in the sandbox.

Install the new wheel into a separate environment and run the command from outside
the source checkout. Check that `sapat.providers.deepgram` imports successfully.
If you test discovery, use an obviously fake test key and do not invoke
transcription. Discovery checks configuration availability; it does not validate
credentials against the service.

The local validation built the wheel, installed it into a separate directory,
confirmed imports resolved from that installation, and discovered Deepgram in
a fresh process. This catches the omission that an editable-only test misses.
The [Linux CI run](https://github.com/honanevan/sapat/actions/runs/34167025609)
also passed all 14 provider contract tests on both Python 3.10 and 3.12, then
built and installed the wheel and checked discovery and CLI help outside the
checkout. The discovery step supplies a dummy key without sending a request.

## Transcribe a disposable recording copy

After configuring a real key, place a short recording copy in a scratch
directory. Confirm FFmpeg is available, then use the current provider option:

```bash
sapat recording.mp4 --provider deepgram --model nova-3 --language en
```

Expect a `recording.txt` beside the recording after a successful response.
Listen to the audio while reading the text. Verify names, numbers, and omitted
phrases before treating the transcript as accurate. A successful HTTP request
establishes delivery and response handling, not correctness of every word.

Do not enable correction as part of the first comparison. Recognition and
post-processing are separate transformations; keeping the original transcript
makes it possible to identify which step introduced a change. This provider
does not support correction, and Sapat reports that limitation if requested.

## Troubleshoot by boundary

### Live validation result and sandbox network access

On 8 September 2026, the full CLI at revision
`7a9bdfb707566224f7681e397bbb64013d4d4dc6` converted an original synthetic MP4,
sent its audio to Deepgram with `--model nova-3 --language en`, and saved a
text file on Windows. The returned text matched the recording's source text:

> This is a short synthetic recording for a transcription test. The package
> converts video to audio and saves the recognized words in a text file.

This establishes one successful end-to-end example, not a general accuracy
benchmark. The key was supplied only to the test process and was not committed.

The same CLI invocation inside Daytona converted the video but failed during
the API request with `ConnectionResetError(104, 'Connection reset by peer')`.
Daytona's [network limits](https://www.daytona.io/docs/en/network-limits/)
restrict outbound access on Tier 1 and Tier 2; Deepgram was not listed in the
published allowlist when checked. This is a likely explanation, not a confirmed
diagnosis of that individual request. Support has been asked about a supported
no-cost way to reach the endpoint. The sandbox was stopped after validation.

Before attempting the live command in a sandbox, verify that its network policy
allows `api.deepgram.com`. A passing offline test suite does not establish that
an external service is reachable. Do not repeatedly upload the recording when
the environment cannot reach the service.

### Other failures

If the provider is unavailable, confirm the environment variable exists in the
same process that starts Sapat. Restart the command after changing it because
provider discovery occurs during the Python process's lifetime. Use
`--provider deepgram`; the older README's `--api` syntax belongs to the legacy
interface.

If conversion fails, run `ffmpeg -version` in the same terminal and verify that
the media contains an audio stream. If authentication fails, check the key and
service account rather than repeatedly uploading the same file. For a timeout,
try a smaller recording and inspect service status before retrying. This provider
does not automatically retry potentially billable requests.

If the command succeeds but the transcript is poor, inspect the recording and
language choice. That is an inference-quality problem, distinct from the HTTP
and packaging checks. Save enough non-sensitive evidence to reproduce the
problem: revision, model, language, approximate duration, and the observed error.

## References

- [Sapat source and processing pipeline](https://github.com/nibzard/sapat)
- [Deepgram integration and tests](https://github.com/nibzard/sapat/pull/79)
- [Deepgram prerecorded API reference](https://developers.deepgram.com/reference/speech-to-text/listen-pre-recorded)
- [Whisper model repository](https://github.com/openai/whisper)
- [Daytona documentation](https://www.daytona.io/docs/en/)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.