Spaces:
Paused
Paused
| from fastapi import FastAPI, UploadFile, File, Query | |
| from fastapi.responses import HTMLResponse | |
| from faster_whisper import WhisperModel | |
| import uuid | |
| import os | |
| app = FastAPI() | |
| AUDIO_DIR = "audio" | |
| os.makedirs(AUDIO_DIR, exist_ok=True) | |
| # ========================= | |
| # STORAGE | |
| # ========================= | |
| TOKENS = {} # token -> nickname | |
| LAST_TEXT = {} # token -> text | |
| # ========================= | |
| # MODEL (быстро) | |
| # ========================= | |
| model = WhisperModel( | |
| "medium", | |
| device="cpu", | |
| compute_type="int8" | |
| ) | |
| # ========================= | |
| # LOGIN | |
| # ========================= | |
| async def login(data: dict): | |
| nickname = data.get("nickname") | |
| if not nickname: | |
| return {"error": "no nickname"} | |
| # если уже есть токен — вернуть его | |
| for token, name in TOKENS.items(): | |
| if name == nickname: | |
| return { | |
| "token": token, | |
| "nickname": nickname | |
| } | |
| # иначе создать новый | |
| token = str(uuid.uuid4())[:10] | |
| TOKENS[token] = nickname | |
| LAST_TEXT[token] = "" | |
| return { | |
| "token": token, | |
| "nickname": nickname | |
| } | |
| # ========================= | |
| # HTML | |
| # ========================= | |
| HTML = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Voice Chat</title> | |
| </head> | |
| <body style="background:#111;color:white;text-align:center;margin-top:80px"> | |
| <h1>VOICE CHAT</h1> | |
| <div> | |
| <input id="tokenInput" placeholder="enter token" | |
| style="width:250px;height:40px;font-size:18px;"> | |
| <button onclick="saveToken()">SAVE</button> | |
| </div> | |
| <br> | |
| <button id="btn" disabled style="width:200px;height:70px;font-size:20px;"> | |
| START RECORD | |
| </button> | |
| <p id="status"></p> | |
| <script> | |
| let token = null; | |
| const btn = document.getElementById("btn"); | |
| const status = document.getElementById("status"); | |
| function saveToken(){ | |
| token = document.getElementById("tokenInput").value; | |
| if(token && token.length > 3){ | |
| btn.disabled = false; | |
| status.innerText = "token accepted"; | |
| } else { | |
| btn.disabled = true; | |
| status.innerText = "invalid token"; | |
| } | |
| } | |
| let mediaRecorder; | |
| let chunks = []; | |
| btn.onclick = async () => { | |
| if(!token){ | |
| status.innerText = "no token"; | |
| return; | |
| } | |
| if(mediaRecorder && mediaRecorder.state === "recording"){ | |
| mediaRecorder.stop(); | |
| btn.innerText = "START RECORD"; | |
| return; | |
| } | |
| const stream = await navigator.mediaDevices.getUserMedia({audio:true}); | |
| mediaRecorder = new MediaRecorder(stream); | |
| chunks = []; | |
| mediaRecorder.ondataavailable = e => chunks.push(e.data); | |
| mediaRecorder.onstop = async () => { | |
| status.innerText = "uploading..."; | |
| const blob = new Blob(chunks, {type:"audio/webm"}); | |
| const fd = new FormData(); | |
| fd.append("file", blob, "voice.webm"); | |
| const res = await fetch(`/upload?token=${token}`, { | |
| method:"POST", | |
| body:fd | |
| }); | |
| const data = await res.json(); | |
| status.innerText = data.nickname + ": " + data.text; | |
| }; | |
| mediaRecorder.start(); | |
| btn.innerText = "STOP"; | |
| }; | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # ========================= | |
| # HOME | |
| # ========================= | |
| async def home(): | |
| return HTML | |
| # ========================= | |
| # UPLOAD (защищённый) | |
| # ========================= | |
| async def upload(file: UploadFile = File(...), token: str = Query(...)): | |
| if token not in TOKENS: | |
| return {"error": "invalid token"} | |
| nickname = TOKENS[token] | |
| path = f"{AUDIO_DIR}/{uuid.uuid4()}.webm" | |
| with open(path, "wb") as f: | |
| f.write(await file.read()) | |
| segments, _ = model.transcribe( | |
| path, | |
| beam_size=1, | |
| temperature=0, | |
| condition_on_previous_text=False | |
| ) | |
| text = "".join([s.text for s in segments]).strip() | |
| # 👇 ВОТ ЭТА СТРОКА ДОЛЖНА БЫТЬ ЗДЕСЬ | |
| LAST_TEXT[token] = text | |
| return { | |
| "nickname": nickname, | |
| "token": token, | |
| "text": text | |
| } | |
| # ========================= | |
| # GET LAST MESSAGE | |
| # ========================= | |
| async def last(token: str): | |
| if token not in LAST_TEXT: | |
| return {"text": "", "nickname": ""} | |
| return { | |
| "text": f"{TOKENS.get(token, 'unknown')}: {LAST_TEXT[token]}" | |
| } | |
| # ========================================= | |
| # RUN | |
| # ========================================= | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |