PyDA Course
متوسط نصف موجّه ⏱ 75 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

محوّل وملخّص الاجتماعات

clitext-processingcsvregex

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

يفترض هذا إنهاء Python 101 ، السلاسل وإدخال/إخراج الملفات والحلقات والدوال. لا شيء أبعد من ذلك: لا تعلم آلة، ولا معالجة صوت، ولا واجهات API خارجية لخط الأنابيب الأساسي (تحويل الكلام الحقيقي إلى نص يحتاج مفتاحًا، وهناك خطوة اختيارية له). هذا اختياري وغير مُقيَّم؛ راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة.

🎯 ما ستفعله

  1. تعريف صيغة ملف نص (ختم زمني، متحدث، دور) وتحليل كل سطر إلى دور منظم.
  2. تجميع الأدوار حسب المتحدث وحساب نصيب كل شخص من زمن الكلام.
  3. إيجاد القرارات ، الجمل التي تختم بأفعال مثل «اتفقنا» و«قررنا» و«أكّدنا».
  4. استخراج بنود التنفيذ ، «سيفعل X كذا» ، مع اسم صاحبها مقترنًا بكل مهمة.
  5. تجميع كل شيء في ملف ملخص واحد قابل للقراءة وتوجيه الأداة إلى محاضر اجتماع حقيقية.

أين تُشغّل هذا

محليًا باستخدام uv هو المسار الأساسي هنا ، خط الأنابيب معالجة نص نقية فوق ملف تتحكم فيه، بحيث تصبح حلقة «ألقِ نصًا داخلًا، واخرج summary.txt» عادة طرفية، والـ CSV الذي تولده ملف يمكنك فتحه في أي جدول بيانات.

GitHub Codespaces يعمل بالطريقة نفسها: افتح codespaces.new/abderrahim-lectures/python-data-analysis-course وتعمل الأوامر الدقيقة أدناه في نافذة متصفح مع Node وPython وuv مثبتة مسبقًا.

Google Colab وKaggle Notebooks وBinder ملائمة فعلًا لكل خطوة أدناه ، لا أسرار ولا GPU، وخط الأنابيب كله بضع خلايا تعمل فوق نص الاجتماع العينة المرافق للدورة (وهو اجتماع واقعي مكتوب باليد). التحفظ الصادق: يستخدم الدفتر نص المثال الثابت ذلك بدلًا من صوت تسجله. تحويل الكلام الحقيقي إلى نص سيحتاج مفتاح API مجانيًا، وتُغطّى تلك الخلية بخطوة اختيارية ، أما الملخص نفسه، فيشغّله دفتر فعليًا.

Open In Colab Open In Kaggle Binder

الإعداد

كل ما تحتاجه قبل تحويل كلمة واحدة: uv، ونص اجتماع عينة واقعي للمضغ.

ثبّت uv وهيئ المشروع

macOS / Linux (الطرفية):

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

أغلق طرفيتك وأعد فتحها، ثم:

uv --version
mkdir meeting-transcriber && cd meeting-transcriber
uv init --bare

صفر حزم إضافية ، هذا المشروع مكتبة قياسية نقية.

اكتب نص اجتماع عينة واقعي

الصق هذا في transcript.txt (كل سطر: [MM:SS] Speaker: words ، الصيغة التي تُصدّرها معظم أدوات النقل، وسهلة القراءة باليد):

[00:00] Priya: Let's review where we stand on the launch.
[00:08] Tom: Design shipped the landing page yesterday.
[00:15] Priya: Great. We agreed the beta opens next Monday.
[00:22] Tom: I'll block out Thursday to prep the demo video.
[00:30] Zara: I will draft the onboarding email today.
[00:38] Priya: Please send it to me for a quick pass.
[00:44] Tom: We decided the pricing page stays as-is.
[00:52] Zara: So action items: Tom owns the video, I own the email.
[01:00] Priya: And I'll publish the changelog on Friday. Meeting's at 30 minutes? No sooner.
[01:06] Zara: Wait, that's not a decision.

