diff --git a/BillNote_frontend/src/App.tsx b/BillNote_frontend/src/App.tsx index 4ee48b5..aa2ca95 100644 --- a/BillNote_frontend/src/App.tsx +++ b/BillNote_frontend/src/App.tsx @@ -2,10 +2,10 @@ import './App.css' import { HomePage } from './pages/HomePage/Home.tsx' import { useTaskPolling } from '@/hooks/useTaskPolling.ts' import SettingPage from './pages/SettingPage/index.tsx' -import { BrowserRouter,HashRouter, Navigate, Routes } from 'react-router-dom' +import { BrowserRouter, HashRouter, Navigate, Routes } from 'react-router-dom' import { Route } from 'react-router-dom' import Index from '@/pages/Index.tsx' -import NotFoundPage from '@/pages/NotFoundPage' // +import NotFoundPage from '@/pages/NotFoundPage' import Model from '@/pages/SettingPage/Model.tsx' import Transcriber from '@/pages/SettingPage/transcriber.tsx' import ProviderForm from '@/components/Form/modelForm/Form.tsx' @@ -15,15 +15,32 @@ import Prompt from '@/pages/SettingPage/Prompt.tsx' import AboutPage from '@/pages/SettingPage/about.tsx' import Downloader from '@/pages/SettingPage/Downloader.tsx' import DownloaderForm from '@/components/Form/DownloaderForm/Form.tsx' +import { useEffect } from 'react' +import { systemCheck } from '@/services/system.ts' +import { useCheckBackend } from '@/hooks/useCheckBackend.ts' +import BackendInitDialog from '@/components/BackendInitDialog' + function App() { useTaskPolling(3000) // 每 3 秒轮询一次 - const steps = [ - { label: '解析链接', key: 'PARSING', icon: }, - { label: '下载音频', key: 'DOWNLOADING' }, - { label: '转写文字', key: 'TRANSCRIBING' }, - { label: '总结内容', key: 'SUMMARIZING' }, - { label: '保存完成', key: 'SUCCESS' }, - ] + const { loading, initialized } = useCheckBackend() + + // 在后端初始化完成后执行系统检查 + useEffect(() => { + if (initialized) { + systemCheck() + } + }, [initialized]) + + // 如果后端还未初始化,显示初始化对话框 + if (!initialized) { + return ( + <> + + + ) + } + + // 后端已初始化,渲染主应用 return ( <> @@ -34,16 +51,12 @@ function App() { } /> }> } /> - {/*} />*/} } /> - {/*}>*/} - {/*}>*/} }> } /> }> - } /> } /> @@ -54,4 +67,4 @@ function App() { ) } -export default App +export default App \ No newline at end of file diff --git a/BillNote_frontend/src/components/BackendInitDialog.tsx b/BillNote_frontend/src/components/BackendInitDialog.tsx new file mode 100644 index 0000000..09bd60a --- /dev/null +++ b/BillNote_frontend/src/components/BackendInitDialog.tsx @@ -0,0 +1,23 @@ +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Loader2 } from 'lucide-react' + +interface Props { + open: boolean +} + + function BackendInitDialog({ open }: Props) { + return ( + + + + + + 后端正在初始化中… + + +

请稍候,系统正在启动后端服务

