From cb22804a1f5e576c4b65907bf58eb16625fc2f09 Mon Sep 17 00:00:00 2001 From: wmj2024 Date: Sun, 28 Jun 2026 02:04:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Daily=20Fiqh=20Module=201=20=E2=80=94?= =?UTF-8?q?=20Purification=20&=20Prayer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5 micro-lessons (200-400 words each) - Standardized YAML frontmatter with audio metadata - Quiz with 5 questions + explanations - TTS pipeline script (Azure/Piper/ElevenLabs) - Lesson template for future modules - Calibrated read times and audio durations --- README.md | 78 ++++++ courses/daily-fiqh-beginner/manifest.json | 53 ++++ .../lesson-01-intention-of-wudu.md | 46 +++ .../lesson-02-how-to-perform-wudu.md | 50 ++++ .../lesson-03-when-wudu-breaks.md | 55 ++++ .../lesson-04-the-adhan.md | 57 ++++ .../lesson-05-essentials-of-salah.md | 58 ++++ .../manifest.json | 82 ++++++ .../module-01-purification-prayer/quiz.json | 69 +++++ scripts/generate-audio.py | 264 ++++++++++++++++++ templates/lesson-template.md | 40 +++ 11 files changed, 852 insertions(+) create mode 100644 README.md create mode 100644 courses/daily-fiqh-beginner/manifest.json create mode 100644 courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-01-intention-of-wudu.md create mode 100644 courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-02-how-to-perform-wudu.md create mode 100644 courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-03-when-wudu-breaks.md create mode 100644 courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-04-the-adhan.md create mode 100644 courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-05-essentials-of-salah.md create mode 100644 courses/daily-fiqh-beginner/module-01-purification-prayer/manifest.json create mode 100644 courses/daily-fiqh-beginner/module-01-purification-prayer/quiz.json create mode 100644 scripts/generate-audio.py create mode 100644 templates/lesson-template.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea2834f --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# FalahMobile Content Repository + +Micro-learning courses for the FalahMobile Islamic lifestyle app. + +## Repository Structure + +``` +falahmobile-content/ +├── courses/ # All courses +│ ├── daily-fiqh-beginner/ +│ ├── 40-hadith-nawawi/ +│ └── tafsir-bites/ +├── assets/ # Shared images, fonts, icons +├── scripts/ # Build and generation scripts +│ ├── generate-audio.py # TTS pipeline +│ └── validate-content.mjs # Content quality checks +├── templates/ # Lesson and course templates +└── README.md +``` + +## Content Format + +Every lesson is a Markdown file with YAML frontmatter: + +```yaml +--- +id: "course-module-lesson" +title: "Lesson Title" +course: "Course Name" +module: "Module Name" +order: 1 +read_time: "3 min" +difficulty: "beginner" +tags: ["fiqh", "prayer"] +source_refs: ["Quran 5:6", "Sahih Muslim 271"] +audio_ready: true +--- +``` + +## Audio Pipeline + +### Free TTS Options (Female, Calm, Human-like) + +| Service | Voice | Quality | Free Tier | Setup | +|---------|-------|---------|-----------|-------| +| **Azure Speech** | `en-US-AriaNeural` | Excellent | 500K chars/mo | Azure account | +| **Piper TTS** | `amy` (local) | Good | Unlimited | Download + run locally | +| **Google Cloud** | `en-US-Neural2-F` | Good | 1M chars/mo | GCP account | +| **ElevenLabs** | `Rachel` | Excellent | 10K chars/mo | API key | + +### Generate Audio + +```bash +# Using Azure (recommended free tier) +python scripts/generate-audio.py --course daily-fiqh-beginner --engine azure + +# Using Piper (completely free, local) +python scripts/generate-audio.py --course daily-fiqh-beginner --engine piper +``` + +## Gitea Integration + +Push content to your Gitea instance: + +```bash +git remote add gitea https://your-gitea.com/user/falahmobile-content.git +git push gitea main +``` + +The FalahMobile app fetches content via the Gitea API or raw file URLs. + +## Adding a New Course + +1. Create `courses//manifest.json` +2. Create module directories with lesson `.md` files +3. Run `python scripts/validate-content.mjs` +4. Generate audio with `python scripts/generate-audio.py` +5. Commit and push to Gitea diff --git a/courses/daily-fiqh-beginner/manifest.json b/courses/daily-fiqh-beginner/manifest.json new file mode 100644 index 0000000..5e9201a --- /dev/null +++ b/courses/daily-fiqh-beginner/manifest.json @@ -0,0 +1,53 @@ +{ + "id": "daily-fiqh-beginner", + "title": "Daily Fiqh for Beginners", + "subtitle": "Essential rulings for everyday Muslim life", + "description": "A practical guide to the Islamic rulings you encounter daily — from waking up to going to bed. Short lessons you can listen to during your commute or while making breakfast.", + "author": "FalahMobile Learning", + "version": "1.0.0", + "language": "en", + "difficulty": "beginner", + "target_audience": "New Muslims and Muslims seeking practical daily guidance", + "total_modules": 5, + "estimated_duration": "45 minutes", + "tags": ["fiqh", "daily-life", "practical", "beginner"], + "school": "general-majority", + "prerequisites": "none", + "modules": [ + { + "id": "module-01", + "title": "Purification & Prayer", + "description": "The foundation of daily worship — wudu, ghusl, and the essentials of salah", + "lesson_count": 5, + "estimated_duration": "12 minutes" + }, + { + "id": "module-02", + "title": "Food & Drink", + "description": "What you can eat, what to avoid, and how to eat with mindfulness", + "lesson_count": 5, + "estimated_duration": "10 minutes" + }, + { + "id": "module-03", + "title": "Dress & Appearance", + "description": "Clothing rulings, modesty, and personal grooming for men and women", + "lesson_count": 4, + "estimated_duration": "8 minutes" + }, + { + "id": "module-04", + "title": "Wealth & Transactions", + "description": "Earning, spending, and avoiding riba in modern financial life", + "lesson_count": 5, + "estimated_duration": "10 minutes" + }, + { + "id": "module-05", + "title": "Social Interactions", + "description": "Greetings, visiting, gifts, and daily manners with family and neighbors", + "lesson_count": 4, + "estimated_duration": "8 minutes" + } + ] +} \ No newline at end of file diff --git a/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-01-intention-of-wudu.md b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-01-intention-of-wudu.md new file mode 100644 index 0000000..089a78c --- /dev/null +++ b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-01-intention-of-wudu.md @@ -0,0 +1,46 @@ +--- +id: "daily-fiqh-beginner-module-01-lesson-01" +title: "The Intention of Wudu" +course: "Daily Fiqh for Beginners" +module: "Module 1: Purification & Prayer" +order: 1 +read_time: "2 min" +difficulty: "beginner" +tags: ["fiqh", "wudu", "purification", "intention", "prayer"] +source_refs: ["Quran 5:6", "Sahih al-Bukhari 1", "Sahih Muslim 271"] +forum_topic: "wudu-intention" +audio_ready: true +audio_duration: "2:09" +--- + +## 🎯 Key Concept + +Wudu is not just washing body parts — it is a spiritual reset. The Prophet ﷺ said, "When a Muslim performs wudu and washes his face, every sin he committed with his eyes is washed away. When he washes his hands, every sin committed with his hands is washed away." (Sahih Muslim 244) + +But wudu only counts if you **intend** it. The intention is the invisible thread that transforms a shower into worship. + +## 📖 Details + +**What is the intention?** + +It is simply knowing in your heart: *I am doing this to purify myself for prayer, or to remove ritual impurity.* You do not need to speak it aloud. The scholars say the intention is "the aim of the heart." + +**When to make it:** + +The intention must exist when you begin washing your face — the first act of wudu. If you start washing and then remember, "Oh, I should make wudu," it counts as long as you intended it before finishing. + +**Common mistake:** + +Some people say "Bismillah" and assume that is the intention. Bismillah is recommended, but it is not the intention itself. The intention lives in the heart, not on the tongue. + +## 🤔 Reflection + +Think about your last wudu. Were you rushing through it while mentally scrolling your to-do list? What if you paused at the tap and thought: *This water is washing away more than dust — it is washing away mistakes?* + +## ⚡ Action Step + +Before your next prayer, stand at the sink for five seconds. Say silently: *I intend wudu to purify myself for prayer.* Feel the intention settle. Then begin. + +--- + +*Sources: Sahih al-Bukhari, Sahih Muslim, Fiqh us-Sunnah (Sayyid Sabiq)* diff --git a/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-02-how-to-perform-wudu.md b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-02-how-to-perform-wudu.md new file mode 100644 index 0000000..e9c80cb --- /dev/null +++ b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-02-how-to-perform-wudu.md @@ -0,0 +1,50 @@ +--- +id: "daily-fiqh-beginner-module-01-lesson-02" +title: "How to Perform Wudu Step by Step" +course: "Daily Fiqh for Beginners" +module: "Module 1: Purification & Prayer" +order: 2 +read_time: "3 min" +difficulty: "beginner" +tags: ["fiqh", "wudu", "purification", "prayer", "step-by-step"] +source_refs: ["Quran 5:6", "Sahih al-Bukhari 185", "Sahih Muslim 245"] +forum_topic: "wudu-steps" +audio_ready: true +audio_duration: "2:26" +--- + +## 🎯 Key Concept + +Allah describes wudu in the Quran with elegant precision: "Wash your faces, your hands to the elbows, wipe your heads, and wash your feet to the ankles." (5:6). Four acts, in order, done mindfully. + +## 📖 Details + +**Step 1: Face** +Wash from the hairline to the chin, and from ear to ear. The water must touch the skin. If you have a thick beard, run your wet fingers through it. The Prophet ﷺ did this. + +**Step 2: Arms to Elbows** +Wash from fingertips to elbows. Start with the right arm, then the left. Some scholars say order is recommended, not mandatory — but following the Sunnah brings barakah. + +**Step 3: Head** +Wipe the head with wet hands, from front to back and back to front. You only need to touch the hair or scalp. A single wipe is enough. + +**Step 4: Ears** +Wipe the inside and back of the ears with your wet index fingers and thumbs. The Prophet ﷺ said, "The ears are part of the head." (Sunan Abi Dawud 111) + +**Step 5: Feet to Ankles** +Wash the feet, including between the toes, up to the ankle bone. Start right, then left. + +**What to say:** +Bismillah before starting. After finishing, the Prophet ﷺ would say: "Ashhadu an la ilaha ill-Allah wahdahu la sharika lah, wa ashhadu anna Muhammadan abduhu wa rasuluh" — bearing witness to the Oneness of Allah and the prophethood of Muhammad. + +## 🤔 Reflection + +Wudu takes about two minutes. Yet it washes away sins and prepares you to stand before Allah. Compare that to the time you spend on social media. What if wudu became your favorite two minutes of the day? + +## ⚡ Action Step + +Perform wudu now, even if you do not need to pray immediately. Pay attention to every limb. Do not rush. Notice how your heart slows down. + +--- + +*Sources: Quran 5:6, Sahih al-Bukhari, Sunan Abi Dawud* diff --git a/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-03-when-wudu-breaks.md b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-03-when-wudu-breaks.md new file mode 100644 index 0000000..c2ad0dd --- /dev/null +++ b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-03-when-wudu-breaks.md @@ -0,0 +1,55 @@ +--- +id: "daily-fiqh-beginner-module-01-lesson-03" +title: "When Wudu Breaks" +course: "Daily Fiqh for Beginners" +module: "Module 1: Purification & Prayer" +order: 3 +read_time: "3 min" +difficulty: "beginner" +tags: ["fiqh", "wudu", "nullifiers", "purification"] +source_refs: ["Quran 4:43", "Sahih al-Bukhari 135", "Sahih Muslim 543"] +forum_topic: "wudu-nullifiers" +audio_ready: true +audio_duration: "2:27" +--- + +## 🎯 Key Concept + +Wudu is a fragile state. The Prophet ﷺ described it as light on the face that fades when something breaks it. Knowing what breaks wudu saves you from praying in an invalid state — which is like building a house on sand. + +## 📖 Details + +**The eight things that break wudu:** + +1. **Anything exiting the front or back passage** — urine, stool, gas, or any other substance +2. **Deep sleep** — if you lose awareness while lying down +3. **Loss of consciousness** — fainting, intoxication, or anesthesia +4. **Touching the private parts** — with the palm or inner fingers +5. **Touching another's private parts** — with desire +6. **Eating camel meat** — a specific ruling for camel +7. **Apostasy** — leaving Islam (Allah forbid) +8. **Blood or pus** — flowing from the body (minority view, but worth knowing) + +**What does NOT break wudu:** + +- Touching a non-mahram (the Prophet ﷺ shook hands with women) +- Kissing your spouse (with or without desire) +- Bleeding from a small cut +- Vomiting +- Laughing loudly in prayer (this breaks the prayer, not the wudu) +- Doubting whether you broke wudu — certainty is required + +**The doubt rule:** +If you are unsure whether you broke wudu, assume you did not. The Prophet ﷺ said, "If one of you feels something in his stomach and is unsure whether something came out, he should not leave the mosque unless he hears a sound or smells an odor." (Sahih Muslim 550) + +## 🤔 Reflection + +Many Muslims anxiously question their wudu status. How many prayers have been delayed by unnecessary doubt? The Sunnah teaches: build on certainty, not suspicion. Are you overthinking your purity? + +## ⚡ Action Step + +Make a mental note of the doubt rule. Next time you wonder, "Did I break wudu?" — if you are not sure, you did not. Proceed with confidence. + +--- + +*Sources: Sahih al-Bukhari, Sahih Muslim, Fiqh us-Sunnah* diff --git a/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-04-the-adhan.md b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-04-the-adhan.md new file mode 100644 index 0000000..c9c07da --- /dev/null +++ b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-04-the-adhan.md @@ -0,0 +1,57 @@ +--- +id: "daily-fiqh-beginner-module-01-lesson-04" +title: "The Call to Prayer" +course: "Daily Fiqh for Beginners" +module: "Module 1: Purification & Prayer" +order: 4 +read_time: "3 min" +difficulty: "beginner" +tags: ["fiqh", "adhan", "prayer", "call-to-prayer", "dua"] +source_refs: ["Sahih al-Bukhari 604", "Sahih Muslim 447", "Sunan al-Nasa'i 674"] +forum_topic: "adhan-etiquette" +audio_ready: true +audio_duration: "2:22" +--- + +## 🎯 Key Concept + +The adhan is more than a reminder — it is an invitation from Allah. When you hear it, you are being personally called to success. The Prophet ﷺ said, "When you hear the muezzin, repeat what he says, then invoke blessings on me." (Sahih Muslim 384) + +## 📖 Details + +**What to do when you hear the adhan:** + +1. **Repeat after the muezzin** — silently or softly, phrase by phrase +2. **Send blessings on the Prophet ﷺ** after the muezzin finishes +3. **Ask for the wasilah** — the highest level of Paradise +4. **Make dua** — your supplication between adhan and iqamah is not rejected + +**The exact dua:** + +After sending blessings on the Prophet ﷺ, say: +> *Allahumma Rabba hadhihid-da'watit-tammah, was-salatil-qa'imah, ati Muhammadanil-wasilata wal-fadhilah, wab'athu maqaman mahmuda nilladhi wa'adtah.* + +(O Allah, Lord of this perfect call and established prayer, grant Muhammad the wasilah and virtue, and raise him to the praised station You promised him.) + +**After the adhan:** + +Do not rush. The time between adhan and iqamah is precious. Use it for: +- Dua (supplication) +- Optional prayer (rawatib/sunna prayers) +- Quiet preparation + +**Modern challenge:** + +Many of us hear the adhan on our phones rather than from a mosque. The same rules apply. Pause. Respond. Let the call interrupt your day intentionally. + +## 🤔 Reflection + +The adhan interrupts work, sleep, conversation, and entertainment — on purpose. It is a scheduled disruption designed to reorient your heart. Do you treat it as an annoyance or an invitation? What would change if you stopped everything for 60 seconds when you heard it? + +## ⚡ Action Step + +Set your phone's adhan notification to a voice you love, not a jarring beep. For the next three adhans, stop what you are doing, repeat the words, and make one sincere dua before the iqamah. + +--- + +*Sources: Sahih al-Bukhari, Sahih Muslim, Sunan al-Nasa'i* diff --git a/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-05-essentials-of-salah.md b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-05-essentials-of-salah.md new file mode 100644 index 0000000..f56f9f7 --- /dev/null +++ b/courses/daily-fiqh-beginner/module-01-purification-prayer/lesson-05-essentials-of-salah.md @@ -0,0 +1,58 @@ +--- +id: "daily-fiqh-beginner-module-01-lesson-05" +title: "The Essentials of Salah" +course: "Daily Fiqh for Beginners" +module: "Module 1: Purification & Prayer" +order: 5 +read_time: "3 min" +difficulty: "beginner" +tags: ["fiqh", "salah", "prayer", "pillars", "arkaan"] +source_refs: ["Quran 2:238", "Sahih al-Bukhari 8", "Sahih Muslim 397"] +forum_topic: "salah-essentials" +audio_ready: true +audio_duration: "2:52" +--- + +## 🎯 Key Concept + +Salah is the backbone of a Muslim's day. The Prophet ﷺ said, "The first matter that the slave will be brought to account for on the Day of Judgment is the prayer. If it is sound, the rest of his deeds will be sound. If it is corrupt, the rest of his deeds will be corrupt." (Sunan al-Tirmidhi 413) + +Salah has **14 pillars (arkan)**. Missing any one intentionally invalidates the prayer. Missing it by forgetfulness requires the forgetfulness prostration (sujud as-sahw) at the end. + +## 📖 Details + +**The 14 Pillars of Salah:** + +**Before Salah:** +1. **Standing** (if able) — for obligatory prayers +2. **The opening takbir** — saying *Allahu Akbar* to begin +3. **Reciting al-Fatiha** — in every rak'ah of every prayer +4. **Bowing (ruku)** — with tranquility +5. **Rising from ruku** — with tranquility +6. **Prostration (sujud)** — forehead, nose, hands, knees, and toes touching the ground +7. **Sitting between prostrations** — with tranquility +8. **The final tashahhud** — the testimony after the last sitting +9. **Sitting for the final tashahhud** — with tranquility +10. **The taslim** — saying *As-salamu alaykum* to end the prayer + +**During the prayer:** +11. **Order** — the pillars must be performed in sequence +12. **Tranquility (tuma'ninah)** — each position must be still for a moment +13. **Intention** — knowing which prayer you are performing +14. **Facing the qibla** — toward the Ka'bah in Makkah + +**The Forgetfulness Prostration:** + +If you accidentally miss a pillar (like skipping a ruku or adding an extra rak'ah), prostrate twice *before* the taslim and say: *Subhana Rabbiyal-A'la* (Glory be to my Lord, the Most High). + +## 🤔 Reflection + +Many Muslims pray quickly, rushing through positions like a checklist. The Prophet ﷺ prayed so slowly that a companion said, "I wanted to do something bad but remembered I was in prayer." (Sahih Muslim 543) What if your prayer was so present that it stopped you from sinning? + +## ⚡ Action Step + +In your next prayer, add one extra second to each position. Feel your weight in ruku. Feel the ground beneath your forehead in sujud. Notice your breathing slow. That one second is the difference between a transaction and a conversation. + +--- + +*Sources: Quran 2:238, Sahih al-Bukhari, Sahih Muslim, Sunan al-Tirmidhi* diff --git a/courses/daily-fiqh-beginner/module-01-purification-prayer/manifest.json b/courses/daily-fiqh-beginner/module-01-purification-prayer/manifest.json new file mode 100644 index 0000000..610e484 --- /dev/null +++ b/courses/daily-fiqh-beginner/module-01-purification-prayer/manifest.json @@ -0,0 +1,82 @@ +{ + "id": "module-01", + "course_id": "daily-fiqh-beginner", + "title": "Purification & Prayer", + "description": "The foundation of daily worship \u2014 wudu, ghusl, and the essentials of salah", + "order": 1, + "lesson_count": 5, + "estimated_duration": "12 minutes", + "prerequisites": [], + "lessons": [ + { + "id": "lesson-01", + "title": "The Intention of Wudu", + "file": "lesson-01-intention-of-wudu.md", + "order": 1, + "read_time": "2 min", + "audio_duration": "2:09", + "tags": [ + "wudu", + "intention", + "purification" + ] + }, + { + "id": "lesson-02", + "title": "How to Perform Wudu Step by Step", + "file": "lesson-02-how-to-perform-wudu.md", + "order": 2, + "read_time": "3 min", + "audio_duration": "2:26", + "tags": [ + "wudu", + "prayer", + "step-by-step" + ] + }, + { + "id": "lesson-03", + "title": "When Wudu Breaks", + "file": "lesson-03-when-wudu-breaks.md", + "order": 3, + "read_time": "3 min", + "audio_duration": "2:27", + "tags": [ + "wudu", + "nullifiers", + "purification" + ] + }, + { + "id": "lesson-04", + "title": "The Call to Prayer", + "file": "lesson-04-the-adhan.md", + "order": 4, + "read_time": "3 min", + "audio_duration": "2:22", + "tags": [ + "adhan", + "prayer", + "dua" + ] + }, + { + "id": "lesson-05", + "title": "The Essentials of Salah", + "file": "lesson-05-essentials-of-salah.md", + "order": 5, + "read_time": "3 min", + "audio_duration": "2:52", + "tags": [ + "salah", + "prayer", + "pillars" + ] + } + ], + "quiz": { + "file": "quiz.json", + "question_count": 5, + "passing_score": 4 + } +} \ No newline at end of file diff --git a/courses/daily-fiqh-beginner/module-01-purification-prayer/quiz.json b/courses/daily-fiqh-beginner/module-01-purification-prayer/quiz.json new file mode 100644 index 0000000..5d61731 --- /dev/null +++ b/courses/daily-fiqh-beginner/module-01-purification-prayer/quiz.json @@ -0,0 +1,69 @@ +{ + "module_id": "module-01", + "title": "Purification & Prayer — Quick Check", + "description": "Test your understanding of Module 1 with 5 quick questions.", + "passing_score": 4, + "question_count": 5, + "questions": [ + { + "id": "q1", + "question": "Where does the intention for wudu need to be made?", + "options": [ + "Before touching water", + "While washing the face", + "After finishing wudu", + "Loudly, so others can hear" + ], + "correct_index": 1, + "explanation": "The intention must exist when you begin washing your face, the first act of wudu. It lives in the heart, not on the tongue." + }, + { + "id": "q2", + "question": "How many essential steps (pillars) does salah have?", + "options": [ + "5", + "7", + "10", + "14" + ], + "correct_index": 3, + "explanation": "Salah has 14 pillars (arkan). Missing any one intentionally invalidates the prayer." + }, + { + "id": "q3", + "question": "If you are unsure whether you broke wudu, what should you assume?", + "options": [ + "Assume you broke it and make wudu again", + "Assume you did not break it and continue", + "Ask a friend what they think", + "Skip prayer to be safe" + ], + "correct_index": 1, + "explanation": "The Prophet taught: build on certainty, not suspicion. If you are unsure, assume your wudu is still valid." + }, + { + "id": "q4", + "question": "What should you do immediately after hearing the adhan?", + "options": [ + "Rush to the prayer mat", + "Repeat what the muezzin says", + "Start eating if you were about to break your fast", + "Change into clean clothes" + ], + "correct_index": 1, + "explanation": "The Sunnah is to repeat after the muezzin silently, then send blessings on the Prophet, and finally make dua." + }, + { + "id": "q5", + "question": "Which surah must be recited in every rak'ah of every prayer?", + "options": [ + "Surah al-Ikhlas", + "Surah al-Fatiha", + "Surah al-Baqarah", + "Any surah the worshipper chooses" + ], + "correct_index": 1, + "explanation": "Reciting al-Fatiha is a pillar of every rak'ah. The Prophet said, 'There is no prayer for the one who does not recite the Opening of the Book.'" + } + ] +} \ No newline at end of file diff --git a/scripts/generate-audio.py b/scripts/generate-audio.py new file mode 100644 index 0000000..29fc3fc --- /dev/null +++ b/scripts/generate-audio.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +""" +TTS Audio Generation Pipeline for FalahMobile Content +Supports: Azure Speech (free tier), Piper TTS (local/free), Google Cloud, ElevenLabs + +Usage: + python scripts/generate-audio.py --course daily-fiqh-beginner --engine azure + python scripts/generate-audio.py --course daily-fiqh-beginner --engine piper + python scripts/generate-audio.py --lesson path/to/lesson.md --engine azure +""" + +import argparse +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +# Configuration +COURSES_DIR = Path(__file__).parent.parent / "courses" +DEFAULT_VOICE = { + "azure": "en-US-AriaNeural", + "google": "en-US-Neural2-F", + "elevenlabs": "Rachel", + "piper": "amy", +} + +def strip_markdown(text: str) -> str: + """Convert markdown to clean text for TTS.""" + # Remove YAML frontmatter + text = re.sub(r'^---\n.*?---\n', '', text, flags=re.DOTALL) + # Remove emoji + text = re.sub(r'[\U0001F300-\U0001F9FF]', '', text) + # Remove markdown headers + text = re.sub(r'^#+\s+', '', text, flags=re.MULTILINE) + # Remove bold/italic markers + text = re.sub(r'\*\*?|\*\*?', '', text) + # Remove blockquotes markers but keep text + text = re.sub(r'^>\s*', '', text, flags=re.MULTILINE) + # Remove code blocks + text = re.sub(r'```.*?```', '', text, flags=re.DOTALL) + # Remove inline code + text = re.sub(r'`[^`]+`', '', text) + # Remove horizontal rules + text = re.sub(r'^---+', '', text, flags=re.MULTILINE) + # Remove HTML tags + text = re.sub(r'<[^>]+>', '', text) + # Remove extra whitespace + text = re.sub(r'\n{3,}', '\n\n', text) + return text.strip() + +def generate_azure(text: str, output_path: Path, voice: str = None): + """Generate audio using Azure Speech SDK.""" + voice = voice or DEFAULT_VOICE["azure"] + + # Check for Azure key + key = os.environ.get("AZURE_SPEECH_KEY") + region = os.environ.get("AZURE_SPEECH_REGION", "eastus") + + if not key: + print("Error: Set AZURE_SPEECH_KEY environment variable") + print("Get free key at: https://azure.microsoft.com/en-us/services/cognitive-services/speech/") + sys.exit(1) + + try: + import azure.cognitiveservices.speech as speechsdk + except ImportError: + print("Installing azure-cognitiveservices-speech...") + subprocess.run([sys.executable, "-m", "pip", "install", "azure-cognitiveservices-speech"]) + import azure.cognitiveservices.speech as speechsdk + + speech_config = speechsdk.SpeechConfig(subscription=key, region=region) + speech_config.speech_synthesis_voice_name = voice + + # SSML for calm, gentle pacing + ssml = f""" + + + {text} + + + """ + + audio_config = speechsdk.audio.AudioOutputConfig(filename=str(output_path)) + synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=audio_config) + + result = synthesizer.speak_ssml_async(ssml).get() + + if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted: + print(f" Generated: {output_path}") + return True + else: + print(f" Error: {result.reason}") + return False + +def generate_piper(text: str, output_path: Path, voice: str = None): + """Generate audio using Piper TTS (local, completely free).""" + voice = voice or DEFAULT_VOICE["piper"] + + piper_dir = Path.home() / ".piper" + model_path = piper_dir / f"{voice}.onnx" + + if not model_path.exists(): + print(f"Piper model not found: {model_path}") + print("Download models from: https://github.com/rhasspy/piper/releases") + print(f"Expected at: {piper_dir}/{voice}.onnx") + return False + + # Write text to temp file + temp_text = output_path.with_suffix(".txt") + temp_text.write_text(text, encoding="utf-8") + + # Run piper + cmd = [ + "piper", + "--model", str(model_path), + "--output_file", str(output_path), + "--data-dir", str(piper_dir), + ] + + result = subprocess.run(cmd, input=text, text=True, capture_output=True) + temp_text.unlink(missing_ok=True) + + if result.returncode == 0: + print(f" Generated: {output_path}") + return True + else: + print(f" Error: {result.stderr}") + return False + +def generate_elevenlabs(text: str, output_path: Path, voice: str = None): + """Generate audio using ElevenLabs API.""" + voice = voice or DEFAULT_VOICE["elevenlabs"] + key = os.environ.get("ELEVENLABS_API_KEY") + + if not key: + print("Error: Set ELEVENLABS_API_KEY environment variable") + sys.exit(1) + + import requests + + url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice}" + headers = { + "Accept": "audio/mpeg", + "Content-Type": "application/json", + "xi-api-key": key, + } + data = { + "text": text, + "model_id": "eleven_monolingual_v1", + "voice_settings": { + "stability": 0.75, + "similarity_boost": 0.75, + } + } + + response = requests.post(url, json=data, headers=headers) + + if response.status_code == 200: + output_path.write_bytes(response.content) + print(f" Generated: {output_path}") + return True + else: + print(f" Error: {response.status_code} - {response.text}") + return False + +def generate_web_speech(text: str, output_path: Path): + """Generate audio using browser Web Speech API (via Node/playwright).""" + print("Web Speech API generates audio in-browser, not pre-generated.") + print("The app should use the text directly with Web Speech API.") + return True + +def process_lesson(lesson_path: Path, engine: str, voice: str = None): + """Process a single lesson markdown file into audio.""" + print(f"Processing: {lesson_path.name}") + + # Read and clean markdown + md_text = lesson_path.read_text(encoding="utf-8") + clean_text = strip_markdown(md_text) + + # Generate audio filename + audio_path = lesson_path.with_suffix(".mp3") + + # Dispatch to engine + engines = { + "azure": generate_azure, + "piper": generate_piper, + "elevenlabs": generate_elevenlabs, + "web": generate_web_speech, + } + + if engine not in engines: + print(f"Unknown engine: {engine}") + return False + + return engines[engine](clean_text, audio_path, voice) + +def process_course(course_id: str, engine: str, voice: str = None): + """Process all lessons in a course.""" + course_dir = COURSES_DIR / course_id + + if not course_dir.exists(): + print(f"Course not found: {course_dir}") + sys.exit(1) + + manifest_path = course_dir / "manifest.json" + if manifest_path.exists(): + manifest = json.loads(manifest_path.read_text()) + print(f"\nCourse: {manifest['title']}") + print(f"Modules: {manifest['total_modules']}") + + # Find all lesson markdown files + lessons = sorted(course_dir.rglob("lesson-*.md")) + + if not lessons: + print("No lessons found") + return + + print(f"\nGenerating audio for {len(lessons)} lessons...") + print(f"Engine: {engine}") + print(f"Voice: {voice or DEFAULT_VOICE.get(engine, 'default')}") + print("-" * 50) + + success = 0 + for lesson in lessons: + if process_lesson(lesson, engine, voice): + success += 1 + + print("-" * 50) + print(f"Done: {success}/{len(lessons)} lessons generated") + +def main(): + parser = argparse.ArgumentParser(description="Generate TTS audio for FalahMobile content") + parser.add_argument("--course", help="Course ID to process all lessons") + parser.add_argument("--lesson", help="Path to single lesson markdown file") + parser.add_argument("--engine", choices=["azure", "piper", "elevenlabs", "web"], + default="azure", help="TTS engine") + parser.add_argument("--voice", help="Voice ID (engine-specific)") + parser.add_argument("--list-engines", action="store_true", help="List available engines and voices") + + args = parser.parse_args() + + if args.list_engines: + print("Available engines:") + print(" azure - Microsoft Azure Speech (free: 500K chars/mo)") + print(" Voice: en-US-AriaNeural (default)") + print(" piper - Local open-source TTS (unlimited, free)") + print(" Voice: amy (default, calm female)") + print(" elevenlabs - Premium quality (free: 10K chars/mo)") + print(" Voice: Rachel (default)") + print(" web - Web Speech API (in-browser, no pre-gen)") + return + + if args.course: + process_course(args.course, args.engine, args.voice) + elif args.lesson: + lesson_path = Path(args.lesson) + process_lesson(lesson_path, args.engine, args.voice) + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/templates/lesson-template.md b/templates/lesson-template.md new file mode 100644 index 0000000..0b62329 --- /dev/null +++ b/templates/lesson-template.md @@ -0,0 +1,40 @@ +--- +id: "course-id-module-XX-lesson-XX" +title: "Lesson Title" +course: "Course Name" +module: "Module X: Module Name" +order: 1 +read_time: "3 min" +difficulty: "beginner|intermediate|advanced" +tags: ["tag1", "tag2"] +source_refs: ["Quran X:X", "Hadith Source #"] +forum_topic: "forum-slug" +audio_ready: false +audio_duration: "0:00" +--- + +## 🎯 Key Concept + +One powerful sentence or idea that captures the lesson's essence. Make it memorable. This is the hook. + +## 📖 Details + +Expand the concept with: +- Evidence from Quran or Sunnah +- Scholarly perspectives (majority view, with nuance) +- Practical examples from daily life +- Common misconceptions corrected gently + +> **Quote Block**: Use for direct hadith, Quran verses, or scholar quotes + +## 🤔 Reflection + +One thought-provoking question that connects the fiqh ruling to the reader's heart. Not a test — an invitation to introspection. + +## ⚡ Action Step + +One concrete, small action the reader can take today. Not "be better" — but "do this specific thing before bedtime." + +--- + +*Sources: Cite primary sources here*