الوحدة 2 — مكوّنات النصّ والصورة والصوت والفيديو
أظهرت الوحدة السابقة أنّ Gradio يترجم أنواع Python إلى مكوّنات واجهة. لكنّ التصنيف وحده لا يكفي: كلّ مكوّن يستقبل صيغة محدّدة، ويُرجع صيغة محدّدة، وخطأ في هذا يُنتج استثناءات صامتة تظهر بعد أوّل نقرة من المستخدم. هذه الوحدة تفكّك ما تحصل عليه دالّتك عندما يرفع المستخدم صورة أو ملفّ صوت، وما يجب عليها إرجاعه ليظهر شيء صحيح على الشاشة.
مكوّن gr.Textbox: أبسط ما يمكن، وأخطر ما يمكن
النصّ يبدو حالة تافهة، ثمّ نكتشف أنّه المصدر الأوّل للأخطاء في العروض الحقيقيّة. المستخدم يُدخل نصًّا فارغًا، أو نصًّا يحوي رموزًا خاصّ ة، أو نصًّا بعشرات آلاف الأحرف. المكوّن يقبل خصائص للحدّ من ذلك:
gr.Textbox(
label="سؤالك",
placeholder="اكتب سؤالًا قصيرًا…",
lines=3,
max_lines=10,
max_length=1000,
rtl=True,
)
rtl=True مفيد جدًّا للواجهة العربيّة، فهو يجعل النصّ يتدفّق من اليمين إلى اليسار. أمّا max_length فيمنع المستخدم من إرسال نصّ طويل جدًّا يُطيل زمن الاستجابة أو يستنزف نافذة السياق. ما يُهمّ هو أنّ Gradio لا يتحقّق تلقائيًّا من محتوى النصّ: يجب أن تعالج الدالّة الفراغ والحدود المنطقيّة صراحةً، لأنّ العرض يُشغَّل غالبًا بمستخدمين لا يعرفون قاعدة استعمال النموذج.
مكوّن gr.Image وصيغه الثلاث
gr.Image هو أكثر مكوّن يُخطئ فيه المبتدئون، لأنّ type تُغيّر شكل ما تستقبله الدالّة تمامًا:
type | ما تستقبله الدالّة | متى تختاره |
|---|---|---|
"numpy" | np.ndarray بشكل (H, W, 3) بقيم uint8 من 0 إلى 255 | معالجة يدويّة بـOpenCV أو NumPy |
"pil" | كائن PIL.Image.Image بنمط RGB | نماذج PyTorch/torchvision التي تقبل PIL |
"filepath" | سلسلة مسار مؤقّت على القرص | مكتبات تفتح الملفّ بنفسها (ffmpeg، exiftool…) |
الصيغة الافتراضيّة (type="numpy") هي أكثر ما يُتوقَّع، لكنّها ليست دائمًا الأنسب. مثال بسيط:
def negatif(image):
return 255 - image # image = np.ndarray (H, W, 3)
gr.Interface(
fn=negatif,
inputs=gr.Image(type="numpy", label="ارفع صورة"),
outputs=gr.Image(label="النتيجة"),
).launch()
أمّا للإرجاع، يقبل gr.Image مصفوفة NumPy أو كائن PIL أو مسار ملفّ بلا تمييز. للفيديو والصور المتحرّكة تُستعمل gr.Video وgr.Gallery.
مكوّن gr.Audio وتردّد أخذ العيّنات
الصوت أكثر تعقيدًا لأنّه يحمل معلومتين: العيّنات وتردّد أخذ العيّنات. صيغة الاستقبال الافتراضيّة زوج:
def duree(audio):
sr, x = audio # sr = تردّد أخذ العيّنات (Hz), x = np.ndarray بأشكال (N,) أو (N, 2)
return f"المدّة: {len(x) / sr:.2f} ثانية عند {sr} هرتز."
gr.Interface(
fn=duree,
inputs=gr.Audio(sources=["microphone", "upload"], type="numpy"),
outputs=gr.Textbox(label="النتيجة"),
).launch()
نسيان أنّ x قد يكون قناتَين (stereo) مصدرٌ شائع للأخطاء عند تمرير الصوت إلى نموذج نسخ يقبل قناة واحدة فقط. الحلّ الآمن هو تحويلها إلى قناة واحدة بمعدّل:
if x.ndim == 2:
x = x.mean(axis=1) # stereo -> mono
كذلك يجب إعادة أخذ العيّنات إل ى تردّد النموذج (16000 هرتز شائع لنماذج النسخ) بمكتبة مثل librosa.resample قبل أيّ استدلال.
مكوّن gr.Label: عرض الاحتمالات بشكل صحيح
عند إرجاع تصنيف، تُرجع الدالّة قاموسًا {etiquette: probability} ويتكفّل gr.Label بعرضه في قائمة مرتّبة مع أشرطة تقدّم:
def classer(image):
return {"قطّة": 0.72, "كلب": 0.18, "أرنب": 0.05, "طائر": 0.03, "أخرى": 0.02}
gr.Interface(
fn=classer,
inputs=gr.Image(type="pil"),
outputs=gr.Label(num_top_classes=3),
).launch()
num_top_classes=3 يعرض أعلى ثلاثة فقط، وهو ما يجب أن يفعله كلّ عرض تصنيف: قائمة طويلة من الاحتمالات الصغيرة تُشتّت المستخدم وتُخفي ما يهمّ.
مثال نسخ صوتيّ متكامل
نبني الآن عرضًا يستقبل صوتًا وينسخه إلى نصّ باستعمال نموذج مفتوح صغير:
import gradio as gr, numpy as np, librosa
from transformers import pipeline
nsx = pipeline("automatic-speech-recognition", model="openai/whisper-small")
def transcrire(audio):
if audio is None:
return "لم يُرفَع أيّ ملفّ صوت."
sr, x = audio
if x.ndim == 2:
x = x.mean(axis=1)
x = librosa.resample(x.astype(np.float32) / 32768.0, orig_sr=sr, target_sr=16000)
return nsx({"array": x, "sampling_rate": 16000})["text"]
gr.Interface(
fn=transcrire,
inputs=gr.Audio(sources=["microphone", "upload"], type="numpy", label="سجّل أو ارفع"),
outputs=gr.Textbox(label="النصّ المكتوب", rtl=True),
title="نسخ صوتيّ",
description="سجّل بضع ثوانٍ من الميكروفون أو ارفع ملفًّا واحصل على النصّ.",
).launch()
المهمّ هنا هو معالجة الحالات الحديّة صراحةً: audio is None عندما لا يرفع المستخدم شيئًا، ndim == 2 للصوت الاستيريو، القسمة على 32768.0 لتحويل int16 إلى float32. تخطّي أيّ منها يُنتج استثناء يُخيف المستخدم ويُوقف الجلسة.
Gradio لا يفرض حدًّا افتراضيًّا صارمًا على حجم الملفّ. مستخدم يرفع ساعة كاملة من الصوت يُشعل معالجة تستغرق دقائق ويحبس قائمة الانتظار. الوحدة 7 ستُقدّم concurrency_limit وmax_batch_size، لكن الحلّ الأبسط هنا هو الاعتراض المبكّر: if len(x) > sr * 60: return "الرجاء تحديد المدّة بدقيقة.".
الخلاصة
gr.Textboxأبسط مكوّن لكنّه أكثر مصادر الأخطاء؛ استعملmax_lengthوrtl=Trueوتحقّق من الفراغ صراحةً.gr.Imageيتغيّر شكل ما تستقبله الدالّة تمامًا بحسبtype: مصفوفة NumPy، أو PIL، أو مسار ملفّ.gr.Audioيُرجع زوج(sample_rate, np.ndarray)؛ عالج قناة الاستيريو وإعادة أخذ العيّنات قبل تمريره إلى النموذج.gr.Labelيعرض الاحتمالات بأشرطة تقدّم؛ حدّnum_top_classesكي لا يُشتّت المستخدم.
الوحدة التالية: gr.Blocks لبناء واجهات متعدّدة الخطوات، حيث نربط نسخ الصوت بتلخيصه في تدفّق واحد.