Spaces:
Running
Running
Update index.html
Browse files- index.html +81 -47
index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>單字閃卡 (
|
| 7 |
<!-- 載入 Tailwind CSS CDN -->
|
| 8 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
<!-- 載入彩帶效果庫 -->
|
|
@@ -434,7 +434,7 @@
|
|
| 434 |
</button>
|
| 435 |
<button id="hw-recognize-btn" class="flex-[2] py-3 bg-indigo-600 text-white rounded-xl font-bold hover:bg-indigo-700 shadow-md transition-colors flex items-center justify-center gap-1">
|
| 436 |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>
|
| 437 |
-
|
| 438 |
</button>
|
| 439 |
</div>
|
| 440 |
</div>
|
|
@@ -668,6 +668,10 @@
|
|
| 668 |
let currentStrokes = []; // 存放所有筆畫: [ [[x], [y], [t]], [[x], [y], [t]], ... ]
|
| 669 |
let currentStroke = {x: [], y: [], t: []}; // 暫存當前筆畫
|
| 670 |
let canvasCtx = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
|
| 672 |
document.addEventListener('DOMContentLoaded', () => {
|
| 673 |
// DOM 元素
|
|
@@ -900,7 +904,7 @@
|
|
| 900 |
|
| 901 |
// 3. 設定 Context
|
| 902 |
canvasCtx = canvas.getContext('2d');
|
| 903 |
-
canvasCtx.lineWidth =
|
| 904 |
canvasCtx.lineCap = 'round';
|
| 905 |
canvasCtx.lineJoin = 'round';
|
| 906 |
canvasCtx.strokeStyle = '#000000';
|
|
@@ -1032,67 +1036,91 @@
|
|
| 1032 |
canvasCtx.stroke();
|
| 1033 |
}
|
| 1034 |
|
| 1035 |
-
// [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
async function recognizeHandwriting() {
|
|
|
|
| 1037 |
if (currentStrokes.length === 0) {
|
| 1038 |
showToast("請先手寫內容!");
|
| 1039 |
return;
|
| 1040 |
}
|
| 1041 |
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
const height = canvas.height;
|
| 1048 |
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
"options": "enable_pre_space",
|
| 1052 |
-
"requests": [{
|
| 1053 |
-
"writing_guide": {
|
| 1054 |
-
"writing_area_width": width,
|
| 1055 |
-
"writing_area_height": height
|
| 1056 |
-
},
|
| 1057 |
-
"ink": currentStrokes
|
| 1058 |
-
}]
|
| 1059 |
-
};
|
| 1060 |
|
| 1061 |
try {
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
headers: { 'Content-Type': 'application/json' },
|
| 1066 |
-
body: JSON.stringify(payload)
|
| 1067 |
-
});
|
| 1068 |
|
| 1069 |
-
|
|
|
|
|
|
|
| 1070 |
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
answerInput.value = candidates[0];
|
| 1077 |
-
|
| 1078 |
-
// 2. 顯示候選字列
|
| 1079 |
-
showCandidates(candidates);
|
| 1080 |
|
| 1081 |
-
|
| 1082 |
answerInput.classList.add('border-green-500', 'bg-green-50');
|
| 1083 |
setTimeout(() => answerInput.classList.remove('border-green-500', 'bg-green-50'), 500);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1084 |
} else {
|
| 1085 |
-
|
| 1086 |
-
if(result[0] === 'INVALID_INPUT_METHOD_NAME') {
|
| 1087 |
-
showToast("API 錯誤:輸入法名稱無效,請確認網路環境或稍後再試。");
|
| 1088 |
-
}
|
| 1089 |
}
|
|
|
|
| 1090 |
} catch (error) {
|
| 1091 |
-
console.error("
|
| 1092 |
-
showToast("
|
| 1093 |
} finally {
|
| 1094 |
hwRecognizeBtn.disabled = false;
|
| 1095 |
-
hwRecognizeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>
|
| 1096 |
}
|
| 1097 |
}
|
| 1098 |
|
|
@@ -1137,12 +1165,18 @@
|
|
| 1137 |
// [關鍵] 延遲時間設為 400ms,配合 CSS transition
|
| 1138 |
setTimeout(() => initHandwritingBoard(), 400);
|
| 1139 |
|
| 1140 |
-
// 3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
answerInput.readOnly = true;
|
| 1142 |
answerInput.placeholder = "請在下方手寫英文...";
|
| 1143 |
answerInput.classList.add('handwriting-mode');
|
| 1144 |
|
| 1145 |
-
//
|
| 1146 |
if (document.activeElement === answerInput) {
|
| 1147 |
answerInput.blur();
|
| 1148 |
}
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>單字閃卡 (TrOCR AI 版)</title>
|
| 7 |
<!-- 載入 Tailwind CSS CDN -->
|
| 8 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
<!-- 載入彩帶效果庫 -->
|
|
|
|
| 434 |
</button>
|
| 435 |
<button id="hw-recognize-btn" class="flex-[2] py-3 bg-indigo-600 text-white rounded-xl font-bold hover:bg-indigo-700 shadow-md transition-colors flex items-center justify-center gap-1">
|
| 436 |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>
|
| 437 |
+
辨識 (TrOCR AI)
|
| 438 |
</button>
|
| 439 |
</div>
|
| 440 |
</div>
|
|
|
|
| 668 |
let currentStrokes = []; // 存放所有筆畫: [ [[x], [y], [t]], [[x], [y], [t]], ... ]
|
| 669 |
let currentStroke = {x: [], y: [], t: []}; // 暫存當前筆畫
|
| 670 |
let canvasCtx = null;
|
| 671 |
+
|
| 672 |
+
// [新增] TrOCR 相關變數
|
| 673 |
+
let ocrPipeline = null; // 存放載入好的模型
|
| 674 |
+
let isModelLoading = false;
|
| 675 |
|
| 676 |
document.addEventListener('DOMContentLoaded', () => {
|
| 677 |
// DOM 元素
|
|
|
|
| 904 |
|
| 905 |
// 3. 設定 Context
|
| 906 |
canvasCtx = canvas.getContext('2d');
|
| 907 |
+
canvasCtx.lineWidth = 10; // [修改] 增加線條寬度以利 OCR 辨識
|
| 908 |
canvasCtx.lineCap = 'round';
|
| 909 |
canvasCtx.lineJoin = 'round';
|
| 910 |
canvasCtx.strokeStyle = '#000000';
|
|
|
|
| 1036 |
canvasCtx.stroke();
|
| 1037 |
}
|
| 1038 |
|
| 1039 |
+
// [新增] 載入 TrOCR AI 模型 (使用 dynamic import)
|
| 1040 |
+
async function initOCR() {
|
| 1041 |
+
if (ocrPipeline) return; // 已載入
|
| 1042 |
+
if (isModelLoading) return; // 載入中
|
| 1043 |
+
|
| 1044 |
+
isModelLoading = true;
|
| 1045 |
+
showToast("正在下載 AI 手寫模型 (首次需約 200MB)...");
|
| 1046 |
+
|
| 1047 |
+
try {
|
| 1048 |
+
// 使用動態導入載入 Transformers.js
|
| 1049 |
+
const { pipeline, env } = await import('https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2');
|
| 1050 |
+
|
| 1051 |
+
// 設定環境:不使用本地模型 (因為是網頁版),強制使用 Browser Cache
|
| 1052 |
+
env.allowLocalModels = false;
|
| 1053 |
+
env.useBrowserCache = true;
|
| 1054 |
+
|
| 1055 |
+
// 載入 TrOCR 模型 (手寫文字辨識)
|
| 1056 |
+
// Xenova/trocr-small-handwritten 是量化後的版本,適合瀏覽器
|
| 1057 |
+
ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-small-handwritten', {
|
| 1058 |
+
progress_callback: (data) => {
|
| 1059 |
+
if (data.status === 'progress') {
|
| 1060 |
+
console.log(`Model loading: ${Math.round(data.progress || 0)}%`);
|
| 1061 |
+
}
|
| 1062 |
+
}
|
| 1063 |
+
});
|
| 1064 |
+
|
| 1065 |
+
showToast("AI 模型載入完成!");
|
| 1066 |
+
isModelLoading = false;
|
| 1067 |
+
|
| 1068 |
+
} catch (error) {
|
| 1069 |
+
console.error("AI 模型載入失敗:", error);
|
| 1070 |
+
showToast("模型載入失敗,請檢查網路連線");
|
| 1071 |
+
isModelLoading = false;
|
| 1072 |
+
}
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
// [修正] 辨識手寫 (改用 TrOCR)
|
| 1076 |
async function recognizeHandwriting() {
|
| 1077 |
+
// 1. 檢查是否有筆跡
|
| 1078 |
if (currentStrokes.length === 0) {
|
| 1079 |
showToast("請先手寫內容!");
|
| 1080 |
return;
|
| 1081 |
}
|
| 1082 |
|
| 1083 |
+
// 2. 確保模型已載入
|
| 1084 |
+
if (!ocrPipeline) {
|
| 1085 |
+
await initOCR();
|
| 1086 |
+
if (!ocrPipeline) return; // 載入失敗
|
| 1087 |
+
}
|
|
|
|
| 1088 |
|
| 1089 |
+
hwRecognizeBtn.disabled = true;
|
| 1090 |
+
hwRecognizeBtn.textContent = "AI 辨識中...";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1091 |
|
| 1092 |
try {
|
| 1093 |
+
const canvas = document.getElementById('handwriting-canvas');
|
| 1094 |
+
// 將 Canvas 轉為 Data URL
|
| 1095 |
+
const imageData = canvas.toDataURL("image/png");
|
|
|
|
|
|
|
|
|
|
| 1096 |
|
| 1097 |
+
// 呼叫 pipeline 進行辨識
|
| 1098 |
+
const output = await ocrPipeline(imageData);
|
| 1099 |
+
// 輸出格式通常是 [{ generated_text: "hello" }]
|
| 1100 |
|
| 1101 |
+
const text = output[0]?.generated_text?.trim();
|
| 1102 |
+
|
| 1103 |
+
if (text) {
|
| 1104 |
+
// TrOCR 可能會回傳句點,如果是單字練習可以移除
|
| 1105 |
+
const cleanedText = text.replace(/\.$/, '');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1106 |
|
| 1107 |
+
answerInput.value = cleanedText;
|
| 1108 |
answerInput.classList.add('border-green-500', 'bg-green-50');
|
| 1109 |
setTimeout(() => answerInput.classList.remove('border-green-500', 'bg-green-50'), 500);
|
| 1110 |
+
showToast("辨識結果:" + cleanedText);
|
| 1111 |
+
|
| 1112 |
+
// 也可以把原文當作候選字 (TrOCR 通常只回傳最可能的結果,不像 API 有多個候選)
|
| 1113 |
+
showCandidates([cleanedText]);
|
| 1114 |
} else {
|
| 1115 |
+
showToast("未能辨識出文字");
|
|
|
|
|
|
|
|
|
|
| 1116 |
}
|
| 1117 |
+
|
| 1118 |
} catch (error) {
|
| 1119 |
+
console.error("AI 辨識失敗:", error);
|
| 1120 |
+
showToast("辨識發生錯誤: " + error.message);
|
| 1121 |
} finally {
|
| 1122 |
hwRecognizeBtn.disabled = false;
|
| 1123 |
+
hwRecognizeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg> 辨識 (TrOCR)`;
|
| 1124 |
}
|
| 1125 |
}
|
| 1126 |
|
|
|
|
| 1165 |
// [關鍵] 延遲時間設為 400ms,配合 CSS transition
|
| 1166 |
setTimeout(() => initHandwritingBoard(), 400);
|
| 1167 |
|
| 1168 |
+
// 3. 觸發模型預載 (Lazy Load)
|
| 1169 |
+
// 當使用者切換到英文輸入模式時,就開始在背景下載模型
|
| 1170 |
+
if (!ocrPipeline && !isModelLoading) {
|
| 1171 |
+
initOCR();
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
// 4. 鎖定輸入框
|
| 1175 |
answerInput.readOnly = true;
|
| 1176 |
answerInput.placeholder = "請在下方手寫英文...";
|
| 1177 |
answerInput.classList.add('handwriting-mode');
|
| 1178 |
|
| 1179 |
+
// 5. 嘗試讓輸入框失去焦點,避免鍵盤彈出
|
| 1180 |
if (document.activeElement === answerInput) {
|
| 1181 |
answerInput.blur();
|
| 1182 |
}
|