الانتقال إلى المحتوى

الأدلة الإرشادية

درس تعليمي: الصور، مع فحوص وحَكَم يراها

قيّم دالة تصنع صورًا، وهي هنا مُنشئ صور مصغّرة للمنتجات يكتب أيضًا تعليقًا. افحص الصور المصغّرة بقياسات لا تحتاج إلى نموذج، واحكم على التعليقات بنموذج يرى كل صورة مصغّرة، وقِس ذلك الحَكَم مقابل شخص، ثم اعتمد تغييرًا حقيقيًا. تعمل كل خطوة على هذا الجهاز دون نموذج ودون شبكة، وتستبدل به خطوة اختيارية نموذج رؤية حقيقيًا.

ما الذي ستبنيه

خدمة كتالوج تأخذ صورة منتج، وتحفظ صورة مصغّرة، وتكتب تعليقًا من سطر واحد يسمّي لون المنتج. وثلاثة أسئلة تحسم هل يجوز اعتماد نسخة:

  • هل تناسب كل صورة مصغّرة الكتالوج: صيغة ويب، و256 في 256 بكسلًا على الأكثر، و50 kB على الأكثر؟ هذا قياس، يجريه media_check من بايتات الصورة المصغّرة.
  • هل ما زالت كل صورة مصغّرة هي صورة المنتج، لا صورة فارغة ولا قصاصة ولا منتجًا آخر؟ يقارنها image_matches_reference بالصورة الأصلية بالبصمة الإدراكية.
  • هل يسمّي كل تعليق اللون الذي يراه المتسوّق؟ هذا يحتاج إلى عينين، لذا تُعرض الصورة المصغّرة على حَكَم بمعيار.

تبقى الصور الأصلية والمصغّرة في مجلد المشروع. ويسجّلها النظام بالإشارة (media/v1): مكان كل ملف ونوعه وحجمه وبصمة sha256 له. والمصطلحات مثل الحالة والتشغيل والحَكَم والبوابة معرَّفة في المفاهيم؛ وكل ما يخص الوسائط موجود في الصور والصوت والفيديو.

المتطلبات المسبقة

  • Python 3.11 أو أحدث، وOloproof مع إضافة الوسائط، التي تضيف Pillow لمقارنة الصور:
python3 -m venv .venv
. .venv/bin/activate
pip install 'oloproof[media]'
  • مشروع المثال، وهو يأتي مع الحزمة. انسخه إلى مجلد جديد واعمل هناك:
oloproof init --example media product-thumbnails
cd product-thumbnails
  • المنفذ 8798 شاغر للحَكَم البديل (غيّره في الموضعين إن لم يكن كذلك).

كل خطوة حتى "اختياري: نموذج رؤية حقيقي" تعمل دون اتصال وحتمية: لا مفتاح API، ولا حساب لدى مزوِّد، ولا تكلفة.

الملفات

product-thumbnails/
  app.py                              the thumbnailer under test (baseline)
  app_v2.py                           the candidate change
  judge_server.py                     a stand-in vision judge speaking the OpenAI API on 127.0.0.1
  rubrics/caption_names_colour.md     the judge's rubric
  oloproof.yaml                       the suite
  release.yaml                        rules for a run
  compare.yaml                        a rule for a comparison
  data/products.jsonl                 20 cases
  photos/                             the 20 product photos the cases name
  make_photos.py                      how the photos were drawn
  labels/reviewer_verdicts.csv        one person's verdicts on the baseline's captions
  fill_labels.py                      copies those verdicts into a labelling sheet

شغّل كل أمر من product-thumbnails/.

التطبيق يسجّل صوره

