Skip to content

fix(deepgram): use punctuated_word for word-level transcripts - #6699

Merged
chenghao-mou merged 4 commits into
livekit:mainfrom
mdylan2:fix/deepgram-punctuated-words
Aug 5, 2026
Merged

fix(deepgram): use punctuated_word for word-level transcripts#6699
chenghao-mou merged 4 commits into
livekit:mainfrom
mdylan2:fix/deepgram-punctuated-words

Conversation

@mdylan2

@mdylan2 mdylan2 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

SpeechData.text and SpeechData.words are built from different Deepgram fields, so the two disagree on the same object.

text comes from the transcript, which honours the punctuate option. The word list was built from the raw per-word field, which is lowercase and unpunctuated regardless of any option:

text=alt["transcript"],                    # honours `punctuate`
...
words=[
    TimedString(
        text=word.get("word", ""),         # never punctuated

Deepgram returns punctuated_word alongside word, in the very dict that comprehension iterates:

keys : ['confidence', 'end', 'punctuated_word', 'start', 'word']
word            : hello my name is alex
punctuated_word : Hello. My name is Alex.

Field selection across the four cases:

_word_text({"word": "alex", "punctuated_word": "Alex."}, punctuate=True)   # -> "Alex."
_word_text({"word": "alex"},                             punctuate=True)   # -> "alex"
_word_text({"word": "alex", "punctuated_word": ""},      punctuate=True)   # -> "alex"
_word_text({"word": "alex", "punctuated_word": "Alex."}, punctuate=False)  # -> "alex"

SpeechData.text comes from alt["transcript"], which Deepgram punctuates. The word
list was built from alt["words"][].word, which is always lowercase and
unpunctuated, so the two fields of the same SpeechData disagreed:

  .text         "We are so excited for you to join the Ribbon team."
  .words[].text "we are so excited for you to join the ribbon team"

Deepgram returns punctuated_word alongside word whenever punctuate is enabled
(it is on by default), so the punctuated form was already in the payload the
comprehension iterates. Prefer it, falling back to word when punctuation is
disabled or the key is absent.

This matters for consumers that render or align on the word list — clickable
transcripts, captions, time-aligned analysis — where the words are the display
text and currently read as an uncased, unpunctuated wall of text.

Applied to both the streaming and prerecorded paths. Word timing is untouched.
@CLAassistant

CLAassistant commented Aug 4, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

mdylan2 added 2 commits August 4, 2026 18:42
Follows the `punctuate` setting rather than inferring from key presence, so the
word list matches SpeechData.text in both configurations:

  punctuate=True   text and words both punctuated
  punctuate=False  text and words both raw

Threads the option from STTOptions through both conversion helpers. The new
keyword defaults to True, matching STT(punctuate=True), so existing callers of
these module-level functions are unaffected.
`word` is an untyped dict, so `.get()` returns Any and returning it directly from
a `-> str` function trips mypy's strict no-any-return. The original code was
inline inside the TimedString(...) call, where there was no declared return type
to violate; extracting the helper is what surfaced it.

Bind to str-annotated locals so the narrowing happens once. Behaviour unchanged.
@mdylan2
mdylan2 marked this pull request as ready for review August 4, 2026 23:14
@mdylan2
mdylan2 requested a review from a team as a code owner August 4, 2026 23:14

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@chenghao-mou chenghao-mou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. thanks for the PR

Comment thread livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt.py Outdated
Per review: smart_format also produces a formatted transcript, so gating the word
form on `punctuate` alone left a hole. Verified against the API:

  punctuate=F smart_format=F   transcript raw          punctuated_word ABSENT
  punctuate=F smart_format=T   transcript PUNCTUATED   punctuated_word present
  punctuate=T smart_format=F   transcript punctuated   punctuated_word present

So STT(punctuate=False, smart_format=True) got a punctuated `text` and raw
`words` — the exact inconsistency this PR removes, reintroduced.

Both call sites now pass `punctuate or smart_format`. Checked the neighbouring
options too: dictation, numerals, and filler_words leave punctuated_word absent
when punctuate is off, so those two are the whole set.

Renamed the keyword to `use_punctuated_word`: the value is a decision derived
from two options, not the `punctuate` option, and naming it after the latter
invites someone to "simplify" it back and silently restore the hole. The keyword
does not exist on main, so the rename breaks no caller.

Streamed live audio through all four combinations; `.words` now matches `.text`
in every one.
@chenghao-mou
chenghao-mou merged commit 62d527d into livekit:main Aug 5, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants