From 62c6775e673ec7556a1b409e53f6ba225eb62054 Mon Sep 17 00:00:00 2001 From: polv Date: Tue, 3 Feb 2026 12:48:21 +0700 Subject: [PATCH] add dedup script --- .vscode/settings.json | 13 + ...る!?中国語_様態補語と程度補語の文法の注意点とは.md | 296 ++++++++++++++++ group-similar.js | 321 ++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 292 ++++++++++++++++ pnpm-workspace.yaml | 2 + server.js | 142 ++++---- 7 files changed, 1009 insertions(+), 58 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 group-similar.js create mode 100644 pnpm-workspace.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b91095ac --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "editor.tabSize": 2, + "json.validate.enable": false, + "[javascript]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.maxTokenizationLineLength": 2500, + }, + "[vue]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, +} \ No newline at end of file diff --git a/docs/みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは.md b/docs/みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは.md index 38dd98e9..79e75177 100644 --- a/docs/みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは.md +++ b/docs/みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは.md @@ -1,5 +1,6 @@ --- interval: 2 +dedup: true video: '/みんなうっかり間違える!?中国語 様態補語と程度補語の文法の注意点とは [m15ta7yPgjw].mp4' --- @@ -16,7 +17,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+0:14 + ![0:18](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/0_18.png) +
+
@@ -28,7 +34,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+0:26 + ![0:34](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/0_34.png) +
+
@@ -44,6 +55,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+1:04 + ![1:10](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/1_10.png) @@ -89,6 +103,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![1:32](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/1_32.png) +
+
@@ -96,7 +112,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+1:34 + ![1:36](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/1_36.png) +
+
@@ -104,11 +125,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+1:38 + ![1:40](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/1_40.png) ![1:42](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/1_42.png) +
+
@@ -120,6 +146,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+1:46 + ![1:48](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/1_48.png) @@ -145,6 +174,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![2:00](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/2_00.png) +
+
@@ -152,11 +183,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+2:02 + ![2:04](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/2_04.png) ![2:06](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/2_06.png) +
+
@@ -168,6 +204,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+2:10 + ![2:12](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/2_12.png) @@ -177,6 +216,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![2:16](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/2_16.png) +
+
@@ -184,6 +225,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+2:18 + ![2:20](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/2_20.png) @@ -197,6 +241,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![2:26](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/2_26.png) +
+
@@ -208,6 +254,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+2:30 + ![2:32](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/2_32.png) @@ -269,6 +318,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![3:02](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_02.png) +
+
@@ -276,6 +327,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+3:04 + ![3:06](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_06.png) @@ -285,6 +339,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![3:10](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_10.png) +
+
@@ -292,11 +348,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+3:12 + ![3:14](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_14.png) ![3:16](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_16.png) +
+
@@ -308,6 +369,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+3:20 + ![3:22](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_22.png) @@ -341,6 +405,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![3:38](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_38.png) +
+
@@ -348,11 +414,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+3:40 + ![3:42](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_42.png) ![3:44](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_44.png) +
+
@@ -364,6 +435,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+3:48 + ![3:50](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/3_50.png) @@ -393,6 +467,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![4:04](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_04.png) +
+
@@ -400,11 +476,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+4:06 + ![4:08](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_08.png) ![4:10](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_10.png) +
+
@@ -412,7 +493,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+4:12 + ![4:14](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_14.png) +
+
@@ -420,6 +506,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+4:16 + ![4:18](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_18.png) @@ -437,6 +526,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![4:26](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_26.png) +
+
@@ -448,6 +539,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+4:30 + ![4:32](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_32.png) @@ -457,6 +551,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![4:36](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_36.png) +
+
@@ -464,6 +560,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+4:38 + ![4:40](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_40.png) @@ -485,6 +584,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![4:50](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_50.png) +
+
@@ -492,6 +593,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+4:52 + ![4:54](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/4_54.png) @@ -517,6 +621,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![5:06](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/5_06.png) +
+
@@ -524,6 +630,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+5:08 + ![5:10](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/5_10.png) @@ -585,6 +694,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![5:40](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/5_40.png) +
+
@@ -592,7 +703,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+5:42 + ![5:44](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/5_44.png) +
+
@@ -600,6 +716,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+5:46 + ![5:48](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/5_48.png) @@ -621,6 +740,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![5:58](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/5_58.png) +
+
@@ -628,6 +749,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+6:00 + ![6:02](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/6_02.png) @@ -649,6 +773,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![6:12](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/6_12.png) +
+
@@ -660,6 +786,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+6:16 + ![6:18](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/6_18.png) @@ -673,6 +802,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![6:24](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/6_24.png) +
+
@@ -680,6 +811,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+6:26 + ![6:28](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/6_28.png) @@ -717,6 +851,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![6:46](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/6_46.png) +
+
@@ -724,6 +860,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+6:48 + ![6:50](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/6_50.png) @@ -785,6 +924,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![7:20](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/7_20.png) +
+
@@ -796,6 +937,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+7:24 + ![7:26](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/7_26.png) @@ -829,6 +973,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![7:42](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/7_42.png) +
+
@@ -836,6 +982,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+7:44 + ![7:46](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/7_46.png) @@ -853,6 +1002,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![7:54](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/7_54.png) +
+
@@ -860,7 +1011,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+7:56 + ![7:58](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/7_58.png) +
+
@@ -868,6 +1024,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+8:00 + ![8:02](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_02.png) @@ -897,6 +1056,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![8:16](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_16.png) +
+
@@ -904,6 +1065,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+8:18 + ![8:20](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_20.png) @@ -913,6 +1077,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![8:24](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_24.png) +
+
@@ -920,7 +1086,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+8:26 + ![8:28](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_28.png) +
+
@@ -928,11 +1099,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+8:30 + ![8:32](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_32.png) ![8:34](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_34.png) +
+
@@ -944,6 +1120,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+8:38 + ![8:40](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_40.png) @@ -969,6 +1148,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![8:52](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_52.png) +
+
@@ -976,11 +1157,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+8:54 + ![8:56](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_56.png) ![8:58](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/8_58.png) +
+
@@ -988,7 +1174,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+9:00 + ![9:02](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_02.png) +
+
@@ -996,6 +1187,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+9:04 + ![9:06](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_06.png) @@ -1017,6 +1211,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![9:16](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_16.png) +
+
@@ -1028,11 +1224,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+9:20 + ![9:22](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_22.png) ![9:24](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_24.png) +
+
@@ -1040,7 +1241,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+9:26 + ![9:28](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_28.png) +
+
@@ -1048,6 +1254,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+9:30 + ![9:32](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_32.png) @@ -1065,6 +1274,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![9:40](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_40.png) +
+
@@ -1072,11 +1283,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+9:42 + ![9:44](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_44.png) ![9:46](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_46.png) +
+
@@ -1084,7 +1300,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+9:48 + ![9:50](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_50.png) +
+
@@ -1092,6 +1313,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+9:52 + ![9:54](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/9_54.png) @@ -1109,6 +1333,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![10:02](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_02.png) +
+
@@ -1116,7 +1342,12 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+10:04 + ![10:06](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_06.png) +
+
@@ -1124,11 +1355,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+10:08 + ![10:10](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_10.png) ![10:12](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_12.png) +
+
@@ -1140,6 +1376,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+10:16 + ![10:18](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_18.png) @@ -1177,6 +1416,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![10:36](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_36.png) +
+
@@ -1188,11 +1429,16 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+10:40 + ![10:42](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_42.png) ![10:44](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_44.png) +
+
@@ -1208,6 +1454,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+10:50 + ![10:52](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/10_52.png) @@ -1237,6 +1486,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![11:06](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_06.png) +
+
@@ -1248,6 +1499,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+11:10 + ![11:12](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_12.png) @@ -1269,6 +1523,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![11:22](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_22.png) +
+
@@ -1276,6 +1532,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+11:24 + ![11:26](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_26.png) @@ -1285,6 +1544,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![11:30](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_30.png) +
+
@@ -1296,6 +1557,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+11:34 + ![11:36](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_36.png) @@ -1313,6 +1577,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![11:44](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_44.png) +
+
@@ -1320,6 +1586,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+11:46 + ![11:48](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_48.png) @@ -1329,6 +1598,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![11:52](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_52.png) +
+
@@ -1336,6 +1607,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+11:54 + ![11:56](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/11_56.png) @@ -1345,6 +1619,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![12:00](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/12_00.png) +
+
@@ -1352,6 +1628,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+12:02 + ![12:04](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/12_04.png) @@ -1373,6 +1652,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![12:14](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/12_14.png) +
+
@@ -1384,6 +1665,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+12:18 + ![12:20](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/12_20.png) @@ -1409,6 +1693,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![12:32](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/12_32.png) +
+
@@ -1420,6 +1706,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+12:36 + ![12:38](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/12_38.png) @@ -1445,6 +1734,8 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![12:50](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/12_50.png) +
+
@@ -1452,6 +1743,9 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 +
+12:52 + ![12:54](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/12_54.png) @@ -1469,5 +1763,7 @@ video: '/みんなうっかり間違える!?中国語 様態補語と程度 ![13:02](./みんなうっかり間違える!?中国語_様態補語と程度補語の文法の注意点とは/13_02.png) +
+
diff --git a/group-similar.js b/group-similar.js new file mode 100644 index 00000000..825dd301 --- /dev/null +++ b/group-similar.js @@ -0,0 +1,321 @@ +// @ts-check +/** + * group_similar.js + * + * Reads a markdown file containing timestamped screenshot references like: + * ![0:11](folder/0_11.png) + * ![0:13](folder/0_13.png) + * ... + * + * Consecutive images that are visually similar get collapsed into a + *
block anchored under the first (non-similar) image. + * + * Usage: + * node group_similar.js [output.md] [--threshold 0.005] + * + * - output.md defaults to input.md (overwrites in place) + * - --threshold: similarity cutoff 0-1. Images with a diff ratio + * BELOW this value are considered "similar". Default: 0.005 (0.5%). + * + * Dependencies: + * npm install sharp + * npm install --save-dev @types/node (for //@ts-check) + */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +// @ts-ignore - sharp ships no bundled .d.ts; types are not needed at runtime +import sharp from "sharp"; + +// --------------------------------------------------------------------------- +// Types (JSDoc only — kept in sync with every function signature) +// --------------------------------------------------------------------------- + +/** + * @typedef {{ type: "image", alt: string, src: string, raw: string }} ImageNode + * @typedef {{ type: "text", raw: string }} TextNode + * @typedef {ImageNode | TextNode} Line + * @typedef {{ blanks: TextNode[], image: ImageNode }} Child + * @typedef {{ type: "group", anchor: ImageNode, children: Child[] }} GroupNode + * @typedef {Line | GroupNode} OutputNode + * @typedef {{ comparisons: number, grouped: number }} Stats + */ + +// --------------------------------------------------------------------------- +// Image comparison (returns 0 = identical … 1 = completely different) +// --------------------------------------------------------------------------- + +const COMPARE_WIDTH = 64; +const COMPARE_HEIGHT = 64; + +/** + * @param {string} imgPath + * @returns {Promise} + */ +async function loadSmall(imgPath) { + return sharp(imgPath) + .resize(COMPARE_WIDTH, COMPARE_HEIGHT, { fit: "cover" }) + .raw() + .toBuffer(); +} + +/** + * Mean squared error normalised to [0, 1]. + * @param {Buffer} bufA + * @param {Buffer} bufB + * @returns {number} + */ +function mse(bufA, bufB) { + if (bufA.length !== bufB.length) return 1; + let sum = 0; + for (let i = 0; i < bufA.length; i++) { + const d = bufA[i] - bufB[i]; + sum += d * d; + } + return sum / bufA.length / (255 * 255); +} + +/** + * @param {string} pathA + * @param {string} pathB + * @param {number} threshold + * @returns {Promise<{ similar: boolean, score: number }>} + */ +async function compare(pathA, pathB, threshold) { + const [a, b] = await Promise.all([loadSmall(pathA), loadSmall(pathB)]); + const score = mse(a, b); + return { similar: score < threshold, score }; +} + +// --------------------------------------------------------------------------- +// Markdown parsing +// --------------------------------------------------------------------------- + +const IMG_RE = /^!\[([^\]]*)\]\(([^)]+)\)\s*$/; + +/** + * @param {string} line + * @returns {Line} + */ +function parseLine(line) { + const m = line.match(IMG_RE); + if (m) return { type: "image", alt: m[1], src: m[2], raw: line }; + return { type: "text", raw: line }; +} + +/** + * @param {Line} node + * @returns {boolean} + */ +function isBlank(node) { + return node.type === "text" && node.raw.trim() === ""; +} + +// --------------------------------------------------------------------------- +// Core grouping logic +// --------------------------------------------------------------------------- + +/** + * Walk parsed lines and group consecutive similar images. + * Blank lines between similar images travel with the group; non-blank + * non-image text always breaks the lookahead. + * + * @param {Line[]} lines + * @param {string} baseDir + * @param {number} threshold + * @param {Stats} stats - mutated in place for the caller's summary + * @returns {Promise} + */ +async function groupImages(lines, baseDir, threshold, stats) { + /** @type {OutputNode[]} */ + const result = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + if (line.type !== "image") { + result.push(line); + i++; + continue; + } + + /** @type {ImageNode} */ + const anchor = line; + /** @type {Child[]} */ + const children = []; + let j = i + 1; + + // lookahead + while (j < lines.length) { + /** @type {TextNode[]} */ + const blanks = []; + while (j < lines.length && isBlank(lines[j])) { + // isBlank already guarantees type === "text" + blanks.push(/** @type {TextNode} */ (lines[j])); + j++; + } + + if (j >= lines.length || lines[j].type !== "image") { + j -= blanks.length; // rewind - blanks stay outside + break; + } + + /** @type {ImageNode} */ + const candidate = /** @type {ImageNode} */ (lines[j]); + const { similar } = await compare( + resolve(baseDir, anchor.src), + resolve(baseDir, candidate.src), + threshold, + ); + stats.comparisons++; + + if (similar) { + stats.grouped++; + children.push({ blanks, image: candidate }); + j++; + } else { + j -= blanks.length; // rewind - blanks stay outside + break; + } + } + + if (children.length > 0) { + result.push({ type: "group", anchor, children }); + } else { + result.push(anchor); + } + + i = j; + } + + return result; +} + +// --------------------------------------------------------------------------- +// Rendering +// --------------------------------------------------------------------------- + +/** + * @param {ImageNode} node + * @returns {string} + */ +function renderImage(node) { + return `![${node.alt}](${node.src})`; +} + +/** + * @param {GroupNode} node + * @returns {string} + */ +function renderGroup(node) { + const parts = []; + + parts.push(renderImage(node.anchor)); + + // First child's blanks are the original spacing between anchor and the + // next image. Emit them so multiple blank lines are preserved. + // Then guarantee at least one blank line exists before
. + const firstBlanks = node.children[0].blanks; + if (firstBlanks.length === 0) parts.push(""); + for (const b of firstBlanks) parts.push(b.raw); + + parts.push("
"); + parts.push(`${node.anchor.alt}`); + parts.push(""); // 1 blank line after + + // first child: blanks already emitted above + parts.push(renderImage(node.children[0].image)); + for (let ci = 1; ci < node.children.length; ci++) { + const { blanks, image } = node.children[ci]; + for (const b of blanks) parts.push(b.raw); + parts.push(renderImage(image)); + } + + parts.push("
"); + parts.push("
"); + return parts.join("\n"); +} + +/** + * @param {OutputNode[]} nodes + * @returns {string} + */ +function render(nodes) { + return nodes + .map((n) => (n.type === "group" ? renderGroup(n) : n.raw)) + .join("\n"); +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Read a markdown file, group similar consecutive images into
+ * blocks, and write the result. + * + * @param {string} inputPath - path to the source .md file + * @param {string} outputPath - path to write (may be the same as inputPath) + * @param {number} [threshold=0.005] - MSE cutoff; lower = stricter grouping + * @returns {Promise} - comparison stats (useful for callers) + */ +export async function renderFile(inputPath, outputPath, threshold = 0.005) { + const resolvedInput = resolve(inputPath); + const resolvedOutput = resolve(outputPath); + const baseDir = dirname(resolvedInput); + + console.log(`Reading : ${resolvedInput}`); + console.log(`Threshold : ${threshold}`); + + const raw = readFileSync(resolvedInput, "utf8"); + const lines = raw.split("\n").map(parseLine); + + /** @type {Stats} */ + const stats = { comparisons: 0, grouped: 0 }; + const grouped = await groupImages(lines, baseDir, threshold, stats); + const output = render(grouped); + + writeFileSync(resolvedOutput, output, "utf8"); + + console.log( + `Comparisons: ${stats.comparisons} | grouped: ${stats.grouped}`, + ); + console.log(`Wrote : ${resolvedOutput}`); + + return stats; +} + +// --------------------------------------------------------------------------- +// CLI entry-point (runs only when executed directly) +// --------------------------------------------------------------------------- + +const __filename = fileURLToPath(import.meta.url); +const isMain = + process.argv[1] && resolve(process.argv[1]) === resolve(__filename); + +if (isMain) { + const args = process.argv.slice(2); + const flagIdx = args.indexOf("--threshold"); + const threshold = flagIdx !== -1 ? parseFloat(args[flagIdx + 1]) : 0.005; + const positional = + flagIdx === -1 + ? args + : args.filter( + (/** @type {string} */ _a, /** @type {number} */ i) => + i !== flagIdx && i !== flagIdx + 1, + ); + + const inputPath = positional[0]; + const outputPath = positional[1] || positional[0]; + + if (!inputPath) { + console.error( + "Usage: node group_similar.js [output.md] [--threshold 0.005]", + ); + process.exit(1); + } + + await renderFile(inputPath, outputPath, threshold); +} diff --git a/package.json b/package.json index fc0f7fef..966b4842 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "license": "MIT", "dependencies": { "gray-matter": "^4.0.3", + "sharp": "^0.34.5", "vite": "^7.3.1", "vitepress": "2.0.0-alpha.15", "vue": "^3.5.27", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1fded94..bac21d6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + sharp: + specifier: ^0.34.5 + version: 0.34.5 vite: specifier: ^7.3.1 version: 7.3.1(@types/node@20.19.30) @@ -186,6 +189,9 @@ packages: search-insights: optional: true + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} @@ -348,6 +354,143 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -714,6 +857,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -914,6 +1061,15 @@ packages: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shell-quote@1.8.3: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} @@ -1296,6 +1452,11 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.2': optional: true @@ -1380,6 +1541,102 @@ snapshots: '@iconify/types@2.0.0': {} + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} '@opentelemetry/api@1.9.0': {} @@ -1714,6 +1971,8 @@ snapshots: dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -1947,6 +2206,39 @@ snapshots: extend-shallow: 2.0.1 kind-of: 6.0.3 + semver@7.7.3: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shell-quote@1.8.3: {} shiki@3.21.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..e6654d63 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - sharp diff --git a/server.js b/server.js index f6bb2560..15779d15 100644 --- a/server.js +++ b/server.js @@ -1,92 +1,118 @@ // @ts-check -import { WebSocketServer } from 'ws'; +import { WebSocketServer } from "ws"; import { readFileSync, mkdirSync, writeFileSync } from "fs"; -import { join as joinPath } from 'path' -import matter from 'gray-matter'; +import { join as joinPath } from "path"; +import matter from "gray-matter"; -const PORT = 8080 +import { renderFile } from "./group-similar.js"; + +const PORT = 8080; const wss = new WebSocketServer({ port: PORT }); console.log(`WebSocket server running on ws://localhost:${PORT}`); -wss.on('connection', (ws) => { - console.log('New client connected'); +wss.on("connection", (ws) => { + console.log("New client connected"); - let filename = '' - const ROOT_DIR = 'docs'; + let filename = ""; + const ROOT_DIR = "docs"; - ws.on('message', (message, isBinary) => { + ws.on("message", (message, isBinary) => { if (message instanceof Buffer && isBinary) { writeFileSync(joinPath(ROOT_DIR, filename), message); return; } - const { name, ...payload } = JSON.parse(message.toString('utf-8')); + const { name, ...payload } = JSON.parse(message.toString("utf-8")); switch (name) { - case 'download_q': { - const { q, duration } = payload; - console.log(`Start ${q}`) + case "download_q": + { + const { q, duration } = payload; + console.log(`Start ${q}`); - const q_stem = q.replace(/\.[a-z0-9]+$/, "").replace(/\[.+?\]/g, '').replace(/\s+/g, '_').trim(); + const q_stem = q + .replace(/\.[a-z0-9]+$/, "") + .replace(/\[.+?\]/g, "") + .replace(/\s+/g, "_") + .trim(); - try { - mkdirSync(joinPath(ROOT_DIR, q_stem)); - } catch (e) {} + try { + mkdirSync(joinPath(ROOT_DIR, q_stem)); + } catch (e) {} - let isChanged = false; + let isChanged = false; + const ts = []; - const ts = []; + const mdFile = joinPath(ROOT_DIR, `${q_stem}.md`); + let { data, content } = matter(readFileSync(mdFile, "utf-8")); + data.video = "/" + q; - let { data, content } = matter(readFileSync(joinPath(ROOT_DIR, `${q_stem}.md`), 'utf-8')); - data.video = '/' + q; + const { dedup, interval } = data; + if (typeof interval === "number") { + for (let t = 0; t < duration; t += interval) { + const min = Math.floor(t / 60); + const sec = Math.floor(t - min * 60); - const interval = data.interval; - if (typeof interval === 'number') { - for (let t = 0; t < duration; t += interval) { - const min = Math.floor(t / 60) - const sec = Math.floor(t - min * 60) + content += `\n${min}:${sec.toString().padStart(2, "0")}\n`; + } + } - content += `\n${min}:${sec.toString().padStart(2, '0')}\n`; + const re = /^(?\d{1,2}):(?\d{2})\s*$/; + const lines = content.split("\n"); + content = ""; + for (const line of lines) { + const m = re.exec(line); + if (m?.groups) { + const { min, sec } = m.groups; + const t = `${min}_${sec}`; + ts.push(t); + content += `\n![${line}](./${q_stem}/${t}.png)\n\n`; + isChanged = true; + } else { + content += `${line}\n`; + } + } + + if (isChanged) { + writeFileSync(mdFile, matter.stringify(content, data)); + + if (dedup) { + renderFile( + mdFile, + mdFile, + typeof dedup === "number" ? dedup : undefined, + ).then((stats) => { + // Don't log a thing yet. + }); + } + + ws.send(JSON.stringify({ name: "seek_q_ts", q, ts })); } } - - const re = /^(?\d{1,2}):(?\d{2})\s*$/; - const lines = content.split('\n') - content = '' - for (const line of lines) { - const m = re.exec(line); - if (m?.groups) { - const { min, sec } = m.groups; - const t = `${min}_${sec}`; - ts.push(t); - content += `\n![${line}](./${q_stem}/${t}.png)\n\n`; - isChanged = true; - } else { - content += `${line}\n`; - } + break; + case "timed_q": + { + const { q, t } = payload; + const q_stem = q + .replace(/\.[a-z0-9]+$/, "") + .replace(/\[.+?\]/g, "") + .trim(); + filename = t ? joinPath(q_stem, `${t}.png`) : ""; } - - if (isChanged){ - writeFileSync(joinPath(ROOT_DIR, `${q_stem}.md`), matter.stringify(content, data)); - ws.send(JSON.stringify({ name: "seek_q_ts", q, ts })); + break; + case "done": + { + const { q } = payload; + console.log(`Done ${q}`); } - } break; - case 'timed_q' : { - const { q, t } = payload; - const q_stem = q.replace(/\.[a-z0-9]+$/, "").replace(/\[.+?\]/g, '').trim(); - filename = t ? joinPath(q_stem, `${t}.png`) : ''; - } break; - case 'done' : { - const { q } = payload - console.log(`Done ${q}`) - } break; + break; } }); - ws.on('close', () => { - console.log('Client disconnected'); + ws.on("close", () => { + console.log("Client disconnected"); }); });