شغّل:

wc -l transcript.txt

✅ قائمة التحقق

  • uv --version يطبع رقم إصدار.
  • ✅ يوجد transcript.txt بـ 11 سطرًا، كلٌّ يبدأ بختم زمني [MM:SS].
  • ✅ يمكنك رصد أفعال القرار (agreed وdecided) وأفعال الملكية (will وowns وpublish) مسبقًا ، تلك هي الكلمات التي سيتعلم المستخرج التقاطها.

الخطوة 1: حلّل النص إلى أدوار

النص قائمة مسطحة من الأسطر؛ والملخص يحتاج قائمة منظمة من الأدوار ، كلٌّ بختم زمني ومتحدث وكلمات. التحليل على بعد split() صادق واحد: الختم الزمني والمتحدث بادئتان شبه ثابتتي العرض، والرسالة كل ما بعد النقطتين الثالثة.

1.1 اكتب محلل الدور

# parse.py
from pathlib import Path

def parse_line(line: str) -> dict:
    line = line.strip()
    time_s = line.split("]")[0].lstrip("[")
    rest = line.split("]", 1)[1].strip()
    speaker, _, message = rest.partition(":")
    return {"time": time_s, "speaker": speaker.strip(), "text": message.strip()}

def load_transcript(path: str) -> list[dict]:
    turns = []
    for line in Path(path).read_text(encoding="utf-8").splitlines():
        if line.strip():
            turns.append(parse_line(line))
    return turns

if __name__ == "__main__":
    for t in load_transcript("transcript.txt")[:3]:
        print(t)

partition(":") يقسم على النقطتين الأولتين ويُرجِع ثلاثية (before, ":", after) ، أكثر أمانًا من split(":") لأن نص المتحدث يمكن أن يحتوي هو نفسه نقطتين (انظر السطر 11: «Wait, that’s not a decision.» بفاصلة عليا، وتخيّل عنوان URL أو وقتًا مثل 01:06 في الرسالة). يفك "."split("]", 1)[1] الختم الزمني بالطريقة نفسها: كل شيء بعد أول ]، حتى لو احتوت الرسالة أقواسًا.

👟 تلميح البداية :

حلل الملف واطبع الأدوار الثلاثة الأولى قبل كتابة أي شيء آخر ، الهدف رؤية Speaker: Priya وtext: Let's review... كحقول نظيفة، لا بوادئ مشوّهة.

🎯 الناتج المتوقع :

ثلاثة dict مثل {'time': '00:00', 'speaker': 'Priya', 'text': "Let's review where we stand on the launch."} ، دون تسرّب [ أو ] إلى حقل الوقت.

🩹 إذا لم يعمل :

إذا خرج speaker كـ Priya بمسافة بادئة، فـ .strip() بعد partition مفقودة. إذا انطلق ValueError: not enough values to unpack، فيخلو سطر من : ، وهو نص مشوّه حقًا، والإصلاح قرر ما إذا كان تخطي الأسطر السيئة أو رفع خطأ؛ يتخطى strip()+المرشح الأسطر الفارغة، لا المشوهة.

1.2 تحقّق من المحلل

✅ قائمة التحقق

  • ✅ يُرجع load_transcript 11 دورًا لـ transcript.txt، كلٌّ dict بـ time وspeaker وtext.
  • ✅ دور تحوي رسالته نقطتين (مثل عنوان URL) ما زال يُحلل مع الرسالة كلها سليمة.
  • ✅ الأسطر ذات الفراغ البيضاء فقط لا تخلق أبدًا أدوارًا فارغة.
  • ✅ يمكنك التنبؤ بما يُرجعه parse_line("[05:00] Sam: A: B") ، وهناك إجابة صحيحة واحدة فقط لـ speaker.

