Merge pull request #101 from JefferyHcool/feature/kuaishou

feat(backend): 添加 Groq供应商支持并优化笔记生成流程- 在 builtin_providers.json 中添加 Groq 供应商信息
- 实现 GroqTranscriber 类以支持 Groq 语音转录服务
- 新增异常处理中间件以提高系统稳定性
- 优化笔记生成流程,增加错误处理和日志记录
- 添加思维导图功能和相关组件
-重构 Markdown 查看器以支持切换视图模式
This commit is contained in:
Jianwu Huang
2025-05-12 15:39:27 +08:00
committed by GitHub
17 changed files with 744 additions and 353 deletions

View File

@@ -35,6 +35,10 @@
"lottie-react": "^2.4.1", "lottie-react": "^2.4.1",
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"markdown-navbar": "^1.4.3", "markdown-navbar": "^1.4.3",
"markmap-common": "^0.18.9",
"markmap-lib": "^0.18.11",
"markmap-toolbar": "^0.18.10",
"markmap-view": "^0.18.10",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pinyin-match": "^1.2.7", "pinyin-match": "^1.2.7",
"react": "^19.0.0", "react": "^19.0.0",

View File

@@ -0,0 +1,8 @@
import { loadCSS, loadJS } from 'markmap-common'
import { Transformer } from 'markmap-lib'
import * as markmap from 'markmap-view'
export const transformer = new Transformer()
const { scripts, styles } = transformer.getAssets()
loadCSS(styles)
loadJS(scripts, { getMarkmap: () => markmap })

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Copy, Download } from 'lucide-react' import { Copy, Download, BrainCircuit } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
@@ -43,6 +43,8 @@ export function MarkdownHeader({
createAt, createAt,
showTranscribe, showTranscribe,
setShowTranscribe, setShowTranscribe,
viewMode,
setViewMode,
}: NoteHeaderProps) { }: NoteHeaderProps) {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@@ -122,6 +124,24 @@ export function MarkdownHeader({
{/* 右侧操作按钮 */} {/* 右侧操作按钮 */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => {
setViewMode(viewMode == 'preview' ? 'map' : 'preview')
}}
variant="ghost"
size="sm"
className="h-8 px-2"
>
<BrainCircuit className="mr-1.5 h-4 w-4" />
<span className="text-sm">{viewMode == 'preview' ? '思维导图' : 'markdown'}</span>
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import { Button } from '@/components/ui/button.tsx' import { Button } from '@/components/ui/button.tsx'
import { Copy, Download, ArrowRight, Play, ExternalLink } from 'lucide-react' import { Copy, Download, ArrowRight, Play, ExternalLink } from 'lucide-react'
@@ -22,6 +22,7 @@ import { useTaskStore } from '@/store/taskStore'
import { noteStyles } from '@/constant/note.ts' import { noteStyles } from '@/constant/note.ts'
import { MarkdownHeader } from '@/pages/HomePage/components/MarkdownHeader.tsx' import { MarkdownHeader } from '@/pages/HomePage/components/MarkdownHeader.tsx'
import TranscriptViewer from '@/pages/HomePage/components/transcriptViewer.tsx' import TranscriptViewer from '@/pages/HomePage/components/transcriptViewer.tsx'
import MarkmapEditor from '@/pages/HomePage/components/MarkmapComponent.tsx'
interface VersionNote { interface VersionNote {
ver_id: string ver_id: string
@@ -58,6 +59,8 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ status }) => {
const retryTask = useTaskStore.getState().retryTask const retryTask = useTaskStore.getState().retryTask
const isMultiVersion = Array.isArray(currentTask?.markdown) const isMultiVersion = Array.isArray(currentTask?.markdown)
const [showTranscribe, setShowTranscribe] = useState(false) const [showTranscribe, setShowTranscribe] = useState(false)
const [viewMode, setViewMode] = useState<'map' | 'preview'>('preview')
const svgRef = useRef<SVGSVGElement>(null)
// 多版本内容处理 // 多版本内容处理
useEffect(() => { useEffect(() => {
if (!currentTask) return if (!currentTask) return
@@ -99,7 +102,33 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ status }) => {
toast.error('复制失败') toast.error('复制失败')
} }
} }
const alertButton = {
id: 'alert',
title: '测试警告',
content: '⚠️',
onClick: () => alert('你点击了自定义按钮!'),
}
const exportButton = {
id: 'export',
title: '导出思维导图',
content: '⤓',
onClick: () => {
const svgEl = svgRef.current
if (!svgEl) return
// 同上面的序列化逻辑
const serializer = new XMLSerializer()
const source = serializer.serializeToString(svgEl)
const blob = new Blob(['<?xml version="1.0" encoding="UTF-8"?>', source], {
type: 'image/svg+xml;charset=utf-8',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'mindmap.svg'
a.click()
URL.revokeObjectURL(url)
},
}
const handleDownload = () => { const handleDownload = () => {
const task = getCurrentTask() const task = getCurrentTask()
const name = task?.audioMeta.title || 'note' const name = task?.audioMeta.title || 'note'
@@ -167,270 +196,285 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ status }) => {
createAt={createTime} createAt={createTime}
showTranscribe={showTranscribe} showTranscribe={showTranscribe}
setShowTranscribe={setShowTranscribe} setShowTranscribe={setShowTranscribe}
viewMode={viewMode}
setViewMode={setViewMode}
/> />
{/* 中间内容区域:滚动容器 */} {viewMode === 'map' ? (
<div className="flex flex-1 overflow-hidden bg-white py-2"> <div className="flex w-full flex-1 overflow-hidden bg-white">
{selectedContent && selectedContent !== 'loading' && selectedContent !== 'empty' ? ( <div className={'w-full'}>
<> <MarkmapEditor
<ScrollArea className="w-full"> value={selectedContent}
<div className={'markdown-body w-full px-2'}> onChange={() => {}}
<ReactMarkdown height="100%" // 根据需求可以设定百分比或固定高度
remarkPlugins={[gfm, remarkMath]} />
rehypePlugins={[rehypeKatex]}
components={{
// Headings with improved styling and anchor links
h1: ({ children, ...props }) => (
<h1
className="text-primary my-6 scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl"
{...props}
>
{children}
</h1>
),
h2: ({ children, ...props }) => (
<h2
className="text-primary mt-10 mb-4 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight first:mt-0"
{...props}
>
{children}
</h2>
),
h3: ({ children, ...props }) => (
<h3
className="text-primary mt-8 mb-4 scroll-m-20 text-xl font-semibold tracking-tight"
{...props}
>
{children}
</h3>
),
h4: ({ children, ...props }) => (
<h4
className="text-primary mt-6 mb-2 scroll-m-20 text-lg font-semibold tracking-tight"
{...props}
>
{children}
</h4>
),
// Paragraphs with better line height
p: ({ children, ...props }) => (
<p className="leading-7 [&:not(:first-child)]:mt-6" {...props}>
{children}
</p>
),
// Enhanced links with special handling for "原片" links
a: ({ href, children, ...props }) => {
const isOriginLink =
typeof children[0] === 'string' &&
(children[0] as string).startsWith('原片 @')
if (isOriginLink) {
const timeMatch = (children[0] as string).match(/原片 @ (\d{2}:\d{2})/)
const timeText = timeMatch ? timeMatch[1] : '原片'
return (
<span className="origin-link my-2 inline-flex">
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
{...props}
>
<Play className="h-3.5 w-3.5" />
<span>{timeText}</span>
</a>
</span>
)
}
// Default link styling with external indicator
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/80 inline-flex items-center gap-0.5 font-medium underline underline-offset-4"
{...props}
>
{children}
{href?.startsWith('http') && (
<ExternalLink className="ml-0.5 inline-block h-3 w-3" />
)}
</a>
)
},
// Enhanced image with zoom capability
img: ({ node, ...props }) => (
<div className="my-8 flex justify-center">
<Zoom>
<img
{...props}
className="max-w-full cursor-zoom-in rounded-lg object-cover shadow-md transition-all hover:shadow-lg"
style={{ maxHeight: '500px' }}
/>
</Zoom>
</div>
),
// Better strong/bold text
strong: ({ children, ...props }) => (
<strong className="text-primary font-bold" {...props}>
{children}
</strong>
),
// Enhanced list items with support for "fake headings"
li: ({ children, ...props }) => {
const rawText = String(children)
const isFakeHeading = /^(\*\*.+\*\*)$/.test(rawText.trim())
if (isFakeHeading) {
return <div className="text-primary my-4 text-lg font-bold">{children}</div>
}
return (
<li className="my-1" {...props}>
{children}
</li>
)
},
// Enhanced unordered lists
ul: ({ children, ...props }) => (
<ul className="my-6 ml-6 list-disc [&>li]:mt-2" {...props}>
{children}
</ul>
),
// Enhanced ordered lists
ol: ({ children, ...props }) => (
<ol className="my-6 ml-6 list-decimal [&>li]:mt-2" {...props}>
{children}
</ol>
),
// Enhanced blockquotes
blockquote: ({ children, ...props }) => (
<blockquote
className="border-primary/20 text-muted-foreground mt-6 border-l-4 pl-4 italic"
{...props}
>
{children}
</blockquote>
),
// Enhanced code blocks with syntax highlighting and copy button
code: ({ inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '')
const codeContent = String(children).replace(/\n$/, '')
if (!inline && match) {
return (
<div className="group bg-muted relative my-6 overflow-hidden rounded-lg border shadow-sm">
<div className="bg-muted text-muted-foreground flex items-center justify-between px-4 py-1.5 text-sm font-medium">
<div>{match[1].toUpperCase()}</div>
<button
onClick={() => {
navigator.clipboard.writeText(codeContent)
toast.success('代码已复制')
}}
className="bg-background/80 hover:bg-background flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors"
>
<Copy className="h-3.5 w-3.5" />
</button>
</div>
<SyntaxHighlighter
style={codeStyle}
language={match[1]}
PreTag="div"
className="!bg-muted !m-0 !p-0"
customStyle={{
margin: 0,
padding: '1rem',
background: 'transparent',
fontSize: '0.9rem',
}}
{...props}
>
{codeContent}
</SyntaxHighlighter>
</div>
)
}
// Inline code styling
return (
<code
className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm"
{...props}
>
{children}
</code>
)
},
// Enhanced tables
table: ({ children, ...props }) => (
<div className="my-6 w-full overflow-y-auto">
<table className="w-full border-collapse text-sm" {...props}>
{children}
</table>
</div>
),
// Table headers
th: ({ children, ...props }) => (
<th
className="border-muted-foreground/20 border px-4 py-2 text-left font-medium [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
>
{children}
</th>
),
// Table cells
td: ({ children, ...props }) => (
<td
className="border-muted-foreground/20 border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
>
{children}
</td>
),
// Horizontal rule
hr: ({ ...props }) => (
<hr className="border-muted-foreground/20 my-8" {...props} />
),
}}
>
{selectedContent}
</ReactMarkdown>
</div>
</ScrollArea>
{showTranscribe && (
<div className={'ml-2 w-2/4'}>
<TranscriptViewer />
</div>
)}
</>
) : (
<div className="flex h-full w-full items-center justify-center">
<div className="w-[300px] flex-col justify-items-center">
<div className="bg-primary-light mb-4 flex h-16 w-16 items-center justify-center rounded-full">
<ArrowRight className="text-primary h-8 w-8" />
</div>
<p className="mb-2 text-neutral-600">"生成笔记"</p>
<p className="text-xs text-neutral-500">YouTube等视频网站</p>
</div>
</div> </div>
)} </div>
</div> ) : (
<div className="flex flex-1 overflow-hidden bg-white py-2">
{selectedContent && selectedContent !== 'loading' && selectedContent !== 'empty' ? (
<>
<ScrollArea className="w-full">
<div className={'markdown-body w-full px-2'}>
<ReactMarkdown
remarkPlugins={[gfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
// Headings with improved styling and anchor links
h1: ({ children, ...props }) => (
<h1
className="text-primary my-6 scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl"
{...props}
>
{children}
</h1>
),
h2: ({ children, ...props }) => (
<h2
className="text-primary mt-10 mb-4 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight first:mt-0"
{...props}
>
{children}
</h2>
),
h3: ({ children, ...props }) => (
<h3
className="text-primary mt-8 mb-4 scroll-m-20 text-xl font-semibold tracking-tight"
{...props}
>
{children}
</h3>
),
h4: ({ children, ...props }) => (
<h4
className="text-primary mt-6 mb-2 scroll-m-20 text-lg font-semibold tracking-tight"
{...props}
>
{children}
</h4>
),
// Paragraphs with better line height
p: ({ children, ...props }) => (
<p className="leading-7 [&:not(:first-child)]:mt-6" {...props}>
{children}
</p>
),
// Enhanced links with special handling for "原片" links
a: ({ href, children, ...props }) => {
const isOriginLink =
typeof children[0] === 'string' &&
(children[0] as string).startsWith('原片 @')
if (isOriginLink) {
const timeMatch = (children[0] as string).match(/原片 @ (\d{2}:\d{2})/)
const timeText = timeMatch ? timeMatch[1] : '原片'
return (
<span className="origin-link my-2 inline-flex">
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
{...props}
>
<Play className="h-3.5 w-3.5" />
<span>{timeText}</span>
</a>
</span>
)
}
// Default link styling with external indicator
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/80 inline-flex items-center gap-0.5 font-medium underline underline-offset-4"
{...props}
>
{children}
{href?.startsWith('http') && (
<ExternalLink className="ml-0.5 inline-block h-3 w-3" />
)}
</a>
)
},
// Enhanced image with zoom capability
img: ({ node, ...props }) => (
<div className="my-8 flex justify-center">
<Zoom>
<img
{...props}
className="max-w-full cursor-zoom-in rounded-lg object-cover shadow-md transition-all hover:shadow-lg"
style={{ maxHeight: '500px' }}
/>
</Zoom>
</div>
),
// Better strong/bold text
strong: ({ children, ...props }) => (
<strong className="text-primary font-bold" {...props}>
{children}
</strong>
),
// Enhanced list items with support for "fake headings"
li: ({ children, ...props }) => {
const rawText = String(children)
const isFakeHeading = /^(\*\*.+\*\*)$/.test(rawText.trim())
if (isFakeHeading) {
return (
<div className="text-primary my-4 text-lg font-bold">{children}</div>
)
}
return (
<li className="my-1" {...props}>
{children}
</li>
)
},
// Enhanced unordered lists
ul: ({ children, ...props }) => (
<ul className="my-6 ml-6 list-disc [&>li]:mt-2" {...props}>
{children}
</ul>
),
// Enhanced ordered lists
ol: ({ children, ...props }) => (
<ol className="my-6 ml-6 list-decimal [&>li]:mt-2" {...props}>
{children}
</ol>
),
// Enhanced blockquotes
blockquote: ({ children, ...props }) => (
<blockquote
className="border-primary/20 text-muted-foreground mt-6 border-l-4 pl-4 italic"
{...props}
>
{children}
</blockquote>
),
// Enhanced code blocks with syntax highlighting and copy button
code: ({ inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '')
const codeContent = String(children).replace(/\n$/, '')
if (!inline && match) {
return (
<div className="group bg-muted relative my-6 overflow-hidden rounded-lg border shadow-sm">
<div className="bg-muted text-muted-foreground flex items-center justify-between px-4 py-1.5 text-sm font-medium">
<div>{match[1].toUpperCase()}</div>
<button
onClick={() => {
navigator.clipboard.writeText(codeContent)
toast.success('代码已复制')
}}
className="bg-background/80 hover:bg-background flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors"
>
<Copy className="h-3.5 w-3.5" />
</button>
</div>
<SyntaxHighlighter
style={codeStyle}
language={match[1]}
PreTag="div"
className="!bg-muted !m-0 !p-0"
customStyle={{
margin: 0,
padding: '1rem',
background: 'transparent',
fontSize: '0.9rem',
}}
{...props}
>
{codeContent}
</SyntaxHighlighter>
</div>
)
}
// Inline code styling
return (
<code
className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm"
{...props}
>
{children}
</code>
)
},
// Enhanced tables
table: ({ children, ...props }) => (
<div className="my-6 w-full overflow-y-auto">
<table className="w-full border-collapse text-sm" {...props}>
{children}
</table>
</div>
),
// Table headers
th: ({ children, ...props }) => (
<th
className="border-muted-foreground/20 border px-4 py-2 text-left font-medium [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
>
{children}
</th>
),
// Table cells
td: ({ children, ...props }) => (
<td
className="border-muted-foreground/20 border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
>
{children}
</td>
),
// Horizontal rule
hr: ({ ...props }) => (
<hr className="border-muted-foreground/20 my-8" {...props} />
),
}}
>
{selectedContent}
</ReactMarkdown>
</div>
</ScrollArea>
{showTranscribe && (
<div className={'ml-2 w-2/4'}>
<TranscriptViewer />
</div>
)}
</>
) : (
<div className="flex h-full w-full items-center justify-center">
<div className="w-[300px] flex-col justify-items-center">
<div className="bg-primary-light mb-4 flex h-16 w-16 items-center justify-center rounded-full">
<ArrowRight className="text-primary h-8 w-8" />
</div>
<p className="mb-2 text-neutral-600">"生成笔记"</p>
<p className="text-xs text-neutral-500">YouTube等视频网站</p>
</div>
</div>
)}
</div>
)}
</div> </div>
) )
} }

