From 13452129d99511297f5069b68b41cd31532a8915 Mon Sep 17 00:00:00 2001 From: Abderrahim Adrabi <184391033+abderrahim-lectures@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:25:03 +0100 Subject: [PATCH] i18n: translate Voice-to-Task Agent project into ar/es/fr Co-Authored-By: Claude Sonnet 5 --- i18n/ar/code.json | 8 + .../current/projects/index.mdx | 4 + .../voice-to-task-agent/_category_.json | 4 + .../projects/voice-to-task-agent/index.md | 352 ++++++++++++++++++ i18n/es/code.json | 8 + .../current/projects/index.mdx | 4 + .../voice-to-task-agent/_category_.json | 4 + .../projects/voice-to-task-agent/index.md | 352 ++++++++++++++++++ i18n/fr/code.json | 8 + .../current/projects/index.mdx | 4 + .../voice-to-task-agent/_category_.json | 4 + .../projects/voice-to-task-agent/index.md | 352 ++++++++++++++++++ 12 files changed, 1104 insertions(+) create mode 100644 i18n/ar/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json create mode 100644 i18n/ar/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md create mode 100644 i18n/fr/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json create mode 100644 i18n/fr/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md diff --git a/i18n/ar/code.json b/i18n/ar/code.json index a05eec5..aff86df 100644 --- a/i18n/ar/code.json +++ b/i18n/ar/code.json @@ -941,5 +941,13 @@ "homepage.projects.triviaBot.summary": { "message": "شغّل جولات Trivia في خادم Discord بـ`discord.py`: لوحة متصدّرين دائمة، وأسئلة تُولَّد حديثًا حول أي موضوع باستخدام LLM من مستوى مجاني.", "description": "Homepage project card summary" + }, + "homepage.projects.voiceToTaskAgent.title": { + "message": "بناء وكيل صوت-إلى-مهام", + "description": "Homepage project card title" + }, + "homepage.projects.voiceToTaskAgent.summary": { + "message": "انسخ مذكرة صوتية محليًا ومجانًا باستخدام نموذج Whisper مفتوح المصدر من OpenAI، ثم استخدم LLM بمستوى مجاني لتحويلها إلى قائمة مهام مُهيكَلة.", + "description": "Homepage project card summary" } } diff --git a/i18n/ar/docusaurus-plugin-content-docs/current/projects/index.mdx b/i18n/ar/docusaurus-plugin-content-docs/current/projects/index.mdx index b80cd36..ca90039 100644 --- a/i18n/ar/docusaurus-plugin-content-docs/current/projects/index.mdx +++ b/i18n/ar/docusaurus-plugin-content-docs/current/projects/index.mdx @@ -170,6 +170,10 @@ import {mergeProjectMeta} from '@site/src/data/projects'; title: 'بناء بوت Trivia على Discord', summary: 'ابنِ بوتًا بـ`discord.py` يشغّل جولات Trivia في خادم، ويتتبّع النقاط على لوحة متصدّرين دائمة، ويمكنه توليد أسئلة جديدة حول أي موضوع باستخدام LLM مجاني.', + id: 'voice-to-task-agent', + title: 'بناء وكيل صوت-إلى-مهام', + summary: + 'انسخ مذكرة صوتية محليًا ومجانًا باستخدام نموذج Whisper مفتوح المصدر من OpenAI، ثم استخدم LLM بمستوى مجاني لتحويلها إلى قائمة مهام مُهيكَلة.', }, ])} /> diff --git a/i18n/ar/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json b/i18n/ar/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json new file mode 100644 index 0000000..aaff0d6 --- /dev/null +++ b/i18n/ar/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "وكيل صوت-إلى-مهام", + "position": 9 +} diff --git a/i18n/ar/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md b/i18n/ar/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md new file mode 100644 index 0000000..b86591e --- /dev/null +++ b/i18n/ar/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md @@ -0,0 +1,352 @@ +--- +id: voice-to-task-agent +title: "بناء وكيل صوت-إلى-مهام" +sidebar_label: "وكيل صوت-إلى-مهام" +slug: /projects/voice-to-task-agent +description: "تخرّج من ملعب المتصفح إلى Python فعلي: انسخ مذكرة صوتية محليًا ومجانًا باستخدام نموذج Whisper مفتوح المصدر من OpenAI، ثم استخدم LLM بمستوى مجاني لتحويلها إلى قائمة مهام مُهيكَلة." +--- + +import ProjectProgressCheckbox from '@site/src/components/ProjectProgressCheckbox'; +import ProjectPublishedDate from '@site/src/components/ProjectPublishedDate'; +import ProjectGreeting from '@site/src/components/ProjectGreeting'; +import {StepChecklist, StepChecklistItem} from '@site/src/components/StepChecklist'; + +# 🌍 بناء وكيل صوت-إلى-مهام + + + + + +كل شيء في الدورة حتى الآن عمل في ملعب مُعزول داخل المتصفح — لذا استطعت البدء في كتابة Python من اليوم الأول بلا أي إعداد. هذا المشروع هو خطوة التخرّج: ثبّت Python فعليًا على جهازك الخاص، ثم استخدمه لبناء شيء مفيد حقًا — خط أنابيب صغير يأخذ مذكرة صوتية مشوّشة ويحوّلها إلى قائمة مهام قصيرة ومُهيكَلة، دون أن تكتب أو تنظّم أيًا منها بنفسك. يفترض هذا Python بمستوى 101؛ لا شيء من تحليل البيانات مطلوب. + +هذا اختياري وغير مُقيَّم. راجع [مشاريع من العالم الحقيقي](/docs/projects) للاطلاع على القائمة الكاملة والنامية. + +## 🎯 ما ستفعله + +1. نسخ مذكرة صوتية قصيرة إلى نص، بالكامل محليًا ومجانًا، باستخدام نموذج Whisper *مفتوح المصدر* من OpenAI (`openai-whisper`، يعمل على وحدة المعالجة المركزية الخاصة بك) — لا API Whisper المدفوعة. +2. كتابة prompt يطلب من LLM بمستوى مجاني قراءة تلك النسخة واستخراج بنود عمل مُهيكَلة: مهمة، وتاريخ استحقاق اختياري، وأولوية اختيارية. +3. تشغيل خط الأنابيب بأكمله من البداية إلى النهاية على تسجيل نموذجي مُقدَّم (أو تسجيلك الخاص)، وحفظ النتيجة كقائمة مهام بسيطة. + +## أين تُشغّل هذا + +**محليًا باستخدام `uv`** هو المسار الأساسي والموصى به — النسخ عمل لوحدة المعالجة المركزية (لا حاجة لـGPU لمقطع قصير بنموذج Whisper صغير)، لذا يعمل براحة على كمبيوتر محمول عادي. يشرح الإعداد أدناه كيفية تثبيت `uv`. + +**GitHub Codespaces** يعمل أيضًا: افتح [مستودع الدورة كاملًا في Codespace مجاني](https://codespaces.new/abderrahim-lectures/python-data-analysis-course) (Node وPython وuv مثبّتة بالفعل) وشغّل نفس أوامر `uv` تمامًا من طرفية في تبويب متصفحك. إنه أبطأ قليلًا من كمبيوتر محمول حديث في خطوة النسخ، لأن أجهزة Codespaces وحدة معالجة مركزية فقط، لكنه عملي تمامًا للمقاطع النموذجية القصيرة هنا. + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) +[![Open In Kaggle](https://kaggle.com/static/images/open-in-kaggle.svg)](https://kaggle.com/kernels/welcome?src=https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/abderrahim-lectures/python-data-analysis-course/main?filepath=examples%2Fvoice-to-task-agent%2Fnotebook.ipynb) + +**Google Colab ملاءمة جيدة بشكل ملحوظ لهذا المشروع** — أفضل من معظم المشاريع الأخرى في هذه السلسلة. سرعة نسخ Whisper تتوسع كثيرًا مع العتاد، ويمنحك Colab GPU مجانيًا لا يملكه كمبيوتر محمول محلي بوحدة معالجة مركزية فقط: `!pip install openai-whisper` في خلية، ثم بيئة تشغيل بـGPU، وحتى أحجام نموذج Whisper الأكبر (أكثر دقة، وعادةً بطيئة جدًا لاعتبارها على وحدة معالجة مركزية) تصبح عملية. إذا أردت التجربة بحجم النموذج مقابل الدقة (انظر النصيحة في الخطوة 1)، فـColab هو مكان ذلك. الشارات أعلاه تفتح [`notebook.ipynb`](https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) جاهزًا يشغّل خط الأنابيب بأكمله بلا إعداد محلي — نفس خط الأنابيب ذي الخطوتين، ونفس الصوت النموذجي، فقط في دفتر ملاحظات مستضاف بدلًا من طرفية. + +## الإعداد + +كل ما هو مطلوب قبل أن تكتب أي كود خط أنابيب — تثبيت `uv`، وإنشاء المشروع، والحصول على مفتاح LLM — يوجد هنا، مرة واحدة، مقدمًا. يبدأ البناء الفعلي في الخطوة 1، بافتراض أن كل هذا في مكانه بالفعل. + +### تثبيت `uv` + +`uv` أداة واحدة تحل محل السلسلة المعتادة "ثبّت Python، ثم ثبّت pip، ثم ثبّت أداة بيئة افتراضية، ثم ثبّت الحزم" — يمكنها تثبيت وإدارة إصدارات Python بنفسها، إلى جانب تبعيات مشروعك. + +**macOS / Linux** (الطرفية): + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +**Windows** (PowerShell): + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +أغلق طرفيتك وأعد فتحها، ثم تأكد من أنها ثُبِّتت: + +```bash +uv --version +``` + +### إعداد المشروع + +```bash +uv init voice-to-task-agent +cd voice-to-task-agent +uv add openai-whisper openai python-dotenv +``` + +`openai-whisper` هو نموذج الكلام-إلى-نص مفتوح المصدر نفسه — رغم اسم الحزمة، يُثبَّت ويُشغَّل هذا *محليًا*، بلا مفتاح API وبلا تكلفة لكل دقيقة؛ فقط يحدث أنه منشور من OpenAI ويشارك اسمه مع واجهة API المستضافة والمدفوعة المنفصلة الخاصة بهم. `openai` هو عميل API البسيط المستخدم في الخطوة 2 لاستدعاء مزود LLM بمستوى مجاني الذي تختاره — عدة منهم يعرضون نقطة نهاية متوافقة مع OpenAI، لذا مكتبة عميل واحدة تغطي الستة جميعًا. يتيح لك `python-dotenv` الاحتفاظ بمفتاح LLM في ملف `.env` محلي بدلًا من `export` في كل جلسة. + +:::tip[أول تشغيل ينزّل النموذج] +لا يحزم `openai-whisper` أوزان نموذجه — أول مرة يستدعي فيها كودك `whisper.load_model(...)` (الخطوة 1)، ينزّل الأوزان إلى `~/.cache/whisper` (نحو 140 ميجابايت لحجم `"base"` المستخدم في هذا المشروع) ويعيد استخدامها في كل تشغيل بعده. ستبدو النسخة الأولى بطيئة؛ ذلك هو التنزيل، لا النسخ نفسه. +::: + +### الحصول على مفتاح LLM مجاني + +**اختر أي مزود تريده** — لا يتطلب أي منهم بطاقة ائتمان وقت كتابة هذا، ولا يفضّل هذا الدورة أحدًا على آخر. المثال في مستودع الدورة ([`examples/voice-to-task-agent/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent)) يدعم الستة جميعًا جاهزين، مُحددين بإعداد واحد. + +| المزود | أين تحصل على مفتاح | لماذا قد تختاره | +|---|---|---| +| **GitHub Models** *(افتراضي مُقترَح)* | [github.com/settings/tokens](https://github.com/settings/tokens) — رمز وصول شخصي بصلاحية `models: read` | لا تسجيل منفصل — لديك بالفعل حساب GitHub. حدود مستوى مجاني أكثر سخاءً من حدود Gemini. | +| Gemini | [Google AI Studio](https://aistudio.google.com/) | الخيار الأكثر ذكرًا؛ مستخدم في مسودات سابقة لهذه الصفحة. | +| Groq | [console.groq.com/keys](https://console.groq.com/keys) | استدلال سريع، مستوى مجاني سخي، بلا بطاقة. | +| Mistral | [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys) | أحد حصص التردد المجانية الدائمة الأكثر سخاءً. | +| Cerebras | [cloud.cerebras.ai](https://cloud.cerebras.ai/) | حجم رموز يومي مرتفع، بلا بطاقة. | +| OpenRouter | [openrouter.ai/keys](https://openrouter.ai/keys) | API واحدة، نماذج مجانية كثيرة — جيد لمقارنة المزودين. | + +أيًا كان ما تختاره، العملية هي نفسها: + +1. سجّل الدخول وأنشئ مفتاح API في موقع ذلك المزود. +2. **لا تلصق هذا المفتاح أبدًا مباشرة في الكود أو تلتزمه في مستودع.** أنشئ ملف `.env` في مجلد مشروعك بدلًا من ذلك (لا تلتزمه أبدًا): + +```bash +# .env +GITHUB_TOKEN=your-key-here +``` + +مفتاح API سرّ، تمامًا مثل كلمة مرور — أي شخص يملكه يستطيع استخدام حصة حسابك. معاملته كمتغير بيئة بدلًا من سلسلة مكتوبة بصيغة ثابتة هو الممارسة المعيارية لهذا السبب بالضبط، وهو نفس العادة المبنية في [مشروع AI Agent](/docs/projects/ai-agent) إذا أنجزت ذلك. + +:::tip[ملف .env غالبًا أكثر ملاءمة من export] +بدلًا من `export` لمفتاح في كل جلسة طرفية جديدة، ملف `.env` في مجلد مشروعك، مُحمَّل تلقائيًا بـ`python-dotenv`، يبقى عبر الجلسات دون أن تضطر إلى تذكره. انظر `.env.example` الخاص بمثال المستودع للقائمة الكاملة لأسماء المتغيرات، واحد لكل مزود. +::: + +مع اكتمال الإعداد، يفترض كل ما يلي: `uv` مثبّت، ومشروعك يحتوي `openai-whisper` و`openai` و`python-dotenv`، و`.env` يحتوي مفتاحًا حقيقيًا للمزود الذي اخترته. + +## الخطوة 1: انسخ مذكرة صوتية نموذجية محليًا + +لا تحتاج ميكروفونًا أو تسجيلًا حقيقيًا لتبدأ — يشحن مستودع الدورة ثلاثة مقاطع صوتية نموذجية قصيرة في [`examples/voice-to-task-agent/sample_audio/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent/sample_audio). خذ واحدًا (أو سجّل واحدًا خاصًا بك بأي تطبيق مذكرات صوتية في هاتف/كمبيوتر محمول وانسخه إلى مشروعك — يعمل كل من `.wav` و`.mp3`). + +أنشئ `voice_to_tasks.py`: + +```python +# voice_to_tasks.py +import sys + +import whisper + +WHISPER_MODEL_SIZE = "base" # tiny / base / small / medium / large -- see the tip below + +_whisper_model = None # loaded lazily so importing this module doesn't load it + + +def get_whisper_model(): + global _whisper_model + if _whisper_model is None: + print(f"Loading Whisper '{WHISPER_MODEL_SIZE}' model...") + _whisper_model = whisper.load_model(WHISPER_MODEL_SIZE) + return _whisper_model + + +def transcribe(audio_path: str) -> str: + """Transcribes an audio file to plain text, entirely locally.""" + model = get_whisper_model() + result = model.transcribe(audio_path) + return result["text"].strip() + + +if __name__ == "__main__": + audio_path = sys.argv[1] if len(sys.argv) > 1 else "sample_audio/memo_1_work_followups.wav" + print(transcribe(audio_path)) +``` + +```bash +uv run python voice_to_tasks.py sample_audio/memo_1_work_followups.wav +``` + +يحمّل `whisper.load_model("base")` شبكة عصبية مدرَّبة على كمية ضخمة من بيانات الكلام متعدد اللغات؛ يشغّلها `model.transcribe(audio_path)` على ملف الصوت الخاص بك ويعيد قاموسًا مفتاحه `"text"` هو النسخ الكامل — يتولى Whisper فك ترميز الصوت بنفسه (عبر `ffmpeg` تحت الغطاء) ويعمل على `.wav` و`.mp3` ومعظم الصيغ الشائعة الأخرى دون أن تحوّل أي شيء يدويًا أولًا. + +:::tip[حجم النموذج مقايضة بين السرعة والدقة] +يأتي Whisper بخمسة أحجام — `tiny`، و`base`، و`small`، و`medium`، و`large` — كل واحد أكثر دقة وأبطأ من الذي قبله. `"base"` افتراضي معقول على وحدة معالجة مركزية في كمبيوتر محمول للكلام الإنجليزي القصير الواضح مثل المقاطع النموذجية؛ الصوت المزعج، واللهجات التي يعالجها النموذج بشكل أقل جودة، أو الكلام غير الإنجليزي تستفيد غالبًا من `"small"` أو `"medium"`، بتكلفة زمن نسخ أطول بشكل ملحوظ. هذا بالضبط نوع المقايضة الذي يستحق تجربة GPU من أجله — انظر "أين تُشغّل هذا" أعلاه لمعرفة لماذا Colab ملاءمة جيدة هنا تحديدًا. +::: + +**✅ قائمة التحقق** + + +يطبع `uv run python voice_to_tasks.py sample_audio/memo_1_work_followups.wav` نسخًا حقيقيًا، لا أثر استدعاء. +النص المطبوع يطابق تقريبًا ما تقوله المذكرة النموذجية فعلًا — لن يكون Whisper مثاليًا، لكن يجب أن يكون واضحًا قابلًا للتمييز. +تشغيله مجددًا أسرع بشكل ملحوظ من التشغيل الأول (أوزان النموذج الآن مخزنة مؤقتًا محليًا، لا تُعاد تنزيلها). + + +**🤔 سؤال (أسئلة) سقراطي(ة)** + +- لا يرسل `transcribe()` صوتك أبدًا إلى أي مكان عبر الشبكة. ماذا يعني ذلك لاستخدام هذا على مذكرة صوتية خاصة حقًا، مقارنة بـAPI نسخ مستضاف في السحابة؟ +- لو شغّلت هذا على مذكرة مع موسيقى خلفية تعزف، أو شخصين يتحدثان فوق بعضهما، ماذا تتوقع أن يحدث لجودة النسخ؟ جرّبه على تسجيلك الخاص إذا كان لديك واحد يناسب ذلك. + +## الخطوة 2: استخرج بنود العمل المُهيكَلة بـLLM مجاني + +النسخ مجرد جدار من نص — مفيد، لكنه ليس قائمة مهام بعد. هذه الخطوة تسلّم النسخ إلى LLM بمستوى مجاني مع prompt يطلب منه قراءته وإعادة بيانات مُهيكَلة فعلية: إدخال واحد لكل بند عمل، كل منها بوصف مهمة وحيث تضمره النسخة، تاريخ استحقاق وأولوية. + +أضف استدعاء LLM إلى `voice_to_tasks.py`: + +```python +# voice_to_tasks.py (additions) +import json +import os + +from dotenv import load_dotenv +from openai import OpenAI + +load_dotenv() + +# All six free-tier providers from the table above happen to expose an +# OpenAI-compatible chat completions endpoint, so one client class covers +# all of them -- only base_url and model change. +PROVIDERS = { + "github": {"env": "GITHUB_TOKEN", "base_url": "https://models.github.ai/inference", "model": "gpt-4o-mini"}, + "gemini": {"env": "GOOGLE_API_KEY", "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/", "model": "gemini-3.5-flash"}, + "groq": {"env": "GROQ_API_KEY", "base_url": "https://api.groq.com/openai/v1", "model": "llama-3.3-70b-versatile"}, + "mistral": {"env": "MISTRAL_API_KEY", "base_url": "https://api.mistral.ai/v1", "model": "mistral-small-latest"}, + "cerebras": {"env": "CEREBRAS_API_KEY", "base_url": "https://api.cerebras.ai/v1", "model": "llama-3.3-70b"}, + "openrouter": {"env": "OPENROUTER_API_KEY", "base_url": "https://openrouter.ai/api/v1", "model": "meta-llama/llama-3.3-70b-instruct:free"}, +} + +EXTRACTION_PROMPT = """You extract action items from a voice memo transcript. + +Return a JSON object shaped exactly like this, with no other text before or +after it, and no markdown code fences: + +{{"tasks": [{{"task": "...", "due_date": "...", "priority": "..."}}]}} + +Rules: +- "task" is a short, clear action (e.g. "Email the client the revised + proposal"), not a raw quote from the transcript. +- "due_date" is null if the transcript doesn't mention one -- do not invent + a specific date that was never said. +- "priority" is "high", "medium", or "low" only if the transcript implies + one; otherwise null. +- If there are no action items at all, return {{"tasks": []}}. + +Transcript: +\"\"\" +{transcript} +\"\"\" +""" + + +def extract_action_items(transcript: str, provider: str | None = None) -> list[dict]: + provider = provider or os.environ.get("LLM_PROVIDER", "github") + config = PROVIDERS[provider] + client = OpenAI(api_key=os.environ[config["env"]], base_url=config["base_url"]) + + response = client.chat.completions.create( + model=config["model"], + messages=[{"role": "user", "content": EXTRACTION_PROMPT.format(transcript=transcript)}], + ) + return json.loads(response.choices[0].message.content)["tasks"] +``` + +```bash +uv run python -c " +from voice_to_tasks import transcribe, extract_action_items +transcript = transcribe('sample_audio/memo_1_work_followups.wav') +print(extract_action_items(transcript)) +" +``` + +هذا هو prompt الذي يقوم بالعمل الفعلي هنا: يخبر النموذج بالضبط بأي شكل يعيد (كائن JSON بقائمة `"tasks"`، لا نثرًا حرًا)، ويعطي قواعد صريحة للأجزاء الصعبة — لا تخترع تاريخ استحقاق لم يُقل أبدًا، ولا تخمّن أولوية غير مُضمَنة فعلًا. هذه نفس فكرة prompt [مشروع RAG](/docs/projects/rag-notes) التي تخبر النموذج بالإجابة *فقط* من السياق المسترجَع: تعليمات واضحة ومحددة تضيّق ما يفعله النموذج، بدلًا من الأمل بأنه يستنتج الشكل الصحيح بنفسه. + +يفترض `json.loads(...)["tasks"]` أن النموذج اتبع التعليمات فعلًا وأعاد JSON نظيفًا — النماذج بمستوى مجاني لا تفعل ذلك أحيانًا (جملة شاردة قبل JSON، أو سياج markdown حوله رغم إخباره ألا يفعل). النسخة الأكمل في [`examples/voice-to-task-agent/voice_to_tasks.py`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent) تزيل سياج كود إذا ظهر وترفع خطأً واضحًا بدلًا من أثر استدعاء مربك إذا ما زال JSON لا يُحلَّل — يستحق النسخ إذا كنت تخطط لتشغيله على أكثر من بضعة مذكرات. + +:::tip[تستخدم مزودًا مختلفًا؟] +كل ما سبق يعمل بالفعل لجميع المزودين الستة في الجدول — فقط اضبط `LLM_PROVIDER` في `.env` الخاص بك (أو مرّر اسم مزود مباشرة إلى `extract_action_items`). هذا يعمل لأن GitHub Models وGemini وGroq وMistral وCerebras وOpenRouter جميعهم يعرضون نقطة نهاية متوافقة مع OpenAI؛ على عكس [مشروع AI Agent](/docs/projects/ai-agent)، لا تحتاج مكتبة عميل مختلفة لكل مزود هنا، لأن هذا السكربت لا يستخدم LangChain. +::: + +**✅ قائمة التحقق** + + +يعيد `extract_action_items(transcript)` قائمة Python من القواميس، لا خطأً. +يحمل كل قاموس مفاتيح `"task"` و`"due_date"` و`"priority"` — حتى عندما تكون قيمة `None`. +تشغيله على `memo_1_work_followups.wav` يجد نحو ثلاث مهام منفصلة، مطابقة لمتابعات الثلاثة المذكورة فعلًا في تلك المذكرة. + + +**🤔 سؤال (أسئلة) سقراطي(ة)** + +- يقول prompt صراحة "لا تخترع تاريخًا محددًا لم يُقل أبدًا". ماذا تتوقع أن يحدث لو أزلت تلك التعليمات وقالت النسخة "في وقت ما الأسبوع المقبل"؟ جرّبه — هل يضيف النموذج تاريخًا حقيقيًا من التقويم على أي حال؟ +- لو ذكرت النسخة نفس المهمة مرتين، مُصاغة بشكل مختلف قليلًا في كل مرة (يفعل الناس هذا عندما يفكرون بصوت عالٍ)، هل تتوقع مهمة واحدة في المخرجات أم اثنتين؟ ماذا يقترح جوابك عن قيد على طلب نموذج للقيام بهذا في مرور واحد، دون خطوة إزالة تكرار خاصة به؟ + +## الخطوة 3: شغّله من البداية إلى النهاية واحفظ قائمة مهام + +اجمع القطعتين معًا في سكربت واحد ينسخ، ويستخرج، ويطبع قائمة قابلة للقراءة، ويحفظها كـJSON: + +```python +# voice_to_tasks.py (additions) +def print_tasks(tasks: list[dict]) -> None: + if not tasks: + print("No action items found in this memo.") + return + markers = {"high": "\U0001f534", "medium": "\U0001f7e1", "low": "\U0001f7e2"} + for item in tasks: + marker = markers.get((item.get("priority") or "").lower(), "⚪") + due = f" (due: {item['due_date']})" if item.get("due_date") else "" + print(f"{marker} {item['task']}{due}") + + +def main() -> None: + audio_path = sys.argv[1] if len(sys.argv) > 1 else "sample_audio/memo_1_work_followups.wav" + + print(f"Transcribing {audio_path} ...") + transcript = transcribe(audio_path) + print("\n--- Transcript ---") + print(transcript) + + print("\nExtracting action items...") + tasks = extract_action_items(transcript) + + print("\n--- Action items ---") + print_tasks(tasks) + + with open("tasks.json", "w", encoding="utf-8") as f: + json.dump(tasks, f, indent=2, ensure_ascii=False) + print(f"\nSaved {len(tasks)} task(s) to tasks.json") + + +if __name__ == "__main__": + main() +``` + +```bash +uv run python voice_to_tasks.py sample_audio/memo_3_project_planning.mp3 +``` + +جرّب المقاطع النموذجية الثلاثة جميعًا، و— إذا كانت لديك طريقة لتسجيل واحد — مذكرتك الصوتية الخاصة أيضًا. قائمة مشتريات قصيرة، أو مجموعة متابعات اجتماع، أو قائمة أعمال منزلية كلها اختبارات جيدة: أي شيء بحفنة من بنود عمل متميزة بطول جملة، منطوقة بالطريقة التي ستحدث بها نفسك فعليًا، لا قائمة مُهيكَلة رسميًا. + +**✅ قائمة التحقق** + + +يطبع `uv run python voice_to_tasks.py` (بأي من المقاطع النموذجية الثلاثة) نسخًا، ثم قائمة مهام موسومة، ثم سطر "Saved N task(s)". +يوجد ملف `tasks.json` الآن في مجلد مشروعك، ومحتواه يطابق ما طُبع. +تشغيله على مذكرة بلا بنود عمل حقيقية فيها (جرّب فقط وصف يومك) يطبع "No action items found" بدلًا من اختلاق وهمية. + + +**🤔 سؤال (أسئلة) سقراطي(ة)** + +- يستبدل `tasks.json` نفسه في كل تشغيل، دون دمج قائمة قديمة مع جديدة. ماذا ستحتاج لإضافته لجعل هذا قائمة مهام جارية مفيدة حقًا عبر مذكرات متعددة، مسجلة في أيام مختلفة؟ +- هذا الخط الأنابيب له نقطتا فشل تتصرفان بشكل مختلف جدًا: Whisper يسمع كلمة خطأً، والـLLM يقرأ جملة مكتوبة بشكل صحيح خطأً. إذا خرجت مهمة خاطئة، كيف ستعرف أي من المرحلتين سبّبها فعلًا؟ + +## ⚠️ مآزق شائعة + +- **الخلط بين Whisper مفتوح المصدر وAPI Whisper المدفوعة.** يعمل `openai-whisper` (هذا المشروع) بالكامل على جهازك الخاص، مجانًا، بلا مفتاح API — إنه ليس نفس شيء `client.audio.transcriptions.create(...)`، نقطة نهاية النسخ *المستضافة* والمدفوعة من OpenAI. كلاهما يُسمى "Whisper" وكلاهما من OpenAI، وهو بالضبط السبب في أنه يستحق التوضيح أي منهما يستخدم أي كود معين. +- **أول تشغيل طويل جدًا، مخطئ في أنه تعلّق.** أول استدعاء لـ`whisper.load_model(...)` ينزّل أوزان النموذج (انظر نصيحة الإعداد) — على اتصال بطيء قد يستغرق هذا وقتًا دون شريط تقدم في الإصدارات الأقدم. اتركه يُكمل مرة؛ كل تشغيل بعده سريع. +- **رد JSON من LLM ليس JSON صالحًا تمامًا.** تلتف النماذج بمستوى مجاني أحيانًا حول إجابتها في سياج كود markdown، أو تضيف جملة شاردة، رغم تعليمات صريحة ألا تفعل. عامِل فشل `json.loads(...)` هنا كحدوث متوقع ومتقطع — لا علامة على أن prompt معطوب جوهريًا — وانظر `_parse_tasks_response` في المثال الأكمل لإصلاح إزالة السياج. +- **حدود المعدل على مستوى LLM المجاني.** النسخ (الخطوة 1) محلي وغير محدود؛ فقط استدعاء الاستخراج في الخطوة 2 يُحتسب ضد حصة مستوى المزود المجاني. خطأ 429 هناك هو المزود يخبرك أن تتباطأ، لا خطأ برمجي — انظر [مشروع AI Agent](/docs/projects/ai-agent#التعامل-مع-حدود-المعدل) لنفس النمط ونهج إعادة محاولة يمكنك نسخه. + +## ما بنيته للتو + +خط أنابيب صغير لكنه كامل يربط نوعين مختلفين حقًا من نماذج الذكاء الاصطناعي: نموذج كلام-إلى-نص محلي مجاني مفتوح الأوزان يقوم بالاستماع، ونموذج لغة مستضاف بمستوى مجاني يقوم بالقراءة-والمُهيكَلة. لا شيء هنا مُزيَّف — بدّل في تسجيل حقيقي أطول وأكثر فوضى، ونفس الخطوتين (انسخ، ثم استخرج) ما زالتا خط الأنابيب بأكمله. هذا أيضًا مثال صغير ملموس على نمط أوسع يستحق الملاحظة: ليست كل مهمة ذكاء اصطناعي تحتاج نموذجًا مستضافًا ضخمًا. Whisper صغير بما يكفي ليعمل محليًا مجانًا؛ فقط الجزء من العمل الذي يستفيد فعلًا من استدلال نموذج لغة كبير — تحويل كلام منطوق فضفاض إلى بيانات مُهيكَلة نظيفة — يمد يده إليه. + +:::tip[شغّل نسخة أكمل دون أي إعداد محلي للكود] +[`examples/voice-to-task-agent/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent) في مستودع الدورة نسخة أكمل قليلًا من الكود أعلاه — نفس خط الأنابيب ذي الخطوتين، بالإضافة إلى إصلاح إزالة السياج المذكور أعلاه ورسائل خطأ أوضح. انسخه، أو افتح المستودع كاملًا في [GitHub Codespace](https://codespaces.new/abderrahim-lectures/python-data-analysis-course)، وشغّله ضد أي من المقاطع النموذجية الثلاثة في `sample_audio/`. +::: + +## إلى أين تذهب من هنا + +- جرّب حجم نموذج Whisper أكبر (`"small"` أو `"medium"`) على تسجيل أطول وأكثر فوضى — ضجيج خلفية، أو متحدثين متعددين، أو مذكرة غير إنجليزية — وانظر أين يبدأ `"base"` في التقصير. هذا عذر رائع لتجربة مسار GPU في Colab من "أين تُشغّل هذا" أعلاه. +- جمّع المهام المستخرجة بالأولوية، أو رتّبها بحسب كيف يبلغ النموذج عن تواريخ الاستحقاق، بدلًا من طباعتها بترتيب النسخ. +- اجعل `tasks.json` تراكميًا: حمّل الملف الموجود (إن وُجد)، وأضف المهام المستخرجة حديثًا بدلًا من الكتابة فوقها، وأزل تكرار أي شيء يبدو كأنه نفس المهمة المذكورة مرتين. +- اربط هذا بشيء يستهلك قائمة المهام فعلًا — إلحاقًا بـAPI تطبيق مهام حقيقي، أو تقويم، أو حتى ملف Markdown لقائمة تحقق جارية — بدلًا من ملف JSON لا يقرؤه أي شيء آخر بعد. + +## شارك مشروعك مع الصف + +بنيت شيئًا فخورًا به؟ [`examples/student-projects/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/student-projects) معرض لمشاريع طلاب آخرين قدَّموها — وملف README الخاص به يحتوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر **pull request**، حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع، وإنشاء فرع، وتثبيت ملفاتك، وفتح الـ PR، خطوة بخطوة. لا يُفترَض أي خبرة سابقة بـ git. + +مرحبًا بك في كتابة Python خارج المتصفح. 🎓 + + diff --git a/i18n/es/code.json b/i18n/es/code.json index 31fe223..97a9c09 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -941,5 +941,13 @@ "homepage.projects.triviaBot.summary": { "message": "Ejecuta rondas de trivia en un servidor de Discord con discord.py: una tabla de clasificación persistente, y preguntas generadas al momento sobre cualquier tema con un LLM de nivel gratuito.", "description": "Homepage project card summary" + }, + "homepage.projects.voiceToTaskAgent.title": { + "message": "Construye un Agente de Voz a Tarea", + "description": "Homepage project card title" + }, + "homepage.projects.voiceToTaskAgent.summary": { + "message": "Transcribe una nota de voz localmente y gratis con el modelo de código abierto Whisper de OpenAI, y luego usa un LLM de nivel gratuito para convertirla en una lista de tareas estructurada.", + "description": "Homepage project card summary" } } diff --git a/i18n/es/docusaurus-plugin-content-docs/current/projects/index.mdx b/i18n/es/docusaurus-plugin-content-docs/current/projects/index.mdx index e198698..db7cd01 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/projects/index.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/projects/index.mdx @@ -170,6 +170,10 @@ Son opcionales y no calificados. Explóralos en cualquier momento — la introdu title: 'Construye un Bot de Trivia para Discord', summary: 'Construye un bot de `discord.py` que ejecuta rondas de trivia en un servidor, lleva el seguimiento de los puntos en una tabla de clasificación persistente, y puede generar preguntas nuevas sobre cualquier tema con un LLM de nivel gratuito.', + id: 'voice-to-task-agent', + title: 'Construye un Agente de Voz a Tarea', + summary: + 'Transcribe una nota de voz localmente y gratis con el modelo de código abierto Whisper de OpenAI, y luego usa un LLM de nivel gratuito para convertirla en una lista de tareas estructurada.', }, ])} /> diff --git a/i18n/es/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json b/i18n/es/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json new file mode 100644 index 0000000..68e53d5 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Agente de Voz a Tarea", + "position": 9 +} diff --git a/i18n/es/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md b/i18n/es/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md new file mode 100644 index 0000000..2060e53 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md @@ -0,0 +1,352 @@ +--- +id: voice-to-task-agent +title: "Construye un Agente de Voz a Tarea" +sidebar_label: "Agente de Voz a Tarea" +slug: /projects/voice-to-task-agent +description: "Pasa del playground en el navegador al Python real: transcribe una nota de voz localmente y gratis con el modelo de código abierto Whisper de OpenAI, y luego usa un LLM de nivel gratuito para convertirla en una lista de tareas estructurada." +--- + +import ProjectProgressCheckbox from '@site/src/components/ProjectProgressCheckbox'; +import ProjectPublishedDate from '@site/src/components/ProjectPublishedDate'; +import ProjectGreeting from '@site/src/components/ProjectGreeting'; +import {StepChecklist, StepChecklistItem} from '@site/src/components/StepChecklist'; + +# 🌍 Construye un Agente de Voz a Tarea + + + + + +Todo en el curso hasta ahora corrió en un playground aislado dentro del navegador — así que pudiste empezar a escribir Python desde el día uno con cero configuración. Este proyecto es el paso de graduación: instala Python de verdad en tu propia máquina, y luego úsalo para construir algo genuinamente útil — un pequeño pipeline que toma una nota de voz divagante y la convierte en una lista de tareas corta y estructurada, sin que tengas que escribir u organizar nada de eso a mano. Esto asume Python 101; no se requiere nada de Análisis de Datos. + +Esto es opcional y no calificado. Consulta [Proyectos del mundo real](/docs/projects) para la lista completa y creciente. + +## 🎯 Lo que harás + +1. Transcribir una nota de voz corta a texto, completamente local y gratis, usando el modelo *open-source* Whisper de OpenAI (`openai-whisper`, ejecutándose en tu propio CPU) — no la API de Whisper de pago. +2. Escribir un prompt que pida a un LLM de nivel gratuito leer esa transcripción y extraer elementos de acción estructurados: una tarea, una fecha límite opcional, una prioridad opcional. +3. Ejecutar todo el pipeline de principio a fin en una grabación de muestra provista (o la tuya), y guardar el resultado como una lista de tareas simple. + +## Dónde ejecutar esto + +**Localmente con `uv`** es el camino principal y recomendado — la transcripción es trabajo de CPU (no se necesita GPU para un clip corto con un modelo Whisper pequeño), así que corre cómodamente en un portátil normal. La configuración de abajo explica cómo instalar `uv`. + +**GitHub Codespaces** también funciona: abre [todo el repositorio del curso en un Codespace gratuito](https://codespaces.new/abderrahim-lectures/python-data-analysis-course) (Node, Python y `uv` ya están instalados) y ejecuta los mismos comandos `uv` exactos desde una terminal en tu pestaña del navegador. Es un poco más lento que un portátil moderno para el paso de transcripción, ya que las máquinas de Codespaces son solo CPU, pero perfectamente funcional para los clips de muestra cortos de aquí. + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) +[![Open In Kaggle](https://kaggle.com/static/images/open-in-kaggle.svg)](https://kaggle.com/kernels/welcome?src=https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/abderrahim-lectures/python-data-analysis-course/main?filepath=examples%2Fvoice-to-task-agent%2Fnotebook.ipynb) + +**Google Colab es un ajuste notablemente bueno para este** — mejor que para la mayoría de los otros proyectos de esta serie. La velocidad de transcripción de Whisper escala mucho con el hardware, y Colab te da una GPU gratuita que un portátil local solo-CPU no tiene: `!pip install openai-whisper` en una celda, luego un runtime con GPU, y hasta los tamaños de modelo Whisper más grandes (más precisos, normalmente demasiado lentos para considerar en una CPU) se vuelven prácticos. Si quieres experimentar con el tamaño del modelo vs. precisión (ver el tip en el Paso 1), Colab es dónde hacerlo. Las insignias de arriba abren un [`notebook.ipynb`](https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) listo que ejecuta todo el pipeline con cero configuración local — el mismo pipeline de dos pasos, el mismo audio de muestra, solo que en un notebook alojado en lugar de una terminal. + +## Configuración + +Todo lo necesario antes de que escribas cualquier código de pipeline — instalar `uv`, crear el proyecto, y obtener una clave de API de LLM — vive aquí, una vez, por adelantado. La construcción real comienza en el Paso 1, asumiendo que todo esto ya está en su lugar. + +### Instalar `uv` + +`uv` es una sola herramienta que reemplaza la cadena habitual de "instala Python, luego instala pip, luego instala una herramienta de entorno virtual, luego instala paquetes" — puede instalar y gestionar versiones de Python por sí misma, junto con las dependencias de tu proyecto. + +**macOS / Linux** (terminal): + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +**Windows** (PowerShell): + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +Cierra y vuelve a abrir tu terminal, luego confirma que se instaló: + +```bash +uv --version +``` + +### Configurar el proyecto + +```bash +uv init voice-to-task-agent +cd voice-to-task-agent +uv add openai-whisper openai python-dotenv +``` + +`openai-whisper` es el modelo de código abierto de voz a texto en sí — a pesar del nombre del paquete, esto se instala y corre *localmente*, sin clave de API y sin costo por minuto; solo pasa que está publicado por OpenAI y comparte nombre con su API alojada, de pago, separada. `openai` es el cliente de API simple usado en el Paso 2 para llamar al proveedor de LLM de nivel gratuito que elijas — varios de ellos exponen un endpoint compatible con OpenAI, así que una sola biblioteca de cliente cubre los seis. `python-dotenv` te permite mantener tu clave de API de LLM en un archivo `.env` local en lugar de `export`-arla en cada sesión. + +:::tip[La primera ejecución descarga el modelo] +`openai-whisper` no incluye los pesos de su modelo — la primera vez que tu código llame a `whisper.load_model(...)` (Paso 1), descarga los pesos a `~/.cache/whisper` (unos 140MB para el tamaño `"base"` usado en este proyecto) y los reutiliza en cada ejecución posterior. La primera transcripción se sentirá lenta; eso es la descarga, no la transcripción en sí. +::: + +### Obtener una clave de API de LLM gratuita + +**Elige el proveedor que prefieras** — ninguno requiere una tarjeta de crédito al momento de escribir esto, y este curso no favorece uno sobre otro. El ejemplo en el repositorio del curso ([`examples/voice-to-task-agent/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent)) soporta los seis listos para usar, seleccionados con una sola configuración. + +| Proveedor | Dónde obtener una clave | Por qué podrías elegirlo | +|---|---|---| +| **GitHub Models** *(sugerido por defecto)* | [github.com/settings/tokens](https://github.com/settings/tokens) — un token de acceso personal con el alcance `models: read` | Sin registro separado — ya tienes una cuenta de GitHub. Límites de nivel gratuito más generosos que los de Gemini. | +| Gemini | [Google AI Studio](https://aistudio.google.com/) | La opción más comúnmente referenciada; usada en borradores anteriores de esta página. | +| Groq | [console.groq.com/keys](https://console.groq.com/keys) | Inferencia rápida, nivel gratuito generoso, sin tarjeta. | +| Mistral | [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys) | Una de las cuotas gratuitas permanentes más generosas. | +| Cerebras | [cloud.cerebras.ai](https://cloud.cerebras.ai/) | Alto volumen de tokens diario, sin tarjeta. | +| OpenRouter | [openrouter.ai/keys](https://openrouter.ai/keys) | Una API, muchos modelos gratuitos — bueno para comparar proveedores. | + +Elijas el que elijas, el proceso es el mismo: + +1. Inicia sesión y genera una clave de API en el sitio de ese proveedor. +2. **Nunca pegues esta clave directamente en el código ni la confirmes en un repositorio.** Crea un archivo `.env` en tu carpeta de proyecto en su lugar (nunca lo confirmes): + +```bash +# .env +GITHUB_TOKEN=your-key-here +``` + +Una clave de API es un secreto, exactamente como una contraseña — cualquiera con ella puede usar la cuota de tu cuenta. Tratarla como una variable de entorno en lugar de una cadena hardcodeada es la práctica estándar por exactamente esta razón, y es el mismo hábito construido en el [proyecto AI Agent](/docs/projects/ai-agent) si has hecho ese. + +:::tip[Un archivo .env a menudo es más conveniente que export] +En lugar de `export`-ar una clave en cada sesión de terminal nueva, un archivo `.env` en tu carpeta de proyecto, cargado automáticamente con `python-dotenv`, persiste entre sesiones sin que tengas que recordarlo. Consulta el `.env.example` del ejemplo en el repositorio para la lista completa de nombres de variables, uno por proveedor. +::: + +Con la configuración hecha, todo lo de abajo asume: `uv` está instalado, tu proyecto tiene `openai-whisper`, `openai`, y `python-dotenv`, y `.env` tiene una clave real para el proveedor que elegiste. + +## Paso 1: Transcribe una nota de voz de muestra localmente + +No necesitas un micrófono o una grabación real para empezar — el repositorio del curso incluye tres clips de muestra de notas de voz cortos en [`examples/voice-to-task-agent/sample_audio/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent/sample_audio). Toma uno (o graba el tuyo con cualquier app de notas de voz de teléfono/portátil y cópialo en tu proyecto — `.wav` y `.mp3` funcionan ambos). + +Crea `voice_to_tasks.py`: + +```python +# voice_to_tasks.py +import sys + +import whisper + +WHISPER_MODEL_SIZE = "base" # tiny / base / small / medium / large -- see the tip below + +_whisper_model = None # loaded lazily so importing this module doesn't load it + + +def get_whisper_model(): + global _whisper_model + if _whisper_model is None: + print(f"Loading Whisper '{WHISPER_MODEL_SIZE}' model...") + _whisper_model = whisper.load_model(WHISPER_MODEL_SIZE) + return _whisper_model + + +def transcribe(audio_path: str) -> str: + """Transcribes an audio file to plain text, entirely locally.""" + model = get_whisper_model() + result = model.transcribe(audio_path) + return result["text"].strip() + + +if __name__ == "__main__": + audio_path = sys.argv[1] if len(sys.argv) > 1 else "sample_audio/memo_1_work_followups.wav" + print(transcribe(audio_path)) +``` + +```bash +uv run python voice_to_tasks.py sample_audio/memo_1_work_followups.wav +``` + +`whisper.load_model("base")` carga una red neuronal entrenada en una gran cantidad de datos de habla multilingüe; `model.transcribe(audio_path)` lo ejecuta en tu archivo de audio y devuelve un dict cuya clave `"text"` es la transcripción completa — Whisper maneja la decodificación de audio en sí (vía `ffmpeg` bajo el capó) y funciona en `.wav`, `.mp3`, y la mayoría de los otros formatos comunes sin que tengas que convertir nada a mano primero. + +:::tip[El tamaño del modelo es una compensación velocidad/precisión] +Whisper viene en cinco tamaños — `tiny`, `base`, `small`, `medium`, `large` — cada uno más preciso y más lento que el anterior. `"base"` es un valor por defecto razonable en un CPU de portátil para habla inglesa corta y clara como los clips de muestra; audio ruidoso, acentos que el modelo maneja peor, o habla no inglesa a menudo se benefician de `"small"` o `"medium"`, al costo de un tiempo de transcripción notablemente más largo. Este es exactamente el tipo de compensación que vale la pena probar con una GPU — ver "Dónde ejecutar esto" arriba para saber por qué Colab es un buen ajuste aquí específicamente. +::: + +**✅ Lista de verificación** + + +`uv run python voice_to_tasks.py sample_audio/memo_1_work_followups.wav` imprime una transcripción real, no un traceback. +El texto impreso coincide aproximadamente con lo que la nota de muestra realmente dice — Whisper no será perfecto, pero debería ser claramente reconocible. +Ejecutarlo de nuevo es notablemente más rápido que la primera vez (los pesos del modelo ahora están cacheados localmente, no re-descargados). + + +**🤔 Pregunta(s) socrática(s)** + +- `transcribe()` nunca envía tu audio a ningún lugar por la red. ¿Qué significa eso para usar esto en una nota de voz genuinamente privada, comparado con una API de transcripción alojada en la nube? +- Si ejecutaras esto en una nota con música de fondo sonando, o dos personas hablando a la vez, ¿qué esperarías que pasara con la calidad de la transcripción? Pruébalo en tu propia grabación si tienes una que encaje. + +## Paso 2: Extrae elementos de acción estructurados con un LLM gratuito + +Una transcripción es solo un muro de texto — útil, pero aún no una lista de tareas. Este paso le entrega la transcripción a un LLM de nivel gratuito con un prompt pidiéndole que la lea y devuelva datos estructurados reales: una entrada por elemento de acción, cada una con una descripción de tarea y, donde la transcripción los implica, una fecha límite y una prioridad. + +Añade la llamada al LLM a `voice_to_tasks.py`: + +```python +# voice_to_tasks.py (additions) +import json +import os + +from dotenv import load_dotenv +from openai import OpenAI + +load_dotenv() + +# All six free-tier providers from the table above happen to expose an +# OpenAI-compatible chat completions endpoint, so one client class covers +# all of them -- only base_url and model change. +PROVIDERS = { + "github": {"env": "GITHUB_TOKEN", "base_url": "https://models.github.ai/inference", "model": "gpt-4o-mini"}, + "gemini": {"env": "GOOGLE_API_KEY", "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/", "model": "gemini-3.5-flash"}, + "groq": {"env": "GROQ_API_KEY", "base_url": "https://api.groq.com/openai/v1", "model": "llama-3.3-70b-versatile"}, + "mistral": {"env": "MISTRAL_API_KEY", "base_url": "https://api.mistral.ai/v1", "model": "mistral-small-latest"}, + "cerebras": {"env": "CEREBRAS_API_KEY", "base_url": "https://api.cerebras.ai/v1", "model": "llama-3.3-70b"}, + "openrouter": {"env": "OPENROUTER_API_KEY", "base_url": "https://openrouter.ai/api/v1", "model": "meta-llama/llama-3.3-70b-instruct:free"}, +} + +EXTRACTION_PROMPT = """You extract action items from a voice memo transcript. + +Return a JSON object shaped exactly like this, with no other text before or +after it, and no markdown code fences: + +{{"tasks": [{{"task": "...", "due_date": "...", "priority": "..."}}]}} + +Rules: +- "task" is a short, clear action (e.g. "Email the client the revised + proposal"), not a raw quote from the transcript. +- "due_date" is null if the transcript doesn't mention one -- do not invent + a specific date that was never said. +- "priority" is "high", "medium", or "low" only if the transcript implies + one; otherwise null. +- If there are no action items at all, return {{"tasks": []}}. + +Transcript: +\"\"\" +{transcript} +\"\"\" +""" + + +def extract_action_items(transcript: str, provider: str | None = None) -> list[dict]: + provider = provider or os.environ.get("LLM_PROVIDER", "github") + config = PROVIDERS[provider] + client = OpenAI(api_key=os.environ[config["env"]], base_url=config["base_url"]) + + response = client.chat.completions.create( + model=config["model"], + messages=[{"role": "user", "content": EXTRACTION_PROMPT.format(transcript=transcript)}], + ) + return json.loads(response.choices[0].message.content)["tasks"] +``` + +```bash +uv run python -c " +from voice_to_tasks import transcribe, extract_action_items +transcript = transcribe('sample_audio/memo_1_work_followups.wav') +print(extract_action_items(transcript)) +" +``` + +El prompt es el que hace el trabajo real aquí: le dice al modelo exactamente qué forma devolver (un objeto JSON con una lista `"tasks"`, no prosa de forma libre), y da reglas explícitas para las partes difíciles — no inventes una fecha límite que nunca se dijo, no adivines una prioridad que no está realmente implicada. Esta es la misma idea que el prompt del [proyecto RAG](/docs/projects/rag-notes) diciéndole al modelo responder *solo* del contexto recuperado: una instrucción clara y específica estrecha lo que el modelo hace, en lugar de esperar que infiera la forma correcta por su cuenta. + +`json.loads(...)["tasks"]` asume que el modelo siguió la instrucción y devolvió JSON limpio — los modelos de nivel gratuito ocasionalmente no lo hacen (una oración suelta antes del JSON, un fence de markdown alrededor a pesar de que se le dijo que no). La versión más completa en [`examples/voice-to-task-agent/voice_to_tasks.py`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent) elimina un fence de código si aparece y lanza un error claro en lugar de un traceback confuso si el JSON aún no se puede parsear — vale la pena copiarla si planeas ejecutarlo en más de un par de notas. + +:::tip[¿Usando un proveedor diferente?] +Todo lo de arriba ya funciona para los seis proveedores de la tabla — solo configura `LLM_PROVIDER` en tu `.env` (o pasa un nombre de proveedor directamente a `extract_action_items`). Esto funciona porque GitHub Models, Gemini, Groq, Mistral, Cerebras, y OpenRouter todos exponen un endpoint compatible con OpenAI; a diferencia del [proyecto AI Agent](/docs/projects/ai-agent), no necesitas una biblioteca de cliente diferente por proveedor aquí, ya que este script no usa LangChain. +::: + +**✅ Lista de verificación** + + +`extract_action_items(transcript)` devuelve una lista de dicts de Python, no un error. +Cada dict tiene las claves `"task"`, `"due_date"`, y `"priority"` — incluso cuando un valor es `None`. +Ejecutarlo en `memo_1_work_followups.wav` encuentra aproximadamente tres tareas separadas, coincidiendo con los tres seguimientos mencionados en esa nota. + + +**🤔 Pregunta(s) socrática(s)** + +- El prompt dice explícitamente "no inventes una fecha específica que nunca se dijo." ¿Qué esperarías que pasara si quitaras esa instrucción y la transcripción dijera "en algún momento de la próxima semana"? Pruébalo — ¿añade el modelo una fecha de calendario real de todos modos? +- Si la transcripción menciona la misma tarea dos veces, formulada de manera ligeramente diferente cada vez (la gente hace esto cuando piensa en voz alta), ¿esperarías una tarea en la salida o dos? ¿Qué sugiere tu respuesta sobre una limitación de pedirle a un modelo que haga esto en una sola pasada, sin paso de deduplicación propio? + +## Paso 3: Ejecútalo de principio a fin y guarda una lista de tareas + +Junta las dos piezas en un script que transcribe, extrae, imprime una lista legible, y la guarda como JSON: + +```python +# voice_to_tasks.py (additions) +def print_tasks(tasks: list[dict]) -> None: + if not tasks: + print("No action items found in this memo.") + return + markers = {"high": "\U0001f534", "medium": "\U0001f7e1", "low": "\U0001f7e2"} + for item in tasks: + marker = markers.get((item.get("priority") or "").lower(), "⚪") + due = f" (due: {item['due_date']})" if item.get("due_date") else "" + print(f"{marker} {item['task']}{due}") + + +def main() -> None: + audio_path = sys.argv[1] if len(sys.argv) > 1 else "sample_audio/memo_1_work_followups.wav" + + print(f"Transcribing {audio_path} ...") + transcript = transcribe(audio_path) + print("\n--- Transcript ---") + print(transcript) + + print("\nExtracting action items...") + tasks = extract_action_items(transcript) + + print("\n--- Action items ---") + print_tasks(tasks) + + with open("tasks.json", "w", encoding="utf-8") as f: + json.dump(tasks, f, indent=2, ensure_ascii=False) + print(f"\nSaved {len(tasks)} task(s) to tasks.json") + + +if __name__ == "__main__": + main() +``` + +```bash +uv run python voice_to_tasks.py sample_audio/memo_3_project_planning.mp3 +``` + +Prueba los tres clips de muestra, y — si tienes forma de grabar uno — tu propia nota de voz también. Una lista corta de compras, un conjunto de seguimientos de reunión, o una lista de tareas del hogar son todas buenas pruebas: cualquier cosa con un puñado de elementos de acción distintos de longitud de oración, hablados como realmente te hablarías a ti mismo, no una lista formalmente estructurada. + +**✅ Lista de verificación** + + +`uv run python voice_to_tasks.py` (con cualquiera de los tres clips de muestra) imprime una transcripción, luego una lista de tareas con marcado, luego una línea "Saved N task(s)". +Un archivo `tasks.json` ahora existe en tu carpeta de proyecto, y su contenido coincide con lo que se imprimió. +Ejecutarlo en una nota sin elementos de acción reales (prueba solo describir tu día) imprime "No action items found" en lugar de inventar unos falsos. + + +**🤔 Pregunta(s) socrática(s)** + +- `tasks.json` se sobreescribe a sí mismo en cada ejecución, sin combinar una lista vieja y una nueva. ¿Qué necesitarías añadir para hacer esto una lista de tareas continua genuinamente útil a través de múltiples notas, grabadas en días diferentes? +- Este pipeline tiene dos puntos de falla que se comportan muy diferente: Whisper escuchando mal una palabra, y el LLM leyendo mal una oración correctamente transcrita. Si una tarea sale mal, ¿cómo distinguirías cuál de las dos etapas realmente la causó? + +## ⚠️ Errores comunes + +- **Confundir Whisper de código abierto con la API de Whisper de pago.** `openai-whisper` (este proyecto) corre completamente en tu propia máquina, gratis, sin clave de API — no es lo mismo que `client.audio.transcriptions.create(...)`, el endpoint de transcripción *alojado* y de pago de OpenAI. Ambos se llaman "Whisper" y ambos vienen de OpenAI, que es exactamente por qué vale la pena ser explícito sobre cuál está usando un código dado. +- **Una primera ejecución muy larga, confundida con un cuelgue.** La primera llamada a `whisper.load_model(...)` descarga los pesos del modelo (ver el tip de Configuración) — en una conexión lenta esto puede tardar un buen rato sin barra de progreso en versiones más antiguas. Déjala terminar una vez; cada ejecución después es rápida. +- **La respuesta JSON del LLM no es JSON válido.** Los modelos de nivel gratuito ocasionalmente envuelven su respuesta en un fence de código markdown, o añaden una oración suelta, a pesar de una instrucción explícita de no hacerlo. Trata el fallo de `json.loads(...)` aquí como una ocurrencia esperada y ocasional — no una señal de que tu prompt está fundamentalmente roto — y mira el `_parse_tasks_response` del ejemplo más completo para una solución de eliminación de fence. +- **Límites de tasa en el nivel gratuito del LLM.** La transcripción (Paso 1) es local e ilimitada; solo la llamada de extracción del Paso 2 cuenta contra la cuota de nivel gratuito de tu proveedor. Un error 429 ahí es el proveedor diciéndote que bajes la velocidad, no un bug — ver el [proyecto AI Agent](/docs/projects/ai-agent#manejar-límites-de-tasa) para el mismo patrón y un enfoque de reintento que puedes copiar. + +## Lo que acabas de construir + +Un pipeline pequeño pero completo que conecta dos tipos genuinamente diferentes de modelo de IA: un modelo de voz a texto local, gratuito y de pesos abiertos haciendo la escucha, y un modelo de lenguaje alojado de nivel gratuito haciendo la lectura-y-estructuración. Nada aquí fue falso — intercambia una grabación real más larga y desordenada, y los mismos dos pasos (transcribe, luego extrae) siguen siendo todo el pipeline. Esto es también un pequeño ejemplo concreto de un patrón más amplio que vale la pena notar: no toda tarea de IA necesita un modelo alojado gigante. Whisper es lo suficientemente pequeño para correr localmente gratis; solo la parte del trabajo que realmente se beneficia del razonamiento de un modelo de lenguaje grande — convertir lenguaje hablado suelto en datos estructurados limpios — recurre a uno. + +:::tip[Ejecuta una versión más completa sin configuración local para el código] +[`examples/voice-to-task-agent/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent) en el repositorio del curso es una versión un poco más completa del código de arriba — el mismo pipeline de dos pasos, más la solución de eliminación de fence mencionada arriba y mensajes de error más claros. Clónalo, o abre todo el repositorio en un [GitHub Codespace](https://codespaces.new/abderrahim-lectures/python-data-analysis-course), y ejecútalo contra cualquiera de los tres clips de muestra en `sample_audio/`. +::: + +## A dónde ir desde aquí + +- Prueba un tamaño de modelo Whisper más grande (`"small"` o `"medium"`) en una grabación más larga y desordenada — ruido de fondo, varios hablantes, o una nota no inglesa — y mira dónde `"base"` empieza a quedarse corto. Esta es una gran excusa para probar el camino de GPU de Colab de "Dónde ejecutar esto" arriba. +- Agrupa las tareas extraídas por prioridad, o ordénalas por cómo el modelo reporta las fechas límite, en lugar de imprimirlas en orden de transcripción. +- Haz `tasks.json` acumulativo: carga el archivo existente (si hay), agrega las tareas recién extraídas en lugar de sobreescribir, y deduplica cualquier cosa que parezca la misma tarea dicha dos veces. +- Conecta esto a algo que realmente consuma la lista de tareas — agregando a la API de una app de tareas real, un calendario, o incluso solo un archivo Markdown de lista de verificación en ejecución — en lugar de un archivo JSON que nada más lee todavía. + +## Comparte tu proyecto con la clase + +¿Construiste algo de lo que estás orgulloso? [`examples/student-projects/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/student-projects) es una galería de proyectos que otros estudiantes han enviado — y su README tiene un recorrido completo y amigable para principiantes sobre cómo agregar el tuyo vía un **pull request**, incluso si nunca has usado git antes: hacer fork del repositorio, crear una rama, confirmar tus archivos, y abrir el PR, un paso a la vez. No se asume experiencia previa con git. + +Bienvenido a escribir Python fuera del navegador. 🎓 + + diff --git a/i18n/fr/code.json b/i18n/fr/code.json index 6ee7769..2e51580 100644 --- a/i18n/fr/code.json +++ b/i18n/fr/code.json @@ -941,5 +941,13 @@ "homepage.projects.triviaBot.summary": { "message": "Organise des manches de trivia dans un serveur Discord avec discord.py : un classement persistant, et des questions générées à la volée sur n'importe quel sujet avec un LLM de niveau gratuit.", "description": "Homepage project card summary" + }, + "homepage.projects.voiceToTaskAgent.title": { + "message": "Construire un Agent de la Voix vers les Tâches", + "description": "Homepage project card title" + }, + "homepage.projects.voiceToTaskAgent.summary": { + "message": "Transcris une note vocale localement et gratuitement avec le modèle open source Whisper d'OpenAI, puis utilise un LLM de niveau gratuit pour la transformer en une liste de tâches structurée.", + "description": "Homepage project card summary" } } diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/projects/index.mdx b/i18n/fr/docusaurus-plugin-content-docs/current/projects/index.mdx index 985bfbc..4596e2d 100644 --- a/i18n/fr/docusaurus-plugin-content-docs/current/projects/index.mdx +++ b/i18n/fr/docusaurus-plugin-content-docs/current/projects/index.mdx @@ -170,6 +170,10 @@ Ils sont optionnels et non notés. Parcourez-les à tout moment — l'introducti title: 'Construire un Bot de Trivia pour Discord', summary: "Construis un bot `discord.py` qui organise des manches de trivia dans un serveur, suit les scores dans un classement persistant, et peut générer des questions inédites sur n'importe quel sujet avec un LLM de niveau gratuit.", + id: 'voice-to-task-agent', + title: 'Construire un Agent de la Voix vers les Tâches', + summary: + "Transcris une note vocale localement et gratuitement avec le modèle open source Whisper d'OpenAI, puis utilise un LLM de niveau gratuit pour la transformer en une liste de tâches structurée.", }, ])} /> diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json b/i18n/fr/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json new file mode 100644 index 0000000..cb4320e --- /dev/null +++ b/i18n/fr/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Agent de la Voix vers les Tâches", + "position": 9 +} diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md b/i18n/fr/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md new file mode 100644 index 0000000..4f50e70 --- /dev/null +++ b/i18n/fr/docusaurus-plugin-content-docs/current/projects/voice-to-task-agent/index.md @@ -0,0 +1,352 @@ +--- +id: voice-to-task-agent +title: "Construire un Agent de la Voix vers les Tâches" +sidebar_label: "Agent de la Voix vers les Tâches" +slug: /projects/voice-to-task-agent +description: "Passe du playground intégré au navigateur au vrai Python : transcris une note vocale localement et gratuitement avec le modèle open source Whisper d'OpenAI, puis utilise un LLM de niveau gratuit pour la transformer en une liste de tâches structurée." +--- + +import ProjectProgressCheckbox from '@site/src/components/ProjectProgressCheckbox'; +import ProjectPublishedDate from '@site/src/components/ProjectPublishedDate'; +import ProjectGreeting from '@site/src/components/ProjectGreeting'; +import {StepChecklist, StepChecklistItem} from '@site/src/components/StepChecklist'; + +# 🌍 Construire un Agent de la Voix vers les Tâches + + + + + +Tout dans le cours jusqu'ici s'est exécuté dans un playground isolé intégré au navigateur — pour que tu puisses commencer à écrire du Python dès le premier jour avec zéro configuration. Ce projet est l'étape de remise des diplômes : installe du vrai Python sur ta propre machine, puis utilise-le pour construire quelque chose d'authentiquement utile — un petit pipeline qui prend une note vocale décousue et la transforme en une courte liste de tâches structurée, sans que tu aies à taper ou à organiser quoi que ce soit à la main. Cela suppose du Python 101 ; rien de l'Analyse de Données n'est requis. + +C'est optionnel et non noté. Voir [Projets du monde réel](/docs/projects) pour la liste complète et croissante. + +## 🎯 Ce que tu vas faire + +1. Transcrire une courte note vocale en texte, entièrement en local et gratuitement, en utilisant le modèle *open source* Whisper d'OpenAI (`openai-whisper`, s'exécutant sur ton propre CPU) — pas l'API Whisper payante. +2. Écrire un prompt qui demande à un LLM de niveau gratuit de lire cette transcription et d'en extraire des éléments d'action structurés : une tâche, une date limite optionnelle, une priorité optionnelle. +3. Exécuter tout le pipeline de bout en bout sur un enregistrement d'exemple fourni (ou le tien), et sauvegarder le résultat sous forme de simple liste de tâches. + +## Où exécuter ceci + +**En local avec `uv`** est le chemin principal et recommandé — la transcription est un travail de CPU (pas besoin de GPU pour un clip court avec un petit modèle Whisper), donc ça tourne confortablement sur un ordinateur portable ordinaire. La configuration ci-dessous explique comment installer `uv`. + +**GitHub Codespaces** fonctionne aussi : ouvre [tout le dépôt du cours dans un Codespace gratuit](https://codespaces.new/abderrahim-lectures/python-data-analysis-course) (Node, Python et `uv` sont déjà installés) et exécute exactement les mêmes commandes `uv` depuis un terminal dans ton onglet de navigateur. C'est un peu plus lent qu'un ordinateur portable moderne pour l'étape de transcription, puisque les machines Codespaces sont CPU uniquement, mais parfaitement utilisable pour les courts clips d'exemple d'ici. + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) +[![Open In Kaggle](https://kaggle.com/static/images/open-in-kaggle.svg)](https://kaggle.com/kernels/welcome?src=https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/abderrahim-lectures/python-data-analysis-course/main?filepath=examples%2Fvoice-to-task-agent%2Fnotebook.ipynb) + +**Google Colab est un très bon ajustement pour celui-ci** — meilleur que pour la plupart des autres projets de cette série. La vitesse de transcription de Whisper évolue beaucoup avec le matériel, et Colab te donne un GPU gratuit qu'un ordinateur portable local CPU uniquement n'a pas : `!pip install openai-whisper` dans une cellule, puis un runtime GPU, et même les tailles de modèle Whisper plus grandes (plus précises, normalement trop lentes pour être envisagées sur un CPU) deviennent pratiques. Si tu veux expérimenter avec la taille du modèle par rapport à la précision (voir le conseil à l'Étape 1), Colab est l'endroit pour le faire. Les badges ci-dessus ouvrent un [`notebook.ipynb`](https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/voice-to-task-agent/notebook.ipynb) prêt à l'emploi qui exécute tout le pipeline avec zéro configuration locale — le même pipeline en deux étapes, le même audio d'exemple, juste dans un notebook hébergé plutôt que dans un terminal. + +## Configuration + +Tout ce qui est nécessaire avant que tu écrives du code de pipeline — installer `uv`, créer le projet, et obtenir une clé API LLM — vit ici, une fois, à l'avance. La construction réelle commence à l'Étape 1, en supposant que tout cela est déjà en place. + +### Installer `uv` + +`uv` est un seul outil qui remplace la chaîne habituelle « installe Python, puis installe pip, puis installe un outil d'environnement virtuel, puis installe les paquets » — il peut installer et gérer les versions de Python lui-même, en plus des dépendances de ton projet. + +**macOS / Linux** (terminal) : + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +**Windows** (PowerShell) : + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +Ferme et rouvre ton terminal, puis confirme que c'est installé : + +```bash +uv --version +``` + +### Configurer le projet + +```bash +uv init voice-to-task-agent +cd voice-to-task-agent +uv add openai-whisper openai python-dotenv +``` + +`openai-whisper` est le modèle open source de parole en texte lui-même — malgré le nom du paquet, cela s'installe et s'exécute *localement*, sans clé API et sans coût par minute ; c'est juste qu'il est publié par OpenAI et partage un nom avec leur API hébergée, payante et séparée. `openai` est le client API simple utilisé à l'Étape 2 pour appeler le fournisseur de LLM de niveau gratuit que tu choisis — plusieurs d'entre eux exposent un endpoint compatible OpenAI, donc une seule bibliothèque client couvre les six. `python-dotenv` te permet de garder ta clé API LLM dans un fichier `.env` local au lieu de l'`export`-er à chaque session. + +:::tip[La première exécution télécharge le modèle] +`openai-whisper` ne fournit pas les poids de son modèle — la première fois que ton code appelle `whisper.load_model(...)` (Étape 1), il télécharge les poids vers `~/.cache/whisper` (environ 140 Mo pour la taille `"base"` utilisée dans ce projet) et les réutilise à chaque exécution suivante. La première transcription semblera lente ; c'est le téléchargement, pas la transcription elle-même. +::: + +### Obtenir une clé API LLM gratuite + +**Choisis le fournisseur que tu préfères** — aucun n'exige une carte de crédit au moment de la rédaction, et ce cours n'en favorise pas un plutôt qu'un autre. L'exemple dans le dépôt du cours ([`examples/voice-to-task-agent/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent)) supporte les six prêts à l'emploi, sélectionnés avec un seul réglage. + +| Fournisseur | Où obtenir une clé | Pourquoi tu pourrais le choisir | +|---|---|---| +| **GitHub Models** *(défaut suggéré)* | [github.com/settings/tokens](https://github.com/settings/tokens) — un jeton d'accès personnel avec le scope `models: read` | Pas d'inscription séparée — tu as déjà un compte GitHub. Des limites de niveau gratuit plus généreuses que celles de Gemini. | +| Gemini | [Google AI Studio](https://aistudio.google.com/) | L'option la plus couramment référencée ; utilisée dans des brouillons précédents de cette page. | +| Groq | [console.groq.com/keys](https://console.groq.com/keys) | Inférence rapide, niveau gratuit généreux, pas de carte. | +| Mistral | [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys) | Un des quotas gratuits permanents les plus généreux. | +| Cerebras | [cloud.cerebras.ai](https://cloud.cerebras.ai/) | Volume de jetons quotidien élevé, pas de carte. | +| OpenRouter | [openrouter.ai/keys](https://openrouter.ai/keys) | Une API, beaucoup de modèles gratuits — bien pour comparer les fournisseurs. | + +Quel que soit ton choix, le processus est le même : + +1. Connecte-toi et génère une clé API sur le site de ce fournisseur. +2. **Ne colle jamais cette clé directement dans le code et ne la committe pas dans un dépôt.** Crée plutôt un fichier `.env` dans ton dossier de projet (ne le committe jamais) : + +```bash +# .env +GITHUB_TOKEN=your-key-here +``` + +Une clé API est un secret, exactement comme un mot de passe — n'importe qui avec elle peut utiliser le quota de ton compte. La traiter comme une variable d'environnement plutôt qu'une chaîne en dur est la pratique standard précisément pour cette raison, et c'est la même habitude construite dans le [projet AI Agent](/docs/projects/ai-agent) si tu as fait celui-là. + +:::tip[Un fichier .env est souvent plus pratique que export] +Au lieu d'`export`-er une clé dans chaque nouvelle session de terminal, un fichier `.env` dans ton dossier de projet, chargé automatiquement avec `python-dotenv`, persiste entre les sessions sans que tu aies à t'en souvenir. Vois le `.env.example` de l'exemple du dépôt pour la liste complète des noms de variables, un par fournisseur. +::: + +Une fois la configuration faite, tout ce qui suit suppose : `uv` est installé, ton projet contient `openai-whisper`, `openai`, et `python-dotenv`, et `.env` contient une vraie clé pour le fournisseur que tu as choisi. + +## Étape 1 : Transcris une note vocale d'exemple localement + +Tu n'as pas besoin d'un microphone ou d'un vrai enregistrement pour commencer — le dépôt du cours fournit trois courts clips d'exemple de notes vocales dans [`examples/voice-to-task-agent/sample_audio/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent/sample_audio). Prends-en un (ou enregistre le tien avec n'importe quelle app de notes vocales de téléphone/ordinateur portable et copie-le dans ton projet — `.wav` et `.mp3` fonctionnent tous les deux). + +Crée `voice_to_tasks.py` : + +```python +# voice_to_tasks.py +import sys + +import whisper + +WHISPER_MODEL_SIZE = "base" # tiny / base / small / medium / large -- see the tip below + +_whisper_model = None # loaded lazily so importing this module doesn't load it + + +def get_whisper_model(): + global _whisper_model + if _whisper_model is None: + print(f"Loading Whisper '{WHISPER_MODEL_SIZE}' model...") + _whisper_model = whisper.load_model(WHISPER_MODEL_SIZE) + return _whisper_model + + +def transcribe(audio_path: str) -> str: + """Transcribes an audio file to plain text, entirely locally.""" + model = get_whisper_model() + result = model.transcribe(audio_path) + return result["text"].strip() + + +if __name__ == "__main__": + audio_path = sys.argv[1] if len(sys.argv) > 1 else "sample_audio/memo_1_work_followups.wav" + print(transcribe(audio_path)) +``` + +```bash +uv run python voice_to_tasks.py sample_audio/memo_1_work_followups.wav +``` + +`whisper.load_model("base")` charge un réseau de neurones entraîné sur une énorme quantité de données de parole multilingue ; `model.transcribe(audio_path)` l'exécute sur ton fichier audio et retourne un dict dont la clé `"text"` est la transcription complète — Whisper gère lui-même le décodage audio (via `ffmpeg` sous le capot) et fonctionne sur `.wav`, `.mp3`, et la plupart des autres formats courants sans que tu aies à convertir quoi que ce soit à la main d'abord. + +:::tip[La taille du modèle est un compromis vitesse/précision] +Whisper est disponible en cinq tailles — `tiny`, `base`, `small`, `medium`, `large` — chacune plus précise et plus lente que la précédente. `"base"` est un défaut raisonnable sur un CPU d'ordinateur portable pour de la parole anglaise courte et claire comme les clips d'exemple ; l'audio bruité, les accents que le modèle gère moins bien, ou la parole non anglaise profitent souvent de `"small"` ou `"medium"`, au prix d'un temps de transcription sensiblement plus long. C'est exactement le genre de compromis qui vaut la peine d'essayer avec un GPU — vois « Où exécuter ceci » ci-dessus pour pourquoi Colab est un bon ajustement ici spécifiquement. +::: + +**✅ Liste de vérification** + + +`uv run python voice_to_tasks.py sample_audio/memo_1_work_followups.wav` affiche une vraie transcription, pas un traceback. +Le texte affiché correspond à peu près à ce que la note d'exemple dit réellement — Whisper ne sera pas parfait, mais il devrait être clairement reconnaissable. +Le relancer est sensiblement plus rapide que la première exécution (les poids du modèle sont maintenant mis en cache localement, pas re-téléchargés). + + +**🤔 Question(s) socratique(s)** + +- `transcribe()` n'envoie jamais ton audio nulle part sur le réseau. Qu'est-ce que cela signifie pour utiliser ça sur une note vocale véritablement privée, par rapport à une API de transcription hébergée dans le cloud ? +- Si tu exécutais ça sur une note avec de la musique de fond, ou deux personnes parlant en même temps, à quoi t'attendrais-tu qu'il arrive à la qualité de la transcription ? Essaie sur ton propre enregistrement si tu en as un qui correspond. + +## Étape 2 : Extrais des éléments d'action structurés avec un LLM gratuit + +Une transcription n'est qu'un mur de texte — utile, mais pas encore une liste de tâches. Cette étape remet la transcription à un LLM de niveau gratuit avec un prompt lui demandant de la lire et de retourner de vraies données structurées : une entrée par élément d'action, chacune avec une description de tâche et, quand la transcription les implique, une date limite et une priorité. + +Ajoute l'appel LLM à `voice_to_tasks.py` : + +```python +# voice_to_tasks.py (additions) +import json +import os + +from dotenv import load_dotenv +from openai import OpenAI + +load_dotenv() + +# All six free-tier providers from the table above happen to expose an +# OpenAI-compatible chat completions endpoint, so one client class covers +# all of them -- only base_url and model change. +PROVIDERS = { + "github": {"env": "GITHUB_TOKEN", "base_url": "https://models.github.ai/inference", "model": "gpt-4o-mini"}, + "gemini": {"env": "GOOGLE_API_KEY", "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/", "model": "gemini-3.5-flash"}, + "groq": {"env": "GROQ_API_KEY", "base_url": "https://api.groq.com/openai/v1", "model": "llama-3.3-70b-versatile"}, + "mistral": {"env": "MISTRAL_API_KEY", "base_url": "https://api.mistral.ai/v1", "model": "mistral-small-latest"}, + "cerebras": {"env": "CEREBRAS_API_KEY", "base_url": "https://api.cerebras.ai/v1", "model": "llama-3.3-70b"}, + "openrouter": {"env": "OPENROUTER_API_KEY", "base_url": "https://openrouter.ai/api/v1", "model": "meta-llama/llama-3.3-70b-instruct:free"}, +} + +EXTRACTION_PROMPT = """You extract action items from a voice memo transcript. + +Return a JSON object shaped exactly like this, with no other text before or +after it, and no markdown code fences: + +{{"tasks": [{{"task": "...", "due_date": "...", "priority": "..."}}]}} + +Rules: +- "task" is a short, clear action (e.g. "Email the client the revised + proposal"), not a raw quote from the transcript. +- "due_date" is null if the transcript doesn't mention one -- do not invent + a specific date that was never said. +- "priority" is "high", "medium", or "low" only if the transcript implies + one; otherwise null. +- If there are no action items at all, return {{"tasks": []}}. + +Transcript: +\"\"\" +{transcript} +\"\"\" +""" + + +def extract_action_items(transcript: str, provider: str | None = None) -> list[dict]: + provider = provider or os.environ.get("LLM_PROVIDER", "github") + config = PROVIDERS[provider] + client = OpenAI(api_key=os.environ[config["env"]], base_url=config["base_url"]) + + response = client.chat.completions.create( + model=config["model"], + messages=[{"role": "user", "content": EXTRACTION_PROMPT.format(transcript=transcript)}], + ) + return json.loads(response.choices[0].message.content)["tasks"] +``` + +```bash +uv run python -c " +from voice_to_tasks import transcribe, extract_action_items +transcript = transcribe('sample_audio/memo_1_work_followups.wav') +print(extract_action_items(transcript)) +" +``` + +Le prompt fait le vrai travail ici : il dit au modèle exactement quelle forme retourner (un objet JSON avec une liste `"tasks"`, pas une prose libre), et donne des règles explicites pour les parties délicates — n'invente pas une date limite qui n'a jamais été dite, ne devine pas une priorité qui n'est pas réellement impliquée. C'est la même idée que le prompt du [projet RAG](/docs/projects/rag-notes) disant au modèle de répondre *uniquement* à partir du contexte récupéré : une instruction claire et spécifique rétrécit ce que fait le modèle, au lieu d'espérer qu'il déduise la bonne forme tout seul. + +`json.loads(...)["tasks"]` suppose que le modèle a réellement suivi l'instruction et retourné du JSON propre — les modèles de niveau gratuit ne le font parfois pas (une phrase parasite avant le JSON, un code fence markdown autour malgré la consigne de ne pas le faire). La version plus complète dans [`examples/voice-to-task-agent/voice_to_tasks.py`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent) retire un code fence s'il apparaît et lève une erreur claire au lieu d'un traceback déroutant si le JSON refuse toujours de s'analyser — à copier si tu prévois de l'exécuter sur plus de deux ou trois notes. + +:::tip[Tu utilises un fournisseur différent ?] +Tout ce qui précède fonctionne déjà pour les six fournisseurs du tableau — il suffit de définir `LLM_PROVIDER` dans ton `.env` (ou de passer un nom de fournisseur directement à `extract_action_items`). Cela fonctionne parce que GitHub Models, Gemini, Groq, Mistral, Cerebras, et OpenRouter exposent tous un endpoint compatible OpenAI ; contrairement au [projet AI Agent](/docs/projects/ai-agent), tu n'as pas besoin d'une bibliothèque client différente par fournisseur ici, puisque ce script n'utilise pas LangChain. +::: + +**✅ Liste de vérification** + + +`extract_action_items(transcript)` retourne une liste Python de dicts, pas une erreur. +Chaque dict a les clés `"task"`, `"due_date"`, et `"priority"` — même quand une valeur est `None`. +L'exécuter sur `memo_1_work_followups.wav` trouve à peu près trois tâches séparées, correspondant aux trois suivis réellement mentionnés dans cette note. + + +**🤔 Question(s) socratique(s)** + +- Le prompt dit explicitement « n'invente pas une date spécifique qui n'a jamais été dite. » À quoi t'attendrais-tu qu'il arrive si tu retirais cette instruction et que la transcription disait « quelque temps la semaine prochaine » ? Essaie — le modèle ajoute-t-il une vraie date de calendrier quand même ? +- Si la transcription mentionne la même tâche deux fois, formulée légèrement différemment à chaque fois (les gens font ça quand ils pensent à voix haute), t'attendrais-tu à une tâche dans la sortie ou deux ? Qu'est-ce que ta réponse suggère sur une limitation de demander à un modèle de faire ça en un seul passage, sans étape de déduplication propre ? + +## Étape 3 : Exécute-le de bout en bout et sauvegarde une liste de tâches + +Assemble les deux morceaux en un seul script qui transcrit, extrait, affiche une liste lisible, et la sauvegarde en JSON : + +```python +# voice_to_tasks.py (additions) +def print_tasks(tasks: list[dict]) -> None: + if not tasks: + print("No action items found in this memo.") + return + markers = {"high": "\U0001f534", "medium": "\U0001f7e1", "low": "\U0001f7e2"} + for item in tasks: + marker = markers.get((item.get("priority") or "").lower(), "⚪") + due = f" (due: {item['due_date']})" if item.get("due_date") else "" + print(f"{marker} {item['task']}{due}") + + +def main() -> None: + audio_path = sys.argv[1] if len(sys.argv) > 1 else "sample_audio/memo_1_work_followups.wav" + + print(f"Transcribing {audio_path} ...") + transcript = transcribe(audio_path) + print("\n--- Transcript ---") + print(transcript) + + print("\nExtracting action items...") + tasks = extract_action_items(transcript) + + print("\n--- Action items ---") + print_tasks(tasks) + + with open("tasks.json", "w", encoding="utf-8") as f: + json.dump(tasks, f, indent=2, ensure_ascii=False) + print(f"\nSaved {len(tasks)} task(s) to tasks.json") + + +if __name__ == "__main__": + main() +``` + +```bash +uv run python voice_to_tasks.py sample_audio/memo_3_project_planning.mp3 +``` + +Essaie les trois clips d'exemple, et — si tu as un moyen d'en enregistrer un — ta propre note vocale aussi. Une courte liste de courses, un ensemble de suivis de réunion, ou une liste de corvées sont tous de bons tests : n'importe quoi avec une poignée d'éléments d'action distincts de longueur de phrase, parlés comme tu te parlerais réellement, pas une liste formellement structurée. + +**✅ Liste de vérification** + + +`uv run python voice_to_tasks.py` (avec n'importe lequel des trois clips d'exemple) affiche une transcription, puis une liste de tâches marquée, puis une ligne « Saved N task(s) ». +Un fichier `tasks.json` existe maintenant dans ton dossier de projet, et son contenu correspond à ce qui a été affiché. +L'exécuter sur une note sans véritables éléments d'action (essaie juste de décrire ta journée) affiche « No action items found » plutôt que d'en inventer des faux. + + +**🤔 Question(s) socratique(s)** + +- `tasks.json` s'écrase lui-même à chaque exécution, sans fusion d'une ancienne liste et d'une nouvelle. Que devrais-tu ajouter pour en faire une liste de tâches courante véritablement utile sur plusieurs notes, enregistrées des jours différents ? +- Ce pipeline a deux points de défaillance qui se comportent très différemment : Whisper qui entend mal un mot, et le LLM qui lit mal une phrase correctement transcrite. Si une tâche sort incorrecte, comment saurais-tu laquelle des deux étapes en a réellement été la cause ? + +## ⚠️ Pièges courants + +- **Confondre Whisper open source avec l'API Whisper payante.** `openai-whisper` (ce projet) s'exécute entièrement sur ta propre machine, gratuitement, sans clé API — ce n'est pas la même chose que `client.audio.transcriptions.create(...)`, l'endpoint de transcription *hébergé* et payant d'OpenAI. Les deux s'appellent « Whisper » et les deux viennent d'OpenAI, ce qui est exactement pourquoi il vaut la peine d'être explicite sur lequel un code donné utilise. +- **Une toute première exécution très longue, prise pour un blocage.** Le premier appel à `whisper.load_model(...)` télécharge les poids du modèle (vois le conseil de Configuration) — sur une connexion lente, ça peut prendre un moment sans barre de progression dans les versions plus anciennes. Laisse-le finir une fois ; chaque exécution après est rapide. +- **La réponse JSON du LLM n'est pas tout à fait du JSON valide.** Les modèles de niveau gratuit enveloppent parfois leur réponse dans un code fence markdown, ou ajoutent une phrase parasite, malgré une instruction explicite de ne pas le faire. Traite l'échec de `json.loads(...)` ici comme un événement attendu et occasionnel — pas un signe que ton prompt est fondamentalement cassé — et vois le `_parse_tasks_response` de l'exemple plus complet pour un correctif de suppression de fence. +- **Les limites de débit sur le niveau gratuit du LLM.** La transcription (Étape 1) est locale et illimitée ; seul l'appel d'extraction de l'Étape 2 compte contre le quota de niveau gratuit de ton fournisseur. Une erreur 429 là-bas, c'est le fournisseur qui te dit de ralentir, pas un bug — vois le [projet AI Agent](/docs/projects/ai-agent#gérer-les-limites-de-débit) pour le même pattern et une approche de nouvelle tentative que tu peux copier. + +## Ce que tu viens de construire + +Un pipeline petit mais complet reliant deux types de modèles d'IA véritablement différents : un modèle de parole en texte local, gratuit et à poids ouverts faisant l'écoute, et un modèle de langage hébergé de niveau gratuit faisant la lecture-et-structuration. Rien ici n'a été truqué — remplace par un vrai enregistrement plus long et plus brouillon, et les mêmes deux étapes (transcris, puis extrais) restent tout le pipeline. C'est aussi un petit exemple concret d'un pattern plus large qui vaut la peine d'être noté : toute tâche d'IA n'a pas besoin d'un énorme modèle hébergé. Whisper est assez petit pour s'exécuter localement gratuitement ; seule la partie du travail qui bénéficie réellement du raisonnement d'un grand modèle de langage — transformer un langage parlé lâche en données structurées propres — fait appel à l'un d'eux. + +:::tip[Exécute une version plus complète sans aucune configuration locale pour le code] +[`examples/voice-to-task-agent/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/voice-to-task-agent) dans le dépôt du cours est une version un peu plus complète du code ci-dessus — le même pipeline en deux étapes, plus le correctif de suppression de fence mentionné ci-dessus et des messages d'erreur plus clairs. Clone-le, ou ouvre tout le dépôt dans un [GitHub Codespace](https://codespaces.new/abderrahim-lectures/python-data-analysis-course), et exécute-le contre n'importe lequel des trois clips d'exemple dans `sample_audio/`. +::: + +## Où aller à partir d'ici + +- Essaie une taille de modèle Whisper plus grande (`"small"` ou `"medium"`) sur un enregistrement plus long et plus brouillon — bruit de fond, plusieurs locuteurs, ou une note non anglaise — et vois où `"base"` commence à montrer ses limites. C'est une excellente excuse pour essayer le chemin GPU de Colab depuis « Où exécuter ceci » ci-dessus. +- Groupe les tâches extraites par priorité, ou trie-les selon la façon dont le modèle rapporte les dates limites, au lieu de les afficher dans l'ordre de la transcription. +- Rends `tasks.json` cumulatif : charge le fichier existant (s'il y en a un), ajoute les tâches nouvellement extraites au lieu de les écraser, et déduplique tout ce qui ressemble à la même tâche dite deux fois. +- Branche ceci sur quelque chose qui consomme réellement la liste de tâches — ajouter à l'API d'une vraie app de tâches, un calendrier, ou même juste un fichier Markdown de checklist courant — au lieu d'un fichier JSON que rien d'autre ne lit encore. + +## Partage ton projet avec la classe + +Tu as construit quelque chose dont tu es fier ? [`examples/student-projects/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/student-projects) est une galerie de projets soumis par d'autres élèves — et son README a un tutoriel complet et adapté aux débutants pour ajouter le tien via une **pull request**, même si tu n'as jamais utilisé git avant : forker le dépôt, créer une branche, commiter tes fichiers, et ouvrir la PR, une étape à la fois. Aucune expérience préalable avec git n'est supposée. + +Bienvenue à l'écriture de Python hors du navigateur. 🎓 + +