feat: 更新推斷模組以使用 search token 進行格式推斷,並新增格式搜尋字詞詞庫
This commit is contained in:
parent
6d948b0873
commit
fcc9e36220
7 changed files with 515 additions and 48 deletions
11
.github/workflows/docker-e2e-tests.yml
vendored
11
.github/workflows/docker-e2e-tests.yml
vendored
|
|
@ -287,6 +287,17 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Free disk space (aggressive)
|
||||||
|
uses: jlumbroso/free-disk-space@main
|
||||||
|
with:
|
||||||
|
tool-cache: true
|
||||||
|
android: true
|
||||||
|
dotnet: true
|
||||||
|
haskell: true
|
||||||
|
large-packages: true
|
||||||
|
docker-images: true
|
||||||
|
swap-storage: false
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
/**
|
/**
|
||||||
* 自動格式推斷前端模組
|
* 智慧搜尋代理 - 前端模組
|
||||||
*
|
*
|
||||||
* 在使用者上傳檔案後,自動推斷最可能的目標格式並填入搜尋欄
|
* 在使用者上傳檔案後,自動推斷最可能的目標格式
|
||||||
|
* 並模擬使用者在搜尋欄輸入 token (prefix matching)
|
||||||
|
*
|
||||||
|
* UI 行為完全等同真人輸入
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} FormatPrediction
|
* @typedef {Object} FormatPrediction
|
||||||
* @property {string} search_format - 預測的搜尋格式
|
* @property {string} search_token - 預測的搜尋 token (用於 prefix matching)
|
||||||
* @property {number} confidence - 預測信心度 (0-1)
|
* @property {number} confidence - 預測信心度 (0-1)
|
||||||
* @property {Array<{format: string, score: number}>} top_k - Top-K 候選格式
|
* @property {Array<{token: string, score: number}>} top_k - Top-K 候選 token
|
||||||
* @property {string[]} reason_codes - 預測原因碼
|
* @property {string[]} reason_codes - 預測原因碼
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -39,7 +42,7 @@ const inferenceWebroot = inferenceWebrootMeta
|
||||||
// 狀態追蹤
|
// 狀態追蹤
|
||||||
let inferenceEnabled = true;
|
let inferenceEnabled = true;
|
||||||
/** @type {string|null} */
|
/** @type {string|null} */
|
||||||
let lastInferredFormat = null;
|
let lastInferredToken = null;
|
||||||
/** @type {string|null} */
|
/** @type {string|null} */
|
||||||
let lastInferredEngine = null;
|
let lastInferredEngine = null;
|
||||||
let isInferredValue = false;
|
let isInferredValue = false;
|
||||||
|
|
@ -119,10 +122,11 @@ async function cancelWarmup() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自動填入推斷的格式
|
* 自動填入推斷的格式
|
||||||
* @param {string} format - 推斷的格式
|
* UI 行為完全等同使用者手動輸入
|
||||||
|
* @param {string} token - 推斷的 search token
|
||||||
* @param {string} [engine] - 推斷的引擎
|
* @param {string} [engine] - 推斷的引擎
|
||||||
*/
|
*/
|
||||||
function autoFillInferredFormat(format, engine) {
|
function autoFillInferredFormat(token, engine) {
|
||||||
/** @type {HTMLInputElement|null} */
|
/** @type {HTMLInputElement|null} */
|
||||||
const searchInput = document.querySelector("input[name='convert_to_search']");
|
const searchInput = document.querySelector("input[name='convert_to_search']");
|
||||||
const convertToPopup = document.querySelector(".convert_to_popup");
|
const convertToPopup = document.querySelector(".convert_to_popup");
|
||||||
|
|
@ -133,30 +137,20 @@ function autoFillInferredFormat(format, engine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 儲存推斷值
|
// 儲存推斷值
|
||||||
lastInferredFormat = format;
|
lastInferredToken = token;
|
||||||
lastInferredEngine = engine || null;
|
lastInferredEngine = engine || null;
|
||||||
isInferredValue = true;
|
isInferredValue = true;
|
||||||
|
|
||||||
// 填入搜尋欄
|
// 填入搜尋欄 - UI 行為完全等同使用者輸入
|
||||||
searchInput.value = format;
|
searchInput.value = token;
|
||||||
|
|
||||||
// 觸發 input 事件以過濾結果
|
// 觸發 input 事件以過濾結果
|
||||||
const inputEvent = new Event("input", { bubbles: true });
|
const inputEvent = new Event("input", { bubbles: true });
|
||||||
searchInput.dispatchEvent(inputEvent);
|
searchInput.dispatchEvent(inputEvent);
|
||||||
|
|
||||||
// 添加視覺提示 (使用淡色邊框)
|
// 不修改任何 UI 樣式 - 純粹模擬使用者輸入
|
||||||
searchInput.style.borderColor = "#22c55e";
|
|
||||||
searchInput.style.borderWidth = "2px";
|
|
||||||
|
|
||||||
// 3秒後恢復原樣
|
console.log(`🎯 Auto-filled search token: ${token}${engine ? ` (engine: ${engine})` : ""}`);
|
||||||
setTimeout(() => {
|
|
||||||
if (isInferredValue && searchInput.value === format) {
|
|
||||||
searchInput.style.borderColor = "";
|
|
||||||
searchInput.style.borderWidth = "";
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
console.log(`🎯 Auto-filled format: ${format}${engine ? ` (engine: ${engine})` : ""}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -164,19 +158,19 @@ function autoFillInferredFormat(format, engine) {
|
||||||
* @param {string} inputExt - 輸入副檔名
|
* @param {string} inputExt - 輸入副檔名
|
||||||
*/
|
*/
|
||||||
function handleSearchClear(inputExt) {
|
function handleSearchClear(inputExt) {
|
||||||
if (isInferredValue && lastInferredFormat) {
|
if (isInferredValue && lastInferredToken) {
|
||||||
// 記錄為負樣本
|
// 記錄為負樣本
|
||||||
logDismissEvent(inputExt, lastInferredFormat, lastInferredEngine || undefined);
|
logDismissEvent(inputExt, lastInferredToken, lastInferredEngine || undefined);
|
||||||
|
|
||||||
// 取消預調用
|
// 取消預調用
|
||||||
cancelWarmup();
|
cancelWarmup();
|
||||||
|
|
||||||
console.log(`❌ User dismissed inference: ${lastInferredFormat}`);
|
console.log(`❌ User dismissed inference: ${lastInferredToken}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置狀態
|
// 重置狀態
|
||||||
isInferredValue = false;
|
isInferredValue = false;
|
||||||
lastInferredFormat = null;
|
lastInferredToken = null;
|
||||||
lastInferredEngine = null;
|
lastInferredEngine = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,14 +182,6 @@ function handleManualInput() {
|
||||||
// 使用者手動修改,取消預調用
|
// 使用者手動修改,取消預調用
|
||||||
cancelWarmup();
|
cancelWarmup();
|
||||||
isInferredValue = false;
|
isInferredValue = false;
|
||||||
|
|
||||||
// 恢復邊框樣式
|
|
||||||
/** @type {HTMLInputElement|null} */
|
|
||||||
const searchInput = document.querySelector("input[name='convert_to_search']");
|
|
||||||
if (searchInput) {
|
|
||||||
searchInput.style.borderColor = "";
|
|
||||||
searchInput.style.borderWidth = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +207,7 @@ function initInferenceModule() {
|
||||||
// 如果是程式設定的值,不處理
|
// 如果是程式設定的值,不處理
|
||||||
if (e.isTrusted && isInferredValue) {
|
if (e.isTrusted && isInferredValue) {
|
||||||
const currentValue = searchInput.value;
|
const currentValue = searchInput.value;
|
||||||
if (currentValue !== lastInferredFormat) {
|
if (currentValue !== lastInferredToken) {
|
||||||
handleManualInput();
|
handleManualInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -439,15 +439,15 @@ async function triggerFormatInference(ext, fileSize) {
|
||||||
const result = await window.inferenceModule.requestFormatInference(ext, fileSizeKb);
|
const result = await window.inferenceModule.requestFormatInference(ext, fileSizeKb);
|
||||||
|
|
||||||
if (result && result.should_auto_fill && result.format) {
|
if (result && result.should_auto_fill && result.format) {
|
||||||
// 自動填入推斷的格式
|
// 自動填入推斷的 search token (模擬使用者輸入)
|
||||||
window.inferenceModule.autoFillInferredFormat(
|
window.inferenceModule.autoFillInferredFormat(
|
||||||
result.format.search_format,
|
result.format.search_token,
|
||||||
result.engine?.engine,
|
result.engine?.engine,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 嘗試自動選擇對應的引擎選項
|
// 嘗試自動選擇對應的引擎選項
|
||||||
if (result.engine) {
|
if (result.engine) {
|
||||||
autoSelectEngine(result.format.search_format, result.engine.engine);
|
autoSelectEngine(result.format.search_token, result.engine.engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
* 格式預測模型
|
* 格式預測模型
|
||||||
*
|
*
|
||||||
* 基於使用者行為歷史和檔案特徵預測最可能的目標格式
|
* 基於使用者行為歷史和檔案特徵預測最可能的目標格式
|
||||||
* 使用輕量級的邏輯回歸模型 + 規則融合
|
* 輸出 search_token 用於 prefix matching 搜尋
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateCandidates } from "./formatCandidateRules";
|
import { generateCandidates } from "./formatCandidateRules";
|
||||||
|
import { normalizeToken } from "./tokenLexicon";
|
||||||
import type { FileFeatures } from "./featureExtraction";
|
import type { FileFeatures } from "./featureExtraction";
|
||||||
import type { UserProfile, FormatConversionStats } from "./behaviorStore";
|
import type { UserProfile, FormatConversionStats } from "./behaviorStore";
|
||||||
|
|
||||||
|
|
@ -13,12 +14,12 @@ import type { UserProfile, FormatConversionStats } from "./behaviorStore";
|
||||||
* 格式預測結果
|
* 格式預測結果
|
||||||
*/
|
*/
|
||||||
export interface FormatPrediction {
|
export interface FormatPrediction {
|
||||||
/** 預測的搜尋格式 */
|
/** 預測的搜尋 token (用於 prefix matching) */
|
||||||
search_format: string;
|
search_token: string;
|
||||||
/** 預測信心度 (0-1) */
|
/** 預測信心度 (0-1) */
|
||||||
confidence: number;
|
confidence: number;
|
||||||
/** Top-K 候選格式 */
|
/** Top-K 候選 token */
|
||||||
top_k: Array<{ format: string; score: number }>;
|
top_k: Array<{ token: string; score: number }>;
|
||||||
/** 預測原因碼 */
|
/** 預測原因碼 */
|
||||||
reason_codes: string[];
|
reason_codes: string[];
|
||||||
}
|
}
|
||||||
|
|
@ -76,15 +77,18 @@ export class FormatPredictionModel {
|
||||||
private weights: ModelWeights;
|
private weights: ModelWeights;
|
||||||
private globalPopularity: Record<string, number>;
|
private globalPopularity: Record<string, number>;
|
||||||
private minConfidenceThreshold: number;
|
private minConfidenceThreshold: number;
|
||||||
|
private coldStartThreshold: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
weights: Partial<ModelWeights> = {},
|
weights: Partial<ModelWeights> = {},
|
||||||
globalPopularity: Record<string, number> = DEFAULT_GLOBAL_POPULARITY,
|
globalPopularity: Record<string, number> = DEFAULT_GLOBAL_POPULARITY,
|
||||||
minConfidenceThreshold = 0.4,
|
minConfidenceThreshold = 0.35,
|
||||||
|
coldStartThreshold = 0.15,
|
||||||
) {
|
) {
|
||||||
this.weights = { ...DEFAULT_WEIGHTS, ...weights };
|
this.weights = { ...DEFAULT_WEIGHTS, ...weights };
|
||||||
this.globalPopularity = globalPopularity;
|
this.globalPopularity = globalPopularity;
|
||||||
this.minConfidenceThreshold = minConfidenceThreshold;
|
this.minConfidenceThreshold = minConfidenceThreshold;
|
||||||
|
this.coldStartThreshold = coldStartThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -171,7 +175,7 @@ export class FormatPredictionModel {
|
||||||
// 排序並取 Top-K
|
// 排序並取 Top-K
|
||||||
scoredFormats.sort((a, b) => b.score - a.score);
|
scoredFormats.sort((a, b) => b.score - a.score);
|
||||||
const topK = scoredFormats.slice(0, 5).map((s) => ({
|
const topK = scoredFormats.slice(0, 5).map((s) => ({
|
||||||
format: s.format,
|
token: normalizeToken(s.format),
|
||||||
score: Math.round(s.score * 100) / 100,
|
score: Math.round(s.score * 100) / 100,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -184,13 +188,20 @@ export class FormatPredictionModel {
|
||||||
// 計算信心度 (使用 softmax 正規化後的相對優勢)
|
// 計算信心度 (使用 softmax 正規化後的相對優勢)
|
||||||
const confidence = this.calculateConfidence(scoredFormats);
|
const confidence = this.calculateConfidence(scoredFormats);
|
||||||
|
|
||||||
|
// 根據是否有使用者歷史選擇不同的閾值
|
||||||
|
// 冷啟動時使用較低閾值,讓系統可以開始學習
|
||||||
|
const hasUserHistory = userProfile && Object.keys(userProfile.format_preferences).length > 0;
|
||||||
|
const effectiveThreshold = hasUserHistory
|
||||||
|
? this.minConfidenceThreshold
|
||||||
|
: this.coldStartThreshold;
|
||||||
|
|
||||||
// 如果信心度低於閾值,不推薦
|
// 如果信心度低於閾值,不推薦
|
||||||
if (confidence < this.minConfidenceThreshold) {
|
if (confidence < effectiveThreshold) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
search_format: topFormat.format,
|
search_token: normalizeToken(topFormat.format),
|
||||||
confidence: Math.round(confidence * 100) / 100,
|
confidence: Math.round(confidence * 100) / 100,
|
||||||
top_k: topK,
|
top_k: topK,
|
||||||
reason_codes: matched_rules,
|
reason_codes: matched_rules,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* 推斷模組統一導出
|
* 推斷模組統一導出
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./tokenLexicon";
|
||||||
export * from "./featureExtraction";
|
export * from "./featureExtraction";
|
||||||
export * from "./formatCandidateRules";
|
export * from "./formatCandidateRules";
|
||||||
export * from "./formatPredictionModel";
|
export * from "./formatPredictionModel";
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ export class InferenceService {
|
||||||
let enginePrediction: EnginePrediction | null = null;
|
let enginePrediction: EnginePrediction | null = null;
|
||||||
if (formatPrediction) {
|
if (formatPrediction) {
|
||||||
enginePrediction = this.engineModel.predict(
|
enginePrediction = this.engineModel.predict(
|
||||||
formatPrediction.search_format,
|
formatPrediction.search_token,
|
||||||
features,
|
features,
|
||||||
userProfile,
|
userProfile,
|
||||||
availableEngines,
|
availableEngines,
|
||||||
|
|
|
||||||
458
src/inference/tokenLexicon.ts
Normal file
458
src/inference/tokenLexicon.ts
Normal file
|
|
@ -0,0 +1,458 @@
|
||||||
|
/**
|
||||||
|
* Token Lexicon - 格式搜尋字詞詞庫
|
||||||
|
*
|
||||||
|
* 定義所有可搜尋的格式 token,用於:
|
||||||
|
* 1. 標準化格式名稱
|
||||||
|
* 2. 支援別名搜尋
|
||||||
|
* 3. 分類格式家族
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式 Token 定義
|
||||||
|
*/
|
||||||
|
export interface FormatToken {
|
||||||
|
/** 主要 token (用於搜尋) */
|
||||||
|
token: string;
|
||||||
|
/** 格式家族 */
|
||||||
|
family: "image" | "video" | "audio" | "document" | "data" | "3d" | "archive" | "font" | "other";
|
||||||
|
/** 別名列表 (也會觸發搜尋匹配) */
|
||||||
|
aliases: string[];
|
||||||
|
/** 顯示名稱 */
|
||||||
|
display_name: string;
|
||||||
|
/** 是否為常用格式 */
|
||||||
|
is_common: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format Lexicon - 所有可搜尋的格式 token
|
||||||
|
*/
|
||||||
|
export const FORMAT_LEXICON: FormatToken[] = [
|
||||||
|
// ==================== 圖片格式 ====================
|
||||||
|
{
|
||||||
|
token: "png",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["portable network graphics"],
|
||||||
|
display_name: "PNG",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "jpeg",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["jpg", "jpe", "jfif"],
|
||||||
|
display_name: "JPEG",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "webp",
|
||||||
|
family: "image",
|
||||||
|
aliases: [],
|
||||||
|
display_name: "WebP",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "gif",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["graphics interchange format"],
|
||||||
|
display_name: "GIF",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "avif",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["av1 image"],
|
||||||
|
display_name: "AVIF",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "svg",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["svgz", "scalable vector graphics"],
|
||||||
|
display_name: "SVG",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "ico",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["icon", "favicon"],
|
||||||
|
display_name: "ICO",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "bmp",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["bitmap", "dib"],
|
||||||
|
display_name: "BMP",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "tiff",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["tif"],
|
||||||
|
display_name: "TIFF",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "heif",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["heic", "heics"],
|
||||||
|
display_name: "HEIF",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "jxl",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["jpeg xl"],
|
||||||
|
display_name: "JPEG XL",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "psd",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["photoshop"],
|
||||||
|
display_name: "PSD",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "eps",
|
||||||
|
family: "image",
|
||||||
|
aliases: ["encapsulated postscript"],
|
||||||
|
display_name: "EPS",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 影片格式 ====================
|
||||||
|
{
|
||||||
|
token: "mp4",
|
||||||
|
family: "video",
|
||||||
|
aliases: ["m4v", "mpeg4"],
|
||||||
|
display_name: "MP4",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "webm",
|
||||||
|
family: "video",
|
||||||
|
aliases: [],
|
||||||
|
display_name: "WebM",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "mkv",
|
||||||
|
family: "video",
|
||||||
|
aliases: ["matroska"],
|
||||||
|
display_name: "MKV",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "avi",
|
||||||
|
family: "video",
|
||||||
|
aliases: [],
|
||||||
|
display_name: "AVI",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "mov",
|
||||||
|
family: "video",
|
||||||
|
aliases: ["quicktime"],
|
||||||
|
display_name: "MOV",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 音訊格式 ====================
|
||||||
|
{
|
||||||
|
token: "mp3",
|
||||||
|
family: "audio",
|
||||||
|
aliases: ["mpeg audio layer 3"],
|
||||||
|
display_name: "MP3",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "wav",
|
||||||
|
family: "audio",
|
||||||
|
aliases: ["wave", "riff"],
|
||||||
|
display_name: "WAV",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "flac",
|
||||||
|
family: "audio",
|
||||||
|
aliases: ["free lossless audio codec"],
|
||||||
|
display_name: "FLAC",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "aac",
|
||||||
|
family: "audio",
|
||||||
|
aliases: ["m4a"],
|
||||||
|
display_name: "AAC",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "ogg",
|
||||||
|
family: "audio",
|
||||||
|
aliases: ["oga", "vorbis"],
|
||||||
|
display_name: "OGG",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "opus",
|
||||||
|
family: "audio",
|
||||||
|
aliases: [],
|
||||||
|
display_name: "Opus",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 文件格式 ====================
|
||||||
|
{
|
||||||
|
token: "pdf",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["portable document format"],
|
||||||
|
display_name: "PDF",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "docx",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["doc", "word"],
|
||||||
|
display_name: "DOCX",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "txt",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["text", "plain text"],
|
||||||
|
display_name: "TXT",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "md",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["markdown"],
|
||||||
|
display_name: "Markdown",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "html",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["htm", "webpage"],
|
||||||
|
display_name: "HTML",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "rtf",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["rich text"],
|
||||||
|
display_name: "RTF",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "epub",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["ebook"],
|
||||||
|
display_name: "EPUB",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "mobi",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["kindle"],
|
||||||
|
display_name: "MOBI",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "xlsx",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["xls", "excel", "spreadsheet"],
|
||||||
|
display_name: "XLSX",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "pptx",
|
||||||
|
family: "document",
|
||||||
|
aliases: ["ppt", "powerpoint", "presentation"],
|
||||||
|
display_name: "PPTX",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 資料格式 ====================
|
||||||
|
{
|
||||||
|
token: "json",
|
||||||
|
family: "data",
|
||||||
|
aliases: ["javascript object notation"],
|
||||||
|
display_name: "JSON",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "yaml",
|
||||||
|
family: "data",
|
||||||
|
aliases: ["yml"],
|
||||||
|
display_name: "YAML",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "xml",
|
||||||
|
family: "data",
|
||||||
|
aliases: ["extensible markup language"],
|
||||||
|
display_name: "XML",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "csv",
|
||||||
|
family: "data",
|
||||||
|
aliases: ["comma separated values"],
|
||||||
|
display_name: "CSV",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "toml",
|
||||||
|
family: "data",
|
||||||
|
aliases: [],
|
||||||
|
display_name: "TOML",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 3D 格式 ====================
|
||||||
|
{
|
||||||
|
token: "obj",
|
||||||
|
family: "3d",
|
||||||
|
aliases: ["wavefront"],
|
||||||
|
display_name: "OBJ",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "stl",
|
||||||
|
family: "3d",
|
||||||
|
aliases: ["stereolithography"],
|
||||||
|
display_name: "STL",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "gltf",
|
||||||
|
family: "3d",
|
||||||
|
aliases: ["glb", "gl transmission format"],
|
||||||
|
display_name: "glTF",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "fbx",
|
||||||
|
family: "3d",
|
||||||
|
aliases: ["filmbox"],
|
||||||
|
display_name: "FBX",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "dae",
|
||||||
|
family: "3d",
|
||||||
|
aliases: ["collada"],
|
||||||
|
display_name: "DAE",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "ply",
|
||||||
|
family: "3d",
|
||||||
|
aliases: ["polygon file format"],
|
||||||
|
display_name: "PLY",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 壓縮格式 ====================
|
||||||
|
{
|
||||||
|
token: "zip",
|
||||||
|
family: "archive",
|
||||||
|
aliases: [],
|
||||||
|
display_name: "ZIP",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "tar",
|
||||||
|
family: "archive",
|
||||||
|
aliases: ["tarball"],
|
||||||
|
display_name: "TAR",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "7z",
|
||||||
|
family: "archive",
|
||||||
|
aliases: ["7zip"],
|
||||||
|
display_name: "7Z",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 字體格式 ====================
|
||||||
|
{
|
||||||
|
token: "ttf",
|
||||||
|
family: "font",
|
||||||
|
aliases: ["truetype"],
|
||||||
|
display_name: "TTF",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "otf",
|
||||||
|
family: "font",
|
||||||
|
aliases: ["opentype"],
|
||||||
|
display_name: "OTF",
|
||||||
|
is_common: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "woff",
|
||||||
|
family: "font",
|
||||||
|
aliases: ["web open font format"],
|
||||||
|
display_name: "WOFF",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "woff2",
|
||||||
|
family: "font",
|
||||||
|
aliases: [],
|
||||||
|
display_name: "WOFF2",
|
||||||
|
is_common: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 建立索引
|
||||||
|
const TOKEN_INDEX = new Map<string, FormatToken>();
|
||||||
|
const ALIAS_INDEX = new Map<string, FormatToken>();
|
||||||
|
|
||||||
|
for (const token of FORMAT_LEXICON) {
|
||||||
|
TOKEN_INDEX.set(token.token, token);
|
||||||
|
for (const alias of token.aliases) {
|
||||||
|
ALIAS_INDEX.set(alias.toLowerCase(), token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根據 token 取得格式定義
|
||||||
|
*/
|
||||||
|
export function getTokenByName(name: string): FormatToken | undefined {
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
return TOKEN_INDEX.get(lower) ?? ALIAS_INDEX.get(lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得指定家族的所有 token
|
||||||
|
*/
|
||||||
|
export function getTokensByFamily(family: FormatToken["family"]): FormatToken[] {
|
||||||
|
return FORMAT_LEXICON.filter((t) => t.family === family);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得所有常用格式
|
||||||
|
*/
|
||||||
|
export function getCommonTokens(): FormatToken[] {
|
||||||
|
return FORMAT_LEXICON.filter((t) => t.is_common);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 檢查是否為有效的 token
|
||||||
|
*/
|
||||||
|
export function isValidToken(token: string): boolean {
|
||||||
|
return TOKEN_INDEX.has(token.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 標準化 token (將別名轉換為主要 token)
|
||||||
|
*/
|
||||||
|
export function normalizeToken(input: string): string {
|
||||||
|
const lower = input.toLowerCase();
|
||||||
|
const token = TOKEN_INDEX.get(lower) ?? ALIAS_INDEX.get(lower);
|
||||||
|
return token?.token ?? lower;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue