Audio-to-text transcription: a worked example

Keep the untouched transcript and edit a separate copy, with each wording change tied to the recording or a documented source. This audio-to-text transcription example follows three output states: no spoken text, readable words with suspect wording, and timed text without speaker labels. The examples use short synthetic recordings, their supplied scripts, and saved conversion results. Recognition accuracy on natural conversations remains unmeasured.

Classify the output before editing

Empty text calls for a conversion diagnosis; readable text calls for wording and structure decisions. A saved Markdown file can contain a transcript, metadata, or nothing. Open the output in a text editor and look for the spoken content before choosing an editing style.

What you see How to identify the case Next action
No spoken text The body is empty, or contains only file metadata Inspect the speech dependencies and selected conversion route
Readable text with questionable wording A word disagrees with the recording or documented terminology Preserve the raw result and record the basis for each correction
Time ranges without speaker labels Text blocks have start/end markers but no identified speaker Use the markers to locate audio and supply speaker attribution from separate evidence

These cases overlap. Our saved audio result contains both questionable wording and time ranges without speaker labels. A transcript intended for quotation needs wording checked against the recording. A reading copy can include documented edits, while a subtitle file also needs suitable display cues. W3C's transcription guidance treats accurate wording and caption presentation as related tasks with different requirements. 1

MarkItDown can return no spoken text

MarkItDown returned no spoken text in our installed environment because its audio dependencies were absent. We ran the public local-file conversion call against the supplied audio and video samples. Both calls returned normally, and both Markdown bodies were empty. This was a missing-feature diagnosis, with no recognition request made. 131417

from pathlib import Path
        from markitdown import MarkItDown
        
        result = MarkItDown(enable_plugins=False).convert_local("input.mp3")
        Path("output.md").write_text(result.markdown, encoding="utf-8")
        print(repr(result.markdown))
        

Output from our audio dependency diagnostic sample, unedited:

conversion_returned: true
        markdown_text: ''
        speech_dependencies: missing (speech_recognition, pydub)
        

The transcript file itself is empty. The diagnostic records the returned value and missing modules. A different installation can return different content, including metadata even when transcription is unavailable.

MarkItDown's documented audio extra supplies optional speech dependencies. In the inspected implementation, the transcription helper raises an exception when those imports failed. The audio converter catches that specific exception and continues to return the content collected so far. An input without useful metadata can therefore yield an empty result without a conversion error. 345

The installed helper uses Google recognition when the dependencies are available. Passing an LLM client does not replace that call in this built-in route. A GitHub report describes the same routing concern, and the inspected source confirms the call. We did not execute that network branch. The comparison here concerns the installed environment, with no basis for ranking the tools' speech recognition accuracy. 56

Condition Visible consequence Interpretation
Audio dependencies are absent No transcript, possibly some metadata Speech conversion was unavailable in this configuration
Dependencies are present A recognition call can run Network service behavior remains a separate dependency
Text exists but has no timing or identity data A readable passage without those annotations The returned prose alone cannot supply aligned words or named speakers

Markovo: a timestamped audio-to-text example

Markovo produced readable, timed text for the supplied audio, with wording that still differed from its source script. The saved result came from authenticated audio-to-Markdown conversion, which the site's capability reference classifies as Signed-in Beta. It includes language information and time ranges, but the contract does not promise speaker identification or a verbatim transcript. 891619

Output from our synthetic spoken-script sample, unedited:

**[00:00:00.000 → 00:00:07.540]** Root audio contract sample 01. Markovo Quality Lab is verifying a real media conversion route.
        
        **[00:00:07.760 → 00:00:17.700]** This is sample 1 of 8. The target number is 126. The speaker name Shanghai, OpenAI, and the Quality Lab.
        
        **[00:00:17.920 → 00:00:23.680]** The transcript begins at second zero and the root contract ends after this checklist statement.
        

The supplied script spells the opening word "Route". The downloaded text says "Root", and the same spelling difference appears near the end. Those words can share a pronunciation, so playback alone may leave their spelling unresolved. The script supplies a documented basis for a terminology edit in this synthetic example. It does not establish how often the recognizer makes that choice on other recordings. 78

The output also changes the script's "speaker names" to "speaker name". Repairing the grammar by sight would hide the distinction between correcting a recognition error and rewriting what someone said. The source script is useful context, but an independently checked transcript would require listening to the recording. The supplied script has not been independently checked against the audio for this guide.

The standard-library client submits an MP3, polls the returned job until it succeeds, and downloads Markdown. Set MARKOVO_API_KEY and an explicit MARKOVO_MAX_CREDIT_UNITS value in the environment. The request fields follow the repository's developer-reference snapshot. We checked the client by replaying the saved responses locally, without submitting another job. 1516

