'commit'
This commit is contained in:
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
import pymysql
|
||||
|
||||
@@ -38,6 +39,9 @@ def get_db_connection():
|
||||
class ParseRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
class SummaryRequest(BaseModel):
|
||||
ids: List[str] = []
|
||||
|
||||
def update_status(id, status, error_msg=None):
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
@@ -258,6 +262,64 @@ def delete_record(id: str):
|
||||
except Exception as 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():
|
||||
"""
|
||||
Check for tasks stuck in PENDING or PROCESSING state (due to server restart)
|
||||
|
||||
@@ -154,12 +154,34 @@
|
||||
|
||||
<!-- Douyin Tab -->
|
||||
<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;">
|
||||
<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>
|
||||
<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>
|
||||
<p style="color: #94a3b8;">自动解析视频、提取文案,构建企业充电知识图谱</p>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,12 @@ createApp({
|
||||
const douyinLoading = ref(false);
|
||||
const douyinRecords = ref([]);
|
||||
let douyinTimer = null;
|
||||
|
||||
|
||||
// Douyin Summary State
|
||||
const showSummaryDialog = ref(false);
|
||||
const summaryLoading = ref(false);
|
||||
const summaryText = ref('');
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
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
|
||||
// ==========================================
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user