annofabcli 1.114.5__py3-none-any.whl → 1.114.7__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.
- annofabcli/annotation/change_annotation_attributes.py +8 -8
- annofabcli/annotation/create_classification_annotation.py +26 -9
- annofabcli/annotation/delete_annotation.py +14 -14
- annofabcli/annotation/import_annotation.py +57 -19
- annofabcli/annotation_zip/list_annotation_3d_bounding_box.py +2 -2
- annofabcli/annotation_zip/list_annotation_bounding_box_2d.py +2 -2
- annofabcli/annotation_zip/list_polygon_annotation.py +2 -2
- annofabcli/annotation_zip/list_polyline_annotation.py +2 -2
- annofabcli/annotation_zip/list_range_annotation.py +2 -2
- annofabcli/annotation_zip/list_single_point_annotation.py +2 -2
- annofabcli/comment/delete_comment.py +2 -2
- annofabcli/comment/put_comment.py +179 -36
- annofabcli/comment/put_comment_simply.py +7 -4
- annofabcli/comment/put_inspection_comment.py +5 -1
- annofabcli/comment/put_onhold_comment.py +5 -1
- annofabcli/filesystem/draw_annotation.py +1 -1
- annofabcli/input_data/list_all_input_data_merged_task.py +3 -3
- annofabcli/instruction/copy_instruction.py +1 -1
- annofabcli/statistics/histogram.py +1 -1
- annofabcli/statistics/list_annotation_count.py +4 -4
- annofabcli/statistics/list_annotation_duration.py +1 -1
- annofabcli/statistics/list_video_duration.py +1 -1
- annofabcli/task/change_operator.py +3 -3
- annofabcli/task/complete_tasks.py +5 -3
- {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/METADATA +1 -1
- {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/RECORD +29 -29
- {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/WHEEL +0 -0
- {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,6 +9,7 @@ from typing import Any
|
|
|
9
9
|
import annofabapi
|
|
10
10
|
import requests
|
|
11
11
|
from annofabapi.models import CommentType, TaskPhase, TaskStatus
|
|
12
|
+
from annofabapi.pydantic_models.input_data_type import InputDataType
|
|
12
13
|
from dataclasses_json import DataClassJsonMixin
|
|
13
14
|
|
|
14
15
|
from annofabcli.comment.utils import get_comment_type_name
|
|
@@ -18,6 +19,18 @@ from annofabcli.common.type_util import assert_noreturn
|
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
22
|
+
SUPPORTED_ANNOTATION_TYPES_FOR_INSPECTION_DATA = frozenset(
|
|
23
|
+
[
|
|
24
|
+
"user_bounding_box",
|
|
25
|
+
"bounding_box",
|
|
26
|
+
"polygon",
|
|
27
|
+
"polyline",
|
|
28
|
+
"point",
|
|
29
|
+
"range",
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
"""検査コメントの座標情報(InspectionData形式)への変換をサポートしているアノテーションタイプ"""
|
|
33
|
+
|
|
21
34
|
|
|
22
35
|
@dataclass
|
|
23
36
|
class AddedComment(DataClassJsonMixin):
|
|
@@ -28,15 +41,18 @@ class AddedComment(DataClassJsonMixin):
|
|
|
28
41
|
comment: str
|
|
29
42
|
"""コメントの中身"""
|
|
30
43
|
|
|
31
|
-
data: dict[str, Any] | None
|
|
44
|
+
data: dict[str, Any] | None = None
|
|
32
45
|
"""コメントを付与する位置や区間"""
|
|
33
46
|
|
|
34
|
-
annotation_id: str | None
|
|
47
|
+
annotation_id: str | None = None
|
|
35
48
|
"""コメントに紐付けるアノテーションID"""
|
|
36
49
|
|
|
37
|
-
phrases: list[str] | None
|
|
50
|
+
phrases: list[str] | None = None
|
|
38
51
|
"""参照している定型指摘ID"""
|
|
39
52
|
|
|
53
|
+
comment_id: str | None = None
|
|
54
|
+
"""コメントID。省略時はUUIDv4が自動生成される。"""
|
|
55
|
+
|
|
40
56
|
|
|
41
57
|
AddedCommentsForTask = dict[str, list[AddedComment]]
|
|
42
58
|
"""
|
|
@@ -51,6 +67,71 @@ keyはtask_id
|
|
|
51
67
|
"""
|
|
52
68
|
|
|
53
69
|
|
|
70
|
+
def convert_annotation_body_to_inspection_data( # noqa: PLR0911
|
|
71
|
+
annotation_body: dict[str, Any],
|
|
72
|
+
annotation_type: str,
|
|
73
|
+
input_data_type: InputDataType,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""
|
|
76
|
+
アノテーションのbody部分を、検査コメントの座標情報(`InspectionData`形式)に変換する。
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
annotation_body: Annotationの座標情報。AnnotationDetailContentOutputのdict形式
|
|
80
|
+
annotation_type: アノテーションタイプ。`SUPPORTED_ANNOTATION_TYPES_FOR_INSPECTION_DATA`のいずれかを指定すること。
|
|
81
|
+
input_data_type: プロジェクトの入力データタイプ。
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
検査コメントの座標情報(`InspectionData`形式)
|
|
85
|
+
"""
|
|
86
|
+
match input_data_type:
|
|
87
|
+
case InputDataType.MOVIE:
|
|
88
|
+
match annotation_type:
|
|
89
|
+
case "range":
|
|
90
|
+
# 区間の開始位置をTime形式に変換
|
|
91
|
+
return {"start": annotation_body["data"]["begin"], "end": annotation_body["data"]["end"], "_type": "Time"}
|
|
92
|
+
case _:
|
|
93
|
+
# "Classification"など、Time形式以外のアノテーションタイプ
|
|
94
|
+
return {"start": 0, "end": 100, "_type": "Time"}
|
|
95
|
+
|
|
96
|
+
case InputDataType.CUSTOM:
|
|
97
|
+
match annotation_type:
|
|
98
|
+
case "user_bounding_box":
|
|
99
|
+
# 3次元バウンディングボックスの場合、data.dataの文字列をパースして返す
|
|
100
|
+
return {"data": annotation_body["data"]["data"], "_type": "Custom"}
|
|
101
|
+
case _:
|
|
102
|
+
# セグメントなど
|
|
103
|
+
return {
|
|
104
|
+
"data": '{"kind": "CUBOID", "shape": {"dimensions": {"width": 1.0, "height": 1.0, "depth": 1.0}, "location": {"x": 0.0, "y": 0.0, "z": 0.0}, "rotation": {"x": 0.0, "y": 0.0, "z": 0.0}, "direction": {"front": {"x": 1.0, "y": 0.0, "z": 0.0}, "up": {"x": 0.0, "y": 0.0, "z": 1.0}}}, "version": "2"}', # noqa: E501
|
|
105
|
+
"_type": "Custom",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case InputDataType.IMAGE:
|
|
109
|
+
match annotation_type:
|
|
110
|
+
case "bounding_box":
|
|
111
|
+
# 中心点を計算
|
|
112
|
+
left_top = annotation_body["data"]["left_top"]
|
|
113
|
+
right_bottom = annotation_body["data"]["right_bottom"]
|
|
114
|
+
center_x = (left_top["x"] + right_bottom["x"]) / 2
|
|
115
|
+
center_y = (left_top["y"] + right_bottom["y"]) / 2
|
|
116
|
+
return {"x": center_x, "y": center_y, "_type": "Point"}
|
|
117
|
+
case "polygon" | "polyline":
|
|
118
|
+
# 先頭の点を取得
|
|
119
|
+
points = annotation_body["data"]["points"]
|
|
120
|
+
assert len(points) > 0
|
|
121
|
+
first_point = points[0]
|
|
122
|
+
return {"x": first_point["x"], "y": first_point["y"], "_type": "Point"}
|
|
123
|
+
case "point":
|
|
124
|
+
# 点の形式に変換(pointキーから取り出す)
|
|
125
|
+
point = annotation_body["data"]["point"]
|
|
126
|
+
return {"x": point["x"], "y": point["y"], "_type": "Point"}
|
|
127
|
+
case "range":
|
|
128
|
+
# 区間の開始位置をTime形式に変換
|
|
129
|
+
return {"start": annotation_body["data"]["begin"], "end": annotation_body["data"]["end"], "_type": "Time"}
|
|
130
|
+
case _:
|
|
131
|
+
# 塗りつぶしや分類など
|
|
132
|
+
return {"x": 0, "y": 0, "_type": "Point"}
|
|
133
|
+
|
|
134
|
+
|
|
54
135
|
class PutCommentMain(CommandLineWithConfirm):
|
|
55
136
|
def __init__(self, service: annofabapi.Resource, project_id: str, comment_type: CommentType, all_yes: bool = False) -> None: # noqa: FBT001, FBT002
|
|
56
137
|
self.service = service
|
|
@@ -60,30 +141,68 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
60
141
|
self.comment_type = comment_type
|
|
61
142
|
self.comment_type_name = get_comment_type_name(comment_type)
|
|
62
143
|
|
|
144
|
+
# プロジェクト情報を取得
|
|
145
|
+
project, _ = self.service.api.get_project(self.project_id)
|
|
146
|
+
self.input_data_type = InputDataType(project["input_data_type"])
|
|
147
|
+
|
|
148
|
+
# アノテーション仕様を取得
|
|
149
|
+
annotation_specs, _ = self.service.api.get_annotation_specs(self.project_id, query_params={"v": "3"})
|
|
150
|
+
self.dict_label_id_annotation_type = {label["label_id"]: label["annotation_type"] for label in annotation_specs["labels"]}
|
|
151
|
+
|
|
63
152
|
CommandLineWithConfirm.__init__(self, all_yes)
|
|
64
153
|
|
|
65
154
|
def _create_request_body(self, task: dict[str, Any], input_data_id: str, comments: list[AddedComment]) -> list[dict[str, Any]]:
|
|
66
155
|
"""batch_update_comments に渡すリクエストボディを作成する。"""
|
|
156
|
+
task_id = task["task_id"]
|
|
157
|
+
|
|
158
|
+
# annotation_idが指定されているがdataがNoneのコメントがあるか確認
|
|
159
|
+
need_annotation_data = any(c.annotation_id is not None and c.data is None for c in comments)
|
|
160
|
+
|
|
161
|
+
dict_annotation_id_label_id: dict[str, str] = {}
|
|
162
|
+
dict_annotation_id_data: dict[str, dict[str, Any]] = {}
|
|
67
163
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
164
|
+
if need_annotation_data:
|
|
165
|
+
# アノテーション詳細を取得
|
|
166
|
+
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
167
|
+
details = editor_annotation["details"]
|
|
72
168
|
|
|
73
|
-
|
|
169
|
+
for detail in details:
|
|
170
|
+
annotation_id = detail["annotation_id"]
|
|
171
|
+
label_id = detail["label_id"]
|
|
172
|
+
dict_annotation_id_label_id[annotation_id] = label_id
|
|
173
|
+
|
|
174
|
+
# annotation_typeを取得
|
|
175
|
+
if label_id in self.dict_label_id_annotation_type:
|
|
176
|
+
# アノテーション仕様に存在しないラベルを使っているアノテーション(アノテーションを作成してから仕様を変更した場合)もあるので、判定する
|
|
177
|
+
annotation_type = self.dict_label_id_annotation_type[label_id]
|
|
178
|
+
dict_annotation_id_data[annotation_id] = convert_annotation_body_to_inspection_data(detail["body"], annotation_type, input_data_type=self.input_data_type)
|
|
179
|
+
else:
|
|
180
|
+
# annotation_idからlabel_idを取得するためだけにAPIを呼ぶ
|
|
181
|
+
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
182
|
+
details = editor_annotation["details"]
|
|
183
|
+
dict_annotation_id_label_id = {e["annotation_id"]: e["label_id"] for e in details}
|
|
74
184
|
|
|
75
|
-
def _convert(comment: AddedComment) -> dict[str, Any]:
|
|
185
|
+
def _convert(comment: AddedComment) -> dict[str, Any] | None:
|
|
186
|
+
data = comment.data
|
|
187
|
+
annotation_id = comment.annotation_id
|
|
188
|
+
|
|
189
|
+
# dataがNoneでannotation_idが指定されている場合、dataを補完
|
|
190
|
+
if data is None:
|
|
191
|
+
assert annotation_id is not None
|
|
192
|
+
data = dict_annotation_id_data[annotation_id]
|
|
193
|
+
|
|
194
|
+
assert data is not None
|
|
76
195
|
return {
|
|
77
|
-
"comment_id": str(uuid.uuid4()),
|
|
196
|
+
"comment_id": comment.comment_id if comment.comment_id is not None else str(uuid.uuid4()),
|
|
78
197
|
"phase": task["phase"],
|
|
79
198
|
"phase_stage": task["phase_stage"],
|
|
80
199
|
"account_id": self.service.api.account_id,
|
|
81
200
|
"comment_type": self.comment_type.value,
|
|
82
201
|
"comment": comment.comment,
|
|
83
202
|
"comment_node": {
|
|
84
|
-
"data":
|
|
85
|
-
"annotation_id":
|
|
86
|
-
"label_id": dict_annotation_id_label_id.get(
|
|
203
|
+
"data": data,
|
|
204
|
+
"annotation_id": annotation_id,
|
|
205
|
+
"label_id": dict_annotation_id_label_id.get(annotation_id) if annotation_id is not None else None,
|
|
87
206
|
"status": "open",
|
|
88
207
|
"_type": "Root",
|
|
89
208
|
},
|
|
@@ -91,7 +210,8 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
91
210
|
"_type": "Put",
|
|
92
211
|
}
|
|
93
212
|
|
|
94
|
-
|
|
213
|
+
converted_comments = [_convert(e) for e in comments]
|
|
214
|
+
return [c for c in converted_comments if c is not None]
|
|
95
215
|
|
|
96
216
|
def change_to_working_status(self, project_id: str, task: dict[str, Any]) -> dict[str, Any]:
|
|
97
217
|
"""
|
|
@@ -129,8 +249,8 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
129
249
|
logger.warning(f"task_id='{task_id}' :: フェーズが検査/受入でないため検査コメントを付与できません。 :: task_phase='{task['phase']}'")
|
|
130
250
|
return False
|
|
131
251
|
|
|
132
|
-
if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.
|
|
133
|
-
logger.warning(f"task_id='{task_id}' ::
|
|
252
|
+
if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.BREAK.value]:
|
|
253
|
+
logger.warning(f"task_id='{task_id}' :: タスクの状態が未着手,休憩中 以外の状態なので、コメントを付与できません。 :: task_status='{task['status']}'")
|
|
134
254
|
return False
|
|
135
255
|
return True
|
|
136
256
|
|
|
@@ -139,7 +259,7 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
139
259
|
task_id: str,
|
|
140
260
|
comments_for_task: AddedCommentsForTask,
|
|
141
261
|
task_index: int | None = None,
|
|
142
|
-
) -> int:
|
|
262
|
+
) -> tuple[int, int]:
|
|
143
263
|
"""
|
|
144
264
|
タスクにコメントを付与します。
|
|
145
265
|
|
|
@@ -149,28 +269,29 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
149
269
|
task_index: タスクの連番
|
|
150
270
|
|
|
151
271
|
Returns:
|
|
152
|
-
|
|
272
|
+
(コメントを付与した入力データの個数, 付与したコメントの個数)のタプル
|
|
153
273
|
"""
|
|
154
274
|
logging_prefix = f"{task_index + 1} 件目" if task_index is not None else ""
|
|
155
275
|
|
|
156
276
|
task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
157
277
|
if task is None:
|
|
158
278
|
logger.warning(f"{logging_prefix} :: task_id='{task_id}' のタスクは存在しないので、スキップします。")
|
|
159
|
-
return 0
|
|
279
|
+
return (0, 0)
|
|
160
280
|
|
|
161
281
|
logger.debug(f"{logging_prefix} : task_id='{task['task_id']}', status='{task['status']}', phase='{task['phase']}'")
|
|
162
282
|
|
|
163
283
|
if not self._can_add_comment(
|
|
164
284
|
task=task,
|
|
165
285
|
):
|
|
166
|
-
return 0
|
|
286
|
+
return (0, 0)
|
|
167
287
|
|
|
168
288
|
if not self.confirm_processing(f"task_id='{task_id}' のタスクに{self.comment_type_name}を付与しますか?"):
|
|
169
|
-
return 0
|
|
289
|
+
return (0, 0)
|
|
170
290
|
|
|
171
291
|
# コメントを付与するには作業中状態にする必要がある
|
|
172
292
|
changed_task = self.change_to_working_status(self.project_id, task)
|
|
173
|
-
|
|
293
|
+
added_input_data_count = 0
|
|
294
|
+
added_comment_count = 0
|
|
174
295
|
for input_data_id, comments in comments_for_task.items():
|
|
175
296
|
if input_data_id not in task["input_data_id_list"]:
|
|
176
297
|
logger.warning(f"{logging_prefix} :: task_id='{task_id}'のタスクに input_data_id='{input_data_id}'の入力データは存在しません。")
|
|
@@ -180,7 +301,8 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
180
301
|
if len(comments) > 0:
|
|
181
302
|
request_body = self._create_request_body(task=changed_task, input_data_id=input_data_id, comments=comments)
|
|
182
303
|
self.service.api.batch_update_comments(self.project_id, task_id, input_data_id, request_body=request_body)
|
|
183
|
-
|
|
304
|
+
added_input_data_count += 1
|
|
305
|
+
added_comment_count += len(comments)
|
|
184
306
|
logger.debug(f"{logging_prefix} :: task_id='{task_id}', input_data_id='{input_data_id}' :: {len(comments)}件のコメントを付与しました。")
|
|
185
307
|
except Exception: # pylint: disable=broad-except
|
|
186
308
|
logger.warning(
|
|
@@ -194,12 +316,12 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
194
316
|
self.service.wrapper.change_task_operator(self.project_id, task_id, task["account_id"])
|
|
195
317
|
logger.debug(f"{logging_prefix} :: task_id='{task_id}' :: 担当者を元のユーザ( account_id='{task['account_id']}')に戻しました。")
|
|
196
318
|
|
|
197
|
-
return
|
|
319
|
+
return (added_input_data_count, added_comment_count)
|
|
198
320
|
|
|
199
321
|
def add_comments_for_task_wrapper(
|
|
200
322
|
self,
|
|
201
323
|
tpl: tuple[int, tuple[str, AddedCommentsForTask]],
|
|
202
|
-
) -> int:
|
|
324
|
+
) -> tuple[int, int]:
|
|
203
325
|
task_index, (task_id, comments_for_task) = tpl
|
|
204
326
|
return self.add_comments_for_task(task_id=task_id, comments_for_task=comments_for_task, task_index=task_index)
|
|
205
327
|
|
|
@@ -208,30 +330,45 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
208
330
|
comments_for_task_list: AddedComments,
|
|
209
331
|
parallelism: int | None = None,
|
|
210
332
|
) -> None:
|
|
211
|
-
|
|
212
|
-
|
|
333
|
+
tasks_count = len(comments_for_task_list)
|
|
334
|
+
input_data_count = sum(len(e) for e in comments_for_task_list.values())
|
|
335
|
+
total_comment_count = sum(len(comments) for comments_for_task in comments_for_task_list.values() for comments in comments_for_task.values())
|
|
336
|
+
logger.info(
|
|
337
|
+
f"{self.comment_type_name}を付与するタスク数: {tasks_count}, {self.comment_type_name}を付与する入力データ数: {input_data_count}, {self.comment_type_name}の総数: {total_comment_count}"
|
|
338
|
+
)
|
|
213
339
|
|
|
214
340
|
if parallelism is not None:
|
|
215
341
|
with multiprocessing.Pool(parallelism) as pool:
|
|
216
|
-
|
|
217
|
-
|
|
342
|
+
result_list = pool.map(self.add_comments_for_task_wrapper, enumerate(comments_for_task_list.items()))
|
|
343
|
+
added_input_data_count = sum(e[0] for e in result_list)
|
|
344
|
+
added_comment_count = sum(e[1] for e in result_list)
|
|
345
|
+
succeeded_tasks_count = sum(1 for e in result_list if e[0] > 0)
|
|
218
346
|
|
|
219
347
|
else:
|
|
220
348
|
# 逐次処理
|
|
221
|
-
|
|
349
|
+
added_input_data_count = 0
|
|
350
|
+
added_comment_count = 0
|
|
351
|
+
succeeded_tasks_count = 0
|
|
222
352
|
for task_index, (task_id, comments_for_task) in enumerate(comments_for_task_list.items()):
|
|
223
353
|
try:
|
|
224
|
-
|
|
354
|
+
result_input_data, result_comment = self.add_comments_for_task(
|
|
225
355
|
task_id=task_id,
|
|
226
356
|
comments_for_task=comments_for_task,
|
|
227
357
|
task_index=task_index,
|
|
228
358
|
)
|
|
229
|
-
|
|
359
|
+
added_input_data_count += result_input_data
|
|
360
|
+
added_comment_count += result_comment
|
|
361
|
+
if result_input_data > 0:
|
|
362
|
+
succeeded_tasks_count += 1
|
|
230
363
|
except Exception: # pylint: disable=broad-except
|
|
231
364
|
logger.warning(f"task_id='{task_id}' :: コメントの付与に失敗しました。", exc_info=True)
|
|
232
365
|
continue
|
|
233
366
|
|
|
234
|
-
logger.info(
|
|
367
|
+
logger.info(
|
|
368
|
+
f"{succeeded_tasks_count} / {tasks_count} 件のタスク, "
|
|
369
|
+
f"{added_input_data_count} / {input_data_count} 件の入力データ, "
|
|
370
|
+
f"{added_comment_count} / {total_comment_count} 件の{self.comment_type_name}を付与しました。"
|
|
371
|
+
)
|
|
235
372
|
|
|
236
373
|
|
|
237
374
|
def convert_cli_comments(dict_comments: dict[str, Any], *, comment_type: CommentType) -> AddedComments:
|
|
@@ -248,7 +385,7 @@ def convert_cli_comments(dict_comments: dict[str, Any], *, comment_type: Comment
|
|
|
248
385
|
comment: str
|
|
249
386
|
"""コメントの中身"""
|
|
250
387
|
|
|
251
|
-
data: dict[str, Any]
|
|
388
|
+
data: dict[str, Any] | None = None
|
|
252
389
|
"""コメントを付与する位置や区間"""
|
|
253
390
|
|
|
254
391
|
annotation_id: str | None = None
|
|
@@ -257,6 +394,9 @@ def convert_cli_comments(dict_comments: dict[str, Any], *, comment_type: Comment
|
|
|
257
394
|
phrases: list[str] | None = None
|
|
258
395
|
"""参照している定型指摘ID"""
|
|
259
396
|
|
|
397
|
+
comment_id: str | None = None
|
|
398
|
+
"""コメントID。省略時はUUIDv4が自動生成される。"""
|
|
399
|
+
|
|
260
400
|
@dataclass
|
|
261
401
|
class AddedOnholdComment(DataClassJsonMixin):
|
|
262
402
|
"""
|
|
@@ -269,13 +409,16 @@ def convert_cli_comments(dict_comments: dict[str, Any], *, comment_type: Comment
|
|
|
269
409
|
annotation_id: str | None = None
|
|
270
410
|
"""コメントに紐付けるアノテーションID"""
|
|
271
411
|
|
|
412
|
+
comment_id: str | None = None
|
|
413
|
+
"""コメントID。省略時はUUIDv4が自動生成される。"""
|
|
414
|
+
|
|
272
415
|
def convert_inspection_comment(comment: dict[str, Any]) -> AddedComment:
|
|
273
416
|
tmp = AddedInspectionComment.from_dict(comment)
|
|
274
|
-
return AddedComment(comment=tmp.comment, data=tmp.data, annotation_id=tmp.annotation_id, phrases=tmp.phrases)
|
|
417
|
+
return AddedComment(comment=tmp.comment, data=tmp.data, annotation_id=tmp.annotation_id, phrases=tmp.phrases, comment_id=tmp.comment_id)
|
|
275
418
|
|
|
276
419
|
def convert_onhold_comment(comment: dict[str, Any]) -> AddedComment:
|
|
277
420
|
tmp = AddedOnholdComment.from_dict(comment)
|
|
278
|
-
return AddedComment(comment=tmp.comment, annotation_id=tmp.annotation_id, data=None, phrases=None)
|
|
421
|
+
return AddedComment(comment=tmp.comment, annotation_id=tmp.annotation_id, data=None, phrases=None, comment_id=tmp.comment_id)
|
|
279
422
|
|
|
280
423
|
if comment_type == CommentType.INSPECTION:
|
|
281
424
|
func_convert = convert_inspection_comment
|
|
@@ -31,6 +31,9 @@ class AddedSimpleComment:
|
|
|
31
31
|
phrases: list[str] | None = None
|
|
32
32
|
"""参照している定型指摘ID"""
|
|
33
33
|
|
|
34
|
+
comment_id: str | None = None
|
|
35
|
+
"""コメントID。省略時はUUIDv4が自動生成される。"""
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
class PutCommentSimplyMain(CommandLineWithConfirm):
|
|
36
39
|
def __init__(self, service: annofabapi.Resource, project_id: str, comment_type: CommentType, all_yes: bool = False) -> None: # noqa: FBT001, FBT002
|
|
@@ -49,7 +52,7 @@ class PutCommentSimplyMain(CommandLineWithConfirm):
|
|
|
49
52
|
def _convert(comment: AddedSimpleComment) -> dict[str, Any]:
|
|
50
53
|
return {
|
|
51
54
|
"comment": comment.comment,
|
|
52
|
-
"comment_id": str(uuid.uuid4()),
|
|
55
|
+
"comment_id": comment.comment_id if comment.comment_id is not None else str(uuid.uuid4()),
|
|
53
56
|
"phase": task["phase"],
|
|
54
57
|
"phase_stage": task["phase_stage"],
|
|
55
58
|
"comment_type": self.comment_type.value,
|
|
@@ -90,11 +93,11 @@ class PutCommentSimplyMain(CommandLineWithConfirm):
|
|
|
90
93
|
|
|
91
94
|
if self.comment_type == CommentType.INSPECTION: # noqa: SIM102
|
|
92
95
|
if task["phase"] == TaskPhase.ANNOTATION.value:
|
|
93
|
-
logger.warning(f"task_id='{task_id}'
|
|
96
|
+
logger.warning(f"task_id='{task_id}' :: フェーズが検査/受入でないため検査コメントを付与できません。 :: task_phase='{task['phase']}'")
|
|
94
97
|
return False
|
|
95
98
|
|
|
96
|
-
if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.
|
|
97
|
-
logger.warning(f"task_id='{task_id}'
|
|
99
|
+
if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.BREAK.value]:
|
|
100
|
+
logger.warning(f"task_id='{task_id}' :: タスクの状態が未着手,休憩中 以外の状態なので、コメントを付与できません。 :: task_status='{task['status']}'")
|
|
98
101
|
return False
|
|
99
102
|
return True
|
|
100
103
|
|
|
@@ -68,7 +68,11 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
68
68
|
parser.add_argument(
|
|
69
69
|
"--json",
|
|
70
70
|
type=str,
|
|
71
|
-
help=(
|
|
71
|
+
help=(
|
|
72
|
+
f"付与する検査コメントの内容をJSON形式で指定してください。``file://`` を先頭に付けると、JSON形式のファイルを指定できます。\n\n"
|
|
73
|
+
f"各コメントには ``comment_id`` を指定することができます。省略した場合は自動的にUUIDv4が生成されます。\n\n"
|
|
74
|
+
f"(ex) ``{json.dumps(SAMPLE_JSON, ensure_ascii=False)}``"
|
|
75
|
+
),
|
|
72
76
|
)
|
|
73
77
|
|
|
74
78
|
parser.add_argument(
|
|
@@ -69,7 +69,11 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
69
69
|
parser.add_argument(
|
|
70
70
|
"--json",
|
|
71
71
|
type=str,
|
|
72
|
-
help=(
|
|
72
|
+
help=(
|
|
73
|
+
f"付与する保留コメントの情報をJSON形式で指定してください。``file://`` を先頭に付けると、JSON形式のファイルを指定できます。\n\n"
|
|
74
|
+
f"各コメントには ``comment_id`` を指定することができます。省略した場合は自動的にUUIDv4が生成されます。\n\n"
|
|
75
|
+
f"(ex) ``{json.dumps(SAMPLE_JSON, ensure_ascii=False)}``"
|
|
76
|
+
),
|
|
73
77
|
)
|
|
74
78
|
|
|
75
79
|
parser.add_argument(
|
|
@@ -182,7 +182,7 @@ class DrawingAnnotationForOneImage:
|
|
|
182
182
|
image.save(output_file)
|
|
183
183
|
|
|
184
184
|
elif image_size is not None:
|
|
185
|
-
image = Image.new("RGBA", image_size, color="black")
|
|
185
|
+
image = Image.new("RGBA", image_size, color="black") # type: ignore[assignment]
|
|
186
186
|
draw = ImageDraw.Draw(image)
|
|
187
187
|
self._draw_annotations(draw, parser)
|
|
188
188
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -38,9 +38,9 @@ def _create_dict_tasks_by_input_data_id(task_list: list[dict[str, Any]]) -> dict
|
|
|
38
38
|
for input_data_index, input_data_id in enumerate(task["input_data_id_list"]):
|
|
39
39
|
new_task = {
|
|
40
40
|
"task_id": task["task_id"],
|
|
41
|
-
"task_status": task["status"],
|
|
42
41
|
"task_phase": task["phase"],
|
|
43
42
|
"task_phase_stage": task["phase_stage"],
|
|
43
|
+
"task_status": task["status"],
|
|
44
44
|
"frame_no": input_data_index + 1,
|
|
45
45
|
}
|
|
46
46
|
result[input_data_id].append(new_task)
|
|
@@ -70,7 +70,7 @@ def create_df_input_data_with_merged_task(input_data_list: list[dict[str, Any]])
|
|
|
70
70
|
"""
|
|
71
71
|
|
|
72
72
|
def get_columns(columns: pandas.Index) -> list[str]:
|
|
73
|
-
task_columns = ["task_id", "
|
|
73
|
+
task_columns = ["task_id", "task_phase", "task_phase_stage", "task_status", "frame_no"]
|
|
74
74
|
new_columns = columns.drop(task_columns)
|
|
75
75
|
return list(new_columns) + task_columns
|
|
76
76
|
|
|
@@ -93,7 +93,7 @@ def create_df_input_data_with_merged_task(input_data_list: list[dict[str, Any]])
|
|
|
93
93
|
# ネストしたオブジェクトを`system_metadata.input_duration`のような列名でアクセスできるようにするため
|
|
94
94
|
df_input_data = pandas.json_normalize(new_input_data_list)
|
|
95
95
|
|
|
96
|
-
for column in ["task_id", "
|
|
96
|
+
for column in ["task_id", "task_phase", "task_phase_stage", "task_status", "frame_no"]:
|
|
97
97
|
if column not in df_input_data.columns:
|
|
98
98
|
df_input_data[column] = pandas.NA
|
|
99
99
|
|
|
@@ -31,7 +31,7 @@ class CopyInstruction(CommandLine):
|
|
|
31
31
|
@staticmethod
|
|
32
32
|
def get_instruction_image_id_from_url(url: str) -> str:
|
|
33
33
|
# URL Queryを除いたURLを取得する
|
|
34
|
-
url_without_query = url.split("?")[0]
|
|
34
|
+
url_without_query = url.split("?", maxsplit=1)[0]
|
|
35
35
|
return url_without_query.split("/")[-1]
|
|
36
36
|
|
|
37
37
|
@staticmethod
|
|
@@ -34,7 +34,7 @@ def get_bin_edges(min_value: float, max_value: float, bin_width: float) -> numpy
|
|
|
34
34
|
# stop引数に、`bin_width*2`を指定している理由:
|
|
35
35
|
# 引数が小数のときは`len(bin_edges)``期待通りにならないときがあるので、ビンの数を少し増やしている
|
|
36
36
|
# https://qiita.com/yuji38kwmt/items/ff00f3cb9083567d083f
|
|
37
|
-
bin_edges = numpy.arange(start=min_value, stop=max_value + bin_width * 2, step=bin_width)
|
|
37
|
+
bin_edges = numpy.arange(start=min_value, stop=max_value + bin_width * 2, step=bin_width) # type: ignore[call-overload]
|
|
38
38
|
return bin_edges
|
|
39
39
|
|
|
40
40
|
|
|
@@ -573,9 +573,9 @@ class AttributeCountCsv:
|
|
|
573
573
|
basic_columns = [
|
|
574
574
|
("project_id", "", ""),
|
|
575
575
|
("task_id", "", ""),
|
|
576
|
-
("task_status", "", ""),
|
|
577
576
|
("task_phase", "", ""),
|
|
578
577
|
("task_phase_stage", "", ""),
|
|
578
|
+
("task_status", "", ""),
|
|
579
579
|
("input_data_count", "", ""),
|
|
580
580
|
("annotation_count", "", ""),
|
|
581
581
|
]
|
|
@@ -622,9 +622,9 @@ class AttributeCountCsv:
|
|
|
622
622
|
basic_columns = [
|
|
623
623
|
("project_id", "", ""),
|
|
624
624
|
("task_id", "", ""),
|
|
625
|
-
("task_status", "", ""),
|
|
626
625
|
("task_phase", "", ""),
|
|
627
626
|
("task_phase_stage", "", ""),
|
|
627
|
+
("task_status", "", ""),
|
|
628
628
|
("input_data_id", "", ""),
|
|
629
629
|
("input_data_name", "", ""),
|
|
630
630
|
("frame_no", "", ""),
|
|
@@ -708,9 +708,9 @@ class LabelCountCsv:
|
|
|
708
708
|
basic_columns = [
|
|
709
709
|
"project_id",
|
|
710
710
|
"task_id",
|
|
711
|
-
"task_status",
|
|
712
711
|
"task_phase",
|
|
713
712
|
"task_phase_stage",
|
|
713
|
+
"task_status",
|
|
714
714
|
"input_data_count",
|
|
715
715
|
"annotation_count",
|
|
716
716
|
]
|
|
@@ -757,9 +757,9 @@ class LabelCountCsv:
|
|
|
757
757
|
basic_columns = [
|
|
758
758
|
"project_id",
|
|
759
759
|
"task_id",
|
|
760
|
-
"task_status",
|
|
761
760
|
"task_phase",
|
|
762
761
|
"task_phase_stage",
|
|
762
|
+
"task_status",
|
|
763
763
|
"input_data_id",
|
|
764
764
|
"input_data_name",
|
|
765
765
|
"frame_no",
|
|
@@ -109,9 +109,9 @@ class AnnotationDuration(DataClassJsonMixin):
|
|
|
109
109
|
|
|
110
110
|
project_id: str
|
|
111
111
|
task_id: str
|
|
112
|
-
task_status: TaskStatus
|
|
113
112
|
task_phase: TaskPhase
|
|
114
113
|
task_phase_stage: int
|
|
114
|
+
task_status: TaskStatus
|
|
115
115
|
|
|
116
116
|
input_data_id: str
|
|
117
117
|
input_data_name: str
|
|
@@ -37,7 +37,7 @@ def get_video_duration_list(task_list: list[dict[str, Any]], input_data_list: li
|
|
|
37
37
|
result = []
|
|
38
38
|
for task in task_list:
|
|
39
39
|
task_id = task["task_id"]
|
|
40
|
-
elm = {"project_id": task["project_id"], "task_id": task_id, "
|
|
40
|
+
elm = {"project_id": task["project_id"], "task_id": task_id, "task_phase": task["phase"], "task_phase_stage": task["phase_stage"], "task_status": task["status"]}
|
|
41
41
|
input_data_id_list = task["input_data_id_list"]
|
|
42
42
|
assert len(input_data_id_list) == 1, f"task_id='{task_id}'には複数の入力データが含まれています。"
|
|
43
43
|
input_data_id = input_data_id_list[0]
|
|
@@ -88,7 +88,7 @@ class ChangeOperatorMain:
|
|
|
88
88
|
|
|
89
89
|
if task.status == TaskStatus.ON_HOLD and not self.include_on_hold:
|
|
90
90
|
logger.warning(
|
|
91
|
-
f"{logging_prefix} :: task_id='{task_id}' :: タスクが保留中状態なので、担当者を変更できません。保留中状態のタスクの担当者も変更する場合は、'--
|
|
91
|
+
f"{logging_prefix} :: task_id='{task_id}' :: タスクが保留中状態なので、担当者を変更できません。保留中状態のタスクの担当者も変更する場合は、'--include_on_hold_task'を指定してください。"
|
|
92
92
|
)
|
|
93
93
|
return False
|
|
94
94
|
|
|
@@ -222,7 +222,7 @@ class ChangeOperator(CommandLine):
|
|
|
222
222
|
project_id = args.project_id
|
|
223
223
|
super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
|
|
224
224
|
|
|
225
|
-
main_obj = ChangeOperatorMain(self.service, all_yes=self.all_yes, include_on_hold=args.
|
|
225
|
+
main_obj = ChangeOperatorMain(self.service, all_yes=self.all_yes, include_on_hold=args.include_on_hold_task)
|
|
226
226
|
main_obj.change_operator(
|
|
227
227
|
project_id,
|
|
228
228
|
task_id_list=task_id_list,
|
|
@@ -253,7 +253,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
253
253
|
argument_parser.add_task_query()
|
|
254
254
|
|
|
255
255
|
parser.add_argument(
|
|
256
|
-
"--
|
|
256
|
+
"--include_on_hold_task",
|
|
257
257
|
action="store_true",
|
|
258
258
|
help="指定した場合、保留中のタスクの担当者も変更します。指定しない場合、保留中のタスクはスキップされます。",
|
|
259
259
|
)
|
|
@@ -179,9 +179,11 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
179
179
|
# 教師付フェーズになった日時より、後に付与された返信コメントを取得する
|
|
180
180
|
answered_comment = first_true(
|
|
181
181
|
comment_list,
|
|
182
|
-
pred=lambda e:
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
pred=lambda e: (
|
|
183
|
+
e["comment_node"]["_type"] == "Reply"
|
|
184
|
+
and e["comment_node"]["root_comment_id"] == parent_comment_id
|
|
185
|
+
and dateutil.parser.parse(e["created_datetime"]) >= dateutil.parser.parse(task_started_datetime)
|
|
186
|
+
),
|
|
185
187
|
)
|
|
186
188
|
return answered_comment is not None
|
|
187
189
|
|