View File

@@ -0,0 +1,118 @@
import React, { useEffect, useRef, useState } from 'react'
import { Markmap } from 'markmap-view'
import { transformer } from '@/lib/markmap.ts'
import { Toolbar, ToolbarButton } from 'markmap-toolbar'
import 'markmap-toolbar/dist/style.css'
export interface MarkmapEditorProps {
/** 要渲染的 Markdown 文本 */
value: string
/** 内容变化时的回调 */
onChange: (value: string) => void
/** Toolbar 上要展示的 item id 列表,默认使用 Toolbar.defaultItems */
toolbarItems?: string[]
/** 自定义按钮列表,会依次注册 */
customButtons?: ToolbarButton[]
/** 容器 SVG 的高度,默认为 600px */
height?: string
}
export default function MarkmapEditor({
value,
onChange,
toolbarItems,
customButtons = [],
height = '600px',
}: MarkmapEditorProps) {
const svgRef = useRef<SVGSVGElement>(null)
const mmRef = useRef<Markmap>()
const toolbarRef = useRef<HTMLDivElement>(null)
// 用于跟踪是否处于全屏状态
const [isFullscreen, setIsFullscreen] = useState(false)
// 监听全屏状态变化
useEffect(() => {
const handler = () => {
setIsFullscreen(!!document.fullscreenElement)
}
document.addEventListener('fullscreenchange', handler)
return () => {
document.removeEventListener('fullscreenchange', handler)
}
}, [])
// 进入全屏
const enterFullscreen = () => {
const el = svgRef.current?.parentElement
if (el && el.requestFullscreen) {
el.requestFullscreen()
}
}
// 退出全屏
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen()
}
}
// 初始化 Markmap 实例 + Toolbar
useEffect(() => {
if (!svgRef.current || mmRef.current) return
const mm = Markmap.create(svgRef.current)
mmRef.current = mm
if (toolbarRef.current) {
toolbarRef.current.innerHTML = ''
const toolbar = new Toolbar()
toolbar.attach(mm)
customButtons.forEach(btn => toolbar.register(btn))
toolbar.setItems(toolbarItems ?? Toolbar.defaultItems)
toolbarRef.current.appendChild(toolbar.render())
}
}, [customButtons, toolbarItems])
// 当 value 变化时,重新渲染数据
useEffect(() => {
const mm = mmRef.current
if (!mm) return
const { root } = transformer.transform(value)
mm.setData(root).then(() => mm.fit())
}, [value])
// 文本输入变化回调(如果你自行添加 textarea 编辑区)
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(e.target.value)
}
return (
<div className="relative flex h-full flex-col bg-white">
{/* 全屏/退出全屏 按钮 */}
<div className="absolute top-2 right-2 z-20 flex space-x-2">
{isFullscreen ? (
<button
onClick={exitFullscreen}
className="rounded p-1 hover:bg-gray-200"
title="退出全屏"
>
🗗
</button>
) : (
<button onClick={enterFullscreen} className="rounded p-1 hover:bg-gray-200" title="全屏">
🗖
</button>
)}
</div>
{/* 如果需要编辑区,就自己加一个 <textarea> 并把 handleChange 绑上 */}
{/* <textarea value={value} onChange={handleChange} className="mb-2 p-2 border rounded" /> */}
{/* 思维导图区 */}
<svg ref={svgRef} className="w-full flex-1" style={{ height, overflow: 'auto' }} />
{/* 如果你还想保留 markmap-toolbar */}
{/* <div ref={toolbarRef} className="absolute right-2 bottom-2 z-10" /> */}
</div>
)
}

