Compare commits
2 Commits
55e88777d9
...
9efe1d3872
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9efe1d3872 | ||
|
|
21fb8b39d8 |
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -99,6 +99,48 @@ body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Markdown Styles */
|
||||
.markdown-body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
color: #334155; /* Slate 700 */
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: #0f172a; /* Slate 900 */
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
padding-left: 2em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.markdown-body strong {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
padding: 0 1em;
|
||||
color: #64748b;
|
||||
border-left: 0.25em solid #cbd5e1;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<link rel="stylesheet" href="css/element-plus.index.css">
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
|
||||
</head>
|
||||
@@ -154,12 +153,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>
|
||||
|
||||
118
static/js/app.js
118
static/js/app.js
@@ -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;
|
||||
@@ -432,11 +437,10 @@ createApp({
|
||||
// Computed Properties (Markdown)
|
||||
// ==========================================
|
||||
|
||||
/*
|
||||
marked.use({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
mangle: false,
|
||||
headerIds: false,
|
||||
renderer: {
|
||||
code(code, language) {
|
||||
if (language === 'echarts') {
|
||||
@@ -447,6 +451,7 @@ createApp({
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
const renderedAiText = computed(() => {
|
||||
if (!aiText.value) return "";
|
||||
@@ -468,6 +473,61 @@ createApp({
|
||||
}
|
||||
});
|
||||
|
||||
// 内置简易 Markdown 解析器,确保不依赖外部库也能正常显示
|
||||
const simpleMarkdown = (text) => {
|
||||
if (!text) return '';
|
||||
let lines = text.split('\n');
|
||||
let html = '';
|
||||
let inList = false;
|
||||
|
||||
// 辅助函数:解析行内样式(粗体、代码等)
|
||||
const parseInline = (str) => {
|
||||
return str
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // Bold
|
||||
.replace(/`(.*?)`/g, '<code style="background:#f1f5f9; padding:2px 4px; border-radius:4px;">$1</code>'); // Code
|
||||
};
|
||||
|
||||
for (let line of lines) {
|
||||
let trimmed = line.trim();
|
||||
|
||||
// 忽略空行
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 标题处理 (###)
|
||||
if (trimmed.startsWith('### ')) {
|
||||
if (inList) { html += '</ul>'; inList = false; }
|
||||
html += `<h3 style="margin-top:16px; margin-bottom:8px; font-weight:bold; font-size:1.1em;">${parseInline(trimmed.substring(4))}</h3>`;
|
||||
}
|
||||
// 列表项处理 (- 或 1.)
|
||||
else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) {
|
||||
if (!inList) { html += '<ul style="padding-left:20px; margin:0;">'; inList = true; }
|
||||
let content = trimmed.replace(/^(- |\d+\. )/, '');
|
||||
html += `<li style="margin-bottom:4px;">${parseInline(content)}</li>`;
|
||||
}
|
||||
// 普通段落
|
||||
else {
|
||||
if (inList) { html += '</ul>'; inList = false; }
|
||||
html += `<p style="margin-bottom:8px;">${parseInline(trimmed)}</p>`;
|
||||
}
|
||||
}
|
||||
if (inList) html += '</ul>';
|
||||
return html;
|
||||
};
|
||||
|
||||
const renderedSummary = computed(() => {
|
||||
if (!summaryText.value) return '';
|
||||
|
||||
// 优先使用内置解析器,确保绝对可用
|
||||
try {
|
||||
return simpleMarkdown(summaryText.value);
|
||||
} catch (e) {
|
||||
console.error("Simple markdown error:", e);
|
||||
return summaryText.value;
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Watchers
|
||||
// ==========================================
|
||||
@@ -572,6 +632,49 @@ createApp({
|
||||
}
|
||||
};
|
||||
|
||||
const openSummaryDialog = () => {
|
||||
showSummaryDialog.value = true;
|
||||
// Optionally auto-start if empty
|
||||
if (!summaryText.value) {
|
||||
fetchDouyinSummary();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSummaryClose = () => {
|
||||
showSummaryDialog.value = false;
|
||||
};
|
||||
|
||||
const fetchDouyinSummary = async () => {
|
||||
if (summaryLoading.value) return;
|
||||
summaryText.value = "";
|
||||
summaryLoading.value = true;
|
||||
try {
|
||||
// Pass empty IDs to summarize all/latest
|
||||
const response = await fetch(apiBase.value + "/api/douyin/summary", {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ids: []})
|
||||
});
|
||||
|
||||
if (!response.body) return;
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value, {stream: true});
|
||||
summaryText.value += chunk;
|
||||
// Auto scroll? The user can scroll the dialog.
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
summaryText.value += "\n(总结过程出错: " + e.message + ")";
|
||||
} finally {
|
||||
summaryLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const statusColor = (status) => {
|
||||
switch(status) {
|
||||
case 'COMPLETED': return '#10b981';
|
||||
@@ -658,6 +761,15 @@ createApp({
|
||||
// Douyin
|
||||
shareText, douyinLoading, douyinRecords,
|
||||
startParsing, fetchDouyinRecords, deleteRecord, statusColor, statusType, formatDate,
|
||||
|
||||
// Douyin Summary
|
||||
showSummaryDialog,
|
||||
summaryLoading,
|
||||
summaryText,
|
||||
renderedSummary,
|
||||
openSummaryDialog,
|
||||
handleSummaryClose,
|
||||
fetchDouyinSummary,
|
||||
// Ad
|
||||
showAd, adCountdown, closeAd
|
||||
};
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
31
test_marked.html
Normal file
31
test_marked.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="output"></div>
|
||||
<div id="error" style="color: red;"></div>
|
||||
<script>
|
||||
try {
|
||||
marked.use({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
renderer: {
|
||||
code(code, language) {
|
||||
if (language === 'echarts') {
|
||||
return 'ECHARTS';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
const text = "- **Test Bold**";
|
||||
const html = marked.parse(text);
|
||||
document.getElementById('output').innerHTML = html;
|
||||
} catch (e) {
|
||||
document.getElementById('error').innerText = e.toString();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user