File size: 18,400 Bytes
b31f748
 
 
 
 
 
 
fda8a7a
73ab668
8c51bed
 
 
 
 
 
 
 
 
 
 
 
b31f748
 
63ba62d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b31f748
 
 
 
 
 
 
8c51bed
 
 
b31f748
8c51bed
 
 
 
 
 
 
 
 
 
 
b31f748
 
fda8a7a
 
 
 
b31f748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73ab668
 
 
b31f748
8c51bed
 
b31f748
8c51bed
b31f748
 
 
 
 
 
 
 
73ab668
8c51bed
73ab668
0f2b045
 
73ab668
0f2b045
b31f748
 
8c51bed
b31f748
8c51bed
b31f748
 
 
 
 
 
 
8c51bed
b31f748
 
 
 
73ab668
 
 
 
 
 
 
 
 
 
 
 
 
b31f748
 
 
fda8a7a
73ab668
b31f748
 
 
8c51bed
b31f748
 
fda8a7a
73ab668
b31f748
 
 
8c51bed
73ab668
 
 
8c51bed
 
 
 
 
 
73ab668
 
8c51bed
73ab668
02c9cf7
8c51bed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f2b045
73ab668
0f2b045
 
73ab668
0f2b045
 
73ab668
 
 
0f2b045
 
73ab668
 
 
0f2b045
b31f748
8c51bed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63ba62d
8c51bed
 
 
 
63ba62d
8c51bed
 
 
63ba62d
8c51bed
 
 
63ba62d
8c51bed
 
 
63ba62d
8c51bed
 
63ba62d
8c51bed
 
 
 
63ba62d
8c51bed
 
 
 
 
 
 
63ba62d
8c51bed
 
63ba62d
8c51bed
 
fda8a7a
8c51bed
fda8a7a
8c51bed
 
 
 
 
 
 
 
 
 
 
 
 
fda8a7a
 
8c51bed
 
 
 
fda8a7a
8c51bed
fda8a7a
b31f748
 
8c51bed
 
 
 
 
 
 
 
 
 
b31f748
a482560
b31f748
8c51bed
b31f748
8c51bed
 
 
 
 
 
 
 
 
 
fda8a7a
a482560
fda8a7a
 
 
8c51bed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b31f748
 
 
73ab668
 
 
 
 
 
0f2b045
 
 
8c51bed
73ab668
8c51bed
73ab668
 
8c51bed
73ab668
0f2b045
 
 
 
 
63ba62d
 
0f2b045
 
 
 
 
 
 
 
 
 
 
 
 
63ba62d
 
0f2b045
 
 
fda8a7a
681cad2
 
fda8a7a
 
 
8c51bed
 
 
fda8a7a
 
 
8c51bed
 
fda8a7a
 
 
 
8c51bed
 
b31f748
fda8a7a
 
8c51bed
 
fda8a7a
8c51bed
 
 
 
 
 
 
681cad2
 
fda8a7a
681cad2
 
fda8a7a
 
 
 
 
8c51bed
 
 
 
 
 
 
 
681cad2
8c51bed
 
681cad2
8c51bed
 
681cad2
8c51bed
 
681cad2
 
 
 
 
 
 
 
fda8a7a
681cad2
 
 
 
 
 
63ba62d
b31f748
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
"""
Gradio demo showcasing ISCC Semantic Text Code.
"""

from loguru import logger as log
import gradio as gr
import iscc_sct as sct
import textwrap
import yaml
import pathlib


HERE = pathlib.Path(__file__).parent.absolute()


custom_css = """
.simbar {
    background: white;
    min-height: 30px;
}
"""


newline_symbols = {
    "\u000a": "⏎",  # Line Feed - Represented by the 'Return' symbol
    "\u000b": "↨",  # Vertical Tab - Represented by the 'Up Down Arrow' symbol
    "\u000c": "␌",  # Form Feed - Unicode Control Pictures representation
    "\u000d": "↵",  # Carriage Return - 'Downwards Arrow with Corner Leftwards' symbol
    "\u0085": "⤓",  # Next Line - 'Downwards Arrow with Double Stroke' symbol
    "\u2028": "↲",  # Line Separator - 'Downwards Arrow with Tip Leftwards' symbol
    "\u2029": "¶",  # Paragraph Separator - Represented by the 'Pilcrow' symbol
}


