mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-07-07 23:51:21 +08:00
Merge pull request #101 from JefferyHcool/feature/kuaishou
feat(backend): 添加 Groq供应商支持并优化笔记生成流程- 在 builtin_providers.json 中添加 Groq 供应商信息 - 实现 GroqTranscriber 类以支持 Groq 语音转录服务 - 新增异常处理中间件以提高系统稳定性 - 优化笔记生成流程,增加错误处理和日志记录 - 添加思维导图功能和相关组件 -重构 Markdown 查看器以支持切换视图模式
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
8
BillNote_frontend/src/lib/markmap.ts
Normal file
8
BillNote_frontend/src/lib/markmap.ts
Normal 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 })
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
38
backend/app/core/exception_handlers.py
Normal file
38
backend/app/core/exception_handlers.py
Normal 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))
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
52
backend/app/transcriber/groq.py
Normal file
52
backend/app/transcriber/groq.py
Normal 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
|
||||||
@@ -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 / cpu),仅 whisper 使用
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
对应类型的转录器实例
|
对应类型的转录器实例
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
|
|||||||
@@ -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.
Reference in New Issue
Block a user