Add code for fal.ai integration #12

Merged
kennethnym merged 8 commits from fal-integration into main 2024-08-25 17:08:48 +01:00
4 changed files with 62 additions and 16 deletions
Showing only changes of commit 8e76c91c72 - Show all commits

14
.gitignore vendored
View File

@@ -4,8 +4,7 @@
### Linux ### ### Linux ###
*~ *~
# temporary files which can be created if a process still has a handle open of a deleted file # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden*
.fuse_hidden*
# KDE directory preferences # KDE directory preferences
.directory .directory
@@ -249,4 +248,13 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/python,windows,macos,linux # End of https://www.toptal.com/developers/gitignore/api/python,windows,macos,linux
*.mp3 0.mp3
1.mp3
2.mp3
3.mp3
4.mp3
5.mp3
6.mp3
7.mp3
8.mp3
9.mp3

View File

@@ -38,3 +38,7 @@ INFERENCE_SERVER_WS_URL=ws://2.3.1.4:1938 fastapi run server.py --port ${ANY_POR
if you have any feature idea, feel free to use the issue tracker to let me know! if you have any feature idea, feel free to use the issue tracker to let me know!
## credits:
- cats by [@PixElthen](https://x.com/pixelthen)

View File

@@ -3,10 +3,11 @@ import os
import websocket import websocket
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from logger import log_info, log_warn from logger import log_info, log_warn
from websocket_connection_manager import WebSocketConnectionManager
# the index of the current audio track from 0 to 9 # the index of the current audio track from 0 to 9
current_index = -1 current_index = -1
@@ -15,6 +16,8 @@ t = None
# websocket connection to the inference server # websocket connection to the inference server
ws = None ws = None
ws_url = "" ws_url = ""
ws_connection_manager = WebSocketConnectionManager()
active_listeners = set()
@asynccontextmanager @asynccontextmanager
@@ -83,7 +86,6 @@ def advance():
current_index = 0 current_index = 0
else: else:
current_index = current_index + 1 current_index = current_index + 1
threading.Thread(target=generate_new_audio).start() threading.Thread(target=generate_new_audio).start()
t = threading.Timer(60, advance) t = threading.Timer(60, advance)
@@ -98,4 +100,34 @@ def get_current_audio():
return FileResponse(f"{current_index}.mp3") return FileResponse(f"{current_index}.mp3")
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket):
await ws_connection_manager.connect(ws)
addr = ""
if ws.client:
addr, _ = ws.client
else:
await ws.close()
ws_connection_manager.disconnect(ws)
await ws_connection_manager.broadcast(f"{len(active_listeners)}")
try:
while True:
msg = await ws.receive_text()
if msg == "playing":
active_listeners.add(addr)
await ws_connection_manager.broadcast(f"{len(active_listeners)}")
elif msg == "paused":
active_listeners.discard(addr)
await ws_connection_manager.broadcast(f"{len(active_listeners)}")
except WebSocketDisconnect:
active_listeners.discard(addr)
ws_connection_manager.disconnect(ws)
await ws_connection_manager.broadcast(f"{len(active_listeners)}")
app.mount("/", StaticFiles(directory="web", html=True), name="web") app.mount("/", StaticFiles(directory="web", html=True), name="web")

Binary file not shown.

BIN
web/audio/cat-meow.mp3 Normal file

Binary file not shown.

BIN
web/favicon.png Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

After

Width:  |  Height:  |  Size: 939 B

BIN
web/images/heart.png Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 B

After

Width:  |  Height:  |  Size: 492 B

View File