🤔 سؤال (أسئلة) سقراطي(ة)

  • نقسم الختم الزمني بـ split("]", 1). ماذا سينكسر لرسالة مثل [00:30] Zara: the link is [here] ، وهل partition على الختم الزمني* خيار أكثر متانة؟
  • يفترض المحلل أختامًا زمنية [MM:SS]. إذا استخدم نص 00:04:32 (أوقات ساعة حقيقية)، فسيغيّر أي حقل شكله بصمت ، وهل يجب على المحلل التحقق من صيغة الوقت، أم يبقى مكتوبًا بشكل متراخٍ؟

الخطوة 2: قسّم المتحدثين واحسب زمن الهواء

النص بعدان: من قاله، وكم قال. تجمّع هذه الخطوة الأدوار في إجماليات لكل متحدث ، كلمات لكل متحدث، وأدوار لكل متحدث ، الأرقام التي تظهر فورًا ما إذا صوّت واحد الاجتماع كله. النمط Counter/تجميع بالمفتاح، وهو الشكل نفسه «جمّع المبيعات بالمنطقة»، مطبّقًا على زمن الكلام.

2.1 اجمع إحصاءات كل متحدث

# segments.py
from collections import Counter
from parse import load_transcript

def speaker_stats(turns: list[dict]) -> dict[str, dict]:
    stats = {}
    for t in turns:
        s = t["speaker"]
        row = stats.setdefault(s, {"words": 0, "turns": 0})
        row["words"] += len(t["text"].split())
        row["turns"] += 1
    return stats

def word_share(stats: dict[str, dict]) -> dict[str, float]:
    total = sum(r["words"] for r in stats.values()) or 1
    return {s: r["words"] / total for s, r in stats.items()}

if __name__ == "__main__":
    turns = load_transcript("transcript.txt")
    stats = speaker_stats(turns)
    for s, r in sorted(stats.items()):
        print(f"{s:<6} {r['words']:>3} words  {r['turns']} turns  {word_share(stats)[s]:.0%}")

يُرجع stats.setdefault(s, {...}) الصف الموجود إذا كان المتحدث قد شوهد من قبل، أو يُدخل صفًا جديدًا مصفّرًا ويُرجع ، اصطلاح «ابنِ dict من صفوف» الذي يبقي التحوير في سطر واحد. تُعدّ الكلمات بـ t["text"].split() والأدوار بعدّ عادي، ويطبّع word_share إلى نسبة مئوية متينة تجاه اجتماع فارغ بفضل حارس or 1.

👟 تلميح البداية :

شغّل الإحصاءات، ثم عدّ كلمات Priya باليد في النص وتأكد أن الرقم يطابق ، الحارس ضد «ثق بالأداة» هو «عددت مرة بالفعل».

🎯 الناتج المتوقع :

ثلاثة أسطر مثل Priya 41 words 5 turns 43% وTom 30 words 3 turns ...% وZara ... ... ...% مجموعة حصصها الكلامية 100٪.

🩹 إذا لم يعمل :

إذا غاب متحدث كليًا، فذلك أن أدواره حُللت تحت اسم مختلف (مسافة ختامية ، تحقق من .strip() على المتحدث في الخطوة 1). إذا جُمعت الحصص إلى 99٪ أو 101٪، فهذا تقريب عائم، لا خلل ، اعرض بـ :.0% أو طبّع مرة؛ إذا طُبع حصص على شكل nan%, فأنت اصطدمت بحافة total = 0 وحارس or 1 غير موجود.

2.2 تحقّق من التقسيم

✅ قائمة التحقق

  • ✅ لكل متحدث متميز في النص صف واحد بالضبط ، ازدواج اليوم نفسه يُدمج في عدّ جارٍ واحد.
  • ✅ متحدث بلا كلمات (إن وُجد) يُظهر 0 words و0% ، أبدًا صفًا مفقودًا.
  • ✅ إجمالي كلماتك المعدود باليد للملف كله يطابق مجموع الصفوف.

