Dinesh/ Blog
cloud engineering

Automating audio generation

From text , generate audio files and publishing them to webapp

13 min readChecking device speech…

In this article

  • Speech succeeds but upload fails. What must cleanup and retry preserve?
  • Explain the version and execution boundaries before applying the examples
Background & source details

Level: intermediate

Application engineering projects · Lesson 5 of 6 ↗
Loading this browser’s progress…

Bring with you

Sources checked: . A source check does not change the publication or update date.

Editorial review: · What review means

In this article · 24 sections

Review and execution boundary

Azure Speech Python SDK result checks and Blob authorization; output sizes derived from format, not universal WAV/MP3 estimates.

Reviewed on 7 October 2026 against the official source snapshots linked below. The review is bounded editorial correction, not certification of every dependency, security property or cloud deployment. Historical setup commands and optional exercises were not executed. No cloud resources, third-party packages or external side effects were created. Old screenshots and unavailable private assets remain in the private recovery archive, not prerequisites for this lesson.

Using Text-to-Audio Conversion Service to Publish Audio Content to a Web App

A reader should be able to press play without waiting for the same article to be synthesized again on every visit. This design uses Azure AI Speech Service to generate an audio file once, then separates that generation work from delivery through Blob Storage and a web player. The walkthrough and reference code show that handoff; batching, access control, and failure cleanup still need application-specific implementation.


Method Provided by LLM

The following approach was generated with the assistance of an LLM (GitHub Copilot). The original outline is retained below; the reference worker now implements the shared synthesis/upload path. Application authorization, job persistence and cloud provisioning remain reader-owned integration work.

Architecture

Read the first half of this line as a generation job and the second as publication. Speech produces bytes; Blob Storage holds them; the web app gives the player a location from which to fetch them. Uploading successfully does not by itself grant a listener permission to read the file.

Text Input → Azure AI Speech Service → .wav/.mp3 file → Azure Blob Storage → Web App
Text input → Azure AI Speech Service → WAV/MP3 file → Azure Blob Storage → Web App. Synthesis creates the audio bytes; storage and the web app deliver them. Listener read authorization is configured separately from upload.
Figure text and layout
Text Input → Azure AI Speech Service → .wav/.mp3 file → Azure Blob Storage → Web App
Text diagram

Approach — Step by Step

1. Provision an Azure AI Speech Resource

  • Go to the Azure Portal → Create a resource → search Speech → select Speech by Microsoft
  • Choose a Resource group (or create one, e.g. rg-audio-gen)
  • Pick a Region that supports the chosen voice/features and your data-location requirements; proximity alone is not enough
  • Select an available pricing tier after checking current quotas, voice support and usage prices; do not assume a free tier covers a production workload
  • After an authorized deployment, supply the Key to a secret store and configure the Region; do not copy keys into notes, screenshots or source control
  • Alternatively, use the Azure CLI: az cognitiveservices account create with --kind SpeechServices

2. Set Up Your Python Environment

  • Install the Azure Speech SDK package: azure-cognitiveservices-speech
  • Store your Speech key and region as environment variables (AZURE_SPEECH_KEY, AZURE_SPEECH_REGION) — never hard-code secrets

3. Write the Text-to-Audio Conversion Logic

  • Create a SpeechConfig object using your key and region
  • Set the voice using speech_synthesis_voice_name — Azure supports a changing voice inventory across supported locales (see voice gallery)
  • Set an explicit output format and create an AudioOutputConfig with a matching filename. The reference worker uses 24 kHz, 96 kbit/s mono MP3 and .mp3
  • Instantiate a SpeechSynthesizer and call speak_text_async() with your input text
  • Handle both the success case (SynthesizingAudioCompleted) and the cancellation/error case
  • The SDK writes the audio directly to the specified output file

