
Stepfun Asr
- 361 installs
- 1.3k repo stars
- Updated August 4, 2026
- daymade/claude-code-skills
Transcribe meetings, voice notes, or support calls via StepFun ASR APIs and feed text into summarization, search, or agent toolchains inside apps.
About
stepfun-asr teaches Claude how to call StepFun automatic speech recognition: prepare audio formats, manage API keys, poll or stream results, and attach transcripts to downstream LLM prompts. It supports voice-first agents, meeting capture, and accessibility features that need accurate Mandarin or multilingual transcription inside production codepaths.
- StepFun ASR API usage
- Streaming and batch audio
- Locale and format handling
- Transcript post-processing
- Voice-enabled agent tools
Stepfun Asr by the numbers
- 361 all-time installs (skills.sh)
- Ranked #2,114 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daymade/claude-code-skills --skill stepfun-asrAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 361 |
|---|---|
| repo stars | ★ 1.3k |
| Last updated | August 4, 2026 |
| Repository | daymade/claude-code-skills ↗ |
What it does
Transcribe meetings, voice notes, or support calls via StepFun ASR APIs and feed text into summarization, search, or agent toolchains inside apps.
Files
StepFun stepaudio-2.5-asr
Transcribe audio with StepFun's stepaudio-2.5-asr (released 2026-04, verified 2026-04-23). Long audio in one call, no chunking — but only if the request hits the right endpoint with the right body shape. The wrong endpoint returns an error that looks identical to "model doesn't exist", which is the #1 reason this skill exists.
Companion: for TTS withstepaudio-2.5-tts(the sibling model), use thestepfun-ttsskill — they share an API key but live on different endpoints with different body shapes.
Why this skill exists — three traps that cost hours
1. Wrong endpoint, wrong error. stepaudio-2.5-asr does not live on /v1/audio/transcriptions (that endpoint serves the older step-asr family). It lives on /v1/audio/asr/sse — SSE streaming, JSON body, base64 audio. Sending it to the wrong endpoint returns {"error":{"message":"model stepaudio-2.5-asr not supported"}}, which is identical in structure to a genuinely nonexistent model name. People waste hours filing whitelist tickets.
2. Plan key vs Normal key, silent failure. StepFun's "Plan" subscription keys (cheap, text-only) cannot call audio endpoints, but the failure manifests as a 4xx with no auth-shaped error message. If your account has a Plan subscription, you need a separate "Normal" key from the same console.
3. SSE error events are real. Censorship can fire on the ASR side too (rarely). Don't assume only transcript.text.delta and transcript.text.done events arrive — handle type: error events in the stream or you'll silently drop them.
Config and auth
API key resolves in this order (fail-fast, no defaults):
1. $STEPFUN_API_KEY environment variable 2. ${CLAUDE_PLUGIN_DATA}/config.json with {"api_key": "..."} (cross-session persistence)
First-time setup:
mkdir -p "${CLAUDE_PLUGIN_DATA}" && cat > "${CLAUDE_PLUGIN_DATA}/config.json" <<EOF
{"api_key": "<paste Normal key here>"}
EOFIf the user has not set a key, ask them to paste it — do not guess or use a placeholder. Get keys at https://platform.stepfun.com/ → API Keys. Use a Normal key, not a Plan key.
Quick start — single file
python3 scripts/asr_transcribe.py /path/to/audio.mp3Output: plain text transcription on stdout.
For machine-readable output with usage / timing:
python3 scripts/asr_transcribe.py /path/to/audio.mp3 --jsonFor non-Chinese audio:
python3 scripts/asr_transcribe.py /path/to/audio.mp3 --language enThe script handles base64 encoding, the nested {audio: {data, input: {transcription, format}}} body, SSE parsing, and the misleading-endpoint pitfall. Prefer it over hand-rolled HTTP calls unless integrating into a larger pipeline.
Decision table
| Scenario | Action |
|---|---|
| Short clip (< 5 min), Chinese or English, mp3/wav/ogg/opus | python3 scripts/asr_transcribe.py audio.mp3 |
| Long audio (5-30 min) | Same script — 32K context handles it in a single call, no chunking needed |
| Audio > 30 min | Split with ffmpeg before sending; the API rejects oversized payloads |
| Need usage/billing data | Add --json to capture usage.input_tokens / usage.total_tokens from transcript.text.done |
| Highly repetitive content (same phrase 5+ times, > 90s) | Cross-validate with step-asr-1.1 — see repetition hallucination in references/known_issues.md |
Hit model stepaudio-2.5-asr not supported | Wrong endpoint. Switch from /v1/audio/transcriptions to /v1/audio/asr/sse |
| Hit silent 4xx auth failure | Verify your key is "Normal" not "Plan" — Plan keys cannot call audio endpoints |
| Need to write raw HTTP (no Python) | Read references/api_reference.md for exact JSON body and SSE event shapes |
Supported audio formats
The script auto-detects from extension; pass --format to override:
| Extension | Format flag | Notes |
|---|---|---|
.mp3 | mp3 | Most common, default |
.wav | wav | Lossless |
.ogg | ogg | OGG container |
.opus | ogg | Opus codec in OGG container — pass through unchanged |
.pcm | pcm | Raw PCM — also requires format.rate, format.channel, format.bits (see API reference) |
For mp4/m4a/webm/etc., transcode to one of the above first via ffmpeg. Production pipelines often pre-transcode everything to OGG/Opus 16kHz mono to minimize base64 payload size.
Capacity and performance (verified 2026-04-23)
- 32K context window — single-call upper limit, no chunking needed for ≤ 30 min audio
- ~85-101× RTF on long audio (17.4 min audio → 10.4s wall clock)
- ~5.3× speedup vs step-asr-1.1 at the 100s+ length range
- Only ~2× speedup at the 5-15s range — the LLM spin-up cost dominates short clips. If your workload is many short clips, the migration ROI is modest
Common error patterns
| Error response | Actual cause | Fix |
|---|---|---|
"model stepaudio-2.5-asr not supported" on /v1/audio/transcriptions | Wrong endpoint | Switch to /v1/audio/asr/sse (script does this) |
| Silent 4xx with no auth message | Using a "Plan" key on audio endpoint | Get a "Normal" key from the StepFun console |
| ASR returns 3-4× expected character count | Repetition hallucination on highly-repetitive audio | Cross-validate with step-asr-1.1; see references/known_issues.md |
data: {"type":"error","message":"content blocked..."} mid-stream | Censorship fired on user-uploaded content | Handle SSE error event explicitly; don't assume only delta/done arrive |
More edge cases in references/known_issues.md.
Design invariants (do not break)
1. Always pass through SSE — don't try to buffer the response with a non-streaming client. The model emits transcript.text.delta for long audio; transcript.text.done carries the authoritative full text and usage. Reject the SSE format entirely and you'll get nothing. 2. Take final text from `transcript.text.done.text` — concatenated deltas can drift on edge cases. Deltas are for progressive UI; the done event is the source of truth. 3. Handle `error` events in the stream — don't treat the SSE stream as if only success events arrive. A blocked-content event mid-stream returns type: error with no done event. 4. Fail-fast on missing API key — never default to a placeholder or empty string. The script does this; preserve the behavior in any wrapper.
When to read references
references/api_reference.md— exact JSON request body, all fields, all SSE event types, response examples. Read when writing raw HTTP calls instead of using the bundled script.references/known_issues.md— repetition hallucination details, the wrong-endpoint diagnostic trail, Plan-vs-Normal key gotcha, ASR-side censorship handling, pricing opacity. Read when debugging anomalous output or evaluating whether to migrate fromstep-asr-1.1.
Pricing (verified 2026-04-23, volatile)
stepaudio-2.5-asr is in invitation beta as of 2026-04-23 — no public per-minute rate. The step-asr-1.1 baseline is 2.2 元/小时. The invitation PDF mentions "成本直降 80%" implying ~0.4 元/小时, but this is not yet on the pricing page. Re-verify at https://platform.stepfun.com/docs/zh/guides/pricing/details before quoting to stakeholders.
Next Step
ASR transcripts almost always benefit from one of two downstream steps. After producing the transcript, suggest the natural next skill:
Transcription complete. The output is raw text from the model — common next steps:
Options:
A) transcript-fixer — clean up ASR errors (homophones, segmentation, filler words). Recommended if the recording is a real-world conversation, podcast, or interview rather than read-aloud text
B) meeting-minutes-taker — turn the transcript into structured minutes with decisions, action items, and speaker attribution. Recommended if the recording is a meeting
C) No thanks — the raw transcript is what I neededSkip the suggestion when the user has already specified the downstream tool, or when the transcription was clearly a one-off lookup (e.g., "what does this 15-second clip say?").
Security scan passed
Scanned at: 2026-04-30T16:44:52.294771
Tool: gitleaks + pattern-based validation
Content hash: a0edbbb580527ed34ca4ae9bda443c6bd93a200434e85989c2a636d4435478a5
stepaudio-2.5-asr API Reference
Exact request/response shapes for stepaudio-2.5-asr. Verified 2026-04-23 against the live StepFun API. Read this when calling the API by hand (curl, custom HTTP client) instead of using the bundled scripts/asr_transcribe.py.
Endpoint (NOT the one you'd guess)
POST https://api.stepfun.com/v1/audio/asr/sse
Content-Type: application/json
Accept: text/event-stream
Authorization: Bearer <STEPFUN_API_KEY>Do NOT send stepaudio-2.5-asr to /v1/audio/transcriptions — that endpoint serves the older step-asr / step-asr-1.1 family and returns a misleading model stepaudio-2.5-asr not supported error which looks identical to a permission/whitelist error. See known_issues.md for the full diagnostic trail.
Request body
{
"audio": {
"data": "<base64-encoded audio bytes>",
"input": {
"transcription": {
"language": "zh",
"model": "stepaudio-2.5-asr",
"enable_itn": true
},
"format": {
"type": "mp3"
}
}
}
}| Path | Required | Type | Notes |
|---|---|---|---|
audio.data | yes | string | base64-encoded audio bytes. Accepts mp3, wav, ogg, opus (in ogg container), pcm |
audio.input.transcription.language | yes | string | zh or en. Dialects and Japanese are not officially supported |
audio.input.transcription.model | yes | string | Must be stepaudio-2.5-asr |
audio.input.transcription.enable_itn | no | bool | Inverse text normalization (数字→words). Default true |
audio.input.format.type | yes | string | mp3 / wav / ogg / pcm |
audio.input.format.rate | pcm only | int | Sample rate (required for raw PCM) |
audio.input.format.channel | pcm only | int | Channel count (required for raw PCM) |
audio.input.format.bits | optional | int | Sample depth, default 16 |
Response — SSE stream
The response is a Server-Sent Events stream. Each line is either empty or starts with data: . Three event types:
data: {"type":"transcript.text.delta","meta":{...},"delta":"你好,"}
data: {"type":"transcript.text.delta","meta":{...},"delta":"我是蕾格。"}
data: {"type":"transcript.text.done","meta":{...},"text":"你好,我是蕾格。","usage":{"type":"tokens","input_tokens":69,"input_token_details":{"text_tokens":69,"audio_tokens":0},"output_tokens":9,"total_tokens":78}}| Event type | Meaning | How to handle |
|---|---|---|
transcript.text.delta | Incremental piece of the transcription | Concatenate for progressive UI; optional if you only need final text |
transcript.text.done | Final, full transcription + usage | Take text as the authoritative result. Also contains usage for billing/telemetry |
error | Server-side error mid-stream | Abort and propagate message to the caller |
Capacity
- 32K context window
- Audio ≤ 30 min can be sent in a single call
- No client-side chunking needed for long audio (unlike step-asr)
- RTF 85-101× on Chinese speech verified 2026-04-23
Known error responses
{"error":{"message":"model stepaudio-2.5-asr not supported","type":"request_params_invalid"}}→ Wrong endpoint. Switch from /v1/audio/transcriptions to /v1/audio/asr/sse.
data: {"type":"error","message":"content blocked ..."}→ Content censorship (rare on ASR). Same triggers as TTS (death/disappearance/political terms).
Comparison with sibling and legacy endpoints
| Model | Endpoint | Request format |
|---|---|---|
stepaudio-2.5-asr (this skill) | /v1/audio/asr/sse | JSON + base64 audio + SSE response |
stepaudio-2.5-tts (sibling, see stepfun-tts skill) | /v1/audio/speech | JSON with instruction (no voice_label) |
step-asr / step-asr-1.1 (legacy) | /v1/audio/transcriptions | multipart/form-data |
step-tts-2 / step-tts-mini (legacy) | /v1/audio/speech | JSON with voice_label |
Legacy step-asr-1.1 is the fallback when stepaudio-2.5-asr hits the repetition-hallucination edge case (see known_issues.md). The endpoint and body shape are entirely different — multipart upload to /v1/audio/transcriptions, no SSE.
Auth and key handling
- Key header:
Authorization: Bearer <key> - Keys can be retrieved at https://platform.stepfun.com/ → API Keys
- "Plan" keys (cheaper subscription) are restricted to text models on
api.stepfun.com/step_plan. They cannot call audio endpoints. Use a "Normal" key for ASR calls. - Same key works for both TTS and ASR — no separate scopes
Rate / throughput notes (observed, not officially documented)
- Long audio ASR (17 min) has succeeded with
timeout=1200in the bundled script - ~400ms sleep between sequential requests avoids 429s in batch processing
- Single TCP connection per request — SSE stream is closed after
transcript.text.done
stepaudio-2.5-asr — Known Issues and Non-Obvious Behavior
Collected from end-to-end testing 2026-04-23. These are things that burned real time to discover; they are not in the official docs.
Wrong endpoint gives a misleading error (the #1 trap)
Symptom: Calling /v1/audio/transcriptions with model=stepaudio-2.5-asr:
{"error":{"message":"model stepaudio-2.5-asr not supported","type":"request_params_invalid"}}This response is identical in structure to sending a genuinely nonexistent model name. It takes real debugging to realize the model exists but on a different endpoint.
Diagnostic sequence that wastes the least time:
1. Try step-asr on the same endpoint — if it works, endpoint access is fine 2. Check the /v1/audio/asr/sse endpoint (the actual stepaudio-2.5-asr home) 3. If both fail, THEN ask BD about whitelist
Don't assume "permission denied" on the first error.
ASR repetition hallucination (real, bounded)
Symptom: Transcribe a TTS-generated audio of highly-repetitive Chinese text (e.g., the same 60-char sentence repeated 10 times) and stepaudio-2.5-asr returns 3-4× the expected character count, with the same sentence restated many extra times in the output.
This is a genuine model hallucination, not a transport bug. Verified by:
1. MD5 diff — run1 vs run2 of the same TTS input produce different audio files (not file corruption) 2. Determinism — re-running ASR on the same audio gives the same 4× output every time (not transient noise) 3. Cross-validation — step-asr and step-asr-1.1 on the exact same audio return the correct character count (~800 chars for 800 input), so the audio itself is fine 4. ffprobe confirms audio duration is normal (~219s for 800 chars at typical speed)
Conclusion: The LLM-based ASR sees a repetitive pattern in the audio and "continues predicting" repetitions that aren't there.
When it triggers:
- Audio duration > 90s AND
- Content is highly repetitive (same phrase appearing 5+ times)
Doesn't trigger on real-world content:
- Podcasts, interviews, varied dialogue, stories — all fine
- Even 17.4-minute audio from 90 different TTS segments: returns correct 6332 chars, RTF 101×
Workaround for edge cases:
- If your domain has genuinely repetitive content (e.g., IVR transcripts, repeated sloganeering), cross-validate with
step-asr-1.1on random samples - For most workflows: just use it; the hallucination mode is exotic
ASR speed scales non-linearly — short audio is a trap
Observation: The headline "5.9× faster than step-asr" from the marketing is true for long audio but misleading for short clips.
| Audio length | stepaudio-2.5-asr | step-asr-1.1 | Speedup |
|---|---|---|---|
| 5-15s clips | ~500ms | ~900ms | 2.0× |
| 115s audio | 1.36s | 7.16s | 5.3× |
| 1046s (17.4 min) | 10.4s | (would need chunking) | ~101× RTF |
Why: The LLM + MTP-5 fusion overhead is amortized over longer contexts. Short requests pay the model-spin-up cost.
Practical implication: If your workload is many short (<10s) clips, the speedup over step-asr-1.1 is modest — 2× not 5×. If your workload is long audio (>2 min), the difference is dramatic and you should migrate.
"Plan key" vs "Normal key" — silent auth failure
StepFun sells a cheap "Plan" subscription for text models (step_plan endpoint). Plan keys cannot call audio endpoints. This silently manifests as 4xx errors that don't mention auth at all.
If you hit auth-shaped failures and your account has a Plan subscription, verify you're using a Normal key (different value, obtained separately in the StepFun console under the same "API Keys" page).
Censorship can fire on the ASR side too
Observed once (rare): An ASR request on a user-uploaded recording of political content returned:
data: {"type":"error","message":"content blocked ..."}Handle the error event type in the SSE stream — don't assume only delta and done events fire. If your code only handles transcript.text.delta and transcript.text.done, a blocked-content event is silently dropped and the request appears to return empty text with no error surfaced to the caller.
The bundled scripts/asr_transcribe.py handles this correctly — see _consume_sse() for the pattern.
Pricing opacity
As of 2026-04-23, stepaudio-2.5-asr is in invitation beta. No public per-minute rate. step-asr-1.1 baseline is 2.2 元/小时. The invitation PDF mentions "成本直降 80%" implying roughly 0.4 元/小时, but this is not yet on the pricing page. Do not quote a price to a stakeholder without re-verifying at https://platform.stepfun.com/docs/zh/guides/pricing/details.
Empty transcript with no error
Symptom: SSE stream completes normally but transcript.text.done.text is empty string.
Possible causes: 1. Audio is silent / pure noise / corrupted 2. Audio language doesn't match the language parameter (e.g., sending English audio with language: zh) 3. Audio format mismatch (e.g., format.type: mp3 but actual bytes are wav)
The bundled script falls back to concatenating delta chunks if the done event has empty text — but if both are empty, the issue is upstream (the audio itself, not the API).
Long-audio timeout behavior
The default urllib/requests timeout is too short for 17+ minute audio. The bundled script uses timeout=1200 (20 minutes). If you write your own client, set the timeout to at least 2× expected wall clock time (RTF ~100× means 17 min audio takes ~10s wall clock, but TCP retries and network jitter can stretch this).
#!/usr/bin/env python3
"""
stepaudio-2.5-asr transcription — single file, SSE endpoint.
Endpoint: POST https://api.stepfun.com/v1/audio/asr/sse (NOT /v1/audio/transcriptions)
Why a dedicated script: naive implementations try to reuse the step-asr-era endpoint
(/v1/audio/transcriptions with multipart), get back `model stepaudio-2.5-asr not supported`,
and waste time debugging what looks like a model/permission issue. The actual cause is
that stepaudio-2.5-asr is a different endpoint entirely — SSE streaming, JSON body,
base64-encoded audio.
Handles:
- Auto-detects audio format from file extension (mp3 / wav / ogg / pcm)
- base64 encodes and wraps in the nested {audio: {data, input: {transcription, format}}} body
- Parses SSE stream: collects transcript.text.delta, returns transcript.text.done.text
- Flags "content blocked" errors distinctly from transport errors
- 32K context / up to ~30 min audio in a single call — no client-side chunking needed
Usage:
python3 asr_transcribe.py path/to/audio.mp3
python3 asr_transcribe.py path/to/audio.mp3 --json # include usage (tokens/timing)
python3 asr_transcribe.py path/to/audio.mp3 --language zh
"""
from __future__ import annotations
import argparse
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
ASR_URL = "https://api.stepfun.com/v1/audio/asr/sse"
MODEL = "stepaudio-2.5-asr"
# Extensions that StepAudio 2.5 ASR accepts natively (no conversion needed)
EXT_TO_FORMAT = {
".mp3": "mp3",
".wav": "wav",
".ogg": "ogg",
".opus": "ogg", # opus in ogg container
".pcm": "pcm",
}
def load_api_key() -> str:
"""Env first, then ${CLAUDE_PLUGIN_DATA}/config.json. Fail fast."""
k = os.environ.get("STEPFUN_API_KEY", "").strip()
if k:
return k
plugin_data = os.environ.get("CLAUDE_PLUGIN_DATA", "").strip()
if plugin_data:
cfg = Path(plugin_data) / "config.json"
if cfg.exists():
try:
k = json.loads(cfg.read_text()).get("api_key", "").strip()
if k:
return k
except json.JSONDecodeError:
pass
print(
"ERROR: no API key found.\n"
" Set $STEPFUN_API_KEY, or create ${CLAUDE_PLUGIN_DATA}/config.json with {\"api_key\": \"...\"}",
file=sys.stderr,
)
sys.exit(2)
def detect_format(path: Path, override: str | None) -> str:
if override:
return override
fmt = EXT_TO_FORMAT.get(path.suffix.lower())
if not fmt:
print(
f"ERROR: cannot detect audio format from extension {path.suffix!r}.\n"
f" Supported: {', '.join(EXT_TO_FORMAT)}.\n"
f" Or pass --format explicitly.",
file=sys.stderr,
)
sys.exit(2)
return fmt
def transcribe(
*,
api_key: str,
audio_path: Path,
audio_format: str,
language: str = "zh",
enable_itn: bool = True,
timeout: int = 1200,
) -> dict[str, Any]:
"""
Returns {ok, text?, usage?, elapsed, deltas_count, err?, censored?}.
Parses the SSE stream; takes the text from transcript.text.done.
"""
audio_b64 = base64.b64encode(audio_path.read_bytes()).decode("ascii")
body = json.dumps(
{
"audio": {
"data": audio_b64,
"input": {
"transcription": {
"language": language,
"model": MODEL,
"enable_itn": enable_itn,
},
"format": {"type": audio_format},
},
}
}
).encode()
req = urllib.request.Request(
ASR_URL,
data=body,
method="POST",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
)
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as e:
err = e.read().decode(errors="replace")[:500]
censored = "censorship" in err.lower() or "blocked" in err.lower()
return {"ok": False, "status": e.code, "err": err, "elapsed": time.time() - t0, "censored": censored}
elapsed = time.time() - t0
text = ""
usage: dict[str, Any] | None = None
deltas = 0
errors: list[str] = []
for line in raw.splitlines():
if not line.startswith("data:"):
continue
payload = line[5:].strip()
if not payload:
continue
try:
ev = json.loads(payload)
except json.JSONDecodeError:
continue
t = ev.get("type")
if t == "transcript.text.delta":
deltas += 1
elif t == "transcript.text.done":
text = ev.get("text", "")
usage = ev.get("usage")
elif t == "error":
errors.append(ev.get("message", ""))
if not text and errors:
return {"ok": False, "status": 200, "err": "; ".join(errors), "elapsed": elapsed}
return {"ok": True, "text": text, "usage": usage, "elapsed": elapsed, "deltas_count": deltas}
def main() -> int:
ap = argparse.ArgumentParser(description="stepaudio-2.5-asr transcription (SSE endpoint)")
ap.add_argument("audio", type=Path, help="Path to audio file (mp3/wav/ogg/opus/pcm)")
ap.add_argument("--language", default="zh", help="Language code (zh/en). Default: zh")
ap.add_argument("--format", help="Audio format override (mp3/wav/ogg/pcm)")
ap.add_argument("--no-itn", action="store_true", help="Disable inverse text normalization")
ap.add_argument("--json", action="store_true", help="Output full JSON (text + usage + timing)")
args = ap.parse_args()
if not args.audio.exists():
print(f"ERROR: audio file not found: {args.audio}", file=sys.stderr)
return 2
api_key = load_api_key()
fmt = detect_format(args.audio, args.format)
result = transcribe(
api_key=api_key,
audio_path=args.audio,
audio_format=fmt,
language=args.language,
enable_itn=not args.no_itn,
)
if not result["ok"]:
if result.get("censored"):
print("ERROR: content blocked by StepFun censorship. The audio likely contains sensitive content.", file=sys.stderr)
else:
print(f"ERROR status={result.get('status')}: {result.get('err', '')}", file=sys.stderr)
return 1
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(result["text"])
return 0
if __name__ == "__main__":
sys.exit(main())