🤔 سؤال (أسئلة) سقراطي(ة)

  • حصة الكلمات مقياس كمية: الشخص الذي يتحدث أكثر يملك الغرفة. ما الذي ستحتاجه «هل هيمنت Priya أم مجرد الموجّه؟» وهذا ما لا تخبره الكلمات وحدها ، تلميحات: متوسط طول الدور، وعدّادات الأسئلة، ونسبة الجمل التقريرية إلى أسطر المتناول اليدوي مثل «Please send it to me»؟
  • نحذف البث بسلسلة المتحدث الدقيقة؛ Priya وpriya سيكونان شخصين. أين نقطة التطبيع الصحيحة ، وقت التحليل، أو وقت التجميع، أو أبدًا ، وماذا يقول الاختيار عن الأداة التي تبنيها؟

الخطوة 3: اعثر على القرارات

الملخصات التي تسرد «أشياء حدثت» تُنسى؛ والملخصات التي تسرد قرارات هي السجل. تفحص هذه الخطوة النص بحثًا عن لغة القطعية ، أفعال مثل agreed وdecided وconfirmed وdecided ، وتستخرج الجملة كلها كقرار. قائم على القواعد وسطحي، لكنه بالضبط كيف يتصرف الممر الأول من بوت ملخص.

3.1 افحص عن أفعال القرار

# decide.py
from parse import load_transcript

DECIDE_VERBS = ("agreed", "decided", "confirmed", "voted", "ruled", "settled")

def find_decisions(turns: list[dict]) -> list[str]:
    decisions = []
    for t in turns:
        for verb in DECIDE_VERBS:
            if verb in t["text"].lower():
                decisions.append(f"{t['time']} {t['speaker']}: {t['text']}")
                break
    return decisions

if __name__ == "__main__":
    for d in find_decisions(load_transcript("transcript.txt")):
        print(d)

تداخل الحلقتين (أدوار × أفعال) صغير بما يكفي ليبقى O(n·m) صادقًا؛ يضمن lower() أن agreed يطابق Agreed، ويضمن break فعلًا واحدًا لكل دور ، سطر يقول «agreed» وهي همسة «confirmed» يحسب مرة. يُعرض القرار مع time وspeaker ملحقين به، بحيث يحفظ الملخص السند («at 00:15 قررت Priya…») بدلًا من جملة عارية.

👟 تلميح البداية :

شغّله، ثم دقّق المخرج مقابل الملف بالنظر ، تتحقق من أن «We agreed the beta opens next Monday» وكذلك «We decided the pricing page stays as-is» يظهران كلاهما، وأن سطر Zara التاسع (will draft) لا يظهر ، «drafting» فعل، لا قرار، وهذا التمييز هو نقطة الخطوة كلها.

🎯 الناتج المتوقع :

سطران ، دور 00:15 «beta opens next Monday» ودور 00:44 «pricing page stays as-is» ، ولا شيء من الأسطر 3 أو 6 أو 9.

🩹 إذا لم يعمل :

إذا ظهر قرار واحد فقط، ففعل فاتته لأن النص استخدم مرادفًا (agree بدلًا من agreed) ، إما وسّع الصف أو اخفض الحالة واجذِر (جرّب startswith على جذر فعل) باستمرار. إذا دخل [00:08] Tom: Design shipped... في المخرج، فكلمة «decided» تظهر داخل جملة عادية («We decided…») ، ذلك إيجابي حقيقي هنا، لكن فعل shipped مستقبلي سيكون إيجابيًا كاذبًا يجب على صفك تجنبه بتسمية كلمات دقيقة.

3.2 تحقّق من القرارات

