Spaces:
Paused
Paused
Shereen Lee commited on
Commit ·
387e3e7
1
Parent(s): 3349b34
feat: immersive Cesium watercolour (stamen imagery + paint post-process + chase cam), polaroid polish (tiny-mono dd/mm/yyyy, 4/5 aspect, longer caption), timeline by date, fix 2D map expand collapse, stadia key config
Browse files- app.py +5 -0
- index.html +72 -26
app.py
CHANGED
|
@@ -1259,6 +1259,10 @@ async def phoenix_proxy_post(request: Request, path: str):
|
|
| 1259 |
from dotenv import load_dotenv
|
| 1260 |
load_dotenv()
|
| 1261 |
CESIUM_ION_TOKEN = os.environ.get("SOZAI_CESIUM_ION_TOKEN", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1262 |
|
| 1263 |
|
| 1264 |
@app.get("/api/config")
|
|
@@ -1271,6 +1275,7 @@ def client_config():
|
|
| 1271 |
return {
|
| 1272 |
"cesiumIonToken": CESIUM_ION_TOKEN,
|
| 1273 |
"cesiumEnabled": bool(CESIUM_ION_TOKEN),
|
|
|
|
| 1274 |
"nsfwEnabled": True, # the gate always runs (open if model absent)
|
| 1275 |
"asrEnabled": True, # the endpoint always exists (may be unavailable)
|
| 1276 |
"asrModel": ASR_MODEL_ID,
|
|
|
|
| 1259 |
from dotenv import load_dotenv
|
| 1260 |
load_dotenv()
|
| 1261 |
CESIUM_ION_TOKEN = os.environ.get("SOZAI_CESIUM_ION_TOKEN", "")
|
| 1262 |
+
# Free Stadia Maps API key (https://client.stadiamaps.com) for the Stamen
|
| 1263 |
+
# Watercolor basemap on the Cesium globe. Optional — without it the globe keeps
|
| 1264 |
+
# its default imagery.
|
| 1265 |
+
STADIA_MAPS_KEY = os.environ.get("SOZAI_STADIA_KEY", "")
|
| 1266 |
|
| 1267 |
|
| 1268 |
@app.get("/api/config")
|
|
|
|
| 1275 |
return {
|
| 1276 |
"cesiumIonToken": CESIUM_ION_TOKEN,
|
| 1277 |
"cesiumEnabled": bool(CESIUM_ION_TOKEN),
|
| 1278 |
+
"stadiaMapsKey": STADIA_MAPS_KEY,
|
| 1279 |
"nsfwEnabled": True, # the gate always runs (open if model absent)
|
| 1280 |
"asrEnabled": True, # the endpoint always exists (may be unavailable)
|
| 1281 |
"asrModel": ASR_MODEL_ID,
|
index.html
CHANGED
|
@@ -272,9 +272,9 @@
|
|
| 272 |
transition: transform .2s; cursor:grab; }
|
| 273 |
.sozai-polaroid-wrap.is-dragging .sozai-polaroid { cursor:grabbing; }
|
| 274 |
.sozai-polaroid:hover { z-index:50; }
|
| 275 |
-
.sozai-polaroid img { display:block; width:
|
| 276 |
-
.sozai-polaroid .cap { font-family:'
|
| 277 |
-
text-align:center; padding:3px
|
| 278 |
.sozai-polaroid .pdel { position:absolute; top:-8px; right:-8px; width:18px; height:18px; border-radius:50%;
|
| 279 |
background:#fff; border:1.5px solid var(--primary,#7c3aed); color:var(--primary,#7c3aed);
|
| 280 |
font-size:11px; line-height:1; display:none; align-items:center; justify-content:center; cursor:pointer; z-index:3; }
|
|
@@ -1622,7 +1622,7 @@
|
|
| 1622 |
className={cn("relative rounded-md border-2 border-primary bg-card p-3 shadow-sm",
|
| 1623 |
destroying ? "sozai-destroy" : (slideDir === "left" ? "sozai-slide-left" : "sozai-slide-right"))}>
|
| 1624 |
{destroying ? <span className="sozai-tear" /> : null}
|
| 1625 |
-
<div className="relative aspect-[4/
|
| 1626 |
<DraggableImage src={photo.src} alt={photo.alt}
|
| 1627 |
focusX={photo.focusX != null ? photo.focusX : 50}
|
| 1628 |
focusY={photo.focusY != null ? photo.focusY : 50}
|
|
@@ -2427,22 +2427,26 @@
|
|
| 2427 |
// components/map/timeline.tsx
|
| 2428 |
// ----------------------------------------------------------------- //
|
| 2429 |
function Timeline({ date, moments, selectedId, onSelect }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2430 |
return (
|
| 2431 |
<div className="flex flex-col gap-4">
|
| 2432 |
<div className="flex items-center justify-between gap-4">
|
| 2433 |
<h2 className="font-serif text-sm font-semibold text-foreground">{date}</h2>
|
| 2434 |
-
<span className="font-serif text-xs italic text-muted-foreground">
|
| 2435 |
</div>
|
| 2436 |
<div className="relative">
|
| 2437 |
<span aria-hidden="true" className="absolute left-0 right-0 top-3 h-0.5 bg-primary/30" />
|
| 2438 |
<ol className="relative flex items-start justify-between">
|
| 2439 |
-
{
|
| 2440 |
const isActive = m.id === selectedId;
|
| 2441 |
return (
|
| 2442 |
<li key={m.id} className="flex flex-1 flex-col items-center gap-2">
|
| 2443 |
-
<button type="button" onClick={() => onSelect && onSelect(m.id)} aria-current={isActive ? "true" : undefined} aria-label={`${m.title}
|
| 2444 |
-
className={cn("z-10 flex size-6 items-center justify-center rounded-full border-2 border-primary bg-card font-mono text-xs font-bold text-primary transition-colors hover:bg-accent", isActive && "bg-primary text-primary-foreground")}>{
|
| 2445 |
-
<span className="text-center font-mono text-[10px] leading-tight text-muted-foreground">{m.
|
| 2446 |
</li>
|
| 2447 |
);
|
| 2448 |
})}
|
|
@@ -3902,7 +3906,8 @@
|
|
| 3902 |
el.className = "sozai-polaroid";
|
| 3903 |
el.style.setProperty("--rot", rot + "deg");
|
| 3904 |
el.style.setProperty("--scale", scale);
|
| 3905 |
-
const
|
|
|
|
| 3906 |
const objPos = (p.focusX != null ? p.focusX : 50) + "% " + (p.focusY != null ? p.focusY : 50) + "%";
|
| 3907 |
el.innerHTML =
|
| 3908 |
'<img src="' + p.src + '" alt="" style="object-position:' + objPos + '">' +
|
|
@@ -3979,8 +3984,12 @@
|
|
| 3979 |
}
|
| 3980 |
}, [isFullscreen]);
|
| 3981 |
useEffect(() => {
|
| 3982 |
-
|
| 3983 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3984 |
}, [isFullscreen]);
|
| 3985 |
function toggleFullscreen() { setIsFullscreen((v) => !v); }
|
| 3986 |
|
|
@@ -4284,7 +4293,7 @@
|
|
| 4284 |
) : null}
|
| 4285 |
|
| 4286 |
{/* the map itself, with the authentic watercolor paper + vignette */}
|
| 4287 |
-
<div className={cn("sozai-map-dropzone relative w-full overflow-hidden rounded-lg border-2 border-primary/40", isFullscreen && "flex-1", dropArmed && "drop-armed")}
|
| 4288 |
onDragOver={(e) => { if (e.dataTransfer.types.includes("application/sozai-photo")) { e.preventDefault(); setDropArmed(true); } }}
|
| 4289 |
onDragLeave={(e) => { if (e.currentTarget === e.target) setDropArmed(false); }}
|
| 4290 |
onDrop={(e) => {
|
|
@@ -4574,6 +4583,37 @@
|
|
| 4574 |
destination: Cesium.Cartesian3.fromDegrees(-123.1207, 49.2827, 1200),
|
| 4575 |
orientation: { heading: Cesium.Math.toRadians(10), pitch: Cesium.Math.toRadians(-25) },
|
| 4576 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4577 |
try {
|
| 4578 |
const tileset = await Cesium.createOsmBuildingsAsync();
|
| 4579 |
if (cancelled) { viewer.destroy(); return; }
|
|
@@ -4795,18 +4835,24 @@
|
|
| 4795 |
const onTick = viewer.clock.onTick.addEventListener((clock) => {
|
| 4796 |
if (token.stop) return;
|
| 4797 |
const t = Cesium.JulianDate.secondsDifference(clock.currentTime, start);
|
| 4798 |
-
// chase camera
|
|
|
|
|
|
|
| 4799 |
try {
|
| 4800 |
-
const
|
| 4801 |
-
|
| 4802 |
-
|
| 4803 |
-
|
| 4804 |
-
|
| 4805 |
-
|
| 4806 |
-
|
| 4807 |
-
|
| 4808 |
-
|
| 4809 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4810 |
}
|
| 4811 |
} catch (e) {}
|
| 4812 |
if (nextStop < stopTimes.length && t >= stopTimes[nextStop] - 0.2) {
|
|
@@ -6544,7 +6590,7 @@
|
|
| 6544 |
<li className="flex w-40 flex-col items-center sm:w-44">
|
| 6545 |
<div className="relative z-10 h-4 w-3 rounded-sm bg-[oklch(0.45_0.06_60)] shadow-sm" aria-hidden="true" />
|
| 6546 |
<figure
|
| 6547 |
-
className={cn("group relative -mt-1 select-none rounded-sm bg-[oklch(0.99_0.01_85)] p-2 pb-
|
| 6548 |
dragging ? "" : "dr-sway", revealed ? "cursor-default" : "cursor-grab active:cursor-grabbing")}
|
| 6549 |
style={{ animationDelay: swayDelay + "s",
|
| 6550 |
transform: dragging ? "rotate(" + (tilt * 0.3) + "deg) translateX(" + (tilt * 0.4) + "px)" : undefined,
|
|
@@ -6569,7 +6615,7 @@
|
|
| 6569 |
</div>
|
| 6570 |
)}
|
| 6571 |
</div>
|
| 6572 |
-
<figcaption className="absolute inset-x-0 bottom-2 line-clamp-
|
| 6573 |
style={revealed ? { animation: "dr-cap-in .6s ease-out both" } : undefined}>
|
| 6574 |
{revealed ? photo.defaultCaption : ""}
|
| 6575 |
</figcaption>
|
|
|
|
| 272 |
transition: transform .2s; cursor:grab; }
|
| 273 |
.sozai-polaroid-wrap.is-dragging .sozai-polaroid { cursor:grabbing; }
|
| 274 |
.sozai-polaroid:hover { z-index:50; }
|
| 275 |
+
.sozai-polaroid img { display:block; width:100px; height:125px; object-fit:cover; }
|
| 276 |
+
.sozai-polaroid .cap { font-family:'Martian Mono',monospace; font-size:9px; line-height:1.25; color:var(--foreground);
|
| 277 |
+
text-align:center; padding:4px 3px 5px; max-width:100px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
| 278 |
.sozai-polaroid .pdel { position:absolute; top:-8px; right:-8px; width:18px; height:18px; border-radius:50%;
|
| 279 |
background:#fff; border:1.5px solid var(--primary,#7c3aed); color:var(--primary,#7c3aed);
|
| 280 |
font-size:11px; line-height:1; display:none; align-items:center; justify-content:center; cursor:pointer; z-index:3; }
|
|
|
|
| 1622 |
className={cn("relative rounded-md border-2 border-primary bg-card p-3 shadow-sm",
|
| 1623 |
destroying ? "sozai-destroy" : (slideDir === "left" ? "sozai-slide-left" : "sozai-slide-right"))}>
|
| 1624 |
{destroying ? <span className="sozai-tear" /> : null}
|
| 1625 |
+
<div className="relative aspect-[4/5] overflow-hidden rounded-sm border-2 border-primary/70">
|
| 1626 |
<DraggableImage src={photo.src} alt={photo.alt}
|
| 1627 |
focusX={photo.focusX != null ? photo.focusX : 50}
|
| 1628 |
focusY={photo.focusY != null ? photo.focusY : 50}
|
|
|
|
| 2427 |
// components/map/timeline.tsx
|
| 2428 |
// ----------------------------------------------------------------- //
|
| 2429 |
function Timeline({ date, moments, selectedId, onSelect }) {
|
| 2430 |
+
// Order chronologically by calendar DATE (photos span multiple years) and
|
| 2431 |
+
// label each stop with its date — not the literal time of day.
|
| 2432 |
+
const shortDate = (s) => { const d = new Date(s); return isNaN(d) ? (s || "") : d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); };
|
| 2433 |
+
const sorted = (moments || []).slice().sort((a, b) => (new Date(a.date).getTime() || 0) - (new Date(b.date).getTime() || 0));
|
| 2434 |
return (
|
| 2435 |
<div className="flex flex-col gap-4">
|
| 2436 |
<div className="flex items-center justify-between gap-4">
|
| 2437 |
<h2 className="font-serif text-sm font-semibold text-foreground">{date}</h2>
|
| 2438 |
+
<span className="font-serif text-xs italic text-muted-foreground">your journey, in order</span>
|
| 2439 |
</div>
|
| 2440 |
<div className="relative">
|
| 2441 |
<span aria-hidden="true" className="absolute left-0 right-0 top-3 h-0.5 bg-primary/30" />
|
| 2442 |
<ol className="relative flex items-start justify-between">
|
| 2443 |
+
{sorted.map((m, i) => {
|
| 2444 |
const isActive = m.id === selectedId;
|
| 2445 |
return (
|
| 2446 |
<li key={m.id} className="flex flex-1 flex-col items-center gap-2">
|
| 2447 |
+
<button type="button" onClick={() => onSelect && onSelect(m.id)} aria-current={isActive ? "true" : undefined} aria-label={`${m.title} on ${shortDate(m.date)}`}
|
| 2448 |
+
className={cn("z-10 flex size-6 items-center justify-center rounded-full border-2 border-primary bg-card font-mono text-xs font-bold text-primary transition-colors hover:bg-accent", isActive && "bg-primary text-primary-foreground")}>{i + 1}</button>
|
| 2449 |
+
<span className="text-center font-mono text-[10px] leading-tight text-muted-foreground">{shortDate(m.date)}</span>
|
| 2450 |
</li>
|
| 2451 |
);
|
| 2452 |
})}
|
|
|
|
| 3906 |
el.className = "sozai-polaroid";
|
| 3907 |
el.style.setProperty("--rot", rot + "deg");
|
| 3908 |
el.style.setProperty("--scale", scale);
|
| 3909 |
+
const _dmy = (s) => { const d = new Date(s); if (isNaN(d)) return s; const z = (n) => String(n).padStart(2, "0"); return z(d.getDate()) + "/" + z(d.getMonth() + 1) + "/" + d.getFullYear(); };
|
| 3910 |
+
const meta = p.date ? _dmy(p.date) : "";
|
| 3911 |
const objPos = (p.focusX != null ? p.focusX : 50) + "% " + (p.focusY != null ? p.focusY : 50) + "%";
|
| 3912 |
el.innerHTML =
|
| 3913 |
'<img src="' + p.src + '" alt="" style="object-position:' + objPos + '">' +
|
|
|
|
| 3984 |
}
|
| 3985 |
}, [isFullscreen]);
|
| 3986 |
useEffect(() => {
|
| 3987 |
+
// Resize the GL canvas several times as the expand/collapse layout
|
| 3988 |
+
// settles — a single early resize() leaves the map blank/collapsed.
|
| 3989 |
+
const doResize = () => { if (mapRef.current) { try { mapRef.current.resize(); } catch (e) {} } };
|
| 3990 |
+
const raf = requestAnimationFrame(doResize);
|
| 3991 |
+
const ts = [setTimeout(doResize, 120), setTimeout(doResize, 350)];
|
| 3992 |
+
return () => { cancelAnimationFrame(raf); ts.forEach(clearTimeout); };
|
| 3993 |
}, [isFullscreen]);
|
| 3994 |
function toggleFullscreen() { setIsFullscreen((v) => !v); }
|
| 3995 |
|
|
|
|
| 4293 |
) : null}
|
| 4294 |
|
| 4295 |
{/* the map itself, with the authentic watercolor paper + vignette */}
|
| 4296 |
+
<div className={cn("sozai-map-dropzone relative w-full overflow-hidden rounded-lg border-2 border-primary/40", isFullscreen && "flex-1 min-h-0", dropArmed && "drop-armed")}
|
| 4297 |
onDragOver={(e) => { if (e.dataTransfer.types.includes("application/sozai-photo")) { e.preventDefault(); setDropArmed(true); } }}
|
| 4298 |
onDragLeave={(e) => { if (e.currentTarget === e.target) setDropArmed(false); }}
|
| 4299 |
onDrop={(e) => {
|
|
|
|
| 4583 |
destination: Cesium.Cartesian3.fromDegrees(-123.1207, 49.2827, 1200),
|
| 4584 |
orientation: { heading: Cesium.Math.toRadians(10), pitch: Cesium.Math.toRadians(-25) },
|
| 4585 |
});
|
| 4586 |
+
// Immersive watercolour basemap: Stamen Watercolor tiles (via Stadia
|
| 4587 |
+
// Maps). Needs a free Stadia API key (SOZAI_STADIA_KEY) — without it
|
| 4588 |
+
// the tiles 401 on dynamic domains, so we keep the default imagery.
|
| 4589 |
+
try {
|
| 4590 |
+
if (cfg.stadiaMapsKey) {
|
| 4591 |
+
viewer.imageryLayers.removeAll();
|
| 4592 |
+
viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
|
| 4593 |
+
url: "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg?api_key=" + cfg.stadiaMapsKey,
|
| 4594 |
+
maximumLevel: 16, // watercolour tiles thin out past z16
|
| 4595 |
+
credit: "Map tiles by Stamen Design, hosted by Stadia Maps; data © OpenStreetMap",
|
| 4596 |
+
}));
|
| 4597 |
+
}
|
| 4598 |
+
} catch (e) { console.warn("[cesium] watercolor imagery failed", e); }
|
| 4599 |
+
// Painterly post-process: posterize into flat washes + darken edges.
|
| 4600 |
+
try {
|
| 4601 |
+
viewer.scene.postProcessStages.add(new Cesium.PostProcessStage({
|
| 4602 |
+
fragmentShader: `
|
| 4603 |
+
uniform sampler2D colorTexture;
|
| 4604 |
+
in vec2 v_textureCoordinates;
|
| 4605 |
+
void main() {
|
| 4606 |
+
vec2 uv = v_textureCoordinates;
|
| 4607 |
+
vec3 c = texture(colorTexture, uv).rgb;
|
| 4608 |
+
c = floor(c * 6.0 + 0.5) / 6.0; // flat washes
|
| 4609 |
+
float o = 1.0 / 1024.0;
|
| 4610 |
+
vec3 dx = texture(colorTexture, uv+vec2(o,0.)).rgb - texture(colorTexture, uv-vec2(o,0.)).rgb;
|
| 4611 |
+
vec3 dy = texture(colorTexture, uv+vec2(0.,o)).rgb - texture(colorTexture, uv-vec2(0.,o)).rgb;
|
| 4612 |
+
float edge = clamp(length(dx)+length(dy), 0.0, 1.0);
|
| 4613 |
+
out_FragColor = vec4(c * (1.0 - 0.35*edge), 1.0);
|
| 4614 |
+
}`,
|
| 4615 |
+
}));
|
| 4616 |
+
} catch (e) { console.warn("[cesium] watercolor post-process failed", e); }
|
| 4617 |
try {
|
| 4618 |
const tileset = await Cesium.createOsmBuildingsAsync();
|
| 4619 |
if (cancelled) { viewer.destroy(); return; }
|
|
|
|
| 4835 |
const onTick = viewer.clock.onTick.addEventListener((clock) => {
|
| 4836 |
if (token.stop) return;
|
| 4837 |
const t = Cesium.JulianDate.secondsDifference(clock.currentTime, start);
|
| 4838 |
+
// immersive chase camera: sit behind the moving point along the
|
| 4839 |
+
// direction of travel and a little above it, looking ahead down the
|
| 4840 |
+
// route (vs. a fixed south-facing top-down view).
|
| 4841 |
try {
|
| 4842 |
+
const lookAhead = isPlane ? 4.0 : 2.0; // s ahead to aim at
|
| 4843 |
+
const chaseDist = isPlane ? 1400 : 260; // m behind the point
|
| 4844 |
+
const chaseHeight = isPlane ? 700 : 140; // m above the point
|
| 4845 |
+
const now = clock.currentTime;
|
| 4846 |
+
const pos = sampled.getValue(now);
|
| 4847 |
+
const ahead = sampled.getValue(Cesium.JulianDate.addSeconds(now, lookAhead, new Cesium.JulianDate())) || pos;
|
| 4848 |
+
if (pos && ahead) {
|
| 4849 |
+
const fwd = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(ahead, pos, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
| 4850 |
+
const up = Cesium.Cartesian3.normalize(pos, new Cesium.Cartesian3()); // geocentric up
|
| 4851 |
+
const cam = Cesium.Cartesian3.clone(pos);
|
| 4852 |
+
Cesium.Cartesian3.add(cam, Cesium.Cartesian3.multiplyByScalar(fwd, -chaseDist, new Cesium.Cartesian3()), cam);
|
| 4853 |
+
Cesium.Cartesian3.add(cam, Cesium.Cartesian3.multiplyByScalar(up, chaseHeight, new Cesium.Cartesian3()), cam);
|
| 4854 |
+
const dir = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(ahead, cam, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
| 4855 |
+
viewer.scene.camera.setView({ destination: cam, orientation: { direction: dir, up: up } });
|
| 4856 |
}
|
| 4857 |
} catch (e) {}
|
| 4858 |
if (nextStop < stopTimes.length && t >= stopTimes[nextStop] - 0.2) {
|
|
|
|
| 6590 |
<li className="flex w-40 flex-col items-center sm:w-44">
|
| 6591 |
<div className="relative z-10 h-4 w-3 rounded-sm bg-[oklch(0.45_0.06_60)] shadow-sm" aria-hidden="true" />
|
| 6592 |
<figure
|
| 6593 |
+
className={cn("group relative -mt-1 select-none rounded-sm bg-[oklch(0.99_0.01_85)] p-2 pb-12 shadow-xl",
|
| 6594 |
dragging ? "" : "dr-sway", revealed ? "cursor-default" : "cursor-grab active:cursor-grabbing")}
|
| 6595 |
style={{ animationDelay: swayDelay + "s",
|
| 6596 |
transform: dragging ? "rotate(" + (tilt * 0.3) + "deg) translateX(" + (tilt * 0.4) + "px)" : undefined,
|
|
|
|
| 6615 |
</div>
|
| 6616 |
)}
|
| 6617 |
</div>
|
| 6618 |
+
<figcaption className="absolute inset-x-0 bottom-2 line-clamp-3 px-2 text-center font-hand text-[12px] leading-tight text-[oklch(0.3_0.04_264)]"
|
| 6619 |
style={revealed ? { animation: "dr-cap-in .6s ease-out both" } : undefined}>
|
| 6620 |
{revealed ? photo.defaultCaption : ""}
|
| 6621 |
</figcaption>
|