Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions docling/backend/webvtt_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ class AnnotatedPar:
items: list[AnnotatedText]


def _detach_span_edge_whitespace(items: list[AnnotatedText]) -> list[AnnotatedText]:
"""Drop the whitespace at the edges of the spans that carry formatting.

Emphasis markers wrap an item's text verbatim, so a space at the edge of a
bold/italic/underline item lands *inside* the markers (``* text*``).
CommonMark does not treat a space-adjacent ``*`` as a flanking delimiter, so
the markup is emitted but never renders. The inline-group serializer already
inserts a separator between the items it joins, so the whitespace can simply
be dropped instead of being moved elsewhere.
"""
detached: list[AnnotatedText] = []
for item in items:
if item.formatting is None:
detached.append(item)
continue
core = item.text.strip()
if not core:
# A whitespace-only span would emit bare markers that render as
# literal asterisks.
continue
detached.append(item.copy_meta(core))
return detached


class WebVTTDocumentBackend(DeclarativeDocumentBackend):
"""Declarative backend for WebVTT (.vtt) files.

Expand Down Expand Up @@ -193,10 +217,11 @@ def _add_text_item(
identifier = str(block.identifier) if block.identifier else None
_extract_components(block.payload)
for par in cue_text:
if not par.items:
items = _detach_span_edge_whitespace(par.items)
if not items:
continue
if len(par.items) == 1:
item = par.items[0]
if len(items) == 1:
item = items[0]
_add_text_item(
text=item.text,
formatting=item.formatting,
Expand All @@ -206,7 +231,7 @@ def _add_text_item(
group = doc.add_inline_group(
"WebVTT cue span", content_layer=ContentLayer.BODY
)
for item in par.items:
for item in items:
_add_text_item(
text=item.text,
formatting=item.formatting,
Expand Down
9 changes: 4 additions & 5 deletions tests/data/webvtt/groundtruth/webvtt_example_04.vtt.itxt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ item-0 at level 0: unspecified: group _root_
item-1 at level 1: text: Last night the chef surprised us with a culinary adventure.
item-2 at level 1: inline: group WebVTT cue span
item-3 at level 2: text: The waiter offered a
item-4 at level 2: text: steaming bowl of
item-4 at level 2: text: steaming bowl of
item-5 at level 2: text: paella
item-6 at level 2: text: that instantly transported the diners to a sunny Mediterranean coast.
item-7 at level 1: inline: group WebVTT cue span
item-8 at level 2: text: The dessert’s
item-9 at level 2: text: unexpected
item-10 at level 2: text:
item-11 at level 2: text: arcobaleno
item-12 at level 2: text: of flavors
item-13 at level 2: text: left everyone in awe.
item-10 at level 2: text: arcobaleno
item-11 at level 2: text: of flavors
item-12 at level 2: text: left everyone in awe.
42 changes: 6 additions & 36 deletions tests/data/webvtt/groundtruth/webvtt_example_04.vtt.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@
},
{
"$ref": "#/texts/9"
},
{
"$ref": "#/texts/10"
}
],
"content_layer": "body",
Expand Down Expand Up @@ -143,8 +140,8 @@
"identifier": "agcvs-08234"
}
],
"orig": "steaming bowl of ",
"text": "steaming bowl of ",
"orig": "steaming bowl of",
"text": "steaming bowl of",
"formatting": {
"bold": false,
"italic": true,
Expand Down Expand Up @@ -264,33 +261,6 @@
"identifier": "agcvs-08234"
}
],
"orig": " ",
"text": " ",
"formatting": {
"bold": false,
"italic": true,
"underline": false,
"strikethrough": false,
"script": "baseline"
}
},
{
"self_ref": "#/texts/8",
"parent": {
"$ref": "#/groups/1"
},
"children": [],
"content_layer": "body",
"label": "text",
"prov": [],
"source": [
{
"kind": "track",
"start_time": 14760.0,
"end_time": 14818.239,
"identifier": "agcvs-08234"
}
],
"orig": "arcobaleno",
"text": "arcobaleno",
"formatting": {
Expand All @@ -302,7 +272,7 @@
}
},
{
"self_ref": "#/texts/9",
"self_ref": "#/texts/8",
"parent": {
"$ref": "#/groups/1"
},
Expand All @@ -318,8 +288,8 @@
"identifier": "agcvs-08234"
}
],
"orig": " of flavors",
"text": " of flavors",
"orig": "of flavors",
"text": "of flavors",
"formatting": {
"bold": false,
"italic": true,
Expand All @@ -329,7 +299,7 @@
}
},
{
"self_ref": "#/texts/10",
"self_ref": "#/texts/9",
"parent": {
"$ref": "#/groups/1"
},
Expand Down
4 changes: 2 additions & 2 deletions tests/data/webvtt/groundtruth/webvtt_example_04.vtt.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Last night the chef surprised us with a culinary adventure.

The waiter offered a *steaming bowl of * *paella* that instantly transported the diners to a sunny Mediterranean coast.
The waiter offered a *steaming bowl of* *paella* that instantly transported the diners to a sunny Mediterranean coast.

The dessert’s ***unexpected*** * * *arcobaleno* * of flavors* left everyone in awe.
The dessert’s ***unexpected*** *arcobaleno* *of flavors* left everyone in awe.
50 changes: 50 additions & 0 deletions tests/test_backend_vtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,56 @@ def test_styling_and_voice_tags_stripped(converter):
assert _process_vtt_doc(doc) == expected


def test_emphasis_markers_stay_clear_of_span_edge_whitespace(converter):
"""A space inside the emphasis markers stops CommonMark from flanking them.

The italic span ends with the space that separates it from the nested voice
span. Emitting that space inside the markers produces ``*steaming bowl of *``,
where the closing ``*`` is preceded by whitespace and therefore is not a
right-flanking delimiter: the markup is written out but never renders.
"""
vtt = """
WEBVTT

00:00:01.000 --> 00:00:04.000
The waiter offered a <i>steaming bowl of <lang es-ES>paella</lang></i> that arrived.
"""
stream = _create_vtt_stream(vtt)
doc = converter.convert(stream).document
md = doc.export_to_markdown(escape_html=False)

assert "*steaming bowl of*" in md
assert "*paella*" in md
assert "*steaming bowl of *" not in md


def test_whitespace_only_span_emits_no_bare_markers(converter):
"""A span holding nothing but whitespace used to emit unrenderable markers.

The space between ``</b>`` and ``<u>`` is itself an italic span, so it was
exported as a pair of markers with the space between them, which shows up as
literal asterisks. The following span started with the same separator space,
which broke its opening marker in the same way.
"""
vtt = """
WEBVTT

00:00:01.000 --> 00:00:04.000
The dessert's <i><b>unexpected</b> <u><lang it>arcobaleno</lang></u> of flavors</i> left everyone in awe.
"""
stream = _create_vtt_stream(vtt)
doc = converter.convert(stream).document
md = doc.export_to_markdown(escape_html=False)

assert "***unexpected***" in md
assert "*arcobaleno*" in md
assert "*of flavors*" in md
# the whitespace-only span contributed a bare pair of markers, and the span
# after it opened with a space
assert "* * *" not in md
assert "* of flavors*" not in md


def test_blank_cue_contributes_no_text(converter):
# First cue has text; second cue is intentionally blank (zero transcript lines).
vtt = """
Expand Down
Loading