init
Browse files- .gitattributes +11 -0
- .gitignore +11 -0
- .gradio/certificate.pem +31 -0
- Dockerfile +14 -0
- README.md +5 -10
- access.log +0 -0
- app.py +0 -0
- app_no_oom.py +0 -0
- backend.log +6 -0
- backend.py +192 -0
- cert.pem +33 -0
- db_utils.py +157 -0
- error.log +14 -0
- key.pem +52 -0
- keypoints_process.py +123 -0
- load_balancer.log +0 -0
- load_balancer.py +132 -0
- manage_nginx.sh +75 -0
- mediapipe +1 -0
- models.py +38 -0
- nginx.conf +104 -0
- preprocess_videos.sh +51 -0
- process_videos.sh +47 -0
- requirements.txt +20 -0
- run.sh +2 -0
- run_load_balancer.sh +10 -0
- run_multiple.sh +18 -0
- run_multiple2.sh +18 -0
- setup_firewall.sh +78 -0
- setup_ssl.sh +56 -0
- stop_apps.sh +18 -0
- video.py +815 -0
- video_audio.py +122 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,14 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.mov filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
*.m4a filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
*.m4v filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
*.m4b filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
*.m4p filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
*.m4r filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
*.m4b filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
*.db filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
predefined/
|
| 2 |
+
predefined/*
|
| 3 |
+
test/
|
| 4 |
+
test/*
|
| 5 |
+
data/
|
| 6 |
+
data/*
|
| 7 |
+
instructions/
|
| 8 |
+
instructions/*
|
| 9 |
+
tmp/
|
| 10 |
+
tmp/*
|
| 11 |
+
hand_landmarker.task
|
.gradio/certificate.pem
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
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
|
| 31 |
+
-----END CERTIFICATE-----
|
Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FROM tensorflow/tensorflow:latest-gpu
|
| 2 |
+
FROM mediapipegpu
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
|
| 8 |
+
RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 libespeak1 -y
|
| 9 |
+
|
| 10 |
+
RUN pip install -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
CMD ["bash", "run.sh"]
|
README.md
CHANGED
|
@@ -1,10 +1,5 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
+
# gradio_finger_recognition
|
| 2 |
+
## video
|
| 3 |
+
Just for video finger recognition
|
| 4 |
+
## video-audio
|
| 5 |
+
Gradio implementation for video with audio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
access.log
ADDED
|
File without changes
|
app.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app_no_oom.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend.log
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
INFO: Will watch for changes in these directories: ['/app']
|
| 2 |
+
INFO: Uvicorn running on https://0.0.0.0:8000 (Press CTRL+C to quit)
|
| 3 |
+
INFO: Started reloader process [7] using StatReload
|
| 4 |
+
INFO: Started server process [103]
|
| 5 |
+
INFO: Waiting for application startup.
|
| 6 |
+
INFO: Application startup complete.
|
backend.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, Body
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from fastapi.responses import FileResponse
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from sqlmodel import Session, select, SQLModel
|
| 6 |
+
from passlib.context import CryptContext
|
| 7 |
+
from models import engine, User, RecordedSession, Feedback, Exercise
|
| 8 |
+
from db_utils import save_session_frames, load_session_frames, create_temp_video_audio, mux_audio_video, cleanup_temp_files
|
| 9 |
+
from typing import List, Dict
|
| 10 |
+
import numpy as np
|
| 11 |
+
import io
|
| 12 |
+
import base64
|
| 13 |
+
import secrets
|
| 14 |
+
from contextlib import asynccontextmanager
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 19 |
+
|
| 20 |
+
async def lifespan(app: FastAPI):
|
| 21 |
+
# ✅ 启动时执行
|
| 22 |
+
SQLModel.metadata.create_all(engine)
|
| 23 |
+
yield
|
| 24 |
+
# ❌ 关闭时(可选)
|
| 25 |
+
# print("App is shutting down...")
|
| 26 |
+
|
| 27 |
+
app = FastAPI(lifespan=lifespan)
|
| 28 |
+
app.add_middleware(
|
| 29 |
+
CORSMiddleware,
|
| 30 |
+
allow_origins=["*"],
|
| 31 |
+
allow_methods=["*"],
|
| 32 |
+
allow_headers=["*"],
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# 注册用户
|
| 36 |
+
class RegisterUser(BaseModel):
|
| 37 |
+
username: str
|
| 38 |
+
|
| 39 |
+
@app.post("/register")
|
| 40 |
+
def register(data: RegisterUser):
|
| 41 |
+
print(data)
|
| 42 |
+
with Session(engine) as session:
|
| 43 |
+
if session.exec(select(User).where(User.username == data.username)).first():
|
| 44 |
+
raise HTTPException(status_code=400, detail="Username already exists")
|
| 45 |
+
user = User(username=data.username)
|
| 46 |
+
session.add(user)
|
| 47 |
+
session.commit()
|
| 48 |
+
return {
|
| 49 |
+
"status": "success",
|
| 50 |
+
"message": "User registered"
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# 登录用户
|
| 54 |
+
class LoginRequest(BaseModel):
|
| 55 |
+
username: str
|
| 56 |
+
password: str
|
| 57 |
+
|
| 58 |
+
@app.post("/login")
|
| 59 |
+
def login(login_data: LoginRequest):
|
| 60 |
+
with Session(engine) as session:
|
| 61 |
+
user = session.exec(select(User).where(User.username == login_data.username)).first()
|
| 62 |
+
if not user:
|
| 63 |
+
hashed = pwd_context.hash(login_data.password)
|
| 64 |
+
user = User(username=login_data.username, password_hash=hashed)
|
| 65 |
+
session.add(user)
|
| 66 |
+
session.commit()
|
| 67 |
+
|
| 68 |
+
session_id = secrets.token_urlsafe(16)
|
| 69 |
+
token = f"mock-token-{secrets.token_urlsafe(8)}"
|
| 70 |
+
|
| 71 |
+
return {
|
| 72 |
+
"status": "success",
|
| 73 |
+
"token": token,
|
| 74 |
+
"session_id": session_id,
|
| 75 |
+
"username": user.username
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# @app.post("/logout")
|
| 79 |
+
# def logout():
|
| 80 |
+
# # 如果你有实际的 session 存储,比如数据库或 Redis,可以在这里清除它
|
| 81 |
+
# # 目前是 mock 登录,所以这里只返回个消息
|
| 82 |
+
# return {"status": "success", "message": "Logged out"}
|
| 83 |
+
|
| 84 |
+
# 获取用户录制列表
|
| 85 |
+
@app.get("/user_sessions/{username}")
|
| 86 |
+
def get_user_sessions(username: str):
|
| 87 |
+
with Session(engine) as session:
|
| 88 |
+
user = session.exec(select(User).where(User.username == username)).first()
|
| 89 |
+
if not user:
|
| 90 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 91 |
+
|
| 92 |
+
sessions_list = session.exec(
|
| 93 |
+
select(RecordedSession)
|
| 94 |
+
.where(RecordedSession.user_id == user.id)
|
| 95 |
+
.order_by(RecordedSession.timestamp.desc())
|
| 96 |
+
).all()
|
| 97 |
+
|
| 98 |
+
return [{
|
| 99 |
+
"session_id": s.id,
|
| 100 |
+
"session_name": s.session_name,
|
| 101 |
+
"timestamp": s.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
| 102 |
+
} for s in sessions_list]
|
| 103 |
+
|
| 104 |
+
# 上传 session 帧
|
| 105 |
+
class UploadSession(BaseModel):
|
| 106 |
+
username: str
|
| 107 |
+
session_name: str
|
| 108 |
+
fps: int
|
| 109 |
+
frames: List[str]
|
| 110 |
+
keypoints: Dict[str, List[str]]
|
| 111 |
+
audio_chunks: List[str]
|
| 112 |
+
|
| 113 |
+
@app.post("/upload_session")
|
| 114 |
+
def upload_session(data: UploadSession):
|
| 115 |
+
def decode_b64_array(b64):
|
| 116 |
+
return np.load(io.BytesIO(base64.b64decode(b64)), allow_pickle=True)
|
| 117 |
+
|
| 118 |
+
min_cnt = min(len(data.frames), len(data.keypoints["Left"]))
|
| 119 |
+
data.frames = data.frames[:min_cnt]
|
| 120 |
+
data.keypoints["Left"] = data.keypoints["Left"][:min_cnt]
|
| 121 |
+
data.keypoints["Right"] = data.keypoints["Right"][:min_cnt]
|
| 122 |
+
left_list = data.keypoints.get("Left", [])
|
| 123 |
+
right_list = data.keypoints.get("Right", [])
|
| 124 |
+
frames = [decode_b64_array(f) for f in data.frames]
|
| 125 |
+
if len(left_list) != len(right_list):
|
| 126 |
+
raise HTTPException(status_code=400, detail="Keypoints Left and Right lengths mismatch")
|
| 127 |
+
keypoints = []
|
| 128 |
+
for l_b64, r_b64 in zip(left_list, right_list):
|
| 129 |
+
left_kps = decode_b64_array(l_b64) if l_b64 else None
|
| 130 |
+
right_kps = decode_b64_array(r_b64) if r_b64 else None
|
| 131 |
+
keypoints.append({"Left": left_kps, "Right": right_kps})
|
| 132 |
+
audio = [decode_b64_array(a) for a in data.audio_chunks]
|
| 133 |
+
|
| 134 |
+
session_id = save_session_frames(
|
| 135 |
+
username=data.username,
|
| 136 |
+
session_name=data.session_name,
|
| 137 |
+
frames=frames,
|
| 138 |
+
keypoints=keypoints,
|
| 139 |
+
audio_chunks=audio,
|
| 140 |
+
fps=data.fps
|
| 141 |
+
)
|
| 142 |
+
return {"message": "Saved", "session_id": session_id}
|
| 143 |
+
|
| 144 |
+
# 加载帧(播放用)
|
| 145 |
+
@app.get("/play_session/{session_id}")
|
| 146 |
+
def generate_playback_response(session_id: str):
|
| 147 |
+
video_path, audio_path = create_temp_video_audio(session_id)
|
| 148 |
+
muxed_path = mux_audio_video(video_path, audio_path)
|
| 149 |
+
# cleanup_temp_files(video_path, audio_path, muxed_path)
|
| 150 |
+
return FileResponse(muxed_path, media_type="video/mp4")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@app.post("/submit_feedback")
|
| 154 |
+
def submit_feedback(username: str = Body(...), content: str = Body(...)):
|
| 155 |
+
with Session(engine) as session:
|
| 156 |
+
fb = Feedback(username=username, content=content)
|
| 157 |
+
session.add(fb)
|
| 158 |
+
session.commit()
|
| 159 |
+
return {"status": "success"}
|
| 160 |
+
|
| 161 |
+
@app.get("/get_feedback/{username}")
|
| 162 |
+
def get_feedback(username: str):
|
| 163 |
+
with Session(engine) as session:
|
| 164 |
+
feedbacks = session.exec(
|
| 165 |
+
select(Feedback).where(Feedback.username == username).order_by(Feedback.timestamp.desc())
|
| 166 |
+
).all()
|
| 167 |
+
return [f"- {f.timestamp.strftime('%Y-%m-%d %H:%M:%S')}: {f.content}" for f in feedbacks]
|
| 168 |
+
|
| 169 |
+
class ExerciseIn(BaseModel):
|
| 170 |
+
username: str
|
| 171 |
+
duration: str
|
| 172 |
+
sta_time: str
|
| 173 |
+
|
| 174 |
+
@app.post("/submit_exercise")
|
| 175 |
+
def submit_exercise(data: ExerciseIn):
|
| 176 |
+
with Session(engine) as session:
|
| 177 |
+
fb = Exercise(**data.dict())
|
| 178 |
+
session.add(fb)
|
| 179 |
+
session.commit()
|
| 180 |
+
return {"status": "success"}
|
| 181 |
+
|
| 182 |
+
@app.get("/get_exercise/{username}")
|
| 183 |
+
def get_exercise(username: str):
|
| 184 |
+
with Session(engine) as session:
|
| 185 |
+
exercises = session.exec(
|
| 186 |
+
select(Exercise).where(Exercise.username == username).order_by(Exercise.sta_time.desc())
|
| 187 |
+
).all()
|
| 188 |
+
return [f"- {f.sta_time}: {f.duration}" for f in exercises]
|
| 189 |
+
|
| 190 |
+
if __name__ == "__main__":
|
| 191 |
+
import uvicorn
|
| 192 |
+
uvicorn.run("backend:app", host="0.0.0.0", port=8000, reload=True, ssl_keyfile="key.pem", ssl_certfile="cert.pem")
|
cert.pem
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIFpzCCA4+gAwIBAgIULUvEMyRzfP652DVlibqIbe/E/ScwDQYJKoZIhvcNAQEL
|
| 3 |
+
BQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlBBMRUwEwYDVQQHDAxQaGlsYWRl
|
| 4 |
+
bHBoaWExDjAMBgNVBAoMBVVQZW5uMQ8wDQYDVQQLDAZWaXNpb24xDzANBgNVBAMM
|
| 5 |
+
BkZpbmdlcjAeFw0yNTAzMTkxOTAwMjhaFw0yNjAzMTkxOTAwMjhaMGMxCzAJBgNV
|
| 6 |
+
BAYTAlVTMQswCQYDVQQIDAJQQTEVMBMGA1UEBwwMUGhpbGFkZWxwaGlhMQ4wDAYD
|
| 7 |
+
VQQKDAVVUGVubjEPMA0GA1UECwwGVmlzaW9uMQ8wDQYDVQQDDAZGaW5nZXIwggIi
|
| 8 |
+
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDSjMNis1BSEtHA59bYWW2wkAZs
|
| 9 |
+
lL2+hbCDXwioDLvUDOjSen422tkr/8zyzyqhLKZAO7TaCbIvlBsQdRZ1w3Y5iNNc
|
| 10 |
+
P1wgjmwfKXkefTJjheU6+IuggB0H8a7oS5berSAizKAaLrXIeRDaVGd0uNWatwal
|
| 11 |
+
Kj5yoTE8ZeDnmVicnf5yhwP0WiUVFqRSoQYqWZIVpcx1xzqxBwR6HuO9XclZLtz9
|
| 12 |
+
fGicfb0RgDXFpmpIZs1DV6ZRYqln5PqQ5OweiqZ5llhzA6uJh4N6fiZKQqN3pjJZ
|
| 13 |
+
UOieujEFzimNVAGUxOkcYzzuUUS5KothIHnoRvs9kJU5gTJCX4Sz8VfF2u13IgOW
|
| 14 |
+
SUxHtS7pQPxdoZUaRF0px7P7e+5oXTbktizynS1pIKlmPm49x8pDg04PPujbY770
|
| 15 |
+
JEJIKaAUyMjMPIbtbJv3txx3FM9yrszwgzgEia4gvwS+ZJOmZIDXKRZTLF70pcx4
|
| 16 |
+
5CpaUUUew7V0Pm2/3xCjN1TY9saMGCSpFth0VQNEV06uHPGmdtbPP1a6BU05KU+4
|
| 17 |
+
Tq0KoMEhD/34lqyeiJfaDVHXQiJyXNtMPSxf+YmOZIq5RzMJuedBITLn/c/VajSn
|
| 18 |
+
mLVI3ySQBiiOUT0MuIdLRzE+sOT5KuLXRv2wwn6HJYgcmbfbntRQ3ZPqdKl4hO64
|
| 19 |
+
8IdbpfO1YAYVH/LG7QIDAQABo1MwUTAdBgNVHQ4EFgQUdNKLpfRiZK+0nrDfVSdF
|
| 20 |
+
RRozQtYwHwYDVR0jBBgwFoAUdNKLpfRiZK+0nrDfVSdFRRozQtYwDwYDVR0TAQH/
|
| 21 |
+
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAEomnTV+8LgrgJRLf4m6qUgqi7Qvo
|
| 22 |
+
FF58+3bZlBshlN/Kb6h8jyTYcgO0J9Glg9aOZ00luRahN+Ahg7y3jFwJnb05k72m
|
| 23 |
+
JRtUdcACYJEGPLgLTvOmie3WiSsIgZHiXzE04cYcLGg7YT/E2WtyAS3+avnLtwyA
|
| 24 |
+
48C4ikDOlhzZLYD0KkNtTs0U2qgLXSGcAkKpRFxaKFayxPTxbhw0zgqhYpcc7G8f
|
| 25 |
+
TXvIF4uTxr+RHo719b2fqkwnzU7BWJAbJhjQoCFo5eMF4azYmHbFPJAQfMpCxgVw
|
| 26 |
+
d2A8bfTjLyvZW7opLqzdYYbTgckHoio/oMm7vaAHYbUcR3GDeFCnfZmXeFL3rtqs
|
| 27 |
+
UqviWwLODRDFuNhaW/8MBIxgHGNFL1IreDVxBzkqxCU/fMvrjJpncIESt4OTDSxb
|
| 28 |
+
sTy4luBlbZ6oLFIKplulM8ANBk+UX3bvyK9QueFDLvtkCS3YeSEcVW1yphS6aJ/q
|
| 29 |
+
upxwLhAFPUgohSlashf1AWolTj4RDHqk0j3os9+K64hE5R5/lHbhABQs1UKfIVJs
|
| 30 |
+
Wun2HyxM9CUjdirBOKqpBl9/hcYcZIIlg+fNp8JAMVk7rgRWxGg7u2pmTnK0jAOO
|
| 31 |
+
3FbQX8igNmnDBySKdoNbl4Y9S74hdD9SN2s7T0ewu9kgOKNuv+Q2JNeOoRgScd8N
|
| 32 |
+
67W65RLVDoYuroM=
|
| 33 |
+
-----END CERTIFICATE-----
|
db_utils.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import io
|
| 3 |
+
from sqlmodel import Session, select
|
| 4 |
+
from models import engine, RecordedSession, VideoFrame, User
|
| 5 |
+
import base64
|
| 6 |
+
import tempfile
|
| 7 |
+
import os
|
| 8 |
+
import threading
|
| 9 |
+
import cv2
|
| 10 |
+
import subprocess
|
| 11 |
+
from scipy.io.wavfile import write as write_wav
|
| 12 |
+
import pdb
|
| 13 |
+
|
| 14 |
+
SAMPLING_RATE = 48000
|
| 15 |
+
|
| 16 |
+
def encode_ndarray(arr: np.ndarray) -> str:
|
| 17 |
+
buf = io.BytesIO()
|
| 18 |
+
np.save(buf, arr)
|
| 19 |
+
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
| 20 |
+
|
| 21 |
+
def ndarray_to_bytes(arr: np.ndarray) -> bytes:
|
| 22 |
+
buf = io.BytesIO()
|
| 23 |
+
np.save(buf, arr)
|
| 24 |
+
return buf.getvalue()
|
| 25 |
+
|
| 26 |
+
def bytes_to_ndarray(blob: bytes) -> np.ndarray:
|
| 27 |
+
return np.load(io.BytesIO(blob), allow_pickle=True)
|
| 28 |
+
|
| 29 |
+
def save_session_frames(username: str,
|
| 30 |
+
session_name: str,
|
| 31 |
+
frames: list[np.ndarray],
|
| 32 |
+
keypoints: list[np.ndarray],
|
| 33 |
+
audio_chunks: list[np.ndarray],
|
| 34 |
+
fps: int
|
| 35 |
+
) -> int:
|
| 36 |
+
samples_per_frame = int(SAMPLING_RATE // fps)
|
| 37 |
+
with Session(engine) as session:
|
| 38 |
+
# 🔍 根据用户名查找 user_id
|
| 39 |
+
user = session.exec(select(User).where(User.username == username)).first()
|
| 40 |
+
if not user:
|
| 41 |
+
raise ValueError(f"Username '{username}' not found!")
|
| 42 |
+
|
| 43 |
+
# 💾 新建一个 session 记录
|
| 44 |
+
rec = RecordedSession(user_id=user.id, session_name=session_name)
|
| 45 |
+
session.add(rec)
|
| 46 |
+
session.commit()
|
| 47 |
+
session.refresh(rec)
|
| 48 |
+
|
| 49 |
+
rec_id = rec.id
|
| 50 |
+
|
| 51 |
+
# 💾 存储帧数据
|
| 52 |
+
for i in range(len(frames)):
|
| 53 |
+
start_idx = int(i * samples_per_frame)
|
| 54 |
+
end_idx = int((i+1) * samples_per_frame)
|
| 55 |
+
f = VideoFrame(
|
| 56 |
+
session_id=rec.id,
|
| 57 |
+
frame_index=i,
|
| 58 |
+
image_array=ndarray_to_bytes(frames[i]),
|
| 59 |
+
keypoints=ndarray_to_bytes(keypoints[i]),
|
| 60 |
+
audio_chunk=ndarray_to_bytes(audio_chunks[start_idx: end_idx]),
|
| 61 |
+
fps=fps
|
| 62 |
+
)
|
| 63 |
+
session.add(f)
|
| 64 |
+
session.commit()
|
| 65 |
+
return rec_id
|
| 66 |
+
|
| 67 |
+
def load_session_frames(session_id: int):
|
| 68 |
+
fps = 0
|
| 69 |
+
with Session(engine) as session:
|
| 70 |
+
result = session.exec(
|
| 71 |
+
select(VideoFrame)
|
| 72 |
+
.where(VideoFrame.session_id == session_id)
|
| 73 |
+
.order_by(VideoFrame.frame_index)
|
| 74 |
+
).all()
|
| 75 |
+
|
| 76 |
+
frames = []
|
| 77 |
+
keypoints_all = []
|
| 78 |
+
audio_chunks = []
|
| 79 |
+
|
| 80 |
+
for f in result:
|
| 81 |
+
frames.append(bytes_to_ndarray(f.image_array))
|
| 82 |
+
keypoints_all.append(bytes_to_ndarray(f.keypoints))
|
| 83 |
+
audio_chunks.append(bytes_to_ndarray(f.audio_chunk))
|
| 84 |
+
fps = f.fps
|
| 85 |
+
|
| 86 |
+
# 🔁 拼接所有音频 chunk 为一个完整的音频数组
|
| 87 |
+
full_audio = np.concatenate(audio_chunks, axis=0) if audio_chunks else None
|
| 88 |
+
|
| 89 |
+
return frames, keypoints_all, full_audio, fps
|
| 90 |
+
|
| 91 |
+
def draw_keypoints_on_frame(frame: np.ndarray, keypoints: dict) -> np.ndarray:
|
| 92 |
+
for hand in ["Left", "Right"]:
|
| 93 |
+
if keypoints.get(hand) is not None:
|
| 94 |
+
for x, y, z in keypoints[hand]:
|
| 95 |
+
cx, cy = int(x * frame.shape[1]), int(y * frame.shape[0])
|
| 96 |
+
cv2.circle(frame, (cx, cy), 3, (0, 255, 0), -1)
|
| 97 |
+
return frame
|
| 98 |
+
|
| 99 |
+
def create_temp_video_audio(session_id: int):
|
| 100 |
+
frames, keypoints, audio_chunks, fps = load_session_frames(session_id)
|
| 101 |
+
|
| 102 |
+
# ✅ 视频文件路径
|
| 103 |
+
video_temp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
|
| 104 |
+
video_path = video_temp.name
|
| 105 |
+
|
| 106 |
+
height, width, _ = frames[0].shape
|
| 107 |
+
out = cv2.VideoWriter(video_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height))
|
| 108 |
+
for f, kpts in zip(frames, keypoints):
|
| 109 |
+
# frame_with_kpts = draw_keypoints_on_frame(f.copy(), kpts)
|
| 110 |
+
# out.write(frame_with_kpts)
|
| 111 |
+
out.write(f)
|
| 112 |
+
out.release()
|
| 113 |
+
|
| 114 |
+
# ✅ 音频文件路径
|
| 115 |
+
audio_temp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
|
| 116 |
+
audio_path = audio_temp.name
|
| 117 |
+
|
| 118 |
+
if audio_chunks.ndim == 1:
|
| 119 |
+
audio_chunks = audio_chunks[:, np.newaxis]
|
| 120 |
+
scaled_audio = (audio_chunks * 32767).astype(np.int16)
|
| 121 |
+
write_wav(audio_path, SAMPLING_RATE, scaled_audio)
|
| 122 |
+
|
| 123 |
+
return video_path, audio_path
|
| 124 |
+
|
| 125 |
+
def mux_audio_video(video_path: str, audio_path: str) -> str:
|
| 126 |
+
output = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 127 |
+
print("📄 合并后文件路径:", output)
|
| 128 |
+
print("video path: ", video_path)
|
| 129 |
+
print("audio path: ", audio_path)
|
| 130 |
+
cmd = [
|
| 131 |
+
"ffmpeg", "-y",
|
| 132 |
+
"-i", video_path,
|
| 133 |
+
"-i", audio_path,
|
| 134 |
+
"-c:v", "libx264", # 视频编码器
|
| 135 |
+
"-c:a", "aac", # 音频编码器
|
| 136 |
+
"-b:a", "192k", # 音频比特率
|
| 137 |
+
"-ac", "2", # 设置为立体声 (2个声道)
|
| 138 |
+
"-shortest", # 保证音频和视频时间一致
|
| 139 |
+
output
|
| 140 |
+
]
|
| 141 |
+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 142 |
+
if result.returncode != 0:
|
| 143 |
+
print("❌ ffmpeg 合并失败:", result.stderr.decode())
|
| 144 |
+
raise RuntimeError("ffmpeg failed to mux audio and video")
|
| 145 |
+
return output
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def cleanup_temp_files(*files):
|
| 149 |
+
def _cleanup():
|
| 150 |
+
import time
|
| 151 |
+
time.sleep(60) # 延迟 60 秒后删除
|
| 152 |
+
for f in files:
|
| 153 |
+
try:
|
| 154 |
+
os.remove(f)
|
| 155 |
+
except Exception as e:
|
| 156 |
+
print(f"❌ 删除临时文件失败 {f}: {e}")
|
| 157 |
+
threading.Thread(target=_cleanup).start()
|
error.log
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[2025-03-20 15:08:44 -0400] [54511] [INFO] Starting gunicorn 23.0.0
|
| 2 |
+
[2025-03-20 15:08:44 -0400] [54511] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 3 |
+
[2025-03-20 15:08:45 -0400] [54511] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 4 |
+
[2025-03-20 15:08:46 -0400] [54511] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 5 |
+
[2025-03-20 15:08:47 -0400] [54511] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 6 |
+
[2025-03-20 15:08:48 -0400] [54511] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 7 |
+
[2025-03-20 15:08:49 -0400] [54511] [ERROR] Can't connect to ('0.0.0.0', 80)
|
| 8 |
+
[2025-03-20 15:10:31 -0400] [57021] [INFO] Starting gunicorn 23.0.0
|
| 9 |
+
[2025-03-20 15:10:31 -0400] [57021] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 10 |
+
[2025-03-20 15:10:32 -0400] [57021] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 11 |
+
[2025-03-20 15:10:33 -0400] [57021] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 12 |
+
[2025-03-20 15:10:34 -0400] [57021] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 13 |
+
[2025-03-20 15:10:35 -0400] [57021] [ERROR] connection to ('0.0.0.0', 80) failed: [Errno 13] Permission denied
|
| 14 |
+
[2025-03-20 15:10:36 -0400] [57021] [ERROR] Can't connect to ('0.0.0.0', 80)
|
key.pem
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN PRIVATE KEY-----
|
| 2 |
+
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDSjMNis1BSEtHA
|
| 3 |
+
59bYWW2wkAZslL2+hbCDXwioDLvUDOjSen422tkr/8zyzyqhLKZAO7TaCbIvlBsQ
|
| 4 |
+
dRZ1w3Y5iNNcP1wgjmwfKXkefTJjheU6+IuggB0H8a7oS5berSAizKAaLrXIeRDa
|
| 5 |
+
VGd0uNWatwalKj5yoTE8ZeDnmVicnf5yhwP0WiUVFqRSoQYqWZIVpcx1xzqxBwR6
|
| 6 |
+
HuO9XclZLtz9fGicfb0RgDXFpmpIZs1DV6ZRYqln5PqQ5OweiqZ5llhzA6uJh4N6
|
| 7 |
+
fiZKQqN3pjJZUOieujEFzimNVAGUxOkcYzzuUUS5KothIHnoRvs9kJU5gTJCX4Sz
|
| 8 |
+
8VfF2u13IgOWSUxHtS7pQPxdoZUaRF0px7P7e+5oXTbktizynS1pIKlmPm49x8pD
|
| 9 |
+
g04PPujbY770JEJIKaAUyMjMPIbtbJv3txx3FM9yrszwgzgEia4gvwS+ZJOmZIDX
|
| 10 |
+
KRZTLF70pcx45CpaUUUew7V0Pm2/3xCjN1TY9saMGCSpFth0VQNEV06uHPGmdtbP
|
| 11 |
+
P1a6BU05KU+4Tq0KoMEhD/34lqyeiJfaDVHXQiJyXNtMPSxf+YmOZIq5RzMJuedB
|
| 12 |
+
ITLn/c/VajSnmLVI3ySQBiiOUT0MuIdLRzE+sOT5KuLXRv2wwn6HJYgcmbfbntRQ
|
| 13 |
+
3ZPqdKl4hO648IdbpfO1YAYVH/LG7QIDAQABAoICAB25SYwRjHFJvtrg3+/DED0w
|
| 14 |
+
1/dUOEVBUl35eJtg0NNqzyOHr3HHC4munxxYKsh+KSpAQ5PUcpdM7VUxcm5FndcZ
|
| 15 |
+
fd00BKmD8bekfzjSq95o9KeSlwOdknugnvSkVzQwdKz0/lUz6u3WSY7JA6HyxhUF
|
| 16 |
+
aaa/g/li0DSanVmUyHGzzEwWy3QflcSYYbJYkSpBYuIzed3Wtm5vcy5aVgRRzS8N
|
| 17 |
+
8pzGh24wQhTKaMzyZWa7PJcZNJ2gtBG6vbTe3IajREU9+FakWf8cZm9Qh89MAQ/P
|
| 18 |
+
IqSBS5W861bKeAs/pMl/0vjy+ZMbXfxWCT84+nzUKHvUgXyQxKuiGHXeyGliMsux
|
| 19 |
+
LB0hNX46jvlv3kC5my4fJIoIpLpOGjoU+K5IVYb3XTFjdMmGFBtrzge1HA47hLol
|
| 20 |
+
HYDld+JOKpZI2MnCEST9sF30+NBGibw1HLpbL6kd2fNMK84vlNHoyE+sIb2kYZou
|
| 21 |
+
8RHjn7vTOIbmdjvdT4R6FoaS6LXa/XKOgyDdLIHCvJizoXnhY4QT66GyRrtY7E+X
|
| 22 |
+
OOT7T674r2Z+u1eC7M1CtTeoHAJixyclnFI/XP/eSzSZ2GJffFRiUdPPuqLGrdLg
|
| 23 |
+
rTzCfiXn9diYzTBs0P0AfiL+D9eh+fC9Jw1u6lWuwCMgYfx8bAs9peOEMKSTOYbK
|
| 24 |
+
ePJ3Cx4XmPJHx4DLRsgBAoIBAQDtd5kCR87lCyD+uhNbnCZL8QR15Dy79hefXW/l
|
| 25 |
+
hWr3C88syjdoaYCNuB9LQ+i678zmmFOFlK28Ge0v7CX+lzMQN3vqcvaqf2Vb1QeS
|
| 26 |
+
I9N8NoJ1ftzqm/r8EqBYKnYVqedNBOBMOcaTUHRUJrDnQL9/+gN0+OkIR4TNJOQm
|
| 27 |
+
Fci0km8o0Q7dkygrigmwfnprGWfjH5gLngjVfhLOfQ+i5nCCxxT4x5vcHJyCCKSU
|
| 28 |
+
TgrxyhVEB7K0mWW4godovNu6O3VBoY1MIraqThoudIKevUOMDSQBMWG8duAupCk8
|
| 29 |
+
ZXgkshojH5tpN2Wj2QQr2rrG4gpRZZ2NHmUOEK2oD6Uv4hNtAoIBAQDi+2EStFsm
|
| 30 |
+
U0N58F8VuBpPyPkD7Yv7DscwqFXoWE2g7MlqdZF7cCqwyf4O/AB0LLKTEzP2NBWO
|
| 31 |
+
e0uxrftGYN4kdlALVx9B2/wXELd4fm9SqtSutUv3ZN5GnK/GltAw9YHEDE36ESHF
|
| 32 |
+
lTO0lv/k+O7W+yhMEDY4j0FmgvDfH+Q0WmFnwxYl39YT9/Jw1sPDKbtDE1gJ0b0N
|
| 33 |
+
YdjZhn4VohfmiRhid5GmndrQbwxGfvLEXsU+syo2/nj9IGCxFq8lka0fKesL92Nx
|
| 34 |
+
DcjHRf8+6VK023RM5M+DKYMmwFLl92NrR1aG+HlDvshHUZjIoSXVAwfSz0Ba9GMY
|
| 35 |
+
FFdGzLIL99GBAoIBAG6AxgoComuBR0RiEJoDyupx6LJ3mC+bcBiv4V88O69kpm7g
|
| 36 |
+
VvJWjgTk1mMu4cED0CTKY6t4qXQr2G0Bhhi4AYIdX6OVBeYHTIJ0WoaN918I+qJ0
|
| 37 |
+
e5cNKLlebZE5iSPBoan8h+fQxvBMcyWpr46dWb/S9wLaxY4dwdW4whZa8r/cmK+0
|
| 38 |
+
wSco0HuaS7H+2Ta3ZtmRHS7ixpeaiGPgXINgmqCwxbiTIptGESqNnRCKVJt5f3Xd
|
| 39 |
+
4zIZY/V9gEekAtfhzUnSRK9WRAxyNcrCWvpFdoZYoxWPBj0uUFpD+BBr44GoA3Ou
|
| 40 |
+
xKIsrjaVyVQi/+GG1GhWUf+WUk4+QqE/To4+tO0CggEADDSAL8VK8XCgvDnUoxJX
|
| 41 |
+
N9sSqMdpM4LD6zXiCLBW3ERfQD46KG0Lnp3970hVremJYKczsBV040h19YPpcwta
|
| 42 |
+
ZpOGElYI9D2j/ImFlBEYY1WUQiC1iQP/f8SFHySU5U0OQUB4IO4y5rDzKs7Dy8gm
|
| 43 |
+
76Bptk1Y3Qm29pAr65OHbdk+S5oN4tN6a3B1tOOXezMPQrgTj9ObWtDcHDZDKV6h
|
| 44 |
+
8l+E1CahylfKoFKYUmIZI74E1S5FItfkIZhQGrWhjV+b84UJgoc27alUFoMJCpT5
|
| 45 |
+
QYhbZJcZIXBmAPtuebcnvWkEmhVaT+4+Trdwg7lGk4GqNge26i0h9vWBC+mN2V7m
|
| 46 |
+
AQKCAQBMdq4Q9Jvr6Mfg9spP/J478/ZvmbYVUuaIpxSgYvVPx2ZRdhjpIrA1OvdS
|
| 47 |
+
nDS7r9Qvhh9lpq7yRxtFjUvH7OFwFDVxUH3X1l5hmeRIX/LTIYw/Gt3fcrpyoaei
|
| 48 |
+
bmXNuKH9FcGdVUcgHbji7pb4ZPgJFmpDXDde1cwNzrvkq5kkPCGiABJYlS2c/FTY
|
| 49 |
+
o72PhSj8nze3VlVwb9Yx5CPS0GqJM202uzUvJN5SdPr4/PbeZcU1fWWoUGvdzOTE
|
| 50 |
+
hhaGD0ByCgBEDJ86uiu28ppVCbHdduWjbgp4OsQM9wcLXY9L0ligi0+C9dyW/fmk
|
| 51 |
+
eDuRtd0FawsrIDxvndnhBysNpuzv
|
| 52 |
+
-----END PRIVATE KEY-----
|
keypoints_process.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from gradio_webrtc import WebRTC
|
| 5 |
+
from twilio.rest import Client
|
| 6 |
+
import os
|
| 7 |
+
import spaces
|
| 8 |
+
from threading import Lock
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
import time
|
| 11 |
+
from bisect import bisect_left
|
| 12 |
+
from scipy.spatial.distance import cdist
|
| 13 |
+
import mediapipe as mp
|
| 14 |
+
from mediapipe.tasks import python
|
| 15 |
+
from mediapipe.tasks.python import vision
|
| 16 |
+
from mediapipe.framework.formats import landmark_pb2
|
| 17 |
+
from mediapipe import solutions
|
| 18 |
+
import pdb
|
| 19 |
+
import json
|
| 20 |
+
from moviepy.editor import VideoFileClip
|
| 21 |
+
import librosa
|
| 22 |
+
|
| 23 |
+
# 启用 GPU 选项
|
| 24 |
+
base_options = mp.tasks.BaseOptions(model_asset_path='gesture_recognizer.task',
|
| 25 |
+
delegate=mp.tasks.BaseOptions.Delegate.GPU)
|
| 26 |
+
|
| 27 |
+
mp_drawing = mp.solutions.drawing_utils
|
| 28 |
+
mp_hands = mp.solutions.hands
|
| 29 |
+
|
| 30 |
+
base_options=mp.tasks.BaseOptions(model_asset_path='hand_landmarker.task',
|
| 31 |
+
delegate=mp.tasks.BaseOptions.Delegate.GPU)
|
| 32 |
+
options = vision.HandLandmarkerOptions(base_options=base_options,
|
| 33 |
+
running_mode=mp.tasks.vision.RunningMode.VIDEO,
|
| 34 |
+
num_hands=2)
|
| 35 |
+
detector = vision.HandLandmarker.create_from_options(options)
|
| 36 |
+
|
| 37 |
+
options_image = vision.HandLandmarkerOptions(base_options=base_options,
|
| 38 |
+
running_mode=mp.tasks.vision.RunningMode.IMAGE,
|
| 39 |
+
num_hands=2)
|
| 40 |
+
detector_image = vision.HandLandmarker.create_from_options(options_image)
|
| 41 |
+
|
| 42 |
+
video_size = (500, 500)
|
| 43 |
+
|
| 44 |
+
previous_timestamp = None
|
| 45 |
+
|
| 46 |
+
class ReferenceVideo:
|
| 47 |
+
def __init__(self):
|
| 48 |
+
self.keypoints = {"Left": [], "Right": []}
|
| 49 |
+
# self.timestamps = []
|
| 50 |
+
# self.duration = 0
|
| 51 |
+
self.frames = [] # 存储原始视频帧
|
| 52 |
+
|
| 53 |
+
def load_video(self, video_path):
|
| 54 |
+
global previous_timestamp
|
| 55 |
+
self.keypoints = {"Left": [], "Right": []}
|
| 56 |
+
self.frames = []
|
| 57 |
+
|
| 58 |
+
video = VideoFileClip(video_path)
|
| 59 |
+
fps = video.fps # 需要存储 fps
|
| 60 |
+
video_size = (video.size[0], video.size[1])
|
| 61 |
+
audio = video.audio.to_soundarray()
|
| 62 |
+
original_sr = video.audio.fps
|
| 63 |
+
audio = librosa.resample(audio.T, orig_sr=original_sr, target_sr=48000).T # 采样率转换
|
| 64 |
+
|
| 65 |
+
if audio.ndim == 2 and audio.shape[1] == 2:
|
| 66 |
+
audio = 0.5 * (audio[:, 0] + audio[:, 1]) # 立体声转单声道
|
| 67 |
+
audio = audio.astype(np.float32)
|
| 68 |
+
|
| 69 |
+
cap = cv2.VideoCapture(video_path)
|
| 70 |
+
while cap.isOpened():
|
| 71 |
+
ret, frame = cap.read()
|
| 72 |
+
if not ret:
|
| 73 |
+
break
|
| 74 |
+
|
| 75 |
+
# 1. 显式拷贝并创建独立的图像数据
|
| 76 |
+
rgb_data = frame.astype(np.uint8).copy()
|
| 77 |
+
rgb = mp.Image(image_format=mp.ImageFormat.SRGB,data=np.array(cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)))
|
| 78 |
+
|
| 79 |
+
# 2. 生成严格递增的时间戳(单位:毫秒)
|
| 80 |
+
timestamp_ms = int(cap.get(cv2.CAP_PROP_POS_MSEC))
|
| 81 |
+
while previous_timestamp is not None and timestamp_ms <= previous_timestamp:
|
| 82 |
+
timestamp_ms = previous_timestamp + 1
|
| 83 |
+
previous_timestamp = timestamp_ms
|
| 84 |
+
|
| 85 |
+
# 3. 调用检测器
|
| 86 |
+
results = detector.detect_for_video(rgb, timestamp_ms)
|
| 87 |
+
|
| 88 |
+
# 4. 处理检测结果
|
| 89 |
+
frame_landmarks = {"Left": None, "Right": None}
|
| 90 |
+
if results.hand_landmarks and results.handedness:
|
| 91 |
+
for idx, hand_landmarks in enumerate(results.hand_landmarks):
|
| 92 |
+
label = results.handedness[idx][0].category_name
|
| 93 |
+
landmarks = [(lm.x, lm.y, lm.z) for lm in hand_landmarks]
|
| 94 |
+
frame_landmarks[label] = landmarks
|
| 95 |
+
|
| 96 |
+
self.keypoints["Left"].append(frame_landmarks["Left"])
|
| 97 |
+
self.keypoints["Right"].append(frame_landmarks["Right"])
|
| 98 |
+
# self.timestamps.append(timestamp_ms / 1000) # 统一使用计算后的时间戳
|
| 99 |
+
self.frames.append(cv2.resize(frame, video_size))
|
| 100 |
+
output_path = os.path.splitext(video_path)[0] + "_keypoints.json"
|
| 101 |
+
with open(output_path, "w") as f:
|
| 102 |
+
json.dump(self.keypoints, f)
|
| 103 |
+
|
| 104 |
+
# 5. 显式释放资源
|
| 105 |
+
del results # 关键:释放检测结果占用的GPU资源
|
| 106 |
+
del rgb # 释放Image实例
|
| 107 |
+
|
| 108 |
+
# self.duration = self.timestamps[-1] if self.timestamps else 0
|
| 109 |
+
cap.release()
|
| 110 |
+
video.close()
|
| 111 |
+
|
| 112 |
+
# np.save(f"{os.path.splitext(video_path)[0]}_frames.npy", np.array(self.frames, dtype=np.uint8))
|
| 113 |
+
# np.save(f"{os.path.splitext(video_path)[0]}_audio.npy", audio)
|
| 114 |
+
metadata = {"fps": fps, "video_size": video_size}
|
| 115 |
+
with open(f"{os.path.splitext(video_path)[0]}_meta.json", "w") as f:
|
| 116 |
+
json.dump(metadata, f)
|
| 117 |
+
|
| 118 |
+
ref_video = ReferenceVideo()
|
| 119 |
+
|
| 120 |
+
video_paths = ['predefined/Move12_preview.mp4', 'predefined/Move12_main.mp4']
|
| 121 |
+
|
| 122 |
+
for video_path in video_paths:
|
| 123 |
+
ref_video.load_video(video_path)
|
load_balancer.log
ADDED
|
Binary file (88.3 kB). View file
|
|
|
load_balancer.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, redirect, request, jsonify
|
| 2 |
+
import requests
|
| 3 |
+
from threading import Lock
|
| 4 |
+
import logging
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
# Configure logging
|
| 11 |
+
logging.basicConfig(
|
| 12 |
+
level=logging.INFO,
|
| 13 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 14 |
+
handlers=[
|
| 15 |
+
logging.FileHandler('load_balancer.log'),
|
| 16 |
+
logging.StreamHandler()
|
| 17 |
+
]
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
app = Flask(__name__)
|
| 21 |
+
|
| 22 |
+
# Configuration
|
| 23 |
+
SERVERS = [f"https://158.130.50.41:{port}" for port in range(7850, 7860)] # Changed to HTTPS
|
| 24 |
+
HEALTH_CHECK_INTERVAL = 30 # seconds
|
| 25 |
+
MAX_FAILURES = 3
|
| 26 |
+
|
| 27 |
+
class LoadBalancer:
|
| 28 |
+
def __init__(self):
|
| 29 |
+
self.servers = SERVERS
|
| 30 |
+
self.current_index = 0
|
| 31 |
+
self.lock = Lock()
|
| 32 |
+
self.server_status = {server: {'healthy': True, 'failures': 0} for server in self.servers}
|
| 33 |
+
self.last_health_check = {server: datetime.now() for server in self.servers}
|
| 34 |
+
|
| 35 |
+
def get_next_server(self):
|
| 36 |
+
with self.lock:
|
| 37 |
+
# Find the next healthy server
|
| 38 |
+
start_index = self.current_index
|
| 39 |
+
while True:
|
| 40 |
+
server = self.servers[self.current_index]
|
| 41 |
+
if self.server_status[server]['healthy']:
|
| 42 |
+
self.current_index = (self.current_index + 1) % len(self.servers)
|
| 43 |
+
return server
|
| 44 |
+
|
| 45 |
+
self.current_index = (self.current_index + 1) % len(self.servers)
|
| 46 |
+
if self.current_index == start_index:
|
| 47 |
+
# All servers are unhealthy
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
def mark_server_failure(self, server):
|
| 51 |
+
with self.lock:
|
| 52 |
+
self.server_status[server]['failures'] += 1
|
| 53 |
+
if self.server_status[server]['failures'] >= MAX_FAILURES:
|
| 54 |
+
self.server_status[server]['healthy'] = False
|
| 55 |
+
logging.warning(f"Server {server} marked as unhealthy")
|
| 56 |
+
|
| 57 |
+
def mark_server_success(self, server):
|
| 58 |
+
with self.lock:
|
| 59 |
+
self.server_status[server]['failures'] = 0
|
| 60 |
+
self.server_status[server]['healthy'] = True
|
| 61 |
+
|
| 62 |
+
def health_check(self, server):
|
| 63 |
+
try:
|
| 64 |
+
# Disable SSL verification for self-signed certificates
|
| 65 |
+
response = requests.get(f"{server}/health", timeout=5, verify=False)
|
| 66 |
+
if response.status_code == 200:
|
| 67 |
+
self.mark_server_success(server)
|
| 68 |
+
return True
|
| 69 |
+
else:
|
| 70 |
+
self.mark_server_failure(server)
|
| 71 |
+
return False
|
| 72 |
+
except Exception as e:
|
| 73 |
+
logging.error(f"Health check failed for {server}: {str(e)}")
|
| 74 |
+
self.mark_server_failure(server)
|
| 75 |
+
return False
|
| 76 |
+
|
| 77 |
+
def get_status(self):
|
| 78 |
+
return {
|
| 79 |
+
'servers': [
|
| 80 |
+
{
|
| 81 |
+
'url': server,
|
| 82 |
+
'healthy': status['healthy'],
|
| 83 |
+
'failures': status['failures']
|
| 84 |
+
}
|
| 85 |
+
for server, status in self.server_status.items()
|
| 86 |
+
]
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
# Initialize load balancer
|
| 90 |
+
load_balancer = LoadBalancer()
|
| 91 |
+
|
| 92 |
+
@app.route('/')
|
| 93 |
+
def proxy():
|
| 94 |
+
server = load_balancer.get_next_server()
|
| 95 |
+
if not server:
|
| 96 |
+
return jsonify({'error': 'No healthy servers available'}), 503
|
| 97 |
+
|
| 98 |
+
# Get the original request path and query parameters
|
| 99 |
+
path = request.path
|
| 100 |
+
query_string = request.query_string.decode('utf-8')
|
| 101 |
+
|
| 102 |
+
# Construct the target URL
|
| 103 |
+
target_url = f"{server}{path}"
|
| 104 |
+
if query_string:
|
| 105 |
+
target_url += f"?{query_string}"
|
| 106 |
+
|
| 107 |
+
logging.info(f"Redirecting to {target_url}")
|
| 108 |
+
return redirect(target_url)
|
| 109 |
+
|
| 110 |
+
@app.route('/health')
|
| 111 |
+
def health():
|
| 112 |
+
return jsonify({'status': 'healthy'}), 200
|
| 113 |
+
|
| 114 |
+
@app.route('/status')
|
| 115 |
+
def status():
|
| 116 |
+
return jsonify(load_balancer.get_status()), 200
|
| 117 |
+
|
| 118 |
+
def run_health_checks():
|
| 119 |
+
"""Background task to check server health"""
|
| 120 |
+
while True:
|
| 121 |
+
for server in SERVERS:
|
| 122 |
+
load_balancer.health_check(server)
|
| 123 |
+
time.sleep(HEALTH_CHECK_INTERVAL)
|
| 124 |
+
|
| 125 |
+
if __name__ == '__main__':
|
| 126 |
+
# Start health check thread
|
| 127 |
+
import threading
|
| 128 |
+
health_check_thread = threading.Thread(target=run_health_checks, daemon=True)
|
| 129 |
+
health_check_thread.start()
|
| 130 |
+
|
| 131 |
+
# Run the Flask app on HTTP port 80
|
| 132 |
+
app.run(host='0.0.0.0', port=7860)
|
manage_nginx.sh
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Function to check if nginx is installed
|
| 4 |
+
check_nginx() {
|
| 5 |
+
if ! command -v nginx &> /dev/null; then
|
| 6 |
+
echo "Nginx is not installed. Installing..."
|
| 7 |
+
sudo apt-get update
|
| 8 |
+
sudo apt-get install -y nginx
|
| 9 |
+
fi
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
# Function to start nginx
|
| 13 |
+
start_nginx() {
|
| 14 |
+
echo "Starting Nginx..."
|
| 15 |
+
sudo cp nginx.conf /etc/nginx/nginx.conf
|
| 16 |
+
sudo nginx -t # Test configuration
|
| 17 |
+
if [ $? -eq 0 ]; then
|
| 18 |
+
sudo systemctl start nginx
|
| 19 |
+
echo "Nginx started successfully"
|
| 20 |
+
else
|
| 21 |
+
echo "Nginx configuration test failed"
|
| 22 |
+
exit 1
|
| 23 |
+
fi
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
# Function to stop nginx
|
| 27 |
+
stop_nginx() {
|
| 28 |
+
echo "Stopping Nginx..."
|
| 29 |
+
sudo systemctl stop nginx
|
| 30 |
+
echo "Nginx stopped"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
# Function to restart nginx
|
| 34 |
+
restart_nginx() {
|
| 35 |
+
echo "Restarting Nginx..."
|
| 36 |
+
sudo cp nginx.conf /etc/nginx/nginx.conf
|
| 37 |
+
sudo nginx -t # Test configuration
|
| 38 |
+
if [ $? -eq 0 ]; then
|
| 39 |
+
sudo systemctl restart nginx
|
| 40 |
+
echo "Nginx restarted successfully"
|
| 41 |
+
else
|
| 42 |
+
echo "Nginx configuration test failed"
|
| 43 |
+
exit 1
|
| 44 |
+
fi
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
# Function to show status
|
| 48 |
+
show_status() {
|
| 49 |
+
echo "Checking Nginx status..."
|
| 50 |
+
sudo systemctl status nginx
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# Main script
|
| 54 |
+
case "$1" in
|
| 55 |
+
start)
|
| 56 |
+
check_nginx
|
| 57 |
+
start_nginx
|
| 58 |
+
;;
|
| 59 |
+
stop)
|
| 60 |
+
stop_nginx
|
| 61 |
+
;;
|
| 62 |
+
restart)
|
| 63 |
+
check_nginx
|
| 64 |
+
restart_nginx
|
| 65 |
+
;;
|
| 66 |
+
status)
|
| 67 |
+
show_status
|
| 68 |
+
;;
|
| 69 |
+
*)
|
| 70 |
+
echo "Usage: $0 {start|stop|restart|status}"
|
| 71 |
+
exit 1
|
| 72 |
+
;;
|
| 73 |
+
esac
|
| 74 |
+
|
| 75 |
+
exit 0
|
mediapipe
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Subproject commit 357dfdba090c5298263b067193d002a4c7a53859
|
models.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlmodel import SQLModel, Field, create_engine
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
class User(SQLModel, table=True):
|
| 6 |
+
id: Optional[int] = Field(default=None, primary_key=True)
|
| 7 |
+
username: str = Field(unique=True, index=True)
|
| 8 |
+
password_hash: str
|
| 9 |
+
display_name: Optional[str]
|
| 10 |
+
|
| 11 |
+
class RecordedSession(SQLModel, table=True):
|
| 12 |
+
id: Optional[int] = Field(default=None, primary_key=True)
|
| 13 |
+
user_id: int
|
| 14 |
+
session_name: str
|
| 15 |
+
timestamp: datetime = Field(default_factory=datetime.now)
|
| 16 |
+
|
| 17 |
+
class VideoFrame(SQLModel, table=True):
|
| 18 |
+
id: Optional[int] = Field(default=None, primary_key=True)
|
| 19 |
+
session_id: int = Field(foreign_key="recordedsession.id")
|
| 20 |
+
frame_index: int
|
| 21 |
+
image_array: bytes # NumPy-encoded frame
|
| 22 |
+
keypoints: bytes # NumPy-encoded keypoints
|
| 23 |
+
audio_chunk: bytes # NumPy-encoded audio chunk
|
| 24 |
+
fps: int
|
| 25 |
+
|
| 26 |
+
class Feedback(SQLModel, table=True):
|
| 27 |
+
id: int = Field(default=None, primary_key=True)
|
| 28 |
+
username: str
|
| 29 |
+
content: str
|
| 30 |
+
timestamp: datetime = Field(default_factory=datetime.now)
|
| 31 |
+
|
| 32 |
+
class Exercise(SQLModel, table=True):
|
| 33 |
+
id: int = Field(default=None, primary_key=True)
|
| 34 |
+
username: str
|
| 35 |
+
duration: str
|
| 36 |
+
sta_time: str
|
| 37 |
+
|
| 38 |
+
engine = create_engine("sqlite:///data/recordings.db")
|
nginx.conf
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
user nginx;
|
| 2 |
+
worker_processes auto;
|
| 3 |
+
error_log /var/log/nginx/error.log warn;
|
| 4 |
+
pid /var/run/nginx.pid;
|
| 5 |
+
|
| 6 |
+
events {
|
| 7 |
+
worker_connections 1024;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
http {
|
| 11 |
+
include /etc/nginx/mime.types;
|
| 12 |
+
default_type application/octet-stream;
|
| 13 |
+
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
| 14 |
+
'$status $body_bytes_sent "$http_referer" '
|
| 15 |
+
'"$http_user_agent" "$http_x_forwarded_for"';
|
| 16 |
+
access_log /var/log/nginx/access.log main;
|
| 17 |
+
sendfile on;
|
| 18 |
+
tcp_nopush on;
|
| 19 |
+
tcp_nodelay on;
|
| 20 |
+
keepalive_timeout 65;
|
| 21 |
+
types_hash_max_size 2048;
|
| 22 |
+
|
| 23 |
+
# SSL configuration
|
| 24 |
+
ssl_protocols TLSv1.2 TLSv1.3;
|
| 25 |
+
ssl_prefer_server_ciphers on;
|
| 26 |
+
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
| 27 |
+
ssl_session_timeout 1d;
|
| 28 |
+
ssl_session_cache shared:SSL:50m;
|
| 29 |
+
ssl_session_tickets off;
|
| 30 |
+
ssl_stapling on;
|
| 31 |
+
ssl_stapling_verify on;
|
| 32 |
+
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
| 33 |
+
resolver_timeout 5s;
|
| 34 |
+
|
| 35 |
+
# Define upstream servers for load balancing
|
| 36 |
+
upstream app_servers {
|
| 37 |
+
least_conn; # Use least connections algorithm for load balancing
|
| 38 |
+
|
| 39 |
+
# Add all 10 servers
|
| 40 |
+
server localhost:7850;
|
| 41 |
+
server localhost:7851;
|
| 42 |
+
server localhost:7852;
|
| 43 |
+
server localhost:7853;
|
| 44 |
+
server localhost:7854;
|
| 45 |
+
server localhost:7855;
|
| 46 |
+
server localhost:7856;
|
| 47 |
+
server localhost:7857;
|
| 48 |
+
server localhost:7858;
|
| 49 |
+
server localhost:7859;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
# Redirect HTTP to HTTPS
|
| 53 |
+
server {
|
| 54 |
+
listen 80;
|
| 55 |
+
server_name 158.130.50.41; # Change this to your domain name
|
| 56 |
+
return 301 https://$server_name$request_uri;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
# HTTPS server configuration
|
| 60 |
+
server {
|
| 61 |
+
listen 443 ssl http2;
|
| 62 |
+
server_name 158.130.50.41; # Change this to your domain name
|
| 63 |
+
|
| 64 |
+
# SSL certificate paths
|
| 65 |
+
ssl_certificate /etc/nginx/ssl/cert.pem;
|
| 66 |
+
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
| 67 |
+
|
| 68 |
+
# WebSocket support
|
| 69 |
+
location / {
|
| 70 |
+
proxy_pass http://app_servers;
|
| 71 |
+
proxy_http_version 1.1;
|
| 72 |
+
proxy_set_header Upgrade $http_upgrade;
|
| 73 |
+
proxy_set_header Connection "upgrade";
|
| 74 |
+
proxy_set_header Host $host;
|
| 75 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 76 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 77 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 78 |
+
proxy_set_header X-Forwarded-Ssl on;
|
| 79 |
+
|
| 80 |
+
# Timeout settings for WebSocket
|
| 81 |
+
proxy_read_timeout 300s;
|
| 82 |
+
proxy_send_timeout 300s;
|
| 83 |
+
proxy_connect_timeout 75s;
|
| 84 |
+
|
| 85 |
+
# Buffer settings
|
| 86 |
+
proxy_buffering off;
|
| 87 |
+
proxy_buffer_size 128k;
|
| 88 |
+
proxy_buffers 4 256k;
|
| 89 |
+
proxy_busy_buffers_size 256k;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
# Health check endpoint
|
| 93 |
+
location /health {
|
| 94 |
+
access_log off;
|
| 95 |
+
return 200 'healthy\n';
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
# Security headers
|
| 99 |
+
add_header Strict-Transport-Security "max-age=63072000" always;
|
| 100 |
+
add_header X-Frame-Options DENY;
|
| 101 |
+
add_header X-Content-Type-Options nosniff;
|
| 102 |
+
add_header X-XSS-Protection "1; mode=block";
|
| 103 |
+
}
|
| 104 |
+
}
|
preprocess_videos.sh
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Directory containing the videos
|
| 4 |
+
VIDEO_DIR="predefined"
|
| 5 |
+
|
| 6 |
+
# Target dimensions
|
| 7 |
+
TARGET_WIDTH=720
|
| 8 |
+
TARGET_HEIGHT=640
|
| 9 |
+
|
| 10 |
+
# Process each video in the directory
|
| 11 |
+
for video in "$VIDEO_DIR"/*.mp4; do
|
| 12 |
+
if [ -f "$video" ]; then
|
| 13 |
+
echo "Processing: $video"
|
| 14 |
+
|
| 15 |
+
# Get video dimensions
|
| 16 |
+
width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$video")
|
| 17 |
+
height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$video")
|
| 18 |
+
|
| 19 |
+
# Calculate crop dimensions for 720:640 aspect ratio
|
| 20 |
+
if [ "$width" -gt "$height" ]; then
|
| 21 |
+
# If video is wider than tall
|
| 22 |
+
crop_width=$(( height * TARGET_WIDTH / TARGET_HEIGHT ))
|
| 23 |
+
crop_height=$height
|
| 24 |
+
x_offset=$(( (width - crop_width) / 2 ))
|
| 25 |
+
y_offset=0
|
| 26 |
+
else
|
| 27 |
+
# If video is taller than wide
|
| 28 |
+
crop_width=$width
|
| 29 |
+
crop_height=$(( width * TARGET_HEIGHT / TARGET_WIDTH ))
|
| 30 |
+
x_offset=0
|
| 31 |
+
y_offset=$(( (height - crop_height) / 2 ))
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
# Process video with ffmpeg
|
| 35 |
+
ffmpeg -i "$video" \
|
| 36 |
+
-af "aresample=48000" \
|
| 37 |
+
-vf "crop=$crop_width:$crop_height:$x_offset:$y_offset,scale=$TARGET_WIDTH:$TARGET_HEIGHT" \
|
| 38 |
+
-c:v libx264 \
|
| 39 |
+
-c:a aac \
|
| 40 |
+
-y \
|
| 41 |
+
"${video%.*}_processed.mp4"
|
| 42 |
+
|
| 43 |
+
echo "Completed: $video"
|
| 44 |
+
|
| 45 |
+
# move the original video to a new name
|
| 46 |
+
mv "$video" "${video%.*}_old.mp4"
|
| 47 |
+
mv "${video%.*}_processed.mp4" "$video"
|
| 48 |
+
fi
|
| 49 |
+
done
|
| 50 |
+
|
| 51 |
+
echo "All videos processed!"
|
process_videos.sh
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Directory containing the videos
|
| 4 |
+
VIDEO_DIR="predefined"
|
| 5 |
+
|
| 6 |
+
# Target dimensions
|
| 7 |
+
TARGET_WIDTH=720
|
| 8 |
+
TARGET_HEIGHT=640
|
| 9 |
+
|
| 10 |
+
# Process each video in the directory
|
| 11 |
+
for video in "$VIDEO_DIR"/*.mov; do
|
| 12 |
+
if [ -f "$video" ]; then
|
| 13 |
+
echo "Processing: $video"
|
| 14 |
+
|
| 15 |
+
# Get video dimensions
|
| 16 |
+
width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$video")
|
| 17 |
+
height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$video")
|
| 18 |
+
|
| 19 |
+
# Calculate crop dimensions for 720:640 aspect ratio
|
| 20 |
+
if [ "$width" -gt "$height" ]; then
|
| 21 |
+
# If video is wider than tall
|
| 22 |
+
crop_width=$(( height * TARGET_WIDTH / TARGET_HEIGHT ))
|
| 23 |
+
crop_height=$height
|
| 24 |
+
x_offset=$(( (width - crop_width) / 2 ))
|
| 25 |
+
y_offset=0
|
| 26 |
+
else
|
| 27 |
+
# If video is taller than wide
|
| 28 |
+
crop_width=$width
|
| 29 |
+
crop_height=$(( width * TARGET_HEIGHT / TARGET_WIDTH ))
|
| 30 |
+
x_offset=0
|
| 31 |
+
y_offset=$(( (height - crop_height) / 2 ))
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
# Process video with ffmpeg
|
| 35 |
+
ffmpeg -i "$video" \
|
| 36 |
+
-af "aresample=48000" \
|
| 37 |
+
-vf "crop=$crop_width:$crop_height:$x_offset:$y_offset,scale=$TARGET_WIDTH:$TARGET_HEIGHT" \
|
| 38 |
+
-c:v libx264 \
|
| 39 |
+
-c:a aac \
|
| 40 |
+
-y \
|
| 41 |
+
"${video%.*}_processed.mov"
|
| 42 |
+
|
| 43 |
+
echo "Completed: $video"
|
| 44 |
+
fi
|
| 45 |
+
done
|
| 46 |
+
|
| 47 |
+
echo "All videos processed!"
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==2.0.1
|
| 2 |
+
gunicorn==20.1.0
|
| 3 |
+
gradio==5.22.0
|
| 4 |
+
gradio_client==1.8.0
|
| 5 |
+
gradio_webrtc==0.0.31
|
| 6 |
+
opencv-python==4.10.0.84
|
| 7 |
+
numpy==1.24.4
|
| 8 |
+
twilio==9.3.7
|
| 9 |
+
scipy==1.15.2
|
| 10 |
+
mediapipe==0.10.18
|
| 11 |
+
moviepy==1.0.3
|
| 12 |
+
librosa==0.10.2.post1
|
| 13 |
+
pydub==0.25.1
|
| 14 |
+
pyttsx3==2.98
|
| 15 |
+
absl-py==2.1.0
|
| 16 |
+
fastapi==0.115.4
|
| 17 |
+
pydantic==2.9.2
|
| 18 |
+
sqlmodel==0.0.24
|
| 19 |
+
passlib==1.7.4
|
| 20 |
+
gTTS==2.5.4
|
run.sh
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
python backend.py > backend.log 2>&1 &
|
| 2 |
+
python app.py
|
run_load_balancer.sh
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Run the load balancer with gunicorn
|
| 4 |
+
echo "Starting load balancer..."
|
| 5 |
+
gunicorn --bind 0.0.0.0:80 \
|
| 6 |
+
--workers 4 \
|
| 7 |
+
--timeout 120 \
|
| 8 |
+
--access-logfile access.log \
|
| 9 |
+
--error-logfile error.log \
|
| 10 |
+
load_balancer:app
|
run_multiple.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Kill any existing Python processes running on these ports
|
| 4 |
+
for port in {7850..7869}; do
|
| 5 |
+
lsof -ti:$port | xargs kill -9 2>/dev/null
|
| 6 |
+
done
|
| 7 |
+
|
| 8 |
+
# Start 10 instances of the app
|
| 9 |
+
for port in {7850..7869}; do
|
| 10 |
+
echo "Starting app on port $port"
|
| 11 |
+
python app.py --port $port &
|
| 12 |
+
# Store the process ID
|
| 13 |
+
echo $! > "app_$port.pid"
|
| 14 |
+
# Wait a bit between launches to prevent port conflicts
|
| 15 |
+
sleep 2
|
| 16 |
+
done
|
| 17 |
+
|
| 18 |
+
echo "All apps started. To stop them, run: ./stop_apps.sh"
|
run_multiple2.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Kill any existing Python processes running on these ports
|
| 4 |
+
for port in {7860..7869}; do
|
| 5 |
+
lsof -ti:$port | xargs kill -9 2>/dev/null
|
| 6 |
+
done
|
| 7 |
+
|
| 8 |
+
# Start 10 instances of the app
|
| 9 |
+
for port in {7860..7869}; do
|
| 10 |
+
echo "Starting app on port $port"
|
| 11 |
+
python app.py --port $port &
|
| 12 |
+
# Store the process ID
|
| 13 |
+
echo $! > "app_$port.pid"
|
| 14 |
+
# Wait a bit between launches to prevent port conflicts
|
| 15 |
+
sleep 2
|
| 16 |
+
done
|
| 17 |
+
|
| 18 |
+
echo "All apps started. To stop them, run: ./stop_apps.sh"
|
setup_firewall.sh
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Function to check if ufw is installed
|
| 4 |
+
check_ufw() {
|
| 5 |
+
if ! command -v ufw &> /dev/null; then
|
| 6 |
+
echo "UFW is not installed. Installing..."
|
| 7 |
+
sudo apt-get update
|
| 8 |
+
sudo apt-get install -y ufw
|
| 9 |
+
fi
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
# Function to enable ufw if not already enabled
|
| 13 |
+
enable_ufw() {
|
| 14 |
+
if ! sudo ufw status | grep -q "Status: active"; then
|
| 15 |
+
echo "Enabling UFW..."
|
| 16 |
+
sudo ufw enable
|
| 17 |
+
fi
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
# Function to allow SSH (important to do this first to prevent lockout)
|
| 21 |
+
allow_ssh() {
|
| 22 |
+
echo "Allowing SSH connections..."
|
| 23 |
+
sudo ufw allow ssh
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
# Function to allow required ports
|
| 27 |
+
allow_ports() {
|
| 28 |
+
echo "Setting up firewall rules..."
|
| 29 |
+
|
| 30 |
+
# Allow HTTP (80)
|
| 31 |
+
sudo ufw allow 80/tcp
|
| 32 |
+
echo "Port 80 (HTTP) allowed"
|
| 33 |
+
|
| 34 |
+
# Allow HTTPS (443)
|
| 35 |
+
sudo ufw allow 443/tcp
|
| 36 |
+
echo "Port 443 (HTTPS) allowed"
|
| 37 |
+
|
| 38 |
+
# Allow ports 7850-7869
|
| 39 |
+
for port in {7850..7869}; do
|
| 40 |
+
sudo ufw allow $port/tcp
|
| 41 |
+
echo "Port $port allowed"
|
| 42 |
+
done
|
| 43 |
+
|
| 44 |
+
# Allow WebSocket connections
|
| 45 |
+
sudo ufw allow 7860/tcp
|
| 46 |
+
echo "Port 7860 (WebSocket) allowed"
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
# Function to show firewall status
|
| 50 |
+
show_status() {
|
| 51 |
+
echo "Current firewall status:"
|
| 52 |
+
sudo ufw status verbose
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# Main script
|
| 56 |
+
echo "Setting up firewall rules..."
|
| 57 |
+
|
| 58 |
+
# Check and install UFW if needed
|
| 59 |
+
check_ufw
|
| 60 |
+
|
| 61 |
+
# Enable UFW
|
| 62 |
+
enable_ufw
|
| 63 |
+
|
| 64 |
+
# Allow SSH first (important!)
|
| 65 |
+
allow_ssh
|
| 66 |
+
|
| 67 |
+
# Allow required ports
|
| 68 |
+
allow_ports
|
| 69 |
+
|
| 70 |
+
# Show final status
|
| 71 |
+
show_status
|
| 72 |
+
|
| 73 |
+
echo "Firewall setup completed. The following ports are now open:"
|
| 74 |
+
echo "- Port 80 (HTTP)"
|
| 75 |
+
echo "- Port 443 (HTTPS)"
|
| 76 |
+
echo "- Ports 7850-7869 (Application servers)"
|
| 77 |
+
echo "- Port 7860 (WebSocket)"
|
| 78 |
+
echo "- Port 22 (SSH)"
|
setup_ssl.sh
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Create SSL directory if it doesn't exist
|
| 4 |
+
sudo mkdir -p /etc/nginx/ssl
|
| 5 |
+
|
| 6 |
+
# Function to generate self-signed certificate
|
| 7 |
+
generate_self_signed() {
|
| 8 |
+
echo "Generating self-signed SSL certificate..."
|
| 9 |
+
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
| 10 |
+
-keyout /etc/nginx/ssl/key.pem \
|
| 11 |
+
-out /etc/nginx/ssl/cert.pem \
|
| 12 |
+
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
|
| 13 |
+
echo "Self-signed certificate generated successfully"
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
# Function to copy existing certificates
|
| 17 |
+
copy_certificates() {
|
| 18 |
+
if [ -f "cert.pem" ] && [ -f "key.pem" ]; then
|
| 19 |
+
echo "Copying existing SSL certificates..."
|
| 20 |
+
sudo cp cert.pem /etc/nginx/ssl/
|
| 21 |
+
sudo cp key.pem /etc/nginx/ssl/
|
| 22 |
+
sudo chmod 600 /etc/nginx/ssl/key.pem
|
| 23 |
+
echo "Certificates copied successfully"
|
| 24 |
+
else
|
| 25 |
+
echo "Certificate files not found. Generating self-signed certificate..."
|
| 26 |
+
generate_self_signed
|
| 27 |
+
fi
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
# Function to check SSL configuration
|
| 31 |
+
check_ssl_config() {
|
| 32 |
+
echo "Checking SSL configuration..."
|
| 33 |
+
sudo nginx -t
|
| 34 |
+
if [ $? -eq 0 ]; then
|
| 35 |
+
echo "SSL configuration is valid"
|
| 36 |
+
else
|
| 37 |
+
echo "SSL configuration check failed"
|
| 38 |
+
exit 1
|
| 39 |
+
fi
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
# Main script
|
| 43 |
+
echo "Setting up SSL for Nginx..."
|
| 44 |
+
|
| 45 |
+
# Copy or generate certificates
|
| 46 |
+
copy_certificates
|
| 47 |
+
|
| 48 |
+
# Check SSL configuration
|
| 49 |
+
check_ssl_config
|
| 50 |
+
|
| 51 |
+
# Restart Nginx to apply changes
|
| 52 |
+
echo "Restarting Nginx to apply SSL configuration..."
|
| 53 |
+
sudo systemctl restart nginx
|
| 54 |
+
|
| 55 |
+
echo "SSL setup completed. Nginx is now configured for HTTPS on port 443"
|
| 56 |
+
echo "You can access your application at https://localhost"
|
stop_apps.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Kill all running instances
|
| 4 |
+
for port in {7850..7869}; do
|
| 5 |
+
if [ -f "app_$port.pid" ]; then
|
| 6 |
+
pid=$(cat "app_$port.pid")
|
| 7 |
+
kill $pid 2>/dev/null
|
| 8 |
+
rm "app_$port.pid"
|
| 9 |
+
echo "Stopped app on port $port"
|
| 10 |
+
fi
|
| 11 |
+
done
|
| 12 |
+
|
| 13 |
+
# Clean up any remaining processes on these ports
|
| 14 |
+
for port in {7850..7869}; do
|
| 15 |
+
lsof -ti:$port | xargs kill -9 2>/dev/null
|
| 16 |
+
done
|
| 17 |
+
|
| 18 |
+
echo "All apps stopped"
|
video.py
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr # 0.0.4
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from gradio_webrtc import WebRTC
|
| 5 |
+
from twilio.rest import Client
|
| 6 |
+
import os
|
| 7 |
+
import spaces
|
| 8 |
+
import time
|
| 9 |
+
from bisect import bisect_left
|
| 10 |
+
from scipy.spatial.distance import cdist
|
| 11 |
+
import mediapipe as mp
|
| 12 |
+
from mediapipe.tasks.python import vision
|
| 13 |
+
from mediapipe.framework.formats import landmark_pb2
|
| 14 |
+
from mediapipe import solutions
|
| 15 |
+
import pdb
|
| 16 |
+
import json
|
| 17 |
+
|
| 18 |
+
# 启用 GPU 选项
|
| 19 |
+
base_options = mp.tasks.BaseOptions(model_asset_path='gesture_recognizer.task',
|
| 20 |
+
delegate=mp.tasks.BaseOptions.Delegate.GPU)
|
| 21 |
+
|
| 22 |
+
mp_drawing = mp.solutions.drawing_utils
|
| 23 |
+
mp_hands = mp.solutions.hands
|
| 24 |
+
|
| 25 |
+
base_options=mp.tasks.BaseOptions(model_asset_path='hand_landmarker.task',
|
| 26 |
+
delegate=mp.tasks.BaseOptions.Delegate.GPU)
|
| 27 |
+
options = vision.HandLandmarkerOptions(base_options=base_options,
|
| 28 |
+
running_mode=mp.tasks.vision.RunningMode.VIDEO,
|
| 29 |
+
num_hands=2)
|
| 30 |
+
detector = vision.HandLandmarker.create_from_options(options)
|
| 31 |
+
|
| 32 |
+
options_image = vision.HandLandmarkerOptions(base_options=base_options,
|
| 33 |
+
running_mode=mp.tasks.vision.RunningMode.IMAGE,
|
| 34 |
+
num_hands=2)
|
| 35 |
+
detector_image = vision.HandLandmarker.create_from_options(options_image)
|
| 36 |
+
|
| 37 |
+
# # 配置 Hand Landmarker
|
| 38 |
+
# hands = HandLandmarker.HandLandmarkerOptions(
|
| 39 |
+
# base_options=base_options,
|
| 40 |
+
# num_hands=2, # 检测最多 2 只手
|
| 41 |
+
# min_hand_detection_confidence=0.5,
|
| 42 |
+
# min_hand_presence_confidence=0.5,
|
| 43 |
+
# min_tracking_confidence=0.5
|
| 44 |
+
# )
|
| 45 |
+
|
| 46 |
+
# # 初始化 MediaPipe Hands 模型
|
| 47 |
+
# hands = mp_hands.Hands(
|
| 48 |
+
# static_image_mode=False,
|
| 49 |
+
# max_num_hands=2,
|
| 50 |
+
# min_detection_confidence=0.3,
|
| 51 |
+
# # model_complexity=1 # 启用分类,
|
| 52 |
+
# )
|
| 53 |
+
|
| 54 |
+
account_sid = os.environ.get("TWILIO_ACCOUNT_SID")
|
| 55 |
+
auth_token = os.environ.get("TWILIO_AUTH_TOKEN")
|
| 56 |
+
|
| 57 |
+
if account_sid and auth_token:
|
| 58 |
+
client = Client(account_sid, auth_token)
|
| 59 |
+
|
| 60 |
+
token = client.tokens.create()
|
| 61 |
+
|
| 62 |
+
rtc_configuration = {
|
| 63 |
+
"iceServers": token.ice_servers,
|
| 64 |
+
"iceTransportPolicy": "relay",
|
| 65 |
+
"video": {
|
| 66 |
+
"width": 500,
|
| 67 |
+
"height": 500
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
else:
|
| 71 |
+
rtc_configuration = None
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# 全局变量
|
| 75 |
+
previous_timestamp = None
|
| 76 |
+
start_time = None
|
| 77 |
+
frame_cnt = 0
|
| 78 |
+
video_size = (500, 500)
|
| 79 |
+
SAMPLING_RATE = 48000
|
| 80 |
+
HAND_CONNECTIONS = [
|
| 81 |
+
(0, 1), (1, 2), (2, 3), (3, 4), # 拇指
|
| 82 |
+
(0, 5), (5, 6), (6, 7), (7, 8), # 食指
|
| 83 |
+
(0, 9), (9, 10), (10, 11), (11, 12), # 中指
|
| 84 |
+
(0, 13), (13, 14), (14, 15), (15, 16), # 无名指
|
| 85 |
+
(0, 17), (17, 18), (18, 19), (19, 20) # 小指
|
| 86 |
+
]
|
| 87 |
+
PREDEFINED_VIDEOS = {
|
| 88 |
+
"Example Video 1": 'predefined/Trim1.mov',
|
| 89 |
+
"Example Video 2": 'predefined/Trim2.mov',
|
| 90 |
+
"Example Video 3": 'predefined/Trim3.mov',
|
| 91 |
+
"Example Video 4": 'predefined/Trim4.mov',
|
| 92 |
+
"Example Video 5": 'predefined/Trim5.mov',
|
| 93 |
+
"Example Video 6": 'predefined/Trim6.mov',
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class ReferenceVideo:
|
| 98 |
+
def __init__(self):
|
| 99 |
+
self.keypoints = {"Left": [], "Right": []}
|
| 100 |
+
# self.timestamps = []
|
| 101 |
+
# self.duration = 0
|
| 102 |
+
self.frames = [] # 存储原始视频帧
|
| 103 |
+
|
| 104 |
+
def load_video(self, video_path):
|
| 105 |
+
self.keypoints = {"Left": [], "Right": []}
|
| 106 |
+
self.frames = []
|
| 107 |
+
cap = cv2.VideoCapture(video_path)
|
| 108 |
+
global previous_timestamp
|
| 109 |
+
while cap.isOpened():
|
| 110 |
+
ret, frame = cap.read()
|
| 111 |
+
if not ret:
|
| 112 |
+
break
|
| 113 |
+
|
| 114 |
+
# 1. 显式拷贝并创建独立的图像数据
|
| 115 |
+
rgb_data = frame.astype(np.uint8).copy()
|
| 116 |
+
rgb = mp.Image(image_format=mp.ImageFormat.SRGB,data=np.array(cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)))
|
| 117 |
+
|
| 118 |
+
# 2. 生成严格递增的时间戳(单位:毫秒)
|
| 119 |
+
timestamp_ms = int(cap.get(cv2.CAP_PROP_POS_MSEC))
|
| 120 |
+
while previous_timestamp is not None and timestamp_ms <= previous_timestamp:
|
| 121 |
+
timestamp_ms = previous_timestamp + 1
|
| 122 |
+
previous_timestamp = timestamp_ms
|
| 123 |
+
|
| 124 |
+
# 3. 调用检测器
|
| 125 |
+
results = detector.detect_for_video(rgb, timestamp_ms)
|
| 126 |
+
|
| 127 |
+
# 4. 处理检测结果
|
| 128 |
+
frame_landmarks = {"Left": None, "Right": None}
|
| 129 |
+
if results.hand_landmarks and results.handedness:
|
| 130 |
+
for idx, hand_landmarks in enumerate(results.hand_landmarks):
|
| 131 |
+
label = results.handedness[idx][0].category_name
|
| 132 |
+
landmarks = [(lm.x, lm.y) for lm in hand_landmarks]
|
| 133 |
+
frame_landmarks[label] = landmarks
|
| 134 |
+
|
| 135 |
+
self.keypoints["Left"].append(frame_landmarks["Left"])
|
| 136 |
+
self.keypoints["Right"].append(frame_landmarks["Right"])
|
| 137 |
+
# self.timestamps.append(timestamp_ms / 1000) # 统一使用计算后的时间戳
|
| 138 |
+
self.frames.append(cv2.resize(frame, video_size))
|
| 139 |
+
|
| 140 |
+
# 5. 显式释放资源
|
| 141 |
+
del results # 关键:释放检测结果占用的GPU资源
|
| 142 |
+
del rgb # 释放Image实例
|
| 143 |
+
|
| 144 |
+
# self.duration = self.timestamps[-1] if self.timestamps else 0
|
| 145 |
+
cap.release()
|
| 146 |
+
if len(self.frames) != 0:
|
| 147 |
+
return "Loaded predefined successfully"
|
| 148 |
+
return "Error: Video not found"
|
| 149 |
+
|
| 150 |
+
def load_keypoints(self, json_path, video_path):
|
| 151 |
+
self.keypoints = {"Left": [], "Right": []}
|
| 152 |
+
self.frames = []
|
| 153 |
+
cap = cv2.VideoCapture(video_path)
|
| 154 |
+
while cap.isOpened():
|
| 155 |
+
ret, frame = cap.read()
|
| 156 |
+
if not ret:
|
| 157 |
+
break
|
| 158 |
+
self.frames.append(cv2.resize(frame, video_size))
|
| 159 |
+
cap.release()
|
| 160 |
+
with open(json_path, "r") as f:
|
| 161 |
+
self.keypoints = json.load(f)
|
| 162 |
+
|
| 163 |
+
# 初始化参考视频
|
| 164 |
+
ref_video = ReferenceVideo()
|
| 165 |
+
|
| 166 |
+
def process_selected_video(video_name):
|
| 167 |
+
video_path = PREDEFINED_VIDEOS.get(video_name, None)
|
| 168 |
+
if video_path and os.path.exists(video_path):
|
| 169 |
+
keypoints_path = os.path.splitext(video_path)[0] + "_keypoints.json" # 假设 keypoints 存在
|
| 170 |
+
ref_video.load_keypoints(keypoints_path, video_path) # 加载关键点
|
| 171 |
+
return "Loaded predefined successfully", video_path # 返回状态 & 视频路径
|
| 172 |
+
return "Error: Video not found", None
|
| 173 |
+
|
| 174 |
+
def compute_overall_similarity(sim_matrix):
|
| 175 |
+
"""计算整体相似度(加权综合)"""
|
| 176 |
+
if sim_matrix is None:
|
| 177 |
+
return 0.0
|
| 178 |
+
|
| 179 |
+
# 对角线权重更高(关键点自身相似度)
|
| 180 |
+
diag_weight = 0.6
|
| 181 |
+
diag_mean = np.mean(np.diag(sim_matrix))
|
| 182 |
+
|
| 183 |
+
# 非对角线部分
|
| 184 |
+
off_diag_weight = 0.4
|
| 185 |
+
mask = np.ones(sim_matrix.shape, dtype=bool)
|
| 186 |
+
np.fill_diagonal(mask, 0)
|
| 187 |
+
off_diag_mean = np.mean(sim_matrix[mask])
|
| 188 |
+
|
| 189 |
+
return diag_weight * diag_mean + off_diag_weight * off_diag_mean
|
| 190 |
+
|
| 191 |
+
# def normalize_hand(hand):
|
| 192 |
+
# """使用手掌宽度归一化手部关键点,避免影响手势形态"""
|
| 193 |
+
# if hand is None:
|
| 194 |
+
# return None
|
| 195 |
+
# hand = np.array(hand)
|
| 196 |
+
|
| 197 |
+
# # 计算手的中心点
|
| 198 |
+
# center = np.mean(hand, axis=0)
|
| 199 |
+
|
| 200 |
+
# # 计算手掌宽度(食指掌关节 - 小指掌关节)
|
| 201 |
+
# palm_width = np.linalg.norm(hand[5] - hand[17]) # keypoints 5 和 17 分别是食指掌关节和小指掌关节
|
| 202 |
+
|
| 203 |
+
# # 避免除零错误
|
| 204 |
+
# palm_width = max(palm_width, 1e-6)
|
| 205 |
+
|
| 206 |
+
# # 归一化关键点
|
| 207 |
+
# normalized_hand = (hand - center) / palm_width
|
| 208 |
+
# return normalized_hand
|
| 209 |
+
|
| 210 |
+
# def compute_distance_matrix(hand):
|
| 211 |
+
# """计算关键点之间的距离矩阵"""
|
| 212 |
+
# if hand is None:
|
| 213 |
+
# return None
|
| 214 |
+
# num_points = 21
|
| 215 |
+
# matrix = np.zeros((num_points, num_points))
|
| 216 |
+
# for i in range(num_points):
|
| 217 |
+
# for j in range(num_points):
|
| 218 |
+
# matrix[i, j] = np.linalg.norm(np.array(hand[i]) - np.array(hand[j]))
|
| 219 |
+
# return matrix
|
| 220 |
+
|
| 221 |
+
# def kl_divergence(p, q):
|
| 222 |
+
# """计算 KL 散度"""
|
| 223 |
+
# p = np.asarray(p) + 1e-10
|
| 224 |
+
# q = np.asarray(q) + 1e-10
|
| 225 |
+
# return np.sum(p * np.log(p / q))
|
| 226 |
+
|
| 227 |
+
# def compute_similarity_matrix(matrix1, matrix2):
|
| 228 |
+
# """计算相似度矩阵,使用 KL 散度"""
|
| 229 |
+
# if matrix1 is None or matrix2 is None:
|
| 230 |
+
# return None
|
| 231 |
+
# similarity_matrix = np.zeros((21, 21))
|
| 232 |
+
# for i in range(21):
|
| 233 |
+
# similarity_matrix[i] = 1 - kl_divergence(matrix1[i], matrix2[i]) # 相似度归一化
|
| 234 |
+
# return similarity_matrix
|
| 235 |
+
|
| 236 |
+
def normalize_hand(hand):
|
| 237 |
+
# """使用手掌对角线归一化手部关键点,避免影响手势形态"""
|
| 238 |
+
# if hand is None:
|
| 239 |
+
# return None
|
| 240 |
+
# hand = np.array(hand)
|
| 241 |
+
|
| 242 |
+
# # 计算手的中心点
|
| 243 |
+
# center = hand[0] # 以手腕 (关键点 0) 作为中心
|
| 244 |
+
|
| 245 |
+
# # 计算手掌对角线(手腕到小指掌关节的距离)
|
| 246 |
+
# palm_size = np.linalg.norm(hand[0] - hand[17]) # keypoints 0 (手腕) 和 17 (小指掌关节)
|
| 247 |
+
|
| 248 |
+
# # 避免除零错误
|
| 249 |
+
# palm_size = max(palm_size, 1e-6)
|
| 250 |
+
|
| 251 |
+
# # 归一化关键点
|
| 252 |
+
# normalized_hand = (hand - center) / palm_size
|
| 253 |
+
# return normalized_hand
|
| 254 |
+
"""改进的归一化:使用多关键点计算缩放因子"""
|
| 255 |
+
if hand is None:
|
| 256 |
+
return None
|
| 257 |
+
hand = np.array(hand)
|
| 258 |
+
|
| 259 |
+
# 使用手腕到中指根部 + 手掌宽度的综合缩放因子
|
| 260 |
+
palm_length = np.linalg.norm(hand[0] - hand[9]) # 手腕到中指根部
|
| 261 |
+
palm_width = np.linalg.norm(hand[5] - hand[17]) # 食指到小指宽度
|
| 262 |
+
scale_factor = 0.5 * (palm_length + palm_width) # 综合指标
|
| 263 |
+
|
| 264 |
+
# 防零处理
|
| 265 |
+
scale_factor = max(scale_factor, 1e-6)
|
| 266 |
+
|
| 267 |
+
# 以手腕为中心归一化
|
| 268 |
+
normalized_hand = (hand - hand[0]) / scale_factor
|
| 269 |
+
return normalized_hand
|
| 270 |
+
|
| 271 |
+
def compute_distance_matrix(hand):
|
| 272 |
+
"""计算关键点之间的欧几里得距离矩阵"""
|
| 273 |
+
if hand is None:
|
| 274 |
+
return None
|
| 275 |
+
num_points = 21
|
| 276 |
+
matrix = np.zeros((num_points, num_points))
|
| 277 |
+
for i in range(num_points):
|
| 278 |
+
for j in range(num_points):
|
| 279 |
+
matrix[i, j] = np.linalg.norm(hand[i] - hand[j])
|
| 280 |
+
return matrix
|
| 281 |
+
|
| 282 |
+
# def compute_angle_features(hand):
|
| 283 |
+
# """计算关键点相对手掌中心的角度特征"""
|
| 284 |
+
# if hand is None:
|
| 285 |
+
# return None
|
| 286 |
+
# hand = np.array(hand)
|
| 287 |
+
# angles = []
|
| 288 |
+
# for i in range(1, 21): # 计算关键点 1-20 相对手腕 (0) 的角度
|
| 289 |
+
# vec = hand[i] - hand[0]
|
| 290 |
+
# angle = np.arctan2(vec[1], vec[0]) # 计算 2D 角度
|
| 291 |
+
# angles.append(angle)
|
| 292 |
+
# return np.array(angles)
|
| 293 |
+
|
| 294 |
+
# def cosine_similarity(p, q):
|
| 295 |
+
# """计算余弦相似度"""
|
| 296 |
+
# if p is None or q is None:
|
| 297 |
+
# return None
|
| 298 |
+
# p = np.asarray(p) + 1e-10
|
| 299 |
+
# q = np.asarray(q) + 1e-10
|
| 300 |
+
# return 1 - cosine(p, q)
|
| 301 |
+
|
| 302 |
+
# def compute_similarity_matrix(matrix1, matrix2):
|
| 303 |
+
# """计算相似度矩阵,使用余弦相似度"""
|
| 304 |
+
# if matrix1 is None or matrix2 is None:
|
| 305 |
+
# return None
|
| 306 |
+
# similarity_matrix = np.zeros((21, 21))
|
| 307 |
+
# for i in range(21):
|
| 308 |
+
# similarity_matrix[i] = cosine_similarity(matrix1[i], matrix2[i]) # 计算相似度
|
| 309 |
+
# return similarity_matrix
|
| 310 |
+
|
| 311 |
+
def kl_divergence(p, q):
|
| 312 |
+
"""计算 KL 散度,并增加动态阈值控制"""
|
| 313 |
+
p = np.asarray(p) + 1e-10
|
| 314 |
+
q = np.asarray(q) + 1e-10
|
| 315 |
+
divergence = np.sum(p * np.log(p / q))
|
| 316 |
+
return divergence
|
| 317 |
+
|
| 318 |
+
def compute_similarity_matrix(matrix1, matrix2):
|
| 319 |
+
# """计算相似度矩阵,结合欧氏距离和 KL 散度"""
|
| 320 |
+
# if matrix1 is None or matrix2 is None:
|
| 321 |
+
# return None
|
| 322 |
+
# num_points = 21
|
| 323 |
+
# similarity_matrix = np.zeros((num_points, num_points))
|
| 324 |
+
|
| 325 |
+
# for i in range(num_points):
|
| 326 |
+
# for j in range(num_points):
|
| 327 |
+
# euclidean_diff = abs(matrix1[i, j] - matrix2[i, j]) # 计算欧氏距离差异
|
| 328 |
+
# euclidean_similarity = np.exp(-euclidean_diff) # 转换为相似性(越小相似度越高)
|
| 329 |
+
|
| 330 |
+
# kl_sim = 1 / (1 + kl_divergence(matrix1[i], matrix2[i])) # KL 散度转换为相似性
|
| 331 |
+
|
| 332 |
+
# similarity_matrix[i, j] = 0.5 * euclidean_similarity + 0.5 * kl_sim # 加权组合相似性
|
| 333 |
+
|
| 334 |
+
# return similarity_matrix
|
| 335 |
+
"""改进的相似度计算:引入高斯核平滑和对称性处理"""
|
| 336 |
+
if matrix1 is None or matrix2 is None:
|
| 337 |
+
return None
|
| 338 |
+
|
| 339 |
+
num_points = 21
|
| 340 |
+
similarity_matrix = np.zeros((num_points, num_points))
|
| 341 |
+
|
| 342 |
+
# 对距离矩阵进行高斯平滑(减少噪声影响)
|
| 343 |
+
sigma = 0.5 # 高斯核宽度,控制平滑程度
|
| 344 |
+
gauss_kernel = np.exp(-(matrix1 - matrix2)**2 / (2 * sigma**2))
|
| 345 |
+
|
| 346 |
+
for i in range(num_points):
|
| 347 |
+
for j in range(num_points):
|
| 348 |
+
# 欧氏距离相似度(高斯核转换)
|
| 349 |
+
euclidean_sim = gauss_kernel[i, j]
|
| 350 |
+
|
| 351 |
+
# JS散度替代KL散度(对称且更稳定)
|
| 352 |
+
p = matrix1[i] + 1e-10
|
| 353 |
+
q = matrix2[i] + 1e-10
|
| 354 |
+
m = 0.5 * (p + q)
|
| 355 |
+
js_div = 0.5 * (kl_divergence(p, m) + kl_divergence(q, m))
|
| 356 |
+
js_sim = 1 / (1 + js_div) # 转换为相似度
|
| 357 |
+
|
| 358 |
+
# 动态权重调整(更依赖欧氏距离)
|
| 359 |
+
similarity_matrix[i, j] = 0.7 * euclidean_sim + 0.3 * js_sim
|
| 360 |
+
|
| 361 |
+
# 增加矩阵整体均值作为稳定性补偿
|
| 362 |
+
matrix_sim = np.mean(similarity_matrix)
|
| 363 |
+
return similarity_matrix * 0.9 + matrix_sim * 0.1
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
# def compute_distance_matrix(hand):
|
| 367 |
+
# """计算关键点归一化距离矩阵(21x21)"""
|
| 368 |
+
# if hand is None or len(hand) != 21:
|
| 369 |
+
# return None
|
| 370 |
+
|
| 371 |
+
# # 转换为numpy数组
|
| 372 |
+
# points = np.array([(x, y) for (x, y) in hand], dtype=np.float32)
|
| 373 |
+
|
| 374 |
+
# # 计算欧氏距离矩阵
|
| 375 |
+
# dist_matrix = np.linalg.norm(points[:, None] - points, axis=2)
|
| 376 |
+
|
| 377 |
+
# # 归一化到[0,1]
|
| 378 |
+
# max_dist = np.max(dist_matrix)
|
| 379 |
+
# if max_dist > 0:
|
| 380 |
+
# dist_matrix /= max_dist
|
| 381 |
+
# return dist_matrix
|
| 382 |
+
|
| 383 |
+
# def row_to_probability(row, temperature=0.1):
|
| 384 |
+
# """将距离行转换为概率分布(Softmax归一化)"""
|
| 385 |
+
# scaled = -row / temperature # 距离越小,概率越高
|
| 386 |
+
# exp_values = np.exp(scaled - np.max(scaled)) # 数值稳定性优化
|
| 387 |
+
# return exp_values / np.sum(exp_values)
|
| 388 |
+
|
| 389 |
+
# def js_divergence(p_row, q_row):
|
| 390 |
+
# """计算对称化散度(Jensen-Shannon)"""
|
| 391 |
+
# p = p_row + 1e-10 # 防止除零
|
| 392 |
+
# q = q_row + 1e-10
|
| 393 |
+
# m = 0.5 * (p + q)
|
| 394 |
+
# return 0.5 * (np.sum(p * np.log(p / m)) + np.sum(q * np.log(q / m)))
|
| 395 |
+
|
| 396 |
+
# def compute_similarity_matrix(matrix1, matrix2, temperature=0.1):
|
| 397 |
+
# """生成21x21相似度矩阵"""
|
| 398 |
+
# if matrix1 is None or matrix2 is None:
|
| 399 |
+
# return None
|
| 400 |
+
|
| 401 |
+
# similarity_matrix = np.zeros((21, 21))
|
| 402 |
+
|
| 403 |
+
# for i in range(21):
|
| 404 |
+
# # 转换行概率分布
|
| 405 |
+
# p_row = row_to_probability(matrix1[i], temperature)
|
| 406 |
+
# q_row = row_to_probability(matrix2[i], temperature)
|
| 407 |
+
|
| 408 |
+
# # 计算对称散度并映射到相似度
|
| 409 |
+
# js = js_divergence(p_row, q_row)
|
| 410 |
+
# similarity_matrix[i] = 1 - np.sqrt(js) # 映射到[0,1]
|
| 411 |
+
# return np.clip(similarity_matrix, 0.0, 1.0)
|
| 412 |
+
|
| 413 |
+
# 存储平滑后的相似度
|
| 414 |
+
smooth_sim_left = np.ones(21) * 0.5 # 初始值设为 0.8
|
| 415 |
+
smooth_sim_right = np.ones(21) * 0.5
|
| 416 |
+
alpha = 0.2
|
| 417 |
+
|
| 418 |
+
# def draw_hand(img, left_landmarks, left_similarity_matrix, right_landmarks, right_similarity_matrix):
|
| 419 |
+
# """绘制手部关键点,颜色由相似度平滑过渡(红 -> 绿)"""
|
| 420 |
+
# global smooth_sim_left, smooth_sim_right, alpha # 平滑参数需在外部初始化
|
| 421 |
+
|
| 422 |
+
# if left_similarity_matrix is not None:
|
| 423 |
+
# for i in range(21):
|
| 424 |
+
# x, y = int(left_landmarks[i][0] * img.shape[1]), int(left_landmarks[i][1] * img.shape[0])
|
| 425 |
+
# # 计算当前关键点的平均相似度(可根据需要调整权重)
|
| 426 |
+
# sim_score = np.mean(left_similarity_matrix[i])
|
| 427 |
+
|
| 428 |
+
# # 指数平滑处理(建议alpha=0.2~0.3)
|
| 429 |
+
# smooth_sim_left[i] = alpha * sim_score + (1 - alpha) * smooth_sim_left[i]
|
| 430 |
+
# sim_score = smooth_sim_left[i]
|
| 431 |
+
|
| 432 |
+
# # 非线性颜色映射增强对比度(sim_score范围[0,1])
|
| 433 |
+
# g = int(255 * (sim_score ** 2)) # 高相似度时绿色更突出
|
| 434 |
+
# r = int(255 * ((1 - sim_score) ** 2)) # 低相似度时红色更突出
|
| 435 |
+
# color = (0, g, r) # OpenCV使用BGR格式
|
| 436 |
+
|
| 437 |
+
# cv2.circle(img, (x, y), 6, color, -1)
|
| 438 |
+
|
| 439 |
+
# # 绘制连接线(保持原逻辑)
|
| 440 |
+
# for i, j in HAND_CONNECTIONS:
|
| 441 |
+
# p1 = (int(left_landmarks[i][0] * img.shape[1]), int(left_landmarks[i][1] * img.shape[0]))
|
| 442 |
+
# p2 = (int(left_landmarks[j][0] * img.shape[1]), int(left_landmarks[j][1] * img.shape[0]))
|
| 443 |
+
# cv2.line(img, p1, p2, (0, 255, 255), 2)
|
| 444 |
+
|
| 445 |
+
# # 右侧手部同理
|
| 446 |
+
# if right_similarity_matrix is not None:
|
| 447 |
+
# for i in range(21):
|
| 448 |
+
# x, y = int(right_landmarks[i][0] * img.shape[1]), int(right_landmarks[i][1] * img.shape[0])
|
| 449 |
+
# sim_score = np.mean(right_similarity_matrix[i])
|
| 450 |
+
# smooth_sim_right[i] = alpha * sim_score + (1 - alpha) * smooth_sim_right[i]
|
| 451 |
+
# sim_score = smooth_sim_right[i]
|
| 452 |
+
|
| 453 |
+
# g = int(255 * (sim_score ** 2))
|
| 454 |
+
# r = int(255 * ((1 - sim_score) ** 2))
|
| 455 |
+
# color = (0, g, r)
|
| 456 |
+
# cv2.circle(img, (x, y), 6, color, -1)
|
| 457 |
+
|
| 458 |
+
# for i, j in HAND_CONNECTIONS:
|
| 459 |
+
# p1 = (int(right_landmarks[i][0] * img.shape[1]), int(right_landmarks[i][1] * img.shape[0]))
|
| 460 |
+
# p2 = (int(right_landmarks[j][0] * img.shape[1]), int(right_landmarks[j][1] * img.shape[0]))
|
| 461 |
+
# cv2.line(img, p1, p2, (0, 255, 255), 2)
|
| 462 |
+
|
| 463 |
+
# return img
|
| 464 |
+
|
| 465 |
+
def draw_hand(img, left_landmarks, left_similarity_matrix, right_landmarks, right_similarity_matrix):
|
| 466 |
+
"""绘制手部关键点,颜色深度由相似度决定"""
|
| 467 |
+
|
| 468 |
+
# if left_similarity_matrix is not None:
|
| 469 |
+
# for i in range(21):
|
| 470 |
+
# x, y = int(left_landmarks[i][0] * img.shape[1]), int(left_landmarks[i][1] * img.shape[0])
|
| 471 |
+
# sim_score = np.mean(left_similarity_matrix[i])
|
| 472 |
+
# color = (0, int(sim_score * 255), int((1 - sim_score) * 255)) # 绿色->红色
|
| 473 |
+
# cv2.circle(img, (x, y), 6, color, -1)
|
| 474 |
+
|
| 475 |
+
# for i, j in HAND_CONNECTIONS:
|
| 476 |
+
# p1 = (int(left_landmarks[i][0] * img.shape[1]), int(left_landmarks[i][1] * img.shape[0]))
|
| 477 |
+
# p2 = (int(left_landmarks[j][0] * img.shape[1]), int(left_landmarks[j][1] * img.shape[0]))
|
| 478 |
+
# cv2.line(img, p1, p2, (0, 255, 255), 2)
|
| 479 |
+
|
| 480 |
+
# if right_similarity_matrix is not None:
|
| 481 |
+
# for i in range(21):
|
| 482 |
+
# x, y = int(right_landmarks[i][0] * img.shape[1]), int(right_landmarks[i][1] * img.shape[0])
|
| 483 |
+
# sim_score = np.mean(right_similarity_matrix[i])
|
| 484 |
+
# color = (0, int(sim_score * 255), int((1 - sim_score) * 255)) # 绿色->红色
|
| 485 |
+
# cv2.circle(img, (x, y), 6, color, -1)
|
| 486 |
+
|
| 487 |
+
# for i, j in HAND_CONNECTIONS:
|
| 488 |
+
# p1 = (int(right_landmarks[i][0] * img.shape[1]), int(right_landmarks[i][1] * img.shape[0]))
|
| 489 |
+
# p2 = (int(right_landmarks[j][0] * img.shape[1]), int(right_landmarks[j][1] * img.shape[0]))
|
| 490 |
+
# cv2.line(img, p1, p2, (0, 255, 255), 2)
|
| 491 |
+
|
| 492 |
+
global smooth_sim_left, smooth_sim_right, alpha # 需要在多帧之间保持平滑值
|
| 493 |
+
|
| 494 |
+
if left_similarity_matrix is not None:
|
| 495 |
+
for i in range(21):
|
| 496 |
+
x, y = int(left_landmarks[i][0] * img.shape[1]), int(left_landmarks[i][1] * img.shape[0])
|
| 497 |
+
sim_score = np.mean(left_similarity_matrix[i])
|
| 498 |
+
|
| 499 |
+
# 平滑处理,减少抖动
|
| 500 |
+
smooth_sim_left[i] = alpha * sim_score + (1 - alpha) * smooth_sim_left[i]
|
| 501 |
+
sim_score = smooth_sim_left[i]
|
| 502 |
+
|
| 503 |
+
# 颜色映射优化
|
| 504 |
+
if sim_score > 0.7:
|
| 505 |
+
color = (0, 255, 0) # 纯绿色(高相似度)
|
| 506 |
+
else:
|
| 507 |
+
color = (0, 0, 255) # 纯红色(低相似度)
|
| 508 |
+
# else:
|
| 509 |
+
# g = int(255 * (sim_score ** 2)) # 平滑过渡的绿色
|
| 510 |
+
# r = int(255 * ((1 - sim_score) ** 2)) # 平滑过渡的红色
|
| 511 |
+
# color = (0, g, r)
|
| 512 |
+
|
| 513 |
+
cv2.circle(img, (x, y), 6, color, -1)
|
| 514 |
+
|
| 515 |
+
for i, j in HAND_CONNECTIONS:
|
| 516 |
+
p1 = (int(left_landmarks[i][0] * img.shape[1]), int(left_landmarks[i][1] * img.shape[0]))
|
| 517 |
+
p2 = (int(left_landmarks[j][0] * img.shape[1]), int(left_landmarks[j][1] * img.shape[0]))
|
| 518 |
+
cv2.line(img, p1, p2, (0, 255, 255), 2)
|
| 519 |
+
|
| 520 |
+
if right_similarity_matrix is not None:
|
| 521 |
+
for i in range(21):
|
| 522 |
+
x, y = int(right_landmarks[i][0] * img.shape[1]), int(right_landmarks[i][1] * img.shape[0])
|
| 523 |
+
sim_score = np.mean(right_similarity_matrix[i])
|
| 524 |
+
|
| 525 |
+
# 平滑处理,减少抖动
|
| 526 |
+
smooth_sim_right[i] = alpha * sim_score + (1 - alpha) * smooth_sim_right[i]
|
| 527 |
+
sim_score = smooth_sim_right[i]
|
| 528 |
+
|
| 529 |
+
# 颜色映射优化
|
| 530 |
+
if sim_score > 0.7:
|
| 531 |
+
color = (0, 255, 0) # 纯绿色(高相似度)
|
| 532 |
+
else:
|
| 533 |
+
color = (0, 0, 255) # 纯红色(低相似度)
|
| 534 |
+
# else:
|
| 535 |
+
# g = int(255 * (sim_score ** 2)) # 平滑过渡的绿色
|
| 536 |
+
# r = int(255 * ((1 - sim_score) ** 2)) # 平滑过渡的红色
|
| 537 |
+
# color = (0, g, r)
|
| 538 |
+
|
| 539 |
+
cv2.circle(img, (x, y), 6, color, -1)
|
| 540 |
+
|
| 541 |
+
for i, j in HAND_CONNECTIONS:
|
| 542 |
+
p1 = (int(right_landmarks[i][0] * img.shape[1]), int(right_landmarks[i][1] * img.shape[0]))
|
| 543 |
+
p2 = (int(right_landmarks[j][0] * img.shape[1]), int(right_landmarks[j][1] * img.shape[0]))
|
| 544 |
+
cv2.line(img, p1, p2, (0, 255, 255), 2)
|
| 545 |
+
|
| 546 |
+
return img
|
| 547 |
+
|
| 548 |
+
def draw_hand_gt(img, left_landmarks, right_landmarks):
|
| 549 |
+
"""绘制手部关键点,全部标记为蓝色"""
|
| 550 |
+
|
| 551 |
+
if left_landmarks is not None:
|
| 552 |
+
for i in range(21):
|
| 553 |
+
x, y = int(left_landmarks[i][0] * img.shape[1]), int(left_landmarks[i][1] * img.shape[0])
|
| 554 |
+
color = (255, 0, 0) # 纯蓝色
|
| 555 |
+
cv2.circle(img, (x, y), 6, color, -1)
|
| 556 |
+
|
| 557 |
+
for i, j in HAND_CONNECTIONS:
|
| 558 |
+
p1 = (int(left_landmarks[i][0] * img.shape[1]), int(left_landmarks[i][1] * img.shape[0]))
|
| 559 |
+
p2 = (int(left_landmarks[j][0] * img.shape[1]), int(left_landmarks[j][1] * img.shape[0]))
|
| 560 |
+
cv2.line(img, p1, p2, (255, 0, 0), 2) # 连接线也是蓝色
|
| 561 |
+
|
| 562 |
+
if right_landmarks is not None:
|
| 563 |
+
for i in range(21):
|
| 564 |
+
x, y = int(right_landmarks[i][0] * img.shape[1]), int(right_landmarks[i][1] * img.shape[0])
|
| 565 |
+
color = (255, 0, 0) # 纯蓝色
|
| 566 |
+
cv2.circle(img, (x, y), 6, color, -1)
|
| 567 |
+
|
| 568 |
+
for i, j in HAND_CONNECTIONS:
|
| 569 |
+
p1 = (int(right_landmarks[i][0] * img.shape[1]), int(right_landmarks[i][1] * img.shape[0]))
|
| 570 |
+
p2 = (int(right_landmarks[j][0] * img.shape[1]), int(right_landmarks[j][1] * img.shape[0]))
|
| 571 |
+
cv2.line(img, p1, p2, (255, 0, 0), 2) # 连接线也是蓝色
|
| 572 |
+
|
| 573 |
+
return img
|
| 574 |
+
|
| 575 |
+
def draw_hand_landmarks(img, real_left_matrix, ref_left_matrix, real_right_matrix, ref_right_matrix, real_hand_data):
|
| 576 |
+
"""在实时视频帧上绘制手势关键点,并连线"""
|
| 577 |
+
img_h, img_w, _ = img.shape
|
| 578 |
+
|
| 579 |
+
def draw_hand(hand_data, real_matrix, ref_matrix):
|
| 580 |
+
if hand_data is not None and real_matrix is not None and ref_matrix is not None:
|
| 581 |
+
# 计算并绘制关键点
|
| 582 |
+
for i, (x, y) in enumerate(hand_data):
|
| 583 |
+
sim = np.exp(-kl_divergence(real_matrix[i], ref_matrix[i]))
|
| 584 |
+
red_intensity = int(255 * (1 - sim)) # 低相似度 => 更红
|
| 585 |
+
green_intensity = int(255 * sim) # 高相似度 => 更绿
|
| 586 |
+
color = (0, green_intensity, red_intensity)
|
| 587 |
+
cv2.circle(img, (int(x * img_w), int(y * img_h)), 5, color, -1)
|
| 588 |
+
|
| 589 |
+
# 绘制关键点连线
|
| 590 |
+
for connection in HAND_CONNECTIONS:
|
| 591 |
+
start_idx, end_idx = connection
|
| 592 |
+
if start_idx < len(hand_data) and end_idx < len(hand_data):
|
| 593 |
+
x1, y1 = hand_data[start_idx]
|
| 594 |
+
x2, y2 = hand_data[end_idx]
|
| 595 |
+
start_pos = (int(x1 * img_w), int(y1 * img_h))
|
| 596 |
+
end_pos = (int(x2 * img_w), int(y2 * img_h))
|
| 597 |
+
cv2.line(img, start_pos, end_pos, (255, 255, 255), 2) # 线条颜色为白色,宽度2px
|
| 598 |
+
|
| 599 |
+
# 处理左手
|
| 600 |
+
draw_hand(real_hand_data["Left"], real_left_matrix, ref_left_matrix)
|
| 601 |
+
|
| 602 |
+
# 处理右手
|
| 603 |
+
draw_hand(real_hand_data["Right"], real_right_matrix, ref_right_matrix)
|
| 604 |
+
|
| 605 |
+
return img
|
| 606 |
+
|
| 607 |
+
def draw_landmarks_on_image(rgb_image, detection_result):
|
| 608 |
+
|
| 609 |
+
hand_landmarks_list = detection_result.hand_landmarks
|
| 610 |
+
handedness_list = detection_result.handedness
|
| 611 |
+
annotated_image = np.copy(rgb_image)
|
| 612 |
+
|
| 613 |
+
for idx in range(len(hand_landmarks_list)):
|
| 614 |
+
hand_landmarks = hand_landmarks_list[idx]
|
| 615 |
+
handedness = handedness_list[idx]
|
| 616 |
+
|
| 617 |
+
# Draw the hand landmarks.
|
| 618 |
+
hand_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
|
| 619 |
+
hand_landmarks_proto.landmark.extend([
|
| 620 |
+
landmark_pb2.NormalizedLandmark(x=landmark.x, y=landmark.y, z=landmark.z) for landmark in hand_landmarks
|
| 621 |
+
])
|
| 622 |
+
solutions.drawing_utils.draw_landmarks(
|
| 623 |
+
annotated_image,
|
| 624 |
+
hand_landmarks_proto,
|
| 625 |
+
solutions.hands.HAND_CONNECTIONS,
|
| 626 |
+
solutions.drawing_styles.get_default_hand_landmarks_style(),
|
| 627 |
+
solutions.drawing_styles.get_default_hand_connections_style())
|
| 628 |
+
return annotated_image
|
| 629 |
+
|
| 630 |
+
def detection(img):
|
| 631 |
+
global start_time, frame_cnt, video_size
|
| 632 |
+
img = cv2.resize(img, video_size)
|
| 633 |
+
# 降低分辨率处理
|
| 634 |
+
# small_img = cv2.resize(img, (640, 480))
|
| 635 |
+
img = cv2.flip(img, 1)
|
| 636 |
+
rgb = mp.Image(image_format=mp.ImageFormat.SRGB,data=np.array(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)))
|
| 637 |
+
results = detector_image.detect(rgb)
|
| 638 |
+
|
| 639 |
+
# annotated_image = draw_landmarks_on_image(rgb.numpy_view(), results)
|
| 640 |
+
# return annotated_image
|
| 641 |
+
|
| 642 |
+
if not ref_video.frames:
|
| 643 |
+
cv2.putText(img, "Reference video not loaded", (50, 50), cv2.FONT_HERSHEY_SIMPLEX,
|
| 644 |
+
1, (0, 0, 255), 2)
|
| 645 |
+
return img, (SAMPLING_RATE, np.zeros(int(SAMPLING_RATE * 0.5), dtype=np.float32))
|
| 646 |
+
|
| 647 |
+
if not results.hand_landmarks:
|
| 648 |
+
cv2.putText(img, "No hand detected", (50, 50), cv2.FONT_HERSHEY_SIMPLEX,
|
| 649 |
+
1, (0, 0, 255), 2)
|
| 650 |
+
|
| 651 |
+
combined_img = cv2.hconcat([img, ref_video.frames[frame_cnt]])
|
| 652 |
+
frame_cnt += 1
|
| 653 |
+
frame_cnt = frame_cnt % len(ref_video.frames)
|
| 654 |
+
return combined_img, (SAMPLING_RATE, np.zeros(int(SAMPLING_RATE * 0.5), dtype=np.float32))
|
| 655 |
+
|
| 656 |
+
if start_time is None:
|
| 657 |
+
start_time = time.time()
|
| 658 |
+
|
| 659 |
+
# # 计算当前时间戳
|
| 660 |
+
# current_time = time.time() - start_time
|
| 661 |
+
# aligned_time = max(0, current_time - time_offset)
|
| 662 |
+
|
| 663 |
+
# 找到最近的参考帧
|
| 664 |
+
# ref_index = find_nearest_frame(aligned_time % ref_video.duration)
|
| 665 |
+
ref_index = frame_cnt
|
| 666 |
+
ref_left = ref_video.keypoints["Left"][ref_index]
|
| 667 |
+
ref_right = ref_video.keypoints["Right"][ref_index]
|
| 668 |
+
|
| 669 |
+
# 解析当前帧
|
| 670 |
+
real_hand_data = {"Left": None, "Right": None}
|
| 671 |
+
for idx, hand_landmarks in enumerate(results.hand_landmarks):
|
| 672 |
+
label = results.handedness[idx][0].category_name
|
| 673 |
+
real_hand_data[label] = [(lm.x, lm.y) for lm in hand_landmarks]
|
| 674 |
+
|
| 675 |
+
# real_left_norm = normalize_hand(real_hand_data["Left"])
|
| 676 |
+
# real_right_norm = normalize_hand(real_hand_data["Right"])
|
| 677 |
+
# ref_left_norm = normalize_hand(ref_left)
|
| 678 |
+
# ref_right_norm = normalize_hand(ref_right)
|
| 679 |
+
# real_left_matrix = compute_distance_matrix(real_left_norm)
|
| 680 |
+
# real_right_matrix = compute_distance_matrix(real_right_norm)
|
| 681 |
+
# ref_left_matrix = compute_distance_matrix(ref_left_norm)
|
| 682 |
+
# ref_right_matrix = compute_distance_matrix(ref_right_norm)
|
| 683 |
+
# sim_left_matrix = compute_similarity_matrix(real_left_matrix, ref_left_matrix)
|
| 684 |
+
# sim_right_matrix = compute_similarity_matrix(real_right_matrix, ref_right_matrix)
|
| 685 |
+
|
| 686 |
+
# 归一化手部关键点
|
| 687 |
+
real_left_norm = normalize_hand(real_hand_data["Left"])
|
| 688 |
+
real_right_norm = normalize_hand(real_hand_data["Right"])
|
| 689 |
+
ref_left_norm = normalize_hand(ref_left)
|
| 690 |
+
ref_right_norm = normalize_hand(ref_right)
|
| 691 |
+
|
| 692 |
+
# 计算距离矩阵
|
| 693 |
+
real_left_matrix = compute_distance_matrix(real_left_norm)
|
| 694 |
+
real_right_matrix = compute_distance_matrix(real_right_norm)
|
| 695 |
+
ref_left_matrix = compute_distance_matrix(ref_left_norm)
|
| 696 |
+
ref_right_matrix = compute_distance_matrix(ref_right_norm)
|
| 697 |
+
|
| 698 |
+
# # 计算角度特征(用于辅助相似度判断)
|
| 699 |
+
# real_left_angles = compute_angle_features(real_left_norm)
|
| 700 |
+
# real_right_angles = compute_angle_features(real_right_norm)
|
| 701 |
+
# ref_left_angles = compute_angle_features(ref_left_norm)
|
| 702 |
+
# ref_right_angles = compute_angle_features(ref_right_norm)
|
| 703 |
+
|
| 704 |
+
# 计算距离矩阵的相似度
|
| 705 |
+
sim_left_matrix = compute_similarity_matrix(real_left_matrix, ref_left_matrix)
|
| 706 |
+
sim_right_matrix = compute_similarity_matrix(real_right_matrix, ref_right_matrix)
|
| 707 |
+
|
| 708 |
+
# # 计算角度特征的相似度(可选)
|
| 709 |
+
# angle_similarity_left = cosine_similarity(real_left_angles, ref_left_angles)
|
| 710 |
+
# angle_similarity_right = cosine_similarity(real_right_angles, ref_right_angles)
|
| 711 |
+
|
| 712 |
+
# # 归一化最终相似度矩阵
|
| 713 |
+
# final_sim_left = sim_left_matrix * angle_similarity_left
|
| 714 |
+
# final_sim_right = sim_right_matrix * angle_similarity_right
|
| 715 |
+
|
| 716 |
+
# sim_left_matrix = compute_similarity_matrix(real_hand_data["Left"], ref_left)
|
| 717 |
+
# sim_right_matrix = compute_similarity_matrix(real_hand_data["Right"], ref_right)
|
| 718 |
+
|
| 719 |
+
# 在实时视频帧 `img` 上绘制关键点
|
| 720 |
+
# img = draw_hand_landmarks(img, real_left_matrix, ref_left_matrix, real_right_matrix, ref_right_matrix, real_hand_data)
|
| 721 |
+
img_left = draw_hand(img, real_hand_data["Left"], sim_left_matrix, real_hand_data["Right"], sim_right_matrix)
|
| 722 |
+
img_right = draw_hand_gt(ref_video.frames[frame_cnt], ref_left, ref_right)
|
| 723 |
+
|
| 724 |
+
# left_sim = compute_overall_similarity(sim_left_matrix)
|
| 725 |
+
# right_sim = compute_overall_similarity(sim_right_matrix)
|
| 726 |
+
# overall_sim = max(left_sim, right_sim)
|
| 727 |
+
# feedback_manager.check_and_generate(overall_sim)
|
| 728 |
+
# try:
|
| 729 |
+
# audio_data = feedback_manager.audio_queue.get_nowait()
|
| 730 |
+
# except feedback_manager.audio_queue.Empty:
|
| 731 |
+
# audio_data = (SAMPLING_RATE, np.zeros(int(SAMPLING_RATE * 0.1), dtype=np.float32))
|
| 732 |
+
# if not feedback_manager.audio_queue.empty():
|
| 733 |
+
# audio_data = feedback_manager.audio_queue.get()
|
| 734 |
+
# else:
|
| 735 |
+
# audio_data = (SAMPLING_RATE, np.zeros(int(SAMPLING_RATE * 0.1), dtype=np.float32))
|
| 736 |
+
|
| 737 |
+
# 拼接 `img` 和比对视频的对应帧
|
| 738 |
+
combined_img = cv2.hconcat([img_left, img_right])
|
| 739 |
+
|
| 740 |
+
frame_cnt += 1
|
| 741 |
+
frame_cnt = frame_cnt % len(ref_video.frames)
|
| 742 |
+
|
| 743 |
+
return combined_img
|
| 744 |
+
|
| 745 |
+
# Gradio 界面
|
| 746 |
+
css = """
|
| 747 |
+
.container { max-width: 1200px; margin: auto; }
|
| 748 |
+
.video-column { background: #f5f5f5; padding: 20px; border-radius: 10px; width: 3000px; height: 50%}
|
| 749 |
+
.alert-panel { min-height: 150px; }
|
| 750 |
+
.loading { text-align: center; padding: 50px; }
|
| 751 |
+
.custom-blue-button {
|
| 752 |
+
background-color: #87CEEB; /* 天蓝色背景 */
|
| 753 |
+
color: white; /* 白色字体 */
|
| 754 |
+
border: none; /* 去掉默认边框 */
|
| 755 |
+
border-radius: 5px; /* 圆角 */
|
| 756 |
+
font-weight: bold; /* 加粗字体 */
|
| 757 |
+
width: 100%; /* 按钮占满列宽 */
|
| 758 |
+
text-align: center; /* 文字居中 */
|
| 759 |
+
cursor: pointer; /* 鼠标悬停时显示手形 */
|
| 760 |
+
transition: background-color 0.3s ease; /* 添加过渡效果 */
|
| 761 |
+
.webcam-container {
|
| 762 |
+
display: flex;
|
| 763 |
+
justify-content: center;
|
| 764 |
+
align-items: center;
|
| 765 |
+
height: 50%; /* 限制 webcam 的高度 */
|
| 766 |
+
max-width: 100%; /* 限制 webcam 宽度 */
|
| 767 |
+
}
|
| 768 |
+
"""
|
| 769 |
+
|
| 770 |
+
with gr.Blocks(css=css) as demo:
|
| 771 |
+
gr.HTML("<h1 style='text-align:center;color:#2c3e50;'>Intelligent Gesture Comparison System</h1>")
|
| 772 |
+
|
| 773 |
+
with gr.Accordion("Reference Video Configuration", open=True):
|
| 774 |
+
with gr.Row():
|
| 775 |
+
with gr.Column():
|
| 776 |
+
gr.Markdown("## Reference video configuration")
|
| 777 |
+
|
| 778 |
+
# 创建两个并排的部分:上传新视频 和 选择已有视频
|
| 779 |
+
with gr.Row(): # 用 gr.Row() 实现平行布局
|
| 780 |
+
with gr.Column():
|
| 781 |
+
# 上传新视频部分
|
| 782 |
+
upload_video = gr.Video(label="Upload standard action video", sources=["upload"], elem_classes="video-upload")
|
| 783 |
+
upload_btn = gr.Button("Analysis video", variant="primary", elem_classes="custom-blue-button")
|
| 784 |
+
|
| 785 |
+
with gr.Column():
|
| 786 |
+
# 选择已有视频部分
|
| 787 |
+
selected_video_display = gr.Video(label="Selected Video", interactive=False)
|
| 788 |
+
video_dropdown = gr.Dropdown(choices=list(PREDEFINED_VIDEOS.keys()), label="Choose Predefined Video")
|
| 789 |
+
select_btn = gr.Button("Load Selected Video", variant="primary", elem_classes="custom-blue-button")
|
| 790 |
+
|
| 791 |
+
upload_status = gr.Textbox(label="Processing status", interactive=False)
|
| 792 |
+
|
| 793 |
+
with gr.Row():
|
| 794 |
+
with gr.Column():
|
| 795 |
+
gr.Markdown("## Real-time detection screen")
|
| 796 |
+
video_output = WebRTC(
|
| 797 |
+
label="Stream",
|
| 798 |
+
rtc_configuration=rtc_configuration,
|
| 799 |
+
)
|
| 800 |
+
# with gr.Row():
|
| 801 |
+
# sim_display = gr.Number(label="Real-time similarity", value=0.0)
|
| 802 |
+
# feedback_audio = gr.Audio(
|
| 803 |
+
# label="Audio",
|
| 804 |
+
# autoplay=True,
|
| 805 |
+
# # interactive=False,
|
| 806 |
+
# # # every=0.5 # 每0.5秒检查更新
|
| 807 |
+
# # type="numpy",
|
| 808 |
+
# )
|
| 809 |
+
|
| 810 |
+
select_btn.click(process_selected_video, inputs=video_dropdown, outputs=[upload_status, selected_video_display])
|
| 811 |
+
upload_btn.click(ref_video.load_video, inputs=upload_video, outputs=upload_status)
|
| 812 |
+
video_output.stream(fn=detection, inputs=[video_output], outputs=[video_output], time_limit=90, concurrency_limit=2)
|
| 813 |
+
|
| 814 |
+
if __name__ == "__main__":
|
| 815 |
+
demo.launch(share=True)
|
video_audio.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
import gradio as gr # 0.0.28
|
| 6 |
+
from mediapipe import solutions
|
| 7 |
+
from gradio_webrtc import WebRTC, AsyncAudioVideoStreamHandler, AudioEmitType, VideoEmitType
|
| 8 |
+
from twilio.rest import Client
|
| 9 |
+
import io
|
| 10 |
+
from pydub import AudioSegment
|
| 11 |
+
import pdb
|
| 12 |
+
|
| 13 |
+
# 🔹 初始化 MediaPipe Hands
|
| 14 |
+
hands = solutions.hands.Hands(static_image_mode=False, max_num_hands=2, min_detection_confidence=0.3)
|
| 15 |
+
|
| 16 |
+
# 🔹 Twilio ICE 配置(WebRTC 服务器)
|
| 17 |
+
account_sid = os.environ.get("TWILIO_ACCOUNT_SID")
|
| 18 |
+
auth_token = os.environ.get("TWILIO_AUTH_TOKEN")
|
| 19 |
+
|
| 20 |
+
if account_sid and auth_token:
|
| 21 |
+
client = Client(account_sid, auth_token)
|
| 22 |
+
token = client.tokens.create()
|
| 23 |
+
rtc_configuration = {"iceServers": token.ice_servers, "iceTransportPolicy": "relay"}
|
| 24 |
+
else:
|
| 25 |
+
rtc_configuration = None
|
| 26 |
+
|
| 27 |
+
# 🎤 采样率
|
| 28 |
+
SAMPLING_RATE = 16000
|
| 29 |
+
|
| 30 |
+
# ================================
|
| 31 |
+
# 📌 实现 WebRTC 处理音视频数据
|
| 32 |
+
# ================================
|
| 33 |
+
class AVHandler(AsyncAudioVideoStreamHandler):
|
| 34 |
+
def __init__(self):
|
| 35 |
+
super().__init__(expected_layout="mono", output_sample_rate=SAMPLING_RATE, output_frame_size=480)
|
| 36 |
+
self.audio_queue = asyncio.Queue()
|
| 37 |
+
self.video_queue = asyncio.Queue()
|
| 38 |
+
self.very_good_audio = self.generate_very_good_audio()
|
| 39 |
+
|
| 40 |
+
def copy(self):
|
| 41 |
+
return AVHandler()
|
| 42 |
+
|
| 43 |
+
async def video_receive(self, frame: np.ndarray):
|
| 44 |
+
"""处理 WebRTC 采集到的视频帧"""
|
| 45 |
+
# 进行手势检测
|
| 46 |
+
img = cv2.resize(frame, (640, 480))
|
| 47 |
+
results = hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
| 48 |
+
|
| 49 |
+
if results.multi_hand_landmarks:
|
| 50 |
+
for landmarks in results.multi_hand_landmarks:
|
| 51 |
+
solutions.drawing_utils.draw_landmarks(img, landmarks, solutions.hands.HAND_CONNECTIONS)
|
| 52 |
+
|
| 53 |
+
self.video_queue.put_nowait(cv2.resize(img, (500, 500)))
|
| 54 |
+
|
| 55 |
+
async def video_emit(self) -> VideoEmitType:
|
| 56 |
+
"""发送处理后的视频流"""
|
| 57 |
+
return await self.video_queue.get()
|
| 58 |
+
|
| 59 |
+
async def receive(self, frame: tuple[int, np.ndarray]) -> None:
|
| 60 |
+
"""接收音频流"""
|
| 61 |
+
_, array = frame
|
| 62 |
+
# if array.ndim == 1: # 确保数据为二维
|
| 63 |
+
# array = np.expand_dims(array, axis=-1) # 变为 (samples, 1)
|
| 64 |
+
self.audio_queue.put_nowait(array)
|
| 65 |
+
|
| 66 |
+
async def emit(self) -> AudioEmitType:
|
| 67 |
+
"""发送音频流"""
|
| 68 |
+
# array = await self.audio_queue.get()
|
| 69 |
+
array = self.very_good_audio.reshape(1, -1)
|
| 70 |
+
# if array.ndim == 1: # 确保数据为二维
|
| 71 |
+
# array = np.expand_dims(array, axis=-1)
|
| 72 |
+
return (SAMPLING_RATE, array)
|
| 73 |
+
|
| 74 |
+
def generate_very_good_audio(self):
|
| 75 |
+
"""使用 pydub 生成‘very good’的音频数据"""
|
| 76 |
+
from gtts import gTTS
|
| 77 |
+
|
| 78 |
+
# 生成语音文件
|
| 79 |
+
tts = gTTS("very good", lang="en")
|
| 80 |
+
audio_io = io.BytesIO()
|
| 81 |
+
tts.write_to_fp(audio_io)
|
| 82 |
+
audio_io.seek(0)
|
| 83 |
+
|
| 84 |
+
# 加载为 pydub AudioSegment
|
| 85 |
+
audio = AudioSegment.from_file(audio_io, format="mp3")
|
| 86 |
+
audio = audio.set_frame_rate(SAMPLING_RATE).set_channels(1)
|
| 87 |
+
|
| 88 |
+
# 转换为 NumPy 数组
|
| 89 |
+
samples = np.array(audio.get_array_of_samples()).astype(np.float32)
|
| 90 |
+
samples /= np.iinfo(audio.array_type).max # 归一化到 [-1, 1]
|
| 91 |
+
|
| 92 |
+
return samples
|
| 93 |
+
|
| 94 |
+
# ================================
|
| 95 |
+
# 📌 Gradio 界面
|
| 96 |
+
# ================================
|
| 97 |
+
css = """
|
| 98 |
+
.my-group {max-width: 600px !important; max-height: 600px !important;}
|
| 99 |
+
.my-column {display: flex !important; justify-content: center !important; align-items: center !important;}
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
with gr.Blocks(css=css) as demo:
|
| 103 |
+
gr.HTML("<h1 style='text-align: center'>实时音视频手势检测 🎥🎤</h1>")
|
| 104 |
+
|
| 105 |
+
with gr.Column(elem_classes=["my-column"]):
|
| 106 |
+
with gr.Group(elem_classes=["my-group"]):
|
| 107 |
+
webrtc = WebRTC(
|
| 108 |
+
label="Stream",
|
| 109 |
+
modality="audio-video",
|
| 110 |
+
mode="send-receive",
|
| 111 |
+
rtc_configuration=rtc_configuration,
|
| 112 |
+
icon="🔵",
|
| 113 |
+
pulse_color="rgb(35, 157, 225)"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# 绑定音视频处理
|
| 117 |
+
webrtc.stream(
|
| 118 |
+
AVHandler(), inputs=[webrtc], outputs=[webrtc], time_limit=90, concurrency_limit=2
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
if __name__ == "__main__":
|
| 122 |
+
demo.launch(share=True)
|