@@ -1,9 +1,11 @@
<!doctype html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>infinifi</title> <title>infinifi</title>
<link rel="icon" type="image/png" href="./favicon.png">
<link rel="preload" href="./style.css" as="style"> <link rel="preload" href="./style.css" as="style">
<link rel="modulepreload" href="./bg.js"> <link rel="modulepreload" href="./bg.js">
<link rel="modulepreload" href="./script.js"> <link rel="modulepreload" href="./script.js">
@@ -11,13 +13,14 @@
<link rel="preload" href="./images/eeping-cat.png" as="image"> <link rel="preload" href="./images/eeping-cat.png" as="image">
<link rel="stylesheet" href="./style.css"> <link rel="stylesheet" href="./style.css">
<meta name="description" content="infinite lo-fi music in the background">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@kennethnym"> <meta name="twitter:site" content="@kennethnym">
<meta name="twitter:title" content="infinifi"> <meta name="twitter:title" content="infinifi">
<meta name="twitter:description" content="infinite lo-fi beats in the background"> <meta name="twitter:description" content="infinite lo-fi beats in the background">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<main> <main>
@@ -26,24 +29,38 @@
<div class="button-container"> <div class="button-container">
<button id="play-btn" class="button">play</button> <button id="play-btn" class="button">play</button>
</div> </div>
<div class="volume-slider-container">
<output id="current-volume-label" for="volume-slider">100%</output> <div class="status-bar">
<input id="volume-slider" type="range" min="0" max="100" step="1" /> <p id="listener-count">0 person tuned in</p>
<div id="volume-slider-container">
<output id="current-volume-label" for="volume-slider">100%</output>
<input id="volume-slider" type="range" min="0" max="100" step="1" />
</div>
</div> </div>
</main> </main>
<img class="cat" src="./images/cat-0.png">
<img class="eeping-cat" src="./images/eeping-cat.png"> <aside role="alert" id="notification">
<p id="notification-title">a little chatty</p>
<p id="notification-body">make milo meow 100 times</p>
</aside>
<img id="cat" src="./images/cat-0.png">
<img id="eeping-cat" src="./images/eeping-cat.png">
<img id="heart" src="./images/heart.png">
</div> </div>
<footer> <footer>
<span>made by kennethnym &lt;3 ·&nbsp;</span> <span>made by kennethnym &lt;3 ·&nbsp;</span>
<a target="_blank" href="https://github.com/kennethnym/infinifi">github</a> <a target="_blank" rel="noopener noreferrer" href="https://github.com/kennethnym/infinifi">github</a>
</footer> </footer>
<canvas id="bg"></canvas> <canvas id="bg"></canvas>
<audio id="click-audio" preload="auto" src="./audio/click.wav"></audio> <audio id="click-audio" preload="auto" src="./audio/click.wav"></audio>
<audio id="click-release-audio" preload="auto" src="./audio/click-release.wav"></audio> <audio id="click-release-audio" preload="auto" src="./audio/click-release.wav"></audio>
<audio id="meow-audio" preload="auto" src="./audio/cat-meow.mp3"></audio>
<audio id="achievement-unlocked-audio" preload="auto" src="./audio/achievement-unlocked.mp3"></audio>
<script type="module" src="./bg.js"></script> <script type="module" src="./bg.js"></script>
<script type="module" src="./script.js"></script> <script type="module" src="./script.js"></script>

View File