import json
        import os
        from pathlib import Path
        import secrets
        import time
        from urllib.request import Request, urlopen
        
        
        def convert(source, destination):
            base = "https://markovo.net"
            headers = {"Authorization": "Bearer " + os.environ["MARKOVO_API_KEY"]}
            boundary = "----audio" + secrets.token_hex(12)
            fields = {"capability_id": "audio-to-markdown", "mode": "fast",
                      "max_credit_units": str(int(os.environ["MARKOVO_MAX_CREDIT_UNITS"]))}
            parts = [f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode()
                     for k, v in fields.items()]
            parts += [f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="input.mp3"\r\nContent-Type: audio/mpeg\r\n\r\n'.encode(),
                      Path(source).read_bytes(), f'\r\n--{boundary}--\r\n'.encode()]
            upload_headers = {**headers, "Content-Type": "multipart/form-data; boundary=" + boundary}
        
            # Submit once. Do not automatically replay an ambiguous POST response.
            with urlopen(Request(base + "/v1/convert", data=b"".join(parts),
                                 headers=upload_headers), timeout=60) as response:
                job_id = json.load(response)["job_id"]
        
            deadline = time.monotonic() + 300
            while time.monotonic() < deadline:
                with urlopen(Request(base + "/v1/jobs/" + job_id, headers=headers), timeout=60) as response:
                    job = json.load(response)["job"]
                if job["status"] == "succeeded":
                    break
                if job["status"] in {"failed", "cancelled"}:
                    raise RuntimeError(job.get("error") or job["status"])
                time.sleep(2)
            else:
                raise TimeoutError("Job did not finish within the polling window")
        
            with urlopen(Request(base + "/v1/jobs/" + job_id + "/download?format=md",
                                 headers=headers), timeout=60) as response:
                Path(destination).write_bytes(response.read())
        
        
        if __name__ == "__main__":
            convert("input.mp3", "output.md")
        

The CLI was not executed for this guide.

The saved video result also contains a transcript, but groups the spoken passage into a single timed block. Its source script differs from the audio script, so this is no test of whether changing containers changes recognition. Markovo's video audio-track conversion is Signed-in Beta and transcribes the soundtrack. It does not analyze the pictures. 10111619

Condition What the saved output shows Remaining work
Speech conversion succeeds Words and time ranges are available Resolve wording against the recording and documented terms
A recognizable word uses the wrong spelling for the project "Root" where the supplied script says "Route" Record the source for the correction
Speaker attribution is required These outputs contain no speaker labels Establish identity separately, without inventing names from timing
Subtitles need individual display cues The video passage arrives as a long timed block Segment and align cues for the intended player

Keep wording edits separate from timing and speakers

Correcting a word, polishing a sentence and assigning a speaker are separate editorial actions.

Our edited copy changes the homophone spelling to match the supplied script and retains the downloaded grammar. It is a source-informed reading copy, not an independently verified verbatim transcript. The original download remains alongside it.

Original output Editorial treatment Basis
"Root audio contract" Change to "Route audio contract" in the edited copy The supplied script identifies the intended term
"root contract ends" Change to "route contract ends" in the edited copy The same script gives the intended closing phrase
"The speaker name Shanghai" Keep unchanged pending audio review Adding a grammatical ending without listening would assume what was spoken
Digits replace a spelled-out quantity Keep the representation when its value agrees with the source A notation change can preserve the spoken meaning

Full verbatim transcription aims to preserve the speaker's wording, including relevant hesitations and repetitions. A cleaned reading copy can remove speech clutter under a declared editorial rule. Rewriting grammar goes further and should remain visible as editing. Smithsonian's guidance for its audio projects asks transcribers to preserve spoken grammar and apply speaker labels consistently. Our scripted samples do not test how either path handles fillers, interruptions or overlapping voices. 2

Time ranges help locate a passage without identifying who spoke it. When transcribing an audio recording to text for quotation, replay the interval containing the proposed quote and resolve uncertain words there. Where the speaker cannot be identified, an unknown-speaker label records that uncertainty. A name mentioned inside the speech supplies no evidence that the named person is speaking.

Subtitle delivery adds another constraint: the text must fit its display intervals. A long paragraph with start and end markers still needs cue segmentation before it can function as readable captions. W3C discusses caption timing and phrase boundaries separately from transcription wording. Neither saved result was evaluated for word alignment or caption synchronization. 1

Keep the original output, record supported edits, and add speaker or caption structure only when the source supports it.

How we tested

The experiment compares the installed conversion paths and the artifacts they produced, not the accuracy of competing speech recognizers. Testing and source inspection took place on 28 October 2026, using Python 3.11.14.

Markovo is the service we build. It ran through the same corpus and scorer as the other paths; its row is reported in full, including where it scored lower.

Inputs and provenance

Local source What it represents Reference material
corpus/speech.mp3 Short synthetic speech with a route name, a quantity and a list of names corpus/speech.truth.json contains its supplied source script
corpus/screen-recording.mp4 Synthetic video with a spoken soundtrack and a related script corpus/screen-recording.truth.json contains its supplied source script