✅ قائمة التحقق

  • ✅ القراران الحقيقيان في العينة يظهران مع الختم الزمني والمتحدث.
  • ✅ لا يُصنَّف «I will draft…» أو «I’ll block out…» سطر فعل خطأً كقرار.
  • ✅ دور لا يذكر أي فعل قرار لا يسهم بشيء في القائمة.
  • ✅ يمكنك شرح الخط المتعمد بين «agreed» (قرار) و«will draft» (فعل).

🤔 سؤال (أسئلة) سقراطي(ة)

  • قائمة أفعالنا صف ثابت، بحيث يتسلل قرار مصاغ كـ«Priya: so the beta is a go» (بلا فعل قرار إطلاقًا). ما الإشارة الثانية المستقلة ، علامة سؤال، أو «right?», أو قناة عودة «yes» ، التي يمكن أن ترفع علمه، وما الإيجابيات الكاذبة التي تضيفها؟
  • «agreed» داخل «I agreed with you earlier that the design was rough» سياقيًا ليس قرارًا، ومع ذلك يبلغ عنه مسحنا. هل «لا قرارات مُبلَّغ عنها» سالب كاذب مقبول دائمًا، وأين سترسم خط الدقة/الاستدعاء لبوت ملاحظات (تلميح: فضّل إيجابيات حقيقية كثيرة على إيجابي كاذب عابر، اليوم)؟

الخطوة 4: استخرج بنود التنفيذ بأصحابها

القرارات تقول ماذا تغير؛ بنود التنفيذ تقول من يفعل ماذا بحلول متى ، وهي الجزء الذي يعيشه الناس فعلًا. نمط الاستخراج: يظهر المالك اسماً يليه مباشرة (ضمن بضع كلمات) فعل زمن مستقبلي (will وowns وpublish). هذا بديل ضحل قابل للتفسير لما سيفعله محول بـ الانتباه ، وبالنسبة لبوت ملاحظات، القابل للتفسير يتفوق على السحري.

4.1 استخرج أزواج المالك + المهمة

# actions.py
from parse import load_transcript

ACTORS = ("Priya", "Tom", "Zara")
TASK_WORDS = ("will", "owns", "draft", "send", "block", "publish", "write", "set")

def extract_actions(turns: list[dict]) -> list[dict]:
    actions = []
    for t in turns:
        lowered = t["text"].lower()
        for actor in ACTORS:
            if actor.lower() not in lowered:
                continue
            for word in TASK_WORDS:
                if word in lowered:
                    actions.append({"time": t["time"], "owner": actor, "task": t["text"]})
                    break
    return actions

if __name__ == "__main__":
    for a in extract_actions(load_transcript("transcript.txt")):
        print(f"{a['time']}  {a['owner']} -> {a['task']}")

حلقتان متداخلتان مجددًا، لكن ترتيب الحارس مهم: فحص actor أولًا وcontinue يتخطى الدور كليًا إذا لم يذكر شخصًا معروفًا ، ذلك هو مرشح «الملكية» كله. يُبقي break بعد كلمة المهمة الأولى سطرًا واحدًا لفعل واحد حتى عندما يقول «will draft the video and then send the email». نص المهمة هو الدور الكامل (تحتفظ بالجملة للسياق)؛ كانت أداة أفخم ستشطّر بالضبط السلسلة الفرعية ، دون ذلك كتبسيط متعمد.

👟 تلميح البداية :

شغّله ثم علّم باليد الأفعال الحقيقية: Tom ← الفيديو، Zara ← البريد، Priya ← سجل التغييرات. يجب أن يسمي المخرج كل مالك والدور الصحيح ، وسطر 6 «send it to me» يجب ألا يسرق فعل Tom.

🎯 الناتج المتوقع :

ثلاثة أدوار كاملة كلٌّ موسوم بمالك: Tom -> I'll block out Thursday to prep the demo video وZara -> I will draft the onboarding email today وPriya -> And I'll publish the changelog on Friday ، بترتيب النص.

