Skip to content

Commit 2369f76

Browse files
committed
更换Quart为FastAPI;针对 gradio视频合成卡播放问题的解决(gradio同步请求阻塞了server的其他请求,而前端的视频加载是通过server的URL加载的,server被阻塞后无法处理URL请求,导致前端被卡视频加载导致卡顿。目前将请求通过线程池(10个)单独托管,避免阻塞)
1 parent 8094272 commit 2369f76

8 files changed

Lines changed: 573 additions & 193 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,6 @@ static/test
166166
output/
167167

168168
static/index copy.html
169-
static/index copy 2.html
169+
static/index copy 2.html
170+
171+
api_server copy.py

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ python:3.10.10
2626

2727
# API
2828

29+
运行后,可以查看API文档:[http://127.0.0.1:8091/docs](http://127.0.0.1:8091/docs)
30+
2931
## 播放视频
3032

3133
### 概述
@@ -95,6 +97,10 @@ python:3.10.10
9597

9698
# 更新日志
9799

100+
- v0.2.0
101+
- 更换Quart为FastAPI
102+
- 针对 gradio视频合成卡播放问题的解决(gradio同步请求阻塞了server的其他请求,而前端的视频加载是通过server的URL加载的,server被阻塞后无法处理URL请求,导致前端被卡视频加载导致卡顿。目前将请求通过线程池(10个)单独托管,避免阻塞)
103+
98104
- v0.1.9
99105
- easy_wav2lip文件传参改用gradio_client传递,可以支持本地文件传递到云API
100106
- 提高httpx日志等级到WARNING

api_server.py

Lines changed: 69 additions & 188 deletions
Large diffs are not rendered by default.

api_server_quart.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
from quart import Quart, websocket, redirect, url_for, jsonify, request, send_from_directory
2+
import asyncio, threading, time, os
3+
import json, logging, traceback
4+
from selenium import webdriver
5+
import re
6+
7+
from utils.config import Config
8+
from utils.common import Common
9+
from utils.logger import Configure_logger
10+
from utils.video_generate import get_video
11+
12+
# 获取 httpx 库的日志记录器
13+
httpx_logger = logging.getLogger("httpx")
14+
# 设置 httpx 日志记录器的级别为 WARNING
15+
httpx_logger.setLevel(logging.WARNING)
16+
17+
common = Common()
18+
# 日志文件路径
19+
file_path = "./log/log-" + common.get_bj_time(1) + ".txt"
20+
Configure_logger(file_path)
21+
config = Config("config.json")
22+
23+
# 获取当前脚本的目录
24+
script_dir = os.path.dirname(os.path.abspath(__file__))
25+
26+
# 设置静态文件夹路径为相对于当前脚本目录的路径
27+
static_folder = os.path.join(script_dir, 'static')
28+
static_folder = os.path.abspath(static_folder)
29+
30+
print(f"static_folder={static_folder}")
31+
32+
app = Quart(__name__, static_folder=static_folder)
33+
connected_websockets = set()
34+
35+
# 队列中非默认视频个数
36+
non_default_video_count = 0
37+
38+
39+
40+
@app.route('/')
41+
async def home():
42+
return redirect(url_for('static', filename='index.html'))
43+
44+
@app.route('/videos/<path:filename>')
45+
async def load_video(filename):
46+
video_path = os.path.join(app.static_folder, 'video', filename)
47+
return await send_from_directory(os.path.dirname(video_path), os.path.basename(video_path))
48+
49+
50+
@app.websocket('/ws')
51+
async def ws():
52+
global non_default_video_count
53+
54+
connected_websockets.add(websocket._get_current_object())
55+
try:
56+
while True:
57+
try:
58+
data = await websocket.receive()
59+
data_json = json.loads(data)
60+
logging.info(f"收到客户端数据: {data_json}")
61+
# 处理从客户端接收的数据的逻辑
62+
if data_json['type'] == "videoEnded":
63+
non_default_video_count = data_json['count']
64+
# 跳过默认视频
65+
if common.get_filename_with_ext(config.get("default_video")) not in data_json['video_path']:
66+
# 在这里添加删除视频文件的逻辑
67+
await delete_video_file(data_json['video_path'])
68+
elif data_json['type'] == "get_default_video":
69+
logging.info(f"发送默认配置 视频路径: {config.get('default_video')}")
70+
# 在这里添加发送消息到客户端的逻辑
71+
await send_to_all_websockets(json.dumps({"type": "set_default_video", "video_path": config.get("default_video")}))
72+
elif data_json['type'] == "show":
73+
logging.info(f"队列中非默认视频个数: {data_json['count']}")
74+
non_default_video_count = data_json['count']
75+
elif data_json['type'] == "get_non_default_video_count":
76+
logging.info(f"队列中非默认视频个数: {data_json['count']}")
77+
non_default_video_count = data_json['count']
78+
except Exception as e:
79+
logging.error(traceback.format_exc())
80+
finally:
81+
connected_websockets.remove(websocket)
82+
83+
84+
# 删除视频文件
85+
async def delete_video_file(video_path: str):
86+
from urllib.parse import unquote
87+
88+
# 根据你的逻辑来获取要删除的视频文件路径
89+
file_name_with_extension = os.path.basename(video_path)
90+
file_name_with_extension = unquote(file_name_with_extension, 'utf-8')
91+
relative_path = os.path.join(static_folder, "videos", file_name_with_extension)
92+
93+
try:
94+
os.remove(relative_path) # 删除视频文件
95+
logging.info(f"成功删除视频文件 {relative_path}。")
96+
except FileNotFoundError:
97+
logging.error(f"未找到视频文件 {relative_path}。")
98+
except Exception as e:
99+
logging.error(f"删除视频文件时发生错误:{str(e)}")
100+
101+
102+
async def send_to_all_websockets(data):
103+
for ws in connected_websockets:
104+
await ws.send(data)
105+
106+
def extract_filename(video_path):
107+
if '=' in video_path:
108+
filepath = video_path.split('=')[1]
109+
else:
110+
filepath = video_path
111+
112+
match = re.search(r'[^\\/:*?"<>|\r\n]+$', filepath)
113+
if match:
114+
return match.group()
115+
else:
116+
return common.get_filename_with_ext(filepath)
117+
118+
@app.route('/show', methods=['POST'])
119+
async def show():
120+
try:
121+
data = await request.get_json()
122+
123+
logging.info(f"收到数据:{data}")
124+
125+
video_path = await get_video(data["type"], data, config)
126+
127+
if video_path:
128+
static_video_path = os.path.join(static_folder, "videos")
129+
logging.debug(f"视频文件移动到的路径:{static_video_path}")
130+
131+
filename = ""
132+
move_file = data.get("move_file")
133+
is_linux = common.detect_os() == "Linux"
134+
135+
if move_file:
136+
if is_linux:
137+
filename = extract_filename(video_path)
138+
ret = common.move_and_rename(video_path, static_video_path, new_filename=filename, move_file=move_file)
139+
else:
140+
ret = common.move_and_rename(video_path, static_video_path, move_file=move_file)
141+
filename = common.get_filename_with_ext(video_path)
142+
else:
143+
if is_linux:
144+
filename = extract_filename(video_path)
145+
ret = common.move_and_rename(video_path, static_video_path, new_filename=filename)
146+
else:
147+
ret = common.move_and_rename(video_path, static_video_path)
148+
filename = common.get_filename_with_ext(video_path)
149+
if ret == False:
150+
return jsonify({"code": 200, "message": "视频移动失败"})
151+
152+
file_url = f"http://127.0.0.1:{config.get('server_port')}/static/videos/{filename}"
153+
154+
if "audio_path" not in data:
155+
data["audio_path"] = None
156+
157+
if "insert_index" not in data:
158+
data["insert_index"] = -1
159+
160+
await send_to_all_websockets(
161+
json.dumps(
162+
{
163+
"type": "show",
164+
"video_path": file_url,
165+
"audio_path": data["audio_path"],
166+
"insert_index": data["insert_index"]
167+
}
168+
)
169+
)
170+
171+
return jsonify({"code": 200, "message": "操作成功"})
172+
173+
return jsonify({"code": 200, "message": "视频合成失败"})
174+
except Exception as e:
175+
logging.error(traceback.format_exc())
176+
return jsonify({"code": -1, "message": f"操作失败: {str(e)}"})
177+
178+
179+
# 停止当前播放的视频,跳转到下一个
180+
@app.route('/stop_current_video', methods=['POST'])
181+
async def stop_current_video():
182+
try:
183+
#data = await request.get_json()
184+
185+
#logging.info(f"收到数据:{data}")
186+
187+
await send_to_all_websockets(
188+
json.dumps(
189+
{
190+
"type": "stop_current_video"
191+
}
192+
)
193+
)
194+
195+
return jsonify({"code": 200, "message": "操作成功"})
196+
except Exception as e:
197+
logging.error(traceback.format_exc())
198+
return jsonify({"code": -1, "message": f"操作失败: {str(e)}"})
199+
200+
# 获取非默认视频个数
201+
@app.route('/get_non_default_video_count', methods=['POST'])
202+
async def get_non_default_video_count():
203+
try:
204+
await send_to_all_websockets(
205+
json.dumps(
206+
{
207+
"type": "get_non_default_video_count"
208+
}
209+
)
210+
)
211+
# 等待ws返回后对数据的更新
212+
await asyncio.sleep(0.5)
213+
return jsonify({"code": 200, "count": non_default_video_count, "message": "操作成功"})
214+
except Exception as e:
215+
logging.error(traceback.format_exc())
216+
return jsonify({"code": -1, "message": f"操作失败: {str(e)}"})
217+
218+
async def main():
219+
# 使用 Quart 提供的 run_task 方法来启动异步的 Web 应用
220+
await app.run_task(host=config.get("server_ip"), port=config.get("server_port"))
221+
222+
223+
class StoppableThread(threading.Thread):
224+
def __init__(self, target=None, stop_event=None):
225+
super().__init__()
226+
self._stop_event = stop_event
227+
self._target = target # 保存目标函数引用
228+
229+
def run(self):
230+
if self._target: # 如果有目标函数,调用它
231+
self._target(self._stop_event)
232+
self._stop_event.wait() # 等待事件被设置
233+
logging.info("Thread is stopping")
234+
235+
def stop(self):
236+
self._stop_event.set()
237+
238+
if __name__ == '__main__':
239+
def start_browser(stop_event):
240+
options = webdriver.ChromeOptions()
241+
# 设置为开发者模式,避免被浏览器识别为自动化程序
242+
options.add_experimental_option('excludeSwitches', ['enable-automation'])
243+
options.add_argument('--autoplay-policy=no-user-gesture-required')
244+
245+
# 使用`options`而不是`chrome_options`作为参数
246+
driver = webdriver.Chrome(options=options)
247+
driver.get(f'http://127.0.0.1:{config.get("server_port")}')
248+
249+
stop_event.wait() # 等待事件被设置
250+
251+
# 创建一个停止事件实例
252+
stop_event = threading.Event()
253+
254+
# 创建一个可停止的线程实例
255+
browser_thread = StoppableThread(target=start_browser, stop_event=stop_event)
256+
257+
# 启动线程
258+
browser_thread.start()
259+
260+
asyncio.run(main())
261+
262+
# 在某个时刻停止线程
263+
# browser_thread.stop()
264+
265+
os._exit(0)

config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"batch_process": "False"
2323
},
2424
"sadtalker": {
25-
"gradio_api_type": "api_name",
25+
"gradio_api_type": "fn_index",
2626
"api_ip_port": "http://127.0.0.1:7860",
2727
"img_file": "E:\\GitHub_pro\\digital_human_video_player\\static\\imgs\\2.png",
2828
"preprocess": "crop",

requirements.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
quart
1+
# quart
22
selenium
3-
gradio_client==0.10.1
4-
colorlog
3+
gradio_client==0.16.3
4+
colorlog
5+
fastapi

0 commit comments

Comments
 (0)