eloigil6 commited on
Commit
e4d14af
·
1 Parent(s): 7abeea5

Initial implementation of the project structure and core functionality.

Browse files
Files changed (7) hide show
  1. .claude/launch.json +12 -0
  2. .gitignore +5 -0
  3. app.py +61 -0
  4. frontend/index.html +27 -0
  5. frontend/main.js +254 -0
  6. frontend/style.css +52 -0
  7. requirements.txt +2 -0
.claude/launch.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.0.1",
3
+ "configurations": [
4
+ {
5
+ "name": "lofinity",
6
+ "runtimeExecutable": ".venv/bin/python",
7
+ "runtimeArgs": ["app.py"],
8
+ "port": 7860,
9
+ "autoPort": false
10
+ }
11
+ ]
12
+ }
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .venv/
2
+ .cache/
3
+ __pycache__/
4
+ *.pyc
5
+ .DS_Store
app.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LoFinity — a vending machine that dispenses endless chill beats.
2
+
3
+ Gradio Server backend: serves the Three.js frontend and exposes the
4
+ generation API. The audio engine is stubbed until stage 4 (MusicGen).
5
+ """
6
+
7
+ import math
8
+ import struct
9
+ import uuid
10
+ import wave
11
+ from pathlib import Path
12
+
13
+ from fastapi.responses import FileResponse
14
+ from fastapi.staticfiles import StaticFiles
15
+ from gradio import FileData
16
+ from gradio.server import Server
17
+
18
+ ROOT = Path(__file__).parent
19
+ FRONTEND = ROOT / "frontend"
20
+ SONGS_DIR = ROOT / ".cache" / "songs"
21
+ SONGS_DIR.mkdir(parents=True, exist_ok=True)
22
+
23
+ app = Server(title="LoFinity")
24
+
25
+
26
+ @app.api(name="generate_song", concurrency_limit=1)
27
+ def generate_song(prompt: str) -> dict:
28
+ """Stub engine: returns a short audible tone so the full
29
+ frontend -> queue -> file pipeline can be exercised before
30
+ MusicGen is wired in."""
31
+ sample_rate = 22050
32
+ seconds = 2.0
33
+ frames = bytearray()
34
+ for i in range(int(sample_rate * seconds)):
35
+ t = i / sample_rate
36
+ fade = min(1.0, t * 4, (seconds - t) * 4)
37
+ sample = int(0.25 * fade * 32767 * math.sin(2 * math.pi * 220 * t))
38
+ frames += struct.pack("<h", sample)
39
+
40
+ out = SONGS_DIR / f"{uuid.uuid4().hex}.wav"
41
+ with wave.open(str(out), "wb") as w:
42
+ w.setnchannels(1)
43
+ w.setsampwidth(2)
44
+ w.setframerate(sample_rate)
45
+ w.writeframes(bytes(frames))
46
+
47
+ return {
48
+ "title": f"Demo Tape — {prompt[:32] or 'untitled'}",
49
+ "audio": FileData(path=str(out)),
50
+ }
51
+
52
+
53
+ @app.get("/")
54
+ async def homepage():
55
+ return FileResponse(FRONTEND / "index.html")
56
+
57
+
58
+ app.mount("/static", StaticFiles(directory=FRONTEND), name="static")
59
+
60
+ if __name__ == "__main__":
61
+ app.launch(show_error=True, allowed_paths=[str(SONGS_DIR)])
frontend/index.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>LoFinity</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600;800&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="/static/style.css" />
11
+ <script type="importmap">
12
+ {
13
+ "imports": {
14
+ "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js"
15
+ }
16
+ }
17
+ </script>
18
+ </head>
19
+ <body>
20
+ <div id="title-overlay">
21
+ <h1 id="title">LoFinity</h1>
22
+ <p id="subtitle">chill beats, freshly vended</p>
23
+ </div>
24
+ <canvas id="scene"></canvas>
25
+ <script type="module" src="/static/main.js"></script>
26
+ </body>
27
+ </html>
frontend/main.js ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // LoFinity — 3D scene. Stage 1: sky intro + camera descent to a
2
+ // placeholder ground scene (the real low-poly world lands in stage 2).
3
+
4
+ import * as THREE from "three";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Renderer / scene / camera
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const canvas = document.getElementById("scene");
11
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
12
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
13
+ renderer.setSize(window.innerWidth, window.innerHeight);
14
+
15
+ const scene = new THREE.Scene();
16
+ scene.fog = new THREE.Fog(0xbfe3ff, 80, 380);
17
+
18
+ const camera = new THREE.PerspectiveCamera(
19
+ 55,
20
+ window.innerWidth / window.innerHeight,
21
+ 0.1,
22
+ 1000
23
+ );
24
+
25
+ // The camera stays put; we animate what it looks at.
26
+ const CAMERA_POS = new THREE.Vector3(0, 5.5, 24);
27
+ const LOOK_SKY = new THREE.Vector3(0, 90, -60);
28
+ const LOOK_SCENE = new THREE.Vector3(0, 3, 0);
29
+ camera.position.copy(CAMERA_POS);
30
+ camera.lookAt(LOOK_SKY);
31
+
32
+ window.addEventListener("resize", () => {
33
+ camera.aspect = window.innerWidth / window.innerHeight;
34
+ camera.updateProjectionMatrix();
35
+ renderer.setSize(window.innerWidth, window.innerHeight);
36
+ });
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Sky: big inverted dome with a vertical anime-blue gradient
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const skyMaterial = new THREE.ShaderMaterial({
43
+ side: THREE.BackSide,
44
+ depthWrite: false,
45
+ uniforms: {
46
+ topColor: { value: new THREE.Color(0x1547a8) },
47
+ horizonColor: { value: new THREE.Color(0xbfe3ff) },
48
+ },
49
+ vertexShader: /* glsl */ `
50
+ varying vec3 vWorldPosition;
51
+ void main() {
52
+ vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
53
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
54
+ }
55
+ `,
56
+ fragmentShader: /* glsl */ `
57
+ uniform vec3 topColor;
58
+ uniform vec3 horizonColor;
59
+ varying vec3 vWorldPosition;
60
+ void main() {
61
+ float h = clamp(normalize(vWorldPosition).y, 0.0, 1.0);
62
+ vec3 color = mix(horizonColor, topColor, pow(h, 0.55));
63
+ gl_FragColor = vec4(color, 1.0);
64
+ }
65
+ `,
66
+ });
67
+ scene.add(new THREE.Mesh(new THREE.SphereGeometry(450, 32, 16), skyMaterial));
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Lights
71
+ // ---------------------------------------------------------------------------
72
+
73
+ scene.add(new THREE.HemisphereLight(0xcfe8ff, 0x7ec850, 0.9));
74
+ const sun = new THREE.DirectionalLight(0xfff4e0, 1.6);
75
+ sun.position.set(40, 80, 30);
76
+ scene.add(sun);
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Clouds: puffy clusters of flattened, flat-shaded icospheres
80
+ // ---------------------------------------------------------------------------
81
+
82
+ const cloudMaterial = new THREE.MeshLambertMaterial({
83
+ color: 0xffffff,
84
+ flatShading: true,
85
+ });
86
+
87
+ function makeCloud() {
88
+ const cloud = new THREE.Group();
89
+ const puffs = 5 + Math.floor(Math.random() * 4);
90
+ for (let i = 0; i < puffs; i++) {
91
+ const r = 3.5 + Math.random() * 4.5;
92
+ const puff = new THREE.Mesh(new THREE.IcosahedronGeometry(r, 1), cloudMaterial);
93
+ puff.position.set(
94
+ (i - puffs / 2) * (r * 0.9) + (Math.random() - 0.5) * 3,
95
+ (Math.random() - 0.5) * 2.5,
96
+ (Math.random() - 0.5) * 4
97
+ );
98
+ puff.scale.y = 0.55;
99
+ cloud.add(puff);
100
+ }
101
+ return cloud;
102
+ }
103
+
104
+ const clouds = [];
105
+ for (let i = 0; i < 12; i++) {
106
+ const cloud = makeCloud();
107
+ cloud.position.set(
108
+ -140 + Math.random() * 280,
109
+ 32 + Math.random() * 50,
110
+ -160 + Math.random() * 110
111
+ );
112
+ const s = 0.7 + Math.random() * 1.1;
113
+ cloud.scale.setScalar(s);
114
+ cloud.userData.speed = 0.5 + Math.random() * 0.8;
115
+ clouds.push(cloud);
116
+ scene.add(cloud);
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Ground + placeholder props (replaced by the real scene in stage 2)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const ground = new THREE.Mesh(
124
+ new THREE.CircleGeometry(320, 48),
125
+ new THREE.MeshLambertMaterial({ color: 0x6abe4f })
126
+ );
127
+ ground.rotation.x = -Math.PI / 2;
128
+ scene.add(ground);
129
+
130
+ // Distant mountains
131
+ const mountainMaterial = new THREE.MeshLambertMaterial({
132
+ color: 0x4d8a4a,
133
+ flatShading: true,
134
+ });
135
+ for (let i = 0; i < 7; i++) {
136
+ const h = 24 + Math.random() * 26;
137
+ const mountain = new THREE.Mesh(new THREE.ConeGeometry(h * 1.4, h, 5), mountainMaterial);
138
+ mountain.position.set(-180 + i * 60 + (Math.random() - 0.5) * 25, h / 2 - 2, -190);
139
+ mountain.rotation.y = Math.random() * Math.PI;
140
+ scene.add(mountain);
141
+ }
142
+
143
+ // Placeholder vending machine: a friendly blue box
144
+ const vending = new THREE.Group();
145
+ const body = new THREE.Mesh(
146
+ new THREE.BoxGeometry(2.4, 4.2, 1.8),
147
+ new THREE.MeshLambertMaterial({ color: 0x6db9e8 })
148
+ );
149
+ body.position.y = 2.1;
150
+ const front = new THREE.Mesh(
151
+ new THREE.BoxGeometry(1.6, 3.0, 0.1),
152
+ new THREE.MeshLambertMaterial({ color: 0xeaf6ff })
153
+ );
154
+ front.position.set(-0.2, 2.5, 0.92);
155
+ vending.add(body, front);
156
+ vending.position.set(-3.5, 0, 0);
157
+ scene.add(vending);
158
+
159
+ // Placeholder bench
160
+ const benchMaterial = new THREE.MeshLambertMaterial({ color: 0x4a90c2 });
161
+ const bench = new THREE.Group();
162
+ const seat = new THREE.Mesh(new THREE.BoxGeometry(4, 0.25, 1.2), benchMaterial);
163
+ seat.position.y = 1;
164
+ const back = new THREE.Mesh(new THREE.BoxGeometry(4, 1.1, 0.2), benchMaterial);
165
+ back.position.set(0, 1.7, -0.5);
166
+ bench.add(seat, back);
167
+ bench.position.set(3, 0, 1);
168
+ scene.add(bench);
169
+
170
+ // Placeholder tree
171
+ const tree = new THREE.Group();
172
+ const trunk = new THREE.Mesh(
173
+ new THREE.CylinderGeometry(0.4, 0.6, 5, 7),
174
+ new THREE.MeshLambertMaterial({ color: 0x8a5a33, flatShading: true })
175
+ );
176
+ trunk.position.y = 2.5;
177
+ tree.add(trunk);
178
+ const leafMaterial = new THREE.MeshLambertMaterial({ color: 0x4caf3f, flatShading: true });
179
+ for (let i = 0; i < 4; i++) {
180
+ const leaves = new THREE.Mesh(new THREE.IcosahedronGeometry(2.2 - i * 0.3, 1), leafMaterial);
181
+ leaves.position.set((Math.random() - 0.5) * 1.6, 5.2 + i * 1.2, (Math.random() - 0.5) * 1.6);
182
+ tree.add(leaves);
183
+ }
184
+ tree.position.set(-9, 0, -4);
185
+ scene.add(tree);
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Intro: hold on the sky, then ease the gaze down to the scene
189
+ // ---------------------------------------------------------------------------
190
+
191
+ const HOLD_MS = 2800; // sky time after everything has loaded
192
+ const DESCENT_MS = 3400;
193
+
194
+ const intro = { phase: "loading", t0: 0 };
195
+ const lookTarget = LOOK_SKY.clone();
196
+
197
+ window.addEventListener("load", () => {
198
+ setTimeout(() => {
199
+ intro.phase = "descending";
200
+ intro.t0 = performance.now();
201
+ document.getElementById("title-overlay").classList.add("hidden");
202
+ }, HOLD_MS);
203
+ });
204
+
205
+ const easeInOutCubic = (t) =>
206
+ t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Render loop
210
+ // ---------------------------------------------------------------------------
211
+
212
+ const clock = new THREE.Clock();
213
+
214
+ function animate() {
215
+ requestAnimationFrame(animate);
216
+ const elapsed = clock.getElapsedTime();
217
+ const dt = clock.getDelta();
218
+
219
+ for (const cloud of clouds) {
220
+ cloud.position.x += cloud.userData.speed * 0.016;
221
+ if (cloud.position.x > 160) cloud.position.x = -160;
222
+ }
223
+
224
+ if (intro.phase === "descending") {
225
+ const t = Math.min((performance.now() - intro.t0) / DESCENT_MS, 1);
226
+ lookTarget.lerpVectors(LOOK_SKY, LOOK_SCENE, easeInOutCubic(t));
227
+ if (t >= 1) intro.phase = "idle";
228
+ } else if (intro.phase === "idle") {
229
+ // Gentle lofi sway
230
+ camera.position.x = CAMERA_POS.x + Math.sin(elapsed * 0.25) * 0.25;
231
+ camera.position.y = CAMERA_POS.y + Math.sin(elapsed * 0.4) * 0.12;
232
+ }
233
+
234
+ camera.lookAt(lookTarget);
235
+ renderer.render(scene, camera);
236
+ }
237
+ animate();
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // Backend bridge (stub check — UI hooks come with the vending machine modal)
241
+ // ---------------------------------------------------------------------------
242
+
243
+ import("https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js")
244
+ .then(async ({ Client }) => {
245
+ const client = await Client.connect(window.location.origin);
246
+ window.lofinity = {
247
+ generate: async (prompt) => {
248
+ const result = await client.predict("/generate_song", { prompt });
249
+ return result.data[0];
250
+ },
251
+ };
252
+ console.log("[LoFinity] backend connected — try: await lofinity.generate('rainy night')");
253
+ })
254
+ .catch((err) => console.warn("[LoFinity] backend not reachable:", err));
frontend/style.css ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html,
2
+ body {
3
+ margin: 0;
4
+ height: 100%;
5
+ overflow: hidden;
6
+ background: #0e2a63;
7
+ }
8
+
9
+ #scene {
10
+ position: fixed;
11
+ inset: 0;
12
+ width: 100%;
13
+ height: 100%;
14
+ display: block;
15
+ }
16
+
17
+ #title-overlay {
18
+ position: fixed;
19
+ inset: 0;
20
+ display: flex;
21
+ flex-direction: column;
22
+ align-items: center;
23
+ justify-content: center;
24
+ pointer-events: none;
25
+ z-index: 10;
26
+ opacity: 1;
27
+ transition: opacity 1.4s ease;
28
+ }
29
+
30
+ #title-overlay.hidden {
31
+ opacity: 0;
32
+ }
33
+
34
+ #title {
35
+ font-family: "Baloo 2", sans-serif;
36
+ font-weight: 800;
37
+ font-size: clamp(3rem, 10vw, 7rem);
38
+ color: #ffffff;
39
+ margin: 0;
40
+ letter-spacing: 0.04em;
41
+ text-shadow: 0 4px 24px rgba(20, 60, 140, 0.45);
42
+ }
43
+
44
+ #subtitle {
45
+ font-family: "Baloo 2", sans-serif;
46
+ font-weight: 600;
47
+ font-size: clamp(1rem, 2.4vw, 1.4rem);
48
+ color: rgba(255, 255, 255, 0.85);
49
+ margin: 0.4rem 0 0;
50
+ letter-spacing: 0.12em;
51
+ text-transform: lowercase;
52
+ }
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # gradio itself is provided by the Space SDK (sdk_version in README.md).
2
+ # Stage 4 will add: transformers, torch, scipy (MusicGen) + ollama client.