1- const ocrRegion1 = { x : 0 , y : 230 , width : 500 , height : 100 } ;
1+ const ocrRegion1 = { x : 0 , y : 230 , width : 500 , height : 100 } ;
22
33( async function ( ) {
44 const data = loadData ( ) ;
5+ const kmHook = new KeyMouseHook ( ) ;
6+ const skipKey = ( typeof settings !== "undefined" && settings && settings . skipKey ) ? settings . skipKey : "R" ;
7+ const autoRecognize = ( typeof settings !== "undefined" && settings && typeof settings . autoRecognize !== "undefined" ) ? settings . autoRecognize : true ;
8+ let skipWait = false ;
9+ kmHook . OnKeyDown ( ( key ) => {
10+ if ( key === skipKey ) {
11+ skipWait = true ;
12+ }
13+ } ) ;
14+ const sleepOrSkip = async ( ms ) => {
15+ const step = 100 ;
16+ const endTime = Date . now ( ) + ms ;
17+ while ( Date . now ( ) < endTime ) {
18+ if ( skipWait ) {
19+ skipWait = false ;
20+ return true ;
21+ }
22+ await sleep ( Math . min ( step , endTime - Date . now ( ) ) ) ;
23+ }
24+ return false ;
25+ } ;
26+ log . info ( "按 {0} 可跳过等待并立刻识别" , skipKey ) ;
27+ log . info ( "自动识别: {0}" , autoRecognize ? "开启" : "关闭" ) ;
28+ try {
529 let emptyCount = 0 ;
630 let lastTextKey = "" ;
731 let lastResultKey = "" ;
832 while ( true ) {
33+ // 自动识别关闭时:仅在按下跳过按键后才进行一次识别
34+ if ( ! autoRecognize ) {
35+ let triggered = await sleepOrSkip ( 24 * 60 * 60 * 1000 ) ;
36+ if ( ! triggered ) {
37+ continue ;
38+ }
39+ }
940 let texts = ocr ( ocrRegion1 . x , ocrRegion1 . y , ocrRegion1 . width , ocrRegion1 . height ) ;
1041 if ( ! texts || texts . length === 0 ) {
1142 emptyCount ++ ;
@@ -14,29 +45,29 @@ const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 };
1445 log . warn ( "连续未识别达到上限,退出循环" ) ;
1546 break ;
1647 }
17- await sleep ( 5000 ) ;
48+ await sleepOrSkip ( 1000 ) ;
1849 continue ;
1950 }
2051
2152 emptyCount = 0 ;
2253 // 仅在识别结果发生变化时输出,避免刷屏
2354 let textKey = texts . join ( " | " ) . trim ( ) ;
2455 if ( textKey === lastTextKey ) {
25- await sleep ( 5000 ) ;
56+ await sleepOrSkip ( 5000 ) ;
2657 continue ;
2758 }
2859 if ( lastResultKey ) {
2960 log . info ( "==== 识别结果 ====" ) ;
3061 }
3162 lastTextKey = textKey ;
3263 lastResultKey = textKey ;
33- log . info ( `识别到文本: ${ textKey } ` ) ;
64+ log . debug ( `识别到文本: ${ textKey } ` ) ;
3465
3566 // 解析 OCR 文本,提取冒号后的台词内容
3667 let parsedList = parseOcrTexts ( texts ) ;
3768 if ( parsedList . length === 0 ) {
3869 log . info ( "未获取到可用台词内容,继续识别" ) ;
39- await sleep ( 5000 ) ;
70+ await sleepOrSkip ( 1000 ) ;
4071 continue ;
4172 }
4273
@@ -51,7 +82,10 @@ const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 };
5182 logMatches ( matches ) ;
5283 }
5384
54- await sleep ( 5000 ) ;
85+ await sleepOrSkip ( 5000 ) ;
86+ }
87+ } finally {
88+ kmHook . Dispose ( ) ;
5589 }
5690} ) ( ) ;
5791
@@ -126,26 +160,57 @@ function parseOcrTexts(texts) {
126160 let content = cleaned . slice ( colonIndex + 1 ) . trim ( ) ;
127161 if ( ! content ) {
128162 continue ;
129- }
163+ } let category = detectCategory ( prefix ) ;
130164 // 没有“前两字/首字”标记时,认为是完整台词
131165 let isFull = prefix . indexOf ( "(前两字)" ) < 0
132166 && prefix . indexOf ( "(前两字)" ) < 0
133167 && prefix . indexOf ( "(首字)" ) < 0
134168 && prefix . indexOf ( "(首字)" ) < 0 ;
135- parsed . push ( { raw, content, isFull, prefix } ) ;
169+ parsed . push ( { raw, content, isFull, prefix, category } ) ;
136170 }
137171 return parsed ;
138172}
139173
174+ function detectCategory ( prefix ) {
175+ let p = String ( prefix || "" ) . replace ( / \s + / g, "" ) ;
176+ if ( p . includes ( "元素战技" ) ) {
177+ return "elementSkill" ;
178+ }
179+ if ( p . includes ( "元素爆发" ) ) {
180+ return "elementBurst" ;
181+ }
182+ if ( p . includes ( "入队语音" ) || p . includes ( "加入队伍" ) ) {
183+ return "joinVoice" ;
184+ }
185+ if ( p . includes ( "倒下语音" ) || p === "倒下" ) {
186+ return "fallVoice" ;
187+ }
188+ if ( p . includes ( "宝箱语音" ) || p . includes ( "打开宝箱" ) || p . includes ( "宝箱" ) ) {
189+ return "chestVoice" ;
190+ }
191+ if ( p . includes ( "命之座" ) ) {
192+ return "constellation" ;
193+ }
194+ if ( p . includes ( "天赋" ) ) {
195+ return "talent" ;
196+ }
197+ return "unknown" ;
198+ }
199+
140200function normalizeText ( text ) {
141201 return String ( text || "" )
142202 . replace ( / \s + / g, "" )
143203 . replace ( / [ \" “ ” ] / g, "" )
204+ . replace ( / [ \p{ P} \p{ S} ] / gu, "" )
144205 . trim ( ) ;
145206}
146207
147208function isDialogueKey ( key ) {
148- return key . indexOf ( "台词" ) >= 0 || key . indexOf ( "鍙拌瘝" ) >= 0 ;
209+ return key . indexOf ( "台词" ) >= 0
210+ || key . indexOf ( "语音" ) >= 0
211+ || key . indexOf ( "命之座" ) >= 0
212+ || key . indexOf ( "天赋" ) >= 0
213+ || key . indexOf ( "鍙拌瘝" ) >= 0 ;
149214}
150215
151216function getName ( item ) {
@@ -178,38 +243,147 @@ function isMatch(content, value, isFull) {
178243 }
179244 // 完整台词严格匹配,前两字/首字走前缀/包含匹配
180245 if ( isFull ) {
181- return v === c ;
246+ if ( v === c ) {
247+ return true ;
248+ }
249+ if ( shouldUseLooseMatch ( v ) ) {
250+ return isLooseMatch ( c , v , 2 ) ;
251+ }
252+ return false ;
182253 }
183254 if ( c . length <= 2 ) {
184255 return v . indexOf ( c ) === 0 ;
185256 }
186257 return v . indexOf ( c ) >= 0 || c . indexOf ( v ) >= 0 ;
187258}
188259
260+ function shouldUseLooseMatch ( text ) {
261+ const specialChars = [ "貘" ] ;
262+ for ( let i = 0 ; i < specialChars . length ; i ++ ) {
263+ if ( text . indexOf ( specialChars [ i ] ) >= 0 ) {
264+ return true ;
265+ }
266+ }
267+ return false ;
268+ }
269+
270+ function isLooseMatch ( shortText , fullText , maxMissing ) {
271+ let a = shortText ;
272+ let b = fullText ;
273+ if ( a . length > b . length ) {
274+ let tmp = a ;
275+ a = b ;
276+ b = tmp ;
277+ }
278+ if ( b . length - a . length > maxMissing ) {
279+ return false ;
280+ }
281+ return lcsLength ( a , b ) >= b . length - maxMissing ;
282+ }
283+
284+ function lcsLength ( a , b ) {
285+ let m = a . length ;
286+ let n = b . length ;
287+ let prev = new Array ( n + 1 ) . fill ( 0 ) ;
288+ let curr = new Array ( n + 1 ) . fill ( 0 ) ;
289+ for ( let i = 1 ; i <= m ; i ++ ) {
290+ for ( let j = 1 ; j <= n ; j ++ ) {
291+ if ( a [ i - 1 ] === b [ j - 1 ] ) {
292+ curr [ j ] = prev [ j - 1 ] + 1 ;
293+ } else {
294+ curr [ j ] = curr [ j - 1 ] > prev [ j ] ? curr [ j - 1 ] : prev [ j ] ;
295+ }
296+ }
297+ let temp = prev ;
298+ prev = curr ;
299+ curr = temp ;
300+ for ( let k = 0 ; k <= n ; k ++ ) {
301+ curr [ k ] = 0 ;
302+ }
303+ }
304+ return prev [ n ] ;
305+ }
189306function findCharactersByLine ( parsed , data ) {
190307 let matches = [ ] ;
191308 if ( ! parsed || ! parsed . content || ! data ) {
192309 return matches ;
193310 }
194- for ( let i = 0 ; i < data . length ; i ++ ) {
195- let item = data [ i ] ;
196- for ( let key in item ) {
197- if ( ! isDialogueKey ( key ) ) {
198- continue ;
199- }
200- let val = item [ key ] ;
201- if ( typeof val !== "string" || ! val ) {
202- continue ;
311+ let allowKey = ( key ) => {
312+ if ( ! key || ! isDialogueKey ( key ) ) {
313+ return false ;
314+ }
315+ switch ( parsed . category ) {
316+ case "elementSkill" :
317+ return key . indexOf ( "元素战技台词" ) >= 0 ;
318+ case "elementBurst" :
319+ return key . indexOf ( "元素爆发台词" ) >= 0 ;
320+ case "joinVoice" :
321+ return key . indexOf ( "入队语音" ) >= 0 ;
322+ case "fallVoice" :
323+ return key . indexOf ( "倒下语音" ) >= 0 ;
324+ case "chestVoice" :
325+ return key . indexOf ( "宝箱语音" ) >= 0 ;
326+ case "constellation" :
327+ return key . indexOf ( "命之座" ) >= 0 ;
328+ case "talent" :
329+ return key . indexOf ( "天赋" ) >= 0 ;
330+ default :
331+ return true ;
332+ }
333+ } ;
334+
335+ let collectMatches = ( isFullFlag ) => {
336+ let out = [ ] ;
337+ for ( let i = 0 ; i < data . length ; i ++ ) {
338+ let item = data [ i ] ;
339+ for ( let key in item ) {
340+ if ( ! allowKey ( key ) ) {
341+ continue ;
342+ }
343+ let val = item [ key ] ;
344+ if ( typeof val !== "string" || ! val ) {
345+ continue ;
346+ }
347+ if ( isMatch ( parsed . content , val , isFullFlag ) ) {
348+ out . push ( {
349+ name : getName ( item ) ,
350+ info : buildCharacterInfo ( item )
351+ } ) ;
352+ break ;
353+ }
203354 }
204- if ( isMatch ( parsed . content , val , parsed . isFull ) ) {
205- matches . push ( {
206- name : getName ( item ) ,
207- info : buildCharacterInfo ( item )
208- } ) ;
209- break ;
355+ }
356+ return out ;
357+ } ;
358+
359+ // 先按“完整匹配”尝试;若完全匹配不到,再降级为“只用前几个字/部分内容匹配”(解决长文本OCR截断问题)
360+ matches = collectMatches ( parsed . isFull ) ;
361+ if ( matches . length > 0 ) {
362+ return matches ;
363+ }
364+
365+ if ( parsed . isFull ) {
366+ let normalized = normalizeText ( parsed . content ) ;
367+ // 太短的内容降级会产生大量误匹配,这里做个下限
368+ if ( normalized . length >= 3 ) {
369+ let relaxed = collectMatches ( false ) ;
370+ if ( relaxed . length > 0 ) {
371+ // 去重(同一角色可能在多个字段命中)
372+ let seen = { } ;
373+ let uniq = [ ] ;
374+ for ( let i = 0 ; i < relaxed . length ; i ++ ) {
375+ let name = relaxed [ i ] . name ;
376+ if ( seen [ name ] ) {
377+ continue ;
378+ }
379+ seen [ name ] = true ;
380+ uniq . push ( relaxed [ i ] ) ;
381+ }
382+ return uniq ;
210383 }
211384 }
212385 }
386+
213387 return matches ;
214388}
215389
@@ -219,7 +393,7 @@ function logMatches(matches) {
219393 }
220394
221395 // “只显示角色名”时仅输出角色名
222- if ( settings && settings . onlyName ) {
396+ if ( typeof settings !== "undefined" && settings && settings . onlyName ) {
223397 for ( let i = 0 ; i < matches . length ; i ++ ) {
224398 let m = matches [ i ] ;
225399 log . info ( "角色名:{0}" , m . name ) ;
@@ -286,3 +460,11 @@ function formatBriefInfo(info, name) {
286460 }
287461 return parts . join ( "," ) ;
288462}
463+
464+
465+
466+
467+
468+
469+
470+
0 commit comments