BiliNote 将视频转换为markdown文章

项目地址

BiliNote

介绍

可以将b站、youtube、快手、抖音、本地视频转换为markdown的文本(带视频截图),带有 ai改写 功能,可以自定义prompt。

修改

1. 删除cors校验

backend->app->main.py

allow_origins=origins,

修改为

allow_origins=["*"],

2. 前端修改端口号

BillNote_frontend->vite.config.ts

const apiBaseUrl = env.VITE_API_BASE_URL || 'http://localhost:8000'

修改为

const apiBaseUrl = 'http://localhost:8483'

2. 去掉图像理解

对我来说,我觉得这个视频理解是完全不必要的,因为每隔几秒截图给deepseek去理解会消耗大量的token(转成base64了),而且人工再次编辑的时候会删除很多图片,等于浪费了token,应该直接处理文本,然后再将图片插入ai处理后的文本中就可以了。所以让ai修改了一下它的python后端app->gpt->universal_gpt.py代码(也修复了乱码)。如下:

from app.gpt.base import GPT
from app.gpt.prompt_builder import generate_base_prompt
from app.models.gpt_model import GPTSource
from app.gpt.prompt import BASE_PROMPT, AI_SUM, SCREENSHOT, LINK
from app.gpt.utils import fix_markdown
from app.models.transcriber_model import TranscriptSegment
from datetime import timedelta
from typing import List
import re