The source scripts are generation inputs, not independently produced human listening references. The scripts differ, so the pair is not a controlled container comparison. We did not test natural conversation, multiple speakers, background noise, language coverage, filler retention or word timing. 718

The input hashes and provenance are retained in corpus/text-media-provenance.json and audio-environment.json. MP3 and MP4 fall outside this site's permitted article-attachment extensions. Their filenames are listed for transparency, but the audio/video binaries are not included among the published evidence links. Downloadable text evidence alone cannot reproduce the listening stage. 14

Implementations and execution

Path Version or identifier What ran
MarkItDown 0.1.8, Python 3.11.14, plugins disabled Fresh convert_local calls on both inputs. speech_recognition and pydub were absent
Markovo Saved public API responses, rate_card_version=2026-09-19.v3-media-unified Supplied upstream conversion outputs, reused in this editorial run, with no new API conversion
HTTP tutorial client Python standard library, 3.11.14 Offline fixture replay of multipart submission, successful polling, download and terminal failure

Markovo's rate-card identifier is not an ASR model version. The receipts do not identify that model. The recorded jobs were created on 27 October in UTC, which was 28 October in Asia/Shanghai. Their original command invocation is not retained here. The tutorial client follows the supplied API contract. Its replay does not validate live authentication, network availability or current service behavior. The CLI equivalent was not executed for this guide. 9111516

Commands executed from the repository root:

tools/lab-venv/bin/pip list
        PYTHONDONTWRITEBYTECODE=1 tools/lab-venv/bin/python runs/2026-10-28-markovo-transcription-audio-to-text-example/lab/run_audio.py
        PYTHONDONTWRITEBYTECODE=1 tools/lab-venv/bin/python tools/lab_score.py --lab runs/2026-10-28-markovo-transcription-audio-to-text-example/lab --out runs/2026-10-28-markovo-transcription-audio-to-text-example/lab/scores.json --markdown runs/2026-10-28-markovo-transcription-audio-to-text-example/lab/scores.md
        PYTHONDONTWRITEBYTECODE=1 tools/lab-venv/bin/python runs/2026-10-28-markovo-transcription-audio-to-text-example/lab/replay_client.py
        

The library example was also executed on the supplied MP3 in a temporary working directory. No dependencies were installed. The Google recognition branch was not executed for this guide. No additional recognizer was installed or benchmarked.

Scoring and complete selected-sample results

The unmodified tools/lab_score.py, also attached as lab/lab_score.py, defines prose_recall as the proportion of source-script sentences of at least four words found as normalized substrings in the output. Normalization lowercases text, folds whitespace and removes selected Markdown punctuation. It does not equate spelled-out quantities with digits or homophones with each other. The measure is exact sentence retention, not word accuracy or word error rate. 12

Tool Input prose_recall elapsed_s error
markovo screen-recording 0.6667 0.0000 null
markovo speech 0.1667 0.0000 null
markitdown speech 0.0000 0.0486 null
markitdown screen-recording 0.0000 0.0076 null

These are all tool-by-input cells for the selected audio/video inputs. Markovo's elapsed_s=0.0000 records reused results (from_cache=true), not instantaneous conversion. MarkItDown's empty results have error=null because its calls returned normally. The audio and video source scripts each contain a single paragraph, so reading_order is null. Page coverage, table, scan and equation metrics do not apply to these inputs.

The Markovo audio row scores lower than its video row under this literal sentence measure. The supplied scripts and output make the differences visible: number notation changes prevent exact sentence matches, alongside wording differences such as Route/Root and names/name. The value must not be read as a percentage of incorrectly recognized words.

Both API snapshots report status="succeeded", pages_total=1, pages_ocr=0 and quality_score=1. These are service fields, not a human transcription assessment. Zero OCR pages describes this media route without identifying its speech provider. 911

Raw outputs: markitdown/speech.md and markitdown/screen-recording.md are empty. Their three-line diagnostics are preserved separately. The unedited API downloads are markovo/speech.md and markovo/screen-recording.md. The clearly labeled editorial derivative is edited-speech.md. 810131417

Sources

  1. W3C WAI: Transcribing Audio to Text
  2. Smithsonian: How to Transcribe Audio
  3. MarkItDown v0.1.8 README
  4. MarkItDown v0.1.8 AudioConverter source
  5. MarkItDown v0.1.8 transcription helper source
  6. MarkItDown issue 1284: audio recognition routing report
  7. Supplied synthetic audio source script
  8. Unedited Markovo audio transcript
  9. Saved audio API response
  10. Unedited Markovo video soundtrack transcript
  11. Saved video API response
  12. Complete scorer output, with selected media cells
  13. Unedited MarkItDown audio diagnostic
  14. Reproduction program for the installed audio environment
  15. Offline HTTP fixture replay result
  16. Markovo developer contract, repository snapshot consulted
  17. Unedited MarkItDown video diagnostic
  18. Supplied synthetic video source script
  19. Markovo capability-status and page-ownership reference, repository snapshot