@@ -1,19 +1,35 @@
const playBtn = document.getElementById("play-btn");
const catImg = document.getElementsByClassName("cat")[0];
const volumeSlider = document.getElementById("volume-slider");
const currentVolumeLabel = document.getElementById("current-volume-label");
const clickAudio = document.getElementById("click-audio");
const clickReleaseAudio = document.getElementById("click-release-audio");
const CROSSFADE_DURATION_MS = 5000; const CROSSFADE_DURATION_MS = 5000;
const CROSSFADE_INTERVAL_MS = 20; const CROSSFADE_INTERVAL_MS = 20;
const AUDIO_DURATION_MS = 60000; const AUDIO_DURATION_MS = 60000;
const SAVE_VOLUME_TIMEOUT_MS = 200;
const ACHIEVEMENT_A_LITTLE_CHATTY = "a-little-chatty";
const playBtn = document.getElementById("play-btn");
const catImg = document.getElementById("cat");
const heartImg = document.getElementById("heart");
const volumeSlider = document.getElementById("volume-slider");
const currentVolumeLabel = document.getElementById("current-volume-label");
const listenerCountLabel = document.getElementById("listener-count");
const notificationContainer = document.getElementById("notification");
const notificationTitle = document.getElementById("notification-title");
const notificationBody = document.getElementById("notification-body");
const clickAudio = document.getElementById("click-audio");
const clickReleaseAudio = document.getElementById("click-release-audio");
const meowAudio = document.getElementById("meow-audio");
const achievementUnlockedAudio = document.getElementById(
"achievement-unlocked-audio",
);
let isPlaying = false; let isPlaying = false;
let isFading = false; let isFading = false;
let currentAudio; let currentAudio;
let maxVolume = 100; let maxVolume = 100;
let currentVolume = 0; let currentVolume = 0;
let saveVolumeTimeout = null;
let meowCount = 0;
let ws = connectToWebSocket();
function playAudio() { function playAudio() {
// add a random query parameter at the end to prevent browser caching // add a random query parameter at the end to prevent browser caching
@@ -21,11 +37,17 @@ function playAudio() {
currentAudio.onplay = () => { currentAudio.onplay = () => {
isPlaying = true; isPlaying = true;
playBtn.innerText = "pause"; playBtn.innerText = "pause";
if (ws) {
ws.send("playing");
}
}; };
currentAudio.onpause = () => { currentAudio.onpause = () => {
isPlaying = false; isPlaying = false;
currentVolume = 0; currentVolume = 0;
playBtn.innerText = "play"; playBtn.innerText = "play";
if (ws) {
ws.send("paused");
}
}; };
currentAudio.onended = () => { currentAudio.onended = () => {
currentVolume = 0; currentVolume = 0;
@@ -121,6 +143,92 @@ function enableSpaceBarControl() {
}); });
} }
function connectToWebSocket() {
const ws = new WebSocket(
`${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/ws`,
);
ws.onmessage = (event) => {
console.log(event.data);
if (typeof event.data !== "string") {
return;
}
const listenerCountStr = event.data;
const listenerCount = Number.parseInt(listenerCountStr);
if (Number.isNaN(listenerCount)) {
return;
}
if (listenerCount <= 1) {
listenerCountLabel.innerText = `${listenerCount} person tuned in`;
} else {
listenerCountLabel.innerText = `${listenerCount} ppl tuned in`;
}
};
return ws;
}
function changeVolume(volume) {
maxVolume = volume;
const v = maxVolume / 100;
currentVolumeLabel.textContent = `${maxVolume}%`;
if (!isFading && currentAudio) {
currentAudio.volume = v;
currentVolume = maxVolume;
}
clickAudio.volume = v;
clickReleaseAudio.volume = v;
meowAudio.volume = v;
}
function loadInitialVolume() {
const savedVolume = localStorage.getItem("volume");
let volume = 100;
if (savedVolume) {
volume = Number.parseInt(savedVolume);
if (Number.isNaN(volume)) {
volume = 100;
}
}
volumeSlider.value = volume;
changeVolume(volume);
document.getElementById("volume-slider-container").style.display = "flex";
}
function loadMeowCount() {
const lastMeowCount = localStorage.getItem("meowCount");
if (!lastMeowCount) {
meowCount = 0;
} else {
const n = Number.parseInt(lastMeowCount);
if (Number.isNaN(n)) {
meowCount = 0;
} else {
meowCount += n;
}
}
}
function showNotification(title, content, duration) {
notificationTitle.innerText = title;
notificationBody.innerText = content;
notificationContainer.style.display = "block";
notificationContainer.style.animation = "0.5s linear 0s notification-fade-in";
setTimeout(() => {
notificationContainer.style.animation =
"0.5s linear 0s notification-fade-out";
setTimeout(() => {
notificationContainer.style.display = "none";
}, 450);
}, duration);
}
playBtn.onmousedown = () => { playBtn.onmousedown = () => {
clickAudio.play(); clickAudio.play();
document.addEventListener( document.addEventListener(
@@ -132,6 +240,10 @@ playBtn.onmousedown = () => {
); );
}; };
catImg.onmousedown = () => {
meowAudio.play();
};
playBtn.onclick = () => { playBtn.onclick = () => {
if (isPlaying) { if (isPlaying) {
pauseAudio(); pauseAudio();
@@ -141,16 +253,50 @@ playBtn.onclick = () => {
}; };
volumeSlider.oninput = () => { volumeSlider.oninput = () => {
maxVolume = volumeSlider.value; const volume = volumeSlider.value;
currentVolumeLabel.textContent = `${maxVolume}%`; changeVolume(volume);
if (!isFading && currentAudio) { if (saveVolumeTimeout) {
currentAudio.volume = maxVolume / 100; clearTimeout(saveVolumeTimeout);
currentVolume = maxVolume; }
saveVolumeTimeout = setTimeout(() => {
localStorage.setItem("volume", `${volume}`);
}, SAVE_VOLUME_TIMEOUT_MS);
};
volumeSlider.value = 100;
meowAudio.onplay = () => {
heartImg.style.display = "block";
heartImg.style.animation = "1s linear 0s heart-animation";
setTimeout(() => {
heartImg.style.display = "none";
heartImg.style.animation = "";
}, 900);
meowCount += 1;
localStorage.setItem("meowCount", `${meowCount}`);
if (meowCount === 100) {
showNotification("a little chatty", "make milo meow 100 times", 5000);
achievementUnlockedAudio.play();
localStorage.setItem(
"achievements",
JSON.stringify([ACHIEVEMENT_A_LITTLE_CHATTY]),
);
} }
clickAudio.volume = volumeSlider.value / 100;
clickReleaseAudio.volume = volumeSlider.value / 100;
}; };
volumeSlider.value = 100; // don't wanna jumpscare ppl
achievementUnlockedAudio.volume = 0.05;
window.addEventListener("offline", () => {
ws = null;
});
window.addEventListener("online", () => {
ws = connectToWebSocket();
});
loadMeowCount();
loadInitialVolume();
animateCat(); animateCat();
enableSpaceBarControl(); enableSpaceBarControl();

View File

@@ -1,6 +1,7 @@
:root { :root {
font-family: monospace; font-family: monospace;
--text: #4c4f69; --text: #4c4f69;
--surface0: #ccd0da;
--surface1: #bcc0cc; --surface1: #bcc0cc;
--base: #eff1f5; --base: #eff1f5;
--lavender: #7287fd; --lavender: #7287fd;
@@ -31,6 +32,7 @@ body {
body { body {
width: calc(100% - 4rem - 2px); width: calc(100% - 4rem - 2px);
height: calc(100vh - 6rem - 2px); height: calc(100vh - 6rem - 2px);
height: calc(100svh - 6rem - 2px);
padding: 2rem; padding: 2rem;
margin: 0; margin: 0;
background-color: var(--base); background-color: var(--base);
@@ -40,6 +42,7 @@ body {
body { body {
width: calc(100% - 8rem - 2px); width: calc(100% - 8rem - 2px);
height: calc(100vh - 10rem - 2px); height: calc(100vh - 10rem - 2px);
height: calc(100svh - 10rem - 2px);
padding: 4rem; padding: 4rem;
} }
} }
@@ -108,32 +111,31 @@ a {
border-radius: 2px; border-radius: 2px;
} }
.status-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -10;
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 2rem;
z-index: 0;
color: var(--text);
}
.status-bar > #listener-count {
margin: 0;
opacity: 0.8;
padding-right: 1rem;
}
.header { .header {
font-weight: 800; font-weight: 800;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.cat {
position: absolute;
bottom: 0px;
right: 2rem;
image-rendering: pixelated;
height: 30px;
}
@media (min-width: 1024px) {
.cat {
right: 4rem;
}
}
.eeping-cat {
position: absolute;
image-rendering: pixelated;
top: -16px;
right: 50%;
height: 15px;
}
@media (min-width: 768px) { @media (min-width: 768px) {
main { main {
width: 80%; width: 80%;
@@ -151,22 +153,6 @@ a {
display: flex; display: flex;
} }
.volume-slider-container {
position: absolute;
top: 0;
right: 0;
padding: 0.5rem 1rem;
margin: 1rem;
display: flex;
justify-content: start;
align-items: center;
}
.volume-slider-container > output {
color: var(--text);
margin-right: 1rem;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) { @media screen and (-webkit-min-device-pixel-ratio: 0) {
input[type="range"] { input[type="range"] {
overflow: hidden; overflow: hidden;
@@ -291,3 +277,104 @@ input[type="range"]::-ms-fill-upper {
opacity: 0.5; opacity: 0.5;
} }
} }
#cat {
position: absolute;
bottom: 0px;
right: 2rem;
image-rendering: pixelated;
height: 30px;
}
@media (min-width: 1024px) {
#cat {
right: 4rem;
}
}
#heart {
display: none;
position: absolute;
bottom: 24px;
right: 2rem;
image-rendering: pixelated;
}
@media (min-width: 1024px) {
#heart {
right: 4rem;
}
}
@keyframes heart-animation {
0% {
display: block;
}
50% {
transform: translate(0, -24px);
}
100% {
opacity: 0;
transform: translate(0, -30px);
}
}
#eeping-cat {
position: absolute;
image-rendering: pixelated;
top: -16px;
right: 50%;
height: 15px;
}
#volume-slider-container {
display: none;
justify-content: start;
align-items: center;
}
#volume-slider-container > output {
color: var(--text);
margin-right: 1rem;
}
aside#notification {
display: none;
position: absolute;
top: 0;
right: 0;
background: var(--surface0);
padding: 1rem 2rem;
margin: 2rem;
border: 2px solid var(--text);
}
aside#notification > p {
margin: 0;
}
aside#notification > #notification-title {
font-weight: 700;
margin-bottom: 0.5rem;
}
@keyframes notification-fade-in {
0% {
display: block;
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes notification-fade-out {
0% {
opactiy: 1;
}
100% {
opacity: 0;
}
}

View File

@@ -0,0 +1,25 @@
import asyncio
from fastapi import WebSocket
class WebSocketConnectionManager:
def __init__(self) -> None:
self.__active_connections: list[WebSocket] = []
async def connect(self, ws: WebSocket):
await ws.accept()
self.__active_connections.append(ws)
def disconnect(self, ws: WebSocket):
self.__active_connections.remove(ws)
async def send_text(self, ws: WebSocket, msg: str):
try:
await ws.send_text(msg)
except:
self.__active_connections.remove(ws)
async def broadcast(self, msg: str):
await asyncio.gather(
*[self.send_text(conn, msg) for conn in self.__active_connections]
)