File size: 21,408 Bytes
7e01f18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
"""
LLaVA 评估工具模块
==================

本模块提供 LLaVA 评估所需的通用工具函数和类。

主要功能:
    1. OpenAIModel 类: 封装 OpenAI API 调用(支持批量并行)
    2. 模型初始化: init_model() - 加载 LLaVA 模型
    3. 数据读取: read_json() - 读取 JSON/JSONL 文件
    4. 同义词映射: object_synonyms_txt - COCO 物体同义词表
    5. 文本处理: remove_negetive_sents(), remove_woodpecker_boxes()

用途:
    - 在各种评估脚本中导入使用
    - 提供统一的 OpenAI API 调用接口
    - 提供物体同义词映射
"""

from __future__ import annotations

import json
import os
import re
import sys
import time
from concurrent.futures import Future, ThreadPoolExecutor, as_completed

# openai / nltk are only used by OpenAIModel and remove_negetive_sents(), which
# the TOSC pipeline never calls. Guard the imports so the package loads with just
# the core inference deps (torch / transformers / peft / ...).
try:
    import nltk
except ImportError:
    nltk = None

try:
    import openai
    from openai import OpenAI
    from openai.types.chat.chat_completion import ChatCompletion
except ImportError:
    openai = None
    OpenAI = None
    ChatCompletion = None

from tqdm import tqdm

# 添加路径以导入 LLaVA 模块
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")))
from llava.model.builder import load_pretrained_model
from llava.utils import disable_torch_init

# ==================== OpenAI API 配置 ====================
# 优先使用环境变量(同时支持官方 API 和兼容代理)
# - OPENAI_BASE_URL / OPENAI_API_BASE: API 根地址(如 https://api.openai.com 或代理地址)
# - OPENAI_API_KEY: API 密钥
DEFAULT_BASE_URL = os.getenv("OPENAI_BASE_URL") or os.getenv("OPENAI_API_BASE") or "https://api.openai.com"
DEFAULT_API_KEY = os.getenv("OPENAI_API_KEY") or ""

# ==================== API 调用配置 ====================
NUM_SECONDS_TO_SLEEP = 0.5  # 失败重试间隔(秒)
MAX_RETRIES = 3             # 最大重试次数


