Spaces:
Sleeping
Sleeping
sync latest frontend + README (social links, corrected tags)
Browse files- README.md +2 -2
- docs/social_post.md +55 -0
- docs/transcript.txt +1 -0
- static/app.js +40 -7
- static/view3d.js +2 -2
README.md
CHANGED
|
@@ -30,7 +30,7 @@ Most translators show you a destination. **Lingo Bridge shows you the journey.**
|
|
| 30 |
|
| 31 |
## 🎬 Demo
|
| 32 |
|
| 33 |
-
▶ **[Demo video](docs/demo.mp4)** · 📣 **
|
| 34 |
|
| 35 |

|
| 36 |
|
|
@@ -79,6 +79,6 @@ python3 app.py
|
|
| 79 |
- **REQ-01 ≤32B/model** — Qwen3-4B + VoxCPM2 (2B). ✓
|
| 80 |
- **REQ-02 Gradio Space in the org** — Docker Space `build-small-hackathon/lingo-bridge`. ✓
|
| 81 |
- **REQ-03 Demo video** — [docs/demo.mp4](docs/demo.mp4). ✓
|
| 82 |
-
- **REQ-04 Social post** —
|
| 83 |
- **REQ-05 ZeroGPU limit** — n/a (GPU on Modal, not ZeroGPU). ✓
|
| 84 |
- **REQ-06 README tags + write-up** — above. ✓
|
|
|
|
| 30 |
|
| 31 |
## 🎬 Demo
|
| 32 |
|
| 33 |
+
▶ **[Demo video](docs/demo.mp4)** · 📣 **Social posts:** [X/Twitter](https://x.com/auracanvas/status/2066666490871558485) · [Bluesky](https://bsky.app/profile/auracanvas.bsky.social/post/3moehclek7k26)
|
| 34 |
|
| 35 |

|
| 36 |
|
|
|
|
| 79 |
- **REQ-01 ≤32B/model** — Qwen3-4B + VoxCPM2 (2B). ✓
|
| 80 |
- **REQ-02 Gradio Space in the org** — Docker Space `build-small-hackathon/lingo-bridge`. ✓
|
| 81 |
- **REQ-03 Demo video** — [docs/demo.mp4](docs/demo.mp4). ✓
|
| 82 |
+
- **REQ-04 Social post** — [X](https://x.com/auracanvas/status/2066666490871558485) · [Bluesky](https://bsky.app/profile/auracanvas.bsky.social/post/3moehclek7k26). ✓
|
| 83 |
- **REQ-05 ZeroGPU limit** — n/a (GPU on Modal, not ZeroGPU). ✓
|
| 84 |
- **REQ-06 README tags + write-up** — above. ✓
|
docs/social_post.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Social posts — Lingo Bridge (copy & post, then send me the URL)
|
| 2 |
+
|
| 3 |
+
Space: https://huggingface.co/spaces/build-small-hackathon/lingo-bridge
|
| 4 |
+
📎 Attach the demo video (`docs/demo.mp4`) to the post — it's the whole pitch.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## ▶ Option A — X / Twitter (single post, ~280 chars)
|
| 9 |
+
|
| 10 |
+
🌉 Most translators show you the destination. Lingo Bridge shows you the *journey*.
|
| 11 |
+
|
| 12 |
+
Watch — and HEAR — a sentence morph into another language across 7 layers: meaning crosses first, then grammar, then word order, in 3D.
|
| 13 |
+
|
| 14 |
+
All small, local models. #BuildSmall
|
| 15 |
+
|
| 16 |
+
https://huggingface.co/spaces/build-small-hackathon/lingo-bridge
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 🧵 Option B — X / Twitter thread
|
| 21 |
+
|
| 22 |
+
1/ 🌉 Lingo Bridge: don't just translate a sentence — *watch and hear it become* another language.
|
| 23 |
+
|
| 24 |
+
Built for the @huggingface × @Gradio #BuildSmall hackathon. 7 stages, 3D, spoken at every step. 👇
|
| 25 |
+
https://huggingface.co/spaces/build-small-hackathon/lingo-bridge
|
| 26 |
+
|
| 27 |
+
2/ How it works: one small text model (Qwen3-4B) breaks the sentence into phrase "units" and aligns them. Then 7 layers build deterministically — meaning flips first, then actions, time, grammar, and finally the word order rearranges into crossing ribbons. 🟪→🟦
|
| 28 |
+
|
| 29 |
+
3/ Every layer is spoken aloud by @OpenBMB's VoxCPM2 (built on MiniCPM-4) — 48kHz, 10 languages. The whole thing runs on @modal GPUs and scales to zero. Tiny models, big toy. 🐜
|
| 30 |
+
|
| 31 |
+
4/ Off-the-shelf translators give you an answer. This gives you the *transformation* — a little language toy you can watch think. Try it 👇
|
| 32 |
+
https://huggingface.co/spaces/build-small-hackathon/lingo-bridge
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
## 💼 Option C — LinkedIn
|
| 37 |
+
|
| 38 |
+
🌉 I built **Lingo Bridge** for the Hugging Face × Gradio "Build Small" hackathon.
|
| 39 |
+
|
| 40 |
+
Most translators show you the destination. Lingo Bridge shows you the *journey*: it turns one sentence into a 7-stage transformation from one language to another — meaning crosses first, then actions, then time words, then grammar, and finally the word order rearranges — rendered as an interactive 3D card stack and **spoken aloud at every stage**.
|
| 41 |
+
|
| 42 |
+
It runs entirely on small, open models (each well under 32B):
|
| 43 |
+
• Qwen3-4B (text decomposition) via llama.cpp
|
| 44 |
+
• OpenBMB VoxCPM2 — built on MiniCPM-4 — for speech, in 10 languages
|
| 45 |
+
• Served on Modal GPUs (scale-to-zero), behind a custom Three.js UI in a Gradio Space
|
| 46 |
+
|
| 47 |
+
Try it: https://huggingface.co/spaces/build-small-hackathon/lingo-bridge
|
| 48 |
+
|
| 49 |
+
#BuildSmall #HuggingFace #Gradio #SmallModels #AI
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## Hashtags / handles to consider
|
| 54 |
+
#BuildSmall #HuggingFace #Gradio #SmallModels #LocalAI
|
| 55 |
+
@huggingface @Gradio @OpenBMB @modal
|
docs/transcript.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Meet Lingo Bridge, where a sentence becomes another language right before your eyes. Instead of one sudden jump, meaning crosses over first, then actions, time words, and grammar follow, and finally the word order rearranges into place. Each stage appears as a glowing card in an interactive 3D stack, and every step is spoken aloud so you can hear the language take shape. It runs on a compact Qwen language model and the VoxCPM speech engine, all served from the cloud. Explore the layers, orbit the stack, and watch translation unfold one bridge at a time.
|
static/app.js
CHANGED
|
@@ -145,7 +145,10 @@ const hooks = {
|
|
| 145 |
state.hover = unit;
|
| 146 |
state.cards.setHover(unit);
|
| 147 |
},
|
| 148 |
-
onPlay: (layerIdx) =>
|
|
|
|
|
|
|
|
|
|
| 149 |
onPerspectiveChange: (t) => {
|
| 150 |
// Called by CardsView when animating perspective automatically
|
| 151 |
const slider = $("#perspective-slider");
|
|
@@ -194,6 +197,8 @@ function showLoader(on, msg) {
|
|
| 194 |
|
| 195 |
// ---- audio -----------------------------------------------------------------
|
| 196 |
const player = $("#player");
|
|
|
|
|
|
|
| 197 |
function langForLayer(idx) {
|
| 198 |
if (!state.data) return "English";
|
| 199 |
return idx === 0 ? state.data.source_lang : state.data.target_lang;
|
|
@@ -208,6 +213,12 @@ async function ttsUrl(text, lang) {
|
|
| 208 |
}
|
| 209 |
async function playLayer(idx) {
|
| 210 |
if (!state.data) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
const layer = state.data.layers[idx];
|
| 212 |
highlightLayer(idx);
|
| 213 |
// Precomputed examples ship per-layer audio -> play instantly, no TTS call.
|
|
@@ -215,7 +226,9 @@ async function playLayer(idx) {
|
|
| 215 |
player.src = url;
|
| 216 |
await player.play().catch(() => {});
|
| 217 |
return new Promise((res) => {
|
|
|
|
| 218 |
player.onended = () => {
|
|
|
|
| 219 |
if (!state.playing) {
|
| 220 |
highlightLayer(-1);
|
| 221 |
}
|
|
@@ -226,19 +239,39 @@ async function playLayer(idx) {
|
|
| 226 |
function highlightLayer(idx) {
|
| 227 |
state.cards.highlightLayer(idx);
|
| 228 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
async function playAll() {
|
| 230 |
-
if (!state.data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
state.playing = true;
|
| 232 |
$("#playall").classList.add("playing");
|
| 233 |
-
$("#playall").textContent = "■
|
| 234 |
for (let i = 0; i < state.data.layers.length; i++) {
|
| 235 |
if (!state.playing) break;
|
| 236 |
await playLayer(i);
|
| 237 |
}
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
boot();
|
|
|
|
| 145 |
state.hover = unit;
|
| 146 |
state.cards.setHover(unit);
|
| 147 |
},
|
| 148 |
+
onPlay: (layerIdx) => {
|
| 149 |
+
if (state.playing) stopPlaying();
|
| 150 |
+
playLayer(layerIdx);
|
| 151 |
+
},
|
| 152 |
onPerspectiveChange: (t) => {
|
| 153 |
// Called by CardsView when animating perspective automatically
|
| 154 |
const slider = $("#perspective-slider");
|
|
|
|
| 197 |
|
| 198 |
// ---- audio -----------------------------------------------------------------
|
| 199 |
const player = $("#player");
|
| 200 |
+
let currentPlayResolve = null;
|
| 201 |
+
|
| 202 |
function langForLayer(idx) {
|
| 203 |
if (!state.data) return "English";
|
| 204 |
return idx === 0 ? state.data.source_lang : state.data.target_lang;
|
|
|
|
| 213 |
}
|
| 214 |
async function playLayer(idx) {
|
| 215 |
if (!state.data) return;
|
| 216 |
+
|
| 217 |
+
if (currentPlayResolve) {
|
| 218 |
+
currentPlayResolve();
|
| 219 |
+
currentPlayResolve = null;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
const layer = state.data.layers[idx];
|
| 223 |
highlightLayer(idx);
|
| 224 |
// Precomputed examples ship per-layer audio -> play instantly, no TTS call.
|
|
|
|
| 226 |
player.src = url;
|
| 227 |
await player.play().catch(() => {});
|
| 228 |
return new Promise((res) => {
|
| 229 |
+
currentPlayResolve = res;
|
| 230 |
player.onended = () => {
|
| 231 |
+
currentPlayResolve = null;
|
| 232 |
if (!state.playing) {
|
| 233 |
highlightLayer(-1);
|
| 234 |
}
|
|
|
|
| 239 |
function highlightLayer(idx) {
|
| 240 |
state.cards.highlightLayer(idx);
|
| 241 |
}
|
| 242 |
+
function stopPlaying() {
|
| 243 |
+
state.playing = false;
|
| 244 |
+
player.pause();
|
| 245 |
+
player.src = "";
|
| 246 |
+
if (currentPlayResolve) {
|
| 247 |
+
currentPlayResolve();
|
| 248 |
+
currentPlayResolve = null;
|
| 249 |
+
}
|
| 250 |
+
$("#playall").classList.remove("playing");
|
| 251 |
+
$("#playall").textContent = "▶ Play all layers";
|
| 252 |
+
highlightLayer(-1);
|
| 253 |
+
}
|
| 254 |
async function playAll() {
|
| 255 |
+
if (!state.data) return;
|
| 256 |
+
if (state.playing) {
|
| 257 |
+
stopPlaying();
|
| 258 |
+
return;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
state.playing = true;
|
| 262 |
$("#playall").classList.add("playing");
|
| 263 |
+
$("#playall").textContent = "■ Stop playing";
|
| 264 |
for (let i = 0; i < state.data.layers.length; i++) {
|
| 265 |
if (!state.playing) break;
|
| 266 |
await playLayer(i);
|
| 267 |
}
|
| 268 |
+
|
| 269 |
+
if (state.playing) {
|
| 270 |
+
state.playing = false;
|
| 271 |
+
$("#playall").classList.remove("playing");
|
| 272 |
+
$("#playall").textContent = "▶ Play all layers";
|
| 273 |
+
highlightLayer(-1);
|
| 274 |
+
}
|
| 275 |
}
|
| 276 |
|
| 277 |
boot();
|
static/view3d.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import * as THREE from "three";
|
| 2 |
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
| 3 |
-
import { mixRGB } from "/static/app.js?v=
|
| 4 |
|
| 5 |
const col = (mix) => {
|
| 6 |
const [r, g, b] = mixRGB(mix);
|
|
@@ -966,7 +966,7 @@ export class CardsView {
|
|
| 966 |
|
| 967 |
// Zoom in closer
|
| 968 |
const { minRadius } = this._getRadii();
|
| 969 |
-
this.targetCamRadius = minRadius +
|
| 970 |
}
|
| 971 |
|
| 972 |
_resize() {
|
|
|
|
| 1 |
import * as THREE from "three";
|
| 2 |
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
| 3 |
+
import { mixRGB } from "/static/app.js?v=20";
|
| 4 |
|
| 5 |
const col = (mix) => {
|
| 6 |
const [r, g, b] = mixRGB(mix);
|
|
|
|
| 966 |
|
| 967 |
// Zoom in closer
|
| 968 |
const { minRadius } = this._getRadii();
|
| 969 |
+
this.targetCamRadius = minRadius + 11.0; // Zoom in nicely to frame the focused row, keeping it slightly zoomed out per user preference
|
| 970 |
}
|
| 971 |
|
| 972 |
_resize() {
|