View File

@@ -150,14 +150,33 @@ const NoteForm = () => {
return return
}, []) }, [])
useEffect(() => { useEffect(() => {
const currentTask = getCurrentTask()
const { formData } = currentTask || {}
if (!currentTask) return if (!currentTask) return
const { formData } = currentTask
console.log('currentTask.formData.platform:', formData.platform)
form.reset({ form.reset({
...formData, platform: formData.platform || 'bilibili',
extras: formData?.extras || '', video_url: formData.video_url || '',
model_name: formData.model_name || modelList[0]?.model_name || '',
style: formData.style || 'minimal',
quality: formData.quality || 'medium',
extras: formData.extras || '',
screenshot: formData.screenshot ?? false,
link: formData.link ?? false,
video_understanding: formData.video_understanding ?? false,
video_interval: formData.video_interval ?? 4,
grid_size: formData.grid_size ?? [3, 3],
format: formData.format ?? [],
}) })
}, [currentTaskId]) }, [
// 当下面任意一个变了,就重新 reset
currentTaskId,
// modelList 用来兜底 model_name
modelList.length,
// 还要加上 formData 的各字段,或者直接 currentTask
currentTask?.formData,
])
/* ---- 帮助函数 ---- */ /* ---- 帮助函数 ---- */
const isGenerating = () => !['SUCCESS', 'FAILED', undefined].includes(getCurrentTask()?.status) const isGenerating = () => !['SUCCESS', 'FAILED', undefined].includes(getCurrentTask()?.status)
@@ -175,6 +194,7 @@ const NoteForm = () => {
} }
const onSubmit = async (values: NoteFormValues) => { const onSubmit = async (values: NoteFormValues) => {
console.log('Not even go here')
const payload: NoteFormValues = { const payload: NoteFormValues = {
...values, ...values,
provider_id: modelList.find(m => m.model_name === values.model_name)!.provider_id, provider_id: modelList.find(m => m.model_name === values.model_name)!.provider_id,
@@ -189,6 +209,10 @@ const NoteForm = () => {
const { data } = await generateNote(payload) const { data } = await generateNote(payload)
addPendingTask(data.task_id, values.platform, payload) addPendingTask(data.task_id, values.platform, payload)
} }
const onInvalid = (errors: FieldErrors<NoteFormValues>) => {
console.warn('表单校验失败:', errors)
message.error('请完善所有必填项后再提交')
}
const handleCreateNew = () => { const handleCreateNew = () => {
// 🔁 这里清空当前任务状态 // 🔁 这里清空当前任务状态
// 比如调用 resetCurrentTask() 或者 navigate 到一个新页面 // 比如调用 resetCurrentTask() 或者 navigate 到一个新页面
@@ -222,7 +246,7 @@ const NoteForm = () => {
return ( return (
<div className="h-full w-full"> <div className="h-full w-full">
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit, onInvalid)} className="space-y-4">
{/* 顶部按钮 */} {/* 顶部按钮 */}
<FormButton></FormButton> <FormButton></FormButton>

View File

@@ -3,5 +3,25 @@ const request = axios.create({
baseURL: '/api', baseURL: '/api',
timeout: 10000, timeout: 10000,
}) })
function handleErrorResponse(response: any) {
if (!response) return '请求失败,请检查网络连接'
if (typeof response.code !== 'number') return '系统异常'
// 错误码判断
switch (response.code) {
case 1001:
return response.msg || '下载失败,请检查视频链接'
case 1002:
return response.msg || '转写失败,请稍后重试'
case 1003:
return response.msg || '总结失败,可能是模型服务异常'
case 2001:
case 2002:
return Array.isArray(response.data)
? response.data.map(e => `${e.field}: ${e.error}`).join('\n')
: response.msg || '参数错误'
default:
return response.msg || '系统异常'
}
}
export default request export default request

View File

@@ -3,7 +3,7 @@
<p align="center"> <p align="center">
<img src="./doc/icon.svg" alt="BiliNote Banner" width="50" height="50" /> <img src="./doc/icon.svg" alt="BiliNote Banner" width="50" height="50" />
</p> </p>
<h1 align="center" > BiliNote v1.6.1</h1> <h1 align="center" > BiliNote v1.7.0</h1>
</div> </div>
<p align="center"><i>AI 视频笔记生成工具 让 AI 为你的视频做笔记</i></p> <p align="center"><i>AI 视频笔记生成工具 让 AI 为你的视频做笔记</i></p>

View File

View File

@@ -0,0 +1,38 @@
# app/core/exception_handlers.py
from fastapi import Request, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from app.utils.logger import get_logger
from app.utils.response import ResponseWrapper
from app.utils.status_code import StatusCode
logger = get_logger(__name__)
def register_exception_handlers(app):
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for err in exc.errors():
loc = err.get("loc", [])
field = loc[-1] if loc else "body"
msg = err.get("msg", "参数不合法")
errors.append({"field": field, "error": msg})
return JSONResponse(
status_code=400,
content=ResponseWrapper.error(msg="参数验证失败", code=StatusCode.PARAM_ERROR, data=errors)
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content=ResponseWrapper.error(msg=str(exc.detail), code=StatusCode.FAIL)
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.exception(f"服务器内部错误: {exc}")
return JSONResponse(
status_code=500,
content=ResponseWrapper.error(msg="服务器内部错误", code=StatusCode.FAIL, data=str(exc))
)

View File

@@ -38,5 +38,13 @@
"logo": "Gemini", "logo": "Gemini",
"api_key": "", "api_key": "",
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai/" "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/"
},
{
"id": "groq",
"name": "Groq",
"type": "built-in",
"logo": "Groq",
"api_key": "",
"base_url": "https://api.groq.com/openai/v1"
} }
] ]

View File

@@ -13,8 +13,10 @@ class OpenAICompatibleProvider:
@staticmethod @staticmethod
def test_connection(api_key: str, base_url: str) -> bool: def test_connection(api_key: str, base_url: str) -> bool:
print(api_key)
try: try:
client = OpenAI(api_key=api_key, base_url=base_url) client = OpenAI(api_key=api_key, base_url=base_url)
client.models.list() client.models.list()
return True return True
except Exception as e: except Exception as e:

View File

@@ -1,6 +1,8 @@
import json import json
from dataclasses import asdict from dataclasses import asdict
from fastapi import HTTPException
from app.downloaders.local_downloader import LocalDownloader from app.downloaders.local_downloader import LocalDownloader
from app.enmus.task_status_enums import TaskStatus from app.enmus.task_status_enums import TaskStatus
import os import os
@@ -33,6 +35,7 @@ from app.transcriber.whisper import WhisperTranscriber
import re import re
from app.utils.note_helper import replace_content_markers from app.utils.note_helper import replace_content_markers
from app.utils.status_code import StatusCode
from app.utils.video_helper import generate_screenshot from app.utils.video_helper import generate_screenshot
# from app.services.whisperer import transcribe_audio # from app.services.whisperer import transcribe_audio
@@ -143,7 +146,15 @@ class NoteGenerator:
return new_markdown return new_markdown
except Exception as e: except Exception as e:
logger.error(f"截图生成失败:{e}") logger.error(f"截图生成失败:{e}")
raise e raise HTTPException(
status_code=500,
detail={
"code": StatusCode.DOWNLOAD_ERROR,
"msg": f"截图生成失败",
"error": str(e)
}
)
@staticmethod @staticmethod
def delete_note(video_id: str, platform: str): def delete_note(video_id: str, platform: str):
@@ -226,8 +237,16 @@ class NoteGenerator:
save_quality=90, save_quality=90,
).run() ).run()
except Exception as e: except Exception as e:
logger.error(f" 下载视频失败task_id={task_id},错误信息:{e}") logger.error(f"Error 下载视频失败task_id={task_id},错误信息:{e}")
self.update_task_status(task_id, TaskStatus.FAILED, message=f"下载音频失败:{e}") self.update_task_status(task_id, TaskStatus.FAILED, message=f"下载音频失败:{e}")
raise HTTPException(
status_code=500,
detail={
"code": StatusCode.DOWNLOAD_ERROR,
"msg": f"下载视频失败task_id={task_id}",
"error": str(e)
}
)
# 没有音频缓存就下载音频(可能同时也带上视频) # 没有音频缓存就下载音频(可能同时也带上视频)
if audio is None: if audio is None:
@@ -241,9 +260,17 @@ class NoteGenerator:
json.dump(asdict(audio), f, ensure_ascii=False, indent=2) json.dump(asdict(audio), f, ensure_ascii=False, indent=2)
logger.info(f"音频下载并缓存成功task_id={task_id}") logger.info(f"音频下载并缓存成功task_id={task_id}")
except Exception as e: except Exception as e:
logger.error(f" 下载音频失败task_id={task_id},错误信息:{e}") logger.error(f"Error 下载音频失败task_id={task_id},错误信息:{e}")
self.update_task_status(task_id, TaskStatus.FAILED, message=f"下载音频失败:{e}") self.update_task_status(task_id, TaskStatus.FAILED, message=f"下载音频失败:{e}")
raise e
raise HTTPException(
status_code=500,
detail={
"code": StatusCode.DOWNLOAD_ERROR,
"msg": f"下载音频失败task_id={task_id}",
"error": str(e)
}
)
# -------- 2. 转写文字 -------- # -------- 2. 转写文字 --------
try: try:
@@ -259,7 +286,7 @@ class NoteGenerator:
segments=[TranscriptSegment(**seg) for seg in transcript_data["segments"]] segments=[TranscriptSegment(**seg) for seg in transcript_data["segments"]]
) )
except (json.JSONDecodeError, KeyError) as e: except (json.JSONDecodeError, KeyError) as e:
logger.warning(f"⚠️ 读取转录缓存失败重新转录task_id={task_id},错误信息:{e}") logger.warning(f"Warning 读取转录缓存失败重新转录task_id={task_id},错误信息:{e}")
transcript: TranscriptResult = self.transcriber.transcript(file_path=audio.file_path) transcript: TranscriptResult = self.transcriber.transcript(file_path=audio.file_path)
with open(transcript_cache_path, "w", encoding="utf-8") as f: with open(transcript_cache_path, "w", encoding="utf-8") as f:
json.dump(asdict(transcript), f, ensure_ascii=False, indent=2) json.dump(asdict(transcript), f, ensure_ascii=False, indent=2)
@@ -269,9 +296,16 @@ class NoteGenerator:
json.dump(asdict(transcript), f, ensure_ascii=False, indent=2) json.dump(asdict(transcript), f, ensure_ascii=False, indent=2)
logger.info(f"文字转写并缓存成功task_id={task_id}") logger.info(f"文字转写并缓存成功task_id={task_id}")
except Exception as e: except Exception as e:
logger.error(f" 转写文字失败task_id={task_id},错误信息:{e}") logger.error(f"Error 转写文字失败task_id={task_id},错误信息:{e}")
self.update_task_status(task_id, TaskStatus.FAILED, message=f"转写文字失败:{e}") self.update_task_status(task_id, TaskStatus.FAILED, message=f"转写文字失败:{e}")
raise e raise HTTPException(
status_code=500,
detail={
"code": StatusCode.GENERATE_ERROR, # =1003
"msg": f"转写文字失败task_id={task_id}",
"error": str(e)
}
)
# -------- 3. 总结内容 -------- # -------- 3. 总结内容 --------
try: try:
@@ -298,9 +332,16 @@ class NoteGenerator:
f.write(markdown) f.write(markdown)
logger.info(f"GPT总结并缓存成功task_id={task_id}") logger.info(f"GPT总结并缓存成功task_id={task_id}")
except Exception as e: except Exception as e:
logger.error(f" 总结内容失败task_id={task_id},错误信息:{e}") logger.error(f"Error 总结内容失败task_id={task_id},错误信息:{e}")
self.update_task_status(task_id, TaskStatus.FAILED, message=f"总结内容失败:{e}") self.update_task_status(task_id, TaskStatus.FAILED, message=f"总结内容失败:{e}")
raise e raise HTTPException(
status_code=500,
detail={
"code": StatusCode.GENERATE_ERROR, # =1003
"msg": f"总结内容失败task_id={task_id}",
"error": str(e)
}
)
# -------- 4. 插入截图 -------- # -------- 4. 插入截图 --------
if _format and 'screenshot' in _format: if _format and 'screenshot' in _format:
@@ -308,12 +349,12 @@ class NoteGenerator:
markdown = self.insert_screenshots_into_markdown(markdown, self.video_path, image_base_url, markdown = self.insert_screenshots_into_markdown(markdown, self.video_path, image_base_url,
output_dir, _format) output_dir, _format)
except Exception as e: except Exception as e:
logger.warning(f"⚠️ 插入截图失败跳过处理task_id={task_id},错误信息:{e}") logger.warning(f"Warning 插入截图失败跳过处理task_id={task_id},错误信息:{e}")
if _format and 'link' in _format: if _format and 'link' in _format:
try: try:
markdown = replace_content_markers(markdown, video_id=audio.video_id, platform=platform) markdown = replace_content_markers(markdown, video_id=audio.video_id, platform=platform)
except Exception as e: except Exception as e:
logger.warning(f"⚠️ 插入链接失败跳过处理task_id={task_id},错误信息:{e}") logger.warning(f"Warning 插入链接失败跳过处理task_id={task_id},错误信息:{e}")
# 注意:截图失败不终止整体流程 # 注意:截图失败不终止整体流程
# -------- 5. 保存数据库记录 -------- # -------- 5. 保存数据库记录 --------
@@ -322,7 +363,7 @@ class NoteGenerator:
# -------- 6. 完成 -------- # -------- 6. 完成 --------
self.update_task_status(task_id, TaskStatus.SUCCESS) self.update_task_status(task_id, TaskStatus.SUCCESS)
logger.info(f" 笔记生成成功task_id={task_id}") logger.info(f"succeed 笔记生成成功task_id={task_id}")
# TODO :改为前端一键清除缓存 # TODO :改为前端一键清除缓存
# if platform != 'local': # if platform != 'local':
# transcription_finished.send({ # transcription_finished.send({
@@ -335,6 +376,15 @@ class NoteGenerator:
) )
except Exception as e: except Exception as e:
logger.error(f" 笔记生成流程异常终止task_id={task_id},错误信息:{e}") logger.error(f"Error 笔记生成流程异常终止task_id={task_id},错误信息:{e}")
self.update_task_status(task_id, TaskStatus.FAILED, message=str(e)) self.update_task_status(task_id, TaskStatus.FAILED, message=str(e))
raise f'❌ 笔记生成流程异常终止task_id={task_id},错误信息:{e}'
# 返回结构化错误信息给前端(可以用于日志 + 显示 + 错误定位)
raise HTTPException(
status_code=500,
detail={
"code": StatusCode.FAIL,
"msg": f"笔记生成流程异常终止task_id={task_id}",
"error": str(e)
}
)

