# SkillPatch skill: azure-speech-to-text-rest-py

This skill provides a Python-based workflow for transcribing short audio files (up to 60 seconds) using the Azure Speech to Text REST API — no SDK required. It covers environment setup, HTTP request construction, audio format requirements, response parsing (simple and detailed formats), and chunked transfer for lower latency. It explicitly scopes itself to short-audio use cases and directs users to alternative APIs for longer or streaming scenarios.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/azure-speech-to-text-rest-py
curl -sSL https://skillpatch.dev/install_skill/azure-speech-to-text-rest-py | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (2)

- `SKILL.md`
- `references/pronunciation-assessment.md`


### `SKILL.md`

````markdown
---
name: azure-speech-to-text-rest-py
description: |-
  Azure Speech to Text REST API for short audio (Python). Use for simple speech recognition of audio files up to 60 seconds without the Speech SDK.
  Triggers: "speech to text REST", "short audio transcription", "speech recognition REST API", "STT REST", "recognize speech REST".
  DO NOT USE FOR: Long audio (>60 seconds), real-time streaming, batch transcription, custom speech models, speech translation. Use Speech SDK or Batch Transcription API instead.
license: MIT
metadata:
  author: Microsoft
  version: "1.0.0"
---

# Azure Speech to Text REST API for Short Audio

Simple REST API for speech-to-text transcription of short audio files (up to 60 seconds). No SDK required - just HTTP requests.

## Prerequisites

