A .NET 9 cloud backend that discovers podcast episodes, prepares audio with FFmpeg, submits asynchronous jobs to Azure AI Speech, and persists workflow state in Azure Cosmos DB.
PodcastTranscribe is an independent backend engineering project built around a multi-stage media workflow rather than a single synchronous API call. The service coordinates external search, bounded audio download, local transcoding, object storage, batch speech recognition, status polling, and durable result retrieval behind an ASP.NET Core REST API.
| Area | Implementation |
|---|---|
| API and application layer | C# · .NET 9 · ASP.NET Core controllers · dependency injection · Swagger/OpenAPI |
| Workflow orchestration | Asynchronous submission · explicit transcription states · idempotent episode checks · polling-based synchronization |
| Cloud services | Azure AI Speech Batch Transcription · Azure Blob Storage · Azure Cosmos DB |
| Media pipeline | HTTP range download · streamed file I/O · FFmpeg transcoding · deterministic blob naming |
| External integration | Listen Notes episode search · Azure SDKs · typed configuration through IOptions<T> |
| Runtime | Multi-stage Docker build · ASP.NET 9 runtime · system FFmpeg · structured ILogger<T> logging |
REST Client
|
v
EpisodeController
|
v
EpisodeService ---------------------------> CosmosDbService ---> Azure Cosmos DB
| |
| +-----------> Listen Notes API
|
+---> TranscriptionSubmissionService
| | |
| | +---> Azure AI Speech
| +----------------> Azure Blob Storage
+---------------------------> Audio host + FFmpeg
The application is a modular monolith: HTTP concerns remain in the controller, use-case decisions live in EpisodeService, and provider-specific behavior is isolated in dedicated services. Interfaces separate high-level orchestration from Blob Storage, Speech, external search, and media-processing details.
GET /api/Episode?title=... searches Cosmos DB first. On a cache miss, the service queries Listen Notes, maps provider data into Episode aggregates, persists previously unseen episodes, and returns reduced EpisodeSummary responses.
POST /api/Episode/{id}/transcription checks the persisted state before starting work. Completed or active episodes are not submitted again; a NotStarted episode enters the asynchronous media pipeline and returns immediately to the caller.
The submission service:
- Transitions the episode to
Processing. - Reuses the deterministic
{episodeId}_audio.mp3blob when available. - Otherwise downloads at most 10 MiB using streamed HTTP range reads.
- Converts the audio to a 16 kbps, 22,050 Hz mono MP3 with FFmpeg.
- Uploads the processed stream to Azure Blob Storage.
- Submits the Blob URL to Azure AI Speech Batch Transcription.
- Persists the provider job URI and state checkpoint in Cosmos DB.
- Removes temporary files in a
finallyblock.
GET /api/Episode/{id}/transcription polls the Speech job, maps provider states into the domain state machine, retrieves the completed result, and persists display-ready transcript text before returning it.
NotStarted -> Processing -> TranscriptionSubmitted -> TranscriptionRunning -> TranscriptionSucceeded
\ | /
+-------------------+-------------------> Failed
- Explicit workflow state: transcription progress is represented by a persisted enum rather than inferred from nullable fields or provider responses.
- Idempotent submission decisions: active and completed episodes do not create duplicate work, while deterministic Blob names allow processed audio to be reused.
- Durable checkpoints: Blob URI, Speech job URI, provider status, and transcript text are persisted after externally meaningful steps.
- Bounded media handling: HTTP response streaming, a fixed download limit, reduced bitrate, mono output, and temporary-file cleanup bound local resource use.
- Provider isolation: Listen Notes, Blob Storage, Cosmos DB, Azure Speech, and FFmpeg behavior remain behind focused service boundaries.
- Dependency-injected composition: service lifetimes and typed cloud configuration are defined centrally in
Program.cs. - Containerized runtime: the Docker image separates SDK build and ASP.NET runtime stages and installs the native FFmpeg dependency explicitly.
| Method | Route | Purpose |
|---|---|---|
GET |
/api/Episode?title={title} |
Search the local catalog, then Listen Notes on a cache miss |
POST |
/api/Episode/{id}/transcription |
Start transcription when the episode state permits it |
GET |
/api/Episode/{id}/transcription |
Synchronize and return the current state or transcript |
GET |
/ |
Basic process health response |
Controllers/
EpisodeController.cs HTTP routes, validation, and response shaping
Services/
EpisodeService.cs Use-case orchestration and state decisions
TranscriptionSubmissionService.cs Download, FFmpeg, Blob, and Speech submission pipeline
AzureSpeechHandlerSeervice.cs Azure batch job submission, polling, and result parsing
AzureBlobStorageService.cs Blob initialization and media operations
CosmosDbService.cs Episode persistence and query operations
ExternalPodcastSearchService.cs Listen Notes integration and catalog population
Models/
Episode.cs Persisted aggregate and transcription state machine
EpisodeSummary.cs Search response projection
Configuration/ Typed cloud-service settings
Program.cs Dependency injection and application composition
Dockerfile Reproducible .NET + FFmpeg runtime
- .NET 9 SDK
- FFmpeg
- Azure Cosmos DB database and container partitioned by
/id - Azure Blob Storage container
- Azure AI Speech resource
- Listen Notes API access
The application reads these environment variables, with .env support for local development:
COSMOS_CONNECTION_STRING=
COSMOS_DB_NAME=
COSMOS_CONTAINER_NAME=
BLOB_CONNECTION_STRING=
BLOB_CONTAINER_NAME=
AZURE_SPEECH_KEY=
AZURE_SPEECH_REGION=
AZURE_SPEECH_API_VERSION=
LISTENNOTES_API_KEY=
LISTENNOTES_END_POINT=dotnet restore
dotnet runThe application listens on http://localhost:5050. Swagger UI is enabled in the Development environment.
docker build -t podcast-transcribe-api .
docker run --env-file .env -p 5050:5050 podcast-transcribe-apiPodcastTranscribe was built outside work to practice conventional backend engineering across API design, dependency injection, asynchronous workflows, media processing, cloud persistence, external-service integration, and containerized deployment.