HumanSignal / HumanSignal/label-studio
Offsets in HTML annotation
- Dominant language
- TypeScript
- Stars
- 28.3k
- Forks
- 3.7k
- Avg merge
- 14h
- Merged PRs (30d)
- 15
Description
**Describe the bug**
Hi!
I don't understand how I should use `HyperText` and `HyperTextLabels` for span annotation in `.html` files.
It seems only `text` field work correctly.
1. length of the `text` is not the same:
```
# for globalOffsets
text length: 83
globaloffsets: 27 - 106
globaloffset 'length': 79
# for endOffset and startOffset
text length: 83
globaloffset 'length': 79
globaloffsets: 27 - 106
```
But according to this, it should be the same?
https://github.com/HumanSignal/label-studio/blob/develop/web/libs/editor/src/tags/object/RichText/domManager.md#content-field
2. In my case, annotations can overlap. Therefore, without reliable offsets, it's impossible to merge them accurately.
3. According to #4843 , it's impossible to "map" `globalOffsets` to text.
4. There are several issues https://github.com/HumanSignal/label-studio/issues?q=globalOffsets
5. I also tried to use xpath `start` and `end` -- same result i can't get same text. Do you have any example how reuse it?
**To Reproduce**
```python
from __future__ import annotations
import json
import logging
import pathlib
def debug_annotation(json_path: str, debug_level: int = logging.DEBUG) -> None:
# create logger
logger = logging.getLogger(__name__)
logger.setLevel(debug_level)
logger.addHandler(logging.StreamHandler())
# read json file
with pathlib.Path(json_path).open("r", encoding="utf-8") as json_file:
json_data = json.load(json_file)
for task in json_data:
logger.info("Task ID: %d", task["id"])
for annotation in task["annotations"]:
logger.info("Annotation ID: %d", annotation["id"])
for item in annotation["result"]:
if len(item["value"]["text"]) != item["value"]["endOffset"] - item["value"]["startOffset"]:
logger.critical(
"LEN OF TEXT NOT MATCHED (`end-start` offsets): Task ID: %d, Annotation ID: %d, Result ID: %s",
task["id"],
annotation["id"],
item["id"],
)
logger.critical("Text: %s", item["value"]["text"])
logger.critical("text length: %d", len(item["value"]["text"]))
logger.critical("offset 'length': %d", item["value"]["endOffset"] - item["value"]["startOffset"])
logger.critical("offsets: %d - %d", item["value"]["startOffset"], item["value"]["endOffset"])
logger.critical("")
if (
len(item["value"]["text"])
!= item["value"]["globalOffsets"]["end"] - item["value"]["globalOffsets"]["start"]
):
logger.critical(
"LEN OF TEXT NOT MATCHED (`end-start` globalOffsets): Task ID: %d, Annotation ID: %d, Result ID: %s",
task["id"],
annotation["id"],
item["id"],
)
logger.critical("Text: %s", item["value"]["text"])
logger.critical("text length: %d", len(item["value"]["text"]))
logger.critical(
"globaloffset 'length': %d",
item["value"]["globalOffsets"]["end"] - item["value"]["globalOffsets"]["start"],
)
logger.critical(
"globaloffsets: %d - %d",
item["value"]["globalOffsets"]["start"],
item["value"]["globalOffsets"]["end"],
)
logger.critical("")
logger.info("Result ID: %s", item["id"])
logger.info("Result text: %s", item["value"]["text"])
logger.info("startOffset: %d", item["value"]["startOffset"])
logger.info("endOffset: %d", item["value"]["endOffset"])
logger.info("globalOffsets (start): %d", item["value"]["globalOffsets"]["start"])
logger.info("globalOffsets (end): %d", item["value"]["globalOffsets"]["end"])
logger.info("")
return None
if __name__ == "__main__":
# see Additional context
path_to_json = "path/to/small.json"
debug_annotation(path_to_json, debug_level=logging.CRITICAL)
```
OUTPUT
```
LEN OF TEXT NOT MATCHED (`end-start` offsets): Task ID: 132570841, Annotation ID: 45365694, Result ID: oicRy3z29K
Text: こんにちは!\nこれはサンプルです。
text length: 18
offset 'length': 10
offsets: 0 - 10
LEN OF TEXT NOT MATCHED (`end-start` globalOffsets): Task ID: 132570841, Annotation ID: 45365694, Result ID: oicRy3z29K
Text: こんにちは!\nこれはサンプルです。
text length: 18
globaloffset 'length': 21
globaloffsets: 6 - 27
LEN OF TEXT NOT MATCHED (`end-start` offsets): Task ID: 132570841, Annotation ID: 45365694, Result ID: 5Fsy0sv91L
Text: こんにちは!\nこれはサンプルです。
text length: 18
offset 'length': 10
offsets: 0 - 10
LEN OF TEXT NOT MATCHED (`end-start` globalOffsets): Task ID: 132570841, Annotation ID: 45365694, Result ID: 5Fsy0sv91L
Text: こんにちは!\nこれはサンプルです。
text length: 18
globaloffset 'length': 21
globaloffsets: 6 - 27
LEN OF TEXT NOT MATCHED (`end-start` offsets): Task ID: 132570842, Annotation ID: 45365741, Result ID: iokeAq7ZtS
Text: This is an example paragraph using the Fira Code font which supports ligatures.\n\n
text length: 83
offset 'length': 79
offsets: 0 - 79
LEN OF TEXT NOT MATCHED (`end-start` globalOffsets): Task ID: 132570842, Annotation ID: 45365741, Result ID: iokeAq7ZtS
Text: This is an example paragraph using the Fira Code font which supports ligatures.\n\n
text length: 83
globaloffset 'length': 79
globaloffsets: 27 - 106
LEN OF TEXT NOT MATCHED (`end-start` offsets): Task ID: 132570842, Annotation ID: 45365741, Result ID: XRLK45yuq-
Text: Ligature Example\nThis is an example paragraph using the Fira Code font which supports ligatures.\n\nCommon ligatures include symbols like: ==, !=, ===, <=,>=, -->, and others.
text length: 176
offset 'length': 75
offsets: 0 - 75
LEN OF TEXT NOT MATCHED (`end-start` globalOffsets): Task ID: 132570842, Annotation ID: 45365741, Result ID: XRLK45yuq-
Text: Ligature Example\nThis is an example paragraph using the Fira Code font which supports ligatures.\n\nCommon ligatures include symbols like: ==, !=, ===, <=,>=, -->, and others.
text length: 176
globaloffset 'length': 181
globaloffsets: 5 - 186
LEN OF TEXT NOT MATCHED (`end-start` offsets): Task ID: 132570842, Annotation ID: 45365741, Result ID: RBB6g6SYJw
Text: Ligature Example\nThis is an example paragraph using the Fira Code font which supports ligatures.\n\nCommon ligatures include symbols like: ==, !=, ===, <=,>=, -->, and others.
text length: 176
offset 'length': 75
offsets: 0 - 75
LEN OF TEXT NOT MATCHED (`end-start` globalOffsets): Task ID: 132570842, Annotation ID: 45365741, Result ID: RBB6g6SYJw
Text: Ligature Example\nThis is an example paragraph using the Fira Code font which supports ligatures.\n\nCommon ligatures include symbols like: ==, !=, ===, <=,>=, -->, and others.
text length: 176
globaloffset 'length': 181
globaloffsets: 5 - 186
```
**Expected behavior**
I expect the offsets and global offsets to match the length of the extracted text.
**Screenshots**
None
**Environment (please complete the following information):**
- OS: macOS Sonoma
- Label Studio Version: Academic License
**Additional context**
**_HTML Files were uploaded via GUI in Google Chrome._**
1) Output json file [small.json](https://github.com/user-attachments/files/17167608/small.json)
2) UI config:
```xml
```
3) HTML samples
№1
```html
サンプル
こんにちは!
これはサンプルです。
```
№2
```html
HTML Page with Ligatures
/* Using Google Fonts to import a ligature-friendly font */
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400&display=swap');
body {
font-family: 'Fira Code', monospace;
margin: 20px;
}
p {
font-size: 16px;
}
Ligature Example
This is an example paragraph using the Fira Code font which supports ligatures.
Common ligatures include symbols like: ==, !=, ===, <=,>=, -->, and others.
```
Contributor guide
Assessment
This issue has not been assessed yet.