Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 5,467 Bytes
0edb292 9822776 0edb292 2592c19 0edb292 2592c19 0edb292 2592c19 0edb292 9822776 0edb292 4e3fc96 b3722c4 4e3fc96 b3722c4 4e3fc96 0edb292 | 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 | """HTML for the results list.
The list is plain markup with `data-` attributes; a single delegated click
handler in the page head turns any timestamp into a seek. No per-card
JavaScript, no component churn — Gradio only ever hands back a string.
"""
from __future__ import annotations
from html import escape
from search import MAX_SHOWN_EVENTS, Hit
def _safe_url(url: str | None) -> str:
"""Only ever emit an http(s) link. The URLs come from the dataset, and a
link labelled 'archive.org' should not be able to send a visitor to a
`javascript:` or `data:` target if a row is ever wrong."""
url = (url or "").strip()
return url if url.startswith(("https://", "http://")) else "#"
def clock(seconds: float) -> str:
seconds = max(0, int(round(seconds)))
h, rem = divmod(seconds, 3600)
m, s = divmod(rem, 60)
return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
MIN_EVENTS = 3
def _events_html(hit: Hit) -> str:
"""Matched events, padded with their neighbours up to a readable minimum.
A semantic hit often matches no event text literally — the query never
appears in the captions — so without padding those results collapse to one
line. Padding is greyed: only literal matches get the accent, which keeps
the colour honest about what it means.
"""
matched = hit.matched[:MAX_SHOWN_EVENTS]
hits = {id(e) for e in matched}
shown = list(matched)
for event in hit.events:
if len(shown) >= MIN_EVENTS:
break
if id(event) not in hits:
shown.append(event)
shown.sort(key=lambda e: e["start"])
if not shown:
return ""
rows = []
for event in shown:
rows.append(
"<li{cls}>"
'<button class="at" data-src="{src}" data-t="{t}" '
'data-title="{title}" data-when="{when}">{when}</button>'
"<span>{text}</span>"
"</li>".format(
cls=' class="hit"' if id(event) in hits else "",
src=escape(hit.video_url, quote=True),
t=f"{event['start']:.2f}",
title=escape(hit.title, quote=True),
when=clock(event["start"]),
text=escape(event["text"]),
)
)
return f'<ul class="events">{"".join(rows)}</ul>'
def moment(hit: Hit) -> str:
when = clock(hit.jump)
stamp = " · ".join(
part
for part in (
str(hit.year) if hit.year else "undated",
f"{clock(hit.chunk_start)}–{clock(hit.chunk_end)}",
)
if part
)
scene = escape(hit.scene)
scene_html = (
f'<details class="scene"><summary>scene</summary><p>{scene}</p></details>'
if scene
else ""
)
return f"""
<article class="moment" id="m-{escape(hit.moment_id, quote=True)}">
<button class="frame" data-src="{escape(hit.video_url, quote=True)}"
data-t="{hit.jump:.2f}" data-title="{escape(hit.title, quote=True)}"
data-when="{when}" aria-label="Play {escape(hit.title, quote=True)} at {when}">
<img src="{escape(hit.thumb_url, quote=True)}" alt="" loading="lazy" decoding="async">
<span class="cue">{when}</span>
</button>
<div class="meta">
<h2>{escape(hit.title)}</h2>
<p class="stamp">{stamp}
<a href="{escape(_safe_url(hit.ia_url), quote=True)}" target="_blank" rel="noopener">archive.org</a>
</p>
{_events_html(hit)}
{scene_html}
</div>
</article>
"""
def opening(hits: list[Hit]) -> str:
"""What an empty search box shows: some of the collection, not a blank page.
Every tile plays, so the first thing the page offers is the thing it does.
"""
if not hits:
return ""
tiles = []
for hit in hits:
when = clock(hit.chunk_start)
tiles.append(
f"""
<div class="tile">
<button class="frame" data-src="{escape(hit.video_url, quote=True)}"
data-t="{hit.chunk_start:.2f}" data-title="{escape(hit.title, quote=True)}"
data-when="{when}" aria-label="Play {escape(hit.title, quote=True)} at {when}">
<img src="{escape(hit.thumb_url, quote=True)}" alt="" loading="lazy" decoding="async">
<span class="cue">{when}</span>
</button>
<h3>{escape(hit.title)}</h3>
<p class="stamp">{hit.year or "undated"}</p>
<!-- Not shown in the grid, but the player panel clones it on click: browsing
at random should show you what the model wrote, not just play the clip. -->
<div class="detail" hidden>
{_events_html(hit)}
<div class="scene"><p>{escape(hit.scene)}</p></div>
</div>
</div>"""
)
return (
'<p class="count">a few moments, at random — click one to read what the '
"model saw</p>"
f'<div class="opening">{"".join(tiles)}</div>'
)
def results(hits: list[Hit], query: str, elapsed_ms: float) -> str:
if not query.strip():
return ""
if not hits:
return (
'<p class="empty">Nothing for <em>%s</em>. '
"The captions describe what is on screen — try what you would "
"<em>see</em>: a place, an object, an action.</p>" % escape(query)
)
films = len({h.identifier for h in hits})
head = (
f'<p class="count">{len(hits)} moments · {films} '
f'{"film" if films == 1 else "films"}'
f'<span class="ms">{elapsed_ms:.0f} ms</span></p>'
)
return head + "".join(moment(h) for h in hits)
|