class UniversalGPT(GPT):
    def __init__(self, client, model: str, temperature: float = 0.7):
        self.client = client
        self.model = model
        self.temperature = temperature
        self.screenshot = False
        self.link = False

    def _format_time(self, seconds: float) -> str:
        return str(timedelta(seconds=int(seconds)))[2:]

    def _build_segment_text(self, segments: List[TranscriptSegment]) -> str:
        return "\n".join(
            f"{self._format_time(seg.start)} - {seg.text.strip()}"
            for seg in segments
        )

    def ensure_segments_type(self, segments) -> List[TranscriptSegment]:
        return [TranscriptSegment(**seg) if isinstance(seg, dict) else seg for seg in segments]

    def create_messages(self, segments: List[TranscriptSegment], **kwargs):
        """
        创建包含图片的消息(保留此方法供其他地方使用)
        但在summarize方法中我们将使用纯文本版本
        """
        content_text = generate_base_prompt(
            title=kwargs.get('title'),
            segment_text=self._build_segment_text(segments),
            tags=kwargs.get('tags'),
            _format=kwargs.get('_format'),
            style=kwargs.get('style'),
            extras=kwargs.get('extras'),
        )

        # 组装 content 数组,支持 text + image_url 混合
        content = [{"type": "text", "text": content_text}]
        video_img_urls = kwargs.get('video_img_urls', [])

        for url in video_img_urls:
            content.append({
                "type": "image_url",
                "image_url": {
                    "url": url,
                    "detail": "auto"
                }
            })

        # 正确格式:整体包在一个 message 里,role + content array
        messages = [{
            "role": "user",
            "content": content
        }]

        return messages

    def _create_text_only_messages(self, segments: List[TranscriptSegment], **kwargs):
        """
        创建纯文本消息,不包含图片
        用于解决请求过大的问题
        """
        content_text = generate_base_prompt(
            title=kwargs.get('title'),
            segment_text=self._build_segment_text(segments),
            tags=kwargs.get('tags'),
            _format=kwargs.get('_format'),
            style=kwargs.get('style'),
            extras=kwargs.get('extras'),
        )

        # 纯文本消息
        messages = [{
            "role": "user",
            "content": content_text  # 注意:这里是纯字符串,不是数组
        }]

        return messages

    def _safe_fix_markdown(self, markdown_text: str) -> str:
        """
        安全的Markdown修复函数,避免乱码
        替换原有的fix_markdown函数
        """
        if not markdown_text:
            return markdown_text
        
        # 移除可能的unicode转义序列(如果存在)
        try:
            import codecs
            # 尝试解码,但如果已经正常则直接返回
            if '\\u' in markdown_text or '\\x' in markdown_text:
                # 有转义序列,尝试解码
                try:
                    return codecs.decode(markdown_text, 'unicode_escape')
                except:
                    # 解码失败,直接返回原文本
                    return markdown_text
            else:
                # 没有转义序列,直接返回
                return markdown_text
        except:
            return markdown_text
    
    def _clean_markdown_format(self, markdown_text: str) -> str:
        """
        清理和优化Markdown格式
        """
        if not markdown_text:
            return markdown_text
        
        # 修复常见的Markdown格式问题
        cleaned = markdown_text
        
        # 1. 修复标题格式(确保标题前有空行)
        cleaned = re.sub(r'(\n)(#+ )', r'\1\2', cleaned)
        
        # 2. 修复列表格式(确保列表项前有空行或正确缩进)
        cleaned = re.sub(r'(\n)(\s*[-*+] )', r'\1\2', cleaned)
        
        # 3. 修复代码块格式
        cleaned = re.sub(r'```(.*?)```', r'\n```\1```\n', cleaned, flags=re.DOTALL)
        
        # 4. 移除多余的空行(超过3个连续空行)
        cleaned = re.sub(r'\n{4,}', '\n\n\n', cleaned)
        
        # 5. 确保文本末尾有一个空行
        cleaned = cleaned.rstrip() + '\n'
        
        return cleaned

    def _add_screenshots_to_markdown(self, markdown_text: str, screenshot_urls: List[str]) -> str:
        """
        在Markdown文本末尾添加截图部分
        使用纯Markdown格式,兼容性更好
        """
        if not screenshot_urls:
            return markdown_text
        
        # 限制截图数量,避免过多
        max_screenshots = 10
        if len(screenshot_urls) > max_screenshots:
            # 均匀采样选择关键截图
            step = max(1, len(screenshot_urls) // max_screenshots)
            selected_urls = []
            for i in range(0, len(screenshot_urls), step):
                if len(selected_urls) < max_screenshots:
                    selected_urls.append(screenshot_urls[i])
            screenshot_info = f"(精选{len(selected_urls)}张,共{len(screenshot_urls)}张)"
        else:
            selected_urls = screenshot_urls
            screenshot_info = f"(共{len(selected_urls)}张)"
        
        # 添加截图部分 - 使用纯Markdown格式
        screenshot_section = "\n\n---\n\n"
        screenshot_section += "## 📸 视频截图\n\n"
        screenshot_section += "以下为视频关键时间点的截图,可用于笔记配图:\n\n"
        
        # 每行显示2张截图(使用表格格式)
        screenshot_section += "| 截图 | 截图 |\n"
        screenshot_section += "| :---: | :---: |\n"
        
        for i in range(0, len(selected_urls), 2):
            row = "|"
            # 第一列
            if i < len(selected_urls):
                img_num = i + 1
                row += f" ![截图{img_num}]({selected_urls[i]})<br>截图 {img_num} |"
            else:
                row += " |"
            
            # 第二列
            if i + 1 < len(selected_urls):
                img_num = i + 2
                row += f" ![截图{img_num}]({selected_urls[i+1]})<br>截图 {img_num} |"
            else:
                row += " |"
            
            screenshot_section += row + "\n"
        
        # 如果截图超过限制数量,显示提示
        if len(screenshot_urls) > max_screenshots:
            screenshot_section += f"\n*还有 {len(screenshot_urls) - max_screenshots} 张截图未显示*\n\n"
        
        # 添加编辑提示
        screenshot_section += "\n---\n\n"
        screenshot_section += "> ** 编辑提示 **\n"
        screenshot_section += "> - 截图仅作为参考,您可以根据需要删除不需要的截图\n"
        screenshot_section += "> - 可以将截图调整到相关内容附近\n"
        screenshot_section += "> - 可以添加图片说明文字\n"
        screenshot_section += "> - 此部分为自动生成,可完全修改\n"
        
        return markdown_text + screenshot_section

    def summarize(self, source: GPTSource) -> str:
        """
        总结文本内容(纯文本处理,解决413请求过大问题)
        截图将在总结完成后作为Markdown图片链接插入到文本末尾
        """
        self.screenshot = source.screenshot
        self.link = source.link
        source.segment = self.ensure_segments_type(source.segment)

        # 使用纯文本消息,不包含图片,避免请求过大
        messages = self._create_text_only_messages(
            source.segment,
            title=source.title,
            tags=source.tags,
            _format=source._format,
            style=source.style,
            extras=source.extras
        )
        
        # 调用API获取纯文本总结
        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=self.temperature
        )
        
        # 获取纯文本总结结果
        text_summary = response.choices[0].message.content.strip()
        
        # 安全地修复Markdown格式(避免乱码)
        text_summary = self._safe_fix_markdown(text_summary)
        
        # 清理和优化Markdown格式
        text_summary = self._clean_markdown_format(text_summary)
        
        # 如果有截图,在总结文本后插入截图
        if self.screenshot and hasattr(source, 'video_img_urls') and source.video_img_urls:
            text_summary = self._add_screenshots_to_markdown(text_summary, source.video_img_urls)
        
        return text_summary

    def list_models(self):
        return self.client.models.list()


作者:spike

分类: Tool

创作时间:2025-12-08

更新时间:2025-12-09

联系方式放在中括号之中例如[[email protected]],回复评论在开头加上标号例如:#1