View File

@@ -0,0 +1,52 @@
from abc import ABC
import os
from app.decorators.timeit import timeit
from app.models.transcriber_model import TranscriptResult, TranscriptSegment
from app.services.provider import ProviderService
from app.transcriber.base import Transcriber
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
class GroqTranscriber(Transcriber, ABC):
@timeit
def transcript(self, file_path: str) -> TranscriptResult:
provider = ProviderService.get_provider_by_id('groq')
if not provider:
raise Exception("Groq 供应商未配置,请配置以后使用。")
client = OpenAI(
api_key=provider.get('api_key'),
base_url=provider.get('base_url')
)
filename = file_path
with open(filename, "rb") as file:
transcription = client.audio.transcriptions.create(
file=(filename, file.read()),
model=os.getenv('GROQ_TRANSCRIBER_MODEL'),
response_format="verbose_json",
)
print(transcription.text)
print(transcription)
segments = []
full_text = ""
for seg in transcription.segments:
text = seg.text.strip()
full_text += text + " "
segments.append(TranscriptSegment(
start=seg.start,
end=seg.end,
text=text
))
result = TranscriptResult(
language=transcription.language,
full_text=full_text.strip(),
segments=segments,
raw=transcription.to_dict()
)
return result

View File

@@ -1,113 +1,115 @@
import os import os
import platform import platform
from enum import Enum
from app.transcriber.groq import GroqTranscriber
from app.transcriber.whisper import WhisperTranscriber from app.transcriber.whisper import WhisperTranscriber
from app.transcriber.bcut import BcutTranscriber from app.transcriber.bcut import BcutTranscriber
from app.transcriber.kuaishou import KuaishouTranscriber from app.transcriber.kuaishou import KuaishouTranscriber
from app.utils.logger import get_logger from app.utils.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
# 只在Apple平台且设置了环境变量时才导入MLX Whisper class TranscriberType(str, Enum):
FAST_WHISPER = "fast-whisper"
MLX_WHISPER = "mlx-whisper"
BCUT = "bcut"
KUAISHOU = "kuaishou"
GROQ = "groq"
# 仅在 Apple 平台启用 MLX Whisper
MLX_WHISPER_AVAILABLE = False
if platform.system() == "Darwin" and os.environ.get("TRANSCRIBER_TYPE") == "mlx-whisper": if platform.system() == "Darwin" and os.environ.get("TRANSCRIBER_TYPE") == "mlx-whisper":
try: try:
from app.transcriber.mlx_whisper_transcriber import MLXWhisperTranscriber from app.transcriber.mlx_whisper_transcriber import MLXWhisperTranscriber
MLX_WHISPER_AVAILABLE = True MLX_WHISPER_AVAILABLE = True
logger.info("MLX Whisper 可用,已导入") logger.info("MLX Whisper 可用,已导入")
except ImportError: except ImportError:
MLX_WHISPER_AVAILABLE = False
logger.warning("MLX Whisper 导入失败,可能未安装或平台不支持") logger.warning("MLX Whisper 导入失败,可能未安装或平台不支持")
else:
MLX_WHISPER_AVAILABLE = False
logger.info('初始化转录服务提供器') logger.info('初始化转录服务提供器')
# 维护各种转录器单例实例 # 转录器单例缓存
_transcribers = { _transcribers = {
'bcut': None, TranscriberType.FAST_WHISPER: None,
'kuaishou': None, TranscriberType.MLX_WHISPER: None,
'mlx-whisper': None, TranscriberType.BCUT: None,
'fast-whisper':None TranscriberType.KUAISHOU: None,
TranscriberType.GROQ: None,
} }
def get_whisper_transcriber(model_size="base", device="cuda"): # 公共实例初始化函数
"""获取 Whisper 转录器实例""" def _init_transcriber(key: TranscriberType, cls, *args, **kwargs):
if _transcribers['fast-whisper'] is None: if _transcribers[key] is None:
logger.info(f'创建 Whisper 转录器实例,参数:{model_size}, {device}') logger.info(f'创建 {cls.__name__} 实例: {key}')
try: try:
_transcribers['whisper'] = WhisperTranscriber(model_size=model_size, device=device) _transcribers[key] = cls(*args, **kwargs)
logger.info('Whisper 转录器创建成功') logger.info(f'{cls.__name__} 创建成功')
except Exception as e: except Exception as e:
logger.error(f"Whisper 转录器创建失败: {e}") logger.error(f"{cls.__name__} 创建失败: {e}")
raise raise
return _transcribers['whisper'] return _transcribers[key]
# 各类型获取方法
def get_groq_transcriber():
return _init_transcriber(TranscriberType.GROQ, GroqTranscriber)
def get_whisper_transcriber(model_size="base", device="cuda"):
return _init_transcriber(TranscriberType.FAST_WHISPER, WhisperTranscriber, model_size=model_size, device=device)
def get_bcut_transcriber(): def get_bcut_transcriber():
"""获取 Bcut 转录器实例""" return _init_transcriber(TranscriberType.BCUT, BcutTranscriber)
if _transcribers['bcut'] is None:
logger.info('创建 Bcut 转录器实例')
try:
_transcribers['bcut'] = BcutTranscriber()
logger.info('Bcut 转录器创建成功')
except Exception as e:
logger.error(f"Bcut 转录器创建失败: {e}")
raise
return _transcribers['bcut']
def get_kuaishou_transcriber(): def get_kuaishou_transcriber():
"""获取快手转录器实例""" return _init_transcriber(TranscriberType.KUAISHOU, KuaishouTranscriber)
if _transcribers['kuaishou'] is None:
logger.info('创建快手转录器实例')
try:
_transcribers['kuaishou'] = KuaishouTranscriber()
logger.info('快手转录器创建成功')
except Exception as e:
logger.error(f"快手转录器创建失败: {e}")
raise
return _transcribers['kuaishou']
def get_mlx_whisper_transcriber(model_size="base"): def get_mlx_whisper_transcriber(model_size="base"):
"""获取 MLX Whisper 转录器实例"""
if not MLX_WHISPER_AVAILABLE: if not MLX_WHISPER_AVAILABLE:
logger.warning("MLX Whisper 不可用请确保在Apple平台且已安装mlx_whisper") logger.warning("MLX Whisper 不可用,请确保在 Apple 平台且已安装 mlx_whisper")
raise ImportError("MLX Whisper 不可用请确保在Apple平台且已安装mlx_whisper") raise ImportError("MLX Whisper 不可用")
return _init_transcriber(TranscriberType.MLX_WHISPER, MLXWhisperTranscriber, model_size=model_size)
if _transcribers['mlx-whisper'] is None:
logger.info(f'创建 MLX Whisper 转录器实例,参数:{model_size}')
try:
_transcribers['mlx-whisper'] = MLXWhisperTranscriber(model_size=model_size)
logger.info('MLX Whisper 转录器创建成功')
except Exception as e:
logger.error(f"MLX Whisper 转录器创建失败: {e}")
raise
return _transcribers['mlx-whisper']
# 通用入口
def get_transcriber(transcriber_type="fast-whisper", model_size="base", device="cuda"): def get_transcriber(transcriber_type="fast-whisper", model_size="base", device="cuda"):
""" """
获取指定类型的转录器实例 获取指定类型的转录器实例
参数: 参数:
transcriber_type: 转录器类型,支持 "fast-whisper", "bcut", "kuaishou", "mlx-whisper"(仅Apple平台) transcriber_type: 支持 "fast-whisper", "mlx-whisper", "bcut", "kuaishou", "groq"
model_size: 模型大小,whisper 和 mlx-whisper 特有参数 model_size: 模型大小,适用于 whisper
device: 设备类型whisper 特有参数 device: 设备类型(如 cuda / cpuwhisper 使用
返回: 返回:
对应类型的转录器实例 对应类型的转录器实例
""" """
logger.info(f'获取转录器类型: {transcriber_type}') logger.info(f'请求转录器类型: {transcriber_type}')
if transcriber_type == "fast-whisper":
whisper_model_size = os.environ.get("WHISPER_MODEL_SIZE",model_size) try:
transcriber_enum = TranscriberType(transcriber_type)
except ValueError:
logger.warning(f'未知转录器类型 "{transcriber_type}",默认使用 fast-whisper')
transcriber_enum = TranscriberType.FAST_WHISPER
whisper_model_size = os.environ.get("WHISPER_MODEL_SIZE", model_size)
if transcriber_enum == TranscriberType.FAST_WHISPER:
return get_whisper_transcriber(whisper_model_size, device=device) return get_whisper_transcriber(whisper_model_size, device=device)
elif transcriber_type == "mlx-whisper":
whisper_model_size = os.environ.get("WHISPER_MODEL_SIZE",model_size) elif transcriber_enum == TranscriberType.MLX_WHISPER:
if not MLX_WHISPER_AVAILABLE: if not MLX_WHISPER_AVAILABLE:
logger.warning("MLX Whisper 不可用,回退到 fast-whisper") logger.warning("MLX Whisper 不可用,回退到 fast-whisper")
return get_whisper_transcriber(whisper_model_size, device=device) return get_whisper_transcriber(whisper_model_size, device=device)
return get_mlx_whisper_transcriber(whisper_model_size) return get_mlx_whisper_transcriber(whisper_model_size)
elif transcriber_type == "bcut":
elif transcriber_enum == TranscriberType.BCUT:
return get_bcut_transcriber() return get_bcut_transcriber()
elif transcriber_type == "kuaishou":
elif transcriber_enum == TranscriberType.KUAISHOU:
return get_kuaishou_transcriber() return get_kuaishou_transcriber()
else:
logger.warning(f'未知转录器类型 "{transcriber_type}",使用默认 whisper') elif transcriber_enum == TranscriberType.GROQ:
whisper_model_size = os.environ.get("WHISPER_MODEL_SIZE",model_size) return get_groq_transcriber()
return get_whisper_transcriber(whisper_model_size, device)
# fallback
logger.warning(f'未识别转录器类型 "{transcriber_type}",使用 fast-whisper 作为默认')
return get_whisper_transcriber(whisper_model_size, device=device)

View File

@@ -4,6 +4,7 @@ import uvicorn
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
from dotenv import load_dotenv from dotenv import load_dotenv
from app.core.exception_handlers import register_exception_handlers
from app.db.model_dao import init_model_table from app.db.model_dao import init_model_table
from app.db.provider_dao import init_provider_table from app.db.provider_dao import init_provider_table
from app.utils.logger import get_logger from app.utils.logger import get_logger
@@ -34,12 +35,12 @@ if not os.path.exists(out_dir):
app = create_app() app = create_app()
app.mount(static_path, StaticFiles(directory=static_dir), name="static") app.mount(static_path, StaticFiles(directory=static_dir), name="static")
app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads") app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")
async def startup_event():
register_handler()
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
register_exception_handlers(app)
register_handler() register_handler()
ensure_ffmpeg_or_raise() ensure_ffmpeg_or_raise()
register_handler()
get_transcriber(transcriber_type=os.getenv("TRANSCRIBER_TYPE","fast-whisper")) get_transcriber(transcriber_type=os.getenv("TRANSCRIBER_TYPE","fast-whisper"))
init_video_task_table() init_video_task_table()
init_provider_table() init_provider_table()

Binary file not shown.