Add admin vision lab and LLM vision verification

这个提交包含在:
cryptocommuniums-afk
2026-03-15 00:41:09 +08:00
父节点 20e183d2da
当前提交 ad83ce9c68
修改 18 个文件,包含 915 行新增16 行删除

查看文件

@@ -15,6 +15,8 @@ import {
trainingReminders, InsertTrainingReminder,
notificationLog, InsertNotificationLog,
backgroundTasks, InsertBackgroundTask,
visionReferenceImages, InsertVisionReferenceImage,
visionTestRuns, InsertVisionTestRun,
} from "../drizzle/schema";
import { ENV } from './_core/env';
@@ -89,8 +91,9 @@ export async function createUsernameAccount(username: string): Promise<{ user: t
if (existing.length > 0) {
const user = await db.select().from(users).where(eq(users.id, existing[0].userId)).limit(1);
if (user.length > 0) {
await db.update(users).set({ lastSignedIn: new Date() }).where(eq(users.id, user[0].id));
return { user: user[0], isNew: false };
const updatedRole = ENV.adminUsernames.includes(username) ? "admin" : user[0].role;
await db.update(users).set({ lastSignedIn: new Date(), role: updatedRole }).where(eq(users.id, user[0].id));
return { user: { ...user[0], role: updatedRole, lastSignedIn: new Date() }, isNew: false };
}
}
@@ -100,6 +103,7 @@ export async function createUsernameAccount(username: string): Promise<{ user: t
openId,
name: username,
loginMethod: "username",
role: ENV.adminUsernames.includes(username) ? "admin" : "user",
lastSignedIn: new Date(),
ntrpRating: 1.5,
totalSessions: 0,
@@ -443,6 +447,223 @@ export async function getLeaderboard(sortBy: "ntrpRating" | "totalMinutes" | "to
}).from(users).orderBy(desc(sortColumn)).limit(limit);
}
// ===== VISION REFERENCE LIBRARY =====
export const VISION_REFERENCE_SEED_DATA: Omit<
InsertVisionReferenceImage,
"id" | "createdAt" | "updatedAt"
>[] = [
{
slug: "commons-forehand-tennispictures",
title: "标准图:正手挥拍",
exerciseType: "forehand",
imageUrl: "https://commons.wikimedia.org/wiki/Special:FilePath/Forehand.jpg",
sourcePageUrl: "https://commons.wikimedia.org/wiki/File:Forehand.jpg",
sourceLabel: "Wikimedia Commons",
author: "Tennispictures",
license: "CC0 1.0",
expectedFocus: ["引拍完整", "击球臂路径", "肩髋转动", "重心转移"],
tags: ["forehand", "reference", "commons", "stroke"],
notes: "用于检测模型对正手引拍、发力和随挥阶段的描述能力。",
sortOrder: 1,
isPublished: 1,
},
{
slug: "commons-backhand-federer",
title: "标准图:反手挥拍",
exerciseType: "backhand",
imageUrl: "https://commons.wikimedia.org/wiki/Special:FilePath/Backhand_Federer.jpg",
sourcePageUrl: "https://commons.wikimedia.org/wiki/File:Backhand_Federer.jpg",
sourceLabel: "Wikimedia Commons",
author: "Ian Gampon",
license: "CC BY 2.0",
expectedFocus: ["非持拍手收回", "躯干旋转", "拍面路径", "击球点位置"],
tags: ["backhand", "reference", "commons", "stroke"],
notes: "用于检测模型对单反/反手击球阶段和身体协同的判断。",
sortOrder: 2,
isPublished: 1,
},
{
slug: "commons-serena-serve",
title: "标准图:发球",
exerciseType: "serve",
imageUrl: "https://commons.wikimedia.org/wiki/Special:FilePath/Serena_Williams_Serves.JPG",
sourcePageUrl: "https://commons.wikimedia.org/wiki/File:Serena_Williams_Serves.JPG",
sourceLabel: "Wikimedia Commons",
author: "Clavecin",
license: "Public Domain",
expectedFocus: ["抛球与击球点", "肩肘链条", "躯干伸展", "落地重心"],
tags: ["serve", "reference", "commons", "overhead"],
notes: "用于检测模型对发球上举、鞭打和击球点的识别能力。",
sortOrder: 3,
isPublished: 1,
},
{
slug: "commons-volley-lewis",
title: "标准图:网前截击",
exerciseType: "volley",
imageUrl: "https://commons.wikimedia.org/wiki/Special:FilePath/Ernest_w._lewis,_volleying.jpg",
sourcePageUrl: "https://commons.wikimedia.org/wiki/File:Ernest_w._lewis,_volleying.jpg",
sourceLabel: "Wikimedia Commons",
author: "Unknown author",
license: "Public Domain",
expectedFocus: ["拍头稳定", "准备姿态", "身体前压", "短促触球"],
tags: ["volley", "reference", "commons", "net-play"],
notes: "用于检测模型对截击站位和紧凑击球结构的识别能力。",
sortOrder: 4,
isPublished: 1,
},
{
slug: "commons-tiafoe-backhand",
title: "标准图:现代反手参考",
exerciseType: "backhand",
imageUrl: "https://commons.wikimedia.org/wiki/Special:FilePath/Frances_Tiafoe_Backhand.jpg",
sourcePageUrl: "https://commons.wikimedia.org/wiki/File:Frances_Tiafoe_Backhand.jpg",
sourceLabel: "Wikimedia Commons",
author: null,
license: "Wikimedia Commons file license",
expectedFocus: ["双手协同", "脚步支撑", "肩髋分离", "随挥方向"],
tags: ["backhand", "reference", "commons", "modern"],
notes: "补充现代职业选手反手样本,便于比较传统与现代动作语言。",
sortOrder: 5,
isPublished: 1,
},
];
export async function seedVisionReferenceImages() {
const db = await getDb();
if (!db) return;
const existing = await db.select().from(visionReferenceImages).limit(1);
if (existing.length > 0) return;
for (const item of VISION_REFERENCE_SEED_DATA) {
await db.insert(visionReferenceImages).values(item);
}
}
export async function listVisionReferenceImages() {
const db = await getDb();
if (!db) return [];
return db.select().from(visionReferenceImages)
.where(eq(visionReferenceImages.isPublished, 1))
.orderBy(asc(visionReferenceImages.sortOrder), asc(visionReferenceImages.id));
}
export async function getVisionReferenceImageById(id: number) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(visionReferenceImages)
.where(eq(visionReferenceImages.id, id))
.limit(1);
return result[0];
}
export async function createVisionTestRun(run: InsertVisionTestRun) {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(visionTestRuns).values(run);
return result[0].insertId;
}
export async function listVisionTestRuns(userId?: number, limit = 50) {
const db = await getDb();
if (!db) return [];
const query = db.select({
id: visionTestRuns.id,
taskId: visionTestRuns.taskId,
userId: visionTestRuns.userId,
userName: users.name,
referenceImageId: visionTestRuns.referenceImageId,
referenceTitle: visionReferenceImages.title,
title: visionTestRuns.title,
exerciseType: visionTestRuns.exerciseType,
imageUrl: visionTestRuns.imageUrl,
status: visionTestRuns.status,
visionStatus: visionTestRuns.visionStatus,
configuredModel: visionTestRuns.configuredModel,
expectedFocus: visionTestRuns.expectedFocus,
summary: visionTestRuns.summary,
corrections: visionTestRuns.corrections,
report: visionTestRuns.report,
warning: visionTestRuns.warning,
error: visionTestRuns.error,
createdAt: visionTestRuns.createdAt,
updatedAt: visionTestRuns.updatedAt,
}).from(visionTestRuns)
.leftJoin(users, eq(users.id, visionTestRuns.userId))
.leftJoin(visionReferenceImages, eq(visionReferenceImages.id, visionTestRuns.referenceImageId))
.orderBy(desc(visionTestRuns.createdAt))
.limit(limit);
if (userId == null) {
return query;
}
return db.select({
id: visionTestRuns.id,
taskId: visionTestRuns.taskId,
userId: visionTestRuns.userId,
userName: users.name,
referenceImageId: visionTestRuns.referenceImageId,
referenceTitle: visionReferenceImages.title,
title: visionTestRuns.title,
exerciseType: visionTestRuns.exerciseType,
imageUrl: visionTestRuns.imageUrl,
status: visionTestRuns.status,
visionStatus: visionTestRuns.visionStatus,
configuredModel: visionTestRuns.configuredModel,
expectedFocus: visionTestRuns.expectedFocus,
summary: visionTestRuns.summary,
corrections: visionTestRuns.corrections,
report: visionTestRuns.report,
warning: visionTestRuns.warning,
error: visionTestRuns.error,
createdAt: visionTestRuns.createdAt,
updatedAt: visionTestRuns.updatedAt,
}).from(visionTestRuns)
.leftJoin(users, eq(users.id, visionTestRuns.userId))
.leftJoin(visionReferenceImages, eq(visionReferenceImages.id, visionTestRuns.referenceImageId))
.where(eq(visionTestRuns.userId, userId))
.orderBy(desc(visionTestRuns.createdAt))
.limit(limit);
}
export async function completeVisionTestRun(taskId: string, data: {
visionStatus: "ok" | "fallback";
summary?: string | null;
corrections: string;
report?: unknown;
warning?: string | null;
}) {
const db = await getDb();
if (!db) return;
await db.update(visionTestRuns).set({
status: "succeeded",
visionStatus: data.visionStatus,
summary: data.summary ?? null,
corrections: data.corrections,
report: data.report ?? null,
warning: data.warning ?? null,
error: null,
}).where(eq(visionTestRuns.taskId, taskId));
}
export async function failVisionTestRun(taskId: string, error: string) {
const db = await getDb();
if (!db) return;
await db.update(visionTestRuns).set({
status: "failed",
visionStatus: "failed",
error,
}).where(eq(visionTestRuns.taskId, taskId));
}
// ===== TUTORIAL OPERATIONS =====
export const TUTORIAL_SEED_DATA: Omit<InsertTutorialVideo, "id">[] = [