kotonebot 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotonebot/backend/context/context.py +1008 -1000
- kotonebot/backend/debug/vars.py +6 -1
- kotonebot/backend/image.py +778 -748
- kotonebot/backend/loop.py +283 -276
- kotonebot/backend/ocr.py +20 -2
- kotonebot/client/device.py +6 -3
- kotonebot/client/host/mumu12_host.py +157 -44
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +5 -0
- kotonebot-0.3.0.dist-info/METADATA +76 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.3.0.dist-info}/RECORD +13 -13
- kotonebot-0.1.0.dist-info/METADATA +0 -204
- {kotonebot-0.1.0.dist-info → kotonebot-0.3.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.3.0.dist-info}/top_level.txt +0 -0
kotonebot/backend/loop.py
CHANGED
|
@@ -1,277 +1,284 @@
|
|
|
1
|
-
import time
|
|
2
|
-
from functools import lru_cache, partial
|
|
3
|
-
from typing import Callable, Any, overload, Literal, Generic, TypeVar, cast, get_args, get_origin
|
|
4
|
-
|
|
5
|
-
from cv2.typing import MatLike
|
|
6
|
-
|
|
7
|
-
from kotonebot.util import Interval
|
|
8
|
-
from kotonebot import device, image, ocr
|
|
9
|
-
from kotonebot.backend.core import Image
|
|
10
|
-
from kotonebot.backend.ocr import TextComparator
|
|
11
|
-
from kotonebot.client.protocol import ClickableObjectProtocol
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class LoopAction:
|
|
15
|
-
def __init__(self, loop: 'Loop', func: Callable[[], ClickableObjectProtocol | None]):
|
|
16
|
-
self.loop = loop
|
|
17
|
-
self.func = func
|
|
18
|
-
self.result: ClickableObjectProtocol | None = None
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
def found(self):
|
|
22
|
-
"""
|
|
23
|
-
是否找到结果。若父 Loop 未在运行中,则返回 False。
|
|
24
|
-
"""
|
|
25
|
-
if not self.loop.running:
|
|
26
|
-
return False
|
|
27
|
-
return bool(self.result)
|
|
28
|
-
|
|
29
|
-
def __bool__(self):
|
|
30
|
-
return self.found
|
|
31
|
-
|
|
32
|
-
def reset(self):
|
|
33
|
-
"""
|
|
34
|
-
重置 LoopAction,以复用此对象。
|
|
35
|
-
"""
|
|
36
|
-
self.result = None
|
|
37
|
-
|
|
38
|
-
def do(self):
|
|
39
|
-
"""
|
|
40
|
-
执行 LoopAction。
|
|
41
|
-
:return: 执行结果。
|
|
42
|
-
"""
|
|
43
|
-
if not self.loop.running:
|
|
44
|
-
return
|
|
45
|
-
if self.loop.found_anything:
|
|
46
|
-
# 本轮循环已执行任意操作,因此不需要再继续检测
|
|
47
|
-
return
|
|
48
|
-
self.result = self.func()
|
|
49
|
-
if self.result:
|
|
50
|
-
self.loop.found_anything = True
|
|
51
|
-
|
|
52
|
-
def click(self, *, at: tuple[int, int] | None = None):
|
|
53
|
-
"""
|
|
54
|
-
点击寻找结果。若结果为空,会跳过执行。
|
|
55
|
-
|
|
56
|
-
:return:
|
|
57
|
-
"""
|
|
58
|
-
if self.result:
|
|
59
|
-
if at is not None:
|
|
60
|
-
device.click(*at)
|
|
61
|
-
else:
|
|
62
|
-
device.click(self.result)
|
|
63
|
-
|
|
64
|
-
def call(self, func: Callable[[ClickableObjectProtocol], Any]):
|
|
65
|
-
pass
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class Loop:
|
|
69
|
-
def __init__(
|
|
70
|
-
self,
|
|
71
|
-
*,
|
|
72
|
-
timeout: float = 300,
|
|
73
|
-
interval: float = 0.3,
|
|
74
|
-
auto_screenshot: bool = True
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
self.
|
|
78
|
-
self.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.
|
|
84
|
-
self.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
self.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def when(self, condition:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"""
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
self.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if
|
|
190
|
-
raise ValueError('
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
# #
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
# #
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
#
|
|
256
|
-
# for
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
# sl.
|
|
269
|
-
#
|
|
270
|
-
# sl.
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
# case '
|
|
275
|
-
# sl.
|
|
276
|
-
#
|
|
1
|
+
import time
|
|
2
|
+
from functools import lru_cache, partial
|
|
3
|
+
from typing import Callable, Any, overload, Literal, Generic, TypeVar, cast, get_args, get_origin
|
|
4
|
+
|
|
5
|
+
from cv2.typing import MatLike
|
|
6
|
+
|
|
7
|
+
from kotonebot.util import Interval
|
|
8
|
+
from kotonebot import device, image, ocr
|
|
9
|
+
from kotonebot.backend.core import Image
|
|
10
|
+
from kotonebot.backend.ocr import TextComparator
|
|
11
|
+
from kotonebot.client.protocol import ClickableObjectProtocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LoopAction:
|
|
15
|
+
def __init__(self, loop: 'Loop', func: Callable[[], ClickableObjectProtocol | None]):
|
|
16
|
+
self.loop = loop
|
|
17
|
+
self.func = func
|
|
18
|
+
self.result: ClickableObjectProtocol | None = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def found(self):
|
|
22
|
+
"""
|
|
23
|
+
是否找到结果。若父 Loop 未在运行中,则返回 False。
|
|
24
|
+
"""
|
|
25
|
+
if not self.loop.running:
|
|
26
|
+
return False
|
|
27
|
+
return bool(self.result)
|
|
28
|
+
|
|
29
|
+
def __bool__(self):
|
|
30
|
+
return self.found
|
|
31
|
+
|
|
32
|
+
def reset(self):
|
|
33
|
+
"""
|
|
34
|
+
重置 LoopAction,以复用此对象。
|
|
35
|
+
"""
|
|
36
|
+
self.result = None
|
|
37
|
+
|
|
38
|
+
def do(self):
|
|
39
|
+
"""
|
|
40
|
+
执行 LoopAction。
|
|
41
|
+
:return: 执行结果。
|
|
42
|
+
"""
|
|
43
|
+
if not self.loop.running:
|
|
44
|
+
return
|
|
45
|
+
if self.loop.found_anything:
|
|
46
|
+
# 本轮循环已执行任意操作,因此不需要再继续检测
|
|
47
|
+
return
|
|
48
|
+
self.result = self.func()
|
|
49
|
+
if self.result:
|
|
50
|
+
self.loop.found_anything = True
|
|
51
|
+
|
|
52
|
+
def click(self, *, at: tuple[int, int] | None = None):
|
|
53
|
+
"""
|
|
54
|
+
点击寻找结果。若结果为空,会跳过执行。
|
|
55
|
+
|
|
56
|
+
:return:
|
|
57
|
+
"""
|
|
58
|
+
if self.result:
|
|
59
|
+
if at is not None:
|
|
60
|
+
device.click(*at)
|
|
61
|
+
else:
|
|
62
|
+
device.click(self.result)
|
|
63
|
+
|
|
64
|
+
def call(self, func: Callable[[ClickableObjectProtocol], Any]):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Loop:
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
timeout: float = 300,
|
|
73
|
+
interval: float = 0.3,
|
|
74
|
+
auto_screenshot: bool = True,
|
|
75
|
+
skip_first_wait: bool = True
|
|
76
|
+
):
|
|
77
|
+
self.running = True
|
|
78
|
+
self.found_anything = False
|
|
79
|
+
self.auto_screenshot = auto_screenshot
|
|
80
|
+
"""
|
|
81
|
+
是否在每次循环开始时(Loop.tick() 被调用时)截图。
|
|
82
|
+
"""
|
|
83
|
+
self.__last_loop: float = -1
|
|
84
|
+
self.__interval = Interval(interval)
|
|
85
|
+
self.screenshot: MatLike | None = None
|
|
86
|
+
"""上次截图时的图像数据。"""
|
|
87
|
+
self.__skip_first_wait = skip_first_wait
|
|
88
|
+
self.__is_first_tick = True
|
|
89
|
+
|
|
90
|
+
def __iter__(self):
|
|
91
|
+
self.__interval.reset()
|
|
92
|
+
self.__is_first_tick = True
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def __next__(self):
|
|
96
|
+
if not self.running:
|
|
97
|
+
raise StopIteration
|
|
98
|
+
self.found_anything = False
|
|
99
|
+
self.__last_loop = time.time()
|
|
100
|
+
return self.tick()
|
|
101
|
+
|
|
102
|
+
def tick(self):
|
|
103
|
+
if not (self.__is_first_tick and self.__skip_first_wait):
|
|
104
|
+
self.__interval.wait()
|
|
105
|
+
self.__is_first_tick = False
|
|
106
|
+
|
|
107
|
+
if self.auto_screenshot:
|
|
108
|
+
self.screenshot = device.screenshot()
|
|
109
|
+
self.__last_loop = time.time()
|
|
110
|
+
self.found_anything = False
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def exit(self):
|
|
114
|
+
"""
|
|
115
|
+
结束循环。
|
|
116
|
+
"""
|
|
117
|
+
self.running = False
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def when(self, condition: Image) -> LoopAction:
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
@overload
|
|
124
|
+
def when(self, condition: TextComparator) -> LoopAction:
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def when(self, condition: Any):
|
|
128
|
+
"""
|
|
129
|
+
判断某个条件是否成立。
|
|
130
|
+
|
|
131
|
+
:param condition:
|
|
132
|
+
:return:
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(condition, Image):
|
|
135
|
+
func = partial(image.find, condition)
|
|
136
|
+
elif isinstance(condition, TextComparator):
|
|
137
|
+
func = partial(ocr.find, condition)
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError('Invalid condition type.')
|
|
140
|
+
la = LoopAction(self, func)
|
|
141
|
+
la.reset()
|
|
142
|
+
la.do()
|
|
143
|
+
return la
|
|
144
|
+
|
|
145
|
+
def until(self, condition: Any):
|
|
146
|
+
"""
|
|
147
|
+
当满足指定条件时,结束循环。
|
|
148
|
+
|
|
149
|
+
等价于 ``loop.when(...).call(lambda _: loop.exit())``
|
|
150
|
+
"""
|
|
151
|
+
return self.when(condition).call(lambda _: self.exit())
|
|
152
|
+
|
|
153
|
+
def click_if(self, condition: Any, *, at: tuple[int, int] | None = None):
|
|
154
|
+
"""
|
|
155
|
+
检测指定对象是否出现,若出现,点击该对象或指定位置。
|
|
156
|
+
|
|
157
|
+
``click_if()`` 等价于 ``loop.when(...).click(...)``。
|
|
158
|
+
|
|
159
|
+
:param condition: 检测目标。
|
|
160
|
+
:param at: 点击位置。若为 None,表示点击找到的目标。
|
|
161
|
+
"""
|
|
162
|
+
return self.when(condition).click(at=at)
|
|
163
|
+
|
|
164
|
+
StateType = TypeVar('StateType')
|
|
165
|
+
class StatedLoop(Loop, Generic[StateType]):
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
states: list[Any] | None = None,
|
|
169
|
+
initial_state: StateType | None = None,
|
|
170
|
+
*,
|
|
171
|
+
timeout: float = 300,
|
|
172
|
+
interval: float = 0.3,
|
|
173
|
+
auto_screenshot: bool = True
|
|
174
|
+
):
|
|
175
|
+
self.__tmp_states = states
|
|
176
|
+
self.__tmp_initial_state = initial_state
|
|
177
|
+
self.state: StateType
|
|
178
|
+
super().__init__(timeout=timeout, interval=interval, auto_screenshot=auto_screenshot)
|
|
179
|
+
|
|
180
|
+
def __iter__(self):
|
|
181
|
+
# __retrive_state_values() 只能在非 __init__ 中调用
|
|
182
|
+
self.__retrive_state_values()
|
|
183
|
+
return super().__iter__()
|
|
184
|
+
|
|
185
|
+
def __retrive_state_values(self):
|
|
186
|
+
# HACK: __orig_class__ 是 undocumented 属性
|
|
187
|
+
if not hasattr(self, '__orig_class__'):
|
|
188
|
+
# 如果 Foo 不是以参数化泛型的方式实例化的,可能没有 __orig_class__
|
|
189
|
+
if self.state is None:
|
|
190
|
+
raise ValueError('Either specify `states` or use StatedLoop[Literal[...]] syntax.')
|
|
191
|
+
else:
|
|
192
|
+
generic_type_args = get_args(self.__orig_class__) # type: ignore
|
|
193
|
+
if len(generic_type_args) != 1:
|
|
194
|
+
raise ValueError('StatedLoop must have exactly one generic type argument.')
|
|
195
|
+
state_values = get_args(generic_type_args[0])
|
|
196
|
+
if not state_values:
|
|
197
|
+
raise ValueError('StatedLoop must have at least one state value.')
|
|
198
|
+
self.states = cast(tuple[StateType, ...], state_values)
|
|
199
|
+
self.state = self.__tmp_initial_state or self.states[0]
|
|
200
|
+
return state_values
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def StatedLoop2(states: StateType) -> StatedLoop[StateType]:
|
|
204
|
+
state_values = get_args(states)
|
|
205
|
+
return cast(StatedLoop[StateType], Loop())
|
|
206
|
+
|
|
207
|
+
if __name__ == '__main__':
|
|
208
|
+
from kotonebot.kaa.tasks import R
|
|
209
|
+
from kotonebot.backend.ocr import contains
|
|
210
|
+
from kotonebot.backend.context import manual_context, init_context
|
|
211
|
+
|
|
212
|
+
# T = TypeVar('T')
|
|
213
|
+
# class Foo(Generic[T]):
|
|
214
|
+
# def get_literal_params(self) -> list | None:
|
|
215
|
+
# """
|
|
216
|
+
# 尝试获取泛型参数 T (如果它是 Literal 类型) 的参数列表。
|
|
217
|
+
# """
|
|
218
|
+
# # self.__orig_class__ 会是 Foo 的具体参数化类型,
|
|
219
|
+
# # 例如 Foo[Literal['p0', 'p1', 'p2', 'p3', 'ap']]
|
|
220
|
+
# if not hasattr(self, '__orig_class__'):
|
|
221
|
+
# # 如果 Foo 不是以参数化泛型的方式实例化的,可能没有 __orig_class__
|
|
222
|
+
# return None
|
|
223
|
+
#
|
|
224
|
+
# # generic_type_args 是传递给 Foo 的类型参数元组
|
|
225
|
+
# # 例如 (Literal['p0', 'p1', 'p2', 'p3', 'ap'],)
|
|
226
|
+
# generic_type_args = get_args(self.__orig_class__)
|
|
227
|
+
#
|
|
228
|
+
# if not generic_type_args:
|
|
229
|
+
# # Foo 没有类型参数
|
|
230
|
+
# return None
|
|
231
|
+
#
|
|
232
|
+
# # T_type 是 Foo 的第一个类型参数
|
|
233
|
+
# # 例如 Literal['p0', 'p1', 'p2', 'p3', 'ap']
|
|
234
|
+
# t_type = generic_type_args[0]
|
|
235
|
+
#
|
|
236
|
+
# # 检查 T_type 是否是 Literal 类型
|
|
237
|
+
# if get_origin(t_type) is Literal:
|
|
238
|
+
# # literal_args 是 Literal 类型的参数元组
|
|
239
|
+
# # 例如 ('p0', 'p1', 'p2', 'p3', 'ap')
|
|
240
|
+
# literal_args = get_args(t_type)
|
|
241
|
+
# return list(literal_args)
|
|
242
|
+
# else:
|
|
243
|
+
# # T 不是 Literal 类型
|
|
244
|
+
# return None
|
|
245
|
+
# f = Foo[Literal['p0', 'p1', 'p2', 'p3', 'ap']]()
|
|
246
|
+
# values = f.get_literal_params()
|
|
247
|
+
# 1
|
|
248
|
+
|
|
249
|
+
from typing_extensions import reveal_type
|
|
250
|
+
slp = StatedLoop[Literal['p0', 'p1', 'p2', 'p3', 'ap']]()
|
|
251
|
+
for l in slp:
|
|
252
|
+
reveal_type(l.states)
|
|
253
|
+
|
|
254
|
+
# init_context()
|
|
255
|
+
# manual_context().begin()
|
|
256
|
+
# for l in Loop():
|
|
257
|
+
# l.when(R.Produce.ButtonUse).click()
|
|
258
|
+
# l.when(R.Produce.ButtonRefillAP).click()
|
|
259
|
+
# l.when(contains("123")).click()
|
|
260
|
+
# l.click_if(contains("!23"), at=(1, 2))
|
|
261
|
+
|
|
262
|
+
# State = Literal['p0', 'p1', 'p2', 'p3', 'ap']
|
|
263
|
+
# for sl in StatedLoop[State]():
|
|
264
|
+
# match sl.state:
|
|
265
|
+
# case 'p0':
|
|
266
|
+
# sl.click_if(R.Produce.ButtonProduce)
|
|
267
|
+
# sl.click_if(contains('master'))
|
|
268
|
+
# sl.when(R.Produce.ButtonPIdolOverview).goto('p1')
|
|
269
|
+
# # AP 不足
|
|
270
|
+
# sl.when(R.Produce.TextAPInsufficient).goto('ap')
|
|
271
|
+
# case 'ap':
|
|
272
|
+
# pass
|
|
273
|
+
# # p1: 选择偶像
|
|
274
|
+
# case 'p1':
|
|
275
|
+
# sl.call(lambda _: select_idol(idol_skin_id), once=True)
|
|
276
|
+
# sl.when(R.Produce.TextAnotherIdolAvailableDialog).call(dialog.no)
|
|
277
|
+
# sl.click_if(R.Common.ButtonNextNoIcon)
|
|
278
|
+
# sl.until(R.Produce.TextStepIndicator2).goto('p2')
|
|
279
|
+
# case 'p2':
|
|
280
|
+
# sl.when(contains("123")).click()
|
|
281
|
+
# case 'p3':
|
|
282
|
+
# sl.click_if(contains("!23"), at=(1, 2))
|
|
283
|
+
# case _:
|
|
277
284
|
# assert_never(sl.state)
|
kotonebot/backend/ocr.py
CHANGED
|
@@ -24,6 +24,25 @@ logger = logging.getLogger(__name__)
|
|
|
24
24
|
StringMatchFunction = Callable[[str], bool]
|
|
25
25
|
REGEX_NUMBERS = re.compile(r'\d+')
|
|
26
26
|
|
|
27
|
+
global_character_mapping: dict[str, str] = {
|
|
28
|
+
'ó': '6',
|
|
29
|
+
'ą': 'a',
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
全局字符映射表。某些字符可能在某些情况下被错误地识别,此时可以在这里添加映射。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def sanitize_text(text: str) -> str:
|
|
36
|
+
"""
|
|
37
|
+
对识别结果进行清理。此函数将被所有 OCR 引擎调用。
|
|
38
|
+
|
|
39
|
+
默认使用 `global_character_mapping` 中的映射数据进行清理。
|
|
40
|
+
可以重写此函数以实现自定义的清理逻辑。
|
|
41
|
+
"""
|
|
42
|
+
for k, v in global_character_mapping.items():
|
|
43
|
+
text = text.replace(k, v)
|
|
44
|
+
return text
|
|
45
|
+
|
|
27
46
|
@dataclass
|
|
28
47
|
class OcrResult:
|
|
29
48
|
text: str
|
|
@@ -330,8 +349,7 @@ class Ocr:
|
|
|
330
349
|
return OcrResultList()
|
|
331
350
|
ret = []
|
|
332
351
|
for r in result:
|
|
333
|
-
|
|
334
|
-
text = unicodedata.normalize('NFKC', r[1]).replace('ą', 'a')
|
|
352
|
+
text = sanitize_text(r[1])
|
|
335
353
|
# r[0] = [左上, 右上, 右下, 左下]
|
|
336
354
|
# 这里有个坑,返回的点不一定是矩形,只能保证是四边形
|
|
337
355
|
# 所以这里需要计算出四个点的外接矩形
|
kotonebot/client/device.py
CHANGED
|
@@ -292,7 +292,10 @@ class Device:
|
|
|
292
292
|
|
|
293
293
|
def swipe_scaled(self, x1: float, y1: float, x2: float, y2: float, duration: float|None = None) -> None:
|
|
294
294
|
"""
|
|
295
|
-
|
|
295
|
+
滑动屏幕,参数为屏幕坐标的百分比。
|
|
296
|
+
|
|
297
|
+
如果设置了 `self.target_resolution`,则参数为逻辑坐标百分比。
|
|
298
|
+
否则为真实坐标百分比。
|
|
296
299
|
|
|
297
300
|
:param x1: 起始点 x 坐标百分比。范围 [0, 1]
|
|
298
301
|
:param y1: 起始点 y 坐标百分比。范围 [0, 1]
|
|
@@ -300,7 +303,7 @@ class Device:
|
|
|
300
303
|
:param y2: 结束点 y 坐标百分比。范围 [0, 1]
|
|
301
304
|
:param duration: 滑动持续时间,单位秒。None 表示使用默认值。
|
|
302
305
|
"""
|
|
303
|
-
w, h = self.screen_size
|
|
306
|
+
w, h = self.target_resolution or self.screen_size
|
|
304
307
|
self.swipe(int(w * x1), int(h * y1), int(w * x2), int(h * y2), duration)
|
|
305
308
|
|
|
306
309
|
def screenshot(self) -> MatLike:
|
|
@@ -334,7 +337,7 @@ class Device:
|
|
|
334
337
|
@property
|
|
335
338
|
def screen_size(self) -> tuple[int, int]:
|
|
336
339
|
"""
|
|
337
|
-
|
|
340
|
+
真实屏幕尺寸。格式为 `(width, height)`。
|
|
338
341
|
|
|
339
342
|
**注意**: 此属性返回的分辨率会随设备方向变化。
|
|
340
343
|
如果 `self.orientation` 为 `landscape`,则返回的分辨率是横屏下的分辨率,
|