huzey commited on
Commit
1a01fdb
·
1 Parent(s): 3effbb0
Files changed (33) hide show
  1. .gitattributes +11 -0
  2. .gitignore +11 -0
  3. .gradio/certificate.pem +31 -0
  4. Dockerfile +14 -0
  5. README.md +5 -10
  6. access.log +0 -0
  7. app.py +0 -0
  8. app_no_oom.py +0 -0
  9. backend.log +6 -0
  10. backend.py +192 -0
  11. cert.pem +33 -0
  12. db_utils.py +157 -0
  13. error.log +14 -0
  14. key.pem +52 -0
  15. keypoints_process.py +123 -0
  16. load_balancer.log +0 -0
  17. load_balancer.py +132 -0
  18. manage_nginx.sh +75 -0
  19. mediapipe +1 -0
  20. models.py +38 -0
  21. nginx.conf +104 -0
  22. preprocess_videos.sh +51 -0
  23. process_videos.sh +47 -0
  24. requirements.txt +20 -0
  25. run.sh +2 -0
  26. run_load_balancer.sh +10 -0
  27. run_multiple.sh +18 -0
  28. run_multiple2.sh +18 -0
  29. setup_firewall.sh +78 -0
  30. setup_ssl.sh +56 -0
  31. stop_apps.sh +18 -0
  32. video.py +815 -0
  33. 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
- title: Finger
3
- emoji: 📈
4
- colorFrom: purple
5
- colorTo: green
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)