الأدلة الإرشادية
درس تعليمي: الصور، مع فحوص وحَكَم يراها
قيّم دالة تصنع صورًا، وهي هنا مُنشئ صور مصغّرة للمنتجات يكتب أيضًا تعليقًا. افحص الصور المصغّرة بقياسات لا تحتاج إلى نموذج، واحكم على التعليقات بنموذج يرى كل صورة مصغّرة، وقِس ذلك الحَكَم مقابل شخص، ثم اعتمد تغييرًا حقيقيًا. تعمل كل خطوة على هذا الجهاز دون نموذج ودون شبكة، وتستبدل به خطوة اختيارية نموذج رؤية حقيقيًا.
ما الذي ستبنيه
خدمة كتالوج تأخذ صورة منتج، وتحفظ صورة مصغّرة، وتكتب تعليقًا من سطر واحد يسمّي لون المنتج. وثلاثة أسئلة تحسم هل يجوز اعتماد نسخة:
- هل تناسب كل صورة مصغّرة الكتالوج: صيغة ويب، و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 runGate: 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 --failuresp03
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 alicecaption_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.yamlthumbnails-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 runGate: 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.yamlthumbnail_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: 300oloproof 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) فأنشأت نسخة جديدة. تحقق من تلك. |
القيود
- الحَكَم البديل يعدّ البكسلات. وهو يوضّح سير العمل، لا جودة الحكم.
- البصمة الإدراكية تقرأ التكوين والضوء، لا اللون: فهي تميّز صورة مصغّرة للصورة الأصلية من صورة فارغة أو مشهد مختلف، لا منتجًا من آخر في الاستوديو نفسه. والرسوم المسطّحة ذات المساحات الفارغة الكبيرة تحرّكها أكثر مما تحرّكها الصور الفوتوغرافية.
- عشرون وسمًا تعطي فترة اتفاق واسعة. ضع وسومًا أكثر، عشوائيًا وعلى نحو أعمى، لحَكَم تعتمد عليه.
- يعمل الصوت والفيديو بالطريقة نفسها مع إعدادات أخرى: انظر الصور والصوت والفيديو.