fix(label-studio): 图片 URL 按实际的 DOCUMENT_ROOT 生成(--document-root)
原先任务里的图片 URL 固定是 /data/local-files/?d=images/<图名>,隐含假设输出目录就是 LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT。服务器上 DOCUMENT_ROOT=/home/deep/labeling, Label Studio 会去找 /home/deep/labeling/images/<图名>,11211 张图全部 404。 - 新增 --document-root,缺省读 LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT;URL 写成相对它的 路径,Local files 存储路径随之算好并打印。 - 输出目录在 DOCUMENT_ROOT 之外时,在 DOCUMENT_ROOT 里建同名软链接指过去,产出仍留在 runs/,不用手动复制;DOCUMENT_ROOT 里已有同名真目录或指向别处的软链接时报错,不覆盖。 - 更正此前"软链接解析后会落到 DOCUMENT_ROOT 外面"的说法:核对 Label Studio 1.23.0 源码,safe_join 用 abspath、存储路径用 normpath 做字符串前缀比较,都不解析软链接。 - 测试照搬 localfiles_data() 的判定逐个任务验证能否被服务,并加入旧 URL 在 /home/deep/labeling 下全部 404 的回归用例。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
891f930de2
commit
7dd0f89d18
@@ -2,6 +2,7 @@
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -19,6 +20,12 @@ import prepare_label_studio as pls # noqa: E402
|
||||
CLASSES = ["grease", "normal", "whitedot"]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_document_root_env(monkeypatch):
|
||||
"""开发机上若设了 LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT,会改变缺省行为。"""
|
||||
monkeypatch.delenv("LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT", raising=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def source(tmp_path):
|
||||
"""模拟 generate_boxes.py 的产出:images/<类别>/ + labels/<类别>/ + data.yaml。"""
|
||||
@@ -451,3 +458,120 @@ def test_prints_import_steps_with_real_paths(label_studio_out, capsys):
|
||||
out = capsys.readouterr().out
|
||||
assert f"LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT={label_studio_out.resolve()}" in out
|
||||
assert "不要点 Sync" in out
|
||||
|
||||
|
||||
# ---------------- DOCUMENT_ROOT 与图片 URL ----------------
|
||||
|
||||
|
||||
def _served_by_label_studio(task, document_root, storage_path):
|
||||
"""照搬 Label Studio 1.23.0 localfiles_data() 的判定:能返回 200 才算 True。
|
||||
|
||||
d 拼到 DOCUMENT_ROOT 后(safe_join,不解析软链接),文件所在目录要以存储路径开头,且文件存在。
|
||||
"""
|
||||
django_os = pytest.importorskip("django.utils._os")
|
||||
import posixpath
|
||||
import urllib.parse as up
|
||||
|
||||
query = up.urlparse(task["data"]["image"]).query
|
||||
d = posixpath.normpath(up.parse_qs(query)["d"][0]).lstrip("/")
|
||||
full_path = django_os.safe_join(str(document_root), d)
|
||||
full_dir = os.path.normpath(os.path.dirname(full_path))
|
||||
return full_dir.startswith(os.path.normpath(str(storage_path))) and os.path.exists(full_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ls_env():
|
||||
pytest.importorskip("label_studio_sdk.converter.imports.yolo")
|
||||
|
||||
|
||||
def test_without_document_root_urls_are_relative_to_output(source, tmp_path, ls_env):
|
||||
out = tmp_path / "runs" / "boxes_label_studio"
|
||||
assert run(source, out) == 0
|
||||
summary = json.loads((out / "summary.json").read_text(encoding="utf-8"))["label_studio"]
|
||||
assert summary["image_root_url"] == "/data/local-files/?d=images"
|
||||
assert all(_served_by_label_studio(t, out, summary["storage_path"]) for t in _tasks(out))
|
||||
|
||||
|
||||
def test_output_outside_document_root_gets_symlinked(source, tmp_path, ls_env):
|
||||
"""用户实际的配置:DOCUMENT_ROOT=/home/deep/labeling,产出留在 runs/ 下。"""
|
||||
root = tmp_path / "labeling"
|
||||
root.mkdir()
|
||||
out = tmp_path / "runs" / "boxes_label_studio"
|
||||
assert run(source, out, "--document-root", str(root)) == 0
|
||||
|
||||
link = root / "boxes_label_studio"
|
||||
assert link.is_symlink() and link.resolve() == out.resolve()
|
||||
summary = json.loads((out / "summary.json").read_text(encoding="utf-8"))["label_studio"]
|
||||
assert summary["image_root_url"] == "/data/local-files/?d=boxes_label_studio/images"
|
||||
assert summary["storage_path"] == str(link / "images")
|
||||
tasks = _tasks(out)
|
||||
assert len(tasks) == 13
|
||||
assert all(_served_by_label_studio(t, root, summary["storage_path"]) for t in tasks)
|
||||
|
||||
|
||||
def test_old_urls_would_404_under_a_different_root(source, tmp_path, ls_env):
|
||||
"""回归:没按 DOCUMENT_ROOT 生成的 ?d=images/... 在 /home/deep/labeling 下全部找不到。"""
|
||||
root = tmp_path / "labeling"
|
||||
root.mkdir()
|
||||
out = tmp_path / "runs" / "boxes_label_studio"
|
||||
assert run(source, out) == 0 # 不告诉它 DOCUMENT_ROOT
|
||||
(root / "boxes_label_studio").symlink_to(out) # 即使手动把目录放进去
|
||||
storage = root / "boxes_label_studio" / "images"
|
||||
assert not any(_served_by_label_studio(t, root, storage) for t in _tasks(out))
|
||||
|
||||
|
||||
def test_output_inside_document_root_needs_no_symlink(source, tmp_path, ls_env):
|
||||
root = tmp_path / "labeling"
|
||||
out = root / "projects" / "boxes"
|
||||
root.mkdir()
|
||||
assert run(source, out, "--document-root", str(root)) == 0
|
||||
assert not [p for p in root.iterdir() if p.is_symlink()]
|
||||
summary = json.loads((out / "summary.json").read_text(encoding="utf-8"))["label_studio"]
|
||||
assert summary["image_root_url"] == "/data/local-files/?d=projects/boxes/images"
|
||||
assert all(_served_by_label_studio(t, root, summary["storage_path"]) for t in _tasks(out))
|
||||
|
||||
|
||||
def test_document_root_defaults_to_environment(source, tmp_path, monkeypatch, ls_env):
|
||||
root = tmp_path / "labeling"
|
||||
root.mkdir()
|
||||
monkeypatch.setenv("LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT", str(root))
|
||||
out = tmp_path / "runs" / "boxes_label_studio"
|
||||
assert run(source, out) == 0
|
||||
assert (root / "boxes_label_studio").is_symlink()
|
||||
|
||||
|
||||
def test_rerun_reuses_existing_symlink(source, tmp_path, ls_env):
|
||||
root = tmp_path / "labeling"
|
||||
root.mkdir()
|
||||
out = tmp_path / "runs" / "boxes_label_studio"
|
||||
assert run(source, out, "--document-root", str(root)) == 0
|
||||
assert run(source, out, "--document-root", str(root), "--overwrite") == 0
|
||||
assert (root / "boxes_label_studio").resolve() == out.resolve()
|
||||
|
||||
|
||||
def test_refuses_to_replace_a_real_directory_in_document_root(source, tmp_path, capsys, ls_env):
|
||||
"""DOCUMENT_ROOT 里已有同名的真目录(例如手动复制的一份),不能覆盖。"""
|
||||
root = tmp_path / "labeling"
|
||||
(root / "boxes_label_studio").mkdir(parents=True)
|
||||
(root / "boxes_label_studio" / "keep.txt").write_text("x", encoding="utf-8")
|
||||
out = tmp_path / "runs" / "boxes_label_studio"
|
||||
assert run(source, out, "--document-root", str(root)) != 0
|
||||
assert "不是软链接" in capsys.readouterr().out
|
||||
assert (root / "boxes_label_studio" / "keep.txt").exists()
|
||||
|
||||
|
||||
def test_refuses_symlink_pointing_elsewhere(source, tmp_path, capsys, ls_env):
|
||||
root = tmp_path / "labeling"
|
||||
root.mkdir()
|
||||
elsewhere = tmp_path / "elsewhere"
|
||||
elsewhere.mkdir()
|
||||
(root / "boxes_label_studio").symlink_to(elsewhere)
|
||||
out = tmp_path / "runs" / "boxes_label_studio"
|
||||
assert run(source, out, "--document-root", str(root)) != 0
|
||||
assert "指向别处" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_missing_document_root_is_an_error(source, tmp_path, capsys, ls_env):
|
||||
out = tmp_path / "runs" / "boxes_label_studio"
|
||||
assert run(source, out, "--document-root", str(tmp_path / "nope")) != 0
|
||||
assert "不存在" in capsys.readouterr().out
|
||||
|
||||
Reference in New Issue
Block a user