🩹 إذا لم يعمل :

إذا غاب سطر «publish» الخاص بـ Priya، فاسمها ليس في ACTORS أو «publish» ليس في TASK_WORDS ، كلاهما بيانات تتحكم فيها؛ أضف الأسماء والأفعال، وفضّل إعادة التشغيل على «ضبط النموذج». إذا ظهر سطر Tom بمالك Zara، فدور النص يذكر Zara (السطر 8: «Zara, Tom owns the video») وكذلك Tom ، غموض حقيقي، محلول الآن بأول ممثلِ وُجد، ويستحق تعليق TODO لديك، لا اختراقًا.

4.2 تحقّق من الأفعال

✅ قائمة التحقق

  • ✅ كل فعل من الأفعال الثلاثة الحقيقية يظهر مرة، بالمالك الصحيح ونص الدور الصحيح.
  • ✅ «send it to me» (طلب) لا يُستخرج كبند تنفيذ لـ Tom أو Zara.
  • ✅ متحدث بلا أفعال فعلية (دور نقري بحت) لا يسهم بشيء.
  • ✅ يمكنك شرح لماذا «يظهر المالك في نفس دور كلمة المهمة» بديل، لا الحقيقة.

🤔 سؤال (أسئلة) سقراطي(ة)

  • المالك هو من يظهر اسمه في دور ، لكن في «Zara, Tom will own the video»، يختلف المالك (Tom) عن المخاطب (Zara). ما البيانات التي ستمكنك من إزالة الالتباس ، ترتيب الكلمات، أو القرب من الفعل، أو موضع الفاعل ، وأيها الإشارة الأرخص؟
  • «I will publish the changelog on Friday» يُسند إلى المتحدث؛ «Tom will publish the changelog» يُسند إلى شخص آخر. يعامل مستخرجنا الاثنين كـ«ذِكر = مالك». ماذا سيفعل فحص speaker == owner بثقة قائمة التنفيذ ، وهل قاعدة المتحدث أولًا افتراضي جيد لمحاضر الاجتماعات؟

الخطوة 5: ضع الملخص وشحنه

كل شيء حتى الآن ينتج شظايا؛ الملخص المنتَج. تجمّع الخطوة 5 إحصاءات المتحدثين والقرارات والأفعال في ملف واحد قابل للقراءة ، الشيء الذي تلصقه فعلًا في محادثة المجموعة بعد اجتماع ، وتحفظه بحيث يمكن لأي شخص فتحه.

5.1 اجمع واكتب التقرير

# summary.py
from pathlib import Path
from parse import load_transcript
from segments import speaker_stats, word_share
from decide import find_decisions
from actions import extract_actions

def build_summary(turns: list[dict]) -> str:
    stats = speaker_stats(turns)
    lines = [f"MEETING SUMMARY — {len(turns)} turns",
             "\nSpeakers by share:",
             *[f"  {s}: {r['words']} words ({word_share(stats)[s]:.0%})"
               for s, r in sorted(stats.items(), key=lambda kv: -kv[1]['words'])],
             "\nDecisions:", *[f"  [{d}]" for d in find_decisions(turns)],
             "\nAction items:", *[f"  [{a['time']}] {a['owner']}: {a['task']}"
                                  for a in extract_actions(turns)]]
    return "\n".join(lines)

if __name__ == "__main__":
    turns = load_transcript("transcript.txt")
    Path("summary.txt").write_text(build_summary(turns), encoding="utf-8")
    print(build_summary(turns))

تأليف تقرير من نتائج فرعية هو مرحلة «التجميع» والعادة التي تحملها إلى أي أداة أكبر: كل خطوة سابقة تبقى دالة صغيرة نقية، وbuild_summary تؤلّفها فقط ، بحيث لا يعرف الملخص أكثر من الأجزاء، والجزء المسيء دالة واحدة تُختبَر. فرز المتحدثين بالكلمات تنازليًا (key=lambda kv: -kv[1]['words']) يضع الصوت المهيمن أولًا، وهو بحد ذاته نتيجة.

