الوحدة 3 — Blocks: التخطيط والأحداث
gr.Interface مثاليّ لدالّة واحدة بمُدخَل ومُخرَج. لكن الأداة الحقيقيّة نادرًا ما تكون كذلك. مساعد نسخ الصوت الذي بدأناه في الوحدة السابقة يحتاج أن يُلخّص النصّ الناتج، وأن يعرض النصّ الأصليّ والملخّص جنبًا إلى جنب، وأن يسمح بإعادة التلخيص بمعلمة مختلفة دون إعادة النسخ. هذه الحاجات تتجاوز gr.Interface وتُوجب الانتقال إلى gr.Blocks.
gr.Blocks: من واجهة إلى تطبيق
gr.Blocks سياق يجمع مكوّنات وأحداث ضمن تخطيط حرّ. لا يوجد افتراض عن عدد الحقول ولا عن ترتيبها. التصميم بيدك:
import gradio as gr
with gr.Blocks(title="أداة النسخ والتلخيص") as demo:
gr.Markdown("# نسخ صوت ثمّ تلخيصه\nارفع ملفًّا، ثمّ اطلب ملخّصًا.")
with gr.Row():
audio_in = gr.Audio(sources=["upload"], type="numpy", label="الصوت")
with gr.Column():
texte_out = gr.Textbox(label="النصّ المكتوب", lines=8, rtl=True)
resume_out = gr.Textbox(label="الملخّص", lines=4, rtl=True)
with gr.Row():
bouton_nsx = gr.Button("انسخ الصوت", variant="primary")
bouton_res = gr.Button("لخّص النصّ")
demo.launch()
هذا السكربت وحده يُنشئ واجهة جاهزة الشكل، لكنّها لا تفعل شيئًا لأنّ الأزرار لم تُربَط بعد بدوالّ. الربط هو ما تفعله الأحداث.
الصفوف والأعمدة: تخطيط شبكيّ بسيط
gr.Row يرصف المكوّنات أفقيًّا، gr.Column عموديًّا، ويمكن التداخل. القاعدة العمليّة: ابدأ بصفّ رئيسيّ ثمّ أعمدة داخله. مثال أوضح:
with gr.Blocks() as demo:
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("## المُدخلات")
fichier = gr.Audio(sources=["upload"], type="numpy")
langue = gr.Dropdown(["العربيّة", "الفرنسيّة", "الإنكليزيّة"], value="العربيّة")
with gr.Column(scale=2):
gr.Markdown("## النتائج")
texte = gr.Textbox(label="النصّ", lines=10)
score = gr.Number(label="عدد الكلمات")
scale=1 مقابل scale=2 يجعل عمود النتائج ضعف عرض عمود المُدخلات. هذه التقنية تُنتج توزيعًا يشبه الأدوات الحقيقيّة، بلا CSS ولا Grid يدويّ.
أحداث النقر والتغيير
المكوّنات في Gradio تُصدر أحداثًا. الأشهر منها اثنان:
.click(fn, inputs, outputs)علىgr.Button: يُشغَّل عند النقر..change(fn, inputs, outputs)على أيّ مكوّن مدخل: يُشغَّل عند تغيّر القيمة.
نربط الأزرار في المثال السابق:
def nsx(audio):
if audio is None:
return "لا يوجد ملفّ."
sr, x = audio
# … نداء نموذج النسخ …
return "هذا نصّ مُتخيَّل يُظهر الآليّة."
def resumer(texte, longueur):
if not texte.strip():
return "لا يوجد نصّ لتلخيصه."
# … نداء نموذج التلخيص …
return texte[:longueur] # ملخّص وهميّ
with gr.Blocks() as demo:
audio_in = gr.Audio(sources=["upload"], type="numpy")
texte_out = gr.Textbox(label="النصّ", lines=8)
curseur = gr.Slider(20, 200, value=80, step=10, label="عدد الأحرف")
resume_out = gr.Textbox(label="الملخّص", lines=3)
bouton_nsx = gr.Button("انسخ")
bouton_res = gr.Button("لخّص")
bouton_nsx.click(fn=nsx, inputs=audio_in, outputs=texte_out)
bouton_res.click(fn=resumer, inputs=[texte_out, curseur], outputs=resume_out)
demo.launch()
هذا هو التسلسل الكامل: يرفع المستخدم صوتًا، ثمّ ينقر «انسخ» فيظهر النصّ، ثمّ ينقر «لخّص» فيظهر الملخّص. لكن لاحظ نقطة مهمّة: تلخيص النصّ يقرأ texte_out مباشرة. إذا حذف المستخدم النصّ أو عدّله، فالتلخيص يعمل على النصّ الحاليّ في الحقل، لا على مخرج النسخ الأصليّ. هذا سلوك مقصود يجعل الأداة تفاعليّة.
سلسلة الأحداث: .then() للتنفيذ المتتابع
في كثير من الحالات نريد أن نقوم بعمليّتين متتاليتين تل قائيًّا: بعد النسخ، لخّص فورًا. Gradio يوفّر .then() لهذا:
bouton_traiter = gr.Button("انسخ ثمّ لخّص")
(bouton_traiter
.click(fn=nsx, inputs=audio_in, outputs=texte_out)
.then(fn=resumer, inputs=[texte_out, curseur], outputs=resume_out))
.then() تُشغّل الدالّة بعد انتهاء السابقة، وتستقبل نتيجتها عبر مكوّن المُخرَج المشترك. هذا يبني تدفّقًا واضحًا بدل حدث واحد ضخم يفعل كلّ شيء.
gr.State: حفظ متغيّرات لا تُعرَض
بعض القيم يجب أن تعبر بين نداءات ولا تظهر في الواجهة: هويّة الجلسة، تاريخ الطلبات، معاملات نموذج مضبوطة. gr.State يفعل هذا:
with gr.Blocks() as demo:
historique = gr.State([])
question = gr.Textbox()
reponse = gr.Textbox()
bouton = gr.Button("اسأل")
def repondre(q, hist):
hist = hist + [q]
return f"سؤالك رقم {len(hist)}: {q}", hist
bouton.click(fn=repondre, inputs=[question, historique], outputs=[reponse, historique])
كلّ نقرة تقرأ الحالة الحاليّة، تُعدّلها، وتُرجعها. Gradio يحفظها لكلّ جلسة مستخدم على حدة، فلا تتداخل جلسات المستخدمين المختلفين. هذا هو ما يجعل مساعد المحادثة في الوحدات القادمة ممكنًا: التاريخ محفوظ في gr.State ويُمرَّر مع كلّ سؤال جديد.
تسلسل حقيقيّ: نسخ ثمّ تلخيص
نجمع كلّ ما سبق في تطبيق يُنشئ خيط عمل حقيقيّ:
import gradio as gr
def nsx(audio):
sr, x = audio
return "نصّ منسوخ من الصوت (مِثال)."
def resumer(texte, n):
return " ".join(texte.split()[:n])
with gr.Blocks(title="من الصوت إلى الملخّص") as demo:
gr.Markdown("# من الصوت إلى الملخّص\nخطوتان: انسخ ثمّ لخّص.")
with gr.Row():
with gr.Column():
audio = gr.Audio(sources=["upload"], type="numpy", label="الصوت")
n_mots = gr.Slider(5, 50, value=15, step=1, label="كلمات الملخّص")
bouton = gr.Button("انسخ ثمّ لخّص", variant="primary")
with gr.Column():
texte = gr.Textbox(label="النصّ الكامل", lines=8, rtl=True)
resume = gr.Textbox(label="الملخّص", lines=3, rtl=True)
(bouton
.click(fn=nsx, inputs=audio, outputs=texte)
.then(fn=resumer, inputs=[texte, n_mots], outputs=resume))
demo.launch()
gr.InterfaceInterface لا يزال الخيار الصحيح لدالّة واحدة بمُدخَل ومُخرَج. الانتقال إلى Blocks لا يُبرَّر إلّا بحاجة حقيقيّة: أكثر من زرّ، أو تخطيط غير قياسي، أو حالة تعبر بين النداءات. تعقيد سابق لأوانه يصعب صيانته.
الخلاصة
gr.Blocksسياق يبني واجهة حرّة التخطيط بدل نموذجInterfaceالصارم.gr.Rowوgr.Columnيبنيان شبكة بسيطة؛scaleيوزّع العرض دون CSS يدويّ..clickو.changeيربطان المكوّنات بدوالّ Python؛.then()يبني سلسلة تنفيذ متتابعة.gr.Stateيحفظ قيمًا لا تُعرَض بين النداءات لكلّ جلسة على حدة، وهو الأساس للتاريخ في المحادثة.
قاعدة عمليّة قبل الانتقال: احتفظ بـgr.Interface كلّما كان ذلك ممكنًا، وابنِ Blocks فقط حين تفرض الحاجة تخطيطًا متعدّد الأعمدة، أو أكثر من زرّ، أو تسلسل خطوات يحتاج then(). كلّ سطر تخطيط تضيفه هو سطر يجب صيانته لاحقًا؛ فالحدّ الأدنى الكافي أفضل من واجهة مزخرفة تُخفي منطق العمل.
الوحدة التالية: gr.ChatInterface لبناء واجهة حوار كاملة مع تاريخ ورسالة نظام.