# app.py (excerpt)
@system(name="thumbnailer", version=VERSION, records=("media/v1",))
def describe(case: dict[str, Any]) -> dict[str, str]:
    photo_path = HERE / case["photo"]
    photo = Image.open(photo_path).convert("RGB")
    thumb = photo.copy()
    thumb.thumbnail((320, 320))
    out = HERE / "thumbs" / VERSION / photo_path.name
    out.parent.mkdir(parents=True, exist_ok=True)
    thumb.save(out)
    given = media_file(photo_path, role="input")
    made = media_file(out, width=thumb.width, height=thumb.height)
    current_case().media([given, made])
    centre = photo.getpixel((photo.width // 2, photo.height // 2))
    return {"caption": f"A {colour_name(centre)} product."}

يصف media_file(path) ملفًا: موقعه file://، ونوع MIME، والحجم، وبصمة sha256، مسمًّى باسم الملف. ويعلّم role="input" الصورة التي أُعطيها النظام؛ أما الصورة المصغّرة فمُخرَج، وهو الافتراضي. ويسجّل current_case().media(...) كليهما كمُخرَج فني واحد من نوع media/v1، ويعلن records: [media/v1] في oloproof.yaml أن النظام يسجّله. ولا يُنسخ شيء: فالفحص أو الحَكَم يقرأ كل ملف لاحقًا من جديد ويرفض الملف الذي لم تعد بايتاته تطابق السجل.

لخط الأساس هذا عيبان. صوره المصغّرة بعرض 320 بكسلًا، وتعليقه يسمّي لون البكسل الذي في وسط الصورة، وهو الخلفية كلما كان المنتج في أحد الجانبين. وتكتب كل نسخة صورها المصغّرة تحت thumbs/<version>/، فلا يكتب تشغيل لاحق أبدًا فوق الملفات التي سجّلها تشغيل سابق.

مجموعة البيانات

{"id": "p03", "input": {"photo": "photos/p03.png"}, "expected": {"photo": "photos/p03.png", "colour": "yellow", "shape": "triangle"}}

input هو ما يتلقاه النظام. وexpected.photo هو المرجع الذي تُقارن به الصورة المصغّرة؛ أما colour وshape فهما هناك لشخص يقرأ الحالة، ولا يقرؤهما أي مُقيِّم.

اختيار المُقيِّمات

evaluators:
  # The catalogue's limits, measured from the thumbnail's bytes: a web format, at most 256 by
  # 256 pixels, at most 50 kB.
  - type: media_check
    criterion: thumbnail_fits
    kind: image
    formats: [png, jpeg, webp]
    max_width: 256
    max_height: 256
    max_bytes: 50000
  # The thumbnail is still the product's photo: a perceptual hash within 10 of 64 bits.
  - type: image_matches_reference
    criterion: same_photo
    reference_field: photo
    max_distance: 10
  # The caption, judged by a model that sees the thumbnail.
  - type: rubric_judge
    criterion: caption_names_colour
    provider: openai_compatible
    model: stand-in-vision
    base_url: http://127.0.0.1:8798/v1
    rubric_file: rubrics/caption_names_colour.md
    media: output

يقيس media_check كل صورة مُخرَجة: الصيغة من بايتاتها الأولى، والحجم من ترويستها، لا مما أعلنه النظام أبدًا. ويحسب image_matches_reference بصمة الصورة المصغّرة وبصمة الصورة التي يسمّيها expected.photo، وينجح حين لا تختلف البصمتان في أكثر من 10 من بتاتهما الـ 64؛ فإعادة التحجيم أو إعادة الضغط تحرّك البصمة بضعة بتات، والصورة الأخرى تحرّكها كثيرًا. ولا يستدعي أيٌّ منهما نموذجًا، لذا يحسم كلاهما من التشغيل الأول.

يُرفق media: output صور الحالة المُخرَجة بطلب الحَكَم، كلٌّ منها مسمًّى في موجِّهه. والحَكَم لا يحسم شيئًا حتى تُظهر وسوم البشر أنه يتفق معهم، لذا تنتظر القاعدة الثالثة ذلك.

الحَكَم البديل

يرسل حَكَم المعيار موجِّهه والصور المرفقة إلى نموذج ويقرأ في المقابل {"pass": true|false, "rationale": "..."}. والنموذج هنا هو judge_server.py، الذي يتحدث بالواجهة نفسها على 127.0.0.1 وليس نموذجًا: فهو يفكّ ترميز الصورة المصغّرة المرفقة، ويجد أكثر ألوانها شيوعًا غير أبيض الخلفية ورماديّها، ويُنجح التعليق الذي يسمّيه. شغّله في طرفية ثانية واتركه يعمل:

python judge_server.py --port 8798

السياسة

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
rules:
  - id: thumbnails-fit
    metric: thumbnail_fits
    kind: observed_count
    max_failures: 0
  - id: same-photo
    metric: same_photo
    kind: observed_count
    max_failures: 0
  - id: caption-floor
    metric: caption_names_colour
    min: 0.80

يجب أن تناسب كل صورة مصغّرة وأن تكون صورتها الأصلية. ويجب أن يسمّي 80% على الأقل من التعليقات اللون الصحيح، ولا يُحسم ذلك إلا حين يتجاوز الحد الأدنى للفترة 80%.

شغّله

oloproof run
Gate: BLOCK (exit 1)
│ thumbnails-fit │ thumbnail_fits       │ FAIL                  │ observed_failures_exceed_limit   │
│ same-photo     │ same_photo           │ PASS                  │ observed_failures_within_limit   │
│ caption-floor  │ caption_names_colour │ INSUFFICIENT_EVIDENCE │ evaluator_not_validated          │
│ thumbnail_fits       │ 0.0%     │ [0.0%, 16.9%]   │ 0 / 20 observed · 0 missing · 0 excluded  │
│ same_photo           │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ caption_names_colour │ 60.0%    │ [36.0%, 80.9%]  │ 12 / 20 observed · 0 missing · 0 excluded │

لا تناسب أي صورة مصغّرة، وكل صورة مصغّرة هي صورتها الأصلية، و12 من 20 تعليقًا تسمّي اللون الصحيح. وقاعدة الحَكَم غير محسومة لأن أحدًا لم يقِس الحَكَم بعد.

افحص الإخفاقات

oloproof inspect RUN_ID --failures
p03
  output: {"caption": "A white product."}
  thumbnail_fits: failed
    measured: p03.png: width 320 px is above 256 px
  caption_names_colour: failed
    judge text, not verified: the product is yellow; the caption says 'a white product.'

سبب الفحص قياس، معلَّم بـ measured؛ وسبب الحَكَم كلماته هو، التي لم يتحقق منها أحد. وتظهر الصور الأصلية والمصغّرة أيضًا بجانب كل حُكم في صفحة الحالة في منصة العمل، بعد oloproof export RUN_ID، مقروءةً من هذا المجلد؛ أما مساحة العمل المستضافة فتجعل متصفحك يجلبها من oloproof collect ولا تحتفظ بها أبدًا (الصور والصوت والفيديو).

قِس الحَكَم مقابل شخص

اسحب عيّنة عشوائية من حالات التشغيل إلى ورقة، دون أحكام الحَكَم، واجعل شخصًا ينظر إلى كل صورة مصغّرة وتعليقها، واستورد ما قاله. وهنا ينوب fill_labels.py عن ذلك الشخص، فينسخ أحكامه من labels/reviewer_verdicts.csv:

oloproof labels export RUN_ID --criterion caption_names_colour --sample 20 --local --out sample.csv
python fill_labels.py sample.csv
oloproof labels import sample.csv

ثم قِس الحَكَم مقابل تلك الوسوم، وسجّل من فعل ذلك:

oloproof evaluators list
oloproof evaluators validate EVALUATOR_ID --by alice
caption_names_colour: sha256:53605dd6... is now VALIDATED
  agreement 100.0% [83.1%, 100.0%] · 20 of 20 labelled cases agreed · 0 labelled but not judged · kappa 1.000

يُقاس الحَكَم الذي يرى الصور تمامًا كالحَكَم الذي يقرأ النص. وفي مساحة عمل مستضافة يعرض طابور المراجعة صور كل حالة على المراجع، ويجلبها متصفحه من مُجمِّعك (الحُكّام).

oloproof gate RUN_ID --policy release.yaml
thumbnails-fit: FAIL (observed_failures_exceed_limit)
same-photo: PASS (observed_failures_within_limit)
caption-floor: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
Gate: BLOCK (exit 1)

يجوز للحَكَم أن يحسم الآن، و60% لا تكفي.

أجرِ تغييرًا حقيقيًا

يصنع app_v2.py صورًا مصغّرة بقياس 256 بكسلًا ويسمّي أكثر ألوان الصورة شيوعًا غير لون الخلفية، أينما كان المنتج. انسخه فوق app.py، واضبط version: main-colour تحت system في oloproof.yaml، وأبقِ الحَكَم يعمل، ثم:

oloproof run
Gate: ALLOW (exit 0)
│ thumbnails-fit │ thumbnail_fits       │ PASS  │ observed_failures_within_limit │
│ same-photo     │ same_photo           │ PASS  │ observed_failures_within_limit │
│ caption-floor  │ caption_names_colour │ PASS  │ lower_bound_meets_minimum      │

الحَكَم هو نسخة المُقيِّم نفسها التي تُحقِّق منها، لذا تُعتمد أحكامه.

قارن المرشَّح بخط الأساس

oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
thumbnail_fits: +100.0 points [+52.9, +100.0] · 20 paired · 0 missing · 0 excluded
caption_names_colour: +40.0 points [+1.8, +72.7] · 20 paired · 0 missing · 0 excluded
  captions-better  caption_names_colour  superiority  PASS  difference_above_zero

اختياري: نموذج رؤية حقيقي

تغادر هذه الخطوة المسار غير المتصل. فهي تحتاج إلى نموذج يقرأ الصور: محليًا، Ollama مع نموذج رؤية (ollama pull qwen2.5vl)، دون مفتاح ودون تكلفة؛ وسحابيًا، provider: anthropic أو openai مع api_key_env، يُحاسَب لكل صورة كما يُحاسَب لكل كلمة.

# live_judge.yaml
type: rubric_judge
criterion: caption_names_colour
provider: openai_compatible
model: qwen2.5vl
base_url: http://localhost:11434/v1
rubric_file: rubrics/caption_names_colour.md
media: output
timeout_s: 300
oloproof evaluators try live_judge.yaml

يحكم ذلك بالنموذج الحقيقي على الحالات التي وضع عليها مراجعك وسومًا ويُبلغ عن اتفاقه، دون اعتماده. ولاعتماده، ضعه في oloproof.yaml مكان البديل: فالنموذج الجديد نسخة مُقيِّم جديدة، لذا تحقق منه مقابل الوسوم كما سبق.

استكشاف الأخطاء وإصلاحها

العَرَضالسبب والإصلاح
caption_names_colour كلها مفقودةخادم الحَكَم لا يعمل أو ليس على base_url؛ ويُظهر oloproof inspect RUN_ID --failures السبب.
image_matches_reference يوقف التشغيل ويسمّي Pillowثبّت إضافة الوسائط: pip install 'oloproof[media]'.
حالة مفقودة مع media_unavailableنُقل ملف مسجَّل أو حُذف بعد التشغيل. أبقِ الملفات التي سجّلها التشغيل ما دمت تحتفظ بالتشغيل.
يتوقف تشغيل مع is not the recorded contentتغيّر ملف مسجَّل بعد التشغيل. لا يقرأ فحص أو حَكَم أبدًا بايتات مختلفة على أنها البايتات المسجَّلة؛ اكتب ملفات كل نسخة في مكانها الخاص.
evaluator_not_validated بعد أن تحققتغيّرت الحَكَم (النموذج، نقطة النهاية، المنفذ، المعيار، media) فأنشأت نسخة جديدة. تحقق من تلك.

القيود

  • الحَكَم البديل يعدّ البكسلات. وهو يوضّح سير العمل، لا جودة الحكم.
  • البصمة الإدراكية تقرأ التكوين والضوء، لا اللون: فهي تميّز صورة مصغّرة للصورة الأصلية من صورة فارغة أو مشهد مختلف، لا منتجًا من آخر في الاستوديو نفسه. والرسوم المسطّحة ذات المساحات الفارغة الكبيرة تحرّكها أكثر مما تحرّكها الصور الفوتوغرافية.
  • عشرون وسمًا تعطي فترة اتفاق واسعة. ضع وسومًا أكثر، عشوائيًا وعلى نحو أعمى، لحَكَم تعتمد عليه.
  • يعمل الصوت والفيديو بالطريقة نفسها مع إعدادات أخرى: انظر الصور والصوت والفيديو.