👟 تلميح البداية :

اكتب summary.txt، ثم افتحه في محرر نصوص واقرأه وكأنك فاتك الاجتماع كله ، معيارك في «يعمل» أن غريبًا قادرًا على إعادة بناء الاجتماع من هذا الملف وحده.

🎯 الناتج المتوقع :

summary.txt يحوي العنوان بعدد الأدوار، والمتحدثين الثلاثة كلهم بإجماليات كلمات وحصص، والقرارين، وثلاثة بنود تنفيذ تحت عناوين واضحة ، مقروءًا من الأعلى للأسفل بلا بقايا Python.

🩹 إذا لم يعمل :

إذا كُتب الملف بأقسام فارغة، فأعادت دالة فرعية [] ، تحقق أن الخطوات السابقة ما زالت تعمل من __main__ قبل التأليف (استيراد مكسور يمرّر None بصمت). إذا احتوى المخرج شظايا None، فـ f-string أصاب عودة None ، يجب أن تُرجع كل دالة فرعية قائمة/سلسلة، لا None؛ شغّل __main__ كل خطوة لعزل.

5.2 تحقّق من الملخص المشحون

✅ قائمة التحقق

  • ✅ يوجد summary.txt ويحوي الأقسام الأربعة كلها تحت عناوينها.
  • ✅ محتوى كل قسم يطابق ما طبعته الخطوات المنفردة ، لا شيء مُضاف، لا شيء مسقط.
  • ✅ يمكن لغريب إعادة بناء متحدثي الاجتماع وقراراتهم وأصحابه من الملف وحده.
  • ✅ إعادة تشغيل البناء من نص مختلف تنتج خط الأنابيب نفسه بنظافة.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يؤلف الملخص شظايا منجزة. ما الذي سيتغير إذا كان على إحصاءات المتحدثين أن تعرض في الملخص بشكل مختلف عنها في الخطوة 2 (مثلًا، دقائق بدلًا من كلمات)؟ هل وظيفة build_summary إعادة صياغة أم نقل ، وماذا يقول ذلك عن أين يجب أن يعيش منطق العرض؟
  • summary.txt لقطة. ما التغيير الذي يحوّلها إلى شيء تعيد تشغيله بعد كل اجتماع بدلًا من مرة (تلميح: علم --from ومخطط مجلد meetings/)? سمِّ قرار الإعداد قبل كتابته.

⚠️ مآزق شائعة

  • نقطتان داخل الرسائل تكسران التحليل. «The demo link: http://…» يحتوي فعلًا نقطتين، وsplit(":") على السطر الكامل يفصل اسم المتحدث وأيضًا الرسالة إلى جزأين. partition(":") بعد تجريد الختم الزمني هو الإصلاح ، اقسم على النقطتين الأولتين فقط، لا كلّها أبدًا.
  • انزياح سلسلة المتحدث ينتج أشخاصًا أشباحًا. Priya مقابل Priya (مسافة ختامية) أو priya مقابل Priya ينشئان صفين في الخطوة 2 ومالكين في الخطوة 4. طبّع الأسماء الدقيقة مرة، وقت التحليل، ودع كل خطوة لاحقة تثق بالسلسلة.
  • زمن الهواء مقاسًا بالكلمات مقابل الأدوار. حصة الكلمات تعامل مونولوج 40 كلمة و5 استطرادات قصيرة كمتحدثين متساويين. الإحصاءتان موجودتان؛ عرض إحداهما وحدها يؤطّر الاجتماع بصمت ، يجب أن يعرض الملخص الكلمات والأدوار، وليكن الهيمنة قراءة، لا تأكيدًا.
  • خلط القرار/الفعل. «We decided the beta opens Monday» قرار؛ «I’ll block Thursday» فعل. تنجح أداة واحدة بالضبط إذا دُمجا، لأن المالكين بلا معنى للقرارات وتسلسلات الأفعال مضللة للأفعال ، أبقِ الماسحين منفصلين، كما تفعل الخطوتان 3 و4.
  • كتابة ملخص لا يمكن لأحد دقّه. الملخص بلا أختام زمنية أو متحدثين رأي؛ معها سجل. يحمل كل رصاصة يصدّرها تقريرنا [time] واسمًا ، أسقطهما وبنيت أداة تعيد الصياغة بدلًا من التوثيق.