def no_nl(text):
    """Replace non-printable newline characters with printable symbols"""
    for char, symbol in newline_symbols.items():
        text = text.replace(char, symbol)
    return text


def no_nl_inner(text):
    """Replace non-printable newline characters with printable symbols, ignoring leading and
    trailing newlines"""
    # Strip leading and trailing whitespace
    stripped_text = text.strip()

    # Replace newline characters within the text
    for char, symbol in newline_symbols.items():
        stripped_text = stripped_text.replace(char, symbol)

    # Add back the leading and trailing newlines
    leading_newlines = len(text) - len(text.lstrip())
    trailing_newlines = len(text) - len(text.rstrip())

    return "\n" * leading_newlines + stripped_text + "\n" * trailing_newlines


def clean_chunk(chunk):
    """Strip consecutive line breaks in text to a maximum of 2."""
    return chunk.replace("\n\n", "\n")


def compute_iscc_code(text1, text2, bit_length):
    code1 = sct.gen_text_code_semantic(text1, bits=bit_length)
    code2 = sct.gen_text_code_semantic(text2, bits=bit_length)
    similarity = compare_codes(code1["iscc"], code2["iscc"], bit_length)
    return code1["iscc"], code2["iscc"], similarity


import binascii


def compare_codes(code_a, code_b, bits):
    if code_a and code_b:
        code_a_str = code_a.value if hasattr(code_a, "value") else str(code_a)
        code_b_str = code_b.value if hasattr(code_b, "value") else str(code_b)
        if code_a_str and code_b_str:
            try:
                distance = sct.iscc_distance(code_a_str, code_b_str)
                return generate_similarity_bar(hamming_to_cosine(distance, bits))
            except binascii.Error:
                # Invalid ISCC code format
                return None
    return None


def truncate_text(text, max_length=70):
    return textwrap.shorten(text, width=max_length, placeholder="...")


def hamming_to_cosine(hamming_distance: int, dim: int) -> float:
    """Aproximate the cosine similarity for a given hamming distance and dimension"""
    result = 1 - (2 * hamming_distance) / dim
    return result


def generate_similarity_bar(similarity):
    """Generate a horizontal bar representing the similarity value, scaled to -100% to +100%."""
    # Scale similarity from [-1, 1] to [-100, 100]
    display_similarity = similarity * 100

    # Calculate the width of the bar based on the absolute value of similarity
    bar_width = int(abs(similarity) * 50)  # 50% is half the width of the container

    # Determine the color and starting position based on the sign of the similarity
    color = "green" if similarity >= 0 else "red"
    position = "left" if similarity >= 0 else "right"

    # Adjust the text position to be centered within the colored bar
    text_position = "left: 50%;" if similarity >= 0 else "right: 50%;"
    text_alignment = (
        "transform: translateX(-50%);" if similarity >= 0 else "transform: translateX(50%);"
    )

    tooltip = "Similarity based on ISCC code comparison, not direct text comparison."

    bar_html = f"""
    <div title="{tooltip}" style='width: 100%; border: 1px solid #ccc; height: 30px; position: relative; background-color: #eee;'>
        <div style='height: 100%; width: {bar_width}%; background-color: {color}; position: absolute; {position}: 50%;'>
            <span style='position: absolute; width: 100%; {text_position} top: 0; line-height: 30px; color: white; {text_alignment}'>{display_similarity:.2f}%</span>
        </div>
    </div>
    """
    return bar_html


def load_samples():
    with open(HERE / "samples.yml", "r", encoding="utf-8") as file:
        return yaml.safe_load(file)["samples"]


samples = load_samples()


iscc_theme = gr.themes.Default(
    font=[gr.themes.GoogleFont("Readex Pro Light")],
    font_mono=[gr.themes.GoogleFont("JetBrains Mono")],
    text_size=gr.themes.sizes.text_lg,
    radius_size=gr.themes.sizes.radius_none,
)

with gr.Blocks(css=custom_css, theme=iscc_theme) as demo:
    with gr.Row(variant="panel"):
        gr.Markdown(
            """
        ## 🔮️ ISCC - Semantic-Code Text
        Demo of cross-lingual Semantic Text-Code (proof of concept)
        """,
        )
    with gr.Row(variant="panel"):
        with gr.Column(variant="panel"):
            sample_dropdown_a = gr.Dropdown(
                choices=["None"] + [lang for lang in samples["a"]],
                label="Select sample for Text A",
                value="None",
            )
        with gr.Column(variant="panel"):
            sample_dropdown_b = gr.Dropdown(
                choices=["None"] + [lang for lang in samples["b"]],
                label="Select sample for Text B",
                value="None",
            )

    with gr.Row(variant="panel"):
        with gr.Column(variant="panel"):
            in_text_a = gr.TextArea(
                label="Text A",
                placeholder="Choose sample text from the dropdown above or type or paste your text.",
                lines=12,
                max_lines=12,
            )
            out_code_a = gr.Textbox(label="ISCC-SCT for Text A")
        with gr.Column(variant="panel"):
            in_text_b = gr.TextArea(
                label="Text B",
                placeholder="Choose sample text from the dropdown above or type or paste your text.",
                lines=12,
                max_lines=12,
            )
            out_code_b = gr.Textbox(label="ISCC-SCT for Text B")

    with gr.Row(variant="panel"):
        with gr.Column(variant="panel"):
            out_similarity_title = gr.Markdown("### ISCC-based Semantic Similarity")
            with gr.Row(elem_classes="simbar"):
                out_similarity = gr.HTML()
            gr.Markdown(
                "**NOTE:** Similarity is calculated based on the generated ISCC-SCT, not the original text."
            )

    with gr.Row(variant="panel"):
        reset_button = gr.Button("Reset All")

    with gr.Accordion(label="🔍 Explore Details & Advanced Options", open=True):
        with gr.Row(variant="panel"):
            with gr.Column(variant="panel"):
                in_iscc_bits = gr.Slider(
                    label="ISCC Bit-Length",
                    info="NUMBER OF BITS FOR OUTPUT ISCC",
                    minimum=64,
                    maximum=256,
                    step=32,
                    value=sct.sct_opts.bits,
                )
            with gr.Column(variant="panel"):
                in_max_tokens = gr.Slider(
                    label="Max Tokens",
                    info="MAXIMUM NUMBER OF TOKENS PER CHUNK",
                    minimum=49,
                    maximum=sct.sct_opts.max_tokens,
                    step=1,
                    value=127,
                )

        with gr.Row(variant="panel"):
            with gr.Column(variant="panel"):
                out_chunks_a = gr.HighlightedText(
                    label="Chunked Text A",
                    interactive=False,
                    elem_id="chunked-text-a",
                )
            with gr.Column(variant="panel"):
                out_chunks_b = gr.HighlightedText(
                    label="Chunked Text B",
                    interactive=False,
                    elem_id="chunked-text-b",
                )

        with gr.Row(variant="panel"):
            with gr.Column(variant="panel"):
                gr.Markdown("### Granular Matches")
                in_granular_matches = gr.Dataframe(
                    headers=["Chunk A", "Similarity", "Chunk B"],
                    column_widths=["45%", "10%", "45%"],
                    wrap=True,
                    elem_classes="granular-matches",
                )

    def update_sample_text(choice, group):
        if choice == "None":
            return ""
        return samples[group][choice]

    sample_dropdown_a.change(
        lambda choice: update_sample_text(choice, "a"),
        inputs=[sample_dropdown_a],
        outputs=[in_text_a],
    )
    sample_dropdown_b.change(
        lambda choice: update_sample_text(choice, "b"),
        inputs=[sample_dropdown_b],
        outputs=[in_text_b],
    )

    def process_and_calculate(text_a, text_b, nbits, max_tokens):
        log.debug(f"Processing text_a: {text_a[:20]}, text_b: {text_b[:20]}")

        def process_single_text(text, suffix):
            out_code_func = globals().get(f"out_code_{suffix}")
            out_chunks_func = globals().get(f"out_chunks_{suffix}")

            if not text:
                return {
                    out_code_func: gr.Textbox(value=None),
                    out_chunks_func: gr.HighlightedText(
                        value=None, elem_id=f"chunked-text-{suffix}"
                    ),
                }

            result = sct.gen_text_code_semantic(
                text,
                bits=nbits,
                simprints=True,
                offsets=True,
                sizes=True,
                contents=True,
                max_tokens=max_tokens,
            )
            iscc = sct.Metadata(**result).to_object_format()

            # Generate chunked text with simprints and overlaps
            features = iscc.features[0]
            highlighted_chunks = []
            overlaps = iscc.get_overlaps()

            for i, feature in enumerate(features.simprints):
                feature: sct.Feature
                content = feature.content

                # Remove leading overlap
                if i > 0 and overlaps[i - 1]:
                    content = content[len(overlaps[i - 1]) :]

                # Remove trailing overlap
                if i < len(overlaps) and overlaps[i]:
                    content = content[: -len(overlaps[i])]

                label = f"{feature.size}:{feature.simprint}"
                highlighted_chunks.append((no_nl_inner(content), label))

                if i < len(overlaps):
                    overlap = overlaps[i]
                    if overlap:
                        highlighted_chunks.append((f"\n{no_nl(overlap)}\n", "overlap"))

            return {
                out_code_func: gr.Textbox(value=iscc.iscc),
                out_chunks_func: gr.HighlightedText(
                    value=highlighted_chunks, elem_id=f"chunked-text-{suffix}"
                ),
                "metadata": iscc,
            }

        result_a = process_single_text(text_a, "a")
        result_b = process_single_text(text_b, "b")

        code_a = result_a[out_code_a] if text_a else None
        code_b = result_b[out_code_b] if text_b else None

        similarity = compare_codes(code_a, code_b, nbits) or out_similarity

        granular_matches = []
        if text_a and text_b:
            matches = sct.granular_similarity(
                result_a["metadata"], result_b["metadata"], threshold=80
            )
            for match in matches:
                granular_matches.append(
                    [
                        match[0].content,
                        f"{match[1]}%",
                        match[2].content,
                    ]
                )

        return (
            result_a[out_code_a],
            result_a[out_chunks_a],
            result_b[out_code_b],
            result_b[out_chunks_b],
            similarity,
            gr.Dataframe(value=granular_matches),
        )

    in_text_a.change(
        process_and_calculate,
        inputs=[in_text_a, in_text_b, in_iscc_bits, in_max_tokens],
        outputs=[
            out_code_a,
            out_chunks_a,
            out_code_b,
            out_chunks_b,
            out_similarity,
            in_granular_matches,
        ],
        show_progress="full",
        trigger_mode="always_last",
    )

    in_text_b.change(
        process_and_calculate,
        inputs=[in_text_a, in_text_b, in_iscc_bits, in_max_tokens],
        outputs=[
            out_code_a,
            out_chunks_a,
            out_code_b,
            out_chunks_b,
            out_similarity,
            in_granular_matches,
        ],
        show_progress="full",
        trigger_mode="always_last",
    )

    in_iscc_bits.change(
        process_and_calculate,
        inputs=[in_text_a, in_text_b, in_iscc_bits, in_max_tokens],
        outputs=[
            out_code_a,
            out_chunks_a,
            out_code_b,
            out_chunks_b,
            out_similarity,
            in_granular_matches,
        ],
        show_progress="full",
    )

    in_max_tokens.change(
        process_and_calculate,
        inputs=[in_text_a, in_text_b, in_iscc_bits, in_max_tokens],
        outputs=[
            out_code_a,
            out_chunks_a,
            out_code_b,
            out_chunks_b,
            out_similarity,
            in_granular_matches,
        ],
        show_progress="full",
    )

    out_code_a.change(
        compare_codes, inputs=[out_code_a, out_code_b, in_iscc_bits], outputs=[out_similarity]
    )
    out_code_b.change(
        compare_codes, inputs=[out_code_a, out_code_b, in_iscc_bits], outputs=[out_similarity]
    )

    def reset_all():
        return (
            gr.Slider(value=64),  # Reset ISCC Bit-Length
            gr.Dropdown(
                value="None", choices=["None"] + [lang for lang in samples["a"]]
            ),  # Reset sample dropdown A
            gr.Dropdown(
                value="None", choices=["None"] + [lang for lang in samples["b"]]
            ),  # Reset sample dropdown B
            gr.TextArea(value=""),  # Reset Text A
            gr.TextArea(value=""),  # Reset Text B
            gr.Textbox(value=""),  # Reset ISCC Code for Text A
            gr.Textbox(value=""),  # Reset ISCC Code for Text B
            gr.HTML(value=""),  # Reset Similarity
            gr.HighlightedText(value=[]),  # Reset Chunked Text A
            gr.HighlightedText(value=[]),  # Reset Chunked Text B
        )

    reset_button.click(
        reset_all,
        outputs=[
            in_iscc_bits,
            sample_dropdown_a,
            sample_dropdown_b,
            in_text_a,
            in_text_b,
            out_code_a,
            out_code_b,
            out_similarity,
            out_chunks_a,
            out_chunks_b,
        ],
    )

    with gr.Row(variant="panel"):
        gr.Markdown(
            """
## Understanding ISCC Semantic Text-Codes

### What is an ISCC Semantic Text-Code?
An ISCC Semantic Text-Code is a digital fingerprint for text content. It captures the meaning of
the text, not just the exact words. Technically it is am ISCC-encoded, binarized multi-lingual
document-embedding.

### How does it work?
1. **Input**: You provide a text in any language.
2. **Processing**: Vector embeddings are created for individual chunks of the text.
3. **Output**: A unique ISCC-UNIT is generated that represents the entire text's content.

### What can it do?
- **Cross-language matching**: It can recognize similar content across different languages.
- **Similarity detection**: It can measure how similar two texts are in meaning, not just in words.
- **Content identification**: It can help identify texts with similar content, even if the wording
    is different.

### How to use this demo:
1. **Enter text**: Type or paste text into either or both text boxes.
2. **Adjust bit length**: Use the slider to change the detail level of the code (higher = more
    detailed).
3. **View results**: See the generated ISCC code for each text.
4. **Compare**: Look at the similarity bar to see how alike the two texts are in meaning, based on
    their ISCC codes.

### Important Note:
The similarity shown is calculated by comparing the ISCC codes, not the original texts. This
allows for efficient and privacy-preserving comparisons, as only the codes need to be shared
or stored.
"""
        )

        gr.Markdown(
            """
### Why is this useful?
- **Content creators**: Find similar content across languages.
- **Researchers**: Quickly compare documents or find related texts in different languages.
- **Publishers**: Identify potential translations or similar works efficiently.

This technology opens up new possibilities for understanding and managing text content across
language barriers!

### Explore Details & Advanced Options

The "Explore Details & Advanced Options" section provides additional tools and information:

1. **ISCC Bit-Length**: Adjust the precision of the ISCC code. Higher values provide more detailed
comparisons but may be more sensitive to minor differences.

2. **Max Tokens**: Set the maximum number of tokens per chunk. This affects how the text is split
for processing.

3. **Chunked Text**: View how each input text is divided into chunks for processing. Each chunk is
color-coded and labeled with its size and simprint (a similarity preserving fingerprint).

4. **Granular Matches**: See a detailed comparison of individual chunks between Text A and Text B.
This table shows which specific parts of the texts are most similar (above 80%), along with their
approximate cosine similarity (scaled -100% to +100%).

For more information about the **ISCC** see:
- https://github.com/iscc
- https://iscc.codes
- https://iscc.io
- [ISO 24138:2024](https://www.iso.org/standard/77899.html)
"""
        )
    with gr.Row():
        gr.Markdown(
            f"iscc-sct v{sct.__version__} | Source Code: https://github.com/iscc/iscc-sct",
            elem_classes="footer",
        )

if __name__ == "__main__":  # pragma: no cover
    demo.launch()