Spaces:
Building
Building
from flask import Flask, render_template, Response | |
from sonatoki.ilo import Ilo | |
from sonatoki.Configs import PrefConfig | |
from atproto import FirehoseSubscribeReposClient, parse_subscribe_repos_message | |
from atproto import CAR, models | |
import json | |
import re | |
import emoji | |
import queue | |
import threading | |
from werkzeug.serving import run_simple | |
app = Flask(__name__) | |
message_queue = queue.Queue() | |
# Your existing code for Ilo and JSONExtra remains the same | |
ilo = Ilo(**PrefConfig) | |
class JSONExtra(json.JSONEncoder): | |
def default(self, obj): | |
try: | |
return json.JSONEncoder.default(self, obj) | |
except: | |
return repr(obj) | |
def clean_text(text: str) -> str: | |
text = emoji.replace_emoji(text, replace='') | |
text = re.sub(r'https?://\S+', '', text) | |
text = re.sub(r'[^A-Za-z\s]', '', text) | |
text = text.strip() | |
return text | |
def process_firehose(): | |
client = FirehoseSubscribeReposClient() | |
def on_message_handler(message): | |
commit = parse_subscribe_repos_message(message) | |
if not isinstance(commit, models.ComAtprotoSyncSubscribeRepos.Commit): | |
return | |
car = CAR.from_bytes(commit.blocks) | |
for op in commit.ops: | |
if op.action in ["create"] and op.cid: | |
raw = car.blocks.get(op.cid) | |
cooked = models.get_or_create(raw, strict=False) | |
if not cooked: | |
continue | |
if cooked.py_type == "app.bsky.feed.post": | |
cleaned_text = clean_text(raw.get("text", "")) | |
if not cleaned_text or len(cleaned_text.split()) < 3: | |
continue | |
if not ilo.is_toki_pona(cleaned_text.lower()): | |
continue | |
url = f'https://bsky.app/profile/{commit.repo}/post/{op.path.split("/")[1]}' | |
message_queue.put({'text': raw.get("text", ""), 'url': url}) | |
client.start(on_message_handler) | |
def generate_sse(): | |
while True: | |
message = message_queue.get() | |
yield f"data: {json.dumps(message)}\n\n" | |
def index(): | |
return """<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Toki Pona Live Stream</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
max-width: 800px; | |
margin: 0 auto; | |
padding: 20px; | |
background-color: #f5f5f5; | |
} | |
.message { | |
background: white; | |
padding: 15px; | |
margin: 10px 0; | |
border-radius: 5px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
a { | |
color: #0066cc; | |
text-decoration: none; | |
} | |
h1 { | |
text-align: center; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Toki Pona Live Stream</h1> | |
<div id="messages"></div> | |
<script> | |
const evtSource = new EventSource("/stream"); | |
const messages = document.getElementById('messages'); | |
evtSource.onmessage = function(event) { | |
const data = JSON.parse(event.data); | |
const messageDiv = document.createElement('div'); | |
messageDiv.className = 'message'; | |
messageDiv.innerHTML = ` | |
<p>${data.text}</p> | |
<a href="${data.url}" target="_blank">View on Bluesky</a> | |
`; | |
messages.insertBefore(messageDiv, messages.firstChild); | |
if (messages.children.length > 50) { | |
messages.removeChild(messages.lastChild); | |
} | |
}; | |
</script> | |
</body> | |
</html>""" | |
def stream(): | |
return Response(generate_sse(), mimetype='text/event-stream') | |
if __name__ == '__main__': | |
# Start the firehose processing in a separate thread | |
threading.Thread(target=process_firehose, daemon=True).start() | |
# Use run_simple instead of app.run | |
run_simple('0.0.0.0', 7860, app, use_reloader=True, use_debugger=True) |