ما بنيته للتو

ملخّص اجتماعات يعمل: نص داخل، وsummary.txt مقروء خارج ، متحدثون مرتبون بالحصة، وقراران مستخرجان بسند، وثلاثة بنود تنفيذ كلٌّ مرتبط بمالك حقيقي. المهارة القابلة للنقل هي عادة خط أنابيب النص كلها: حلّل إلى أدوار منظمة، واجمع وشارك بالمجموعات، وتعرف على الأنماط بالقواعد، ثم ألّف تقريرًا ، الهيكل نفسه الذي يشغّل فرز البريد ودَورية تذاكر الدعم وحفر السجلات و(بنموذج أثقل في المنتصف) كل «ملخّص LLM» ألصقته يومًا مع تسجيل مكالمة. يخصّك بشفافية، ومُختبَر سطرًا بسطر، ولا يحتاج مفتاح API ليستحق بقاءه.

شغّل نسخة أكمل دون أي إعداد محلي

examples/meeting-transcriber/ في مستودع الدورة يحزم وحدات المحلل والأقسام والقرارات والأفعال والملخص زائد نص العينة ودفترًا يشغّل كل خطوة بالترتيب. استنسخه، أو افتح المستودع كله في GitHub Codespace، وابنِ ملخصًا في نافذة متصفح.

إلى أين تذهب من هنا

  • تحويل كلام حقيقي إلى نص (اختياري). إذا كان لديك مفتاح API بطبقة مجانية لخدمة نقل (أو أداة whisper الخاصة بحاسوبك)، فاستبدل load_transcript باستدعاء عملية فرعية يأخذ .m4a ويُصدر SRT ، كل خطوة لاحقة تعمل بالفعل على المخرج.
  • تصدير إلى CSV. يحوّل csv.writer بنود التنفيذ إلى صفوف (owner, task, time) يمكنك فرزها بالمالك أو استيرادها إلى متعقب مهام ، يبقى الملخص قابلًا للقراءة البشرية، ويصبح الـ CSV مقروء الآلة، وهما عرضان لتحليل واحد.
  • مرشح --speaker Sam يلخص أدوار شخص واحد ، نفس خط الأنابيب، وسطة مرشح واحدة، مفيدة فورًا لسؤال «ما الذي التزمت به أنا؟».
  • استخراج أثقل عبر LLM (اختياري). قدّم الأدوار المحللة لنموذج بطبقة مجانية مع موجه نظام مثل «أرجع JSON بالقرارات والأفعال» ، يبقى المستخرج القائم على القواعد كبديل دون اتصال، ويصبح الـ LLM خط الأساس، وستقيس أين يتفوق أحدهما على الآخر.

شارك مشروعك مع الصف

بنيت شيئًا فخورًا به ، ملخصًا التقط اجتماعًا حقيقيًا، أو قائمة تنفيذ استخدمتها فعلًا؟ examples/student-projects/ معرض لمشاريع طلاب آخرين قدَّموها، وملف README الخاص به يرشدك من البداية إلى النهاية لإضافة مشروعك عبر pull request: عمل fork والتفريع والتثبيت وفتح الـ PR. لا يُفترَض أي خبرة سابقة بـ git.

مرحبًا بك في كتابة Python خارج المتصفح. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.