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:
Your Name 2026-01-26 12:47:16 +08:00
parent 1173d505f7
commit 6d948b0873
18 changed files with 3710 additions and 14 deletions

View file

@ -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
View 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 };
}
});