Skip to content

Commit 6a5aea0

Browse files
Merge pull request #13 from openai/enhance-gabriel.view-for-paraphrase-display
Enhance gabriel.view to render paraphrases and media inputs
2 parents 01442c6 + c898d08 commit 6a5aea0

1 file changed

Lines changed: 259 additions & 3 deletions

File tree

src/gabriel/utils/passage_viewer.py

Lines changed: 259 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import math
44
import html
55
import json
6+
import os
67
import random
78
import re
89
import uuid
@@ -28,6 +29,9 @@
2829
except Exception: # pragma: no cover - optional dependency
2930
plt = None
3031

32+
from .audio_utils import encode_audio
33+
from .media_utils import load_image_inputs
34+
3135

3236
def _generate_distinct_colors(n: int) -> List[str]:
3337
"""Generate ``n`` visually distinct hex colors.
@@ -984,6 +988,49 @@ def show(self):
984988
.gabriel-codify-viewer .gabriel-text p {
985989
margin: 0 0 1em 0;
986990
}
991+
.gabriel-codify-viewer .gabriel-paraphrase {
992+
display: flex;
993+
flex-direction: column;
994+
gap: 12px;
995+
}
996+
.gabriel-codify-viewer .gabriel-paraphrase-label {
997+
text-transform: uppercase;
998+
font-size: 10px;
999+
letter-spacing: 0.12em;
1000+
color: rgba(255, 255, 255, 0.55);
1001+
margin-bottom: 6px;
1002+
}
1003+
.gabriel-codify-viewer .gabriel-paraphrase-body {
1004+
font-size: 15px;
1005+
line-height: 1.7;
1006+
color: rgba(245, 247, 250, 0.96);
1007+
}
1008+
.gabriel-codify-viewer .gabriel-paraphrase-body--emphasis {
1009+
font-weight: 600;
1010+
}
1011+
.gabriel-codify-viewer .gabriel-paraphrase-divider {
1012+
height: 1px;
1013+
background: rgba(255, 255, 255, 0.12);
1014+
border-radius: 999px;
1015+
}
1016+
.gabriel-codify-viewer .gabriel-media {
1017+
display: flex;
1018+
justify-content: center;
1019+
align-items: center;
1020+
padding: 18px;
1021+
border-radius: 16px;
1022+
border: 1px solid rgba(255, 255, 255, 0.12);
1023+
background: rgba(255, 255, 255, 0.04);
1024+
}
1025+
.gabriel-codify-viewer .gabriel-media--image img {
1026+
max-width: min(100%, 520px);
1027+
height: auto;
1028+
border-radius: 12px;
1029+
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.35);
1030+
}
1031+
.gabriel-codify-viewer .gabriel-media--audio audio {
1032+
width: min(480px, 100%);
1033+
}
9871034
.gabriel-codify-viewer .gabriel-snippet {
9881035
position: relative;
9891036
border-radius: 6px;
@@ -1052,6 +1099,19 @@ def show(self):
10521099
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-text {
10531100
color: #1f2933;
10541101
}
1102+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-paraphrase-label {
1103+
color: rgba(15, 23, 42, 0.6);
1104+
}
1105+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-paraphrase-body {
1106+
color: #1f2933;
1107+
}
1108+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-paraphrase-divider {
1109+
background: rgba(15, 23, 42, 0.12);
1110+
}
1111+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-media {
1112+
background: rgba(15, 23, 42, 0.04);
1113+
border-color: rgba(15, 23, 42, 0.12);
1114+
}
10551115
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item {
10561116
background: rgba(15, 23, 42, 0.06);
10571117
border-color: rgba(15, 23, 42, 0.12);
@@ -1600,6 +1660,186 @@ def _format_header_value(value: Any) -> str:
16001660
return str(value)
16011661

16021662

1663+
_IMAGE_EXTENSIONS = {
1664+
".png",
1665+
".jpg",
1666+
".jpeg",
1667+
".gif",
1668+
".bmp",
1669+
".webp",
1670+
".tiff",
1671+
".tif",
1672+
".svg",
1673+
}
1674+
_AUDIO_EXTENSIONS = {
1675+
".mp3",
1676+
".wav",
1677+
".m4a",
1678+
".aac",
1679+
".ogg",
1680+
".flac",
1681+
".opus",
1682+
}
1683+
_IMAGE_MIME_MAP = {
1684+
".png": "image/png",
1685+
".jpg": "image/jpeg",
1686+
".jpeg": "image/jpeg",
1687+
".gif": "image/gif",
1688+
".bmp": "image/bmp",
1689+
".webp": "image/webp",
1690+
".tiff": "image/tiff",
1691+
".tif": "image/tiff",
1692+
".svg": "image/svg+xml",
1693+
}
1694+
1695+
1696+
def _looks_like_base64(text: str) -> bool:
1697+
if not text:
1698+
return False
1699+
cleaned = text.strip()
1700+
if len(cleaned) < 120:
1701+
return False
1702+
return bool(re.fullmatch(r"[A-Za-z0-9+/=\s]+", cleaned))
1703+
1704+
1705+
def _resolve_paraphrase_columns(
1706+
df: pd.DataFrame,
1707+
column_name: str,
1708+
) -> Optional[Tuple[str, str]]:
1709+
base = f"{column_name}_revised"
1710+
if base in df.columns:
1711+
return column_name, base
1712+
pattern = re.compile(rf"^{re.escape(base)}_(\d+)$")
1713+
matches: List[Tuple[int, str]] = []
1714+
for col in df.columns:
1715+
match = pattern.match(str(col))
1716+
if match:
1717+
try:
1718+
matches.append((int(match.group(1)), str(col)))
1719+
except ValueError:
1720+
continue
1721+
if matches:
1722+
matches.sort(key=lambda item: item[0])
1723+
return column_name, matches[0][1]
1724+
return None
1725+
1726+
1727+
def _media_from_value(value: Any) -> Optional[Dict[str, str]]:
1728+
if _is_na(value):
1729+
return None
1730+
if isinstance(value, (list, tuple, set)):
1731+
for item in value:
1732+
media = _media_from_value(item)
1733+
if media:
1734+
return media
1735+
return None
1736+
if isinstance(value, dict):
1737+
file_url = value.get("file_url") or value.get("url")
1738+
if isinstance(file_url, str) and file_url.strip():
1739+
return {"type": "audio", "src": file_url.strip()}
1740+
data = value.get("data")
1741+
if isinstance(data, str) and data.strip():
1742+
fmt = value.get("format") or "wav"
1743+
return {"type": "audio", "src": f"data:audio/{fmt};base64,{data}"}
1744+
return None
1745+
if not isinstance(value, str):
1746+
return None
1747+
text = value.strip()
1748+
if not text:
1749+
return None
1750+
lowered = text.lower()
1751+
if lowered.startswith("data:image"):
1752+
return {"type": "image", "src": text}
1753+
if lowered.startswith("data:audio"):
1754+
return {"type": "audio", "src": text}
1755+
if lowered.startswith("http://") or lowered.startswith("https://"):
1756+
ext = os.path.splitext(lowered)[1]
1757+
if ext in _IMAGE_EXTENSIONS:
1758+
return {"type": "image", "src": text}
1759+
if ext in _AUDIO_EXTENSIONS:
1760+
return {"type": "audio", "src": text}
1761+
return None
1762+
ext = os.path.splitext(lowered)[1]
1763+
if os.path.exists(text):
1764+
if ext in _IMAGE_EXTENSIONS:
1765+
encoded = load_image_inputs(text)
1766+
if encoded:
1767+
mime = _IMAGE_MIME_MAP.get(ext, "image/png")
1768+
return {"type": "image", "src": f"data:{mime};base64,{encoded[0]}"}
1769+
if ext in _AUDIO_EXTENSIONS:
1770+
encoded_audio = encode_audio(text)
1771+
if encoded_audio and encoded_audio.get("data"):
1772+
fmt = encoded_audio.get("format") or ext.lstrip(".")
1773+
return {
1774+
"type": "audio",
1775+
"src": f"data:audio/{fmt};base64,{encoded_audio['data']}",
1776+
}
1777+
return None
1778+
if _looks_like_base64(text):
1779+
return {"type": "image", "src": f"data:image/png;base64,{text}"}
1780+
return None
1781+
1782+
1783+
def _build_media_html(media: Dict[str, str]) -> str:
1784+
media_type = media.get("type")
1785+
src = media.get("src", "")
1786+
safe_src = html.escape(src, quote=True)
1787+
if media_type == "image":
1788+
return (
1789+
"<div class='gabriel-media gabriel-media--image'>"
1790+
f"<img src='{safe_src}' alt='Image input'/>"
1791+
"</div>"
1792+
)
1793+
if media_type == "audio":
1794+
return (
1795+
"<div class='gabriel-media gabriel-media--audio'>"
1796+
f"<audio controls src='{safe_src}'></audio>"
1797+
"</div>"
1798+
)
1799+
return ""
1800+
1801+
1802+
def _build_paraphrase_html(paraphrased: str, original: str) -> str:
1803+
return (
1804+
"<div class='gabriel-paraphrase'>"
1805+
"<div class='gabriel-paraphrase-section'>"
1806+
"<div class='gabriel-paraphrase-label'>Paraphrased text</div>"
1807+
"<div class='gabriel-paraphrase-body gabriel-paraphrase-body--emphasis'>"
1808+
f"{paraphrased}</div>"
1809+
"</div>"
1810+
"<div class='gabriel-paraphrase-divider'></div>"
1811+
"<div class='gabriel-paraphrase-section'>"
1812+
"<div class='gabriel-paraphrase-label'>Original text</div>"
1813+
f"<div class='gabriel-paraphrase-body'>{original}</div>"
1814+
"</div>"
1815+
"</div>"
1816+
)
1817+
1818+
1819+
def _build_passage_body(
1820+
original_text: str,
1821+
revised_text: Optional[str],
1822+
snippet_map: Dict[str, List[str]],
1823+
category_colors: Dict[str, str],
1824+
category_labels: Dict[str, str],
1825+
media: Optional[Dict[str, str]] = None,
1826+
) -> str:
1827+
if media:
1828+
original_html = _build_media_html(media)
1829+
else:
1830+
original_html = _build_highlighted_text(
1831+
original_text, snippet_map, category_colors, category_labels
1832+
)
1833+
if revised_text is None:
1834+
return original_html
1835+
revised_value = revised_text if revised_text is not None else ""
1836+
if revised_value and revised_value.strip():
1837+
paraphrased_html = html.escape(revised_value).replace("\n", "<br/>")
1838+
else:
1839+
paraphrased_html = "<div class='gabriel-empty'>No paraphrased text available.</div>"
1840+
return _build_paraphrase_html(paraphrased_html, original_html)
1841+
1842+
16031843
def _build_highlighted_text(
16041844
text: str,
16051845
snippet_map: Dict[str, List[str]],
@@ -1921,6 +2161,7 @@ def _render_passage_viewer(
19212161

19222162
df = _normalize_structured_dataframe(df, snippet_columns)
19232163
normalized_headers = _normalize_header_columns(header_columns)
2164+
paraphrase_columns = _resolve_paraphrase_columns(df, column_name)
19242165

19252166
numeric_bounds: Dict[str, List[Optional[float]]] = {
19262167
spec.column: [None, None] for spec in numeric_specs
@@ -1959,8 +2200,16 @@ def _render_passage_viewer(
19592200

19602201
passages: List[Dict[str, Any]] = []
19612202
for _, row in df.iterrows():
1962-
raw_text = row.get(column_name)
2203+
if paraphrase_columns:
2204+
original_column, revised_column = paraphrase_columns
2205+
raw_text = row.get(original_column)
2206+
raw_revised = row.get(revised_column)
2207+
revised_text = "" if _is_na(raw_revised) else str(raw_revised)
2208+
else:
2209+
raw_text = row.get(column_name)
2210+
revised_text = None
19632211
text = "" if _is_na(raw_text) else str(raw_text)
2212+
media = _media_from_value(raw_text)
19642213
snippet_map: Dict[str, List[str]] = {cat: [] for cat in category_names}
19652214
bool_values: Dict[str, Optional[bool]] = {}
19662215
numeric_values: Dict[str, Optional[float]] = {}
@@ -2021,6 +2270,8 @@ def _render_passage_viewer(
20212270
passages.append(
20222271
{
20232272
"text": text,
2273+
"revised": revised_text,
2274+
"media": media,
20242275
"snippets": snippet_map,
20252276
"header": header_rows,
20262277
"active": active_categories,
@@ -2079,8 +2330,13 @@ def _render_passage_viewer(
20792330

20802331
render_entries: List[Dict[str, Any]] = []
20812332
for idx, payload in enumerate(passages):
2082-
body_html = _build_highlighted_text(
2083-
payload["text"], payload["snippets"], category_colors, category_labels
2333+
body_html = _build_passage_body(
2334+
payload["text"],
2335+
payload.get("revised"),
2336+
payload["snippets"],
2337+
category_colors,
2338+
category_labels,
2339+
media=payload.get("media"),
20842340
)
20852341
header_html = _build_header_html(
20862342
payload["header"], payload["active"]

0 commit comments

Comments
 (0)