4. Provision Azure Blob Storage

  • Create a Storage Account in the same resource group (e.g. via az storage account create)
  • Create a Blob Container (e.g. audio-files) private by default; decide explicitly how authorized listeners obtain read access
  • Install azure-storage-blob and azure-identity alongside the Speech SDK
  • Configure BLOB_ACCOUNT_URL and an identity usable by DefaultAzureCredential (for example, the deployed worker's managed identity). Grant a scoped Blob data role such as Storage Blob Data Contributor. The reference code does not consume a storage connection string

5. Upload the Generated Audio File

  • Use BlobServiceClient(account_url=..., credential=...) as shown in the worker; Speech key authentication and Blob identity authentication are separate
  • Get a BlobClient for your container and target blob name
  • Open the local .mp3 file in binary mode and call upload_blob() with overwrite=False and the audio/mpeg content type. A unique name avoids overwriting another job; stable job keys additionally need conflict reconciliation
  • The blob client exposes a .url property — this is an address, not proof of anonymous read permission
  • Always release local temporary files on success and failure. Retain durable job state separately so cleanup does not erase retry identity

6. Serve the Audio in Your Web App

  • The uploaded blob has an address like (read authorization remains required): https://<account>.blob.core.windows.net/audio-files/output.mp3
  • Embed it in HTML using the <audio controls> element with a <source> tag
  • In a Flask example application, a route can render the audio player with an authorized playback URL passed as a template variable; this is not a claim about the current blog implementation

7. End-to-End Flow

Combine everything into a single function that:

  1. Takes text input as a parameter
  2. Synthesizes it to a local .mp3 file via Azure Speech
  3. Uploads the file to Blob Storage
  4. Cleans up the local file
  5. Returns the blob address; access is authorized separately

Key Azure Services Used

ServicePurpose
Azure AI SpeechConverts text to natural-sounding audio using neural voices
Azure Blob StorageStores generated audio; listener access is authorized separately

Cost Estimate

No current price quotation is established by this lesson. Estimate synthesis characters/voice tier, storage bytes/transactions, delivery egress and worker compute using the selected region/SKU price sheet and dated usage assumptions. A budget alert does not stop spending. The old $1-per-million and free-storage assumptions have been removed rather than carried forward.

Important Notes

  • Security: Use environment variables for all keys and connection strings. Consider Azure Key Vault for production.
  • Voice selection: Experiment with different speech_synthesis_voice_name values. Multi-lingual voices (e.g. en-US-JennyMultilingualNeural) can handle mixed-language text.
  • Output format: Select the format explicitly with speech_config.set_speech_synthesis_output_format(); changing a filename extension does not change the encoded bytes.
  • SSML: For finer control over pronunciation, pauses, pitch, and speed, use SSML markup instead of plain text with speak_ssml_async().
  • Batch processing: For long documents, split at sentence/paragraph boundaries, bound concurrency to current service quotas, and retain chunk indices so completion order does not become narration order.

Cleanup

Use temporary-file cleanup within the worker. Cloud-resource retirement is a separately approved operation: inventory shared dependencies, retention obligations and billable resources first. Do not paste a resource-group deletion command as routine application cleanup.

References


Background & Prerequisites — What You Need to Know Before Writing This Blog

To build a text-to-audio pipeline on Azure, you need to understand speech synthesis, cloud storage, and web serving. Below are the foundational topics.


1. Text-to-Speech (TTS) Fundamentals

Why: The core of this project is converting text to audio — you need to understand how modern TTS works.

  • Concatenative TTS — Older approach: record a voice actor speaking thousands of phoneme combinations, then concatenate matching segments at runtime. Join discontinuities and limited recorded coverage can affect naturalness.
  • Neural TTS — Neural systems learn text-to-acoustics and waveform-generation stages, either separately or jointly. An acoustic representation is not yet playable audio; a synthesis/vocoder stage produces waveform samples. Naturalness and pronunciation depend on the model, voice and input, not simply the label “neural.”
  • Azure Neural Voices — Microsoft's neural TTS offering has a changing, region-dependent voice inventory. Available styles and multilingual support are voice-specific; check the chosen voice rather than assuming every voice supports every style.
  • Phonemes & Prosody — Phonemes are the individual units of sound. Prosody covers rhythm, stress, and intonation. Neural TTS models learn prosody from training data but can be controlled via SSML.
  • Vocoder — Converts the model's intermediate representation (mel spectrogram) into an audio waveform. HiFi-GAN and WaveRNN are common vocoders.

2. SSML (Speech Synthesis Markup Language)

Why: Plain text can be sufficient for narration; SSML adds explicit control when pronunciation, pauses or speaking style need adjustment.

  • What is SSML — An XML-based markup language that controls how text is spoken. Azure, Google and AWS Polly support their own subsets and extensions; an SSML document is not automatically portable across engines or voices.
  • Key tags —
    • <speak> — Root element
    • <voice> — Select a specific voice
    • <prosody> — Control rate, pitch, volume (e.g., <prosody rate="slow" pitch="+10%">)
    • <break> — Insert pauses (e.g., <break time="500ms"/>)
    • <emphasis> — Request word-level stress only on voices documented to support it
    • <say-as> — Control interpretation (dates, numbers, abbreviations: <say-as interpret-as="date" format="ymd">2026-02-22</say-as>)
    • <phoneme> — Specify exact pronunciation using IPA (International Phonetic Alphabet)
  • Multi-voice conversations — Use multiple supported <voice> elements within one SSML document to create dialogue-style audio. For SSML input, the document selects the voice: validating only the function's voice argument does not validate embedded <voice name="..."> elements. Reject unapproved voices, remote <audio src="..."> references and unsupported controls before calling speak_ssml_async().

3. Audio Formats & Encoding

Why: Choosing the right audio format affects file size, quality, and browser compatibility.

  • WAV (Waveform Audio) — A container often carrying PCM; size depends on rate, channels and bit depth, not a fixed per-minute constant. PCM avoids lossy encoding, but quality still depends on the source. WAV is usable for short clips; bandwidth often favors compressed formats.
  • MP3 — Compressed, lossy. ~1MB per minute at 128kbps. A common web-delivery choice; verify playback in the browsers and devices you support.
  • OGG/Opus — Opus is a codec; Ogg is a container. Compression quality depends on content and encoder settings. Support varies by browser/OS version: test the actual codec/container with canPlayType() and playback rather than ranking formats universally.
  • Azure SDK output formats — Configurable via SpeechSynthesisOutputFormat enum. Options include Audio16Khz32KBitRateMonoMp3, Riff24Khz16BitMonoPcm, etc. Choose based on quality vs size tradeoff.
  • Sample rate — Sample rate limits representable frequency content; raising it does not restore detail absent from the source. At fixed channels/bit depth, PCM bytes increase with sample rate. At fixed compressed bitrate, duration—not sample rate alone—dominates file size.

4. Azure Blob Storage Fundamentals

Why: Audio files need to be stored and served — Blob Storage is the hosting layer.

  • Storage account types — Standard general-purpose v2 is a suitable baseline for this block-blob example. Specialized premium accounts have different cost/performance and tiering constraints; “always GPv2” is not a universal rule.
  • Blob types — Block blobs (for files like audio), Append blobs (logs), Page blobs (VM disks). Audio files are block blobs.
  • Access tiers — Hot is a reasonable starting point for frequently played audio; Cool and Cold remain online with different storage/access and minimum-duration charges. Archive is offline and must be rehydrated before playback. Choose from measured access patterns and current account/tier rules, not simply “web app = Hot.” Minimum-duration charges are billing rules, not a ban on earlier deletion.
  • Access levels — Private requires an authorized delivery path. Anonymous Blob-level access permits reading blobs; Container-level access additionally permits listing. These settings only work if the account allows anonymous access. Keep this pipeline private unless public distribution is a deliberate product decision.
  • SAS (Shared Access Signature) — Time-limited, scoped bearer tokens for accessing private blobs. Possession grants the encoded authority until expiry/revocation rules apply; HTTPS, narrow read scope and short lifetimes matter. SAS issuance is not implemented by this worker.
  • CDN integration — An edge cache can reduce origin traffic and latency for cacheable audio. For a new Azure design evaluate Front Door Standard/Premium, not a new Microsoft classic CDN profile: classic onboarding has ended. Private audio additionally needs deliberate origin access, cache-key and authorization rules; never let a cached response bypass per-listener access checks.

5. Azure AI Speech SDK

Why: The SDK is the primary tool for implementing text-to-audio conversion.

  • SpeechConfig — Configuration object holding the subscription key, region, voice name, and output format. The reference creates one per job so mutable voice/format settings are not accidentally shared between concurrent requests.
  • SpeechSynthesizer — The main class. Methods: speak_text_async() (plain text), speak_ssml_async() (SSML input). Can output to a file, audio stream, or speaker.
  • AudioOutputConfig — Controls where audio goes: file path, audio stream, or default speaker. For server-side processing, output to a file or memory stream.
  • Event-driven architecture — The SDK fires events: synthesis_started, synthesizing (partial audio chunks), synthesis_completed, synthesis_canceled. Use these for progress tracking and error handling.
  • Error handling — Check SpeechSynthesisResult.reason. Values: ResultReason.SynthesizingAudioCompleted (success), ResultReason.Canceled (failure). On cancellation, inspect result.cancellation_details in the Python SDK; redact diagnostics before logging or returning an error.
  • Batch synthesis — For large documents, synthesize bounded chunks and record their original indices, format and result status. Use a media-aware join/remux or decode/re-encode workflow (for example with ffmpeg) after every required chunk succeeds; raw byte concatenation of WAV headers is not a valid join. Recheck duration and seams. This is a design exercise, not an implemented batch-synthesis API client.

6. Web App Integration

Why: The generated audio needs to be served and played in a browser.

  • HTML5 <audio> element — Native browser audio player. Codec/container support depends on the browser. controls requests the native player UI. preload="metadata" is a hint to fetch metadata, not a guarantee about downloaded bytes; browsers may ignore it.
  • Streaming vs Download — An authorized blob URL or application route can deliver short or long clips; there is no five-minute cutoff. Byte-range support helps seeking and progressive delivery, but must work through the full authorization/proxy path. Azure Media Services retired in 2024 and is not a new-deployment option.
  • Flask integration — Use POST to submit an authorized generation job; the worker synthesizes and uploads, and a separately authorized playback route serves the completed result. Render that playback URL in a Jinja2 template. The route contract below explains ownership, quotas and retry identity.
  • Responsive design — The audio player should work on mobile. Constrain the player width in the application layout and test narrow screens, keyboard controls and accessible transcript access; native controls alone are not a responsive-layout guarantee.

TODO / Remaining Work

These are optional implementation and deployment exercises, not missing prerequisites for understanding the conceptual pipeline. The worker and template below are reviewed fragments; no deployed application is claimed.

  • Implement the text-to-audio conversion script using Azure Speech SDK
  • Test with different neural voices and compare quality
  • Implement SSML support for fine-grained control
  • Set up Azure Blob Storage and implement upload logic
  • Build the Flask route and audio player page
  • Add batch processing for long documents
  • Document cost analysis with real usage numbers
  • Add architecture diagram (Mermaid) of the full pipeline

Reference Implementation

The worker below shares the text/SSML upload path and cleans local files on both success and failure. Inputs must already pass authorization, length, voice and SSML validation. SDK packages and storage resources are dependencies, not installed by this lesson. Use azure-cognitiveservices-speech, azure-storage-blob and azure-identity; record your resolved versions and platform requirements before an authorized integration run. BLOB_ACCOUNT_URL is configuration, not a secret or a SAS URL. DefaultAzureCredential needs a configured credential source and an assigned data-plane role—it does not grant access by itself.

import os
import tempfile
import uuid
from pathlib import Path
import azure.cognitiveservices.speech as speechsdk
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient, ContentSettings
 
def synthesize(text: str, *, is_ssml: bool = False,
               voice: str = "en-US-JennyNeural") -> str:
    cfg = speechsdk.SpeechConfig(subscription=os.environ["AZURE_SPEECH_KEY"],
                                 region=os.environ["AZURE_SPEECH_REGION"])
    cfg.speech_synthesis_voice_name = voice
    cfg.set_speech_synthesis_output_format(
        speechsdk.SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3)
    with tempfile.TemporaryDirectory(prefix="tts-") as directory:
        output = Path(directory) / "audio.mp3"
        synth = speechsdk.SpeechSynthesizer(speech_config=cfg,
            audio_config=speechsdk.audio.AudioOutputConfig(filename=str(output)))
        future = synth.speak_ssml_async(text) if is_ssml else synth.speak_text_async(text)
        result = future.get()
        if result.reason != speechsdk.ResultReason.SynthesizingAudioCompleted:
            raise RuntimeError("Speech synthesis failed; inspect redacted worker diagnostics")
        with DefaultAzureCredential() as credential:
            with BlobServiceClient(account_url=os.environ["BLOB_ACCOUNT_URL"],
                                   credential=credential) as service:
                blob = service.get_blob_client("audio-files", f"{uuid.uuid4().hex}.mp3")
                with output.open("rb") as stream:
                    blob.upload_blob(stream, overwrite=False,
                        content_settings=ContentSettings(content_type="audio/mpeg"))
                return blob.url  # address only; authorization is still required

The shown UUID names make each call a new object; the function is not idempotent. For retry deduplication replace the random object name with a server-owned job/version key and reconcile upload conflicts. Persist the principal, normalized input/content hash, selected voice(s), format and stage in the job record before starting work; SSML settings belong in that identity too. A lost upload acknowledgment can mean the object already exists: inspect the expected object identity/hash before retrying, rather than overwriting it or publishing a second result. Do not log bearer tokens, keys, full SSML or private audio URLs. The storage managed identity needs a scoped Blob data-plane role. This remains an unexecuted SDK worker fragment; the local file cleanup pattern is standard Python, not a claimed Speech-service test.

Flask Route

The web route should submit a bounded job, not block on Speech and Blob calls for every page view. Define the route contract as POST /audio-jobs with text, an allowlisted voice and a content version; return 202 plus a job identifier. Authenticate the principal, authorize that content, enforce cookie-CSRF protection where applicable, validate text length without silent truncation, and reserve a per-user/global job quota before enqueueing. An idempotency key tied to principal/content-version/voice/format avoids duplicate charges on repeated submits.

The worker calls the synthesis function and stores a private blob reference under the job owner. GET /audio-jobs/id checks the same ownership before returning queued/running/complete/failed. On completion, a playback route either streams the authorized blob (including range handling where supported) or issues a narrowly scoped expiring URL. A copied job ID must not bypass authorization. Store a safe error category on failure, not the provider's raw message or input text. Cancellation is cooperative; canceling an HTTP request does not prove a provider synthesis operation stopped or its charge was reversed.

For a first single-user learning application a synchronous route can be acceptable under strict concurrency/duration limits, but label it as such. It still needs authorization, input validation and cleanup; a Flask decorator alone provides none of those controls. No queue, Flask app or cloud provider was executed here.

Template Snippet

A Jinja template receives an already-authorized playback URL. Autoescape the value, allow only the application's expected HTTPS origin/path or a controlled same-origin playback route, and do not accept an arbitrary caller-supplied URL. A private raw blob address alone will not play for an unauthenticated browser.

<audio controls preload="metadata" src="{{ audio_url }}">
  Your browser does not support audio playback.
</audio>
<p><a href="{{ transcript_url }}">Read the transcript</a></p>

Exercise: a short-lived URL expires while the user seeks. Solution: the player needs a recoverable authorized refresh flow, not permanent public access as a workaround. Test playback to the end, seeking/ranges, expiry, keyboard controls and transcript access using nonprivate fixtures. Never log a signed playback URL, because its query string can carry temporary access authority.

Corrected contracts and failure analysis

A blob URL is an address, not a public-access grant. Keep audio private by default and serve through an authorized application or narrowly scoped short-lived access mechanism. If the content is intentionally public, make that a reviewed product decision. Upload permission does not imply listener permission.

Check the Speech result reason before uploading. Synthesis and upload have independent failures, so use a temporary directory that cleans up on either failure. The surrounding job system must supply stable content/version/voice/format identity and conflict handling for idempotency; the UUID-based reference worker alone does not. Serialize publication after success; retries should not expose partial audio. The SDK's .get() blocks while waiting and belongs in a worker, not an unbounded async web handler.

PCM size = sample rate × channels × bytes per sample × seconds, plus container overhead. At 24 kHz mono 16-bit, a minute is 2,880,000 bytes, not a universal 10 MB. MP3 at 96 kbit/s is approximately 720,000 bytes per minute before overhead. SSML is XML: escape user text, allowlist supported controls/voices, and do not concatenate untrusted markup. Voice inventory, regional availability and prices must be checked rather than frozen as marketing counts.

Boundary exercise with solution

Speech succeeds but upload fails. What must cleanup and retry preserve?

Solution and reasoning

Clean local temporary files in finally/TemporaryDirectory; retain the job identity and failure stage, not the secret or raw exception. If the upload outcome is uncertain, first reconcile the expected blob and job identity. Retry boundedly with the same content/version/voice/format identity, and only publish the playable URL after upload and access checks. Cleanup can force re-synthesis and another charge; avoiding that requires a separately designed durable intermediate/recovery policy, not keeping a secret-bearing temporary path. Private blobs should remain private through failures.

Source-backed review notes

Rechecked on 9 October 2026: Python Speech synthesis, voice-specific SSML controls, Blob upload and identity dependencies, anonymous-access prerequisites, access tiers, Front Door/CDN lifecycle, and the HTML media preload contract. These are dated service/API contracts, not pinned package versions or a deployment test. Local checks exercise the actual Python worker with fake Speech/Blob dependencies to inspect branching and temporary-file cleanup; no SDK or service was executed. The earlier source passages below are retained as historical review evidence.

Pause / Recall / Apply

Can you explain it without the page?

Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.

Stored in this browser only. No account, no sync. Clearing browser data removes your record.