Files
autoclip/backend/api/v1/bilibili.py
T
Kris Ka 8ee2d50044 修复WebSocket进度更新和前端状态自动更新问题
- 修复WebSocketNotificationService.send_processing_progress方法参数不匹配问题
- 修复前端RealTimeStatus组件WebSocket消息处理逻辑
- 修复Celery Worker队列配置,确保任务正确路由到processing队列
- 修复Celery应用导入冲突,统一使用正确的celery_app配置
- 添加实时项目状态更新功能,前端无需手动刷新即可看到处理进度
- 完善系统启动脚本,修复PYTHONPATH未绑定变量错误
- 优化流水线处理逻辑,确保所有6个步骤正常执行
- 添加完整的项目文档和启动指南

测试结果:
- WebSocket进度更新正常工作(16%, 33%, 100%)
- 流水线处理完全正常(6个步骤全部成功)
- 前端状态自动更新正常
- 项目状态正确同步到数据库
2025-09-08 17:21:31 +08:00

338 lines
13 KiB
Python

"""
B站相关API路由
处理B站视频解析和下载功能
"""
import logging
from typing import Optional
from fastapi import APIRouter, HTTPException, Form, UploadFile, File
from pydantic import BaseModel
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent.parent))
from ...utils.bilibili_downloader import BilibiliDownloader, get_bilibili_video_info
from ...core.config import get_data_directory
from pathlib import Path
import uuid
import asyncio
from datetime import datetime
logger = logging.getLogger(__name__)
router = APIRouter()
# 存储下载任务的状态
download_tasks = {}
class BilibiliParseRequest(BaseModel):
url: str
browser: Optional[str] = None
class BilibiliDownloadRequest(BaseModel):
url: str
project_name: str
video_category: Optional[str] = "default"
browser: Optional[str] = None
class BilibiliVideoInfo(BaseModel):
title: str
description: str
duration: int
uploader: str
upload_date: str
view_count: int
like_count: int
thumbnail: str
class BilibiliDownloadTask(BaseModel):
id: str
url: str
project_name: str
video_category: str
status: str # pending, processing, completed, failed
progress: float
error_message: Optional[str] = None
project_id: Optional[str] = None
created_at: str
updated_at: str
@router.post("/parse")
async def parse_bilibili_video(
url: str = Form(...),
browser: Optional[str] = Form(None)
):
"""解析B站视频信息"""
try:
logger.info(f"开始解析B站视频: {url}")
# 验证URL格式
downloader = BilibiliDownloader(browser=browser)
if not downloader.validate_bilibili_url(url):
raise HTTPException(status_code=400, detail="无效的B站视频链接")
# 获取真实的视频信息
video_info = await downloader.get_video_info(url)
logger.info(f"视频信息解析成功: {video_info.title}")
return {
"success": True,
"video_info": {
"title": video_info.title,
"description": video_info.description,
"duration": video_info.duration,
"uploader": video_info.uploader,
"upload_date": video_info.upload_date,
"view_count": video_info.view_count,
"like_count": 0, # B站API可能不提供点赞数
"thumbnail": video_info.thumbnail_url
}
}
except Exception as e:
logger.error(f"解析B站视频失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"解析失败: {str(e)}")
@router.post("/download")
async def create_bilibili_download_task(request: BilibiliDownloadRequest):
"""创建B站视频下载任务"""
try:
logger.info(f"创建B站下载任务: {request.url}")
# 生成任务ID
task_id = str(uuid.uuid4())
# 创建任务记录
task = BilibiliDownloadTask(
id=task_id,
url=request.url,
project_name=request.project_name,
video_category=request.video_category,
status="pending",
progress=0.0,
created_at=str(uuid.uuid1().time),
updated_at=str(uuid.uuid1().time)
)
# 存储任务
download_tasks[task_id] = task
# 异步启动下载任务
asyncio.create_task(process_download_task(task_id, request))
return task
except Exception as e:
logger.error(f"创建下载任务失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"创建任务失败: {str(e)}")
@router.get("/tasks/{task_id}")
async def get_bilibili_task_status(task_id: str):
"""获取下载任务状态"""
if task_id not in download_tasks:
raise HTTPException(status_code=404, detail="任务不存在")
return download_tasks[task_id]
@router.get("/tasks")
async def get_all_bilibili_tasks():
"""获取所有下载任务"""
return list(download_tasks.values())
async def process_download_task(task_id: str, request: BilibiliDownloadRequest):
"""处理下载任务"""
try:
# 更新任务状态为处理中
download_tasks[task_id].status = "processing"
download_tasks[task_id].progress = 10.0
# 获取视频信息
video_info = await get_bilibili_video_info(request.url, request.browser)
download_tasks[task_id].progress = 30.0
# 下载视频
data_dir = get_data_directory()
download_dir = data_dir / "temp"
download_dir.mkdir(exist_ok=True)
from ...utils.bilibili_downloader import download_bilibili_video
download_result = await download_bilibili_video(
request.url,
download_dir,
request.browser
)
video_path = download_result.get('video_path', '')
subtitle_path = download_result.get('subtitle_path', '')
# 如果没有字幕文件,优先使用Whisper生成字幕
if not subtitle_path and video_path:
logger.info("优先使用Whisper生成高质量字幕")
try:
from ...utils.speech_recognizer import generate_subtitle_for_video, SpeechRecognitionError
from pathlib import Path
video_file_path = Path(video_path)
# 根据视频信息选择合适的模型
model = "base" # 默认使用平衡模型
language = "auto" # 默认自动检测语言
# 可以根据视频标题或描述判断内容类型
if video_info.title and any(keyword in video_info.title.lower() for keyword in ['教程', '教学', '知识', '科普']):
model = "small" # 知识类内容使用更准确的模型
language = "zh"
elif video_info.title and any(keyword in video_info.title.lower() for keyword in ['演讲', '讲座', '分享']):
model = "medium" # 演讲内容使用高精度模型
language = "zh"
logger.info(f"使用Whisper生成字幕 - 语言: {language}, 模型: {model}")
generated_subtitle = generate_subtitle_for_video(
video_file_path,
language=language,
model=model
)
subtitle_path = str(generated_subtitle)
logger.info(f"Whisper字幕生成成功: {subtitle_path}")
except SpeechRecognitionError as e:
logger.error(f"Whisper字幕生成失败: {e}")
# Whisper失败时,可以考虑尝试下载平台字幕作为备用
logger.info("尝试下载平台字幕作为备用方案")
try:
# 这里可以添加下载平台字幕的逻辑
pass
except Exception as backup_error:
logger.error(f"备用字幕获取也失败: {backup_error}")
except Exception as e:
logger.error(f"生成字幕过程中发生未知错误: {e}")
download_tasks[task_id].progress = 80.0
# 创建项目
from ...services.project_service import ProjectService
from ...core.database import SessionLocal
db = SessionLocal()
try:
project_service = ProjectService(db)
# 创建项目
project_data = {
"name": request.project_name,
"description": f"从B站下载: {video_info.title}",
"project_type": request.video_category,
"source_url": request.url,
"source_file": video_path,
"settings": {
"bilibili_info": {
"title": video_info.title,
"uploader": video_info.uploader,
"duration": video_info.duration,
"view_count": video_info.view_count
},
"subtitle_path": subtitle_path if subtitle_path else None
}
}
# 创建项目数据
from ...schemas.project import ProjectCreate
project_create_data = ProjectCreate(
name=project_data["name"],
description=project_data["description"],
project_type=project_data["project_type"],
source_url=project_data["source_url"],
video_path=project_data["source_file"], # 映射到video_path
settings=project_data["settings"]
)
project = project_service.create_project(project_create_data)
# 创建项目目录并移动文件
project_root = data_dir.parent / "data" / "projects" / str(project.id)
raw_dir = project_root / "raw"
raw_dir.mkdir(parents=True, exist_ok=True)
# 移动视频文件到项目目录
import shutil
from pathlib import Path
if video_path:
video_file_path = Path(video_path)
if video_file_path.exists():
# 重命名视频文件为input.mp4
new_video_path = raw_dir / "input.mp4"
shutil.move(str(video_file_path), str(new_video_path))
logger.info(f"视频文件已移动到: {new_video_path}")
# 更新项目中的视频路径
project.video_path = str(new_video_path)
# 移动字幕文件到项目目录
if subtitle_path and subtitle_path.strip():
subtitle_file_path = Path(subtitle_path)
if subtitle_file_path.exists():
# 重命名字幕文件为input.srt
new_subtitle_path = raw_dir / "input.srt"
shutil.move(str(subtitle_file_path), str(new_subtitle_path))
logger.info(f"字幕文件已移动到: {new_subtitle_path}")
# 更新项目处理配置中的字幕路径
if not project.processing_config:
project.processing_config = {}
project.processing_config["subtitle_path"] = str(new_subtitle_path)
# 保存项目更新
db.commit()
# 更新任务状态
download_tasks[task_id].status = "completed"
download_tasks[task_id].progress = 100.0
download_tasks[task_id].project_id = str(project.id)
download_tasks[task_id].updated_at = datetime.now().isoformat()
logger.info(f"B站下载任务完成: {task_id}, 项目ID: {project.id}")
# 自动启动处理流程
try:
# 更新项目状态为等待处理
from ...schemas.project import ProjectStatus
project.status = ProjectStatus.PENDING # 改为PENDING,让自动化服务启动
db.commit()
logger.info(f"B站项目 {project.id} 下载完成,等待自动化流水线启动")
# 异步启动自动化流水线
import asyncio
from ...services.auto_pipeline_service import auto_pipeline_service
# 创建事件循环来运行异步函数
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 启动自动化流水线
pipeline_result = loop.run_until_complete(
auto_pipeline_service.auto_start_pipeline(str(project.id))
)
if pipeline_result['status'] == 'started':
logger.info(f"B站项目 {project.id} 自动化流水线已启动: {pipeline_result}")
else:
logger.warning(f"B站项目 {project.id} 自动化流水线启动结果: {pipeline_result}")
except Exception as e:
logger.error(f"启动B站项目 {project.id} 自动化流水线失败: {str(e)}")
# 即使处理启动失败,也要返回下载成功
# 用户可以通过重试按钮重新启动处理
finally:
db.close()
except Exception as e:
logger.error(f"处理下载任务失败: {str(e)}")
download_tasks[task_id].status = "failed"
download_tasks[task_id].error_message = str(e)
download_tasks[task_id].progress = 0.0