1. **Azure subscription** - [Create one free](https://azure.microsoft.com/free/)
2. **Speech resource** - Create in [Azure Portal](https://portal.azure.com/#create/Microsoft.CognitiveServicesSpeechServices)
3. **Get credentials** - After deployment, go to resource > Keys and Endpoint

## Environment Variables

```bash
# Required
AZURE_SPEECH_KEY=<your-speech-resource-key>
AZURE_SPEECH_REGION=<region>  # e.g., eastus, westus2, westeurope

# Alternative: Use endpoint directly
AZURE_SPEECH_ENDPOINT=https://<region>.stt.speech.microsoft.com
```

## Installation

```bash
pip install requests
```

## Quick Start

```python
import os
import requests

def transcribe_audio(audio_file_path: str, language: str = "en-US") -> dict:
    """Transcribe short audio file (max 60 seconds) using REST API."""
    region = os.environ["AZURE_SPEECH_REGION"]
    api_key = os.environ["AZURE_SPEECH_KEY"]
    
    url = f"https://{region}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1"
    
    headers = {
        "Ocp-Apim-Subscription-Key": api_key,
        "Content-Type": "audio/wav; codecs=audio/pcm; samplerate=16000",
        "Accept": "application/json"
    }
    
    params = {
        "language": language,
        "format": "detailed"  # or "simple"
    }
    
    with open(audio_file_path, "rb") as audio_file:
        response = requests.post(url, headers=headers, params=params, data=audio_file)
    
    response.raise_for_status()
    return response.json()

# Usage
result = transcribe_audio("audio.wav", "en-US")
print(result["DisplayText"])
```

## Audio Requirements

| Format | Codec | Sample Rate | Notes |
|--------|-------|-------------|-------|
| WAV | PCM | 16 kHz, mono | **Recommended** |
| OGG | OPUS | 16 kHz, mono | Smaller file size |

**Limitations:**
- Maximum 60 seconds of audio
- For pronunciation assessment: maximum 30 seconds
- No partial/interim results (final only)

## Content-Type Headers

```python
# WAV PCM 16kHz
"Content-Type": "audio/wav; codecs=audio/pcm; samplerate=16000"

# OGG OPUS
"Content-Type": "audio/ogg; codecs=opus"
```

## Response Formats

### Simple Format (default)

```python
params = {"language": "en-US", "format": "simple"}
```

```json
{
  "RecognitionStatus": "Success",
  "DisplayText": "Remind me to buy 5 pencils.",
  "Offset": "1236645672289",
  "Duration": "1236645672289"
}
```

### Detailed Format

```python
params = {"language": "en-US", "format": "detailed"}
```

```json
{
  "RecognitionStatus": "Success",
  "Offset": "1236645672289",
  "Duration": "1236645672289",
  "NBest": [
    {
      "Confidence": 0.9052885,
      "Display": "What's the weather like?",
      "ITN": "what's the weather like",
      "Lexical": "what's the weather like",
      "MaskedITN": "what's the weather like"
    }
  ]
}
```

## Chunked Transfer (Recommended)

For lower latency, stream audio in chunks:

```python
import os
import requests

def transcribe_chunked(audio_file_path: str, language: str = "en-US") -> dict:
    """Stream audio in chunks for lower latency."""
    region = os.environ["AZURE_SPEECH_REGION"]
    api_key = os.environ["AZURE_SPEECH_KEY"]
    
    url = f"https://{region}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1"
    
    headers = {
        "Ocp-Apim-Subscription-Key": api_key,
        "Content-Type": "audio/wav; codecs=audio/pcm; samplerate=16000",
        "Accept": "application/json",
        "Transfer-Encoding": "chunked",
        "Expect": "100-continue"
    }
    
    params = {"language": language, "format": "detailed"}
    
    def generate_chunks(file_path: str, chunk_size: int = 1024):
        with open(file_path, "rb") as f:
            while chunk := f.read(chunk_size):
                yield chunk
    
    response = requests.post(
        url, 
        headers=headers, 
        params=params, 
        data=generate_chunks(audio_file_path)
    )
    
    response.raise_for_status()
    return response.json()
```

## Authentication Options

### Option 1: Subscription Key (Simple)

```python
headers = {
    "Ocp-Apim-Subscription-Key": os.environ["AZURE_SPEECH_KEY"]
}
```

### Option 2: Bearer Token

```python
import requests
import os

def get_access_token() -> str:
    """Get access token from the token endpoint."""
    region = os.environ["AZURE_SPEECH_REGION"]
    api_key = os.environ["AZURE_SPEECH_KEY"]
    
    token_url = f"https://{region}.api.cognitive.microsoft.com/sts/v1.0/issueToken"
    
    response = requests.post(
        token_url,
        headers={
            "Ocp-Apim-Subscription-Key": api_key,
            "Content-Type": "application/x-www-form-urlencoded",
            "Content-Length": "0"
        }
    )
    response.raise_for_status()
    return response.text

# Use token in requests (valid for 10 minutes)
token = get_access_token()
headers = {
    "Authorization": f"Bearer {token}",
    "Content-Type": "audio/wav; codecs=audio/pcm; samplerate=16000",
    "Accept": "application/json"
}
```

## Query Parameters

| Parameter | Required | Values | Description |
|-----------|----------|--------|-------------|
| `language` | **Yes** | `en-US`, `de-DE`, etc. | Language of speech |
| `format` | No | `simple`, `detailed` | Result format (default: simple) |
| `profanity` | No | `masked`, `removed`, `raw` | Profanity handling (default: masked) |

## Recognition Status Values

| Status | Description |
|--------|-------------|
| `Success` | Recognition succeeded |
| `NoMatch` | Speech detected but no words matched |
| `InitialSilenceTimeout` | Only silence detected |
| `BabbleTimeout` | Only noise detected |
| `Error` | Internal service error |

## Profanity Handling

```python
# Mask profanity with asterisks (default)
params = {"language": "en-US", "profanity": "masked"}

# Remove profanity entirely
params = {"language": "en-US", "profanity": "removed"}

# Include profanity as-is
params = {"language": "en-US", "profanity": "raw"}
```

## Error Handling

```python
import requests

def transcribe_with_error_handling(audio_path: str, language: str = "en-US") -> dict | None:
    """Transcribe with proper error handling."""
    region = os.environ["AZURE_SPEECH_REGION"]
    api_key = os.environ["AZURE_SPEECH_KEY"]
    
    url = f"https://{region}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1"
    
    try:
        with open(audio_path, "rb") as audio_file:
            response = requests.post(
                url,
                headers={
                    "Ocp-Apim-Subscription-Key": api_key,
                    "Content-Type": "audio/wav; codecs=audio/pcm; samplerate=16000",
                    "Accept": "application/json"
                },
                params={"language": language, "format": "detailed"},
                data=audio_file
            )
        
        if response.status_code == 200:
            result = response.json()
            if result.get("RecognitionStatus") == "Success":
                return result
            else:
                print(f"Recognition failed: {result.get('RecognitionStatus')}")
                return None
        elif response.status_code == 400:
            print(f"Bad request: Check language code or audio format")
        elif response.status_code == 401:
            print(f"Unauthorized: Check API key or token")
        elif response.status_code == 403:
            print(f"Forbidden: Missing authorization header")
        else:
            print(f"Error {response.status_code}: {response.text}")
        
        return None
        
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        return None
```

## Async Version

```python
import os
import aiohttp
import asyncio

async def transcribe_async(audio_file_path: str, language: str = "en-US") -> dict:
    """Async version using aiohttp."""
    region = os.environ["AZURE_SPEECH_REGION"]
    api_key = os.environ["AZURE_SPEECH_KEY"]
    
    url = f"https://{region}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1"
    
    headers = {
        "Ocp-Apim-Subscription-Key": api_key,
        "Content-Type": "audio/wav; codecs=audio/pcm; samplerate=16000",
        "Accept": "application/json"
    }
    
    params = {"language": language, "format": "detailed"}
    
    async with aiohttp.ClientSession() as session:
        with open(audio_file_path, "rb") as f:
            audio_data = f.read()
        
        async with session.post(url, headers=headers, params=params, data=audio_data) as response:
            response.raise_for_status()
            return await response.json()

# Usage
result = asyncio.run(transcribe_async("audio.wav", "en-US"))
print(result["DisplayText"])
```

## Supported Languages

Common language codes (see [full list](https://learn.microsoft.com/azure/ai-services/speech-service/language-support)):

| Code | Language |
|------|----------|
| `en-US` | English (US) |
| `en-GB` | English (UK) |
| `de-DE` | German |
| `fr-FR` | French |
| `es-ES` | Spanish (Spain) |
| `es-MX` | Spanish (Mexico) |
| `zh-CN` | Chinese (Mandarin) |
| `ja-JP` | Japanese |
| `ko-KR` | Korean |
| `pt-BR` | Portuguese (Brazil) |

## Best Practices

1. **Pick sync OR async and stay consistent.** Do not mix `azure.xxx` sync clients with `azure.xxx.aio` async clients in the same call path. Choose one mode per module.
2. **Always use context managers for clients.** Use `with httpx.Client(...) as client:` (sync) or `async with httpx.AsyncClient(...) as client:` (async) so connections are pooled and closed deterministically.
3. **Use WAV PCM 16kHz mono** for best compatibility
4. **Enable chunked transfer** for lower latency
5. **Cache access tokens** for 9 minutes (valid for 10)
6. **Specify the correct language** for accurate recognition
7. **Use detailed format** when you need confidence scores
8. **Handle all RecognitionStatus values** in production code

## When NOT to Use This API

Use the Speech SDK or Batch Transcription API instead when you need:

- Audio longer than 60 seconds
- Real-time streaming transcription
- Partial/interim results
- Speech translation
- Custom speech models
- Batch transcription of many files

## Reference Files

| File | Contents |
|------|----------|
| [references/pronunciation-assessment.md](references/pronunciation-assessment.md) | Pronunciation assessment parameters and scoring |

````


### `references/pronunciation-assessment.md`

````markdown
# Pronunciation Assessment

Evaluate pronunciation quality of speech input using the REST API for short audio.

## Limitations

- Maximum 30 seconds of audio (vs 60 seconds for regular transcription)
- Requires reference text to compare against

## Headers

Add the `Pronunciation-Assessment` header with Base64-encoded JSON parameters:

```python
import base64
import json
import os
import requests

def transcribe_with_pronunciation_assessment(
    audio_file_path: str,
    reference_text: str,
    language: str = "en-US"
) -> dict:
    """Transcribe with pronunciation assessment."""
    region = os.environ["AZURE_SPEECH_REGION"]
    api_key = os.environ["AZURE_SPEECH_KEY"]
    
    url = f"https://{region}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1"
    
    # Build pronunciation assessment parameters
    pron_params = {
        "ReferenceText": reference_text,
        "GradingSystem": "HundredMark",
        "Granularity": "Word",
        "Dimension": "Comprehensive",
        "EnableProsodyAssessment": True
    }
    
    # Base64 encode the JSON
    pron_header = base64.b64encode(
        json.dumps(pron_params).encode("utf-8")
    ).decode("utf-8")
    
    headers = {
        "Ocp-Apim-Subscription-Key": api_key,
        "Content-Type": "audio/wav; codecs=audio/pcm; samplerate=16000",
        "Accept": "application/json",
        "Pronunciation-Assessment": pron_header
    }
    
    params = {"language": language, "format": "detailed"}
    
    with open(audio_file_path, "rb") as audio_file:
        response = requests.post(url, headers=headers, params=params, data=audio_file)
    
    response.raise_for_status()
    return response.json()

# Usage
result = transcribe_with_pronunciation_assessment(
    "greeting.wav",
    "Good morning.",
    "en-US"
)

# Access pronunciation scores
if result["RecognitionStatus"] == "Success":
    nbest = result["NBest"][0]
    print(f"Accuracy: {nbest['AccuracyScore']}")
    print(f"Fluency: {nbest['FluencyScore']}")
    print(f"Prosody: {nbest.get('ProsodyScore', 'N/A')}")
    print(f"Completeness: {nbest['CompletenessScore']}")
    print(f"Overall: {nbest['PronScore']}")
```

## Parameters

| Parameter | Required | Description |
|-----------|----------|-------------|
| `ReferenceText` | **Yes** | Text the pronunciation is evaluated against |
| `GradingSystem` | No | `FivePoint` (0-5) or `HundredMark` (0-100). Default: `FivePoint` |
| `Granularity` | No | `Phoneme`, `Word`, or `FullText`. Default: `Phoneme` |
| `Dimension` | No | `Basic` (accuracy only) or `Comprehensive`. Default: `Basic` |
| `EnableMiscue` | No | Enable omission/insertion detection. Default: `False` |
| `EnableProsodyAssessment` | No | Enable prosody scoring. Default: `False` |
| `ScenarioId` | No | GUID for custom point system |

## Grading Systems

### FivePoint (Default)
- Returns floating-point scores from 0.0 to 5.0

### HundredMark
- Returns floating-point scores from 0.0 to 100.0
- More intuitive for most applications

## Granularity Levels

### Phoneme (Default)
Shows scores at full-text, word, and phoneme levels:
```json
{
  "AccuracyScore": 95.0,
  "Words": [
    {
      "Word": "good",
      "AccuracyScore": 100.0,
      "Phonemes": [
        {"Phoneme": "g", "AccuracyScore": 100.0},
        {"Phoneme": "uh", "AccuracyScore": 95.0},
        {"Phoneme": "d", "AccuracyScore": 100.0}
      ]
    }
  ]
}
```

### Word
Shows scores at full-text and word levels only.

### FullText
Shows score at full-text level only.

## Response with Pronunciation Assessment

```json
{
  "RecognitionStatus": "Success",
  "Offset": 700000,
  "Duration": 8400000,
  "DisplayText": "Good morning.",
  "SNR": 38.76819,
  "NBest": [
    {
      "Confidence": 0.98503506,
      "Lexical": "good morning",
      "Display": "Good morning.",
      "AccuracyScore": 100.0,
      "FluencyScore": 100.0,
      "ProsodyScore": 87.8,
      "CompletenessScore": 100.0,
      "PronScore": 95.1,
      "Words": [
        {
          "Word": "good",
          "Offset": 700000,
          "Duration": 2600000,
          "AccuracyScore": 100.0,
          "ErrorType": "None",
          "Feedback": {
            "Prosody": {
              "Break": {
                "ErrorTypes": ["None"],
                "BreakLength": 0
              },
              "Intonation": {
                "ErrorTypes": [],
                "Monotone": {
                  "Confidence": 0.0
                }
              }
            }
          }
        },
        {
          "Word": "morning",
          "Offset": 3400000,
          "Duration": 5700000,
          "AccuracyScore": 100.0,
          "ErrorType": "None"
        }
      ]
    }
  ]
}
```

## Score Definitions

| Score | Description |
|-------|-------------|
| `AccuracyScore` | How closely phonemes match native speaker pronunciation |
| `FluencyScore` | How well speech matches native speaker's use of silent breaks |
| `ProsodyScore` | Naturalness including stress, intonation, speaking speed, rhythm |
| `CompletenessScore` | Ratio of pronounced words to reference text |
| `PronScore` | Overall pronunciation quality (weighted aggregate) |

## Error Types

When `EnableMiscue` is true, words include `ErrorType`:

| ErrorType | Description |
|-----------|-------------|
| `None` | Word pronounced correctly |
| `Omission` | Word in reference was skipped |
| `Insertion` | Extra word not in reference |
| `Mispronunciation` | Word pronounced incorrectly |

## Complete Example with All Features

```python
import base64
import json
import os
import requests

def full_pronunciation_assessment(
    audio_path: str,
    reference_text: str,
    language: str = "en-US"
) -> None:
    """Full pronunciation assessment with detailed output."""
    region = os.environ["AZURE_SPEECH_REGION"]
    api_key = os.environ["AZURE_SPEECH_KEY"]
    
    url = f"https://{region}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1"
    
    pron_params = {
        "ReferenceText": reference_text,
        "GradingSystem": "HundredMark",
        "Granularity": "Word",
        "Dimension": "Comprehensive",
        "EnableMiscue": True,
        "EnableProsodyAssessment": True
    }
    
    pron_header = base64.b64encode(
        json.dumps(pron_params).encode()
    ).decode()
    
    headers = {
        "Ocp-Apim-Subscription-Key": api_key,
        "Content-Type": "audio/wav; codecs=audio/pcm; samplerate=16000",
        "Accept": "application/json",
        "Pronunciation-Assessment": pron_header
    }
    
    with open(audio_path, "rb") as f:
        response = requests.post(
            url,
            headers=headers,
            params={"language": language, "format": "detailed"},
            data=f
        )
    
    response.raise_for_status()
    result = response.json()
    
    if result["RecognitionStatus"] != "Success":
        print(f"Recognition failed: {result['RecognitionStatus']}")
        return
    
    nbest = result["NBest"][0]
    
    print(f"\n=== Overall Scores ===")
    print(f"Accuracy:     {nbest['AccuracyScore']:.1f}")
    print(f"Fluency:      {nbest['FluencyScore']:.1f}")
    print(f"Prosody:      {nbest.get('ProsodyScore', 'N/A')}")
    print(f"Completeness: {nbest['CompletenessScore']:.1f}")
    print(f"Overall:      {nbest['PronScore']:.1f}")
    
    print(f"\n=== Word-Level Analysis ===")
    for word in nbest["Words"]:
        error = word.get("ErrorType", "None")
        accuracy = word.get("AccuracyScore", 0)
        marker = "" if error == "None" else f" [{error}]"
        print(f"  {word['Word']}: {accuracy:.1f}{marker}")

# Usage
full_pronunciation_assessment(
    "user_speech.wav",
    "The quick brown fox jumps over the lazy dog.",
    "en-US"
)
```

````