+
+
+ ) +} +export default BackendInitDialog \ No newline at end of file diff --git a/BillNote_frontend/src/components/ui/dialog.tsx b/BillNote_frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..6cb123b --- /dev/null +++ b/BillNote_frontend/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/BillNote_frontend/src/hooks/useCheckBackend.ts b/BillNote_frontend/src/hooks/useCheckBackend.ts new file mode 100644 index 0000000..52f0c7d --- /dev/null +++ b/BillNote_frontend/src/hooks/useCheckBackend.ts @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react' +import request from '@/utils/request' + +const MAX_RETRIES = 3 +const RETRY_INTERVAL = 10000 // 10秒 + +export const useCheckBackend = () => { + const [loading, setLoading] = useState(false) + const [initialized, setInitialized] = useState(false) + + useEffect(() => { + let retries = 0 + + const check = async () => { + try { + await request.get('/sys_health') + setInitialized(true) + setLoading(false) + } catch { + if (retries === 0) { + // 第一次失败时开始显示加载状态 + setLoading(true) + } + + if (retries < MAX_RETRIES) { + retries++ + setTimeout(check, RETRY_INTERVAL) + } else { + // 达到重试上限,继续轮询直到后端就绪 + waitUntilBackendReady() + } + } + } + + const waitUntilBackendReady = async () => { + while (true) { + try { + await request.get('/sys_health') + setInitialized(true) + setLoading(false) + break + } catch { + await new Promise(res => setTimeout(res, RETRY_INTERVAL)) + } + } + } + + check() + }, []) + + return { loading, initialized } +} \ No newline at end of file diff --git a/BillNote_frontend/src/services/system.ts b/BillNote_frontend/src/services/system.ts new file mode 100644 index 0000000..99a6b7c --- /dev/null +++ b/BillNote_frontend/src/services/system.ts @@ -0,0 +1,5 @@ +import request from '@/utils/request' + +export const systemCheck=async()=>{ + return await request.get('/sys_health') +} diff --git a/backend/app/gpt/prompt.py b/backend/app/gpt/prompt.py index 0e11c6a..1f6011d 100644 --- a/backend/app/gpt/prompt.py +++ b/backend/app/gpt/prompt.py @@ -22,8 +22,8 @@ BASE_PROMPT = ''' - 或者使用 `## 1. 内容` 的形式作为标题。 请确保以下格式 **不会出现误渲染**: -❌ `1. **xxx**` -✅ `1\. **xxx**` 或 `## 1. xxx` + `1. **xxx**` + `1\. **xxx**` 或 `## 1. xxx` 视频分段(格式:开始时间 - 内容): diff --git a/backend/app/gpt/universal_gpt.py b/backend/app/gpt/universal_gpt.py index 49ced8a..568dbb1 100644 --- a/backend/app/gpt/universal_gpt.py +++ b/backend/app/gpt/universal_gpt.py @@ -52,7 +52,7 @@ class UniversalGPT(GPT): } }) - # ✅ 正确格式:整体包在一个 message 里,role + content array + # 正确格式:整体包在一个 message 里,role + content array messages = [{ "role": "user", "content": content diff --git a/backend/app/models/audio_model.py b/backend/app/models/audio_model.py index fb35ef1..d2808b5 100644 --- a/backend/app/models/audio_model.py +++ b/backend/app/models/audio_model.py @@ -11,5 +11,5 @@ class AudioDownloadResult: platform: str # 平台,如 "bilibili" video_id: str # 唯一视频ID raw_info: dict # yt-dlp 的原始 info 字典 - video_path: Optional[str] = None # ✅ 新增字段:可选视频文件路径 + video_path: Optional[str] = None # 新增字段:可选视频文件路径 diff --git a/backend/app/routers/config.py b/backend/app/routers/config.py index f60eaec..8a43df0 100644 --- a/backend/app/routers/config.py +++ b/backend/app/routers/config.py @@ -4,6 +4,7 @@ from typing import Optional from app.utils.response import ResponseWrapper as R from app.services.cookie_manager import CookieConfigManager +from ffmpeg_helper import ensure_ffmpeg_or_raise router = APIRouter() cookie_manager = CookieConfigManager() @@ -30,3 +31,11 @@ def update_cookie(data: CookieUpdateRequest): return R.success( ) + +@router.get("/sys_health") +async def sys_health(): + try: + ensure_ffmpeg_or_raise() + return R.success() + except EnvironmentError: + return R.error(msg="系统未安装 ffmpeg 请先进行安装") diff --git a/backend/app/routers/note.py b/backend/app/routers/note.py index c80d9e0..42bb992 100644 --- a/backend/app/routers/note.py +++ b/backend/app/routers/note.py @@ -238,7 +238,7 @@ async def image_proxy(request: Request, url: str): resp.aiter_bytes(), media_type=content_type, headers={ - "Cache-Control": "public, max-age=86400", # ✅ 缓存一天 + "Cache-Control": "public, max-age=86400", # 缓存一天 "Content-Type": content_type, } ) diff --git a/backend/app/routers/provider.py b/backend/app/routers/provider.py index 5ded464..942a362 100644 --- a/backend/app/routers/provider.py +++ b/backend/app/routers/provider.py @@ -10,7 +10,7 @@ from app.services.provider import ProviderService router = APIRouter() -# ✅ 新增 type 字段 +# 新增 type 字段 class ProviderRequest(BaseModel): name: str api_key: str diff --git a/backend/app/transcriber/whisper.py b/backend/app/transcriber/whisper.py index 7b438f4..4fb4b50 100644 --- a/backend/app/transcriber/whisper.py +++ b/backend/app/transcriber/whisper.py @@ -79,13 +79,13 @@ class WhisperTranscriber(Transcriber): def is_cuda() -> bool: try: if is_cuda_available(): - print("✅ CUDA 可用,使用 GPU") + print(" CUDA 可用,使用 GPU") return True elif is_torch_installed(): print("⚠️ 只装了 torch,但没有 CUDA,用 CPU") return False else: - print("❌ 还没有安装 torch,请先安装") + print(" 还没有安装 torch,请先安装") return False except ImportError: diff --git a/backend/ffmpeg_helper.py b/backend/ffmpeg_helper.py index e2f3608..b116aee 100644 --- a/backend/ffmpeg_helper.py +++ b/backend/ffmpeg_helper.py @@ -31,7 +31,7 @@ def ensure_ffmpeg_or_raise(): if not check_ffmpeg_exists(): logger.error("未检测到 ffmpeg,请先安装后再使用本功能。") raise EnvironmentError( - "❌ 未检测到 ffmpeg,请先安装后再使用本功能。\n" + " 未检测到 ffmpeg,请先安装后再使用本功能。\n" "👉 下载地址:https://ffmpeg.org/download.html\n" "🪟 Windows 推荐:https://www.gyan.dev/ffmpeg/builds/\n" "💡 如果你已安装,请将其路径写入 `.env` 文件,例如:\n" diff --git a/backend/main.py b/backend/main.py index 981df38..fc15226 100644 --- a/backend/main.py +++ b/backend/main.py @@ -39,7 +39,6 @@ if not os.path.exists(out_dir): @asynccontextmanager async def lifespan(app: FastAPI): register_handler() - ensure_ffmpeg_or_raise() init_db() get_transcriber(transcriber_type=os.getenv("TRANSCRIBER_TYPE", "fast-whisper")) seed_default_providers() @@ -48,7 +47,7 @@ async def lifespan(app: FastAPI): app = create_app(lifespan=lifespan) app.add_middleware( CORSMiddleware, - allow_origins=["tauri://localhost"], # ✅ 加上 Tauri 的 origin + allow_origins=["tauri://localhost"], # 加上 Tauri 的 origin allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/doc/wechat.png b/doc/wechat.png index 4e2e4a9..f88ccf1 100644 Binary files a/doc/wechat.png and b/doc/wechat.png differ