mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-06 00:00:05 +08:00
optimize
This commit is contained in:
@@ -35,7 +35,7 @@ def generate_md5(text: str) -> str:
|
|||||||
|
|
||||||
def generate_embedding_json(text: str) -> str:
|
def generate_embedding_json(text: str) -> str:
|
||||||
"""辅助函数:调用大模型生成向量,并序列化为 JSON 字符串"""
|
"""辅助函数:调用大模型生成向量,并序列化为 JSON 字符串"""
|
||||||
raw_vec = embedder_model.encode([text], normalize_embeddings=True)[0]
|
raw_vec = embedder_model.encode([text], normalize_embeddings=True, show_progress_bar=False)[0]
|
||||||
truncated_vec = [round(float(x), 5) for x in raw_vec]
|
truncated_vec = [round(float(x), 5) for x in raw_vec]
|
||||||
return json.dumps(truncated_vec, separators=(',', ':'))
|
return json.dumps(truncated_vec, separators=(',', ':'))
|
||||||
|
|
||||||
|
|||||||
@@ -37,18 +37,48 @@ def _normalize_text(text: str) -> str:
|
|||||||
return text.strip().casefold()
|
return text.strip().casefold()
|
||||||
|
|
||||||
|
|
||||||
|
_EMBEDDING_CACHE: dict[str, np.ndarray] = {}
|
||||||
|
MAX_CACHE_SIZE = 10000
|
||||||
|
|
||||||
def _build_keyword_embedding_map(keywords: list[str]) -> dict[str, np.ndarray]:
|
def _build_keyword_embedding_map(keywords: list[str]) -> dict[str, np.ndarray]:
|
||||||
"""
|
"""
|
||||||
批量生成关键词向量,并返回原词到向量的映射。
|
批量生成或从缓存获取关键词向量,并返回原词到向量的映射。
|
||||||
这里要求向量已归一化,后续可直接用点积表示余弦相似度。
|
结合了批量推理(Batching)的极速优势和内存缓存的 O(1) 读取优势。
|
||||||
"""
|
"""
|
||||||
if not keywords:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
vectors = embedder_model.encode(keywords, normalize_embeddings=True)
|
|
||||||
result: dict[str, np.ndarray] = {}
|
result: dict[str, np.ndarray] = {}
|
||||||
for keyword, vec in zip(keywords, vectors):
|
if not keywords:
|
||||||
result[keyword] = np.asarray(vec, dtype=np.float32)
|
return result
|
||||||
|
|
||||||
|
uncached_keywords = []
|
||||||
|
|
||||||
|
# 1. 尝试从缓存获取
|
||||||
|
for keyword in keywords:
|
||||||
|
if not keyword:
|
||||||
|
continue
|
||||||
|
if keyword in _EMBEDDING_CACHE:
|
||||||
|
result[keyword] = _EMBEDDING_CACHE[keyword]
|
||||||
|
else:
|
||||||
|
uncached_keywords.append(keyword)
|
||||||
|
|
||||||
|
# 2. 对未命中的词进行统一的批量推理
|
||||||
|
if uncached_keywords:
|
||||||
|
# 去重,避免同一个未缓存的词被计算多次
|
||||||
|
unique_uncached = list(dict.fromkeys(uncached_keywords))
|
||||||
|
|
||||||
|
vectors = embedder_model.encode(unique_uncached, normalize_embeddings=True, show_progress_bar=False)
|
||||||
|
|
||||||
|
# 防止缓存无限增长:超过阈值时清空最早存入的一半(简单粗暴的内存控制)
|
||||||
|
if len(_EMBEDDING_CACHE) > MAX_CACHE_SIZE:
|
||||||
|
keys_to_delete = list(_EMBEDDING_CACHE.keys())[: MAX_CACHE_SIZE // 2]
|
||||||
|
for k in keys_to_delete:
|
||||||
|
del _EMBEDDING_CACHE[k]
|
||||||
|
|
||||||
|
# 3. 将新计算的向量存入缓存并回填结果
|
||||||
|
for keyword, vec in zip(unique_uncached, vectors):
|
||||||
|
vec_array = np.asarray(vec, dtype=np.float32)
|
||||||
|
_EMBEDDING_CACHE[keyword] = vec_array
|
||||||
|
result[keyword] = vec_array
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def normalize_topic_keywords(topic_candidates: list[dict[str, Any]]) -> list[dic
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
keywords = [item["keyword"] for item in topic_candidates]
|
keywords = [item["keyword"] for item in topic_candidates]
|
||||||
vectors = embedder_model.encode(keywords, normalize_embeddings=True)
|
vectors = embedder_model.encode(keywords, normalize_embeddings=True, show_progress_bar=False)
|
||||||
|
|
||||||
clusters: list[dict[str, Any]] = []
|
clusters: list[dict[str, Any]] = []
|
||||||
for item, vector in zip(topic_candidates, vectors):
|
for item, vector in zip(topic_candidates, vectors):
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.svg">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>InsightRadar - 全网热点监控中枢</title>
|
<title>InsightRadar - 全网热点监控中枢</title>
|
||||||
<!-- Font Awesome 图标库 -->
|
<!-- Font Awesome 图标库 -->
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
|
||||||
|
<style>
|
||||||
|
/* 核心呼吸灯动画 */
|
||||||
|
.ai-core-glow {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: core-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 雷达圈旋转动画 */
|
||||||
|
.radar-ring {
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radar-ring.outer {
|
||||||
|
animation: spin-reverse 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radar-ring.inner {
|
||||||
|
animation: spin 12s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数据连线光流效果 */
|
||||||
|
.data-link {
|
||||||
|
stroke-dasharray: 4;
|
||||||
|
animation: flow 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes core-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(2.2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-reverse {
|
||||||
|
from { transform: rotate(360deg); }
|
||||||
|
to { transform: rotate(0deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flow {
|
||||||
|
from { stroke-dashoffset: 8; }
|
||||||
|
to { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<circle class="radar-ring outer" cx="16" cy="16" r="14" stroke="#3b82f6" stroke-width="1" stroke-dasharray="4 8" opacity="0.4" />
|
||||||
|
|
||||||
|
<circle class="radar-ring inner" cx="16" cy="16" r="9" stroke="#3b82f6" stroke-width="1.5" stroke-dasharray="12 4" opacity="0.6" />
|
||||||
|
|
||||||
|
<path class="data-link" d="M16 16 L25 7 M16 16 L7 22 L5 20 M16 16 L23 25" stroke="#3b82f6" stroke-width="1" opacity="0.3" />
|
||||||
|
|
||||||
|
<circle class="data-node" cx="25" cy="7" r="1.5" fill="#3b82f6" opacity="0.7" />
|
||||||
|
<circle class="data-node" cx="7" cy="22" r="1.5" fill="#3b82f6" opacity="0.7" />
|
||||||
|
<circle class="data-node" cx="23" cy="25" r="1" fill="#3b82f6" opacity="0.5" />
|
||||||
|
|
||||||
|
<circle class="ai-core" cx="16" cy="16" r="3.5" fill="#3b82f6" />
|
||||||
|
|
||||||
|
<circle class="ai-core-glow" cx="16" cy="16" r="3.5" fill="#3b82f6" opacity="0.4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -135,6 +135,9 @@ function getHotLevel(score: number): { label: string; color: string; bg: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatRelativeTime(dateStr: string): string {
|
function formatRelativeTime(dateStr: string): string {
|
||||||
|
if (!dateStr.endsWith('Z') && !dateStr.includes('+')) {
|
||||||
|
dateStr += 'Z' // 补偿 SQLite 丢失的 UTC 时区标识
|
||||||
|
}
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const target = new Date(dateStr).getTime()
|
const target = new Date(dateStr).getTime()
|
||||||
const diff = now - target
|
const diff = now - target
|
||||||
|
|||||||
@@ -47,8 +47,15 @@ function getPlatformIcon(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 格式化时间 */
|
/** 格式化时间 */
|
||||||
|
function safeParseTime(dateStr: string): number {
|
||||||
|
if (!dateStr.endsWith('Z') && !dateStr.includes('+')) {
|
||||||
|
dateStr += 'Z'
|
||||||
|
}
|
||||||
|
return new Date(dateStr).getTime()
|
||||||
|
}
|
||||||
|
|
||||||
function formatTime(dateStr: string): string {
|
function formatTime(dateStr: string): string {
|
||||||
const d = new Date(dateStr)
|
const d = new Date(safeParseTime(dateStr))
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const diff = now - d.getTime()
|
const diff = now - d.getTime()
|
||||||
const minutes = Math.floor(diff / 60000)
|
const minutes = Math.floor(diff / 60000)
|
||||||
@@ -75,7 +82,7 @@ const revisionChains = computed<RevisionChain[]>(() => {
|
|||||||
const chains: RevisionChain[] = []
|
const chains: RevisionChain[] = []
|
||||||
for (const [event_id, items] of groups) {
|
for (const [event_id, items] of groups) {
|
||||||
// 组内按时间升序
|
// 组内按时间升序
|
||||||
items.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
items.sort((a, b) => safeParseTime(a.created_at) - safeParseTime(b.created_at))
|
||||||
|
|
||||||
// 拼接标题链,避免重复(相邻记录的 revised 与下一条 previous 通常相同)
|
// 拼接标题链,避免重复(相邻记录的 revised 与下一条 previous 通常相同)
|
||||||
const titles: string[] = [items[0].previous_headline]
|
const titles: string[] = [items[0].previous_headline]
|
||||||
@@ -102,7 +109,7 @@ const revisionChains = computed<RevisionChain[]>(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 最终按最新修改时间降序
|
// 最终按最新修改时间降序
|
||||||
chains.sort((a, b) => new Date(b.last_at).getTime() - new Date(a.last_at).getTime())
|
chains.sort((a, b) => safeParseTime(b.last_at) - safeParseTime(a.last_at))
|
||||||
return chains
|
return chains
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user