class OpenAIModel:
    """
    OpenAI API 封装类
    
    提供方便的接口调用 OpenAI API(或兼容 API),支持:
    - 批量并行请求(提高效率)
    - 自动重试(处理 rate limit 等错误)
    - 灵活的输入格式(支持 users/systems 或 messages)
    
    Attributes:
        client: OpenAI 客户端实例
        model: 默认使用的模型名称(如 "gpt-4")
    
    Example:
        >>> openai_model = OpenAIModel(model="gpt-4")
        >>> # 单个请求
        >>> response = openai_model.gen(users="Describe this image.")
        >>> # 批量请求(并行)
        >>> responses = openai_model.gen(
        ...     users=["Question 1", "Question 2"],
        ...     use_parallel=True
        ... )
    """
    
    def __init__(
        self,
        base_url: str | None = None,
        api_key: str | None = None,
        model: str | None = None,
        timeout_sec: int = 20,
    ):
        """
        初始化 OpenAI 客户端
        
        Args:
            base_url: API 根地址(如果为 None,从环境变量读取)
            api_key: API 密钥(如果为 None,从环境变量读取)
            model: 默认模型名称
            timeout_sec: 请求超时时间(秒)
        """
        base_url = (base_url or DEFAULT_BASE_URL).strip()
        api_key = (api_key or DEFAULT_API_KEY).strip()

        # 标准化 base_url 为 ".../v1/" 格式
        # 接受多种输入格式:
        # - https://api.openai.com
        # - https://api.openai.com/v1
        # - https://api.openai.com/v1/
        if base_url.endswith("/"):
            base_url = base_url[:-1]
        if base_url.endswith("/v1"):
            base_url = base_url[: -len("/v1")]
        base_url = f"{base_url}/v1/"

        # 验证 API Key
        if not api_key:
            raise ValueError(
                "缺少 OpenAI API key。请设置环境变量 OPENAI_API_KEY 或通过 --openai_key 参数传递。"
            )
        
        # 初始化 OpenAI 客户端
        self.client: OpenAI = OpenAI(
            base_url=base_url,
            api_key=api_key,
            timeout=timeout_sec,
            max_retries=MAX_RETRIES,
        )
        self.model: str | None = model

    def _u_a_to_messages(self, users: list[str], systems: list[str]) -> list[list[dict]]:
        assert len(users) == len(systems), "Length of users and systems must be the same."
        messages_list: list[list[dict]] = []
        for u, s in zip(users, systems):
            messages: list[dict] = []
            if s:
                messages.append({"role": "system", "content": s})
            if u:
                messages.append({"role": "user", "content": u})
            messages_list.append(messages)
        return messages_list

    def _prepare_messages_list(
        self,
        users: list[str] | str | None,
        systems: list[str] | str | None,
        messages: list[list[dict]] | list[dict] | None,
    ) -> list[list[dict]]:
        def ensure_lists(*args) -> list:
            """
            确保输入的参数都是列表形式,并且第一个参数的长度决定了后续参数的列表长度。

            Args:
                多个参数,每个参数可以是单个元素或列表。
            Returns:
                处理后的参数列表,每个参数都是列表形式。
                第一个参数的长度将决定后续参数的列表长度。
                如果某个参数是单个元素,则会被转换为包含该元素的列表。
                如果某个参数是列表,则保持不变。
            """
            if not args:
                return []
            first_arg: list = args[0] if isinstance(args[0], list) else [args[0]]
            length: int = len(first_arg)
            result = [first_arg] + [
                a
                if isinstance(a, list) and len(a) == length
                else ([a] * length if not isinstance(a, list) else a * (length // len(a)) + a[: length % len(a)])
                for a in args[1:]
            ]
            return result if len(result) > 1 else result[0]

        if users is not None:
            users, systems = ensure_lists(users, systems)
            return self._u_a_to_messages(users, systems)
        elif systems is not None:
            systems, users = ensure_lists(systems, users)
            return self._u_a_to_messages(users, systems)
        else:
            if not isinstance(messages[0], list):
                return [messages]
        return messages

    def gen(
        self,
        users: list[str] | str | None = None,
        systems: list[str] | list | None = None,
        messages: list[list[dict]] | list[dict] | None = None,
        temperature: float = 0.2,
        max_tokens: int = 512,
        model: str | None = None,
        sample=False,
        force_list: bool = False,
        return_completions: bool = False,
        use_parallel: bool = True,
        use_tqdm: bool = False,
        max_workers: int = 64,
    ) -> list[str] | str:
        """
        调用 OpenAI API 生成文本
        
        支持两种输入方式:
            1. 使用 users 和 systems 参数(推荐,简洁)
            2. 使用 messages 参数(灵活,完全控制)
        
        Args:
            users: 用户消息(单个或列表)
            systems: 系统消息(单个或列表)
            messages: 完整的消息列表(与 users/systems 二选一)
            temperature: 采样温度(0=确定性,越高越随机)
            max_tokens: 最大生成 token 数
            model: 模型名称(如果为 None 使用初始化时的 model)
            sample: 是否采样多个候选(n=5)
            force_list: 是否强制返回列表
            return_completions: 是否返回完整的 completion 对象
            use_parallel: 是否使用并行请求(多个请求时)
            use_tqdm: 是否显示进度条
            max_workers: 并行线程数
        
        Returns:
            str | list[str]: 生成的文本(单个或列表)
        
        Example:
            >>> # 单个请求
            >>> response = model.gen(users="Hello")
            >>> # 批量并行请求
            >>> responses = model.gen(
            ...     users=["Q1", "Q2", "Q3"],
            ...     use_parallel=True
            ... )
        """
        assert (users is None and systems is None) == (messages is not None), "Invalid input arguments."

        messages: list[list[dict]] = self._prepare_messages_list(users, systems, messages)

        n = 1 if not sample else 5
        outputs: list[str | list[str]] = [None] * len(messages)

        def gen_completion(messages: list[dict]) -> str | list[str]:
            completions: ChatCompletion = self._gen(
                messages=messages,
                model=model if model else self.model,
                temperature=temperature,
                max_tokens=max_tokens,
                n=n,
            )

            if return_completions:
                return completions
            if len(completions.choices) == 1:
                return completions.choices[0].message.content.strip()
            else:
                return [choice.message.content.strip() for choice in completions.choices]

        if len(messages) == 1:
            use_tqdm = False

        if use_tqdm:
            pb = tqdm(total=len(messages))

        if use_parallel and len(messages) > 1:
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                futures: dict[Future, int] = {executor.submit(gen_completion, m): i for i, m in enumerate(messages)}

                for completed_future in as_completed(futures):
                    if use_tqdm:
                        pb.update(1)
                    index: int = futures[completed_future]
                    outputs[index] = completed_future.result()
        else:
            for i, m in enumerate(messages):
                outputs[i] = gen_completion(m)
                if use_tqdm:
                    pb.update(1)

        return outputs if force_list or len(outputs) > 1 else outputs[0]

    def _gen(
        self,
        messages: list[dict],
        max_tokens: int = 512,
        model: str | None = None,
        temperature: float = 0.2,
        n: int = 1,
        seed: int | None = None,
        top_p: float = 1.0,
    ) -> ChatCompletion:
        assert model or self.model, "Model must be provided."
        last_err: Exception | None = None
        for _attempt in range(MAX_RETRIES):
            try:
                response: ChatCompletion = self.client.chat.completions.create(
                    messages=messages,
                    model=model if model else self.model,
                    temperature=temperature,
                    max_tokens=max_tokens,
                    n=n,
                    seed=seed,
                    top_p=top_p,
                )
                return response
            except openai.RateLimitError:
                last_err = None
            except Exception as e:
                last_err = e
                print(f"Error when generating: {e}")
            time.sleep(NUM_SECONDS_TO_SLEEP)
        raise RuntimeError(
            "OpenAI request failed after retries. "
            "If you're behind a proxy, set OPENAI_BASE_URL/OPENAI_API_BASE; "
            "otherwise check networking/DNS and that your key is valid."
        ) from last_err


def init_model(args):
    """
    初始化 LLaVA 模型
    
    加载预训练的 LLaVA 模型,支持基础模型和 LoRA 微调模型。
    
    Args:
        args: 参数对象,包含:
            - model_path: 模型路径(基础模型或 LoRA 模型)
            - model_base: 基础模型路径(使用 LoRA 时需要)
    
    Returns:
        tuple: (tokenizer, model, image_processor)
    
    Example:
        >>> args.model_path = "llava-hf/llava-1.5-7b-hf"
        >>> args.model_base = None
        >>> tokenizer, model, image_processor = init_model(args)
    """
    disable_torch_init()  # 禁用 torch 的默认初始化(加速)
    model_path = os.path.expanduser(args.model_path)

    # 判断是否使用 LoRA
    if not args.model_base or args.model_base == "None" or len(args.model_base) < 5:
        # 使用基础模型
        model_base = None
        model_name = "llava-v1.5-7b"
    else:
        # 使用 LoRA 微调模型
        model_base = args.model_base
        model_name = "llava-v1.5-7b-lora"
    
    print(f"正在加载模型: {model_path},基础模型: {model_base}...")
    tokenizer, model, image_processor, _ = load_pretrained_model(model_path, model_base, model_name)

    return tokenizer, model, image_processor


def read_json(file_path: str) -> list[dict] | dict:
    """
    读取 JSON 文件,支持多种格式
    
    支持的格式:
        - .json / .jsonfile: 标准 JSON 格式
        - .jsonl: JSON Lines 格式(每行一个 JSON 对象)
    
    Args:
        file_path: 文件路径
    
    Returns:
        - .json: 返回 dict 或 list
        - .jsonl: 返回 list[dict]
    
    Raises:
        ValueError: 不支持的文件扩展名
    """
    ext = os.path.splitext(file_path)[-1]
    if ext == ".json" or ext == ".jsonfile":
        with open(os.path.expanduser(file_path), "r", encoding="utf-8") as f:
            data = json.load(f)
    elif ext == ".jsonl":
        with open(os.path.expanduser(file_path), "r", encoding="utf-8") as f:
            data = [json.loads(line) for line in f]
    else:
        raise ValueError(f"不支持的文件扩展名 {ext},文件: {file_path}")
    return data


# ==================== COCO 物体同义词映射表 ====================
# 来源: https://github.com/LisaAnne/Hallucination/blob/master/data/synonyms.txt
# 用于物体幻觉检测:判断模型提到的词是否指同一物体
# 格式: 每行第一个词是代表词,后面是同义词
object_synonyms_txt = """
person, girl, boy, man, woman, kid, child, chef, baker, people, adult, rider, children, baby, worker, passenger, sister, brother, biker, policeman, cop, officer, lady, cowboy, bride, groom, male, female, guy, traveler, mother, father, gentleman, pitcher, player, skier, snowboarder, skater, skateboarder, guy, foreigner, child, gentleman, caller, offender, coworker, trespasser, patient, politician, soldier, grandchild, serviceman, walker, drinker, doctor, bicyclist, thief, buyer, teenager, student, camper, driver, solider, hunter, shopper, villager, pedestrian
bicycle, bike, unicycle, minibike, trike
car, automobile, van, minivan, sedan, suv, hatchback, cab, jeep, coupe, taxicab, limo, taxi
motorcycle, scooter, motor bike, motor cycle, motorbike, scooter, moped
airplane, jetliner, plane, air plane, monoplane, aircraft, jet, jetliner, airbus, biplane, seaplane
bus, minibus, trolley
train, locomotive, tramway, caboose
truck, pickup, lorry, hauler, firetruck
boat, ship, liner, sailboat, motorboat, dinghy, powerboat, speedboat, canoe, skiff, yacht, kayak, catamaran, pontoon, houseboat, vessel, rowboat, trawler, ferryboat, watercraft, tugboat, schooner, barge, ferry, sailboard, paddleboat, lifeboat, freighter, steamboat, riverboat, battleship, steamship
traffic light, street light, traffic signal, stop light, streetlight, stoplight
fire hydrant, hydrant
stop sign
parking meter
bench, pew
bird, ostrich, owl, seagull, goose, duck, parakeet, falcon, robin, pelican, waterfowl, heron, hummingbird, mallard, finch, pigeon, sparrow, seabird, osprey, blackbird, fowl, shorebird, woodpecker, egret, chickadee, quail, bluebird, kingfisher, buzzard, willet, gull, swan, bluejay, flamingo, cormorant, parrot, loon, gosling, waterbird, pheasant, rooster, sandpiper, crow, raven, turkey, oriole, cowbird, warbler, magpie, peacock, cockatiel, lorikeet, puffin, vulture, condor, macaw, peafowl, cockatoo, songbird
cat, kitten, feline, tabby
dog, puppy, beagle, pup, chihuahua, schnauzer, dachshund, rottweiler, canine, pitbull, collie, pug, terrier, poodle, labrador, doggie, doberman, mutt, doggy, spaniel, bulldog, sheepdog, weimaraner, corgi, cocker, greyhound, retriever, brindle, hound, whippet, husky
horse, colt, pony, racehorse, stallion, equine, mare, foal, palomino, mustang, clydesdale, bronc, bronco
sheep, lamb, ram, lamb, goat, ewe
cow, cattle, oxen, ox, calf, cattle, holstein, heifer, buffalo, bull, zebu, bison
elephant
bear, panda
zebra
giraffe
backpack, knapsack
umbrella
handbag, wallet, purse, briefcase
tie, bow, bow tie
suitcase, suit case, luggage
frisbee
skis, ski
snowboard
sports ball, ball
kite
baseball bat
baseball glove
skateboard
surfboard, longboard, skimboard, shortboard, wakeboard
tennis racket, racket
bottle
wine glass
cup
fork
knife, pocketknife, knive
spoon
bowl, container
banana
apple
sandwich, burger, sub, cheeseburger, hamburger
orange
broccoli
carrot
hot dog
pizza
donut, doughnut, bagel
cake, cheesecake, cupcake, shortcake, coffeecake, pancake
chair, seat, stool
couch, sofa, recliner, futon, loveseat, settee, chesterfield
potted plant, houseplant
bed
dining table, table, desk, coffee table
toilet, urinal, commode, toilet, lavatory, potty
tv, monitor, televison, television
laptop, computer, notebook, netbook, lenovo, macbook, laptop computer
mouse
remote, remote control
keyboard
cell phone, mobile phone, phone, cellphone, telephone, phon, smartphone, iPhone
microwave
oven, stovetop, stove, stove top oven
toaster
sink
refrigerator, fridge, fridge, freezer
book
clock
vase
scissors
teddy bear, teddybear
hair drier, hairdryer
toothbrush
"""

# ==================== Visual Genome 常见物体列表 ====================
# Visual Genome 数据集中最常见的物体
visual_genome_obj: list[str] = [
    "tree",
    "window",
    "shirt",
    "building",
    "person",
    "table",
    "car",
    "door",
    "light",
    "fence",
    "chair",
    "people",
    "plate",
    "glass",
    "jacket",
    "sidewalk",
    "snow",
    "flower",
    "hat",
    "bag",
    "track",
    "roof",
    "umbrella",
    "helmet",
    "plant",
    "train",
    "bench",
    "box",
    "food",
    "pillow",
    "bus",
    "bowl",
    "horse",
    "trunk",
    "clock",
    "mountain",
    "elephant",
    "giraffe",
    "banana",
    "house",
    "cabinet",
    "hill",
    "dog",
    "book",
    "bike",
    "coat",
    "glove",
    "zebra",
    "bird",
    "motorcycle",
    "lamp",
    "cow",
    "skateboard",
    "surfboard",
    "beach",
    "sheep",
    "kite",
    "cat",
    "pizza",
    "bed",
    "bear",
    "windshield",
    "towel",
    "desk",
]

# ==================== 关系同义词映射 ====================
# 用于判断两个空间关系是否相等
relation_sysnonyms_txt = """
in, on, at
equals, is
belongs to, is part of
"""

# ==================== COCO 双词物体列表 ====================
# 包含两个词的物体名称(如 "hot dog", "cell phone")
# 在分词时需要特殊处理,避免被拆分
coco_double_words = [
    "motor bike",
    "motor cycle",
    "air plane",
    "traffic light",
    "street light",
    "traffic signal",
    "stop light",
    "fire hydrant",
    "stop sign",
    "parking meter",
    "suit case",
    "sports ball",
    "baseball bat",
    "baseball glove",
    "tennis racket",
    "wine glass",
    "hot dog",
    "cell phone",
    "mobile phone",
    "teddy bear",
    "hair drier",
    "potted plant",
    "bow tie",
    "laptop computer",
    "stove top oven",
    "hot dog",
    "teddy bear",
    "home plate",
    "train track",
    "dining table",
    "coffee table",
]
# ==================== 特殊物体类别 ====================
# 动物类词汇
animal_words = ["bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "animal", "cub"]
# 交通工具类词汇
vehicle_words = ["jet", "train"]


# ==================== 文本处理工具函数 ====================

def remove_negetive_sents(caption: str) -> str:
    """
    移除描述中的否定句
    
    移除包含 "There is no" 或 "There are no" 的句子,
    因为这些句子不包含实际物体信息。
    
    Args:
        caption: 输入描述文本
    
    Returns:
        str: 移除否定句后的文本
    
    Example:
        >>> remove_negetive_sents("A dog is here. There is no cat.")
        "A dog is here."
    """
    sents: list[str] = nltk.sent_tokenize(caption)
    sents = [sent for sent in sents if "There is no" not in sent and "There are no" not in sent]
    return " ".join(sents)


def remove_woodpecker_boxes(text: str) -> str:
    """
    移除 Woodpecker 生成的边界框标记
    
    Woodpecker 是一个视觉幻觉纠正方法,会在输出中添加边界框标记。
    本函数移除这些标记以便评估。
    
    标记格式: ([x1, y1, x2, y2]) 或 ([...];
    
    Args:
        text: 包含边界框标记的文本
    
    Returns:
        str: 移除标记后的文本
    """
    text = re.sub(r"\(\[.*?\]\)", "", text)  # 移除 ([...])
    text = re.sub(r"\(\[.*?\]\;", "", text)  # 移除 ([...];
    text = re.sub(r"\[.*?\]\;", "", text)    # 移除 [...];
    return text