Preparing Interview Transcripts for Qualitative Coding
Transcribe interviews and focus groups on your own computer, de-identify the text, and set up one case per participant before coding and exporting to NVivo.
Interview and focus group recordings become coded excerpts in five steps, namely transcription with speaker labels and timestamps, de-identification, a choice between whole transcripts and excerpts, loading with one case per participant, and coding with export. Potato transcribes recordings on your own computer with WhisperX or faster-whisper, codes the text in QDA Mode with memos and cases, and exports the coded project to NVivo, ATLAS.ti, or MAXQDA as a REFI-QDA .qdpx file. Potato does not de-identify transcripts, so your team replaces names and other identifiers before the text is loaded.
Transcription turns recorded speech into text, and the level of detail in a transcript is an analytic decision. Oliver, Serovich, and Mason (2005) name two conventions (reference 2). Naturalized transcription captures every utterance in as much detail as possible. Denaturalized transcription corrects grammar, removes stutters and pauses, and standardizes nonstandard accents. Speaker diarization labels who spoke when, and de-identification removes or replaces details that could identify a participant. In Potato, a case groups the items that belong to one participant, and a memo is an analytic note on a transcript or a highlighted passage. REFI-QDA is the exchange standard that NVivo, ATLAS.ti, and MAXQDA read and write.
The guide covers the preparation around coding. Code Interview Transcripts With Two Coders and Report Alpha covers reliability between two coders, and How to Annotate Whisper Transcripts covers correcting speech recognition output against the audio. Developing a Codebook for Qualitative Research covers writing the codes, and Qualitative Data Analysis Software Compared covers the choice of tool.
Transcription conventions to settle first
McLellan, MacQueen, and Neidig (2003) observe that no single transcription format fits every kind of qualitative data, setting, or theoretical approach, and that poorly prepared transcripts can delay or compromise analysis (reference 1). A written convention keeps every transcriber and every correction pass on the same rules, and your team agrees on the convention before the first recording is transcribed. The convention covers three decisions:
- Detail. The convention says whether fillers, pauses, overlapping speech, and laughter are marked, and in what notation.
- Speaker labels. Each speaker gets a role or a participant ID, such as
InterviewerorP07, and never a real name. - Timestamps. A time at the start of each turn lets a coder find the passage in the recording.
Potato's local transcription writes the text the Whisper model produces, and no Potato setting asks the model to mark pauses, overlaps, or fillers. A naturalized transcript therefore needs a correction pass against the recording. How to Annotate Whisper Transcripts sets up the correction pass in Potato.
Transcribing recordings on your own computer
Consent forms and ethics approvals often limit who may hear interview recordings and where the audio may go. potato transcripts --transcribe runs speaker diarization with sherpa-onnx and speech recognition with WhisperX when WhisperX is installed, or with faster-whisper otherwise. The transcribe extra installs faster-whisper and sherpa-onnx, and WhisperX is a separate install (pip install whisperx). All three tools run on your own computer with no cloud service:
pip install 'potato-annotation[transcribe]'
potato transcripts ./recordings --transcribe --asr-model small.en --diarize --num-speakers 2 -o data/transcripts.jsonThe command writes one item per recording, with the speaker, start time, end time, and text of every turn. Whisper models run from tiny to large-v3, with base as the default. A larger model runs slower and transcribes more accurately. Potato's help text notes that a two-person interview diarizes markedly better when the speaker count is given with --num-speakers. A test on a 21-second synthesized interview with two voices printed the following summary:
Scanned 1 file(s):
int07.wav local ASR + diarization 5 turns 21.4s 2 speaker(s): SPEAKER_00, SPEAKER_01Potato 2.14.0 prints the engine in place of local ASR, as ASR (whisperx) or ASR (faster-whisper).
Diarization names the speakers SPEAKER_00, SPEAKER_01, and so on, in no fixed order. In the test, SPEAKER_01 was the interviewer, and the preparation script below maps each label to a role or a participant ID. Potato's own source notes that the clustering-based diarization does badly on crosstalk. Check the speaker labels of every focus group against the recording before coding starts.
The model weights download on first use. The two diarization model files come to about 47 MB together, and the Whisper download grows with the model size. Once the weights are on the machine, transcription runs air-gapped, as Air-Gapped and Offline Deployment describes. For an offline machine, copy a CTranslate2 Whisper model directory across and pass the directory's path to --asr-model, and set POTATO_MODEL_CACHE to a folder holding the diarization models.
Potato caches each raw transcript beside the recording as <name>.whisper.json, and the cache keeps every name in place. Keep the cache files with the recordings, or pass --asr-cache to write the files to an equally protected folder. Voice-activity detection runs before transcription to keep Whisper from inventing text over silence. The detection can also drop quiet or whispered speech without a warning. If a transcript comes back shorter than the recording, run the command again with --asr-no-vad. WhisperX always runs voice-activity detection, so with WhisperX installed, add --asr-engine faster-whisper for --asr-no-vad to take effect.
De-identifying transcripts before coding
Potato has no tool that finds or replaces names, places, or other identifiers in a transcript. De-identification therefore happens before Potato loads the text, because every coder screen, memo, quotation report, and .qdpx export reproduces the transcript as loaded. Potato's one text scrub runs at the end of a project. python -m potato.publish --scrub-pii replaces email addresses and phone numbers with [EMAIL] and [PHONE] in a published dataset, and the option catches no names.
The UK Data Service recommends replacing identifying details with pseudonyms or descriptive tags such as "[colleague 1]" rather than deleting the content. The same guidance asks for each replacement to be applied consistently across transcripts and for a log of the changes. The guidance also asks for restraint, because over-anonymization removes contextual detail and reduces the data's interpretive value (reference 4). Saunders, Kitzinger, and Kitzinger (2015) describe the trade-off in interviews with family members of patients in vegetative and minimally conscious states (reference 3). The authors replaced hospital names with labels such as "Hospital 1" and report that some protections cost the analysis part of the data's richness.
The preparation script in the next section applies the replacement table pseudonyms.csv to every transcript, and the table doubles as the anonymization log. Because the table maps pseudonyms back to real names, store the table outside the project folder Potato serves. A table catches only the spellings the table lists. A person therefore still reads every transcript after the replacement and judges indirect identifiers such as a workplace or a rare occupation. In the test transcript, the table replaced the participant's sister's name with Ana and a street name with "[street name]", and the bakery where the sister works stayed for a reader to judge. After loading, search the corpus for each name in the log with Potato's full-text search. Each search should return no transcript.
Whole transcripts or excerpts
The unit a coder sees is one item in the data file. A whole transcript as one item keeps the interview's flow in front of the coder, and the .qdpx export then writes one source per interview. A project can instead split each transcript into excerpts, such as one item per participant answer, and the export then writes one source per excerpt. Excerpts fit two situations better:
- Intercoder reliability. Agreement is computed over units both coders answer, and a fixed excerpt gives both coders the same unit. The two-coder tutorial splits transcripts into answers for that reason.
- Focus groups. Potato files each item under at most one case, and a group transcript loaded as one item therefore belongs to one case only. One item per participant turn lets every participant's turns form that participant's case.
Flattening transcripts into speaker-labeled text
The output of potato transcripts stores each transcript as a nested list of turns, and the .qdpx export skips any item whose text is not a plain string. A project headed for NVivo, ATLAS.ti, or MAXQDA therefore flattens each transcript into speaker-labeled text first. Three small CSV files hold the decisions the script applies:
speakers.csv transcript,speaker,name
int07,SPEAKER_01,Interviewer
int07,SPEAKER_00,P07
participants.csv participant_id,site,age_group
P07,rural,35-49
pseudonyms.csv original,replacement
Maria,Ana
Main Street,[street name]The script merges consecutive segments from one speaker into one turn, writes a timestamp at the start of each turn, applies the replacement table, and copies the participant's attributes onto the item. With --excerpts, the script writes one item per participant turn instead:
"""Turn `potato transcripts` output into speaker-labeled text for coding.
Usage:
python prepare_transcripts.py data/transcripts.json speakers.csv participants.csv \
pseudonyms.csv > data/interviews.jsonl
python prepare_transcripts.py ... --excerpts > data/excerpts.jsonl
"""
import csv
import json
import re
import sys
transcripts, speakers_csv, participants_csv, pseudonyms_csv = sys.argv[1:5]
excerpts = "--excerpts" in sys.argv
names = {(r["transcript"], r["speaker"]): r["name"] for r in csv.DictReader(open(speakers_csv))}
people = {r["participant_id"]: r for r in csv.DictReader(open(participants_csv))}
swaps = [(re.compile(rf"\b{re.escape(r['original'])}\b"), r["replacement"])
for r in csv.DictReader(open(pseudonyms_csv))]
def clock(seconds):
m, s = divmod(int(seconds), 60)
return f"{m // 60:02d}:{m % 60:02d}:{s:02d}"
def pseudonymize(text):
for pattern, replacement in swaps:
text = pattern.sub(replacement, text)
return text
for item in json.load(open(transcripts, encoding="utf-8")):
tid = item["id"]
turns = [] # merge consecutive segments from the same speaker into one turn
for t in item["conversation"]["turns"]:
name = names.get((tid, t.get("speaker")), t.get("speaker") or "Unknown")
if turns and turns[-1]["name"] == name:
turns[-1]["text"] += " " + t["text"]
else:
turns.append({"name": name, "start": t.get("start") or 0, "text": t["text"]})
for t in turns:
t["text"] = pseudonymize(t["text"])
if excerpts: # one item per participant turn, for focus groups
for n, t in enumerate(turns):
if t["name"] in people:
print(json.dumps({"id": f"{tid}-{n:03d}", "transcript": tid,
**people[t["name"]],
"text": f"[{clock(t['start'])}] {t['name']}: {t['text']}"}))
continue
participant = next(t["name"] for t in turns if t["name"] in people)
text = "\n\n".join(f"[{clock(t['start'])}] {t['name']}: {t['text']}" for t in turns)
print(json.dumps({"id": tid, **people[participant], "text": text}))On the test recording, the script wrote one item, shown here with the text shortened:
{"id": "int07", "participant_id": "P07", "site": "rural", "age_group": "35-49", "text": "[00:00:00] Interviewer: Thanks for joining, can you tell me how you get to the clinic?\n\n[00:00:04] P07: I take two buses, and it takes most of the morning, my sister Ana drives me when she can, but she works at the bakery on [street name].\n\n..."}Transcripts that already exist as files go through the same script. Without --transcribe, potato transcripts reads 21 transcript and subtitle formats, including WhisperX JSON, SubRip, WebVTT, Praat TextGrid, and ELAN EAF, as Transcript Formats lists. A project coded in another QDA program comes across through potato import, as Importing a Project describes.
Coding in QDA Mode
The config below sets up open coding of whole transcripts, with one case per participant. The config passes python -m potato.validate_cli config.yaml --strict on Potato 2.13.1:
annotation_task_name: Clinic access interviews
task_dir: .
output_annotation_dir: annotation_output/
data_files:
- data/interviews.jsonl
item_properties:
id_key: id
text_key: text
user_config:
allow_all_users: true
users: []
# Open coding by one analyst: a codebook that grows while reading, memos, cases, search
qda_mode:
enabled: true
# One case per participant, with the participant's site and age group on the case
cases:
enabled: true
key: participant_id
attributes: [site, age_group]
annotation_ui:
memos: true
search:
enabled: true
annotator_claim: true
annotation_schemes:
- annotation_type: span
name: codes
description: Highlight a passage and apply a code, or press i to create a code from the highlighted words.
codebook: true
labels: [travel to the clinic, cost of care, family support, delaying care]QDA Mode sets the codebook to open. The analyst adds codes while reading, and the four labels only seed the codebook. Selecting a passage and pressing i creates an in-vivo code from the selected words. Renaming or merging a code later leaves earlier codings under the old name, as Shared Codebook for Qualitative Coding explains. The search block turns on the full-text search used for the name check above. Start the server with potato start config.yaml -p 8000.
The cases block groups items by participant_id and stores site and age_group on each case. Potato 2.13.1 builds cases only from item fields and offers no way to assign an item to a case by hand. Every item therefore needs the participant ID. The test project's server log reported Cases auto-detect: 1 case(s), 1 instance(s) assigned. While coding is under way, the admin API endpoint /admin/api/code_crosstab?attribute=site counts codes by a case attribute, as Cases and Case Attributes describes.
Memos open in the Notes panel on each transcript. A new memo starts as private. A coder who wants the rest of the team to read a memo picks Shared in the memo's visibility menu, as Analytic Memos describes. QDA Mode assumes one analyst over the whole corpus. For two coders on the same excerpts, follow the two-coder tutorial and Intercoder Reliability in Qualitative Research. For a language model as a second coder, see LLM-Assisted Qualitative Coding.
Exporting coded transcripts
Three exports cover the usual deliverables:
python -m potato.export -c config.yaml -f qdpx -o export/
python -m potato.export -c config.yaml -f quotation_report --option include_memos=true -o export/
python -m potato.export -c config.yaml -f csv -o export/The first command writes a .qdpx file with one text source per transcript, the codes as a flat list with no hierarchy, and every coding with its coder. The export leaves out memos and cases. Because the config sets cases.enabled and annotation_ui.memos explicitly, the export prints a warning that names memos and cases. The second command writes quotations.csv, with one row per highlighted passage giving the code, the quote, the character offsets, the transcript ID, and the coder, followed by one row per memo. The third command writes every coder's annotations as a table for R or Python. The case attributes stay in participants.csv, and the data file's id and participant_id fields join the attributes to the quotation report.
Frequently asked questions
Should interview transcripts be verbatim or clean verbatim?
Choose the level of detail from the analysis. Oliver, Serovich, and Mason (2005) note that naturalized, fully verbatim transcription is most common in conversation analysis. A study of what participants said about a topic can usually work from a denaturalized, clean transcript. Potato's local transcription writes the Whisper model's text. A verbatim convention therefore needs a correction pass against the recording.
When should interview transcripts be de-identified?
De-identify transcripts before coding starts, because Potato shows and exports the text exactly as loaded. Potato has no de-identification tool. Apply a replacement table of consistent pseudonyms before loading, read each transcript for indirect identifiers, and search the loaded corpus for each original name.
Can faster-whisper transcribe interviews without uploading the audio?
Yes, faster-whisper runs Whisper on your own computer, and potato transcripts --transcribe uses faster-whisper to turn a folder of recordings into a Potato data file when WhisperX is not installed. With WhisperX installed, the command runs WhisperX on your own computer instead, and --asr-engine faster-whisper keeps faster-whisper. The model weights download on first use, and transcription needs no network after the first download.
Further reading
- Code Interview Transcripts With Two Coders and Report Alpha, for reliability between two coders and adjudication
- How to Annotate Whisper Transcripts, for correcting machine transcripts against the audio
- Transcript Formats, for every transcript format Potato reads and the local transcription options
- QDA Mode, Shared Codebook, Analytic Memos, Cases and Case Attributes, and Full-Text Search
- Project Snapshots, for freezing the coded project as an audit trail
- Developing a Codebook for Qualitative Research
- Intercoder Reliability in Qualitative Research
- Air-Gapped and Offline Deployment
- Qualitative research in Potato, for the qualitative features in one place
References
- McLellan, E., MacQueen, K. M., & Neidig, J. L. (2003). Beyond the Qualitative Interview: Data Preparation and Transcription. Field Methods, 15(1), 63-84. https://doi.org/10.1177/1525822X02239573
- Oliver, D. G., Serovich, J. M., & Mason, T. L. (2005). Constraints and Opportunities with Interview Transcription: Towards Reflection in Qualitative Research. Social Forces, 84(2), 1273-1289. https://doi.org/10.1353/sof.2006.0023
- Saunders, B., Kitzinger, J., & Kitzinger, C. (2015). Anonymising interview data: challenges and compromise in practice. Qualitative Research, 15(5), 616-632. https://doi.org/10.1177/1468794114550439
- UK Data Service. Anonymisation for text data. Retrieved November 2, 2026, from https://ukdataservice.ac.uk/learning-hub/data-producer-support/preparing-data-for-sharing-and-reuse/anonymising-data-and-protecting-participants/anonymisationfortextdata/