feat: implement automatic format inference service with candidate generation and prediction model
- Add formatCandidateRules.ts to define rules for generating format candidates based on file features. - Introduce formatPredictionModel.ts to predict the most likely target format based on user behavior and file features. - Create inferenceService.ts to integrate all inference modules and provide a unified inference API. - Develop inference.tsx as an API endpoint for frontend calls to predict formats and engines based on file extensions. - Implement logging for conversion events and dismissals, along with user profile retrieval. - Ensure warmup management for engine predictions and provide status checks.
This commit is contained in:
parent
1173d505f7
commit
6d948b0873
18 changed files with 3710 additions and 14 deletions
|
|
@ -8,6 +8,7 @@ import { Jobs } from "../db/types";
|
|||
import { WEBROOT } from "../helpers/env";
|
||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
||||
import { userService } from "./user";
|
||||
import { inferenceService } from "../inference";
|
||||
|
||||
export const convert = new Elysia().use(userService).post(
|
||||
"/convert",
|
||||
|
|
@ -67,6 +68,12 @@ export const convert = new Elysia().use(userService).post(
|
|||
);
|
||||
|
||||
// Start the conversion process in the background
|
||||
// 記錄轉檔開始時間
|
||||
const conversionStartTime = Date.now();
|
||||
|
||||
// 取得輸入檔案的副檔名 (從第一個檔案)
|
||||
const inputExt = fileNames[0]?.split(".").pop()?.toLowerCase() ?? "";
|
||||
|
||||
handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId)
|
||||
.then(() => {
|
||||
// All conversions are done, update the job status to 'completed'
|
||||
|
|
@ -74,11 +81,41 @@ export const convert = new Elysia().use(userService).post(
|
|||
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
|
||||
}
|
||||
|
||||
// 記錄轉檔行為 (用於推斷學習)
|
||||
try {
|
||||
const durationMs = Date.now() - conversionStartTime;
|
||||
inferenceService.logConversion({
|
||||
userId: parseInt(user.id, 10),
|
||||
inputExt: inputExt,
|
||||
searchedFormat: convertTo,
|
||||
selectedEngine: converterName,
|
||||
success: true,
|
||||
durationMs: durationMs,
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn("Failed to log conversion event:", logError);
|
||||
}
|
||||
|
||||
// Delete all uploaded files in userUploadsDir
|
||||
// rmSync(userUploadsDir, { recursive: true, force: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error in conversion process:", error);
|
||||
|
||||
// 記錄失敗的轉檔
|
||||
try {
|
||||
const durationMs = Date.now() - conversionStartTime;
|
||||
inferenceService.logConversion({
|
||||
userId: parseInt(user.id, 10),
|
||||
inputExt: inputExt,
|
||||
searchedFormat: convertTo,
|
||||
selectedEngine: converterName,
|
||||
success: false,
|
||||
durationMs: durationMs,
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn("Failed to log conversion event:", logError);
|
||||
}
|
||||
});
|
||||
|
||||
// Redirect the client immediately
|
||||
|
|
|
|||
139
src/pages/inference.tsx
Normal file
139
src/pages/inference.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* 推斷 API 端點
|
||||
*
|
||||
* 提供前端呼叫的推斷 API
|
||||
*/
|
||||
|
||||
import { Elysia, t } from "elysia";
|
||||
import { inferenceService } from "../inference";
|
||||
|
||||
export const inferenceApi = new Elysia({ prefix: "/inference" })
|
||||
/**
|
||||
* 根據副檔名推斷最可能的目標格式和引擎
|
||||
*/
|
||||
.post(
|
||||
"/predict",
|
||||
async ({ body, cookie: { user_id } }) => {
|
||||
const userId = parseInt(String(user_id?.value ?? "0"), 10);
|
||||
|
||||
try {
|
||||
const result = await inferenceService.inferFromExtension(
|
||||
body.ext,
|
||||
userId,
|
||||
body.file_size_kb,
|
||||
body.available_engines,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
format: result.format,
|
||||
engine: result.engine,
|
||||
should_auto_fill: result.should_auto_fill,
|
||||
warmup_status: result.warmup_status,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Inference prediction error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to predict format",
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
ext: t.String({ description: "輸入檔案副檔名" }),
|
||||
file_size_kb: t.Optional(t.Number({ description: "檔案大小 (KB)" })),
|
||||
available_engines: t.Optional(t.Array(t.String(), { description: "可用引擎列表" })),
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 記錄推薦被拒絕事件
|
||||
*/
|
||||
.post(
|
||||
"/dismiss",
|
||||
async ({ body, cookie: { user_id } }) => {
|
||||
const userId = parseInt(String(user_id?.value ?? "0"), 10);
|
||||
|
||||
try {
|
||||
const dismissParams: {
|
||||
userId: number;
|
||||
inputExt: string;
|
||||
dismissedFormat: string;
|
||||
dismissedEngine?: string;
|
||||
} = {
|
||||
userId,
|
||||
inputExt: body.input_ext,
|
||||
dismissedFormat: body.dismissed_format,
|
||||
};
|
||||
|
||||
if (body.dismissed_engine !== undefined) {
|
||||
dismissParams.dismissedEngine = body.dismissed_engine;
|
||||
}
|
||||
|
||||
inferenceService.logDismiss(dismissParams);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to log dismiss event:", error);
|
||||
return { success: false };
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
input_ext: t.String({ description: "輸入檔案副檔名" }),
|
||||
dismissed_format: t.String({ description: "被拒絕的推薦格式" }),
|
||||
dismissed_engine: t.Optional(t.String({ description: "被拒絕的推薦引擎" })),
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 取消預調用
|
||||
*/
|
||||
.post("/cancel-warmup", () => {
|
||||
try {
|
||||
inferenceService.cancelWarmup();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to cancel warmup:", error);
|
||||
return { success: false };
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 取得預調用狀態
|
||||
*/
|
||||
.get("/warmup-status", () => {
|
||||
try {
|
||||
const status = inferenceService.getWarmupStatus();
|
||||
return {
|
||||
success: true,
|
||||
data: status,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to get warmup status:", error);
|
||||
return { success: false, data: null };
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 取得使用者 Profile
|
||||
*/
|
||||
.get("/profile", ({ cookie: { user_id } }) => {
|
||||
const userId = parseInt(String(user_id?.value ?? "0"), 10);
|
||||
|
||||
try {
|
||||
const profile = inferenceService.getUserProfile(userId);
|
||||
return {
|
||||
success: true,
|
||||
data: profile,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to get user profile:", error);
|
||||
return { success: false, data: null };
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue