Skip to content

Commit

Permalink
更换Quart为FastAPI;针对 gradio视频合成卡播放问题的解决(gradio同步请求阻塞了server的其他请求,而前端的视频…
Browse files Browse the repository at this point in the history
…加载是通过server的URL加载的,server被阻塞后无法处理URL请求,导致前端被卡视频加载导致卡顿。目前将请求通过线程池(10个)单独托管,避免阻塞)
  • Loading branch information
Ikaros-521 committed Jun 29, 2024
1 parent 8094272 commit 2369f76
Show file tree
Hide file tree
Showing 8 changed files with 573 additions and 193 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,6 @@ static/test
output/

static/index copy.html
static/index copy 2.html
static/index copy 2.html

api_server copy.py
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ python:3.10.10

# API

运行后,可以查看API文档:[http://127.0.0.1:8091/docs](http://127.0.0.1:8091/docs)

## 播放视频

### 概述
Expand Down Expand Up @@ -95,6 +97,10 @@ python:3.10.10

# 更新日志

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

- v0.1.9
- easy_wav2lip文件传参改用gradio_client传递,可以支持本地文件传递到云API
- 提高httpx日志等级到WARNING
Expand Down
257 changes: 69 additions & 188 deletions api_server.py

Large diffs are not rendered by default.

265 changes: 265 additions & 0 deletions api_server_quart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
from quart import Quart, websocket, redirect, url_for, jsonify, request, send_from_directory
import asyncio, threading, time, os
import json, logging, traceback
from selenium import webdriver
import re

from utils.config import Config
from utils.common import Common
from utils.logger import Configure_logger
from utils.video_generate import get_video

# 获取 httpx 库的日志记录器
httpx_logger = logging.getLogger("httpx")
# 设置 httpx 日志记录器的级别为 WARNING
httpx_logger.setLevel(logging.WARNING)

common = Common()
# 日志文件路径
file_path = "./log/log-" + common.get_bj_time(1) + ".txt"
Configure_logger(file_path)
config = Config("config.json")

# 获取当前脚本的目录
script_dir = os.path.dirname(os.path.abspath(__file__))

# 设置静态文件夹路径为相对于当前脚本目录的路径
static_folder = os.path.join(script_dir, 'static')
static_folder = os.path.abspath(static_folder)

print(f"static_folder={static_folder}")

app = Quart(__name__, static_folder=static_folder)
connected_websockets = set()

# 队列中非默认视频个数
non_default_video_count = 0



@app.route('/')
async def home():
return redirect(url_for('static', filename='index.html'))

@app.route('/videos/<path:filename>')
async def load_video(filename):
video_path = os.path.join(app.static_folder, 'video', filename)
return await send_from_directory(os.path.dirname(video_path), os.path.basename(video_path))


@app.websocket('/ws')
async def ws():
global non_default_video_count

connected_websockets.add(websocket._get_current_object())
try:
while True:
try:
data = await websocket.receive()
data_json = json.loads(data)
logging.info(f"收到客户端数据: {data_json}")
# 处理从客户端接收的数据的逻辑
if data_json['type'] == "videoEnded":
non_default_video_count = data_json['count']
# 跳过默认视频
if common.get_filename_with_ext(config.get("default_video")) not in data_json['video_path']:
# 在这里添加删除视频文件的逻辑
await delete_video_file(data_json['video_path'])
elif data_json['type'] == "get_default_video":
logging.info(f"发送默认配置 视频路径: {config.get('default_video')}")
# 在这里添加发送消息到客户端的逻辑
await send_to_all_websockets(json.dumps({"type": "set_default_video", "video_path": config.get("default_video")}))
elif data_json['type'] == "show":
logging.info(f"队列中非默认视频个数: {data_json['count']}")
non_default_video_count = data_json['count']
elif data_json['type'] == "get_non_default_video_count":
logging.info(f"队列中非默认视频个数: {data_json['count']}")
non_default_video_count = data_json['count']
except Exception as e:
logging.error(traceback.format_exc())
finally:
connected_websockets.remove(websocket)


# 删除视频文件
async def delete_video_file(video_path: str):
from urllib.parse import unquote

# 根据你的逻辑来获取要删除的视频文件路径
file_name_with_extension = os.path.basename(video_path)
file_name_with_extension = unquote(file_name_with_extension, 'utf-8')
relative_path = os.path.join(static_folder, "videos", file_name_with_extension)

try:
os.remove(relative_path) # 删除视频文件
logging.info(f"成功删除视频文件 {relative_path}。")
except FileNotFoundError:
logging.error(f"未找到视频文件 {relative_path}。")
except Exception as e:
logging.error(f"删除视频文件时发生错误:{str(e)}")


async def send_to_all_websockets(data):
for ws in connected_websockets:
await ws.send(data)

def extract_filename(video_path):
if '=' in video_path:
filepath = video_path.split('=')[1]
else:
filepath = video_path

match = re.search(r'[^\\/:*?"<>|\r\n]+$', filepath)
if match:
return match.group()
else:
return common.get_filename_with_ext(filepath)

@app.route('/show', methods=['POST'])
async def show():
try:
data = await request.get_json()

logging.info(f"收到数据:{data}")

video_path = await get_video(data["type"], data, config)

if video_path:
static_video_path = os.path.join(static_folder, "videos")
logging.debug(f"视频文件移动到的路径:{static_video_path}")

filename = ""
move_file = data.get("move_file")
is_linux = common.detect_os() == "Linux"

if move_file:
if is_linux:
filename = extract_filename(video_path)
ret = common.move_and_rename(video_path, static_video_path, new_filename=filename, move_file=move_file)
else:
ret = common.move_and_rename(video_path, static_video_path, move_file=move_file)
filename = common.get_filename_with_ext(video_path)
else:
if is_linux:
filename = extract_filename(video_path)
ret = common.move_and_rename(video_path, static_video_path, new_filename=filename)
else:
ret = common.move_and_rename(video_path, static_video_path)
filename = common.get_filename_with_ext(video_path)
if ret == False:
return jsonify({"code": 200, "message": "视频移动失败"})

file_url = f"http://127.0.0.1:{config.get('server_port')}/static/videos/{filename}"

if "audio_path" not in data:
data["audio_path"] = None

if "insert_index" not in data:
data["insert_index"] = -1

await send_to_all_websockets(
json.dumps(
{
"type": "show",
"video_path": file_url,
"audio_path": data["audio_path"],
"insert_index": data["insert_index"]
}
)
)

return jsonify({"code": 200, "message": "操作成功"})

return jsonify({"code": 200, "message": "视频合成失败"})
except Exception as e:
logging.error(traceback.format_exc())
return jsonify({"code": -1, "message": f"操作失败: {str(e)}"})


# 停止当前播放的视频,跳转到下一个
@app.route('/stop_current_video', methods=['POST'])
async def stop_current_video():
try:
#data = await request.get_json()

#logging.info(f"收到数据:{data}")

await send_to_all_websockets(
json.dumps(
{
"type": "stop_current_video"
}
)
)

return jsonify({"code": 200, "message": "操作成功"})
except Exception as e:
logging.error(traceback.format_exc())
return jsonify({"code": -1, "message": f"操作失败: {str(e)}"})

# 获取非默认视频个数
@app.route('/get_non_default_video_count', methods=['POST'])
async def get_non_default_video_count():
try:
await send_to_all_websockets(
json.dumps(
{
"type": "get_non_default_video_count"
}
)
)
# 等待ws返回后对数据的更新
await asyncio.sleep(0.5)
return jsonify({"code": 200, "count": non_default_video_count, "message": "操作成功"})
except Exception as e:
logging.error(traceback.format_exc())
return jsonify({"code": -1, "message": f"操作失败: {str(e)}"})

async def main():
# 使用 Quart 提供的 run_task 方法来启动异步的 Web 应用
await app.run_task(host=config.get("server_ip"), port=config.get("server_port"))


class StoppableThread(threading.Thread):
def __init__(self, target=None, stop_event=None):
super().__init__()
self._stop_event = stop_event
self._target = target # 保存目标函数引用

def run(self):
if self._target: # 如果有目标函数,调用它
self._target(self._stop_event)
self._stop_event.wait() # 等待事件被设置
logging.info("Thread is stopping")

def stop(self):
self._stop_event.set()

if __name__ == '__main__':
def start_browser(stop_event):
options = webdriver.ChromeOptions()
# 设置为开发者模式,避免被浏览器识别为自动化程序
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_argument('--autoplay-policy=no-user-gesture-required')

# 使用`options`而不是`chrome_options`作为参数
driver = webdriver.Chrome(options=options)
driver.get(f'http://127.0.0.1:{config.get("server_port")}')

stop_event.wait() # 等待事件被设置

# 创建一个停止事件实例
stop_event = threading.Event()

# 创建一个可停止的线程实例
browser_thread = StoppableThread(target=start_browser, stop_event=stop_event)

# 启动线程
browser_thread.start()

asyncio.run(main())

# 在某个时刻停止线程
# browser_thread.stop()

os._exit(0)
2 changes: 1 addition & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"batch_process": "False"
},
"sadtalker": {
"gradio_api_type": "api_name",
"gradio_api_type": "fn_index",
"api_ip_port": "http://127.0.0.1:7860",
"img_file": "E:\\GitHub_pro\\digital_human_video_player\\static\\imgs\\2.png",
"preprocess": "crop",
Expand Down
7 changes: 4 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
quart
# quart
selenium
gradio_client==0.10.1
colorlog
gradio_client==0.16.3
colorlog
fastapi
Loading

0 comments on commit 2369f76

Please sign in to comment.