This commit is contained in:
HuangHai
2026-01-20 21:50:32 +08:00
parent 55e88777d9
commit 21fb8b39d8
6 changed files with 99 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ from datetime import datetime
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, HTTPException, BackgroundTasks from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi.responses import StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
import pymysql import pymysql
@@ -38,6 +39,9 @@ def get_db_connection():
class ParseRequest(BaseModel): class ParseRequest(BaseModel):
text: str text: str
class SummaryRequest(BaseModel):
ids: List[str] = []
def update_status(id, status, error_msg=None): def update_status(id, status, error_msg=None):
try: try:
conn = get_db_connection() conn = get_db_connection()
@@ -258,6 +262,64 @@ def delete_record(id: str):
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/douyin/summary")
async def generate_summary(request: SummaryRequest):
try:
# Fetch transcripts
conn = get_db_connection()
cursor = conn.cursor()
if request.ids:
# Secure way to handle list in SQL
format_strings = ','.join(['%s'] * len(request.ids))
sql = f"SELECT video_name, transcript FROM t_douyin_record WHERE id IN ({format_strings}) AND status='COMPLETED'"
cursor.execute(sql, tuple(request.ids))
else:
# Default to latest 20
cursor.execute("SELECT video_name, transcript FROM t_douyin_record WHERE status='COMPLETED' ORDER BY create_time DESC LIMIT 20")
records = cursor.fetchall()
conn.close()
if not records:
# If no records, just return a simple message stream
async def empty_stream():
yield "未找到可总结的已完成记录,请先解析视频。"
return StreamingResponse(empty_stream(), media_type="text/event-stream")
# Prepare text
full_text = ""
for r in records:
if r['transcript']:
full_text += f"【标题:{r['video_name']}】\n内容:{r['transcript']}\n\n"
if not full_text:
async def empty_text_stream():
yield "记录中没有有效的文案内容。"
return StreamingResponse(empty_text_stream(), media_type="text/event-stream")
# Prompt
prompt = f"""
请对以下充电行业相关的视频内容进行知识精华提取。
要求:
1. 忽略无关闲聊和口语化表达;
2. 按条目列出核心知识点,不要长篇大论;
3. 保持简洁专业,只保留干货;
4. 返回格式为Markdown列表。
内容如下:
{full_text[:15000]}
"""
# Limit context to avoid errors, 15000 chars is roughly safe for most models,
# but if using a small model, might need less. Assuming robust model.
return StreamingResponse(get_llm_response(prompt), media_type="text/event-stream")
except Exception as e:
logger.error(f"Summary generation failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
async def recover_pending_tasks(): async def recover_pending_tasks():
""" """
Check for tasks stuck in PENDING or PROCESSING state (due to server restart) Check for tasks stuck in PENDING or PROCESSING state (due to server restart)

View File

@@ -154,12 +154,34 @@
<!-- Douyin Tab --> <!-- Douyin Tab -->
<div class="douyin-container" v-show="activeTab==='douyin'" style="padding: 24px; max-width: 95%; margin: 0 auto;"> <div class="douyin-container" v-show="activeTab==='douyin'" style="padding: 24px; max-width: 95%; margin: 0 auto;">
<!-- Summary Dialog -->
<el-dialog v-model="showSummaryDialog" title="💡 知识精华总结" width="60%" :before-close="handleSummaryClose">
<div class="dialog-content" style="max-height: 60vh; overflow-y: auto; padding: 10px;">
<div v-if="summaryLoading && !summaryText" style="text-align: center; padding: 20px; color: #6b7280;">
<p>⚡ AI正在阅读您的知识库并提炼精华,请稍候...</p>
</div>
<div class="markdown-body" v-html="renderedSummary" style="font-size: 15px; line-height: 1.8; color: #1f2937;"></div>
<span v-if="summaryLoading && summaryText" class="cursor-blink">|</span>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleSummaryClose">关闭</el-button>
<el-button type="primary" @click="fetchDouyinSummary" :loading="summaryLoading">
{{ summaryText ? '重新生成' : '开始总结' }}
</el-button>
</span>
</template>
</el-dialog>
<div class="douyin-header" style="text-align: center; margin-bottom: 40px;"> <div class="douyin-header" style="text-align: center; margin-bottom: 40px;">
<h1 style="font-size: 2.5rem; font-weight: bold; color: #f1f5f9; margin-bottom: 0.5rem;"> <h1 style="font-size: 2.5rem; font-weight: bold; color: #f1f5f9; margin-bottom: 0.5rem;">
抖音知识库 抖音知识库
<button class="btn-primary" @click="fetchDouyinRecords" style="font-size: 1rem; padding: 4px 12px; margin-left: 12px; vertical-align: middle;"> <button class="btn-primary" @click="fetchDouyinRecords" style="font-size: 1rem; padding: 4px 12px; margin-left: 12px; vertical-align: middle;">
🔄 刷新列表 🔄 刷新列表
</button> </button>
<button class="btn-primary" @click="openSummaryDialog" style="font-size: 1rem; padding: 4px 12px; margin-left: 12px; vertical-align: middle; background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); border: none;">
✨ 提取知识精华
</button>
</h1> </h1>
<p style="color: #94a3b8;">自动解析视频、提取文案,构建企业充电知识图谱</p> <p style="color: #94a3b8;">自动解析视频、提取文案,构建企业充电知识图谱</p>
</div> </div>

View File

@@ -20,7 +20,12 @@ createApp({
const douyinLoading = ref(false); const douyinLoading = ref(false);
const douyinRecords = ref([]); const douyinRecords = ref([]);
let douyinTimer = null; let douyinTimer = null;
// Douyin Summary State
const showSummaryDialog = ref(false);
const summaryLoading = ref(false);
const summaryText = ref('');
// Handle window resize // Handle window resize
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 768; isMobile.value = window.innerWidth <= 768;
@@ -468,6 +473,15 @@ createApp({
} }
}); });
const renderedSummary = computed(() => {
if (!summaryText.value) return '';
try {
return marked.parse(summaryText.value);
} catch (e) {
return summaryText.value;
}
});
// ========================================== // ==========================================
// Watchers // Watchers
// ========================================== // ==========================================