diff --git a/.env.example b/.env.example index bb4525702fee728cbe3e7d985b073a934c15ee95..efcfa568f04071122fed79b7eff71676ff5f509f 100644 --- a/.env.example +++ b/.env.example @@ -1,86 +1,12 @@ -# Copy to .env and fill in. .env is git-ignored and is not copied into the Docker image. -# On Hugging Face, set these as Space *secrets* instead (Settings -> Variables and secrets). - -# Sawtak Arabi TTS key for the model-answer playback (https://sawtakarabi.ai/dashboard). -# Without it the page falls back to the device's Arabic voice. -SAWTAK_API_KEY= -# Optional: the Sawtak voice used for normal model-answer playback. Empty = Raef. -# Pronunciation-score baselines keep their fixed reference voices and do not use this setting. -SAWTAK_VOICE_ID= -# its display name (default אמיר); playback only, the score baseline stays Raef + Fadwa -SAWTAK_VOICE_NAME= - -# Optional: the edge-tts voice of the spoken Hebrew prompts. Empty = he-IL-HilaNeural. -HEBREW_VOICE= - -# Optional: where the generated speech (Arabic answers, Hebrew prompts) is cached. Empty = ~/.cache/ihki-tts. -TTS_CACHE_DIR= -# The Hebrew read aloud gets niqqud first (Dicta's free Nakdan, cached in TTS_CACHE_DIR). 0 turns it off. -HEBREW_NIQQUD= - -# Optional: the Hugging Face CTC phoneme model the scorer runs. Unset = facebook/wav2vec2-xlsr-53-espeak-cv-ft. -# Leave the line commented out rather than empty: an empty value is used as is and fails to load. -# scripts/bench.py and scripts/rescore.py read it too, so a benchmark runs on the same model as the server. -# MODEL_ID=facebook/wav2vec2-xlsr-53-espeak-cv-ft - -# Optional: how a vowel's GOP pools its frames (docs/gop-pooling.md): max | mean | topk | lse | energy | gated. -# Empty = gated. "max" is the scoring before pooling. The lab's results show it beside the version, to compare runs. -GOP_RATIO_POOL= - -# Optional: which frames a vowel's competitors are read from. Empty = wide (from the sound before to the one after, -# as consonants). "narrow" = its aligned frames +- 1, or a number for another margin. Narrow drops spikes in the -# transitions, but also misses a substitution held just outside it; see scripts/gop_window_bench.py. -GOP_VOWEL_WINDOW= - -# Google sign-in through Supabase (Project Settings -> API Keys). Both values are public: the page gets them too. -# Empty = no sign-in, the service is open. On Hugging Face these can be plain Space *variables*. +# Optional Supabase authentication. Leave both empty for a public local app. SUPABASE_URL= SUPABASE_PUBLISHABLE_KEY= -# Optional: the teacher's account (or several, separated by commas or spaces). After sign-in a teacher chooses -# between "תרגול" (the same as a student) and "ניהול" (the management panel: every student's progress, and the -# classes). Who may sign in is set by the classes the teacher opens there (see the README), not here. +# Required for class roster management. +SUPABASE_SECRET_KEY= TEACHER_EMAILS= - -# Bootstrap accounts with full access, including the Lab. Admins do not get Lab access. Keep at least one address here -# so database permission mistakes cannot lock every developer out. DEVELOPER_EMAILS= -# Needed by the management panel and the classes: the project's secret key (Project Settings -> API Keys -> secret, or the -# legacy service_role key). It reads every student's rows, so it is NOT public: on Hugging Face set it as a *secret*. -# scripts/bench.py needs it (with the two values above) to read the lab's recordings. -SUPABASE_SECRET_KEY= - -# Public URL of the restricted Grok Business MCP connector. On Hugging Face this is inferred from -# SPACE_HOST; set it explicitly for a custom domain or local OAuth test. It must end in /mcp/ and -# exactly match the URL entered in Grok. Authentication is handled by Supabase OAuth, not a shared secret. -MCP_PUBLIC_URL= - -# Optional durable store for flag and Lab recordings. Set AUDIO_BUCKET to a private Hugging Face Storage Bucket -# and HF_TOKEN as a Space secret with write access. New audio goes only to HF. To migrate old Supabase audio once -# while preserving a fixed compatibility set, set AUDIO_MIGRATE_BEFORE to the oldest timestamp you want to keep. -AUDIO_BUCKET= -AUDIO_BUCKET_PREFIX=ihki-audio -AUDIO_MIGRATE_BEFORE=2026-10-17T00:00:00+03:00 - -# Optional: accounts (separated by commas or spaces) that, like the teachers, get a flag under each score, for a recording they said -# right and the system scored wrong. A flag is kept in the Supabase table `flags` with its recording (see the README), -# so it needs SUPABASE_SECRET_KEY. These accounts may sign in without being in a class. -TUTORS_EMAIL= - -# Optional deployment metadata shown to teachers at the bottom of the management panel. -# Without these, the app uses a content hash, the newest source-file time, and the local git commit when available. -APP_VERSION= -BUILD_DATE= -GIT_COMMIT= -GIT_COMMIT_MESSAGE= - -# Lets a developer replace SAWTAK_API_KEY / ANTHROPIC_API_KEY from the management panel (kept encrypted in Supabase, -# table app_secrets; see the README). Make one with: -# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" -# Keep it only here / as a Space secret. Losing it loses the keys set in the panel (the ones above still work). -SECRETS_MASTER_KEY= - -# the daily video's morning reminder (app/push.py): make one with python -m app.push -VAPID_PRIVATE_KEY= -# PUSH_HOUR=8 +# Enables photographed-problem hints and solutions. +ANTHROPIC_API_KEY= +# MATH_MODEL=claude-opus-5-5 diff --git a/Dockerfile b/Dockerfile index 9893f370938d202bde6f17ccb245ec7d076ccc48..348f6d746325554e16a7f0b60411a41b7d41ae15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,27 +2,15 @@ # CPU image; runs on Hugging Face Spaces (Docker SDK), Render, Fly, or any VM. FROM python:3.11-slim -RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* - # Spaces runs the container as uid 1000 RUN useradd -m -u 1000 user ENV HOME=/home/user HF_HOME=/home/user/.cache/huggingface PYTHONUNBUFFERED=1 WORKDIR /home/user/app -RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -ARG MODEL_ID=facebook/wav2vec2-xlsr-53-espeak-cv-ft -ENV MODEL_ID=${MODEL_ID} USER user -# bake the model into the image so cold starts don't download it (1.2-1.6 GB). MODEL_ID is a Space variable: -# a wav2vec2 repo, or powsm-onnx: for the exported POWSM (app/powsm.py). A private repo needs a Space secret -# HF_TOKEN; it is mounted for this step only and never stored in the image. -COPY --chown=user app/__init__.py app/powsm.py app/prefetch.py ./app/ -RUN --mount=type=secret,id=HF_TOKEN,mode=0444,required=false \ - if [ -s /run/secrets/HF_TOKEN ]; then export HF_TOKEN="$(cat /run/secrets/HF_TOKEN)"; fi; python -m app.prefetch - COPY --chown=user app ./app COPY --chown=user static ./static diff --git a/README.md b/README.md index dc4b584fc65464bd0d13dfbc15879d92922f3a32..ee5b7e0290a80da295fa28670099dfe73cb80ea6 100644 --- a/README.md +++ b/README.md @@ -1,986 +1,25 @@ ---- -title: mathly -emoji: ➗ -colorFrom: blue -colorTo: gray -sdk: docker -app_port: 7860 ---- +# Mathly -# احكي · ihki — pronunciation scoring service +Mathly is a Hebrew ninth-grade algebra practice path. It contains 56 linked skills across six units, immediate feedback for known mistakes, and optional photographed-problem help. -Hebrew prompt → the student says it in spoken Arabic → per-phoneme feedback. - -The service replaces the browser speech-recognition diff with phoneme-level scoring: - -1. **Phoneme posteriors.** `facebook/wav2vec2-xlsr-53-espeak-cv-ft`, a multilingual wav2vec2 CTC model that outputs IPA phonemes. It was fine-tuned on Common Voice data that includes Arabic, so its vocab should cover the pharyngeals; `/api/health` confirms this. -2. **Forced alignment.** A CTC Viterbi pass (`app/align.py`, numpy) aligns the audio against each accepted variant's phoneme string and keeps the best-fitting variant. Example: "قديش" said with *k* aligns to the urban variant `ʔ a d d eː ʃ`. -3. **GOP per phoneme.** Inside each phoneme's window (the frames between its neighbours), the scorer compares the target's peak posterior with the strongest competing phoneme. - - Example: for وين الحمام said with a Hebrew ח, the window of `ħ` is dominated by `χ`. The result is `ħ` score ≈ 0, `heard: χ`, and the Hebrew tip for ح→خ. -4. **Scoring.** - - A phoneme's score is a softened target/competitor ratio (`GopConfig.leniency`); a vowel heard as its neighbour (i/e, u/o, short/long) scores at least 90; a dropped glottal stop is free. - - A word scores halfway between its weakest phoneme and the mean of its phonemes. - - The sentence score averages the phoneme mean and the word mean, so one wrong consonant moves "excellent" down to "almost". - - If the greedy phoneme decode is far from every variant, the response says "different sentence?" instead of listing tips. -5. **Native baseline.** With a TTS key, the phrase's model answer is scored by the same model, and each of the learner's sounds is measured against the score the voice got on it (`app/reference.py`). A sound the model mishears from the native voice too is not marked down and gets no tip. - -**Why a letter is coloured.** The answer is shown letter by letter, each coloured by the weakest of its sounds (on a story's line and in every other exercise). A letter under 75 comes with its reason from the server: `why`, the sound and what was heard in its place ("ח (ح) נשמעה כמו ח' (خ)"), and `tip`, the advice for that substitution when `app/tips.py` has one. On the page the reason is the letter's tooltip. A screen with no mouse has no tooltip, so such a letter is also underlined with a dotted line, a tap on it opens the reason in a bubble over the letter, and a line under the result says so. - -``` -app/ - align.py CTC Viterbi alignment, GOP, greedy decode, edit distance (no torch) - scorer.py model wrapper, IPA → vocab mapping with fallbacks, result assembly, ffmpeg decode - tips.py Hebrew feedback keyed by (expected, heard) phoneme pairs - reference.py native baseline: the TTS voices scored by the same model - phrases.py phrase bank: accepted variants (Arabic + IPA) per phrase - grammar.py day-2 sentences as grammar: every pronoun × positive/negative, for the negation and conjugation buttons - workbook.py the course workbook lesson by lesson: each lesson's topics (pronouns, verbs, conjugations, the Hebrew → Arabic and Arabic → Hebrew sentences), and the levels - verb_game.py "פועל יוצא" levels 1–5: exercise generation, adaptive choice, server-side grading - main.py FastAPI: page, phrases/scoring, and "פועל יוצא" endpoints -static/index.html frontend (MediaRecorder → /api/score) -train/finetune.py CTC fine-tuning on your own recordings -tests/ synthetic-emission tests, including an API test through ffmpeg -``` - -## Run locally (Ubuntu) +## Run locally ```bash -sudo apt install ffmpeg -python3 -m venv .venv && source .venv/bin/activate -pip install torch --index-url https://download.pytorch.org/whl/cpu -pip install -r requirements.txt -uvicorn app.main:app --host 0.0.0.0 --port 7860 +python3 -m venv venv +venv/bin/python -m pip install -r requirements.txt -r requirements-dev.txt +venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 7860 ``` -Open http://localhost:7860. The first start downloads the model (~1.2 GB). CPU inference on a 3 s clip takes about a second. +Open . Authentication is disabled when the Supabase values are empty. -Then check `http://localhost:7860/api/health`. Its `phoneme_fallbacks` field lists any phoneme from `phrases.py` that the model's vocab lacks, and what it was mapped to (e.g. `"sˤ": "s"`). A fallback on an emphatic means that contrast cannot be scored until you fine-tune. Look at this first. +## Configuration -Tests (no model needed): -```bash -pip install -r requirements-dev.txt && pytest -q -``` +Copy `.env.example` to `.env`. `ANTHROPIC_API_KEY` enables photographed-problem help. Supabase settings enable Google sign-in and math-class rosters. -### Browse OpenSpec requirements locally +For an existing deployment, back up Supabase and then run [docs/mathly-supabase.sql](docs/mathly-supabase.sql) in its SQL editor. The migration deliberately deletes the removed product's tables and non-math classes. -Run [OpenSpec WebUI](https://github.com/oioi555/openspec-webui) from the project root (needs Node 20.19+), then open http://127.0.0.1:3001: +## Tests ```bash -openspec-webui -``` - -## פועל יוצא (Switch & Speak) - -The Games tab contains an RTL, touch-first conjugation drill with two paths: learning new verbs and a daily retrieval workout. Levels 1–3 implement recognition, pronoun matching, and timed switching. Level 4 records a spoken conjugation; Level 5 starts with a controlled Palestinian-Arabic prompt and evaluates the target conjugation in the response. - -Conjugations come only from complete tables in `app/grammar.py` and the fully populated tables verified from the parsed Level 1/2 booklets. Every added booklet form must resolve to an Arabic entry in `workbook.LEX`; incomplete fill-in tables are excluded. Each verb carries the lesson where it is introduced, and signed-in learners receive only forms from their class's lessons. - -API endpoints: - -- `GET /api/verb-game` returns the available levels, tenses, and lesson-unlocked verbs. -- `POST /api/verb-game/question` accepts `{"level": 1..3, "mastery": {...}, "recent": [...]}` and returns an opaque question. Correct answers are not sent to the browser. -- `POST /api/verb-game/answer` accepts `{"question_id": "...", "answer": ..., "response_ms": 1234}`. A question can be graded only once. For Level 2, `answer` is a map from pronoun-card IDs to form-card IDs. -- `POST /api/verb-game/speech` accepts multipart `question_id` and `audio` for Levels 4–5. `SpeechEvaluationService` wraps the existing phoneme scorer so another dialect ASR can replace it later. An unrelated or low-confidence recognition returns `outcome: "uncertain"` and `counts_as_attempt: false`; it never becomes a grammatical error automatically. - -Mastery is currently stored in browser local storage under `ihki.verbMastery` and `ihki.verbSummary`. `VERB_THRESH` in `static/index.html` configures the 85%/90% recognition gates, the Level 3 median response-time gate, and the 85% spoken-response gate; minimum attempt counts are defined next to it. `LEVELS` in `app/verb_game.py` configures choice counts and response windows. Persistent server mastery and spaced repetition remain separate work. - -## Deploy for the teacher: Hugging Face Spaces (free, https) - -1. Create a Space at huggingface.co → New Space → SDK **Docker** → hardware **CPU basic** (free). -2. Push this folder to the Space's git repo. The YAML header of this README configures it. -3. The build bakes the model into the image. The page is then live at `https://-ihki.hf.space`. - -The teacher only needs that link. Free Spaces sleep after inactivity, so the first request after a pause takes ~30 s. - -`docker build -t ihki . && docker run -p 7860:7860 ihki` runs the same image anywhere. - -## Sign-in and progress (Supabase) - -Optional. With `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` set, the page asks for a sign-in (through Supabase Auth): with Google, or with a link sent to the student's email (no password), for one without a Google account. The practice endpoints refuse calls without a session. Who may sign in is set by the teacher's classes (see below). Without the two values the service is open. - -The email link needs, in Supabase: Authentication → Sign In / Providers → Email enabled; Authentication → URL Configuration → the app's address as the Site URL (or among the Redirect URLs); and Authentication → Emails → SMTP Settings → your own SMTP server, as Supabase's built-in one sends only to the project's team members, a few emails an hour. - -Each scored recording adds metadata to the table `attempts`, written with the student's own token so row-level security ties the row to them. The recording bytes are never written to this table or to Storage. Only a teacher/tutor's explicit `/api/flags` report stores its recording in the private `flags` bucket. Create `attempts` once in the Supabase SQL editor: - -```sql -create table public.attempts ( - id bigint generated always as identity primary key, - user_id uuid not null default auth.uid() references auth.users (id) on delete cascade, - cat text not null, -- the topic id, e.g. l1-verbs - phrase text not null, -- the Hebrew prompt - score int not null, -- 0-100 - unrelated boolean not null default false, -- "different sentence?" - issues jsonb not null default '[]'::jsonb, -- compact [{word, expected, heard, score}], never audio - model text not null default '', - version text not null default '', - created_at timestamptz not null default now() -); -create index attempts_user_time on public.attempts (user_id, created_at desc); -create index attempts_created_at_idx on public.attempts (created_at desc); - -alter table public.attempts enable row level security; -grant select, insert on public.attempts to authenticated; -create policy "students add their own attempts" on public.attempts - for insert to authenticated with check (user_id = auth.uid()); -create policy "students read their own attempts" on public.attempts - for select to authenticated using (user_id = auth.uid()); - --- Anonymous aggregate buckets for the teacher's Monitoring graph. Only the backend service role may call it. -create or replace function public.activity_buckets(p_hours integer default 24, p_bucket_minutes integer default 15) -returns table (bucket timestamptz, attempts bigint, active_students bigint) -language sql stable security invoker set search_path = '' as $$ - select date_bin(make_interval(mins => greatest(1, least(p_bucket_minutes, 60))), a.created_at, - timestamptz '2000-01-01 00:00:00+00'), - count(*)::bigint, count(distinct a.user_id)::bigint - from public.attempts a - where a.created_at >= now() - make_interval(hours => greatest(1, least(p_hours, 168))) - group by 1 order by 1 -$$; -revoke execute on function public.activity_buckets(integer, integer) from public, anon, authenticated; -grant execute on function public.activity_buckets(integer, integer) to service_role; - --- Totals for the full selected window. This is separate from the buckets so a student who practised in several --- buckets is counted only once. -drop function if exists public.activity_summary(integer); -create or replace function public.activity_summary(p_hours integer default 24) -returns table (attempts bigint, active_students bigint, returning_students bigint, active_days bigint) -language sql stable security invoker set search_path = '' as $$ - with windowed as ( - select a.user_id, (a.created_at at time zone 'Asia/Jerusalem')::date as practice_day - from public.attempts a - where a.created_at >= now() - make_interval(hours => greatest(1, least(p_hours, 168))) - ), per_user as ( - select user_id, count(distinct practice_day) as days from windowed group by user_id - ) - select (select count(*) from windowed)::bigint, - (select count(*) from per_user)::bigint, - (select count(*) from per_user where days >= 2)::bigint, - (select count(distinct practice_day) from windowed)::bigint -$$; -revoke execute on function public.activity_summary(integer) from public, anon, authenticated; -grant execute on function public.activity_summary(integer) to service_role; - --- The same aggregates, limited to the students visible to a subject admin. Keeping this in PostgreSQL avoids --- downloading and truncating a busy class's attempts in the monitoring page. -create or replace function public.activity_buckets_scoped( - p_user_ids uuid[], p_since timestamptz, p_bucket_minutes integer default 15 -) -returns table (bucket timestamptz, attempts bigint, active_students bigint) -language sql stable security invoker set search_path = '' as $$ - select date_bin(make_interval(mins => greatest(1, least(p_bucket_minutes, 60))), a.created_at, - timestamptz '2000-01-01 00:00:00+00'), - count(*)::bigint, count(distinct a.user_id)::bigint - from public.attempts a - where a.created_at >= p_since and a.user_id = any(p_user_ids) - group by 1 order by 1 -$$; -revoke execute on function public.activity_buckets_scoped(uuid[], timestamptz, integer) from public, anon, authenticated; -grant execute on function public.activity_buckets_scoped(uuid[], timestamptz, integer) to service_role; - -create or replace function public.activity_summary_scoped(p_user_ids uuid[], p_since timestamptz) -returns table (attempts bigint, active_students bigint, returning_students bigint, active_days bigint) -language sql stable security invoker set search_path = '' as $$ - with windowed as ( - select a.user_id, (a.created_at at time zone 'Asia/Jerusalem')::date as practice_day - from public.attempts a - where a.created_at >= p_since and a.user_id = any(p_user_ids) - ), per_user as ( - select user_id, count(distinct practice_day) as days from windowed group by user_id - ) - select (select count(*) from windowed)::bigint, - (select count(*) from per_user)::bigint, - (select count(*) from per_user where days >= 2)::bigint, - (select count(distinct practice_day) from windowed)::bigint -$$; -revoke execute on function public.activity_summary_scoped(uuid[], timestamptz) from public, anon, authenticated; -grant execute on function public.activity_summary_scoped(uuid[], timestamptz) to service_role; - --- Recent system activity shown only in the management panel. The backend writes with the service-role key; --- no browser role gets direct table access. -create table public.system_events ( - id bigint generated always as identity primary key, - kind text not null check (kind in ('exercise','game','permission','content')), - actor_id uuid references auth.users(id) on delete set null, - actor_email text not null default '', - label text not null default '', - details jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now() -); -create index system_events_created_at_idx on public.system_events (created_at desc); -alter table public.system_events enable row level security; -``` - -API keys a developer replaces from the panel (`SAWTAK_API_KEY`, `ANTHROPIC_API_KEY`, see `app/keystore.py`), and the tutor's model (`TUTOR_MODEL`, chosen from a list or typed), so changing one needs no access to the Space. Each value is stored encrypted with `SECRETS_MASTER_KEY`, a Space secret, and the page never gets a key back (the model, which is no secret, it shows). No browser role can read the table: - -```sql -create table public.app_secrets ( - name text primary key check (name in ('SAWTAK_API_KEY','ANTHROPIC_API_KEY','TUTOR_MODEL')), - ciphertext text not null, - last4 text not null default '', - updated_by text not null default '', - updated_at timestamptz not null default now() -); -alter table public.app_secrets enable row level security; -revoke all on public.app_secrets from anon, authenticated; -``` - -A table made before the panel could set `TUTOR_MODEL` needs its name check widened once (saving the model answers 409 with this SQL until then): - -```sql -alter table public.app_secrets drop constraint if exists app_secrets_name_check; -alter table public.app_secrets add constraint app_secrets_name_check - check (name in ('SAWTAK_API_KEY','ANTHROPIC_API_KEY','TUTOR_MODEL')); -``` - -For an existing `attempts` table, add the compact diagnostics once. Until this migration is run, attempts continue to be saved without diagnostics: - -```sql -alter table public.attempts - add column if not exists issues jsonb not null default '[]'::jsonb, - add column if not exists model text not null default '', - add column if not exists version text not null default ''; -``` - -The page reads these rows back through `GET /api/progress` (best score and tries per phrase, recordings, days practised, distinct practice days in the current week, and current/longest consecutive-day streaks); the best scores then follow the student between devices. Above the path, **הערבית שכבר שלכם** turns every practised phrase into a bead, except that a story or dialogue is represented by one bead for the whole topic. A phrase turns gold at 75 or more; a story's bead turns gold only when all of its lines do. Completing every listed spoken phrase in a lesson awards one **מסבחה**. The prominent collection card separately counts regular beads, gold beads, and מסבחות; its detail view marks each completed lesson's מסבחה. A compact collector at the top of the exercise card opens the same collection. When a new exercise bead is earned, its regular or gold bead flies from the microphone into that collector instead of showing a text popup. It also shows a four-day weekly practice ring and a 🔥 consecutive-day streak; yesterday keeps a streak alive until the student has had today to continue it. Gold upgrades, a newly completed lesson's מסבחה, meaningful personal bests, streaks and completed topics are celebrated on the practice card. Moving past a topic's last sentence once every sentence was practised opens the topic's end: a bead per sentence (gold 75+, red under 60). With no red bead it is a medal, stars, confetti and a chime, and the button goes on to the next topic (the lesson's next, or the next lesson's first). With red beads it offers a short round of just those sentences, after which the dialog comes back showing what improved; the student can also go on regardless. A bead opens its sentence. Hands-free driving mode skips the dialog. The teacher's **דיווחים** tab aggregates `issues` without returning student identities. It shows a candidate when the same judgement occurred in at least five attempts, for at least three students and 30% of the students who attempted that phrase. These are investigation candidates, not proof of a scoring error; a teacher must explicitly turn one into a calibration draft and apply it. - -### The teacher - -Access is managed from **ניהול → הרשאות**. Its matrix grants practice, management, Lab, games, the digital library, -problem reporting, יאללה נדבר, and the learning path independently. יאללה נדבר has its own permission because each -conversation uses the paid model API; unlike the other student features it is off until explicitly granted. -Developers have full access including the Lab; admins do not receive Lab access. `DEVELOPER_EMAILS` bootstraps one or more developer -accounts; keep at least one there to prevent a database mistake from locking every developer out. Existing -`TEACHER_EMAILS` accounts retain the old management abilities during migration, but no longer receive Lab access. - -On first use, the permissions tab displays this migration to run in the Supabase SQL editor: - -```sql -create table public.user_access ( - user_id uuid primary key references auth.users(id) on delete cascade, - email text not null, - roles text[] not null default array['student']::text[], - entitlements text[] not null default '{}'::text[], - subject text not null default '', -- developer-set subject for admin-created classes - updated_at timestamptz not null default now(), - updated_by uuid references auth.users(id), - constraint valid_roles check (roles <@ array['student','teacher','flagger','admin','developer']::text[]), - constraint valid_entitlements check (entitlements <@ array['practice','games','tutor','path','library']::text[]) -); -create unique index user_access_email_idx on public.user_access (lower(email)); -alter table public.user_access enable row level security; -create table public.access_audit ( - id bigint generated always as identity primary key, - target_user_id uuid not null, - changed_by uuid not null, - old_access jsonb not null, - new_access jsonb not null, - created_at timestamptz not null default now() -); -alter table public.access_audit enable row level security; -``` - -If `user_access` was created with the earlier three student features, run this once: - -```sql -alter table public.user_access add column if not exists subject text not null default ''; -alter table public.user_access drop constraint if exists valid_entitlements; -update public.user_access -set entitlements = array_append(entitlements, 'practice') -where 'path' = any(entitlements) and not ('practice' = any(entitlements)); -alter table public.user_access add constraint valid_entitlements check ( - entitlements <@ array['practice','games','tutor','path','library']::text[] -); -``` - -`TEACHER_EMAILS` names the teacher's Google account (several are separated by commas or spaces). After sign-in a teacher chooses between **תרגול**, the same practice a student gets, and **ניהול**, the management panel. The panel lists every student with their usage (recordings, those of the last 7 days, days practised, last practice), progress (sentences practised) and grade (the average of the best scores), and pressing a student opens their own screen in place of the class's: their figures, their homework, every lesson of the class with its topics, and their best and weakest sentences, all counted as the class counts. "חזרה לכיתה", or the browser's back button, returns to the class. - -### Flags: a score that is wrong - -Sometimes a sentence is said right and the system scores it wrong. The teachers (`TEACHER_EMAILS`) report such cases, and `TUTORS_EMAIL` names more accounts that do (several are separated by commas or spaces; they may sign in without being in a class, and get no management panel). The rank is kept by email in the table `tutors`: - -```sql -create table public.tutors ( - email text primary key, - granted_by text not null default '', -- the teacher who gave the rank - created_at timestamptz not null default now() -); -alter table public.tutors enable row level security; -- no policy: only the secret key reads and writes it -``` - -Under each score a teacher or a tutor gets an optional note and the button "🚩 אמרתי נכון, המערכת טעתה". Pressing it sends the recording to `POST /api/flags` (teachers and tutors only), which keeps the structured diagnosis in the table `flags`, written with `SUPABASE_SECRET_KEY`. By default the recording is kept in the private Supabase `flags` Storage bucket. When `AUDIO_BUCKET` names a private Hugging Face Storage Bucket (and the `HF_TOKEN` Space secret can write it), new recordings go only below `AUDIO_BUCKET_PREFIX/flags`. Reads fall back to Supabase for existing recordings. Once, recordings older than the fixed `AUDIO_MIGRATE_BEFORE` boundary are copied to HF and only successfully copied objects are deleted from Supabase; a completion marker prevents later startup scans. Its default is `2026-10-17T00:00:00+03:00`, preserving the seven days that were current when this migration was introduced; that boundary does not move. The row holds: - -- who and what: `email`, `cat`, `phrase`, `he`, `note`; -- the answer the scorer matched (`ar`, `ipa`), its free decode (`heard_ipa`), `score` and `unrelated`; -- `result`: the whole score response, including the structured diagnostic trace (every word and phoneme, pipeline stage, threshold, score, replacement, tip and issue); -- `model` and `version`, so a flag can be checked against the model and build that produced it; -- `audio`: the private Storage object name when the upload succeeded, otherwise null. - -Create the table once (until then a flag is refused with this command in the message): - -```sql -create table public.flags ( - id bigint generated always as identity primary key, - email text not null, -- the tutor - cat text not null, -- the topic id - phrase text not null, - he text not null, -- the Hebrew prompt - ar text not null, -- the accepted answer the scorer matched - ipa text not null, -- its phonemes: what the tutor says was said - heard_ipa text not null, -- the model's free decode - score int not null, - unrelated boolean not null default false, - note text not null default '', - result jsonb not null, -- the whole score response - model text not null, - version text not null default '', - audio text, -- object name in the private flags Storage bucket - status text not null default 'pending', -- pending, confirmed, rejected, or fixed - category text, - reviewed_by text, - reviewed_at timestamptz, - review_note text not null default '', - false_issues jsonb not null default '[]'::jsonb, -- [{word, expected, heard}] - created_at timestamptz not null default now() -); -alter table public.flags enable row level security; -- no policy: only the secret key reads and writes it - -insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types) -values ( - 'flags', - 'flags', - false, - 2000000, - array['audio/webm','audio/ogg','audio/mp4','audio/mpeg','audio/wav','audio/x-wav'] -) -on conflict (id) do update set - public = excluded.public, - file_size_limit = excluded.file_size_limit, - allowed_mime_types = excluded.allowed_mime_types; -``` - -For a `flags` table created before reviews were added, run once: - -```sql -alter table public.flags - add column if not exists status text not null default 'pending', - add column if not exists category text, - add column if not exists reviewed_by text, - add column if not exists reviewed_at timestamptz, - add column if not exists review_note text not null default '', - add column if not exists false_issues jsonb not null default '[]'::jsonb; -``` - -The management panel's **דיווחים** tab lets a teacher open the existing diagnostic dashboard beside the approval action, hear the stored recording, confirm or reject each report, classify it, and select the individual sound judgements that were false. The dashboard includes synchronized recording playback and a copyable, identity-free text payload for asking a chat assistant to distinguish acoustic-model, alignment, rule, threshold and feedback failures and suggest regression tests. The tab groups confirmed `expected → heard` pairs by reports, reporters, and phrases. Only confirmed reports should be used for calibration. - -### The lab (מעבדה) - -A teacher gets a third button on the opening screen, **מעבדה**, beside תרגול and ניהול. It is the recording bench for measuring the phoneme recognition. [app/lab.py](app/lab.py) holds a fixed diagnostic list: hard letters as syllables (حا, عا, صا …), minimal pairs (حال / هال / خال, صيف / سيف, طين / تين …) and each hard sound at the start, middle and end of a word. Before recording an item, the teacher marks how it will be said: right, or with a planted mistake on one sound. `POST /api/lab/score` scores the recording as practice does (with the native baseline and the stage-by-stage trace) and keeps it with that label. The page shows, sound by sound, whether the scorer agreed with the label: a sound said right but flagged, or a planted mistake not flagged. - -The lab opens on **מבחן**, a test that walks one speaker through the whole list. The speaker types their name; the list shows a row per item (what to say, its state, and record / play / accept / reject), grouped like the list, with the next row not yet submitted highlighted. Recording stops by itself on silence and the take plays back at once; after it was heard, **✓ אישור** keeps it as said right (`POST /api/lab/score`, the speaker's name with it) and moves the highlight to the next row, and **✗** discards it so the row can be recorded again. Every row is new, recorded (waiting for approval), submitted or rejected. Submitted comes from the server (`GET /api/lab?speaker=` returns the speaker's own counts as `mine`), so it holds across devices; rejected is remembered on the device; a take waiting for approval lives in the page only. Any row can be recorded out of turn. Keys, on the highlighted row: space records, Enter accepts, Backspace rejects. **הקלטה** is the single-item bench, where a planted mistake can be marked. Under it, **מבחן אוטומטי בקול TTS** runs the same test without a person: a TTS voice (ראאף or חנאן) says each item not yet submitted (`POST /api/lab/tts`), and it is kept as said right by the speaker "TTS ", with a log line per item (its score, what was flagged, ▶ to hear what was sent). It is scored without the native baseline, which is made of the same voice and would measure every sound against itself. A TTS voice is one more near-native speaker for the benchmark, not a replacement for people: compare them in **תוצאות** by speaker. - -The lab has two more tabs. **דיווחים** runs the confirmed score reports through the current scorer, and shows for each sound a teacher marked in ניטור as wrongly flagged whether it is still flagged. **תוצאות** is the benchmark of the lab's recordings as they were scored, per sound: how often a sound said right was flagged (false rejections), how often a planted mistake was caught (detection), the mean score, what was heard instead, and how many confirmed reports name the sound. To rescore everything with the current model and rules and compare against the stored numbers, run `python scripts/bench.py` (`--flags` adds the confirmed reports, `--no-baseline` scores without the TTS baseline, `--speaker` keeps one speaker). Run it before shipping a scoring or model change: false rejections should go down without detection going down with them. - -Recordings go to the table `lab_samples` and, by default, the private Supabase storage bucket `lab`, written with `SUPABASE_SECRET_KEY`. With `AUDIO_BUCKET`, new recordings go only below `AUDIO_BUCKET_PREFIX/lab`; the optional fixed-cutoff migration first copies older recordings to Hugging Face and only then removes their Supabase copies. Until the table exists, the lab still scores but keeps nothing, and shows this SQL to run once: - -```sql -create table public.lab_samples ( - id bigint generated always as identity primary key, email text not null, - item text not null, ar text not null, ipa text not null, speaker text not null default '', - label text not null, -- 'correct' or 'mistake' - mistake jsonb, -- {"index", "expected"}: the sound said wrong on purpose - device text not null default '', score int not null, heard_ipa text not null, - flagged jsonb not null default '[]'::jsonb, -- the sounds the scorer marked down, "expected>heard" - result jsonb not null, model text not null, version text not null default '', - audio text, -- the recording's name in the storage bucket lab - created_at timestamptz not null default now()); -alter table public.lab_samples enable row level security; -insert into storage.buckets (id, name) values ('lab', 'lab') on conflict (id) do nothing; -``` - -### The trace dashboard (לוח אבחון) - -[static/trace.html](static/trace.html) shows one recording through the scorer, for engineers, linguists and teachers. Open it with **לוח אבחון** from a lab recording or from a teacher's phoneme dialog; it reads that result's trace in a new tab. It has four panels that follow one selected sound (hover it anywhere, click to pin, ← → to step): - -1. the frames around the sound: target, competitor and neighbour posteriors, frame energy, and the window it was read from (the end of the sound before to the start of the one after) -2. its score stage by stage: raw GOP → exemptions → cluster cap → native baseline → teacher floor → final -3. the readings the audio was aligned to, with the winning CTC path, and the edit-distance gauge for a different sentence -4. the sentence by word and sound, coloured by score, with how sounds add up to words and words to the sentence. When the recording was scored against native baselines, each TTS voice gets a row of its own scores under the sounds; the voice a sound was measured against (the lowest) is in bold. The waterfall marks every voice on the baseline step, and the table has a column per voice - -The data's schema is [docs/trace-schema.ts](docs/trace-schema.ts). `python scripts/mock_trace.py` regenerates the example (`static/assets/trace-mock.json`, the "דוגמה" button) by running the real scorer on synthetic posteriors, with a substitution, a teacher floor, a native baseline and a noise spike beside a short vowel planted in it (the spike shows how a sound's window takes in its neighbours' blank frames). `?src=mock` opens the example, `?i=6` pins a sound. - -### Calibration proposals - -Beside every confirmed `expected → heard` pattern, the teacher can create a calibration proposal. A proposal is only a draft until the teacher chooses its score floor and applies it, globally or to one of the supporting phrases. Applied rules are loaded into the scorer at startup and refreshed immediately after a proposal is applied, updated, or disabled; Supabase is not queried while scoring each recording. Phrase-specific rules take precedence over global ones. Create the private table once: - -```sql -create table public.calibrations ( - id bigint generated always as identity primary key, - expected text not null, - heard text not null, - floor numeric not null default 70, - scope text not null default 'global', -- global or phrase - phrase text not null default '', - evidence jsonb not null default '{}', -- report/reporter counts and supporting phrase keys - model text not null default '', - status text not null default 'draft', -- draft, applied, or rejected - proposed_by text not null, - reviewed_by text, - reviewed_at timestamptz, - created_at timestamptz not null default now() -); -alter table public.calibrations enable row level security; -``` - -An applied rule raises only the matching phoneme score to its configured floor; it never lowers a score. The score response includes `calibrated` when a rule changed it, so a flagged result records which rule effect was involved. - -### Pronunciation rules - -The scorer reads its dialect and Hebrew-speaker rules from the yaml blocks of `static/assets/pronunciation-rules.md` (see `app/rules.py` and `docs/scoring-dialects-plan.md`). The **ניטור** tab lists every rule taken from that document, under **כללי ההגייה מהמסמך**: which realisations of a letter count as correct, how a mistake is judged (exemptions, per-pair margins), and the feedback each rule gives. All are on by default. A teacher can switch one off, which brings back the behaviour from before the document for every student from the next recording on. The switches are loaded into the scorer at startup and refreshed right after a change. Create the private table once (without it the list still shows, all on, and cannot be changed): - -```sql -create table public.scoring_rules ( - id text primary key, -- a rule of rules.TOGGLES, e.g. dialect_Q, margins, rule_pharyngeal_h - active boolean not null default true, - updated_by text not null default '', - updated_at timestamptz not null default now() -); -alter table public.scoring_rules enable row level security; -``` - -### Levels - -The course has levels, each with a planned number of lessons (`LEVELS` in `app/workbook.py`: שלב א and שלב ב, 10 lessons each). Every lesson in `LESSONS` is keyed by its level and number, `(level, lesson)`; one outside the plan stops the app at startup. The path shows each level under its name, and the lessons not written yet as "בקרוב". Level 1's lessons keep their first ids ("1", topics "l1-…") so recorded attempts and saved classes still match; later levels' are "2.1", topics "l2.1-…". - -A lesson's homework in the workbook, sentences of the student's own, is a topic of the kind `"write"`, the lesson's last ("כתיבת משפטים": `l1-hw`, `l2-hw`). It has no sentences to say: its task and the number of sentences are in `WRITING` in `app/workbook.py`, and on the path it opens a box with the task and a line for each sentence; it is done once every line is filled. Nothing is scored, so the lesson's count of practised sentences leaves it out. - -What a signed-in student writes is sent to the server as they write (`PUT /api/writing/`) and kept in the table `writings`, a row per student and topic, so it follows them to any device (`GET /api/writing`). The device keeps a copy too: lines the server did not take stay there and are sent on the next visit, and without sign-in the copy is all there is. Create the table once in the Supabase SQL editor: - -```sql -create table public.writings ( - user_id uuid not null default auth.uid() references auth.users (id) on delete cascade, - topic text not null, -- the topic id, e.g. l1-hw - lines jsonb not null, -- the sentences as written, "" for one not written yet - updated_at timestamptz not null default now(), - primary key (user_id, topic) -); - -alter table public.writings enable row level security; -grant select, insert, update on public.writings to authenticated; -create policy "students read their own writings" on public.writings - for select to authenticated using (user_id = auth.uid()); -create policy "students add their own writings" on public.writings - for insert to authenticated with check (user_id = auth.uid()); -create policy "students change their own writings" on public.writings - for update to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid()); -``` - -Until the table exists the sentences stay on each student's device and the panel shows none. - -A writing topic can be the class's homework like any topic, and is the one offered: the new-class steps open with it chosen, and in a class's "עריכה" setting a day with no topic chosen picks the written homework of the class's last lesson that has one, so the teacher has only the day to set. The panel then counts its sentences ("נכתבו 3 מתוך 5") in the homework's summary and column, and a student's own screen lists the sentences they wrote under the topic. - -### Classes - -In the panel the teacher opens a class in five steps, a question each: its name (such as "מתחילים אוקטובר 2026"), its level (the class studies all the level's lessons; one not written yet opens to the class once it is), its students (ticked from the accounts the panel knows, or added by Google email, each with an optional nickname), its homework, and a summary to confirm, which says where all of it is changed later. That is the class's edit dialog ("עריכה", beside the class in the panel's list of classes): the name, the lessons one by one (by level: a level's box ticks all of it), the homework, and the students, a line each with the Google email and an optional nickname (`dana@gmail.com דנה`); the class is deleted there too. A student may be in several classes: after the opening screen the page asks which one they practise in now, and the learning path shows that class's lessons (a teacher, and anyone before the first class, sees every lesson and is not asked). The page shows the class under the app's name, and pressing it lists the class's students, with a button to move to another class; each student appears by their nickname in it or else their Google name (no addresses; `GET /api/classmates`). Once there is a class, only the classes' students and the teachers can sign in; before the first one, any Google account can. - -The panel's parts are chosen at its top (**תוכן** is described under "Content and videos"). **כיתות** lists the classes, each with its lessons, students and homework and its buttons: "עריכה", "מחיקה", and "התקדמות התלמידים", which moves to the other part; "כיתה חדשה" opens the steps above. An admin sees, edits and deletes only classes whose `subject` matches the subject assigned to that admin by a developer; developers see every subject. A new class receives its admin's subject on the server, regardless of anything sent by the browser. **תלמידים** starts with one unique list of the students in those visible classes, with an "all students" filter and a filter for each class; every row names the student's classes. An admin with no matching classes sees no students; the old unscoped setup shows every signed-in account before its first class is created. The all-students view uses the student's overall progress, and their page offers buttons for switching into each class's context. A class filter counts only what its students practised since the class was opened (its `created_at`), so a new class starts clean even when its students practised the same lessons before, in another class or in none; the students' own path keeps all they did. Every non-empty recording is a try and completes its exercise, including one that did not match the sentence on the screen (`unrelated`); only an empty recording remains incomplete. The panel still shows unmatched recordings diagnostically as "לא זוהו" beside the student and each topic. A class filter shows the class's figures, its lessons (each with the sentences the class finds hard: the class average of each student's best score under 70) and its students, with the same columns however many lessons the class has: the homework topic (when one is set), all the class's sentences, and the grade. This replaces the former `ALLOWED_EMAILS`, which is no longer read. The lessons limit what the path shows; the sentences themselves stay public, as before. - -### Content and videos - -The panel's third part, **תוכן**, shows the course as the students get it: every level, its lessons (those not written yet as "בקרוב"), and under each ready lesson its topics in the path's order. There the teacher adds a video to a lesson: a name and a Vimeo or YouTube link (any form of a YouTube video's link: `youtube.com/watch?v=…`, `youtu.be/…`, a short, a live), a Vimeo one as Vimeo's share button gives it (`https://vimeo.com/1233611297/d46ac373c8?share=copy&fl=sv&fe=ci`; the hash after the number is what opens an unlisted video, and the sharing parameters are dropped). The video becomes a topic of the lesson, with ▶ on its circle, and pressing it plays it in a box over the page, in Vimeo's player. A lesson may have several videos. A new one is added at the lesson's end, and the ↑ ↓ arrows on its row move it a step at a time to its place: before the first topic, between two topics, beside another video, or after them all. The path shows the lesson in that order. A video is not practised, so it counts in no figure; the path marks the ones a student opened ("✓ נצפה"), on their device only. In the panel each video also has "צפייה" and "מחיקה". Students get the videos of their classes' lessons (`GET /api/videos`); adding, moving and deleting (`POST /api/videos`, `PUT /api/content/order`, `DELETE /api/videos/{id}`) answer teachers only. - -The videos are kept in Supabase like the classes, read and written with `SUPABASE_SECRET_KEY` only. Create the table once (until it exists the lessons have no videos, and adding one is refused with this command in the message): - -```sql -create table public.videos ( - id bigint generated always as identity primary key, - lesson text not null, -- the lesson id, e.g. 2 or 2.1 - title text not null, -- the topic's name on the path - url text not null, -- https://vimeo.com/[/] or https://www.youtube.com/watch?v= - pos int, -- how many of the lesson's own topics stand before it; null = after them all - ord int not null default 0, -- its turn among the videos of one place - created_at timestamptz not null default now() -); -alter table public.videos enable row level security; -``` - -A table made before videos had places needs the two columns (until then its videos show after their lessons' topics, and adding or moving one is refused with this command in the message): - -```sql -alter table public.videos add column if not exists pos int, add column if not exists ord int not null default 0; -``` - -### Daily videos - -Apart from a lesson's videos, each level has a library of daily videos. In **תוכן** each level opens and closes on its name (it stays as it was left, on that device); inside, the card "סרטונים יומיים" lists them; the teacher adds one (a name and a Vimeo or YouTube link) and deletes one there. A video in the library is shown to nobody until a class gives it a day: in the class's dialog ("עריכה"), and in the fourth step of opening a class, every daily video of the class's levels has a date field. The students find them in the "סרטון יומי" button above the path: it opens today's video, and is highlighted until that one is opened (on that device); with no video to watch today it opens the list of the earlier ones, the latest first, so a missed day can be caught up. A video whose day has not come, one without a date, one deleted from the library, or one of a level the class does not study, is not shown. It is the first of three buttons that stay in sight above the path: "סרטון יומי", "תרגול יומי" and "שיעורי בית" (the nearest one still to do). A teacher, who practises in no class, sees the daily videos of every class. - -The library is kept in Supabase, read and written with `SUPABASE_SECRET_KEY` only. Create the table once (until it exists adding a video is refused with this command in the message), and add the `dailies` column to `classes` (see "Classes" below): - -```sql -create table public.dailies ( - id bigint generated always as identity primary key, - level text not null, -- a level id, e.g. 1 - title text not null, - url text not null, -- https://vimeo.com/[/] or a YouTube link - created_at timestamptz not null default now() -); -alter table public.dailies enable row level security; +venv/bin/python -m pytest ``` - -### The daily video's morning reminder - -On the "סרטון יומי" screen a student taps "🔔 הזכירו לי כל בוקר" to get a push notification on that device each morning that one of their classes has a daily video (a teacher's account: any class's). It goes at `PUSH_HOUR` (default 8) Jerusalem time, once a day per device, and opens the daily videos' screen. On an iPhone it works only from the app added to the home screen (iOS 16.4+). See `app/push.py`. - -Set the Space secret `VAPID_PRIVATE_KEY` (make one with `python -m app.push`; changing it drops every device's subscription), and create the table once: - -```sql -create table public.push_subs ( - endpoint text primary key, -- the browser's push address - email text not null, - p256dh text not null, - auth text not null, - every_class boolean not null default false, -- a teacher's: every class's daily videos - last_daily date, -- the day it was last reminded - created_at timestamptz not null default now() -); -alter table public.push_subs enable row level security; -revoke all on public.push_subs from anon, authenticated; -``` - -The Space must be awake at that hour for the reminder to go. - -### Video library - -Apart from those there is one library of videos for the whole course: they are not part of the path, need no date, and are the same for every level and class. In **תוכן** the card "ספריה דיגיטלית" comes first, above the levels; the teacher adds a video (a name and a link), renames one, moves one up or down the list with its arrows, and deletes one there. A permitted student enters it from the "ספרייה דיגיטלית" button on the opening screen: a list appears, and a video plays in the same box as the path's videos. The ones opened are marked (on that device). Create the table once (its `level` column stays empty): - -```sql -create table public.library ( - id bigint generated always as identity primary key, - level text not null, -- a level id, e.g. 1 - title text not null, - url text not null, - ord int, -- its turn in the list; none until the teacher moves a video - created_at timestamptz not null default now() -); -alter table public.library enable row level security; -``` - -A library made before its videos could be moved has no `ord` column; until it is added, moving a video is refused with this command in the message (renaming works without it): - -```sql -alter table public.library add column if not exists ord int; -notify pgrst, 'reload schema'; -``` - -For the video to play inside the page, its Vimeo privacy setting has to allow embedding (on this site's domain, or anywhere); a YouTube video has to allow embedding too (it does unless its owner turned that off), and plays from `youtube-nocookie.com`. - -Keeping the link from being passed on is done in Vimeo, not here: the page has to give the browser the player's address, so a student who looks for it finds it (the server only keeps the link itself, `url`, for teachers). In the video's privacy settings on Vimeo, hide it from vimeo.com and allow embedding on specific domains only, naming the app's own domain (on Hugging Face, `-.hf.space`). The video then plays inside the app and nowhere else: its link, opened or embedded anywhere else, shows nothing. Domain-level privacy needs a paid Vimeo plan. - -#### Managing the library and class rosters from Grok - -The app can expose a restricted [Model Context Protocol](https://modelcontextprotocol.io/) connector so a teacher can manage the digital library and basic class rosters, and read privacy-preserving monitoring data, from Grok Business without receiving a Supabase secret, Hugging Face token, or general administrator access. It exposes fifteen tools: four library tools, three class tools, and eight read-only monitoring/analytics tools: `get_activity_timeseries`, `get_current_activity`, `get_system_health`, `get_exercise_difficulty`, `get_pronunciation_difficulties`, `get_learning_progress`, `compare_class_difficulties`, and `get_learning_bottlenecks`. Class changes are limited to creating or renaming a class and adding or removing one student at a time. - -The connector uses the teacher's existing Supabase account through OAuth 2.1. Grok receives revocable user tokens; it never receives `SUPABASE_SECRET_KEY`. On every MCP request the app asks Supabase who the token belongs to and freshly checks that the account still has `management.access`, so removing the admin/developer role stops the connector even if Grok still has a token. - -Configure Supabase first: - -1. In **Authentication → Signing Keys**, migrate to an asymmetric signing key such as ES256 or RS256 if the project still uses HS256. -2. In **Authentication → URL Configuration**, set **Site URL** to the public app origin, for example `https://-.hf.space`. -3. In **Authentication → OAuth Server**, enable the OAuth 2.1 server and **Dynamic Client Registration**. Set **Authorization Path** to `/oauth/consent`. -4. Keep explicit user approval enabled. Dynamic registration lets compatible MCP clients register their exact callback URL, so periodically review OAuth applications and grants in Supabase. - -In Hugging Face **Settings → Variables and secrets**, keep `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, and `SUPABASE_SECRET_KEY` configured as described above. `SUPABASE_SECRET_KEY` remains a Space secret. `MCP_PUBLIC_URL` is normally inferred from `SPACE_HOST`; for a custom domain set it to the exact final URL: - -```text -https:///mcp/ -``` - -Restart the Space, then check these URLs in a browser: - -- `https:///.well-known/oauth-protected-resource/mcp/` names the MCP resource and the Supabase authorization server. -- `https://.supabase.co/.well-known/oauth-authorization-server/auth/v1` is Supabase's OAuth discovery document. -- `https:///oauth/consent` shows an error without an active `authorization_id`; that is expected because Supabase supplies it during sign-in. - -In Grok Business, add a Custom MCP connector whose final Streamable HTTP URL is: - -```text -https://-.hf.space/mcp/ -``` - -Only the name and Server URL are needed. Grok should discover Supabase OAuth and display **Sign in**. Sign in with an account that currently has the app's admin or developer role, review the client name, callback and scopes on the Hebrew consent screen, then approve. A student or a user without `management.access` cannot approve or call MCP tools. - -Grok must first call `prepare_library_video`; the app returns the normalized title, URL and a single-use approval that expires after ten minutes. Only after the teacher explicitly confirms those details should Grok call `publish_library_video`. Pending approvals disappear safely when the Space restarts. - -Class mutations use the same two-step rule. Useful prompts include: - -```text -הצג את הכיתות והתלמידים שאני רשאי/ת לנהל. -הכן יצירת כיתה בשם "מתחילים נובמבר" עם התלמיד dana@example.com. אל תבצע עדיין. -הכן הוספת התלמיד noam@example.com לכיתה 12 עם הכינוי נועם. אל תבצע עדיין. -הכן הסרת התלמיד old@example.com מכיתה 12. אל תבצע עדיין. -``` - -Grok calls `prepare_class_change` with one of `create_class`, `rename_class`, `add_student`, or `remove_student`, then displays the normalized class and email details. After the teacher checks them and explicitly confirms, Grok calls `publish_class_change` with the returned approval id. The approval lasts ten minutes, is single-use, is bound to that signed-in manager, and is refused if the class or the manager's subject scope changed meanwhile. - -Class listing returns only class id, name, subject, student emails, and optional class-local nicknames. The connector cannot delete a class or account, create a login account, change roles or entitlements, assign teachers, edit lessons/homework/daily videos, or read individual progress, grades, raw attempts, or recordings. Those operations remain in the browser management screen where applicable. Adding an email to a class does not create a Supabase user; that student still signs in normally. - -The eight monitoring and analytics tools are read-only and do not use the prepare/publish flow. `get_activity_timeseries` accepts `from_date` and `to_date` as `YYYY-MM-DD`, an `interval` of `day` or `week`, and an optional `class_id`. The inclusive range is limited to 90 non-future days. With a class id it returns only that authorized class; without one it combines only classes currently visible to the signed-in manager. The response contains aggregate attempts, the peak simultaneously active learners reported by the source buckets, and a range summary. It never contains student names, emails, ids, individual scores, answers, recordings, or exact login events. - -`get_current_activity` accepts `window_minutes` equal to `5` or `15` (default `5`) and an optional `class_id`. `recent_practice` counts distinct learners and stored attempts in that window but explicitly sets `presence_claim: false`: a recent attempt does not prove that the learner is still connected. `live_presence` is available only during an explicit live-class session and defines active as a heartbeat within 20 seconds of `checked_at`; this process-local state disappears when the session ends or the service restarts. Combined class scopes deduplicate learners enrolled in more than one class. - -Attempt-derived counts and timestamps require at least three learners and five attempts. Live counts of one or two are suppressed. The result distinguishes `no_activity` (a successful window query with no events), `no_data` (no observation establishes a last time), `unavailable` (the source does not apply or cannot be read), and `suppressed` (data exists but privacy prevents display). Stored attempts are current as of the database read, not real-time telemetry. Timestamps use UTC ISO 8601 and include an `Asia/Jerusalem` rendering. The app stores no durable general application-activity event, so `last_application_activity` is `unavailable`; lack of practice does not mean the app was unused. No new tracking or retention policy is introduced. - -Example current-activity response (shortened): - -```json -{ - "ok": true, - "class_id": 12, - "checked_at": "2026-10-27T10:10:00Z", - "timezone": "Asia/Jerusalem", - "privacy_threshold": {"minimum_students": 3, "minimum_attempts": 5}, - "recent_practice": { - "status": "complete", - "source": "persisted_attempts", - "window_minutes": 5, - "students_with_activity": 4, - "attempts": 7, - "presence_claim": false, - "update_semantics": "current_as_of_database_read" - }, - "live_presence": { - "status": "complete", - "source": "live_session_heartbeat", - "active_users": 3, - "heartbeat_ttl_seconds": 20, - "ephemeral": true - }, - "last_practice_activity": { - "status": "complete", - "event_type": "practice_attempt", - "timestamp": "2026-10-27T10:09:40Z", - "local_timestamp": "2026-10-27T12:09:40+02:00", - "timezone": "Asia/Jerusalem", - "elapsed_seconds": 20 - }, - "last_application_activity": {"status": "unavailable", "reason_code": "not_collected"} -} -``` - -`get_system_health` returns only `healthy`, `degraded`, `unavailable`, or `not_configured` for the application, authentication, database, model, and text-to-speech components. `not_configured` for an optional component such as text-to-speech does not by itself make the service unhealthy. A developer may also see a non-sensitive build version and revision. No caller receives raw logs, stack traces, database queries, internal URLs, paths, environment variables, tokens, credentials, or secret-presence values. - -`get_exercise_difficulty` accepts `from_date`, `to_date`, optional `class_id` and `exercise_type`, `limit` from 1 through 20 (default 20), and `min_attempts` (default 10). Exercise types are `pronunciation`, `verb`, `game`, and `other`. It returns stable exercise metadata, attempts, students, successes, success rate, attempts per student, average score, retry metrics, and rank. Existing aliases such as `exercise`, `category`, `attempt_count`, and `student_count` remain available. - -`get_pronunciation_difficulties` uses only stored, actually flagged phoneme diagnostics. It returns a phoneme, safe example words, error attempts, affected students, average diagnostic score, and weekly trend. Stored `issues` do not contain every correctly pronounced exposure, so `pronunciation_error_rate` is currently `null` rather than a fabricated denominator. Missing optional diagnostic columns produce `status: "insufficient_data"`. - -`get_learning_progress` returns `day`, `week`, or `month` buckets with attempts, students, success and average score, first-observed-attempt success, retry success, and an aggregate score trend. Its optional stable `exercise_id` restricts the report. “First” means first persisted attempt inside the requested range, not lifetime first. - -`compare_class_difficulties` applies the same definitions to optional `class_ids` and normalizes by participating students and observed exercise exposure. Overlapping enrollment is disclosed because class populations may not be mutually exclusive. - -`get_learning_bottlenecks` returns qualifying exercise/topic aggregates for affected students, repeated-failure rate, attempts until success, improvement rate, and severity. Measured `observed_metrics` remain separate from `suggested_intervention`, which is labeled `recommendation` and lists its measured basis. - -Across analytics, success means `score >= 75` and `unrelated = false`. A group requires at least three students and `max(5, min_attempts)` attempts. Sparse groups are suppressed or marked `insufficient_data`; missing metrics are `null`. Dates use ISO 8601 and Asia/Jerusalem calendar boundaries. - -An exercise is omitted unless at least three students made at least five attempts in the selected scope and period. Suppressed exercise names and counts are not revealed. “Did not reach success” is not called abandonment because attempt data cannot show whether somebody quit. The report never returns student identities, individual scores or sequences, timestamps, answers, recordings, or pronunciation traces. If the bounded attempt safety cap is reached, the tool refuses partial analysis and asks for a narrower date range or one class. - -Example difficulty response (shortened): - -```json -{ - "ok": true, - "status": "complete", - "from_date": "2026-10-01", - "to_date": "2026-10-26", - "privacy_threshold": {"minimum_students": 3, "minimum_attempts": 10}, - "exercises": [{ - "exercise_id": "a1b2c3d4e5f6", - "exercise_name": "תרגיל לדוגמה", - "exercise_type": "pronunciation", - "topic": "הגופים", - "total_attempts": 18, - "unique_students": 6, - "successful_attempts": 7, - "success_rate": 38.9, - "average_attempts_per_student": 3.0, - "average_score": 61.4, - "difficulty_rank": 1 - }] -} -``` - -Persisted data has no reliable session boundaries, abandoned or incomplete exercise event, unanswered exposure, or item-level correctness for every non-spoken game. The tools therefore do not report sessions or abandonment and return `insufficient_data` for untracked answers. Trends describe association; they do not prove that practice caused learning. - -Useful read-only prompts include: - -```text -הצג גרף פעילות יומי של כיתה 12 בין 2026-10-01 ל-2026-10-26. אל תציג מידע אישי. -הצג פעילות שבועית בכל הכיתות שאני מורשה/ית לראות ב-30 הימים האחרונים. -הצג פעילות עדכנית בכיתה 12 בחמש הדקות האחרונות, והפרד בין נוכחות חיה לבין תרגול שנשמר. -מתי היה ניסיון התרגול האחרון בכל הכיתות שאני מורשה/ית לראות? ציין UTC וגם שעון ישראל ואל תציג מידע אישי. -בדוק את מצב המערכת והסבר בעברית פשוטה אם רכיב כלשהו אינו זמין. -הצג את 10 התרגילים הקשים ביותר בכל הכיתות שאני מורשה/ית לראות בין 2026-10-01 ל-2026-10-26. הסבר את המדדים ואל תציג מידע אישי. -אילו פונמות ערביות גרמו לשגיאות הגייה מדודות בכיתה 12 בחודש האחרון? הצג רק קבוצות שעוברות את סף הפרטיות. -הצג התקדמות שבועית בכיתה 12, כולל הצלחה בניסיון הראשון לעומת ניסיונות חוזרים. -השווה בין כיתות 12 ו-15 לפי הצלחה, השתתפות וחשיפה לתרגילים. ציין אם יש תלמידים החופפים בין הכיתות. -מצא צווארי בקבוק בלמידה בכל הכיתות המורשות. הפרד בבירור בין המדדים שנמדדו לבין המלצות הוראה. -``` - -The connector does not make calls to the xAI API, so an xAI developer API key and API credits are not required. Grok Business plan or connector charges, if any, are handled by xAI. To revoke one connector, revoke its grant or OAuth application in Supabase. To revoke the teacher immediately, remove their admin/developer role in the management screen; the next MCP request is refused. Existing videos and ordinary management-screen access are otherwise unaffected. Connector mutations appear in the existing management activity log when its optional `system_events` table is installed; application logs are the fallback and never contain OAuth tokens. - -If Grok does not show sign-in, confirm that both discovery URLs return JSON, dynamic registration is enabled, `MCP_PUBLIC_URL` matches Grok's URL including `/mcp/`, and the Space uses HTTPS. A registration 403/404 usually means dynamic registration is disabled. `invalid_grant` usually means the authorization code expired, its PKCE verifier differs, or the registered callback does not match exactly; remove and recreate the connector. A consent-page configuration error means the Site URL, Authorization Path, or Supabase public settings are missing. OAuth configuration failures never fall back to public or shared-secret access. - -### Texts - -In **תוכן** the teacher also adds texts to a lesson, as many as they like: a story or a dialogue, practised like the course's own conversation "שיחת היכרות". "+ הוספת טקסט" under the lesson asks for a name and the text twice, a line per sentence: in Hebrew, and the same lines, in the same order, in Arabic written in Hebrew letters as the course writes it (`כִּיף חַאלַכּ?`). Each is pasted into its box, or read into it from a plain-text file (`.txt`, UTF-8), and can be corrected there before it is added. Empty lines are not counted, and the two must have the same number of lines; otherwise the text is refused, with both numbers in the message. The kind is chosen there too: - -- **סיפור**: every line is played in the voice the student chose in the settings. -- **שיחה**: the teacher picks a voice for the odd lines and one for the even lines, from the app's voices. - -The text becomes a topic of the lesson, laid out as a story: all its lines on the card, said one after the other, with "השמעת השיחה" and "בוחן". It counts like any topic (the lesson's progress, the class's figures, and it can be the homework). Like a video it is added at the lesson's end and moved with the ↑ ↓ arrows on its row, past topics, videos and other texts (`PUT /api/content/order` places both kinds); "תרגול" on the row opens it as the students get it, and "מחיקה" removes it (the recordings made in it stay in `attempts`, uncounted). - -The scorer needs each line's phonemes and the answer's voice needs it in Arabic letters, so the server reads the transliteration word by word (`app/texts.py`). A word of the course is found by its transliteration and gets the course's own Arabic spelling and accepted pronunciations (dagesh, shva and final letters are not compared; an unpointed word is found by its letters alone). Any other word is read by rule from its letters and points, which is close but not always right: vowel length and some spellings (ى, ء, ة) cannot be told from the transliteration, and an unpointed word outside the course has no vowels to read. The panel lists those words on the text's row ("מילים שאינן במילון הקורס נקראו לפי הכתיב"), so the teacher knows which lines are worth a listen. A word the course should know is added to `LEX` in `app/workbook.py`. - -A line's phrase id is past the bank's (1,000,000 + the text's id × 1000 + the line's number) and its topic is `t`; an attempt is recorded under that topic and the line's Hebrew (numbered from its second appearance in the text: `כן (2)`). The texts are kept in Supabase like the videos (`GET /api/texts` gives a student those of their classes' lessons; `POST /api/texts` and `DELETE /api/texts/{id}` answer teachers only). Create the table once: - -```sql -create table public.texts ( - id bigint generated always as identity primary key, - lesson text not null, -- the lesson id, e.g. 2 or 2.1 - title text not null, -- the topic's name on the path - voices text[] not null default '{}', -- a dialogue's two voices by name (odd lines, even lines); empty = a story - lines jsonb not null, -- [[Hebrew, Arabic in Hebrew letters], ...] - pos int, -- as in videos - ord int not null default 0, - created_at timestamptz not null default now() -); -alter table public.texts enable row level security; -``` - -### Homework - -#### מדריך להגדרת שיעורי בית - -1. היכנסו לחשבון המורה ובחרו **ניהול** ואז **כיתות**. -2. בכיתה קיימת לחצו **עריכה**. בעת יצירת כיתה חדשה מגיעים לאותה הגדרה בשלב **שיעורי הבית**. -3. בשורת שיעורי הבית בחרו נושא מתוך השיעורים שסומנו לכיתה. אפשר לבחור גם **כתיבת משפטים** או טקסט שהוספתם לשיעור. -4. בחרו תאריך הגשה, אם יש. אפשר להשאיר את התאריך ריק כדי להציג את המטלה ללא מועד אחרון. -5. כדי לתת יותר ממטלה אחת לחצו **+ הוספת שיעורי בית**, ובחרו נושא ותאריך לכל שורה. אי אפשר לבחור את אותו נושא פעמיים. -6. לחצו **שמירה**. התלמידים יראו מעל הנושא במסלול בועה של **שיעורי בית** ואת הזמן שנותר להגשה; השינוי יופיע אצלם בפתיחה הבאה של העמוד. - -כדי לשנות מטלה חוזרים ל־**ניהול** → **כיתות** → **עריכה** ומשנים את הנושא או התאריך. להסרת מטלה לוחצים **✕** בשורה שלה ושומרים. התקדמות והשלמה מופיעות בכרטיס הכיתה ובמסך התלמידים; מטלת תרגול מושלמת לאחר שכל משפטי הנושא תורגלו, ומטלת כתיבה לאחר שכל השורות מולאו. - -A class may have several homeworks at once, a topic each with its own day: for example the last topic of every lesson it studies (the written homework). In the class's dialog each is a row, the topic (named with its lesson) and its day, with "✕" to drop it; "+ הוספת שיעורי בית" adds the last topic of the first ticked lesson that has none yet. A topic can be homework once. On the path every homework topic has its own bubble; in the panel the class's card has a line per homework (who finished, who is still to), the tile counts the students who finished all of them, and the students' "שיעורי בית" column sums them. They are kept in the class's `homeworks` column, `[{"topic", "due"}]`; `homework` and `due` hold the first, as before. What follows describes one homework. - -In the class's dialog the teacher sets its homework: one topic of the class's lessons (for example the conversation "שיחת היכרות"), and optionally the day it is due. The students see it on the path itself: the bubble over the homework topic's circle says "שיעורי בית" and counts the days left ("נשארו 3 ימים", "מגישים מחר!", "מגישים היום!", and "באיחור של יומיים" once the day passed). On the last two days the bubble and the circle's ring turn amber, and red after them. The path's focus does not move: the page still opens on the next topic in the course's order, which keeps its own "התחילו" bubble, so the homework's bubble may be further up or down the path. The homework is done when every sentence of the topic was practised, whenever that was; the circle then gets its ✓ and "שיעורי הבית הושלמו". In the panel the class's card shows how many students finished and names those who did not, the furthest from it first. The students get the homework with `GET /api/me`, so a change reaches them the next time they open the page. - -### The daily session - -Above the path, "תרגול יומי" opens a session of 10 sentences, so a student with five minutes does not have to choose a topic. Half are sentences they already said with a best score under 75, drawn from their 15 weakest (so the same ones do not return every day); the other half are new, in the course's order, from the homework topic and then the rest of its lesson, or else from the first unfinished lesson. When one kind runs short the other fills in; stories are left out. At the end the page says how many were practised and how many scores went up, and the button shows "✓ תרגלתם היום" for the rest of the day (kept on the device, and only when at least half the sentences were said). The numbers are `DAILY` in `static/index.html`. Driving mode works in a session too. - -Under the microphone of a sentence translated from Hebrew to Arabic (the topics named "... עברית ← ערבית"), "תרגול שקט ללא ציון" swaps it for an exercise on the same sentence, for a student who cannot speak where they are: the sentence is built from its words, tapped in order out of a bank that also holds a few words of the topic's other sentences. The other topics (words, conjugations, Arabic → Hebrew, conversations) have no such exercise: their card keeps the microphone and has no button. Nothing is scored or sent to the server: a sentence answered this way is still not practised, on the path, in the homework and in the teacher's panel. It gets a mark of its own, kept on the device ("✓ בלי מיקרופון" in the list of sentences), and leaves the round. The mode lasts until the button is pressed again or the page is reloaded. `SILENT` in `static/index.html` is how many words too many the bank holds. - -A sentence translated from Arabic to Hebrew (the topics named "... ערבית ← עברית") can be answered in either language. Its card has two microphones side by side, each with its language's letter on it and a line under it: the filled one (ع, "קוראים בערבית") records the Arabic read aloud and scores its pronunciation, as before; the outlined one (א, "מתרגמים לעברית") listens to the translation instead, with the browser's own speech recognition (`he-IL`), and compares the words it heard with the translation. The card then shows the translation's grade, 0 to 100: the share of the translation's words that were heard, in order ("ציון התרגום: 70"), with what was heard and how many words matched, since a correct translation may be worded differently. 80 or more is "תרגום נכון" and shows the translation. The grade is shown only: it is not kept and does not reach the server, so such a sentence is still not practised and the teacher's panel counts the Arabic recordings alone. In Chrome the audio goes to Google's recognition service (the privacy page says so). A browser without speech recognition (Firefox) has the Arabic microphone alone, as before, and one that turns out not to recognise Hebrew drops the Hebrew one after the first try. The numbers are `HE` in `static/index.html`. - -### The story quiz - -A story topic (a conversation, all its lines on the card) has a "בוחן" button: the student says the whole conversation in one go. Each line is recorded by itself, once; its score appears beside the line, its Arabic under it, and the next line is recorded, with no answer played and no second try. A line that was not heard waits for the microphone button; one that was skipped comes back after the last line. At the end the card shows the grade (the average of the lines' scores, and how many were said) and the three mistakes made most often across the lines, each with its tip and the words it was heard in (`QUIZ` in `static/index.html`). "סיום הבוחן" ends it early on the lines said so far. The lines are recorded as attempts like any other, so a quiz counts in the student's progress and in the panel. Story driving mode is listening-only and asks how to play the conversation: **Arabic only** plays every Arabic line with a three-second pause between lines; **Hebrew then Arabic** reads each Hebrew sentence followed by its Arabic line. Throughout driving mode, the screen shows the Hebrew meaning and the spoken Arabic in Hebrew transliteration together. - -The classes are kept in Supabase and read and written with `SUPABASE_SECRET_KEY` only, so the table has row-level security and no policy. Create it once: - -```sql -create table public.classes ( - id bigint generated always as identity primary key, - name text not null unique, -- e.g. אוקטובר 2026 - emails text[] not null default '{}', -- the students' Google emails, lower-case - nicknames jsonb not null default '{}', -- {email: nickname}, for the students who have one - lessons text[] not null default '{}', -- the lesson ids the class studies; empty = every lesson - subject text not null default 'ערבית', -- existing classes; new ones copy the creating admin - teachers text[] not null default '{}', -- class-scoped teachers; each address is also in emails - homework text not null default '', -- the topic id the class practises now, e.g. l2-talk; '' = none - due date, -- the day the homework is due, if it has one - homeworks jsonb not null default '[]', -- every homework of the class: [{"topic": ..., "due": ...}] - dailies jsonb not null default '[]', -- the days of its daily videos: [{"video": id, "date": ...}] - created_at timestamptz not null default now() -); -alter table public.classes enable row level security; -``` - -A table made before the nicknames, the lessons, the homework or the daily videos needs the columns (a class saved before then studies every lesson and has no homework). Until they are there, saving a class in the panel is refused with this command in the message: - -```sql -alter table public.classes add column if not exists nicknames jsonb not null default '{}', - add column if not exists lessons text[] not null default '{}', - add column if not exists homework text not null default '', - add column if not exists due date, - add column if not exists homeworks jsonb not null default '[]', - add column if not exists teachers text[] not null default '{}', - add column if not exists dailies jsonb not null default '[]', - add column if not exists subject text not null default 'ערבית'; -update public.classes set subject = 'ערבית' where subject = ''; -``` - -A subject is one of a fixed list, `SUBJECT_LABELS` in `app/main.py`: ערבית (served at `/`, and `/arabic`) and מתמטיקה (`/math`). It is kept in Hebrew, as the panel shows it, and the developer picks an admin's subject from that list in Monitoring; any other text is refused. `/api/me` names the subject of the account (an admin's own, or their class's), and the page moves an account that opened another subject's address to its own. A developer stays where they are. Each subject has its own title, colour and opening screen (`SUBJECTS`), and the installed app of a subject other than Arabic has its own manifest, `/math/manifest.webmanifest`, so its home-screen icon is named for it and opens `/math`. - -**The math path.** On `/math`, the start button opens the math path (`static/math.js`, `static/math.css`) in place of the Arabic one. Its skill graph is `app/math_algebra9.json`, served at `/api/math/skills`: 9th-grade algebra for the 5-unit track, by the Ministry's 2026/27 teaching schedule, in six units (foundations, algebraic technique, function properties, the quadratic function and equation, powers and roots, word problems). Each skill names the skills it needs, and has a sample question whose wrong choices are each a known mistake; a mistake sends the student back to the skill it comes from. `app/math_skills.py` refuses a graph with a cycle, an unknown skill, a redundant link, a question without exactly one right answer, or a mistake that sends a student forward. A skill is open once every skill it needs is done, and the path goes on from the first open one; a locked skill can still be opened, with the skills to do first named on it. For now a right answer to the sample question marks the skill done, and progress is kept on the device (`ihki.math.v1:`). The questions were written for the app and still need a 5-unit teacher's review. - -**A photographed problem.** "צילום תרגיל" on the math path lets a student photograph a problem (camera or gallery). The page shrinks the photo to about 1600 px and sends it to `POST /api/math/photo`, where Claude (`app/math_photo.py`) reads it and solves it in a fixed JSON shape: the problem as read, the solution as steps (each a hint and the step itself), the answer, and the path's skill it practises. The sheet shows the reading and one hint; further hints, each step and the full solution with its answer come only when the student asks, and the skill links into the path. It runs on the tutor's Anthropic key (`ANTHROPIC_API_KEY`, or the panel's) and on `MATH_MODEL` (default `claude-opus-5-5`, with the API's default fallback model when it declines). A signed-in student has `DAILY_PHOTOS` (20) a day, counted in memory; a photo it could not read does not count, and developers are exempt. The photo is sent to Anthropic's API and not kept by the app; say so on the privacy page before students use it. - -The server keeps the list for a minute, so a change in the SQL editor (rather than the panel) applies within that time. - -### Current lesson and the focused home - -The first time a student enters an Arabic class, the page asks which lesson the class is on now, offering only that class's lessons. A class must study lessons of exactly one level for this: a class whose lessons span several levels, or that names no lessons (and so studies every one), shows a message asking to correct the class instead. Math classes keep their own path and are not asked. After the choice the student lands on a focused home of at most three actions, the first three that apply of: the nearest-due homework, today's daily video when not yet watched on that device, continuing the current lesson, and a short review of up to five sentences. The full path is one button away and opens at the current lesson. "שינוי" next to the lesson changes it; no progress is lost. Teachers, and the open service without sign-in, keep the path as their first screen. - -Each student's choice is kept for each class in the table `learner_class_state`, read and written with `SUPABASE_SECRET_KEY` only. Create it once: - -```sql -create table public.learner_class_state ( - email text not null, -- the student's sign-in email, lower-case - class_id bigint not null references public.classes (id) on delete cascade, - current_lesson text not null, -- a lesson id of the class, e.g. 5 or 2.3 - created_at timestamptz not null default now(), - updated_at timestamptz not null default now(), - primary key (email, class_id) -- one current lesson per student and class -); -alter table public.learner_class_state enable row level security; -revoke all on public.learner_class_state from anon, authenticated; -``` - -`/api/me` gives each of the student's classes its `id`, its `level` (`null` when it is not one level), the `current_lesson` the student chose (`null` before a choice, or when the class no longer has that lesson) and `teacher: true` for a class the account teaches. The page saves a choice with: - -``` -PUT /api/me/classes/{class_id}/current-lesson -{"lesson": "5"} -``` - -The server takes the email from the session, never from the body. It answers 401 without sign-in, 403 for a class the caller does not study in (or teaches), 409 for a class that is not one level, 400 for a lesson that is not the class's, and 503 when Supabase cannot save it. Until the table exists, or while Supabase is unreachable, the page keeps the choice on the device under `ihki.lesson.v1:::`, so another account on the same device does not get it, and sends it again on a later sign-in. A choice the server has wins over the device's. - -### Live lessons - -An admin can choose several class teachers from the class's own student list. The assignment is scoped to that class: -a person may teach one class and remain a student in another. Developers can edit every class; subject admins can edit -only classes in their assigned subject. - -A class teacher opens **שיעור חי**, chooses their class and starts its single active session. Students see active sessions -from their registered classes in the same tab. A six-digit code is also shown as a fallback; any signed-in account may -join with it for that session only, without becoming a member of the class. The teacher chooses an existing sentence or -searches the class's lesson bank by Hebrew text. No translation or LLM is used. - -The page polls every three seconds. The teacher sees the whole roster, who has joined, who is online, and each submitted -score; a student sees only their own result. Every student gets one scored attempt per question. Empty or undecodable -recordings may be retried because they never became a scored attempt. Session state, attendance and answers live only in -the application process: ending a session, restarting the server, or two hours without a teacher heartbeat erases them. -No live-session audio or result history is stored. - -The **ניטור** activity dashboard can switch between the last 5 minutes, 24 hours and 7 days. It also reports Sawtak cache usage: cached WAV count, disk size, cache hits, generated misses, and the hit rate since the current server process started. **הכנת כל ההקלטות** asks for a Sawtak voice id and starts a resumable, sequential background pass over every unique phrase for that voice; existing files are skipped and progress is returned by `GET /api/tts-cache`. Recordings are stored below `TTS_CACHE_DIR//`. When `TTS_BUCKET` is set (a Hugging Face Storage Bucket id, e.g. `wzender/ihki-storage`, with an `HF_TOKEN` secret that can write to it), the sweep also uploads every WAV through the Bucket API to `TTS_BUCKET_PREFIX//` (default prefix `ihki-tts`), and at the end uploads every other WAV already on disk; files already in the bucket with the same size are skipped. Writing to a mounted `/data` folder alone was not reaching the bucket, and was slow (the mount flushes a few files every ~30 s), so with `TTS_BUCKET` set new recordings are written to a local staging folder (`TTS_STAGING_DIR`, default `/tmp/ihki-tts-staging`) and uploaded through the API: the sweep in batches of 25 (at least every 30 s), playback misses one by one in the background. Lookups check the staging folder first, then `TTS_CACHE_DIR`. Clearing the cache resets the process counters and leaves the bucket untouched. - -The panel reads through `GET /api/class`, which answers teachers only. Row-level security hides other students' rows from the teacher's own token, so the server reads them with `SUPABASE_SECRET_KEY` (Supabase → Project Settings → API Keys → secret key). Unlike the publishable key it must stay private: set it as a Space **secret**. Without it the panel opens and says the data is not available. - -The same figures as a query, in the SQL editor (it runs as the owner, so it sees every student): - -```sql -select u.email, a.cat, count(distinct a.phrase) as phrases, count(*) as tries, - round(avg(a.score)) as average, max(a.created_at) as last_practice -from public.attempts a join auth.users u on u.id = a.user_id -group by u.email, a.cat order by u.email, a.cat; -``` - -## Fine-tuning - -The base model was not trained on Palestinian Arabic, and it has heard few Hebrew-accented pharyngeals. Expect the zero-shot scores to be noisy until it is fine-tuned on matched data. - -**Data to collect** (CSV format in `data/example.csv`): -- **Native recordings.** The teacher records each phrase 3–5 times, labelled with the main variant's IPA. -- **Learner recordings, labelled with what was actually said.** Label الحمام said with ח as `… χ a m m aː m`, not `ħ`. These rows teach the model the exact contrasts the tips depend on (ħ/χ, ħ/h, ʕ/ʔ, ʔ/k, r/ʁ, emphatics). - -A few hundred utterances are enough to move the needle with the feature encoder frozen. - -```bash -pip install -r requirements-train.txt -python -m train.finetune --train data/train.csv --eval data/eval.csv --out checkpoints/ihki-ft -MODEL_ID=checkpoints/ihki-ft uvicorn app.main:app --host 0.0.0.0 --port 7860 -``` - -The script reports phoneme error rate (PER) before and after training. A GPU is advisable. A free Colab T4 handles a few hundred clips in minutes. - -### POWSM on the Space - -The most accurate model so far is POWSM-CTC fine-tuned on the Minerva lines with short padding (`train/finetune_powsm.py --pad random`). On the test lines it reaches 16.5% PER, against 38% for the base model, at 0.57 s a recording on 2 CPU threads. See [docs/zero-shot-phone-models.md](docs/zero-shot-phone-models.md). The Space runs it as an ONNX export, with `onnxruntime` and torch only. ESPnet, which training needs, stays on the training machine. - -1. **Export it.** Do this once per fine-tune, on the training machine, after running `scripts/setup_zero_shot_deps.sh`: - ```bash - python scripts/export_powsm_onnx.py checkpoints/powsm-ft-shortpad/powsm_ft.pt exports/powsm-ft-shortpad \ - --check data/minerva/dev.csv # each frame's best phone should match ESPnet's 100% - ``` -2. **Upload the folder to a Hub model repo.** Use `--private` if the model should not be public: it was trained on the copyrighted Minerva clips. - ```bash - hf repo create wzender/ihki-powsm-ft --repo-type model --private - hf upload wzender/ihki-powsm-ft exports/powsm-ft-shortpad . - ``` -3. **Point the Space at it.** In the Space settings, set the variable `MODEL_ID=powsm-onnx:wzender/ihki-powsm-ft`, and `POWSM_ONNX_THREADS=2` to match the free hardware. For a private repo, also add a secret `HF_TOKEN` with read access. The Docker build passes that secret only to the step that downloads the model, which is then baked into the image. Saving the settings rebuilds the Space. `/api/health` and the panel's פרטי המערכת show the model. - -Settings: -- `POWSM_ONNX_PRECISION=int8` loads the int8 copy: 0.38 s a recording, but ط and ص drop by 5–6 points. -- `POWSM_PAD_SECONDS` overrides the padding (default: what the export was trained for, none here). -- `POWSM_ONNX_REPO` names the repo the panel's "מודלים" tab offers. - -To roll back, delete `MODEL_ID`. The Space rebuilds onto the base model. - -## Calibration knobs - -- `ScorerConfig.flag_below` (50): the phoneme score under which a consonant gets a tip. -- `ScorerConfig.unrelated_ratio` (0.65): above this, the response says "different sentence". -- `GopConfig.*`: the deletion thresholds, and the target posterior at which confidence stops lowering the score. - -Tune them on a handful of labelled learner recordings. A useful target is that natives score ≥ 85 and known ħ→χ errors get flagged. - -## Playback voice (Sawtak Arabi) - -"🔊 השמע" plays the model answer through `GET /api/tts/{phrase_id}`, which calls the [Sawtak Arabi](https://sawtakarabi.ai/docs) TTS API server-side and caches each phrase on disk. The key never reaches the browser. - -- Local: `cp .env.example .env` and set `SAWTAK_API_KEY` (`.env` is git-ignored and excluded from the Docker image). -- Hugging Face: add `SAWTAK_API_KEY` as a Space **secret** (Settings → Variables and secrets). It is injected as an environment variable at runtime. -- Model-answer playback uses `SAWTAK_VOICE_ID`, or the Palestinian voice רַאאִף when it is unset. -- Without a key the endpoint answers 503 and the page falls back to the device's Arabic voice. -- Score calibration keeps its fixed native baseline voices; it does not use `SAWTAK_VOICE_ID`. A baseline is prepared once for every phrase that gets scored, also when the learner never presses "השמע". The first score of a phrase waits up to 8 s for it. Without a key the scores are not calibrated; `/api/health` reports this as `references`. - -## Cost estimate (API keys) - -Estimated 2026-10-18 for **100 students, each doing 5 tutor chats a day (the daily limit, `DAILY_LIMIT`) of 10 turns each**: 500 chats a day, 15,000 a month (30 days). Anthropic prices are list prices per million tokens; the Sawtak price is the rate your own account was charged. - -### Anthropic (`ANTHROPIC_API_KEY`) — the tutor game יאללה נדבר - -How a chat is billed: 11 calls (the opening plus 10 turns). Each call sends the scenario's system prompt and the reply schema (the prefix), plus the conversation so far. The prefix grows with the lessons' vocabulary: 4,364 tokens in lesson 1, 16,466 mid-course and 22,728 in the last lesson (measured with `count_tokens`; the 5.5 models count the same). Each turn adds about 540 tokens of history, and each answer is about 490 output tokens. `cache_control` caches the prompt, so every call after the first reads the earlier part from the cache and writes only the new turn. That assumes the turns are less than 5 minutes apart, which a spoken conversation is. - -Tokens per chat (mid-course): ~22K cache writes (1.25× the input price), ~189K cache reads, ~5.7K output. - -| Model (`TUTOR_MODEL`) | $ in / out / cache read per 1M | Per chat (mid-course) | Per day | **Per month**: lesson 1 / mid / last | Per student per month (mid) | -|---|---|---|---|---|---| -| `claude-haiku-5-5` (the default) | 0.10 / 0.50 / 0.01 | $0.0075 | $3.73 | **$71 / $112 / $133** | $1.12 | -| `claude-haiku-4-5` | 1 / 5 / 0.10 | $0.075 | $37 | $712 / $1,120 / $1,332 | $11.20 | -| `claude-sonnet-5-5` | 2 / 10 / 0.20 | $0.149 | $75 | $1,424 / $2,241 / $2,663 | $22.40 | -| `claude-opus-5-5` ¹ | 4 / 20 / 0.20 | $0.261 | $130 | $2,644 / $3,914 / $4,572 | $39.10 | -| `claude-fable-5-1` ¹ | 10 / 50 / 0.25 | $0.605 | $303 | $6,354 / $9,077 / $10,486 | $90.80 | - -- **Without the prompt cache** the input would cost 3–3.5× more (for example Haiku 5.5 mid-course: $359 a month instead of $112). Keep `cache_control` in `Tutor._ask`. -- These are upper bounds for the volume: every student uses all 5 chats, every day, all 10 turns. Real use is usually lower; `tutor_chats` records each chat's `input_tokens` and `output_tokens`, so the real bill can be checked against this table. -- ¹ Opus 5.5 and Fable 5.1 cannot turn thinking off, so the tutor leaves it adaptive at `effort: "low"` (`thinking_for()` in `app/tutor.py`). The thinking tokens they spend are billed as output and are not in this table, so expect somewhat more for these two. -- Set a monthly spending limit in the Console (Settings → Limits), so that a bug or unexpected use cannot cost more than planned. - -### Sawtak Arabi (`SAWTAK_API_KEY`) — the model-answer voice - -Sawtak charges per character of text spoken. This account was charged **$5.00 for 99,600 characters (about $0.05 per 1,000 characters, $50 per million)** in the 30 days before 2026-10-18 (`GET /v1/usage`). - -- **If the tutor's messages are spoken with Sawtak too** (the code does not do this yet), each one costs again: every message is new text, so none comes from the cache. A message is one or two short sentences, about 90 characters with vowel marks (the phrase bank averages 51 per phrase). A chat has 11 messages (the opening and 10 answers), so about 1,000 characters: - - | | Characters | Cost | - |---|---|---| - | Per chat | ~990 | ~$0.05 | - | Per day (500 chats) | ~495,000 | ~$25 | - | **Per month (15,000 chats)** | ~14.9 million | **~$745** (between ~$500 and ~$1,000 for messages of 60–120 characters) | - | Per student per month | ~149,000 | ~$7.45 | - - That is more than the tutor's model costs on Haiku 5.5 (~$112 a month), so voicing the tutor would be most of the bill. -- The rest of the cost comes from "🔊 השמע" and from the native baseline the scores are calibrated against, both for the phrase bank: -- Each phrase is generated once and kept in the voice cache (`TTS_CACHE_DIR`). All 3,546 phrases come to about 180,000 characters with their vowel marks, so **voicing the whole bank costs about $9, once**. A text's dialogue lines in the second voice add a little. -- **The number of students does not change this cost**: 100 students cost the same as 1, because the second student to play a phrase gets it from the cache. -- What does cost more: the cache is a folder on the server's disk. A Hugging Face Space without persistent storage loses it on every restart and rebuild, and "מחיקת מטמון ההקלטות" in the panel empties it. After either, the phrases are generated again as they are played, up to about $9 each time. To avoid that, give the Space persistent storage and set `TTS_CACHE_DIR=/data/ihki-tts`. diff --git a/app/access.py b/app/access.py index 04e9697a7dda79edc1b1304cb3319566a880ef1d..e5e9a8a264f05ba45ca412e4b0375d4cb4b09ff2 100644 --- a/app/access.py +++ b/app/access.py @@ -1,72 +1,36 @@ -"""Shared user-session and permission resolution for browser and MCP callers.""" +"""Mathly session roles and permissions.""" from __future__ import annotations -from .auth import AuthUnavailable, SupabaseAuth +from .auth import SupabaseAuth - -ROLES = frozenset({"student", "teacher", "flagger", "admin", "developer"}) -DEFAULT_ROLES = ("student", "flagger") -ENTITLEMENTS = frozenset({"practice", "games", "tutor", "path", "library"}) -DEFAULT_ENTITLEMENTS = ENTITLEMENTS - {"tutor"} +ROLES = frozenset({"student", "teacher", "admin", "developer"}) +DEFAULT_ROLES = ("student",) ROLE_PERMISSIONS = { - "student": frozenset(), - "teacher": frozenset({"exercises.use_all", "students.view", "games.use", "path.use", "exercises.use", "library.view"}), - "flagger": frozenset({"flags.create"}), - "admin": frozenset({"exercises.use_all", "students.view", "management.access", "users.manage", - "classes.manage", "content.manage", "flags.review", "games.use", "path.use", - "exercises.use", "library.view"}), + "student": frozenset({"path.use"}), + "teacher": frozenset({"path.use", "students.view"}), + "admin": frozenset({"path.use", "students.view", "management.access", "classes.manage"}), "developer": frozenset({"*"}), } -ENTITLEMENT_PERMISSIONS = {"practice": {"practice.use", "exercises.use"}, "games": {"games.use"}, - "tutor": {"tutor.use"}, "path": {"path.use"}, "library": {"library.view"}} - -def resolve_access(a: SupabaseAuth, token: str, *, fresh_permissions: bool = False) -> dict | None: - """Return the current application identity for a verified Supabase access token. - ``fresh_permissions`` bypasses the short user-access cache. MCP uses it on every - request so revoking management access takes effect immediately. - """ - user = a.user(token) +def resolve_access(store: SupabaseAuth, token: str, *, fresh_permissions: bool = False) -> dict | None: + user = store.user(token) if user is None: return None email = user["email"].lower() - classes = a.classes() - mine = [c for c in classes if email in c["emails"]] - teaches = [c for c in classes if email in (c.get("teachers") or [])] - member = bool(mine or teaches or not classes) - roles, entitlements, assigned = {"student"}, set(DEFAULT_ENTITLEMENTS) if member else set(), None - if hasattr(a, "access_for"): - try: - assigned = a.access_for(user["id"], fresh=fresh_permissions) - except TypeError: # compatible with small test/deployment adapters predating ``fresh`` - assigned = a.access_for(user["id"]) - if assigned: - roles = set(assigned.get("roles") or ["student"]) & ROLES - entitlements = set(assigned.get("entitlements") or []) & ENTITLEMENTS - if email in getattr(a, "teachers", ()): - roles.update({"teacher", "admin", "flagger"}) - if email in getattr(a, "tutors", ()): - roles.add("flagger") - if email in getattr(a, "developers", ()): + classes = store.classes() + mine = [row for row in classes if email in row.get("emails", ()) or email in row.get("teachers", ())] + assigned = store.access_for(user["id"], fresh=fresh_permissions) + roles = set((assigned or {}).get("roles") or DEFAULT_ROLES) & ROLES + if email in store.teachers: + roles.update({"teacher", "admin"}) + if email in store.developers: roles.add("developer") - privileged = roles - {"student"} - if classes and not mine and not privileged and not entitlements: + if classes and not mine and roles == {"student"}: return {**user, "denied": True, "permissions": []} - if not assigned: - roles.update(DEFAULT_ROLES) - if "flagger" not in roles: - try: - if email in (a.ranked() or ()): - roles.add("flagger") - except AuthUnavailable: - pass - permissions = set().union(*(ROLE_PERMISSIONS[r] for r in roles)) - permissions.update(*[ENTITLEMENT_PERMISSIONS[e] for e in entitlements]) - return {**user, "token": token, "roles": sorted(roles), "entitlements": sorted(entitlements), - "subject": str((assigned or {}).get("subject") or ""), "permissions": sorted(permissions), - "teacher": bool(roles & {"teacher", "admin", "developer"}), - "tutor": "flags.create" in permissions or "*" in permissions, "classes": mine, "teaches": teaches} + permissions = set().union(*(ROLE_PERMISSIONS[role] for role in roles)) + return {**user, "token": token, "roles": sorted(roles), "permissions": sorted(permissions), + "teacher": bool(roles & {"teacher", "admin", "developer"}), "classes": mine} def has_permission(user: dict | None, permission: str) -> bool: diff --git a/app/align.py b/app/align.py deleted file mode 100644 index 6e348abc95b2fdc2da7962249bdd7e1bf4ab3af1..0000000000000000000000000000000000000000 --- a/app/align.py +++ /dev/null @@ -1,316 +0,0 @@ -"""CTC forced alignment and goodness-of-pronunciation (GOP), in plain numpy. - -Inputs are frame-level log-posteriors `logp` of shape (T, V) from a CTC phoneme model. -Nothing here depends on torch, so it is unit-testable with synthetic emissions. -""" -from __future__ import annotations - -import logging -import os -from dataclasses import dataclass, field - -import numpy as np - -log = logging.getLogger("ihki.align") - -NEG = -1e30 -POOLS = ("max", "mean", "topk", "lse", "energy", "gated") -DEFAULT_POOL = "gated" - - -def env_pool() -> str: - """The ratio pool GOP_RATIO_POOL names (read when a GopConfig is made, so after main.py loads .env). - Empty or unset: DEFAULT_POOL. An unknown name is logged and DEFAULT_POOL is used.""" - pool = os.environ.get("GOP_RATIO_POOL", "").strip().lower() or DEFAULT_POOL - if pool not in POOLS: - log.warning("GOP_RATIO_POOL=%r is not one of %s: using %s", pool, ", ".join(POOLS), DEFAULT_POOL) - return DEFAULT_POOL - return pool - - -NARROW_PAD = 1 - - -def env_vowel_window() -> int | None: - """GOP_VOWEL_WINDOW: empty, unset or "wide" (None: a vowel reads the consonants' wide window), "narrow" (its - aligned span +- NARROW_PAD frames), or that pad as a frame count. Anything else is logged and "wide" used. - Wide is the default: with a pooled ratio it absorbs a transition spike without the narrow window's blind spot - (docs/gop-pooling.md, scripts/gop_window_bench.py).""" - raw = os.environ.get("GOP_VOWEL_WINDOW", "").strip().lower() - if raw in ("", "wide"): - return None - if raw == "narrow": - return NARROW_PAD - if raw.isdigit(): - return int(raw) - log.warning("GOP_VOWEL_WINDOW=%r is not wide, narrow or a frame count: using wide", raw) - return None - - -@dataclass -class Alignment: - total_logp: float # log-prob of the best CTC path - spans: list[tuple[int, int]] # [start, end) frames where each target token is emitted - - -def ctc_viterbi(logp: np.ndarray, tokens: list[int], blank: int) -> Alignment | None: - """Best CTC path that emits exactly `tokens`. Returns None if the audio is too short.""" - T, _ = logp.shape - N = len(tokens) - if N == 0 or T < N: - return None - S = 2 * N + 1 - ext = np.array([blank if s % 2 == 0 else tokens[s // 2] for s in range(S)]) - # skip transition s-2 -> s allowed for non-blank states whose label differs from s-2 - can_skip = np.zeros(S, dtype=bool) - can_skip[2:] = (ext[2:] != blank) & (ext[2:] != ext[:-2]) - - dp = np.full((T, S), NEG) - bp = np.zeros((T, S), dtype=np.int8) # 0 = stay, 1 = from s-1, 2 = from s-2 - dp[0, 0] = logp[0, blank] - dp[0, 1] = logp[0, ext[1]] - for t in range(1, T): - prev = dp[t - 1] - stay = prev - one = np.concatenate(([NEG], prev[:-1])) - two = np.where(can_skip, np.concatenate(([NEG, NEG], prev[:-2])), NEG) - stacked = np.stack([stay, one, two]) - choice = stacked.argmax(0) - dp[t] = stacked[choice, np.arange(S)] + logp[t, ext] - bp[t] = choice - - # path must end in the last token or the trailing blank - end = S - 1 if dp[T - 1, S - 1] >= dp[T - 1, S - 2] else S - 2 - total = float(dp[T - 1, end]) - if total <= NEG / 2: - return None - - states = np.empty(T, dtype=np.int64) - s = end - for t in range(T - 1, -1, -1): - states[t] = s - s -= int(bp[t, s]) - - spans: list[list[int]] = [[-1, -1] for _ in range(N)] - for t, st in enumerate(states): - if st % 2 == 1: - i = st // 2 - if spans[i][0] < 0: - spans[i][0] = t - spans[i][1] = t + 1 - return Alignment(total, [tuple(x) for x in spans]) - - -@dataclass -class TokenScore: - score: float # 0..100 - gop: float # log ratio target vs strongest competitor, <= 0 - peak: float # max log-posterior of the target inside its window - competitor: int | None # strongest non-target, non-blank token in the window (None: nothing, or a sound we do not name) - deleted: bool # target barely present and nothing else either - realised: int | None = None # a class target (several correct realisations): the one said - other: bool = False # the strongest competitor is a sound outside the named set - comp_peak: float = NEG # max log-posterior of the strongest competitor (named or not) in the window - comp_frames: int = 0 # frames where that competitor clears deleted_comp (duration evidence) - window: tuple[int, int] = (0, 0) # [start, end) frames the target and its competitors were read from - - -@dataclass -class GopConfig: - deleted_peak: float = float(np.log(0.05)) # target this weak ... - deleted_comp: float = float(np.log(0.20)) # ... and no strong competitor -> deletion - confident_peak: float = 0.30 # target posterior at which the peak no longer lowers the score - # Softens the target/competitor ratio: at 0.5 a competitor twice as likely still scores 71, not 50. - # The zero-shot model is noisy on accented speech, so a linear ratio punishes its own uncertainty. - leniency: float = 0.5 - # How the target/competitor ratio pools the window's frames (docs/gop-pooling.md). The peak behind - # `confidence` and `deleted` stays the max, so a peaky CTC emission is never diluted by its blanks. - ratio_pool: str = field(default_factory=env_pool) # max | mean | topk | lse | energy | gated (GOP_RATIO_POOL) - pool_k: int = 2 # topk: frames averaged per column - pool_tau: float = 0.5 # lse: temperature (0 -> max, 1 -> mean) - energy_gamma: float = 1.0 # energy: frame weight = (RMS / loudest in the window) ** gamma - energy_gate: float = 0.3 # gated: frames quieter than this fraction of the window's loudest are dropped - energy_spread: int = 1 # frames the energy is max-filtered over (CTC peaks lead/lag the acoustics) - pool_vowels_only: bool = True # consonants keep the max (bursts and voiceless sounds are short and quiet) - # Consonants need some surrounding blank for a CTC peak that leads/lags the forced span, but must not inherit an - # arbitrarily long pause up to the next token (e.g. a 760 ms window across a sentence break). - consonant_window: int | None = 4 - # A vowel's competitors are read from its aligned span +- this many frames (None: the wide window consonants - # use). Its presence (`peak`, `deleted`) is still read from the wide window. GOP_VOWEL_WINDOW sets it. - vowel_window: int | None = field(default_factory=env_vowel_window) - - -def pool_frames(seg: np.ndarray, how: str = "max", k: int = 2, tau: float = 0.5, - weight: np.ndarray | None = None) -> np.ndarray: - """Pool a window's (n, V) log-posteriors over time into one log-score per column. - - max: the single best frame. topk: log of the mean of each column's k best frames. lse: the power mean - tau * log(mean(p ** (1/tau))), between the max (tau -> 0) and the mean (tau = 1). mean / energy / gated: - log of the weighted mean posterior, `weight` (uniform when None) normalised over the window. - """ - n = len(seg) - if how == "max" or n == 1: - return seg.max(0) - if how == "topk": - k = max(1, min(k, n)) - return np.logaddexp.reduce(np.partition(seg, n - k, axis=0)[n - k:], axis=0) - np.log(k) - if how == "lse": - return tau * (np.logaddexp.reduce(seg / tau, axis=0) - np.log(n)) - w = np.ones(n) if weight is None else np.asarray(weight, dtype=np.float64) - w = w / w.sum() if w.sum() > 0 else np.full(n, 1.0 / n) - return np.logaddexp.reduce(seg + np.log(np.maximum(w, 1e-300))[:, None], axis=0) - - -def energy_weights(energy: np.ndarray, how: str, gamma: float = 1.0, gate: float = 0.3) -> np.ndarray: - """Frame weights of one window from its (already smoothed) RMS: soft (`energy`) or a 0/1 voice gate (`gated`).""" - e = energy / max(float(energy.max()), 1e-12) - return (e >= gate).astype(np.float64) if how == "gated" else e ** gamma - - -def spread_energy(energy: np.ndarray, spread: int) -> np.ndarray: - """Max-filter over +-spread frames: a CTC emission a frame off the loud nucleus still gets its weight.""" - out = energy.astype(np.float64).copy() - for s in range(1, spread + 1): - out[s:] = np.maximum(out[s:], energy[:-s]) - out[:-s] = np.maximum(out[:-s], energy[s:]) - return out - - -def token_window(spans: list[tuple[int, int]], i: int, T: int, pad: int | None = None) -> tuple[int, int]: - """[start, end) frames token i is read from. Wide (pad None): from the end of token i-1 to the start of token i+1, - the blanks around the emission where a substituted consonant peaks. Narrow: its own span +- `pad` frames, inside - the wide window (a vowel is its syllable's loud nucleus, not a sound hiding in the blanks).""" - a = spans[i - 1][1] if i > 0 else 0 - b = spans[i + 1][0] if i + 1 < len(spans) else T - a, b = min(a, spans[i][0]), max(b, spans[i][1]) - if pad is None: - return a, b - return max(a, spans[i][0] - pad), min(b, spans[i][1] + pad) - - -def compute_token_gop(logp: np.ndarray, spans: list[tuple[int, int]], i: int, tokens: list[int], blank: int, - cfg: GopConfig, vowel: bool = False, accept: list[set[int] | None] | None = None, - n_real: int | None = None, named: set[int] | None = None, energy: np.ndarray | None = None, - edge: tuple[set[int], set[int]] | None = None, pooled: bool | None = None) -> TokenScore: - """GOP of token i (see score_tokens), its window and pooling chosen by its class. - - Presence is read from the wide window for every token: `peak` (behind the confidence) and `deleted` (target under - deleted_peak and no competitor over deleted_comp), so a vowel aligned to a single frame is still found deleted. - The competitor ratio of a vowel is read from its narrow window (cfg.vowel_window) and pooled with cfg.ratio_pool; - a consonant is capped at its span +- cfg.consonant_window and keeps the max unless cfg.pool_vowels_only is off. - `pooled` overrides the latter. - `edge`: ids that are no competitor on the frames between the token's span and its left / right neighbour (a vowel - gliding into j or w passes through i or u: coarticulation, not a substitution). `energy` is already spread. - """ - T = logp.shape[0] - accept = accept or [None] * len(tokens) - a, b = token_window(spans, i, T, cfg.consonant_window) if not vowel else token_window(spans, i, T) - ra, rb = token_window(spans, i, T, cfg.vowel_window) if vowel and cfg.vowel_window is not None else (a, b) - tok, (s0, s1) = tokens[i], spans[i] - seg = logp[a:b] - peak = float(seg[:, tok].max()) - members = accept[i] or set() - realised = max(members, key=lambda m: seg[:, m].max()) if members else None - - cols = (seg[:, :n_real] if n_real else seg).copy() - if edge: - for ids, lo, hi in ((edge[0], a, s0), (edge[1], s1, b)): - ids = [x for x in ids if x < cols.shape[1]] - if ids and hi > lo: - cols[lo - a:hi - a, ids] = NEG - mask = np.zeros(cols.shape[1], dtype=bool) - mask[blank] = True - for j in (i - 1, i, i + 1): - if 0 <= j < len(tokens): - for x in {tokens[j], *(accept[j] or ())}: - if x < len(mask): - mask[x] = True - deleted = bool(peak < cfg.deleted_peak and float(np.where(mask, NEG, cols.max(0)).max()) < cfg.deleted_comp) - - how = cfg.ratio_pool if (pooled if pooled is not None else vowel or not cfg.pool_vowels_only) else "max" - weight = None - if how in ("energy", "gated"): - if energy is not None: - weight = energy_weights(energy[ra:rb], how, cfg.energy_gamma, cfg.energy_gate) - weight[max(0, s0 - ra):max(0, s1 - ra)] = 1.0 # the frames the token was emitted on count in full - how = "mean" - rcols = cols[ra - a:rb - a] - maxed = rcols.max(0) - ratio = maxed.copy() if how == "max" else pool_frames(rcols, how, cfg.pool_k, cfg.pool_tau, weight) - ratio[mask] = NEG - comp = int(ratio.argmax()) - comp_peak = float(maxed[comp]) if ratio[comp] > NEG / 2 else NEG - # A lone CTC spike can be a breath or microphone onset rather than a produced consonant. Keep duration - # evidence separately from the peak so pronunciation policy can distinguish transients from substitutions. - comp_frames = int(np.count_nonzero(rcols[:, comp] >= cfg.deleted_comp)) if ratio[comp] > NEG / 2 else 0 - tseg = logp[ra:rb, [tok]] - target = float(tseg.max()) if how == "max" else float(pool_frames(tseg, how, cfg.pool_k, cfg.pool_tau, weight)[0]) - - gop = target - max(target, float(ratio[comp])) - confidence = min(1.0, float(np.exp(peak)) / cfg.confident_peak) - score = 100.0 * float(np.exp(gop * cfg.leniency)) * float(np.sqrt(confidence)) - present = comp_peak > NEG / 2 - other = bool(present and named is not None and comp not in named) - return TokenScore(round(score, 1), round(gop, 3), round(peak, 3), comp if present and not other else None, - deleted, realised, other, round(comp_peak, 3), comp_frames, (int(ra), int(rb))) - - -def score_tokens(logp: np.ndarray, align: Alignment, tokens: list[int], blank: int, - cfg: GopConfig | None = None, accept: list[set[int] | None] | None = None, - n_real: int | None = None, named: set[int] | None = None, - energy: np.ndarray | None = None, pooled: list[bool] | None = None, - vowels: list[bool] | None = None, - edges: list[tuple[set[int], set[int]] | None] | None = None) -> list[TokenScore]: - """Per-token GOP (compute_token_gop for each token). - - The neighbours' own labels are excluded from the competitor set (coarticulation is not an error). - A class token (a virtual column pooling its correct realisations, `accept[i]` their ids) is scored on the - pooled column; its members are not competitors. Competitors come from the first `n_real` columns only, and - one outside `named` is reported as `other` rather than by name. - - `vowels` marks the tokens that are vowels (narrow window, pooled ratio). Without it every token is read from the - wide window and pooled, unless `pooled` marks which are. `energy` is the per-frame RMS the energy pools need; - without it they fall back to the plain mean. `edges[i]`: see compute_token_gop. - """ - cfg = cfg or GopConfig() - if energy is not None: - energy = spread_energy(np.asarray(energy)[:logp.shape[0]], cfg.energy_spread) - return [compute_token_gop(logp, align.spans, i, tokens, blank, cfg, bool(vowels and vowels[i]), accept, n_real, - named, energy, edges[i] if edges else None, - (pooled[i] if pooled is not None else None if vowels is not None else True)) - for i in range(len(tokens))] - - -def greedy_decode(logp: np.ndarray, blank: int) -> list[int]: - ids = logp.argmax(1) - out, prev = [], None - for x in ids: - x = int(x) - if x != prev and x != blank: - out.append(x) - prev = x - return out - - -def edit_distance(a: list, b: list, same=None) -> int: - same = same or (lambda x, y: x == y) - d = list(range(len(b) + 1)) - for i, x in enumerate(a, 1): - prev, d[0] = d[0], i - for j, y in enumerate(b, 1): - prev, d[j] = d[j], min(d[j] + 1, d[j - 1] + 1, prev + (not same(x, y))) - return d[len(b)] - - -def weighted_edit_distance(a: list, b: list, sub, dele, ins) -> float: - """Edit distance of `a` (heard) from `b` (target) with a cost per edit: sub(x, j) for x heard in place of b[j], - dele(j) for b[j] not heard, ins(x, j) for x heard after b[:j] (j == 0 or len(b): at an edge).""" - n = len(b) - d = [0.0] * (n + 1) - for j in range(1, n + 1): - d[j] = d[j - 1] + dele(j - 1) - for x in a: - prev, d[0] = d[0], d[0] + ins(x, 0) - for j in range(1, n + 1): - prev, d[j] = d[j], min(d[j] + ins(x, j), d[j - 1] + dele(j - 1), prev + sub(x, j - 1)) - return d[n] diff --git a/app/arabic_diacritics.py b/app/arabic_diacritics.py deleted file mode 100644 index 121f4a284c286eee2fdd360f722f1e070f95d24d..0000000000000000000000000000000000000000 --- a/app/arabic_diacritics.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Add Arabic vowel marks to an answer from the pronunciation stored beside it. - -The phrase bank deliberately keeps a plain Arabic spelling and an exact IPA reading. -This module combines them for the learner-facing answer without changing the spelling -used by the scorer or by text-to-speech. -""" -from __future__ import annotations - -import re -from functools import lru_cache - -from .archi import _LETTERS, _align, _letters, _units -from .phones import base - -FATHA, DAMMA, KASRA, SUKUN, SHADDA = "\u064e", "\u064f", "\u0650", "\u0652", "\u0651" -_MARKS = re.compile(r"[\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06ed]") -_VOWELS = {"a", "i", "u", "e", "o", "ə"} -_VOWEL_MARK = {"a": FATHA, "i": KASRA, "e": KASRA, "ə": KASRA, "u": DAMMA, "o": DAMMA} - - -def _vowel(ph: str) -> str | None: - sound = base(ph) - return sound if sound in _VOWELS else None - - -def _point_word(word: str, ipa_word: str) -> str: - plain = _MARKS.sub("", word) - chars = list(plain) - letters = _letters(plain) - phs = ipa_word.split() - units = _units(phs) - if not letters or not units: - return plain - - matched, _cost = _align(letters, [sound for sound, _positions in units]) - unit_for_letter: dict[int, int] = {} - for letter_i, hits in matched.items(): - real = [hit for hit in hits if hit >= 0] - if len(hits) == 1 and len(real) == 1: - unit_for_letter[letter_i] = real[0] - - if not unit_for_letter: - return plain - - marks: dict[int, list[str]] = {} - ordered = sorted(unit_for_letter.items(), key=lambda item: item[1]) - for order_i, (letter_i, unit_i) in enumerate(ordered): - positions = units[unit_i][1] - if len(positions) > 1: - marks.setdefault(letter_i, []).append(SHADDA) - - end = positions[-1] + 1 - next_start = units[unit_i + 1][1][0] if unit_i + 1 < len(units) else len(phs) - following = next((_vowel(phs[i]) for i in range(end, next_start) if _vowel(phs[i])), None) - if following: - target = letter_i - next_letter_i = ordered[order_i + 1][0] if order_i + 1 < len(ordered) else len(letters) - between = range(letter_i + 1, next_letter_i) - # A final ta marbuta / h carries its spoken ending (qahwe, madrase). - ending = next((i for i in between if letters[i] in "ةه" and i == len(letters) - 1), None) - if ending is not None: - target = ending - marks.setdefault(target, []).append(_VOWEL_MARK[following]) - else: - marks.setdefault(letter_i, []).append(SUKUN) - - # A word-initial vowel belongs on its alif/hamza, before the first aligned consonant. - first_unit = ordered[0][1] if ordered else 0 - first_phone = units[first_unit][1][0] - initial = next((_vowel(ph) for ph in phs[:first_phone] if _vowel(ph)), None) - if initial: - first_letter = ordered[0][0] - carrier = next((i for i in range(first_letter + 1) if letters[i] in "اٱأإآء"), first_letter) - marks.setdefault(carrier, []).append(_VOWEL_MARK[initial]) - - # In a sun-letter article the lam is silent but is still explicitly vowelless. - if plain.startswith("ال") and 1 not in unit_for_letter: - marks.setdefault(1, []).append(SUKUN) - - output = [] - letter_i = 0 - for char in chars: - output.append(char) - if char in _LETTERS: - own = marks.get(letter_i, []) - output.extend(mark for mark in (SHADDA if SHADDA in own else "", next((m for m in own if m != SHADDA), "")) if mark) - letter_i += 1 - return "".join(output) - - -@lru_cache(maxsize=16384) -def point_arabic(arabic: str, ipa: str) -> str: - """Return the original Arabic spelling with marks for its IPA pronunciation.""" - ipa_words = [part.strip() for part in ipa.split("|")] - word_i = 0 - - def point(match: re.Match[str]) -> str: - nonlocal word_i - word = match.group(0) - if word_i >= len(ipa_words): - return word - result = _point_word(word, ipa_words[word_i]) - word_i += 1 - return result - - return re.sub(r"[^\s]+", point, arabic) if len(arabic.split()) == len(ipa_words) else arabic - - -# Spellings for the voice only, never shown: Sawtak reads the pointed هُوِّ / هِيِّ as "hu" / "hi". It reads these as -# huwwe / hiyye: هوّة like the -e of قهوة, هِيِّي with the -e as a written yā'. Checked with the phoneme model -# (the pronoun alone: 58 -> 95 and 65 -> 95). -_TTS_RESPELL = {("هو", "h u w w e"): "هوّة", ("هي", "h i j j e"): "هِيِّي"} - - -def tts_arabic(arabic: str, ipa: str) -> str: - """The text sent to the Arabic voice: point_arabic, with the words it misreads respelled.""" - ipa_words = [part.strip() for part in ipa.split("|")] - if len(arabic.split()) != len(ipa_words): - return point_arabic(arabic, ipa) - words = iter(ipa_words) - - def say(match: re.Match[str]) -> str: - word, ipa_word = match.group(0), next(words) - bare = _MARKS.sub("", word) - tail = re.search(r"[؟?!.,،]*$", bare).group(0) # punctuation stuck to the word stays after it - core = bare[:len(bare) - len(tail)] if tail else bare - if core.startswith("و") and ipa_word.startswith("u "): # وهيّ "and she": the و, then the pronoun - respelled = _TTS_RESPELL.get((core[1:], ipa_word[2:])) - if respelled: - return "وُ" + respelled + tail - respelled = _TTS_RESPELL.get((core, ipa_word)) - return respelled + tail if respelled else _point_word(word, ipa_word) - - return re.sub(r"[^\s]+", say, arabic) diff --git a/app/archi.py b/app/archi.py deleted file mode 100644 index bd6630c678a982f533895680e3ca87a2dd8e601c..0000000000000000000000000000000000000000 --- a/app/archi.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Abstract symbols for letters with more than one correct pronunciation (section 5 of pronunciation-rules.md). - -phrases.py writes the realisation its author chose (ق as ʔ in قديش, q in القدس). The scorer must accept -the others too (kaddeesh is rural Palestinian), so before tokenising, archify() rewrites each phoneme that -comes from such a letter as "SYMBOL:written" (Q:ʔ), and tokenize() scores it against every correct -realisation at once. The written phoneme is kept for display, tips and calibration keys. - -The letter behind a phoneme is found by aligning the Arabic letters of the word with its IPA consonants. -A phrase can also write a symbol itself: "Q", "Q:k", or with its own set of realisations, "TH{t,s}". -""" -from __future__ import annotations - -import re -from functools import lru_cache - -from .phones import base - -# letter -> the symbol its phoneme is rewritten to -LETTER_SYMBOL = {"ق": "Q", "ك": "K", "ث": "TH", "ذ": "DH", "ظ": "ZH", "ج": "J"} - -# letter -> base() names of the phonemes it is said as in the phrase bank (dialects, MSA, careless spelling) -_LETTERS = { - "ب": {"b"}, "ت": {"t"}, "ث": {"t", "s", "θ"}, "ج": {"dʒ", "g"}, "ح": {"ħ"}, "خ": {"x"}, "د": {"d"}, - "ذ": {"d", "z", "ð"}, "ر": {"r"}, "ز": {"z"}, "س": {"s"}, "ش": {"ʃ"}, "ص": {"sˤ", "s"}, "ض": {"dˤ", "d"}, - "ط": {"tˤ", "t"}, "ظ": {"ðˤ", "dˤ", "zˤ", "z", "ð"}, "ع": {"ʕ"}, "غ": {"ɣ"}, "ف": {"f"}, "ق": {"ʔ", "q", "k", "g"}, - "ك": {"k", "tʃ"}, "ل": {"l"}, "م": {"m"}, "ن": {"n"}, "ه": {"h"}, "و": {"w"}, "ي": {"j"}, "ى": set(), - "ء": {"ʔ"}, "أ": {"ʔ"}, "إ": {"ʔ"}, "آ": {"ʔ"}, "ؤ": {"ʔ"}, "ئ": {"ʔ"}, "ا": {"ʔ"}, "ة": {"t"}, -} -# letters that are often not said as a consonant at all (vowel letters, hamza seats, the article's ل): cheap to skip -_WEAK = set("اوىيةهءأإآؤئل") -_VOWELS = {"a", "i", "u", "e", "o", "ə"} - -_TOKEN = re.compile(r"^([A-Z]+)(?:\{([^}]*)\})?(?::(.+))?$") - - -def split_token(tok: str) -> tuple[str | None, list[str] | None, str]: - """"Q:ʔ" -> ("Q", None, "ʔ"); "TH{t,s}" -> ("TH", ["t", "s"], "t"); "ʕ" -> (None, None, "ʕ"). - - The phoneme of a symbol written without one is its first listed realisation, or "" (the caller picks).""" - m = _TOKEN.match(tok) - if not m: - return None, None, tok - sym, members, written = m.group(1), m.group(2), m.group(3) - members = [p.strip() for p in members.split(",") if p.strip()] if members else None - return sym, members, written or (members[0] if members else "") - - -def written(tok: str) -> str: - """The phoneme a token is written as in the phrase (Q:ʔ -> ʔ).""" - return split_token(tok)[2] or tok - - -def _letters(ar_word: str) -> list[str]: - return [c for c in ar_word if c in _LETTERS] - - -def _units(phs: list[str]) -> list[tuple[str, list[int]]]: - """The consonants of an IPA word, a geminate run as one unit: (base name, its positions).""" - out: list[tuple[str, list[int]]] = [] - for i, ph in enumerate(phs): - b = base(ph) - if b in _VOWELS: - continue - if out and out[-1][0] == b and out[-1][1][-1] == i - 1: - out[-1][1].append(i) - else: - out.append((b, [i])) - return out - - -_SKIP_LETTER, _SKIP_WEAK, _SKIP_UNIT, _MISMATCH = 1.0, 0.3, 1.0, 1.6 - - -def _align(letters: list[str], units: list[str]) -> tuple[dict[int, set[int]], float]: - """For each letter, the units it matches on some cheapest alignment of letters to consonants. - - A letter matched to different units on two cheapest alignments (or matched on one and skipped on - another) is ambiguous; the caller leaves it alone.""" - n, m = len(letters), len(units) - inf = float("inf") - - def cost(i, j): - return 0.0 if units[j] in _LETTERS[letters[i]] else _MISMATCH - - def skip(i): - return _SKIP_WEAK if letters[i] in _WEAK else _SKIP_LETTER - - f = [[inf] * (m + 1) for _ in range(n + 1)] - f[0][0] = 0.0 - for i in range(n + 1): - for j in range(m + 1): - if i < n: - f[i + 1][j] = min(f[i + 1][j], f[i][j] + skip(i)) - if j < m: - f[i][j + 1] = min(f[i][j + 1], f[i][j] + _SKIP_UNIT) - if i < n and j < m: - f[i + 1][j + 1] = min(f[i + 1][j + 1], f[i][j] + cost(i, j)) - b = [[inf] * (m + 1) for _ in range(n + 1)] - b[n][m] = 0.0 - for i in range(n, -1, -1): - for j in range(m, -1, -1): - if i < n: - b[i][j] = min(b[i][j], b[i + 1][j] + skip(i)) - if j < m: - b[i][j] = min(b[i][j], b[i][j + 1] + _SKIP_UNIT) - if i < n and j < m: - b[i][j] = min(b[i][j], b[i + 1][j + 1] + cost(i, j)) - best, eps = f[n][m], 1e-9 - matched: dict[int, set[int]] = {} - for i in range(n): - for j in range(m): - if cost(i, j) == 0.0 and abs(f[i][j] + b[i + 1][j + 1] - best) < eps: - matched.setdefault(i, set()).add(j) - # skipping letter i on a cheapest alignment - if any(abs(f[i][j] + skip(i) + b[i + 1][j] - best) < eps for j in range(m + 1)): - matched.setdefault(i, set()).add(-1) - return matched, best - - -def archify_word(ipa_word: str, ar_word: str, off: frozenset[str] = frozenset()) -> tuple[str, list[str]]: - """One word: its IPA with symbol tokens, and the letters that could not be placed (ambiguous). - Symbols in `off` (switched off by the teacher) are left as the phoneme the phrase writes.""" - phs = ipa_word.split() - if any(split_token(p)[0] for p in phs): - return ipa_word, [] # the phrase writes its symbols itself - letters, units = _letters(ar_word), _units(phs) - matched, _ = _align(letters, [u for u, _ in units]) - out, unresolved = list(phs), [] - for i, letter in enumerate(letters): - sym = LETTER_SYMBOL.get(letter) - if sym is None or sym in off: - continue - hits = matched.get(i, {-1}) - if len(hits) != 1: - unresolved.append(letter) - continue - j = next(iter(hits)) - if j < 0: - continue # not said at all (a dropped ق): nothing to rewrite - for k in units[j][1]: - out[k] = f"{sym}:{phs[k]}" - # the vowel of a final ة (and ه written for it): a, e and i are all correct (section 6.3 #5) - if ("E" not in off and out and ar_word.endswith(("ة", "ه")) and "لله" not in ar_word - and base(out[-1]) in {"a", "e", "i"} and len(out) > 1): - out[-1] = f"E:{out[-1]}" - return " ".join(out), unresolved - - -@lru_cache(maxsize=32768) # the whole phrase bank (~12k variants) and the verb game's forms -def archify(ar: str, ipa: str, off: frozenset[str] = frozenset()) -> str: - """The variant's IPA with each phoneme of a many-ways letter rewritten as its symbol (see the module doc). - - A variant whose Arabic and IPA word counts differ is returned as it is.""" - ipa_words, ar_words = ipa.split("|"), ar.split() - if len(ipa_words) != len(ar_words): - return ipa - return " | ".join(archify_word(w.strip(), a, off)[0] for w, a in zip(ipa_words, ar_words)) - - -def unresolved(ar: str, ipa: str) -> list[tuple[str, str]]: - """(Arabic word, letter) for every many-ways letter archify() could not place: the phrase must mark it.""" - ipa_words, ar_words = ipa.split("|"), ar.split() - if len(ipa_words) != len(ar_words): - return [] - return [(a, letter) for w, a in zip(ipa_words, ar_words) for letter in archify_word(w.strip(), a)[1]] - - -def diphthong_variants(ar: str, ipa: str) -> list[str]: - """The same variant with eː said as a j and oː as a w where the Arabic writes ي / و (section 6.3 #8). - - A diphthong is two tokens against one, so it is a variant of its own rather than a class.""" - ipa_words, ar_words = ipa.split("|"), ar.split() - if len(ipa_words) != len(ar_words): - return [] - out = [] - for long, glide, letter in (("eː", "j", "ي"), ("oː", "w", "و")): - words = [" ".join(f"a {glide}" if ph == long and letter in a else ph for ph in w.split()) - for w, a in zip(ipa_words, ar_words)] - new = " | ".join(words) - if new != " | ".join(w.strip() for w in ipa_words): - out.append(new) - return out diff --git a/app/auth.py b/app/auth.py index 8b0653317d5ce113128377eecc6f76fdbe89c43b..52f79cf6f659a868c6de017258e139bcc360141e 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,112 +1,20 @@ -"""Sign-in: the page logs in through Supabase, with Google or with a link sent to the email, and sends the session token with each call. - -The token is checked by asking Supabase who it belongs to (GET /auth/v1/user), so no signing key is -kept here and a session revoked in Supabase stops working within CACHE_TTL. - -Progress: each scored recording is added to the Supabase table `attempts` with the student's own -token, so row-level security ties the row to them (the table's SQL is in the README). - -Teachers: the accounts in TEACHER_EMAILS also get the management panel, which shows every student's -progress. Those rows are read with the project's secret key, which passes row-level security and -never leaves the server. - -Classes: the teacher opens classes in the panel (a name, e.g. "October 2026", the lessons it studies, its -students' emails and an optional nickname of each, and the homework: a topic of its lessons, with the day it is due), -kept in the Supabase table `classes`. A student may be in several. Once there is a class, only the classes' -students and the teachers are let in; before the first one (or without the secret key, which reads the table) any account is. - -Videos: the teacher adds lesson videos in the panel's content part (a lesson, a title and a Vimeo or YouTube link) and moves -each to its place among the lesson's topics, kept in the Supabase table `videos`. A student's path shows them as -topics of their lessons. - -Texts: the teacher also adds texts there (a story or a dialogue: a lesson, a title, its lines in Hebrew and in -Arabic written in Hebrew letters, and a dialogue's two voices), kept in the Supabase table `texts` and placed among -the lesson's topics like the videos. Each is a topic practised line by line (see app/texts.py). - -Daily videos: a library the teacher keeps in the content part, a level's videos each with a title and a Vimeo or -YouTube link, in the Supabase table `dailies`. A class gives a video its day (the class's `dailies`); on that day its -students get a reminder above the path to watch it. - -Library: other videos (a title and a link each), in the Supabase table `library`, one for every level. Every student -browses them from the tab at the screen's foot, whatever their level and class. - -Current lessons: a student tells, once for each class, which lesson the class is on now; the focused home of the page -starts from it. It is the student's own, kept by email and class id in the Supabase table `learner_class_state`, -read and written with the secret key only (the server takes the email from the session, never from the request). - -Tutors: the teacher gives a student the tutor's rank in the panel, in the list of all the students above the classes' tabs. It is the student's, -whatever classes they are in, so it is kept by email in the Supabase table `tutors`. - -Flags: a teacher, a tutor, and an account in TUTORS_EMAIL, gets a flag under each score, for a recording that was said right and scored -wrong. A flag keeps what describes the case in the Supabase table `flags`, and the recording itself in the storage -bucket `flags`, both written with the secret key: the material for changing the scoring, and later for fine-tuning. - -Lab: a teacher's recordings of the lab's diagnostic list (see lab.py), each labelled said right or with one planted -mistake, in the Supabase table `lab_samples` and the storage bucket `lab`, written with the secret key: the -benchmark set of the phoneme recognition. - -Writing: a lesson's written homework (see workbook.py) is kept in the Supabase table `writings`, a row per student -and topic, written with the student's own token like the attempts, so it follows them to any device. The panel -reads every student's with the secret key. - -Tutor: each conversation of the game יאללה נדבר (see tutor.py), what the student said and how the tutor answered, in the -Supabase table `tutor_chats`, written with the secret key, for the teacher to read. - -Env: SUPABASE_URL (https://.supabase.co) - SUPABASE_PUBLISHABLE_KEY (the publishable/anon key; it is public and also goes to the page) - TEACHER_EMAILS (optional: the accounts with the management panel, separated by commas or spaces) - TUTORS_EMAIL (optional: more accounts that flag a wrong score, as the teachers do; separated by commas or spaces) - DEVELOPER_EMAILS (optional bootstrap accounts with full access, including the Lab) - SUPABASE_SECRET_KEY (the secret/service-role key, needed by the panel, the classes and the flags; never public) -Without them the service is open, as before. -""" +"""Optional Supabase sign-in and math-class storage.""" from __future__ import annotations import json -import hashlib import logging -import mimetypes import os import re -import tempfile import threading import time import urllib.error import urllib.parse import urllib.request -from datetime import datetime, timezone -from pathlib import Path -from zoneinfo import ZoneInfo -log = logging.getLogger("ihki.auth") +log = logging.getLogger("mathly.auth") TIMEOUT = 10 -CACHE_TTL = 300 # seconds a checked token is trusted without asking again -CACHE_MAX = 2000 -PAGE = 1000 # rows per request: Supabase's default ceiling -MAX_ATTEMPTS = 20_000 # of one student, read for their progress -MAX_CLASS_ATTEMPTS = 200_000 # of all students, read for the teacher's panel -MAX_MONITOR_ATTEMPTS = 5_000 # newest compact attempts sampled for pronunciation patterns; never audio -MAX_DIFFICULTY_ATTEMPTS = 50_000 # bounded scoped metadata; refuse rather than rank a partial sample -CLASSES_TTL = 60 # seconds the class list is trusted without reading it again (each sign-in check uses it) -# One-time migration boundary requested on 24 Oct 2026: the then-current seven days remain in Supabase forever. -DEFAULT_AUDIO_MIGRATION_CUTOFF = "2026-10-17T00:00:00+03:00" -CREATE_LEARNER_CLASS_STATE = ( - "create table public.learner_class_state (email text not null, " - "class_id bigint not null references public.classes (id) on delete cascade, current_lesson text not null, " - "created_at timestamptz not null default now(), updated_at timestamptz not null default now(), " - "primary key (email, class_id)); " - "alter table public.learner_class_state enable row level security; " - "revoke all on public.learner_class_state from anon, authenticated;") - -# Application-owned data. Supabase's auth users and managed schemas are deliberately not copied. -BACKUP_TABLES = ( - "dailies", "library", "videos", "texts", "classes", "writings", "tutors", "flags", - "lab_samples", "calibrations", "scoring_rules", "user_access", "access_audit", "app_secrets", - "system_events", "tutor_chats", "model_settings", -) -BACKUP_NATURAL_KEYS = {"writings": "user_id,topic", "tutors": "email", "scoring_rules": "id", - "user_access": "user_id", "app_secrets": "name", "tutor_chats": "id", - "model_settings": "model"} +CACHE_TTL = 300 +CLASSES_TTL = 60 class AuthUnavailable(RuntimeError): @@ -114,1133 +22,127 @@ class AuthUnavailable(RuntimeError): class ClassConflict(ValueError): - """Supabase refused a class: its name is taken.""" - - -class HFAudioStore: - """Private durable audio storage in a Hugging Face Storage Bucket.""" - - def __init__(self, bucket_id: str, prefix: str = "ihki-audio", token: str | None = None): - self.bucket_id, self.prefix, self.token = bucket_id, prefix.strip("/"), token - - def path(self, bucket: str, name: str) -> str: - rel = f"{bucket}/{name.lstrip('/')}" - return f"{self.prefix}/{rel}" if self.prefix else rel - - def put(self, bucket: str, name: str, data: bytes) -> None: - from huggingface_hub import batch_bucket_files - batch_bucket_files(self.bucket_id, add=[(data, self.path(bucket, name))], token=self.token) - - def get(self, bucket: str, name: str) -> bytes: - from huggingface_hub import download_bucket_files - with tempfile.TemporaryDirectory(prefix="ihki-audio-") as directory: - target = Path(directory) / Path(name).name - download_bucket_files(self.bucket_id, [(self.path(bucket, name), target)], - raise_on_missing_files=True, token=self.token) - return target.read_bytes() - - def delete(self, bucket: str, names: list[str]) -> None: - if not names: - return - from huggingface_hub import batch_bucket_files - batch_bucket_files(self.bucket_id, delete=[self.path(bucket, name) for name in names], token=self.token) + pass class SupabaseAuth: def __init__(self, url: str, key: str, teachers: frozenset[str] = frozenset(), secret: str = "", - tutors: frozenset[str] = frozenset(), developers: frozenset[str] = frozenset(), - audio_store: HFAudioStore | None = None, audio_migrate_before: str = ""): - self.url, self.key = url.rstrip("/"), key - self.teachers = teachers # lower-case emails of the accounts with the management panel - self.tutors = tutors # lower-case emails of the accounts that flag a wrong score, besides the teachers - self.developers = developers # bootstrap accounts; only developers may use the lab - self.secret = secret # reads every student's rows and the classes; empty = the panel has no data - self.audio_store = audio_store - self.audio_migrate_before = audio_migrate_before - self._cache: dict[str, tuple[float, dict]] = {} + developers: frozenset[str] = frozenset()): + self.url, self.key, self.secret = url.rstrip("/"), key, secret + self.teachers, self.developers = teachers, developers + self._users: dict[str, tuple[float, dict]] = {} self._classes: tuple[float, list[dict]] | None = None - self._videos: tuple[float, list[dict]] | None = None - self._texts: tuple[float, list[dict]] | None = None - self._ranked: tuple[float, frozenset[str] | None] | None = None self._access: dict[str, tuple[float, dict | None]] = {} - self._level_videos: dict[str, tuple[float, list[dict]]] = {} # by table: the daily videos, the library self._lock = threading.Lock() def user(self, token: str) -> dict | None: - """The signed-in user of this access token ({"id", "email"}), or None when it is not valid.""" now = time.monotonic() with self._lock: - hit = self._cache.get(token) - if hit and hit[0] > now: - return hit[1] - req = urllib.request.Request( - self.url + "/auth/v1/user", - headers={"Authorization": f"Bearer {token}", "apikey": self.key, "User-Agent": "ihki/1.0"}, - ) + cached = self._users.get(token) + if cached and cached[0] > now: + return cached[1] + request = urllib.request.Request(self.url + "/auth/v1/user", + headers={"Authorization": f"Bearer {token}", "apikey": self.key, "User-Agent": "mathly/1.0"}) try: - with urllib.request.urlopen(req, timeout=TIMEOUT) as res: - data = json.load(res) - except urllib.error.HTTPError as e: - if e.code in (400, 401, 403): + with urllib.request.urlopen(request, timeout=TIMEOUT) as response: + data = json.load(response) + except urllib.error.HTTPError as exc: + if exc.code in (400, 401, 403): return None - raise AuthUnavailable(f"supabase answered {e.code}") from e - except (urllib.error.URLError, TimeoutError, ValueError) as e: - raise AuthUnavailable(str(e)) from e + raise AuthUnavailable(f"supabase answered {exc.code}") from exc + except (urllib.error.URLError, TimeoutError, ValueError) as exc: + raise AuthUnavailable(str(exc)) from exc if not data.get("id"): return None user = {"id": data["id"], "email": data.get("email", "")} with self._lock: - if len(self._cache) >= CACHE_MAX: - self._cache.clear() - self._cache[token] = (now + CACHE_TTL, user) + self._users[token] = (now + CACHE_TTL, user) return user - def record(self, token: str, row: dict) -> None: - """Adds one row to `attempts` as the student of this token. A failure is logged, never raised.""" - def request(values: dict): - return urllib.request.Request( - self.url + "/rest/v1/attempts", data=json.dumps(values).encode(), method="POST", - headers={"Authorization": f"Bearer {token}", "apikey": self.key, "Content-Type": "application/json", - "Prefer": "return=minimal", "User-Agent": "ihki/1.0"}) - try: - urllib.request.urlopen(request(row), timeout=TIMEOUT).close() - except urllib.error.HTTPError as e: - if e.code == 400 and any(k in row for k in ("issues", "model", "version")): - legacy = {k: v for k, v in row.items() if k not in ("issues", "model", "version")} - try: - urllib.request.urlopen(request(legacy), timeout=TIMEOUT).close() - log.warning("attempt stored without issue diagnostics; migrate the attempts table (see README)") - return - except (urllib.error.URLError, TimeoutError) as retry: - e = retry - log.warning("attempt not recorded: %s", e) - except (urllib.error.URLError, TimeoutError) as e: # HTTPError is a URLError - log.warning("attempt not recorded: %s", e) - - def attempts(self, token: str) -> list[dict]: - """The student's non-empty scored recordings, newest first: cat, phrase, score, unrelated, created_at. - Row-level security limits them to the token's student. What cannot be read is logged.""" - rows: list[dict] = [] - while len(rows) < MAX_ATTEMPTS: - query = urllib.parse.urlencode({"select": "cat,phrase,score,unrelated,created_at", - "order": "created_at.desc", "limit": PAGE, "offset": len(rows)}) - try: - batch = self._get(f"/rest/v1/attempts?{query}", token, self.key) - except (urllib.error.URLError, TimeoutError, ValueError) as e: - log.warning("attempts not read: %s", e) - break - rows += batch - if len(batch) < PAGE: - break - return rows - - def writings(self, token: str) -> dict[str, list[str]]: - """The student's written homework, {topic id: its lines}. Row-level security limits it to the token's student. - Before the table is created there is none; any other failure is raised, so the page keeps its own copy.""" - try: - rows = self._get("/rest/v1/writings?select=topic,lines", token, self.key) - except urllib.error.HTTPError as e: - if e.code == 404: - log.warning("the table `writings` is missing (see the README): written homework stays on the device") - raise AuthUnavailable("the table writings is missing") from e - raise AuthUnavailable(f"supabase answered {e.code}") from e - except (urllib.error.URLError, TimeoutError, ValueError) as e: - raise AuthUnavailable(str(e)) from e - return {r["topic"]: [str(x) for x in r["lines"]] for r in rows if isinstance(r.get("lines"), list)} - - def save_writing(self, token: str, user_id: str, topic: str, lines: list[str]) -> None: - """Keeps a topic's lines as the student of this token, in place of what they wrote in it before.""" - row = {"user_id": user_id, "topic": topic, "lines": lines, "updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())} - req = urllib.request.Request( - self.url + "/rest/v1/writings?on_conflict=user_id,topic", data=json.dumps(row).encode(), method="POST", - headers={"Authorization": f"Bearer {token}", "apikey": self.key, "Content-Type": "application/json", - "Prefer": "resolution=merge-duplicates,return=minimal", "User-Agent": "ihki/1.0"}) - try: - urllib.request.urlopen(req, timeout=TIMEOUT).close() - except urllib.error.HTTPError as e: - raise AuthUnavailable(f"supabase answered {e.code}: {e.read().decode(errors='replace')[:300]}") from e - except (urllib.error.URLError, TimeoutError) as e: - raise AuthUnavailable(str(e)) from e - - def all_writings(self) -> list[dict] | None: - """Every student's written homework, for the panel: user_id, topic, lines. Before the table is created it is - None: nothing a student writes is kept, which the panel says.""" - try: - return self._admin(f"/rest/v1/writings?select=user_id,topic,lines&limit={PAGE * 10}") - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - log.warning("the table `writings` is missing (see the README): the panel shows no written homework") - return None - - def _get(self, path: str, token: str, key: str): - req = urllib.request.Request( - self.url + path, headers={"Authorization": f"Bearer {token}", "apikey": key, "User-Agent": "ihki/1.0"}) - with urllib.request.urlopen(req, timeout=TIMEOUT) as res: - return json.load(res) - - def _admin(self, path: str, method: str = "GET", body: dict | None = None, prefer: str = "return=representation", - timeout: int = TIMEOUT): - """A call with the secret key, for the teacher's panel. Unlike a student's own progress, a failure is raised.""" + def _admin(self, path: str, method: str = "GET", body=None, prefer: str = "return=representation"): if not self.secret: raise AuthUnavailable("SUPABASE_SECRET_KEY is not set") - req = urllib.request.Request( - self.url + path, method=method, data=json.dumps(body).encode() if body is not None else None, - headers={"Authorization": f"Bearer {self.secret}", "apikey": self.secret, "Content-Type": "application/json", - "Prefer": prefer, "User-Agent": "ihki/1.0"}) - try: - with urllib.request.urlopen(req, timeout=timeout) as res: - return json.loads(res.read() or b"null") - except urllib.error.HTTPError as e: - detail = e.read().decode(errors="replace")[:300] - if e.code == 409: - raise ClassConflict(detail) from e - raise AuthUnavailable(f"supabase answered {e.code}: {detail}") from e - except (urllib.error.URLError, TimeoutError, ValueError) as e: - raise AuthUnavailable(str(e)) from e - - def backup_rows(self) -> dict[str, list[dict]]: - """All application-owned table rows. Tables not installed in an older deployment are omitted.""" - result: dict[str, list[dict]] = {} - for table in BACKUP_TABLES: - rows = [] - try: - for offset in range(0, 10_000_000, PAGE): - path = f"/rest/v1/{table}?select=*&limit={PAGE}&offset={offset}" - for attempt in range(3): - try: - batch = self._admin(path, timeout=60) or [] - break - except AuthUnavailable as e: - if attempt == 2 or not any(x in str(e).lower() for x in ("timed out", "timeout")): - raise - rows.extend(batch) - if len(batch) < PAGE: - break - result[table] = rows - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - return result - - def final_scores(self) -> list[dict]: - """One compact row per student and exercise: the highest score that drives the panel's final grades.""" - best: dict[tuple[str, str, str], dict] = {} - for offset in range(0, MAX_CLASS_ATTEMPTS, PAGE): - query = urllib.parse.urlencode({"select": "user_id,cat,phrase,score,created_at", - "order": "id.asc", "limit": PAGE, "offset": offset}) - path = f"/rest/v1/attempts?{query}" - for attempt in range(3): - try: - batch = self._admin(path, timeout=60) or [] - break - except AuthUnavailable as e: - if attempt == 2 or not any(x in str(e).lower() for x in ("timed out", "timeout")): - raise - for row in batch: - key = (str(row["user_id"]), str(row["cat"]), str(row["phrase"])) - if key not in best or int(row["score"]) > int(best[key]["score"]): - best[key] = row - if len(batch) < PAGE: - break - return list(best.values()) - - def restore_final_scores(self, rows: list[dict]) -> int: - """Restore only grades that improve the currently stored best score, without restoring attempt history.""" - current = {(r["user_id"], r["cat"], r["phrase"]): int(r["score"]) for r in self.final_scores()} - add = [{**r, "unrelated": False} for r in rows - if int(r.get("score", -1)) > current.get((r.get("user_id"), r.get("cat"), r.get("phrase")), -1)] - for start in range(0, len(add), 200): - self._admin("/rest/v1/attempts", "POST", add[start:start + 200], prefer="return=minimal", timeout=60) - return len(add) - - def restore_rows(self, tables: dict[str, list[dict]]) -> dict[str, int]: - """Merge a validated application backup. Existing rows are updated; unrelated current rows remain.""" - restored: dict[str, int] = {} - text_ids: dict[int, int] = {} - daily_ids: dict[int, int] = {} - for table in BACKUP_TABLES: - rows = [dict(r) for r in tables.get(table, [])] - if not rows: - continue - if table == "classes": - for row in rows: - row["dailies"] = [{**d, "video": daily_ids.get(d.get("video"), d.get("video"))} - for d in (row.get("dailies") or [])] - for field in ("homework",): - value = row.get(field, "") - if isinstance(value, str) and value.startswith("t") and value[1:].isdigit(): - row[field] = "t" + str(text_ids.get(int(value[1:]), int(value[1:]))) - row["homeworks"] = [{**h, "topic": "t" + str(text_ids.get(int(h["topic"][1:]), int(h["topic"][1:]))) - if isinstance(h.get("topic"), str) and h["topic"].startswith("t") and h["topic"][1:].isdigit() - else h.get("topic")} for h in (row.get("homeworks") or [])] - if table == "writings": - field = "topic" - for row in rows: - value = row.get(field, "") - if isinstance(value, str) and value.startswith("t") and value[1:].isdigit(): - row[field] = "t" + str(text_ids.get(int(value[1:]), int(value[1:]))) - if table in BACKUP_NATURAL_KEYS: - key = BACKUP_NATURAL_KEYS[table] - for start in range(0, len(rows), 200): - self._admin(f"/rest/v1/{table}?on_conflict={key}", "POST", rows[start:start + 200], - prefer="resolution=merge-duplicates,return=minimal") - restored[table] = len(rows) - continue - for original in rows: - row = dict(original) - old_id = row.get("id") - if old_id is not None: - patch = dict(row) - patch.pop("id", None) - existing = self._admin(f"/rest/v1/{table}?id=eq.{urllib.parse.quote(str(old_id))}", "PATCH", patch) - if existing: - new_id = old_id - else: - row.pop("id", None) # identity columns reject explicitly supplied values - made = self._admin(f"/rest/v1/{table}", "POST", row) - new_id = made[0].get("id") if made else None - if table == "texts" and new_id is not None: - text_ids[int(old_id)] = int(new_id) - if table == "dailies" and new_id is not None: - daily_ids[int(old_id)] = int(new_id) - else: - self._admin(f"/rest/v1/{table}", "POST", row, prefer="return=minimal") - restored[table] = len(rows) - self._classes = self._videos = self._texts = self._ranked = None - self._level_videos.clear() - self._access.clear() - return restored + request = urllib.request.Request(self.url + path, method=method, + data=json.dumps(body).encode() if body is not None else None, + headers={"Authorization": f"Bearer {self.secret}", "apikey": self.secret, + "Content-Type": "application/json", "Prefer": prefer, "User-Agent": "mathly/1.0"}) + try: + with urllib.request.urlopen(request, timeout=TIMEOUT) as response: + return json.loads(response.read() or b"null") + except urllib.error.HTTPError as exc: + detail = exc.read().decode(errors="replace")[:300] + if exc.code == 409: + raise ClassConflict(detail) from exc + raise AuthUnavailable(f"supabase answered {exc.code}: {detail}") from exc + except (urllib.error.URLError, TimeoutError, ValueError) as exc: + raise AuthUnavailable(str(exc)) from exc def classes(self, fresh: bool = False) -> list[dict]: - """Every class, newest first: id, name, emails (lower-case), nicknames ({email: nickname}), lessons (lesson ids; - empty in a class saved before classes had lessons), homeworks ([{"topic": a topic id, "due": its ISO date or - None}], one for each topic the class has as homework), homework and due (the first of them, as a class had - before it could have several; a class saved then has only these), dailies ([{"video": the id of a daily - video, "date": the ISO date the class watches it}]), created_at. - Kept for CLASSES_TTL, as each sign-in - check asks. Without the secret key, or before the table is created, there are none.""" now = time.monotonic() with self._lock: - hit = self._classes - if hit and hit[0] > now and not fresh: - return hit[1] + cached = self._classes + if cached and cached[0] > now and not fresh: + return cached[1] rows: list[dict] = [] if self.secret: try: - # every column: a table made before the nicknames still lets its students in - rows = self._admin("/rest/v1/classes?select=*&order=created_at.desc") - for r in rows: - r["nicknames"], r["lessons"] = r.get("nicknames") or {}, r.get("lessons") or [] - r["homework"], r["due"] = r.get("homework") or "", r.get("due") or None - r["homeworks"], r["dailies"] = r.get("homeworks") or [], r.get("dailies") or [] - r["teachers"] = [str(e).lower() for e in (r.get("teachers") or [])] - except AuthUnavailable as e: - if "answered 404" not in str(e): + rows = self._admin("/rest/v1/classes?select=*&order=created_at.desc") or [] + except AuthUnavailable as exc: + if "answered 404" not in str(exc): raise - log.warning("the table `classes` is missing (see the README): every account is let in") + log.warning("the classes table is missing; every account is allowed") + # During the database migration, ignore rows belonging to a removed product. + rows = [row for row in rows if not row.get("subject") or row.get("subject") == "מתמטיקה"] + for row in rows: + row["emails"] = [str(value).lower() for value in row.get("emails") or []] + row["teachers"] = [str(value).lower() for value in row.get("teachers") or []] + row["lessons"] = row.get("lessons") or [] + row["nicknames"] = row.get("nicknames") or {} with self._lock: self._classes = (now + CLASSES_TTL, rows) return rows - def database_health(self) -> bool: - """Perform one bounded, read-only PostgREST probe without returning table data.""" - if not self.secret: - return False - try: - self._admin("/rest/v1/classes?select=id&limit=1") - except AuthUnavailable as exc: - if "answered 404" not in str(exc): - raise - return True - - def save_class(self, id: int | None, name: str, emails: list[str], nicknames: dict[str, str], - lessons: list[str], homeworks: list[dict] = (), dailies: list[dict] | None = None, - subject: str = "", teachers: list[str] | None = None) -> None: - """Adds a class (id None) or replaces one's name, emails, nicknames, lessons and homeworks ([{"topic", "due"}]), - and its daily videos' days ([{"video", "date"}]; None leaves them as they are, so a table made before them - still takes a class that has none). A name already taken raises ClassConflict.""" - first = homeworks[0] if homeworks else {"topic": "", "due": None} # the columns of a class with one homework + def save_class(self, class_id: int | None, name: str, emails: list[str], nicknames: dict[str, str], + lessons: list[str], *, teachers: list[str] | None = None, **_ignored) -> None: body = {"name": name, "emails": emails, "nicknames": nicknames, "lessons": lessons, - "homework": first["topic"], "due": first["due"], "homeworks": list(homeworks)} - if dailies is not None: - body["dailies"] = list(dailies) - if teachers is not None: - body["teachers"] = list(teachers) - if id is None: - body["subject"] = subject + "teachers": list(teachers or [])} + if class_id is None: self._admin("/rest/v1/classes", "POST", body) - elif not self._admin(f"/rest/v1/classes?id=eq.{int(id)}", "PATCH", body): - raise KeyError(id) + elif not self._admin(f"/rest/v1/classes?id=eq.{int(class_id)}", "PATCH", body): + raise KeyError(class_id) self._classes = None - def delete_class(self, id: int) -> None: - self._admin(f"/rest/v1/classes?id=eq.{int(id)}", "DELETE") + def delete_class(self, class_id: int) -> None: + self._admin(f"/rest/v1/classes?id=eq.{int(class_id)}", "DELETE") self._classes = None - def videos(self, fresh: bool = False) -> list[dict]: - """Every lesson video, each lesson's in the order the teacher gave them: id, lesson (a lesson id), title, url - (the Vimeo or YouTube link), pos (how many of the lesson's own topics stand before it; None in a table made before videos - had places: after them all) and ord (its turn among the videos of one place). - Kept for CLASSES_TTL, as the path asks. Without the secret key, or before the table is created, there are none.""" - now = time.monotonic() - with self._lock: - hit = self._videos - if hit and hit[0] > now and not fresh: - return hit[1] - rows: list[dict] = [] - if self.secret: - try: - # every column: a table made before the places still shows its videos - rows = self._admin("/rest/v1/videos?select=*&order=id.asc") - for r in rows: - r["pos"], r["ord"] = r.get("pos"), r.get("ord") or 0 - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - log.warning("the table `videos` is missing (see the README): the lessons have no videos") - with self._lock: - self._videos = (now + CLASSES_TTL, rows) - return rows - - def add_video(self, lesson: str, title: str, url: str, pos: int, ord: int) -> None: - self._admin("/rest/v1/videos", "POST", {"lesson": lesson, "title": title, "url": url, "pos": pos, "ord": ord}) - self._videos = None - - def place(self, places: list[tuple[str, int, int, int]]) -> None: - """Moves videos and texts: (table, id, pos, ord) of each; the table is "videos" or "texts".""" - try: - for table, id, pos, ord in places: - assert table in ("videos", "texts") - self._admin(f"/rest/v1/{table}?id=eq.{int(id)}", "PATCH", {"pos": pos, "ord": ord}) - finally: - self._videos = self._texts = None - - def texts(self, fresh: bool = False) -> list[dict]: - """Every text the teacher added: id, lesson (a lesson id), title, voices (a dialogue's two voices by name, - the odd lines' and the even lines'; empty for a story), lines ([Hebrew, Arabic in Hebrew letters] each), and - pos and ord as a video has them (see `videos`). - Kept for CLASSES_TTL, as each score asks. Without the secret key, or before the table is created, there are none.""" - now = time.monotonic() - with self._lock: - hit = self._texts - if hit and hit[0] > now and not fresh: - return hit[1] - rows: list[dict] = [] - if self.secret: - try: - rows = self._admin("/rest/v1/texts?select=*&order=id.asc") - for r in rows: - r["voices"], r["ord"] = r.get("voices") or [], r.get("ord") or 0 - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - log.warning("the table `texts` is missing (see the README): the lessons have no texts") - with self._lock: - self._texts = (now + CLASSES_TTL, rows) - return rows - - def add_text(self, lesson: str, title: str, voices: list[str], lines: list[list[str]], pos: int, ord: int) -> None: - self._admin("/rest/v1/texts", "POST", {"lesson": lesson, "title": title, "voices": voices, "lines": lines, - "pos": pos, "ord": ord}) - self._texts = None - - def delete_text(self, id: int) -> None: - self._admin(f"/rest/v1/texts?id=eq.{int(id)}", "DELETE") - self._texts = None - - def delete_video(self, id: int) -> None: - self._admin(f"/rest/v1/videos?id=eq.{int(id)}", "DELETE") - self._videos = None - - def level_videos(self, table: str, fresh: bool = False) -> list[dict]: - """A level's videos that stand outside the path, in the order the teacher gave them (`ord`, see - `order_level_videos`) and then, those never moved, in the order they were added: id, level (a level id), title, - url (the Vimeo or YouTube link). `table` is "dailies" (the daily videos) or "library" (the video library, - which is one for every level: its `level` is empty). Kept for CLASSES_TTL. Without the secret key, or before the table is created, there are none.""" - assert table in ("dailies", "library") - now = time.monotonic() - with self._lock: - hit = self._level_videos.get(table) - if hit and hit[0] > now and not fresh: - return hit[1] - rows: list[dict] = [] - if self.secret: - try: - rows = self._admin(f"/rest/v1/{table}?select=*&order=id.asc") - rows.sort(key=lambda r: (r.get("ord") is None, r.get("ord") or 0, r["id"])) - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - log.warning("the table `%s` is missing (see the README): it has no videos", table) - with self._lock: - self._level_videos[table] = (now + CLASSES_TTL, rows) - return rows - - def add_level_video(self, table: str, level: str, title: str, url: str) -> None: - assert table in ("dailies", "library") - self._admin(f"/rest/v1/{table}", "POST", {"level": level, "title": title, "url": url}) - self._level_videos.pop(table, None) - - def delete_level_video(self, table: str, id: int) -> None: - assert table in ("dailies", "library") - self._admin(f"/rest/v1/{table}?id=eq.{int(id)}", "DELETE") - self._level_videos.pop(table, None) - - def rename_level_video(self, table: str, id: int, title: str) -> None: - assert table in ("dailies", "library") - try: - self._admin(f"/rest/v1/{table}?id=eq.{int(id)}", "PATCH", {"title": title}) - finally: - self._level_videos.pop(table, None) - - def order_level_videos(self, table: str, ids: list[int]) -> None: - """Puts the table's videos in this order: each one's `ord` is its turn.""" - assert table in ("dailies", "library") - try: - for k, id in enumerate(ids): - self._admin(f"/rest/v1/{table}?id=eq.{int(id)}", "PATCH", {"ord": k}) - finally: - self._level_videos.pop(table, None) - - def ranked(self, fresh: bool = False) -> frozenset[str] | None: - """The lower-case emails the teacher gave the tutor's rank (the table `tutors`). Kept for CLASSES_TTL, as each - sign-in check asks. None before the table is created; without the secret key there are none.""" - now = time.monotonic() - with self._lock: - hit = self._ranked - if hit and hit[0] > now and not fresh: - return hit[1] - emails: frozenset[str] | None = frozenset() - if self.secret: - try: - emails = frozenset(r["email"].lower() for r in self._admin("/rest/v1/tutors?select=email")) - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - log.warning("the table `tutors` is missing (see the README): no student has the tutor's rank") - emails = None - with self._lock: - self._ranked = (now + CLASSES_TTL, emails) - return emails - - def set_tutor(self, email: str, on: bool, by: str) -> None: - """Gives the account of this email the tutor's rank, or takes it back. `by` is the teacher who gave it.""" - email = email.lower() - try: - if on: - self._admin("/rest/v1/tutors", "POST", {"email": email, "granted_by": by}) - else: - self._admin(f"/rest/v1/tutors?email=eq.{urllib.parse.quote(email)}", "DELETE") - except ClassConflict: # has it already - pass - finally: - self._ranked = None - - def current_lessons(self, email: str) -> dict[int, str]: - """The current lesson this account chose in each of its classes: {class id: lesson id}. Empty without the - secret key or before the table is created (the page then keeps the choice on the device); other failures raise.""" - if not self.secret: - return {} - q = urllib.parse.urlencode({"select": "class_id,current_lesson", "email": f"eq.{email.strip().lower()}"}) - try: - rows = self._admin(f"/rest/v1/learner_class_state?{q}") or [] - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - log.warning("the table `learner_class_state` is missing (see the README): current lessons stay on the device") - return {} - return {int(r["class_id"]): str(r["current_lesson"]) for r in rows if r.get("current_lesson")} - - def set_current_lesson(self, email: str, class_id: int, lesson: str) -> None: - """Keeps this account's current lesson in one class, replacing the one it had there. Only the account's own row: - the caller passes the session's email. A missing table raises with the command that creates it.""" - row = {"email": email.strip().lower(), "class_id": int(class_id), "current_lesson": str(lesson).strip(), - "updated_at": datetime.now(timezone.utc).isoformat()} - try: - self._admin("/rest/v1/learner_class_state?on_conflict=email,class_id", "POST", row, - prefer="resolution=merge-duplicates,return=minimal") - except AuthUnavailable as e: - if "answered 404" in str(e): - raise AuthUnavailable("the table learner_class_state is missing; run in Supabase's SQL editor: " - f"{CREATE_LEARNER_CLASS_STATE}") from e - raise - - def _put_object(self, bucket: str, name: str, data: bytes, mime: str) -> bool: - """Stores a recording in a private storage bucket; False (logged) when it cannot be stored.""" - if not self.secret: - return False - req = urllib.request.Request( - f"{self.url}/storage/v1/object/{bucket}/{urllib.parse.quote(name)}", data=data, method="POST", - headers={"Authorization": f"Bearer {self.secret}", "apikey": self.secret, "Content-Type": mime, - "User-Agent": "ihki/1.0"}) - try: - urllib.request.urlopen(req, timeout=TIMEOUT).close() - return True - except (urllib.error.URLError, TimeoutError) as e: # HTTPError is a URLError - log.warning("a recording not stored (is there a storage bucket `%s`? see the README): %s", bucket, e) - return False - - def _delete_objects(self, bucket: str, names: list[str]) -> bool: - """Removes recordings of a private storage bucket; False (logged) when some could not be removed.""" - ok = True - for k in range(0, len(names), 1000): # the storage API takes up to 1000 names a call - req = urllib.request.Request( - f"{self.url}/storage/v1/object/{bucket}", method="DELETE", - data=json.dumps({"prefixes": names[k:k + 1000]}).encode(), - headers={"Authorization": f"Bearer {self.secret}", "apikey": self.secret, - "Content-Type": "application/json", "User-Agent": "ihki/1.0"}) - try: - urllib.request.urlopen(req, timeout=TIMEOUT).close() - except (urllib.error.URLError, TimeoutError) as e: - log.warning("recordings of the storage bucket `%s` not removed: %s", bucket, e) - ok = False - return ok - - def _get_object(self, bucket: str, name: str) -> tuple[bytes, str]: - """A recording of a private storage bucket and its media type.""" - req = urllib.request.Request( - f"{self.url}/storage/v1/object/{bucket}/{urllib.parse.quote(name)}", - headers={"Authorization": f"Bearer {self.secret}", "apikey": self.secret, "User-Agent": "ihki/1.0"}) - try: - with urllib.request.urlopen(req, timeout=TIMEOUT) as res: - data = res.read() - media_type = res.headers.get_content_type() - except (urllib.error.URLError, TimeoutError) as e: - raise AuthUnavailable(f"{bucket} audio unavailable: {e}") from e - if media_type == "application/octet-stream": - media_type = mimetypes.guess_type(name)[0] or media_type - return data, media_type - - def _put_audio(self, bucket: str, name: str, data: bytes, mime: str) -> bool: - """Keep audio in HF when configured; Supabase is the legacy/default store only.""" - if self.audio_store: - try: - self.audio_store.put(bucket, name, data) - return True - except Exception as e: - log.warning("%s audio not stored in Hugging Face bucket: %s", bucket, e) - return False - return self._put_object(bucket, name, data, mime) - - def _get_audio(self, bucket: str, name: str, created_at: str = "") -> tuple[bytes, str]: - """Read new HF audio first, then fall back to legacy Supabase recordings.""" - sources = ("hf", "supabase") if self.audio_store else ("supabase",) - errors = [] - for source in sources: - if source == "hf": - if not self.audio_store: - continue - try: - return self.audio_store.get(bucket, name), mimetypes.guess_type(name)[0] or "application/octet-stream" - except Exception as e: - errors.append(f"Hugging Face: {e}") - else: - try: - return self._get_object(bucket, name) - except AuthUnavailable as e: - errors.append(f"Supabase: {e}") - raise AuthUnavailable(f"{bucket} audio unavailable ({'; '.join(errors)})") - - def migrate_legacy_audio(self) -> None: - """One-way migration before a fixed cutoff; newer legacy Supabase files remain indefinitely.""" - if not self.audio_store or not self.secret or not self.audio_migrate_before: - return - threading.Thread(target=self._migrate_legacy_audio, name="ihki-audio-migration", daemon=True).start() - - def _migrate_legacy_audio(self) -> None: - """Copy pre-cutoff audio to HF, then remove only the successfully copied Supabase objects.""" - cutoff = self.audio_migrate_before - marker = hashlib.sha256(cutoff.encode()).hexdigest()[:16] + ".complete" - try: - self.audio_store.get("_migration", marker) - return - except Exception: - pass - complete = True - try: - for table, bucket in (("flags", "flags"), ("lab_samples", "lab")): - rows = [] - for offset in range(0, 10_000_000, PAGE): - query = urllib.parse.urlencode({"select": "audio,created_at", "audio": "not.is.null", - "created_at": f"lt.{cutoff}", "order": "created_at.asc", - "limit": PAGE, "offset": offset}) - batch = self._admin(f"/rest/v1/{table}?{query}") or [] - rows.extend(batch) - if len(batch) < PAGE: - break - copied = [] - for row in rows: - name = row.get("audio") - if not name: - continue - try: - data, _mime = self._get_object(bucket, name) - self.audio_store.put(bucket, name, data) - copied.append(name) - except Exception as e: - complete = False - log.warning("old %s audio %s not migrated; Supabase copy retained: %s", bucket, name, e) - if copied and self._delete_objects(bucket, copied): - log.info("migrated %d old %s recordings to Hugging Face and removed Supabase copies", - len(copied), bucket) - if complete: - self.audio_store.put("_migration", marker, cutoff.encode()) - except Exception as e: - log.warning("legacy audio migration failed: %s", e) - - def ensure_backup_bucket(self) -> None: - """Create the private application-backup bucket on first use.""" - try: - self._admin("/storage/v1/bucket/backups") - except AuthUnavailable as e: - detail = str(e).lower() - if not any(mark in detail for mark in ("answered 404", '"statuscode":"404"', "nosuchbucket", "bucket not found")): - raise - self._admin("/storage/v1/bucket", "POST", {"id": "backups", "name": "backups", "public": False}, - prefer="return=minimal") - - def save_backup(self, name: str, data: bytes) -> None: - self.ensure_backup_bucket() - req = urllib.request.Request( - f"{self.url}/storage/v1/object/backups/{urllib.parse.quote(name)}", data=data, method="POST", - headers={"Authorization": f"Bearer {self.secret}", "apikey": self.secret, - "Content-Type": "application/zip", "User-Agent": "ihki/1.0"}) - try: - urllib.request.urlopen(req, timeout=max(TIMEOUT, 60)).close() - except urllib.error.HTTPError as e: - raise AuthUnavailable(f"supabase storage answered {e.code}: {e.read().decode(errors='replace')[:300]}") from e - except (urllib.error.URLError, TimeoutError) as e: - raise AuthUnavailable(str(e)) from e - - def list_backups(self) -> list[dict]: - self.ensure_backup_bucket() - rows = self._admin("/storage/v1/object/list/backups", "POST", - {"prefix": "", "limit": 100, "offset": 0, "sortBy": {"column": "name", "order": "desc"}}) - return [r for r in (rows or []) if str(r.get("name", "")).endswith(".zip")] - - def add_flag(self, row: dict, audio: bytes, mime: str, name: str) -> bool: - """Keeps a flagged score and its recording in HF, or Supabase when HF is not configured.""" - kept = self._put_audio("flags", name, audio, mime) - self._admin("/rest/v1/flags", "POST", {**row, "audio": name if kept else None}) - return kept - - def list_flags(self, status: str = "pending") -> list[dict]: - """Flagged scores for the teacher's review, newest first.""" - query = urllib.parse.urlencode({"select": "*", "status": f"eq.{status}", "order": "created_at.desc"}) - return self._admin(f"/rest/v1/flags?{query}") - - def review_flag(self, flag_id: int, values: dict) -> None: - """Records a teacher's decision about one flagged score.""" - self._admin(f"/rest/v1/flags?id=eq.{int(flag_id)}", "PATCH", values) - - def flag(self, flag_id: int) -> dict | None: - """One flag's row, or None.""" - rows = self._admin(f"/rest/v1/flags?{urllib.parse.urlencode({'select': '*', 'id': f'eq.{int(flag_id)}', 'limit': 1})}") - return rows[0] if rows else None - - def flag_audio(self, flag_id: int) -> tuple[bytes, str] | None: - """Returns one private flag recording and its media type, or None when it was not stored.""" - query = urllib.parse.urlencode({"select": "audio,created_at", "id": f"eq.{int(flag_id)}", "limit": 1}) - rows = self._admin(f"/rest/v1/flags?{query}") - name = rows[0].get("audio") if rows else None - return self._get_audio("flags", name, rows[0].get("created_at", "")) if name else None - - def add_lab_sample(self, row: dict, audio: bytes, mime: str, name: str, attempts: int = 1) -> bool: - """Keeps a lab recording (see lab.py): the recording in the storage bucket `lab`, the row in the table - `lab_samples`. As with a flag, a recording that cannot be stored leaves the row without it (False). - `attempts` retries the upload (a bulk import outlives the storage's transient 5xx errors).""" - kept = False - for attempt in range(max(1, attempts)): - if attempt: - time.sleep(2 * attempt) - kept = self._put_audio("lab", name, audio, mime) - if kept: - break - self._admin("/rest/v1/lab_samples", "POST", {**row, "audio": name if kept else None}, prefer="return=minimal") - return kept - - def lab_samples(self, full: bool = False) -> list[dict]: - """The lab's recordings, oldest first: what labels each one (and, with `full`, the scorer's whole result).""" - cols = "*" if full else "id,item,speaker,label,mistake,score,heard_ipa,flagged,model,audio,created_at" - # PostgREST caps a response (1000 rows by default), so the table is read in pages by id. - rows, last, page = [], 0, 500 if full else 1000 - while True: - query = urllib.parse.urlencode({"select": cols, "order": "id.asc", "id": f"gt.{last}", "limit": page}) - batch = self._admin(f"/rest/v1/lab_samples?{query}") or [] - rows.extend(batch) - if len(batch) < page: - return rows - last = batch[-1]["id"] - - def lab_sample(self, sample_id: int) -> dict | None: - """One lab recording's row, without the scorer's result, or None.""" - query = urllib.parse.urlencode({"select": "id,item,ar,ipa,speaker,label,mistake,audio,result", "id": f"eq.{int(sample_id)}", - "limit": 1}) - rows = self._admin(f"/rest/v1/lab_samples?{query}") - return rows[0] if rows else None - - def lab_audio(self, sample_id: int) -> tuple[bytes, str] | None: - """One lab recording and its media type, or None when it was not stored.""" - row = self.lab_sample(sample_id) - return self._get_audio("lab", row["audio"], row.get("created_at", "")) if row and row.get("audio") else None - - def update_lab_result(self, sample_id: int, result: dict) -> None: - """Updates a lab result's review metadata (the original scoring fields remain in the same JSON document).""" - self._admin(f"/rest/v1/lab_samples?id=eq.{int(sample_id)}", "PATCH", {"result": result}) - - def delete_lab_samples(self, speaker: str | None = None) -> int: - """Deletes the lab's recordings of one speaker ("": the unnamed), or of every speaker when None: the rows, - then their files in the bucket `lab` (a file that cannot be removed is logged and left). Returns how many rows.""" - where = {"speaker": f"eq.{speaker}"} if speaker is not None else {"id": "gt.0"} # a DELETE needs a filter - rows = self._admin(f"/rest/v1/lab_samples?{urllib.parse.urlencode({**where, 'select': 'id,audio'})}", - "DELETE") or [] - names = [r["audio"] for r in rows if r.get("audio")] - if names: - self._delete_objects("lab", names) - if self.audio_store: - try: - self.audio_store.delete("lab", names) - except Exception as e: - log.warning("lab recordings not deleted from Hugging Face bucket: %s", e) - return len(rows) - - def calibrations(self, status: str | None = None) -> list[dict]: - """Calibration proposals, optionally filtered by their workflow status.""" - params = {"select": "*", "order": "created_at.desc"} - if status: - params["status"] = f"eq.{status}" - return self._admin(f"/rest/v1/calibrations?{urllib.parse.urlencode(params)}") - - def add_calibration(self, row: dict) -> dict: - """Creates one draft calibration proposal and returns its stored row.""" - made = self._admin("/rest/v1/calibrations", "POST", row) - return made[0] if made else row - - def update_calibration(self, calibration_id: int, values: dict) -> None: - """Updates and optionally applies or rejects a calibration proposal.""" - self._admin(f"/rest/v1/calibrations?id=eq.{int(calibration_id)}", "PATCH", values) - - def delete_calibration(self, calibration_id: int) -> bool: - """Deletes a calibration proposal or rule; returns whether it existed.""" - return bool(self._admin(f"/rest/v1/calibrations?id=eq.{int(calibration_id)}", "DELETE")) - - def scoring_rules(self) -> list[dict]: - """The teacher's switches over the rules of pronunciation-rules.md: id, active, updated_by, updated_at. - A rule with no row is on.""" - return self._admin("/rest/v1/scoring_rules?select=*") or [] - - def set_scoring_rule(self, rule_id: str, active: bool, by: str) -> None: - """Switches one rule on or off (an upsert on its id).""" - self._admin("/rest/v1/scoring_rules", "POST", {"id": rule_id, "active": bool(active), "updated_by": by, - "updated_at": datetime.now(timezone.utc).isoformat()}, - prefer="resolution=merge-duplicates,return=minimal") - - def model_settings(self) -> list[dict]: - """The scorer settings tuned per model in the panel's "מודלים" tab: model, settings, updated_by, updated_at.""" - return self._admin("/rest/v1/model_settings?select=*") or [] - - def set_model_settings(self, model: str, settings: dict, by: str) -> None: - """Replaces one model's settings (an upsert on the model id).""" - self._admin("/rest/v1/model_settings", "POST", {"model": model, "settings": settings, "updated_by": by, - "updated_at": datetime.now(timezone.utc).isoformat()}, - prefer="resolution=merge-duplicates,return=minimal") - - def users(self) -> list[dict]: - """Every account that has signed in: id, email, name (as Google gave it; empty for one who signed in by email).""" - users: list[dict] = [] - for page in range(1, 51): - batch = self._admin(f"/auth/v1/admin/users?page={page}&per_page={PAGE}").get("users", []) - users += [{"id": u["id"], "email": u.get("email") or "", - "name": (u.get("user_metadata") or {}).get("full_name") or ""} for u in batch] - if len(batch) < PAGE: - break - return users - def access_for(self, user_id: str, fresh: bool = False) -> dict | None: - """The roles and student entitlements assigned to an account. None also means the migration table is absent.""" + if not self.secret: + return None now = time.monotonic() with self._lock: - hit = self._access.get(user_id) - if not fresh and hit and hit[0] > now: - return hit[1] + cached = self._access.get(user_id) + if cached and cached[0] > now and not fresh: + return cached[1] try: - rows = self._admin("/rest/v1/user_access?" + urllib.parse.urlencode({ - "user_id": f"eq.{user_id}", "select": "user_id,email,roles,entitlements,subject,updated_at"})) + rows = self._admin("/rest/v1/user_access?" + urllib.parse.urlencode( + {"user_id": f"eq.{user_id}", "select": "user_id,email,roles,updated_at"})) value = rows[0] if rows else None - except AuthUnavailable as e: - if "subject" in str(e).lower(): - rows = self._admin("/rest/v1/user_access?" + urllib.parse.urlencode({ - "user_id": f"eq.{user_id}", "select": "user_id,email,roles,entitlements,updated_at"})) - value = ({**rows[0], "subject": ""} if rows else None) - elif "answered 404" not in str(e): + except AuthUnavailable as exc: + if "answered 404" not in str(exc): raise - else: - value = None + value = None with self._lock: self._access[user_id] = (now + CLASSES_TTL, value) return value - def access_rows(self) -> list[dict] | None: - """All explicit access assignments, or None until the user_access table is installed.""" - try: - return self._admin("/rest/v1/user_access?select=user_id,email,roles,entitlements,subject,updated_at") or [] - except AuthUnavailable as e: - if "subject" in str(e).lower(): - return [{**r, "subject": ""} for r in - (self._admin("/rest/v1/user_access?select=user_id,email,roles,entitlements,updated_at") or [])] - if "answered 404" in str(e): - return None - raise - - def set_access(self, target: dict, roles: list[str], entitlements: list[str], actor: dict) -> None: - """Upsert an assignment and append a best-effort audit record.""" - old = self.access_for(target["id"]) - row = {"user_id": target["id"], "email": target["email"].lower(), "roles": roles, - "entitlements": entitlements, "updated_at": datetime.now(timezone.utc).isoformat(), - "updated_by": actor["id"]} - self._admin("/rest/v1/user_access?on_conflict=user_id", "POST", row, - prefer="resolution=merge-duplicates,return=minimal") - with self._lock: - self._access.pop(target["id"], None) - try: - self._admin("/rest/v1/access_audit", "POST", - {"target_user_id": target["id"], "changed_by": actor["id"], - "old_access": old or {}, "new_access": row}, prefer="return=minimal") - except AuthUnavailable as e: - log.warning("access changed but audit row was not stored: %s", e) - self.log_event("permission", actor, target["email"], {"roles": roles, "entitlements": entitlements}) - - def set_admin_subject(self, target: dict, subject: str, actor: dict) -> None: - """Set an admin's subject without changing their access assignment.""" - old = self.access_for(target["id"]) or {} - row = {"user_id": target["id"], "email": target["email"].lower(), - "roles": old.get("roles") or ["student", "flagger"], "entitlements": old.get("entitlements") or [], - "subject": subject, "updated_at": datetime.now(timezone.utc).isoformat(), - "updated_by": actor["id"]} - self._admin("/rest/v1/user_access?on_conflict=user_id", "POST", row, - prefer="resolution=merge-duplicates,return=minimal") - with self._lock: - self._access.pop(target["id"], None) - self.log_event("permission", actor, target["email"], {"admin_subject": subject}) - - def secret_rows(self) -> list[dict] | None: - """The API keys set in the panel, encrypted (see keystore.py), or None until the table `app_secrets` exists.""" - try: - return self._admin("/rest/v1/app_secrets?select=name,ciphertext,last4,updated_by,updated_at") or [] - except AuthUnavailable as e: - if "answered 404" in str(e): - return None - raise - - def save_secret(self, row: dict) -> None: - self._admin("/rest/v1/app_secrets?on_conflict=name", "POST", - {**row, "updated_at": datetime.now(timezone.utc).isoformat()}, - prefer="resolution=merge-duplicates,return=minimal") - - def delete_secret(self, name: str) -> None: - self._admin("/rest/v1/app_secrets?" + urllib.parse.urlencode({"name": f"eq.{name}"}), "DELETE", - prefer="return=minimal") - - def all_attempts(self, limit: int = MAX_CLASS_ATTEMPTS) -> list[dict]: - """Every student's attempts, as `attempts` gives one student's, each with its user_id. "Different sentence" - rows remain marked as unrelated for diagnostics, but count toward progress like every non-empty recording.""" - limit = max(1, min(int(limit), MAX_CLASS_ATTEMPTS)) - rows: list[dict] = [] - while len(rows) < limit: - page = min(PAGE, limit - len(rows)) - query = urllib.parse.urlencode({"select": "user_id,cat,phrase,score,unrelated,created_at", - "order": "id.desc", "limit": page, "offset": len(rows)}) - batch = self._admin(f"/rest/v1/attempts?{query}") - rows += batch - if len(batch) < page: - break - return rows - - def scoped_attempts(self, user_ids: set[str], since: datetime, until: datetime, - limit: int = MAX_DIFFICULTY_ATTEMPTS, *, - include_diagnostics: bool = False) -> tuple[list[dict], bool] | tuple[list[dict], bool, bool]: - """Bounded attempt metadata for authorized users and ``since <= created_at < until``. - - The caller gets at most ``limit`` rows plus a truncation signal. Audio, diagnostics, - answers, and attempts from other users are never requested. - """ - ids = sorted({str(user_id) for user_id in user_ids if str(user_id)}) - if not ids: - return ([], False, True) if include_diagnostics else ([], False) - limit = max(1, min(int(limit), MAX_DIFFICULTY_ATTEMPTS)) - wanted, rows = limit + 1, [] - while len(rows) < wanted: - page = min(PAGE, wanted - len(rows)) - fields = "id,user_id,cat,phrase,score,unrelated,created_at" - if include_diagnostics: - fields += ",issues,model,version" - query = urllib.parse.urlencode([ - ("select", fields), - ("user_id", f"in.({','.join(ids)})"), - ("created_at", f"gte.{since.astimezone(timezone.utc).isoformat()}"), - ("created_at", f"lt.{until.astimezone(timezone.utc).isoformat()}"), - ("order", "created_at.asc,id.asc"), ("limit", page), ("offset", len(rows)), - ]) - try: - batch = self._admin(f"/rest/v1/attempts?{query}") or [] - except AuthUnavailable as exc: - if include_diagnostics and any(name in str(exc) for name in ("issues", "model", "version")): - return [], False, False - raise - rows += batch - if len(batch) < page: - break - result = (rows[:limit], len(rows) > limit) - return (*result, True) if include_diagnostics else result - - def activity(self, hours: int = 24, bucket_minutes: int = 15) -> dict: - """Aggregated attempt counts for monitoring, without returning student identities. - - ``activity_summary`` supplies a distinct-student count for the whole window. Keep the buckets usable with - older Supabase installations until that small migration is applied. - """ - buckets = self._admin("/rest/v1/rpc/activity_buckets", "POST", - {"p_hours": hours, "p_bucket_minutes": bucket_minutes}) - summary = None - try: - rows = self._admin("/rest/v1/rpc/activity_summary", "POST", {"p_hours": hours}) or [] - summary = rows[0] if rows else None - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - return {"buckets": buckets, "summary": summary} - - def scoped_activity(self, user_ids: set[str], since: datetime, bucket_minutes: int = 15) -> dict: - """Aggregated monitoring data for one admin's students, calculated in Postgres without downloading attempts.""" - if not user_ids: - return {"buckets": [], "summary": {"attempts": 0, "active_students": 0, - "returning_students": 0, "active_days": 0}} - body = {"p_user_ids": sorted(user_ids), "p_since": since.isoformat()} - try: - buckets = self._admin("/rest/v1/rpc/activity_buckets_scoped", "POST", - {**body, "p_bucket_minutes": bucket_minutes}) - rows = self._admin("/rest/v1/rpc/activity_summary_scoped", "POST", body) or [] - return {"buckets": buckets or [], "summary": rows[0] if rows else None} - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise - log.warning("scoped activity RPCs are missing; using the paged timestamp fallback") - return self._scoped_activity_fallback(user_ids, since, bucket_minutes) - - def _scoped_activity_fallback(self, user_ids: set[str], since: datetime, bucket_minutes: int) -> dict: - """Accurate compatibility path until the scoped aggregate RPC migration is installed.""" - rows: list[dict] = [] - for offset in range(0, MAX_CLASS_ATTEMPTS, PAGE): - query = urllib.parse.urlencode({"select": "user_id,created_at", "user_id": f"in.({','.join(sorted(user_ids))})", - "created_at": f"gte.{since.isoformat()}", "order": "created_at.desc", - "limit": PAGE, "offset": offset}) - batch = self._admin(f"/rest/v1/attempts?{query}") or [] - rows += batch - if len(batch) < PAGE: - break - seconds, buckets, per_user = max(1, min(bucket_minutes, 60)) * 60, {}, {} - local = ZoneInfo("Asia/Jerusalem") - for row in rows: - stamp = datetime.strptime(row["created_at"][:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc) - slot = int(stamp.timestamp()) // seconds * seconds - item = buckets.setdefault(slot, {"users": set(), "attempts": 0}) - item["attempts"] += 1; item["users"].add(row["user_id"]) - per_user.setdefault(row["user_id"], set()).add(stamp.astimezone(local).date()) - return {"buckets": [{"bucket": datetime.fromtimestamp(slot, timezone.utc).isoformat(), - "attempts": item["attempts"], "active_students": len(item["users"])} - for slot, item in sorted(buckets.items())], - "summary": {"attempts": len(rows), "active_students": len(per_user), - "returning_students": sum(len(days) >= 2 for days in per_user.values()), - "active_days": len({day for days in per_user.values() for day in days})}} - - def log_event(self, kind: str, user: dict | None, label: str = "", details: dict | None = None) -> None: - """Best-effort append to the management activity feed; a missing audit table never blocks the action.""" - try: - self._admin("/rest/v1/system_events", "POST", { - "kind": kind, "actor_id": user.get("id") if user else None, - "actor_email": (user or {}).get("email", ""), "label": label[:200], "details": details or {}, - }, prefer="return=minimal") - except AuthUnavailable as e: - log.warning("system activity event not stored: %s", e) - - def recent_events(self, limit: int = 60) -> list[dict] | None: - """Newest audit events, or None when the optional table has not been installed yet.""" - query = urllib.parse.urlencode({"select": "kind,actor_email,label,details,created_at", - "order": "created_at.desc", "limit": max(1, min(limit, 200))}) - try: - return self._admin(f"/rest/v1/system_events?{query}") or [] - except AuthUnavailable as e: - if "answered 404" in str(e): - return None - raise - - def save_tutor_chat(self, row: dict) -> None: - """Keeps a conversation with the tutor (see tutor.py), in place of what was kept of it before. A failure is - logged, never raised: the conversation goes on without it.""" - try: - self._admin("/rest/v1/tutor_chats?on_conflict=id", "POST", row, - prefer="resolution=merge-duplicates,return=minimal") - except AuthUnavailable as e: - log.warning("a tutor conversation not kept%s: %s", - " (the table `tutor_chats` is missing, see tutor.CREATE_TUTOR_CHATS)" if "answered 404" in str(e) else "", e) - - def tutor_chats_since(self, user_id: str, since: str) -> int: - """How many conversations with the tutor the student started since this ISO time. 0 when they cannot be read.""" - query = urllib.parse.urlencode({"select": "id", "user_id": f"eq.{user_id}", "created_at": f"gte.{since}"}) - try: - return len(self._admin(f"/rest/v1/tutor_chats?{query}") or []) - except AuthUnavailable as e: - log.warning("tutor conversations not counted: %s", e) - return 0 - - def all_attempt_issues(self) -> list[dict] | None: - """Compact phoneme issues from recent attempts. None means the diagnostic columns have not been migrated. - - Monitoring only needs a useful recent sample; bounding this query keeps opening the panel independent of the - full, ever-growing attempt history. Rows are requested newest first. - """ - rows: list[dict] = [] - while len(rows) < MAX_MONITOR_ATTEMPTS: - limit = min(PAGE, MAX_MONITOR_ATTEMPTS - len(rows)) - query = urllib.parse.urlencode({"select": "user_id,phrase,score,issues,model,version", - "unrelated": "is.false", "order": "id.desc", - "limit": limit, "offset": len(rows)}) - try: - batch = self._admin(f"/rest/v1/attempts?{query}") - except AuthUnavailable as e: - if any(name in str(e) for name in ("issues", "model", "version")): - return None - raise - rows += batch - if len(batch) < limit: - break - return rows - def load_auth() -> SupabaseAuth | None: url = os.environ.get("SUPABASE_URL", "").strip() key = os.environ.get("SUPABASE_PUBLISHABLE_KEY", "").strip() if not url or not key: - missing = [n for n, v in (("SUPABASE_URL", url), ("SUPABASE_PUBLISHABLE_KEY", key)) if not v] - seen = sorted(k for k in os.environ if "SUPA" in k.upper()) # names only: shows a misspelt variable - log.info("no sign-in, the service is open: %s missing or empty (similar names in the environment: %s)", - " and ".join(missing), seen or "none") + log.info("sign-in is disabled; the math path is public") return None emails = lambda name: frozenset(re.split(r"[\s,;]+", os.environ.get(name, "").strip().lower())) - {""} - teachers, tutors, developers = emails("TEACHER_EMAILS"), emails("TUTORS_EMAIL"), emails("DEVELOPER_EMAILS") - secret = os.environ.get("SUPABASE_SECRET_KEY", "").strip() - log.info("sign-in on: %s", "the students of the classes" if secret else "any account (no SUPABASE_SECRET_KEY)") - if os.environ.get("ALLOWED_EMAILS", "").strip(): - log.warning("ALLOWED_EMAILS is no longer read: the teacher lists the students in the panel's classes") - if teachers and not secret: - log.warning("TEACHER_EMAILS is set without SUPABASE_SECRET_KEY: the management panel has no data to show") - if tutors and not secret: - log.warning("TUTORS_EMAIL is set without SUPABASE_SECRET_KEY: a flag cannot be saved") - audio_store = None - if bucket := os.environ.get("AUDIO_BUCKET", "").strip(): - audio_store = HFAudioStore(bucket, os.environ.get("AUDIO_BUCKET_PREFIX", "ihki-audio"), - os.environ.get("HF_TOKEN", "").strip() or None) - log.info("voice reports use Hugging Face bucket %s", bucket) - cutoff = os.environ.get("AUDIO_MIGRATE_BEFORE", DEFAULT_AUDIO_MIGRATION_CUTOFF).strip() - return SupabaseAuth(url, key, teachers, secret, tutors, developers, audio_store, cutoff) + return SupabaseAuth(url, key, emails("TEACHER_EMAILS"), os.environ.get("SUPABASE_SECRET_KEY", "").strip(), + emails("DEVELOPER_EMAILS")) diff --git a/app/class_management.py b/app/class_management.py deleted file mode 100644 index cdb3ccb0ab9691acaea7ff386ea9ce010328d8c8..0000000000000000000000000000000000000000 --- a/app/class_management.py +++ /dev/null @@ -1,208 +0,0 @@ -"""Shared, scope-aware class and roster operations for browser and MCP callers.""" -from __future__ import annotations - -import hashlib -import json -import re -from typing import Any - -from .access import has_permission -from .auth import AuthUnavailable, ClassConflict, SupabaseAuth - - -MAX_CLASS_NAME = 80 -MAX_NICKNAME = 40 -EMAIL = re.compile(r"[^@\s,;<>]+@[^@\s,;<>]+\.[^@\s,;<>]+") -OPERATIONS = frozenset({"create_class", "rename_class", "add_student", "remove_student"}) -PRESERVED = ("name", "emails", "nicknames", "lessons", "homeworks", "dailies", "subject", "teachers") - - -class ClassManagementError(ValueError): - """Safe error suitable for management API and MCP results.""" - - -class ClassStorageUnavailable(ClassManagementError): - pass - - -def normalize_class_name(value: object) -> str: - name = " ".join(str(value or "").split()) - if not name or len(name) > MAX_CLASS_NAME: - raise ClassManagementError("לכיתה צריך שם (עד 80 תווים)") - return name - - -def parse_roster(text: str) -> tuple[list[str], dict[str, str]]: - """Parse the management UI's line-oriented ``email [nickname]`` format.""" - emails, nicknames = set(), {} - for line in re.split(r"[\r\n]+", text): - found = EMAIL.findall(line) - rest = re.sub(r"\s+", " ", EMAIL.sub(" ", line)).strip(" ,;:-–<>()\t") - if (not found and rest) or (len(found) > 1 and rest.strip(" ,;")): - raise ValueError(line.strip()) - for email in found: - emails.add(email.lower()) - if len(found) == 1 and rest: - nicknames[found[0].lower()] = rest[:MAX_NICKNAME] - return sorted(emails), nicknames - - -def normalize_student(email: object, nickname: object = "") -> tuple[str, str]: - email = str(email or "").strip().lower() - if not EMAIL.fullmatch(email): - raise ClassManagementError("כתובת המייל של התלמיד/ה אינה תקינה") - nickname = " ".join(str(nickname or "").split())[:MAX_NICKNAME] - return email, nickname - - -def visible_classes(auth: SupabaseAuth, actor: dict | None, *, fresh: bool = False) -> list[dict]: - try: - rows = auth.classes(fresh=fresh) - except AuthUnavailable as exc: - raise ClassStorageUnavailable(f"כיתות אינן זמינות: {exc}") from exc - if not actor or "developer" in actor.get("roles", ()) or not hasattr(auth, "access_for"): - return rows - subject = str(actor.get("subject") or "").strip() - return [row for row in rows if subject and str(row.get("subject") or "").strip() == subject] - - -def public_classes(auth: SupabaseAuth, actor: dict) -> list[dict]: - return [{"id": row["id"], "name": row["name"], "subject": str(row.get("subject") or ""), - "students": [{"email": email, **({"nickname": row.get("nicknames", {}).get(email)} - if row.get("nicknames", {}).get(email) else {})} - for email in row.get("emails", [])]} - for row in visible_classes(auth, actor, fresh=True)] - - -def fingerprint(row: dict) -> str: - content = {key: row.get(key, [] if key in {"emails", "lessons", "homeworks", "dailies", "teachers"} else - {} if key == "nicknames" else "") for key in PRESERVED} - return hashlib.sha256(json.dumps(content, ensure_ascii=False, sort_keys=True, - separators=(",", ":")).encode()).hexdigest() - - -def _find(auth: SupabaseAuth, actor: dict, class_id: object) -> dict: - if not isinstance(class_id, int) or isinstance(class_id, bool) or class_id <= 0: - raise ClassManagementError("מזהה הכיתה אינו תקין") - row = next((item for item in visible_classes(auth, actor, fresh=True) if item.get("id") == class_id), None) - if row is None: - raise ClassManagementError("הכיתה לא נמצאה") - return row - - -def _unique(rows: list[dict], name: str, except_id: int | None = None) -> None: - key = name.casefold() - if any(row.get("id") != except_id and " ".join(str(row.get("name") or "").split()).casefold() == key - for row in rows): - raise ClassManagementError("כבר יש כיתה בשם הזה") - - -def prepare(auth: SupabaseAuth, actor: dict, operation: str, *, class_id: int | None = None, - name: str = "", subject: str = "", email: str = "", nickname: str = "", - students: list[dict[str, str]] | None = None, - initial_lessons: dict[str, str]) -> dict[str, Any]: - if operation not in OPERATIONS: - raise ClassManagementError("פעולת הכיתה אינה מותרת") - if not has_permission(actor, "management.access"): - raise ClassManagementError("אין הרשאת ניהול") - if operation == "create_class": - clean_name = normalize_class_name(name) - own_subject = str(actor.get("subject") or "").strip() - clean_subject = own_subject if "developer" not in actor.get("roles", ()) else " ".join(subject.split()) - if clean_subject not in initial_lessons: - raise ClassManagementError("יש לבחור מקצוע נתמך לכיתה") - _unique(visible_classes(auth, actor, fresh=True), clean_name) - emails, nicknames = [], {} - for student in students or []: - if not isinstance(student, dict): - raise ClassManagementError("רשימת התלמידים אינה תקינה") - student_email, student_nickname = normalize_student(student.get("email"), student.get("nickname")) - if student_email in emails: - raise ClassManagementError("אותו תלמיד מופיע פעמיים") - emails.append(student_email) - if student_nickname: - nicknames[student_email] = student_nickname - return {"operation": operation, "name": clean_name, "subject": clean_subject, - "emails": sorted(emails), "nicknames": nicknames, - "lesson": initial_lessons[clean_subject]} - - row = _find(auth, actor, class_id) - plan: dict[str, Any] = {"operation": operation, "class_id": row["id"], "class_name": row["name"], - "state": fingerprint(row)} - if operation == "rename_class": - clean_name = normalize_class_name(name) - _unique(visible_classes(auth, actor, fresh=True), clean_name, int(row["id"])) - if clean_name == row["name"]: - raise ClassManagementError("זה כבר שם הכיתה") - plan["name"] = clean_name - else: - clean_email, clean_nickname = normalize_student(email, nickname) - enrolled = clean_email in row.get("emails", []) - if operation == "add_student" and enrolled: - raise ClassManagementError("התלמיד/ה כבר בכיתה") - if operation == "remove_student" and not enrolled: - raise ClassManagementError("התלמיד/ה אינו/ה בכיתה") - if operation == "remove_student" and clean_email in (row.get("teachers") or []): - raise ClassManagementError("אי אפשר להסיר מורה באמצעות פעולת תלמידים") - plan.update(email=clean_email, nickname=clean_nickname) - return plan - - -def _save(auth: SupabaseAuth, row: dict) -> None: - args = (int(row["id"]), row["name"], list(row.get("emails") or []), dict(row.get("nicknames") or {}), - list(row.get("lessons") or []), list(row.get("homeworks") or []), list(row.get("dailies") or []), - str(row.get("subject") or "")) - try: - auth.save_class(*args, teachers=list(row.get("teachers") or [])) - except TypeError: - auth.save_class(*args) - - -def apply(auth: SupabaseAuth, actor: dict, plan: dict, *, initial_lessons: dict[str, str]) -> dict[str, Any]: - operation = str(plan.get("operation") or "") - if not has_permission(actor, "management.access") or operation not in OPERATIONS: - raise ClassManagementError("אין הרשאת ניהול") - try: - if operation == "create_class": - name, subject = normalize_class_name(plan.get("name")), str(plan.get("subject") or "") - own_subject = str(actor.get("subject") or "").strip() - if subject not in initial_lessons or ("developer" not in actor.get("roles", ()) and subject != own_subject): - raise ClassManagementError("אין הרשאה למקצוע של הכיתה") - _unique(visible_classes(auth, actor, fresh=True), name) - auth.save_class(None, name, list(plan.get("emails") or []), dict(plan.get("nicknames") or {}), - [initial_lessons[subject]], [], None, subject) - created = next((row for row in visible_classes(auth, actor, fresh=True) - if " ".join(row.get("name", "").split()).casefold() == name.casefold()), None) - return {"operation": operation, "class_id": created.get("id") if created else None, - "class_name": name, "subject": subject} - - row = _find(auth, actor, plan.get("class_id")) - if fingerprint(row) != plan.get("state"): - raise ClassManagementError("הכיתה השתנתה מאז ההכנה. הכינו את הפעולה מחדש") - if operation == "rename_class": - name = normalize_class_name(plan.get("name")) - _unique(visible_classes(auth, actor, fresh=True), name, int(row["id"])) - row = {**row, "name": name} - elif operation == "add_student": - email, nickname = normalize_student(plan.get("email"), plan.get("nickname")) - if email in row.get("emails", []): - raise ClassManagementError("התלמיד/ה כבר בכיתה") - emails, nicknames = sorted([*row.get("emails", []), email]), dict(row.get("nicknames") or {}) - if nickname: - nicknames[email] = nickname - row = {**row, "emails": emails, "nicknames": nicknames} - else: - email, _ = normalize_student(plan.get("email")) - if email not in row.get("emails", []): - raise ClassManagementError("התלמיד/ה אינו/ה בכיתה") - if email in (row.get("teachers") or []): - raise ClassManagementError("אי אפשר להסיר מורה באמצעות פעולת תלמידים") - row = {**row, "emails": [item for item in row.get("emails", []) if item != email], - "nicknames": {key: value for key, value in (row.get("nicknames") or {}).items() if key != email}} - _save(auth, row) - return {"operation": operation, "class_id": row["id"], "class_name": row["name"], - **({"email": plan["email"]} if operation in {"add_student", "remove_student"} else {})} - except (ClassManagementError, ClassConflict): - raise - except AuthUnavailable as exc: - raise ClassStorageUnavailable(f"לא ניתן לשמור את הכיתה: {exc}") from exc diff --git a/app/conjugation.py b/app/conjugation.py deleted file mode 100644 index fae3fc7270739e5907af86db41d7ef7c146b937d..0000000000000000000000000000000000000000 --- a/app/conjugation.py +++ /dev/null @@ -1,215 +0,0 @@ -"""Stage A's verbs (שלב א') in the past and the future, every pronoun, for the "איתן" levels of פועל יוצא. - -The future's prefixes are the Hebrew ones, אותיות איתן: ا/א for "I", ت/ת for "you" and "she", ي/י for "he" and -"they", ن/נ for "we"; "you" (f.) adds ـي/ـי and the plurals ـو/ـו, as in Hebrew. Each form is split into -prefix, body and ending so the page can colour the parts, beside the Hebrew form split the same way. - -Arabic forms come from LEX, the app's source of truth. A future form that LEX lacks comes from future_draft.py and -marks its verb a draft: only teachers see draft verbs. -""" -from __future__ import annotations - -from .future_draft import FUTURE_DRAFT -from .grammar import PRONOUNS -from .workbook import LEX, VERBS_1, VERBS_3, VERBS_4, VERBS_5, VERBS_6, VERBS_8, VERBS_9, VERBS_10, _plain - -STAGE_A = [("1", VERBS_1), ("3", VERBS_3), ("4", VERBS_4), ("5", VERBS_5), ("6", VERBS_6), ("8", VERBS_8), - ("9", VERBS_9), ("10", VERBS_10)] - -# The Hebrew future, unpointed: the "I" form, the body after the prefix, and the body before ـי / ـו (קנה -> תקני). -HE_FUTURE = { - "يفتح": ("אפתח", "פתח", "פתח"), "ياكل": ("אוכל", "אכל", "אכל"), "يسأل": ("אשאל", "שאל", "שאל"), - "يقعد": ("אשב", "שב", "שב"), "يدخل": ("איכנס", "יכנס", "יכנס"), "يسبح": ("אשחה", "שחה", "שח"), - "يطبخ": ("אבשל", "בשל", "בשל"), "ينقل": ("אעבור", "עבור", "עבר"), "يجي": ("אבוא", "בוא", "בוא"), - "يحكي": ("אדבר", "דבר", "דבר"), "يعطي": ("אתן", "יתן", "יתנ"), "يشتري": ("אקנה", "קנה", "קנ"), - "يروح": ("אלך", "לך", "לכ"), "يقول": ("אומר", "אמר", "אמר"), "يزور": ("אבקר", "בקר", "בקר"), - "يكون": ("אהיה", "היה", "הי"), "يبوس": ("אנשק", "נשק", "נשק"), "يسوق": ("אנהג", "נהג", "נהג"), - "يشوف": ("אראה", "ראה", "רא"), "يفوت": ("איכנס", "יכנס", "יכנס"), "ياخد": ("אקח", "יקח", "יקח"), - "يشرب": ("אשתה", "שתה", "שת"), "يطلع": ("אצא", "צא", "צא"), "يسمع": ("אשמע", "שמע", "שמע"), - "يفهم": ("אבין", "בין", "בינ"), "يرجع": ("אחזור", "חזור", "חזר"), "يشكر": ("אודה", "ודה", "וד"), - "يتغدّى": ("אוכל", "אכל", "אכל"), "ينزل": ("ארד", "רד", "רד"), "يوصل": ("אגיע", "גיע", "גיע"), - "يضحك": ("אצחק", "צחק", "צחק"), "يلبس": ("אלבש", "לבש", "לבש"), "يلعب": ("אשחק", "שחק", "שחק"), -} - -FUTURE_PREFIX = "اتتيتنتي" # in PRONOUNS order -FUTURE_SUFFIX = ["", "", "ي", "", "", "", "و", "و"] -PAST_SUFFIX = ["ت", "ت", "تي", "", "ت", "نا", "تو", "و"] -HE_PREFIX = "אתתיתנתי" -HE_FUTURE_SUFFIX = ["", "", "י", "", "", "", "ו", "ו"] -HE_PAST_SUFFIX = [("תי",), ("ת",), ("ת",), ("",), ("ה",), ("נו",), ("תם", "תן"), ("ו",)] -# the tiles the student builds with: (Arabic, as the page shows it in Hebrew transliteration). A future prefix tile -# shows the verb's own vowel (תֻ, תִ, תְ), filled in per question. -PREFIX_TILES = [("ا", "א"), ("ت", "ת"), ("ي", "י"), ("ن", "נ")] -FUTURE_SUFFIX_TILES = [("", ""), ("ي", "\u05B4י"), ("و", "וּ")] -PAST_SUFFIX_TILES = [("", ""), ("ت", "ת"), ("تي", "תִי"), ("نا", "נַא"), ("تو", "תוּ"), ("و", "וּ")] -# a past form's ending in transliteration, per pronoun (فتّي is written פֻתִّי) -PAST_SUFFIX_TR = [("ת",), ("ת",), ("תִי", "תִّי"), ("",), ("ת",), ("נַא", "ّא"), ("תוּ", "תّוּ"), ("וּ",)] -MARKS = set(chr(c) for c in range(0x0591, 0x05C8)) | {"'", "\u0651"} -FINALS = dict(zip("כמנפצ", "ךםןףץ")) -# Arabic forms the rule (prefix + the "he" form's body + ending) does not give -IRREGULAR = {"يجي": ["اجي", "تيجي", "تيجي", "يجي", "تيجي", "نيجي", "تيجو", "يجو"]} -ANA_IRREGULAR = {"ياكل": "آكل", "ياخد": "آخد"} -SHADDA = "ّ" - - -def plain(ar: str) -> str: - """The form as the tiles spell it: without shadda (كنّا, فتّي).""" - return ar.replace(SHADDA, "") - - -def compose(prefix: str, body: str, suffix: str) -> str: - """Prefix + body + ending, as the page builds it: ا + اكل is آكل, and a body ending in ي/ى gives way to the - ending (تحكي + و -> تحكو, تتغدّى + ي -> تتغدّي).""" - word = prefix + body - if word.startswith("اا"): - word = "آ" + word[2:] - if suffix and body and body[-1] in "يى": - return word[:-1] + suffix - return word + suffix - - -def _future_arabic(he: str) -> list[str]: - if he in IRREGULAR: - return IRREGULAR[he] - body = he[1:] - forms = [compose(pre, body, suf) for pre, suf in zip(FUTURE_PREFIX, FUTURE_SUFFIX)] - forms[0] = ANA_IRREGULAR.get(he, forms[0]) - return forms - - -def _lookup(ar: str, pronoun: int) -> tuple[str, list[str], bool] | None: - """LEX's entry for a future form (checked), else the draft's; "I" takes the entry read אַ (اسمع#i, not اسمع).""" - keys = [k for k in LEX if k.partition("#")[0] == ar and not k.endswith("#imp")] - if pronoun == 0: - keys.sort(key=lambda k: (not LEX[k][0].startswith(("אַ", "אֹ")), "#" in k)) - else: - keys = [k for k in keys if not LEX[k][0].startswith("א")] or keys - keys.sort(key=lambda k: "#" in k) - if keys: - tr, ipas = LEX[keys[0]] - return tr, list(ipas), False - if ar in FUTURE_DRAFT: - tr, ipas = FUTURE_DRAFT[ar] - return tr, list(ipas), True - return None - - -def _future_body(he: str, target: str, pronoun: int) -> str: - """The body the page shows between the slots: the "he" form's, unless that does not build this form (تيجي).""" - prefix, suffix = FUTURE_PREFIX[pronoun], FUTURE_SUFFIX[pronoun] - rest = ("ا" + target[1:]) if target.startswith("آ") else target[1:] - stripped = rest[:-1] if suffix and rest.endswith(suffix) else rest - for body in (he[1:], rest, stripped + "ي", stripped): - if plain(compose(prefix, body, suffix)) == plain(target): - return body - raise ValueError(f"no body builds {target}") - - -def ar_parts(ar: str, tense: str, pronoun: int) -> dict[str, str]: - """A form split for colouring: {pre, body, suf}.""" - if tense == "future": - suf = FUTURE_SUFFIX[pronoun] if FUTURE_SUFFIX[pronoun] and ar.endswith(FUTURE_SUFFIX[pronoun]) else "" - return {"pre": ar[0], "body": ar[1:len(ar) - len(suf)], "suf": suf} - suf = PAST_SUFFIX[pronoun] - if suf and ar.endswith(SHADDA + suf[-1]) and len(suf) == 2 and ar[:-2].endswith(suf[0]): # كنّا, فتّي - return {"pre": "", "body": ar[:-3], "suf": ar[-3:]} - if suf and ar.endswith(suf): - return {"pre": "", "body": ar[:-len(suf)], "suf": suf} - return {"pre": "", "body": ar, "suf": ""} - - -def lead(tr: str) -> tuple[str, str]: - """A transliteration's first letter with its marks (תֻ, גִ'), and the rest.""" - i = 1 - while i < len(tr) and tr[i] in MARKS: - i += 1 - return tr[:i], tr[i:] - - -def tr_parts(tr: str, tense: str, pronoun: int) -> dict[str, str]: - """A transliteration split for colouring, as ar_parts splits the Arabic: {pre, body, suf}.""" - if tense == "past": - for suf in PAST_SUFFIX_TR[pronoun]: - if suf and tr.endswith(suf) and len(tr) > len(suf): - return {"pre": "", "body": tr[:-len(suf)], "suf": suf} - return {"pre": "", "body": tr, "suf": ""} - pre, rest = lead(tr) - if FUTURE_SUFFIX[pronoun] == "ي" and rest.endswith("י"): - body = rest[:-1] - cut = body.rfind("\u05B4") - if cut >= len(body) - 3: # the ending's hiriq, on the last letter (before a geresh: חִ'י) - body = body[:cut] + body[cut + 1:] - return {"pre": pre, "body": body, "suf": "\u05B4י"} - if FUTURE_SUFFIX[pronoun] == "و" and rest.endswith("וּ"): - return {"pre": pre, "body": rest[:-2], "suf": "וּ"} - return {"pre": pre, "body": rest, "suf": ""} - - -def compose_tr(pre: str, body: str, suf: str) -> str: - """The transliteration the tiles spell, as the page shows it while building: a final letter at the end only, and - an ending's hiriq before a geresh (חִ'י).""" - i = len(body) - while i and body[i - 1] in MARKS: - i -= 1 - letter = body[i - 1] if i else "" - regular = {v: k for k, v in FINALS.items()} - if suf and letter in regular: - body = body[:i - 1] + regular[letter] + body[i:] - elif not suf and letter in FINALS and i == len(body): # כּ at the end stays as the course writes it - body = body[:i - 1] + FINALS[letter] + body[i:] - if suf.startswith("\u05B4") and body.endswith("'"): - return pre + body[:-1] + "\u05B4'" + suf[1:] - return pre + body + suf - - -def _he_past_parts(he: str, pronoun: int) -> dict[str, str]: - for suf in HE_PAST_SUFFIX[pronoun]: - if suf and he.endswith(suf): - return {"pre": "", "body": he[:-len(suf)], "suf": suf} - return {"pre": "", "body": he, "suf": ""} - - -def _he_future_parts(spec: tuple[str, str, str], pronoun: int) -> dict[str, str]: - ana, body, before_suffix = spec - if pronoun == 0: - return {"pre": ana[0], "body": ana[1:], "suf": ""} - suf = HE_FUTURE_SUFFIX[pronoun] - return {"pre": HE_PREFIX[pronoun], "body": before_suffix if suf else body, "suf": suf} - - -def _verb(lesson: str, item: tuple) -> dict: - he_past, keys, he = item[0].split(), item[1].split(), item[2] - name = _plain(he_past[3]) + (f" {item[3]}" if len(item) > 3 else "") - past = [] - for p, key in enumerate(keys): - tr, ipas = LEX[key] - ar = key.partition("#")[0] - past.append({"ar": ar, "tr": tr, "ipas": list(ipas), "parts": ar_parts(ar, "past", p), - "tr_parts": tr_parts(tr, "past", p), - "he": _he_past_parts(_plain(he_past[p]), p), "draft": False, - "body": ar_parts(plain(ar), "past", p)["body"]}) - future = [] - for p, ar in enumerate(_future_arabic(he)): - found = _lookup(ar, p) - if not found: - raise ValueError(f"{he}: no entry for {ar}") - tr, ipas, draft = found - future.append({"ar": ar, "tr": tr, "ipas": ipas, "parts": ar_parts(ar, "future", p), - "tr_parts": tr_parts(tr, "future", p), - "he": _he_future_parts(HE_FUTURE[he], p), "draft": draft, "body": _future_body(he, ar, p)}) - return {"id": he, "lesson": lesson, "name": name, "arabic": f"{keys[3].partition('#')[0]} · {he}", - "transliteration": f"{past[3]['tr']} · {future[3]['tr']}", - "past": past, "future": future, "draft": any(f["draft"] for f in future)} - - -def _stage_a() -> dict[str, dict]: - out = {} - for lesson, items in STAGE_A: - for item in items: - if item[2] not in out: - out[item[2]] = _verb(lesson, item) - return out - - -STAGE_A_VERBS = _stage_a() -assert all(len(v["past"]) == len(v["future"]) == len(PRONOUNS) for v in STAGE_A_VERBS.values()) diff --git a/app/future_draft.py b/app/future_draft.py deleted file mode 100644 index e6c01f95fbcafb96c170ebc9a80f403c38e060b6..0000000000000000000000000000000000000000 --- a/app/future_draft.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Draft future forms of stage A's verbs (שלב א'): those that LEX does not have yet. - -Made by rule from the forms LEX has (another prefix letter on a known form, or ـي / ـو added), so they are NOT checked. -The verb game shows a verb whose future is not all in LEX to teachers only, marked טיוטה. Once a teacher has checked -a form, move it into LEX (workbook.py) and delete it here: the verb then reaches students when all eight are in LEX. -The review list is docs/future-forms-draft.md. -""" - -FUTURE_DRAFT: dict[str, tuple[str, list[str]]] = { - # يفتح - "تفتح": ("תִפְתַח", ["t i f t a ħ"]), - "تفتحي": ("תִפְתַחִי", ["t i f t a ħ i"]), - "نفتح": ("נִפְתַח", ["n i f t a ħ"]), - "تفتحو": ("תִפְתַחוּ", ["t i f t a ħ u"]), - "يفتحو": ("יִפְתַחוּ", ["j i f t a ħ u"]), - # ياكل - "ياكلو": ("יֹאכְּלוּ", ["j oː k u l u", "j oː k i l u", "j aː k u l u", "j oː k l u", "j aː k l u"]), - # يدخل - "تدخلي": ("תֻדְחֹ'לִי", ["t u d x u l i"]), - "تدخلو": ("תֻדְחֹ'לוּ", ["t u d x u l u"]), - "يدخلو": ("יֻדְחֹ'לוּ", ["j u d x u l u"]), - # يسبح - "اسبح": ("אַסְבַּח", ["ʔ a s b a ħ"]), - "تسبحي": ("תִסְבַּחִי", ["t i s b a ħ i"]), - "تسبحو": ("תִסְבַּחוּ", ["t i s b a ħ u"]), - "يسبحو": ("יִסְבַּחוּ", ["j i s b a ħ u"]), - # يطبخ - "تطبخو": ("תֻטְבֹּח'וּ", ["t u tˤ b u x u"]), - "يطبخو": ("יֻטְבֹּח'וּ", ["j u tˤ b u x u"]), - # ينقل - "انقل": ("אַנְקֹל", ["ʔ a n ʔ u l", "ʔ a n q u l"]), - "تنقلي": ("תֻנְקֹלִי", ["t u n ʔ u l i", "t u n q u l i"]), - "ننقل": ("נֻנְקֹל", ["n u n ʔ u l", "n u n q u l"]), - "تنقلو": ("תֻנְקֹלוּ", ["t u n ʔ u l u", "t u n q u l u"]), - # يعطي - "نعطي": ("נֵעַטִי", ["n a ʕ tˤ i", "n i ʕ tˤ i"]), - "تعطو": ("תֵעַטוּ", ["t a ʕ tˤ u", "t i ʕ tˤ u"]), - "يعطو": ("יֵעַטוּ", ["j a ʕ tˤ u", "j i ʕ tˤ u"]), - # يشتري - "تشترو": ("תִשְתְרוּ", ["t i ʃ t r u", "t i ʃ t i r u"]), - "يشترو": ("יִשְתְרוּ", ["j i ʃ t r u", "j i ʃ t i r u"]), - # يروح - "تروحو": ("תְרוּחוּ", ["t r uː ħ u", "t i r uː ħ u"]), - # يقول - "تقولي": ("תְקוּלִי", ["t ʔ uː l i", "t q uː l i"]), - "نقول": ("נְקוּל", ["n ʔ uː l", "n q uː l"]), - "تقولو": ("תְקוּלוּ", ["t ʔ uː l u", "t q uː l u"]), - "يقولو": ("יִקוּלוּ", ["j ʔ uː l u", "i ʔ uː l u", "j q uː l u", "j i ʔ uː l u"]), - # يكون - "تكونو": ("תְכּוּנוּ", ["t k uː n u"]), - "يكونو": ("יִכּוּנוּ", ["j k uː n u", "i k uː n u", "j i k uː n u"]), - # يبوس - "تبوسي": ("תְבּוּסִי", ["t b uː s i"]), - "نبوس": ("נְבּוּס", ["n b uː s"]), - "تبوسو": ("תְבּוּסוּ", ["t b uː s u"]), - "يبوسو": ("יִבּוּסוּ", ["j b uː s u", "i b uː s u", "j i b uː s u"]), - # يسوق - "نسوق": ("נְסוּק", ["n s uː ʔ", "n s uː q"]), - "تسوقو": ("תְסוּקוּ", ["t s uː ʔ u", "t s uː q u"]), - "يسوقو": ("יִסוּקוּ", ["j s uː ʔ u", "j s uː q u", "i s uː ʔ u", "j i s uː ʔ u"]), - # يفوت - "تفوتي": ("תְפוּתִי", ["t f uː t i"]), - "نفوت": ("נְפוּת", ["n f uː t"]), - "تفوتو": ("תְפוּתוּ", ["t f uː t u"]), - "يفوتو": ("יִפוּתוּ", ["j f uː t u", "i f uː t u", "j i f uː t u"]), - # ياخد - "ناخد": ("נֹאחֵ'ד", ["n oː x i d", "n aː x u d"]), - "تاخدو": ("תֹאחְ'דוּ", ["t oː x i d u", "t aː x u d u", "t oː x d u", "t aː x d u", "t oː x u d u"]), - # يفهم - "نفهم": ("נִפְהַם", ["n i f h a m"]), - "تفهمو": ("תִפְהַמוּ", ["t i f h a m u"]), - # يرجع - "نرجع": ("נִרְגַ'ע", ["n i r dʒ a ʕ"]), - # يشكر - "اشكر": ("אַשְכֹּר", ["ʔ a ʃ k u r"]), - "تشكر": ("תֻשְכֹּר", ["t u ʃ k u r"]), - "تشكري": ("תֻשְכֹּרִי", ["t u ʃ k u r i"]), - "تشكرو": ("תֻשְכֹּרוּ", ["t u ʃ k u r u"]), - "يشكرو": ("יֻשְכֹּרוּ", ["j u ʃ k u r u"]), - # ينزل - "انزل": ("אַנְזֵל", ["ʔ a n z i l", "ʔ a n z a l"]), - "تنزلي": ("תִנְזֵלִי", ["t i n z i l i", "t i n z a l i"]), - "ننزل": ("נִנְזֵל", ["n i n z i l", "n i n z a l"]), - "ينزلو": ("יִנְזְלוּ", ["j i n z i l u", "j i n z a l u", "j i n z l u"]), - # يوصل - "توصلو": ("תוֹצַלוּ", ["t oː sˤ a l u"]), - # يضحك - "يضحكو": ("יִדְֿחַכּוּ", ["j i dˤ ħ a k u"]), - # يلبس - "البس": ("אַלְבַּס", ["ʔ a l b a s"]), - "نلبس": ("נִלְבַּס", ["n i l b a s"]), - "تلبسو": ("תִלְבַּסוּ", ["t i l b a s u"]), - "يلبسو": ("יִלְבַּסוּ", ["j i l b a s u"]), -} diff --git a/app/grammar.py b/app/grammar.py deleted file mode 100644 index e5d41a4085f3f98c4701e1b50c6eaa2ad37c7547..0000000000000000000000000000000000000000 --- a/app/grammar.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Day 2 sentences as grammar, so the page can ask for the same sentence negated or with another subject. - -A sentence is a subject pronoun, one or two past-tense verbs and fixed words. From each sentence -pattern this module builds every pronoun x {positive, negative} phrase in the format of phrases.py. -Only the forms taught in the course are "listed" (shown in the tab); the rest are reached through -the page's "שלילה" and "הטיית פעלים" buttons. - -Negation is ما before the verb (ما اكلت); the -ش suffix is not accepted yet. -Transliterations follow the course material: אֵל for the article, (ל) for its silent ל, ّ for doubling. -""" -from __future__ import annotations - -import itertools - -# subject pronouns, in the order used by every verb table below -# key Hebrew course tr (arabic, ipa) readings, main one first -PRONOUNS = [ - # The initial glottal stop is routinely absent in natural Palestinian speech. Keep both alignments so a weak - # breathy onset is not forced against ʔ and reported as h, while the written and displayed form stays identical. - ("ana", "אני", "אַנַא", [("انا", "ʔ a n a"), ("انا", "a n a")]), - ("inte", "אתה", "אִנְתֵ", [("انت", "ʔ i n t e"), ("انت", "ʔ i n t a")]), - ("inti", "את", "אִנְתִי", [("انتي", "ʔ i n t i")]), - ("huwwe", "הוא", "הוּّ", [("هوّ", "h u w w e"), ("هو", "h uː")]), - ("hiyye", "היא", "הִיֵّ", [("هيّ", "h i j j e"), ("هي", "h iː")]), - ("ihna", "אנחנו", "אִחְנַא", [("احنا", "ʔ i ħ n a")]), - ("intu", "אתם", "אִנְתוּ", [("انتو", "ʔ i n t u")]), - ("humme", "הם", "הֹםّ", [("همّ", "h u m m e"), ("هم", "h o m")]), -] -P = [p[0] for p in PRONOUNS] - -# past tense, one row per pronoun (PRONOUNS order): Hebrew, course tr, Arabic, IPA readings -VERBS = { - "akal": [ - ("אכלתי", "אַכַּלְת", "اكلت", ["ʔ a k a l t"]), - ("אכלת", "אַכַּלְת", "اكلت", ["ʔ a k a l t"]), - ("אכלת", "אַכַּלְתִי", "اكلتي", ["ʔ a k a l t i"]), - ("אכל", "אַכַּל", "اكل", ["ʔ a k a l"]), - ("אכלה", "אַכְּלַת", "اكلت", ["ʔ a k l a t"]), - ("אכלנו", "אַכַּלְנַא", "اكلنا", ["ʔ a k a l n a"]), - ("אכלתם", "אַכַּלְתוּ", "اكلتو", ["ʔ a k a l t u"]), - ("אכלו", "אַכְּלוּ", "اكلو", ["ʔ a k l u", "ʔ a k a l u"]), - ], - "qaad": [ - ("ישבתי", "קַעַדְת", "قعدت", ["ʔ a ʕ a d t", "q a ʕ a d t"]), - ("ישבת", "קַעַדְת", "قعدت", ["ʔ a ʕ a d t", "q a ʕ a d t"]), - ("ישבת", "קַעַדְתִי", "قعدتي", ["ʔ a ʕ a d t i", "q a ʕ a d t i"]), - ("ישב", "קַעַד", "قعد", ["ʔ a ʕ a d", "q a ʕ a d"]), - ("ישבה", "קַעְדַת", "قعدت", ["ʔ a ʕ d a t", "q a ʕ d a t"]), - ("ישבנו", "קַעַדְנַא", "قعدنا", ["ʔ a ʕ a d n a", "q a ʕ a d n a"]), - ("ישבתם", "קַעַדְתוּ", "قعدتو", ["ʔ a ʕ a d t u", "q a ʕ a d t u"]), - ("ישבו", "קַעַדוּ", "قعدو", ["ʔ a ʕ a d u", "q a ʕ a d u", "ʔ a ʕ d u", "q a ʕ d u"]), - ], - "fatah": [ - ("פתחתי", "פַתַחְת", "فتحت", ["f a t a ħ t"]), - ("פתחת", "פַתַחְת", "فتحت", ["f a t a ħ t"]), - ("פתחת", "פַתַחְתִי", "فتحتي", ["f a t a ħ t i"]), - ("פתח", "פַתַח", "فتح", ["f a t a ħ"]), - ("פתחה", "פַתְחַת", "فتحت", ["f a t ħ a t"]), - ("פתחנו", "פַתַחְנַא", "فتحنا", ["f a t a ħ n a"]), - ("פתחתם", "פַתַחְתוּ", "فتحتو", ["f a t a ħ t u"]), - ("פתחו", "פַתַחוּ", "فتحو", ["f a t a ħ u", "f a t ħ u"]), - ], - "saal": [ - ("שאלתי", "סַאַלֵת", "سألت", ["s a ʔ a l t", "s a ʔ a l i t"]), - ("שאלת", "סַאַלְת", "سألت", ["s a ʔ a l t", "s a ʔ a l i t"]), - ("שאלת", "סַאַלְתִי", "سألتي", ["s a ʔ a l t i"]), - ("שאל", "סַאַל", "سأل", ["s a ʔ a l"]), - ("שאלה", "סַאְלַת", "سألت", ["s a ʔ l a t"]), - ("שאלנו", "סַאַלְנַא", "سألنا", ["s a ʔ a l n a"]), - ("שאלתם", "סַאַלְתוּ", "سألتو", ["s a ʔ a l t u"]), - ("שאלו", "סַאַלוּ", "سألو", ["s a ʔ a l u", "s a ʔ l u"]), - ], -} - -# every other word: arabic -> (course tr, IPA readings) -WORDS = { - "بندورة": ("בַּנְדוֹרַה", ["b a n d oː r a"]), "على": ("עַלַא", ["ʕ a l a"]), - "الطّاولة": ("אֵ(ל)טַّאוְלֵה", ["i tˤ tˤ aː w l e"]), "في": ("פִי", ["f i"]), - "الحارة": ("אֵלְחַארַה", ["i l ħ aː r a"]), "الكرسي": ("אֵלְכֻּרְסִי", ["i l k u r s i"]), - "فلافل": ("פַלַאפֵל", ["f a l aː f i l"]), "الشّبّاك": ("אֵ(ל)שֻّבַּّאכּ", ["i ʃ ʃ u b b aː k"]), - "الدّكتور": ("אֵ(ל)דַّכְּתוֹר", ["i d d a k t oː r", "i d d u k t oː r"]), "الباب": ("אֵלְבַּאבּ", ["i l b aː b"]), - "المعلّم": ("אֵלְמְעַלֵّם", ["i l m ʕ a l l i m"]), "الأكل": ("אֵלְאַכֵּל", ["i l ʔ a k i l", "i l ʔ a k l"]), - "مع": ("מַע", ["m a ʕ"]), "اخوها": ("אַחַ'וּהַא", ["ʔ a x uː h a"]), - "الصّاحب": ("אֵ(ל)צַّאחֵבּ", ["i sˤ sˤ aː ħ i b"]), "السّيّارة": ("אֵ(ל)סַّיַّארַה", ["i s s a j j aː r a"]), - "البيت": ("אֵלְבֵּית", ["i l b eː t"]), "اليوم": ("אֵלְיוֹם", ["i l j oː m"]), - "الطّيّارة": ("אֵ(ל)טַّיַّארַה", ["i tˤ tˤ a j j aː r a"]), "الولد": ("אֵלְוַלַד", ["i l w a l a d"]), - "سؤال": ("סֻאַאל", ["s u ʔ aː l"]), "للكلب": ("לַלְכַּלְבּ", ["l a l k a l b"]), - "الكلب": ("אֵלְכַּלְבּ", ["i l k a l b"]), "احمد": ("אַחְמַד", ["ʔ a ħ m a d"]), - "الكبيرة": ("אֵלְכְּבִּירֵה", ["i l k b iː r e"]), "كبير": ("כְּבִּיר", ["k b iː r"]), - "ابوي": ("אַבּוּי", ["ʔ a b uː j"]), "من": ("מִן", ["m i n"]), -} -NOT = ("ما", "מַא", "m a") # before a verb -AND_NOT = ("وما", "וּמַא", "u m a") # before a second verb - -# The course's sentences: subject taught, Hebrew pattern, Arabic pattern. -# S = subject pronoun, V:verb = conjugated verb, W:verb = "and" + conjugated verb ({S} {V} {W} in the Hebrew). -SENTENCES = [ - ("inti", "{S} {V} עגבנייה ליד השולחן", "S V:akal بندورة على الطّاولة"), - ("inte", "{S} {V} בשכונה", "S V:qaad في الحارة"), - ("huwwe", "{S} {V} על הכיסא ו{W} פלאפל", "S V:qaad على الكرسي W:akal فلافل"), - ("hiyye", "{S} {V} את החלון", "S V:fatah الشّبّاك"), - ("ana", "{S} {V} את הרופא", "S V:saal الدّكتور"), - ("ihna", "{S} {V} את הדלת", "S V:fatah الباب"), - ("intu", "{S} {V} ליד השולחן", "S V:qaad على الطّاولة"), - ("humme", "{S} {V} את המורה", "S V:saal المعلّم"), - ("huwwe", "{S} {V} את האוכל על החלון", "S V:akal الأكل على الشّبّاك"), - ("hiyye", "{S} {V} עם אחיה", "S V:qaad مع اخوها"), - ("inti", "{S} {V} את החבר", "S V:saal الصّاحب"), - ("humme", "{S} {V} את החלון", "S V:fatah الشّبّاك"), - ("intu", "{S} {V} במכונית", "S V:akal في السّيّارة"), - ("ana", "{S} {V} בבית היום", "S V:qaad في البيت اليوم"), - ("inte", "{S} {V} את המטוס", "S V:fatah الطّيّارة"), - ("ihna", "{S} {V} את הילד שאלה", "S V:saal الولد سؤال"), - ("huwwe", "{S} {V} את הדלת לכלב", "S V:fatah الباب للكلب"), - ("hiyye", "{S} {V} את הדלת ו{W} עם אחמד", "S V:fatah الباب W:akal مع احمد"), - ("inti", "{S} {V} את השולחן הגדול", "S V:fatah الطّاولة الكبيرة"), - ("humme", "{S} {V} את החבר שאלה גדולה", "S V:saal الصّاحب سؤال كبير"), - ("inte", "{S} {V} פלאפל ליד השולחן", "S V:akal فلافل على الطّاولة"), - ("intu", "{S} {V} את הדלת היום", "S V:fatah الباب اليوم"), - ("hiyye", "{S} {V} את הרופא", "S V:saal الدّكتور"), - ("ana", "{S} {V} עגבנייה עם אבא שלי", "S V:akal بندورة مع ابوي"), - ("ihna", "{S} {V} בשכונה", "S V:qaad في الحارة"), - ("huwwe", "{S} {V} את המורה שאלה", "S V:saal المعلّم سؤال"), - ("inte", "{S} {V} עם הכלב", "S V:qaad مع الكلب"), - ("hiyye", "{S} {V} את האוכל על החלון", "S V:akal الأكل على الشّبّاك"), - ("inti", "{S} {V} את המטוס", "S V:fatah الطّيّارة"), - ("ihna", "{S} {V} את החבר ו{W}", "S V:saal الصّاحب W:qaad"), - ("humme", "{S} {V} עגבנייה מהשולחן", "S V:akal بندورة من الطّاولة"), -] -MAX_VARIANTS = 16 - - -def _build(he_pattern: str, pattern: str, pron: int, neg: bool) -> dict: - """One phrase: lists of alternative (arabic, ipa) per word, then every combination of them.""" - _, he_pron, tr_pron, pron_alts = PRONOUNS[pron] - slots: list[list[tuple[str, str]]] = [] # per word: its (arabic, ipa) readings - tr: list[str] = [] - he = {"S": he_pron} - for tok in pattern.split(): - kind, _, verb = tok.partition(":") - if tok == "S": - slots.append(pron_alts) - tr.append(tr_pron) - elif kind in ("V", "W") and verb: - he_verb, tr_verb, ar, ipas = VERBS[verb][pron] - he[kind] = ("לא " if neg else "") + he_verb - if kind == "W" and neg: # ... وما اكل - slots.append([AND_NOT[::2]]) - tr.append(AND_NOT[1]) - elif kind == "W": # ... واكل: the و is part of the word - ar, ipas, tr_verb = "و" + ar, ["u " + i for i in ipas], "וּ" + tr_verb - elif neg: # ما اكل ... - slots.append([NOT[::2]]) - tr.append(NOT[1]) - slots.append([(ar, i) for i in ipas]) - tr.append(tr_verb) - else: - tr_word, ipas = WORDS[tok] - slots.append([(tok, i) for i in ipas]) - tr.append(tr_word) - variants = [(" ".join(a for a, _ in combo), " | ".join(i for _, i in combo)) - for combo in itertools.islice(itertools.product(*slots), MAX_VARIANTS)] - return dict(cat="day2", he=he_pattern.format(**he), tr=" ".join(tr), variants=variants) - - -def day2_phrases() -> list[dict]: - """Every pattern x pronoun x polarity. `grp`/`pron`/`neg` let the page move between them; - `listed` marks the course's own sentences, which are the ones the tab starts from, and - `seq` is their 1-based place in SENTENCES: the tab shows them in that order.""" - groups: dict[tuple[str, str], int] = {} - taught: dict[tuple[int, int], int] = {} - for seq, (pron, he_pattern, pattern) in enumerate(SENTENCES, 1): - g = groups.setdefault((he_pattern, pattern), len(groups)) - taught.setdefault((g, P.index(pron)), seq) - out = [] - for (he_pattern, pattern), g in groups.items(): - for pron in range(len(PRONOUNS)): - for neg in (False, True): - phrase = _build(he_pattern, pattern, pron, neg) - phrase.update(grp=g, pron=pron, neg=neg, listed=(g, pron) in taught and not neg) - if phrase["listed"]: - phrase["seq"] = taught[g, pron] - out.append(phrase) - return out diff --git a/app/grok_mcp.py b/app/grok_mcp.py deleted file mode 100644 index 269fc3cd52222466db9c72a8868a15391c0b849e..0000000000000000000000000000000000000000 --- a/app/grok_mcp.py +++ /dev/null @@ -1,490 +0,0 @@ -"""Restricted MCP connector through which Grok can manage library videos.""" -from __future__ import annotations - -import logging -import os -import secrets -import threading -import time -from collections import deque -from dataclasses import dataclass -from typing import Any, Callable, Literal - -from mcp.server.auth.middleware.auth_context import get_access_token -from mcp.server.auth.provider import AccessToken -from mcp.server.auth.settings import AuthSettings -from mcp.server.mcpserver import MCPServer -from pydantic import AnyHttpUrl - -from . import library -from . import class_management -from . import monitoring -from .access import has_permission, resolve_access -from .auth import AuthUnavailable - - -log = logging.getLogger("ihki.grok_mcp") -APPROVAL_TTL = 10 * 60 -MAX_APPROVALS = 100 -MUTATION_LIMIT = 20 -MUTATION_WINDOW = 60 - - -@dataclass(frozen=True) -class Approval: - title: str - url: str - expires_at: float - - -class ApprovalStore: - def __init__(self, clock: Callable[[], float] = time.monotonic): - self.clock = clock - self._items: dict[str, Approval] = {} - self._lock = threading.Lock() - - def _prune(self, now: float) -> None: - self._items = {key: item for key, item in self._items.items() if item.expires_at > now} - while len(self._items) >= MAX_APPROVALS: - self._items.pop(next(iter(self._items))) - - def prepare(self, title: str, url: str) -> tuple[str, Approval]: - now = self.clock() - with self._lock: - self._prune(now) - approval_id = secrets.token_urlsafe(24) - item = Approval(title, url, now + APPROVAL_TTL) - self._items[approval_id] = item - return approval_id, item - - def consume(self, approval_id: str) -> Approval | None: - now = self.clock() - with self._lock: - item = self._items.pop(str(approval_id), None) - self._prune(now) - return item if item and item.expires_at > now else None - - -@dataclass(frozen=True) -class ClassApproval: - actor_id: str - plan: dict[str, Any] - expires_at: float - - -class ClassApprovalStore: - def __init__(self, clock: Callable[[], float] = time.monotonic): - self.clock = clock - self._items: dict[str, ClassApproval] = {} - self._lock = threading.Lock() - - def _prune(self, now: float) -> None: - self._items = {key: item for key, item in self._items.items() if item.expires_at > now} - while len(self._items) >= MAX_APPROVALS: - self._items.pop(next(iter(self._items))) - - def prepare(self, actor_id: str, plan: dict[str, Any]) -> tuple[str, ClassApproval]: - now = self.clock() - with self._lock: - self._prune(now) - approval_id = secrets.token_urlsafe(24) - item = ClassApproval(actor_id, plan, now + APPROVAL_TTL) - self._items[approval_id] = item - return approval_id, item - - def consume(self, approval_id: str, actor_id: str) -> ClassApproval | None: - now = self.clock() - with self._lock: - self._prune(now) - item = self._items.get(str(approval_id)) - if item is None or item.actor_id != actor_id: - return None - self._items.pop(str(approval_id), None) - return item if item.expires_at > now else None - - -class MutationLimiter: - def __init__(self, clock: Callable[[], float] = time.monotonic): - self.clock = clock - self._uses: deque[float] = deque(maxlen=MUTATION_LIMIT) - self._lock = threading.Lock() - - def allow(self) -> bool: - now = self.clock() - with self._lock: - while self._uses and self._uses[0] <= now - MUTATION_WINDOW: - self._uses.popleft() - if len(self._uses) >= MUTATION_LIMIT: - return False - self._uses.append(now) - return True - - -def oauth_urls(app) -> tuple[str, str]: - """Return the public MCP resource and Supabase OAuth issuer URLs.""" - auth = getattr(app.state, "auth", None) - supabase = (getattr(auth, "url", "") or os.environ.get("SUPABASE_URL", "")).rstrip("/") - public = os.environ.get("MCP_PUBLIC_URL", "").strip() - if not public and os.environ.get("SPACE_HOST", "").strip(): - public = f"https://{os.environ['SPACE_HOST'].strip()}/mcp/" - public = public or "http://localhost:8000/mcp/" - return public.rstrip("/") + "/", (supabase + "/auth/v1" if supabase else "https://invalid.invalid/auth/v1") - - -class SupabaseTokenVerifier: - """Validate a Supabase OAuth token and its current ihki management permission.""" - - def __init__(self, app): - self.app = app - - async def verify_token(self, token: str) -> AccessToken | None: - auth = getattr(self.app.state, "auth", None) - if auth is None or not token: - return None - try: - # SupabaseAuth is deliberately synchronous throughout ihki. Keeping the same - # verifier path also avoids retaining a worker/thread beyond an MCP request. - user = resolve_access(auth, token, fresh_permissions=True) - except AuthUnavailable as exc: - log.warning("MCP OAuth verification unavailable: %s", exc) - return None - if not user or user.get("denied") or not has_permission(user, "management.access"): - return None - _, issuer = oauth_urls(self.app) - actor = {key: user.get(key) for key in ("id", "email", "roles", "subject", "permissions")} - return AccessToken(token=token, client_id="supabase-oauth", scopes=["openid", "email"], - subject=str(user["id"]), claims={"iss": issuer, "ihki_actor": actor}) - - -def build(app, *, approvals: ApprovalStore | None = None, limiter: MutationLimiter | None = None, - class_approvals: ClassApprovalStore | None = None, - actor_provider: Callable[[], dict[str, Any] | None] | None = None): - """Return ``(server, authenticated ASGI app, raw Starlette app)``.""" - resource_url, issuer_url = oauth_urls(app) - server = MCPServer( - "ihki-digital-library", - instructions=("Manage only the ihki digital video library and scoped class rosters, and provide read-only " - "privacy-preserving monitoring. Always prepare a mutation, show its exact effect to the teacher, " - "and publish only after explicit confirmation."), - token_verifier=SupabaseTokenVerifier(app), - auth=AuthSettings(issuer_url=AnyHttpUrl(issuer_url), resource_server_url=AnyHttpUrl(resource_url), - validate_token_resource=False, required_scopes=[]), - ) - approvals = approvals or ApprovalStore() - class_approvals = class_approvals or ClassApprovalStore() - limiter = limiter or MutationLimiter() - - def auth(): - return getattr(app.state, "auth", None) - - def actor() -> dict[str, Any] | None: - if actor_provider: - return actor_provider() - access = get_access_token() - value = (access.claims or {}).get("ihki_actor") if access else None - return value if isinstance(value, dict) else None - - def initial_lessons() -> dict[str, str]: - return dict(getattr(app.state, "initial_class_lessons", {})) - - def class_audit(action: str, current: dict[str, Any] | None, outcome: str, - result: dict[str, Any] | None = None, reason: str = "") -> None: - safe = result or {} - log.info("Grok class action=%s outcome=%s class_id=%s email=%s reason=%s", action, outcome, - safe.get("class_id"), safe.get("email"), reason) - store = auth() - if store is not None and hasattr(store, "log_event"): - store.log_event("content", current or {"id": None, "email": ""}, - str(safe.get("class_name") or action), - {"action": action, "outcome": outcome, - **({"class_id": safe["class_id"]} if safe.get("class_id") is not None else {}), - **({"email": safe["email"]} if safe.get("email") else {}), - **({"reason": reason} if reason else {})}) - - def result(action: str, operation: Callable[[], dict[str, Any]]) -> dict[str, Any]: - if not limiter.allow(): - library.audit(auth(), action, action, "refused", reason="rate_limit") - return {"ok": False, "error": "בוצעו יותר מדי פעולות. נסו שוב בעוד דקה."} - try: - answer = operation() - library.audit(auth(), action, str(answer.get("title") or action), "success", - video_id=answer.get("video_id")) - return {"ok": True, **answer} - except library.LibraryError as exc: - library.audit(auth(), action, action, "refused", reason=type(exc).__name__) - return {"ok": False, "error": str(exc)} - - @server.tool(description="List the digital-library videos in their current order. Returns no learner data.", - structured_output=True) - async def list_library_videos() -> dict[str, Any]: - try: - videos = library.rows(auth(), fresh=True) - return {"ok": True, "videos": [ - {"id": video["id"], "title": video["title"], "url": video["url"], "position": at + 1} - for at, video in enumerate(videos) - ]} - except library.LibraryError as exc: - return {"ok": False, "error": str(exc), "videos": []} - - @server.tool(description="Validate a YouTube or Vimeo video and create a ten-minute approval. This does not publish it.", - structured_output=True) - async def prepare_library_video(title: str, url: str) -> dict[str, Any]: - def prepare() -> dict[str, Any]: - clean_title = library.normalize_title(title) - clean_url = library.normalize_url(url, app.state.video_link) - library.ensure_unique(auth(), clean_url, fresh=True) - approval_id, item = approvals.prepare(clean_title, clean_url) - return {"approval_id": approval_id, "title": item.title, "url": item.url, - "expires_in_seconds": APPROVAL_TTL, "published": False} - return result("prepare_library_video", prepare) - - @server.tool(description="Publish a previously prepared video after the teacher explicitly confirms the shown title and URL.", - structured_output=True) - async def publish_library_video(approval_id: str) -> dict[str, Any]: - def publish() -> dict[str, Any]: - item = approvals.consume(approval_id) - if item is None: - raise library.LibraryError("האישור אינו תקף, פג תוקף או כבר נוצל. הכינו את הסרטון מחדש.") - library.add(auth(), item.title, item.url) - created = next((video for video in library.rows(auth(), fresh=True) if video.get("url") == item.url), None) - return {"video_id": created.get("id") if created else None, "title": item.title, - "url": item.url, "published": True} - return result("publish_library_video", publish) - - @server.tool(description="Rename an existing digital-library video without changing its URL or position.", - structured_output=True) - async def rename_library_video(video_id: int, title: str) -> dict[str, Any]: - def rename() -> dict[str, Any]: - clean_title = library.normalize_title(title) - library.rename(auth(), video_id, clean_title) - return {"video_id": video_id, "title": clean_title} - return result("rename_library_video", rename) - - @server.tool(description="List only classes in the current manager's scope and their student emails/nicknames.", - structured_output=True) - async def list_classes() -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "אין הרשאת ניהול", "classes": []} - try: - return {"ok": True, "classes": class_management.public_classes(store, current)} - except class_management.ClassManagementError as exc: - return {"ok": False, "error": str(exc), "classes": []} - - @server.tool(description=("Prepare one class change for confirmation. Allowed operations: create_class, rename_class, " - "add_student, remove_student. This does not mutate data."), structured_output=True) - async def prepare_class_change( - operation: Literal["create_class", "rename_class", "add_student", "remove_student"], - class_id: int | None = None, name: str = "", subject: str = "", email: str = "", - nickname: str = "", students: list[dict[str, str]] | None = None, - ) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "אין הרשאת ניהול"} - if not limiter.allow(): - class_audit("prepare_class_change", current, "refused", reason="rate_limit") - return {"ok": False, "error": "בוצעו יותר מדי פעולות. נסו שוב בעוד דקה."} - try: - plan = class_management.prepare(store, current, operation, class_id=class_id, name=name, - subject=subject, email=email, nickname=nickname, students=students, - initial_lessons=initial_lessons()) - approval_id, _ = class_approvals.prepare(str(current["id"]), plan) - shown = {key: value for key, value in plan.items() if key not in {"state", "emails", "nicknames", "lesson"}} - if operation == "create_class": - shown["students"] = [{"email": item, **({"nickname": plan["nicknames"][item]} - if item in plan["nicknames"] else {})} - for item in plan["emails"]] - class_audit("prepare_class_change", current, "success", shown) - return {"ok": True, "approval_id": approval_id, **shown, - "expires_in_seconds": APPROVAL_TTL, "published": False} - except class_management.ClassManagementError as exc: - class_audit("prepare_class_change", current, "refused", reason=type(exc).__name__) - return {"ok": False, "error": str(exc)} - - @server.tool(description="Publish a prepared class change only after the teacher explicitly confirms its exact effect.", - structured_output=True) - async def publish_class_change(approval_id: str) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "אין הרשאת ניהול"} - if not limiter.allow(): - class_audit("publish_class_change", current, "refused", reason="rate_limit") - return {"ok": False, "error": "בוצעו יותר מדי פעולות. נסו שוב בעוד דקה."} - item = class_approvals.consume(approval_id, str(current["id"])) - if item is None: - class_audit("publish_class_change", current, "refused", reason="invalid_approval") - return {"ok": False, "error": "האישור אינו תקף, פג תוקף, שייך למשתמש אחר או כבר נוצל."} - try: - answer = class_management.apply(store, current, item.plan, initial_lessons=initial_lessons()) - class_audit("publish_class_change", current, "success", answer) - return {"ok": True, **answer, "published": True} - except class_management.ClassManagementError as exc: - class_audit("publish_class_change", current, "refused", reason=type(exc).__name__) - return {"ok": False, "error": str(exc)} - - @server.tool(description=("Return a read-only daily or weekly aggregate activity series for one authorized class " - "or all classes in the current manager's scope. Returns no learner identities."), - structured_output=True) - async def get_activity_timeseries( - from_date: str, to_date: str, interval: Literal["day", "week"] = "day", - class_id: int | None = None, - ) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "נתוני הפעילות אינם זמינים"} - try: - return {"ok": True, **monitoring.activity_timeseries( - store, current, from_date=from_date, to_date=to_date, interval=interval, class_id=class_id)} - except monitoring.MonitoringError as exc: - log.info("MCP activity outcome=refused actor_id=%s class_id=%s range=%s..%s reason=%s", - current.get("id"), class_id, from_date, to_date, type(exc).__name__) - return {"ok": False, "error": str(exc)} - - @server.tool(description=("Return privacy-thresholded current activity for one authorized class or all visible " - "classes. Recent persisted practice never implies that learners are online; current " - "presence is available only for an active live-class heartbeat session."), - structured_output=True) - async def get_current_activity( - window_minutes: Literal[5, 15] = 5, class_id: int | None = None, - ) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "נתוני הפעילות אינם זמינים"} - try: - answer = monitoring.current_activity( - store, current, window_minutes=window_minutes, class_id=class_id, - live_store=getattr(app.state, "live", None)) - log.info("MCP current activity outcome=%s actor_id=%s class_id=%s window=%s", - answer["status"], current.get("id"), class_id, window_minutes) - return {"ok": True, **answer} - except monitoring.MonitoringError as exc: - log.info("MCP current activity outcome=refused actor_id=%s class_id=%s window=%s reason=%s", - current.get("id"), class_id, window_minutes, type(exc).__name__) - return {"ok": False, "error": str(exc)} - - @server.tool(description=("Return a read-only coarse application health summary. Never returns logs, errors, " - "credentials, internal addresses, or configuration values."), structured_output=True) - async def get_system_health() -> dict[str, Any]: - current = actor() - if current is None: - return {"ok": False, "error": "אין הרשאת ניהול"} - try: - return {"ok": True, **monitoring.system_health(app, current)} - except monitoring.MonitoringError as exc: - return {"ok": False, "error": str(exc)} - - @server.tool(description=("Return a read-only privacy-thresholded ranking of difficult exercises for one " - "authorized class or all classes in the current manager's scope. Returns aggregates " - "only and never identifies learners."), structured_output=True) - async def get_exercise_difficulty( - from_date: str, to_date: str, class_id: int | None = None, - exercise_type: Literal["pronunciation", "verb", "game", "other"] | None = None, - limit: int = 20, min_attempts: int = 10, - ) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "נתוני הקושי אינם זמינים"} - try: - answer = monitoring.exercise_difficulty( - store, current, from_date=from_date, to_date=to_date, class_id=class_id, - exercise_type=exercise_type, limit=limit, min_attempts=min_attempts) - log.info("MCP difficulty outcome=success actor_id=%s class_id=%s range=%s..%s returned=%s", - current.get("id"), class_id, from_date, to_date, answer["returned"]) - return {"ok": True, **answer} - except monitoring.MonitoringError as exc: - log.info("MCP difficulty outcome=refused actor_id=%s class_id=%s range=%s..%s reason=%s", - current.get("id"), class_id, from_date, to_date, type(exc).__name__) - return {"ok": False, "error": str(exc)} - - @server.tool(description=("Return measured pronunciation errors from stored phoneme diagnostics for one " - "authorized class or all visible classes. Returns aggregates only and never infers " - "linguistic difficulty when diagnostics are missing."), structured_output=True) - async def get_pronunciation_difficulties( - from_date: str, to_date: str, class_id: int | None = None, - limit: int = 20, min_attempts: int = 5, - ) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "נתוני ההגייה אינם זמינים"} - try: - answer = monitoring.pronunciation_difficulties( - store, current, from_date=from_date, to_date=to_date, class_id=class_id, - limit=limit, min_attempts=min_attempts) - log.info("MCP pronunciation outcome=%s actor_id=%s class_id=%s range=%s..%s returned=%s", - answer["status"], current.get("id"), class_id, from_date, to_date, answer["returned"]) - return {"ok": True, **answer} - except monitoring.MonitoringError as exc: - log.info("MCP pronunciation outcome=refused actor_id=%s class_id=%s range=%s..%s reason=%s", - current.get("id"), class_id, from_date, to_date, type(exc).__name__) - return {"ok": False, "error": str(exc)} - - @server.tool(description=("Return a privacy-thresholded learning progress series for one authorized class or " - "all visible classes, optionally restricted to a stable exercise identifier."), - structured_output=True) - async def get_learning_progress( - from_date: str, to_date: str, interval: Literal["day", "week", "month"] = "week", - class_id: int | None = None, exercise_id: str | None = None, min_attempts: int = 5, - ) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "נתוני הלמידה אינם זמינים"} - try: - answer = monitoring.learning_progress( - store, current, from_date=from_date, to_date=to_date, interval=interval, - class_id=class_id, exercise_id=exercise_id, min_attempts=min_attempts) - log.info("MCP progress outcome=%s actor_id=%s class_id=%s range=%s..%s", - answer["status"], current.get("id"), class_id, from_date, to_date) - return {"ok": True, **answer} - except monitoring.MonitoringError as exc: - return {"ok": False, "error": str(exc)} - - @server.tool(description=("Compare privacy-thresholded learning difficulty across authorized classes using " - "normalized participation and exercise-exposure metrics. Returns no learner identities."), - structured_output=True) - async def compare_class_difficulties( - from_date: str, to_date: str, class_ids: list[int] | None = None, - exercise_type: Literal["pronunciation", "verb", "game", "other"] | None = None, - min_attempts: int = 5, - ) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "נתוני ההשוואה אינם זמינים"} - try: - answer = monitoring.compare_class_difficulties( - store, current, from_date=from_date, to_date=to_date, class_ids=class_ids, - exercise_type=exercise_type, min_attempts=min_attempts) - log.info("MCP class comparison outcome=%s actor_id=%s classes=%s range=%s..%s", - answer["status"], current.get("id"), len(answer["classes"]), from_date, to_date) - return {"ok": True, **answer} - except monitoring.MonitoringError as exc: - return {"ok": False, "error": str(exc)} - - @server.tool(description=("Identify privacy-thresholded exercise and topic bottlenecks from repeated failures " - "and measured improvement. Recommendations are labeled separately from observations."), - structured_output=True) - async def get_learning_bottlenecks( - from_date: str, to_date: str, class_id: int | None = None, - exercise_type: Literal["pronunciation", "verb", "game", "other"] | None = None, - limit: int = 20, min_attempts: int = 10, - ) -> dict[str, Any]: - current, store = actor(), auth() - if current is None or store is None: - return {"ok": False, "error": "נתוני הלמידה אינם זמינים"} - try: - answer = monitoring.learning_bottlenecks( - store, current, from_date=from_date, to_date=to_date, class_id=class_id, - exercise_type=exercise_type, limit=limit, min_attempts=min_attempts) - log.info("MCP bottlenecks outcome=%s actor_id=%s class_id=%s range=%s..%s returned=%s", - answer["status"], current.get("id"), class_id, from_date, to_date, answer["returned"]) - return {"ok": True, **answer} - except monitoring.MonitoringError as exc: - return {"ok": False, "error": str(exc)} - - raw = server.streamable_http_app( - streamable_http_path="/", - stateless_http=True, - json_response=True, - max_request_body_size=64 * 1024, - host="0.0.0.0", - ) - return server, raw, raw diff --git a/app/keystore.py b/app/keystore.py deleted file mode 100644 index 623f2eaa7a047f7148b26e34ca8828c71ae126db..0000000000000000000000000000000000000000 --- a/app/keystore.py +++ /dev/null @@ -1,204 +0,0 @@ -"""API keys a developer sets in the management panel (SAWTAK_API_KEY, ANTHROPIC_API_KEY), and the tutor's model -(TUTOR_MODEL), so changing one needs no access to the host (the Hugging Face Space). - -Each key is kept in the Supabase table `app_secrets`, encrypted with Fernet under SECRETS_MASTER_KEY, which lives only in -the host's environment: the table, a backup of it or a look at the Supabase dashboard shows no key. The table has -row-level security on and no policy, so only the server's secret key reads it. A key never goes back to the page: the -panel sees whether it is set, where from, and its last four characters. A new key is tried with its provider before it -is kept, so a typo cannot switch the playback or the tutor off. TUTOR_MODEL is no secret: it is kept the same way, but -the panel shows it, and a model is checked by asking Anthropic for it with the key in use. - -A key kept in the table takes the place of the environment's; without one (or without the table, the secret key or the -master key) the environment's is used, as before. - -Env: SECRETS_MASTER_KEY (a Fernet key; make one with - python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" - and keep it as a Space secret. Losing it loses the stored keys, not the environment's.) -""" -from __future__ import annotations - -import logging -import os -import re -import threading -import urllib.error -import urllib.request - -from .auth import AuthUnavailable - -log = logging.getLogger("ihki.keys") -# the keys the panel may set, and how the panel names them; nothing else (the master key, the Supabase keys) is settable -NAMES = {"SAWTAK_API_KEY": "Sawtak Arabi · השמעת התשובה", "ANTHROPIC_API_KEY": "Anthropic · המורה (יאללה נדבר)", - "TUTOR_MODEL": "המודל של המורה (TUTOR_MODEL)"} -FALLBACK = {"ANTHROPIC_API_KEY": ("ANTHROPIC_API_KEY", "CLAUDE_API_KEY"), "SAWTAK_API_KEY": ("SAWTAK_API_KEY",), - "TUTOR_MODEL": ("TUTOR_MODEL",)} -PUBLIC = frozenset({"TUTOR_MODEL"}) # settings the panel may show in full -# the panel's list of tutor models (any other id can be typed); empty = the code's default (tutor.DEFAULT_MODEL) -MODEL_CHOICES = [("claude-haiku-5-5", "Haiku 5.5 · ברירת המחדל, הזול ביותר"), ("claude-sonnet-5-5", "Sonnet 5.5"), - ("claude-opus-5-5", "Opus 5.5"), ("claude-fable-5-1", "Fable 5.1 · היקר ביותר"), ("claude-haiku-4-5", "Haiku 4.5")] -MODEL_ID = re.compile(r"^claude-[a-z0-9][a-z0-9.-]{2,60}$") -MAX_LEN = 300 -TIMEOUT = 15 -CREATE_APP_SECRETS = ("create table public.app_secrets (name text primary key check (name in ('SAWTAK_API_KEY','ANTHROPIC_API_KEY','TUTOR_MODEL')), " - "ciphertext text not null, last4 text not null default '', updated_by text not null default '', " - "updated_at timestamptz not null default now()); " - "alter table public.app_secrets enable row level security; " - "revoke all on public.app_secrets from anon, authenticated;") -# for a table made before TUTOR_MODEL could be kept in it -MIGRATE_APP_SECRETS = ("alter table public.app_secrets drop constraint if exists app_secrets_name_check; " - "alter table public.app_secrets add constraint app_secrets_name_check " - "check (name in ('SAWTAK_API_KEY','ANTHROPIC_API_KEY','TUTOR_MODEL'));") - - -class KeyRefused(ValueError): - """The provider refused the key, or it could not be tried. The message is safe to show.""" - - -class KeyStore: - def __init__(self, auth, master: str = ""): - self.auth = auth - self._fernet, self.why = None, "" # why: what keeps the panel from saving keys, in words for the page - if master: - try: - from cryptography.fernet import Fernet - self._fernet = Fernet(master.strip().encode()) - except ImportError: - self.why = "החבילה cryptography לא מותקנת בשרת." - except ValueError: - log.error("SECRETS_MASTER_KEY is not a valid Fernet key: keys set in the panel are off") - self.why = "SECRETS_MASTER_KEY בשרת אינו מפתח Fernet תקין." - else: - self.why = "חסר SECRETS_MASTER_KEY בסודות השרת, ולכן אי אפשר לשמור מפתחות מהפאנל." - if not self.why and not getattr(auth, "secret", ""): - self.why = "חסר SUPABASE_SECRET_KEY בשרת, ולכן אי אפשר לשמור מפתחות מהפאנל." - self.missing = False # the table is not there yet (CREATE_APP_SECRETS) - self._rows: dict[str, dict] = {} # name -> {"value", "last4", "updated_by", "updated_at"} - self._lock = threading.Lock() - - @property - def writable(self) -> bool: - return not self.why and not self.missing - - def _seal(self, name: str, value: str) -> str: - # the name goes inside the ciphertext, so one key's row copied into another's does not decrypt as it - return self._fernet.encrypt(f"{name}\n{value}".encode()).decode() - - def _open(self, name: str, token: str) -> str | None: - from cryptography.fernet import InvalidToken - try: - got, _, value = self._fernet.decrypt(token.encode()).decode().partition("\n") - except (InvalidToken, ValueError): - return None - return value if got == name else None - - def load(self) -> None: - """Reads the stored keys. A failure is logged and leaves the environment's keys in use.""" - if self.why or not hasattr(self.auth, "secret_rows"): - return - try: - rows = self.auth.secret_rows() - except AuthUnavailable as e: - log.warning("stored API keys not read, the environment's are used: %s", e) - return - if rows is None: - self.missing = True - return - found = {} - for r in rows: - value = self._open(r["name"], r.get("ciphertext") or "") if r.get("name") in NAMES else None - if value is None: - log.warning("stored key %s does not decrypt with this SECRETS_MASTER_KEY: the environment's is used", r.get("name")) - continue - found[r["name"]] = {"value": value, "last4": r.get("last4") or "", "updated_by": r.get("updated_by") or "", - "updated_at": r.get("updated_at")} - with self._lock: - self._rows = found - - @staticmethod - def _env(name: str) -> str: - return next((v for v in (os.environ.get(n, "").strip() for n in FALLBACK[name]) if v), "") - - def get(self, name: str) -> str: - """The key in use: the panel's, else the environment's, else empty.""" - with self._lock: - row = self._rows.get(name) - return row["value"] if row else self._env(name) - - def status(self) -> list[dict]: - """What the panel shows of each key: never the key itself.""" - out = [] - for name, label in NAMES.items(): - with self._lock: - row = self._rows.get(name) - env = self._env(name) - item = {"name": name, "label": label, "source": "db" if row else "env" if env else "none", - "last4": "" if name in PUBLIC else row["last4"] if row else env[-4:] if len(env) >= 12 else "", - "updated_by": row["updated_by"] if row else "", "updated_at": row["updated_at"] if row else None} - if name in PUBLIC: - item.update(value=row["value"] if row else env, choices=[{"id": i, "label": l} for i, l in MODEL_CHOICES]) - out.append(item) - return out - - def set(self, name: str, value: str, by: str) -> None: - """Encrypts and keeps a key that check() accepted.""" - row = {"name": name, "ciphertext": self._seal(name, value), "last4": value[-4:], "updated_by": by} - self.auth.save_secret(row) - with self._lock: - self._rows[name] = {"value": value, "last4": row["last4"], "updated_by": by, "updated_at": None} - - def clear(self, name: str) -> None: - """Forgets the panel's key; the environment's (if any) is used again.""" - self.auth.delete_secret(name) - with self._lock: - self._rows.pop(name, None) - - -def check(name: str, value: str, anthropic_key: str = "") -> None: - """Tries the key with its provider, with a call that costs nothing; a tutor model, by asking Anthropic for it with - `anthropic_key` (the key in use). Raises KeyRefused.""" - if name == "TUTOR_MODEL": - if not MODEL_ID.match(value): - raise KeyRefused("מזהה מודל נראה כך: claude-haiku-5-5.") - if not anthropic_key: - raise KeyRefused("אין מפתח Anthropic, ולכן אי אפשר לבדוק את המודל. שמרו קודם מפתח.") - import anthropic - try: - anthropic.Anthropic(api_key=anthropic_key, timeout=TIMEOUT, max_retries=0).models.retrieve(value) - except anthropic.NotFoundError: - raise KeyRefused(f"Anthropic לא מכיר את המודל {value}.") from None - except (anthropic.AuthenticationError, anthropic.PermissionDeniedError): - raise KeyRefused("מפתח Anthropic שבשימוש נדחה, ולכן אי אפשר לבדוק את המודל.") from None - except anthropic.APIStatusError as e: - raise KeyRefused(f"לא ניתן לבדוק את המודל מול Anthropic כרגע ({e.status_code}). נסו שוב.") from None - except anthropic.APIConnectionError: - raise KeyRefused("אין חיבור ל-Anthropic כרגע. נסו שוב.") from None - return - if name == "SAWTAK_API_KEY": - req = urllib.request.Request("https://api.sawtakarabi.ai/v1/voices", headers={ - "Authorization": f"Bearer {value}", "User-Agent": "ihki/1.0"}) - try: - with urllib.request.urlopen(req, timeout=TIMEOUT): - return - except urllib.error.HTTPError as e: - if e.code == 403: # a valid key restricted to `tts` may not list voices; speech is all it is used for - return - if e.code == 401: - raise KeyRefused("Sawtak דחה את המפתח.") from None - raise KeyRefused(f"לא ניתן לבדוק את המפתח מול Sawtak כרגע ({e.code}). נסו שוב.") from None - except OSError: - raise KeyRefused("אין חיבור ל-Sawtak כרגע. נסו שוב.") from None - import anthropic - try: - anthropic.Anthropic(api_key=value, timeout=TIMEOUT, max_retries=0).models.list(limit=1) - except (anthropic.AuthenticationError, anthropic.PermissionDeniedError): - raise KeyRefused("Anthropic דחה את המפתח.") from None - except anthropic.APIStatusError as e: - raise KeyRefused(f"לא ניתן לבדוק את המפתח מול Anthropic כרגע ({e.status_code}). נסו שוב.") from None - except anthropic.APIConnectionError: - raise KeyRefused("אין חיבור ל-Anthropic כרגע. נסו שוב.") from None - - -def load_keys(auth) -> KeyStore: - keys = KeyStore(auth, os.environ.get("SECRETS_MASTER_KEY", "")) - keys.load() - return keys diff --git a/app/lab.py b/app/lab.py deleted file mode 100644 index c42e0c1c28a322fb01d6fdd3c6af39f4912eecc0..0000000000000000000000000000000000000000 --- a/app/lab.py +++ /dev/null @@ -1,378 +0,0 @@ -"""The lab (מעבדה): a fixed diagnostic list for measuring the phoneme recognition, and its recordings. - -Every speaker reads the same short items once. Each item tests one sound (`focus`), so its answer is known and -labelling a recording costs nothing: it was said right, or the speaker planted one mistake and marks which sound. -Three groups: - - syllables: the hard letter alone before a long a (حا, عا, صا ...), and its easy neighbour beside it - - pairs: minimal pairs (or a three) that differ in that sound only (حال / هال / خال, صيف / سيف ...): can the - model tell them apart - - positions: the sound at the start, the middle and the end of a word - - vowels: a short vowel beside a glide (ولد, يلّا, شويّ: the glide's colour on the transition frames, GLIDE_KIN), - between consonants (كلب, بنت, كلّ: a spike from a neighbour's release), and short / long (من / مين). - Planting a↔i or u↔a on these measures whether the GOP pooling and windows (docs/gop-pooling.md) - still catch a real vowel substitution - -The recordings are kept in the Supabase table `lab_samples` and the storage bucket `lab` (see CREATE_LAB in main.py), -the material for a per-sound benchmark: how often a sound said right is flagged, and how often a planted mistake is not. -The IPA follows the bank's (phrases.py), and a word the bank has keeps the bank's reading. -""" -from __future__ import annotations - -import json -from functools import lru_cache -from pathlib import Path - -from .align import POOLS -from .scorer import LETTER_GOOD -from .tips import VOWELS -from .translit import to_hebrew - -GROUPS = {"syllables": "הברות", "pairs": "זוגות מינימליים", "positions": "מיקום במילה", "vowels": "תנועות"} - - -def _item(id: str, group: str, focus: str, he: str, *variants: tuple[str, str], pair: str = "") -> dict: - return {"id": id, "group": group, "focus": focus, "he": he, "variants": list(variants), "pair": pair} - - -ITEMS = [ - # syllables: the hard letter, then the sound a Hebrew speaker puts in its place - _item("syl-ħ", "syllables", "ħ", "ח גרונית", ("حا", "ħ aː")), - _item("syl-x", "syllables", "x", "ח' (כמו ח בעברית)", ("خا", "x aː")), - _item("syl-h", "syllables", "h", "ה", ("ها", "h aː")), - _item("syl-ʕ", "syllables", "ʕ", "ע גרונית", ("عا", "ʕ aː")), - _item("syl-ʔ", "syllables", "ʔ", "א", ("آ", "ʔ aː")), - _item("syl-ɣ", "syllables", "ɣ", "ע' (ר גרונית)", ("غا", "ɣ aː")), - _item("syl-r", "syllables", "r", "ר מתגלגלת", ("را", "r aː")), - _item("syl-sˤ", "syllables", "sˤ", "צ' (ס נחצית)", ("صا", "sˤ aː")), - _item("syl-s", "syllables", "s", "ס", ("سا", "s aː")), - _item("syl-tˤ", "syllables", "tˤ", "ט נחצית", ("طا", "tˤ aː")), - _item("syl-t", "syllables", "t", "ת", ("تا", "t aː")), - _item("syl-dˤ", "syllables", "dˤ", "צ' (ד נחצית)", ("ضا", "dˤ aː")), - _item("syl-d", "syllables", "d", "ד", ("دا", "d aː")), - # minimal pairs: the two words of a pair differ in the focus sound only - _item("pair-ħaal", "pairs", "ħ", "מצב", ("حال", "ħ aː l"), pair="ħ / h / x"), - _item("pair-haal", "pairs", "h", "הל (תבלין)", ("هال", "h aː l"), pair="ħ / h / x"), - _item("pair-xaal", "pairs", "x", "דוד (אח של אמא)", ("خال", "x aː l"), pair="ħ / h / x"), - _item("pair-ʕalam", "pairs", "ʕ", "דגל", ("علم", "ʕ a l a m"), pair="ʕ / ʔ"), - _item("pair-ʔalam", "pairs", "ʔ", "כאב", ("ألم", "ʔ a l a m"), pair="ʕ / ʔ"), - _item("pair-ɣaali", "pairs", "ɣ", "יקר", ("غالي", "ɣ aː l i"), pair="ɣ / x"), - _item("pair-xaali", "pairs", "x", "הדוד שלי", ("خالي", "x aː l i"), pair="ɣ / x"), - _item("pair-sˤeef", "pairs", "sˤ", "קיץ", ("صيف", "sˤ eː f"), pair="sˤ / s"), - _item("pair-seef", "pairs", "s", "חרב", ("سيف", "s eː f"), pair="sˤ / s"), - _item("pair-tˤiin", "pairs", "tˤ", "בוץ", ("طين", "tˤ iː n"), pair="tˤ / t"), - _item("pair-tiin", "pairs", "t", "תאנה", ("تين", "t iː n"), pair="tˤ / t"), - _item("pair-dˤaar", "pairs", "dˤ", "מזיק", ("ضار", "dˤ aː r"), pair="dˤ / d"), - _item("pair-daar", "pairs", "d", "בית", ("دار", "d aː r"), pair="dˤ / d"), - # no ق / ك pair: ق is said ʔ, k or q (archi.py), so قلب said kalb is right and the pair tells nothing - # positions: the focus sound at the start, the middle and the end of a word - _item("pos-ħ-1", "positions", "ħ", "חלב", ("حليب", "ħ a l iː b")), - _item("pos-ħ-2", "positions", "ħ", "בשר", ("لحم", "l a ħ m"), ("لحم", "l a ħ i m")), - _item("pos-ħ-3", "positions", "ħ", "בוקר", ("صباح", "sˤ a b aː ħ")), - _item("pos-ʕ-1", "positions", "ʕ", "עין", ("عين", "ʕ eː n")), - _item("pos-ʕ-2", "positions", "ʕ", "רחוק", ("بعيد", "b ʕ iː d"), ("بعيد", "b a ʕ iː d")), - _item("pos-ʕ-3", "positions", "ʕ", "שבע", ("سبع", "s a b ʕ"), ("سبع", "s a b i ʕ")), - _item("pos-x-1", "positions", "x", "הדוד שלי", ("خالي", "x aː l i")), - _item("pos-x-2", "positions", "x", "בסדר", ("بخير", "b i x eː r"), ("بخير", "b x eː r")), - _item("pos-x-3", "positions", "x", "מטבח", ("مطبخ", "m a tˤ b a x")), - _item("pos-ɣ-1", "positions", "ɣ", "חדר", ("غرفة", "ɣ u r f e")), - _item("pos-ɣ-2", "positions", "ɣ", "קטן", ("صغير", "sˤ ɣ iː r")), - _item("pos-sˤ-2", "positions", "sˤ", "נמוך", ("قصير", "ʔ a sˤ iː r"), ("قصير", "q a sˤ iː r")), - _item("pos-sˤ-3", "positions", "sˤ", "זול", ("رخيص", "r x iː sˤ")), - _item("pos-tˤ-1", "positions", "tˤ", "שולחן", ("طاولة", "tˤ aː w l e")), - _item("pos-tˤ-2", "positions", "tˤ", "תפוחי אדמה", ("بطاطا", "b a tˤ aː tˤ a")), - _item("pos-dˤ-2", "positions", "dˤ", "ירוק", ("اخضر", "ʔ a x dˤ a r")), - _item("pos-dˤ-3", "positions", "dˤ", "לבן", ("ابيض", "ʔ a b j a dˤ")), - _item("pos-ʔ-2", "positions", "ʔ", "שאלה", ("سؤال", "s u ʔ aː l")), - _item("pos-h-2", "positions", "h", "חודש", ("شهر", "ʃ a h r"), ("شهر", "ʃ a h i r")), - # vowels: the short vowel is the focus. Beside a glide: its i / u colour must not count against the vowel - _item("vow-walad", "vowels", "a", "ילד", ("ولد", "w a l a d")), - _item("vow-jalla", "vowels", "a", "יאללה", ("يلّا", "j a l l a")), - _item("vow-ʃwajj", "vowels", "a", "קצת", ("شويّ", "ʃ w a j j")), - _item("vow-majj", "vowels", "a", "מים", ("ميّ", "m a j j"), ("ميّ", "m a j j e")), - # between consonants: a release or a burst beside the vowel must not count against it - _item("vow-kalb", "vowels", "a", "כלב", ("كلب", "k a l b")), - _item("vow-bint", "vowels", "i", "בת", ("بنت", "b i n t")), - _item("vow-kull", "vowels", "u", "הכל", ("كلّ", "k u l l")), - _item("vow-sukkar", "vowels", "u", "סוכר", ("سكّر", "s u k k a r")), - _item("vow-bass", "vowels", "a", "רק / די", ("بسّ", "b a s s")), - # short / long: the same consonants, the vowel's length only - _item("vow-min", "vowels", "i", "מ־ (מן)", ("من", "m i n"), pair="i / iː"), - _item("vow-miin", "vowels", "iː", "מי", ("مين", "m iː n"), pair="i / iː"), -] -BY_ID = {item["id"]: item for item in ITEMS} - -# A deliberately small, single-take screen for comparing acoustic models with Hebrew speakers. Unlike the full -# diagnostic list above, this is not saved as labelled benchmark data: the same take is scored by every comparison -# model and shown side by side. It mixes the two pharyngeals, Hebrew ח' /x/, emphatics and short/long vowels. زمن -# is intentional: Hebrew speakers commonly insert an alif-like long /a:/ and say zama:n although both vowels are -# short. كتاب is the positive control, where the alif really should be long. -QUICK_QUIZ = [ - _item("quick-halib", "quick", "ħ", "חַלִיב — ח גרונית", ("حليب", "ħ a l iː b")), - _item("quick-baid", "quick", "ʕ", "בַּעִיד — ע גרונית", ("بعيد", "b a ʕ iː d")), - _item("quick-khali", "quick", "x", "חַ'אלִי — ח׳", ("خالي", "x aː l i")), - _item("quick-zaman", "quick", "a", "זַמַן — שתי תנועות קצרות (לא זַמַאן)", ("زمن", "z a m a n")), - _item("quick-kitab", "quick", "aː", "כִּתַאבּ — אליף ארוכה", ("كتاب", "k i t aː b")), - _item("quick-sabah", "quick", "ħ", "סַבַּאח — ח בסוף ואליף ארוכה", ("صباح", "sˤ a b aː ħ")), - _item("quick-sual", "quick", "ʔ", "סֻאַאל — המזה ואליף ארוכה", ("سؤال", "s u ʔ aː l")), - _item("quick-tawle", "quick", "tˤ", "טַאוְלֶה — ט נחצית", ("طاولة", "tˤ aː w l e")), - _item("quick-zghir", "quick", "ɣ", "זְעִ'יר — ע׳", ("صغير", "sˤ ɣ iː r")), - _item("quick-bint", "quick", "i", "בִּנְת — תנועה קצרה", ("بنت", "b i n t")), -] -QUICK_BY_ID = {item["id"]: item for item in QUICK_QUIZ} -MINERVA_METADATA = Path(__file__).resolve().parent.parent / "data" / "minerva_metadata.json" - - -@lru_cache(maxsize=1) -def minerva_metadata() -> tuple[dict, ...]: - """The reviewed Minerva captions used by both the lab page and every rescore. - - Supabase owns the recording and its historical result; this file owns the sentence, timing and expected phones. - Keeping that boundary explicit means an OCR correction takes effect on the next server start without reimporting - audio or editing database rows. - """ - raw = json.loads(MINERVA_METADATA.read_text(encoding="utf-8")) - if not isinstance(raw, list): - raise ValueError("Minerva metadata must be a JSON array") - videos, seen = [], set() - for video in raw: - video_id = str(video.get("youtube_id") or "") - segments = video.get("segments") - if not video_id or video_id in seen or not isinstance(segments, list): - raise ValueError(f"invalid Minerva video metadata: {video_id or ''}") - seen.add(video_id) - lines = [] - for line, segment in enumerate(segments, 1): - timing = segment.get("voice_time_seconds") or {} - required = (segment.get("arabic"), segment.get("hebrew_transliteration"), - segment.get("hebrew"), segment.get("expected_phonemes")) - if not all(isinstance(value, str) and value.strip() for value in required): - raise ValueError(f"incomplete Minerva metadata: {video_id}:{line:02d}") - try: - start, end = float(timing["start"]), float(timing["end"]) - except (KeyError, TypeError, ValueError) as exc: - raise ValueError(f"invalid Minerva timing: {video_id}:{line:02d}") from exc - if end <= start: - raise ValueError(f"invalid Minerva timing: {video_id}:{line:02d}") - lines.append({"item": f"minerva:{video_id}:{line:02d}", "line": line, "start": start, "end": end, - "ar": required[0], "tr": required[1], "he": required[2], "ipa": required[3]}) - videos.append({"id": video_id, "title": str(video.get("title_hebrew") or video_id), - "filename": str(video.get("file_name") or ""), "segments": tuple(lines)}) - return tuple(videos) - - -def minerva_segments() -> dict[str, dict]: - """Reviewed Minerva segments by their persisted lab item key.""" - return {segment["item"]: segment for video in minerva_metadata() for segment in video["segments"]} - - -def phrase(item: dict) -> dict: - """An item as the scorer takes a phrase.""" - return {"cat": "lab", "key": f"lab:{item['id']}", "he": item["he"], "variants": item["variants"]} - - -def sample_phrase(sample: dict) -> dict | None: - """The phrase of a saved recording, including imported lines outside the fixed diagnostic list. - - Fixed items keep their current variants. An importer stores the exact Arabic/IPA pair that was used for - scoring, so later rescores, sweeps and traces do not depend on the importer's manifest still being present. - """ - item = BY_ID.get(sample.get("item")) - if item is not None: - return phrase(item) - segment = minerva_segments().get(sample.get("item")) - if segment is not None: - return {"cat": "lab", "key": segment["item"], "he": segment["he"], - "variants": [(segment["ar"], segment["ipa"])]} - ar, ipa = sample.get("ar"), sample.get("ipa") - if not ar or not ipa: - return None - key = str(sample.get("item") or f"sample:{sample.get('id', '')}") - return {"cat": "lab", "key": key, "he": sample.get("speaker") or key, "variants": [(ar, ipa)]} - - -def public_items() -> list[dict]: - """The list for the lab's page (teachers only): each item with its first reading's sounds, which a planted - mistake is marked on, and that reading in Hebrew letters.""" - out = [] - for item in ITEMS: - ar, ipa = item["variants"][0] - out.append({"id": item["id"], "group": item["group"], "focus": item["focus"], "he": item["he"], - "pair": item["pair"], "ar": ar, "tr": " ".join(to_hebrew(ipa, ar)), - "sounds": [s for s in ipa.split() if s != "|"]}) - return out - - -def public_quick_quiz() -> list[dict]: - """The ten model-comparison prompts, without exposing any extra scorer internals.""" - return [{"id": item["id"], "number": number, "focus": item["focus"], "he": item["he"], - "ar": item["variants"][0][0], "tr": " ".join(to_hebrew(item["variants"][0][1], item["variants"][0][0]))} - for number, item in enumerate(QUICK_QUIZ, 1)] - - -# ---------- the benchmark ---------- -# A recording's sounds are judged against its label: a sound said right should not be flagged (one that is, is a false -# rejection), and the planted mistake should be (one that is not, is a miss). The numbers are per sound as written. -GOOD_SCORE = 75 # a recording said right should score at least this (the card's "מצוין") - - -def flag_phrase(row: dict) -> dict: - """The bank phrase a flag was said for, or (a verb-game form, a teacher's text line) the variant it was scored on.""" - from .phrases import PHRASES - - key = row.get("phrase") - for p in PHRASES: - if p.get("key", p["he"]) == key: - return p - return {"variants": [(row["ar"], row["ipa"])], "key": key, "he": row.get("he", ""), "cat": row.get("cat", "")} - - -def flagged_sounds(result: dict) -> list[str]: - """The sounds a result marked down, as "expected>heard".""" - return [f"{p.get('sound', p['ipa'])}>{p.get('heard') or '_'}" for w in result.get("words", []) - for p in w.get("phonemes", []) if p.get("flagged")] - - -def judged(sample: dict, result: dict | None = None) -> list[dict]: - """The sounds of a lab recording, each {"sound", "planted", "flagged", "score", "heard"}: `planted` is the one the - speaker said wrong on purpose (the first sound written as the label's; the scorer may have chosen another reading - of the item, so it is found by sound and not by place). `result` replaces the stored one (a rescore).""" - result = result or sample.get("result") or {} - phones = [p for w in result.get("words", []) for p in w.get("phonemes", [])] - want = (sample.get("mistake") or {}).get("expected") if sample.get("label") == "mistake" else None - planted = next((k for k, p in enumerate(phones) if want in (p["ipa"], p.get("sound"))), None) if want else None - return [{"sound": p["ipa"], "planted": k == planted, "flagged": bool(p.get("flagged")), "score": p["score"], - "heard": p.get("heard") or "_"} for k, p in enumerate(phones)] - - -def benchmark(samples: list[dict], results: dict | None = None, reports: list[dict] = ()) -> dict: - """Per-sound numbers of the lab's recordings. `results` ({sample id: result}) replaces their stored results, for a - rescore with the current scorer; `reports` are the confirmed flags, whose `false_issues` are false rejections a - teacher found in practice (counted per sound, without a denominator: what was said right is not known there). - {"sounds": [{"sound", "right", "false_flags", "frr", "planted", "caught", "detection", "mean", "confusions", - "reports"}], "totals": {...}}, the sounds that went wrong most first.""" - results = results or {} - rows: dict[str, dict] = {} - - def row(sound: str) -> dict: - return rows.setdefault(sound, {"sound": sound, "right": 0, "false_flags": 0, "planted": 0, "caught": 0, - "scores": [], "confusions": {}, "reports": 0}) - - totals = {"recordings": 0, "correct": 0, "mistake": 0, "unrelated": 0, "good": 0, "right": 0, "false_flags": 0, - "planted": 0, "caught": 0, "unplaced": 0, "said_right": 0} - for s in samples: - result = results.get(s.get("id")) or s.get("result") or {} - if not result.get("words"): - continue - totals["recordings"] += 1 - totals[s["label"] if s.get("label") in ("correct", "mistake") else "correct"] += 1 - if result.get("unrelated"): # a different sentence: its sounds tell nothing - totals["unrelated"] += 1 - continue - if s.get("label") != "mistake": - totals["said_right"] += 1 - totals["good"] += result.get("score", 0) >= GOOD_SCORE - sounds = judged(s, result) - if s.get("label") == "mistake" and not any(x["planted"] for x in sounds): - totals["unplaced"] += 1 - for x in sounds: - r = row(x["sound"]) - if x["planted"]: - r["planted"] += 1; totals["planted"] += 1 - if x["flagged"]: - r["caught"] += 1; totals["caught"] += 1 - continue - r["right"] += 1; totals["right"] += 1 - r["scores"].append(x["score"]) - if x["flagged"]: - r["false_flags"] += 1; totals["false_flags"] += 1 - r["confusions"][x["heard"]] = r["confusions"].get(x["heard"], 0) + 1 - for f in reports: - for issue in f.get("false_issues") or []: - if issue.get("expected"): - row(issue["expected"])["reports"] += 1 - - rate = lambda a, b: round(a / b, 3) if b else None - out = [] - for r in rows.values(): - scores = r.pop("scores") - r["mean"] = round(sum(scores) / len(scores), 1) if scores else None - r["frr"], r["detection"] = rate(r["false_flags"], r["right"]), rate(r["caught"], r["planted"]) - r["confusions"] = dict(sorted(r["confusions"].items(), key=lambda kv: -kv[1])) - out.append(r) - out.sort(key=lambda r: (-(r["frr"] or 0), -(1 - r["detection"] if r["detection"] is not None else 0), -r["reports"], - r["sound"])) - totals.update(frr=rate(totals["false_flags"], totals["right"]), detection=rate(totals["caught"], totals["planted"]), - good_share=rate(totals["good"], totals["said_right"])) - return {"sounds": out, "totals": totals} - - -def compact(result: dict) -> dict: - """What the benchmark reads of a result (the sounds, their scores and flags), for the page to send back.""" - keep = ("ipa", "sound", "score", "flagged", "heard") - return {"score": result.get("score"), "heard_ipa": result.get("heard_ipa", ""), - "unrelated": bool(result.get("unrelated")), - "words": [{**{k: w[k] for k in ("ar", "tr", "heard_tr", "score") if k in w}, - "phonemes": [{k: p[k] for k in keep if k in p} for p in w.get("phonemes", [])]} - for w in result.get("words", [])]} - - -def still_flagged(result: dict, issue: dict) -> bool: - """Whether a sound a teacher marked as wrongly flagged (a confirmed flag's false issue: its word and written sound) - is flagged again in this result.""" - words = result.get("words", []) - if not 0 <= issue.get("word", -1) < len(words): - return False - return any(p.get("flagged") and p["ipa"] == issue.get("expected") for p in words[issue["word"]].get("phonemes", [])) - - -# ---------- the sweep over the GOP settings ---------- -# Every GOP_RATIO_POOL with every GOP_VOWEL_WINDOW that differs in practice (a larger pad reaches the wide window). -WINDOWS = (None, 0, 1, 2) -SWEEP = [(pool, window) for pool in POOLS for window in WINDOWS] - - -def sweep_key(pool: str, window: int | None) -> str: - """The name of a setting, as the env writes it: "gated|wide", "max|1".""" - return f"{pool}|{'wide' if window is None else window}" - - -SWEEP_KEYS = {sweep_key(p, w): (p, w) for p, w in SWEEP} - - -def quality(totals: dict) -> float | None: - """The pipeline's one number, 0..100: balanced accuracy over the judged sounds, the mean of - (1 - false rejection rate) and the detection rate. Each half is a rate, so the many sounds said right do not - drown the few planted mistakes, and a setting cannot win by flagging nothing (detection 0) or everything - (false rejections 1). Without planted mistakes it is 1 - frr alone (see `quality_partial`). None: no sound judged.""" - frr, det = totals.get("frr"), totals.get("detection") - if frr is None: - return None - return round(100 * ((1 - frr) + det) / 2, 1) if det is not None else round(100 * (1 - frr), 1) - - -def quality_partial(totals: dict) -> bool: - """True when `quality` could not weigh the detection (no planted mistake was judged).""" - return totals.get("detection") is None - - -def vowel_stats(samples: list[dict], results: dict | None = None) -> dict: - """The vowels of the recordings, by score rather than flag: the scorer flags no vowel but the u of rule 3.11, so the - flag counts of `benchmark` cannot see a GOP change on vowels. Over recordings of the item's sentence (not - "unrelated"): {"right": vowels said right, "mean": their mean score, "weak": the share of them under LETTER_GOOD - (shown to the learner as weak), "planted": planted vowel mistakes, "auc": the chance that a vowel said right - scores above a planted wrong one (ties count half; None without planted vowels)}.""" - results = results or {} - right, wrong = [], [] - for s in samples: - result = results.get(s.get("id")) or s.get("result") or {} - if not result.get("words") or result.get("unrelated"): - continue - for x in judged(s, result): - if x["sound"] in VOWELS: - (wrong if x["planted"] else right).append(x["score"]) - auc = None - if right and wrong: - auc = round(sum(1.0 if r > w else 0.5 if r == w else 0.0 for r in right for w in wrong) / (len(right) * len(wrong)), 3) - return {"right": len(right), "mean": round(sum(right) / len(right), 1) if right else None, - "weak": round(sum(x < LETTER_GOOD for x in right) / len(right), 3) if right else None, - "planted": len(wrong), "auc": auc} diff --git a/app/library.py b/app/library.py deleted file mode 100644 index 3a7e3f3ceab50ae9cb75371f6f8d18d948d5eb33..0000000000000000000000000000000000000000 --- a/app/library.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Shared digital-library validation and persistence operations.""" -from __future__ import annotations - -import logging -from typing import Callable - -from .auth import AuthUnavailable, SupabaseAuth - - -MAX_TITLE = 80 -log = logging.getLogger("ihki.library") - - -class LibraryError(ValueError): - """A safe, user-facing library operation error.""" - - -class LibraryUnavailable(LibraryError): - """The optional persistent content store cannot be used.""" - - -def normalize_title(value: object) -> str: - title = " ".join(str(value or "").split()) - if not title or len(title) > MAX_TITLE: - raise LibraryError(f"לסרטון צריך שם (עד {MAX_TITLE} תווים)") - return title - - -def normalize_url(value: object, parse: Callable[[str], str | None]) -> str: - url = parse(str(value or "")) - if not url: - raise LibraryError("זה לא קישור לסרטון ב-Vimeo או ב-YouTube") - return url - - -def rows(auth: SupabaseAuth | None, *, fresh: bool = False) -> list[dict]: - if auth is None: - raise LibraryUnavailable("ספריית הסרטונים אינה זמינה: Supabase אינו מוגדר") - try: - return auth.level_videos("library", fresh=fresh) - except AuthUnavailable as exc: - raise LibraryUnavailable(f"ספריית הסרטונים אינה זמינה: {exc}") from exc - - -def ensure_unique(auth: SupabaseAuth | None, url: str, *, fresh: bool = True) -> None: - if any(video.get("url") == url for video in rows(auth, fresh=fresh)): - raise LibraryError("הסרטון כבר נמצא בספרייה הדיגיטלית") - - -def add(auth: SupabaseAuth | None, title: str, url: str, *, reject_duplicate: bool = True) -> None: - if reject_duplicate: - ensure_unique(auth, url, fresh=True) - try: - assert auth is not None - auth.add_level_video("library", "", title, url) - except AuthUnavailable as exc: - raise LibraryUnavailable(f"לא ניתן להוסיף לספרייה: {exc}") from exc - - -def rename(auth: SupabaseAuth | None, video_id: int, title: str) -> None: - if not isinstance(video_id, int) or isinstance(video_id, bool) or video_id <= 0: - raise LibraryError("מזהה הסרטון אינו תקין") - if all(video.get("id") != video_id for video in rows(auth, fresh=True)): - raise LibraryError("הסרטון אינו נמצא בספרייה") - try: - assert auth is not None - auth.rename_level_video("library", video_id, title) - except AuthUnavailable as exc: - raise LibraryUnavailable(f"לא ניתן לשנות את שם הסרטון: {exc}") from exc - - -def audit(auth: SupabaseAuth | None, action: str, label: str, outcome: str, **details: object) -> None: - """Best-effort audit using the existing management activity feed.""" - log.info("Grok library action=%s outcome=%s label=%s details=%s", action, outcome, label[:200], details) - if auth is None or not hasattr(auth, "log_event"): - return - auth.log_event("integration", {"id": None, "email": "grok-library"}, label, - {"action": action, "outcome": outcome, **details}) diff --git a/app/live.py b/app/live.py deleted file mode 100644 index 2860340eeacc62ff2a59eed7399ff41694eb43dc..0000000000000000000000000000000000000000 --- a/app/live.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Temporary live-class state. - -Live sessions deliberately live in process memory: class/teacher assignments are durable in -Supabase, while attendance, questions and answers disappear when a session ends or the service -restarts. The browser polls this store; no recording bytes are retained here. -""" -from __future__ import annotations - -import secrets -import threading -import time -from datetime import datetime, timezone - - -SESSION_TTL = 2 * 60 * 60 -PRESENCE_TTL = 20 - - -def _now_iso() -> str: - return datetime.now(timezone.utc).isoformat() - - -class LiveConflict(ValueError): - pass - - -class LiveStore: - def __init__(self): - self._lock = threading.Lock() - self._sessions: dict[str, dict] = {} - - def _prune(self, now: float) -> None: - for code, session in list(self._sessions.items()): - if session["ended"] or now - session["teacher_seen"] > SESSION_TTL: - self._sessions.pop(code, None) - - def _new_code(self) -> str: - for _ in range(100): - code = f"{secrets.randbelow(1_000_000):06d}" - if code not in self._sessions: - return code - raise RuntimeError("could not allocate a live-session code") - - def start(self, course: dict, teacher: dict) -> dict: - now = time.monotonic() - with self._lock: - self._prune(now) - existing = next((s for s in self._sessions.values() if s["class_id"] == course["id"]), None) - if existing: - existing["teacher_seen"] = now - return self._public(existing) - code = self._new_code() - roster = {email: (course.get("nicknames") or {}).get(email) or email.split("@", 1)[0] - for email in course.get("emails", [])} - session = {"code": code, "class_id": course["id"], "class_name": course["name"], - "teacher_id": teacher["id"], "teacher_email": teacher["email"].lower(), - "started_at": _now_iso(), "teacher_seen": now, "ended": False, - "question": None, "participants": {}, "roster": roster, "answers": {}, "question_no": 0} - self._sessions[code] = session - return self._public(session) - - def list_for(self, class_ids: set[int], user_id: str) -> list[dict]: - now = time.monotonic() - with self._lock: - self._prune(now) - return [self._public(s) for s in self._sessions.values() - if s["class_id"] in class_ids or user_id in s["participants"]] - - def get(self, code: str) -> dict | None: - now = time.monotonic() - with self._lock: - self._prune(now) - return self._sessions.get(code) - - def presence_summary(self, class_ids: set[int], *, now: float | None = None) -> dict: - """Return identity-free presence for active sessions in the selected classes.""" - instant = time.monotonic() if now is None else float(now) - selected = {int(class_id) for class_id in class_ids} - with self._lock: - self._prune(instant) - sessions = [session for session in self._sessions.values() if session["class_id"] in selected] - active = {user_id for session in sessions for user_id, person in session["participants"].items() - if instant - person["last_seen"] <= PRESENCE_TTL} - return {"active_session": bool(sessions), "active_users": len(active), - "heartbeat_ttl_seconds": PRESENCE_TTL, "ephemeral": True} - - def join(self, code: str, user: dict, name: str = "") -> dict: - now = time.monotonic() - with self._lock: - self._prune(now) - session = self._sessions.get(code) - if not session: - raise KeyError(code) - session["participants"][user["id"]] = {"email": user["email"].lower(), - "name": name or user["email"].split("@", 1)[0], - "last_seen": now} - return self._public(session) - - def heartbeat(self, code: str, user: dict, teacher: bool = False) -> dict: - now = time.monotonic() - with self._lock: - self._prune(now) - session = self._sessions.get(code) - if not session: - raise KeyError(code) - if teacher: - session["teacher_seen"] = now - elif user["id"] in session["participants"]: - session["participants"][user["id"]]["last_seen"] = now - else: - raise PermissionError(code) - return self._detail(session, user["id"], teacher, now) - - def ask(self, code: str, phrase: dict) -> dict: - now = time.monotonic() - with self._lock: - session = self._sessions.get(code) - if not session: - raise KeyError(code) - variants = phrase.get("variants") or [] - arabic = phrase.get("ar") or (variants[0][0] if variants else "") - session["question_no"] += 1 - session["question"] = {"no": session["question_no"], "phrase_id": phrase["id"], - "he": phrase["he"], "ar": arabic, "tr": phrase["tr"], - "started_at": _now_iso()} - session["answers"] = {} - session["teacher_seen"] = now - return self._public(session) - - def begin_answer(self, code: str, user_id: str) -> dict: - with self._lock: - session = self._sessions.get(code) - if not session or not session["question"]: - raise KeyError(code) - if user_id not in session["participants"]: - raise PermissionError(code) - if user_id in session["answers"]: - raise LiveConflict("one attempt only") - session["answers"][user_id] = {"pending": True} - return dict(session["question"]) - - def cancel_answer(self, code: str, user_id: str) -> None: - with self._lock: - session = self._sessions.get(code) - if session and session["answers"].get(user_id, {}).get("pending"): - session["answers"].pop(user_id, None) - - def finish_answer(self, code: str, user_id: str, answer: dict) -> None: - with self._lock: - session = self._sessions.get(code) - if not session or user_id not in session["answers"]: - raise KeyError(code) - session["answers"][user_id] = answer - - def end(self, code: str) -> None: - with self._lock: - if code not in self._sessions: - raise KeyError(code) - self._sessions.pop(code, None) - - @staticmethod - def _public(session: dict) -> dict: - return {k: session[k] for k in ("code", "class_id", "class_name", "started_at", "question")} - - def _detail(self, session: dict, user_id: str, teacher: bool, now: float) -> dict: - base = self._public(session) - if teacher: - participants = [] - joined_emails = set() - for uid, person in session["participants"].items(): - joined_emails.add(person["email"]) - answer = session["answers"].get(uid) - participants.append({"id": uid, "name": person["name"], "email": person["email"], - "online": now - person["last_seen"] <= PRESENCE_TTL, - "answer": None if not answer or answer.get("pending") else answer}) - participants += [{"id": "", "name": name, "email": email, "online": False, "answer": None} - for email, name in session["roster"].items() if email not in joined_emails] - return {**base, "participants": participants} - answer = session["answers"].get(user_id) - return {**base, "answered": user_id in session["answers"], - "answer": None if not answer or answer.get("pending") else answer} diff --git a/app/main.py b/app/main.py index 26047257857df502b94ebed0c64864ed2a5873ac..0c1d04c0d6079acaf3af30a8df62d3e3ebd6688b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,3568 +1,182 @@ -"""HTTP service: serves the practice page and scores recordings. - -Run: uvicorn app.main:app --host 0.0.0.0 --port 7860 -Env: MODEL_ID (default facebook/wav2vec2-xlsr-53-espeak-cv-ft, or a path to a fine-tuned checkpoint; - a POWSM fine-tune's .pt file or powsm: loads POWSM-CTC, see app/powsm.py) - SAWTAK_API_KEY (model-answer playback with the Raef voice, see app/tts.py) - SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY (Google sign-in, see app/auth.py) - TEACHER_EMAILS, SUPABASE_SECRET_KEY (the teacher's management panel, its classes and the lesson videos, see app/auth.py) - TUTORS_EMAIL (more accounts that, like the teachers, flag a recording scored wrong, see app/auth.py) - ANTHROPIC_API_KEY, TUTOR_MODEL (the permission-controlled AI tutor game יאללה נדבר, see app/tutor.py) - SECRETS_MASTER_KEY (lets a developer replace the two API keys from the panel, see app/keystore.py) -A local .env file is loaded if present; real environment variables win over it. -""" +"""Math-only HTTP service for the Mathly algebra path.""" from __future__ import annotations -import hashlib -import csv -import io -import asyncio -import json import logging import os -import re -import shutil -import tempfile -import subprocess -import threading -import time -import urllib.parse -import uuid -import zipfile -from contextlib import asynccontextmanager -from dataclasses import replace as dc_replace -from datetime import date, datetime, timedelta, timezone +from datetime import datetime from pathlib import Path -from queue import Queue from zoneinfo import ZoneInfo from dotenv import load_dotenv -from fastapi import BackgroundTasks, Body, Depends, FastAPI, File, Form, HTTPException, Query, Request, UploadFile -from fastapi.responses import HTMLResponse, JSONResponse, Response, StreamingResponse +from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile +from fastapi.responses import HTMLResponse, Response from fastapi.staticfiles import StaticFiles -from .arabic_diacritics import point_arabic, tts_arabic -from . import lab -from .auth import AuthUnavailable, ClassConflict, MAX_MONITOR_ATTEMPTS, SupabaseAuth, load_auth -from .keystore import (CREATE_APP_SECRETS, MAX_LEN as MAX_KEY_LEN, MIGRATE_APP_SECRETS, NAMES as KEY_NAMES, KeyRefused, - check as check_key, load_keys) -from .live import LiveConflict, LiveStore -from .math_photo import (DAILY_PHOTOS, MAX_PHOTO, MEDIA_TYPES as PHOTO_TYPES, PhotoSolver, PhotoUnavailable, math_model, - photo_client) +from .access import has_permission, resolve_access +from .auth import AuthUnavailable, ClassConflict, SupabaseAuth, load_auth +from .math_photo import (DAILY_PHOTOS, MAX_PHOTO, MEDIA_TYPES as PHOTO_TYPES, + PhotoSolver, PhotoUnavailable, math_model, photo_client) from .math_skills import algebra9 -from . import push as pushes -from .phrases import CATEGORIES, GAMES, LESSONS, LEVELS, PHRASES, public_phrases -from .prompt_tts import ENGINES, PromptTTSUnavailable, hebrew_mp3 -from .model_settings import CREATE_MODEL_SETTINGS, build as build_model_config, clean as clean_model_settings, describe as describe_model_settings -from .models import (POWSM_ONNX_REPO, availability as model_availability, catalog as model_catalog, - known as model_known) -from .reference import References -from .rules import TOGGLES, check_detectable -from .archi import archify -from .scorer import SAMPLE_RATE, Scorer, decode_audio, decode_audio_with_timing, frame_energy, load_scorer, tokenize -from .speech_evaluation import SpeechEvaluationService -from .texts import read_line, text_phrases -from .tts import (BASELINE_VOICES, LINE_VOICES, RAEF_VOICE_ID, VOICES, SawtakTTS, TTSUnavailable, load_tts, - playback_voices, voice_name) -from .tutor import (BY_ID, DAILY_LIMIT, MAX_TEXT, MAX_TURNS, SCENARIOS, Tutor, TutorLimit, - DEFAULT_MODEL as DEFAULT_TUTOR_MODEL, TutorUnavailable, load_tutor, tutor_client) -from .verb_game import VerbExerciseGenerator -from .grok_mcp import build as build_grok_mcp, oauth_urls -from . import library as video_library -from .access import (DEFAULT_ENTITLEMENTS, DEFAULT_ROLES, ENTITLEMENTS, ENTITLEMENT_PERMISSIONS, - ROLES, ROLE_PERMISSIONS, has_permission, resolve_access) -from .class_management import (ClassManagementError, normalize_class_name, parse_roster, - visible_classes) logging.basicConfig(level=logging.INFO) -log = logging.getLogger("ihki") -STATIC = Path(__file__).resolve().parent.parent / "static" -load_dotenv(STATIC.parent / ".env") -# A class topic's opening screen, by the path it is served at; "/" is Arabic. -SUBJECTS = { - "arabic": {"__TITLE__": "אהלא וסהלא", "__SPLASH_LOGO__": "/static/logo-intro.svg", - "__SPLASH_ALT__": "אהלא וסהלא, בית הספר ללימוד השפה הערבית", "__START_LABEL__": "יאללה נבלש", - "__TAGLINE__": "מדברים ערבית, ומקבלים משוב על כל צליל", - "__APP_NAME__": "אהלא וסהלא", "__THEME_COLOR__": "#4B6A2C", "__MANIFEST__": "/static/manifest.webmanifest"}, - "math": {"__TITLE__": "מתמטיקה בכיף", "__SPLASH_LOGO__": "/static/logo-math.svg", - "__SPLASH_ALT__": "מתמטיקה בכיף, המורה הפרטי למתמטיקה", "__START_LABEL__": "יאללה נתחיל", - "__TAGLINE__": "מתרגלים מתמטיקה, ומקבלים עזרה בכל צעד", - "__APP_NAME__": "מתמטיקה בכיף", "__THEME_COLOR__": "#000000", "__MANIFEST__": "/math/manifest.webmanifest"}, -} -# A subject as an admin and a class keep it (in Hebrew, as the panel shows it), by the path its pages are served at. -SUBJECT_LABELS = {"arabic": "ערבית", "math": "מתמטיקה"} -# What the installed app of a subject other than Arabic changes in /static/manifest.webmanifest: its own name and -# start page, so a math student's home-screen icon opens /math. The scope stays "/", the service worker's. -MANIFESTS = {"math": {"id": "/math", "name": "מתמטיקה בכיף", "short_name": "מתמטיקה בכיף", "description": "תרגול מתמטיקה", - "start_url": "/math", "theme_color": "#000000", "background_color": "#000000"}} - - -def subject_code(label: object) -> str: - """The path of a stored subject ("ערבית" -> "arabic"); "" for none, and for one that is not in SUBJECT_LABELS.""" - label = " ".join(str(label or "").split()) - return next((code for code, name in SUBJECT_LABELS.items() if name == label), "") -MODEL_ID = os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft") -ENV_MODEL_ID = MODEL_ID # the one the server starts with; the panel's "מודלים" tab can switch until a restart -LOCAL_TZ = ZoneInfo("Asia/Jerusalem") # a day of practice is the students' calendar day -WARM_WORKERS = 3 # Sawtak requests the cache-warm sweep runs at once (tts.MAX_CONCURRENT and its pace apply) -MAX_UPLOAD = 2_000_000 # bytes; 15 s of opus is ~100 KB -MAX_BACKUP_UPLOAD = 250_000_000 -EMAIL = re.compile(r"[^@\s,;<>]+@[^@\s,;<>]+\.[^@\s,;<>]+") -MAX_NICKNAME = 40 -ALL_LESSONS = [l["id"] for l in LESSONS] # with the lessons still to come: a class may take them ahead -READY_LESSONS = [l["id"] for l in LESSONS if not l.get("soon")] # the lessons the path shows: a video is added to one of them -MAX_TITLE = 80 -# a Vimeo link: the video's number and, for an unlisted one, its hash (vimeo.com/1233611297/d46ac373c8?share=copy, -# or the player's own player.vimeo.com/video/1233611297?h=d46ac373c8) -VIMEO = re.compile(r"(?:https?://)?(?:www\.)?(?:vimeo\.com/(?:video/)?|player\.vimeo\.com/video/)(\d{6,12})" - r"(?:/([0-9a-f]{6,20})(?![0-9a-z]))?(?:[/?#]\S*)?", re.I) -# a YouTube link: the video's 11 characters, from any of the forms its links take (youtube.com/watch?v=dQw4w9WgXcQ&t=5, -# youtu.be/dQw4w9WgXcQ?si=..., youtube.com/shorts/..., /live/..., /embed/...) -YOUTUBE = re.compile(r"(?:https?://)?(?:(?:www\.|m\.|music\.)?youtube\.com/(?:watch/?\?(?:\S*?&)?v=|(?:shorts|live|embed|v)/)" - r"|(?:www\.)?youtube-nocookie\.com/embed/|youtu\.be/)([\w-]{11})(?![\w-])(?:[/?#&]\S*)?", re.I) -YOUTUBE_KEPT = "https://www.youtube.com/watch?v=" -CREATE_VIDEOS = ("create table public.videos (id bigint generated always as identity primary key, lesson text not null, " - "title text not null, url text not null, pos int, ord int not null default 0, " - "created_at timestamptz not null default now()); alter table public.videos enable row level security;") -MIGRATE_VIDEOS = "alter table public.videos add column if not exists pos int, add column if not exists ord int not null default 0;" -# The daily videos: a library by level; a class gives a video its day (the class's `dailies`: [{"video", "date"}]) -# The library: other videos, which every student browses, whatever the level and the class (its `level` is empty) -# The library's videos stand in the order the teacher gave them (`ord`; one never moved has none, and comes after those) -CREATE_LEVEL_VIDEOS = {t: (f"create table public.{t} (id bigint generated always as identity primary key, level text not null, " - f"title text not null, url text not null, {'ord int, ' if t == 'library' else ''}created_at timestamptz not null default now()); " - f"alter table public.{t} enable row level security;") for t in ("dailies", "library")} -MIGRATE_LIBRARY = "alter table public.library add column if not exists ord int;" -LEVEL_IDS = [v["id"] for v in LEVELS] -LESSON_LEVEL = {l["id"]: l["level"] for l in LESSONS} -MAX_DAILIES = 400 -LESSON_TOPICS = {l: sum(c["lesson"] == l for c in CATEGORIES) for l in READY_LESSONS} # a video's place is 0..this many - - -def placed(rows: list[dict], lesson: str) -> list[dict]: - """A lesson's videos, or its texts, in their order on the path, each with its place: `pos` of the lesson's own - topics stand before it (one without a place, or past the topics the lesson has now, stands after them all), and - `ord` is its turn among the videos and texts of that place.""" - n = LESSON_TOPICS[lesson] - mine = [{**v, "pos": n if v.get("pos") is None else max(0, min(n, v["pos"])), "ord": v.get("ord") or 0} - for v in rows if v["lesson"] == lesson] - return sorted(mine, key=lambda v: (v["pos"], v["ord"], v["id"])) - - -# A text the teacher added (see texts.py) is a topic, "t", and each of its lines a phrase whose id is past the -# bank's: TEXT_BASE + the text's id * TEXT_SPAN + the line's number, so it holds as texts come and go. -TEXT_BASE, TEXT_SPAN = 1_000_000, 1000 -MAX_LINES, MAX_LINE = 200, 300 -# Historical dialogues retain their speaker names even though every line is now played by Raef. -VOICE_NAMES = ["אמיר", "ראאף", "חנאן"] -CREATE_TEXTS = ("create table public.texts (id bigint generated always as identity primary key, lesson text not null, " - "title text not null, voices text[] not null default '{}', lines jsonb not null, pos int, " - "ord int not null default 0, created_at timestamptz not null default now()); " - "alter table public.texts enable row level security;") -_BUILT: dict[tuple[int, int], tuple[list[dict], list[str]]] = {} - - -def built(row: dict) -> tuple[list[dict], list[str]]: - """A text's lines as phrases, and its words that were read by rule (see `texts.text_phrases`); built once.""" - key = (row["id"], hash(str(row["lines"]))) - if key not in _BUILT: - _BUILT[key] = text_phrases(f"t{row['id']}", row["lines"]) - return _BUILT[key] - - -def text_bank(rows: list[dict]) -> dict[int, dict]: - """{phrase id: phrase} of the texts' lines.""" - return {TEXT_BASE + r["id"] * TEXT_SPAN + k: p for r in rows for k, p in enumerate(built(r)[0])} -# The topics the student writes in (see workbook.py): how many sentences each asks for. What is written is kept in -# the Supabase table `writings`, a row per student and topic. -WRITE_TOPICS = {c["id"]: c["write"]["lines"] for c in CATEGORIES if "write" in c} -CREATE_WRITINGS = ("create table public.writings (user_id uuid not null default auth.uid() references auth.users (id) on delete cascade, " - "topic text not null, lines jsonb not null, updated_at timestamptz not null default now(), primary key (user_id, topic)); " - "alter table public.writings enable row level security; grant select, insert, update on public.writings to authenticated; " - 'create policy "students read their own writings" on public.writings for select to authenticated using (user_id = auth.uid()); ' - 'create policy "students add their own writings" on public.writings for insert to authenticated with check (user_id = auth.uid()); ' - 'create policy "students change their own writings" on public.writings for update to authenticated ' - "using (user_id = auth.uid()) with check (user_id = auth.uid());") -# A tutor's flags (see auth.py): a recording that was said right and scored wrong, with what the scorer made of it. -# `ipa` is what was said, so `audio,ipa` of these rows is training data as train/finetune.py reads it. -CREATE_FLAGS = ("create table public.flags (id bigint generated always as identity primary key, email text not null, " - "cat text not null, phrase text not null, he text not null, ar text not null, ipa text not null, heard_ipa text not null, " - "score int not null, unrelated boolean not null default false, note text not null default '', result jsonb not null, " - "model text not null, version text not null default '', audio text, status text not null default 'pending', " - "category text, reviewed_by text, reviewed_at timestamptz, review_note text not null default '', " - "false_issues jsonb not null default '[]'::jsonb, created_at timestamptz not null default now()); " - "alter table public.flags enable row level security; " - "insert into storage.buckets (id, name) values ('flags', 'flags') on conflict (id) do nothing;") -MIGRATE_FLAGS = ("alter table public.flags add column if not exists status text not null default 'pending', " - "add column if not exists category text, add column if not exists reviewed_by text, " - "add column if not exists reviewed_at timestamptz, add column if not exists review_note text not null default '', " - "add column if not exists false_issues jsonb not null default '[]'::jsonb;") -FLAG_STATUSES = frozenset({"pending", "confirmed", "rejected", "fixed"}) -FLAG_CATEGORIES = frozenset({"recognition", "valid_variant", "alternative_answer", "noise", "other"}) -CREATE_CALIBRATIONS = ("create table public.calibrations (id bigint generated always as identity primary key, " - "expected text not null, heard text not null, floor numeric not null default 70, " - "scope text not null default 'global', phrase text not null default '', evidence jsonb not null default '{}', " - "model text not null default '', status text not null default 'draft', proposed_by text not null, " - "reviewed_by text, reviewed_at timestamptz, created_at timestamptz not null default now()); " - "alter table public.calibrations enable row level security;") -CALIBRATION_STATUSES = frozenset({"draft", "applied", "rejected"}) -# The teacher's switches over the rules of pronunciation-rules.md (rules.TOGGLES): a rule with no row is on. -CREATE_SCORING_RULES = ("create table public.scoring_rules (id text primary key, active boolean not null default true, " - "updated_by text not null default '', updated_at timestamptz not null default now()); " - "alter table public.scoring_rules enable row level security;") -# The students the teacher gave the tutor's rank (see auth.py): theirs in every class, so it is kept by email. -CREATE_TUTORS = ("create table public.tutors (email text primary key, granted_by text not null default '', " - "created_at timestamptz not null default now()); alter table public.tutors enable row level security;") -CREATE_USER_ACCESS = ("create table public.user_access (user_id uuid primary key references auth.users(id) on delete cascade, " - "email text not null, roles text[] not null default array['student','flagger']::text[], " - "entitlements text[] not null default '{}'::text[], subject text not null default '', " - "updated_at timestamptz not null default now(), " - "updated_by uuid references auth.users(id), " - "constraint valid_roles check (roles <@ array['student','teacher','flagger','admin','developer']::text[]), " - "constraint valid_entitlements check (entitlements <@ array['practice','games','tutor','path','library']::text[])); " - "create unique index user_access_email_idx on public.user_access (lower(email)); " - "alter table public.user_access enable row level security; " - "create table public.access_audit (id bigint generated always as identity primary key, " - "target_user_id uuid not null, changed_by uuid not null, old_access jsonb not null, " - "new_access jsonb not null, created_at timestamptz not null default now()); " - "alter table public.access_audit enable row level security;") -CREATE_SYSTEM_EVENTS = ("create table public.system_events (id bigint generated always as identity primary key, " - "kind text not null check (kind in ('exercise','game','permission','content')), " - "actor_id uuid references auth.users(id) on delete set null, actor_email text not null default '', " - "label text not null default '', details jsonb not null default '{}'::jsonb, " - "created_at timestamptz not null default now()); " - "create index system_events_created_at_idx on public.system_events (created_at desc); " - "alter table public.system_events enable row level security;") -MIGRATE_USER_ACCESS = ("alter table public.user_access add column if not exists subject text not null default ''; " - "alter table public.user_access drop constraint if exists valid_entitlements; " - "alter table public.user_access alter column roles set default array['student','flagger']::text[]; " - "update public.user_access set entitlements = array_append(entitlements, 'practice') " - "where 'path' = any(entitlements) and not ('practice' = any(entitlements)); " - "alter table public.user_access add constraint valid_entitlements check " - "(entitlements <@ array['practice','games','tutor','path','library']::text[]);") -MAX_NOTE = 500 -MIGRATE_ATTEMPT_ISSUES = ("alter table public.attempts add column if not exists issues jsonb not null default '[]'::jsonb, " - "add column if not exists model text not null default '', " - "add column if not exists version text not null default '';" ) -# The lab's recordings (see lab.py): an item of the diagnostic list, who said it, and whether it was said right -# (`label` "correct") or with one planted mistake (`label` "mistake", `mistake` {"index", "expected"}: the sound of the -# item's first reading that was said wrong). `flagged` is the sounds the scorer marked down, as "expected>heard". -CREATE_LAB = ("create table public.lab_samples (id bigint generated always as identity primary key, email text not null, " - "item text not null, ar text not null, ipa text not null, speaker text not null default '', label text not null, " - "mistake jsonb, device text not null default '', score int not null, heard_ipa text not null, " - "flagged jsonb not null default '[]'::jsonb, result jsonb not null, model text not null, " - "version text not null default '', audio text, created_at timestamptz not null default now()); " - "alter table public.lab_samples enable row level security; " - "insert into storage.buckets (id, name) values ('lab', 'lab') on conflict (id) do nothing;") -MAX_SPEAKER = 254 # an email address, the longest a speaker is -# The lab's automatic test: the list said by the TTS voices, each kept as a speaker of its own ("TTS ראאף") -LAB_TTS_VOICES = {**{name.translate({c: None for c in range(0x591, 0x5c8)}): v for name, v in VOICES.items()}, **LINE_VOICES} -AUDIO_EXT = {"webm": "webm", "ogg": "ogg", "mp4": "m4a", "mpeg": "mp3", "wav": "wav", "x-wav": "wav"} # by the upload's type -TOPIC_LESSON = {c["id"]: c["lesson"] for c in CATEGORIES} # a topic's lesson: homework is a topic of the class's lessons -MIGRATE_CLASSES = ("alter table public.classes add column if not exists nicknames jsonb not null default '{}', " - "add column if not exists lessons text[] not null default '{}', " - "add column if not exists homework text not null default '', add column if not exists due date, " - "add column if not exists homeworks jsonb not null default '[]', " - "add column if not exists dailies jsonb not null default '[]', " - "add column if not exists subject text not null default 'ערבית', " - "add column if not exists teachers text[] not null default '{}'; " - "update public.classes set subject = 'ערבית' where subject = '';") -MAX_HOMEWORKS = 30 - - -def is_math(c: dict) -> bool: - return subject_code(c.get("subject")) == "math" - - -def lessons_of(c: dict) -> list[str]: - """A class's Arabic lessons in the course's order; a class saved before classes had lessons has every one. - A math class has none: its `lessons` column holds the math units it studies (see `units_of`).""" - if is_math(c): - return [] - return [l for l in ALL_LESSONS if l in c["lessons"]] or ALL_LESSONS - - -def units_of(c: dict) -> list[str]: - """A math class's units of the path (app/math_algebra9.json), in the path's order; none chosen means every one.""" - ids = [u["id"] for u in algebra9()["units"]] - return [u for u in ids if u in (c.get("lessons") or [])] or ids - - -def class_lessons(c: dict) -> list[str]: - """What a class studies, as the page gets it: math units for a math class, Arabic lessons for any other.""" - return units_of(c) if is_math(c) else lessons_of(c) - - -def class_level(c: dict) -> str | None: - """The one course level an Arabic class studies, which its current lesson is chosen from. None for a math class - (its path has no lessons to choose) and for a class whose lessons span several levels, or that names none and so - studies every lesson: that class's configuration has to be corrected before a current lesson can be chosen.""" - if is_math(c): - return None - levels = {LESSON_LEVEL[l] for l in c.get("lessons") or [] if l in LESSON_LEVEL} - return next(iter(levels)) if len(levels) == 1 else None - - -def saved_lesson(c: dict, saved: dict[int, str]) -> str | None: - """The current lesson the student keeps for this class, when it is still one of the class's lessons.""" - lesson = saved.get(c.get("id")) - return lesson if lesson and class_level(c) and lesson in lessons_of(c) else None - - -def homework_of(c: dict, topics: dict[str, str] = TOPIC_LESSON) -> dict: - """A class's homework: the topic it practises now (a category id, e.g. the conversation "l2-talk"; "" when none - is set) and the day it is due (ISO date, or None). A topic of a lesson the class no longer studies, or one the - course no longer has, is no homework. `topics` is every topic's lesson: the course's, and the teacher's texts'. - A class may have several, one topic each: "homeworks" is all of them, [{"topic", "due"}], in the order of the - class's lessons, and "homework" and "due" the first (a class saved before it could have several has only that one).""" - mine = lessons_of(c) - listed = c.get("homeworks") or ([{"topic": c["homework"], "due": c.get("due")}] if c.get("homework") else []) - ok = [{"topic": h["topic"], "due": h.get("due") or None} for h in listed - if isinstance(h, dict) and topics.get(h.get("topic")) in mine] - ok.sort(key=lambda h: mine.index(topics[h["topic"]])) - first = ok[0] if ok else {"topic": "", "due": None} - return {"homework": first["topic"], "due": first["due"], "homeworks": ok} - - -def dailies_of(c: dict, library: list[dict]) -> list[dict]: - """A class's daily videos, by their days: [{"id", "title", "embed": the player's address, "date": ISO}]. A video - the library no longer has, or one of a level the class does not study, is left out.""" - levels = {LESSON_LEVEL[l] for l in lessons_of(c)} - by = {v["id"]: v for v in library if v["level"] in levels} - mine = [{"id": v["id"], "title": v["title"], "embed": video_embed(v["url"]), "date": d["date"]} - for d in c.get("dailies") or [] if isinstance(d, dict) and d.get("date") and (v := by.get(d.get("video")))] - return sorted(mine, key=lambda d: (d["date"], d["id"])) - - -def vimeo_link(text: str) -> str | None: - """A Vimeo link as it is kept: "https://vimeo.com/", with "/" for an unlisted video (the sharing - link's tracking parameters are dropped). None when the text is not a link to a Vimeo video.""" - m = VIMEO.fullmatch(text.strip()) - if not m: - return None - h = m[2] or next(iter(urllib.parse.parse_qs(urllib.parse.urlsplit(text.strip()).query).get("h", [])), "") - return f"https://vimeo.com/{m[1]}" + (f"/{h.lower()}" if re.fullmatch(r"[0-9a-fA-F]{6,20}", h) else "") - - -def youtube_link(text: str) -> str | None: - """A YouTube link as it is kept: "https://www.youtube.com/watch?v=" (the playlist, the time and the sharing - parameters are dropped). None when the text is not a link to a YouTube video.""" - m = YOUTUBE.fullmatch(text.strip()) - return YOUTUBE_KEPT + m[1] if m else None - - -def video_link(text: str) -> str | None: - """A link to a video as it is kept, of Vimeo or of YouTube. None when the text is neither.""" - return vimeo_link(text) or youtube_link(text) - - -def video_embed(url: str) -> str: - """The player's address of a kept link (see `video_link`), for the page's frame.""" - if url.startswith(YOUTUBE_KEPT): # without cookies until the video is played, and with no other channels' videos at its end - return f"https://www.youtube-nocookie.com/embed/{url.removeprefix(YOUTUBE_KEPT)}?rel=0" - number, _, h = url.removeprefix("https://vimeo.com/").partition("/") - return f"https://player.vimeo.com/video/{number}?" + urllib.parse.urlencode({ - **({"h": h} if h else {}), "dnt": 1, - "vimeo_logo": 0, "title": 0, "byline": 0, "portrait": 0, - }) - - -def class_members(text: str) -> tuple[list[str], dict[str, str]]: - """The panel's student list: a line per student, "email nickname" (the nickname is optional). A line with - several addresses and nothing else, e.g. pasted "a@x.com, b@x.com", is those students without nicknames. - Gives the emails (lower-case, sorted) and {email: nickname}; text that is not an address raises ValueError.""" - return parse_roster(text) - - -def build_version() -> str: - """Debug build stamp: short content hash of the served code + newest file time (UTC). The hash also stamps the - page's own scripts and styles (?v=), so a change to one of them reaches the service worker's cache-first copy.""" - files = (sorted(Path(__file__).resolve().parent.glob("*.py")) + [STATIC / "index.html"] - + sorted(STATIC.glob("*.js")) + sorted(STATIC.glob("*.css"))) - h = hashlib.sha1() - for f in files: - h.update(f.read_bytes()) - newest = max(f.stat().st_mtime for f in files) - return f"{h.hexdigest()[:7]} · {time.strftime('%Y-%m-%d %H:%M', time.gmtime(newest))} UTC" - - -def build_metadata() -> dict[str, str]: - """Deployment details for the teacher panel, with useful local-development fallbacks.""" - files = sorted(Path(__file__).resolve().parent.glob("*.py")) + [STATIC / "index.html"] - h = hashlib.sha1() - for f in files: - h.update(f.read_bytes()) - newest = max(f.stat().st_mtime for f in files) - commit = next((os.environ.get(name, "").strip() for name in - ("GIT_COMMIT", "COMMIT_SHA", "SOURCE_VERSION", "SPACE_REVISION") - if os.environ.get(name, "").strip()), "") - message = os.environ.get("GIT_COMMIT_MESSAGE", "").strip() - if not commit or not message: - try: - git = subprocess.run( - ["git", "log", "-1", "--format=%H%n%s"], cwd=STATIC.parent, check=True, - capture_output=True, text=True, timeout=2, - ).stdout.splitlines() - if not commit and git: - commit = git[0].strip() - if not message and len(git) > 1: - message = git[1].strip() - except (OSError, subprocess.SubprocessError): - pass - return { - "version": os.environ.get("APP_VERSION", "").strip() or h.hexdigest()[:7], - "updated_at": os.environ.get("BUILD_DATE", "").strip() - or datetime.fromtimestamp(newest, timezone.utc).isoformat(timespec="seconds"), - "commit": commit or "unknown", - "commit_message": message, - } - - -def utc(stamp: str) -> datetime: - """A Supabase timestamp. The seconds part only: the fraction's length varies, which Python 3.10 does not parse.""" - return datetime.strptime(stamp[:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc) - - -def summary(rows: list[dict], texts: dict[int, dict] | None = None) -> dict: - """One student's attempts summed up: best score and tries per phrase id, recordings (all, and of the last - 7 days), days practised, time of the last one. Every non-empty scored recording is a try and completes its - exercise, including one the recognizer labels unrelated. Those are still counted per phrase id in "missed" - and all together in "unmatched" as diagnostics.""" - shown = {c["id"] for c in CATEGORIES + GAMES} - ids = {(p["cat"], p["he"]): i for i, p in enumerate(PHRASES) if p["cat"] in shown} - ids.update({(p["cat"], p["key"]): i for i, p in (texts or {}).items()}) # the lines of the teacher's texts (see `text_bank`) - best: dict[int, int] = {} - tries: dict[int, int] = {} - missed: dict[int, int] = {} - unmatched = 0 - days, week, last = set(), 0, None - since = datetime.now(timezone.utc) - timedelta(days=7) - for r in rows: - at = utc(r["created_at"]) - days.add(at.astimezone(LOCAL_TZ).date()) - week += at >= since - last = max(last, at) if last else at - i = ids.get((r["cat"], r["phrase"])) - if r.get("unrelated"): - unmatched += 1 - if i is not None: - missed[i] = missed.get(i, 0) + 1 - if i is not None: # a sentence the page no longer lists still counts as a recording - best[i] = max(best.get(i, 0), r["score"]) - tries[i] = tries.get(i, 0) + 1 - return {"best": best, "tries": tries, "missed": missed, "unmatched": unmatched, "recordings": len(rows), "days": len(days), "week": week, - "last": last.isoformat() if last else None} - - -def weekly_practice_days(rows: list[dict]) -> int: - """Distinct Jerusalem calendar days practised since Monday of the current week.""" - today = datetime.now(LOCAL_TZ).date() - monday = today - timedelta(days=today.weekday()) - return len({utc(r["created_at"]).astimezone(LOCAL_TZ).date() for r in rows - if monday <= utc(r["created_at"]).astimezone(LOCAL_TZ).date() <= today}) +log = logging.getLogger("mathly") +ROOT = Path(__file__).resolve().parent.parent +STATIC = ROOT / "static" +LOCAL_TZ = ZoneInfo("Asia/Jerusalem") +SUBJECT = "מתמטיקה" +load_dotenv(ROOT / ".env") -CLASS_WEEK_GOAL = 25 # sentences a student, for the class's shared goal of the week -CLASS_WEEK_TTL = 300 # seconds a class's week totals are kept before they are read again -FREEZE_DAYS = 7 # a missed day is forgiven once in this many days (the streak's "יום חופש") +def units_of(row: dict) -> list[str]: + known = [unit["id"] for unit in algebra9()["units"]] + selected = set(row.get("lessons") or known) + return [unit for unit in known if unit in selected] -def practice_streak(rows: list[dict]) -> dict: - """Current and longest runs of consecutive Jerusalem calendar days. - - Yesterday may anchor the current run: a student does not lose the visible streak - before they have had a chance to practise today. One missed day between two practice - days is forgiven once every FREEZE_DAYS days (a streak freeze), so a single slip does - not wipe out weeks of practice. `frozen` is the last forgiven day; `freeze_ready` says - whether a missed yesterday would be forgiven now. - """ - days = sorted({utc(r["created_at"]).astimezone(LOCAL_TZ).date() for r in rows}) - today = datetime.now(LOCAL_TZ).date() - if not days: - return {"current": 0, "best": 0, "last": None, "frozen": None, "freeze_ready": True} - longest = run = 1 - frozen = None - - def can_forgive(missed): - return frozen is None or (missed - frozen).days >= FREEZE_DAYS - - for previous, day in zip(days, days[1:]): - gap = (day - previous).days - if gap == 1: - run += 1 - elif gap == 2 and can_forgive(day - timedelta(days=1)): - run += 1 - frozen = day - timedelta(days=1) - else: - run = 1 - longest = max(longest, run) - ready = can_forgive(today - timedelta(days=1)) - since = (today - days[-1]).days - alive = since <= 1 or (since == 2 and ready) - return {"current": run if alive else 0, "best": longest, "last": days[-1].isoformat(), - "frozen": frozen.isoformat() if frozen else None, "freeze_ready": ready} - - -def create_app(scorer: Scorer | None = None, tts: SawtakTTS | None = None, - auth: SupabaseAuth | None = None, tutor: Tutor | None = None) -> FastAPI: - @asynccontextmanager - async def lifespan(app: FastAPI): - app.state.scorer = scorer or load_scorer(MODEL_ID) - if app.state.auth and hasattr(app.state.auth, "migrate_legacy_audio"): - app.state.auth.migrate_legacy_audio() - app.state.keys = load_keys(app.state.auth) # the API keys a developer set in the panel, else the environment's - app.state.tts = tts or load_tts(app.state.keys.get("SAWTAK_API_KEY")) - app.state.tts_warm = {"running": False, "voice_id": "", "total": 0, "done": 0, "generated": 0, - "skipped": 0, "failed": 0, "current": "", "waiting": 0, "error": "", - "bucket": "", "uploaded": 0, "upload_failed": 0, "upload_error": "", "stopped": False, - "started_at": 0, "phase": "", "to_generate": 0} - app.state.tts_warm_lock = threading.Lock() - app.state.tts_warm_stop = threading.Event() # set by POST /api/tts-cache/warm/stop - app.state.tutor = tutor or load_tutor(app.state.keys.get("ANTHROPIC_API_KEY"), app.state.keys.get("TUTOR_MODEL")) - # Baselines deliberately stay fixed; SAWTAK_VOICE_ID changes playback only (see tts.load_tts). - app.state.refs = References(app.state.scorer, app.state.tts, list(BASELINE_VOICES.values())) if app.state.tts else None - if app.state.auth and hasattr(app.state.auth, "calibrations"): - try: - rows = app.state.auth.calibrations("applied") - app.state.scorer.set_calibrations([r for r in rows if not r.get("model") or r.get("model") == MODEL_ID]) - except AuthUnavailable as e: - log.warning("calibrations not loaded: %s", e) - apply_model_settings(app.state.scorer, MODEL_ID) - if app.state.auth and hasattr(app.state.auth, "scoring_rules"): - try: - reload_scoring_rules(app.state.auth) - except AuthUnavailable as e: # no table yet: every rule stays on - log.warning("scoring rule switches not loaded: %s", e) - app.state.push = pushes.load_push(app.state.auth) - reminders = (asyncio.create_task(pushes.loop(app.state.push, daily_today, lambda: datetime.now(LOCAL_TZ))) - if app.state.push and app.state.push.on else None) - async with app.state.mcp_server.session_manager.run(): - yield - if reminders: - reminders.cancel() - - app = FastAPI(title="ihki", lifespan=lifespan) +def create_app(auth: SupabaseAuth | None = None) -> FastAPI: + """Build the Mathly application.""" + app = FastAPI(title="Mathly") app.state.auth = auth or load_auth() - app.state.verb_game = VerbExerciseGenerator() - app.state.live = LiveStore() - app.state.video_link = video_link - app.state.initial_class_lessons = {SUBJECT_LABELS["arabic"]: READY_LESSONS[0], - SUBJECT_LABELS["math"]: algebra9()["units"][0]["id"]} - metadata = build_metadata() - app.state.mcp_build_info = {"version": metadata["version"], "revision": metadata["commit"][:40]} - app.state.mcp_server, mcp_app, app.state.mcp_http = build_grok_mcp(app) - app.mount("/mcp", mcp_app, name="mcp") + app.state.photo = None + app.state.photo_key = None app.mount("/static", StaticFiles(directory=STATIC), name="static") - def signed_in(request: Request) -> dict | None: - """The caller's user; 401 when sign-in is configured and the call carries no valid session token.""" - a: SupabaseAuth | None = app.state.auth - if a is None: + async def signed_in(request: Request) -> dict | None: + store: SupabaseAuth | None = app.state.auth + if store is None: return None scheme, _, token = request.headers.get("authorization", "").partition(" ") + if scheme.lower() != "bearer" or not token.strip(): + raise HTTPException(401, "sign in required") try: - user = resolve_access(a, token.strip()) if scheme.lower() == "bearer" and token.strip() else None - except AuthUnavailable as e: - raise HTTPException(503, f"sign-in check failed: {e}") + user = resolve_access(store, token.strip()) + except AuthUnavailable as exc: + raise HTTPException(503, f"sign-in check failed: {exc}") from exc if user is None: raise HTTPException(401, "sign in required") if user.get("denied"): - raise HTTPException(403, "this account is not on the list") + raise HTTPException(403, "this account is not in a math class") return user - def require(permission: str): - def allowed(user: dict | None = Depends(signed_in)) -> SupabaseAuth: - if app.state.auth is None: # authentication is optional in local/open deployments - return app.state.auth - if not has_permission(user, permission): - raise HTTPException(403, f"missing permission: {permission}") - return app.state.auth - return allowed - - admin_only = require("management.access") - lab_only = require("lab.access") - teacher_only = admin_only # compatibility name for the management endpoints below - - members = [Depends(signed_in)] - - @app.get("/.well-known/oauth-protected-resource/mcp/", include_in_schema=False) - @app.get("/.well-known/oauth-protected-resource/mcp", include_in_schema=False) - def mcp_oauth_metadata(): - resource, issuer = oauth_urls(app) - return JSONResponse({"resource": resource, "authorization_servers": [issuer], - "scopes_supported": ["openid", "email"], - "bearer_methods_supported": ["header"], - "resource_name": "ihki digital library"}) - - @app.get("/api/oauth/eligibility", include_in_schema=False) - def oauth_eligibility(user: dict | None = Depends(signed_in)): - return {"eligible": has_permission(user, "management.access")} - - @app.get("/oauth/consent", response_class=HTMLResponse, include_in_schema=False) - def oauth_consent(): - html = (STATIC / "oauth-consent.html").read_text(encoding="utf-8") - a: SupabaseAuth | None = app.state.auth - html = html.replace("__SUPABASE_URL_JSON__", json.dumps(a.url if a else "")) - html = html.replace("__SUPABASE_KEY_JSON__", json.dumps(a.key if a else "")) - return HTMLResponse(html, headers={"Cache-Control": "no-store", "Referrer-Policy": "no-referrer"}) - - def managed_classes(a: SupabaseAuth, actor: dict | None, fresh: bool = False) -> list[dict]: - """Classes visible in management: developers see all; admins only their own subject.""" - return visible_classes(a, actor, fresh=fresh) - - def text_rows() -> list[dict]: - """The teacher's texts in the lessons the path shows. None without sign-in, or when they cannot be read.""" - a: SupabaseAuth | None = app.state.auth - if a is None: - return [] - try: - return [r for r in a.texts() if r["lesson"] in READY_LESSONS] - except AuthUnavailable as e: - log.warning("texts not read: %s", e) - return [] - - def topics() -> dict[str, str]: - """Every topic's lesson: the course's topics, and the teacher's texts.""" - return {**TOPIC_LESSON, **{f"t{r['id']}": r["lesson"] for r in text_rows()}} - - def daily_rows() -> list[dict]: - """The library of daily videos. None without sign-in, or when it cannot be read.""" - a: SupabaseAuth | None = app.state.auth - if a is None: - return [] - try: - return a.level_videos("dailies") - except AuthUnavailable as e: - log.warning("daily videos not read: %s", e) - return [] - - def daily_today(email: str, every_class: bool) -> list[dict]: - """The daily videos an account has today: its classes' (as a student or as their teacher), or every class's.""" - a: SupabaseAuth | None = app.state.auth - if a is None: - return [] - day, email, library, found = datetime.now(LOCAL_TZ).date().isoformat(), email.lower(), daily_rows(), {} - for c in a.classes(): - if every_class or email in c["emails"] or email in (c.get("teachers") or []): - for v in dailies_of(c, library): - if v["date"] == day: - found.setdefault(v["id"], v) - return list(found.values()) - - def phrase_of(phrase_id: int) -> dict: - """A phrase of the bank, or a line of a teacher's text; 404 for any other id.""" - if 0 <= phrase_id < len(PHRASES): - return PHRASES[phrase_id] - phrase = text_bank(text_rows()).get(phrase_id) if phrase_id >= TEXT_BASE else None - if phrase is None: - raise HTTPException(404, "unknown phrase") - return phrase + async def manager(user: dict | None = Depends(signed_in)) -> dict: + if not has_permission(user, "management.access"): + raise HTTPException(403, "management permission required") + return user - def page(subject: str) -> HTMLResponse: + def page() -> HTMLResponse: html = (STATIC / "index.html").read_text(encoding="utf-8") - a: SupabaseAuth | None = app.state.auth - version = build_version() - marks = {"__VERSION__": version, "__ASSET_V__": version.split(" ")[0], "__SUPABASE_URL__": a.url if a else "", - "__SUPABASE_KEY__": a.key if a else "", "__SUBJECT__": subject, "__HOME__": home, **SUBJECTS[subject]} - for mark, value in marks.items(): - html = html.replace(mark, value) + store = app.state.auth + html = html.replace("__SUPABASE_URL__", store.url if store else "") + html = html.replace("__SUPABASE_KEY__", store.key if store else "") return HTMLResponse(html, headers={"Cache-Control": "no-cache"}) - # the subject "/" opens: Arabic, or on a Space of its own another one (HOME_SUBJECT=math on mathly); each also has its own path - home = os.environ.get("HOME_SUBJECT", "").strip() or "arabic" - if home not in SUBJECTS: - raise RuntimeError(f"HOME_SUBJECT must be one of: {', '.join(SUBJECTS)}") - - @app.get("/") - def index(): - return page(home) - - for subject in SUBJECTS: # the same app under a class topic's own opening screen: /arabic, /math - app.get(f"/{subject}", include_in_schema=False)(lambda subject=subject: page(subject)) - - def manifest(subject: str) -> Response: - base = json.loads((STATIC / "manifest.webmanifest").read_text(encoding="utf-8")) - return Response(json.dumps({**base, **MANIFESTS[subject]}, ensure_ascii=False), media_type="application/manifest+json", - headers={"Cache-Control": "no-cache"}) + @app.get("/", response_class=HTMLResponse) + @app.get("/math", response_class=HTMLResponse, include_in_schema=False) + async def index(): + return page() - for subject in MANIFESTS: - app.get(f"/{subject}/manifest.webmanifest", include_in_schema=False)(lambda subject=subject: manifest(subject)) + @app.get("/manifest.webmanifest", include_in_schema=False) + async def manifest(): + return Response((STATIC / "manifest.webmanifest").read_text(encoding="utf-8"), + media_type="application/manifest+json") - @app.get("/service-worker.js", include_in_schema=False) - def service_worker(): - return Response( - (STATIC / "service-worker.js").read_text(encoding="utf-8"), - media_type="application/javascript", - headers={"Cache-Control": "no-cache", "Service-Worker-Allowed": "/"}, - ) + @app.get("/api/health") + async def health(): + return {"ok": True, "app": "mathly"} @app.get("/api/me") - def me(user: dict | None = Depends(signed_in)): - """Who is signed in, their classes (each with its lessons, its homework, see `homework_of`, and its daily - videos with their days, see `dailies_of`: a student in - several chooses one to practise in; each also has its "id", its one "level" (None when its lessons span several, - see `class_level`), the "current_lesson" the student chose in it (None before they choose), and "teacher" when - the account teaches it), for a teacher "dailies" (the daily videos of every class), and the lessons of all of them together (every lesson for a teacher, and for an account in no class). The page - asks this right after sign-in to learn whether the account is let in.""" - mine = user["classes"] if user else [] - can_all = bool(user and ({"*", "exercises.use_all"} & set(user.get("permissions", ())))) - lessons = ALL_LESSONS if not mine or can_all else \ - [l for l in ALL_LESSONS if any(l in lessons_of(c) for c in mine)] - # a teacher is in no class, and sees the daily videos of every class, each once by its earliest day - every: dict[int, dict] = {} - can_manage = bool(user and ({"*", "management.access"} & set(user.get("permissions", ())))) - for d in [d for c in app.state.auth.classes() for d in dailies_of(c, daily_rows())] if can_manage else []: - if d["id"] not in every or d["date"] < every[d["id"]]["date"]: - every[d["id"]] = d - # the subject whose pages the account belongs on: an admin's own, else their class's (a student has one) - subject = subject_code(user.get("subject")) if user else "" - subject = subject or next((s for c in mine if (s := subject_code(c.get("subject")))), "") - # the current lesson the student chose in each class; unreadable, the page keeps its own choice on the device - email, saved = (user["email"].lower() if user else ""), {} - if user and mine and hasattr(app.state.auth, "current_lessons"): - try: - got = app.state.auth.current_lessons(email) - saved = got if isinstance(got, dict) else {} - except AuthUnavailable as e: - log.warning("current lessons not read: %s", e) - return {"email": user["email"] if user else "", "teacher": bool(user and user["teacher"]), - **({"subject": subject} if subject else {}), - **({"tutor": True} if user and user["tutor"] else {}), # the page shows them the flag under a score - "roles": user.get("roles", []) if user else [], "entitlements": user.get("entitlements", []) if user else [], - "permissions": user.get("permissions", []) if user else ["*"], - **({"dailies": sorted(every.values(), key=lambda d: (d["date"], d["id"]))} if can_all else {}), - "classes": [{"id": c.get("id"), "name": c["name"], "lessons": class_lessons(c), **homework_of(c, topics()), - "dailies": dailies_of(c, daily_rows()), "level": class_level(c), - "current_lesson": saved_lesson(c, saved), - **({"teacher": True} if email in (c.get("teachers") or []) else {}), - **({"subject": s} if (s := subject_code(c.get("subject"))) else {})} for c in mine], - **({"teaches": [{"id": c["id"], "name": c["name"], "lessons": class_lessons(c)} - for c in user.get("teaches", [])]} if user and user.get("teaches") else {}), - "lessons": lessons} - - @app.put("/api/me/classes/{class_id}/current-lesson") - def set_current_lesson(class_id: int, body: dict = Body(...), user: dict | None = Depends(signed_in)): - """The lesson a student's class is on now ({"lesson": a lesson id}), kept for that student and class only. The - class must be one the caller studies in (not one they teach), of one level, and the lesson one of its lessons.""" - a: SupabaseAuth | None = app.state.auth - if a is None or user is None: - raise HTTPException(401, "sign in required") - email = user["email"].lower() - course = next((c for c in user["classes"] if c.get("id") == class_id), None) - if course is None or email in (course.get("teachers") or []): - raise HTTPException(403, "רק תלמיד/ה בכיתה יכול/ה לבחור את השיעור הנוכחי שלה") - if class_level(course) is None: - raise HTTPException(409, "בכיתה הזאת אין שלב אחד, ולכן אי אפשר לבחור שיעור. צריך לתקן את הגדרות הכיתה.") - lesson = str(body.get("lesson") or "").strip() - if lesson not in lessons_of(course): - raise HTTPException(400, "השיעור הזה אינו מהשיעורים של הכיתה") - try: - a.set_current_lesson(email, class_id, lesson) - except AuthUnavailable as e: - raise HTTPException(503, f"current lesson not saved: {e}") - return {"class_id": class_id, "current_lesson": lesson} - - def live_course(class_id: int) -> dict: - course = next((c for c in app.state.auth.classes() if c["id"] == class_id), None) - if not course: - raise HTTPException(404, "הכיתה לא נמצאה") - return course - - def live_teacher(course: dict, user: dict | None) -> None: - if not user or user["email"].lower() not in (course.get("teachers") or []): - raise HTTPException(403, "רק מורה של הכיתה יכול/ה לנהל את המפגש") - - @app.get("/api/live") - def live_list(response: Response, user: dict | None = Depends(signed_in)): - response.headers["Cache-Control"] = "no-store" - if not user: - return {"sessions": [], "teaches": []} - mine = {c["id"] for c in user.get("classes", [])} - teaches = {c["id"] for c in user.get("teaches", [])} - sessions = app.state.live.list_for(mine | teaches, user["id"]) - return {"sessions": [{**s, "teacher": s["class_id"] in teaches} for s in sessions], - "teaches": [{"id": c["id"], "name": c["name"], "lessons": class_lessons(c)} - for c in user.get("teaches", [])]} - - @app.post("/api/live") - def live_start(body: dict = Body(...), user: dict | None = Depends(signed_in)): - try: - class_id = int(body.get("class_id")) - except (TypeError, ValueError): - raise HTTPException(400, "צריך לבחור כיתה") - course = live_course(class_id) - live_teacher(course, user) - return {"session": app.state.live.start(course, user)} - - @app.post("/api/live/join") - def live_join(body: dict = Body(...), user: dict | None = Depends(signed_in)): - code = re.sub(r"\D", "", str(body.get("code", ""))) - if len(code) != 6: - raise HTTPException(400, "קוד המפגש מכיל 6 ספרות") - session = app.state.live.get(code) - if not session: - raise HTTPException(404, "לא נמצא מפגש פעיל בקוד הזה") - course = live_course(session["class_id"]) - email = user["email"].lower() - name = (course.get("nicknames") or {}).get(email, "") - try: - joined = app.state.live.join(code, user, name) - except KeyError: - raise HTTPException(404, "המפגש הסתיים") - return {"session": joined} - - def live_session_teacher(code: str, user: dict | None) -> tuple[dict, dict]: - session = app.state.live.get(code) - if not session: - raise HTTPException(404, "המפגש הסתיים") - course = live_course(session["class_id"]) - live_teacher(course, user) - return session, course - - @app.get("/api/live/{code}") - def live_status(code: str, response: Response, user: dict | None = Depends(signed_in)): - response.headers["Cache-Control"] = "no-store" - session = app.state.live.get(code) - if not session: - raise HTTPException(404, "המפגש הסתיים") - course = live_course(session["class_id"]) - teacher = bool(user and user["email"].lower() in (course.get("teachers") or [])) - try: - return {"session": app.state.live.heartbeat(code, user, teacher)} - except PermissionError: - raise HTTPException(403, "יש להצטרף למפגש תחילה") - except KeyError: - raise HTTPException(404, "המפגש הסתיים") - - @app.post("/api/live/{code}/question") - def live_question(code: str, body: dict = Body(...), user: dict | None = Depends(signed_in)): - _session, course = live_session_teacher(code, user) - phrase_id = body.get("phrase_id") - if not isinstance(phrase_id, int): - raise HTTPException(400, "צריך לבחור משפט מהמאגר") - phrase = phrase_of(phrase_id) - if topics().get(phrase["cat"]) not in lessons_of(course): - raise HTTPException(403, "המשפט אינו בשיעורים של הכיתה") - return {"session": app.state.live.ask(code, {**phrase, "id": phrase_id})} - - @app.post("/api/live/{code}/answer") - def live_answer(code: str, audio: UploadFile = File(...), user: dict | None = Depends(signed_in)): - try: - question = app.state.live.begin_answer(code, user["id"]) - except LiveConflict: - raise HTTPException(409, "כבר ענית על השאלה הזו") - except PermissionError: - raise HTTPException(403, "יש להצטרף למפגש תחילה") - except KeyError: - raise HTTPException(404, "אין כרגע שאלה פעילה") - try: - data = audio.file.read(MAX_UPLOAD + 1) - if len(data) > MAX_UPLOAD: - raise HTTPException(413, "recording too long") - try: - wav = decode_audio(data) - except ValueError: - raise HTTPException(400, "could not decode audio") - if wav.size < SAMPLE_RATE * .3: - raise HTTPException(400, "לא נשמע מספיק דיבור. נסו שוב.") - phrase = phrase_of(question["phrase_id"]) - refs = app.state.refs.get(question["phrase_id"], phrase) if app.state.refs else [] - result = app.state.scorer.score(wav, phrase, refs) - if result.get("error"): - raise HTTPException(400, result.get("message") or "ההקלטה לא זוהתה") - compact = {"score": round(result["score"]), "unrelated": bool(result.get("unrelated")), - "expected_tr": result.get("variant", {}).get("tr") or phrase.get("tr", ""), - "heard_tr": " ".join(w.get("heard_tr") or "–" for w in result.get("words", [])), - "issues": result.get("issues", [])[:8]} - app.state.live.finish_answer(code, user["id"], compact) - return {"answer": compact, "result": result} - except Exception: - app.state.live.cancel_answer(code, user["id"]) - raise - - @app.delete("/api/live/{code}") - def live_end(code: str, user: dict | None = Depends(signed_in)): - live_session_teacher(code, user) - try: - app.state.live.end(code) - except KeyError: - raise HTTPException(404, "המפגש הסתיים") - return {"ok": True} - - @app.get("/api/progress") - def progress(user: dict | None = Depends(signed_in)): - """The student's best scores and compact practice totals, including this week's active days.""" - rows = app.state.auth.attempts(user["token"]) if user else [] - s = summary(rows, text_bank(text_rows())) - return {**{k: s[k] for k in ("best", "tries", "recordings", "days")}, - "week_days": weekly_practice_days(rows), "streak": practice_streak(rows)} - - @app.get("/api/writing", dependencies=[Depends(require("practice.use"))]) - def writing(user: dict | None = Depends(signed_in)): - """The signed-in student's written homework, {topic id: its lines}. 503 when it cannot be read (or without - sign-in): the page then shows the copy it keeps on the device.""" - if not user: - raise HTTPException(503, "no sign-in: written homework stays on the device") - try: - mine = app.state.auth.writings(user["token"]) - except AuthUnavailable as e: - raise HTTPException(503, f"written homework not available: {e}") - return {"writing": {t: lines[:WRITE_TOPICS[t]] for t, lines in mine.items() if t in WRITE_TOPICS}} + async def me(user: dict | None = Depends(signed_in)): + mine = (user or {}).get("classes", []) + email = (user or {}).get("email", "") + return {"email": email, "roles": (user or {}).get("roles", []), + "permissions": (user or {}).get("permissions", []), + "teacher": bool((user or {}).get("teacher")), + "classes": [{"id": row["id"], "name": row["name"], "lessons": units_of(row), + "teacher": email.lower() in (row.get("teachers") or [])} for row in mine]} - @app.put("/api/writing/{topic}", dependencies=[Depends(require("practice.use"))]) - def write(topic: str, body: dict = Body(...), user: dict | None = Depends(signed_in)): - """Keeps what the student wrote in a writing topic: {"lines": a string per sentence, an empty one for a - sentence not written yet}, in place of what was kept before.""" - if topic not in WRITE_TOPICS: - raise HTTPException(404, "unknown topic") - lines = body.get("lines") - if not isinstance(lines, list) or len(lines) > WRITE_TOPICS[topic] or not all(isinstance(x, str) and len(x) <= MAX_LINE for x in lines): - raise HTTPException(400, f"up to {WRITE_TOPICS[topic]} sentences, each up to {MAX_LINE} characters") - if not user: - raise HTTPException(503, "no sign-in: written homework stays on the device") - try: - app.state.auth.save_writing(user["token"], user["id"], topic, [" ".join(x.split()) for x in lines]) - except AuthUnavailable as e: - if "answered 404" in str(e): - log.warning("the table `writings` is missing. Run in the Supabase SQL editor: %s", CREATE_WRITINGS) - raise HTTPException(503, f"written homework not kept: {e}") - return {"ok": True} - - @app.get("/api/class") - def class_progress(a: SupabaseAuth = Depends(teacher_only), actor: dict | None = Depends(signed_in)): - """The teacher's panel: the classes (with their lessons and homework), and every student's summary (see `summary`) with - "written", their written homework ({topic id: its lines}), with - "tutor" (the teacher gave them the tutor's rank, see `set_tutor`; "tutor_fixed" when TEACHER_EMAILS or TUTORS_EMAIL gives it, which the panel cannot change), - and their classes ({name: nickname}). The summary is the student's whole history, so what they practised in one class - still counts in another class with the same lessons (moving class keeps progress). The students are the classes' accounts, or every account that signed in - when there is no class yet; one that never signed in has joined = false.""" - try: - classes, users, rows, wrote = managed_classes(a, actor, fresh=True), a.users(), a.all_attempts(), a.all_writings() - ranked = a.ranked(fresh=True) - except AuthUnavailable as e: - raise HTTPException(503, str(e)) - by: dict[str, list[dict]] = {} - for r in rows: - by.setdefault(r["user_id"], []).append(r) - written: dict[str, dict[str, list[str]]] = {} # user id -> {topic: lines} - for w in wrote or []: - if w["topic"] in WRITE_TOPICS and isinstance(w.get("lines"), list): - written.setdefault(w["user_id"], {})[w["topic"]] = [str(x) for x in w["lines"]] - member: dict[str, dict[str, str]] = {} # email -> {class name: nickname} - for c in classes: - for e in c["emails"]: - member.setdefault(e, {})[c["name"]] = c["nicknames"].get(e, "") - bank, lesson_of = text_bank(text_rows()), topics() - - def student(email: str, name: str, joined: bool, mine: list[dict], wrote: dict | None = None) -> dict: - into = member.get(email.lower(), {}) - fixed = email.lower() in a.teachers | a.tutors - return {"email": email, "name": name, "joined": joined, "classes": into, "written": wrote or {}, - "tutor": fixed or email.lower() in (ranked or ()), **({"tutor_fixed": True} if fixed else {}), - "streak": practice_streak(mine), **summary(mine, bank)} - - subject_scoped = bool(actor and "developer" not in actor.get("roles", ()) and hasattr(a, "access_for")) - students = [student(u["email"], u["name"], True, by.get(u["id"], []), written.get(u["id"])) - for u in users if (not classes and not subject_scoped) or u["email"].lower() in member] - missing = member.keys() - {s["email"].lower() for s in students} - students += [student(e, "", False, []) for e in sorted(missing)] - # "setup": what the teacher still has to run in Supabase, in words and as SQL; without it the panel would - # show written homework as not done, though the students wrote it (it stays on their devices) - setup = [{"what": "הטבלה writings לא קיימת ב-Supabase, ולכן שיעורי הבית בכתיבה לא נשמרים: מה שהתלמידים כתבו נשאר במכשיר שלהם " - "והפאנל מראה שלא כתבו. אחרי יצירת הטבלה, המשפטים נשלחים בכניסה הבאה של כל תלמיד.", - "sql": CREATE_WRITINGS}] if wrote is None else [] - if ranked is None: - setup.append({"what": "הטבלה tutors לא קיימת ב-Supabase, ולכן אי אפשר לתת לתלמיד דרגת מדווח/ת (דגל 🚩 מתחת לציון).", - "sql": CREATE_TUTORS}) - return {"classes": [{**c, "lessons": class_lessons(c), **homework_of(c, lesson_of)} for c in classes], "students": students, - "setup": setup} - - @app.get("/api/activity") - def activity(hours: int = Query(24, ge=1, le=168), bucket_minutes: int = Query(15, ge=1, le=60), - window_minutes: int | None = Query(None, ge=1, le=60), - period: str | None = Query(None, pattern="^(5m|hour|day|week|month|all)$"), - a: SupabaseAuth = Depends(teacher_only), actor: dict | None = Depends(signed_in)): - """Anonymous, aggregated practice activity for the teacher's monitoring graph.""" - try: - scoped = bool(actor and "developer" not in actor.get("roles", ()) and hasattr(a, "access_for")) - if period: - slots, bucket_minutes = {"5m": (5, 1), "hour": (12, 5), "day": (96, 15), - "week": (168, 60), "month": (720, 60), - "all": (None, 60)}[period] - now = datetime.now(timezone.utc) - if slots is None: - start, hours = now - timedelta(days=365 * 30), 365 * 30 * 24 - else: - seconds = bucket_minutes * 60 - end = datetime.fromtimestamp(int(now.timestamp()) // seconds * seconds, timezone.utc) - start, hours = end - timedelta(seconds=(slots - 1) * seconds), slots * bucket_minutes / 60 - users = a.users() - if scoped: - classes = managed_classes(a, actor, fresh=True) - emails = {email for c in classes for email in c.get("emails", ())} - ids = {u["id"] for u in users if u["email"].lower() in emails} - else: - ids = {u["id"] for u in users} - data = a.scoped_activity(ids, start, bucket_minutes) - return {"period": period, "hours": hours, - "bucket_minutes": bucket_minutes, **data} - if scoped: - classes, users = managed_classes(a, actor, fresh=True), a.users() - emails = {email for c in classes for email in c.get("emails", ())} - ids = {u["id"] for u in users if u["email"].lower() in emails} - start = datetime.now(timezone.utc) - (timedelta(minutes=window_minutes) if window_minutes else timedelta(hours=hours)) - data = a.scoped_activity(ids, start, bucket_minutes) - elif window_minutes is not None: - ids = None - start = datetime.now(timezone.utc) - timedelta(minutes=window_minutes) - rows = [r for r in a.all_attempts(limit=MAX_MONITOR_ATTEMPTS) - if (ids is None or r.get("user_id") in ids) and utc(r["created_at"]) >= start] - seconds, buckets = bucket_minutes * 60, {} - for row in rows: - stamp = int(utc(row["created_at"]).timestamp()) // seconds * seconds - item = buckets.setdefault(stamp, {"users": set(), "attempts": 0}) - item["attempts"] += 1 - item["users"].add(row["user_id"]) - per_user = {} - for row in rows: - per_user.setdefault(row["user_id"], set()).add(utc(row["created_at"]).astimezone(LOCAL_TZ).date()) - data = {"buckets": [{"bucket": datetime.fromtimestamp(stamp, timezone.utc).isoformat(), - "attempts": item["attempts"], "active_students": len(item["users"])} - for stamp, item in sorted(buckets.items())], - "summary": {"attempts": len(rows), "active_students": len(per_user), - "returning_students": sum(len(days) >= 2 for days in per_user.values()), - "active_days": len({day for days in per_user.values() for day in days})}} - else: - data = a.activity(hours, bucket_minutes) - # Accept the former list return shape from lightweight test doubles and older integrations. - if isinstance(data, list): - data = {"buckets": data} - elif data.get("summary") is None: - data = {"buckets": data["buckets"]} - return {"hours": window_minutes / 60 if window_minutes else hours, - **({"window_minutes": window_minutes} if window_minutes else {}), - "bucket_minutes": bucket_minutes, **data} - except AuthUnavailable as e: - raise HTTPException(503, str(e)) - - @app.get("/api/system-activity") - def system_activity(limit: int = Query(60, ge=1, le=200), a: SupabaseAuth = Depends(teacher_only)): - """Recent product activity for managers; no recording or exercise answer is returned.""" - try: - events = a.recent_events(limit) - return {"events": events or [], **({"setup": CREATE_SYSTEM_EVENTS} if events is None else {})} - except AuthUnavailable as e: - raise HTTPException(503, str(e)) - - @app.put("/api/tutors") - def set_tutor(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), user: dict | None = Depends(signed_in)): - """Gives a student the tutor's rank, or takes it back: {"email", "tutor": true or false}. The rank is the - student's in every class: they get the flag under each score (see `flag`).""" - email = str(body.get("email", "")).strip().lower() - if not re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", email) or not isinstance(body.get("tutor"), bool): - raise HTTPException(400, "צריך כתובת אימייל של תלמיד/ה ו-tutor: true או false") - try: - a.set_tutor(email, body["tutor"], user["email"].lower()) - except AuthUnavailable as e: - if "answered 404" in str(e): - raise HTTPException(503, "הטבלה tutors לא קיימת ב-Supabase. הריצו ב-SQL Editor: " - f"{CREATE_TUTORS} אם כבר נוצרה, הריצו שם: notify pgrst, 'reload schema';") - raise HTTPException(503, f"Supabase לא זמין: {e}") - return {"ok": True} - - @app.get("/api/access/users") - def access_users(a: SupabaseAuth = Depends(require("users.manage")), - actor: dict | None = Depends(signed_in)): - """All accounts and their explicit roles/entitlements for the permission editor.""" - if not hasattr(a, "access_rows"): - return {"users": [], "setup": CREATE_USER_ACCESS, "migration": MIGRATE_USER_ACCESS, - "me": actor["id"] if actor else ""} - try: - users, rows = a.users(), a.access_rows() - except AuthUnavailable as e: - raise HTTPException(503, f"Supabase לא זמין: {e}") - if rows is None: - return {"users": [], "setup": CREATE_USER_ACCESS, "migration": MIGRATE_USER_ACCESS, - "me": actor["id"] if actor else ""} - assigned = {r["user_id"]: r for r in rows} - result = [] - for account in users: - row = assigned.get(account["id"], {}) - email = account["email"].lower() - roles = set(row.get("roles") or ["student"]) if row else set(DEFAULT_ROLES) - fixed = [] - if email in getattr(a, "teachers", ()): - roles.update({"teacher", "admin", "flagger"}); fixed += ["teacher", "admin", "flagger"] - if email in getattr(a, "tutors", ()): - roles.add("flagger"); fixed.append("flagger") - if email in getattr(a, "developers", ()): - roles.add("developer"); fixed.append("developer") - entitlements = (set(row.get("entitlements") or []) if row else set(DEFAULT_ENTITLEMENTS)) & ENTITLEMENTS - effective = set().union(*(ROLE_PERMISSIONS[r] for r in roles & ROLES)) - effective.update(*[ENTITLEMENT_PERMISSIONS[e] for e in entitlements]) - result.append({**account, "roles": sorted(roles & ROLES), - "entitlements": sorted(entitlements), "permissions": sorted(effective), - "fixed_roles": sorted(set(fixed))}) - # A successful read cannot tell us which CHECK expression PostgreSQL has, and showing the - # migration unconditionally makes it look unfinished even after it was run. If an old - # constraint rejects a newly selected entitlement, update_access returns the migration there. - return {"users": result, "setup": "", "migration": "", - "me": actor["id"] if actor else ""} - - @app.put("/api/access/users/{target_id}") - def update_access(target_id: str, body: dict = Body(...), - a: SupabaseAuth = Depends(require("users.manage")), - actor: dict | None = Depends(signed_in)): - roles, entitlements = body.get("roles"), body.get("entitlements") - if not isinstance(roles, list) or not roles or not all(isinstance(x, str) for x in roles): - raise HTTPException(400, "roles must be a non-empty list") - if not isinstance(entitlements, list) or not all(isinstance(x, str) for x in entitlements): - raise HTTPException(400, "entitlements must be a list") - roles, entitlements = set(roles), set(entitlements) - if not roles <= ROLES or not entitlements <= ENTITLEMENTS: - raise HTTPException(400, "unknown role or entitlement") - if not actor or target_id == actor["id"]: - raise HTTPException(403, "you cannot change your own access") - is_developer = "*" in actor.get("permissions", ()) - try: - users = a.users() - target = next((u for u in users if u["id"] == target_id), None) - if not target: - raise HTTPException(404, "account not found") - old = a.access_for(target_id) if hasattr(a, "access_for") else None - old_roles = set((old or {}).get("roles") or ["student"]) - if (("developer" in roles) != ("developer" in old_roles)) and not is_developer: - raise HTTPException(403, "only developers can change developer access") - if target["email"].lower() in getattr(a, "developers", ()) and "developer" not in roles: - raise HTTPException(403, "this developer is configured by the server") - if "developer" in old_roles and "developer" not in roles: - rows = a.access_rows() or [] - remaining = sum("developer" in (r.get("roles") or []) and r.get("user_id") != target_id for r in rows) - remaining += sum(e != target["email"].lower() for e in getattr(a, "developers", ())) - if not remaining: - raise HTTPException(409, "the last developer cannot be removed") - a.set_access(target, sorted(roles), sorted(entitlements), actor) - except HTTPException: - raise - except AuthUnavailable as e: - if "answered 404" in str(e): - raise HTTPException(503, f"permissions table is missing; run: {CREATE_USER_ACCESS}") - if any(mark in str(e).lower() for mark in ("valid_entitlements", "check constraint", "23514")): - raise HTTPException(409, f"permissions table needs upgrading; run: {MIGRATE_USER_ACCESS}") - raise HTTPException(503, f"Supabase לא זמין: {e}") - return {"ok": True} - - @app.get("/api/admin-subjects") - def admin_subjects(a: SupabaseAuth = Depends(require("*")), actor: dict | None = Depends(signed_in)): - """Developer-only data for the dedicated subject editor in Monitoring.""" - if not actor or "*" not in actor.get("permissions", ()): - raise HTTPException(403, "developers only") - try: - users, rows = a.users(), a.access_rows() - except AuthUnavailable as e: - raise HTTPException(503, f"Supabase לא זמין: {e}") - subjects = [{"code": code, "label": label} for code, label in SUBJECT_LABELS.items()] - if rows is None: - return {"admins": [], "subjects": subjects, "setup": CREATE_USER_ACCESS, "migration": MIGRATE_USER_ACCESS} - assigned = {r["user_id"]: r for r in rows} - out = [] - for account in users: - row, email = assigned.get(account["id"], {}), account["email"].lower() - roles = set(row.get("roles") or []) - if email in getattr(a, "teachers", ()): - roles.add("admin") - if "admin" in roles: - out.append({"id": account["id"], "email": account["email"], "name": account.get("name", ""), - "subject": str(row.get("subject") or "")}) - return {"admins": out, "subjects": subjects, "setup": "", "migration": ""} - - @app.put("/api/admin-subjects/{target_id}") - def update_admin_subject(target_id: str, body: dict = Body(...), - a: SupabaseAuth = Depends(require("*")), actor: dict | None = Depends(signed_in)): - if not actor or "*" not in actor.get("permissions", ()): - raise HTTPException(403, "developers only") - subject = " ".join(str(body.get("subject", "")).split()) - if not subject_code(subject): # a fixed list: a class matches its admin's subject only when both are spelled the same - raise HTTPException(400, "subject must be one of: " + ", ".join(SUBJECT_LABELS.values())) - try: - target = next((u for u in a.users() if u["id"] == target_id), None) - if not target: - raise HTTPException(404, "account not found") - old = a.access_for(target_id) or {} - roles = set(old.get("roles") or []) - if target["email"].lower() in getattr(a, "teachers", ()): - roles.add("admin") - if "admin" not in roles: - raise HTTPException(400, "the account is not an admin") - a.set_admin_subject(target, subject, actor) - except HTTPException: - raise - except AuthUnavailable as e: - if "subject" in str(e).lower(): - raise HTTPException(503, f"permissions table needs upgrading; run: {MIGRATE_USER_ACCESS}") - raise HTTPException(503, f"Supabase לא זמין: {e}") - return {"ok": True, "subject": subject} - - def make_backup_archive(a: SupabaseAuth) -> tuple[str, bytes]: - tables, archive = a.backup_rows(), io.BytesIO() - tables["final_scores"] = a.final_scores() - with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as out: - manifest = {"format": "ihki-backup", "version": 1, - "created_at": datetime.now(timezone.utc).isoformat(), "tables": tables, "files": []} - out.writestr("backup.json", json.dumps(manifest, ensure_ascii=False, separators=(",", ":"))) - stamp = datetime.now(LOCAL_TZ).strftime("%Y-%m-%d_%H-%M-%S") - return f"ihki-backup-{stamp}.zip", archive.getvalue() - - def apply_backup_archive(raw: bytes, a: SupabaseAuth) -> dict[str, int]: - with zipfile.ZipFile(io.BytesIO(raw)) as src: - if sum(x.file_size for x in src.infolist()) > MAX_BACKUP_UPLOAD * 2: - raise ValueError("הארכיון גדול מדי לאחר פתיחה") - manifest = json.loads(src.read("backup.json")) - if manifest.get("format") != "ihki-backup" or manifest.get("version") != 1: - raise ValueError("זה אינו קובץ גיבוי נתמך של המערכת") - tables, files = manifest.get("tables"), manifest.get("files") - if not isinstance(tables, dict) or not isinstance(files, list) or \ - any(not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows) - for rows in tables.values()): - raise ValueError("מבנה הנתונים בגיבוי אינו תקין") - restored = a.restore_rows(tables) - if "final_scores" in tables: - restored["final_scores"] = a.restore_final_scores(tables["final_scores"]) - return restored - - @app.post("/api/admin/backups") - def create_saved_backup(a: SupabaseAuth = Depends(require("*")), actor: dict | None = Depends(signed_in)): - """Create a timestamped database-only archive in the private backups bucket.""" - if not actor or "*" not in actor.get("permissions", ()): - raise HTTPException(403, "developers only") - try: - name, data = make_backup_archive(a) - a.save_backup(name, data) - return {"ok": True, "name": name} - except AuthUnavailable as e: - raise HTTPException(503, f"הגיבוי נכשל: {e}") - - @app.get("/api/admin/backups") - def saved_backups(a: SupabaseAuth = Depends(require("*")), actor: dict | None = Depends(signed_in)): - if not actor or "*" not in actor.get("permissions", ()): - raise HTTPException(403, "developers only") - try: - return {"backups": a.list_backups()} - except AuthUnavailable as e: - raise HTTPException(503, f"טעינת הגיבויים נכשלה: {e}") - - @app.get("/api/admin/backups/file") - def download_saved_backup(name: str = Query(...), a: SupabaseAuth = Depends(require("*")), - actor: dict | None = Depends(signed_in)): - if not actor or "*" not in actor.get("permissions", ()): - raise HTTPException(403, "developers only") - if not re.fullmatch(r"ihki-backup-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.zip", name): - raise HTTPException(400, "שם גיבוי לא תקין") - try: - data, _ = a._get_object("backups", name) - return Response(data, media_type="application/zip", headers={ - "Content-Disposition": f'attachment; filename="{name}"', "Cache-Control": "no-store"}) - except AuthUnavailable as e: - raise HTTPException(503, f"הורדת הגיבוי נכשלה: {e}") - - @app.get("/api/admin/backup") - def download_backup(a: SupabaseAuth = Depends(require("*")), actor: dict | None = Depends(signed_in)): - """Download a fresh archive directly; retained for manual off-site copies.""" - if not actor or "*" not in actor.get("permissions", ()): - raise HTTPException(403, "developers only") - try: - name, data = make_backup_archive(a) - return Response(data, media_type="application/zip", headers={ - "Content-Disposition": f'attachment; filename="{name}"', "Cache-Control": "no-store"}) - except AuthUnavailable as e: - raise HTTPException(503, f"הגיבוי נכשל: {e}") - - @app.post("/api/admin/backups/restore") - def restore_saved_backup(body: dict = Body(...), a: SupabaseAuth = Depends(require("*")), - actor: dict | None = Depends(signed_in)): - if not actor or "*" not in actor.get("permissions", ()): - raise HTTPException(403, "developers only") - name = str(body.get("name", "")) - if not re.fullmatch(r"ihki-backup-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.zip", name): - raise HTTPException(400, "שם גיבוי לא תקין") - try: - raw, _ = a._get_object("backups", name) - restored = apply_backup_archive(raw, a) - return {"ok": True, "tables": restored, "name": name} - except (ValueError, KeyError, json.JSONDecodeError, zipfile.BadZipFile) as e: - raise HTTPException(400, f"קובץ הגיבוי אינו תקין: {e}") - except AuthUnavailable as e: - raise HTTPException(503, f"השחזור נעצר: {e}") - - @app.post("/api/admin/restore") - async def restore_backup(backup: UploadFile = File(...), a: SupabaseAuth = Depends(require("*")), - actor: dict | None = Depends(signed_in)): - """Merge a portable app backup. It updates matching rows but never deletes unrelated current data.""" - if not actor or "*" not in actor.get("permissions", ()): - raise HTTPException(403, "developers only") - raw = await backup.read(MAX_BACKUP_UPLOAD + 1) - if len(raw) > MAX_BACKUP_UPLOAD: - raise HTTPException(413, "קובץ הגיבוי גדול מדי (עד 250 MB)") - try: - restored = apply_backup_archive(raw, a) - return {"ok": True, "tables": restored} - except (ValueError, KeyError, json.JSONDecodeError, zipfile.BadZipFile) as e: - raise HTTPException(400, f"קובץ הגיבוי אינו תקין: {e}") - except AuthUnavailable as e: - raise HTTPException(503, f"השחזור נעצר: {e}") - - @app.get("/api/flags") - def flags(status: str = Query("pending"), a: SupabaseAuth = Depends(teacher_only)): - """The score reports awaiting a teacher's decision (or one reviewed state).""" - if status not in FLAG_STATUSES: - raise HTTPException(400, "invalid flag status") - try: - return {"flags": a.list_flags(status)} - except AuthUnavailable as e: - detail = str(e) - if "status" in detail or "false_issues" in detail: - raise HTTPException(503, f"לטבלה flags חסרות עמודות הבדיקה. הריצו ב-SQL Editor: {MIGRATE_FLAGS}") - raise HTTPException(503, f"Supabase לא זמין: {e}") - - @app.put("/api/flags/{flag_id}") - def review_flag(flag_id: int, body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), - user: dict | None = Depends(signed_in)): - """Confirms or rejects a report and records which displayed sound judgements were false.""" - status, category = body.get("status"), body.get("category") - issues = body.get("false_issues", []) - if status not in FLAG_STATUSES - {"pending"} or category not in FLAG_CATEGORIES: - raise HTTPException(400, "invalid flag review") - if not isinstance(issues, list) or len(issues) > 30: - raise HTTPException(400, "invalid false_issues") - clean = [] - for issue in issues: - if not isinstance(issue, dict) or not isinstance(issue.get("word"), int): - raise HTTPException(400, "invalid false_issues") - expected, heard = str(issue.get("expected", ""))[:20], str(issue.get("heard", ""))[:20] - if not expected: - raise HTTPException(400, "invalid false_issues") - clean.append({"word": max(0, issue["word"]), "expected": expected, "heard": heard}) - values = {"status": status, "category": category, "false_issues": clean, - "review_note": " ".join(str(body.get("review_note", "")).split())[:MAX_NOTE], - "reviewed_by": user["email"].lower(), "reviewed_at": datetime.now(timezone.utc).isoformat()} - try: - a.review_flag(flag_id, values) - except AuthUnavailable as e: - detail = str(e) - if any(column in detail for column in ("status", "category", "false_issues", "reviewed_by")): - raise HTTPException(503, f"לטבלה flags חסרות עמודות הבדיקה. הריצו ב-SQL Editor: {MIGRATE_FLAGS}") - raise HTTPException(503, f"Supabase לא זמין: {e}") - return {"ok": True} - - @app.get("/api/flags/{flag_id}/audio") - def flag_audio(flag_id: int, a: SupabaseAuth = Depends(teacher_only)): - """Streams a private flagged recording to a teacher without exposing the storage bucket or secret key.""" - try: - found = a.flag_audio(flag_id) - except AuthUnavailable as e: - raise HTTPException(503, f"ההקלטה לא זמינה כרגע: {e}") - if found is None: - raise HTTPException(404, "לדיווח הזה אין הקלטה שמורה") - data, media_type = found - return Response(data, media_type=media_type, headers={"Cache-Control": "private, no-store"}) - - def lab_error(e: AuthUnavailable) -> HTTPException: - if "answered 404" in str(e): - return HTTPException(503, f"הטבלה lab_samples לא קיימת. הריצו ב-SQL Editor: {CREATE_LAB} " - "אם כבר נוצרה, הריצו שם: notify pgrst, 'reload schema';") - return HTTPException(503, f"Supabase לא זמין: {e}") - - @app.get("/api/lab") - def lab_items(speaker: str = Query(""), a: SupabaseAuth = Depends(lab_only), - user: dict | None = Depends(signed_in)): - """The lab's diagnostic list, with how many recordings each item has (`counts`) and how many of them are - `speaker`'s, by default the signed-in teacher's (`mine`: the lab's test marks an item submitted by it; the - automatic test asks for its "TTS "). `setup` is the SQL to run while the table is missing: the list - still shows and scores, but nothing is kept.""" - try: - rows, setup = a.lab_samples(), "" - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise lab_error(e) - rows, setup = [], CREATE_LAB - counts: dict[str, dict[str, int]] = {} - mine: dict[str, dict[str, int]] = {} - who = " ".join(speaker.split())[:MAX_SPEAKER] or user["email"].lower() - for r in rows: - label = r["label"] if r["label"] in ("correct", "mistake") else "correct" - for tally in (counts, mine) if who and (r.get("speaker") or "") == who else (counts,): - tally.setdefault(r["item"], {"correct": 0, "mistake": 0})[label] += 1 - return {"items": lab.public_items(), "quick_quiz": lab.public_quick_quiz(), "groups": lab.GROUPS, - "counts": counts, "mine": mine, "setup": setup} - - @app.post("/api/lab/score") - def lab_score(request: Request, item: str = Form(...), audio: UploadFile = File(...), - mistake: int = Form(-1), save: bool = Form(True), a: SupabaseAuth = Depends(lab_only), - user: dict | None = Depends(signed_in)): - """Scores a recording of a lab item as a practice recording is scored (with the native baseline), with the - teacher's stage-by-stage trace, and keeps it with its label unless `save` is off, as said by the signed-in - teacher (the speaker is their email, never a name typed in the page). `mistake` is the index of the - planted mistake among the item's sounds (-1: said right). {"result", "saved": whether the row was kept, - "audio": whether the recording was kept too, "error": why the row was not kept}.""" - found = lab.BY_ID.get(item) - if found is None: - raise HTTPException(404, "no such lab item") - sounds = [x for x in found["variants"][0][1].split() if x != "|"] - if not -1 <= mistake < len(sounds): - raise HTTPException(400, "invalid mistake") - data = audio.file.read(MAX_UPLOAD + 1) - if len(data) > MAX_UPLOAD: - raise HTTPException(413, "recording too long") - try: - wav = decode_audio(data) - except ValueError: - raise HTTPException(400, "could not decode audio") - if wav.size < 16_000 * 0.3: - return {"result": {"error": "too_short", "message": "לא נשמע דיבור. נסו שוב, קרוב יותר למיקרופון."}, "saved": False} - phrase = lab.phrase(found) - refs = app.state.refs.get(phrase["key"], phrase) if app.state.refs else [] - result = app.state.scorer.score(wav, phrase, refs, diagnostics=True) - if not save: - return {"result": result, "saved": False} - mime = (audio.content_type or "application/octet-stream").lower() - ext = AUDIO_EXT.get(mime.partition(";")[0].strip().rpartition("/")[2], "bin") - flagged = [f"{p.get('sound', p['ipa'])}>{p.get('heard') or '_'}" for w in result.get("words", []) - for p in w.get("phonemes", []) if p.get("flagged")] - row = {"email": user["email"].lower(), "item": item, "ar": result["variant"]["ar"], "ipa": result["variant"]["ipa"], - "speaker": user["email"].lower(), "label": "correct" if mistake < 0 else "mistake", - "mistake": None if mistake < 0 else {"index": mistake, "expected": sounds[mistake]}, - "device": request.headers.get("user-agent", "")[:200], "score": result["score"], - "heard_ipa": result["heard_ipa"], "flagged": flagged, "result": result, "model": MODEL_ID, - "version": build_version()} - try: - kept = a.add_lab_sample(row, data, mime, f"{date.today():%Y-%m}/{uuid.uuid4().hex}.{ext}") - except AuthUnavailable as e: - return {"result": result, "saved": False, "error": lab_error(e).detail} - return {"result": result, "saved": True, "audio": kept} - - def lab_tts_audio(item: str, voice: str) -> tuple[dict, bytes]: - """The lab item and its first reading said by a TTS voice (by name, LAB_TTS_VOICES), as a WAV.""" - found = lab.BY_ID.get(item) - if found is None: - raise HTTPException(404, "no such lab item") - if voice not in LAB_TTS_VOICES: - raise HTTPException(400, "unknown voice") - if app.state.tts is None: - raise HTTPException(503, "אין TTS בשרת הזה (SAWTAK_API_KEY לא מוגדר)") - try: - data, _ = app.state.tts.wav(tts_arabic(*found["variants"][0]), LAB_TTS_VOICES[voice]) - except TTSUnavailable as e: - raise HTTPException(503, f"ה-TTS לא זמין: {e}") - return found, data - - @app.get("/api/lab/tts") - def lab_tts_voices(a: SupabaseAuth = Depends(lab_only)): - """The voices the automatic test can use (none without TTS on this server).""" - return {"voices": list(LAB_TTS_VOICES) if app.state.tts else []} - - @app.get("/api/lab/tts/{item}") - def lab_tts_listen(item: str, voice: str = Query(...), a: SupabaseAuth = Depends(lab_only)): - """What the automatic test sent for an item: the voice's WAV (cached, so no new synthesis).""" - return Response(lab_tts_audio(item, voice)[1], media_type="audio/wav", headers={"Cache-Control": "private, no-store"}) - - @app.post("/api/lab/tts") - def lab_tts_score(item: str = Form(...), voice: str = Form(...), save: bool = Form(True), - a: SupabaseAuth = Depends(lab_only), user: dict | None = Depends(signed_in)): - """The automatic test, one item: a TTS voice says it, and it is scored and kept as said right by the speaker - "TTS ", like an approved take of the test. Scored without the native baseline, which is made of the same - voice and would measure every sound against itself. {"result", "saved", "audio", "speaker", "error"?}.""" - found, data = lab_tts_audio(item, voice) - try: - wav = decode_audio(data) - except ValueError: - raise HTTPException(502, "the TTS audio could not be decoded") - phrase = lab.phrase(found) - result = app.state.scorer.score(wav, phrase, [], diagnostics=True) - speaker = f"TTS {voice}" - if result.get("error") or not save: - return {"result": result, "saved": False, "speaker": speaker} - row = {"email": user["email"].lower(), "item": item, "ar": result["variant"]["ar"], "ipa": result["variant"]["ipa"], - "speaker": speaker, "label": "correct", "mistake": None, "device": f"tts:{LAB_TTS_VOICES[voice]}", - "score": result["score"], "heard_ipa": result["heard_ipa"], "flagged": lab.flagged_sounds(result), - "result": result, "model": MODEL_ID, "version": build_version()} - try: - kept = a.add_lab_sample(row, data, "audio/wav", f"{date.today():%Y-%m}/tts-{uuid.uuid4().hex}.wav") - except AuthUnavailable as e: - return {"result": result, "saved": False, "speaker": speaker, "error": lab_error(e).detail} - return {"result": result, "saved": True, "audio": kept, "speaker": speaker} - - @app.get("/api/lab/results") - def lab_results(speaker: str = Query(""), a: SupabaseAuth = Depends(lab_only)): - """The benchmark of the lab's recordings as they were scored (see lab.benchmark), of one speaker or of all; - with the confirmed flags' false issues beside each sound. scripts/bench.py rescores them with the current scorer.""" - try: - samples = a.lab_samples(full=True) - except AuthUnavailable as e: - raise lab_error(e) - try: - reports = a.list_flags("confirmed") - except AuthUnavailable as e: # the reports are a side column: the benchmark stands without them - log.warning("confirmed flags not read for the lab: %s", e) - reports = [] - speakers = sorted({s.get("speaker") or "" for s in samples}) - if speaker: - samples = [s for s in samples if (s.get("speaker") or "") == speaker] - # the recordings themselves, newest first, for the trace dashboard's picker - recordings = [{"id": s["id"], "item": s["item"], "speaker": s.get("speaker") or "", "label": s.get("label"), - "mistake": s.get("mistake"), "score": s.get("score"), "flagged": s.get("flagged") or [], - "unrelated": bool((s.get("result") or {}).get("unrelated")), "created_at": s.get("created_at")} - for s in reversed(samples) if s.get("audio")] - return {**lab.benchmark(samples, reports=reports), "speakers": speakers, "model": MODEL_ID, - "version": build_version(), "samples": [s["id"] for s in samples if s.get("audio")], - "recordings": recordings} - - @app.get("/api/lab/minerva") - def lab_minerva(a: SupabaseAuth = Depends(lab_only)): - """The imported Minerva audio joined with its reviewed JSON sentence metadata.""" - try: - samples = a.lab_samples(full=True) - except AuthUnavailable as e: - raise lab_error(e) - manifest = lab.minerva_metadata() - definitions = {segment["item"]: (video, segment) for video in manifest for segment in video["segments"]} - videos: dict[str, dict] = {} - for s in samples: - definition = definitions.get(s.get("item")) - if (s.get("speaker") or "") != "מינרווה" or definition is None: - continue - meta_video, segment = definition - video_id = meta_video["id"] - stored = s.get("result") or {} - excluded = stored.get("_lab_excluded_words") or {} - if not isinstance(excluded, dict): - excluded = {} - video = videos.setdefault(video_id, {"id": video_id, "title": meta_video["title"], - "filename": meta_video["filename"], "samples": []}) - video["samples"].append({"id": s["id"], "item": s["item"], "line": segment["line"], - "start": segment["start"], "end": segment["end"], - "ar": segment["ar"], "tr": segment["tr"], "he": segment["he"], - "ipa": segment["ipa"], - "score": s.get("score"), "heard_ipa": s.get("heard_ipa") or "", - "flagged": s.get("flagged") or [], - "unrelated": bool(stored.get("unrelated")), "audio": bool(s.get("audio")), - "excluded_words": {str(k): str(v) for k, v in excluded.items()}}) - order = [video["id"] for video in manifest] - out = list(videos.values()) - out.sort(key=lambda v: order.index(v["id"]) if v["id"] in order else len(order)) - for video in out: - video["samples"].sort(key=lambda s: s["line"]) - return {"videos": out, "model": MODEL_ID, "version": build_version()} - - @app.delete("/api/lab/samples") - def lab_delete_samples(speaker: str | None = Query(None), everyone: bool = Query(False), - a: SupabaseAuth = Depends(lab_only)): - """Deletes the lab's recordings, rows and files, to record the list afresh: one `speaker`'s ("" the unnamed), - or with `everyone` every speaker's. One of the two is required, so a missing parameter never deletes all. - {"deleted": how many recordings}.""" - if (speaker is None) == (not everyone): - raise HTTPException(400, "give a speaker, or everyone=true") - try: - n = a.delete_lab_samples(None if everyone else " ".join(speaker.split())[:MAX_SPEAKER]) - except AuthUnavailable as e: - raise lab_error(e) - log.info("lab: %d recordings deleted (%s)", n, "every speaker" if everyone else f"speaker {speaker!r}") - return {"deleted": n} - - @app.get("/api/lab/samples/{sample_id}/audio") - def lab_sample_audio(sample_id: int, a: SupabaseAuth = Depends(lab_only)): - """Streams a private saved lab recording to a teacher, including imported Minerva segments.""" - try: - found = a.lab_audio(sample_id) - except AuthUnavailable as e: - raise lab_error(e) - if found is None: - raise HTTPException(404, "להקלטה הזו אין קובץ שמור") - data, media_type = found - return Response(data, media_type=media_type, headers={"Cache-Control": "private, no-store"}) - - @app.post("/api/lab/samples/{sample_id}/exclude-word") - def lab_exclude_word(sample_id: int, body: dict = Body(...), a: SupabaseAuth = Depends(lab_only)): - """Includes or excludes one word from Minerva evaluation metrics, without changing the pipeline's score.""" - word, excluded = body.get("word"), body.get("excluded") - if not isinstance(word, int) or word < 0 or not isinstance(excluded, bool): - raise HTTPException(400, "invalid word exclusion") - try: - row = next((s for s in a.lab_samples(full=True) if s.get("id") == sample_id), None) - except AuthUnavailable as e: - raise lab_error(e) - if row is None: - raise HTTPException(404, "no such lab recording") - result = row.get("result") or {} - if word >= len(result.get("words") or []): - raise HTTPException(400, "invalid word index") - saved_marks = result.get("_lab_excluded_words") or {} - marks = {str(k): str(v) for k, v in saved_marks.items()} if isinstance(saved_marks, dict) else {} - if excluded: - reason = " ".join(str(body.get("reason") or "").split())[:200] - marks[str(word)] = reason or "לא עבר אימות" - else: - marks.pop(str(word), None) - result["_lab_excluded_words"] = marks - try: - a.update_lab_result(sample_id, result) - except AuthUnavailable as e: - raise lab_error(e) - return {"id": sample_id, "excluded_words": marks} - - @app.post("/api/lab/samples/{sample_id}/rescore") - def lab_rescore_sample(sample_id: int, trace: bool = Query(False), setting: str = Query(""), - a: SupabaseAuth = Depends(lab_only)): - """A lab recording scored again by the current scorer (as practice scores, with the native baseline), nothing - kept: {"id", "result": what the benchmark reads of it (lab.compact)}. The page rescores the recordings one by - one and sends the results to POST /api/lab/results. - `trace`: the whole result with the teacher's stage-by-stage trace instead, for the trace dashboard. `setting` - (a lab.SWEEP key, "gated|wide"): scored under that GOP setting, its native baseline rebuilt under it too.""" - if setting and setting not in lab.SWEEP_KEYS: - raise HTTPException(400, "unknown setting") - try: - row = a.lab_sample(sample_id) - found = a.lab_audio(sample_id) if row else None - except AuthUnavailable as e: - raise lab_error(e) - phrase = lab.sample_phrase(row) if row else None - if phrase is None: - raise HTTPException(404, "no such lab recording") - if found is None: - raise HTTPException(404, "להקלטה הזו אין קובץ שמור") - try: - wav, trim_start = decode_audio_with_timing(found[0]) - except ValueError: - raise HTTPException(400, "could not decode audio") - scorer = app.state.scorer - if setting: - pool, window = lab.SWEEP_KEYS[setting] - scorer = scorer.with_gop(ratio_pool=pool, vowel_window=window) - refs = sweep_references(scorer, phrase, setting) - else: - refs = app.state.refs.get(phrase["key"], phrase) if app.state.refs else [] - result = scorer.score(wav, phrase, refs, diagnostics=trace) - if trace and result.get("trace"): - result["trace"]["audio"] = {"trim_start_ms": round(1000 * trim_start / SAMPLE_RATE, 1)} - if not trace: - return {"id": sample_id, "result": lab.compact(result)} - g = scorer.cfg.gop - excluded = (row.get("result") or {}).get("_lab_excluded_words") or {} - return {"id": sample_id, "result": result, "setting": lab.sweep_key(g.ratio_pool, g.vowel_window), - "sample": {**{k: row.get(k) for k in ("id", "item", "speaker", "label", "mistake")}, - "excluded_words": excluded if isinstance(excluded, dict) else {}}} - - # The sweep (POST /api/lab/samples/{id}/sweep) rebuilds each native baseline under every setting: the voices' - # posteriors are kept by (voice, wav hash), their References by setting too, for the life of the server. - sweep_emits: dict[tuple[str, str], tuple] = {} - sweep_refs: dict[tuple, object] = {} - - def sweep_references(scorer: Scorer, phrase: dict, setting: str) -> list: - """The native baseline of `phrase` under `scorer`'s setting, from the TTS voices whose WAV is cached (the first - voice's is made on the way, as a rescore makes it). Empty without TTS.""" - refs = app.state.refs - if refs is None: - return [] - refs.get(phrase["key"], phrase) - text, out = tts_arabic(*phrase["variants"][0]), [] - for voice in refs.voices: - try: - if not refs.tts.cached(text, voice): - continue - data, _ = refs.tts.wav_for_voice(text, voice, source="baseline") - except Exception as e: # provider down: score without this voice, as References does - log.warning("sweep: no %s voice for %r: %s", voice, text, e) - continue - emitted = (voice, hashlib.sha1(data).hexdigest()) - if emitted not in sweep_emits: - if len(sweep_emits) > 400: - sweep_emits.clear(); sweep_refs.clear() - wav = decode_audio(data) - logp = app.state.scorer.emit(wav) - sweep_emits[emitted] = (logp, frame_energy(wav, len(logp))) - key = (*emitted, phrase["key"], setting, scorer.disabled) - if key not in sweep_refs: - logp, energy = sweep_emits[emitted] - ref = scorer.reference_logp(logp, phrase, energy) - sweep_refs[key] = dc_replace(ref, voice=voice_name(voice)) if ref is not None else None - if sweep_refs[key] is not None: - out.append(sweep_refs[key]) - return out - - @app.post("/api/lab/samples/{sample_id}/sweep") - def lab_sweep_sample(sample_id: int, a: SupabaseAuth = Depends(lab_only)): - """A lab recording scored under each GOP setting of lab.SWEEP (GOP_RATIO_POOL x GOP_VOWEL_WINDOW), nothing kept. - The model runs once; each setting scores its posteriors against the native baseline rebuilt under it. - {"id", "results": {setting: what the benchmark reads (lab.compact)}}; the page sends them to POST /api/lab/sweep.""" - try: - row = a.lab_sample(sample_id) - found = a.lab_audio(sample_id) if row else None - except AuthUnavailable as e: - raise lab_error(e) - phrase = lab.sample_phrase(row) if row else None - if phrase is None: - raise HTTPException(404, "no such lab recording") - if found is None: - raise HTTPException(404, "להקלטה הזו אין קובץ שמור") - try: - wav = decode_audio(found[0]) - except ValueError: - raise HTTPException(400, "could not decode audio") - base = app.state.scorer - logp = base.emit(wav) - energy, duration = frame_energy(wav, len(logp)), len(wav) / SAMPLE_RATE - results = {} - for setting, (pool, window) in lab.SWEEP_KEYS.items(): - scorer = base.with_gop(ratio_pool=pool, vowel_window=window) - refs = sweep_references(scorer, phrase, setting) - results[setting] = lab.compact(scorer.score_logp(logp, phrase, refs, False, duration, energy)) - return {"id": sample_id, "results": results} - - @app.post("/api/lab/sweep") - def lab_sweep_results(body: dict = Body(...), a: SupabaseAuth = Depends(lab_only)): - """The benchmark of every setting from the sweeps of the recordings ({"results": {sample id: {setting: result}}}): - {"settings": [{"key", "pool", "window", "quality", "partial", "totals", "vowels" (lab.vowel_stats), - "changed": how many sound scores differ from max|wide's, "sounds"}] best first, "current": the server's own - setting, "recordings", "model", "version"}. `quality` is lab.quality (balanced accuracy).""" - given = body.get("results") - if not isinstance(given, dict) or len(given) > 5000: - raise HTTPException(400, "invalid results") - try: - results = {int(k): v for k, v in given.items() if isinstance(v, dict)} - except ValueError: - raise HTTPException(400, "invalid results") - try: - samples = [s for s in a.lab_samples(full=True) if s["id"] in results] - except AuthUnavailable as e: - raise lab_error(e) - keep = ("recordings", "unrelated", "right", "false_flags", "planted", "caught", "frr", "detection", "good_share") - ref = lab.sweep_key("max", None) # the scoring before pooling and windows: what each setting changed of it - scores = lambda r: [p.get("score") for w in r.get("words", []) for p in w.get("phonemes", [])] - settings = [] - for setting, (pool, window) in lab.SWEEP_KEYS.items(): - per = {i: r[setting] for i, r in results.items() if isinstance(r.get(setting), dict)} - mine = [s for s in samples if s["id"] in per] - totals = lab.benchmark(mine, per)["totals"] - settings.append({"key": setting, "pool": pool, "window": "wide" if window is None else window, - "quality": lab.quality(totals), "partial": lab.quality_partial(totals), - "totals": {k: totals[k] for k in keep}, "vowels": lab.vowel_stats(mine, per), - # the sounds this setting scored otherwise than `ref`, of the sounds both scored - "changed": sum(a != b for i, r in results.items() if isinstance(r.get(ref), dict) - and i in per for a, b in zip(scores(per[i]), scores(r[ref]))), - "sounds": sum(len(scores(per[i])) for i in per)}) - # by quality; its ties (the flags rarely see a vowel) by how well the vowels separate, then how few are weak - settings.sort(key=lambda r: (r["quality"] is None, -(r["quality"] or 0), -(r["vowels"]["auc"] or 0), - r["vowels"]["weak"] if r["vowels"]["weak"] is not None else 1, - -(r["vowels"]["mean"] or 0))) - g = app.state.scorer.cfg.gop - return {"settings": settings, "current": lab.sweep_key(g.ratio_pool, g.vowel_window), "recordings": len(samples), - "model": MODEL_ID, "version": build_version()} - - @app.post("/api/lab/results") - def lab_rescored_results(body: dict = Body(...), a: SupabaseAuth = Depends(lab_only)): - """The benchmark of rescored recordings ({"results": {sample id: result}}, from the rescore above), beside the - benchmark of the same recordings as they were stored: {"now", "stored", "model", "version"}.""" - given = body.get("results") - if not isinstance(given, dict) or len(given) > 5000: - raise HTTPException(400, "invalid results") - try: - results = {int(k): v for k, v in given.items() if isinstance(v, dict)} - except ValueError: - raise HTTPException(400, "invalid results") - try: - samples = [s for s in a.lab_samples(full=True) if s["id"] in results] - except AuthUnavailable as e: - raise lab_error(e) - try: - reports = a.list_flags("confirmed") - except AuthUnavailable as e: - log.warning("confirmed flags not read for the lab: %s", e) - reports = [] - cfg = getattr(app.state.scorer, "cfg", None) # the env, not the code, picks the pool: the version cannot tell - return {"now": lab.benchmark(samples, results, reports), "stored": lab.benchmark(samples, reports=reports), - "model": MODEL_ID, "version": build_version(), - "pool": cfg.gop.ratio_pool if cfg else None, - "vowel_window": ("wide" if cfg.gop.vowel_window is None else f"±{cfg.gop.vowel_window}") if cfg else None} - - @app.post("/api/lab/flags/{flag_id}/rescore") - def lab_rescore_flag(flag_id: int, a: SupabaseAuth = Depends(lab_only)): - """A flagged recording scored again by the current scorer, against what was kept with the flag: {"old", - "new": score and flagged sounds, "issues": the sounds a teacher marked as wrongly flagged, each with - `still` (flagged again), "result": the new result with its trace}.""" - try: - row = a.flag(flag_id) - found = a.flag_audio(flag_id) if row else None - except AuthUnavailable as e: - raise HTTPException(503, f"Supabase לא זמין: {e}") - if row is None: - raise HTTPException(404, "no such flag") - if found is None: - raise HTTPException(404, "לדיווח הזה אין הקלטה שמורה") - try: - wav = decode_audio(found[0]) - except ValueError: - raise HTTPException(400, "could not decode audio") - phrase = lab.flag_phrase(row) - key = next((i for i, p in enumerate(PHRASES) if p is phrase), ("flag", phrase.get("key"))) - refs = app.state.refs.get(key, phrase) if app.state.refs else [] - result = app.state.scorer.score(wav, phrase, refs, diagnostics=True) - if result.get("error"): - raise HTTPException(400, result["message"]) - old = row.get("result") or {} - return {"old": {"score": row.get("score"), "flagged": lab.flagged_sounds(old)}, - "new": {"score": result["score"], "flagged": lab.flagged_sounds(result)}, - "issues": [{**i, "still": lab.still_flagged(result, i)} for i in row.get("false_issues") or []], - "result": result} - - def calibration_error(e: AuthUnavailable) -> HTTPException: - if "answered 404" in str(e): - return HTTPException(503, f"הטבלה calibrations לא קיימת. הריצו ב-SQL Editor: {CREATE_CALIBRATIONS}") - return HTTPException(503, f"כללי הכיול לא זמינים: {e}") - - def reload_calibrations(a: SupabaseAuth) -> None: - rows = a.calibrations("applied") - app.state.scorer.set_calibrations([r for r in rows if not r.get("model") or r.get("model") == MODEL_ID]) - - def reload_scoring_rules(a: SupabaseAuth) -> None: - app.state.scorer.set_disabled(r["id"] for r in a.scoring_rules() if not r.get("active", True)) - - def scoring_rules_error(e: AuthUnavailable) -> HTTPException: - if "answered 404" in str(e): - return HTTPException(503, f"הטבלה scoring_rules לא קיימת. הריצו ב-SQL Editor: {CREATE_SCORING_RULES}") - return HTTPException(503, f"כללי ההגייה לא זמינים: {e}") - - @app.get("/api/scoring-rules") - def scoring_rules(a: SupabaseAuth = Depends(teacher_only)): - """Every rule taken from pronunciation-rules.md, with whether it is on. All are on until a teacher says otherwise. - `stored` is False while the table is missing: the list still shows, but cannot be changed.""" - s: Scorer = app.state.scorer - stored, rows = True, {} - try: - rows = {r["id"]: r for r in a.scoring_rules()} - except AuthUnavailable as e: - if "answered 404" not in str(e): - raise scoring_rules_error(e) - stored = False - detectable = check_detectable(s.rules, lambda ph: s.vocab.sound_id(ph) is not None, s.vocab.model_id) - return {"stored": stored, "create_sql": None if stored else CREATE_SCORING_RULES, "rules": [ - {"id": t.id, "group": t.group, "section": t.section, "title": t.title, "description": t.description, - "active": t.id not in s.disabled, - "updated_by": rows.get(t.id, {}).get("updated_by", ""), "updated_at": rows.get(t.id, {}).get("updated_at"), - # a section-3 rule this model cannot hear at all (vowel length): switching it changes nothing - "detectable": detectable.get(t.id.removeprefix("rule_"), {}).get("detectable", True)} - for t in TOGGLES]} - - @app.put("/api/scoring-rules/{rule_id}") - def switch_scoring_rule(rule_id: str, body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), - user: dict | None = Depends(signed_in)): - if rule_id not in {t.id for t in TOGGLES} or not isinstance(body.get("active"), bool): - raise HTTPException(400, "unknown rule or invalid switch") - try: - a.set_scoring_rule(rule_id, body["active"], user["email"].lower()) - reload_scoring_rules(a) - except AuthUnavailable as e: - raise scoring_rules_error(e) - return {"ok": True, "disabled": sorted(app.state.scorer.disabled)} - - @app.get("/api/calibrations") - def calibration_proposals(a: SupabaseAuth = Depends(teacher_only)): - try: - return {"calibrations": a.calibrations()} - except AuthUnavailable as e: - raise calibration_error(e) - - @app.get("/api/attempt-patterns") - def attempt_patterns(a: SupabaseAuth = Depends(teacher_only)): - """Repeated phoneme judgements among students, aggregated without returning identities or recordings.""" - try: - rows = a.all_attempt_issues() - except AuthUnavailable as e: - raise HTTPException(503, f"דפוסי התלמידים לא זמינים: {e}") - if rows is None: - raise HTTPException(503, f"לטבלה attempts חסרות עמודות האבחון. הריצו ב-SQL Editor: {MIGRATE_ATTEMPT_ISSUES}") - phrase_students: dict[str, set[str]] = {} - patterns: dict[tuple[str, str, str, str, str], dict] = {} - for row in rows: - if row.get("model") and row.get("model") != MODEL_ID: - continue - phrase, student = str(row.get("phrase", "")), str(row.get("user_id", "")) - phrase_students.setdefault(phrase, set()).add(student) - seen = set() - for issue in row.get("issues") or []: - if not isinstance(issue, dict): - continue - expected, heard = str(issue.get("expected", ""))[:20], str(issue.get("heard", ""))[:20] - pair = (expected, heard) - if not expected or not heard or pair in seen: - continue - seen.add(pair) - key = (phrase, expected, heard, str(row.get("model", "")), str(row.get("version", ""))) - p = patterns.setdefault(key, {"phrase": phrase, "expected": expected, "heard": heard, - "model": key[3], "version": key[4], "attempts": 0, - "students": set(), "scores": []}) - p["attempts"] += 1 - p["students"].add(student) - p["scores"].append(row.get("score", 0)) - result = [] - for p in patterns.values(): - students = len(p.pop("students")); scores = p.pop("scores"); total = len(phrase_students.get(p["phrase"], ())) - result.append({**p, "students": students, "student_rate": round(students / total, 2) if total else 0, - "average_score": round(sum(scores) / len(scores)) if scores else 0}) - result.sort(key=lambda p: (p["students"], p["attempts"]), reverse=True) - return {"patterns": result} - - @app.post("/api/calibrations") - def propose_calibration(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), - user: dict | None = Depends(signed_in)): - expected, heard = str(body.get("expected", ""))[:20], str(body.get("heard", ""))[:20] - evidence = body.get("evidence", {}) - if not expected or not heard or not isinstance(evidence, dict): - raise HTTPException(400, "invalid calibration proposal") - row = {"expected": expected, "heard": heard, "floor": 70, "scope": "global", "phrase": "", - "evidence": evidence, "model": MODEL_ID, "status": "draft", "proposed_by": user["email"].lower()} - try: - return {"calibration": a.add_calibration(row)} - except AuthUnavailable as e: - raise calibration_error(e) - - @app.put("/api/calibrations/{calibration_id}") - def decide_calibration(calibration_id: int, body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), - user: dict | None = Depends(signed_in)): - status, scope, phrase = body.get("status"), body.get("scope", "global"), str(body.get("phrase", ""))[:200] - floor = body.get("floor", 70) - if status not in CALIBRATION_STATUSES or scope not in {"global", "phrase"} or isinstance(floor, bool): - raise HTTPException(400, "invalid calibration decision") - try: - floor = float(floor) - except (TypeError, ValueError): - raise HTTPException(400, "invalid calibration floor") - if not 0 <= floor <= 100 or scope == "phrase" and not phrase: - raise HTTPException(400, "invalid calibration scope or floor") - values = {"status": status, "scope": scope, "phrase": phrase if scope == "phrase" else "", "floor": floor, - "reviewed_by": user["email"].lower(), "reviewed_at": datetime.now(timezone.utc).isoformat()} - try: - a.update_calibration(calibration_id, values) - reload_calibrations(a) - except AuthUnavailable as e: - raise calibration_error(e) - return {"ok": True, "active": len(app.state.scorer.calibrations)} - - @app.delete("/api/calibrations/{calibration_id}") - def delete_calibration(calibration_id: int, a: SupabaseAuth = Depends(teacher_only)): - """Deletes a draft or applied calibration rule and immediately refreshes the scorer's active rules.""" - try: - if not a.delete_calibration(calibration_id): - raise HTTPException(404, "calibration proposal not found") - reload_calibrations(a) - except AuthUnavailable as e: - raise calibration_error(e) - return {"ok": True, "active": len(app.state.scorer.calibrations)} - - def class_store_error(e: AuthUnavailable) -> str: - """The teacher's message when a class cannot be written; a 404 means Supabase has no `classes` table.""" - if "answered 404" in str(e): - return ("הטבלה classes לא קיימת ב-Supabase. צרו אותה ב-SQL Editor (הפקודה ב-README); " - "אם כבר נוצרה, הריצו שם: notify pgrst, 'reload schema';") - if any(col in str(e) for col in ("nicknames", "lessons", "homework", "'due'", "dailies", "subject", "teachers")): # a table made before them - return f"לטבלה classes חסרות עמודות. הריצו ב-SQL Editor של Supabase: {MIGRATE_CLASSES}" - return f"Supabase לא זמין: {e}" - - def class_subject(a: SupabaseAuth, id: int | None, body: dict, actor: dict | None) -> str: - """The subject of a class being saved: a stored class keeps its own; a new one takes its admin's, and one - opened by a developer (who has none) the subject the page asks for ("subject", as SUBJECT_LABELS spells it).""" - if id is not None: - try: - stored = next((c for c in managed_classes(a, actor, fresh=True) if c["id"] == id), None) - except AuthUnavailable as e: - raise HTTPException(503, class_store_error(e)) - return str((stored or {}).get("subject") or "").strip() - own = str((actor or {}).get("subject") or "").strip() - asked = " ".join(str(body.get("subject") or "").split()) - return own or (asked if subject_code(asked) else "") + @app.get("/api/math/skills") + async def math_skills(): + return algebra9() - def save_class(a: SupabaseAuth, id: int | None, body: dict, actor: dict | None = None) -> dict: - """Checks a class from the panel ({"name", "emails": the student list, see `class_members`, "lessons": lesson - ids, "homeworks": [{"topic": a topic of one of them, "due": its ISO date, or ""}], each topic once; or, as - before a class could have several, "homework" and "due" for a single one; "dailies": [{"video": the id of - a daily video, "date": the ISO date the class watches it, or "" for none}], each video once; without it - the class's daily videos stay as they are) and stores it. A student may be in several classes, e.g. a beginners' class and an advanced one.""" - try: - name = normalize_class_name(body.get("name")) - except ClassManagementError as e: - raise HTTPException(400, str(e)) - try: - emails, nicks = class_members(str(body.get("emails", ""))) - except ValueError as e: - raise HTTPException(400, f"בשורה הזו אין כתובת מייל: {e}") - subject = class_subject(a, id, body, actor) - math = subject_code(subject) == "math" # a math class studies units of the math path, and has no homework or daily videos yet - known = [u["id"] for u in algebra9()["units"]] if math else ALL_LESSONS - lessons = body.get("lessons") - if not isinstance(lessons, list) or not lessons or not set(map(str, lessons)) <= set(known): - raise HTTPException(400, "בחרו לפחות יחידה אחת לכיתה" if math else "בחרו לפחות שיעור אחד לכיתה") - lessons = [l for l in known if l in map(str, lessons)] - teachers = body.get("teachers") - if teachers is not None: - if not isinstance(teachers, list) or not all(isinstance(e, str) for e in teachers): - raise HTTPException(400, "רשימת המורים אינה תקינה") - teachers = list(dict.fromkeys(e.strip().lower() for e in teachers if e.strip())) - if not set(teachers) <= set(emails): - raise HTTPException(400, "מורה של הכיתה צריך להיות אחד מהתלמידים הרשומים בה") - listed = [] if math else body.get("homeworks") - if listed is None: - listed = [{"topic": body.get("homework"), "due": body.get("due")}] - if not isinstance(listed, list) or len(listed) > MAX_HOMEWORKS or not all(isinstance(h, dict) for h in listed): - raise HTTPException(400, "שיעורי הבית לא תקינים") - homeworks, lesson_of = [], topics() - for h in listed: - topic, due = str(h.get("topic") or ""), str(h.get("due") or "") - if not topic: # no date without homework - continue - if lesson_of.get(topic) not in lessons: - raise HTTPException(400, "שיעורי הבית הם נושא מאחד משיעורי הכיתה") - if any(topic == was["topic"] for was in homeworks): - raise HTTPException(400, "אותו נושא מופיע פעמיים בשיעורי הבית") - try: - homeworks.append({"topic": topic, "due": date.fromisoformat(due).isoformat() if due else None}) - except ValueError: - raise HTTPException(400, "התאריך של שיעורי הבית לא תקין") - dailies = None if math else body.get("dailies") - if dailies is not None: - if not isinstance(dailies, list) or len(dailies) > MAX_DAILIES or not all(isinstance(d, dict) for d in dailies): - raise HTTPException(400, "הסרטונים היומיים לא תקינים") - known, days = {v["id"] for v in daily_rows()}, {} - for d in dailies: - day = str(d.get("date") or "") - if not day: # a video without a day is not shown to the class - continue - if not isinstance(d.get("video"), int) or d["video"] not in known or d["video"] in days: - raise HTTPException(400, "סרטון יומי שאינו בספרייה, או שמופיע פעמיים. רעננו ונסו שוב") - try: - days[d["video"]] = date.fromisoformat(day).isoformat() - except ValueError: - raise HTTPException(400, "התאריך של הסרטון היומי לא תקין") - dailies = [{"video": v, "date": day} for v, day in days.items()] - try: - # a class's name is its own: the same words in other letter case or spacing are the same name - same = lambda other: " ".join(other.split()).casefold() == name.casefold() - visible = managed_classes(a, actor, fresh=True) - if id is not None and not any(c["id"] == id for c in visible): - raise HTTPException(404, "הכיתה לא נמצאה") - if taken := next((c["name"] for c in visible if c["id"] != id and same(c["name"])), None): - raise HTTPException(409, f"כבר יש כיתה בשם {taken}") - # a class that has no daily videos, and had none, is saved without them: a table made before them takes it - had = any(c["id"] == id and c.get("dailies") for c in visible) - if (id is None and hasattr(a, "access_for") and actor and "admin" in actor.get("roles", ()) - and "developer" not in actor.get("roles", ()) and not subject): - raise HTTPException(409, "לא הוגדר מקצוע למנהל/ת. מפתח/ת צריך להגדיר אותו בניטור.") - args = (id, name, emails, nicks, lessons, homeworks, dailies if dailies or had else None, subject) - if teachers is None: - a.save_class(*args) - else: - a.save_class(*args, teachers=teachers) - except ClassConflict: - raise HTTPException(409, f"כבר יש כיתה בשם {name}") - except KeyError: - raise HTTPException(404, "הכיתה לא נמצאה") - except AuthUnavailable as e: - raise HTTPException(503, class_store_error(e)) - return {"ok": True} + @app.get("/api/classes") + async def classes(_user: dict = Depends(manager)): + try: + return {"classes": [{**row, "lessons": units_of(row)} + for row in app.state.auth.classes(fresh=True)]} + except AuthUnavailable as exc: + raise HTTPException(503, str(exc)) from exc + + def save_class(class_id: int | None, body: dict): + name = " ".join(str(body.get("name") or "").split()) + if not name or len(name) > 80: + raise HTTPException(400, "class name is required (up to 80 characters)") + known = [unit["id"] for unit in algebra9()["units"]] + selected = [unit for unit in known if unit in set(body.get("lessons") or known)] + if not selected: + raise HTTPException(400, "choose at least one math unit") + raw = body.get("emails", "") + parts = raw.replace(",", " ").split() if isinstance(raw, str) else raw + emails = sorted({str(part).strip().lower() for part in parts if str(part).strip()}) + try: + app.state.auth.save_class(class_id, name, emails, {}, selected, + teachers=body.get("teachers") or []) + except ClassConflict as exc: + raise HTTPException(409, "a class with that name already exists") from exc + except AuthUnavailable as exc: + raise HTTPException(503, str(exc)) from exc + except KeyError as exc: + raise HTTPException(404, "class not found") from exc + return {"ok": True, "id": class_id, "name": name, "lessons": selected} @app.post("/api/classes") - def add_class(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), actor: dict | None = Depends(signed_in)): - return save_class(a, None, body, actor) + async def create_class(body: dict, _user: dict = Depends(manager)): + return save_class(None, body) @app.put("/api/classes/{class_id}") - def edit_class(class_id: int, body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), actor: dict | None = Depends(signed_in)): - return save_class(a, class_id, body, actor) + async def update_class(class_id: int, body: dict, _user: dict = Depends(manager)): + return save_class(class_id, body) @app.delete("/api/classes/{class_id}") - def delete_class(class_id: int, a: SupabaseAuth = Depends(teacher_only), actor: dict | None = Depends(signed_in)): - try: - if not any(c["id"] == class_id for c in managed_classes(a, actor, fresh=True)): - raise HTTPException(404, "הכיתה לא נמצאה") - a.delete_class(class_id) - except HTTPException: - raise - except AuthUnavailable as e: - raise HTTPException(503, class_store_error(e)) - return {"ok": True} - - @app.get("/api/videos", dependencies=[Depends(require("path.use"))]) - def videos(user: dict | None = Depends(signed_in)): - """The lesson videos, each lesson's in its order on the path: id, lesson, title, url (the Vimeo or YouTube link, for a teacher only), embed - (its player, for the page's frame) and pos (how many of the lesson's own topics stand before it, see - `placed`) and ord. A student gets those of their classes' lessons; a teacher, and an account in no - class, every one. One whose lesson the course no longer has is left out.""" - if not user: - return {"videos": []} - mine = user["classes"] - lessons = READY_LESSONS if not mine or user["teacher"] else \ - [l for l in READY_LESSONS if any(l in lessons_of(c) for c in mine)] - try: - rows = app.state.auth.videos() - except AuthUnavailable as e: - raise HTTPException(503, f"videos not available: {e}") - # the link itself goes to teachers only: a student's page gets the player's address, which it cannot do without - return {"videos": [{"id": v["id"], "lesson": v["lesson"], "title": v["title"], - **({"url": v["url"]} if user["teacher"] else {}), - "embed": video_embed(v["url"]), "pos": v["pos"], "ord": v["ord"]} - for l in lessons for v in placed(rows, l)]} - - def video_store_error(e: AuthUnavailable) -> str: - """The teacher's message when a video cannot be written; a 404 means Supabase has no `videos` table.""" - if "answered 404" in str(e): - return ("הטבלה videos לא קיימת ב-Supabase. הריצו ב-SQL Editor: " - f"{CREATE_VIDEOS} אם כבר נוצרה, הריצו שם: notify pgrst, 'reload schema';") - if "'pos'" in str(e) or "'ord'" in str(e) or "pos" in str(e) and "column" in str(e): # a table made before the places - return f"לטבלה videos חסרות עמודות. הריצו ב-SQL Editor של Supabase: {MIGRATE_VIDEOS}" - return f"Supabase לא זמין: {e}" - - @app.post("/api/videos") - def add_video(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), - user: dict | None = Depends(signed_in)): - """Adds a video to a lesson, after its topics and the videos it has: {"lesson": the id of a ready lesson, - "title", "url": a Vimeo or YouTube link}. The teacher then moves it to its place (see `order_videos`).""" - lesson, title = str(body.get("lesson", "")), " ".join(str(body.get("title", "")).split()) - if lesson not in READY_LESSONS: - raise HTTPException(400, "סרטון מוסיפים לשיעור מוכן") - if not title or len(title) > MAX_TITLE: - raise HTTPException(400, f"לסרטון צריך שם (עד {MAX_TITLE} תווים)") - url = video_link(str(body.get("url", ""))) - if not url: - raise HTTPException(400, "זה לא קישור לסרטון ב-Vimeo או ב-YouTube. הדביקו קישור כמו https://vimeo.com/1233611297/d46ac373c8 או https://youtu.be/dQw4w9WgXcQ") - try: - a.add_video(lesson, title, url, LESSON_TOPICS[lesson], next_ord(a, lesson)) - except AuthUnavailable as e: - raise HTTPException(503, video_store_error(e)) - a.log_event("content", user, title, {"type": "video", "lesson": lesson}) - return {"ok": True} - - def next_ord(a: SupabaseAuth, lesson: str) -> int: - """The turn of a new video or text: after the lesson's topics and whatever was added to it before.""" - return max((r.get("ord") or 0 for r in a.videos(fresh=True) + a.texts(fresh=True) if r["lesson"] == lesson), default=0) + 1 - - @app.put("/api/content/order") - def order_content(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only)): - """Places what the teacher added to a lesson among its topics: {"lesson", "order": [{"kind": "video" or - "text", "id", "pos"}]}, every video and text of the lesson once, in their order on the path; `pos` is how - many of the lesson's own topics stand before it.""" - lesson, order = str(body.get("lesson", "")), body.get("order") - if lesson not in READY_LESSONS or not isinstance(order, list): - raise HTTPException(400, "unknown lesson") - try: - places = [(str(o["kind"]), int(o["id"]), int(o["pos"])) for o in order] - except (KeyError, TypeError, ValueError): - raise HTTPException(400, "each one needs a kind, an id and a place") - try: - mine = {("video", v["id"]) for v in a.videos(fresh=True) if v["lesson"] == lesson} | \ - {("text", t["id"]) for t in a.texts(fresh=True) if t["lesson"] == lesson} - if len(places) != len(mine) or {(kind, i) for kind, i, _ in places} != mine: # added or deleted meanwhile, in another tab - raise HTTPException(409, "תוכן השיעור השתנה. רעננו ונסו שוב") - if any(not 0 <= pos <= LESSON_TOPICS[lesson] for _, _, pos in places): - raise HTTPException(400, "no such place in the lesson") - a.place([(kind + "s", i, pos, k) for k, (kind, i, pos) in enumerate(places)]) - except AuthUnavailable as e: - raise HTTPException(503, text_store_error(e) if "texts" in str(e) else video_store_error(e)) - return {"ok": True} - - @app.get("/api/texts", dependencies=[Depends(require("path.use"))]) - def texts(user: dict | None = Depends(signed_in)): - """The teacher's texts as the page takes topics and phrases (see `/api/phrases`), for the lessons a video is - given for (see `videos`). "categories": a topic per text, laid out as a story: id ("t"), name, lesson, story, - text (the text's own id), voices (a dialogue's two, by name), and pos and ord as a video has them; a teacher - also gets "guessed", the words that were read by rule. "phrases": the lines, in each text's order.""" - if not user: - return {"categories": [], "phrases": []} - mine = user["classes"] - lessons = READY_LESSONS if not mine or user["teacher"] else \ - [l for l in READY_LESSONS if any(l in lessons_of(c) for c in mine)] - rows = [r for l in lessons for r in placed(text_rows(), l)] - return {"categories": [{"id": f"t{r['id']}", "name": r["title"], "lesson": r["lesson"], "story": True, "text": r["id"], - **({"voices": r["voices"]} if r["voices"] else {}), "pos": r["pos"], "ord": r["ord"], - **({"guessed": built(r)[1]} if user["teacher"] else {})} for r in rows], - "phrases": [{"id": i, "cat": p["cat"], "he": p["he"], "tr": p["tr"], "ar": p["variants"][0][0], - "ar_pointed": point_arabic(*p["variants"][0]), "seq": p["seq"]} - for i, p in text_bank(rows).items()]} - - def text_store_error(e: AuthUnavailable) -> str: - """The teacher's message when a text cannot be written; a 404 means Supabase has no `texts` table.""" - if "answered 404" in str(e): - return ("הטבלה texts לא קיימת ב-Supabase. הריצו ב-SQL Editor: " - f"{CREATE_TEXTS} אם כבר נוצרה, הריצו שם: notify pgrst, 'reload schema';") - return f"Supabase לא זמין: {e}" - - @app.post("/api/texts") - def add_text(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), - user: dict | None = Depends(signed_in)): - """Adds a text to a lesson, after its topics and what was added before: {"lesson": the id of a ready lesson, - "title", "he": its lines in Hebrew, "tr": the same lines in Arabic written in Hebrew letters, "voices": a - dialogue's two voices by name (the odd lines', the even lines'); none for a story, which the student's own - voice reads}. Answers with "guessed": the words the course does not have, which were read by rule.""" - lesson, title = str(body.get("lesson", "")), " ".join(str(body.get("title", "")).split()) - if lesson not in READY_LESSONS: - raise HTTPException(400, "טקסט מוסיפים לשיעור מוכן") - if not title or len(title) > MAX_TITLE: - raise HTTPException(400, f"לטקסט צריך שם (עד {MAX_TITLE} תווים)") - voices = body.get("voices") or [] - if not isinstance(voices, list) or voices and (len(voices) != 2 or not set(map(str, voices)) <= set(VOICE_NAMES)): - raise HTTPException(400, "לשיחה בוחרים שני קולות מהרשימה") - he, tr = ([" ".join(str(x).split()) for x in body.get(k) or []] if isinstance(body.get(k), list) else [] for k in ("he", "tr")) - if not he or not tr: - raise HTTPException(400, "צריך את השורות פעמיים: בעברית, ובערבית בתעתיק עברי") - if len(he) != len(tr): - raise HTTPException(400, f"מספר השורות לא זהה: {len(he)} בעברית ו-{len(tr)} בערבית") - if len(he) > MAX_LINES or any(len(x) > MAX_LINE for x in he + tr): - raise HTTPException(400, f"טקסט הוא עד {MAX_LINES} שורות, ושורה עד {MAX_LINE} תווים") - for n, (h, t) in enumerate(zip(he, tr), 1): - if not h or not read_line(t)[0]: - raise HTTPException(400, f"בשורה {n} חסר טקסט, או שבערבית שלה אין אותיות עבריות") - try: - a.add_text(lesson, title, list(map(str, voices)), [[h, t] for h, t in zip(he, tr)], LESSON_TOPICS[lesson], next_ord(a, lesson)) - except AuthUnavailable as e: - raise HTTPException(503, text_store_error(e)) - a.log_event("content", user, title, {"type": "text", "lesson": lesson}) - return {"ok": True, "guessed": text_phrases("", [[h, t] for h, t in zip(he, tr)])[1]} - - @app.delete("/api/texts/{text_id}") - def delete_text(text_id: int, a: SupabaseAuth = Depends(teacher_only)): - try: - a.delete_text(text_id) - except AuthUnavailable as e: - raise HTTPException(503, text_store_error(e)) - return {"ok": True} - - @app.delete("/api/videos/{video_id}") - def delete_video(video_id: int, a: SupabaseAuth = Depends(teacher_only)): - try: - a.delete_video(video_id) - except AuthUnavailable as e: - raise HTTPException(503, video_store_error(e)) - return {"ok": True} - - def level_store_error(table: str, e: AuthUnavailable) -> str: - """The teacher's message when a daily or library video cannot be written; a 404 means Supabase has no such table.""" - if "answered 404" in str(e): - return (f"הטבלה {table} לא קיימת ב-Supabase. הריצו ב-SQL Editor: " - f"{CREATE_LEVEL_VIDEOS[table]} אם כבר נוצרה, הריצו שם: notify pgrst, 'reload schema';") - if "'ord'" in str(e) or "ord" in str(e) and "column" in str(e): # a library made before its videos could be moved - return f"לטבלה {table} חסרה עמודה. הריצו ב-SQL Editor של Supabase: {MIGRATE_LIBRARY} notify pgrst, 'reload schema';" - return f"Supabase לא זמין: {e}" - - @app.get("/api/dailies") - def dailies(a: SupabaseAuth = Depends(teacher_only)): - """The library of daily videos, for the panel: id, level, title, url and embed (its player) of each.""" - try: - rows = a.level_videos("dailies") - except AuthUnavailable as e: - raise HTTPException(503, f"daily videos not available: {e}") - return {"dailies": [{"id": v["id"], "level": v["level"], "title": v["title"], "url": v["url"], - "embed": video_embed(v["url"])} for v in rows if v["level"] in LEVEL_IDS]} - - def push_on(): - p = getattr(app.state, "push", None) - if not p or not p.on: - raise HTTPException(503, f"push reminders are off: {p.why if p else 'no sign-in'}") - return p - - @app.get("/api/push", dependencies=members) - def push_info(): - """The server's push key, for the page to subscribe with; empty when the reminders are off.""" - p = getattr(app.state, "push", None) - return {"key": p.public if p and p.on else "", "hour": p.hour if p else None} - - @app.post("/api/push/subscribe") - def push_subscribe(body: dict = Body(...), user: dict | None = Depends(signed_in)): - """Keeps this browser's subscription ({"endpoint", "keys": {"p256dh", "auth"}}) for the daily video's reminder.""" - p = push_on() - try: - p.save(user["email"], body, bool(user.get("teacher"))) - except ValueError as e: - raise HTTPException(400, str(e)) - except AuthUnavailable as e: - raise HTTPException(503, str(e)) - return {"ok": True} - - @app.post("/api/push/unsubscribe") - def push_unsubscribe(body: dict = Body(...), user: dict | None = Depends(signed_in)): - p = push_on() - try: - p.delete(str(body.get("endpoint") or ""), user["email"]) - except AuthUnavailable as e: - raise HTTPException(503, str(e)) - return {"ok": True} - - @app.post("/api/push/test") - def push_test(user: dict | None = Depends(signed_in)): - """Sends the caller's devices a notification now: today's daily video, or a sample when there is none.""" - p = push_on() - try: - rows = p.mine(user["email"]) - videos = daily_today(user["email"], bool(user.get("teacher"))) - except AuthUnavailable as e: - raise HTTPException(503, str(e)) - message = pushes.payload(videos) if videos else {**pushes.payload([{"title": "ככה תיראה התזכורת לסרטון היומי"}]), - "title": "🔔 התזכורות פועלות"} - return {"devices": len(rows), "sent": sum(p.send(r, message) for r in rows)} - - def push_audience(a: SupabaseAuth, actor: dict | None, rows: list[dict]) -> tuple[dict[str, list[dict]], list[dict]]: - """An admin's reach: the subscriptions of each class they manage (its students and teachers), and those of - "everyone": every subscription for a developer, else those of their classes together.""" - by_class = {} - for c in managed_classes(a, actor, fresh=True): - people = set(c["emails"]) | {e.lower() for e in c.get("teachers") or []} - by_class[c["name"]] = [r for r in rows if r["email"] in people] - developer = not actor or "developer" in actor.get("roles", ()) or not hasattr(a, "access_for") - everyone = rows if developer else list({r["endpoint"]: r for v in by_class.values() for r in v}.values()) - return by_class, everyone - - @app.get("/api/push/audience") - def push_audience_counts(a: SupabaseAuth = Depends(admin_only), actor: dict | None = Depends(signed_in)): - """How many devices get an admin's message: {"all": n, "classes": {name: n}, "outside": the devices of "all" whose - account is in none of the classes (a developer's "all" has every subscription, also of teachers and accounts in no class)}.""" - p = push_on() - try: - by_class, everyone = push_audience(a, actor, p.every()) - except AuthUnavailable as e: - raise HTTPException(503, str(e)) - inside = {r["endpoint"] for v in by_class.values() for r in v} - return {"all": len(everyone), "classes": {name: len(v) for name, v in by_class.items()}, - "outside": sum(r["endpoint"] not in inside for r in everyone), - "people": push_people(a, actor, everyone)} - - def push_people(a: SupabaseAuth, actor: dict | None, rows: list[dict]) -> list[dict]: - """Who turned the notifications on: [{"email", "name" (a class's nickname, else the Google name), "devices", - "classes": the names of the admin's classes they are in}], by name.""" - classes = managed_classes(a, actor, fresh=True) - try: - google = {u["email"].lower(): u["name"] for u in a.users()} - except (AuthUnavailable, AttributeError): - google = {} - devices: dict[str, int] = {} - for r in rows: - devices[r["email"]] = devices.get(r["email"], 0) + 1 - people = [] - for email, n in devices.items(): - mine = [c for c in classes if email in c["emails"] or email in (c.get("teachers") or [])] - nick = next((c.get("nicknames", {}).get(email) for c in mine if c.get("nicknames", {}).get(email)), "") - people.append({"email": email, "name": nick or google.get(email, ""), "devices": n, - "classes": [c["name"] for c in mine]}) - return sorted(people, key=lambda x: ((x["name"] or x["email"]).lower(), x["email"])) - - @app.post("/api/push/broadcast") - def push_broadcast(body: dict = Body(...), a: SupabaseAuth = Depends(admin_only), actor: dict | None = Depends(signed_in)): - """An admin's notification ({"title", "body", "class": a class's name, or "" for everyone}) sent now.""" - p = push_on() - try: - msg = pushes.message(str(body.get("title") or ""), str(body.get("body") or "")) - except ValueError as e: - raise HTTPException(400, str(e)) - target = str(body.get("class") or "") - try: - by_class, everyone = push_audience(a, actor, p.every()) - except AuthUnavailable as e: - raise HTTPException(503, str(e)) - if target and target not in by_class: - raise HTTPException(404, "no such class") - rows = by_class[target] if target else everyone - sent = p.send_many(rows, msg) - a.log_event("push", actor, f"{msg['title']} → {target or 'כולם'}", {"class": target, "devices": len(rows), "sent": sent}) - return {"devices": len(rows), "sent": sent} - - @app.get("/api/library", dependencies=[Depends(require("library.view"))]) - def library(user: dict | None = Depends(signed_in)): - """The video library, which stands outside the path and is one for every level and class: id, title, url - (for a teacher only) and embed (its player) of each, in the teacher's order (see `order_library`).""" - if not user: - return {"library": []} - try: - rows = app.state.auth.level_videos("library") - except AuthUnavailable as e: - raise HTTPException(503, f"library not available: {e}") - return {"library": [{"id": v["id"], "title": v["title"], **({"url": v["url"]} if user["teacher"] else {}), - "embed": video_embed(v["url"])} for v in rows]} - - def add_level_video(table: str, body: dict, a: SupabaseAuth, user: dict | None) -> dict: - # a daily video is a level's; the library is one for every level (its `level` is empty) - level, title = str(body.get("level", "")) if table == "dailies" else "", " ".join(str(body.get("title", "")).split()) - if table == "dailies" and level not in LEVEL_IDS: - raise HTTPException(400, "unknown level") - if not title or len(title) > MAX_TITLE: - raise HTTPException(400, f"לסרטון צריך שם (עד {MAX_TITLE} תווים)") - url = video_link(str(body.get("url", ""))) - if not url: - raise HTTPException(400, "זה לא קישור לסרטון ב-Vimeo או ב-YouTube. הדביקו קישור כמו https://vimeo.com/1233611297/d46ac373c8 או https://youtu.be/dQw4w9WgXcQ") - try: - a.add_level_video(table, level, title, url) - except AuthUnavailable as e: - raise HTTPException(503, level_store_error(table, e)) - a.log_event("content", user, title, {"type": table, "level": level}) - return {"ok": True} - - def delete_level_video(table: str, id: int, a: SupabaseAuth) -> dict: - try: - a.delete_level_video(table, id) - except AuthUnavailable as e: - raise HTTPException(503, level_store_error(table, e)) - return {"ok": True} - - @app.post("/api/dailies") - def add_daily(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), - user: dict | None = Depends(signed_in)): - """Adds a video to a level's daily videos: {"level": a level id, "title", "url": a Vimeo or YouTube link}. - A class then gives it its day (see `save_class`).""" - return add_level_video("dailies", body, a, user) - - @app.delete("/api/dailies/{daily_id}") - def delete_daily(daily_id: int, a: SupabaseAuth = Depends(teacher_only)): - return delete_level_video("dailies", daily_id, a) - - @app.post("/api/library") - def add_library(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only), - user: dict | None = Depends(signed_in)): - """Adds a video to the library: {"title", "url": a Vimeo or YouTube link}.""" + async def delete_class(class_id: int, _user: dict = Depends(manager)): try: - title = video_library.normalize_title(body.get("title", "")) - url = video_library.normalize_url(body.get("url", ""), video_link) - video_library.add(a, title, url, reject_duplicate=False) - except video_library.LibraryError as e: - if isinstance(e, video_library.LibraryUnavailable): - raise HTTPException(503, level_store_error("library", AuthUnavailable(str(e)))) - detail = str(e) - if detail == "זה לא קישור לסרטון ב-Vimeo או ב-YouTube": - detail += ". הדביקו קישור כמו https://vimeo.com/1233611297/d46ac373c8 או https://youtu.be/dQw4w9WgXcQ" - raise HTTPException(400, detail) - a.log_event("content", user, title, {"type": "library", "level": ""}) + app.state.auth.delete_class(class_id) + except AuthUnavailable as exc: + raise HTTPException(503, str(exc)) from exc return {"ok": True} - @app.delete("/api/library/{video_id}") - def delete_library(video_id: int, a: SupabaseAuth = Depends(teacher_only)): - return delete_level_video("library", video_id, a) + photo_counts: dict[tuple[str, str], int] = {} - @app.put("/api/library/order") - def order_library(body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only)): - """Puts the library's videos in the teacher's order: {"order": [id]}, every video of the library once.""" - order = body.get("order") - if not isinstance(order, list) or not all(isinstance(i, int) and not isinstance(i, bool) for i in order): - raise HTTPException(400, "the order is a list of video ids") - try: - if len(set(order)) != len(order) or set(order) != {v["id"] for v in a.level_videos("library", fresh=True)}: # added or deleted meanwhile, in another tab - raise HTTPException(409, "הספריה השתנתה. רעננו ונסו שוב") - a.order_level_videos("library", order) - except AuthUnavailable as e: - raise HTTPException(503, level_store_error("library", e)) - return {"ok": True} - - @app.put("/api/library/{video_id}") - def rename_library(video_id: int, body: dict = Body(...), a: SupabaseAuth = Depends(teacher_only)): - """Gives a video of the library another name: {"title"}.""" - try: - title = video_library.normalize_title(body.get("title", "")) - video_library.rename(a, video_id, title) - except video_library.LibraryUnavailable as e: - raise HTTPException(503, level_store_error("library", AuthUnavailable(str(e)))) - except video_library.LibraryError as e: - if str(e) == "הסרטון אינו נמצא בספרייה": - raise HTTPException(404, "הסרטון כבר לא בספריה. רעננו ונסו שוב") - raise HTTPException(400, str(e)) - return {"ok": True} - - @app.get("/api/classmates") - def classmates(user: dict | None = Depends(signed_in)): - """The signed-in student's classes: each one's name and its students, each by the nickname the teacher gave - in that class, or else the name Google gave (one with neither, who has not signed in yet, is only counted). - No addresses.""" - if not user or not user["classes"]: - return {"classes": []} - try: - google = {u["email"].lower(): u["name"] for u in app.state.auth.users()} - except AuthUnavailable as e: # the nicknames are still known - log.warning("classmates' names not read: %s", e) - google = {} - me, out = user["email"].lower(), [] - for c in user["classes"]: - named = [{"name": c["nicknames"].get(e) or google.get(e, ""), "me": e == me} for e in c["emails"]] - students = sorted((s for s in named if s["name"]), key=lambda s: s["name"]) - out.append({"name": c["name"], "students": students, "unnamed": len(named) - len(students)}) - return {"classes": out} - - class_weeks: dict[tuple, tuple[float, dict]] = {} # (class id, week) -> (when read, totals): a few minutes old at most - - @app.get("/api/class/week") - def class_week(user: dict | None = Depends(signed_in)): - """This week (from Monday, Jerusalem time) in each of the signed-in student's classes, as totals only: how many - sentences its students said, how many of them practised, a goal for the class (CLASS_WEEK_GOAL a student), - and how many the student said. Nobody else's own numbers, and no names.""" - if not user or not user["classes"]: - return {"classes": []} - a = app.state.auth - today = datetime.now(LOCAL_TZ).date() - monday = today - timedelta(days=today.weekday()) - since = datetime(monday.year, monday.month, monday.day, tzinfo=LOCAL_TZ) - try: - ids = {u["email"].lower(): u["id"] for u in a.users()} - except AuthUnavailable as e: - raise HTTPException(503, f"class week not available: {e}") - me = user["email"].lower() - out = [] - for c in user["classes"]: - teachers = {e.lower() for e in c.get("teachers") or []} - students = [e.lower() for e in c["emails"] if e.lower() not in teachers] - key, now = (c["id"], monday.isoformat()), datetime.now().timestamp() - cached = class_weeks.get(key) - if cached and now - cached[0] < CLASS_WEEK_TTL: - totals = cached[1] - else: - try: - summary = a.scoped_activity({ids[e] for e in students if e in ids}, since)["summary"] or {} - except AuthUnavailable as e: - raise HTTPException(503, f"class week not available: {e}") - totals = {"said": int(summary.get("attempts") or 0), "active": int(summary.get("active_students") or 0)} - class_weeks[key] = (now, totals) - try: - mine = (a.scoped_activity({ids[me]}, since)["summary"] or {}).get("attempts", 0) if me in ids else 0 - except AuthUnavailable: - mine = 0 - out.append({"name": c["name"], "students": len(students), **totals, "mine": int(mine or 0), - "goal": CLASS_WEEK_GOAL * max(1, len(students))}) - return {"classes": out} - - @app.get("/api/math/skills") - def math_skills(): - """The math path's skill graph (see app/math_skills.py). Public, as the phrase list is.""" - return algebra9() - - photo_counts: dict[tuple[str, str], int] = {} # (account, day) -> photos solved; in memory, so a restart resets it - - def photo_solver() -> PhotoSolver: - """The solver on the key in use now (the panel's, else the environment's): a key changed in the panel applies.""" - key = (app.state.keys.get("ANTHROPIC_API_KEY") or "").strip() + def solver() -> PhotoSolver: + key = os.environ.get("ANTHROPIC_API_KEY", "").strip() or os.environ.get("CLAUDE_API_KEY", "").strip() if not key: - raise HTTPException(503, "פתרון מתמונה לא מוגדר בשרת (חסר ANTHROPIC_API_KEY)") - if getattr(app.state, "photo_key", None) != key: - app.state.photo, app.state.photo_key = PhotoSolver(photo_client(key), math_model(), algebra9()["skills"]), key + raise HTTPException(503, "photographed-problem help is not configured") + if app.state.photo_key != key: + app.state.photo = PhotoSolver(photo_client(key), math_model(), algebra9()["skills"]) + app.state.photo_key = key return app.state.photo @app.post("/api/math/photo") - def math_photo(photo: UploadFile = File(...), user: dict | None = Depends(signed_in)): - """A photographed problem, solved by Claude (see app/math_photo.py): {readable, problem, steps: [{hint, work}], - answer, skill, note, left}. `left` is how many photos the account has left today (None: no limit). 429 past - DAILY_PHOTOS a day (developers and an open service are exempt), 502 when the model did not solve it.""" - solver = photo_solver() - day = datetime.now(LOCAL_TZ).date().isoformat() - who = (user["id"], day) if user and "developer" not in user.get("roles", ()) else None - if who and photo_counts.get(who, 0) >= DAILY_PHOTOS: - raise HTTPException(429, f"צילמתם {DAILY_PHOTOS} תרגילים היום. נתראה מחר!") + async def solve_photo(photo: UploadFile = File(...), user: dict | None = Depends(signed_in)): + service = solver() + email = (user or {}).get("email", "").lower() + exempt = "developer" in (user or {}).get("roles", ()) + counter = (email, datetime.now(LOCAL_TZ).date().isoformat()) if email and not exempt else None + if counter and photo_counts.get(counter, 0) >= DAILY_PHOTOS: + raise HTTPException(429, "daily photo limit reached") if photo.content_type not in PHOTO_TYPES: - raise HTTPException(415, "אפשר לשלוח תמונה מסוג JPEG,‏ PNG או WebP.") + raise HTTPException(415, "send a JPEG, PNG, or WebP image") image = photo.file.read(MAX_PHOTO + 1) if not image: - raise HTTPException(400, "התמונה ריקה.") + raise HTTPException(400, "the image is empty") if len(image) > MAX_PHOTO: - raise HTTPException(413, "התמונה גדולה מדי.") - try: - out = solver.solve(image, photo.content_type) - except PhotoUnavailable as e: - raise HTTPException(502, str(e)) - if who and out.get("readable"): # an unreadable photo does not count against the day - photo_counts[who] = photo_counts.get(who, 0) + 1 - return {**out, "left": DAILY_PHOTOS - photo_counts.get(who, 0) if who else None} - - @app.get("/api/phrases") - def phrases(): - return {"levels": LEVELS, "lessons": LESSONS, "categories": CATEGORIES, "games": GAMES, "phrases": public_phrases(), - "voices": [{"id": i, "name": n} for n, i in playback_voices().items()]} - - def verb_lessons(user: dict | None) -> list[str]: - """Lessons available to this learner, using the existing class rules.""" - if not user or user["teacher"] or not user["classes"]: - return READY_LESSONS - return [lesson for lesson in READY_LESSONS - if any(lesson in lessons_of(course) for course in user["classes"])] - - @app.get("/api/verb-game", dependencies=[Depends(require("games.use"))]) - def verb_game(user: dict | None = Depends(signed_in)): - """Configuration for "פועל יוצא" and the complete paradigms currently unlocked.""" - meta = app.state.verb_game.metadata(verb_lessons(user)) - if not (user and user["teacher"]): # draft verbs are teachers' only - meta["eitan_verbs"] = [v for v in meta["eitan_verbs"] if not v["draft"]] - return meta - - @app.post("/api/verb-game/question", dependencies=[Depends(require("games.use"))]) - def verb_question(body: dict = Body(...), user: dict | None = Depends(signed_in)): - """Create a Level 1--5 exercise; answers remain server-side.""" - try: - level = int(body.get("level", 1)) - mastery = body.get("mastery") if isinstance(body.get("mastery"), dict) else {} - recent = body.get("recent") if isinstance(body.get("recent"), list) else [] - return app.state.verb_game.question(level, verb_lessons(user), mastery, list(map(str, recent[-10:])), - drafts=bool(user and user["teacher"])) - except (TypeError, ValueError) as e: - raise HTTPException(400, str(e)) - except LookupError as e: - raise HTTPException(404, str(e)) - - @app.post("/api/verb-game/answer", dependencies=[Depends(require("games.use"))]) - def verb_answer(tasks: BackgroundTasks, body: dict = Body(...), user: dict | None = Depends(signed_in)): - """Grade one opaque question once and return feedback plus measured client response time.""" - try: - result = app.state.verb_game.answer(str(body.get("question_id", "")), body.get("answer"), - int(body.get("response_ms", 0))) - if user: - tasks.add_task(app.state.auth.log_event, "game", user, "פועל יוצא", {"level": result.get("level")}) - return result - except (TypeError, ValueError) as e: - raise HTTPException(400, str(e)) - except LookupError as e: - raise HTTPException(410, str(e)) - - @app.post("/api/verb-game/speech", dependencies=[Depends(require("games.use"))]) - def verb_speech(audio: UploadFile = File(...), question_id: str = Form(...), - user: dict | None = Depends(signed_in)): - """Evaluate Levels 4–5 audio without turning uncertain recognition into an error.""" - try: - question, phrase = app.state.verb_game.speech_phrase(question_id) - except LookupError as e: - raise HTTPException(410, str(e)) - data = audio.file.read(MAX_UPLOAD + 1) - if len(data) > MAX_UPLOAD: - raise HTTPException(413, "recording too long") - try: - wav = decode_audio(data) - except ValueError: - raise HTTPException(400, "could not decode audio") - if wav.size < 16_000 * .3: - return {"outcome": "uncertain", "counts_as_attempt": False, - "message": "לא נשמע מספיק דיבור. האזינו להקלטה ונסו שוב."} - result = SpeechEvaluationService(app.state.scorer).evaluate( - wav, phrase, diagnostics=bool(user and user["teacher"])) - result["level"] = question.mode - return result - - @app.post("/api/verb-game/flag") - def verb_flag(question_id: str = Form(...), audio: UploadFile = File(...), note: str = Form(""), - user: dict | None = Depends(signed_in)): - """A tutor reports an incorrect judgement of a dynamic spoken verb answer.""" - if not user or not user["tutor"]: - raise HTTPException(403, "tutors only") - try: - _question, phrase = app.state.verb_game.speech_phrase(question_id) - except LookupError as e: - raise HTTPException(410, str(e)) - data = audio.file.read(MAX_UPLOAD + 1) - if len(data) > MAX_UPLOAD: - raise HTTPException(413, "recording too long") - try: - wav = decode_audio(data) - except ValueError: - raise HTTPException(400, "could not decode audio") - result = app.state.scorer.score(wav, phrase, [], diagnostics=True) - if result.get("error"): - raise HTTPException(400, result["message"]) - result.pop("phoneme_heatmap", None) - return save_flag(phrase, result, note, user, data, audio.content_type or "application/octet-stream") - - def tutor_for(user: dict | None) -> Tutor: - """The paid tutor game, after the tutor.use dependency authorized the account (or on an open service).""" - if app.state.tutor is None: - raise HTTPException(503, "המורה לא מוגדר בשרת (חסר ANTHROPIC_API_KEY)") - return app.state.tutor - - def tutor_day(user: dict | None) -> tuple[str, str, int | None, object]: - """The account's key for the day's count, the day, its limit (None only without sign-in) and how to read - the count Supabase keeps.""" - now = datetime.now(LOCAL_TZ) - day = now.date().isoformat() - if not user or "developer" in user.get("roles", ()): - return "open", day, None, None - since = now.replace(hour=0, minute=0, second=0, microsecond=0).astimezone(timezone.utc).isoformat() - return user["id"], day, DAILY_LIMIT, lambda: app.state.auth.tutor_chats_since(user["id"], since) - - @app.get("/api/tutor", dependencies=[Depends(require("tutor.use"))]) - def tutor_info(user: dict | None = Depends(signed_in)): - """יאללה נדבר: whether this account may play, its scenario cards (one for each lesson, in the course's order; the - page shows those of the lessons the student has started), and how many conversations are left today (None: no - limit).""" - try: - t = tutor_for(user) - except HTTPException: - return {"available": False} - student, day, limit, seed = tutor_day(user) - return {"available": True, "turns": MAX_TURNS, - "daily_limit": limit, - "left": None if limit is None else max(0, limit - t.started(student, day, seed)), - "scenarios": [{k: s[k] for k in ("id", "lesson", "title", "desc", "who", "ar", "she")} for s in SCENARIOS]} - - @app.post("/api/tutor/start", dependencies=[Depends(require("tutor.use"))]) - def tutor_start(tasks: BackgroundTasks, body: dict = Body(...), user: dict | None = Depends(signed_in)): - """A new conversation on a scenario card: {"scenario", "gender": "m" or "f"}. Gives its id, the tutor's opening - message (see Tutor.message) and how many conversations are left today; 429 when none are, 502 when the model - did not answer.""" - t = tutor_for(user) - scenario, gender = body.get("scenario"), body.get("gender") - if scenario not in BY_ID or gender not in ("m", "f"): - raise HTTPException(400, "unknown scenario or gender") - student, day, limit, seed = tutor_day(user) - try: - chat, opening = t.start(scenario, gender, user["id"] if user else None, - user["email"].lower() if user else "", student, day, limit, seed) - except TutorLimit: - raise HTTPException(429, f"סיימתם את {DAILY_LIMIT} השיחות של היום. נתראה מחר!") - except TutorUnavailable as e: - raise HTTPException(502, str(e)) - if user and app.state.auth: - tasks.add_task(app.state.auth.save_tutor_chat, chat.row()) - return {"id": chat.id, "message": opening, "turns": MAX_TURNS, - "left": None if limit is None else max(0, limit - t.started(student, day))} - - @app.post("/api/tutor/turn", dependencies=[Depends(require("tutor.use"))]) - def tutor_turn(tasks: BackgroundTasks, body: dict = Body(...), user: dict | None = Depends(signed_in)): - """The student's sentence, as the page heard it: {"id", "text"}. Gives the tutor's answer (see Tutor.turn).""" - t = tutor_for(user) - text = " ".join(str(body.get("text", "")).split()) - if not text or len(text) > MAX_TEXT: - raise HTTPException(400, f"a sentence of 1 to {MAX_TEXT} characters") - chat = t.get(str(body.get("id", "")), user["id"] if user else None) - if chat is None: - raise HTTPException(410, "השיחה הזו כבר לא פעילה. התחילו שיחה חדשה.") - if chat.done: - raise HTTPException(409, "השיחה הסתיימה") - try: - result = t.turn(chat, text) - except TutorUnavailable as e: - raise HTTPException(502, str(e)) - if user and app.state.auth: - tasks.add_task(app.state.auth.save_tutor_chat, chat.row()) - return result - - @app.get("/api/health") - def health(): - s: Scorer = app.state.scorer - return {"version": build_version(), "build": build_metadata(), "model": MODEL_ID, "tts": app.state.tts is not None, - "references": app.state.refs is not None, "sign_in": app.state.auth is not None, - "phoneme_fallbacks": s.vocab.downgraded, - # per rule of pronunciation-rules.md: can this model catch it at all (and if not, why) - "rules": check_detectable(s.rules, lambda ph: s.vocab.sound_id(ph) is not None, s.vocab.model_id), - "dialect_realisations_missing": {k: sorted(v) for k, v in s.vocab.class_missing.items()}} - - @app.get("/api/tts/{phrase_id}", dependencies=[Depends(require("practice.use"))]) - def tts_phrase(phrase_id: int): - """The model answer in the playback voice (SAWTAK_VOICE_ID, else Raef), or a dialogue line's own voice. - 503 tells the page to use the device voice.""" - names = {i: n for n, i in {**playback_voices(), **LINE_VOICES}.items()} - phrase = phrase_of(phrase_id) - text = tts_arabic(*phrase["variants"][0]) - if app.state.tts is None: - raise HTTPException(503, "tts not configured") - try: - data, used = app.state.tts.wav(text, LINE_VOICES.get(phrase.get("voice"), RAEF_VOICE_ID)) - except TTSUnavailable as e: - raise HTTPException(503, str(e)) - tts = app.state.tts # debug: lets the page show which voice spoke - return Response(data, media_type="audio/wav", headers={ - "Cache-Control": "private, no-store", "X-TTS-Provider": "sawtakarabi.ai", - "X-TTS-Voice-Id": urllib.parse.quote(used), - "X-TTS-Voice-Name": urllib.parse.quote(names.get(used) or getattr(tts, "voice_name", ""))}) - - @app.get("/api/tts-cache") - def tts_cache_usage(_a: SupabaseAuth = Depends(teacher_only)): - """Cache size and process-lifetime hit rate for Monitoring; contains no phrase or user data.""" - if app.state.tts is None: - return {"available": False, "files": 0, "bytes": 0, "hits": 0, "misses": 0, "hit_rate": None} - try: - with app.state.tts_warm_lock: - warm = dict(app.state.tts_warm) - return {"available": True, "voice_id": getattr(app.state.tts, "voice_id", RAEF_VOICE_ID), - **app.state.tts.cache_stats(), "warm": warm} - except TTSUnavailable as e: - raise HTTPException(503, str(e)) - - @app.delete("/api/tts-cache") - def clear_tts_cache(_a: SupabaseAuth = Depends(teacher_only)): - """Delete generated Sawtak recordings; the next playback generates them again.""" - if app.state.tts is None: - raise HTTPException(503, "tts not configured") - with app.state.tts_warm_lock: - if app.state.tts_warm["running"]: - raise HTTPException(409, "cache warm is running") - try: - removed = app.state.tts.clear_cache() - except TTSUnavailable as e: - raise HTTPException(503, str(e)) - if app.state.refs: - app.state.refs.invalidate() - return {"ok": True, "removed": removed} - - @app.get("/api/tts-cache/activity") - def tts_cache_activity(_a: SupabaseAuth = Depends(teacher_only)): - """What the voice cache is doing: Sawtak requests and bucket uploads in flight and the latest results.""" - tts = app.state.tts - if tts is None or not hasattr(tts, "activity"): - return {"now": time.time(), "active": [], "events": [], "pace": None} - from .tts import MAX_CONCURRENT, MIN_REQUEST_INTERVAL - return {**tts.activity.snapshot(), - "pace": {"per_minute": round(60 / MIN_REQUEST_INTERVAL) if MIN_REQUEST_INTERVAL else None, - "max_concurrent": MAX_CONCURRENT, - "sweep_workers": WARM_WORKERS}} - - @app.post("/api/tts-cache/import") - def import_tts_cache(archive: UploadFile = File(...), _a: SupabaseAuth = Depends(teacher_only)): - """Load an offline copy (a zip from GET /api/tts-cache/download) back into the cache: each WAV that is not - cached yet is kept, then uploaded to the bucket in the background. manifest.csv and anything else is ignored. - - Answers {"added", "skipped", "invalid", "uploading"}.""" - tts = app.state.tts - if tts is None: - raise HTTPException(503, "tts not configured") - part = r"[A-Za-z0-9][A-Za-z0-9._-]{0,99}" - name_ok = re.compile(rf"(?:ihki-tts/)?((?:{part}/)?[0-9a-f]{{40}}\.wav)") - added: list[tuple[Path, str]] = [] - skipped = invalid = 0 - spool = tempfile.TemporaryFile() # a plain file: zipfile seeks, and Python 3.10's spooled upload cannot - try: - shutil.copyfileobj(archive.file, spool, 1024 * 1024) - spool.seek(0) - with zipfile.ZipFile(spool) as zf: - for info in zf.infolist(): - if info.is_dir() or info.filename == "manifest.csv": - continue - m = name_ok.fullmatch(info.filename) - if not m or ".." in info.filename or info.file_size > 20 * 1024 * 1024: - invalid += 1 - continue - rel = m.group(1) - existing = tts.has(rel) - if existing is not None and existing.stat().st_size == info.file_size: - skipped += 1 - continue - data = zf.read(info) - if data[:4] != b"RIFF" or data[8:12] != b"WAVE": - invalid += 1 - continue - added.append((tts.store(rel, data), rel)) - except zipfile.BadZipFile: - raise HTTPException(400, "זה לא קובץ zip של עותק אופליין") - except OSError as e: - raise HTTPException(503, f"could not write the voice cache: {e}") - finally: - spool.close() - mirror = getattr(tts, "mirror", None) - if mirror is not None and added: - def upload() -> None: - for i in range(0, len(added), 25): - batch = added[i:i + 25] - try: - mirror.upload([(path, mirror.remote_path(rel)) for path, rel in batch], source="import") - except Exception as e: # the next cache-warm sweep uploads what is still missing - log.warning("tts import: bucket upload of %d files failed: %s", len(batch), e) - threading.Thread(target=upload, name="ihki-tts-import", daemon=True).start() - if app.state.refs and added: - app.state.refs.invalidate() - return {"added": len(added), "skipped": skipped, "invalid": invalid, - "uploading": bool(mirror is not None and added)} - - @app.get("/api/tts-cache/download") - def download_tts_cache(_a: SupabaseAuth = Depends(teacher_only)): - """An offline copy of the Sawtak recordings: every cached WAV, plus manifest.csv naming each file's phrase. - - The zip is streamed (stored, not compressed: WAV barely compresses), so the whole cache never sits in memory.""" - tts = app.state.tts - if tts is None: - raise HTTPException(503, "tts not configured") - try: - files = tts.cached_files() - except OSError as e: - raise HTTPException(503, f"could not read the voice cache: {e}") - names = {i: n for n, i in {**VOICES, **LINE_VOICES}.items()} - known: dict[str, dict] = {} # file name (sha1 of version, voice and text) -> what it says - for phrase_id, phrase in enumerate(PHRASES): - text = tts_arabic(*phrase["variants"][0]) - for voice in names: - info = known.setdefault(Path(tts.cache_key(text, voice)).name, - {"voice_id": voice, "voice": names[voice], "text": text, - "he": phrase.get("he", ""), "tr": phrase.get("tr", ""), "phrase_ids": []}) - info["phrase_ids"].append(str(phrase_id)) - - def chunks(): - class Sink(io.RawIOBase): # unseekable: zipfile writes data descriptors instead of seeking back - def __init__(self): self.parts: list[bytes] = [] - def writable(self): return True - def write(self, b): self.parts.append(bytes(b)); return len(b) - sink = Sink() - - def drain() -> bytes: - out = b"".join(sink.parts); sink.parts.clear(); return out - - rows = io.StringIO() - writer = csv.writer(rows) - writer.writerow(["file", "voice_id", "voice", "phrase_ids", "arabic", "hebrew", "transliteration"]) - with zipfile.ZipFile(sink, "w", zipfile.ZIP_STORED, allowZip64=True) as zf: - for path, rel in files: - try: - zf.write(path, f"ihki-tts/{rel}") - except OSError as e: # removed or unreadable meanwhile: leave it out, keep the rest - log.warning("tts download skipped %s: %s", rel, e) - continue - info = known.get(path.name, {}) - writer.writerow([f"ihki-tts/{rel}", info.get("voice_id", rel.split("/")[0] if "/" in rel else ""), - info.get("voice", ""), " ".join(info.get("phrase_ids", [])), info.get("text", ""), - info.get("he", ""), info.get("tr", "")]) - yield drain() - zf.writestr("manifest.csv", "\ufeff" + rows.getvalue()) # BOM: Excel opens the Hebrew/Arabic right - yield drain() - - name = f"ihki-tts-{datetime.now(timezone.utc):%Y-%m-%d_%H-%M}.zip" - return StreamingResponse(chunks(), media_type="application/zip", headers={ - "Content-Disposition": f'attachment; filename="{name}"', "Cache-Control": "no-store", - "X-File-Count": str(len(files))}) - - @app.post("/api/tts-cache/warm") - def warm_tts_cache(body: dict = Body(...), _a: SupabaseAuth = Depends(teacher_only)): - """Generate the finite phrase bank for a selected voice, one request at a time.""" - tts = app.state.tts - if tts is None: - raise HTTPException(503, "tts not configured") - try: - voice = str(uuid.UUID(str(body.get("voice_id") or ""))) - except (ValueError, AttributeError, TypeError): - raise HTTPException(400, "invalid Sawtak voice id") - with app.state.tts_warm_lock: - if app.state.tts_warm["running"]: - return dict(app.state.tts_warm) - jobs, seen = [], set() - for phrase in PHRASES: - text = tts_arabic(*phrase["variants"][0]) - if text not in seen: - seen.add(text); jobs.append(text) - mirror = getattr(tts, "mirror", None) - app.state.tts_warm = {"running": True, "voice_id": voice, "total": len(jobs), "done": 0, "generated": 0, - "skipped": 0, "failed": 0, "current": "", "waiting": 0, "error": "", - "bucket": mirror.bucket_id if mirror else "", "uploaded": 0, "upload_failed": 0, - "upload_error": "", "stopped": False, "started_at": time.time(), - "phase": "scanning", "to_generate": 0} - stop = app.state.tts_warm_stop - stop.clear() - - # Every WAV is also uploaded to the Storage Bucket through its API (see tts.BucketMirror): writes to the - # mounted /data folder alone did not reach the bucket. Files already there with the same size are not resent. - remote: dict[str, int] = {} - pending: list[tuple[Path | bytes, str, int]] = [] - oldest = [0.0] # when the oldest pending file was queued: a batch waits at most 30 s for its 25th file - uploads = threading.Lock() # guards remote, pending and oldest: the workers queue at the same time - - def flush() -> None: - with uploads: - batch = pending[:] - pending.clear() - if not batch: - return - try: - mirror.upload([(src, dest) for src, dest, _size in batch], source="sweep") - with uploads: - remote.update({dest: size for _src, dest, size in batch}) - with app.state.tts_warm_lock: - app.state.tts_warm["uploaded"] += len(batch) - except Exception as e: - log.warning("tts bucket upload of %d files failed: %s", len(batch), e) - with app.state.tts_warm_lock: - app.state.tts_warm["upload_failed"] += len(batch) - app.state.tts_warm["upload_error"] = str(e)[:300] - - def queue_upload(src: Path | bytes, rel: str) -> None: - if mirror is None: - return - try: - size = src.stat().st_size if isinstance(src, Path) else len(src) - except OSError: - return - dest = mirror.remote_path(rel) - with uploads: - if remote.get(dest) == size or any(d == dest for _s, d, _z in pending): - return - if not pending: - oldest[0] = time.monotonic() - pending.append((src, dest, size)) - due = len(pending) >= 25 or time.monotonic() - oldest[0] > 30 - if due: - flush() - - def bump(**counts: int) -> None: - with app.state.tts_warm_lock: - for name, n in counts.items(): - app.state.tts_warm[name] += n - - halted = threading.Event() # a phrase failed for good: no worker starts another - - def generate(text: str) -> bool: - """One phrase, retried on a rate limit, a provider 5xx or a dropped connection. False: stopped.""" - for attempt in range(5): - if stop.is_set() or halted.is_set(): - return False - try: - data, _used = tts.wav_for_voice(text, voice) - if mirror is not None: # the disk copy if it was kept, else the bytes themselves - queue_upload(tts.cache_file(text, voice) or data, tts.cache_key(text, voice)) - return True - except TTSUnavailable as e: - if not e.transient or attempt == 4: - raise - delay = max(.2, min(e.retry_after or 2 ** (attempt + 1), 60)) - if e.status == 429 and hasattr(tts, "hold"): - tts.hold(delay) # the other workers wait too, instead of hitting the limit again - with app.state.tts_warm_lock: - app.state.tts_warm["waiting"] = round(delay, 1) - interrupted = stop.wait(delay) # a stop request ends the wait at once - with app.state.tts_warm_lock: - app.state.tts_warm["waiting"] = 0 - if interrupted: - return False - return False - - def run() -> None: - listed = False - if mirror is not None: - try: - remote.update(mirror.remote_sizes()) - listed = True - except Exception as e: # listing failed: upload everything; the uploads report their own errors - log.warning("tts bucket listing failed: %s", e) - with app.state.tts_warm_lock: - app.state.tts_warm["upload_error"] = f"listing failed: {str(e)[:300]}" - - def is_cached(text: str) -> bool: - """With a bucket listing: the local staging folder or the listing, both instant. Asking the bucket - mount instead costs a network lookup per missing file, minutes for the whole bank.""" - if not listed or getattr(tts, "staging_dir", None) is None: - return tts.cached(text, voice) - rel = tts.cache_key(text, voice) - return ((tts.staging_dir / rel).is_file() or mirror.remote_path(rel) in remote - or mirror.remote_path(rel.rsplit("/", 1)[-1]) in remote) - - # Already cached: skip without reading it. The upload pass at the end still sends it if the bucket - # lacks it. Workers start on the missing phrases while the scan goes on. - work: Queue = Queue() - - def worker() -> None: - while not (stop.is_set() or halted.is_set()): - text = work.get() - if text is None: - return - with app.state.tts_warm_lock: - app.state.tts_warm["current"] = text - try: - if generate(text): - bump(generated=1, done=1) - except Exception as e: - log.warning("tts cache warm stopped at %r: %s", text, e) - halted.set() - with app.state.tts_warm_lock: - app.state.tts_warm["failed"] += 1 - app.state.tts_warm["error"] = str(e) - - workers = [threading.Thread(target=worker, name=f"ihki-tts-warm-{i}", daemon=True) - for i in range(WARM_WORKERS)] - for w in workers: - w.start() - for text in jobs: - if stop.is_set() or halted.is_set(): - break - try: - cached = is_cached(text) - except OSError: - cached = False - if cached: - bump(skipped=1, done=1) - else: - bump(to_generate=1) - work.put(text) - with app.state.tts_warm_lock: - app.state.tts_warm["phase"] = "generating" - for _ in workers: - work.put(None) - for w in workers: - w.join() - if mirror is not None and (stop.is_set() or halted.is_set()): - flush() # what was generated still reaches the bucket; the full pass waits for the next run - elif mirror is not None: - # also everything cached earlier: other voices, playback misses, the legacy flat layout - with app.state.tts_warm_lock: - app.state.tts_warm["current"] = "uploading cached files" - app.state.tts_warm["phase"] = "uploading" - try: - for path, rel in tts.cached_files(): - queue_upload(path, rel) - except OSError as e: - log.warning("tts cache listing failed: %s", e) - flush() - with app.state.tts_warm_lock: - app.state.tts_warm["running"] = False - app.state.tts_warm["current"] = "" - app.state.tts_warm["phase"] = "" - app.state.tts_warm["stopped"] = stop.is_set() - - threading.Thread(target=run, name="ihki-tts-warm", daemon=True).start() - with app.state.tts_warm_lock: - return dict(app.state.tts_warm) - - @app.post("/api/tts-cache/warm/stop") - def stop_tts_cache_warm(_a: SupabaseAuth = Depends(teacher_only)): - """Ask the running sweep to stop after its current phrase. Nothing is lost: the next run resumes.""" - app.state.tts_warm_stop.set() - with app.state.tts_warm_lock: - return dict(app.state.tts_warm) - - def keys_only(user: dict | None = Depends(signed_in)) -> SupabaseAuth: - """Only developers ("*"): a key decides where the bills go. Unlike require(), an open deployment (no sign-in) - is refused too.""" - permissions = set((user or {}).get("permissions", ())) - if app.state.auth is None or ("*" not in permissions and "keys.manage" not in permissions): - raise HTTPException(403, "missing permission: keys.manage") - return app.state.auth - - def keys_status(response: Response) -> dict: - response.headers["Cache-Control"] = "no-store" - keys = app.state.keys - return {"keys": keys.status(), "writable": keys.writable, "why": keys.why, - "setup": CREATE_APP_SECRETS if keys.missing else ""} - - def use_key(name: str) -> None: - """Puts the key now in force to work, without a restart. The voice cache, the scores' native baseline and the - tutor's open conversations stay.""" - value = app.state.keys.get(name) - if name == "TUTOR_MODEL": # open conversations go on with the new model from their next sentence - if app.state.tutor is not None: - app.state.tutor.model = value or DEFAULT_TUTOR_MODEL - elif name == "SAWTAK_API_KEY": - if app.state.tts is not None and value: - app.state.tts.api_key = value - else: - app.state.tts = load_tts(value) - app.state.refs = References(app.state.scorer, app.state.tts, list(BASELINE_VOICES.values())) if app.state.tts else None - elif app.state.tutor is not None and value: - app.state.tutor.client = tutor_client(value) - else: - app.state.tutor = load_tutor(value, app.state.keys.get("TUTOR_MODEL")) - - def key_name(name: str) -> str: - if name not in KEY_NAMES: - raise HTTPException(404, "unknown key") - return name - - @app.get("/api/admin/keys") - def api_keys(response: Response, _a: SupabaseAuth = Depends(keys_only)): - """Which API keys are set and where from (the panel, the server's environment, or none); never the keys.""" - return keys_status(response) - - @app.put("/api/admin/keys/{name}") - def set_api_key(name: str, response: Response, body: dict = Body(...), _a: SupabaseAuth = Depends(keys_only), - user: dict | None = Depends(signed_in)): - """Tries a new key with its provider, then keeps it encrypted and puts it to work. The key is never logged.""" - name, value = key_name(name), body.get("value") - if not isinstance(value, str) or not value.strip() or len(value.strip()) > MAX_KEY_LEN or any(c.isspace() for c in value.strip()): - raise HTTPException(400, "a key is one word of up to 300 characters") - if not app.state.keys.writable: - raise HTTPException(503, app.state.keys.why or f"the table app_secrets is missing; run: {CREATE_APP_SECRETS}") - value = value.strip() - try: - check_key(name, value, app.state.keys.get("ANTHROPIC_API_KEY")) - except KeyRefused as e: - raise HTTPException(400, str(e)) - try: - app.state.keys.set(name, value, (user or {}).get("email", "")) - except AuthUnavailable as e: - if any(mark in str(e).lower() for mark in ("check constraint", "23514")): # a table made before TUTOR_MODEL - raise HTTPException(409, f"הטבלה app_secrets צריכה עדכון; הריצו ב-Supabase: {MIGRATE_APP_SECRETS}") - raise HTTPException(503, f"Supabase לא זמין: {e}") - use_key(name) - log.info("API key %s replaced by %s", name, (user or {}).get("email", "?")) - if app.state.auth: - app.state.auth.log_event("permission", user, f"מפתח API: {name}", {"key": name, "action": "set"}) - return keys_status(response) - - @app.delete("/api/admin/keys/{name}") - def clear_api_key(name: str, response: Response, _a: SupabaseAuth = Depends(keys_only), - user: dict | None = Depends(signed_in)): - """Forgets the panel's key: the server's environment key (if any) is used again.""" - name = key_name(name) - if not app.state.keys.writable: - raise HTTPException(503, app.state.keys.why or "the table app_secrets is missing") - try: - app.state.keys.clear(name) - except AuthUnavailable as e: - raise HTTPException(503, f"Supabase לא זמין: {e}") - use_key(name) - log.info("API key %s cleared by %s", name, (user or {}).get("email", "?")) - if app.state.auth: - app.state.auth.log_event("permission", user, f"מפתח API: {name}", {"key": name, "action": "clear"}) - return keys_status(response) - - # ---- the phoneme model: which one scores, switched from the panel's "מודלים" tab ---- - app.state.model_switch = {"state": "idle", "target": "", "error": "", "started_at": 0.0, "by": ""} - model_lock = threading.Lock() - - def models_only(user: dict | None = Depends(signed_in)) -> SupabaseAuth: - """Only developers ("*") switch the model: every learner's score changes with it. Like keys_only, an open - deployment (no sign-in) is refused too.""" - permissions = set((user or {}).get("permissions", ())) - if app.state.auth is None or ("*" not in permissions and "models.manage" not in permissions): - raise HTTPException(403, "missing permission: models.manage") - return app.state.auth - - def models_status(user: dict | None) -> dict: - permissions = set((user or {}).get("permissions", ())) - return {"active": MODEL_ID, "default": ENV_MODEL_ID, "switch": dict(app.state.model_switch), - "can_switch": app.state.auth is not None and bool(permissions & {"*", "models.manage"}), - "models": model_catalog(MODEL_ID)} - - def prepare_scorer(model_id: str) -> Scorer: - """A scorer for `model_id` set up as the running one would be: its stored settings, the calibrations made for - it and the rules switched off in ניטור.""" - scorer = load_scorer(model_id) - apply_model_settings(scorer, model_id) - if app.state.auth and hasattr(app.state.auth, "calibrations"): - try: - rows = app.state.auth.calibrations("applied") - scorer.set_calibrations([r for r in rows if not r.get("model") or r.get("model") == model_id]) - except AuthUnavailable as e: - log.warning("calibrations not loaded for %s: %s", model_id, e) - if app.state.auth and hasattr(app.state.auth, "scoring_rules"): - try: - scorer.set_disabled(r["id"] for r in app.state.auth.scoring_rules() if not r.get("active", True)) - except AuthUnavailable as e: - log.warning("scoring rules not loaded for %s: %s", model_id, e) - return scorer - - def use_model(model_id: str, user: dict | None) -> None: - """Loads `model_id` beside the running one, then swaps it in: requests in flight finish on the old one. - The swap carries over what belongs to the scoring, not to the model: the switched-off rules, the native - baseline (rebuilt from the cached voices, scored by the new model) and the calibrations made for it.""" - global MODEL_ID - try: - scorer = prepare_scorer(model_id) - app.state.compare.pop(model_id, None) # the comparison's own copy, if it had one, is this one now - old = app.state.scorer - app.state.scorer, MODEL_ID = scorer, model_id - if app.state.tts: - app.state.refs = References(scorer, app.state.tts, list(BASELINE_VOICES.values())) - del old - try: # hand the old model's memory back (the GPU has room for one model at a time) - import gc - import torch - gc.collect() - if torch.cuda.is_available(): - torch.cuda.empty_cache() - except ImportError: - pass - app.state.model_switch.update(state="idle", error="") - log.info("phoneme model switched to %s by %s", model_id, (user or {}).get("email", "?")) - if app.state.auth: - app.state.auth.log_event("permission", user, f"מודל: {model_id}", {"model": model_id, "action": "switch"}) - except Exception as e: # the running model stays; the panel shows why - log.exception("loading the model %s failed", model_id) - app.state.model_switch.update(state="error", error=f"{type(e).__name__}: {e}"[:400]) - - @app.get("/api/admin/models") - def models(response: Response, _a: SupabaseAuth = Depends(admin_only), user: dict | None = Depends(signed_in)): - """The models this server can score with, what each was measured to do, and which one is scoring now.""" - response.headers["Cache-Control"] = "no-store" - return models_status(user) - - @app.put("/api/admin/models/active") - def switch_model(response: Response, body: dict = Body(...), _a: SupabaseAuth = Depends(models_only), - user: dict | None = Depends(signed_in)): - """Starts loading another model in the background; GET /api/admin/models shows when it took over. - The choice lasts until the server restarts (MODEL_ID decides the one it starts with).""" - response.headers["Cache-Control"] = "no-store" - model_id = body.get("model_id") - if not isinstance(model_id, str) or not (model_known(model_id) or model_id == ENV_MODEL_ID): - raise HTTPException(400, "unknown model") - ok, why = model_availability(model_id) - if not ok: - raise HTTPException(409, why) - with model_lock: - if app.state.model_switch["state"] == "loading": - raise HTTPException(409, "מודל אחר נטען כרגע") - if model_id == MODEL_ID: - return models_status(user) - app.state.model_switch = {"state": "loading", "target": model_id, "error": "", "started_at": time.time(), - "by": (user or {}).get("email", "")} - threading.Thread(target=use_model, args=(model_id, user), daemon=True, name="model-switch").start() - return models_status(user) - - # ---- debugging: one recording scored by several models side by side (the practice page's 🔬 button) ---- - app.state.compare = {} # model id -> (scorer, References or None), loaded on first use and kept - compare_lock = threading.Lock() - - def compare_ids() -> list[str]: - """COMPARE_MODELS (comma-separated), else the base model, the wav2vec2 fine-tune and POWSM (the local - export, or the Hub one). The model scoring now always comes first; models this server can't load are left out.""" - raw = os.environ.get("COMPARE_MODELS", "").strip() - if raw: - ids = [m.strip() for m in raw.split(",") if m.strip()] - else: - local = "exports/powsm-ft-shortpad" - ids = ["facebook/wav2vec2-xlsr-53-espeak-cv-ft", "checkpoints/ihki-ft-minerva", - local if Path(local, "powsm_onnx.json").is_file() else f"powsm-onnx:{POWSM_ONNX_REPO}"] - ids = [MODEL_ID] + [m for m in ids if m != MODEL_ID] - return [m for m in ids if m == MODEL_ID or model_availability(m)[0]] - - def quick_compare_ids() -> list[str]: - """The stable three columns promised by the lab's quick Hebrew-speaker benchmark.""" - local = "exports/powsm-ft-shortpad" - ids = ["facebook/wav2vec2-xlsr-53-espeak-cv-ft", "checkpoints/ihki-ft-minerva", - local if Path(local, "powsm_onnx.json").is_file() else f"powsm-onnx:{POWSM_ONNX_REPO}"] - return [model_id for model_id in ids if model_id == MODEL_ID or model_availability(model_id)[0]] - - def compare_scorer(model_id: str): - """(scorer, native baselines) for a model: the running ones for the active model, else loaded once.""" - if model_id == MODEL_ID: - return app.state.scorer, app.state.refs - with compare_lock: - if model_id not in app.state.compare: - scorer = prepare_scorer(model_id) - refs = References(scorer, app.state.tts, list(BASELINE_VOICES.values())) if app.state.tts else None - app.state.compare[model_id] = (scorer, refs) - return app.state.compare[model_id] - - def compare_view(result: dict) -> dict: - """What the comparison panel draws from a result: the verdict, what was heard, each word's sounds.""" - words = [{"tr": w.get("tr", ""), "heard_tr": w.get("heard_tr", ""), "score": w.get("score"), - "phonemes": [{"ipa": p.get("ipa", ""), "score": round(p.get("score", 0)), "flagged": bool(p.get("flagged")), - "heard": p.get("heard") or ""} for p in w.get("phonemes", [])]} - for w in result.get("words", [])] - return {"score": result.get("score"), "unrelated": bool(result.get("unrelated")), - "heard_tr": result.get("heard_tr", ""), "heard_ipa": result.get("heard_ipa", ""), "words": words, - "tips": [t.get("title", "") for t in result.get("tips", [])], "references": refs if isinstance(refs := result.get("references"), int) else len(refs or [])} - - @app.post("/api/admin/compare-models") - def compare_models(phrase_id: int | None = Form(None), item: str = Form(""), audio: UploadFile = File(...), - _a: SupabaseAuth = Depends(models_only), user: dict | None = Depends(signed_in)): - """One recording scored by each comparison model, as /api/score would score it with that model. Nothing is - saved. The first call loads the models that aren't running (several seconds each).""" - if item: - found = lab.QUICK_BY_ID.get(item) - if found is None: - raise HTTPException(404, "no such quick benchmark item") - phrase, reference_key = lab.phrase(found), f"quick:{item}" - elif phrase_id is not None: - phrase, reference_key = phrase_of(phrase_id), phrase_id - else: - raise HTTPException(400, "phrase_id or item is required") - data = audio.file.read(MAX_UPLOAD + 1) - if len(data) > MAX_UPLOAD: - raise HTTPException(413, "recording too long") - try: - wav = decode_audio(data) - except ValueError: - raise HTTPException(400, "could not decode audio") - names = {m["id"]: m["name"] for m in model_catalog(MODEL_ID)} - out = [] - for model_id in (quick_compare_ids() if item else compare_ids()): - row = {"model": model_id, "name": names.get(model_id, model_id), "active": model_id == MODEL_ID} - try: - t0 = time.perf_counter() - scorer, refs = compare_scorer(model_id) - row["load_ms"] = round(1000 * (time.perf_counter() - t0)) - t0 = time.perf_counter() - baselines = refs.get(reference_key, phrase) if refs else [] - result = scorer.score(wav, phrase, baselines, diagnostics=False) - row["ms"] = round(1000 * (time.perf_counter() - t0)) - row.update(compare_view(result)) - except Exception as e: # one model failing must not hide the others - log.exception("comparison with %s failed", model_id) - row["error"] = f"{type(e).__name__}: {e}"[:300] - out.append(row) - return {"phrase": phrase.get("key", phrase.get("he", "")), "models": out} - - # ---- the scorer's settings per model (thresholds, floors, GOP), tuned in the "מודלים" tab ---- - def model_settings_row(model_id: str) -> tuple[bool, dict | None]: - """(is the table there, the model's stored row or None). Raises AuthUnavailable on other failures.""" - a = app.state.auth - if not a or not hasattr(a, "model_settings"): - return False, None - try: - rows = a.model_settings() - except AuthUnavailable as e: - if "answered 404" in str(e): - return False, None - raise - return True, next((r for r in rows if r.get("model") == model_id), None) - - def apply_model_settings(scorer: Scorer, model_id: str) -> None: - """Puts a model's stored settings on its scorer. No row leaves the scorer as it was built.""" - try: - _, row = model_settings_row(model_id) - if row: - scorer.cfg = build_model_config(model_id, row.get("settings") or {}) - except (AuthUnavailable, ValueError, TypeError) as e: - log.warning("model settings for %s not applied: %s", model_id, e) - - def sound_counts() -> dict[str, int]: - """How often each sound occurs in the phrase bank: the tab shows which thresholds matter most.""" - if getattr(app.state, "sound_counts", None) is None: - counts: dict[str, int] = {} - vocab = app.state.scorer.vocab - for p in PHRASES: - for ar, ipa in p["variants"]: - try: - for t in tokenize(archify(ar, ipa), vocab): - counts[t.sound] = counts.get(t.sound, 0) + 1 - except KeyError: - pass - app.state.sound_counts = counts - return app.state.sound_counts - - def model_settings_payload(model_id: str, user: dict | None) -> dict: - try: - stored, row = model_settings_row(model_id) - except AuthUnavailable as e: - raise HTTPException(503, f"Supabase לא זמין: {e}") - permissions = set((user or {}).get("permissions", ())) - row = row or {} - try: - described = describe_model_settings(model_id, row.get("settings") or {}, sound_counts()) - except ValueError as e: # a row written by hand that no longer validates: show the defaults and why - described = {**describe_model_settings(model_id, {}, sound_counts()), "error": str(e)} - return {"model": model_id, "active": model_id == MODEL_ID, "stored": stored, - "create_sql": None if stored else CREATE_MODEL_SETTINGS, - "can_edit": stored and app.state.auth is not None and bool(permissions & {"*", "models.manage"}), - "updated_by": row.get("updated_by", ""), "updated_at": row.get("updated_at"), **described} - - def settings_model(model_id) -> str: - if not isinstance(model_id, str) or not (model_known(model_id) or model_id in (MODEL_ID, ENV_MODEL_ID)): - raise HTTPException(400, "unknown model") - return model_id - - @app.get("/api/admin/model-settings") - def model_settings(response: Response, model: str = Query(""), _a: SupabaseAuth = Depends(admin_only), - user: dict | None = Depends(signed_in)): - """One model's scorer settings: each with its default, its value now and whether it was changed.""" - response.headers["Cache-Control"] = "no-store" - return model_settings_payload(settings_model(model or MODEL_ID), user) - - @app.put("/api/admin/model-settings") - def save_model_settings(response: Response, body: dict = Body(...), _a: SupabaseAuth = Depends(models_only), - user: dict | None = Depends(signed_in)): - """Replaces a model's settings (an empty object: back to the defaults). For the model scoring now they take - effect at once, and the native baselines are scored again under them.""" - response.headers["Cache-Control"] = "no-store" - model_id = settings_model(body.get("model")) - try: - settings = clean_model_settings(body.get("settings", {})) - cfg = build_model_config(model_id, settings) - except (ValueError, TypeError) as e: - raise HTTPException(400, str(e)) - stored, _ = model_settings_row(model_id) - if not stored: - raise HTTPException(503, f"הטבלה model_settings לא קיימת. הריצו ב-SQL Editor: {CREATE_MODEL_SETTINGS}") - try: - app.state.auth.set_model_settings(model_id, settings, (user or {}).get("email", "").lower()) - except AuthUnavailable as e: - raise HTTPException(503, f"Supabase לא זמין: {e}") - if model_id == MODEL_ID: - app.state.scorer.cfg = cfg - if app.state.refs is not None: - app.state.refs.invalidate() - log.info("model settings for %s replaced by %s: %s", model_id, (user or {}).get("email", "?"), settings) - app.state.auth.log_event("permission", user, f"הגדרות מודל: {model_id}", - {"model": model_id, "action": "settings", "settings": settings}) - return model_settings_payload(model_id, user) - - @app.get("/api/prompt-audio/{phrase_id}", dependencies=[Depends(require("practice.use"))]) - def prompt_audio(phrase_id: int, engine: str = "gtts"): - """The Hebrew prompt read aloud (edge-tts or gTTS). 503 tells the page to stay silent.""" - phrase = phrase_of(phrase_id) - if engine not in ENGINES: - raise HTTPException(400, "unknown engine") - try: - data, used = hebrew_mp3(phrase.get("he_say") or phrase["he"], engine) # pointed where the plain text is misread - except PromptTTSUnavailable as e: - raise HTTPException(503, str(e)) - return Response(data, media_type="audio/mpeg", headers={ - "Cache-Control": "private, max-age=86400", "X-TTS-Engine": used}) - - @app.get("/api/say", dependencies=[Depends(require("practice.use"))]) - def say(text: str = Query(..., min_length=1, max_length=400), engine: str = "gtts"): - """A short Hebrew text read aloud: driving mode's spoken messages.""" - if engine not in ENGINES: - raise HTTPException(400, "unknown engine") - if not re.search(r"[\u05d0-\u05ea]", text) or re.search(r"[A-Za-z\x00-\x1f]", text): - raise HTTPException(400, "hebrew text only") - try: - data, used = hebrew_mp3(text, engine, cache_it=False) - except PromptTTSUnavailable as e: - raise HTTPException(503, str(e)) - return Response(data, media_type="audio/mpeg", headers={"Cache-Control": "no-store", "X-TTS-Engine": used}) - - @app.post("/api/score", dependencies=[Depends(require("practice.use"))]) - def score(tasks: BackgroundTasks, phrase_id: int = Form(...), audio: UploadFile = File(...), - user: dict | None = Depends(signed_in)): - phrase = phrase_of(phrase_id) - data = audio.file.read(MAX_UPLOAD + 1) - if len(data) > MAX_UPLOAD: - raise HTTPException(413, "recording too long") - try: - wav = decode_audio(data) - except ValueError: - raise HTTPException(400, "could not decode audio") - if wav.size < 16_000 * 0.3: - return {"error": "too_short", "message": "לא נשמע דיבור. נסו שוב, קרוב יותר למיקרופון."} - refs = app.state.refs.get(phrase_id, phrase) if app.state.refs else [] - result = app.state.scorer.score(wav, phrase, refs, diagnostics=bool(user and user["teacher"])) - if user and result.get("score") is not None: # the student's progress; sent after the answer - issues = [{"word": wi, "expected": p["ipa"], "heard": p.get("heard") or "_", "score": round(p["score"])} - for wi, word in enumerate(result.get("words", [])) for p in word.get("phonemes", []) if p.get("flagged")] - tasks.add_task(app.state.auth.record, user["token"], { - "cat": phrase["cat"], "phrase": phrase.get("key", phrase["he"]), "score": round(result["score"]), - "unrelated": bool(result.get("unrelated")), "issues": issues[:12], "model": MODEL_ID, - "version": build_version()}) - game_ids = {g["id"] for g in GAMES} - tasks.add_task(app.state.auth.log_event, "game" if phrase["cat"] in game_ids else "exercise", user, - phrase.get("he", ""), {"cat": phrase["cat"], "score": round(result["score"]), - "flags": len(issues), "unrelated": bool(result.get("unrelated"))}) - return result - - @app.post("/api/flags") - def flag(phrase_id: int = Form(...), audio: UploadFile = File(...), note: str = Form(""), - user: dict | None = Depends(signed_in)): - """A tutor's flag: this recording of the phrase was said right and the score was wrong. It is scored again in - memory. The structured diagnosis is kept in `flags` and the audio in its private Storage bucket.""" - if not user or not user["tutor"]: - raise HTTPException(403, "tutors only") - phrase = phrase_of(phrase_id) - data = audio.file.read(MAX_UPLOAD + 1) - if len(data) > MAX_UPLOAD: - raise HTTPException(413, "recording too long") - try: - wav = decode_audio(data) - except ValueError: - raise HTTPException(400, "could not decode audio") - refs = app.state.refs.get(phrase_id, phrase) if app.state.refs else [] - # Keep a structured pipeline trace for the diagnostic dashboard alongside the private recording. - result = app.state.scorer.score(wav, phrase, refs, diagnostics=True) - if result.get("error"): - raise HTTPException(400, result["message"]) - # The trace already contains every value used by the dashboard. The dense heatmap is only a display aid - # for the immediate scoring screen and would make every database report unnecessarily large. - result.pop("phoneme_heatmap", None) - return save_flag(phrase, result, note, user, data, audio.content_type or "application/octet-stream") - - def save_flag(phrase: dict, result: dict, note: str, user: dict, audio_data: bytes, audio_mime: str) -> dict: - """Store a reviewed-score candidate and its recording from a bank or dynamic verb-game phrase.""" - row = {"email": user["email"].lower(), "cat": phrase["cat"], "phrase": phrase.get("key", phrase["he"]), - "he": phrase["he"], "ar": result["variant"]["ar"], "ipa": result["variant"]["ipa"], - "heard_ipa": result["heard_ipa"], "score": result["score"], "unrelated": bool(result["unrelated"]), - "note": " ".join(note.split())[:MAX_NOTE], "result": result, "model": MODEL_ID, "version": build_version()} - mime = audio_mime.lower().partition(";")[0].strip() - ext = AUDIO_EXT.get(mime.rpartition("/")[2], "bin") - name = f"{date.today():%Y-%m}/{uuid.uuid4().hex}.{ext}" - try: - kept = app.state.auth.add_flag(row, audio_data, mime, name) - except AuthUnavailable as e: - if "answered 404" in str(e): - raise HTTPException(503, "הטבלה flags לא קיימת ב-Supabase. הריצו ב-SQL Editor: " - f"{CREATE_FLAGS} אם כבר נוצרה, הריצו שם: notify pgrst, 'reload schema';") - raise HTTPException(503, f"Supabase לא זמין: {e}") - row["audio"] = name if kept else None - return {"ok": True, "audio": kept, "flag": row} + raise HTTPException(413, "the image is too large") + try: + result = service.solve(image, photo.content_type) + except PhotoUnavailable as exc: + raise HTTPException(503, str(exc)) from exc + if counter and result.get("readable"): + photo_counts[counter] = photo_counts.get(counter, 0) + 1 + return {**result, "left": DAILY_PHOTOS - photo_counts.get(counter, 0) if counter else None} return app diff --git a/app/math_photo.py b/app/math_photo.py index 6735384e5d9206fa186f0efe7b8019db0f20d047..6c6ae568b85ee52df4098f78a24d57552e07237b 100644 --- a/app/math_photo.py +++ b/app/math_photo.py @@ -5,8 +5,8 @@ the problem as it read it (so the student can check the reading), the solution a not give the step away and the step itself, the final answer, and the path's skill the problem practises. The page shows the hints one by one, and the steps and the answer only when the student asks for them. -Runs on the server's Anthropic key (the tutor's, ANTHROPIC_API_KEY or the panel's), on MATH_MODEL (default -DEFAULT_MODEL). The photo is sent to the API and not kept. +Runs on the server's ``ANTHROPIC_API_KEY``, using ``MATH_MODEL`` (or ``DEFAULT_MODEL``). +The photo is sent to the API and is not retained. """ from __future__ import annotations diff --git a/app/model_settings.py b/app/model_settings.py deleted file mode 100644 index 891433c14b0ee609cbdbbddddc287e8c068515b1..0000000000000000000000000000000000000000 --- a/app/model_settings.py +++ /dev/null @@ -1,159 +0,0 @@ -"""The scorer's settings a developer tunes per model from the panel's "מודלים" tab (stored in model_settings). - -A model starts from ScorerConfig's defaults and FLAG_BELOW_BY_MODEL (scorer.py). A stored row overrides some of them: - {"flag_below": 45, "gop.leniency": 0.4, "flag_below_sound": {"ħ": 30, "ʕ": null}} -`null` for a sound means "the general threshold", which also clears a built-in one (the base model's ħ at 20). -Settings belong to the model they were tuned on: a threshold means something else under another model's scores. -""" -from __future__ import annotations - -from dataclasses import replace - -from .align import POOLS -from .scorer import ScorerConfig, scorer_config - -CREATE_MODEL_SETTINGS = ("create table public.model_settings (model text primary key, " - "settings jsonb not null default '{}'::jsonb, updated_by text not null default '', " - "updated_at timestamptz not null default now()); " - "alter table public.model_settings enable row level security;") - -# key: a ScorerConfig field, or gop.. kind: number | int | choice | window (an int, or None: wide) -FIELDS: list[dict] = [ - {"key": "flag_below", "group": "flag", "kind": "number", "min": 0, "max": 100, "step": 1, - "label": "סף סימון כללי", - "help": "הגה שקיבל ציון מתחת לסף מסומן כטעות ומקבל טיפ. הגים עם סף משלהם (למטה) משתמשים בו במקום זה."}, - {"key": "issue_below", "group": "flag", "kind": "number", "min": 0, "max": 100, "step": 1, - "label": "סף \"לא מדויק\"", - "help": "הגה מתחת לסף נרשם ברשימת ההגים כלא מדויק, גם בלי טיפ. גם תנועה ארוכה שנאמרה קצרה נמדדת מולו."}, - {"key": "max_issues", "group": "flag", "kind": "int", "min": 1, "max": 20, "step": 1, - "label": "הכי הרבה הגים ברשימה", "help": "כמה הגים לא מדויקים מוצגים לתלמיד לכל היותר בכל הקלטה."}, - {"key": "unrelated_ratio", "group": "unrelated", "kind": "number", "min": 0.1, "max": 1, "step": 0.05, - "label": "סף \"משפט אחר\"", - "help": "כמה רחוק מה שנשמע מהמשפט הצפוי עד שהמערכת אומרת שנאמר משפט אחר. גבוה יותר: סלחני יותר."}, - {"key": "unrelated_veto_share", "group": "unrelated", "kind": "number", "min": 0, "max": 1, "step": 0.05, - "label": "וטו: חלק העיצורים שנמצאו", - "help": "אם לפחות החלק הזה של העיצורים נמצא בבירור ביישור, המשפט לא ייחשב אחר גם כשהמרחק גדול."}, - {"key": "unrelated_veto_mean", "group": "unrelated", "kind": "number", "min": 0, "max": 100, "step": 1, - "label": "וטו: ציון ממוצע מינימלי", "help": "וטו \"משפט אחר\" חל רק כשהציון הממוצע של ההגים לפחות כזה."}, - {"key": "near_vowel_floor", "group": "floors", "kind": "number", "min": 0, "max": 100, "step": 1, - "label": "תנועה שנשמעה כשכנתה", - "help": "ציון מינימום לתנועה שנשמעה כתנועה קרובה (i/e, u/o, קצרה/ארוכה): הבדל שלא כדאי להעניש עליו."}, - {"key": "ayin_deleted_floor", "group": "floors", "kind": "number", "min": 0, "max": 100, "step": 1, - "label": "ע שלא נשמעה", - "help": "ציון מינימום ל־ע שהמודל לא שמע בכלל: המודל המקורי מפיל לעתים ע שנאמרה נכון."}, - {"key": "onset_transient_floor", "group": "floors", "kind": "number", "min": 0, "max": 100, "step": 1, - "label": "עיצור ראשון מול רעש פתיחה", "help": "ציון מינימום לעיצור ראשון שהפסיד לקפיצה של פריים אחד בתחילת ההקלטה."}, - {"key": "ref_noise_floor", "group": "floors", "kind": "number", "min": 0, "max": 100, "step": 1, - "label": "הגה שהמודל לא מודד במשפט", - "help": "אם הקול הילידי (TTS) קיבל על הגה ציון מתחת לזה, ההגה לא נמדד במשפט הזה והתלמיד מקבל עליו ציון מלא."}, - {"key": "ref_min_mean", "group": "floors", "kind": "number", "min": 0, "max": 100, "step": 1, - "label": "אמינות הקול הילידי", - "help": "קול ילידי שהממוצע שלו מתחת לזה לא משמש בסיס להשוואה."}, - {"key": "word_min_weight", "group": "floors", "kind": "number", "min": 0, "max": 1, "step": 0.05, - "label": "משקל ההגה החלש במילה", "help": "כמה הגה אחד חלש מוריד את ציון המילה כולה (0: לא משפיע, 1: קובע לבד)."}, - {"key": "gop.leniency", "group": "gop", "kind": "number", "min": 0, "max": 1, "step": 0.05, - "label": "רכות הציון", - "help": "כמה הציון סלחני כשהמודל מתלבט בין ההגה הנכון לאחר. 0: ליניארי וקשוח; גבוה יותר: מתחרה כפול עדיין מקבל ציון סביר."}, - {"key": "gop.confident_peak", "group": "gop", "kind": "number", "min": 0.05, "max": 0.95, "step": 0.05, - "label": "ביטחון מספיק", "help": "מאיזו הסתברות של ההגה הנכון השיא כבר לא מוריד את הציון."}, - {"key": "gop.ratio_pool", "group": "gop", "kind": "choice", "choices": list(POOLS), - "label": "איחוד הפריימים", "help": "איך מצרפים את הפריימים של הגה לציון אחד (docs/gop-pooling.md). gated: רק הפריימים הרועשים."}, - {"key": "gop.energy_gate", "group": "gop", "kind": "number", "min": 0, "max": 1, "step": 0.05, - "label": "סף עוצמה (gated)", "help": "פריימים שקטים מהחלק הזה של הפריים החזק ביותר לא נספרים."}, - {"key": "gop.vowel_window", "group": "gop", "kind": "window", "min": 0, "max": 10, - "label": "חלון התנועה", "help": "מכמה פריימים סביב תנועה קוראים את המתחרים שלה. רחב: כמו עיצורים."}, - {"key": "gop.consonant_window", "group": "gop", "kind": "window", "min": 1, "max": 12, - "label": "חלון העיצור", "help": "כמה פריימים של שקט סביב עיצור נכללים בקריאה שלו. רחב: עד ההגה הבא."}, -] -GROUPS = {"flag": "סימון טעויות", "unrelated": "זיהוי \"משפט אחר\"", "floors": "רצפות והקלות", - "gop": "ניקוד ההגה (מתקדם)"} -FIELD = {f["key"]: f for f in FIELDS} - -# Per-sound thresholds: the sounds tokens stand for (TargetToken.sound), grouped as a Hebrew speaker meets them. -SOUND_GROUPS = [ - ("גרוניים", [("ħ", "ח (ح)"), ("ʕ", "ע (ع)"), ("h", "ה (ه)"), ("ʔ", "א (ء)"), ("x", "ח׳ (خ)"), ("ɣ", "ע׳ (غ)")]), - ("מודגשים", [("tˤ", "ט (ط)"), ("sˤ", "צ (ص)"), ("dˤ", "ד׳ (ض)"), ("ZH", "ט׳ (ظ)")]), - ("אותיות עם כמה הגיות", [("Q", "ק (ق)"), ("K", "כ (ك)"), ("J", "ג׳ (ج)"), ("TH", "ת׳ (ث)"), ("DH", "ד״ (ذ)")]), - ("עיצורים נוספים", [("r", "ר (ر)"), ("l", "ל (ل)"), ("ʃ", "ש (ش)"), ("s", "ס (س)"), ("z", "ז (ز)"), ("t", "ת (ت)"), - ("d", "ד (د)"), ("k", "כ (ك)"), ("b", "ב (ب)"), ("f", "פ (ف)"), ("m", "מ (م)"), ("n", "נ (ن)"), - ("w", "ו (و)"), ("j", "י (ي)")]), -] -SOUNDS = {s for _, group in SOUND_GROUPS for s, _ in group} - - -def _get(cfg: ScorerConfig, key: str): - return getattr(cfg.gop, key[4:]) if key.startswith("gop.") else getattr(cfg, key) - - -def defaults(model_id: str) -> dict: - """What a model runs with before any stored setting: the fields, and the sounds with a built-in threshold.""" - cfg = scorer_config(model_id) - return {"fields": {f["key"]: _get(cfg, f["key"]) for f in FIELDS}, "sounds": dict(cfg.flag_below_sound)} - - -def clean(raw: dict) -> dict: - """A stored or submitted settings object, checked field by field. Raises ValueError (in Hebrew) on a bad one.""" - if not isinstance(raw, dict): - raise ValueError("ההגדרות צריכות להיות אובייקט") - out: dict = {} - for key, value in raw.items(): - if key == "flag_below_sound": - if not isinstance(value, dict): - raise ValueError("ספי ההגים צריכים להיות אובייקט") - sounds = {} - for sound, t in value.items(): - if sound not in SOUNDS: - raise ValueError(f"הגה לא מוכר: {sound}") - if t is not None and (isinstance(t, bool) or not isinstance(t, (int, float)) or not 0 <= t <= 100): - raise ValueError(f"סף ל־{sound} צריך להיות בין 0 ל־100") - sounds[sound] = None if t is None else float(t) - out[key] = sounds - continue - f = FIELD.get(key) - if f is None: - raise ValueError(f"הגדרה לא מוכרת: {key}") - kind = f["kind"] - if kind == "choice": - if value not in f["choices"]: - raise ValueError(f"{f['label']}: ערך לא מוכר") - elif kind == "window": - if value is not None and (isinstance(value, bool) or not isinstance(value, int) - or not f["min"] <= value <= f["max"]): - raise ValueError(f"{f['label']}: רחב, או מספר פריימים בין {f['min']} ל־{f['max']}") - else: - if isinstance(value, bool) or not isinstance(value, (int, float)) or not f["min"] <= value <= f["max"]: - raise ValueError(f"{f['label']}: ערך בין {f['min']} ל־{f['max']}") - if kind == "int" and int(value) != value: - raise ValueError(f"{f['label']}: מספר שלם") - value = int(value) if kind == "int" else float(value) - out[key] = value - return out - - -def build(model_id: str, settings: dict | None) -> ScorerConfig: - """The model's ScorerConfig with its stored settings applied.""" - cfg = scorer_config(model_id) - settings = clean(settings or {}) - gop = {k[4:]: v for k, v in settings.items() if k.startswith("gop.")} - top = {k: v for k, v in settings.items() if not k.startswith("gop.") and k != "flag_below_sound"} - sounds = dict(cfg.flag_below_sound) - for sound, t in settings.get("flag_below_sound", {}).items(): - if t is None: - sounds.pop(sound, None) - else: - sounds[sound] = t - return replace(cfg, **top, flag_below_sound=sounds, gop=replace(cfg.gop, **gop)) - - -def describe(model_id: str, settings: dict | None, sound_counts: dict[str, int]) -> dict: - """Everything the tab draws: each field with its default and value, each sound with its threshold.""" - base, cfg = defaults(model_id), build(model_id, settings) - stored = clean(settings or {}) - fields = [{**f, "group_label": GROUPS[f["group"]], "default": base["fields"][f["key"]], "value": _get(cfg, f["key"]), - "changed": f["key"] in stored} for f in FIELDS] - stored_sounds = stored.get("flag_below_sound", {}) - sounds = [{"group": name, "sounds": [{"sound": s, "label": label, "count": sound_counts.get(s, 0), - "default": base["sounds"].get(s), "value": cfg.flag_below_sound.get(s), - "changed": s in stored_sounds} for s, label in group]} - for name, group in SOUND_GROUPS] - return {"fields": fields, "sounds": sounds, "general": cfg.flag_below} diff --git a/app/models.py b/app/models.py deleted file mode 100644 index e944977a7ed234f59fe001b05e3c91d5cd891580..0000000000000000000000000000000000000000 --- a/app/models.py +++ /dev/null @@ -1,107 +0,0 @@ -"""The phoneme models the management panel's "מודלים" tab offers, with what each was measured to do. - -Numbers: Minerva test split (255 native lines), greedy CTC mapped onto the labels' phonemes, measured 2026-10-25 -with scripts/zero_shot_phone_models.py (docs/zero-shot-phone-models.md). Speeds: one 2.7 s recording, on this -project's laptop GPU and on 2 CPU threads (about what the free Space has). POWSM runs padded to 8 s (app/powsm.py): -its PER, consonant PER and long vowels below are measured that way; its other sounds at the 20 s they were first -measured at (8 s changed PER by 0.4 points). The short-padding fine-tune runs unpadded, measured that way throughout. -""" -from __future__ import annotations - -from pathlib import Path - -import os - -from .powsm import DEPS, ONNX_PREFIX, parse_model_id - -# the Hub repo the exported POWSM is uploaded to (scripts/export_powsm_onnx.py; README "POWSM on the Space") -POWSM_ONNX_REPO = os.environ.get("POWSM_ONNX_REPO", "wzender/ihki-powsm-ft").strip() - -ROOT = Path(__file__).resolve().parent.parent - -# Per model: recall of each sound (None: the model has no symbol for it), PER and consonant PER. -CATALOG: list[dict] = [ - {"id": "facebook/wav2vec2-xlsr-53-espeak-cv-ft", "name": "Facebook (מקורי)", "family": "wav2vec2", - "summary": "המודל הרב־לשוני שהאתר נבנה עליו. לא אומן על ערבית פלסטינית: כמעט לא שומע ח ו־ע.", - "trained": False, "free_space": True, - "metrics": {"per": .38, "consonant_per": .29, "ħ": .09, "ʕ": .02, "tˤ": None, "sˤ": None, "dˤ": 0.0, - "long_vowels": .45}, - "speed": {"gpu": .05, "cpu": .8}, "fps": 50}, - {"id": "checkpoints/ihki-ft-minerva", "name": "wav2vec2 מאומן", "family": "wav2vec2", - "summary": "אותו מודל, מאומן על הקלטות Minerva. מחליף ישיר: אותו גודל ואותה מהירות, גם על השרת החינמי.", - "trained": True, "free_space": True, - "metrics": {"per": .23, "consonant_per": .15, "ħ": .94, "ʕ": .69, "tˤ": None, "sˤ": None, "dˤ": .04, - "long_vowels": .74}, - "speed": {"gpu": .05, "cpu": .8}, "fps": 50}, - {"id": "checkpoints/powsm-ft-minerva/powsm_ft.pt", "name": "POWSM מאומן", "family": "POWSM-CTC", - "summary": "מודל פונטי חדש (2026), מאומן על הקלטות Minerva. הכי מדויק, והיחיד ששומע את העיצורים המודגשים (ط ص ض).", - "trained": True, "free_space": False, - "metrics": {"per": .16, "consonant_per": .10, "ħ": .94, "ʕ": .89, "tˤ": .82, "sˤ": .77, "dˤ": .46, - "long_vowels": .68}, - "speed": {"gpu": .29, "cpu": 1.8}, "fps": 25}, - {"id": "checkpoints/powsm-ft-shortpad/powsm_ft.pt", "name": "POWSM מאומן ומהיר", "family": "POWSM-CTC", - "summary": "אותו POWSM מאומן, שאומן שוב גם על הקלטות קצרות בלי ריפוד: מדויק כמו המקורי ומהיר פי 5 במעבד.", - "trained": True, "free_space": True, - "metrics": {"per": .16, "consonant_per": .105, "ħ": .92, "ʕ": .90, "tˤ": .81, "sˤ": .68, "dˤ": .64, - "long_vowels": .69}, - "speed": {"gpu": .06, "cpu": .8}, "fps": 25}, - {"id": f"{ONNX_PREFIX}{POWSM_ONNX_REPO}", "name": "POWSM מאומן ומהיר (לשרת)", "family": "POWSM-CTC · ONNX", - "summary": "אותו מודל, מיוצא ל־ONNX: רץ בלי ESPnet, גם על השרת החינמי, ומהיר מהמודל הנוכחי. נטען מ־Hugging Face.", - "trained": True, "free_space": True, - "metrics": {"per": .165, "consonant_per": .105, "ħ": .92, "ʕ": .90, "tˤ": .81, "sˤ": .68, "dˤ": .64, - "long_vowels": .69}, - "speed": {"gpu": None, "cpu": .57}, "fps": 25}, - {"id": "exports/powsm-ft-shortpad", "name": "POWSM מאומן ומהיר (ONNX מקומי)", "family": "POWSM-CTC · ONNX", - "summary": "הייצוא ל־ONNX מהתיקייה המקומית, לפני ההעלאה ל־Hugging Face.", - "trained": True, "free_space": True, - "metrics": {"per": .165, "consonant_per": .105, "ħ": .92, "ʕ": .90, "tˤ": .81, "sˤ": .68, "dˤ": .64, - "long_vowels": .69}, - "speed": {"gpu": None, "cpu": .57}, "fps": 25}, - {"id": "powsm:", "name": "POWSM (מקורי)", "family": "POWSM-CTC", - "summary": "POWSM כפי שפורסם, עם הנחיית ערבית. שומע ח ו־ע היטב בלי אימון, אבל לא כותב תנועות ארוכות.", - "trained": False, "free_space": False, - "metrics": {"per": .37, "consonant_per": .22, "ħ": .87, "ʕ": .85, "tˤ": .66, "sˤ": .72, "dˤ": .54, - "long_vowels": 0.0}, - "speed": {"gpu": .29, "cpu": 1.8}, "fps": 25}, -] - - -def availability(model_id: str) -> tuple[bool, str]: - """(can this server load it, why not). A Hub id counts as loadable: it downloads on first use.""" - if model_id.startswith(ONNX_PREFIX) or (Path(model_id) / "powsm_onnx.json").is_file(): - try: - import onnxruntime # noqa: F401 - except ImportError: - return False, "חסרה החבילה onnxruntime בשרת (requirements.txt)" - return True, "" - if model_id.startswith("exports/"): - return False, "הייצוא ל־ONNX לא נמצא בשרת (scripts/export_powsm_onnx.py)" - powsm = parse_model_id(model_id) - if powsm: - if not (DEPS / "pkgs").exists(): - return False, "חסרות חבילות ההרצה של POWSM בשרת (scripts/setup_zero_shot_deps.sh)" - if powsm[1] and not Path(powsm[1]).is_file(): - return False, "קובץ המודל לא נמצא בשרת" - return True, "" - local = Path(model_id) - if model_id.startswith(("checkpoints/", "./", "/")) or local.exists(): - if not (local / "config.json").is_file(): - return False, "תיקיית המודל לא נמצאה בשרת" - return True, "" - return True, "" - - -def catalog(active: str) -> list[dict]: - """Every catalog model, plus the active one when it isn't in the catalog (a MODEL_ID of its own).""" - rows = [dict(m) for m in CATALOG] - if not any(m["id"] == active for m in rows): - rows.insert(0, {"id": active, "name": "מודל מותאם", "family": "", "summary": "הוגדר ב־MODEL_ID; לא נמדד כאן.", - "trained": None, "free_space": None, "metrics": None, "speed": None, "fps": None}) - for m in rows: - m["available"], m["why"] = availability(m["id"]) - m["active"] = m["id"] == active - return rows - - -def known(model_id: str) -> bool: - return any(m["id"] == model_id for m in CATALOG) diff --git a/app/monitoring.py b/app/monitoring.py deleted file mode 100644 index dfdd84e28b833e440824fe5e6da41ed0c8f3908e..0000000000000000000000000000000000000000 --- a/app/monitoring.py +++ /dev/null @@ -1,718 +0,0 @@ -"""Read-only, privacy-preserving monitoring shared by MCP callers.""" -from __future__ import annotations - -import hashlib -import math -from collections import defaultdict -from datetime import date, datetime, time, timedelta, timezone -from typing import Any, Literal -from zoneinfo import ZoneInfo - -from .access import has_permission -from .auth import AuthUnavailable -from .class_management import ClassManagementError, visible_classes - - -LOCAL_TZ = ZoneInfo("Asia/Jerusalem") -MAX_DAYS = 90 -INTERVALS = frozenset({"day", "week", "month"}) -STATUSES = frozenset({"healthy", "degraded", "unavailable", "not_configured"}) -SUCCESS_SCORE = 75 -MIN_DIFFICULTY_STUDENTS = 3 -MIN_DIFFICULTY_ATTEMPTS = 5 -MAX_DIFFICULTY_RESULTS = 20 -MAX_ANALYTICS_RESULTS = 50 -EXERCISE_TYPES = frozenset({"pronunciation", "verb", "game", "other"}) -CURRENT_ACTIVITY_WINDOWS = frozenset({5, 15}) - - -class MonitoringError(ValueError): - """A safe monitoring error suitable for returning through MCP.""" - - -class MonitoringUnavailable(MonitoringError): - pass - - -def _date(value: object, label: str) -> date: - if not isinstance(value, str): - raise MonitoringError(f"{label} חייב להיות תאריך בפורמט YYYY-MM-DD") - try: - parsed = date.fromisoformat(value) - except ValueError as exc: - raise MonitoringError(f"{label} חייב להיות תאריך בפורמט YYYY-MM-DD") from exc - if parsed.isoformat() != value: - raise MonitoringError(f"{label} חייב להיות תאריך בפורמט YYYY-MM-DD") - return parsed - - -def validate_range(from_date: object, to_date: object, interval: object, - *, today: date | None = None) -> tuple[date, date, str]: - start, end = _date(from_date, "from_date"), _date(to_date, "to_date") - current = today or datetime.now(LOCAL_TZ).date() - if interval not in INTERVALS: - raise MonitoringError("interval חייב להיות day, week או month") - if start > end: - raise MonitoringError("from_date חייב להיות לפני או שווה ל-to_date") - if start > current or end > current: - raise MonitoringError("אי אפשר לבקש טווח עתידי") - if (end - start).days + 1 > MAX_DAYS: - raise MonitoringError("אפשר לבקש עד 90 ימים") - return start, end, str(interval) - - -def _scope(auth: Any, actor: dict[str, Any], class_id: int | None) -> tuple[list[dict], dict[str, Any]]: - if not has_permission(actor, "management.access"): - raise MonitoringError("אין הרשאת ניהול") - try: - classes = visible_classes(auth, actor, fresh=True) - except ClassManagementError as exc: - raise MonitoringUnavailable("נתוני הפעילות אינם זמינים") from exc - if class_id is not None: - if not isinstance(class_id, int) or isinstance(class_id, bool) or class_id <= 0: - raise MonitoringError("מזהה הכיתה אינו תקין") - selected = [row for row in classes if row.get("id") == class_id] - if not selected: - raise MonitoringError("הכיתה אינה זמינה") - row = selected[0] - return selected, {"class_id": row["id"], "class_name": row.get("name", "")} - return classes, {"class_id": None, "class_name": "כל הכיתות המורשות"} - - -def _scoped_user_ids(auth: Any, classes: list[dict]) -> set[str]: - emails = {str(email).strip().lower() for row in classes for email in row.get("emails", ()) if email} - if not emails: - return set() - users = auth.users() - return {str(user["id"]) for user in users - if str(user.get("email") or "").strip().lower() in emails and user.get("id")} - - -def _stamp(value: object) -> datetime: - text = str(value or "").replace("Z", "+00:00") - parsed = datetime.fromisoformat(text) - return (parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)).astimezone(LOCAL_TZ) - - -def _period_key(day: date, interval: str) -> date: - if interval == "day": - return day - if interval == "week": - return day - timedelta(days=day.weekday()) - return day.replace(day=1) - - -def activity_timeseries(auth: Any, actor: dict[str, Any], *, from_date: object, to_date: object, - interval: object = "day", class_id: int | None = None, - today: date | None = None) -> dict[str, Any]: - start, end, clean_interval = validate_range(from_date, to_date, interval, today=today) - classes, scope = _scope(auth, actor, class_id) - try: - user_ids = _scoped_user_ids(auth, classes) - since = datetime.combine(start, time.min, LOCAL_TZ).astimezone(timezone.utc) - raw = auth.scoped_activity(user_ids, since, 60) - except (AuthUnavailable, AttributeError, KeyError, TypeError) as exc: - raise MonitoringUnavailable("נתוני הפעילות אינם זמינים") from exc - - keys: list[date] = [] - cursor = start - while cursor <= end: - key = _period_key(cursor, clean_interval) - if not keys or keys[-1] != key: - keys.append(key) - cursor += timedelta(days=1) - grouped = {key: {"date": key.isoformat(), "attempts": 0, "peak_active_students": 0} for key in keys} - for item in raw.get("buckets", ()) if isinstance(raw, dict) else (): - try: - local_day = _stamp(item.get("bucket")).date() - if not start <= local_day <= end: - continue - bucket = grouped.get(_period_key(local_day, clean_interval)) - if bucket is None: - continue - bucket["attempts"] += max(0, int(item.get("attempts") or 0)) - bucket["peak_active_students"] = max( - bucket["peak_active_students"], max(0, int(item.get("active_students") or 0))) - except (TypeError, ValueError, OverflowError): - continue - - summary = raw.get("summary") if isinstance(raw, dict) and isinstance(raw.get("summary"), dict) else {} - safe_summary = {key: max(0, int(summary.get(key) or 0)) - for key in ("attempts", "active_students", "returning_students", "active_days")} - return {**scope, "from_date": start.isoformat(), "to_date": end.isoformat(), - "interval": clean_interval, "series": list(grouped.values()), "summary": safe_summary} - - -def _utc_iso(value: datetime) -> str: - return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") - - -def _activity_stamp(value: object) -> datetime | None: - try: - return _stamp(value).astimezone(timezone.utc) - except (TypeError, ValueError, OverflowError): - return None - - -def current_activity(auth: Any, actor: dict[str, Any], *, window_minutes: object = 5, - class_id: int | None = None, checked_at: datetime | None = None, - live_store: Any = None) -> dict[str, Any]: - """Return scoped aggregate recency without treating recent attempts as presence.""" - if (not isinstance(window_minutes, int) or isinstance(window_minutes, bool) - or window_minutes not in CURRENT_ACTIVITY_WINDOWS): - raise MonitoringError("window_minutes חייב להיות 5 או 15") - now = checked_at or datetime.now(timezone.utc) - if now.tzinfo is None: - now = now.replace(tzinfo=timezone.utc) - now = now.astimezone(timezone.utc) - classes, scope = _scope(auth, actor, class_id) - try: - user_ids = _scoped_user_ids(auth, classes) - since = now - timedelta(minutes=window_minutes) - rows, truncated = auth.scoped_attempts(user_ids, since, now + timedelta(microseconds=1)) - except (AuthUnavailable, AttributeError, KeyError, TypeError) as exc: - raise MonitoringUnavailable("נתוני הפעילות אינם זמינים") from exc - if truncated: - raise MonitoringError("יש יותר מדי אירועים בחלון. נסו חלון קצר יותר.") - - events: dict[str, tuple[str, datetime]] = {} - for index, row in enumerate(rows if isinstance(rows, list) else []): - user_id = str(row.get("user_id") or "") if isinstance(row, dict) else "" - stamp = _activity_stamp(row.get("created_at")) if isinstance(row, dict) else None - if not user_id or user_id not in user_ids or stamp is None or not since <= stamp <= now: - continue - event_id = str(row.get("id") or f"row-{index}") - events.setdefault(event_id, (user_id, stamp)) - - attempts = len(events) - learners = len({user_id for user_id, _ in events.values()}) - checked_iso = _utc_iso(now) - threshold = {"minimum_students": MIN_DIFFICULTY_STUDENTS, - "minimum_attempts": MIN_DIFFICULTY_ATTEMPTS} - recent: dict[str, Any] = { - "source": "persisted_attempts", "window_minutes": window_minutes, - "checked_at": checked_iso, "collection_delay_seconds": None, - "update_semantics": "current_as_of_database_read", - "presence_claim": False, - } - last_practice: dict[str, Any] = {"source": "persisted_attempts", "checked_at": checked_iso} - if not events: - recent["status"] = "no_activity" - last_practice.update(status="no_data", reason_code="no_observation_in_window") - elif learners < MIN_DIFFICULTY_STUDENTS or attempts < MIN_DIFFICULTY_ATTEMPTS: - recent["status"] = "suppressed" - last_practice.update(status="suppressed", reason_code="privacy_threshold") - else: - latest = max(stamp for _, stamp in events.values()) - recent.update(status="complete", students_with_activity=learners, attempts=attempts) - last_practice.update( - status="complete", event_type="practice_attempt", timestamp=_utc_iso(latest), - local_timestamp=latest.astimezone(LOCAL_TZ).isoformat(), timezone="Asia/Jerusalem", - elapsed_seconds=max(0, int((now - latest).total_seconds())), - ) - live_presence: dict[str, Any] = { - "status": "unavailable", "reason_code": "no_active_live_session", - "source": "live_session_heartbeat", "checked_at": checked_iso, - } - if live_store is not None: - try: - summary = live_store.presence_summary({int(row["id"]) for row in classes if row.get("id") is not None}) - if summary.get("active_session"): - active = max(0, int(summary.get("active_users") or 0)) - live_presence = { - "status": "suppressed" if len(user_ids) < MIN_DIFFICULTY_STUDENTS or active in {1, 2} - else "complete", - "source": "live_session_heartbeat", "checked_at": checked_iso, - "heartbeat_ttl_seconds": max(1, int(summary.get("heartbeat_ttl_seconds") or 20)), - "ephemeral": True, - "definition": "heartbeat_received_within_ttl", - } - if live_presence["status"] == "complete": - live_presence["active_users"] = active - else: - live_presence["reason_code"] = "privacy_threshold" - except (AttributeError, KeyError, TypeError, ValueError): - live_presence = { - "status": "unavailable", "reason_code": "live_source_unavailable", - "source": "live_session_heartbeat", "checked_at": checked_iso, - } - return { - **scope, "status": recent["status"], "checked_at": checked_iso, - "timezone": "Asia/Jerusalem", "privacy_threshold": threshold, - "recent_practice": recent, - "live_presence": live_presence, - "last_practice_activity": last_practice, - "last_application_activity": { - "status": "unavailable", "reason_code": "not_collected", - "checked_at": checked_iso, - "note": "General application activity is not stored; missing practice does not imply no app use.", - }, - } - - -def _exercise_id(category: str, phrase: str) -> str: - return hashlib.sha256(f"{category}\0{phrase}".encode()).hexdigest()[:12] - - -def _catalog() -> dict[str, dict[str, Any]]: - try: - from .phrases import CATEGORIES, GAMES - categories = {str(row["id"]): row for row in [*CATEGORIES, *GAMES]} - except (ImportError, KeyError, TypeError): - categories = {} - return categories - - -def _metadata(category: str, phrase: str) -> dict[str, Any]: - row = _catalog().get(category, {}) - unit = str(row.get("unit") or "") - if category == "verb-game" or "פועל" in unit: - kind = "verb" - elif row.get("bubble") or category.startswith("game-"): - kind = "game" - elif row: - kind = "pronunciation" - else: - kind = "other" - return {"exercise_id": _exercise_id(category, phrase), "exercise_name": phrase, - "exercise_type": kind, "topic": row.get("name"), "lesson": row.get("lesson")} - - -def normalize_attempts(rows: list[dict]) -> list[dict[str, Any]]: - normalized = [] - for row in rows: - try: - user_id, category = str(row.get("user_id") or "").strip(), str(row.get("cat") or "").strip() - phrase = " ".join(str(row.get("phrase") or "").split()) - score = float(row.get("score")) - stamp = _stamp(row.get("created_at")) - if not user_id or not category or not phrase or not math.isfinite(score): - continue - score = max(0.0, min(100.0, score)) - issues = [] - for issue in row.get("issues") or []: - if not isinstance(issue, dict): - continue - expected = str(issue.get("expected") or "").strip()[:20] - heard = str(issue.get("heard") or "").strip()[:20] - if not expected: - continue - word = issue.get("word") - issues.append({"expected": expected, "heard": heard, - "word": word if isinstance(word, int) and not isinstance(word, bool) else None, - "score": issue.get("score")}) - normalized.append({"id": int(row.get("id") or 0), "user_id": user_id, - "category": category, "phrase": phrase, "score": score, - "unrelated": bool(row.get("unrelated")), "stamp": stamp, - "local_date": stamp.date(), "issues": issues, - "model": str(row.get("model") or ""), - "version": str(row.get("version") or ""), - **_metadata(category, phrase)}) - except (TypeError, ValueError, OverflowError): - continue - return normalized - - -def _threshold(value: object, *, default: int = MIN_DIFFICULTY_ATTEMPTS) -> int: - if value is None: - return max(MIN_DIFFICULTY_ATTEMPTS, default) - if not isinstance(value, int) or isinstance(value, bool) or value < 1 or value > 10_000: - raise MonitoringError("min_attempts חייב להיות מספר בין 1 ל-10000") - return max(MIN_DIFFICULTY_ATTEMPTS, value if value is not None else default) - - -def _exercise_metrics(items: list[dict[str, Any]]) -> dict[str, Any]: - students: dict[str, list[dict[str, Any]]] = defaultdict(list) - for item in items: - students[item["user_id"]].append(item) - successes = sum(not item["unrelated"] and item["score"] >= SUCCESS_SCORE for item in items) - first_success, never, changes = [], 0, [] - for learner_items in students.values(): - ordered = sorted(learner_items, key=lambda item: (item["stamp"], item["id"])) - succeeded = [at for at, item in enumerate(ordered, 1) - if not item["unrelated"] and item["score"] >= SUCCESS_SCORE] - if succeeded: - first_success.append(succeeded[0]) - else: - never += 1 - if len(ordered) >= 2: - changes.append(ordered[-1]["score"] - ordered[0]["score"]) - count, learners = len(items), len(students) - return {"total_attempts": count, "unique_students": learners, - "successful_attempts": successes, - "success_rate": round(successes * 100 / count, 1) if count else None, - "average_attempts_per_student": round(count / learners, 2) if learners else None, - "average_score": round(sum(item["score"] for item in items) / count, 1) if count else None, - "average_attempts_to_first_success": round(sum(first_success) / len(first_success), 2) - if first_success else None, "never_succeeded_students": never, - "average_score_change": round(sum(changes) / len(changes), 1) if changes else None} - - -def aggregate_exercise_difficulty(rows: list[dict], *, limit: int, - min_attempts: int = MIN_DIFFICULTY_ATTEMPTS, - exercise_type: str | None = None) -> list[dict[str, Any]]: - grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) - for item in normalize_attempts(rows): - if exercise_type and item["exercise_type"] != exercise_type: - continue - grouped[item["exercise_id"]].append(item) - ranked = [] - for exercise_id, items in grouped.items(): - metrics = _exercise_metrics(items) - if metrics["unique_students"] < MIN_DIFFICULTY_STUDENTS or metrics["total_attempts"] < min_attempts: - continue - first = items[0] - ranked.append({"exercise_id": exercise_id, "exercise_name": first["exercise_name"], - "exercise_type": first["exercise_type"], "topic": first["topic"], - "category": first["category"], "exercise": first["phrase"], **metrics, - "attempt_count": metrics["total_attempts"], - "student_count": metrics["unique_students"]}) - ranked.sort(key=lambda item: (item["success_rate"], - -(item["average_attempts_to_first_success"] or math.inf), - -item["total_attempts"], item["exercise_id"])) - for at, item in enumerate(ranked[:limit], 1): - item["difficulty_rank"] = at - return ranked[:limit] - - -def exercise_difficulty(auth: Any, actor: dict[str, Any], *, from_date: object, to_date: object, - class_id: int | None = None, limit: object = 20, - exercise_type: str | None = None, min_attempts: object = None, - today: date | None = None) -> dict[str, Any]: - start, end, _ = validate_range(from_date, to_date, "day", today=today) - if not isinstance(limit, int) or isinstance(limit, bool) or not 1 <= limit <= MAX_DIFFICULTY_RESULTS: - raise MonitoringError("limit חייב להיות מספר בין 1 ל-20") - if exercise_type is not None and exercise_type not in EXERCISE_TYPES: - raise MonitoringError("exercise_type אינו מוכר") - effective = _threshold(min_attempts, default=MIN_DIFFICULTY_ATTEMPTS) - classes, scope = _scope(auth, actor, class_id) - try: - user_ids = _scoped_user_ids(auth, classes) - since = datetime.combine(start, time.min, LOCAL_TZ).astimezone(timezone.utc) - until = datetime.combine(end + timedelta(days=1), time.min, LOCAL_TZ).astimezone(timezone.utc) - rows, truncated = auth.scoped_attempts(user_ids, since, until) - except (AuthUnavailable, AttributeError, KeyError, TypeError) as exc: - raise MonitoringUnavailable("נתוני הקושי אינם זמינים") from exc - if truncated: - raise MonitoringError("יש יותר מדי ניסיונות לניתוח מלא. צמצמו את טווח התאריכים או בחרו כיתה אחת.") - exercises = aggregate_exercise_difficulty(rows, limit=limit, min_attempts=effective, - exercise_type=exercise_type) - return {**scope, "from_date": start.isoformat(), "to_date": end.isoformat(), - "status": "complete" if exercises else "insufficient_data", "success_score": SUCCESS_SCORE, - "exercise_type": exercise_type, - "privacy_threshold": {"minimum_students": MIN_DIFFICULTY_STUDENTS, - "minimum_attempts": effective}, - "definitions": {"success": "score >= 75 and unrelated = false", - "first_attempt": "first observed attempt in requested range"}, - "exercises": exercises, "returned": len(exercises)} - - -def pronunciation_difficulties(auth: Any, actor: dict[str, Any], *, from_date: object, to_date: object, - class_id: int | None = None, limit: object = 20, - min_attempts: object = 5, today: date | None = None) -> dict[str, Any]: - start, end, _ = validate_range(from_date, to_date, "day", today=today) - if not isinstance(limit, int) or isinstance(limit, bool) or not 1 <= limit <= MAX_ANALYTICS_RESULTS: - raise MonitoringError("limit חייב להיות מספר בין 1 ל-50") - effective = _threshold(min_attempts) - classes, scope = _scope(auth, actor, class_id) - try: - user_ids = _scoped_user_ids(auth, classes) - since = datetime.combine(start, time.min, LOCAL_TZ).astimezone(timezone.utc) - until = datetime.combine(end + timedelta(days=1), time.min, LOCAL_TZ).astimezone(timezone.utc) - result = auth.scoped_attempts(user_ids, since, until, include_diagnostics=True) - rows, truncated, diagnostics = result - except (AuthUnavailable, AttributeError, KeyError, TypeError, ValueError) as exc: - raise MonitoringUnavailable("נתוני ההגייה אינם זמינים") from exc - if truncated: - raise MonitoringError("יש יותר מדי ניסיונות לניתוח מלא. צמצמו את הטווח או בחרו כיתה אחת.") - base = {**scope, "from_date": start.isoformat(), "to_date": end.isoformat(), - "privacy_threshold": {"minimum_students": MIN_DIFFICULTY_STUDENTS, - "minimum_attempts": effective}} - if not diagnostics: - return {**base, "status": "insufficient_data", "diagnostics_available": False, - "phonemes": [], "returned": 0} - attempts = normalize_attempts(rows) - evaluated = [item for item in attempts if not item["unrelated"] and item["model"]] - groups: dict[str, dict[str, Any]] = defaultdict(lambda: { - "attempts": set(), "students": set(), "scores": [], "examples": set(), "dates": defaultdict(int)}) - for item in evaluated: - words = item["phrase"].split() - for issue in item["issues"]: - group = groups[issue["expected"]] - group["attempts"].add((item["user_id"], item["id"])) - group["students"].add(item["user_id"]) - group["dates"][_period_key(item["local_date"], "week")] += 1 - try: - score = float(issue["score"]) - if math.isfinite(score): - group["scores"].append(max(0.0, min(100.0, score))) - except (TypeError, ValueError): - pass - word = issue["word"] - if word is not None and 0 <= word < len(words): - group["examples"].add(words[word]) - phonemes = [] - for phoneme, group in groups.items(): - count, learners = len(group["attempts"]), len(group["students"]) - if count < effective or learners < MIN_DIFFICULTY_STUDENTS: - continue - dates = sorted(group["dates"]) - trend = (group["dates"][dates[-1]] - group["dates"][dates[0]]) if len(dates) >= 2 else None - phonemes.append({"phoneme": phoneme, "example_words": sorted(group["examples"])[:5], - "total_evaluated_attempts": len(evaluated), "error_attempts": count, - "pronunciation_error_rate": None, "affected_students": learners, - "average_pronunciation_score": round(sum(group["scores"]) / len(group["scores"]), 1) - if group["scores"] else None, - "trend": {"interval": "week", "error_attempt_change": trend}}) - phonemes.sort(key=lambda item: (-item["affected_students"], -item["error_attempts"], item["phoneme"])) - phonemes = phonemes[:limit] - return {**base, "status": "complete" if phonemes else "insufficient_data", - "diagnostics_available": True, - "diagnostic_coverage": {"evaluated_attempts": len(evaluated), "scored_attempts": len(attempts)}, - "rate_note": "null: stored diagnostics contain flagged errors but not correct phoneme exposure", - "phonemes": phonemes, "returned": len(phonemes)} - - -def _read_attempts(auth: Any, classes: list[dict], start: date, end: date) -> list[dict[str, Any]]: - try: - user_ids = _scoped_user_ids(auth, classes) - since = datetime.combine(start, time.min, LOCAL_TZ).astimezone(timezone.utc) - until = datetime.combine(end + timedelta(days=1), time.min, LOCAL_TZ).astimezone(timezone.utc) - rows, truncated = auth.scoped_attempts(user_ids, since, until) - except (AuthUnavailable, AttributeError, KeyError, TypeError, ValueError) as exc: - raise MonitoringUnavailable("נתוני הלמידה אינם זמינים") from exc - if truncated: - raise MonitoringError("יש יותר מדי ניסיונות לניתוח מלא. צמצמו את הטווח או בחרו כיתה אחת.") - return normalize_attempts(rows) - - -def _rate(numerator: int, denominator: int) -> float | None: - return round(numerator * 100 / denominator, 1) if denominator else None - - -def learning_progress(auth: Any, actor: dict[str, Any], *, from_date: object, to_date: object, - class_id: int | None = None, exercise_id: str | None = None, - interval: object = "week", min_attempts: object = 5, - today: date | None = None) -> dict[str, Any]: - start, end, clean_interval = validate_range(from_date, to_date, interval, today=today) - effective = _threshold(min_attempts) - classes, scope = _scope(auth, actor, class_id) - attempts = _read_attempts(auth, classes, start, end) - if exercise_id is not None: - if not isinstance(exercise_id, str) or len(exercise_id) != 12: - raise MonitoringError("התרגיל אינו זמין") - attempts = [item for item in attempts if item["exercise_id"] == exercise_id] - if not attempts: - raise MonitoringError("התרגיל אינו זמין") - - ordered = sorted(attempts, key=lambda item: (item["stamp"], item["id"])) - seen: set[tuple[str, str]] = set() - for item in ordered: - key = item["user_id"], item["exercise_id"] - item["retry"] = key in seen - seen.add(key) - keys, cursor = [], start - while cursor <= end: - key = _period_key(cursor, clean_interval) - if not keys or keys[-1] != key: - keys.append(key) - cursor += timedelta(days=1) - grouped: dict[date, list[dict[str, Any]]] = {key: [] for key in keys} - for item in ordered: - key = _period_key(item["local_date"], clean_interval) - if key in grouped: - grouped[key].append(item) - series = [] - for key, items in grouped.items(): - learners = {item["user_id"] for item in items} - if len(items) < effective or len(learners) < MIN_DIFFICULTY_STUDENTS: - series.append({"date": key.isoformat(), "status": "insufficient_data", "attempts": 0, - "unique_students": 0, "success_rate": None, "average_score": None, - "first_attempt_success_rate": None, "repeated_attempt_success_rate": None}) - continue - first = [item for item in items if not item["retry"]] - retries = [item for item in items if item["retry"]] - success = lambda item: not item["unrelated"] and item["score"] >= SUCCESS_SCORE - series.append({"date": key.isoformat(), "status": "complete", "attempts": len(items), - "unique_students": len(learners), - "success_rate": _rate(sum(success(item) for item in items), len(items)), - "average_score": round(sum(item["score"] for item in items) / len(items), 1), - "first_attempt_success_rate": _rate(sum(success(item) for item in first), len(first)), - "repeated_attempt_success_rate": _rate(sum(success(item) for item in retries), len(retries))}) - measured = [item for item in series if item["status"] == "complete"] - trend = (round(measured[-1]["average_score"] - measured[0]["average_score"], 1) - if len(measured) >= 2 else None) - return {**scope, "from_date": start.isoformat(), "to_date": end.isoformat(), - "interval": clean_interval, "exercise_id": exercise_id, - "status": "complete" if measured else "insufficient_data", - "privacy_threshold": {"minimum_students": MIN_DIFFICULTY_STUDENTS, - "minimum_attempts": effective}, - "definitions": {"success": "score >= 75 and unrelated = false", - "first_attempt": "first observed attempt in requested range"}, - "series": series, "improvement_trend": trend} - - -def compare_class_difficulties(auth: Any, actor: dict[str, Any], *, from_date: object, to_date: object, - class_ids: list[int] | None = None, exercise_type: str | None = None, - min_attempts: object = 5, today: date | None = None) -> dict[str, Any]: - start, end, _ = validate_range(from_date, to_date, "day", today=today) - effective = _threshold(min_attempts) - if exercise_type is not None and exercise_type not in EXERCISE_TYPES: - raise MonitoringError("exercise_type אינו מוכר") - visible, _ = _scope(auth, actor, None) - by_id = {row.get("id"): row for row in visible} - if class_ids is None: - selected = visible - else: - if (not isinstance(class_ids, list) or not class_ids or - any(not isinstance(item, int) or isinstance(item, bool) or item <= 0 for item in class_ids)): - raise MonitoringError("class_ids אינה רשימה תקינה") - if len(set(class_ids)) != len(class_ids) or any(item not in by_id for item in class_ids): - raise MonitoringError("אחת הכיתות אינה זמינה") - selected = [by_id[item] for item in class_ids] - user_sets = {int(row["id"]): _scoped_user_ids(auth, [row]) for row in selected} - overlap = any(user_sets[a] & user_sets[b] for at, a in enumerate(user_sets) for b in list(user_sets)[at + 1:]) - results = [] - for row in sorted(selected, key=lambda item: int(item["id"])): - attempts = _read_attempts(auth, [row], start, end) - if exercise_type: - attempts = [item for item in attempts if item["exercise_type"] == exercise_type] - learners, exercises = {item["user_id"] for item in attempts}, {item["exercise_id"] for item in attempts} - if len(attempts) < effective or len(learners) < MIN_DIFFICULTY_STUDENTS: - results.append({"class_id": row["id"], "class_name": row.get("name", ""), - "status": "insufficient_data"}) - continue - successes = sum(not item["unrelated"] and item["score"] >= SUCCESS_SCORE for item in attempts) - results.append({"class_id": row["id"], "class_name": row.get("name", ""), "status": "complete", - "total_attempts": len(attempts), "participating_students": len(learners), - "observed_exercises": len(exercises), "success_rate": _rate(successes, len(attempts)), - "average_score": round(sum(item["score"] for item in attempts) / len(attempts), 1), - "attempts_per_participant": round(len(attempts) / len(learners), 2), - "attempts_per_student_exercise_exposure": - round(len(attempts) / (len(learners) * len(exercises)), 2) if exercises else None}) - return {"from_date": start.isoformat(), "to_date": end.isoformat(), "exercise_type": exercise_type, - "status": "complete" if any(row["status"] == "complete" for row in results) else "insufficient_data", - "privacy_threshold": {"minimum_students": MIN_DIFFICULTY_STUDENTS, - "minimum_attempts": effective}, - "overlapping_enrollment": overlap, - "overlap_note": "class populations are not mutually exclusive" if overlap else None, - "classes": results, "returned": len(results)} - - -def _bottleneck_item(items: list[dict[str, Any]], *, label: str, identifier: str, - level: str) -> dict[str, Any]: - students: dict[str, list[dict[str, Any]]] = defaultdict(list) - for item in items: - students[item["user_id"]].append(item) - repeated_failures, first_success, changes, improved = 0, [], [], 0 - for learner_items in students.values(): - ordered = sorted(learner_items, key=lambda item: (item["stamp"], item["id"])) - failures = [item for item in ordered if item["unrelated"] or item["score"] < SUCCESS_SCORE] - if len(failures) >= 2: - repeated_failures += 1 - successes = [at for at, item in enumerate(ordered, 1) - if not item["unrelated"] and item["score"] >= SUCCESS_SCORE] - if successes: - first_success.append(successes[0]) - if len(ordered) >= 2: - change = ordered[-1]["score"] - ordered[0]["score"] - changes.append(change) - improved += change > 0 - affected, attempts = len(students), len(items) - repeated_rate = _rate(repeated_failures, affected) - improvement_rate = _rate(improved, len(changes)) - average_until = round(sum(first_success) / len(first_success), 2) if first_success else None - basis = [] - if repeated_rate is not None and repeated_rate >= 60: - basis.append("high_repeated_failure_rate") - if improvement_rate is not None and improvement_rate < 40: - basis.append("low_improvement_rate") - if average_until is None or average_until >= 3: - basis.append("slow_or_absent_success") - severity = "high" if len(basis) >= 2 else "medium" if basis else "low" - suggestion = ("לפרק את התרגיל לשלבים קצרים, להדגים שוב ולבצע תרגול מונחה" - if severity == "high" else "לשלב חזרה ממוקדת ומשוב מיידי") - return {"level": level, "id": identifier, "name": label, - "observed_metrics": {"affected_students": affected, "total_attempts": attempts, - "repeated_failure_rate": repeated_rate, - "average_attempts_until_success": average_until, - "improvement_rate": improvement_rate, - "average_score_change": round(sum(changes) / len(changes), 1) if changes else None}, - "severity": severity, - "suggested_intervention": {"type": "recommendation", "text": suggestion, "basis": basis}} - - -def learning_bottlenecks(auth: Any, actor: dict[str, Any], *, from_date: object, to_date: object, - class_id: int | None = None, exercise_type: str | None = None, - limit: object = 20, min_attempts: object = 10, - today: date | None = None) -> dict[str, Any]: - start, end, _ = validate_range(from_date, to_date, "day", today=today) - if not isinstance(limit, int) or isinstance(limit, bool) or not 1 <= limit <= MAX_ANALYTICS_RESULTS: - raise MonitoringError("limit חייב להיות מספר בין 1 ל-50") - if exercise_type is not None and exercise_type not in EXERCISE_TYPES: - raise MonitoringError("exercise_type אינו מוכר") - effective = _threshold(min_attempts, default=10) - classes, scope = _scope(auth, actor, class_id) - attempts = _read_attempts(auth, classes, start, end) - if exercise_type: - attempts = [item for item in attempts if item["exercise_type"] == exercise_type] - exercise_groups: dict[str, list[dict[str, Any]]] = defaultdict(list) - topic_groups: dict[str, list[dict[str, Any]]] = defaultdict(list) - for item in attempts: - exercise_groups[item["exercise_id"]].append(item) - if item["topic"]: - topic_groups[str(item["topic"])].append(item) - results = [] - for exercise_id, items in exercise_groups.items(): - if len(items) >= effective and len({item["user_id"] for item in items}) >= MIN_DIFFICULTY_STUDENTS: - results.append(_bottleneck_item(items, label=items[0]["exercise_name"], - identifier=exercise_id, level="exercise")) - for topic, items in topic_groups.items(): - if len(items) >= effective and len({item["user_id"] for item in items}) >= MIN_DIFFICULTY_STUDENTS: - results.append(_bottleneck_item(items, label=topic, - identifier=hashlib.sha256(topic.encode()).hexdigest()[:12], - level="topic")) - results = [item for item in results if item["severity"] != "low"] - severity_order = {"high": 0, "medium": 1} - results.sort(key=lambda item: (severity_order[item["severity"]], - -(item["observed_metrics"]["repeated_failure_rate"] or 0), item["id"])) - results = results[:limit] - return {**scope, "from_date": start.isoformat(), "to_date": end.isoformat(), - "exercise_type": exercise_type, "status": "complete" if results else "insufficient_data", - "privacy_threshold": {"minimum_students": MIN_DIFFICULTY_STUDENTS, - "minimum_attempts": effective}, - "bottlenecks": results, "returned": len(results)} - - -def system_health(app: Any, actor: dict[str, Any]) -> dict[str, Any]: - if not has_permission(actor, "management.access"): - raise MonitoringError("אין הרשאת ניהול") - state = app.state - auth = getattr(state, "auth", None) - database = "not_configured" if auth is None else "healthy" - if auth is not None: - try: - if hasattr(auth, "database_health"): - if not auth.database_health(): - database = "unavailable" - else: - auth.classes(fresh=True) - except Exception: # The response deliberately never carries infrastructure exception text. - database = "unavailable" - components = { - "application": "healthy", - "authentication": "healthy" if auth is not None else "not_configured", - "database": database, - "model": "healthy" if getattr(state, "scorer", None) is not None else "unavailable", - "text_to_speech": "healthy" if getattr(state, "tts", None) is not None else "not_configured", - } - required = (components["application"], components["authentication"], components["database"], components["model"]) - overall = "healthy" if all(value == "healthy" for value in required) else "degraded" - answer: dict[str, Any] = {"status": overall, - "checked_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), - "components": components} - if "developer" in actor.get("roles", ()): - info = getattr(state, "mcp_build_info", {}) - if isinstance(info, dict): - answer["build"] = {key: str(info[key])[:100] for key in ("version", "revision") if info.get(key)} - return answer diff --git a/app/nakdan.py b/app/nakdan.py deleted file mode 100644 index 3628d1ff41447a875e7630f11c41b623df2c0375..0000000000000000000000000000000000000000 --- a/app/nakdan.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Adds niqqud to Hebrew before it is read aloud: the TTS voices misread many unpointed words. - -Uses Dicta's free Nakdan service (no key). A word that already has niqqud (hand pointed, as in `he_say`) -is kept as written; every other word takes Nakdan's first choice. Results are cached on disk, so each -sentence is sent once. When the service cannot be reached, the text is returned as it was. - -Env: HEBREW_NIQQUD "0" turns it off (the tests do) - NAKDAN_URL optional; default Dicta's public endpoint - TTS_CACHE_DIR optional; default ~/.cache/ihki-tts (the cache is niqqud.json inside it) -""" -from __future__ import annotations - -import json -import logging -import os -import re -import threading -from pathlib import Path - -log = logging.getLogger("ihki.nakdan") -NIQQUD = re.compile(r"[֑-ׇ]") -HEBREW = re.compile(r"[א-ת]") -DEFAULT_URL = "https://nakdan-5-3.loadbalancer.dicta.org.il/api" -_lock = threading.Lock() -_cache: dict[str, str] | None = None - - -def _cache_path() -> Path: - return Path(os.environ.get("TTS_CACHE_DIR", "").strip() or Path.home() / ".cache" / "ihki-tts") / "niqqud.json" - - -def _load() -> dict[str, str]: - global _cache - if _cache is None: - try: - _cache = json.loads(_cache_path().read_text("utf-8")) - except (OSError, ValueError): - _cache = {} - return _cache - - -def _save() -> None: - path = _cache_path() - try: - path.parent.mkdir(parents=True, exist_ok=True) - tmp = path.with_suffix(".tmp") - tmp.write_text(json.dumps(_cache, ensure_ascii=False), "utf-8") - tmp.replace(path) - except OSError as e: - log.warning("niqqud cache write failed: %s: %s", type(e).__name__, e) - - -def _fetch(text: str) -> list[dict]: - import requests - - r = requests.post(os.environ.get("NAKDAN_URL", "").strip() or DEFAULT_URL, timeout=8, json={ - "task": "nakdan", "data": text, "genre": "modern", "addmorph": False, "keepqq": False, - "nodageshdefmem": False, "patachma": False, "keepmetagim": False}) - r.raise_for_status() - return r.json() - - -# second person endings, (said to a woman, said to a man): שְׁלוֹמֵךְ / שְׁלוֹמְךָ, שָׁבַרְתְּ / שָׁבַרְתָּ -ENDINGS = (("\u05b5\u05da\u05b0", "\u05b0\u05da\u05b8"), ("\u05ea\u05bc\u05b0", "\u05ea\u05bc\u05b8")) - - -def _choose(options: list[str], gender: str | None) -> str: - """Nakdan's first choice, unless the sentence is said to a man ("m") or a woman ("f") and it says otherwise.""" - first = options[0] - if gender in ("m", "f"): - for fem, male in ENDINGS: - wrong, right = (fem, male) if gender == "m" else (male, fem) - if first.endswith(wrong): - return next((o for o in options if o.endswith(right)), first) - return first - - -def _join(words: list[dict], gender: str | None = None) -> str: - out = [] - for w in words: - word = w.get("word", "") - options = w.get("options") or [] - if w.get("sep") or not options or NIQQUD.search(word) or not HEBREW.search(word): - out.append(word) # spaces, punctuation, numbers, and words already pointed - else: - best = _choose(options, gender).replace("|", "") # "בְּ|בַיִת": the bar marks a prefix - out.append(best if NIQQUD.sub("", best) == word else word) # never a different spelling - return "".join(out) - - -def point(text: str, gender: str | None = None) -> tuple[str, bool]: - """(text with niqqud, final). Final is False only when Nakdan failed: the text is unchanged and worth retrying. - - gender: "m" or "f" when the sentence is said to a man or a woman (its note, "(לגבר)" or "(לאישה)"). - """ - if os.environ.get("HEBREW_NIQQUD", "").strip() == "0" or not HEBREW.search(text): - return text, True - with _lock: - cache = _load() - key = f"{gender}|{text}" if gender else text - if key in cache: - return cache[key], True - try: - pointed = _join(_fetch(text), gender) - except Exception as e: # network, rate limit, odd reply: read it unpointed this time - log.warning("nakdan failed: %s: %s", type(e).__name__, e) - return text, False - if NIQQUD.sub("", pointed) != NIQQUD.sub("", text): - log.warning("nakdan changed more than niqqud, ignored: %r -> %r", text, pointed) - return text, False - cache[key] = pointed - _save() - return pointed, True diff --git a/app/phones.py b/app/phones.py deleted file mode 100644 index 3a3ff396e97c492a80e85944eca2ad4a0f366b85..0000000000000000000000000000000000000000 --- a/app/phones.py +++ /dev/null @@ -1,19 +0,0 @@ -"""The sound a model symbol stands for. - -The model's vocab is multilingual (392 symbols) and spells one sound several ways: t / t̪ / tʰ / tː, -r / ɾ, a / æ / ɐ, x / χ. Left alone, a correctly said t loses to its own spelling variant t̪. -base() names the sound a symbol stands for; Vocab.merge() (scorer.py) pools the posteriors of each sound, -and rules.py writes its rules in the same names. -""" -_STRIP = set("ːʰʲ.^[:\"̪̩̝̞̊̃") # length, aspiration, palatalisation, dental, syllabic, nasal ... -_SAME = {"æ": "a", "ɐ": "a", "ɑ": "a", "ä": "a", "ʌ": "a", "ɪ": "i", "ʊ": "u", "ɛ": "e", "ɔ": "o", - "ɾ": "r", "ɫ": "l", "ɡ": "g", "χ": "x", "ʁ": "ɣ", "ʒ": "dʒ", "dZ": "dʒ", "tS": "tʃ", "S": "ʃ", - "d͡ʒ": "dʒ", "t͡ʃ": "tʃ", "t͡s": "ts"} - - -def base(symbol: str) -> str: - """The sound a model symbol stands for, ignoring length and spelling variants. Emphatics stay distinct. - Mandarin tone numbers go too (i5, iː1, a2: 85 symbols in the espeak vocab): a correct ي heard as i5 otherwise - lost to its own tone variant (هيّ scored 36 on its vowel). A diphthong (ai5) stays a diphthong.""" - s = "".join(c for c in symbol if c not in _STRIP).rstrip("0123456789") - return _SAME.get(s, s) or symbol diff --git a/app/phrases.py b/app/phrases.py deleted file mode 100644 index b44b08a2f67dc776e62449043ffb29e22c7a9428..0000000000000000000000000000000000000000 --- a/app/phrases.py +++ /dev/null @@ -1,154 +0,0 @@ -"""Phrase bank. - -Each phrase: Hebrew prompt, Hebrew transliteration hint, category, and accepted variants. -A variant is (arabic, ipa). IPA phonemes are space-separated and words are separated by " | ". -The Arabic text and the IPA must have the same number of words (checked in tests). - -Conventions (Palestinian urban speech as the main form): - ق -> ʔ (urban); a variant with q or k is listed where the rural form is common. - ث -> t, ذ -> d (normal in the dialect). - ج -> dʒ. - Emphatics are written with ˤ (sˤ tˤ dˤ ðˤ), long vowels with ː. - Geminates are written doubled; the tokenizer collapses them for CTC. -""" -from .arabic_diacritics import point_arabic -from .grammar import day2_phrases -from .workbook import CATEGORIES as WORKBOOK_CATEGORIES, GAMES as WORKBOOK_GAMES, LESSON_TABS, LEVEL_TABS, workbook_phrases - -PHRASES = [ - # ---- היכרות ---- - dict(cat="meet", he="שלום", tr="מַרְחַבַּא", variants=[ - ("مرحبا", "m a r ħ a b a"), - ("اهلا", "ʔ a h l a"), - ("اهلين", "ʔ a h l eː n"), - ]), - dict(cat="meet", he="מה שלומך? (לגבר)", tr="כִּיפַכּ", variants=[ - ("كيفك", "k iː f a k"), - ("كيف حالك", "k iː f | ħ aː l a k"), - ]), - dict(cat="meet", he="אני בסדר, ברוך השם", tr="מְנִיח, אִלְחַמְדִלַّה", variants=[ - ("منيح الحمد للّه", "m n iː ħ | i l ħ a m d | i l l a"), - ("منيح والحمد للّه", "m n iː ħ | w i l ħ a m d | i l l a"), - ("تمام الحمد للّه", "t a m aː m | i l ħ a m d | i l l a"), - ]), - dict(cat="meet", he="איך קוראים לך? (לגבר)", tr="שוּ אִסְמַכּ", variants=[ - ("شو اسمك", "ʃ u | ʔ i s m a k"), - ("شو اسمك", "ʃ u | s m a k"), - ("ايش اسمك", "ʔ eː ʃ | ʔ i s m a k"), - ]), - dict(cat="meet", he="אני מירושלים", tr="אַנַא מִן אִלְקֻדְס", variants=[ - ("انا من القدس", "ʔ a n a | m i n | i l ʔ u d s"), - ("انا من القدس", "ʔ a n a | m i n | i l q u d s"), - ]), - dict(cat="meet", he="בוקר טוב", tr="צַבַּאח אִלְח'ֵיר", variants=[ - ("صباح الخير", "sˤ a b aː ħ | i l x eː r"), - ]), - dict(cat="meet", he="לילה טוב (לגבר)", tr="תִצְבַּח עַלַא ח'ֵיר", variants=[ - ("تصبح على خير", "t i sˤ b a ħ | ʕ a l a | x eː r"), - ]), - dict(cat="meet", he="תודה רבה", tr="שֻכְּרַן כְּתִיר", variants=[ - ("شكرا كتير", "ʃ u k r a n | k t iː r"), - ("مشكور كتير", "m a ʃ k uː r | k t iː r"), - ]), - dict(cat="meet", he="להתראות", tr="מַע אִ(ל)סַّלַאמֶה", variants=[ - ("مع السّلامة", "m a ʕ | i s s a l aː m e"), - ("يلّا باي", "j a l l a | b aː j"), - ]), - dict(cat="meet", he="סליחה, תסלח לי", tr="סַאמְחְנִי", variants=[ - ("سامحني", "s aː m ħ i n i"), - ("اسف", "ʔ aː s i f"), - ]), - # ---- שיחה ---- - dict(cat="talk", he="מה אתה עושה? (עכשיו)", tr="שוּ בְּתִעְמַל", variants=[ - ("شو بتعمل", "ʃ u | b t i ʕ m a l"), - ("شو بتسوّي", "ʃ u | b t s a w w i"), - ]), - dict(cat="talk", he="אני עובד בבית חולים", tr="בַּשְתְע'ִל בִּלְמֻסְתַשְפַא", variants=[ - ("بشتغل بالمستشفى", "b a ʃ t ɣ i l | b i l m u s t a ʃ f a"), - ("بشتغل في المستشفى", "b a ʃ t ɣ i l | f i | l m u s t a ʃ f a"), - ]), - dict(cat="talk", he="אני לא מבין (גבר)", tr="מִש פַאהֵם", variants=[ - ("مش فاهم", "m i ʃ | f aː h i m"), - ("ما فهمت", "m aː | f h i m t"), - ]), - dict(cat="talk", he="אתה יכול לדבר לאט?", tr="מֻמְכִּן תִחְכִּי שְוַיّ שְוַיّ", variants=[ - ("ممكن تحكي شويّ شويّ", "m u m k i n | t i ħ k i | ʃ w a j j | ʃ w a j j"), - ("ممكن تحكي شويّ", "m u m k i n | t i ħ k i | ʃ w a j j"), - ]), - dict(cat="talk", he="אין בעיה", tr="מִש מֻשְכִּלֶה", variants=[ - ("مش مشكلة", "m i ʃ | m u ʃ k i l e"), - ("ما في مشكلة", "m aː | f i | m u ʃ k i l e"), - ]), - dict(cat="talk", he="נתראה מחר", tr="בְּנְשוּף בַּעְד' בֻּכְּרַה", variants=[ - ("بنشوف بعض بكرة", "b n ʃ uː f | b a ʕ dˤ | b u k r a"), - ("منشوف بعض بكرة", "m n ʃ uː f | b a ʕ dˤ | b u k r a"), - ("بنشوفك بكرة", "b n ʃ uː f a k | b u k r a"), - ]), - dict(cat="talk", he="היום חם מאוד", tr="אִלְיוֹם שוֹבּ כְּתִיר", variants=[ - ("اليوم شوب كتير", "i l j oː m | ʃ oː b | k t iː r"), - ("اليوم حرّ كتير", "i l j oː m | ħ a r r | k t iː r"), - ("الجوّ حرّ كتير", "i l dʒ a w w | ħ a r r | k t iː r"), - ]), - # ---- בעיר ---- - dict(cat="city", he="איפה התחנה?", tr="וֵין אִלְמַחַטַّה", variants=[ - ("وين المحطّة", "w eː n | i l m a ħ a tˤ tˤ a"), - ]), - dict(cat="city", he="איפה השירותים?", tr="וֵין אִלְחַמַّאם", variants=[ - ("وين الحمّام", "w eː n | i l ħ a m m aː m"), - ]), - dict(cat="city", he="מה השעה?", tr="קַדֵّיש אִ(ל)סַّאעַה", variants=[ - ("قدّيش السّاعة", "ʔ a d d eː ʃ | i s s aː ʕ a"), - ("قدّيش السّاعة", "q a d d eː ʃ | i s s aː ʕ a"), - ("كم السّاعة", "k a m | i s s aː ʕ a"), - ]), - # ---- אוכל וקניות ---- - dict(cat="food", he="אני רוצה כוס קפה", tr="בִּדִّי פִנְג'אן קַהְוֶה", variants=[ - ("بدّي فنجان قهوة", "b i d d i | f i n dʒ aː n | ʔ a h w e"), - ("بدّي فنجان قهوة", "b i d d i | f i n dʒ aː n | q a h w e"), - ("بدّي كاسة قهوة", "b i d d i | k aː s e t | ʔ a h w e"), - ]), - dict(cat="food", he="תן לי מים, בבקשה", tr="אַעְטִינִי מַיّ לַוּ סַמַחְת", variants=[ - ("اعطيني ميّ لو سمحت", "ʔ a ʕ tˤ iː n i | m a j j | l a w | s a m a ħ t"), - ("عطيني ميّ لو سمحت", "ʕ tˤ iː n i | m a j j | l a w | s a m a ħ t"), - ]), - dict(cat="food", he="אני אוהב אוכל ערבי", tr="בַּחִבּّ אִלְאַכְּל אִלְעַרַבִּי", variants=[ - ("بحبّ الأكل العربي", "b a ħ i b b | i l ʔ a k l | i l ʕ a r a b i"), - ]), - dict(cat="food", he="כמה זה עולה?", tr="קַדֵّיש חַקّוֹ", variants=[ - ("قدّيش حقّه", "ʔ a d d eː ʃ | ħ a ʔ ʔ o"), - ("قدّيش حقّه", "q a d d eː ʃ | ħ a q q o"), - ("بقدّيش", "b i ʔ a d d eː ʃ"), - ]), - dict(cat="food", he="החשבון, בבקשה", tr="אִלְחְסַאבּ לַוּ סַמַחְת", variants=[ - ("الحساب لو سمحت", "i l ħ s aː b | l a w | s a m a ħ t"), - ("الحساب لو سمحت", "i l ħ i s aː b | l a w | s a m a ħ t"), - ]), -] -# ---- יום 2 ---- generated: every sentence pattern x pronoun x positive/negative (see grammar.py) -PHRASES += day2_phrases() -# ---- שיעורים ---- the workbook's translation exercises, lesson by lesson (see workbook.py) -PHRASES += workbook_phrases() - -# The page asks for a lesson first, then one of its categories: the course's lessons (workbook.py). -# The phrases that belong to no lesson (meet, talk, city, food, day2) have no tab. They stay in the -# bank, so phrase ids and the attempts recorded on them hold; public_phrases leaves them out. -LEVELS = [*LEVEL_TABS] -LESSONS = [*LESSON_TABS] # every planned lesson; one not written yet is `soon` and has no categories -CATEGORIES = [*WORKBOOK_CATEGORIES] -GAMES = [*WORKBOOK_GAMES] - - -def public_phrases(): - """What the frontend needs: no IPA, first variant's Arabic as the model answer. - - Grammar-built phrases also carry grp / pron / neg / listed / seq (see grammar.py); the page lists a - category's `listed` phrases, in `seq` order, and reaches the others with its negation and - conjugation buttons. Workbook phrases carry seq and read (see workbook.py). - """ - shown = {c["id"] for c in CATEGORIES + GAMES} - return [ - {"id": i, "cat": p["cat"], "he": p["he"], "tr": p["tr"], "ar": p["variants"][0][0], - "ar_pointed": point_arabic(*p["variants"][0]), - **{k: p[k] for k in ("grp", "pron", "neg", "listed", "seq", "read", "unlock", "binyan", "gizra", "speaker", "he_say") if k in p}} - for i, p in enumerate(PHRASES) if p["cat"] in shown - ] diff --git a/app/powsm.py b/app/powsm.py deleted file mode 100644 index 25f79ad08ef6ae8d0a5075f4808917cb6c5563d5..0000000000000000000000000000000000000000 --- a/app/powsm.py +++ /dev/null @@ -1,237 +0,0 @@ -"""POWSM-CTC (espnet/powsm_ctc) as the app's phoneme model: an Emitter like HFEmitter. - -Serve a fine-tune with MODEL_ID=checkpoints/powsm-ft-minerva/powsm_ft.pt (train/finetune_powsm.py), or the -published model with MODEL_ID=powsm:. It needs the extra packages from scripts/setup_zero_shot_deps.sh -(.zero-shot-deps/), which are not in requirements.txt: this is for trying the model locally, not yet for the Space. - -What the adapter changes so the scorer can read POWSM's output unchanged: -- columns: only POWSM's phone tokens (6,405 of 40,002; the rest are text pieces and task tags) and the blank, - renormalised. Phone names lose their slashes and tie bars (/d͡ʒ/ -> dʒ), the spelling the scorer's rules use. -- frames: the language/task prompt's two positions and the padding after the speech are dropped, and every frame - is repeated once. POWSM runs at 25 frames/s; the scorer's frame counts (vowel length, alignment padding) were - tuned at wav2vec2's 50. -""" -from __future__ import annotations - -import glob -import os -import sys -from pathlib import Path - -import numpy as np - -ROOT = Path(__file__).resolve().parent.parent -DEPS = ROOT / ".zero-shot-deps" -SAMPLE_RATE = 16_000 -# POWSM was trained on 20 s inputs (its model card says pad to 20 s) and loses accuracy without padding (test PER -# 16% -> 21%). 8 s keeps it (16.2%) at 1.8 s a recording on 2 CPU threads instead of 4.4 s. A fine-tune trained on -# random padding (train/finetune_powsm.py --pad random) needs none: unpadded it scores 16.2% at ~0.8 s. -# POWSM_PAD_SECONDS, when set, overrides both (0: no padding). -SECONDS = 8.0 - - -def pad_seconds(checkpoint: str | None) -> float: - """The padding a model runs with: POWSM_PAD_SECONDS if set, none for a short-padding fine-tune, else SECONDS.""" - env = os.environ.get("POWSM_PAD_SECONDS", "").strip() - if env: - return float(env) - if checkpoint: - import torch - args = torch.load(checkpoint, map_location="cpu").get("args") or {} - if args.get("pad") == "random": - return 0.0 - return SECONDS -PROMPT = 2 # the language and task tokens the encoder prepends to its output -UPSAMPLE = 2 # 25 frames/s -> the 50 the scorer was tuned at - - -def use_extra_deps() -> None: - """Make .zero-shot-deps importable, after the app's own packages so its torch/numpy always win.""" - if not (DEPS / "pkgs").exists(): - raise RuntimeError("POWSM needs extra packages: run scripts/setup_zero_shot_deps.sh first") - for sub in ("pkgs", "stubs"): - if str(DEPS / sub) not in sys.path: - sys.path.append(str(DEPS / sub)) - - -def patch_espnet_for_powsm_ctc() -> None: - """ESPnet 202511 (the newest release for Python 3.10) can't run POWSM-CTC as published: the encoder hands - Conv2dSubsampling the language/task prompt, which that release's layer doesn't accept. Same logic as ESPnet - master: linear -> prepend the prompt embeddings -> positional encoding.""" - import torch - from espnet.nets.pytorch_backend.transformer import subsampling - - def forward(self, x, x_mask, prefix_embeds=None): - x = self.conv(x.unsqueeze(1)) - b, c, t, f = x.size() - linear, pos_enc = self.out[0], self.out[1] - x = linear(x.transpose(1, 2).contiguous().view(b, t, c * f)) - if x_mask is not None: - x_mask = x_mask[:, :, :-2:2][:, :, :-2:2] - if prefix_embeds is not None: - x = torch.cat([prefix_embeds, x], dim=1) - if x_mask is not None: - ones = torch.ones(x_mask.shape[0], 1, prefix_embeds.size(1), dtype=x_mask.dtype, device=x_mask.device) - x_mask = torch.cat([ones, x_mask], dim=-1) - return pos_enc(x), x_mask - - subsampling.Conv2dSubsampling.forward = forward - - -def load_powsm(lang: str = "", checkpoint: str | None = None, device: str | None = None): - """espnet/powsm_ctc as ESPnet's greedy-CTC Speech2Text, with an optional fine-tune's weights on top.""" - use_extra_deps() - import torch - from huggingface_hub import snapshot_download - patch_espnet_for_powsm_ctc() - from espnet2.bin.s2t_inference_ctc import Speech2TextGreedySearch - device = device or ("cuda" if torch.cuda.is_available() else "cpu") - s2t = Speech2TextGreedySearch.from_pretrained("espnet/powsm_ctc", device=device, lang_sym=lang, task_sym="") - model = s2t.s2t_model - # The checkpoint stores the subsampling linear layer as `embed.out.*` (master's layout); the release expects - # `embed.out.0.*` and silently leaves it random. Copy it in. - ckpt = glob.glob(f"{snapshot_download('espnet/powsm_ctc')}/exp/s2t_train*/*.pth")[0] - state = torch.load(ckpt, map_location="cpu") - lin = model.encoder.embed.out[0] - with torch.no_grad(): - lin.weight.copy_(state["encoder.embed.out.weight"]) - lin.bias.copy_(state["encoder.embed.out.bias"]) - if checkpoint: # train/finetune_powsm.py saves only the weights it trained - saved = torch.load(checkpoint, map_location="cpu") - missing = set(saved["state"]) - set(model.state_dict()) - if missing: - raise KeyError(f"{checkpoint} has weights this model lacks: {sorted(missing)[:5]}") - model.load_state_dict(saved["state"], strict=False) - model.eval() - return s2t - - -def parse_model_id(model_id: str) -> tuple[str, str | None] | None: - """(language token, checkpoint) when MODEL_ID names a POWSM model, else None. - 'powsm' or 'powsm:' is the published model; a path to a .pt file is a fine-tune (always ).""" - if model_id == "powsm" or model_id.startswith("powsm:"): - return (model_id.split(":", 1)[1] if ":" in model_id else ""), None - if model_id.endswith(".pt"): - return "", model_id - return None - - -class PowsmEmitter: - """POWSM-CTC -> (T, V) log-posteriors over its phone tokens, at 50 frames/s (see the module docstring).""" - - def __init__(self, lang: str = "", checkpoint: str | None = None, device: str | None = None): - import torch - self.torch = torch - self.s2t = load_powsm(lang, checkpoint, device) - self.pad = pad_seconds(checkpoint) - model = self.s2t.s2t_model - tokens = model.token_list - cols, names = [model.blank_id], {"": 0} - for i, t in enumerate(tokens): - if len(t) > 2 and t[0] == "/" and t[-1] == "/": - name = t[1:-1].replace("͡", "") - if name not in names: # a tie-barred spelling that collides with a plain one keeps the plain one - names[name] = len(cols) - cols.append(i) - self.cols = torch.tensor(cols) - self.token_to_id: dict[str, int] = names - self.blank_token = "" - self.lang_id = self.s2t.converter.token2id[lang] - self.task_id = self.s2t.converter.token2id[""] - - def __call__(self, wav: np.ndarray) -> np.ndarray: - torch = self.torch - model, device = self.s2t.s2t_model, self.s2t.device - n = int(self.pad * SAMPLE_RATE) - x = np.pad(wav, (0, max(0, n - len(wav)))).astype(np.float32) # longer recordings run as they are - speech = torch.from_numpy(x)[None].to(device) - prefix = torch.tensor([[self.lang_id, self.task_id]], device=device) - prev = torch.tensor([[model.na]], device=device) - with torch.inference_mode(): - enc, _ = model.encode(speech, torch.tensor([speech.shape[1]], device=device), prev, - torch.tensor([1], device=device), prefix, torch.tensor([2], device=device)) - if isinstance(enc, tuple): - enc = enc[0] - enc = enc[0, PROMPT:] - keep = max(1, round(enc.shape[0] * len(wav) / len(x))) # frames that cover the speech - logits = model.ctc.ctc_lo(enc[:keep].float())[:, self.cols.to(device)] - logp = torch.log_softmax(logits, -1).cpu().numpy() - return np.repeat(logp, UPSAMPLE, axis=0) - - -# ---- the exported model (scripts/export_powsm_onnx.py): what the Space runs, with onnxruntime and torch only ---- - -ONNX_PREFIX = "powsm-onnx:" - - -def onnx_model_dir(model_id: str, download: bool = True) -> Path | None: - """The folder of an exported POWSM when MODEL_ID names one, else None: powsm-onnx: (downloaded once, - then from the Hugging Face cache) or a local folder with powsm_onnx.json.""" - if model_id.startswith(ONNX_PREFIX): - repo = model_id[len(ONNX_PREFIX):] - if not download: - return Path(repo) - from huggingface_hub import snapshot_download - return Path(snapshot_download(repo, allow_patterns=["powsm_onnx.json", "frontend.npz", onnx_precision_file()])) - local = Path(model_id) - return local if (local / "powsm_onnx.json").is_file() else None - - -def onnx_precision_file(meta: dict | None = None) -> str: - """The weights file POWSM_ONNX_PRECISION picks: fp32 (the default) or int8. On the Minerva test lines, 2 CPU - threads: fp32 PER 16.5% at 0.57 s a recording; int8 16.6% at 0.38 s, but ط 81% -> 76% and ص 68% -> 62%.""" - precision = os.environ.get("POWSM_ONNX_PRECISION", "fp32").strip().lower() or "fp32" - files = (meta or {}).get("files") or {"fp32": "model.onnx", "int8": "model.int8.onnx"} - return files.get(precision) or files["fp32"] - - -class PowsmOnnxEmitter: - """An exported POWSM-CTC -> (T, V) log-posteriors over its phone tokens at 50 frames/s, like PowsmEmitter but - without ESPnet. The front end (STFT, mel, log, global mean/variance) runs in torch from frontend.npz; the encoder - and CTC head in onnxruntime. The prompt (, ) is baked into the export.""" - - def __init__(self, folder: str, precision: str | None = None): - import json - - import onnxruntime as ort - import torch - self.torch, self.folder = torch, Path(folder) - self.meta = json.loads((self.folder / "powsm_onnx.json").read_text(encoding="utf-8")) - weights = (self.meta["files"].get(precision) or self.meta["files"]["fp32"]) if precision \ - else onnx_precision_file(self.meta) - opts = ort.SessionOptions() - opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - # a container can see more cores than it may use (the free Space has 2): POWSM_ONNX_THREADS caps them - threads = int(os.environ.get("POWSM_ONNX_THREADS", "0") or 0) - if threads > 0: - opts.intra_op_num_threads, opts.inter_op_num_threads = threads, 1 - self.session = ort.InferenceSession(str(self.folder / weights), opts, providers=["CPUExecutionProvider"]) - fe = np.load(self.folder / "frontend.npz") - self.window = torch.from_numpy(fe["window"].astype(np.float32)) - self.melmat = torch.from_numpy(fe["melmat"].astype(np.float32)) - self.mean = torch.from_numpy(fe["mean"].astype(np.float32)) - self.std = torch.from_numpy(fe["std"].astype(np.float32)) - self.n_fft, self.win, self.hop = int(fe["n_fft"]), int(fe["win_length"]), int(fe["hop_length"]) - self.token_to_id: dict[str, int] = self.meta["vocab"] - self.blank_token = "" - env = os.environ.get("POWSM_PAD_SECONDS", "").strip() - self.pad = float(env) if env else float(self.meta.get("pad_seconds", SECONDS)) - self.weights = weights - - def features(self, x: np.ndarray): - """ESPnet's DefaultFrontend + GlobalMVN: centred STFT, power, 80 mel bands, natural log, (x - mean) / std.""" - torch = self.torch - spec = torch.stft(torch.from_numpy(x), self.n_fft, hop_length=self.hop, win_length=self.win, - window=self.window, center=True, pad_mode="reflect", normalized=False, onesided=True, - return_complex=True) - power = spec.real ** 2 + spec.imag ** 2 # (F, T) - mel = torch.clamp(power.transpose(0, 1) @ self.melmat, min=1e-10).log() - return ((mel - self.mean) / self.std)[None].numpy() - - def __call__(self, wav: np.ndarray) -> np.ndarray: - n = int(self.pad * SAMPLE_RATE) - x = np.pad(wav, (0, max(0, n - len(wav)))).astype(np.float32) # a longer recording runs as it is - feats = self.features(x) - logp = self.session.run(None, {"feats": feats, "feats_lens": np.array([feats.shape[1]], dtype=np.int64)})[0][0] - keep = max(1, round(logp.shape[0] * len(wav) / len(x))) # frames that cover the speech - return np.repeat(logp[:keep], UPSAMPLE, axis=0) diff --git a/app/prefetch.py b/app/prefetch.py deleted file mode 100644 index 614b6e708b19e15a87e577ef06e3a2b021843373..0000000000000000000000000000000000000000 --- a/app/prefetch.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Downloads MODEL_ID's files while the Docker image is built, so a cold start doesn't (see the Dockerfile). - - MODEL_ID=powsm-onnx: python -m app.prefetch # an exported POWSM (scripts/export_powsm_onnx.py) - MODEL_ID=facebook/wav2vec2-xlsr-53-espeak-cv-ft python -m app.prefetch - -A private repo needs HF_TOKEN (the Dockerfile passes the Space secret of that name to this step only). -""" -from __future__ import annotations - -import os -import sys - -from .powsm import ONNX_PREFIX, onnx_model_dir, parse_model_id - -DEFAULT = "facebook/wav2vec2-xlsr-53-espeak-cv-ft" - - -def main() -> None: - model_id = os.environ.get("MODEL_ID", "").strip() or DEFAULT - if model_id.startswith(ONNX_PREFIX): - print("exported POWSM:", onnx_model_dir(model_id)) - return - if parse_model_id(model_id): - sys.exit(f"MODEL_ID={model_id} needs ESPnet, which the image doesn't have: export it with " - f"scripts/export_powsm_onnx.py and use MODEL_ID={ONNX_PREFIX}") - from huggingface_hub import hf_hub_download - from transformers import AutoFeatureExtractor, Wav2Vec2ForCTC - AutoFeatureExtractor.from_pretrained(model_id) - Wav2Vec2ForCTC.from_pretrained(model_id) - hf_hub_download(model_id, "vocab.json") - print("wav2vec2:", model_id) - - -if __name__ == "__main__": - main() diff --git a/app/prompt_tts.py b/app/prompt_tts.py deleted file mode 100644 index e7b5b42994e47d477ab6a81c276b36f1974fea53..0000000000000000000000000000000000000000 --- a/app/prompt_tts.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Reads the Hebrew prompt aloud with a free TTS engine: edge-tts (Microsoft Edge's online voices) -or gTTS (Google Translate's voice). The page asks for edge-tts in Edge and gTTS elsewhere. - -Before it is read, the text gets niqqud (nakdan.py): unpointed Hebrew is often misread. - -No API key. Both engines call an unofficial public endpoint, so either can fail or be rate limited: -the other one is tried before giving up. Each prompt is generated once per engine and cached on disk. - -Env: HEBREW_VOICE edge-tts voice (default he-IL-HilaNeural; he-IL-AvriNeural is the male one) - TTS_CACHE_DIR optional; default ~/.cache/ihki-tts -""" -from __future__ import annotations - -import asyncio -import hashlib -import io -import logging -import os -import re -import threading -from pathlib import Path - -from .nakdan import point - -log = logging.getLogger("ihki.prompt_tts") -ENGINES = ("edge", "gtts") -_lock = threading.Lock() - - -class PromptTTSUnavailable(Exception): - pass - - -def _edge(text: str) -> bytes: - import edge_tts - - voice = os.environ.get("HEBREW_VOICE", "").strip() or "he-IL-HilaNeural" - - async def run() -> bytes: - out = b"" - async for chunk in edge_tts.Communicate(text, voice).stream(): - if chunk["type"] == "audio": - out += chunk["data"] - return out - - return asyncio.run(run()) - - -def _gtts(text: str) -> bytes: - from gtts import gTTS - - buf = io.BytesIO() - gTTS(text, lang="iw").write_to_fp(buf) # "iw" is Google's code for Hebrew - return buf.getvalue() - - -_SYNTH = {"edge": _edge, "gtts": _gtts} - - -def spoken(he: str) -> str: - """The prompt as it should be read: without bracketed notes such as "(לגבר)".""" - return re.sub(r"\s*\([^)]*\)", "", he).strip() - - -def gender_of(he: str) -> str | None: - """"m" or "f" when the note says who the sentence is said to: "(לגבר)", "(לאישה)".""" - return "m" if "(לגבר)" in he else "f" if "(לאישה)" in he else None - - -def hebrew_mp3(he: str, engine: str, cache_it: bool = True) -> tuple[bytes, str]: - """(MP3, engine that produced it) for a Hebrew text; `engine` is tried first, then the other. - - Prompts are cached; one-off texts (the spoken assessment of driving mode) pass cache_it=False. - A text that could not get niqqud is read as it is and not cached, so it is pointed next time. - """ - text, final = point(spoken(he), gender_of(he)) - cache_it = cache_it and final - cache = Path(os.environ.get("TTS_CACHE_DIR", "").strip() or Path.home() / ".cache" / "ihki-tts") / "he" - order = [engine] + [e for e in ENGINES if e != engine] - with _lock: - for name in order: - path = cache / f"{name}-{hashlib.sha1(text.encode()).hexdigest()}.mp3" - if cache_it and path.exists(): - return path.read_bytes(), name - try: - data = _SYNTH[name](text) - except Exception as e: # network, rate limit, missing package: fall through to the other engine - log.warning("hebrew tts %s failed: %s: %s", name, type(e).__name__, e) - continue - if not data: - continue - if not cache_it: - return data, name - try: - cache.mkdir(parents=True, exist_ok=True) - tmp = path.with_suffix(".tmp") - tmp.write_bytes(data) - tmp.replace(path) - except OSError as e: # read-only or full cache dir: the audio is still good, it just isn't kept - log.warning("hebrew tts cache write failed: %s: %s", type(e).__name__, e) - return data, name - raise PromptTTSUnavailable("hebrew tts unavailable") diff --git a/app/push.py b/app/push.py deleted file mode 100644 index 3047c4828071bae33b753448a2d318aa4dae74ad..0000000000000000000000000000000000000000 --- a/app/push.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Push notifications: each morning, a reminder of the day's daily video (see "Daily videos" in auth.py). - -A student turns them on from the daily videos' screen. The browser gives a subscription (an address of its push service -and two keys), kept in the Supabase table `push_subs` with the account's email; only the server's secret key reads it. -Every few minutes the server looks whether it is PUSH_HOUR or later in Jerusalem; if so, each subscription whose account -has a daily video today (a class's student or teacher, or, for a staff account, any class's) and was not reminded today -gets one notification. A subscription the push service no longer knows (404/410) is deleted. - -On an iPhone the browser sends them only from the app added to the home screen (iOS 16.4 and later). - -Env: VAPID_PRIVATE_KEY (the server's push key; make one with python -m app.push and keep it as a Space secret. - Changing it drops every subscription: each device has to turn the reminders on again.) - VAPID_SUBJECT (a contact the push services may write to, "mailto:..."; default: the first developer's email) - PUSH_HOUR (the hour of the reminder in Jerusalem, 0-23; default 8) -""" -from __future__ import annotations - -import asyncio -import base64 -import json -import logging -import os -import urllib.parse -from datetime import date, datetime -from typing import Callable - -from .auth import AuthUnavailable - -log = logging.getLogger("ihki.push") -CHECK_EVERY = 300 # seconds between looks at the clock -MAX_ENDPOINT = 1000 -CREATE_PUSH_SUBS = ("create table public.push_subs (endpoint text primary key, email text not null, p256dh text not null, " - "auth text not null, every_class boolean not null default false, last_daily date, " - "created_at timestamptz not null default now()); " - "alter table public.push_subs enable row level security; " - "revoke all on public.push_subs from anon, authenticated;") - - -def _b64(raw: bytes) -> str: - return base64.urlsafe_b64encode(raw).rstrip(b"=").decode() - - -def new_private_key() -> str: - """A new VAPID private key, as VAPID_PRIVATE_KEY keeps it: the raw 32 bytes, base64url.""" - from cryptography.hazmat.primitives.asymmetric import ec - return _b64(ec.generate_private_key(ec.SECP256R1()).private_numbers().private_value.to_bytes(32, "big")) - - -def public_key(private: str) -> str: - """The browser's applicationServerKey for this private key: the uncompressed point, base64url.""" - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import ec - raw = base64.urlsafe_b64decode(private + "=" * (-len(private) % 4)) - key = ec.derive_private_key(int.from_bytes(raw, "big"), ec.SECP256R1()) - return _b64(key.public_key().public_bytes(serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint)) - - -class Push: - def __init__(self, auth, private: str = "", subject: str = "", hour: int = 8): - self.auth, self.private, self.subject, self.hour = auth, private.strip(), subject, hour - self.public, self.why = "", "" # why: what keeps the reminders off, in words for the page - if not self.private: - self.why = "VAPID_PRIVATE_KEY is not set" - elif not getattr(auth, "secret", ""): - self.why = "SUPABASE_SECRET_KEY is not set" - else: - try: - self.public = public_key(self.private) - except ValueError as e: - log.error("VAPID_PRIVATE_KEY is not a valid key: push reminders are off (%s)", e) - self.why = "VAPID_PRIVATE_KEY is not a valid key" - - @property - def on(self) -> bool: - return not self.why - - # the table - def _rows(self, query: str = "") -> list[dict]: - try: - return self.auth._admin("/rest/v1/push_subs?select=*" + query) or [] - except AuthUnavailable as e: - if "answered 404" in str(e): - raise AuthUnavailable(f"the table push_subs is missing; run in Supabase's SQL editor: {CREATE_PUSH_SUBS}") from e - raise - - def save(self, email: str, sub: dict, every_class: bool) -> None: - """Keeps a browser's subscription, raising ValueError when it is not one.""" - endpoint, keys = str(sub.get("endpoint") or ""), sub.get("keys") or {} - if not endpoint.startswith("https://") or len(endpoint) > MAX_ENDPOINT or not isinstance(keys, dict): - raise ValueError("not a push subscription") - p256dh, auth = str(keys.get("p256dh") or ""), str(keys.get("auth") or "") - if not (0 < len(p256dh) <= 200 and 0 < len(auth) <= 100): - raise ValueError("not a push subscription") - self._rows("&limit=0") # a missing table is told in words that say how to make it - self.auth._admin("/rest/v1/push_subs?on_conflict=endpoint", "POST", - {"endpoint": endpoint, "email": email.lower(), "p256dh": p256dh, "auth": auth, - "every_class": every_class}, prefer="resolution=merge-duplicates,return=minimal") - - def delete(self, endpoint: str, email: str | None = None) -> None: - q = {"endpoint": f"eq.{endpoint}", **({"email": f"eq.{email.lower()}"} if email else {})} - self.auth._admin("/rest/v1/push_subs?" + urllib.parse.urlencode(q), "DELETE", prefer="return=minimal") - - def _sent(self, endpoint: str, day: str) -> None: - self.auth._admin("/rest/v1/push_subs?" + urllib.parse.urlencode({"endpoint": f"eq.{endpoint}"}), "PATCH", - {"last_daily": day}, prefer="return=minimal") - - # sending - def send(self, row: dict, payload: dict) -> bool: - """One notification; False when it did not go (a subscription that is gone is deleted).""" - from pywebpush import WebPushException, webpush - try: - webpush({"endpoint": row["endpoint"], "keys": {"p256dh": row["p256dh"], "auth": row["auth"]}}, - json.dumps(payload, ensure_ascii=False), vapid_private_key=self.private, - vapid_claims={"sub": self.subject}, ttl=12 * 3600, timeout=15) - return True - except WebPushException as e: - status = getattr(e.response, "status_code", None) - if status in (404, 410): - log.info("push subscription of %s is gone: deleted", row.get("email")) - try: - self.delete(row["endpoint"]) - except AuthUnavailable as e2: - log.warning("push subscription not deleted: %s", e2) - else: - log.warning("push to %s failed: %s", row.get("email"), e) - except Exception as e: # a network failure: tried again on the next look - log.warning("push to %s failed: %s", row.get("email"), e) - return False - - def every(self) -> list[dict]: - """Every subscription (an admin's message goes to some of them).""" - return self._rows() - - def send_many(self, rows: list[dict], message: dict) -> int: - """One notification to each, a few at a time; how many went.""" - from concurrent.futures import ThreadPoolExecutor - if not rows: - return 0 - with ThreadPoolExecutor(max_workers=min(8, len(rows))) as pool: - return sum(pool.map(lambda r: self.send(r, message), rows)) - - def mine(self, email: str) -> list[dict]: - return self._rows("&" + urllib.parse.urlencode({"email": f"eq.{email.lower()}"})) - - def remind(self, today_of: Callable[[str, bool], list[dict]], day: str) -> int: - """Sends the day's reminder to each subscription not reminded on `day`; how many went. - `today_of(email, every_class)` gives the daily videos that account has on that day.""" - sent = 0 - for row in self._rows("&" + urllib.parse.urlencode({"or": f"(last_daily.is.null,last_daily.lt.{day})"})): - videos = today_of(row["email"], bool(row.get("every_class"))) - if not videos: - continue - if self.send(row, payload(videos)): - sent += 1 - self._sent(row["endpoint"], day) - return sent - - -MAX_TITLE, MAX_BODY = 60, 240 - - -def message(title: str, body: str) -> dict: - """An admin's notification; ValueError when it is empty or too long. Each has its own tag, so one does not replace another.""" - title, body = " ".join(title.split()), body.strip() - if not title or len(title) > MAX_TITLE or len(body) > MAX_BODY: - raise ValueError(f"a title of 1-{MAX_TITLE} characters, and a message of up to {MAX_BODY}") - return {"title": title, "body": body, "url": "/", "tag": f"msg-{datetime.now().timestamp():.0f}"} - - -def payload(videos: list[dict]) -> dict: - """What the service worker shows: the day's video (or how many there are).""" - body = videos[0]["title"] if len(videos) == 1 else f"{len(videos)} סרטונים חדשים: " + " · ".join(v["title"] for v in videos) - return {"title": "🎬 הסרטון היומי מחכה לכם", "body": body, "url": "/?view=daily", "tag": "daily-video"} - - -async def loop(push: Push, today_of: Callable[[str, bool], list[dict]], now: Callable[[], datetime]) -> None: - """Looks at the clock every CHECK_EVERY seconds and sends the day's reminders from PUSH_HOUR on.""" - while True: - try: - t = now() - if t.hour >= push.hour: - n = await asyncio.to_thread(push.remind, today_of, t.date().isoformat()) - if n: - log.info("daily video reminders sent: %d", n) - except AuthUnavailable as e: - log.warning("daily video reminders not sent: %s", e) - except Exception: - log.exception("daily video reminders failed") - await asyncio.sleep(CHECK_EVERY) - - -def load_push(auth) -> Push | None: - if auth is None: - return None - subject = os.environ.get("VAPID_SUBJECT", "").strip() - if not subject: - dev = sorted(getattr(auth, "developers", ()) or getattr(auth, "teachers", ())) - subject = f"mailto:{dev[0]}" if dev else "mailto:admin@example.com" - try: - hour = max(0, min(23, int(os.environ.get("PUSH_HOUR", "8")))) - except ValueError: - hour = 8 - push = Push(auth, os.environ.get("VAPID_PRIVATE_KEY", ""), subject, hour) - log.info("daily video reminders: %s", f"on, at {hour}:00" if push.on else f"off ({push.why})") - return push - - -if __name__ == "__main__": - key = new_private_key() - print(f"VAPID_PRIVATE_KEY={key}\n# its public key (the page reads it from the server): {public_key(key)}") diff --git a/app/reference.py b/app/reference.py deleted file mode 100644 index e9d2ba6963e4fd64fc561e1ee3eb8c7309e4f15e..0000000000000000000000000000000000000000 --- a/app/reference.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Native baseline for the scores: the playback voices saying the phrase, scored by the same model. - -The phoneme model mishears some sounds even from a native (ʕ in على as dropped, l as r), and a -learner should not be marked down or get a tip for those. So each practised phrase is also scored -on its TTS model answer, and Scorer measures the learner against that (see Scorer.score_logp). - -One voice is generated per phrase (the first of the list); the other voices are used only when -the page has already played them, so calibration costs at most one TTS call per phrase. -References are built in the background and kept in memory; a score waits a few seconds for them -and is given without calibration if they are not ready by then. -""" -from __future__ import annotations - -import logging -import threading -import time -from dataclasses import replace -from concurrent.futures import Future, ThreadPoolExecutor, wait - -from .arabic_diacritics import tts_arabic -from .scorer import Reference, Scorer, decode_audio -from .tts import voice_name - -log = logging.getLogger("ihki.reference") - - -class References: - def __init__(self, scorer: Scorer, tts, voices: list[str], wait_s: float = 8.0, retry_s: float = 300.0): - self.scorer, self.tts, self.voices = scorer, tts, voices - self.wait_s, self.retry_s = wait_s, retry_s - self._pool = ThreadPoolExecutor(max_workers=1, thread_name_prefix="ihki-ref") - self._jobs: dict[tuple[int, str], Future] = {} - self._retry_at: dict[tuple[int, str], float] = {} # failed builds are not retried before this time - self._lock = threading.Lock() - - def _build(self, phrase: dict, text: str, voice: str) -> Reference | None: - # wav() would turn a voice it does not offer for playback (فدوى) into Raef's - data, _ = self.tts.wav_for_voice(text, voice, source="baseline", upload=True) - ref = self.scorer.reference(decode_audio(data), phrase) - if ref is None: - log.info("reference not usable: %r by voice %s", text, voice) - return None - return replace(ref, voice=voice_name(voice)) - - def get(self, phrase_id: int, phrase: dict) -> list[Reference]: - """The references of a phrase that are ready within the wait.""" - text = tts_arabic(*phrase["variants"][0]) - jobs: list[tuple[tuple[int, str], Future]] = [] - with self._lock: - for n, voice in enumerate(self.voices): - key = (phrase_id, voice) - job = self._jobs.get(key) - if (job is None and time.monotonic() >= self._retry_at.get(key, 0) - and (n == 0 or self.tts.cached(text, voice))): - job = self._jobs[key] = self._pool.submit(self._build, phrase, text, voice) - if job is not None: - jobs.append((key, job)) - done, _ = wait([job for _, job in jobs], timeout=self.wait_s) - refs = [] - for key, job in jobs: - if job not in done: - continue - try: - ref = job.result() - except Exception as e: # provider down, undecodable audio: score without this voice - log.warning("reference for phrase %s failed: %s", key[0], e) - with self._lock: - if self._jobs.get(key) is job: - del self._jobs[key] - self._retry_at[key] = time.monotonic() + self.retry_s - continue - if ref is not None: - refs.append(ref) - return refs - - def invalidate(self, phrase_id: int | None = None, voice: str | None = None) -> int: - """Forget matching in-memory baselines so the next score rebuilds them from the current WAV.""" - with self._lock: - keys = [key for key in self._jobs - if (phrase_id is None or key[0] == phrase_id) and (voice is None or key[1] == voice)] - for key in keys: - job = self._jobs.pop(key) - job.cancel() # harmless when generation or analysis has already started - self._retry_at.pop(key, None) - retry_keys = [key for key in self._retry_at - if (phrase_id is None or key[0] == phrase_id) and (voice is None or key[1] == voice)] - for key in retry_keys: - self._retry_at.pop(key, None) - return len(keys) diff --git a/app/rules.py b/app/rules.py deleted file mode 100644 index 90f1bc10f9cb848e3c6aa0d0520c2f5a96ccb46a..0000000000000000000000000000000000000000 --- a/app/rules.py +++ /dev/null @@ -1,433 +0,0 @@ -"""Pronunciation rules for Hebrew-speaking learners, read from the yaml blocks of pronunciation-rules.md. - -The document is written for two audiences: its student sections are served as a guide, and its scorer -sections (the ```yaml blocks) say what the scorer accepts and how it judges errors: - - archiphonemes letters said differently in the urban and the rural dialect, and their correct realisations - checks per rule: (target, heard) -> severity and the Hebrew feedback - scoring margins per (target, heard) pair, max_tips, ... - heard_labels the Hebrew name of every heard symbol - -Every symbol is stored by its base() name (phones.py), the names the scorer compares in, so a rule written -with ʁ matches a model that heard ɣ. check_detectable() tells which rules a model can catch at all. -""" -from __future__ import annotations - -import re -import math -from dataclasses import dataclass, field -from functools import lru_cache -from pathlib import Path -from typing import Callable - -import yaml - -from .phones import base - -RULES_PATH = Path(__file__).resolve().parent.parent / "static" / "assets" / "pronunciation-rules.md" - -# Realisations correct only in learned words (ثقافة as saqaafe): accepted where the phrase writes them, not elsewhere. -LEARNED_ONLY = {"TH": {"s"}, "DH": {"z"}} -# Classes set by position, not by letter (section 6.3 #5 and #6): the vowel of a final ة, the vowel of a pronoun ending. -POSITIONAL = {"E": ["a", "e", "i"], "O": ["o", "u"]} - - -def norm(symbol: str) -> str: - """A rule symbol in the scorer's names. "_" (nothing heard) and the abstract symbols stay as they are.""" - return symbol if symbol == "_" or symbol.isupper() else base(symbol) - - -@dataclass(frozen=True) -class Check: - rule: str - target: str - heard: str - severity: int - feedback: str - - -@dataclass -class Rules: - archiphonemes: dict[str, dict[str, list[str]]] - dialects: list[str] - optional: bool - scoring: dict - checks: dict[tuple[str, str], Check] - titles: dict[str, str] # rule id -> title - heard_labels: dict[str, str] - margins: dict[tuple[str, str], float] = field(default_factory=dict) - # a vowel heard between two consonants of a word (3.10): rule, severity, min_peak, min_frames, score_cap, feedback - insertion: dict | None = None - # short vowels a rule flags although short vowels are not flagged (6.3 #7): vowel -> rule id (3.11: u) - flag_vowels: dict[str, str] = field(default_factory=dict) - # a long vowel said short, judged by its duration (3.8): rule, severity, min_ratio, prior_frames, ..., feedback - length: dict | None = None - # (target, heard) pairs a native reference never forgives (scoring.baseline_protected, 6.4) - baseline_protected: set[tuple[str, str]] = field(default_factory=set) - - def members(self, symbol: str, written: str | None = None) -> list[str]: - """The correct realisations of an abstract symbol, in base() names, first the dialects' own. - - A learned-only realisation (TH as s) counts only when the phrase writes it (`written`).""" - if symbol in POSITIONAL: - return list(POSITIONAL[symbol]) - spec = self.archiphonemes.get(symbol) - if spec is None: - return [] - out: list[str] = [] - keys = [*self.dialects, "also_ok", *(["optional"] if self.optional else [])] - for key in keys: - for ph in spec.get(key, []): - ph = base(ph) - if ph not in out and (ph not in LEARNED_ONLY.get(symbol, ()) or ph == written): - out.append(ph) - return out - - def dialects_of(self, symbol: str, phone: str) -> set[str]: - """The dialects in which `phone` is a correct realisation of `symbol` (empty: in none, or not dialectal).""" - spec = self.archiphonemes.get(symbol, {}) - return {d for d in ("urban", "rural") if phone in {base(p) for p in spec.get(d, [])}} - - def check(self, targets: str | list[str], heard: str | None) -> Check | None: - """The rule for hearing `heard` instead of the target: the first of `targets` that has one.""" - if not heard: - return None - for t in [targets] if isinstance(targets, str) else targets: - c = self.checks.get((norm(t), norm(heard))) - if c is not None: - return c - return None - - def margin(self, targets: str | list[str], heard: str | None) -> float: - """How much likelier than the target `heard` must be before it is a mistake (natural log). The first - of `targets` (the abstract symbol, then the written phoneme) with a margin of its own decides.""" - default = float(self.scoring.get("margin_default", 0.0)) - for t in [targets] if isinstance(targets, str) else targets: - m = self.margins.get((norm(t), norm(heard or "_"))) - if m is not None: - return m - return default - - def protected(self, targets: str | list[str], heard: str | None) -> bool: - """Is (target, heard) a pair a native reference never forgives? The target as an abstract symbol or written.""" - return heard is not None and any((norm(t), norm(heard)) in self.baseline_protected - for t in ([targets] if isinstance(targets, str) else targets)) - - def label(self, heard: str | None) -> str | None: - """The Hebrew name of a heard symbol, as section 8 gives it.""" - if heard is None: - return None - return self.heard_labels.get(heard) or self.heard_labels.get(norm(heard)) - - def interference(self) -> set[str]: - """Every symbol some rule expects to hear instead of the target: the Hebrew sounds worth naming.""" - return {h for (_, h) in self.checks if h != "_"} - - -def _blocks(text: str) -> list[dict]: - out = [] - for block in re.findall(r"```yaml\n(.*?)```", text, re.S): - for doc in re.split(r"^---$", block, flags=re.M): - if doc.strip(): - out.append(yaml.safe_load(doc)) - return out - - -def parse(text: str) -> Rules: - archi, dialects, optional, scoring, labels = {}, ["urban", "rural"], False, {}, {} - checks: dict[tuple[str, str], Check] = {} - titles: dict[str, str] = {} - insertion, flag_vowels, length = None, {}, None - for doc in _blocks(text): - if "duration" in doc: - length = {"rule": doc["id"], **doc["duration"]} - if "insertion" in doc: - titles[doc["id"]] = doc.get("title", doc["id"]) - insertion = {"rule": doc["id"], **doc["insertion"]} - for v in doc.get("flag_vowels", []): - flag_vowels[str(v)] = doc["id"] - if "archiphonemes" in doc: - archi = {k: {d: list(v.get(d, [])) for d in ("urban", "rural", "also_ok", "optional")} - for k, v in doc["archiphonemes"].items()} - dialects = list(doc.get("dialects_enabled", dialects)) - optional = bool(doc.get("optional_realisations_enabled", False)) - if "scoring" in doc: - scoring = dict(doc["scoring"]) - if "heard_labels" in doc: - labels = {str(k): str(v) for k, v in doc["heard_labels"].items()} - if "checks" in doc: - titles[doc["id"]] = doc.get("title", doc["id"]) - for c in doc["checks"]: - for heard in c["heard"]: - key = (norm(str(c["target"])), norm(str(heard))) - # two spellings of one sound (ʁ and ɣ) meet here: the first rule written wins - checks.setdefault(key, Check(doc["id"], key[0], key[1], int(c["severity"]), str(c["feedback"]))) - rules = Rules(archi, dialects, optional, scoring, checks, titles, labels, insertion=insertion, flag_vowels=flag_vowels, - length=length) - for m in scoring.get("margins", []): - # base() drops vowel length, so a margin for long vowels would land on quality pairs (a / i) it was not meant for - for t in (t for t in m["target"] if "ː" not in str(t)): - for h in m["heard"]: - rules.margins[(norm(str(t)), norm(str(h)))] = float(m["margin"]) - rules.baseline_protected = {(norm(str(p["target"])), norm(str(p["heard"]))) for p in scoring.get("baseline_protected", [])} - return rules - - -@lru_cache(maxsize=1) -def load(path: str = str(RULES_PATH)) -> Rules: - return parse(Path(path).read_text(encoding="utf-8")) - - -def undetectable(check: Check, rules: Rules, in_model: Callable[[str], bool], - heard_as: Callable[[str], object] | None = None) -> str | None: - """Why the model cannot catch this mistake, or None if it can. - - `heard_as` names the model symbol a sound is heard as (stand-ins included: ðˤ as z); two sounds heard - as the same symbol cannot be told apart.""" - same = (lambda a, b: a == b or (heard_as(a) is not None and heard_as(a) == heard_as(b))) if heard_as \ - else (lambda a, b: a == b) - if rules.length and check.rule == rules.length["rule"]: - return None # a long vowel said short is measured by its duration, not its symbol - if same(check.target, check.heard): - return "same_sound" # the two collapse to one sound (aː / a: length is not modelled) - accepted = rules.members(check.target) if check.target.isupper() else [check.target] - if any(same(check.heard, p) for p in accepted): - return "accepted" # the 'mistake' is a correct realisation of the target - if not any(in_model(p) for p in accepted): - return "target_not_in_model" - if check.heard != "_" and not in_model(check.heard): - return "heard_not_in_model" - return None - - -def check_detectable(rules: Rules, in_model: Callable[[str], bool], - heard_as: Callable[[str], object] | None = None) -> dict[str, dict]: - """Per rule: whether at least one of its checks can fire, and why each of the others cannot.""" - out: dict[str, dict] = {} - for c in rules.checks.values(): - r = out.setdefault(c.rule, {"title": rules.titles.get(c.rule, c.rule), "detectable": False, "missing": []}) - why = undetectable(c, rules, in_model, heard_as) - if why is None: - r["detectable"] = True - else: - r["missing"].append({"target": c.target, "heard": c.heard, "why": why}) - return out - - -# ---------- exemptions (section 6.3): what the model hears as a mistake but is ordinary speech ---------- - -VOWELS = {"a", "i", "u", "e", "o", "aː", "iː", "uː", "eː", "oː"} -_VOWEL_GROUPS = [set("iɪeɛ"), set("uʊoɔ"), set("aæɐɑəɛe")] # a raised towards e is ordinary Palestinian speech - - -def near_vowels(expected: str, heard: str | None) -> bool: - """Same vowel with another length, or the neighbouring quality (i/e, u/o, a/ə, a/e).""" - if expected not in VOWELS or not heard: - return False - e, h = expected.rstrip("ː"), heard.rstrip("ː") - return any(e in g and h in g for g in _VOWEL_GROUPS) - - -def elided_article(toks, i: int, ar_words: list[str]) -> bool: - """Is token i the vowel of the article ال right after a word that ends in a vowel? - - There it is not said (على الطّاولة is ʕ a l a | tˤ tˤ aː w l e), so its absence is not an error. - """ - t = toks[i] - return (i > 0 and t.phoneme in VOWELS and toks[i - 1].word == t.word - 1 and toks[i - 1].phoneme in VOWELS - and t.word < len(ar_words) and ar_words[t.word].startswith("ال")) - - -# A glottal stop that is simply not there is how natives say it too (انا, اهلا; ق between vowels): #1, #2. -# A glottal stop replaced by another consonant still counts, unless the class accepts it (ق said as k). -# At utterance onset, though, one weak competitor frame is commonly a breath or microphone transient. Treat that -# as an omission regardless of the phone the multilingual model names; two frames (~40 ms) remain a substitution. -def _glottal_dropped(toks, i, sc, heard, ar_words): - if toks[i].sound not in ("ʔ", "Q"): - return False - transient_onset = i == 0 and sc.peak < math.log(0.05) and sc.comp_frames < 2 - return sc.deleted or transient_onset - - -# Rule 31: the same onset transient in front of any other first consonant. The multilingual model hears a -# word-initial stop (طاولتنا: closure silence, then a short burst) weakly, and a single breath or microphone frame -# above deleted_comp then outscores it under an arbitrary name (tˤ heard as j). The target must still be there -# (not deleted, peak at least deleted_peak); an omission, or a competitor held two frames (~40 ms), still counts. -def _onset_transient(toks, i, sc, heard, ar_words): - t = toks[i] - return (i == 0 and t.phoneme not in VOWELS and t.sound not in ("ʔ", "Q") and not sc.deleted - and sc.comp_frames == 1 and sc.peak >= math.log(0.05)) - - -def _article_vowel_elided(toks, i, sc, heard, ar_words): # #3 - return sc.deleted and elided_article(toks, i, ar_words) - - -# Neighbouring vowel qualities and vowel length vary between speakers and towns (il / el), -# and the model confuses them on its own: not worth marking a word down for (#7). -def _near_vowel(toks, i, sc, heard, ar_words): - return near_vowels(toks[i].phoneme, heard) - - -# Short vowels are deliberately not judged by the learner policy, except u after a consonant inside the same word -# (rule 3.11). Their acoustic score must follow that decision: an unflagged 1/100 must not collapse the word score. -def _unmarked_short_vowel(toks, i, sc, heard, ar_words): - t = toks[i] - checked_u = (t.phoneme == "u" and i > 0 and toks[i - 1].word == t.word - and toks[i - 1].phoneme not in VOWELS) - return t.phoneme in {"a", "i", "u", "e", "o"} and not checked_u - - -# Rule 28: a native final /b/ can lose its acoustic voicing before a voiceless consonant across a word boundary. -# The multilingual CTC then strongly labels that closure /p/ (ħaliib tˤaare), although Arabic has no phonemic /p/. -# Keep this narrow: a learner's /p/ inside a word, or before a voiced sound, must not be hidden. -_VOICELESS_ONSETS = {"t", "tˤ", "k", "q", "ʔ", "s", "sˤ", "ʃ", "f", "x", "ħ", "h", "θ"} - - -def _final_b_devoicing(toks, i, sc, heard, ar_words): - if base(toks[i].phoneme) != "b" or base(heard or "") != "p" or i + 1 >= len(toks): - return False - following = toks[i + 1] - return following.word == toks[i].word + 1 and base(following.phoneme) in _VOICELESS_ONSETS - - -# Rule 32: plain t heard as d, or d heard as t. Hebrew has the same contrast (ת / ד), so a learner hardly ever swaps -# them, while the multilingual model often does: a t after n (انتي) or between vowels (فتح) gets some voicing in -# natural speech and is labelled d. Emphatics (tˤ, dˤ) are not covered: their plain counterparts are rule 3.3. -_VOICING_PAIR = {("t", "d"), ("d", "t")} - - -def _t_d_voicing(toks, i, sc, heard, ar_words): - return not sc.deleted and (base(toks[i].phoneme), base(heard or "")) in _VOICING_PAIR - - -# Rule 29: in connected speech, a word-final short vowel flows into a vowel-initial next word. The next word is -# commonly represented with an initial ʔ token, so accept either V or ʔ+V as the hiatus context. CTC may assign the -# transition to only one side and call the final vowel deleted even in native speech (على إيدك). -def _word_final_vowel_linking(toks, i, sc, heard, ar_words): - t = toks[i] - if not sc.deleted or t.phoneme not in {"a", "i", "u", "e", "o"} or i + 1 >= len(toks): - return False - following = toks[i + 1] - if following.word != t.word + 1: - return False - if following.phoneme in VOWELS: - return True - return (following.sound in {"ʔ", "Q"} and i + 2 < len(toks) - and toks[i + 2].word == following.word and toks[i + 2].phoneme in VOWELS) - - -# (name, applies(tokens, index, TokenScore, heard symbol, Arabic words), score it gets: None = full marks, -# else the name of the ScorerConfig floor). The first that applies wins. -EXEMPTIONS = [ - ("glottal_dropped", _glottal_dropped, None), - ("onset_transient", _onset_transient, "onset_transient_floor"), - ("article_vowel_elided", _article_vowel_elided, None), - ("near_vowel", _near_vowel, "near_vowel_floor"), - ("final_b_devoicing", _final_b_devoicing, None), - ("t_d_voicing", _t_d_voicing, None), - ("word_final_vowel_linking", _word_final_vowel_linking, "near_vowel_floor"), - ("unmarked_short_vowel", _unmarked_short_vowel, "near_vowel_floor"), -] - - -# ---------- the rules a teacher can switch off (ניטור): every behaviour taken from pronunciation-rules.md ---------- - -@dataclass(frozen=True) -class Toggle: - id: str - group: str # "accept": what counts as correct · "judge": how a mistake is judged · "feedback": what is said - section: str # where pronunciation-rules.md has it - title: str - description: str # what the scorer does while the rule is on; off is the behaviour before the document - - -def _toggles() -> list[Toggle]: - letters = [ - ("Q", "ق", "ʔ (עירוני), k (כפרי) או q", "אַהְוֶה, כַּהְוֶה וקַהְוֶה נכונות שלושתן."), - ("K", "ك", "k או tʃ (כפרי)", "כַּלְבּ וצ'ַלְבּ נכונות שתיהן."), - ("TH", "ث", "t או θ (כפרי), ו־s במילים ספרותיות כשהמשפט כותב s", "תְלַאתֶה ות'לַאת'ֶה נכונות שתיהן."), - ("DH", "ذ", "d או ð (כפרי), ו־z במילים ספרותיות כשהמשפט כותב z", "דַהַבּ וד״ַהַבּ נכונות שתיהן."), - ("ZH", "ظ", "dˤ, zˤ או ðˤ", "ד'ֻהְר וט'ֻהְר נכונות שתיהן."), - ("J", "ج", "dʒ או ʒ", "ג' כמו בג'ינס או ז' כמו בז'קט."), - ] - out = [Toggle(f"dialect_{s}", "accept", "5", f"{ar}: {said}", - f"כל המימושים נכונים, וההגה נמדד מול סכום ההסתברויות שלהם (6.1). {ex} " - f"כבוי: נבדק רק המימוש שכתוב במשפט.") for s, ar, said, ex in letters] - out += [ - Toggle("final_vowel", "accept", "6.3 #5", "תנועה סופית של ة: a, e או i", - "אַהְוֶה, אַהְוַה ואַהְוִי נכונות. כבוי: נבדקת התנועה שכתובה במשפט."), - Toggle("diphthong", "accept", "6.3 #8", "eː גם כ־aj, ו־oː גם כ־aw", - "בֵּית ובַּיְת נכונות שתיהן (נבדק כווריאנט נוסף). כבוי: רק התנועה הארוכה."), - Toggle("glottal_dropped", "judge", "6.3 #1–2", "עצירה סותמית שהושמטה אינה טעות", - "ʔ (ء, ו־ق עירונית) שלא נשמעה מקבלת ציון מלא, כמו אצל דוברים ילידיים (אַנַא ← אנה)."), - Toggle("onset_transient", "judge", "6.3 #31", "קפיצה של פריים אחד לפני העיצור הראשון", - "עיצור שפותח את המשפט, שנשמע אך חלש, ומתחרה שהופיע בפריים אחד בלבד (נשימה או רעש מיקרופון) מקבל " - "לפחות 90 ולא מסומן. השמטה, או מתחרה בשני פריימים ומעלה, עדיין נבדקים."), - Toggle("article_vowel_elided", "judge", "6.3 #3", "תנועת ה־ال אחרי תנועה", - "ה־i של ال שלא נאמרה אחרי מילה שמסתיימת בתנועה (עַלַא לְ...) אינה טעות."), - Toggle("near_vowel", "judge", "6.3 #7", "תנועות קרובות", - "תנועה שנשמעה כשכנתה (i/e, u/o, a/e) או באורך אחר מקבלת לפחות 90."), - Toggle("unmarked_short_vowel", "judge", "6.3 #30", "תנועה קצרה שאינה נבדקת", - "תנועה קצרה שהמדיניות אינה מסמנת מקבלת לפחות 90, כדי שלא תפיל את ציון המילה. " - "u אחרי עיצור בתוך אותה מילה עדיין נבדקת לפי כלל הקובוץ."), - Toggle("final_b_devoicing", "judge", "6.3 #28", "b סופית לפני עיצור אטום", - "b בסוף מילה שהמודל שמע כ־p לפני עיצור אטום במילה הבאה מקבלת ציון מלא; " - "באמצע מילה או לפני עיצור קולי אין פטור."), - Toggle("t_d_voicing", "judge", "6.3 #32", "t ו־d לא מתחלפות", - "t שהמודל שמע כ־d, או d שנשמעה כ־t, מקבלת ציון מלא: דוברי עברית מבחינים בין ת ל־ד, " - "והמודל טועה בזוג הזה (אחרי n, בין תנועות). עיצורים מודגשים (ط, ض) נבדקים כרגיל."), - Toggle("word_final_vowel_linking", "judge", "6.3 #29", "תנועה סופית שמתחברת לתנועה הבאה", - "תנועה קצרה בסוף מילה שהמודל רואה כמחוקה לפני מילה שמתחילה בתנועה מקבלת לפחות 90; " - "באמצע מילה או לפני עיצור אין פטור."), - Toggle("margins", "judge", "6.2", "מרווח ביטחון לזוגות שהמודל מבלבל", - "עיצור מודגש נחשב שגוי רק אם הרגיל סביר ממנו פי 2 בערך, ħ מול h פי 1.5, " - "ו־h נחשבת שנבלעה רק אם היא חלשה פי 2 מסף ההשמטה. " - "כבוי: כל מתחרה שמנצח מסמן טעות."), - Toggle("unnamed_sounds", "judge", "7", "צליל שאינו בערבית או בעברית לא נקרא בשמו", - "כשהמודל הרב־לשוני שומע צליל זר (נקישה, צליל רטרופלקסי) הוא מדווח כ'לא ברור'. כבוי: נאמר שם הסמל."), - Toggle("tips_by_severity", "feedback", "6.2", "סדר הטיפים לפי חומרה, עד 3", - "טעות שמשנה משמעות (ח, ע) מוצגת לפני מבטא, ואחריה הציון הנמוך. כבוי: לפי הציון בלבד, עד 4."), - ] - own = { - "vowel_length": "המודל לא מבחין בין a ל־aː, ולכן האורך נמדד בזמן: תנועה ארוכה בין שני עיצורים שקצרה בבירור " - "ביחס לתנועות הקצרות של אותו דובר יורדת לכל היותר ל־60, עם טיפ בחומרה 1. " - "כבוי: אורך התנועה לא נבדק.", - "cluster": "תנועה שהמודל שומע בבירור בין שני עיצורים של אותה מילה (katiir במקום ktiir) מורידה את העיצור השני " - "לכל היותר ל־60, עם טיפ בחומרה 1. לא בצרור שפותח את המשפט, ולא ליד w או j. כבוי: תנועה שנוספה לא נבדקת.", - "kubutz": "u אחרי עיצור היא התנועה הקצרה היחידה שמסומנת: כשנשמטה, או נשמעה כ־e, i או שווא. " - "כ־o היא עדיין תנועה קרובה. כבוי: כמו שאר התנועות הקצרות, לא מסומנת.", - } - for rule_id, title, section in (("pharyngeal_h", "ח לועית", "3.1"), ("ayin", "ע לועית", "3.2"), - ("emphatics", "עיצורים מודגשים", "3.3"), ("r_tap", "ר מתגלגלת", "3.4"), - ("ghain", "ע' היא ה־ר העברית", "3.5"), ("h", "ה נהגית תמיד", "3.6"), - ("w", "ו היא w", "3.7"), ("b", "ב היא תמיד b", "3.7"), ("f", "פ היא תמיד f", "3.7"), - ("vowel_length", "תנועה ארוכה", "3.8"), ("jim", "ג' ולא g", "3.9"), - ("cluster", "שני עיצורים צמודים", "3.10"), ("kubutz", "קובוץ ברור", "3.11")): - out.append(Toggle(f"rule_{rule_id}", "judge" if rule_id in own else "feedback", section, title, - own.get(rule_id) or - f"הטיפ והחומרה (בסוגריים) מהמסמך כשנשמע: {_confusions(rule_id)}. " - "כבוי: טיפ כללי בחומרה בינונית; ההחלטה אם לסמן לא משתנה.")) - return out - - -def _confusions(rule_id: str) -> str: - """'ח ← ח עברית (3), ה (2), נבלעת (2)': the substitutions a section-3 rule speaks for, in Hebrew labels.""" - rules = load() - by_target: dict[str, list[str]] = {} - for c in rules.checks.values(): - if c.rule == rule_id: - heard = rules.label(c.heard) or c.heard - item = f"{'נבלעת' if c.heard == '_' else heard} ({c.severity})" - if item not in by_target.setdefault(c.target, []): - by_target[c.target].append(item) - return "; ".join(f"{_LETTER.get(t, t)} ← {', '.join(items)}" for t, items in by_target.items()) - - -# a target sound by the letter of the transliteration (section 1), for the teacher's list -_LETTER = {"ħ": "ח", "ʕ": "ע", "sˤ": "צ", "tˤ": "ט", "dˤ": "ד'", "ZH": "ט'", "r": "ר", "ɣ": "ע'", "h": "ה", "w": "ו", - "b": "ב", "f": "פ", "J": "ג'", "a": "אַא", "i": "ִי", "u": "וּ", "e": "ֵי", "o": "וֹ"} - - -TOGGLES = _toggles() -TOGGLE_IDS = frozenset(t.id for t in TOGGLES) diff --git a/app/scorer.py b/app/scorer.py deleted file mode 100644 index 4c2d2f3263757c719c11b6e5ac0753dc0e9672c4..0000000000000000000000000000000000000000 --- a/app/scorer.py +++ /dev/null @@ -1,1150 +0,0 @@ -"""Pronunciation scorer: phoneme CTC model + forced alignment against the expected phonemes.""" -from __future__ import annotations - -import copy -import json -import logging -import os -import subprocess -from dataclasses import dataclass, field, replace -from difflib import SequenceMatcher -from typing import Callable, Protocol - -import numpy as np - -from .align import (Alignment, GopConfig, TokenScore, ctc_viterbi, edit_distance, greedy_decode, score_tokens, - weighted_edit_distance) -from .archi import archify, diphthong_variants, split_token, written -from .phones import base -from .rules import EXEMPTIONS, TOGGLE_IDS, Check, Rules, elided_article, near_vowels, norm, load as load_rules # noqa: F401 (re-exported) -from .tips import VOWELS, letter_reason, sound_name, tip_for -from .translit import CONSONANTS as _HEB_C, HEARD_ONLY as _HEB_HEARD, VOWELS as _HEB_V, to_hebrew, word_letters, word_to_hebrew - -HEBREW_SOUNDS = set(_HEB_C) | set(_HEB_V) | set(_HEB_HEARD) -# a vowel gliding into (or out of) j or w passes through these: on the frames between the two, coarticulation -GLIDE_KIN = {"j": ("i", "iː", "e", "eː"), "w": ("u", "uː", "o", "oː")} - -log = logging.getLogger("ihki.scorer") - -SAMPLE_RATE = 16_000 -MAX_SECONDS = 32 # the recording includes the time to read the sentence and think (the page stops at 30 s) - -# Fallbacks when the model vocab lacks a phoneme. The first symbol found in the vocab wins. -FALLBACKS = { - "sˤ": ["sˤ", "sˠ", "s"], "tˤ": ["tˤ", "tˠ", "t"], "dˤ": ["dˤ", "dˠ", "d"], "zˤ": ["zˤ", "zˠ", "z"], - "ðˤ": ["ðˤ", "ðˠ", "zˤ", "z"], "dʒ": ["dʒ", "d͡ʒ", "ʒ"], - "aː": ["aː", "a"], "iː": ["iː", "i"], "uː": ["uː", "u"], "eː": ["eː", "e"], "oː": ["oː", "o"], - "x": ["x", "χ"], "ɣ": ["ɣ", "ʁ"], "q": ["q", "k"], "r": ["r", "ɾ"], "g": ["ɡ", "g"], -} - - -class Emitter(Protocol): - """Anything that turns 16 kHz mono float32 audio into (T, V) log-posteriors.""" - def __call__(self, wav: np.ndarray) -> np.ndarray: ... - - -class Vocab: - def __init__(self, token_to_id: dict[str, int], blank_token: str = "", rules: Rules | None = None): - self.t2i = token_to_id - self.i2t = {i: t for t, i in token_to_id.items()} - self.blank = token_to_id[blank_token] - self.rules = rules or load_rules() - self.downgraded: dict[str, str] = {} - # sound classes: every id points at one representative id (the plain symbol when the vocab has it) - groups: dict[str, list[int]] = {} - for t, i in token_to_id.items(): - if i != self.blank and not t.startswith("<"): - groups.setdefault(base(t), []).append(i) - self.canon = {i: i for i in self.i2t} - self._groups: list[tuple[int, list[int]]] = [] - self.rep_of: dict[str, int] = {} # base() name -> representative id - for name, ids in groups.items(): - rep = token_to_id.get(name, ids[0]) if token_to_id.get(name) in ids else ids[0] - self.rep_of[name] = rep - for i in ids: - self.canon[i] = rep - if len(ids) > 1: - self._groups.append((rep, ids)) - # Letters with several correct realisations (archi.py): one virtual id per (symbol, realisations), - # numbered after the real vocab, whose column extend() fills with the pooled posterior of its members. - self.n_real = max(self.i2t) + 1 - self.members: dict[int, set[int]] = {} - self._classes: dict[tuple[str, frozenset[int]], int] = {} - self.class_missing: dict[str, set[str]] = {} # symbol -> realisations the model has no symbol for - - def merge(self, logp: np.ndarray) -> np.ndarray: - """Pool each sound's spelling variants into its representative column; the other columns go silent.""" - out = logp.copy() - for rep, ids in self._groups: - out[:, ids] = -1e9 - out[:, rep] = np.logaddexp.reduce(logp[:, ids], axis=1) - return out - - def sound_id(self, phone: str) -> int | None: - """The representative id of a sound (any spelling of it), or None if the model has no symbol for it.""" - return self.rep_of.get(base(phone)) - - def model_id(self, phone: str) -> int | None: - """The id the model hears `phone` as: its own, else its first FALLBACKS stand-in (ðˤ -> z), else None.""" - for cand in [phone, *FALLBACKS.get(phone, [])]: - i = self.sound_id(cand) - if i is not None: - return i - return None - - def add_class(self, symbol: str, phones: list[str]) -> int: - """The id that stands for 'any of `phones`': a virtual id, or the real one when only one is in the model. - A realisation the model lacks is heard as its stand-in, as a plain phoneme would be (zˤ as z).""" - ids: list[int] = [] - for ph in phones: - i = self.model_id(ph) - if i is None: - self.class_missing.setdefault(symbol, set()).add(ph) - continue - if i != self.sound_id(ph): - self.downgraded[ph] = self.i2t[i] - if i not in ids: - ids.append(i) - if not ids: - raise KeyError(f"no realisation of {symbol} ({phones}) is in the model vocab") - if len(ids) == 1: - return ids[0] - key = (symbol, frozenset(ids)) - if key not in self._classes: - vid = self.n_real + len(self._classes) - self._classes[key], self.members[vid] = vid, set(ids) - self.i2t[vid], self.canon[vid] = symbol, vid - return self._classes[key] - - def extend(self, logp: np.ndarray) -> np.ndarray: - """Merged posteriors with one more column per class: the pooled posterior of its realisations.""" - logp = logp[:, :self.n_real] - if not self.members: - return logp - out = np.full((logp.shape[0], self.n_real + len(self.members)), -1e9, dtype=logp.dtype) - out[:, :logp.shape[1]] = logp - for vid, ids in self.members.items(): - out[:, vid] = np.logaddexp.reduce(logp[:, sorted(ids)], axis=1) - return out - - def resolve(self, phoneme: str) -> list[int]: - for cand in FALLBACKS.get(phoneme, [phoneme]): - if cand in self.t2i: - if cand != phoneme: - self.downgraded[phoneme] = cand - return [self.t2i[cand]] - # last resort: greedy longest-match split into vocab symbols - ids, rest = [], phoneme - while rest: - for k in range(len(rest), 0, -1): - if rest[:k] in self.t2i: - ids.append(self.t2i[rest[:k]]) - rest = rest[k:] - break - else: - raise KeyError(f"phoneme {phoneme!r} cannot be expressed in the model vocab") - self.downgraded[phoneme] = " ".join(self.i2t[i] for i in ids) - return ids - - def token(self, tok: str) -> tuple[list[int], str, str]: - """A token of an IPA string (a phoneme, or an archi.py symbol "Q:ʔ") -> (model ids, phoneme as written, sound). - - The sound is the symbol for a many-ways letter, the phoneme otherwise.""" - sym, members, ph = split_token(tok) - if sym is None: - return self.resolve(tok), tok, tok - phones = members or self.rules.members(sym, written=base(ph) if ph else None) - ph = ph or (phones[0] if phones else sym) - try: - return [self.add_class(sym, phones)], ph, sym - except KeyError: - return self.resolve(ph), ph, sym - - -LETTER_GOOD = 75 # a letter scoring under this is coloured on the page (`band` in static/index.html) - - -@dataclass -class TargetToken: - phoneme: str # phoneme as written in phrases.py - id: int # model token id (a virtual id for a letter with several correct realisations) - word: int # word index in the variant - sound: str = "" # what the token stands for: the archi.py symbol (Q) or the phoneme itself - - def __post_init__(self): - self.sound = self.sound or self.phoneme - - -def tokenize(ipa: str, vocab: Vocab) -> list[TargetToken]: - """IPA string -> model tokens. Geminates (and any repeated id) collapse to one CTC token.""" - out: list[TargetToken] = [] - for w, word in enumerate(ipa.split("|")): - for tok in word.split(): - ids, ph, sound = vocab.token(tok) - for tid in ids: - if out and out[-1].id == tid: - continue - out.append(TargetToken(ph, tid, w, sound)) - return out - - -@dataclass -class Reference: - """How the model scored a native voice saying a phrase: the baseline a learner is measured against.""" - sounds: list[tuple[int, str]] # (word, sound) of each sound of the variant the voice said (Q, not ʔ / k) - scores: list[float] # their scores, same order - voice: str = "" # who said it (the TTS voice's name), for the trace dashboard - # per sound, what took its place in the voice (Scorer._heard): "_" nothing, a symbol, None kept. - # Empty for a reference built before this was recorded: its failures are then unknown. - heard: list[str | None] = field(default_factory=list) - - -@dataclass -class ScorerConfig: - flag_below: float = 50.0 # phoneme score under which a consonant gets a tip - flag_below_sound: dict[str, float] = field(default_factory=dict) # per sound, in place of flag_below (see - # FLAG_BELOW_BY_MODEL: it depends on the model) - # weighted edit distance (Scorer._distance) over the weight of the sounds that can be wrong, above which we say - # "different sentence". 0.65 with plain edit distance over all sounds; the weighted ratio runs lower - unrelated_ratio: float = 0.5 - # ... unless the forced alignment clearly found the sentence: this share of its consonants (at least 2) peak - # at unrelated_veto_peak or more, and the mean GOP score (before the exemptions) is at least unrelated_veto_mean - unrelated_veto_share: float = 0.6 - unrelated_veto_peak: float = 0.5 - unrelated_veto_mean: float = 60.0 - issue_below: float = 90.0 # phoneme score under which it is listed as imprecise - max_issues: int = 8 - max_tips: int | None = None # None: as many as the rules document says (scoring.max_tips) - near_vowel_floor: float = 90.0 # a vowel heard as its neighbour (i/e, u/o, a/ə, short/long) scores at least this - word_min_weight: float = 0.35 # one weak phoneme matters, without dominating half of the whole word - ref_min_mean: float = 60.0 # a native reference whose mean phoneme score is under this is not trusted - # a sound the native reference scored under this, the model cannot measure in this phrase (ħ in رحنا: 0.2): the - # learner's score on it is noise too, so it is given back in full and the unrelated gate does not count it - ref_noise_floor: float = 10.0 - ayin_deleted_floor: float = 60.0 # the base model often drops a correctly pronounced pharyngeal ʕ - onset_transient_floor: float = 90.0 # a first consonant outscored by a one-frame onset spike (rule 31) - # The learner-facing pipeline is more encouraging than the generic GOP primitive: a competitor twice as likely - # scores about 78, while a clearly dominant competitor can still pull the phoneme below the feedback threshold. - gop: GopConfig = field(default_factory=lambda: GopConfig(leniency=0.35)) - - -def frame_energy(wav: np.ndarray | None, frames: int) -> np.ndarray | None: - """RMS energy of the audio under each of the model's `frames` (they split the recording evenly, ~20 ms each).""" - if wav is None or frames <= 0 or len(wav) == 0: - return None - edges = np.linspace(0, len(wav), frames + 1).astype(int) - sq = np.concatenate([[0.0], np.cumsum(np.asarray(wav, dtype=np.float64) ** 2)]) - n = np.maximum(edges[1:] - edges[:-1], 1) - return np.sqrt((sq[edges[1:]] - sq[edges[:-1]]) / n) - - -@dataclass -class Fit: - """The accepted variant the audio fits best, and how each of its sounds scored.""" - ar: str - ipa: str # as written in the phrase bank (display, transliteration) - aipa: str # the same with archi.py symbols, one token per phoneme of `ipa` - toks: list[TargetToken] - al: Alignment - scores: list[TokenScore] - heard_ids: list[int] # greedy decode, real symbols only - unrelated: bool - logp: np.ndarray # merged posteriors with the class columns (Vocab.extend) - # for the teacher's stage-by-stage view (Scorer._trace) - candidates: list[tuple[str, str, float | None]] = field(default_factory=list) # (ar, ipa, path log-prob) - gop_scores: list[TokenScore] = field(default_factory=list) # before the exemptions - exempted: list[str | None] = field(default_factory=list) # per token, the exemption that applied - exempt_scores: list[TokenScore] = field(default_factory=list) # after the exemptions, before the inserted vowels - inserted: dict[int, str] = field(default_factory=dict) # token -> the vowel heard before it (rules 3.10) - insert_scores: list[TokenScore] = field(default_factory=list) # after the inserted vowels, before vowel length - shortened: dict[int, dict] = field(default_factory=dict) # token -> how short its long vowel was (rules 3.8) - distance: float = 0.0 # weighted edit distance of the greedy decode from the chosen variant (_distance) - raw_distance: int = 0 # the same, every edit costing 1 - distance_weight: float = 1.0 # what `distance` is divided by: the weight of the sounds that can be wrong - veto: bool = False # the alignment found the sentence, so it is not called unrelated (_veto) - energy: np.ndarray | None = None # per frame RMS (frame_energy), when the audio was given - paths: list[list[str]] = field(default_factory=list) # each candidate's tokens (archi symbols), as `candidates` - - -class Scorer: - def __init__(self, emit: Emitter, vocab: Vocab, cfg: ScorerConfig | None = None): - self.emit, self.vocab, self.cfg = emit, vocab, cfg or ScorerConfig() - self.rules = vocab.rules - self.calibrations: dict[tuple[str, str, str], float] = {} - # the sounds a competitor is named by: Arabic, the Hebrew sounds the rules expect instead, and their labels; - # anything else the multilingual model offers (clicks, retroflexes) is reported as "not clear" - names = (set(HEBREW_SOUNDS) | self.rules.interference() | set(self.rules.heard_labels) - | {p for s in self.rules.archiphonemes for p in self.rules.members(s)}) - self.named = {i for i in (vocab.sound_id(n) for n in names) if i is not None} - self.disabled: frozenset[str] = frozenset() # rules.TOGGLES the teacher switched off (ניטור) - - def with_gop(self, **changes) -> "Scorer": - """This scorer with some GopConfig fields changed (ratio_pool, vowel_window ...), sharing its model, vocab and - rules: for the lab's sweep over the settings. Nothing of this scorer changes.""" - other = copy.copy(self) - other.cfg = replace(self.cfg, gop=replace(self.cfg.gop, **changes)) - return other - - def set_disabled(self, ids) -> None: - """Atomically replaces the switched-off rules; unknown ids are ignored.""" - self.disabled = frozenset(ids) & TOGGLE_IDS - - def _on(self, toggle: str) -> bool: - return toggle not in self.disabled - - def _off_symbols(self) -> frozenset[str]: - """The archi.py symbols not to use: letters whose dialect rule is off, and E for the final-vowel rule.""" - off = {t.removeprefix("dialect_") for t in self.disabled if t.startswith("dialect_")} - return frozenset(off | ({"E"} if "final_vowel" in self.disabled else set())) - - def _check(self, tok: TargetToken, heard: str | None): - """The rule of pronunciation-rules.md for this substitution, unless the teacher switched it off. - A rule for a short vowel (3.11: u) speaks for that vowel only, not for its long form (base() drops length).""" - check = self.rules.check([tok.sound, tok.phoneme], heard) - if check and check.target in self.rules.flag_vowels and tok.phoneme != check.target: - return None - return check if check and self._on(f"rule_{check.rule}") else None - - def _flag_vowel(self, toks: list[TargetToken], i: int) -> bool: - """Is token i a short vowel a rule flags (3.11: u after a consonant of the same word, not the و of 'and')?""" - t = toks[i] - rule = self.rules.flag_vowels.get(t.phoneme) - return bool(rule and self._on(f"rule_{rule}") and i > 0 and toks[i - 1].word == t.word - and toks[i - 1].phoneme not in VOWELS) - - _GLIDES = {"w", "j"} # the way into or out of a glide sounds like a vowel in anyone's speech - - def _glide_edges(self, toks: list[TargetToken]) -> list[tuple[set[int], set[int]] | None]: - """For a vowel beside j or w: the vowels that glide is made of (GLIDE_KIN), no competitor on the frames - between the vowel and that glide.""" - kin = lambda k: {x for p in GLIDE_KIN.get(toks[k].phoneme, ()) if (x := self.vocab.sound_id(p)) is not None} - return [(kin(i - 1) if i else set(), kin(i + 1) if i + 1 < len(toks) else set()) - if t.phoneme in VOWELS else None for i, t in enumerate(toks)] - - def _insertions(self, logp: np.ndarray, toks: list[TargetToken], al: Alignment) -> dict[int, str]: - """Rules 3.10: token -> the vowel the model clearly heard between it and the consonant before it, in one word - (katiir for ktiir). The cluster that opens the sentence is spared (6.3 #16), and so are pairs with w or j.""" - spec = self.rules.insertion - if not spec or not self._on(f"rule_{spec['rule']}"): - return {} - vowels = {i: base(self.vocab.i2t[i]) for i in (self.vocab.sound_id(v) for v in ("a", "e", "i", "o", "u", "ə")) - if i is not None} - real = logp[:, :self.vocab.n_real] - greedy = real.argmax(1) - min_peak, min_frames = float(np.log(spec.get("min_peak", 0.5))), int(spec.get("min_frames", 2)) - out: dict[int, str] = {} - for j in range(2, len(toks)): # j == 1 is the cluster that opens the sentence - a, b = toks[j - 1], toks[j] - if (a.word != b.word or a.phoneme in VOWELS or b.phoneme in VOWELS - or {base(a.phoneme), base(b.phoneme)} & self._GLIDES): - continue - frames = [t for t in range(al.spans[j - 1][0], al.spans[j][1]) - if int(greedy[t]) in vowels and real[t, greedy[t]] >= min_peak] - if len(frames) >= min_frames: - out[j] = vowels[int(greedy[frames[0]])] - return out - - _SHORT = {"a", "i", "u", "e", "o"} - - def _shortened(self, toks: list[TargetToken], al: Alignment, scores: list[TokenScore], - energy: np.ndarray | None = None) -> dict[int, dict]: - """Rules 3.8: token -> how short its long vowel was, for each long vowel said short. - - The model pools a and aː (base() drops length), so length is read from time: the frames between the - sounds on either side of the vowel, against the same span around this speaker's short vowels (with a - prior for sentences that have few). With the audio, only the frames at least half as loud as the loudest - of the span count, so a held nasal or a pause after the vowel does not make it long. Only between two - consonants: at the edges of the recording, or beside another vowel, the span says nothing about this one.""" - spec = self.rules.length - if not spec or not self._on(f"rule_{spec['rule']}"): - return {} - - def span(k: int) -> int | None: - if not 0 < k < len(toks) - 1 or toks[k - 1].phoneme in VOWELS or toks[k + 1].phoneme in VOWELS \ - or scores[k].deleted: - return None - a, b = al.spans[k - 1][1], al.spans[k + 1][0] - if energy is None: - return b - a - seg = energy[a:b] - return int((seg >= 0.5 * seg.max()).sum()) if seg.size and seg.max() > 0 else 0 - - shorts = [g for k, t in enumerate(toks) if t.phoneme in self._SHORT and (g := span(k)) is not None] - prior = float(spec["prior_frames"] if energy is not None else spec["prior_frames_plain"]) - weight = float(spec["prior_weight"]) - ref = (sum(shorts) + prior * weight) / (len(shorts) + weight) - targets = {str(t): float(r) for t, r in spec["min_ratio"].items()} - out: dict[int, dict] = {} - for k, t in enumerate(toks): - g = span(k) if t.phoneme in targets else None - if g is not None and g / ref < targets[t.phoneme]: - out[k] = {"frames": g, "short_frames": round(ref, 2), "ratio": round(g / ref, 2), - "min_ratio": targets[t.phoneme]} - return out - - def set_calibrations(self, rows: list[dict]) -> None: - """Atomically replaces applied score floors. A blank phrase is a global rule.""" - rules = {} - for row in rows: - expected, heard = str(row.get("expected", "")), str(row.get("heard", "")) - phrase = str(row.get("phrase", "")) if row.get("scope") == "phrase" else "" - if expected and heard: - rules[(expected, heard, phrase)] = max(0.0, min(100.0, float(row.get("floor", 0)))) - self.calibrations = rules - - def score(self, wav: np.ndarray, phrase: dict, refs: list[Reference] = (), diagnostics: bool = False) -> dict: - logp = self.emit(wav) - duration_s = len(wav) / SAMPLE_RATE if wav is not None else None - return self.score_logp(logp, phrase, refs, diagnostics, duration_s, frame_energy(wav, len(logp))) - - def reference(self, wav: np.ndarray, phrase: dict) -> Reference | None: - """The baseline from a native recording of the phrase, or None when the model does not - recognise the phrase in it (then it says nothing about single sounds).""" - logp = self.emit(wav) - return self.reference_logp(logp, phrase, frame_energy(wav, len(logp))) - - def reference_logp(self, logp: np.ndarray, phrase: dict, energy: np.ndarray | None = None) -> Reference | None: - """Scorer.reference from the model's posteriors of the native recording (`emit`), and its frame energy.""" - fit = self._fit(self.vocab.merge(logp), phrase, energy) - if fit is None: - return None - if fit.unrelated or float(np.mean([sc.score for sc in fit.scores])) < self.cfg.ref_min_mean: - return None - return Reference([(t.word, t.sound) for t in fit.toks], [sc.score for sc in fit.scores], - heard=[self._heard(t, sc) for t, sc in zip(fit.toks, fit.scores)]) - - def _baseline(self, toks: list[TargetToken], scores: list[TokenScore], - refs: list[Reference]) -> tuple[list[float | None], list[str | None]]: - """Per target token, the lowest score a native reference got on the same sound, counting only a reference - that is evidence about the learner's realisation (None: none counts), and why compensation was refused. - - A reference counts when the learner kept the sound (a weak but correct sound), or when the reference lost it - too, however (the model's limitation in this phrase, as ʕ heard as ʔ). It does not count when it kept the - sound ("kept"), nor for a pair the rules protect, which a TTS voice can say the learner's way ("protected": - ħ heard as x). - References may have said another accepted variant: only the sounds the two variants share are matched. - Sounds are compared by symbol, so a voice that said ق as q is a baseline for a learner who says ʔ. - """ - out: list[float | None] = [None] * len(toks) - why: list[str | None] = [None] * len(toks) - learner = [self._heard(t, sc) for t, sc in zip(toks, scores)] - rows = self._ref_rows(toks, refs) - for i, tok in enumerate(toks): - lh = learner[i] - have = [cell for row in rows if (cell := row[i]) is not None] - if not have: - continue - if lh is not None and self.rules.protected([tok.sound, tok.phoneme], lh): - why[i] = "protected" - continue - counting = [score for score, heard, known in have if lh is None or not known or heard is not None] - if counting: - out[i] = min(counting) - else: - why[i] = "kept" - return out, why - - @staticmethod - def _ref_rows(toks: list[TargetToken], refs: list[Reference]) -> list[list[tuple[float, str | None, bool] | None]]: - """Per reference, per target token: (its score, what took the sound's place, whether that is known), or None - when the reference does not have the sound.""" - sounds = [(t.word, t.sound) for t in toks] - out = [] - for ref in refs: - known = len(ref.heard) == len(ref.sounds) - row: list[tuple[float, str | None, bool] | None] = [None] * len(toks) - for a, b, n in SequenceMatcher(None, ref.sounds, sounds, autojunk=False).get_matching_blocks(): - for k in range(n): - row[b + k] = (ref.scores[a + k], ref.heard[a + k] if known else None, known) - out.append(row) - return out - - @staticmethod - def _ref_scores(toks: list[TargetToken], refs: list[Reference]) -> list[list[float | None]]: - """Per reference, per target token, the score that reference got on the same sound (None: it does not have it).""" - return [[None if cell is None else cell[0] for cell in row] for row in Scorer._ref_rows(toks, refs)] - - def _carriers(self, ipa: str) -> list[list[list[int]]]: - """For each word, for each of its phonemes, the indexes in _targets(ipa) of the tokens that carry it. - A phoneme that collapsed into its neighbour (a geminate, a merged sound) shares the neighbour's token.""" - out, n, last = [], -1, None - for word in ipa.split("|"): - row = [] - for ph in word.split(): - idx = [] - for tid in self.vocab.token(ph)[0]: - tid = self.vocab.canon[tid] - if tid != last: - n, last = n + 1, tid - idx.append(n) - row.append(sorted(set(idx))) - out.append(row) - return out - - def _targets(self, ipa: str) -> list[TargetToken]: - """Target tokens on the merged sound classes (two neighbours that became one sound collapse, as in CTC).""" - out: list[TargetToken] = [] - for t in tokenize(ipa, self.vocab): - tid = self.vocab.canon[t.id] - if not out or out[-1].id != tid: - out.append(replace(t, id=tid)) - return out - - @staticmethod - def _spellings(ipa: str, toks: list[TargetToken]) -> list[str]: - """How each target token is written in the phrase bank, geminates included ("m m").""" - runs: list[tuple[int, str, int]] = [] # (word, phoneme, repeat) - for w, word in enumerate(ipa.split("|")): - for ph in map(written, word.split()): - if runs and runs[-1][:2] == (w, ph): - runs[-1] = (w, ph, runs[-1][2] + 1) - else: - runs.append((w, ph, 1)) - if [(w, ph) for w, ph, _ in runs] != [(t.word, t.phoneme) for t in toks]: - return [t.phoneme for t in toks] # tokens merged across a boundary: no geminate info - return [" ".join([ph] * n) for _, ph, n in runs] - - def _heard_hebrew(self, logp: np.ndarray, toks: list[TargetToken], al, ipa: str, ar_words: list[str]) -> list[str]: - """What the model heard, in Hebrew letters, cut at the times where the expected words begin. - - A sound heard as expected is written the way the answer writes it (length, gemination, emphasis: - things the scorer does not tell apart), so the two lines differ only where the scorer heard a difference. - A letter with several correct realisations is written as the one said (כּ for a rural ق), not coloured. - """ - n_words = len(ipa.split("|")) - starts = [None] * n_words # first frame of each expected word - expected: dict[int, tuple[int, str]] = {} # frame -> (token id, its spelling) - for tok, (a, b), spell in zip(toks, al.spans, self._spellings(ipa, toks)): - if starts[tok.word] is None: - starts[tok.word] = a - for t in range(a, b): - expected[t] = (tok.id, spell) - heard: list[list[str]] = [[] for _ in range(n_words)] - prev, w = None, 0 - for t, i in enumerate(logp[:, :self.vocab.n_real].argmax(1)): - i = int(i) - if i != prev and i != self.vocab.blank: - while w + 1 < n_words and starts[w + 1] is not None and t >= starts[w + 1]: - w += 1 - ph = base(self.vocab.i2t[i]) - tid, spell = expected.get(t, (None, "")) - if tid == i: - heard[w].append(spell) - elif i in self.vocab.members.get(tid, ()): - heard[w].append(spell if base(spell.split()[0]) == ph else ph) - elif ph in HEBREW_SOUNDS: - heard[w].append(ph) - prev = i - # the Arabic spelling is passed so the heard line follows the answer's conventions (ק for ق, silent ל) - return [word_to_hebrew(" ".join(phs), ar_words[w] if w < len(ar_words) else "") for w, phs in enumerate(heard)] - - def _variants(self, phrase: dict) -> list[tuple[str, str, str, list[TargetToken]]]: - """(arabic, ipa, ipa with symbols, tokens) for each accepted variant and its diphthong forms. Variants - that differ only in how a many-ways letter is said (ʔ / q / k for ق) become one: the first written. - A diphthong form the model has no symbols for is left out.""" - out, seen, off = [], set(), self._off_symbols() - for ar, ipa in phrase["variants"]: - forms = (ipa, *diphthong_variants(ar, ipa)) if self._on("diphthong") else (ipa,) - for n, form in enumerate(forms): - aipa = archify(ar, form, off) - if aipa in seen: - continue - seen.add(aipa) - try: - out.append((ar, form, aipa, self._targets(aipa))) - except KeyError: - if n == 0: - raise - return out - - def _fit(self, logp: np.ndarray, phrase: dict, energy: np.ndarray | None = None) -> Fit | None: - """The accepted variant the audio fits best and the score of each of its sounds, from merged - posteriors, or None if the recording is too short for every variant.""" - blank, vocab = self.vocab.blank, self.vocab - heard_ids = greedy_decode(logp[:, :vocab.n_real], blank) - cands = self._variants(phrase) - ext = vocab.extend(logp) # after tokenising: a variant may have brought a new class - best, tried, paths = None, [], [] - for ar, ipa, aipa, toks in cands: - al = ctc_viterbi(ext, [t.id for t in toks], blank) - tried.append((ar, ipa, round(al.total_logp, 2) if al else None)) - paths.append([t.sound for t in toks]) - if al and (best is None or al.total_logp > best[4].total_logp): - best = (ar, ipa, aipa, toks, al) - if best is None: - return None - - ar, ipa, aipa, toks, al = best - ids = [t.id for t in toks] - ar_words = ar.split() - scores = score_tokens(ext, al, ids, blank, self.cfg.gop, [vocab.members.get(i) for i in ids], - vocab.n_real, self.named if self._on("unnamed_sounds") else None, energy, - vowels=[t.phoneme in VOWELS for t in toks], edges=self._glide_edges(toks)) - gop_scores, exempted = list(scores), [None] * len(scores) - for i, (t, sc) in enumerate(zip(toks, scores)): - heard = vocab.i2t.get(sc.competitor) if sc.competitor is not None else None - for name, applies, floor in EXEMPTIONS: - if self._on(name) and applies(toks, i, sc, heard, ar_words): - scores[i] = replace(sc, score=max(sc.score, 100.0 if floor is None else getattr(self.cfg, floor))) - exempted[i] = name - break - exempt_scores = list(scores) - inserted = self._insertions(logp, toks, al) - cap = float((self.rules.insertion or {}).get("score_cap", 60)) - for j in inserted: - scores[j] = replace(scores[j], score=min(scores[j].score, cap)) - insert_scores = list(scores) - shortened = self._shortened(toks, al, scores, energy) - cap = float((self.rules.length or {}).get("score_cap", 60)) - for j in shortened: - scores[j] = replace(scores[j], score=min(scores[j].score, cap)) - same = lambda h, t: h == t or h in vocab.members.get(t, ()) - raw = edit_distance(heard_ids, ids, same) - distance, weight = self._distance(heard_ids, toks, gop_scores, exempted, same) - veto = self._veto(toks, gop_scores, exempted) - unrelated = bool(distance / weight > self.cfg.unrelated_ratio) and not veto - return Fit(ar, ipa, aipa, toks, al, scores, heard_ids, unrelated, ext, candidates=tried, gop_scores=gop_scores, - exempted=exempted, exempt_scores=exempt_scores, inserted=inserted, insert_scores=insert_scores, - shortened=shortened, distance=distance, energy=energy, - paths=paths, raw_distance=raw, distance_weight=weight, veto=veto) - - # heard at the very start or end of the recording, these are usually breath or noise, not a sound of another word - _EDGE_NOISE = {"h", "θ", "f", "s", "x", "χ"} - _ANY_VOWEL = {"a", "e", "i", "o", "u", "ə", "ɪ", "ʊ", "ɛ", "ɔ", "æ", "ɐ", "ɑ"} - _FREE_DROPS = {"glottal_dropped", "article_vowel_elided"} - _SHORT_VOWELS = {"a", "e", "i", "o", "u"} - - def _distance(self, heard_ids: list[int], toks: list[TargetToken], gop_scores: list[TokenScore], - exempted: list[str | None], same, lost: frozenset[int] = frozenset()) -> float: - """How far the free decode is from the chosen variant, forgiving only what the exemptions mean: - - a vowel heard as another vowel: 0 when it is the competitor an exemption forgave, else 0.25 (a consonant - for a vowel: 1); - - a consonant heard as the competitor its exemption forgave (b/p, t/d): 0. The exemptions that forgive a - dropped sound (_FREE_DROPS) do not forgive a replaced one; - - a deletion: 0 for _FREE_DROPS, 0.25 for another exempted short vowel, else 1; - - edge noise (_EDGE_NOISE before the first or after the last sound): 0.25; - - a sound in `lost` (the native reference lost it too, under ref_noise_floor) replaced or dropped: 0.25. - Returns (distance, weight): weight is the sum of the deletion costs, the sounds that can be wrong at all, and - at least half the sounds, so a short word does not turn unrelated on one stray sound.""" - i2t, n = self.vocab.i2t, len(toks) - name = lambda h: base(i2t.get(h, "")) - comp = [name(sc.competitor) if sc.competitor is not None else None for sc in gop_scores] - - def sub(h, j): - if same(h, toks[j].id): - return 0.0 - if j in lost: - return 0.25 - heard = name(h) - if toks[j].phoneme in VOWELS: - return (0.0 if exempted[j] and comp[j] == heard else 0.25) if heard in self._ANY_VOWEL else 1.0 - if (exempted[j] and exempted[j] not in self._FREE_DROPS and comp[j] == heard - and heard not in self._ANY_VOWEL): - return 0.0 - return 1.0 - - def dele(j): - if exempted[j] in self._FREE_DROPS: - return 0.0 - return 0.25 if exempted[j] and toks[j].phoneme in self._SHORT_VOWELS else 1.0 - - def dele_cost(j): - return min(dele(j), 0.25) if j in lost else dele(j) - - def ins(h, j): - return 0.25 if j in (0, n) and name(h) in self._EDGE_NOISE else 1.0 - - weight = max(1.0, n / 2, sum(dele(j) for j in range(n))) # the same with or without `lost` - return weighted_edit_distance(heard_ids, [t.id for t in toks], sub, dele_cost, ins), weight - - def _veto(self, toks: list[TargetToken], gop_scores: list[TokenScore], exempted: list[str | None], - lost: frozenset[int] = frozenset()) -> bool: - """The forced alignment clearly found the sentence's consonants (ScorerConfig.unrelated_veto_*). - A consonant that says nothing either way is left out: one an exemption lets drop (ʔ), and one the native - reference lost too (`lost`).""" - cfg = self.cfg - mute = lambda i: i in lost or exempted[i] in self._FREE_DROPS - cons = [sc for i, (t, sc) in enumerate(zip(toks, gop_scores)) if t.phoneme not in VOWELS and not mute(i)] - rest = [sc.score for i, sc in enumerate(gop_scores) if not mute(i)] - if not cons or not rest: - return False - sure = sum(float(np.exp(sc.peak)) >= cfg.unrelated_veto_peak for sc in cons) - return (sure >= 2 and sure / len(cons) >= cfg.unrelated_veto_share - and float(np.mean(rest)) >= cfg.unrelated_veto_mean) - - def _heatmap(self, logp: np.ndarray, toks: list[TargetToken], alignment, scores, - duration_s: float | None) -> dict: - """A compact posterior heatmap for teacher diagnostics: expected sounds, strongest competitors and blank. - - Max-pooling preserves short phoneme peaks while capping the response at 180 time columns. - A letter with several correct realisations is one row, the pooled posterior, labelled "ʔ/k/q". - """ - expected_ids = list(dict.fromkeys(t.id for t in toks)) - competitor_ids = list(dict.fromkeys(sc.competitor for sc in scores - if sc.competitor is not None and sc.competitor not in expected_ids))[:6] - ids = expected_ids + competitor_ids + ([self.vocab.blank] if self.vocab.blank not in expected_ids else []) - frames, bins = len(logp), min(len(logp), 180) - edges = np.linspace(0, frames, bins + 1, dtype=int) - labels = {t.id: ("/".join(self.vocab.i2t[m] for m in sorted(self.vocab.members[t.id])) - if t.id in self.vocab.members else t.phoneme) for t in toks} - rows = [] - for token_id in ids: - probability = np.exp(logp[:, token_id]) - values = [round(100 * float(probability[edges[i]:edges[i + 1]].max())) for i in range(bins)] - rows.append({"phoneme": labels.get(token_id, "blank" if token_id == self.vocab.blank - else self.vocab.i2t.get(token_id, "?")), - "expected": token_id in expected_ids, "values": values}) - greedy = logp[:, :self.vocab.n_real].argmax(1) - heard = [] - start = 0 - for end in range(1, frames + 1): - if end < frames and greedy[end] == greedy[start]: - continue - token_id = int(greedy[start]) - if token_id != self.vocab.blank: - heard.append({"phoneme": self.vocab.i2t.get(token_id, "?"), "start": start, "end": end}) - start = end - expected = [{"phoneme": tok.phoneme, "start": span[0], "end": span[1]} - for tok, span in zip(toks, alignment.spans)] - return {"duration_ms": round(1000 * duration_s) if duration_s is not None else None, - "frames": frames, "frame_bins": bins, "rows": rows, - "tracks": {"expected": expected, "heard": heard}} - - def _max_tips(self) -> int: - """As many tips as the rules document says (3), or the 4 of before when its severity order is off.""" - if self.cfg.max_tips: - return self.cfg.max_tips - return int(self.rules.scoring.get("max_tips", 4)) if self._on("tips_by_severity") else 4 - - def _beyond_margin(self, tok: TargetToken, sc: TokenScore, heard: str | None) -> bool: - """Is the competitor likelier than the target by more than the pair's margin? Pairs the model confuses - on natives too (emphatics, a dropped h) need a clearer win before they count as a mistake.""" - targets = [tok.sound, tok.phoneme] - if sc.deleted: - return sc.peak <= self.cfg.gop.deleted_peak - self._margin(targets, "_") - if heard is None: - return True - return -sc.gop >= self._margin(targets, heard) - - def _evidenced(self, sc: TokenScore) -> bool: - """Did the competitor show up in the audio: win at least one frame, or reach the confident posterior? - A competitor barely likelier than a weak target, and winning no frame, is no finding (a tip needs this).""" - return sc.comp_frames >= 1 or sc.comp_peak >= float(np.log(self.cfg.gop.confident_peak)) - - def _heard(self, tok: TargetToken, sc: TokenScore) -> str | None: - """What took the sound's place, the same way for a learner and a native reference: "_" when the sound is - missing, the competitor's symbol when it won beyond the pair's margin with evidence, None when it was kept.""" - if sc.deleted: - return "_" - sym = self.vocab.i2t.get(sc.competitor) if sc.competitor is not None else None - if sym is None or sc.gop >= 0 or not self._evidenced(sc) or not self._beyond_margin(tok, sc, sym): - return None # no competitor, the target won, no evidence, or not by the pair's margin - return sym - - def _margin(self, targets: list[str], heard: str | None) -> float: - """The pair's margin (rules 6.2), or the default for every pair when the teacher switched margins off.""" - return self.rules.margin(targets if self._on("margins") else [], heard) - - def _dialect(self, toks: list[TargetToken], scores: list[TokenScore]) -> dict: - """Which dialect the accepted realisations point to: {"dialect": urban | rural} when they agree, - {"mixed_dialect": True} when one is urban-only and another rural-only. Never part of the score.""" - seen: set[str] = set() - for t, sc in zip(toks, scores): - if sc.realised is None or sc.score < self.cfg.issue_below: - continue - ds = self.rules.dialects_of(t.sound, base(self.vocab.i2t[sc.realised])) - if len(ds) == 1: - seen |= ds - if len(seen) > 1: - return {"mixed_dialect": True} - return {"dialect": next(iter(seen))} if seen else {} - - def score_logp(self, logp: np.ndarray, phrase: dict, refs: list[Reference] = (), diagnostics: bool = False, - duration_s: float | None = None, energy: np.ndarray | None = None) -> dict: - logp = self.vocab.merge(logp) - fit = self._fit(logp, phrase, energy) - if fit is None: - return {"error": "too_short", "message": "ההקלטה קצרה מדי. החזיקו את הכפתור ואמרו את כל המשפט."} - ar, ipa, toks, al, scores, unrelated = fit.ar, fit.ipa, fit.toks, fit.al, fit.scores, fit.unrelated - i2t = self.vocab.i2t - heard = " ".join(i2t[i] for i in fit.heard_ids) - # Measure against what the model gives a native on the same sound: a sound it misses in the - # reference voice too (ʕ in على heard as dropped) is the model's weakness, not the learner's. - heatmap = self._heatmap(fit.logp, toks, al, scores, duration_s) if diagnostics else None - raw, (bases, refused) = scores, self._baseline(toks, scores, refs) - floor_ = self.cfg.ref_noise_floor - # under the noise floor the ratio of two near-zero scores is noise (0.3 / 0.2 was capped to 30): give it back - scores = [sc if b is None or b >= 100 - else replace(sc, score=100.0) if b < floor_ - else replace(sc, score=round(min(100.0, 100.0 * sc.score / b), 1)) - for sc, b in zip(raw, bases)] - after_baseline = scores - # the unrelated gate ran before the references were known: a sound they lost too is the model's limitation - # in this phrase, not a sign of another sentence, so the gate is decided again without counting it - lost = frozenset(i for i, b in enumerate(bases) if b is not None and b < floor_) - if lost and unrelated: - same = lambda h, t: h == t or h in self.vocab.members.get(t, ()) - distance, weight = self._distance(fit.heard_ids, toks, fit.gop_scores, fit.exempted, same, lost) - veto = self._veto(toks, fit.gop_scores, fit.exempted, lost) - unrelated = bool(distance / weight > self.cfg.unrelated_ratio) and not veto - fit = replace(fit, distance=distance, distance_weight=weight, veto=veto, unrelated=unrelated) - # Teacher floors take priority. The base model has a recurring false negative for pharyngeal ʕ; - # when it reports only silence there, keep a modest penalty instead of claiming it was certainly absent. - phrase_key, calibrated = str(phrase.get("key", phrase.get("he", ""))), [] - adjusted, floors = [], [] - for tok, sc in zip(toks, scores): - heard_ph = "_" if sc.deleted else (i2t.get(sc.competitor) if sc.competitor is not None else "") - floor = next((self.calibrations[k] for k in ((tok.sound, heard_ph, phrase_key), (tok.sound, heard_ph, ""), - (tok.phoneme, heard_ph, phrase_key), (tok.phoneme, heard_ph, "")) - if k in self.calibrations), None) - if floor is None and tok.sound == "ʕ" and heard_ph == "_": - floor = self.cfg.ayin_deleted_floor - floors.append(floor) - if floor is not None and sc.score < floor: - calibrated.append({"expected": tok.phoneme, "heard": heard_ph, "from": round(sc.score), "to": round(floor)}) - sc = replace(sc, score=floor) - adjusted.append(sc) - scores = adjusted - forgiven = {t.word for t, sc, r in zip(toks, scores, raw) if sc.score > r.score} - - phonemes, tips, seen = [], [], set() - heard_as = [] # per phoneme, what took its place ("_": nothing), flagged or not: the reason a letter gives - checks = [] # per phoneme, the rule for what took its place (None: no rule) - beyond = [] # per phoneme, whether what took its place won by the pair's margin - ar_words = ar.split() - tr_words = to_hebrew(ipa, ar) - for idx, (tok, sc) in enumerate(zip(toks, scores)): - heard_ph = "_" if sc.deleted else (i2t.get(sc.competitor) if sc.competitor is not None else None) - # a competitor with no acoustic evidence (won no frame, never confident) is not named: no substitution - # tip and no "heard as" for its letter, whose score stays as it is - unevidenced = heard_ph is not None and not sc.deleted and not self._evidenced(sc) - if unevidenced: - heard_ph = None - check = self._check(tok, heard_ph) - beyond.append(self._beyond_margin(tok, sc, heard_ph)) - flagged = (bool(sc.score < self.cfg.flag_below_sound.get(tok.sound, self.cfg.flag_below)) and beyond[-1] - and not unevidenced and (tok.phoneme not in VOWELS or self._flag_vowel(toks, idx))) - # a long vowel said short (3.8): heard as its short vowel, with the rule's tip, unless the native - # reference was as short (the baseline gave the score back) - short = idx in fit.shortened and sc.score < self.cfg.issue_below - if short: - spec, heard_ph, flagged, beyond[-1] = self.rules.length, tok.phoneme.rstrip("ː"), True, True - check = Check(spec["rule"], tok.phoneme, heard_ph, int(spec["severity"]), str(spec["feedback"])) - if idx in fit.inserted: # a vowel before this consonant (3.10): the reason its letter gives - spec = self.rules.insertion - heard_as.append("+" + fit.inserted[idx]) - checks.append(Check(spec["rule"], tok.phoneme, "+", int(spec["severity"]), str(spec["feedback"]))) - else: - heard_as.append(heard_ph) - checks.append(check) - p = {"ipa": tok.phoneme, "word": tok.word, "score": sc.score, - "flagged": flagged, "heard": heard_ph if flagged else None} - if idx in fit.inserted: - p["inserted"] = fit.inserted[idx] - if short: - p["shortened"] = fit.shortened[idx]["ratio"] - if tok.sound != tok.phoneme: - p["sound"] = tok.sound - if sc.realised is not None: - p["realised"] = base(i2t[sc.realised]) - phonemes.append(p) - if short and not unrelated: - if check.rule not in seen: - seen.add(check.rule) - tips.append({"title": self.rules.titles.get(check.rule, check.rule), "text": check.feedback, - "key": check.rule, "severity": check.severity, - "word": ar_words[tok.word] if tok.word < len(ar_words) else "", - "word_tr": tr_words[tok.word], "idx": idx, "score": round(sc.score)}) - elif flagged and not unrelated: - tip = tip_for(tok.sound, heard_ph, check) - if tip and tip["key"] not in seen: - seen.add(tip["key"]) - tip["word"] = ar_words[tok.word] if tok.word < len(ar_words) else "" - tip["word_tr"] = tr_words[tok.word] - tip["idx"] = idx - tip["score"] = round(sc.score) - tips.append(tip) - if idx in fit.inserted and not unrelated and "cluster" not in seen: - seen.add("cluster") - spec = self.rules.insertion - tips.append({"title": self.rules.titles.get(spec["rule"], spec["rule"]), "text": str(spec["feedback"]), - "key": spec["rule"], "severity": int(spec["severity"]), - "word": ar_words[tok.word] if tok.word < len(ar_words) else "", - "word_tr": tr_words[tok.word], "idx": idx, "score": round(sc.score)}) - - n_words = len(ipa.split("|")) - heard_words = self._heard_hebrew(logp, toks, al, fit.aipa, ar_words) - words, k = [], self.cfg.word_min_weight - carriers, ipa_words = self._carriers(fit.aipa), ipa.split("|") - for w in range(n_words): - ws = [p["score"] for p in phonemes if p["word"] == w] - # the transliteration letter by letter, each with the weakest score among its sounds; a letter the - # page colours (under LETTER_GOOD) also says why: what that sound was heard as, and how to say it - letters = [] - for text, at in word_letters(ipa_words[w], ar_words[w] if w < len(ar_words) else ""): - got = [i for j in at for i in carriers[w][j] if 0 <= i < len(phonemes)] - worst = min(got, key=lambda i: phonemes[i]["score"]) if got else None - letter = {"t": text, "score": phonemes[worst]["score"] if got else None} - if got and letter["score"] < LETTER_GOOD and not unrelated: - letter.update(letter_reason(toks[worst].sound, heard_as[worst], checks[worst])) - letters.append(letter) - if w in forgiven and min(ws) >= self.cfg.issue_below: - heard_words[w] = tr_words[w] # what differs from the answer here is what a native voice gets too - words.append({"ar": ar_words[w] if w < len(ar_words) else "", "tr": tr_words[w], "heard_tr": heard_words[w], - "score": round(k * min(ws) + (1 - k) * float(np.mean(ws)), 1) if ws else 0.0, - "letters": letters, "phonemes": [p for p in phonemes if p["word"] == w]}) - - weights = np.array([0.5 if p["ipa"] in VOWELS else 1.0 for p in phonemes]) - phone_mean = float(np.dot(weights, [p["score"] for p in phonemes]) / weights.sum()) - word_mean = float(np.mean([w["score"] for w in words])) - overall = 0.5 * phone_mean + 0.5 * word_mean # one wrong consonant must pull the sentence down - if unrelated: - overall = min(overall, 30.0) - # most crucial first: the mistakes that change meaning (severity 3), then the worst-scoring sounds - max_tips = self._max_tips() - tips = (sorted(tips, key=lambda t: (-t["severity"], t["score"])) if self._on("tips_by_severity") - else sorted(tips, key=lambda t: t["score"]))[:max_tips] - tip_idx, issue_idx = {t["idx"] for t in tips}, [] - - # Anything short of 100 must say why: every imprecise sound that did not get a full tip, - # weakest first. If nothing is clearly off, name the weakest few. - issues = [] - if round(overall) < 100 and not unrelated: - tipped = {t.pop("idx") for t in tips} - rest = sorted((i for i in range(len(toks)) if i not in tipped and scores[i].score < 100), - key=lambda i: scores[i].score) - picked = [i for i in rest if scores[i].score < self.cfg.issue_below and beyond[i]] - if not picked and not tips: - picked = rest[:3] - issue_idx = picked[:self.cfg.max_issues] - for i in issue_idx: - tok, sc = toks[i], scores[i] - # a one-frame onset spike (rule 31) is no sound the learner said: listed, but not named - comp = (i2t.get(sc.competitor) if sc.competitor is not None and fit.exempted[i] != "onset_transient" - else None) - if i in fit.inserted: - issues.append({"sound": "תנועה מיותרת לפני " + (sound_name(tok.phoneme) or tok.phoneme), - "word_tr": tr_words[tok.word], "score": round(sc.score), "missing": False, - "heard": sound_name(fit.inserted[i])}) - continue - if phonemes[i].get("shortened") is not None: - issues.append({"sound": sound_name(tok.phoneme) or tok.phoneme, "word_tr": tr_words[tok.word], - "score": round(sc.score), "missing": False, - "heard": sound_name(tok.phoneme.rstrip("ː"))}) - continue - issues.append({"sound": sound_name(tok.sound) or sound_name(tok.phoneme) or tok.phoneme, - "word_tr": tr_words[tok.word], "score": round(sc.score), "missing": bool(sc.deleted), - "heard": None if sc.deleted else sound_name(comp)}) - for t in tips: - t.pop("idx", None) - result = {"score": round(overall), "variant": {"ar": ar, "tr": " ".join(tr_words), "ipa": ipa}, "heard_ipa": heard, "heard_tr": " ".join(h for h in heard_words if h), - "unrelated": unrelated, "words": words, "tips": tips, "issues": issues, "references": len(refs), - **self._dialect(toks, scores)} - if calibrated: - result["calibrated"] = calibrated - if heatmap is not None: - result["phoneme_heatmap"] = heatmap - if diagnostics: - result["trace"] = self._trace(fit, refs, bases, after_baseline, floors, scores, phonemes, checks, beyond, - words, tips, tip_idx, set(issue_idx), phone_mean, word_mean, overall, heard, - duration_s, refused) - return result - - def _trace(self, fit: Fit, refs, bases, after_baseline, floors, scores, phonemes, checks, beyond, - words, tips, tip_idx, issue_idx, phone_mean, word_mean, overall, heard, - duration_s: float | None = None, refused: list[str | None] | None = None) -> dict: - """Every stage of the classification with its numbers, weights and thresholds, for the teacher's view - (the 'stages' part of the phoneme analysis). Nothing here changes the result.""" - cfg, g, i2t = self.cfg, self.cfg.gop, self.vocab.i2t - rows = self._ref_rows(fit.toks, refs) - per_ref = [[None if c is None else c[0] for c in row] for row in rows] - per_heard = [[None if c is None else c[1] for c in row] for row in rows] # what took the sound's place in each voice - refused = refused or [None] * len(fit.toks) - p = lambda lp: round(float(np.exp(lp)), 4) if lp > -1e20 else 0.0 - max_tips = self._max_tips() - n, T = len(fit.toks), len(fit.logp) - pr = lambda col, a, b: [round(float(x), 3) for x in np.exp(fit.logp[a:b, col])] if col is not None else None - tokens = [] - for i, (tok, gsc, sc) in enumerate(zip(fit.toks, fit.gop_scores, scores)): - # the frames the curves show: the sound's window (score_tokens) and a few around it - ca, cb = max(0, gsc.window[0] - 4), min(T, gsc.window[1] + 4) - curves = {"start": ca, "target": pr(tok.id, ca, cb), "competitor": pr(gsc.competitor, ca, cb), - "prev": pr(fit.toks[i - 1].id, ca, cb) if i else None, - "next": pr(fit.toks[i + 1].id, ca, cb) if i + 1 < n else None, - "blank": pr(self.vocab.blank, ca, cb)} - heard_ph = "_" if gsc.deleted else (i2t.get(gsc.competitor) if gsc.competitor is not None else None) - members = self.vocab.members.get(tok.id) - targets = [tok.sound, tok.phoneme] - confidence = min(1.0, p(gsc.peak) / g.confident_peak) - check = checks[i] - b = bases[i] - stages = [ - {"stage": "gop", "score": gsc.score}, - {"stage": "exemption", "score": fit.exempt_scores[i].score, "applied": fit.exempted[i]}, - {"stage": "insertion", "score": fit.insert_scores[i].score, "applied": fit.inserted.get(i)}, - {"stage": "length", "score": fit.scores[i].score, "applied": fit.shortened.get(i)}, - {"stage": "baseline", "score": after_baseline[i].score, "ref": b, - "refs": [row[i] for row in per_ref], "heard": [None if h is None else norm(h) for h in (row[i] for row in per_heard)], - "refused": refused[i], "applied": b is not None and b < 100}, - {"stage": "calibration", "score": sc.score, "floor": floors[i], - "applied": floors[i] is not None and after_baseline[i].score < floors[i]}, - ] - tokens.append({ - "i": i, "word": tok.word, "ipa": tok.phoneme, "sound": tok.sound, - "class": sorted(base(i2t[m]) for m in members) if members else None, - "realised": base(i2t[gsc.realised]) if gsc.realised is not None else None, - "span": list(fit.al.spans[i]), "window": list(gsc.window), "curves": curves, - "p_target": p(gsc.peak), "competitor": heard_ph if not gsc.deleted else None, - "competitor_other": gsc.other, "p_competitor": p(gsc.comp_peak), - "competitor_frames": gsc.comp_frames, - "gop": gsc.gop, "confidence": round(confidence, 3), - "gop_factor": round(float(np.exp(gsc.gop * g.leniency)), 3), - "confidence_factor": round(float(np.sqrt(confidence)), 3), - "deleted": gsc.deleted, "stages": stages, "final": sc.score, - "margin": {"needed": self._margin(targets, "_" if gsc.deleted else heard_ph), - "got": (round(g.deleted_peak - gsc.peak, 3) if gsc.deleted else round(-gsc.gop, 3)), - "beyond": beyond[i]}, - "vowel": tok.phoneme in VOWELS, "weight": 0.5 if tok.phoneme in VOWELS else 1.0, - "flagged": phonemes[i]["flagged"], - "rule": {"id": check.rule, "title": self.rules.titles.get(check.rule, check.rule), - "severity": check.severity} if check else None, - "tip": i in tip_idx, "issue": i in issue_idx, - }) - return { - "config": { - "leniency": g.leniency, "confident_peak": g.confident_peak, - "consonant_window": g.consonant_window, - "deleted_peak": round(float(np.exp(g.deleted_peak)), 3), - "deleted_comp": round(float(np.exp(g.deleted_comp)), 3), - "flag_below": cfg.flag_below, "flag_below_sound": dict(cfg.flag_below_sound), "issue_below": cfg.issue_below, "near_vowel_floor": cfg.near_vowel_floor, - "word_min_weight": cfg.word_min_weight, "vowel_weight": 0.5, "unrelated_ratio": cfg.unrelated_ratio, - "unrelated_veto": {"share": cfg.unrelated_veto_share, "peak": cfg.unrelated_veto_peak, - "mean": cfg.unrelated_veto_mean}, - "unrelated_cap": 30.0, "letter_good": LETTER_GOOD, "max_tips": max_tips, "max_issues": cfg.max_issues, - "ref_min_mean": cfg.ref_min_mean, "ref_noise_floor": cfg.ref_noise_floor, "margin_default": float(self.rules.scoring.get("margin_default", 0.0)), - "ayin_deleted_floor": cfg.ayin_deleted_floor, "onset_transient_floor": cfg.onset_transient_floor, - "margins": [{"target": t, "heard": h, "margin": m} for (t, h), m in sorted(self.rules.margins.items())] - if self._on("margins") else [], - "exemptions": [name for name, _, _ in EXEMPTIONS if self._on(name)], - "insertion": ({k: self.rules.insertion[k] for k in ("score_cap", "min_peak", "min_frames")} - if self.rules.insertion and self._on(f"rule_{self.rules.insertion['rule']}") else None), - "vowel_length": ({k: v for k, v in self.rules.length.items() if k not in ("rule", "feedback")} - if self.rules.length and self._on(f"rule_{self.rules.length['rule']}") else None), - "disabled": sorted(self.disabled), - }, - "variants": [{"ar": ar, "ipa": ipa, "logp": lp, "chosen": ar == fit.ar and ipa == fit.ipa, - "tokens": fit.paths[k] if k < len(fit.paths) else []} - for k, (ar, ipa, lp) in enumerate(fit.candidates)], - "frames": {"count": T, "ms": round(1000 * duration_s / T, 2) if duration_s and T else 20.0, - "energy": ([round(float(e), 3) for e in fit.energy / max(float(fit.energy.max()), 1e-9)] - if fit.energy is not None else None)}, - "aipa": fit.aipa, - "unrelated": {"heard": heard, "distance": round(fit.distance, 2), "raw_distance": fit.raw_distance, - "tokens": n, "weight": round(fit.distance_weight, 2), - "ratio": round(fit.distance / fit.distance_weight, 3), "threshold": cfg.unrelated_ratio, - "veto": fit.veto, "unrelated": fit.unrelated}, - "references": [{"voice": r.voice or f"קול {k + 1}", "mean": round(float(np.mean(r.scores)), 1), - "sounds": len(r.sounds)} for k, r in enumerate(refs)], - "tokens": tokens, - "words": [{"tr": w["tr"], "scores": [ph["score"] for ph in w["phonemes"]], - "min": min((ph["score"] for ph in w["phonemes"]), default=0.0), - "mean": round(float(np.mean([ph["score"] for ph in w["phonemes"]])), 1) if w["phonemes"] else 0.0, - "score": w["score"]} for w in words], - "sentence": {"phone_mean": round(phone_mean, 1), "word_mean": round(word_mean, 1), - "phone_weight": 0.5, "word_weight": 0.5, - "before_cap": round(0.5 * phone_mean + 0.5 * word_mean, 1), "final": round(overall), - "capped": fit.unrelated}, - "tips": [{"key": t["key"], "severity": t["severity"], "score": t["score"]} for t in tips], - } - - -# ---------- real model ---------- - -class HFEmitter: - """wav2vec2 phoneme CTC model from the Hugging Face hub (or a local fine-tuned checkpoint).""" - - def __init__(self, model_id: str, device: str | None = None): - import torch - from huggingface_hub import hf_hub_download - from transformers import AutoFeatureExtractor, Wav2Vec2ForCTC - - self.torch = torch - self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") - self.fe = AutoFeatureExtractor.from_pretrained(model_id) - self.model = Wav2Vec2ForCTC.from_pretrained(model_id).to(self.device).eval() - vocab_path = (os.path.join(model_id, "vocab.json") if os.path.isdir(model_id) - else hf_hub_download(model_id, "vocab.json")) - with open(vocab_path, encoding="utf-8") as f: - self.token_to_id: dict[str, int] = json.load(f) - - def __call__(self, wav: np.ndarray) -> np.ndarray: - inputs = self.fe(wav, sampling_rate=SAMPLE_RATE, return_tensors="pt") - with self.torch.inference_mode(): - logits = self.model(inputs.input_values.to(self.device)).logits[0] - return self.torch.log_softmax(logits.float(), -1).cpu().numpy() - - -def decode_audio_with_timing(data: bytes) -> tuple[np.ndarray, int]: - """Decodes audio and returns (trimmed 16 kHz samples, samples removed from its start).""" - proc = subprocess.run( - ["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", "pipe:0", - "-t", str(MAX_SECONDS), "-ac", "1", "-ar", str(SAMPLE_RATE), "-f", "f32le", "pipe:1"], - input=data, capture_output=True, check=False) - if proc.returncode != 0: - raise ValueError(proc.stderr.decode(errors="replace")[:300]) - wav = np.frombuffer(proc.stdout, dtype=np.float32) - trim_start = 0 - # trim leading/trailing silence so CTC isn't fed seconds of blank - if wav.size: - frame = 320 - energy = np.sqrt(np.convolve(wav ** 2, np.ones(frame) / frame, mode="same")) - voiced = np.where(energy > max(0.01, energy.max() * 0.05))[0] - if voiced.size: - pad = SAMPLE_RATE // 5 - trim_start = max(0, int(voiced[0]) - pad) - wav = wav[trim_start:voiced[-1] + pad] - return wav, trim_start - - -def decode_audio(data: bytes) -> np.ndarray: - """Any browser recording (webm/opus, ogg, mp4/aac, wav) -> trimmed 16 kHz mono float32.""" - return decode_audio_with_timing(data)[0] - - -# Per-sound flag thresholds, chosen per model on the Minerva dev split and checked on its test split -# (scripts/minerva_benchmark.py --sweep / --compare, with the TTS baselines). A threshold belongs to the model it was -# measured on: another model (a fine-tuned checkpoint) starts from the global flag_below until it is measured too. -FLAG_BELOW_BY_MODEL: dict[str, dict[str, float]] = { - # The base model hears a correct ح as ه/خ with scores in the 20s-40s: at 50 it flagged 12.7% of natives' ح; - # at 20, 5.6%, still catching 69% of خ and 71% of ه said for ح (was 76% / 75%). - "facebook/wav2vec2-xlsr-53-espeak-cv-ft": {"ħ": 20.0}, -} - - -def scorer_config(model_id: str) -> ScorerConfig: - return ScorerConfig(flag_below_sound=dict(FLAG_BELOW_BY_MODEL.get(model_id, {}))) - - -def load_scorer(model_id: str) -> Scorer: - from .powsm import PowsmEmitter, PowsmOnnxEmitter, onnx_model_dir, parse_model_id - powsm, exported = parse_model_id(model_id), onnx_model_dir(model_id) - if exported: # an exported POWSM-CTC (scripts/export_powsm_onnx.py): onnxruntime only, what the Space runs - emitter = PowsmOnnxEmitter(str(exported)) - vocab = Vocab(emitter.token_to_id, blank_token=emitter.blank_token) - elif powsm: # POWSM-CTC through ESPnet (app/powsm.py): a local trial, needs .zero-shot-deps - emitter = PowsmEmitter(*powsm) - vocab = Vocab(emitter.token_to_id, blank_token=emitter.blank_token) - else: - emitter = HFEmitter(model_id) - vocab = Vocab(emitter.token_to_id) - scorer = Scorer(emitter, vocab, scorer_config(model_id)) - # resolve every phoneme once at startup so missing symbols show up in the logs and /api/health - from .phrases import PHRASES - for p in PHRASES: - for ar, ipa in p["variants"]: - tokenize(archify(ar, ipa), vocab) - if vocab.downgraded: - log.warning("phonemes mapped to fallbacks: %s", vocab.downgraded) - if vocab.class_missing: - log.warning("realisations the model cannot hear, left out of their letter's class: %s", vocab.class_missing) - return scorer diff --git a/app/speech_evaluation.py b/app/speech_evaluation.py deleted file mode 100644 index 3dbd0b0bdd4aeace407fed640abdf8b03430ff4c..0000000000000000000000000000000000000000 --- a/app/speech_evaluation.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Replaceable speech policy for spoken conjugation exercises. - -The acoustic scorer is Palestinian-Arabic aware at the phoneme level, but it is not -treated as a general dialect ASR. In particular, an unrelated/low-confidence result -asks for a retry instead of becoming a grammatical error. -""" -from __future__ import annotations - -from typing import Any - - -class SpeechEvaluationService: - def __init__(self, scorer: Any, pass_score: int = 75): - self.scorer = scorer - self.pass_score = pass_score - - def evaluate(self, wav, phrase: dict, diagnostics: bool = False) -> dict: - result = self.scorer.score(wav, phrase, diagnostics=diagnostics) - if result.get("error"): - return {**result, "outcome": "uncertain", "counts_as_attempt": False} - if result.get("unrelated"): - return {**result, "outcome": "uncertain", "counts_as_attempt": False, - "message": "לא הצלחתי לזהות בביטחון את ההטיה. האזינו להקלטה ונסו שוב."} - correct = int(result.get("score", 0)) >= self.pass_score - return {**result, "outcome": "correct" if correct else "needs_work", - "error_kind": None if correct else "undetermined", - "correct": correct, "counts_as_attempt": True, - "message": "נכון" if correct else "לא הצלחתי לאשר את התשובה. הקשיבו להקלטה ולתשובה ונסו שוב."} diff --git a/app/texts.py b/app/texts.py deleted file mode 100644 index 79a5fcdb3753feba0a814636c32acc6596bfaa0f..0000000000000000000000000000000000000000 --- a/app/texts.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Texts the teacher adds in the panel: a story or a dialogue, practised line by line like the course's conversation. - -The teacher gives each line twice: in Hebrew, and in Arabic written in Hebrew letters, as the course writes it -(pointed, geresh for the sounds Hebrew lacks, ّ for doubling: כִּיף חַאלַכּ). The scorer needs the line's phonemes -and the answer's voice needs it in Arabic letters, so each word is read here: - a word of the course (LEX in workbook.py), found by its transliteration, gets the course's own Arabic spelling and - accepted pronunciations. Dagesh, shva and the final letters' forms are not compared, and a word written without - points is found by its letters alone; - any other word is read by rule, letter by letter (`_guess`). Short vowels come from the points, so an unpointed - word that the course does not have is read badly. Such words are reported back to the teacher (`guessed`). -""" -from __future__ import annotations - -import re - -from .workbook import LEX, _variants - -SHVA, DAGESH, SHADDA = "\u05b0", "\u05bc", "\u0651" -MARKS = re.compile("[\u05b0-\u05bc\u05c1\u05c2\u05c7\u0651]") -GERESH = "'\u05f3\u2019\u2018`\u00b4" -FINALS = {"ך": "כ", "ם": "מ", "ן": "נ", "ף": "פ", "ץ": "צ"} -VOWEL_OF = {"\u05b7": "a", "\u05b8": "a", "\u05b2": "a", "\u05b4": "i", "\u05bb": "u", "\u05b6": "e", "\u05b1": "e", - "\u05b5": "E", "\u05b9": "o", "\u05ba": "o", "\u05b3": "o", "\u05c7": "o"} # E: tsere, i alone and eː before י -# letter -> (phoneme, Arabic letter); ק is the urban ʔ, with q as a second reading -CONS = {"א": ("ʔ", "ا"), "ב": ("b", "ب"), "ג": ("g", "ج"), "ד": ("d", "د"), "ה": ("h", "ه"), "ו": ("w", "و"), - "ז": ("z", "ز"), "ח": ("ħ", "ح"), "ט": ("tˤ", "ط"), "י": ("j", "ي"), "כ": ("k", "ك"), "ל": ("l", "ل"), - "מ": ("m", "م"), "נ": ("n", "ن"), "ס": ("s", "س"), "ע": ("ʕ", "ع"), "פ": ("f", "ف"), "צ": ("sˤ", "ص"), - "ק": ("ʔ", "ق"), "ר": ("r", "ر"), "ש": ("ʃ", "ش"), "ת": ("t", "ت")} -GERESHED = {"ג": ("dʒ", "ج"), "ח": ("x", "خ"), "ע": ("ɣ", "غ"), "ד": ("dˤ", "ض"), "ט": ("ðˤ", "ظ"), "ת": ("t", "ث"), - "ז": ("dʒ", "ج"), "צ": ("sˤ", "ص")} -PUNCT_AR = {"?": "؟", ",": "،", ";": "؛"} -Cluster = tuple[str, bool, str] # a letter, whether it has a geresh, its points - - -def _clusters(word: str) -> list[Cluster]: - """The word's letters, each with its geresh and points. A letter in brackets, the article's silent (ל), is left out.""" - out: list[list] = [] - for ch in re.sub(r"\([^)]*\)", "", word): - if "א" <= ch <= "ת": - out.append([FINALS.get(ch, ch), False, ""]) - elif out and ch in GERESH: - out[-1][1] = True - elif out and MARKS.fullmatch(ch): - out[-1][2] += ch - return [(l, g, m) for l, g, m in out] - - -def _key(cl: list[Cluster], points: bool = True) -> str: - """What two spellings of one word share: the letters and their vowels, without dagesh (but the vav's) and shva. - The article's vowel is one, hiriq or tsere.""" - def marks(k: int, letter: str, m: str) -> str: - keep = {"\u05b7" if c == "\u05b8" else c for c in m if c != SHVA and (c != DAGESH or letter == "ו")} - if k == 0 and letter == "א" and "\u05b4" in keep: - keep = keep - {"\u05b4"} | {"\u05b5"} - return "".join(sorted(keep)) - return "".join(l + "'" * g + (marks(k, l, m) if points else "") for k, (l, g, m) in enumerate(cl)) - - -def _lexicon() -> tuple[dict, dict]: - pointed, plain = {}, {} - for ar, (tr, ipas) in LEX.items(): - cl = _clusters(tr) - entry = (ar.partition("#")[0], ipas) - pointed.setdefault(_key(cl), entry) - plain.setdefault(_key(cl, False), entry) - return pointed, plain - - -POINTED, PLAIN = _lexicon() - - -def _guess(cl: list[Cluster], silent_l: bool = False) -> tuple[str, list[str]]: - """(Arabic spelling, readings) of a word the course does not have, from its letters and points. `silent_l`: the - word was written with the article's (ל), so its first vowel is the article.""" - phs: list[str] = [] - ar = "" - n, i = len(cl), 0 - bare = lambda c: not c[1] and not any(m in VOWEL_OF or m == SHADDA for m in c[2]) # no vowel of its own - pointed = any(m in VOWEL_OF for _, _, marks in cl for m in marks) - if n > 1 and cl[0][0] == "ו" and bare(cl[0]) and (DAGESH in cl[0][2] or SHVA in cl[0][2]): # "and" on the word - phs, ar, i = ["u"], "و", 1 - start, prev = i, None # prev: the vowel the letter before ended in - while i < n: - letter, geresh, marks = cl[i] - last, vowel = i == n - 1, next((VOWEL_OF[m] for m in marks if m in VOWEL_OF), None) - double = SHADDA in marks - i += 1 - if bare((letter, geresh, marks)) and i - 1 > start: # a letter that only lengthens the vowel before it - long = None - if letter == "א": - long, ar = ("a" if last else "aː"), ar + "ا" - if prev: - phs.pop() - elif letter == "י" and (prev in ("i", "E") or not prev and last or not pointed and not last): - long, ar = ("eː" if prev == "E" else "i" if last else "iː"), ar + "ي" - if prev: - phs.pop() - elif letter == "ו" and not prev and (DAGESH in marks or not pointed and not last): - long, ar = ("u" if last else "uː"), ar + "و" - elif letter == "ה" and last and (prev in ("a", "e", "E") or not pointed): # the feminine ending - ar += "ة" - if not prev: - phs.append("a") - elif prev == "E": - phs[-1] = "e" - break - if long: - phs.append(long) - prev = None - continue - if letter == "ו" and DAGESH in marks and double and not vowel and not prev: # וּّ - phs += ["u", "w", "w"] - ar, prev = ar + "وّ", None - continue - if letter == "ו" and vowel == "o" and not double and not prev and i - 1 > start: # וֹ - phs.append("o" if last else "oː") - ar, prev = ar + "و", None - continue - ph, ar_letter = (GERESHED if geresh and letter in GERESHED else CONS)[letter] - if letter == "א" and i - 1 == start and vowel in ("i", "a", "E") and i < n and ( - cl[i][0] == "ל" and bare(cl[i]) and i + 1 < n or silent_l): # the article: אִלְ, or אִ(ל) before a doubled letter - phs.append("i") - ar += "ال" - if cl[i][0] == "ל" and bare(cl[i]): - phs.append("l") - i += 1 - prev = None - continue - phs += [ph, ph] if double else [ph] - ar += ("ا" if i - 1 == start else "أ") if letter == "א" else ar_letter + (SHADDA if double else "") - prev = vowel - if vowel: - phs.append(("e" if last else "i") if vowel == "E" else vowel) # a tsere is i inside the word, e at its end - main = " ".join(phs) - return ar, [main] + ([main.replace("ʔ", "q") if not cl[start][0] == "א" else "ʔ" + main[1:].replace("ʔ", "q")] - if any(l == "ק" for l, _, _ in cl) else []) - - -def _word(token: str) -> tuple[str, list[str], bool] | None: - """(Arabic, readings, whether the course has the word) of one written word; None when it has no letters.""" - cl = _clusters(token) - if not cl: - return None - tail = "".join(PUNCT_AR.get(c, c) for c in re.search(r"[?!.,;:]*$", token)[0]) - pointed = any(m in VOWEL_OF for _, _, marks in cl for m in marks) - for cut, pre_ar, pre_ipa in ((0, "", ""), (1, "و", "u ")): # the word itself, then "and" written on it - if cut and not (len(cl) > 1 and cl[0][0] == "ו" and not cl[0][1] and not any(m in VOWEL_OF for m in cl[0][2])): - break - hit = POINTED.get(_key(cl[cut:])) if pointed else PLAIN.get(_key(cl[cut:], False)) - if hit: - return pre_ar + hit[0] + tail, [pre_ipa + ipa for ipa in hit[1]], True - rest = cl[2:] if len(cl) > 2 and cl[0][0] == "א" and cl[1][0] == "ל" and not any(m in VOWEL_OF for m in cl[1][2]) else \ - cl[1:] if len(cl) > 1 and cl[0][0] == "א" and "(ל)" in token else None - hit = rest and (POINTED.get(_key(rest)) if pointed else PLAIN.get(_key(rest, False))) - if hit and not hit[0].startswith("ال"): # the article on a word of the course: אִלְבֵּית, אִ(ל)סַّיַּארַה - said = "i l " if rest is cl[2:] or len(rest) == len(cl) - 2 else "i " - double = lambda ipa: ipa if said == "i l " else f"{ipa.split()[0]} {ipa}" - return "ال" + hit[0] + tail, [said + double(ipa) for ipa in hit[1]], True - ar, ipas = _guess(cl, "(ל)" in token) - return (ar + tail, ipas, False) if ipas[0] else None - - -def read_line(tr: str) -> tuple[list[tuple[str, str, list[str]]], list[str]]: - """A line of Arabic in Hebrew letters as the words of a phrase, (Arabic, transliteration, readings) each, and - the words that were read by rule, as written.""" - words, guessed = [], [] - for token in tr.split(): - word = _word(token) - if word is None: # a dash, a number - continue - ar, ipas, known = word - words.append((ar, token, ipas)) - if not known: - guessed.append(token.strip("?!.,;:\"()")) - return words, guessed - - -def text_phrases(cat: str, lines: list[list[str]]) -> tuple[list[dict], list[str]]: - """A text's lines ([Hebrew, transliteration] each) as phrases in the format of phrases.py, in the text's order, - and its words that were read by rule. `key` is what a recording of the line is kept under: the Hebrew, numbered - from its second appearance in the text ("כן (2)"), so that two lines with the same Hebrew stay apart.""" - out, guessed, seen = [], [], {} - for seq, (he, tr) in enumerate(lines, 1): - words, new = read_line(tr) - guessed += [w for w in new if w not in guessed] - seen[he] = seen.get(he, 0) + 1 - out.append(dict(cat=cat, he=he, tr=tr, variants=_variants(words), seq=seq, - key=he if seen[he] == 1 else f"{he} ({seen[he]})")) - return out, guessed diff --git a/app/tips.py b/app/tips.py deleted file mode 100644 index ffbe86fa4c2a57c0a5086970f6dedacd9e612e44..0000000000000000000000000000000000000000 --- a/app/tips.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Hebrew feedback for the substitutions Hebrew speakers typically make. - -Keys are (expected phoneme, heard phoneme); "_" means the expected phoneme was missing. -Phonemes are in the IPA used by phrases.py; HEARD_ALIASES maps model outputs onto those symbols. -""" -import re - -from .rules import load as load_rules - -NAMES = { - "ħ": "ח (ح)", "ʕ": "ע (ع)", "x": "ח' (خ)", "ɣ": "ע' (غ)", "r": "ר (ر)", - "q": "ק (ق)", "ʔ": "עצירה סותמית (ء / ق)", "h": "ה (ه)", - "sˤ": "צ (ص)", "tˤ": "ט (ط)", "dˤ": "ד' (ض)", "ðˤ": "ט' (ظ)", - "k": "כ (ك)", "s": "ס (س)", "t": "ת (ت)", "d": "ד (د)", "z": "ז (ز)", - "ʃ": "ש (ش)", "dʒ": "ג' (ج)", "b": "ב (ب)", "f": "פ (ف)", "m": "מ (م)", - "n": "נ (ن)", "l": "ל (ل)", "w": "ו (و)", "j": "י (ي)", - "tʃ": "צ' (tʃ)", "θ": "ת' (th)", "ð": "ד״ (th)", - # letters with several correct realisations (archi.py): named by the letter, not by one way of saying it - "Q": "ק (ق)", "K": "כ (ك)", "TH": "ת' (ث)", "DH": "ד״ (ذ)", "ZH": "ט' (ظ)", "J": "ג' (ج)", -} - -# model output symbol -> symbol used in TIPS keys -HEARD_ALIASES = {"χ": "x", "ɦ": "h", "ɡ": "g", "ʁ": "ʁ"} - -_EMPH = "גב הלשון נסוג לאחור והלסת יורדת מעט; התנועות סביב העיצור נשמעות כהות יותר." - -TIPS = { - ("ħ", "x"): "נהגתה כמו ח' (خ), החי\"ת של העברית המודרנית. ح נוצרת בלוע ולא בחך: מכווצים את הגרון ונושפים, בלי רעש גירוד.", - ("ħ", "h"): "נהגתה כמו ה. צריך יותר כיווץ בלוע: הצליל יבש ומתוח, כמו לחישה חזקה מגרון צר.", - ("ħ", "_"): "לא נשמעה. ح היא עיצור לועי: מכווצים את שרירי הגרון ונושפים.", - ("ʕ", "ʔ"): "נהגתה כמו עצירה (א). ع היא עיצור לועי קולי: מכווצים את הגרון תוך כדי השמעת קול, בלי לסגור אותו.", - ("ʕ", "a"): "נבלעה לתוך התנועה. ع דורשת כיווץ ברור של הלוע לפני התנועה.", - ("ʕ", "_"): "נבלעה. ע (ع) נהגית תמיד, גם באמצע מילה.", - ("x", "ħ"): "נהגתה רכה מדי, כמו ح. خ היא חיכוך בחך הרך, כמו ח' בעברית.", - ("x", "h"): "נהגתה כמו ה. خ דורשת חיכוך בחך הרך, כמו ח' בעברית.", - ("x", "k"): "נהגתה כעצירה (כ). خ היא חיכוך רציף, בלי סגירה.", - ("ɣ", "x"): "נהגתה בלי קול, כמו خ. غ קולית, קרובה מאוד לר' הגרונית של העברית.", - ("ɣ", "r"): "נהגתה כמו ר מתגלגלת. غ קרובה לר' העברית הגרונית.", - ("ɣ", "g"): "נהגתה כעצירה (ג). غ היא חיכוך רציף ולא סגירה.", - ("r", "ʁ"): "ר (ر) נהגתה גרונית, כמו ר' עברית. ر מתגלגלת בקצה הלשון מאחורי השיניים העליונות.", - ("r", "ɣ"): "ר (ر) נהגתה גרונית, כמו ר' עברית, וזוהתה כ־غ. ر מתגלגלת בקצה הלשון.", - ("r", "x"): "ר (ر) נהגתה גרונית ובלי קול. ر מתגלגלת בקצה הלשון מאחורי השיניים.", - ("q", "k"): "ق נהגתה כמו כ/ק עברית. במבטא הכפרי זה מקובל, אבל ق נוצרת עמוק יותר, בענבל.", - ("ʔ", "k"): "ق נהגתה כמו כ/ק עברית. בדיבור העירוני ق נהגית כעצירה קצרה בגרון (\"אַהְוֶה\", לא \"קַהְוֶה\").", - ("h", "ħ"): "נהגתה חזקה מדי, כמו ح. ه היא נשיפה רכה בלבד.", - ("h", "x"): "נהגתה כמו ח' עברית. ه היא נשיפה רכה בלבד, בלי חיכוך.", - ("h", "_"): "נבלעה. בערבית ה נהגית תמיד, גם לפני עיצור (קַהְוֶה, פַאהֵם).", - ("sˤ", "s"): "נהגתה כ־ס רגילה. ص היא ס מודגשת: " + _EMPH, - ("tˤ", "t"): "נהגתה כ־ת רגילה. ط היא ת מודגשת: " + _EMPH, - ("dˤ", "d"): "נהגתה כ־ד רגילה. ض היא ד מודגשת: " + _EMPH, - ("ðˤ", "z"): "נהגתה כ־ז רגילה. ظ היא ז מודגשת: " + _EMPH, - ("ðˤ", "ð"): "נהגתה בלי הדגשה. ظ היא עיצור מודגש: " + _EMPH, -} - -# bare Arabic letter in a tip text -> "Hebrew (Arabic)", so Arabic never appears without a Hebrew reading -_PAIRED = {name[name.index("(") + 1]: name for name in NAMES.values() if "/" not in name} -DEFAULT_SEVERITY = 2 # a tip no rule in pronunciation-rules.md grades -_BARE_AR = re.compile(r"(? str: - return _BARE_AR.sub(lambda m: _PAIRED.get(m.group(), m.group()), text) - - -VOWEL_NAMES = { - "a": "תנועת אַ", "i": "תנועת אִ", "u": "תנועת אֻ", "e": "תנועת אֶ", "o": "תנועת אוֹ", - "aː": "תנועה ארוכה אַא", "iː": "תנועה ארוכה אִי", "uː": "תנועה ארוכה אוּ", - "eː": "תנועה ארוכה אֵי", "oː": "תנועה ארוכה אוֹ", -} - - -def sound_name(phoneme: str | None) -> str | None: - """Hebrew name of a phoneme (expected or heard), or None if we have no name for it. - Sounds Arabic does not have (p, v, ts) are named by the labels of pronunciation-rules.md.""" - if phoneme is None: - return None - phoneme = HEARD_ALIASES.get(phoneme, phoneme) - return NAMES.get(phoneme) or VOWEL_NAMES.get(phoneme) or load_rules().label(phoneme) - - -def letter_reason(expected: str, heard: str | None, check=None) -> dict: - """Why a letter of the answer is coloured: {"why": the sound and what was heard in its place ("_": nothing was), - "tip": how to say it, from the rule for that substitution (`check`, rules.Check) or TIPS}.""" - name = sound_name(expected) or expected - heard = HEARD_ALIASES.get(heard, heard) - other = sound_name(heard) - if heard and heard.startswith("+"): # a vowel added before this consonant (rules 3.10) - why = f"נוספה {sound_name(heard[1:]) or 'תנועה'} לפני {name}" - elif heard == "_": - why = f"{name} לא נשמעה" - elif other and other != name: - why = f"{name} נשמעה כמו {other}" - else: - why = f"{name} לא נשמעה בבירור" - text = check.feedback if check else TIPS.get((expected, heard)) - return {"why": why, **({"tip": with_hebrew(text)} if text else {})} - - -VOWELS = {"a", "i", "u", "e", "o", "aː", "iː", "uː", "eː", "oː"} -# the glottal stop (ء, and ق said the urban way) is often dropped by natives too; don't nag about it -NO_DELETION_TIP = {"ʔ", "Q"} - - -def tip_for(expected: str, heard: str | None, check=None) -> dict | None: - """Feedback for one flagged phoneme, or None if there is nothing useful to say. - - `check` (rules.Check) is the rule of pronunciation-rules.md for this substitution: its feedback and - severity win over TIPS. The key stays "expected>heard" either way.""" - if expected in VOWELS and not check: # a short vowel gets a tip only from a rule that flags it (3.11) - return None - heard = HEARD_ALIASES.get(heard, heard) if heard else "_" - if heard == "_" and expected in NO_DELETION_TIP: - return None - name = NAMES.get(expected, expected) - key = f"{expected}>{heard}" - if check: - return {"title": name, "text": with_hebrew(check.feedback), "key": key, "severity": check.severity} - text = TIPS.get((expected, heard)) - if text: - return {"title": name, "text": with_hebrew(text), "key": key, "severity": DEFAULT_SEVERITY} - if heard == "_": - return {"title": name, "text": "לא נשמעה בבירור.", "key": key, "severity": DEFAULT_SEVERITY} - other = sound_name(heard) - text = f"נשמעה כמו {other}." if other else "נשמעה אחרת מהצפוי." - return {"title": name, "text": text, "key": key, "severity": DEFAULT_SEVERITY} diff --git a/app/translit.py b/app/translit.py deleted file mode 100644 index 6a553b145ba11d07784186102742b907466da602..0000000000000000000000000000000000000000 --- a/app/translit.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Hebrew transliteration generated from the IPA in phrases.py. - -Used wherever Arabic is shown for a specific variant or word, so every variant gets a reading -without hand-writing one. Follows the conventions of the hand-written `tr` hints: pointed Hebrew, -geresh for sounds Hebrew lacks (ח' ע' ג' ד' ט'), and the Arabic shadda (ّ, the small w above -the letter) for geminates, as the course material writes it. A dagesh only marks the stops בּ כּ. -""" - -CONSONANTS = { - "ʔ": "א", "b": "בּ", "t": "ת", "dʒ": "ג'", "ħ": "ח", "x": "ח'", "d": "ד", "r": "ר", "z": "ז", - "s": "ס", "ʃ": "ש", "sˤ": "צ", "dˤ": "ד'", "tˤ": "ט", "ðˤ": "ט'", "ʕ": "ע", "ɣ": "ע'", "f": "פ", - "q": "ק", "k": "כּ", "g": "ג", "l": "ל", "m": "מ", "n": "נ", "h": "ה", "w": "ו", "j": "י", - # rural realisations (pronunciation-rules.md, section 1) - "tʃ": "צ'", "θ": "ת'", "ð": "ד״", -} -# Sounds Arabic does not have, written only in the line of what was heard, never in an answer: a Hebrew -# speaker's p for ف, v for و / ب, and Hebrew צ (ts) for ص. Without them the heard line would drop the letter. -HEARD_ONLY = {"p": "פּ", "v": "ב", "β": "ב", "ʋ": "ב", "ts": "טס"} -# vowel -> ((mark on the consonant, letter after it) mid-word, (same) word-finally) -VOWELS = { - "a": (("ַ", ""), ("ַ", "א")), "i": (("ִ", ""), ("ִ", "י")), "u": (("ֻ", ""), ("", "וּ")), - "e": (("ֶ", ""), ("ֶ", "ה")), "o": (("", "וֹ"),) * 2, - "aː": (("ַ", "א"),) * 2, "iː": (("ִ", "י"),) * 2, "uː": (("", "וּ"),) * 2, - "eː": (("ֵ", "י"),) * 2, "oː": (("", "וֹ"),) * 2, -} -FINALS = {"מ": "ם", "נ": "ן", "צ": "ץ", "פ": "ף"} -SHADDA, SHVA = "\u0651", "\u05b0" - - -def _letter(base: str, marks: str) -> str: - """Points go on the letter itself, before a geresh (or gershayim), and on the first letter of טס.""" - return base[0] + marks + base[1:] if len(base) > 1 and base[1] != "\u05bc" else base + marks - - -def word_letters(ipa_word: str, ar_word: str = "") -> list[tuple[str, list[int]]]: - """The word letter by letter: each Hebrew letter (with its points) and the positions, in the IPA word, - of the phonemes it stands for, so that a score per phoneme can colour the letters. The bracketed - silent ל stands for none. - - The Arabic spelling, when given, settles what the IPA cannot: an urban ʔ that is written ق - stays ק (as in the hand-written hints), a final a written ة/ه ends in ה, and the ل of the - article ال that is swallowed by the next consonant is shown in brackets: אִ(ל)דַّכְּתוֹר.""" - phs = ipa_word.split() - if "ق" in ar_word: - phs = ["q" if ph == "ʔ" else ph for ph in phs] - final_a = "ה" if ar_word.endswith(("ة", "ه")) else "א" - # written ال, said as a vowel + doubled consonant: the ل is silent - silent_l = (ar_word.startswith("ال") and len(phs) > 2 and phs[0] in VOWELS - and phs[1] == phs[2] and phs[1] != "l") - out: list[tuple[str, list[int]]] = [] - i = 0 - if len(phs) > 1 and phs[0] == "u": # the conjunction و before a word: וּ, not an alef - out, i = [("וּ", [0])], 1 - while i < len(phs): - ph, start = phs[i], i - if ph in VOWELS: # vowel with no consonant before it (word-initial): carry it on an alef - base, shadda = "א", "" - else: - base, shadda = CONSONANTS.get(ph) or HEARD_ONLY.get(ph, ph), "" - i += 1 - if i < len(phs) and phs[i] == ph: # geminate - shadda = SHADDA - i += 1 - if i < len(phs) and phs[i] in VOWELS: - vowel = phs[i] - i += 1 - mark, after = VOWELS[vowel][i == len(phs)] - if vowel == "a" and after: - after = final_a - out.append((_letter(base, mark + shadda) + after, list(range(start, i)))) - if silent_l and i == 1: - out.append(("(ל)", [])) - elif i < len(phs): - out.append((_letter(base, SHVA + shadda), list(range(start, i)))) - else: - out.append((_letter(FINALS.get(base, base), shadda), list(range(start, i)))) - return out - - -def word_to_hebrew(ipa_word: str, ar_word: str = "") -> str: - return "".join(text for text, _ in word_letters(ipa_word, ar_word)) - - -def to_hebrew(ipa: str, ar: str = "") -> list[str]: - """One Hebrew word per IPA word (words are separated by "|").""" - ipa_words, ar_words = ipa.split("|"), ar.split() - if len(ar_words) != len(ipa_words): - ar_words = [""] * len(ipa_words) - return [word_to_hebrew(w, a) for w, a in zip(ipa_words, ar_words)] diff --git a/app/tts.py b/app/tts.py deleted file mode 100644 index 3d48a62bb26e18bcc4fca82f646a6a65134b9f9a..0000000000000000000000000000000000000000 --- a/app/tts.py +++ /dev/null @@ -1,492 +0,0 @@ -"""Model-answer playback through the Sawtak Arabi TTS API (https://sawtakarabi.ai/docs). - -The API key stays on the server: the browser asks for a phrase by id and gets a WAV back. -Only phrases from the phrase bank can be synthesised, and each one is generated once and cached on disk. - -Env: SAWTAK_API_KEY required; without it /api/tts answers 503 and the page falls back to the device voice - SAWTAK_VOICE_ID optional voice used to read sentences; default is Raef - TTS_CACHE_DIR optional; default ~/.cache/ihki-tts - TTS_BUCKET optional Hugging Face Storage Bucket id (e.g. "user/ihki-storage"); the cache-warm sweep - uploads every cached WAV to it through the Bucket API (needs HF_TOKEN with write access) - TTS_BUCKET_PREFIX folder inside the bucket; default "ihki-tts" - TTS_STAGING_DIR with TTS_BUCKET: a fast local folder new recordings are written to before their upload; - default /ihki-tts-staging. A bucket mounted at /data takes ~10 s per written file. -""" -from __future__ import annotations - -import hashlib -import itertools -from collections import deque -import http.client -import io -import json -import logging -import os -import tempfile -import threading -import time -import urllib.error -import urllib.request -import wave -from pathlib import Path - -log = logging.getLogger("ihki.tts") -BASE_URL = "https://api.sawtakarabi.ai/v1" -MODEL = "arabic-tts-1" -CACHE_VERSION = "raef-pointed-enhanced-provider-default-v1" -TIMEOUT = 60 # per socket operation: a response that keeps trickling never hits it, hence the two caps below -MAX_SECONDS = 90 # one whole generation, download included -MAX_AUDIO_SECONDS = 60 # the longest phrase is ~240 characters; Sawtak's broken answer is minutes of silence -# The account allows 60 requests a minute and 5 at once (GET /v1/me). Request starts are spaced to at most 45 a minute -# for this server, and at most 3 are in flight, so student playback and other clients keep some room. 429s are retried. -MIN_REQUEST_INTERVAL = 60 / 45 -MAX_CONCURRENT = 3 -# The default Sawtak voice used throughout the application. -RAEF_VOICE_ID = "273d81b2-991b-5b76-8525-eeacc6a3d759" -VOICES = {"רַאאִף": RAEF_VOICE_ID} -# A female voice, only for the lines a dialogue gives her (workbook.LINE_VOICES); never offered on the page. -LINE_VOICES = {"חנאן": "e41781f5-127c-5eb0-b83c-50bf52a64caf"} -FADWA_VOICE_ID = "39a7e0e0-087e-52b3-afff-5203e430fc39" # فدوى: Palestinian, female, conversational -# The native voices the scores are measured against (reference.py), first one first: only it is generated for -# the baseline; the others count when their recording is already cached (the cache-warm sweep made them). -BASELINE_VOICES = {"ראאף": RAEF_VOICE_ID, "פדוא": FADWA_VOICE_ID} - - -def playback_voices() -> dict[str, str]: - """The playback voices by name: VOICES, plus the one SAWTAK_VOICE_ID sets, named by SAWTAK_VOICE_NAME (אמיר). - Read when called, after main.py has loaded .env. Playback only: the baseline stays BASELINE_VOICES.""" - out = dict(VOICES) - voice = os.environ.get("SAWTAK_VOICE_ID", "").strip() - if voice and voice not in out.values(): - out[os.environ.get("SAWTAK_VOICE_NAME", "").strip() or "אַמִיר"] = voice - return out - - -def voice_name(voice_id: str) -> str: - """A voice id's display name, without vowel points (ראאף, חנאן); the id itself when it is not one of ours.""" - names = {v: k for k, v in {**playback_voices(), **LINE_VOICES, **BASELINE_VOICES}.items()} - return names.get(voice_id, voice_id).translate({c: None for c in range(0x591, 0x5c8)}) - -class TTSUnavailable(Exception): - """No key configured, or the provider refused / failed. The message is safe to show.""" - - def __init__(self, message: str, status: int | None = None, retry_after: float | None = None, - transient: bool = False): - super().__init__(message) - self.status, self.retry_after = status, retry_after - # worth retrying: rate limited, a provider-side 5xx, or the connection dropped mid-request - self.transient = transient or status == 429 or (status is not None and status >= 500) - - -class ActivityLog: - """What the voice cache is doing, for the monitoring panel: requests in flight and the latest results. - - Holds phrase-bank text only (no user data), kept in memory for the life of the process.""" - - def __init__(self, size: int = 80): - self._lock = threading.Lock() - self._events: deque[dict] = deque(maxlen=size) - self._active: dict[int, dict] = {} - self._ids = itertools.count(1) - - def begin(self, **info) -> int: - with self._lock: - n = next(self._ids) - self._active[n] = {**info, "id": n, "started": time.time()} - return n - - def end(self, n: int, **result) -> None: - with self._lock: - event = self._active.pop(n, None) or {"started": time.time()} - now = time.time() - event.update(result, at=now, ms=round((now - event["started"]) * 1000)) - self._events.appendleft(event) - - def snapshot(self) -> dict: - with self._lock: - now = time.time() - active = [{**a, "elapsed_ms": round((now - a["started"]) * 1000)} for a in self._active.values()] - return {"now": now, "active": sorted(active, key=lambda a: a["started"]), "events": list(self._events)} - - -class BucketMirror: - """Copies cached WAVs into a Hugging Face Storage Bucket through its API. - - Writing to a mounted /data folder is not enough on its own: files written there have been seen to never reach - the bucket. The sweep therefore uploads explicitly, comparing sizes so files already there are not sent again.""" - - def __init__(self, bucket_id: str, prefix: str = "ihki-tts", token: str | None = None): - self.bucket_id, self.prefix, self.token = bucket_id, prefix.strip("/"), token - self.activity: ActivityLog | None = None # set when attached to a SawtakTTS - self._stats_lock = threading.Lock() - self._stats: tuple[float, int, int] | None = None - - def remote_path(self, rel: str) -> str: - return f"{self.prefix}/{rel}" if self.prefix else rel - - def remote_sizes(self) -> dict[str, int]: - """{bucket path: size} of every file under the prefix.""" - from huggingface_hub import list_bucket_tree - out = {} - for item in list_bucket_tree(self.bucket_id, prefix=f"{self.prefix}/" if self.prefix else None, - recursive=True, token=self.token): - size = getattr(item, "size", None) - if size is not None: - out[item.path] = size - return out - - def cache_stats(self, max_age: float = 60) -> tuple[int, int] | None: - """Cached WAV count/size in the shared bucket; refresh without holding up the monitoring request.""" - stats = self._stats - if (stats is None or time.monotonic() - stats[0] > max_age) and self._stats_lock.acquire(blocking=False): - def scan() -> None: - try: - sizes = self.remote_sizes() - wavs = [size for path, size in sizes.items() if path.endswith(".wav")] - self._stats = (time.monotonic(), len(wavs), sum(wavs)) - except Exception as e: - log.warning("could not inspect TTS bucket: %s", e) - finally: - self._stats_lock.release() - threading.Thread(target=scan, name="ihki-tts-bucket-count", daemon=True).start() - return (stats[1], stats[2]) if stats else None - - def upload(self, files: list[tuple[Path | bytes, str]], source: str = "") -> None: - """Upload (local path or bytes, bucket path) pairs in one batch.""" - from huggingface_hub import batch_bucket_files - size = sum(len(src) if isinstance(src, bytes) else src.stat().st_size for src, _dest in files) - n = self.activity.begin(kind="upload", source=source, files=len(files), bytes=size, - text=files[0][1].rsplit("/", 1)[-1] if len(files) == 1 else "") if self.activity else 0 - try: - batch_bucket_files(self.bucket_id, add=[(str(src) if isinstance(src, Path) else src, dest) - for src, dest in files], token=self.token) - except Exception as e: - if self.activity: - self.activity.end(n, status="error", message=str(e)[:200]) - raise - if self.activity: - self.activity.end(n, status="ok") - - -class SawtakTTS: - def __init__(self, api_key: str, voice_id: str = RAEF_VOICE_ID, cache_dir: Path | None = None): - self.api_key, self.voice_id = api_key, voice_id.strip() or RAEF_VOICE_ID - self.voice_name = voice_name(self.voice_id) - self.cache_dir = cache_dir or Path.home() / ".cache" / "ihki-tts" - # Separate locks, so nothing slow holds up a generation: walking a bucket-mounted cache folder for the - # panel's file count takes tens of seconds, and it once stalled every sweep worker behind it. - self._lock = threading.Lock() # clearing the cache - self._counts = threading.Lock() # the hit and miss counters - self._keys_guard = threading.Lock() # the per-recording locks - self._stats_lock = threading.Lock() # one disk walk at a time for cache_stats - self._disk: tuple[float, int, int] | None = None # (when, files, bytes) of the last walk - self._pace = threading.Lock() # hands out request start times, MIN_REQUEST_INTERVAL apart - self._next_start = 0.0 - self._in_flight = threading.Semaphore(MAX_CONCURRENT) - self._key_locks: dict[str, threading.Lock] = {} # one generation per recording at a time - self._cache_hits = self._cache_misses = 0 - self.activity = ActivityLog() # Sawtak requests and bucket uploads, for the monitoring panel - self._mirror: BucketMirror | None = None # set by load_tts when TTS_BUCKET is configured - # With a mirror, new recordings are written here (local disk) and uploaded through the Bucket API, instead - # of into cache_dir: a bucket mounted there only flushes a few files every ~30 s, so each write crawled. - self.staging_dir: Path | None = None - - @property - def mirror(self) -> BucketMirror | None: - return self._mirror - - @mirror.setter - def mirror(self, mirror: BucketMirror | None) -> None: - self._mirror = mirror - if mirror is not None: - mirror.activity = self.activity - - def _request(self, path: str, body: dict | None = None): - req = urllib.request.Request( - BASE_URL + path, - data=json.dumps(body).encode() if body is not None else None, - # explicit User-Agent: the provider's CDN answers Python's default one with 403 (error 1010) - headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", - "User-Agent": "ihki/1.0"}, - ) - try: - return urllib.request.urlopen(req, timeout=TIMEOUT) - except urllib.error.HTTPError as e: - try: - err = json.load(e).get("error", {}) - except ValueError: - err = {} - log.warning("sawtak %s -> %s %s (request %s)", path, e.code, err.get("code"), err.get("request_id")) - try: - retry_after = float(e.headers.get("Retry-After", "")) - except (TypeError, ValueError): - retry_after = None - raise TTSUnavailable(f"tts provider error {e.code}: {err.get('message', 'request failed')}", - e.code, retry_after) from None - except OSError as e: - log.warning("sawtak %s unreachable: %s", path, e) - raise TTSUnavailable("tts provider unreachable", transient=True) from None - - def _path(self, text: str, voice: str) -> Path: - name = hashlib.sha1(f"{CACHE_VERSION}|{MODEL}|{voice}|{text}".encode()).hexdigest() + ".wav" - return self.cache_dir / voice / name - - def _legacy_path(self, path: Path) -> Path: - """The flat cache layout used before recordings were grouped by voice id.""" - return self.cache_dir / path.name - - def cache_key(self, text: str, voice: str) -> str: - """The recording's path relative to the cache folder ("/.wav"), also its path in the bucket.""" - return self._path(text, voice).relative_to(self.cache_dir).as_posix() - - def _candidates(self, text: str, voice: str) -> list[Path]: - """Where a recording may be: the local staging folder first (fast), then the cache folder, then its - legacy flat layout.""" - path = self._path(text, voice) - staged = [self.staging_dir / self.cache_key(text, voice)] if self.staging_dir else [] - return staged + [path, self._legacy_path(path)] - - def cache_file(self, text: str, voice: str) -> Path | None: - """The cached WAV on disk, wherever it is; None when not cached.""" - return next((c for c in self._candidates(text, voice) if c.is_file()), None) - - def cached_files(self) -> list[tuple[Path, str]]: - """Every cached WAV on disk with its path relative to its folder; a staged copy wins over the cache's.""" - found: dict[str, Path] = {} - for root in (self.cache_dir, self.staging_dir): # staging last, so it overrides - if root is not None and root.is_dir(): - for p in root.rglob("*.wav"): - if p.is_file(): - found[p.relative_to(root).as_posix()] = p - return [(found[rel], rel) for rel in sorted(found)] - - def store(self, rel: str, data: bytes) -> Path: - """Keep a recording from an offline copy at `rel` ("/.wav" or a legacy ".wav"): in the - staging folder with a mirror (the slow mount is not written), else in the cache folder.""" - path = (self.staging_dir or self.cache_dir) / rel - path.parent.mkdir(parents=True, exist_ok=True) - tmp = path.with_suffix(".tmp") - tmp.write_bytes(data) - tmp.replace(path) - return path - - def has(self, rel: str) -> Path | None: - """The cached file at `rel`, staged or in the cache folder; None when neither has it.""" - for root in (self.staging_dir, self.cache_dir): - if root is not None and (root / rel).is_file(): - return root / rel - return None - - def cached(self, text: str, voice: str) -> bool: - """Has this voice said `text` before? Then wav() answers from disk, without the provider.""" - return any(c.exists() for c in self._candidates(text, voice)) - - def hold(self, seconds: float) -> None: - """No new request starts for `seconds` (after a 429, so the other requests in flight back off too).""" - with self._pace: - self._next_start = max(self._next_start, time.monotonic() + seconds) - - def _key_lock(self, key: str) -> threading.Lock: - with self._keys_guard: - return self._key_locks.setdefault(key, threading.Lock()) - - def _generate(self, text: str, voice: str, source: str = "") -> bytes: - """Generate one uncached WAV recording: paced, and at most MAX_CONCURRENT at once.""" - queued = time.monotonic() - with self._pace: - start = max(time.monotonic(), self._next_start) - self._next_start = start + MIN_REQUEST_INTERVAL - if start > time.monotonic(): - time.sleep(start - time.monotonic()) - with self._in_flight: - n = self.activity.begin(kind="sawtak", source=source, text=text[:80], voice=voice, - waited_ms=round((time.monotonic() - queued) * 1000)) - try: - data = self._generate_now(text, voice) - except TTSUnavailable as e: - self.activity.end(n, status=e.status or "error", message=str(e)[:200]) - raise - except Exception as e: - self.activity.end(n, status="error", message=f"{type(e).__name__}: {e}"[:200]) - raise - try: - with wave.open(io.BytesIO(data)) as w: - audio = round(w.getnframes() / w.getframerate(), 2) - except (wave.Error, EOFError): - audio = None - self.activity.end(n, status=200, bytes=len(data), audio_s=audio) - return data - - def _generate_now(self, text: str, voice: str) -> bytes: - # Sawtak currently returns 320 seconds of silence whenever `temperature` is present, - # including for 0 and 0.1. Omitting it uses the provider's deterministic supported path. - with self._request("/audio/speech", {"model": MODEL, "voice": voice, "input": text, - "response_format": "pcm", "enhance_pronunciation": True, - }) as res: - rate = int(res.headers.get("X-Sample-Rate", 24000)) - started, parts, size = time.monotonic(), [], 0 - try: - while chunk := res.read(65536): - parts.append(chunk) - size += len(chunk) - if size > rate * 2 * MAX_AUDIO_SECONDS or time.monotonic() - started > MAX_SECONDS: - log.warning("sawtak audio for %r abandoned: %d bytes after %.0fs", text[:40], size, - time.monotonic() - started) - raise TTSUnavailable("tts provider sent overlong or stalled audio", transient=True) - except (http.client.HTTPException, OSError) as e: # e.g. IncompleteRead: the audio arrived cut off - log.warning("sawtak audio download broke: %s: %s", type(e).__name__, e) - raise TTSUnavailable("tts audio download broke off", transient=True) from None - pcm = b"".join(parts) - if len(pcm) < rate // 5: - raise TTSUnavailable("tts provider returned no audio") - buf = io.BytesIO() - with wave.open(buf, "wb") as w: - w.setnchannels(1) - w.setsampwidth(2) - w.setframerate(rate) - w.writeframes(pcm[: len(pcm) // 2 * 2]) - return buf.getvalue() - - def clear_cache(self) -> int: - """Delete only generated audio files from this instance's dedicated cache directory.""" - with self._lock: - removed = 0 - roots = [r for r in (self.cache_dir, self.staging_dir) if r is not None and r.is_dir()] - for path in (p for root in roots for p in root.rglob("*")): - if path.is_file() and (path.suffix == ".wav" or path.name.endswith(".tmp")): - try: - path.unlink() - removed += 1 - except FileNotFoundError: - pass - except OSError as e: - raise TTSUnavailable(f"could not clear voice cache: {e}") from None - with self._counts: - self._cache_hits = self._cache_misses = 0 - self._disk = None - return removed - - def cache_stats(self, max_age: float = 30, wait: bool = False) -> dict: - """Anonymous disk and process-lifetime cache usage for the monitoring panel. Counting the files on a bucket - mount takes minutes, so a stale count is refreshed in a background thread and the call returns at once with - the previous count (files and bytes are None before the first count ends). `wait`: count now instead.""" - disk = self._disk - if disk is None or time.monotonic() - disk[0] > max_age: - if wait: - with self._stats_lock: - self._count_files() - elif self._stats_lock.acquire(blocking=False): - def walk() -> None: - try: - self._count_files() - except TTSUnavailable as e: - log.warning("%s", e) - finally: - self._stats_lock.release() - threading.Thread(target=walk, name="ihki-tts-count", daemon=True).start() - disk = self._disk - with self._counts: - hits, misses = self._cache_hits, self._cache_misses - requests = hits + misses - shared = self.mirror.cache_stats() if self.mirror is not None and hasattr(self.mirror, "cache_stats") else None - if self.mirror is not None and hasattr(self.mirror, "cache_stats"): - files, size = shared if shared is not None else (None, None) - else: - files, size = (disk[1], disk[2]) if disk else (None, None) - return {"files": files, "bytes": size, "hits": hits, - "misses": misses, "hit_rate": round(hits / requests, 4) if requests else None} - - def _count_files(self) -> None: - """Walk the cache folder; the caller holds _stats_lock.""" - files = size = 0 - try: - for path, _rel in self.cached_files(): - files += 1 - size += path.stat().st_size - except OSError as e: - raise TTSUnavailable(f"could not inspect voice cache: {e}") from None - self._disk = (time.monotonic(), files, size) - - def _wav(self, text: str, voice: str, upload: bool = False, source: str = "") -> tuple[bytes, str]: - """Generate or read `text` for an already validated voice id. `upload`: send a new recording to the bucket - in the background (playback); the cache-warm sweep uploads its own in batches. Different recordings can be - generated at the same time (see _generate); the same one waits for the first request and then reads it.""" - with self._key_lock(self.cache_key(text, voice)): - found = self.cache_file(text, voice) if self.staging_dir else None - if found is not None: # with a mirror the cache folder may be a slow mount: no migration there - if source == "playback": self._count(hit=True) - return found.read_bytes(), voice - path = self._path(text, voice) - legacy = self._legacy_path(path) - if self.staging_dir is None and not path.exists() and legacy.exists(): - try: - path.parent.mkdir(parents=True, exist_ok=True) - legacy.replace(path) - except OSError as e: - log.warning("tts cache migration failed: %s: %s", type(e).__name__, e) - path = legacy - if path.exists(): - if source == "playback": self._count(hit=True) - return path.read_bytes(), voice - if source == "playback": self._count(hit=False) - # The caller supplies the phrase's Palestinian vowel marks. Sawtak preserves existing - # marks; enhancement fills remaining pronunciation cues using the selected voice's dialect. - data = self._generate(text, voice, source) - if self.staging_dir is not None: - path = self.staging_dir / self.cache_key(text, voice) - try: - path.parent.mkdir(parents=True, exist_ok=True) - tmp = path.with_suffix(".tmp") - tmp.write_bytes(data) - tmp.replace(path) - except OSError as e: # read-only or full cache dir: the audio is still good, it just isn't kept - log.warning("tts cache write failed: %s: %s", type(e).__name__, e) - if upload and self.mirror is not None: - self._upload_soon(data, self.cache_key(text, voice)) - return data, voice - - def _count(self, hit: bool) -> None: - with self._counts: - if hit: - self._cache_hits += 1 - else: - self._cache_misses += 1 - - def _upload_soon(self, data: bytes, rel: str) -> None: - """One playback recording to the bucket, off the request: the student does not wait for it.""" - mirror = self.mirror - - def go() -> None: - try: - mirror.upload([(data, mirror.remote_path(rel))], source="playback") - except Exception as e: # the next cache-warm sweep uploads what is still missing - log.warning("tts bucket upload of %s failed: %s", rel, e) - threading.Thread(target=go, name="ihki-tts-upload", daemon=True).start() - - def wav(self, text: str, voice: str = "") -> tuple[bytes, str]: - """(WAV, voice id) in the configured voice, or a dialogue line's own voice.""" - voice = voice if voice in LINE_VOICES.values() else self.voice_id - return self._wav(text, voice, upload=True, source="playback") - - def wav_for_voice(self, text: str, voice: str, source: str = "sweep", upload: bool = False) -> tuple[bytes, str]: - """(WAV, voice id) for a teacher-selected, validated Sawtak voice. Unlike wav(), any voice id is kept. - `upload`: a new recording also goes to the bucket in the background.""" - return self._wav(text, voice, upload=upload, source=source) - - -def load_tts(key: str | None = None) -> SawtakTTS | None: - """`key`: the one the panel set (see keystore.py); None reads the environment's.""" - key = (os.environ.get("SAWTAK_API_KEY", "") if key is None else key).strip() - if not key: - log.info("SAWTAK_API_KEY not set: playback uses the device voice") - return None - cache = os.environ.get("TTS_CACHE_DIR", "").strip() - voice = os.environ.get("SAWTAK_VOICE_ID", RAEF_VOICE_ID).strip() or RAEF_VOICE_ID - tts = SawtakTTS(key, voice, Path(cache) if cache else None) - bucket = os.environ.get("TTS_BUCKET", "").strip() - if bucket: - tts.mirror = BucketMirror(bucket, os.environ.get("TTS_BUCKET_PREFIX", "ihki-tts"), - os.environ.get("HF_TOKEN", "").strip() or None) - staging = os.environ.get("TTS_STAGING_DIR", "").strip() - tts.staging_dir = Path(staging) if staging else Path(tempfile.gettempdir()) / "ihki-tts-staging" - return tts diff --git a/app/tutor.py b/app/tutor.py deleted file mode 100644 index 08b1408483d7cbfd356aec91442bb949ff476ff0..0000000000000000000000000000000000000000 --- a/app/tutor.py +++ /dev/null @@ -1,534 +0,0 @@ -"""יאללה נדבר: short spoken conversations with an AI tutor, on scenario cards, one for each lesson of the course. - -The tutor writes; the student talks. The page turns the student's speech into text (the browser's own recognition) -and sends the text; the tutor's answer comes back as text too, so the student sees what the tutor understood. -The students do not read Arabic script, so what the page shows is Arabic in Hebrew letters as the course writes it: -the tutor's message, and the student's own sentence as the tutor read it. The Arabic script is kept for the teacher. - -Each scenario belongs to a lesson (SCENARIOS) and plays out that lesson's theme: its story, its words and its grammar. -The tutor speaks with the words of that lesson and of the lessons before it (`vocabulary`); the page shows each word -of the tutor's message with its Hebrew translation, and marks a word the student has not learnt yet (`is_known`). -A sentence of the student's that is wrong in grammar or wording gets a correction, explained in Hebrew. -A conversation lasts MAX_TURNS sentences of the student's and ends with a short summary in Hebrew. - -The model is Claude Haiku (DEFAULT_MODEL, or TUTOR_MODEL), called with the server's key; the browser never sees it. A conversation lives in -this process's memory while it runs (a restart ends it) and is kept, turn by turn, in the Supabase table -`tutor_chats` (CREATE_TUTOR_CHATS), so the teacher can read what was said and how the tutor answered. - -Env: ANTHROPIC_API_KEY (or CLAUDE_API_KEY) required; without it the game is not offered - Access is granted per account in the management panel; an open service remains open. - TUTOR_MODEL optional: the Claude model the tutor runs on (default DEFAULT_MODEL), e.g. claude-sonnet-5-5 - (request_options() fits the request to it: Haiku 5.5/4.5, Sonnet 5.5, Opus 5.5 and Fable 5.1 all run) -""" -from __future__ import annotations - -import functools -import json -import logging -import os -import re -import threading -import time -import uuid -from dataclasses import dataclass, field - -from .phrases import CATEGORIES, LESSONS, PHRASES -from .workbook import LEX - -log = logging.getLogger("ihki.tutor") -DEFAULT_MODEL = "claude-haiku-5-5" -NO_FIX = re.compile(r"אין (?:כאן )?(?:צורך|תיקון|טעות)|(?:המשפט|הדוגמה|התשובה) (?:שלך |שלכם )?נכונ") # a correction whose note says there is none -MAX_TURNS = 10 # the student's sentences in one conversation -DAILY_LIMIT = 5 # conversations per signed-in account per day; developers are exempt in main.py -MAX_TEXT = 300 # characters of one sentence of the student's -TTL = 2 * 3600 # seconds a conversation nobody continues is kept in memory -MAX_LIVE = 2000 # conversations in memory at once -CREATE_TUTOR_CHATS = ("create table public.tutor_chats (id uuid primary key, user_id uuid, email text not null default '', " - "scenario text not null, gender text not null default '', messages jsonb not null default '[]'::jsonb, " - "turns int not null default 0, finished boolean not null default false, model text not null default '', " - "input_tokens int not null default 0, output_tokens int not null default 0, " - "created_at timestamptz not null default now(), updated_at timestamptz not null default now()); " - "alter table public.tutor_chats enable row level security;") - -# A scenario for each lesson the path shows: who the tutor plays (`who`, `ar`: the name on the chat's avatar, `she`: -# the character is a woman), the card (`title`, `desc`), and for the model the conversation's aim (`goal`) and the -# lesson's grammar to bring in (`focus`). `story`: the lesson's story or dialogue, the model's example of its words. -SCENARIOS = [ - {"id": "l1", "lesson": "1", "who": "ראמי", "ar": "رامي", "she": False, "story": "l1-say", - "title": "מי, איפה ולמה?", "desc": "ראמי שואל אתכם מה עשיתם אתמול: מי, איפה, מתי ולמה", - "goal": "a friend asks the student simple questions about yesterday: who sat in the car, what they ate, who " - "opened the door, where their friend lives, and asks why and when", - "focus": "the question words مين، وين، ليش، ايمتى and the past tense of سأل، فتح، اكل، قعد"}, - {"id": "l2", "lesson": "2", "who": "ח'ליל", "ar": "خليل", "she": False, "story": "l2-talk", - "title": "היכרות", "desc": "ח'ליל פוגש אתכם בפעם הראשונה: שם, גיל, מאיפה אתם, משפחה ועבודה", - "goal": "Khalil, from Tur'an in the north, now living in Jerusalem, meets the student for the first time and " - "gets to know them: their name, how they are, their age, where they are from and live, whether they are " - "married and have children, and their work", - "focus": "greetings and the possessive endings (اسمي، اسمك، بيتي)"}, - {"id": "l3", "lesson": "3", "who": "רובה", "ar": "روبا", "she": True, "story": "l3-story", - "title": "השכנה רובה", "desc": "רובה עברה לשכונה שלכם: מספר הבית, הטלפון, הגיל והמשפחה", - "goal": "Ruba, eighteen, has just moved to the student's neighbourhood. She asks for their house number and " - "phone number, their age, and how many brothers and sisters they have, and tells about her own family", - "focus": "numbers (house numbers, phone numbers, ages) and the past tense"}, - {"id": "l4", "lesson": "4", "who": "סמיר", "ar": "سمير", "she": False, "story": "l4-story", - "title": "השכן החדש", "desc": "באתם עם עוגה לשכן החדש סמיר. הוא מציע קפה או תה", - "goal": "the student visits Samir Atallah, the new neighbour, bringing him a cake. Samir welcomes them, offers " - "coffee or tea (with sugar or without), asks whether they are tired or hungry, and they talk about " - "their families", - "focus": "بدّي (I want), بلا، كل، and the past tense"}, - {"id": "l5", "lesson": "5", "who": "אבו מחמד", "ar": "ابو محمد", "she": False, "story": "l5-story", - "title": "בשוק", "desc": "קונים ירקות ופירות אצל אבו מחמד: מחיר, קילו וחצי", - "goal": "Abu Mohammad sells vegetables and fruit in the market. The student buys: he asks what they want, " - "tells prices in shekels per kilo, and they bargain a little", - "focus": "prices and quantities (كيلو، نصّ، شيكل) and the negation ما"}, - {"id": "l6", "lesson": "6", "who": "קפטן איוב", "ar": "ايّوب", "she": False, "story": "l6-story", - "title": "במונית של קפטן איוב", "desc": "נוסעים במונית של קפטן איוב, והוא שואל מה תעשו מחר", - "goal": "Captain Ayyub, a talkative taxi driver, drives the student. He asks where they are going, what they " - "will do tomorrow, whether they are going to a party, and talks about himself", - "focus": "the future tense (بروح، باكل) next to the past"}, - {"id": "l7", "lesson": "7", "who": "הדודה אם סמיר", "ar": "امّ سمير", "she": True, "story": "l7-say", - "title": "ביקור אצל הדודה", "desc": "הדודה אם סמיר מארחת אתכם ושואלת על הילדים, האחים והמכוניות", - "goal": "the student visits their aunt Umm Samir. She serves maqluba and asks how many children, brothers, " - "sisters and cars they have, and how old everyone is", - "focus": "masculine and feminine, the dual (ولدين، بنتين) and numbers with nouns"}, - {"id": "l8", "lesson": "8", "who": "סוהא", "ar": "سهى", "she": True, "story": "l8-story", - "title": "בחנות הבגדים", "desc": "סוהא המוכרת עוזרת לכם לבחור מכנסיים ושמלה: איזה צבע וכמה זה עולה", - "goal": "Suha works in a clothes shop. She asks what the student is looking for, which colour they like, " - "and tells how much each item costs; they ask how many there are", - "focus": "colours, قدّيش (how much) and كم (how many)"}, - {"id": "l9", "lesson": "9", "who": "אבו נדאל", "ar": "ابو نضال", "she": False, "story": "l9-story", - "title": "קפה אצל אבו נדאל", "desc": "אבו נדאל הבדואי מארח אתכם לקפה ושואל מה תרצו לעשות בשבוע הבא", - "goal": "Abu Nidal, a Bedouin near Arad, welcomes the student, serves coffee (bitter, without sugar or milk), " - "asks about their family and work, and what they want to do next week", - "focus": "the future with بدّ (بدّي، بدّك) and hospitality phrases"}, - {"id": "l10", "lesson": "10", "who": "הדוד סעיד", "ar": "سعيد", "she": False, "story": "l10-story", - "title": "הדוד סעיד בא לבקר", "desc": "הדוד סעיד הגיע לבקר. משווים: מה יותר גדול, יותר יפה, הכי טוב", - "goal": "uncle Said has come to visit. He asks the student about their life, football, the beach and the " - "train, and they compare things: which is bigger, nicer, the best", - "focus": "the comparative and superlative (أكبر، أحلى، أحسن) and هاد، هاي، هدول"}, - {"id": "l2.1", "lesson": "2.1", "who": "אבו חסן", "ar": "ابو حسن", "she": False, "story": "l2.1-story", - "title": "במכולת של אבו חסן", "desc": "אורחים באים היום: קונים במכולת של אבו חסן לפי הרשימה", - "goal": "Abu Hassan runs the village grocery. Guests are coming to the student today; they buy what is on " - "their list. He asks what they need, who is coming and what they will cook", - "focus": "the future with رح and must with لازم"}, - {"id": "l2.2", "lesson": "2.2", "who": "עלי", "ar": "علي", "she": False, "story": "l2.2-story", - "title": "בדוכן הפלאפל", "desc": "מזמינים מנת פלאפל אצל עלי, ומספרים מה אתם עושים בדרך כלל", - "goal": "Ali owns a falafel restaurant. He takes the student's order (which salads and sauces in the pita) and " - "chats about what they usually do on each day of the week", - "focus": "the present tense with بـ (باكل، بتروح) and the days of the week"}, - {"id": "l2.3", "lesson": "2.3", "who": "אבו ג'מיל", "ar": "ابو جميل", "she": False, "story": "l2.3-talk", - "title": "סליחה, מה השעה?", "desc": "אבו ג'מיל ברחוב שואל מה השעה, ומספר על יום ההולדת של הערב", - "goal": "Abu Jamil stops the student in the street to ask the time, because his phone is broken. They talk " - "about the time of tonight's birthday party and how to get there", - "focus": "telling the time (ربع، ثلث، نصّ) and form II verbs (فعّل)"}, - {"id": "l2.4", "lesson": "2.4", "who": "שאדי", "ar": "شادي", "she": False, "story": "l2.4-talk2", - "title": "שיחת טלפון", "desc": "שאדי מתקשר: קובעים להיפגש מחר, באיזו שעה ואיפה", - "goal": "Shadi phones the student. He asks how they are and arranges to meet tomorrow: at what time, where, " - "and asks for a phone number", - "focus": "phone phrases (ألو، مين معي) and the relative words اللي، انّو"}, - {"id": "l2.5", "lesson": "2.5", "who": "אבו ויסאם", "ar": "ابو وسام", "she": False, "story": "l2.5-story", - "title": "במוסך", "desc": "האוטו עושה רעש: אבו ויסאם במוסך שואל מה קרה", - "goal": "Abu Wisam owns a garage. The student's car is making a noise and the engine light is red. He asks " - "what happened and when, checks the oil, and tells them how long it will take", - "focus": "صار (happened, became) and verbs like جاب (brought)"}, - {"id": "l2.6", "lesson": "2.6", "who": "פואד", "ar": "فؤاد", "she": False, "story": "l2.6-story", - "title": "הכד העתיק", "desc": "מצאתם כד עתיק. מר פואד רוצה לקנות אותו: יקר או זול?", - "goal": "the student found an old jar and brings it to Mr Fuad Arafat, who buys antiques. He asks where they " - "found it, and they bargain over the price in dinars: expensive or cheap", - "focus": "form III verbs (ساعد، لاقى) and the object suffix after a vowel"}, - {"id": "l2.7", "lesson": "2.7", "who": "יוסף", "ar": "يوسف", "she": False, "story": "l2.7-story", - "title": "בתחנת האוטובוס", "desc": "התחנה מלאה. יוסף מחכה לקו לטבריה ונותן לכם עצות", - "goal": "Yusuf is waiting at a crowded bus stop at the Furaydis junction. He talks with the student about " - "which line goes to Tiberias and when it comes, and tells them what to do", - "focus": "the imperative (استنّى، تعال، قول) next to the past, present and future"}, - {"id": "l2.8", "lesson": "2.8", "who": "ד\"ר לילא", "ar": "ليلى", "she": True, "story": "l2.8-story", - "title": "אצל הרופאה", "desc": "הרופאה לילא שואלת מה כואב ומה אתם עושים עכשיו", - "goal": "Dr Layla, a doctor, asks the student what hurts, since when, and what they are doing for it right now, " - "and gives advice (rest, aspirin, health)", - "focus": "عم for an action happening now and doubled verbs (حبّ، ضلّ)"}, - {"id": "l2.9", "lesson": "2.9", "who": "המוכתר", "ar": "المختار", "she": False, "story": "l2.9-story", - "title": "המוכתר של הכפר", "desc": "המוכתר פוגש אתכם בכפר: לאן הולכים, מאיפה באים ומה אתם חושבים", - "goal": "the village mukhtar meets the student on the road and asks where they are going and coming from, " - "whether they live here, and tells the story of the father, the son and the donkey", - "focus": "the active participle (رايح، جاي، ساكن، عارف)"}, -] -BY_ID = {s["id"]: s for s in SCENARIOS} -READY = [l["id"] for l in LESSONS if not l.get("soon")] -LESSON_OF = {c["id"]: c["lesson"] for c in CATEGORIES} -LEARN = {c["id"] for c in CATEGORIES if c.get("learn")} -TASHKEEL = re.compile(r"[ً-ْٰـ]") -PUNCT = "،؟!.,:;\"'()«»…-" -ARABIC = re.compile(r"[ء-ي]") -PREFIXES = ("وال", "بال", "فال", "لل", "ال", "و", "ب", "ف", "ل", "ع", "م") -SUFFIXES = ("كم", "هم", "ها", "نا", "ني", "تي", "ين", "ات", "ي", "ك", "ه", "و", "ت", "ة") - - -def norm(word: str) -> str: - """An Arabic word as it is compared: no tag, vowel signs or punctuation; one alef; ى as ي and ة as ه.""" - w = TASHKEEL.sub("", word.split("#")[0]).strip(PUNCT) - return w.translate(str.maketrans("أإآٱى", "ااااي")).replace("ة", "ه") - - -TR_OF = {norm(k): v[0] for k, v in LEX.items()} # a word's reading in Hebrew letters, as the course writes it - - -@functools.lru_cache(maxsize=None) -def vocabulary(lesson: str) -> tuple[list[tuple[str, str, str]], frozenset[str]]: - """What a student knows by the end of this lesson: the new words of it and of the lessons before it, as - (Arabic, Hebrew letters, Hebrew), and every word form their sentences used (normalised, see `norm`).""" - upto = set(READY[:READY.index(lesson) + 1]) - learnt, forms = [], set() - for p in PHRASES: - if LESSON_OF.get(p["cat"]) not in upto: - continue - if p["cat"] in LEARN: - learnt.append((p["variants"][0][0], p["tr"], p["he"])) - for ar, _ in p["variants"]: - forms.update(n for n in map(norm, ar.split()) if n) - return learnt, frozenset(forms) - - -def is_known(word: str, forms: frozenset[str]) -> bool: - """Whether the student has met this word: as it is, or without a prefix (و، ال، بـ...) or an ending (ي، ك، ها...) - that they learnt as such. Lenient on purpose: a word marked new must be one the lessons never had.""" - w = norm(word) - if not w or w.isdigit() or not re.search(r"[ء-ي]", w): - return True - stems = {w} | {w[len(p):] for p in PREFIXES if w.startswith(p) and len(w) - len(p) >= 2} - stems |= {s[:-len(x)] for s in list(stems) for x in SUFFIXES if s.endswith(x) and len(s) - len(x) >= 2} - return any(s in forms or s + "ه" in forms for s in stems) - - -def _lines(cat: str) -> list[str]: - """A topic of the course, line by line (Arabic | Hebrew letters | Hebrew): the model's example of the lesson.""" - return [f"{p['variants'][0][0]} | {p['tr']} | {p['he']}" for p in PHRASES if p["cat"] == cat] - - -# The tutor's every answer, as JSON (structured output). heard_tr: the student's sentence as the tutor read it, in -# Hebrew letters; words: the tutor's message word by word; fix_*: the student's last sentence said right, empty when -# it was right; summary: only in the last answer. -WORD_SCHEMA = { - "type": "object", - "properties": {"ar": {"type": "string"}, "tr": {"type": "string"}, "he": {"type": "string"}, - "name": {"type": "boolean"}}, - "required": ["ar", "tr", "he", "name"], - "additionalProperties": False, -} -REPLY_SCHEMA = { - "type": "object", - "properties": { - "heard_tr": {"type": "string"}, - "words": {"type": "array", "items": WORD_SCHEMA}, - "he": {"type": "string"}, - "fix_ar": {"type": "string"}, "fix_tr": {"type": "string"}, "fix_note": {"type": "string"}, - "summary": {"type": "string"}, - }, - "required": ["heard_tr", "words", "he", "fix_ar", "fix_tr", "fix_note", "summary"], - "additionalProperties": False, -} -OPEN = "(The student has opened the chat. Start the conversation: greet them and begin the scenario.)" - - -def system_prompt(sc: dict, gender: str) -> str: - """The tutor's instructions: the character, the scenario, the words it may use, the answer's fields.""" - student = "a woman: speak to her in the feminine (حالِك، اسمِك، انتِ، بتشتغلي)" if gender == "f" else \ - "a man: speak to him in the masculine (حالَك، اسمَك، انتَ، بتشتغل)" - who = f"{sc['ar']}, a Palestinian {'woman' if sc['she'] else 'man'} (speak of yourself in the " \ - f"{'feminine' if sc['she'] else 'masculine'})" - learnt, forms = vocabulary(sc["lesson"]) - words = "\n".join(f"{a} | {t} | {h}" for a, t, h in learnt) - lesson = next(l["name"] for l in LESSONS if l["id"] == sc["lesson"]) - return f"""You play {who} in a short conversation with a Hebrew-speaking beginner who is learning spoken Palestinian -Arabic. The student is {student}. - -The scenario: {sc["goal"]}. Ask one thing at a time, answer the student's questions in your character, and keep -the conversation going. Read each message to its end before you answer: never ask for something the student has -already told you. If the student goes off the topic, answer in one short sentence and bring the talk back. - -The student has studied up to {lesson} of the course. This lesson's grammar, to bring in naturally: {sc["focus"]}. -Speak ONLY with the words the student knows: the word list and the word forms below (with their usual prefixes and -endings: و، ال، بـ، the possessive and verb endings). Prefer the words of this lesson's story. Use a word outside the -lists only when there is no natural way to say it with known words, and then keep it simple. - -The student cannot read Arabic script: they read Arabic only in Hebrew letters. They mostly speak, and the browser's -speech recognition writes down what it heard in Arabic script, so a message may be misspelled, missing words or partly -wrong; a student who types writes the Arabic in Hebrew letters. Answer what you understood. If you understood nothing, -say so kindly and ask again in simpler words. Never use Modern Standard Arabic: speak the spoken Palestinian dialect -of the course (شو، وين، كيف، قدّيش، بدّي، كتير، هلّق). Keep each message to one or two short sentences. - -Arabic in Hebrew letters ("Hebrew letters" below) is written exactly as the course writes it, as in the lesson lines -below: pointed Hebrew; ח' for خ, ע' for غ, ג' for ج, ד' for ض, ט' for ظ, ת' for ث; ح is ח, ع is ע, ص is צ, ط is ט, -ق is ק; the shadda ّ after a doubled letter; a hyphen before a pronoun suffix (אִסְמַ-כּ, חַאלֵ-כּ, עִנְד-י). - -Every answer has these fields: -- heard_tr: the student's last message exactly as you understood it, in Hebrew letters. Write what they said, mistakes - and all: do not correct it here (corrections go in fix_*). Empty when there is no student message yet. -- words: your message, word by word in order. For each word: ar (the word in Arabic script, as the course writes it, - no vowel signs except shadda ّ), tr (the word in Hebrew letters, with the punctuation that follows it, e.g. "חַאלַ-כּ?"), - he (its meaning here in Hebrew, one to three words), name (true only for a person's or a place's name). -- he: a natural Hebrew translation of your whole message. -- fix_ar, fix_tr, fix_note: only for a clear mistake in the student's last sentence: a wrong verb form, gender or - person, a wrong word, or Modern Standard Arabic or Hebrew where the dialect has its own word (ثلاثون for تلاتين). - Then give the sentence as they should have said it (in Arabic script and in Hebrew letters), and in fix_note one - short, simple line in Hebrew naming exactly what changed (e.g. "בניב אומרים תַלַאתִין ולא ת'לַאת'וּן"). Most - sentences of a student are fine: then leave all three empty. Leave them empty too when you are not sure, when the - difference is only spelling or vowels (that came from the speech recognition, not from the student), or when the - sentence is a correct way to say it even if you would say it differently. Never correct a correct sentence. -- summary: empty, except in your very last message (you will be told when it is). Then write in Hebrew, addressed to - the student, two or three sentences on what went well and the two or three things most worth practising. -fix_note and summary are in Hebrew, and any Arabic word in them is written in Hebrew letters, never in Arabic script. - -Lines of this lesson (Arabic | Hebrew letters | Hebrew): -{chr(10).join(_lines(sc["story"]))} - -The words the student has learnt (Arabic | Hebrew letters | Hebrew): -{words} - -Every word form the lessons so far have used: -{" ".join(sorted(forms))} - -YOUR ROLE NOW: you are {sc["ar"]}. {sc["goal"]}. From your very first message, be this character in this situation -and lead the conversation toward it; every answer of yours moves the scenario on. Speak to the student as -{student.split(":")[0]}.""" - - -class TutorUnavailable(Exception): - """No key, or the model failed. The message is safe to show the student (Hebrew).""" - - -class TutorLimit(Exception): - """The student has started their conversations for today.""" - - -@dataclass -class Chat: - id: str - scenario: str - gender: str - user_id: str | None - email: str - system: str - messages: list[dict] = field(default_factory=list) # as the model reads them: user text, assistant JSON - log: list[dict] = field(default_factory=list) # as the page and the teacher read them - turns: int = 0 - done: bool = False - seen: float = field(default_factory=time.monotonic) - input_tokens: int = 0 - output_tokens: int = 0 - model: str = DEFAULT_MODEL - - def row(self) -> dict: - """The conversation as the table `tutor_chats` keeps it.""" - return {"id": self.id, "user_id": self.user_id, "email": self.email, "scenario": self.scenario, - "gender": self.gender, "messages": self.log, "turns": self.turns, "finished": self.done, - "model": self.model, "input_tokens": self.input_tokens, "output_tokens": self.output_tokens, - "updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())} - - -# models that take a `role: "system"` message in the middle of `messages` (the others answer 400) -SYSTEM_MESSAGES = ("claude-opus-5", "claude-opus-4-8", "claude-fable-5", "claude-mythos-5", "claude-sonnet-5-5", "claude-haiku-5-5") - - -def thinking_for(model: str) -> dict | None: - """As little thinking as each model allows for a chat answer. Sonnet 5.5 refuses "disabled" and turns thinking off - with "between_tools"; Opus 5.5 and Fable 5.x cannot turn it off at all (None: the parameter is left out, so it is - adaptive, and effort "low" keeps it short); the others take "disabled". Only the answer's text goes back into the - history, never a thinking block, so the history stays append-only and nothing is checked against it.""" - if model.startswith("claude-sonnet-5-5"): - return {"type": "between_tools"} - if model.startswith(("claude-opus-5-5", "claude-fable-5", "claude-mythos-5")): - return None - return {"type": "disabled"} - - -def request_options(model: str) -> dict: - """The arguments of a chat answer that differ by model: thinking (see thinking_for), effort (Haiku 4.5 has none), - and room for a thinking model to think before it answers.""" - thinking = thinking_for(model) - config = {"format": {"type": "json_schema", "schema": REPLY_SCHEMA}} - if not model.startswith("claude-haiku-4-5"): - config["effort"] = "low" - return {"max_tokens": 4000 if thinking else 8000, "output_config": config, **({"thinking": thinking} if thinking else {})} - - -def last_turn(model: str, messages: list[dict], note: str) -> list[dict]: - """Tells the model this is the last sentence: as a system message where the model takes one, else after the - student's sentence in the same user turn.""" - if model.startswith(SYSTEM_MESSAGES): - return messages + [{"role": "system", "content": note}] - *before, student = messages - return before + [{"role": "user", "content": [{"type": "text", "text": student["content"]}, - {"type": "text", "text": f"(Instruction: {note})"}]}] - - -class Tutor: - def __init__(self, client, model: str = DEFAULT_MODEL): - self.client = client # an anthropic.Anthropic - self.model = model - self._chats: dict[str, Chat] = {} - self._started: dict[tuple[str, str], int] = {} # (student, day): conversations started - self._lock = threading.Lock() - - def started(self, student: str, day: str, seed=None) -> int: - """How many conversations the student started on this day. `seed` reads the number kept in Supabase the first - time it is asked (a restart forgets the count in memory).""" - key = (student, day) - with self._lock: - if key in self._started: - return self._started[key] - n = seed() if seed else 0 - with self._lock: - return self._started.setdefault(key, n) - - def start(self, scenario: str, gender: str, user_id: str | None, email: str, student: str, day: str, - limit: int | None, seed=None) -> tuple[Chat, dict]: - """A new conversation and the tutor's opening message (see `message`). Raises TutorLimit past the day's - `limit` (None: none), and TutorUnavailable when the model fails (the conversation is then not counted).""" - sc = BY_ID[scenario] - if limit is not None and self.started(student, day, seed) >= limit: - raise TutorLimit() - chat = Chat(id=str(uuid.uuid4()), scenario=scenario, gender=gender, user_id=user_id, email=email, - system=system_prompt(sc, gender), model=self.model) - messages = [{"role": "user", "content": OPEN}] - data = self._answer(chat, messages) - opening = self.message(data, sc) - chat.log.append({"from": "tutor", **opening}) - with self._lock: - self._started[(student, day)] = self._started.get((student, day), 0) + 1 - self._sweep() - self._chats[chat.id] = chat - return chat, opening - - def get(self, chat_id: str, user_id: str | None) -> Chat | None: - with self._lock: - chat = self._chats.get(chat_id) - return chat if chat and chat.user_id == user_id else None - - @staticmethod - def message(data: dict, sc: dict) -> dict: - """The tutor's message as the page shows it: {"ar", "tr", "he", "words": [{tr, he, new}]}. A word is new when - the lessons up to the scenario's never had it (a name never is).""" - _, forms = vocabulary(sc["lesson"]) - words: list[dict] = [] - for w in data["words"]: - tr, ar = w["tr"].strip(), w["ar"].strip() - if not tr: - continue - if not re.search(r"[א-ת]", tr): # punctuation the model gave as a word of its own - if words: - words[-1]["tr"] += tr.strip() - words[-1]["ar"] += ar.strip() - continue - if ARABIC.search(tr): # Arabic letters in the Hebrew ones: the course's own reading of the word, if it has it - tr = TR_OF.get(norm(ar), re.sub(r"[؀-ۿ]+", "", tr).strip() or "…") + tr[len(tr.rstrip(PUNCT + "?")):] - words.append({"ar": ar, "tr": tr, "he": w["he"].strip(), "new": not w["name"] and not is_known(ar, forms)}) - return {"ar": " ".join(w["ar"] for w in words), "tr": " ".join(w["tr"] for w in words), - "he": data["he"].strip(), "words": [{k: w[k] for k in ("tr", "he", "new")} for w in words]} - - def turn(self, chat: Chat, text: str) -> dict: - """The student's sentence, and the tutor's answer to it: {"heard": the sentence as the tutor read it, in Hebrew - letters, "reply": the tutor's message (see `message`), "fix": {ar, tr, note} or None, "summary": text or None, - "turn", "turns", "done"}. Raises TutorUnavailable when the model fails; the student's sentence is then not - counted, and can be said again.""" - if chat.done: - raise ValueError("the conversation has ended") - last = chat.turns + 1 >= MAX_TURNS - messages = chat.messages + [{"role": "user", "content": text}] - if last: - messages = last_turn(self.model, messages, "This is the student's last sentence. Answer it and say " - "goodbye warmly, and write the summary.") - data = self._answer(chat, messages) - chat.turns += 1 - chat.done = last - fix = {"ar": data["fix_ar"].strip(), "tr": data["fix_tr"].strip(), "note": data["fix_note"].strip()} - if not fix["ar"] or not fix["note"] or NO_FIX.search(fix["note"]) or \ - [norm(w) for w in fix["ar"].split()] == [norm(w) for w in text.split()]: - fix = None # no correction, or one that changes nothing: the model is not to correct a correct sentence - reply = self.message(data, BY_ID[chat.scenario]) - summary = data["summary"].strip() if last else "" - heard = data["heard_tr"].strip() or text - chat.log.append({"from": "student", "text": text, "tr": heard, **({"fix": fix} if fix else {})}) - chat.log.append({"from": "tutor", **reply, **({"summary": summary} if summary else {})}) - return {"heard": heard, "reply": reply, "fix": fix, "summary": summary or None, "turn": chat.turns, - "turns": MAX_TURNS, "done": chat.done} - - def _answer(self, chat: Chat, messages: list[dict]) -> dict: - """Asks the model and keeps the exchange in the conversation's history (only once it has answered).""" - data, usage = self._ask(chat.system, messages) - chat.model = self.model # the panel may have changed it since the conversation began - chat.messages = messages + [{"role": "assistant", "content": json.dumps(data, ensure_ascii=False)}] - chat.seen = time.monotonic() - chat.input_tokens += usage[0] - chat.output_tokens += usage[1] - return data - - def _ask(self, system: str, messages: list[dict]) -> tuple[dict, tuple[int, int]]: - import anthropic - try: - res = self.client.messages.create( - model=self.model, system=system, messages=messages, cache_control={"type": "ephemeral"}, - **request_options(self.model), - ) - except anthropic.RateLimitError as e: - log.warning("tutor rate limited: %s", e) - raise TutorUnavailable("המורה עמוס כרגע. נסו שוב בעוד דקה.") from e - except anthropic.AuthenticationError as e: - log.error("tutor: the API key was refused: %s", e) - raise TutorUnavailable("המורה לא זמין כרגע (מפתח ה-API לא תקין).") from e - except anthropic.APIStatusError as e: - log.warning("tutor: the API answered %s: %s", e.status_code, e.message) - raise TutorUnavailable("המורה לא זמין כרגע. נסו שוב.") from e - except anthropic.APIConnectionError as e: - log.warning("tutor: no connection: %s", e) - raise TutorUnavailable("אין חיבור למורה כרגע. נסו שוב.") from e - if res.stop_reason == "refusal": - log.warning("tutor: the model declined (%s)", res.stop_details) - raise TutorUnavailable("המורה לא יכול לענות על זה. נסו לומר משהו אחר.") - if res.stop_reason == "max_tokens": - raise TutorUnavailable("התשובה נקטעה. נסו שוב.") - text = next((b.text for b in res.content if b.type == "text"), "") - try: - data = json.loads(text) - except ValueError as e: - log.warning("tutor: an answer that is not JSON: %r", text[:300]) - raise TutorUnavailable("המורה לא הצליח לענות. נסו שוב.") from e - return data, (res.usage.input_tokens + (res.usage.cache_read_input_tokens or 0) - + (res.usage.cache_creation_input_tokens or 0), res.usage.output_tokens) - - def _sweep(self) -> None: - """Drops conversations nobody continued for TTL, and the oldest past MAX_LIVE. Called with the lock held.""" - now = time.monotonic() - for k in [k for k, c in self._chats.items() if now - c.seen > TTL]: - del self._chats[k] - while len(self._chats) >= MAX_LIVE: - del self._chats[min(self._chats, key=lambda k: self._chats[k].seen)] - if len(self._started) > 10 * MAX_LIVE: - self._started.clear() - - -def tutor_client(key: str): - import anthropic - return anthropic.Anthropic(api_key=key, timeout=30.0, max_retries=2) - - -def load_tutor(key: str | None = None, model: str | None = None) -> Tutor | None: - """`key`, `model`: the ones the panel set (see keystore.py); None reads the environment's.""" - if key is None: - key = os.environ.get("ANTHROPIC_API_KEY", "") or os.environ.get("CLAUDE_API_KEY", "") - key = key.strip() - if not key: - log.info("no ANTHROPIC_API_KEY: the tutor game (יאללה נדבר) is off") - return None - try: - import anthropic - except ImportError: - log.warning("ANTHROPIC_API_KEY is set but the anthropic package is not installed: the tutor game is off") - return None - model = (os.environ.get("TUTOR_MODEL", "") if model is None else model).strip() or DEFAULT_MODEL - log.info("the tutor game (יאללה נדבר) runs on %s", model) - return Tutor(tutor_client(key), model) - - -def tutor_open() -> bool: - """Whether every student gets the game, and not only the teachers.""" - return os.environ.get("TUTOR_OPEN", "").strip().lower() in ("1", "true", "yes", "on") diff --git a/app/verb_game.py b/app/verb_game.py deleted file mode 100644 index 7f77c0d9fd030dd43fc9e922e5c9d125e3826561..0000000000000000000000000000000000000000 --- a/app/verb_game.py +++ /dev/null @@ -1,411 +0,0 @@ -"""Exercise generation and server-side checking for "פועל יוצא" levels 1--5. - -The grammar tables are deliberately the only linguistic source used here. A verb -without a complete table is not eligible for the game: guessing a missing form is -worse than offering a smaller, accurate exercise pool. -""" -from __future__ import annotations - -import random -import secrets -import time -from dataclasses import dataclass, field -from typing import Any - -from .conjugation import (FUTURE_PREFIX, FUTURE_SUFFIX, FUTURE_SUFFIX_TILES, PAST_SUFFIX, PAST_SUFFIX_TILES, - PAST_SUFFIX_TR, PREFIX_TILES, STAGE_A_VERBS, compose, lead, plain) -from .grammar import PRONOUNS, VERBS -from .workbook import LEX - - -VERB_META = { - "akal": {"name": "אכל", "unlock": "1"}, - "qaad": {"name": "ישב", "unlock": "1"}, - "fatah": {"name": "פתח", "unlock": "1"}, - "saal": {"name": "שאל", "unlock": "1"}, - "tabakh": {"name": "בישל", "unlock": "3"}, - "aja": {"name": "בא", "unlock": "4"}, - "raah": {"name": "הלך", "unlock": "5"}, - "faat": {"name": "נכנס / עבר", "unlock": "6"}, - "fihem": {"name": "הבין", "unlock": "8"}, - "laeb": {"name": "שיחק", "unlock": "10"}, - "libes": {"name": "לבש", "unlock": "2.3"}, - "labbas": {"name": "הלביש", "unlock": "2.3"}, - "jaab": {"name": "הביא", "unlock": "2.5"}, - "saaad": {"name": "עזר", "unlock": "2.6"}, - "rakad": {"name": "רץ", "unlock": "2.7"}, - "dall": {"name": "נשאר / המשיך", "unlock": "2.8"}, -} -LEVELS = { - 1: {"name": "בחירת ההטיה", "choices": 4, "window_ms": 12000}, - 2: {"name": "התאמת הגופים", "pairs": 6}, - 3: {"name": "החלפה מהירה", "choices": 3, "window_ms": 6000}, - 4: {"name": "אומרים בקול"}, - 5: {"name": "תגובה בשיחה"}, -} - -# "בונים עם איתן": stage A's verbs, past and future together, built from tiles (אותיות איתן and the endings). -# 6 shows the Hebrew beside the slots, 7 only after the answer; 8 shows a form and asks every pronoun it can be. -EITAN_LEVELS = { - 6: {"name": "בונים עם עברית"}, - 7: {"name": "בונים לבד"}, - 8: {"name": "מי אמר?"}, -} -TENSE_NAMES = {"past": "עבר", "future": "עתיד"} - -# Complete, filled past-tense tables in the parsed book. Order is PRONOUNS order. -# Arabic is not copied or inferred from transliteration: each form must resolve back -# to the application's existing LEX source of truth, otherwise the paradigm is dropped. -BOOK_PAST_TRANSLITERATIONS = { - "tabakh": ["טַבַּחֵ'ת", "טַבַּחֵ'ת", "טַבַּחְ'תִי", "טַבַּח'", "טַבְּחַ'ת", "טַבַּחְ'נַא", "טַבַּחְ'תוּ", "טַבַּח'וּ"], - "aja": ["אַגֵ'ית", "אַגֵ'ית", "אַגֵ'יתִי", "אַגַ'א", "אַגַ'ת", "אַגֵ'ינַא", "אַגֵ'יתוּ", "אַג'וּ"], - "raah": ["רֻחֵת", "רֻחֵת", "רֻחְתִי", "רַאח", "רַאחַת", "רֻחְנַא", "רֻחְתוּ", "רַאחוּ"], - "faat": ["פֻתֵת", "פֻתֵת", "פֻתִّי", "פַאת", "פַאתַת", "פֻתְנַא", "פֻתّוּ", "פַאתוּ"], - "fihem": ["פְהִמֵת", "פְהִמֵת", "פְהִמְתִי", "פִהֵם", "פִהְמַת", "פְהִמְנַא", "פְהִמְתוּ", "פִהְמוּ"], - "laeb": ["לְעִבֵּת", "לְעִבֵּת", "לְעִבְּתִי", "לִעֵבּ", "לִעְבַּת", "לְעִבְּנַא", "לְעִבְּתוּ", "לִעְבּוּ"], - "libes": ["לְבִּסֵת", "לְבִּסֵת", "לְבִּסְתִי", "לִבֵּס", "לִבְּסַת", "לְבִּסְנַא", "לְבִּסְתוּ", "לִבְּסוּ"], - "labbas": ["לַבַּّסֵת", "לַבַּّסֵת", "לַבַּّסְתִי", "לַבַּّס", "לַבַּّסַת", "לַבַּّסְנַא", "לַבַּّסְתוּ", "לַבַּّסוּ"], - "jaab": ["גִ'בֵּת", "גִ'בֵּת", "גִ'בְּתִי", "גַ'אבּ", "גַ'אבַּת", "גִ'בְּנַא", "גִ'בְּתוּ", "גַ'אבּוּ"], - "saaad": ["סַאעַדֵת", "סַאעַדֵת", "סַאעַדְתִי", "סַאעַד", "סַאעַדַת", "סַאעַדְנַא", "סַאעַדְתוּ", "סַאעַדוּ"], - "rakad": ["רַכַּדֵֿת", "רַכַּדֵֿת", "רַכַּדְֿתִי", "רַכַּדֿ", "רַכְּדַֿת", "רַכַּדְֿנַא", "רַכַּדְֿתוּ", "רַכַּדֿוּ"], - "dall": ["דַֿלֵّית", "דַֿלֵّית", "דַֿלֵّיתִי", "דַֿלّ", "דַֿלַّת", "דַֿלֵّינַא", "דַֿלֵّיתוּ", "דַֿלّוּ"], -} - - -def _book_paradigms() -> dict[str, list[tuple[str, str]]]: - by_tr: dict[str, tuple[str, str]] = {} - for key, (tr, _ipas) in LEX.items(): - by_tr.setdefault(tr, (key.partition("#")[0], tr, list(_ipas))) - return {verb: [by_tr[tr] for tr in forms] for verb, forms in BOOK_PAST_TRANSLITERATIONS.items() - if all(tr in by_tr for tr in forms)} - - -BOOK_VERBS = _book_paradigms() - - -def _form(verb: str, pronoun: int) -> dict[str, Any]: - if verb in VERBS: - he, tr, arabic, ipas = VERBS[verb][pronoun] - return {"hebrew": he, "transliteration": tr, "arabic": arabic, "ipas": list(ipas)} - arabic, tr, ipas = BOOK_VERBS[verb][pronoun] - return {"hebrew": "", "transliteration": tr, "arabic": arabic, "ipas": ipas} - - -def _paradigm(verb: str) -> list: - return VERBS[verb] if verb in VERBS else BOOK_VERBS[verb] - - -def _distance(a: str, b: str) -> int: - """Small edit distance, used only to rank real database forms as distractors.""" - row = list(range(len(b) + 1)) - for i, x in enumerate(a, 1): - old = row[0] - row[0] = i - for j, y in enumerate(b, 1): - above = row[j] - row[j] = min(row[j] + 1, row[j - 1] + 1, old + (x != y)) - old = above - return row[-1] - - -@dataclass -class StoredQuestion: - mode: int - verb: str - correct: dict[str, str] - made_at: float - forms: dict[str, tuple[str, str]] = field(default_factory=dict) - phrase: dict[str, Any] | None = None - - -class PracticeSessionManager: - """Short-lived opaque questions prevent clients from grading themselves.""" - - def __init__(self, ttl_seconds: int = 1800, rng: random.Random | None = None): - self.ttl_seconds = ttl_seconds - self.rng = rng or random.Random() - self.questions: dict[str, StoredQuestion] = {} - - def put(self, question: StoredQuestion) -> str: - now = time.monotonic() - self.questions = {k: v for k, v in self.questions.items() if now - v.made_at < self.ttl_seconds} - key = secrets.token_urlsafe(18) - self.questions[key] = question - return key - - def take(self, key: str) -> StoredQuestion | None: - question = self.questions.pop(key, None) - if question and time.monotonic() - question.made_at < self.ttl_seconds: - return question - return None - - def get(self, key: str) -> StoredQuestion | None: - question = self.questions.get(key) - if question and time.monotonic() - question.made_at < self.ttl_seconds: - return question - self.questions.pop(key, None) - return None - - def finish(self, key: str) -> None: - self.questions.pop(key, None) - - -class VerbExerciseGenerator: - def __init__(self, sessions: PracticeSessionManager | None = None): - self.sessions = sessions or PracticeSessionManager() - self.rng = self.sessions.rng - - @staticmethod - def available(lessons: list[str]) -> list[str]: - allowed = set(lessons) - return [v for v in VERB_META - if (v in VERBS or v in BOOK_VERBS) - and len(_paradigm(v)) == len(PRONOUNS) - and VERB_META[v]["unlock"] in allowed] - - def metadata(self, lessons: list[str]) -> dict[str, Any]: - return { - "levels": [{"id": n, **cfg} for n, cfg in LEVELS.items()], - "verbs": [{"id": v, "name": VERB_META[v]["name"], "unlock": VERB_META[v]["unlock"]} - for v in self.available(lessons)], - "tenses": [{"id": "past", "name": "עבר"}], - "eitan_levels": [{"id": n, **cfg} for n, cfg in EITAN_LEVELS.items()], - "eitan_verbs": [{"id": v, "name": STAGE_A_VERBS[v]["name"], "draft": STAGE_A_VERBS[v]["draft"]} - for v in self.eitan_available(lessons, drafts=True)], - } - - def _target(self, verbs: list[str], mastery: dict, recent: list[str]) -> tuple[str, int]: - candidates = [(v, p) for v in verbs for p in range(len(PRONOUNS))] - def priority(item: tuple[str, int]) -> float: - v, p = item - key = f"{v}:past:{PRONOUNS[p][0]}" - m = mastery.get(key, {}) if isinstance(mastery, dict) else {} - attempts = max(0, int(m.get("attempts", 0) or 0)) - accuracy = float(m.get("correct", 0) or 0) / attempts if attempts else 0.0 - slow = min(float(m.get("median_ms", 0) or 0) / 4000, 1.5) - recency_penalty = 5 if key in recent[-3:] else 0 - # Incorrect, slow, and unseen forms rise; a little noise avoids a fixed drill order. - return (3 if attempts and accuracy < .7 else 0) + slow + (2 if not attempts else 0) - recency_penalty + self.rng.random() - return max(candidates, key=priority) - - @staticmethod - def eitan_available(lessons: list[str], drafts: bool = False) -> list[str]: - """Stage A's verbs of the lessons given; a verb with draft future forms only when drafts are wanted (teachers).""" - allowed = set(lessons) - return [v for v, verb in STAGE_A_VERBS.items() if verb["lesson"] in allowed and (drafts or not verb["draft"])] - - def question(self, level: int, lessons: list[str], mastery: dict | None = None, - recent: list[str] | None = None, drafts: bool = False) -> dict[str, Any]: - if level in EITAN_LEVELS: - return self._eitan(level, lessons, mastery or {}, recent or [], drafts) - if level not in LEVELS: - raise ValueError("unsupported level") - verbs = self.available(lessons) - if not verbs: - raise LookupError("no complete verb paradigms are unlocked") - if level == 2: - return self._matching(verbs, mastery or {}, recent or []) - if level in (4, 5): - return self._spoken(level, verbs, mastery or {}, recent or []) - verb, pron = self._target(verbs, mastery or {}, recent or []) - right = _form(verb, pron) - count = LEVELS[level]["choices"] - pool = [] - for other_pron in range(len(PRONOUNS)): - if other_pron != pron: - pool.append((verb, other_pron, _form(verb, other_pron))) - if level == 3 and len(verbs) > 1: - for other in verbs: - if other != verb: - pool.append((other, pron, _form(other, pron))) - pool.sort(key=lambda x: (_distance(right["transliteration"], x[2]["transliteration"]), self.rng.random())) - unique, seen = [], {(right["transliteration"], right["arabic"])} - for _, _, form in pool: - signature = (form["transliteration"], form["arabic"]) - if signature not in seen: - seen.add(signature) - unique.append(form) - if len(unique) == count - 1: - break - choices = [{"id": secrets.token_urlsafe(7), "transliteration": f["transliteration"], "arabic": f["arabic"]} - for f in [right, *unique]] - self.rng.shuffle(choices) - correct_id = next(c["id"] for c in choices if (c["transliteration"], c["arabic"]) == - (right["transliteration"], right["arabic"])) - qid = self.sessions.put(StoredQuestion(level, verb, {"choice": correct_id}, time.monotonic())) - pronoun = PRONOUNS[pron] - return { - "question_id": qid, "level": level, "tense": {"id": "past", "name": "עבר"}, - "verb": {"id": verb, "name": VERB_META[verb]["name"], "arabic": _form(verb, 3)["arabic"]}, - "pronoun": {"id": pronoun[0], "hebrew": pronoun[1], "transliteration": pronoun[2]}, - "choices": choices, "time_limit_ms": LEVELS[level]["window_ms"], - "mastery_key": f"{verb}:past:{pronoun[0]}", - } - - def _matching(self, verbs: list[str], mastery: dict, recent: list[str]) -> dict[str, Any]: - verb, _ = self._target(verbs, mastery, recent) - count = min(LEVELS[2]["pairs"], len(PRONOUNS)) - indices = self.rng.sample(range(len(PRONOUNS)), count) - left, right, correct, forms = [], [], {}, {} - for p in indices: - pron = PRONOUNS[p] - form = _form(verb, p) - pid, fid = secrets.token_urlsafe(6), secrets.token_urlsafe(6) - left.append({"id": pid, "hebrew": pron[1], "transliteration": pron[2]}) - right.append({"id": fid, "transliteration": form["transliteration"], "arabic": form["arabic"]}) - correct[pid] = fid - forms[fid] = (form["transliteration"], form["arabic"]) - self.rng.shuffle(left) - self.rng.shuffle(right) - qid = self.sessions.put(StoredQuestion(2, verb, correct, time.monotonic(), forms)) - return {"question_id": qid, "level": 2, "tense": {"id": "past", "name": "עבר"}, - "verb": {"id": verb, "name": VERB_META[verb]["name"], "arabic": _form(verb, 3)["arabic"]}, - "pronouns": left, "forms": right, "pair_count": count, - "mastery_key": f"{verb}:past:matching"} - - def _spoken(self, level: int, verbs: list[str], mastery: dict, recent: list[str]) -> dict[str, Any]: - if level == 5: - # A controlled first conversation: the learner answers about themself. Keeping - # the target to ana makes the prompt natural and the grammar target unambiguous. - scored = [(v, 0) for v in verbs] - verb, pron = max(scored, key=lambda x: self.rng.random() + - (2 if not mastery.get(f"{x[0]}:past:ana") else 0)) - else: - verb, pron = self._target(verbs, mastery, recent) - form = _form(verb, pron) - p = PRONOUNS[pron] - variants = [(form["arabic"], ipa) for ipa in form["ipas"]] - if level == 5: - pron_ar, pron_ipa = PRONOUNS[0][3][0] - variants += [(f"{pron_ar} {form['arabic']}", f"{pron_ipa} | {ipa}") for ipa in form["ipas"]] - phrase = {"variants": variants, "cat": "verb-game", "key": f"{verb}:past:{p[0]}:spoken", - "he": f"{p[1]} · {VERB_META[verb]['name']} (עבר)"} - qid = self.sessions.put(StoredQuestion(level, verb, {}, time.monotonic(), phrase=phrase)) - result = { - "question_id": qid, "level": level, "tense": {"id": "past", "name": "עבר"}, - "verb": {"id": verb, "name": VERB_META[verb]["name"], "arabic": _form(verb, 3)["arabic"]}, - "pronoun": {"id": p[0], "hebrew": p[1], "transliteration": p[2]}, - "mastery_key": f"{verb}:past:{p[0]}:spoken", - } - if level == 5: - result["conversation"] = {"arabic": "شو عملت مبارح؟", "transliteration": "שוּ עְמֵלְת אִמְבַּארֵח?", - "hebrew": "מה עשית אתמול?"} - return result - - def _pick(self, keys: list[tuple[str, Any]], mastery: dict, recent: list[str]) -> tuple[str, Any]: - """(mastery key, item): the weakest, slowest or unseen first, never one of the last three.""" - def priority(entry: tuple[str, Any]) -> float: - m = mastery.get(entry[0], {}) if isinstance(mastery, dict) else {} - m = m if isinstance(m, dict) else {} - attempts = max(0, int(m.get("attempts", 0) or 0)) - accuracy = float(m.get("correct", 0) or 0) / attempts if attempts else 0.0 - slow = min(float(m.get("median_ms", 0) or 0) / 6000, 1.5) - return ((3 if attempts and accuracy < .7 else 0) + slow + (2 if not attempts else 0) - - (5 if entry[0] in recent[-3:] else 0) + self.rng.random()) - return max(keys, key=priority) - - def _eitan(self, level: int, lessons: list[str], mastery: dict, recent: list[str], drafts: bool) -> dict[str, Any]: - verbs = self.eitan_available(lessons, drafts) - if not verbs: - raise LookupError("no stage A verbs with a checked future are unlocked") - if level == 8: - entries = [(f"{v}:{t}:who:{p}", (v, t, p)) for v in verbs for t in ("past", "future") for p in range(len(PRONOUNS))] - key, (verb_id, tense, pron) = self._pick(entries, mastery, recent) - verb, form = STAGE_A_VERBS[verb_id], STAGE_A_VERBS[verb_id][tense][pron] - same = lambda f: plain(f["ar"]) == plain(form["ar"]) and f["tr"] == form["tr"] - who = [PRONOUNS[p][0] for p, f in enumerate(verb[tense]) if same(f)] - qid = self.sessions.put(StoredQuestion(level, verb_id, {"who": who, "tense": tense, "pronoun": pron}, - time.monotonic())) - return {"question_id": qid, "level": level, "tense": {"id": tense, "name": TENSE_NAMES[tense]}, - "verb": {"id": verb_id, "name": verb["name"], "transliteration": verb["transliteration"], - "draft": verb["draft"]}, - "form": {"arabic": form["ar"], "transliteration": form["tr"], "parts": form["tr_parts"]}, - "pronouns": [{"id": p[0], "hebrew": p[1], "transliteration": p[2]} for p in PRONOUNS], - "mastery_key": key} - entries = [(f"{v}:pf:{PRONOUNS[p][0]}", (v, p)) for v in verbs for p in range(len(PRONOUNS))] - key, (verb_id, pron) = self._pick(entries, mastery, recent) - verb, p = STAGE_A_VERBS[verb_id], PRONOUNS[pron] - past, future = verb["past"][pron], verb["future"][pron] - # a tile: its Arabic (what is sent back and graded) and its transliteration (what the page shows) - tiles = lambda pairs: [{"arabic": a, "label": h} for a, h in pairs] - leads = {FUTURE_PREFIX[i]: lead(verb["future"][i]["tr"])[0] for i in (0, 1, 3, 5)} # this verb's vowels - question = { - "question_id": self.sessions.put(StoredQuestion(level, verb_id, {"pronoun": pron}, time.monotonic())), - "level": level, "tense": {"id": "both", "name": "עבר ועתיד"}, - "verb": {"id": verb_id, "name": verb["name"], "transliteration": verb["transliteration"], - "draft": verb["draft"]}, - "pronoun": {"id": p[0], "hebrew": p[1], "transliteration": p[2]}, - "past": {"body": past["tr_parts"]["body"], "tiles": tiles(PAST_SUFFIX_TILES)}, - "future": {"body": future["tr_parts"]["body"], - "prefix_tiles": tiles([(a, leads[a]) for a, _ in PREFIX_TILES]), - "suffix_tiles": tiles(FUTURE_SUFFIX_TILES)}, - "mastery_key": key, - } - if level == 6: - question["past"]["hebrew"], question["future"]["hebrew"] = past["he"], future["he"] - return question - - @staticmethod - def _shown(form: dict) -> dict[str, Any]: - return {"arabic": form["ar"], "transliteration": form["tr"], "parts": form["tr_parts"], "hebrew": form["he"], - "draft": form["draft"]} - - def _eitan_answer(self, q: StoredQuestion, answer: Any, response_ms: int) -> dict[str, Any]: - verb = STAGE_A_VERBS[q.verb] - supplied = answer if isinstance(answer, dict) else {} - if q.mode == 8: - chosen = supplied.get("who") if isinstance(supplied.get("who"), list) else [] - chosen = sorted({str(c) for c in chosen}) - ok = chosen == sorted(q.correct["who"]) - tense = q.correct["tense"] - explain = ("בעתיד התחילית אומרת מי: א=אני, ת=אתה/את/היא/אתם, י=הוא/הם, נ=אנחנו, כמו בעברית. " - "ת לבדה היא גם אתה וגם היא." if tense == "future" else - "בעבר הסיומת אומרת מי. ת בסוף היא אני, אתה וגם היא, ורק התנועות מבדילות ביניהם.") - return {"correct": ok, "who": q.correct["who"], "response_ms": response_ms, - "forms": [{"pronoun": PRONOUNS[i][0], **self._shown(f)} for i, f in enumerate(verb[tense])], - **({} if ok else {"explanation": explain})} - pron = q.correct["pronoun"] - past, future = verb["past"][pron], verb["future"][pron] - text = lambda k: str(supplied.get(k) or "") - past_ok = plain(past["body"] + text("past")) == plain(past["ar"]) - future_ok = plain(compose(text("prefix"), future["body"], text("suffix"))) == plain(future["ar"]) - result = {"correct": past_ok and future_ok, "past_correct": past_ok, "future_correct": future_ok, - "past": self._shown(past), "future": self._shown(future), "response_ms": response_ms} - if not future_ok: - he, parts = "".join(future["he"].values()), future["tr_parts"] - ending = f"…{parts['suf'].lstrip(chr(0x05B4))}" if parts["suf"] else "" # …י, not a hiriq on the dots - result["explanation"] = f"בעתיד: {parts['pre']}{ending}, כמו {he} בעברית." - elif not past_ok: - ending = PAST_SUFFIX_TR[pron][0] - result["explanation"] = f"בעבר הסיומת של {PRONOUNS[pron][1]} היא {'…' + ending if ending else 'בלי סיומת'}." - return result - - def speech_phrase(self, question_id: str) -> tuple[StoredQuestion, dict[str, Any]]: - question = self.sessions.get(question_id) - if not question or question.mode not in (4, 5) or not question.phrase: - raise LookupError("spoken question expired") - return question, question.phrase - - def answer(self, question_id: str, answer: Any, response_ms: int) -> dict[str, Any]: - q = self.sessions.take(question_id) - if not q: - raise LookupError("question expired or already answered") - response_ms = max(0, min(int(response_ms), 300_000)) - if q.mode in EITAN_LEVELS: - return self._eitan_answer(q, answer, response_ms) - if q.mode == 2: - supplied = answer if isinstance(answer, dict) else {} - # A duplicated written/spoken form is interchangeable with its identical twin. - pair_results = {pid: supplied.get(pid) in q.forms and q.forms[supplied[pid]] == q.forms[expected] - for pid, expected in q.correct.items()} - correct = sum(pair_results.values()) - total = len(q.correct) - return {"correct": correct == total, "correct_pairs": correct, "total_pairs": total, - "pair_results": pair_results, "response_ms": response_ms} - chosen = str(answer) - ok = chosen == q.correct["choice"] - result = {"correct": ok, "response_ms": response_ms} - if not ok: - result["explanation"] = "הסיומת משתנה לפי הגוף. התאימו את ההטיה לכינוי שמופיע בשאלה." - result["correct_choice_id"] = q.correct["choice"] - return result diff --git a/app/workbook.py b/app/workbook.py deleted file mode 100644 index 49a1990158dbcbd2333279f09d0c6dba022e0c21..0000000000000000000000000000000000000000 --- a/app/workbook.py +++ /dev/null @@ -1,4155 +0,0 @@ -"""The course workbooks (שלב א', שלב ב') lesson by lesson: the student picks a lesson, then one of its topics. - -A topic is a list of items, each written once as (Hebrew, Arabic), and is one of five kinds: - "learn": the lesson's new words, before anything is said. The page shows them as cards: the Hebrew, then on a tap - the transliteration, the Arabic and its voice; the student marks each one known or not yet. Nothing is - scored, and the cards are not `listed` (see workbook_phrases), so no tally counts them. - "match": a game on those words ("התאימו זוגות"): the Hebrew in one column, the transliteration in the other, and - the student joins the pairs. It has no items: its category names the lesson's "learn" topic (`match`). - "say": Hebrew -> Arabic. The usual flow: Hebrew prompt, the student says it in Arabic. - "read": Arabic -> Hebrew. The page shows the transliteration, the student reads it aloud (and is - scored on it) and can reveal the translation one word at a time. - "write": the workbook's homework, sentences of the student's own. It has no items: the page shows its task - (WRITING) and a line for each sentence. What is written is kept for the student (the Supabase table - `writings`) and read by the teacher in the panel. Nothing is said or scored. -Single words (the pronouns, a verb's past and future) and conjugations are "say" items like the -sentences. An ordered topic is shown in the workbook's order; the others are shuffled. - -Each Arabic word is looked up in LEX, which gives its transliteration as the workbook writes it and -its accepted pronunciations. A word spelled like another form carries a tag after "#" -(اكلت = I ate, اكلت#f = she ate); the tag is not shown. A leading و ("and") is split off when the -whole word is not in LEX. Punctuation stays on its word. -""" -from __future__ import annotations - -import itertools -import re - -from .grammar import MAX_VARIANTS, PRONOUNS, WORDS - -# כינויי הקניין (lesson 2), one row per owner in PRONOUNS order: Hebrew, Hebrew as read aloud where the -# plain text is ambiguous, Arabic suffix, LEX tag, transliteration, IPA readings, starts with a vowel -OWNERS = [ - ("שלי", "", "ي", "", "-י", ["i"], True), - ("שלך (לגבר)", "שֶׁלְּךָ", "ك", "", "ַ-כּ", ["a k"], True), - ("שלך (לאישה)", "שֶׁלָּךְ", "ك", "#f", "ֵ-כּ", ["i k"], True), - ("שלו", "", "ه", "", "-וֹ", ["o"], True), - ("שלה", "", "ها", "", "-הַא", ["h a"], False), - ("שלנו", "", "نا", "", "-נַא", ["n a"], False), - ("שלכם", "", "كم", "", "-כֹּם", ["k o m", "k u"], False), - ("שלהם", "", "هم", "", "-הֹםّ", ["h o m"], False), -] -# Words that take those suffixes: Arabic stem (ة written ت), transliteration and IPA before a vowel -# suffix, then before a consonant one where the stem changes (طاولتي, طاولتنا). IPA readings: " / ". -POSSESSED = [ - ("بنت", "בִּנְת", "b i n t"), ("بنات", "בַּנַאת", "b a n aː t"), - ("ابن", "אִבְּנ", "ʔ i b n", "אִבֵּן", "ʔ i b i n"), - ("طاولت", "טַאוְלְת", "tˤ aː w l t", "טַאוְלֵת", "tˤ aː w l i t"), - ("كلب", "כַּלְבּ", "k a l b"), ("بيت", "בֵּית", "b eː t"), - ("حفيد", "חַפִיד", "ħ a f iː d"), ("حفيدت", "חַפִידְת", "ħ a f iː d t", "חַפִידֵת", "ħ a f iː d i t"), - # סבא and סבתא have two words each: the other one is accepted as well - ("جدّ", "גִ'דّ", "dʒ i d d / s iː d"), ("سيد", "סִיד", "s iː d / dʒ i d d"), - ("جدّت", "גִ'דְّת", "dʒ i d d t / s i t t", "גִ'דֵّת", "dʒ i d d i t / s i t t"), - ("ستّ", "סִתّ", "s i t t / dʒ i d d t", "סִתّ", "s i t t / dʒ i d d i t"), - ("خال", "חַ'אל", "x aː l"), ("خالت", "חַ'אלְת", "x aː l t", "חַ'אלֵת", "x aː l i t"), - ("عمّ", "עַמّ", "ʕ a m m", "עַםّ", "ʕ a m m"), ("عمّت", "עַמְّת", "ʕ a m m t", "עַמֵّת", "ʕ a m m i t"), - ("امّ", "אִמّ", "ʔ i m m / ʔ u m m", "אִםّ", "ʔ i m m / ʔ u m m"), - ("اخت", "אֹחְ'ת", "ʔ u x t"), ("اخوت", "אִחְ'וֵת", "ʔ i x w e t"), - ("صاحب", "צַאחְבּ", "sˤ aː ħ b", "צַאחֵבּ", "sˤ aː ħ i b"), - ("جوز", "ג'וֹז", "dʒ oː z"), ("مرت", "מַרְת", "m a r t", "מַרַת", "m a r a t"), - ("جار", "גַ'אר", "dʒ aː r"), ("جارت", "גַ'ארְת", "dʒ aː r t", "גַ'ארֵת", "dʒ aː r i t"), - ("صاحبت", "צַאחִבְּת", "sˤ aː ħ i b t"), ("اهل", "אַהְל", "ʔ a h l", "אַהֵל", "ʔ a h i l"), - ("عيلت", "עֵילְת", "ʕ eː l t", "עֵילֵת", "ʕ eː l i t"), - ("اسم", "אִסְמ", "ʔ i s m", "אִסֵם", "ʔ i s i m"), ("عمر", "עֹמְר", "ʕ u m r", "עֹמֵר", "ʕ u m u r / ʕ u m i r"), - ("اصل", "אַצְל", "ʔ a sˤ l", "אַצֵל", "ʔ a sˤ i l"), ("حال", "חַאל", "ħ aː l"), - ("سيّارت", "סַיַّארְת", "s a j j aː r t", "סַיַّארֵת", "s a j j aː r i t"), - ("حارت", "חַארְת", "ħ aː r t", "חַארֵת", "ħ aː r i t"), - ("عند", "עִנְד", "ʕ i n d"), ("مع", "מַע", "m a ʕ"), -] - - -def _possessed(stem: str, tr_v: str, ipa_v: str, tr_c: str = "", ipa_c: str = "") -> dict: - """The LEX entries of one word with every owner's suffix: بنتي, بنتك, بنتك#f ...""" - out = {} - for _, _, suffix, tag, tr, ends, vowel in OWNERS: - tr_stem, ipa = (tr_v, ipa_v) if vowel else (tr_c or tr_v, ipa_c or ipa_v) - out[stem + suffix + tag] = (tr_stem + tr, [f"{s} {e}" for s in ipa.split(" / ") for e in ends]) - return out - - -# arabic[#tag] -> (workbook transliteration, IPA readings, main one first) -LEX: dict[str, tuple[str, list[str]]] = { - **WORDS, - **{alts[0][0]: (tr, [ipa for _, ipa in alts]) for _, _, tr, alts in PRONOUNS}, - **{word: entry for row in POSSESSED for word, entry in _possessed(*row).items()}, - # ---- verbs: past tense, then the future's base form ---- - "اكل": ("אַכַּל", ["ʔ a k a l"]), "اكلت": ("אַכַּלֵת", ["ʔ a k a l t"]), "اكلت#f": ("אַכְּלַת", ["ʔ a k l a t"]), - "اكلتي": ("אַכַּלְתִי", ["ʔ a k a l t i"]), - "اكلنا": ("אַכַּלְנַא", ["ʔ a k a l n a"]), "اكلتو": ("אַכַּלְתוּ", ["ʔ a k a l t u"]), - "اكلو": ("אַכַּלוּ", ["ʔ a k a l u", "ʔ a k l u"]), - "قعد": ("קַעַד", ["ʔ a ʕ a d", "q a ʕ a d"]), "قعدت": ("קַעַדֵת", ["ʔ a ʕ a d t", "q a ʕ a d t"]), - "قعدت#f": ("קַעְדַת", ["ʔ a ʕ d a t", "q a ʕ d a t"]), - "قعدتي": ("קַעַדְתִי", ["ʔ a ʕ a d t i", "q a ʕ a d t i"]), "قعدنا": ("קַעַדְנַא", ["ʔ a ʕ a d n a", "q a ʕ a d n a"]), - "قعدتو": ("קַעַדְתוּ", ["ʔ a ʕ a d t u", "q a ʕ a d t u"]), "قعدو": ("קַעַדוּ", ["ʔ a ʕ a d u", "q a ʕ a d u"]), - "فتح": ("פַתַח", ["f a t a ħ"]), "فتحت": ("פַתַחֵת", ["f a t a ħ t"]), "فتحتي": ("פַתַחְתִי", ["f a t a ħ t i"]), - "فتحت#f": ("פַתְחַת", ["f a t ħ a t"]), "فتحنا": ("פַתַחְנַא", ["f a t a ħ n a"]), - "فتحتو": ("פַתַחְתוּ", ["f a t a ħ t u"]), "فتحو": ("פַתַחוּ", ["f a t a ħ u", "f a t ħ u"]), - "سأل": ("סַאַל", ["s a ʔ a l"]), "سألت": ("סַאַלֵת", ["s a ʔ a l t", "s a ʔ a l i t"]), - "سألت#f": ("סַאְלַת", ["s a ʔ l a t"]), "سألنا": ("סַאַלְנַא", ["s a ʔ a l n a"]), - "سألتي": ("סַאַלְתִי", ["s a ʔ a l t i"]), "سألتو": ("סַאַלְתוּ", ["s a ʔ a l t u"]), "سألو": ("סַאַלוּ", ["s a ʔ a l u", "s a ʔ l u"]), - "يفتح": ("יִפְתַח", ["j i f t a ħ"]), "ياكل": ("יֹאכֹּל", ["j oː k u l", "j oː k i l", "j aː k u l"]), - "يسأل": ("יִסְאַל", ["j i s ʔ a l"]), "يقعد": ("יֻקְעֹד", ["j u ʔ ʕ u d", "j u q ʕ u d"]), - # ... with an object suffix - "سألنا#us": ("סַאַל-נַא", ["s a ʔ a l n a"]), "سألته": ("סַאַלְת-וֹ", ["s a ʔ a l t o"]), - "سألتك": ("סַאַלְתַ-כּ", ["s a ʔ a l t a k"]), "سألتك#f": ("סַאְלַתֵ-כּ", ["s a ʔ l a t i k"]), - "سألتها": ("סַאַלְת-הַא", ["s a ʔ a l t h a"]), "سألتوني": ("סַאַלְתוּ-נִי", ["s a ʔ a l t uː n i"]), - "سألتيني": ("סַאַלְתִי-נִי", ["s a ʔ a l t iː n i"]), - # ---- everything else ---- - "لا": ("לַא", ["l aː"]), "شو": ("שוּ", ["ʃ u"]), "ليش": ("לֵיש", ["l eː ʃ"]), "مين": ("מִין", ["m iː n"]), - "وين": ("וֵין", ["w eː n"]), "كمان": ("כַּמַאן", ["k a m aː n"]), "يا": ("יַא", ["j a"]), - "مفتوح": ("מַפְתוּח", ["m a f t uː ħ"]), "باب": ("בַּאבּ", ["b aː b"]), "ساكن": ("סַאכֵּן", ["s aː k i n"]), - "الحمد": ("אֵלְחַמְדֻ", ["i l ħ a m d u", "i l ħ a m d"]), "للّه": ("לִלַّה", ["l i l l a", "i l l a"]), - "صاحبي": ("צַאחְבּ-י", ["sˤ aː ħ b i"]), "صاحبه": ("צַאחְבּ-וֹ", ["sˤ aː ħ b o"]), - "بنتي": ("בִּנְת-י", ["b i n t i"]), "البنت": ("אֵלְבִּנְת", ["i l b i n t"]), "معها": ("מַע-הַא", ["m a ʕ h a"]), - "بيتي": ("בֵּית-י", ["b eː t i"]), "بيته": ("בֵּית-וֹ", ["b eː t o"]), - "اخوي": ("אַח'וּי", ["ʔ a x uː j"]), "اختي": ("אֹחְ'ת-י", ["ʔ u x t i"]), "امّي": ("אִמّ-י", ["ʔ i m m i"]), - "فاطمة": ("פַאטְמֵה", ["f aː tˤ m e"]), "شاطرة": ("שַאטְרַה", ["ʃ aː tˤ r a"]), - "النّجّار": ("אֵ(ל)נַّגַّ'אר", ["i n n a dʒ dʒ aː r"]), - "الدّكتورة": ("אֵ(ל)דַّכְּתוֹרַה", ["i d d a k t oː r a", "i d d u k t oː r a"]), - "المعلّمة": ("אֵלְמְעַלְّמֵה", ["i l m ʕ a l l m e"]), - "القدس": ("אֵלְקֻדְס", ["i l ʔ u d s", "i l q u d s"]), - "شاورما": ("שַוַארְמַה", ["ʃ a w aː r m a"]), "بيتسا": ("בִּיתְצַה", ["b iː t s a"]), "خيار": ("חְ'יַאר", ["x j aː r"]), - "كنافة": ("כְּנַאפֵה", ["k n aː f e"]), "بقلاوة": ("בַּקְלַאוַה", ["b a ʔ l aː w a", "b a q l aː w a"]), - # ---- lesson 2 ---- - # the dual - "اسبوع": ("אֻסְבּוּע", ["ʔ u s b uː ʕ"]), "اسبوعين": ("אֻסְבּוּעֵין", ["ʔ u s b uː ʕ eː n"]), - "كاسة": ("כַּאסֵה", ["k aː s e"]), "كاستين": ("כַּאסְתֵין", ["k aː s t eː n"]), - "ايد": ("אִיד", ["ʔ iː d"]), "ايدين": ("אִידֵין", ["ʔ iː d eː n"]), "بابين": ("בַּאבֵּין", ["b aː b eː n"]), - "فنجان": ("פִנְגַ'אן", ["f i n dʒ aː n"]), "فنجانين": ("פִנְגַ'אנֵין", ["f i n dʒ aː n eː n"]), - "بلوزة": ("בְּלוּזֵה", ["b l uː z e"]), "بلوزتين": ("בְּלוּזְתֵין", ["b l uː z t eː n"]), - "حارة": ("חַארַה", ["ħ aː r a"]), "حارتين": ("חַארְתֵין", ["ħ aː r t eː n"]), - "حارة#of": ("חַארֵת", ["ħ aː r i t", "ħ aː r e t"]), - "ولد": ("וַלַד", ["w a l a d"]), "ولدين": ("וַלַדֵין", ["w a l a d eː n"]), "سؤالين": ("סֻאַאלֵין", ["s u ʔ aː l eː n"]), - "اخ": ("אַח'", ["ʔ a x"]), "اخين": ("אַחֵ'ין", ["ʔ a x eː n"]), - "طيّارة": ("טַיַّארַה", ["tˤ a j j aː r a"]), "طيّارتين": ("טַיַّארְתֵין", ["tˤ a j j aː r t eː n"]), - "سيّارة": ("סַיַّארַה", ["s a j j aː r a"]), "سيّارتين": ("סַיַّארְתֵין", ["s a j j aː r t eː n"]), - "بنت": ("בִּנְת", ["b i n t"]), "بنتين": ("בִּנְתֵין", ["b i n t eː n"]), - "يوم": ("יוֹם", ["j oː m"]), "يومين": ("יוֹמֵין", ["j oː m eː n"]), - "طاولة": ("טַאוְלֵה", ["tˤ aː w l e"]), "طاولتين": ("טַאוְלְתֵין", ["tˤ aː w l t eː n"]), - "حفيد": ("חַפִיד", ["ħ a f iː d"]), "حفيدين": ("חַפִידֵין", ["ħ a f iː d eː n"]), - "كلب": ("כַּלְבּ", ["k a l b"]), "كلبين": ("כַּלְבֵּין", ["k a l b eː n"]), - # the family - "اهل": ("אַהֵל", ["ʔ a h i l", "ʔ a h l"]), "اب": ("אַבּ", ["ʔ a b"]), "امّ": ("אִםّ", ["ʔ i m m", "ʔ u m m"]), - "عمّ": ("עַםّ", ["ʕ a m m"]), "عمّة": ("עַמֵّה", ["ʕ a m m e"]), "خال": ("חַ'אל", ["x aː l"]), "خالة": ("חַ'אלֵה", ["x aː l e"]), - "اخت": ("אֹחְ'ת", ["ʔ u x t"]), "ابن": ("אִבֵּן", ["ʔ i b i n", "ʔ i b n"]), "حفيدة": ("חַפִידֵה", ["ħ a f iː d e"]), - "سيد": ("סִיד", ["s iː d"]), "جدّ": ("גִ'דّ", ["dʒ i d d"]), "ستّ": ("סִתّ", ["s i t t"]), "جدّة": ("גִ'דֵّה", ["dʒ i d d e"]), - "بكر": ("בִּכֵּר", ["b i k i r", "b i k r"]), "بكرة": ("בִּכְּרֵה", ["b i k r e"]), - "آخر": ("אַאחֵ'ר", ["ʔ aː x i r"]), "العنقود": ("אֵלְעַנְקוּד", ["i l ʕ a n ʔ uː d", "i l ʕ a n q uː d"]), - "جوز": ("ג'וֹז", ["dʒ oː z", "z oː dʒ"]), "مرة": ("מַרַה", ["m a r a"]), "نسوان": ("נִסְוַאן", ["n i s w aː n"]), - "زلمة": ("זַלַמֵה", ["z a l a m e"]), "اولاد": ("אַוְלַאד", ["ʔ a w l aː d"]), - "البكر": ("אֵלְבִּכֵּר", ["i l b i k i r", "i l b i k r"]), "البكرة": ("אֵלְבִּכְּרֵה", ["i l b i k r e"]), - "ابو": ("אַבּוּ", ["ʔ a b u"]), "ابوك": ("אַבּוּכּ", ["ʔ a b uː k"]), "ابوها": ("אַבּוּ-הַא", ["ʔ a b uː h a"]), - # verbs with an object suffix - "سألته#f": ("סַאְלַת-וֹ", ["s a ʔ l a t o"]), "سألتها#f": ("סַאְלַת-הַא", ["s a ʔ l a t h a"]), - "سألتنا": ("סַאְלַת-נַא", ["s a ʔ l a t n a"]), "سألتهم": ("סַאַלְת-הֹםّ", ["s a ʔ a l t h o m"]), - "سألتني": ("סַאַלְת-נִי", ["s a ʔ a l t n i"]), "سألني": ("סַאַל-נִי", ["s a ʔ a l n i"]), - "سألوني": ("סַאַלוּ-נִי", ["s a ʔ a l uː n i"]), "فتحته": ("פַתַחְת-וֹ", ["f a t a ħ t o"]), - # the sentences' other words - "قبل": ("קַבֵּל", ["ʔ a b i l", "q a b i l", "ʔ a b l"]), "بعدين": ("בַּעַדֵין", ["b a ʕ d eː n", "b a ʕ a d eː n"]), - "ايمتى": ("אֵימְתַא", ["ʔ eː m t a", "w eː n t a"]), "هون": ("הוֹן", ["h oː n"]), "عند": ("עִנְד", ["ʕ i n d"]), - "يعني": ("יַעְנִי", ["j a ʕ n i"]), "اذا": ("אִזַא", ["ʔ i z a"]), "بسّ": ("בַּסّ", ["b a s s"]), - "ولّا": ("וִלַّא", ["w i l l a"]), "واحد": ("וַאחַד", ["w aː ħ a d", "w aː ħ i d"]), "واحدة": ("וַאחְדֵה", ["w aː ħ d e"]), - "كبيرة": ("כְּבִּירֵה", ["k b iː r e"]), "الكبير": ("אֵלְכְּבִּיר", ["i l k b iː r"]), - "صغيرة": ("צְעִ'ירֵה", ["z ɣ iː r e", "sˤ ɣ iː r e"]), - "الصّغير": ("(אֵ)לְצְעִ'יר", ["l i z ɣ iː r", "l i sˤ ɣ iː r", "i z z ɣ iː r"]), - "الجديد": ("אֵלְגְ'דִיד", ["i l dʒ d iː d", "l i dʒ d iː d"]), "زاكي": ("זַאכִּי", ["z aː k i"]), - "اكل#n": ("אַכֵּל", ["ʔ a k i l", "ʔ a k l"]), "ممتاز": ("מֻמְתַאז", ["m u m t aː z"]), - "ساكنة": ("סַאכְּנֵה", ["s aː k n e"]), "ساكنين": ("סַאכְּנִין", ["s aː k n iː n"]), - "بيت": ("בֵּית", ["b eː t"]), "اسم": ("אִסֵם", ["ʔ i s i m", "ʔ i s m"]), - "الصّالون": ("אֵ(ל)צַّאלוֹן", ["i sˤ sˤ aː l oː n"]), "الكراج": ("אֵלְכַּרַאג'", ["i l k a r aː dʒ"]), - "المطعم": ("אֵלְמַטְעַם", ["i l m a tˤ ʕ a m"]), "المطبخ": ("אֵלְמַטְבַּח'", ["i l m a tˤ b a x"]), - "البحر": ("אֵלְבַּחַר", ["i l b a ħ a r", "i l b a ħ r"]), "الغرفة": ("אֵלְעֻ'רְפֵה", ["i l ɣ u r f e"]), - "الاوتيل": ("אֵלְאוֹתֵיל", ["i l ʔ oː t eː l"]), "المدرسة": ("אֵלְמַדְרַסֵה", ["i l m a d r a s e"]), - "البلد": ("אֵלְבַּלַד", ["i l b a l a d"]), "الشّيخ": ("אֵ(ל)שֵّיח'", ["i ʃ ʃ eː x"]), "شيخ": ("שֵיח'", ["ʃ eː x"]), - "الطّبّاخ": ("אֵ(ל)טַّבַּّאח'", ["i tˤ tˤ a b b aː x"]), "الكعكة": ("אֵלְכַּעְכֵּה", ["i l k a ʕ k e"]), - "الشّاورما": ("אֵ(ל)שַّוַארְמַה", ["i ʃ ʃ a w aː r m a"]), "بطّيخة": ("בַּטִّיחַ'ה", ["b a tˤ tˤ iː x a"]), - "مقلوبة": ("מַקְלוּבֵּה", ["m a ʔ l uː b e", "m a q l uː b e"]), - "الخليل": ("אֵלְחַ'לִיל", ["i l x a l iː l"]), "نابلس": ("נַאבְּלֵס", ["n aː b l i s"]), "حيفا": ("חֵיפַא", ["ħ eː f a"]), - "تلبيوت": ("תַלְבְּיוֹת", ["t a l b j oː t"]), "تلّ": ("תַלّ", ["t a l l"]), "ابيب": ("אַבִּיבּ", ["ʔ a b iː b"]), - "عسقلان": ("עַסְקַלַאן", ["ʕ a s ʔ a l aː n", "ʕ a s q a l aː n", "ʔ a s k a l aː n"]), - "طرعان": ("טֻרְעַאן", ["tˤ u r ʕ aː n"]), "الشّمال": ("אֵ(ל)שַّמַאל", ["i ʃ ʃ a m aː l"]), - "مونا": ("מוֹנַא", ["m oː n a"]), "روبا": ("רוּבַּה", ["r uː b a"]), "شوكو": ("שוֹכּוֹ", ["ʃ oː k o"]), - "شريف": ("שַרִיף", ["ʃ a r iː f"]), "هيثم": ("הַיְתַ'ם", ["h a j t a m", "h a j s a m"]), - "محمود": ("מַחְמוּד", ["m a ħ m uː d"]), "خليل": ("חַ'לִיל", ["x a l iː l"]), "خالد": ("חַ'אלֵד", ["x aː l i d"]), - "محمّد": ("מְחַמַّד", ["m ħ a m m a d", "m u ħ a m m a d"]), "نور": ("נוּר", ["n uː r"]), "داود": ("דַאוּד", ["d aː w uː d"]), - # the conversation (שיחת היכרות) - "السّلام": ("אֵ(ל)סַّלַאםֻ", ["i s s a l aː m u", "i s s a l aː m"]), "عليكم": ("עַלֵיכֹּם", ["ʕ a l eː k u m"]), - "مرحبا": ("מַרְחַבַּא", ["m a r ħ a b a"]), "كيف": ("כִּיף", ["k iː f"]), "مبسوط": ("מַבְּסוּט", ["m a b s uː tˤ"]), - "عايشين": ("עַאיְשִין", ["ʕ aː j ʃ iː n"]), "تشرّفنا": ("תְשַרַّפְנַא", ["t ʃ a r r a f n a"]), - "يزيدك": ("יִזִידַ-כּ", ["j i z iː d a k"]), "شرف": ("שַרַף", ["ʃ a r a f"]), - "قدّيش": ("קַדֵّיש", ["ʔ a d d eː ʃ", "q a d d eː ʃ"]), "حبيبي": ("חַבִּיבּ-י", ["ħ a b iː b i"]), "سنة": ("סַנֵה", ["s a n e"]), - "الله": ("אַללַّה", ["ʔ a l l a"]), "يطوّل": ("יְטַוֵّל", ["j tˤ a w w i l", "i tˤ a w w i l"]), - "يخلّيهم": ("יְחַ'לִّי-הֹםّ", ["j x a l l iː h o m", "i x a l l iː h o m"]), - "يحفظك": ("יִחְפַזֿ-כּ", ["j i ħ f a ðˤ a k", "j i ħ f a z a k"]), - "متجوّز": ("מִתְגַ'וֵّז", ["m i t dʒ a w w i z"]), "متجوّزة": ("מִתְגַ'וְّזֵה", ["m i t dʒ a w w z e"]), - "عزّابي": ("עַזַّאבִּי", ["ʕ a z z aː b i"]), "بتشتغل": ("בְּתִשְתְעֵ'ל", ["b t i ʃ t ɣ i l"]), - "محامي": ("מֻחַאמִי", ["m u ħ aː m i"]), "محاميّة": ("מֻחַאמִיֵّה", ["m u ħ aː m i j j e"]), - "جرسون": ("גַרְסוֹן", ["g a r s oː n", "dʒ a r s oː n"]), "حلو": ("חִלוּ", ["ħ i l u"]), "كتير": ("כְּתִיר", ["k t iː r"]), - "طيّب": ("טַיֵّבּ", ["tˤ a j j i b"]), "مستعجل": ("מִסְתַעְגֵ'ל", ["m i s t a ʕ dʒ i l"]), "شويّ": ("שְוַיّ", ["ʃ w a j j"]), - "عن": ("עַן", ["ʕ a n"]), "اذنك": ("אִזְנַ-כּ", ["ʔ i z n a k"]), "لازم": ("לַאזֵם", ["l aː z i m"]), "اروح": ("אַרוּח", ["ʔ a r uː ħ"]), - "انشالله": ("אִנְשַאללַّה", ["ʔ i n ʃ aː l l a"]), "نشوفك": ("נְשוּפַ-כּ", ["n ʃ uː f a k"]), - "السّلامة": ("אֵ(ל)סַّלַאמֵה", ["i s s a l aː m e"]), - # numbers - "تلاتة": ("תַלַאתֵה", ["t a l aː t e"]), "اربع": ("אַרְבַּע", ["ʔ a r b a ʕ"]), "خمسة": ("חַ'מְסֵה", ["x a m s e"]), - "ستّة": ("סִתֵّה", ["s i t t e"]), "تسعة": ("תִסְעַה", ["t i s ʕ a"]), "عشرين": ("עִשְרִין", ["ʕ i ʃ r iː n"]), - "تلاتين": ("תַלַאתִין", ["t a l aː t iː n"]), "اربعين": ("אַרְבַּעִין", ["ʔ a r b a ʕ iː n"]), - # ---- lesson 3 ---- - # verbs: past tense, then the future's base form - "دخل": ("דַחַ'ל", ["d a x a l"]), "دخلت": ("דַחַ'לֵת", ["d a x a l t"]), "دخلت#f": ("דַחְ'לַת", ["d a x l a t"]), - "دخلتي": ("דַחַ'לְתִי", ["d a x a l t i"]), "دخلنا": ("דַחַ'לְנַא", ["d a x a l n a"]), - "دخلتو": ("דַחַ'לְתוּ", ["d a x a l t u"]), "دخلو": ("דַחַ'לוּ", ["d a x a l u", "d a x l u"]), - "سبح": ("סַבַּח", ["s a b a ħ"]), "سبحت": ("סַבַּחֵת", ["s a b a ħ t"]), "سبحت#f": ("סַבְּחַת", ["s a b ħ a t"]), - "سبحتي": ("סַבַּחְתִי", ["s a b a ħ t i"]), "سبحنا": ("סַבַּחְנַא", ["s a b a ħ n a"]), - "سبحتو": ("סַבַּחְתוּ", ["s a b a ħ t u"]), "سبحو": ("סַבַּחוּ", ["s a b a ħ u", "s a b ħ u"]), - "طبخ": ("טַבַּח'", ["tˤ a b a x"]), "طبخت": ("טַבַּחֵ'ת", ["tˤ a b a x t"]), "طبخت#f": ("טַבְּחַ'ת", ["tˤ a b x a t"]), - "طبختي": ("טַבַּחְ'תִי", ["tˤ a b a x t i"]), "طبخنا": ("טַבַּחְ'נַא", ["tˤ a b a x n a"]), - "طبختو": ("טַבַּחְ'תוּ", ["tˤ a b a x t u"]), "طبخو": ("טַבַּח'וּ", ["tˤ a b a x u", "tˤ a b x u"]), - "نقل": ("נַקַל", ["n a ʔ a l", "n a q a l"]), "نقلت": ("נַקַלֵת", ["n a ʔ a l t", "n a q a l t"]), - "نقلت#f": ("נַקְלַת", ["n a ʔ l a t", "n a q l a t"]), "نقلتي": ("נַקַלְתִי", ["n a ʔ a l t i", "n a q a l t i"]), - "نقلنا": ("נַקַלְנַא", ["n a ʔ a l n a", "n a q a l n a"]), "نقلتو": ("נַקַלְתוּ", ["n a ʔ a l t u", "n a q a l t u"]), - "نقلو": ("נַקַלוּ", ["n a ʔ a l u", "n a q a l u"]), - "يدخل": ("יֻדְחֹ'ל", ["j u d x u l"]), "يسبح": ("יִסְבַּח", ["j i s b a ħ"]), "يطبخ": ("יֻטְבֹּח'", ["j u tˤ b u x"]), - "ينقل": ("יֻנְקֹל", ["j u n ʔ u l", "j u n q u l"]), - # ... with a suffix - "سألتني#f": ("סַאְלַת-נִי", ["s a ʔ l a t n i"]), "سأله": ("סַאַל-וֹ", ["s a ʔ a l o"]), - "سألوها": ("סַאַלוּ-הַא", ["s a ʔ a l uː h a"]), "فتحه": ("פַתַח-וֹ", ["f a t a ħ o"]), - "طبختله": ("טַבַּחְ'תִ-לّוֹ", ["tˤ a b a x t i l l o", "tˤ a b a x t l o"]), "طبختله#f": ("טַבְּחַ'ת-לוֹ", ["tˤ a b x a t l o"]), - "اشترت#f": ("אִשְתַרַת", ["ʔ i ʃ t a r a t"]), - # the numbers; ث is written and said ت, as in the lessons before - "صفر": ("צִפֵר", ["sˤ i f i r", "sˤ i f r"]), "تنين": ("תְנֵין", ["t n eː n"]), "اربعة": ("אַרְבַּעַה", ["ʔ a r b a ʕ a"]), - "سبعة": ("סַבְּעַה", ["s a b ʕ a"]), "تمانية": ("תַמַאנְיֵה", ["t a m aː n j e"]), "عشرة": ("עַשַרַה", ["ʕ a ʃ a r a"]), - "حدعش": ("חְדַעַש", ["ħ d a ʕ a ʃ"]), "تنعش": ("תְנַעַש", ["t n a ʕ a ʃ"]), - "تلاتعش": ("תַלַאתַעַש", ["t a l aː t a ʕ a ʃ", "t a l a t a ʕ a ʃ"]), "اربعتعش": ("אַרְבַּעְתַעַש", ["ʔ a r b a ʕ t a ʕ a ʃ"]), - "خمستعش": ("חַ'מֵסְתַעַש", ["x a m i s t a ʕ a ʃ", "x a m s t a ʕ a ʃ"]), "ستّعش": ("סִתַّעַש", ["s i t t a ʕ a ʃ"]), - "سبعتعش": ("סַבְּעְתַעַש", ["s a b ʕ t a ʕ a ʃ", "s a b a ʕ t a ʕ a ʃ"]), "تمنتعش": ("תַמַנְתַעַש", ["t a m a n t a ʕ a ʃ"]), - "تسعتعش": ("תִסְעַתַעַש", ["t i s ʕ a t a ʕ a ʃ", "t i s ʕ t a ʕ a ʃ"]), - "خمسين": ("חַ'מְסִין", ["x a m s iː n"]), "ستّين": ("סִתִّין", ["s i t t iː n"]), "سبعين": ("סַבְּעִין", ["s a b ʕ iː n"]), - "تمانين": ("תַמַאנִין", ["t a m aː n iː n"]), "تسعين": ("תִסְעִין", ["t i s ʕ iː n"]), - "ميّة": ("מִיֵّה", ["m i j j e"]), "ميّة#of": ("מִיֵّת", ["m i j j e t", "m iː t"]), "ميتين": ("מִיתֵין", ["m iː t eː n"]), - "الف": ("אַלְף", ["ʔ a l f", "ʔ a l i f"]), "الفين": ("אַלְפֵין", ["ʔ a l f eː n"]), "الاف": ("אַלַאף", ["ʔ a l aː f", "t a l aː f"]), - "عشرة#of": ("עַשְרַת", ["ʕ a ʃ r a t", "ʕ a ʃ a r t"]), "مليون": ("מַלְיוֹן", ["m a l j oː n", "m i l j oː n"]), - # ... before a noun: 3-10 lose their ة, 11-19 gain a ر - "تلات": ("תַלַאת", ["t a l a t", "t a l aː t", "t l a t"]), "خمس": ("חַ'מְס", ["x a m s", "x a m i s"]), - "سبع": ("סַבְּע", ["s a b ʕ", "s a b i ʕ"]), - "حدعشر": ("חְדַעְשַר", ["ħ d a ʕ ʃ a r", "ħ d a ʕ a ʃ a r"]), "تنعشر": ("תְנַעְשַר", ["t n a ʕ ʃ a r", "t n a ʕ a ʃ a r"]), - "سبعتعشر": ("סַבְּעְתַעְשַר", ["s a b ʕ t a ʕ ʃ a r", "s a b a ʕ t a ʕ ʃ a r", "s a b ʕ t a ʕ a ʃ a r"]), - "تمنتعشر": ("תַמַנְתַעְשַר", ["t a m a n t a ʕ ʃ a r", "t a m a n t a ʕ a ʃ a r"]), - "تسعتعشر": ("תִסְעַתַעְשַר", ["t i s ʕ a t a ʕ ʃ a r", "t i s ʕ t a ʕ ʃ a r", "t i s ʕ a t a ʕ a ʃ a r"]), - "التّاني": ("אֵ(ל)תַّאנִי", ["i t t aː n i"]), "التّنين": ("(א)לִתְנֵין", ["l i t n eː n", "i t t n eː n"]), - "رقم": ("רַקַם", ["r a ʔ a m", "r a q a m"]), "كيلو": ("כִּילוֹ", ["k iː l o"]), "شيكل": ("שֵיכֵּל", ["ʃ eː k e l"]), - # plurals and duals - "سيّارات": ("סַיַّארַאת", ["s a j j aː r aː t"]), "ابواب": ("אַבְּוַאבּ", ["ʔ a b w aː b"]), - "شبابيك": ("שַבַּאבִּיכּ", ["ʃ a b aː b iː k"]), "شبّاكين": ("שֻבַּّאכֵּין", ["ʃ u b b aː k eː n"]), - "كراسي": ("כַּרַאסִי", ["k a r aː s i"]), "حارات": ("חַארַאת", ["ħ aː r aː t"]), "بيوت": ("בְּיוּת", ["b j uː t"]), - "بنات": ("בַּנַאת", ["b a n aː t"]), "احفاد": ("אַחְפַאד", ["ʔ a ħ f aː d"]), "حفيدات": ("חַפִידַאת", ["ħ a f iː d aː t"]), - "اخوة": ("אִחְ'וֵה", ["ʔ i x w e"]), "اختين": ("אֹחְ'תֵין", ["ʔ u x t eː n"]), - "بطّيختين": ("בַּטִّיחְ'תֵין", ["b a tˤ tˤ iː x t eː n"]), - "سنتين": ("סַנְתֵין", ["s a n t eː n"]), "ساعة": ("סֵיעַה", ["s eː ʕ a", "s aː ʕ a"]), - "ساعتين": ("סַאעַתֵין", ["s aː ʕ a t eː n", "s aː ʕ t eː n"]), - # the sentences' other words - "لهون": ("לַהוֹן", ["l a h oː n"]), "نفس": ("נַפְס", ["n a f s", "n a f i s"]), "اه": ("אַה", ["ʔ aː", "ʔ a h"]), - "امبارح": ("אִמְבַּארֵח", ["ʔ i m b aː r i ħ"]), "برّا": ("בַּרַّא", ["b a r r a"]), "كلّنا": ("כֻּלְّנַא", ["k u l l n a"]), - "يمّا": ("יַאמַّא", ["j a m m a"]), "ماما": ("מַאמַא", ["m aː m a"]), "يلّا": ("יַאללַّה", ["j a l l a"]), - "تعالي": ("תַעַאלִי", ["t a ʕ aː l i"]), - "جديد": ("גְ'דִיד", ["dʒ d iː d"]), "جديدة": ("גְ'דִידֵה", ["dʒ d iː d e"]), "طيّبة": ("טַיְّבֵּה", ["tˤ a j j b e"]), - "بعيدة": ("בְּעִידֵה", ["b ʕ iː d e"]), - "شبّاك": ("שֻבַּّאכּ", ["ʃ u b b aː k"]), "غرفة": ("עֻ'רְפֵה", ["ɣ u r f e"]), "البركة": ("אֵלְבִּרְכֵּה", ["i l b i r k e"]), - "المدينة": ("אֵלְמַדִינֵה", ["i l m a d iː n e"]), "الجرسون": ("אֵלְגַרְסוֹן", ["i l g a r s oː n", "i l dʒ a r s oː n"]), - "شكشوكة": ("שַכְּשוּכֵּה", ["ʃ a k ʃ uː k e"]), "منسف": ("מַנְסַף", ["m a n s a f"]), "عجّة": ("עִגֵّ'ה", ["ʕ i dʒ dʒ e"]), - "تفّاح": ("תֻפַّאח", ["t u f f aː ħ"]), "التّفّاح": ("אֵ(ל)תֻّפַّאח", ["i t t u f f aː ħ"]), - "بطاطا": ("בַּטַאטַה", ["b a tˤ aː tˤ a"]), "الفلافل": ("אֵלְפַלַאפֵל", ["i l f a l aː f i l"]), "كولا": ("כּוֹלַה", ["k oː l a"]), - "لاختك": ("לַאֹחְ'תַ-כּ", ["l a ʔ u x t a k"]), "لحفيدك#f": ("לַחַפִידֵ-כּ", ["l a ħ a f iː d i k"]), - "لابن": ("לַאִבֵּן", ["l a ʔ i b i n", "l a ʔ i b n"]), - "لحم": ("לַחֵם", ["l a ħ i m", "l a ħ m"]), "جبل": ("גַ'בֵּל", ["dʒ a b a l", "dʒ a b i l"]), - "المكبّر": ("אֵלְמֻכַּבֵּّר", ["i l m u k a b b i r"]), - "سمير": ("סַמִיר", ["s a m iː r"]), "سامي": ("סַאמִי", ["s aː m i"]), "سليم": ("סַלִים", ["s a l iː m"]), - "ابراهيم": ("אִבְּרַאהִים", ["ʔ i b r aː h iː m"]), "بيان": ("בַּיַאן", ["b a j aː n"]), - # ---- lesson 4 ---- - # verbs that end in ا: past tense, then the future's base form - "اجا": ("אַגַ'א", ["ʔ a dʒ a"]), "اجيت": ("אַגֵ'ית", ["ʔ a dʒ eː t"]), "اجيتي": ("אַגֵ'יתִי", ["ʔ a dʒ eː t i"]), - "اجيتو": ("אַגֵ'יתוּ", ["ʔ a dʒ eː t u"]), "اجينا": ("אַגֵ'ינַא", ["ʔ a dʒ eː n a"]), "اجت": ("אַגַ'ת", ["ʔ a dʒ a t"]), - "اجو": ("אַג'וּ", ["ʔ a dʒ u"]), "يجي": ("יִגִ'י", ["j iː dʒ i", "j i dʒ i"]), - "حكى": ("חַכַּא", ["ħ a k a"]), "حكيت": ("חַכֵּית", ["ħ a k eː t"]), "حكيتي": ("חַכֵּיתִי", ["ħ a k eː t i"]), - "حكيتو": ("חַכֵּיתוּ", ["ħ a k eː t u"]), "حكينا": ("חַכֵּינַא", ["ħ a k eː n a"]), "حكت": ("חַכַּת", ["ħ a k a t"]), - "حكو": ("חַכּוּ", ["ħ a k u"]), "يحكي": ("יִחְכִּי", ["j i ħ k i"]), - "اعطى": ("אַעְטַא", ["ʔ a ʕ tˤ a"]), "اعطيت": ("אַעַטֵית", ["ʔ a ʕ tˤ eː t"]), "اعطيتي": ("אַעַטֵיתִי", ["ʔ a ʕ tˤ eː t i"]), - "اعطيتو": ("אַעַטֵיתוּ", ["ʔ a ʕ tˤ eː t u"]), "اعطينا": ("אַעַטֵינַא", ["ʔ a ʕ tˤ eː n a"]), "اعطت": ("אַעַטַת", ["ʔ a ʕ tˤ a t"]), - "اعطو": ("אַעְטוּ", ["ʔ a ʕ tˤ u"]), "يعطي": ("יֵעַטִי", ["j a ʕ tˤ i", "j i ʕ tˤ i"]), - "اشترى": ("אִשְתַרַא", ["ʔ i ʃ t a r a"]), "اشتريت": ("אִשְתַרֵית", ["ʔ i ʃ t a r eː t"]), - "اشتريتي": ("אִשְתַרֵיתִי", ["ʔ i ʃ t a r eː t i"]), "اشتريتو": ("אִשְתַרֵיתוּ", ["ʔ i ʃ t a r eː t u"]), - "اشترينا": ("אִשְתַרֵינַא", ["ʔ i ʃ t a r eː n a"]), "اشترو": ("אִשְתַרוּ", ["ʔ i ʃ t a r u"]), - "يشتري": ("יִשְתְרִי", ["j i ʃ t r i", "j i ʃ t i r i"]), - # ... with a suffix - "اجاني": ("אַגַ'א-נִי", ["ʔ a dʒ aː n i"]), "حكالي": ("חַכַּא-לִי", ["ħ a k aː l i"]), "حكالها": ("חַכַּא-לְהַא", ["ħ a k aː l h a"]), - "حكتلي": ("חַכַּת-לִי", ["ħ a k a t l i"]), "حكيتلك": ("חַכֵּית-לַכּ", ["ħ a k eː t l a k"]), - "اعطاني": ("אַעַטַא-נִי", ["ʔ a ʕ tˤ aː n i"]), "اعطاك": ("אַעַטַא-כּ", ["ʔ a ʕ tˤ aː k"]), - "اعطانا": ("אַעַטַא-נַא", ["ʔ a ʕ tˤ aː n a"]), "اعطاهم": ("אַעַטַא-הֹםّ", ["ʔ a ʕ tˤ aː h o m"]), - "اعطيته": ("אַעַטֵית-וֹ", ["ʔ a ʕ tˤ eː t o"]), "اعطيتيني": ("אַעַטֵיתִי-נִי", ["ʔ a ʕ tˤ eː t iː n i"]), - "اشتريتها": ("אִשְתַרֵית-הַא", ["ʔ i ʃ t a r eː t h a"]), - "قلتله": ("קֻלְתִ-לّוֹ", ["ʔ u l t i l l o", "q u l t i l l o"]), "جابلنا": ("גַ'אבּ-לְנַא", ["dʒ aː b l n a", "dʒ aː b i l n a"]), - "اجيبله": ("אַגִ'יבּ-לוֹ", ["ʔ a dʒ iː b l o"]), - # the story's verbs, met before they are taught - "قال": ("קַאל", ["ʔ aː l", "q aː l"]), "قالت": ("קַאלַת", ["ʔ aː l a t", "q aː l a t"]), - "قلت": ("קֻלֵת", ["ʔ u l t", "q u l t", "ʔ u l i t"]), "رحت": ("רֻחֵת", ["r u ħ t", "r u ħ i t"]), - "اروح": ("אַרוּח", ["ʔ a r uː ħ"]), "روح": ("רוּח", ["r uː ħ"]), "شوف": ("שוּף", ["ʃ uː f"]), - "تعال": ("תַעַאל", ["t a ʕ aː l"]), "بدّي": ("בִּדّ-י", ["b i d d i"]), "بدّك": ("בִּדַّ-כּ", ["b i d d a k"]), - "سمحت": ("סַמַחֵת", ["s a m a ħ t"]), "سمحتي": ("סַמַחְתִי", ["s a m a ħ t i"]), - "يبارك": ("יְבַּארֵכּ", ["j b aː r i k", "i b aː r i k"]), - # ل on a word - "لي": ("לִי", ["l i"]), "لعند": ("לַעִנְד", ["l a ʕ i n d"]), "لعندي": ("לַעִנְד-י", ["l a ʕ i n d i"]), - "لعنده": ("לַעִנְד-וֹ", ["l a ʕ i n d o"]), "لمرتي": ("לַמַרְת-י", ["l a m a r t i"]), - "لامّي": ("לַאֻמّ-י", ["l a ʔ u m m i", "l a ʔ i m m i"]), "لابوي": ("לַאַבּוּי", ["l a ʔ a b uː j"]), - "لاولاده": ("לַאַוְלַאד-וֹ", ["l a ʔ a w l aː d o"]), "لصاحبي": ("לַצַאחְבּ-י", ["l a sˤ aː ħ b i"]), - "لصاحبتي": ("לַצַאחִבְּת-י", ["l a sˤ aː ħ i b t i"]), "للدّكتور": ("לַ(ל)דַّכְּתוֹר", ["l a d d a k t oː r", "l a d d u k t oː r"]), - # the sentences' other words - "انّه": ("אִנّוֹ", ["ʔ i n n o"]), "لمّا": ("לַמַّא", ["l a m m a"]), "مش": ("מִש", ["m i ʃ", "m u ʃ"]), - "اكم": ("אַכַּם", ["ʔ a k a m"]), "لو": ("לַוְ", ["l a w", "l o"]), "ايوه": ("אַיְוַה", ["ʔ a j w a"]), - "منّه": ("מִנّוֹ", ["m i n n o"]), "فيك": ("פִיכּ", ["f iː k"]), "بدون": ("בִּדוּן", ["b i d uː n"]), - "تاني": ("תַאנִי", ["t aː n i"]), "كلّ": ("כֻּלّ", ["k u l l"]), "كلّهم": ("כֻּלְّהֹםّ", ["k u l l h o m"]), - "اشي": ("אִשִי", ["ʔ i ʃ i"]), "شكرا": ("שֻכְּרַן", ["ʃ u k r a n"]), "مبروك": ("מַבְּרוּכּ", ["m a b r uː k"]), - "اهلا": ("אַהְלַא", ["ʔ a h l a"]), "سهلا": ("סַהְלַא", ["s a h l a"]), "اهلين": ("אַהְלֵין", ["ʔ a h l eː n"]), - "تمام": ("תַמַאם", ["t a m aː m"]), - "بعيد": ("בְּעִיד", ["b ʕ iː d"]), "قريب": ("קַרִיבּ", ["ʔ a r iː b", "q a r iː b"]), - "صغير": ("צְעִ'יר", ["z ɣ iː r", "sˤ ɣ iː r"]), "حلوة": ("חִלְוֵה", ["ħ i l w e"]), - "جوعان": ("ג'וֹעַאן", ["dʒ oː ʕ aː n"]), "جوعانة": ("ג'וֹעַאנֵה", ["dʒ oː ʕ aː n e"]), "تعبان": ("תַעְבַּאן", ["t a ʕ b aː n"]), - "خاطب": ("חַ'אטֵבּ", ["x aː tˤ i b"]), "خطيبتي": ("חַ'טִיבְּת-י", ["x a tˤ iː b t i"]), - "جار": ("גַ'אר", ["dʒ aː r"]), "دار": ("דַאר", ["d aː r"]), "مطعم": ("מַטְעַם", ["m a tˤ ʕ a m"]), "كرسي": ("כֻּרְסִי", ["k u r s i"]), - "السّوق": ("אֵ(ל)סّוּק", ["i s s uː ʔ", "i s s uː q"]), "البيّاع": ("אֵלְבַּיַّאע", ["i l b a j j aː ʕ"]), - "الاولاد": ("אֵלְאַוְלַאד", ["i l ʔ a w l aː d"]), "اولاده": ("אַוְלַאד-וֹ", ["ʔ a w l aː d o"]), - "بركة": ("בִּרְכֵּה", ["b i r k e"]), "بركة#of": ("בִּרְכֵּת", ["b i r k e t", "b i r k i t"]), - "السّباحة": ("אֵ(ל)סְّבַּאחַה", ["i s s b aː ħ a"]), - "البلوزة": ("(א)לִבְּלוּזֵה", ["l i b l uː z e", "i l b l uː z e"]), "البندورة": ("אֵלְבַּנְדוֹרַה", ["i l b a n d oː r a"]), - "البطّيخة": ("אֵלְבַּטִّיחַ'ה", ["i l b a tˤ tˤ iː x a"]), "كعكة": ("כַּעְכֵּה", ["k a ʕ k e"]), - "قهوة": ("קַהְוֵה", ["ʔ a h w e", "q a h w e"]), "شاي": ("שַאיְ", ["ʃ aː j"]), "سكّر": ("סֻכַּّר", ["s u k k a r"]), - "سنين": ("סְנִין", ["s n iː n"]), - "خمستعشر": ("חַ'מֵסְתַעְשַר", ["x a m i s t a ʕ ʃ a r", "x a m s t a ʕ ʃ a r", "x a m i s t a ʕ a ʃ a r"]), - "اربعتعشر": ("אַרְבַּעְתַעְשַר", ["ʔ a r b a ʕ t a ʕ ʃ a r", "ʔ a r b a ʕ t a ʕ a ʃ a r"]), - "رام": ("רַאם", ["r aː m"]), "القواسمة": ("אֵלְקַוַאסְמֵה", ["i l ʔ a w aː s m e", "i l q a w aː s m e"]), - "راغب": ("רַאעֵ'בּ", ["r aː ɣ i b"]), "عبدالله": ("עַבְּדַאללַّה", ["ʕ a b d a l l a"]), "عطالله": ("עַטַאללַّה", ["ʕ a tˤ a l l a"]), - "حمدان": ("חַמְדַאן", ["ħ a m d aː n"]), "يوسف": ("יוּסֵף", ["j uː s i f"]), - # ---- lesson 5 ---- - # verbs of גזרת ע"ו: past tense (رحت, قلت, قال and قالت are the story's, above), then the future's base form - "راح": ("רַאח", ["r aː ħ"]), "رحتي": ("רֻחְתִי", ["r u ħ t i"]), "رحتو": ("רֻחְתוּ", ["r u ħ t u"]), - "رحنا": ("רֻחְנַא", ["r u ħ n a"]), "راحت": ("רַאחַת", ["r aː ħ a t"]), "راحو": ("רַאחוּ", ["r aː ħ u"]), - "يروح": ("יִרוּח", ["j r uː ħ", "i r uː ħ", "j i r uː ħ"]), - "قلتي": ("קֻלְתִי", ["ʔ u l t i", "q u l t i"]), "قلتو": ("קֻלְתוּ", ["ʔ u l t u", "q u l t u"]), - "قلنا": ("קֻלְנַא", ["ʔ u l n a", "q u l n a"]), "قالو": ("קַאלוּ", ["ʔ aː l u", "q aː l u"]), - "يقول": ("יִקוּל", ["j ʔ uː l", "i ʔ uː l", "j q uː l", "j i ʔ uː l"]), - "زار": ("זַאר", ["z aː r"]), "زرت": ("זֻרֵת", ["z u r t", "z u r i t"]), "زرتي": ("זֻרְתִי", ["z u r t i"]), - "زرتو": ("זֻרְתוּ", ["z u r t u"]), "زرنا": ("זֻרְנַא", ["z u r n a"]), "زارت": ("זַארַת", ["z aː r a t"]), - "زارو": ("זַארוּ", ["z aː r u"]), "يزور": ("יִזוּר", ["j z uː r", "i z uː r", "j i z uː r"]), - "كان": ("כַּאן", ["k aː n"]), "كنت": ("כֻּנֵת", ["k u n t", "k u n i t"]), "كنتي": ("כֻּנְתִי", ["k u n t i"]), - "كنتو": ("כֻּנְתוּ", ["k u n t u"]), "كنّا": ("כֻּנַّא", ["k u n n a"]), "كانت": ("כַּאנַת", ["k aː n a t"]), - "كانو": ("כַּאנוּ", ["k aː n u"]), "يكون": ("יִכּוּן", ["j k uː n", "i k uː n", "j i k uː n"]), - # ... with a suffix - "قالّي": ("קַאלִّ-י", ["ʔ aː l l i", "q aː l l i"]), "قالّه": ("קַאלّ-וֹ", ["ʔ aː l l o", "q aː l l o"]), - "قالّك#f": ("קַאלֵّ-כּ", ["ʔ aː l l i k", "q aː l l i k"]), "قالنا": ("קַאלְ-נַא", ["ʔ aː l n a", "q aː l n a"]), - "قالتلي": ("קַאלַת-לִי", ["ʔ aː l a t l i", "q aː l a t l i"]), "قلتولي": ("קֻלְתוּ-לִי", ["ʔ u l t uː l i", "q u l t uː l i"]), - "قلتيلي": ("קֻלְתִי-לִّי", ["ʔ u l t iː l i", "q u l t iː l i"]), - "زارتني": ("זַארַת-נִי", ["z aː r a t n i"]), "زارتنا": ("זַארַת-נַא", ["z aː r a t n a"]), - "زرناها": ("זֻרְנַא-הַא", ["z u r n aː h a"]), "زرتها": ("זֻרְת-הַא", ["z u r t h a"]), "زرتك": ("זֻרְתַ-כּ", ["z u r t a k"]), - "زرتك#f": ("זֻרְתֵ-כּ", ["z u r t i k"]), "زرتوني": ("זֻרְתוּ-נִי", ["z u r t uː n i"]), - "اعطتني": ("אַעְטַת-נִי", ["ʔ a ʕ tˤ a t n i"]), "اعطيني": ("אַעַטִי-נִי", ["ʔ a ʕ tˤ iː n i"]), - "يعطيك": ("יַעְטִי-כּ", ["j a ʕ tˤ iː k"]), "يعفيك": ("יֵעְפִי-כּ", ["j i ʕ f iː k", "j ʕ aː f iː k"]), - "اشترالك": ("אִשְתַרַא-לַכּ", ["ʔ i ʃ t a r aː l a k"]), "اشترالك#f": ("אִשְתַרַא-לֵכּ", ["ʔ i ʃ t a r aː l i k"]), - "اشترالي": ("אִשְתַרַא-לִי", ["ʔ i ʃ t a r aː l i"]), "حكيتلك#f": ("חַכֵּית-לֵכּ", ["ħ a k eː t l i k"]), - "حكيتلها": ("חַכֵּית-לְהַא", ["ħ a k eː t i l h a", "ħ a k eː t l h a"]), "اجيبلها": ("אַגִ'יבּ-לְהַא", ["ʔ a dʒ iː b l h a", "ʔ a dʒ iː b i l h a"]), - "جابتلنا": ("גַ'אבַּת-לְנַא", ["dʒ aː b a t l n a", "dʒ aː b a t i l n a"]), - "روحو": ("רוּחוּ", ["r uː ħ u"]), "وصلنا": ("וְצִלְנַא", ["w sˤ i l n a", "u sˤ i l n a"]), "تفضّل": ("תְפַדַֿّל", ["t f a dˤ dˤ a l"]), - "بدّه": ("בִּדّ-וֹ", ["b i d d o"]), - # the negation: ما before a verb, and a ش after it - "ما": ("מַא", ["m a"]), - "سألتكش": ("סַאַלְתַ-כְּש", ["s a ʔ a l t a k ʃ"]), "سألتنيش": ("סַאְלַת-נִיש", ["s a ʔ l a t n iː ʃ"]), - "دخلتوش": ("דַחַ'לְתוּש", ["d a x a l t uː ʃ"]), "قعدتش": ("קַעַדְתֵש", ["ʔ a ʕ a d t i ʃ", "q a ʕ a d t i ʃ"]), - "نقلتش": ("נַקַלְתֵש", ["n a ʔ a l t i ʃ", "n a q a l t i ʃ"]), - # ل, ب and ع on a word - "لاخوي": ("לַאַח'וּי", ["l a ʔ a x uː j"]), "لابنه": ("לַאִבְּנ-וֹ", ["l a ʔ i b n o"]), "لخالي": ("לַחַ'אל-י", ["l a x aː l i"]), - "لصاحبه": ("לַצַאחְבּ-וֹ", ["l a sˤ aː ħ b o"]), "لصاحبها": ("לַצַאחֵבּ-הַא", ["l a sˤ aː ħ i b h a"]), - "لوين": ("לַוֵין", ["l a w eː n"]), "لقدّيش": ("לַקַדֵّיש", ["l a ʔ a d d eː ʃ", "l a q a d d eː ʃ"]), - "لاسبوع": ("לַאֻסְבּוּע", ["l a ʔ u s b uː ʕ"]), "للكيلو": ("לַלְכִּילוֹ", ["l a l k iː l o"]), - "بالباص": ("בִּלְבַּאץ", ["b i l b aː sˤ"]), "عالسّوق": ("עַ(ל)סّוּק", ["ʕ a s s uː ʔ", "ʕ a s s uː q"]), - # the sentences' other words - "بعد": ("בַּעֵד", ["b a ʕ i d", "b a ʕ d"]), "بعض": ("בַּעַדֿ", ["b a ʕ a dˤ", "b a ʕ dˤ"]), "حوالي": ("חַוַאלִי", ["ħ a w aː l i"]), - "عشان": ("עַשַאן", ["ʕ a ʃ aː n"]), "والله": ("וַאללַّה", ["w a l l a"]), "بكرا": ("בֻּכְּרַא", ["b u k r a"]), - "هلّا": ("הַלַّא", ["h a l l a", "h a l l a ʔ", "ʔ i s s a", "h a l ʔ eː t"]), "هناك": ("הֻנַאכּ", ["h u n aː k", "h oː n aː k"]), - "اوّل": ("אַוַّל", ["ʔ a w w a l"]), "مرّة": ("מַרַّה", ["m a r r a"]), "مرّات": ("מַרַّאת", ["m a r r aː t"]), - "وقت": ("וַקֵת", ["w a ʔ i t", "w a ʔ t", "w a q i t", "w a q t"]), "ليلة": ("לֵילֵה", ["l eː l e"]), - "شهر": ("שַהֵר", ["ʃ a h i r", "ʃ a h r"]), "ساعات": ("סַאעַאת", ["s aː ʕ aː t"]), "متر": ("מֵתֵר", ["m i t i r", "m i t r"]), - "نصّ": ("נֻצّ", ["n u sˤ sˤ"]), "شويّة#of": ("שְוַיֵّת", ["ʃ w a j j e t", "ʃ w a j j i t"]), - "تانية": ("תַאנְיֵה", ["t aː n j e"]), "فضلك": ("פַדְֿלַ-כּ", ["f a dˤ l a k"]), "وحياتك": ("וִחְיַאתַ-כּ", ["w i ħ j aː t a k", "u ħ j aː t a k"]), - "جاي": ("גַ'איְ", ["dʒ aː j"]), "بالي": ("בַּאל-י", ["b aː l i"]), "حقّ": ("חַקّ", ["ħ a ʔ ʔ", "ħ a q q"]), - "مبسوطة": ("מַבְּסוּטַה", ["m a b s uː tˤ a"]), "تعبانة": ("תַעְבַּאנֵה", ["t a ʕ b aː n e"]), - "تعبانين": ("תַעְבַּאנִין", ["t a ʕ b aː n iː n"]), "حبيبتي": ("חַבִּיבְּת-י", ["ħ a b iː b t i"]), - "ابوكي": ("אַבּוּכִּי", ["ʔ a b uː k i"]), "اخوكي": ("אַח'וּכִּי", ["ʔ a x uː k i"]), "اولادهم": ("אַוְלַאד-הֹםّ", ["ʔ a w l aː d h o m"]), - "الاب": ("אֵלְאַבּ", ["i l ʔ a b"]), "العيلة": ("אֵלְעֵילֵה", ["i l ʕ eː l e"]), "هديّة": ("הַדִיֵّה", ["h a d i j j e"]), - "الفنجان": ("אֵלְפִנְגַ'אן", ["i l f i n dʒ aː n"]), "المفتاح": ("אֵלְמֻפְתַאח", ["i l m u f t aː ħ", "i l m i f t aː ħ"]), - "المستشفى": ("אֵלְמֻסְתַשְפַא", ["i l m u s t a ʃ f a"]), "البستان": ("(א)לְבֻּסְתַאן", ["i l b u s t aː n", "l i b u s t aː n"]), - "السّوبرماركت": ("אֵ(ל)סּוּבֵּרְמַרְכֵּת", ["i s s uː b e r m a r k e t"]), "الباص": ("אֵלְבַּאץ", ["i l b aː sˤ"]), - "محلّ": ("מַחַלّ", ["m a ħ a l l"]), "حلويات": ("חִלְוַיַאת", ["ħ i l w a j aː t", "ħ a l a w i j aː t"]), - "الحلويات": ("אֵלְחִלְוַיַאת", ["i l ħ i l w a j aː t", "i l ħ a l a w i j aː t"]), - "قصّة#of": ("קֹצַّת", ["ʔ i sˤ sˤ i t", "q i sˤ sˤ a t", "ʔ u sˤ sˤ a t", "q u sˤ sˤ a t"]), - "خضرة": ("חֻ'דְֿרַה", ["x u dˤ r a"]), "فواكه": ("פַוַאכֵּה", ["f a w aː k e", "f a w aː k i h"]), - "الخيار": ("(א)לִחְ'יַאר", ["l i x j aː r", "i l x j aː r"]), "الكنافة": ("אֵלְכְּנַאפֵה", ["i l k n aː f e", "l i k n aː f e"]), - "حمّص": ("חֻמֹّצ", ["ħ u m m u sˤ"]), "معكرونة": ("מַעְכַּרוֹנֵה", ["m a ʕ k a r oː n e"]), "العافية": ("אֵלְעַאפְיֵה", ["i l ʕ aː f j e"]), - "باريس": ("בַּארִיס", ["b aː r iː s"]), "مكّة": ("מַכֵّה", ["m a k k e"]), "رمضان": ("רַמַדַֿאן", ["r a m a dˤ aː n"]), - "بير": ("בִּיר", ["b iː r"]), "السّبع": ("אֵ(ל)סַّבֵּע", ["i s s a b i ʕ", "i s s a b ʕ"]), - "نبيل": ("נַבִּיל", ["n a b iː l"]), "ياسمين": ("יַאסְמִין", ["j aː s m iː n"]), "عاطف": ("עַאטֵף", ["ʕ aː tˤ i f"]), - "عدنان": ("עַדְנַאן", ["ʕ a d n aː n"]), "ايمن": ("אַיְמַן", ["ʔ a j m a n"]), "خديجة": ("חַ'דִיגֵ'ה", ["x a d iː dʒ e"]), - # ---- lesson 6 ---- - # the future of three verbs taught before, a form for each pronoun - "اسمع#i": ("אַסְמַע", ["ʔ a s m a ʕ"]), "تسمع": ("תִסְמַע", ["t i s m a ʕ"]), "تسمعي": ("תִסְמַעִי", ["t i s m a ʕ i"]), - "يسمع": ("יִסְמַע", ["j i s m a ʕ"]), "نسمع": ("נִסְמַע", ["n i s m a ʕ"]), "تسمعو": ("תִסְמַעוּ", ["t i s m a ʕ u"]), - "يسمعو": ("יִסְמַעוּ", ["j i s m a ʕ u"]), - "اقعد": ("אַקְעֹד", ["ʔ a ʔ ʕ u d", "ʔ a q ʕ u d"]), "تقعد": ("תֻקְעֹד", ["t u ʔ ʕ u d", "t u q ʕ u d"]), - "تقعدي": ("תֻקְעֹדִי", ["t u ʔ ʕ u d i", "t u q ʕ u d i"]), "نقعد": ("נֻקְעֹד", ["n u ʔ ʕ u d", "n u q ʕ u d"]), - "تقعدو": ("תֻקְעֹדוּ", ["t u ʔ ʕ u d u", "t u q ʕ u d u"]), "يقعدو": ("יֻקְעֹדוּ", ["j u ʔ ʕ u d u", "j u q ʕ u d u"]), - "ازور": ("אַזוּר", ["ʔ a z uː r"]), "تزور": ("תְזוּר", ["t z uː r", "d z uː r"]), "تزوري": ("תְזוּרִי", ["t z uː r i", "d z uː r i"]), - "نزور": ("נְזוּר", ["n z uː r"]), "تزورو": ("תְזוּרוּ", ["t z uː r u", "d z uː r u"]), - "يزورو": ("יִזוּרוּ", ["j z uː r u", "i z uː r u", "j i z uː r u"]), - # ... and of others, as the sentences need them - "ادخل": ("אַדְחֹ'ל", ["ʔ a d x u l"]), "تروح": ("תְרוּח", ["t r uː ħ"]), "نسبح": ("נִסְבַּח", ["n i s b a ħ"]), - "تسبح": ("תִסְבַּח", ["t i s b a ħ"]), "تسألي": ("תִסְאַלִי", ["t i s ʔ a l i"]), "احكي": ("אַחְכִּי", ["ʔ a ħ k i"]), - "ناكل": ("נֹאכֹּל", ["n oː k u l", "n oː k i l", "n aː k u l"]), "يجو": ("יִיג'וּ", ["j iː dʒ u", "j i dʒ u"]), - "اشتري": ("אַשְתְרִי", ["ʔ a ʃ t r i", "ʔ a ʃ t i r i"]), "اعطيك": ("אַעַטִי-כּ", ["ʔ a ʕ tˤ iː k"]), - "تزوريني": ("תְזוּרִי-נִי", ["t z uː r iː n i", "d z uː r iː n i"]), - # the present of "I" and of "he", in the dialogue: بـ before the future - "برجع": ("בַּרְגַ'ע", ["b a r dʒ a ʕ"]), "بياخد": ("בֹּאחֵ'ד", ["b oː x i d", "b j aː x u d", "b j oː x i d"]), - # verbs: more of גזרת ע"ו and اخد, past tense, then the future's base form - "باس": ("בַּאס", ["b aː s"]), "بست": ("בֻּסֵת", ["b u s t", "b u s i t"]), "بستي": ("בֻּסְתִי", ["b u s t i"]), - "بستو": ("בֻּסְתוּ", ["b u s t u"]), "بسنا": ("בֻּסְנַא", ["b u s n a"]), "باست": ("בַּאסַת", ["b aː s a t"]), - "باسو": ("בַּאסוּ", ["b aː s u"]), "يبوس": ("יִבּוּס", ["j b uː s", "i b uː s", "j i b uː s"]), - "ساق": ("סַאק", ["s aː ʔ", "s aː q"]), "سقت": ("סֻקֵת", ["s u ʔ t", "s u q t", "s u ʔ i t"]), "سقتي": ("סֻקְתִי", ["s u ʔ t i", "s u q t i"]), - "سقتو": ("סֻקְתוּ", ["s u ʔ t u", "s u q t u"]), "سقنا": ("סֻקְנַא", ["s u ʔ n a", "s u q n a"]), "ساقت": ("סַאקַת", ["s aː ʔ a t", "s aː q a t"]), - "ساقو": ("סַאקוּ", ["s aː ʔ u", "s aː q u"]), "يسوق": ("יִסוּק", ["j s uː ʔ", "j s uː q", "i s uː ʔ", "j i s uː ʔ"]), - "شاف": ("שַאף", ["ʃ aː f"]), "شفت": ("שֻפֵת", ["ʃ u f t", "ʃ u f i t"]), "شفتي": ("שֻפְתִי", ["ʃ u f t i"]), - "شفتو": ("שֻפְתוּ", ["ʃ u f t u"]), "شفنا": ("שֻפְנַא", ["ʃ u f n a"]), "شافت": ("שַאפַת", ["ʃ aː f a t"]), - "شافو": ("שַאפוּ", ["ʃ aː f u"]), "يشوف": ("יִשוּף", ["j ʃ uː f", "i ʃ uː f", "j i ʃ uː f"]), - "فات": ("פַאת", ["f aː t"]), "فتت": ("פֻתֵת", ["f u t t", "f u t i t"]), "فتّي": ("פֻתִّי", ["f u t t i"]), - "فتّو": ("פֻתّוּ", ["f u t t u"]), "فتنا": ("פֻתְנַא", ["f u t n a"]), "فاتت": ("פַאתַת", ["f aː t a t"]), - "فاتو": ("פַאתוּ", ["f aː t u"]), "يفوت": ("יִפוּת", ["j f uː t", "i f uː t", "j i f uː t"]), - "اخد": ("אַחַ'ד", ["ʔ a x a d"]), "اخدت": ("אַחַ'דֵת", ["ʔ a x a d t"]), "اخدت#f": ("אַחְ'דַת", ["ʔ a x d a t"]), - "اخدتي": ("אַחַ'דְתִי", ["ʔ a x a d t i"]), "اخدنا": ("אַחַ'דְנַא", ["ʔ a x a d n a"]), "اخدتو": ("אַחַ'דְתוּ", ["ʔ a x a d t u"]), - "اخدو": ("אַחַ'דוּ", ["ʔ a x a d u", "ʔ a x d u"]), "ياخد": ("יֹאחֵ'ד", ["j oː x i d", "j aː x u d", "j oː x u d"]), - # ... with a suffix, or negated with ش - "ساقها": ("סַאק-הַא", ["s aː ʔ h a", "s aː q h a"]), "باستنا": ("בַּאסַת-נַא", ["b aː s a t n a"]), - "بسناها": ("בֻּסְנַא-הַא", ["b u s n aː h a"]), "شفتيني": ("שֻפְתִי-נִי", ["ʃ u f t iː n i"]), - "زاروني": ("זַארוּ-נִי", ["z aː r uː n i"]), "زارني": ("זַאר-נִי", ["z aː r n i"]), "زارته": ("זַארַת-וֹ", ["z aː r a t o"]), - "اعطيتك": ("אַעַטֵיתַ-כּ", ["ʔ a ʕ tˤ eː t a k"]), "اعطيتني": ("אַעַטֵית-נִי", ["ʔ a ʕ tˤ eː t n i"]), - "اعطيتها": ("אַעַטֵית-הַא", ["ʔ a ʕ tˤ eː t h a"]), "جابتله": ("גַ'אבַּת-לוֹ", ["dʒ aː b a t l o"]), - "قلتلك": ("קֻלְתִ-לַّכּ", ["ʔ u l t i l l a k", "q u l t i l l a k"]), - "شفتش": ("שֻפְתֵש", ["ʃ u f t i ʃ"]), "شفتهاش": ("שֻפְת-הַאש", ["ʃ u f t h aː ʃ"]), "شافونيش": ("שַאפוּ-נִיש", ["ʃ aː f uː n iː ʃ"]), - "طبختش": ("טַבַּחְ'תֵש", ["tˤ a b a x t i ʃ"]), "كنتش": ("כֻּנְתֵש", ["k u n t i ʃ"]), "رحتش": ("רֻחְתֵש", ["r u ħ t i ʃ"]), - "اسمع": ("אִסְמַע", ["ʔ i s m a ʕ"]), "اسمعي": ("אִסְמַעִי", ["ʔ i s m a ʕ i"]), - # لحال and زيّ with their suffixes, and other words with one - "لحالي": ("לַחַאל-י", ["l a ħ aː l i"]), "لحاله": ("לַחַאל-וֹ", ["l a ħ aː l o"]), "لحالها": ("לַחַאל-הַא", ["l a ħ aː l h a"]), - "لحالنا": ("לַחַאל-נַא", ["l a ħ aː l n a"]), "لحالك#f": ("לַחַאלֵ-כּ", ["l a ħ aː l i k"]), - "زيّ": ("זַיّ", ["z a j j"]), "زيّه": ("זַיّ-וֹ", ["z a j j o"]), "زيّها": ("זַיّ-הַא", ["z a j j h a"]), - "زيّك#f": ("זַיֵّ-כּ", ["z a j j i k"]), - "فيها": ("פִי-הַא", ["f iː h a"]), "فيه": ("פִי-וֹ", ["f iː o", "f iː h", "f iː"]), "كلّه": ("כֻּלّ-וֹ", ["k u l l o"]), - "لعندها": ("לַעִנְד-הַא", ["l a ʕ i n d h a"]), "لبنتي": ("לַבִּנְת-י", ["l a b i n t i"]), "لبيت": ("לַבֵּית", ["l a b eː t"]), - "غرفته": ("עֻ'רְפְת-וֹ", ["ɣ u r f t o", "ɣ u r f i t o"]), "نصيبه": ("נַצִיבּ-וֹ", ["n a sˤ iː b o"]), - "حبيبتك": ("חַבִּיבְּתַ-כּ", ["ħ a b iː b t a k"]), "اصحابنا": ("אַצְחַאבּ-נַא", ["ʔ a sˤ ħ aː b n a"]), - "صاحباتها": ("צַאחְבַּאת-הַא", ["sˤ aː ħ b aː t h a"]), - "بالسّيّارة": ("בִּ(ל)סַّיַّארַה", ["b i s s a j j aː r a"]), "بالحياة": ("בִּ(א)לְחַיַאה", ["b i l ħ a j aː", "b i l ħ a j aː h"]), - "بالمرّة": ("בִּלְמַרַّה", ["b i l m a r r a"]), - # the sentences' other words - "هادا": ("הַאדַא", ["h aː d a"]), "مطلّق": ("מְטַלַّק", ["m tˤ a l l a ʔ", "m tˤ a l l a q"]), "مرّتين": ("מַרְّתֵין", ["m a r r t eː n"]), - "فاضي": ("פַאדִֿי", ["f aː dˤ i"]), "بارد": ("בַּארֵד", ["b aː r i d"]), "مكتوب": ("מַכְּתוּבּ", ["m a k t uː b"]), - "عزّابيّة": ("עַזַّאבִּיֵّה", ["ʕ a z z aː b i j j e"]), "ستّعشر": ("סִתַّעְשַר", ["s i t t a ʕ ʃ a r", "s i t t a ʕ a ʃ a r"]), - "تخت": ("תַחְ'ת", ["t a x t"]), "التّخت": ("אֵ(ל)תַّחְ'ת", ["i t t a x t"]), "تخوت": ("תְח'וּת", ["t x uː t"]), - "التّاكسي": ("אֵ(ל)תַّכְּסִי", ["i t t a k s i"]), "الحفلة": ("אֵלְחַפְלֵה", ["i l ħ a f l e"]), - "الزّلمة": ("אֵ(ל)זַّלַמֵה", ["i z z a l a m e"]), "كابتن": ("כַּבְּתֵן", ["k a b t i n"]), - "ايوب": ("אַיּוּבּ", ["ʔ a j j uː b"]), "نسرين": ("נַסְרִין", ["n a s r iː n"]), "يونس": ("יוּנֵס", ["j uː n i s"]), - "عادل": ("עַאדֵל", ["ʕ aː d i l"]), "حمّودي": ("חַמּוּדִי", ["ħ a m m uː d i"]), "طه": ("טַהַה", ["tˤ aː h a", "tˤ a h a"]), - # ---- lesson 7 (a review: few new words) ---- - "بيتين": ("בֵּיתֵין", ["b eː t eː n"]), "تختين": ("תַחְ'תֵין", ["t a x t eː n"]), - "تلاتعشر": ("תַלַאתַעְשַר", ["t a l aː t a ʕ ʃ a r", "t a l a t a ʕ ʃ a r", "t a l aː t a ʕ a ʃ a r"]), - "تمن": ("תַמַן", ["t a m a n", "t a m aː n"]), - "شفته": ("שֻפְת-וֹ", ["ʃ u f t o"]), "شفناهم": ("שֻפְנַא-הֹםّ", ["ʃ u f n aː h o m"]), - "حكتلها": ("חַכַּת-לְהַא", ["ħ a k a t l h a", "ħ a k a t i l h a"]), - "بدّنا": ("בִּדّ-נַא", ["b i d d n a"]), "نروح": ("נְרוּח", ["n r uː ħ"]), "لعندنا": ("לַעִנְד-נַא", ["l a ʕ i n d n a"]), - "عمر": ("עֹמֵר", ["ʕ u m u r", "ʕ u m r"]), "قصّة": ("קֹצַّה", ["ʔ i sˤ sˤ a", "q i sˤ sˤ a", "ʔ u sˤ sˤ a", "q u sˤ sˤ a"]), - "طويلة": ("טַוִילֵה", ["tˤ a w iː l e"]), "الجديدة": ("אֵלְגְ'דִידֵה", ["i l dʒ d iː d e", "l i dʒ d iː d e"]), - "القهوة": ("אֵלְקַהְוֵה", ["i l ʔ a h w e", "i l q a h w e"]), - # ---- lesson 8 ---- - # verbs of גזרת פִעֵל: past tense, then the future's base form (يسمع is lesson 6's) - "شرب": ("שִרֵבּ", ["ʃ i r i b"]), "شربت": ("שְרִבֵּת", ["ʃ r i b t", "ʃ r i b i t"]), "شربتي": ("שְרִבְּתִי", ["ʃ r i b t i"]), - "شربتو": ("שְרִבְּתוּ", ["ʃ r i b t u"]), "شربنا": ("שְרִבְּנַא", ["ʃ r i b n a"]), "شربت#f": ("שִרְבַּת", ["ʃ i r b a t"]), - "شربو": ("שִרְבּוּ", ["ʃ i r b u"]), "يشرب": ("יִשְרַבּ", ["j i ʃ r a b"]), - "طلع": ("טִלֵע", ["tˤ i l i ʕ"]), "طلعت": ("טְלִעֵת", ["tˤ l i ʕ t", "tˤ l i ʕ i t"]), "طلعتي": ("טְלִעְתִי", ["tˤ l i ʕ t i"]), - "طلعتو": ("טְלִעְתוּ", ["tˤ l i ʕ t u"]), "طلعنا": ("טְלִעְנַא", ["tˤ l i ʕ n a"]), "طلعت#f": ("טִלְעַת", ["tˤ i l ʕ a t"]), - "طلعو": ("טִלְעוּ", ["tˤ i l ʕ u"]), "يطلع": ("יִטְלַע", ["j i tˤ l a ʕ"]), - "سمع": ("סִמֵע", ["s i m i ʕ"]), "سمعت": ("סְמִעֵת", ["s m i ʕ t", "s m i ʕ i t"]), "سمعتي": ("סְמִעְתִי", ["s m i ʕ t i"]), - "سمعتو": ("סְמִעְתוּ", ["s m i ʕ t u"]), "سمعنا": ("סְמִעְנַא", ["s m i ʕ n a"]), "سمعت#f": ("סִמְעַת", ["s i m ʕ a t"]), - "سمعو": ("סִמְעוּ", ["s i m ʕ u"]), - "فهم": ("פִהֵם", ["f i h i m"]), "فهمت": ("פְהִמֵת", ["f h i m t", "f h i m i t"]), "فهمتي": ("פְהִמְתִי", ["f h i m t i"]), - "فهمتو": ("פְהִמְתוּ", ["f h i m t u"]), "فهمنا": ("פְהִמְנַא", ["f h i m n a"]), "فهمت#f": ("פִהְמַת", ["f i h m a t"]), - "فهمو": ("פִהְמוּ", ["f i h m u"]), "يفهم": ("יִפְהַם", ["j i f h a m"]), - "رجع": ("רִגֵ'ע", ["r i dʒ i ʕ"]), "رجعت": ("רְגִ'עֵת", ["r dʒ i ʕ t", "r dʒ i ʕ i t"]), "رجعتي": ("רְגִ'עְתִי", ["r dʒ i ʕ t i"]), - "رجعتو": ("רְגִ'עְתוּ", ["r dʒ i ʕ t u"]), "رجعنا": ("רְגִ'עְנַא", ["r dʒ i ʕ n a"]), "رجعت#f": ("רִגְ'עַת", ["r i dʒ ʕ a t"]), - "رجعو": ("רִגְ'עוּ", ["r i dʒ ʕ u"]), "يرجع": ("יִרְגַ'ע", ["j i r dʒ a ʕ"]), - # ... with a suffix, or negated with ش - "سمعته": ("סְמִעְת-וֹ", ["s m i ʕ t o"]), "سمعني": ("סִמֵע-נִי", ["s i m i ʕ n i"]), "فهمتك": ("פְהִמְתַ-כּ", ["f h i m t a k"]), - "سمعتش": ("סְמִעְתֵש", ["s m i ʕ t i ʃ"]), "سمعتهوش": ("סְמִעְתוֹ-ש", ["s m i ʕ t oː ʃ"]), - "شافته": ("שַאפַת-וֹ", ["ʃ aː f a t o"]), "قالتله": ("קַאלַת-לוֹ", ["ʔ aː l a t l o", "q aː l a t l o"]), - "قلتلها": ("קֻלְתִ-לְהַא", ["ʔ u l t i l h a", "q u l t i l h a"]), "اخدها": ("אַחַ'ד-הַא", ["ʔ a x a d h a"]), - "اعطيناهم": ("אַעַטֵינַא-הֹםّ", ["ʔ a ʕ tˤ eː n aː h o m"]), "شوفي": ("שוּפִי", ["ʃ uː f i"]), "بحبّ": ("בַּחִבّ", ["b a ħ i b b"]), - # the colours: masculine, feminine, and the article where a sentence needs it - "احمر": ("אַחְמַר", ["ʔ a ħ m a r"]), "حمرا": ("חַמְרַא", ["ħ a m r a"]), - "ازرق": ("אַזְרַק", ["ʔ a z r a ʔ", "ʔ a z r a q"]), "زرقا": ("זַרְקַא", ["z a r ʔ a", "z a r q a"]), - "الازرق": ("אֵלְאַזְרַק", ["i l ʔ a z r a ʔ", "i l ʔ a z r a q"]), - "اخضر": ("אַחְ'דַֿר", ["ʔ a x dˤ a r"]), "خضرا": ("חַ'דְֿרַא", ["x a dˤ r a"]), "الاخضر": ("אֵלְאַחְ'דַֿר", ["i l ʔ a x dˤ a r"]), - "الخضرا": ("אֵלְחַ'דְֿרַא", ["i l x a dˤ r a"]), - "ابيض": ("אַבְּיַדֿ", ["ʔ a b j a dˤ"]), "بيضا": ("בֵּידַֿא", ["b eː dˤ a"]), "الابيض": ("אֵלְאַבְּיַדֿ", ["i l ʔ a b j a dˤ"]), - "اسود": ("אַסְוַד", ["ʔ a s w a d"]), "سودا": ("סוֹדַא", ["s oː d a"]), "الاسود": ("אֵלְאַסְוַד", ["i l ʔ a s w a d"]), - "اصفر": ("אַצְפַר", ["ʔ a sˤ f a r"]), "صفرا": ("צַפְרַא", ["sˤ a f r a"]), - "بنّي": ("בֻּנִّי", ["b u n n i"]), "بنّيّة": ("בֻּנִّיֵّה", ["b u n n i j j e"]), "البنّي": ("אֵלְבֻּנִّי", ["i l b u n n i"]), - "زهري": ("זַהְרִי", ["z a h r i"]), "زهريّة": ("זַהְרִיֵّה", ["z a h r i j j e"]), - "وردي": ("וַרְדִי", ["w a r d i"]), "ورديّة": ("וַרְדִיֵّה", ["w a r d i j j e"]), - "بنفسجي": ("בַּנַפְסַגִ'י", ["b a n a f s a dʒ i"]), "بنفسجيّة": ("בַּנַפְסַגִ'יֵّה", ["b a n a f s a dʒ i j j e"]), - "برتقالي": ("בֻּרְתֻקַאלִי", ["b u r t u ʔ aː l i", "b u r t u q aː l i"]), - "برتقاليّة": ("בֻּרְתֻקַאלִיֵّה", ["b u r t u ʔ aː l i j j e", "b u r t u q aː l i j j e"]), - "سكني": ("סַכַּנִי", ["s a k a n i"]), "سكنيّة": ("סַכַּנִיֵّה", ["s a k a n i j j e"]), - "رمادي": ("רַמַאדִי", ["r a m aː d i"]), "رماديّة": ("רַמַאדִיֵّה", ["r a m aː d i j j e"]), - "فضّة": ("פִדַֿّה", ["f i dˤ dˤ a"]), "فضّيّة": ("פִדִֿّיֵّה", ["f i dˤ dˤ i j j e"]), - "سماوي": ("סַמַאוִי", ["s a m aː w i"]), "سماويّة": ("סַמַאוִיֵّה", ["s a m aː w i j j e"]), - "فاتح": ("פַאתֵח", ["f aː t i ħ"]), "فاتحة": ("פַאתְחַה", ["f aː t ħ a"]), - "غامق": ("עַ'אמֵק", ["ɣ aː m i ʔ", "ɣ aː m i q"]), "غامقة": ("עַ'אמְקַה", ["ɣ aː m ʔ a", "ɣ aː m q a"]), - "لون": ("לוֹן", ["l oː n"]), "لونه": ("לוֹנ-וֹ", ["l oː n o"]), "لونها": ("לוֹנ-הַא", ["l oː n h a"]), - "بلون": ("בִּלוֹן", ["b i l oː n", "b l oː n"]), "اللّون": ("אֵ(ל)לּוֹן", ["i l l oː n"]), - "الوان": ("אַלְוַאן", ["ʔ a l w aː n"]), "الالوان": ("אֵלְאַלְוַאן", ["i l ʔ a l w aː n"]), - # the colours' song, written for children: its nouns end in a ي that is only sung - "موجود": ("מַוְג'וּד", ["m a w dʒ uː d", "m oː dʒ uː d"]), "التّوتي": ("אֵ(ל)תּוּתִי", ["i t t uː t i"]), - "العشبي": ("(א)לְעֻשְבִּי", ["i l ʕ u ʃ b i"]), "الشّجري": ("אֵ(ל)שַّגַ'רִי", ["i ʃ ʃ a dʒ a r i"]), - "الشّمسي": ("אֵ(ל)שַّמְסִי", ["i ʃ ʃ a m s i"]), "الموزي": ("(א)לְמוֹזִי", ["i l m oː z i"]), "البحري": ("(א)לְבַּחְרִי", ["i l b a ħ r i"]), - "السّماي": ("אֵ(ל)סַّמַאִי", ["i s s a m aː ʔ i", "i s s a m aː j"]), "الخشبي": ("(א)לְחַ'שַבִּי", ["i l x a ʃ a b i"]), - "الشّوكولات": ("אֵ(ל)שּוֹקוֹלַאת", ["i ʃ ʃ oː k oː l aː t", "i ʃ ʃ u k oː l aː t a"]), "الزّهري": ("אֵ(ל)זַّהְרִי", ["i z z a h r i"]), - "الحلوى": ("(א)לְחַלְוַא", ["i l ħ a l w a"]), "الفحمي": ("(א)לְפַחְמִי", ["i l f a ħ m i"]), "اللّيلي": ("אֵ(ל)לֵّילִי", ["i l l eː l i"]), - "البيضي": ("(א)לְבֵּידִֿי", ["i l b eː dˤ i"]), "التّلجي": ("אֵ(ל)תַّלְגִ'י", ["i t t a l dʒ i"]), - # the sentences' other words - "هيك": ("הֵיכּ", ["h eː k"]), "امّا": ("אַמַّא", ["ʔ a m m a"]), "ايش": ("אֵיש", ["ʔ eː ʃ"]), "هادي": ("הַאדִי", ["h aː d i"]), - "لايّ": ("לַאַיְ", ["l a ʔ a j j", "l a ʔ a j"]), "منّي": ("מִנִّ-י", ["m i n n i"]), "عليها": ("עַלֵי-הַא", ["ʕ a l eː h a"]), - "بوقت": ("בִּוַקְת", ["b i w a ʔ t", "b i w a q t", "b w a ʔ t"]), "حقّها": ("חַקّ-הַא", ["ħ a ʔ ʔ h a", "ħ a q q h a"]), - "لمرتك": ("לַמַרְתַ-כּ", ["l a m a r t a k"]), "ابوهم": ("אַבּוּ-הֹםّ", ["ʔ a b uː h o m"]), - "مصاري": ("מַצַארִי", ["m a sˤ aː r i"]), "دقيقة": ("דַקִיקַה", ["d a ʔ iː ʔ a", "d a q iː q a"]), "عشر": ("עַשֵר", ["ʕ a ʃ i r", "ʕ a ʃ a r", "ʕ a ʃ r"]), - "اخوات": ("אַחַ'וַאת", ["ʔ a x a w aː t"]), "ميّ": ("מַיّ", ["m a j j", "m a j j e"]), - "جوعانين": ("ג'וֹעַאנִין", ["dʒ oː ʕ aː n iː n"]), "مهمّ": ("מֻהִםّ", ["m u h i m m"]), "مفهوم": ("מַפְהוּם", ["m a f h uː m"]), - "الصّغيرة": ("(אֵ)לְצְעִ'ירֵה", ["l i z ɣ iː r e", "l i sˤ ɣ iː r e", "i z z ɣ iː r e"]), - "الاولى": ("אֵלְאוּלַא", ["i l ʔ uː l a"]), "التّانية": ("אֵ(ל)תַّאנְיֵה", ["i t t aː n j e"]), - "الشّغل": ("אֵ(ל)שֻّעֻ'ל", ["i ʃ ʃ u ɣ u l", "i ʃ ʃ u ɣ l"]), "عرس": ("עֹרְס", ["ʕ u r s", "ʕ u r u s"]), - "العرس": ("אֵלְעֹרְס", ["i l ʕ u r s", "i l ʕ u r u s"]), "حفلة#of": ("חַפְלֵת", ["ħ a f l e t", "ħ a f l i t"]), - "الجيران": ("אֵלְגִ'ירַאן", ["i l dʒ iː r aː n"]), "غنوة": ("עִ'נְוֵה", ["ɣ i n w e", "ɣ u n w e"]), - "الغنوة": ("אֵלְעִ'נְוֵה", ["i l ɣ i n w e", "i l ɣ u n w e"]), "غنوة#of": ("עִ'נְוֵת", ["ɣ i n w e t", "ɣ i n w i t"]), - "موسيقى": ("מוּסִיקַא", ["m uː s iː ʔ a", "m uː s iː q a"]), "صالون": ("צַאלוֹן", ["sˤ aː l oː n"]), - "الجزدان": ("אֵלְגֻ'זְדַאן", ["i l dʒ u z d aː n"]), "الفستان": ("אֵלְפֻסְתַאן", ["i l f u s t aː n"]), - "بنطلون": ("בַּנְטַלוֹן", ["b a n tˤ a l oː n"]), "نسكافيه": ("נֵס-כַּפֵה", ["n e s k a f e", "n i s k a f e"]), - "السّؤال": ("אֵ(ל)סֻّאַאל", ["i s s u ʔ aː l"]), "الاستاذ": ("אֵלְאֻסְתַאז", ["i l ʔ u s t aː z"]), "المكتب": ("אֵלְמַכְּתַבּ", ["i l m a k t a b"]), - "الحساب": ("(א)לִחְסַאבּ", ["l i ħ s aː b", "i l ħ s aː b"]), "ضجّة": ("דַֿגֵّ'ה", ["dˤ a dʒ dʒ e"]), - "القمر": ("אֵלְקַמַר", ["i l ʔ a m a r", "i l q a m a r"]), "الشّمس": ("אֵ(ל)שַّמְס", ["i ʃ ʃ a m s"]), - "السّما": ("אֵ(ל)סַّמַא", ["i s s a m a"]), "غروب": ("עֻ'רוּבּ", ["ɣ u r uː b"]), "الحشيش": ("(א)לְחַשִיש", ["l i ħ a ʃ iː ʃ", "i l ħ a ʃ iː ʃ"]), - "الشّجرة": ("אֵ(ל)שַّגַ'רַה", ["i ʃ ʃ a dʒ a r a"]), "وردة": ("וַרְדֵה", ["w a r d e"]), "مزهريّة": ("מַזְהַרִיֵّה", ["m a z h a r i j j e"]), - "البسّ": ("אֵלְבִּסّ", ["i l b i s s"]), "البسّة": ("אֵלְבִּסֵّה", ["i l b i s s e"]), "جبنة": ("גִ'בְּנֵה", ["dʒ i b n e"]), - "امريكا": ("אַמְרִיכַּא", ["ʔ a m r iː k a"]), - # ---- lesson 9 ---- - # verbs: past tense, then the future's base form - "شكر": ("שַכַּר", ["ʃ a k a r"]), "شكرت": ("שַכַּרֵת", ["ʃ a k a r t"]), "شكرت#f": ("שַכְּרַת", ["ʃ a k r a t"]), - "شكرتي": ("שַכַּרְתִי", ["ʃ a k a r t i"]), "شكرنا": ("שַכַּרְנַא", ["ʃ a k a r n a"]), "شكرتو": ("שַכַּרְתוּ", ["ʃ a k a r t u"]), - "شكرو": ("שַכַּרוּ", ["ʃ a k a r u", "ʃ a k r u"]), "يشكر": ("יֻשְכֹּר", ["j u ʃ k u r"]), - "تغدّى": ("תְעַ'דַّא", ["t ɣ a d d a"]), "تغدّيت": ("תְעַ'דֵّית", ["t ɣ a d d eː t"]), "تغدّيتي": ("תְעַ'דֵّיתִי", ["t ɣ a d d eː t i"]), - "تغدّيتو": ("תְעַ'דֵّיתוּ", ["t ɣ a d d eː t u"]), "تغدّينا": ("תְעַ'דֵّינַא", ["t ɣ a d d eː n a"]), "تغدّت": ("תְעַ'דַّת", ["t ɣ a d d a t"]), - "تغدّو": ("תְעַ'דּוּ", ["t ɣ a d d u"]), "يتغدّى": ("יִתְעַ'דַّא", ["j i t ɣ a d d a"]), - # the future after بدّ: every pronoun's form of the three verbs drilled (سمع's are lesson 6's), then the sentences' - "بدّك#f": ("בִּדֵّ-כּ", ["b i d d i k"]), "بدّها": ("בִּדّ-הַא", ["b i d d h a"]), "بدّكم": ("בִּדّ-כֹּם", ["b i d d k u m", "b i d d k u"]), - "بدّهم": ("בִּדّ-הֹם", ["b i d d h o m"]), - "اطلع": ("אַטְלַע", ["ʔ a tˤ l a ʕ"]), "تطلع": ("תִטְלַע", ["t i tˤ l a ʕ"]), "تطلعي": ("תִטְלַעִי", ["t i tˤ l a ʕ i"]), - "نطلع": ("נִטְלַע", ["n i tˤ l a ʕ"]), "تطلعو": ("תִטְלַעוּ", ["t i tˤ l a ʕ u"]), "يطلعو": ("יִטְלַעוּ", ["j i tˤ l a ʕ u"]), - "اتغدّى": ("אַתְעַ'דַّא", ["ʔ a t ɣ a d d a"]), "تتغدّى": ("תִתְעַ'דַّא", ["t i t ɣ a d d a"]), "تتغدّي": ("תִתְעַ'דִّי", ["t i t ɣ a d d i"]), - "نتغدّى": ("נִתְעַ'דַّא", ["n i t ɣ a d d a"]), "تتغدّو": ("תִתְעַ'דּוּ", ["t i t ɣ a d d u"]), "يتغدّو": ("יִתְעַ'דּוּ", ["j i t ɣ a d d u"]), - "اشرب": ("אַשְרַבּ", ["ʔ a ʃ r a b"]), "تشرب": ("תִשְרַבּ", ["t i ʃ r a b"]), "نشرب": ("נִשְרַבּ", ["n i ʃ r a b"]), - "تشربو": ("תִשְרַבּוּ", ["t i ʃ r a b u"]), "ترجع": ("תִרְגַ'ע", ["t i r dʒ a ʕ"]), "ارجع": ("אַרְגַ'ע", ["ʔ a r dʒ a ʕ"]), - "تفهم": ("תִפְהַם", ["t i f h a m"]), "تفهمي": ("תִפְהַמִי", ["t i f h a m i"]), "تحكي": ("תִחְכִּי", ["t i ħ k i"]), - "نحكي": ("נִחְכִּי", ["n i ħ k i"]), "تشتري": ("תִשְתְרִי", ["t i ʃ t r i", "t i ʃ t i r i"]), - "تسوقي": ("תְסוּקִי", ["t s uː ʔ i", "t s uː q i"]), "اقول": ("אַקוּל", ["ʔ a ʔ uː l", "ʔ a q uː l"]), - "نكون": ("נְכּוּן", ["n k uː n"]), "تكوني": ("תְכּוּנִי", ["t k uː n i"]), "نشوف": ("נְשוּף", ["n ʃ uː f"]), - "ندخل": ("נֻדְחֹ'ל", ["n u d x u l"]), "ينقلو": ("יֻנְקֹלוּ", ["j u n ʔ u l u", "j u n q u l u"]), - "تاخد": ("תֹאחֵ'ד", ["t oː x i d", "t aː x u d"]), "تطبخ": ("תֻטְבֹּח'", ["t u tˤ b u x"]), "آكل": ("אַאכֹּל", ["ʔ aː k u l", "ʔ aː k i l"]), - "تيجي": ("תִיגִ'י", ["t iː dʒ i"]), "يتعلّم": ("יִתְעַלַّם", ["j i t ʕ a l l a m"]), "اسأل": ("אִסְאַל", ["ʔ i s ʔ a l"]), - # ... with a suffix, or negated with ش - "تسألني": ("תִסְאַל-נִי", ["t i s ʔ a l n i"]), "يسألني": ("יִסְאַל-נִי", ["j i s ʔ a l n i"]), - "تسألوني": ("תִסְאַלוּ-נִי", ["t i s ʔ a l uː n i"]), "تزوروني": ("תְזוּרוּ-נִי", ["t z uː r uː n i", "d z uː r uː n i"]), - "يزورني": ("יִזוּר-נִי", ["j z uː r n i", "i z uː r n i", "j i z uː r n i"]), "زارها": ("זַאר-הַא", ["z aː r h a"]), - "اسمعك#f": ("אַסְמַעֵ-כּ", ["ʔ a s m a ʕ i k"]), "سمعتك#f": ("סְמִעְתֵ-כּ", ["s m i ʕ t i k"]), - "احكيلك#f": ("אַחְכִּי-לֵכּ", ["ʔ a ħ k iː l i k"]), "نجيبلها": ("נְגִ'יבּ-לְהַא", ["n dʒ iː b l h a", "n dʒ iː b i l h a"]), - "حكالنا": ("חַכַּא-לְנַא", ["ħ a k aː l n a"]), "قلناله": ("קֻלְנַא-לוֹ", ["ʔ u l n aː l o", "q u l n aː l o"]), - "شكرته": ("שַכַּרְת-וֹ", ["ʃ a k a r t o"]), "اخدته": ("אַחַ'דְת-וֹ", ["ʔ a x a d t o"]), "جابت": ("גַ'אבַּת", ["dʒ aː b a t"]), - "بيقولولي": ("בִּיקוּלוּ-לִי", ["b i ʔ uː l uː l i", "b i q uː l uː l i"]), "كنتيش": ("כֻּנְתִיש", ["k u n t iː ʃ"]), - "نوّرت": ("נַוַّרֵת", ["n a w w a r t"]), "نوّرتي": ("נַוַّרְתִי", ["n a w w a r t i"]), - # greetings and blessings - "تسلم": ("תִסְלַם", ["t i s l a m"]), "يسلّمك": ("יְסַלְّמַ-כּ", ["j s a l l m a k", "i s a l l m a k"]), - "يسلّمك#f": ("יְסַלְّמֵ-כּ", ["j s a l l m i k", "i s a l l m i k"]), "بوجودكم": ("בִּוְג'וּדְכֹּם", ["b i w dʒ uː d k u m", "b w dʒ uː d k u m"]), - # the sentences' other words - "جنب": ("גַ'נְבּ", ["dʒ a n b", "dʒ a m b"]), "سادة": ("סַאדַא", ["s aː d a"]), "حليب": ("חַלִיבּ", ["ħ a l iː b"]), - "منيحة": ("מְנִיחַה", ["m n iː ħ a"]), "موجودة": ("מַוְג'וּדֵה", ["m a w dʒ uː d e", "m oː dʒ uː d e"]), - "قصيرة": ("קַצִירֵה", ["ʔ a sˤ iː r e", "q a sˤ iː r e"]), "السّودا": ("אֵ(ל)סּוֹדַא", ["i s s oː d a"]), "الزّرقا": ("אֵ(ל)זַّרְקַא", ["i z z a r ʔ a", "i z z a r q a"]), - "جمعة": ("גֻ'מְעַה", ["dʒ u m ʕ a"]), "الجمعة": ("אֵלְגֻ'מְעַה", ["i l dʒ u m ʕ a"]), "جمعتين": ("גֻ'מְעַתֵין", ["dʒ u m ʕ a t eː n", "dʒ u m ʕ t eː n"]), - "الاسبوع": ("אֵלְאֻסְבּוּע", ["i l ʔ u s b uː ʕ"]), "الجاي": ("אֵלְגַ'איְ", ["i l dʒ aː j"]), - "الجامعة": ("אֵלְגַ'אמְעַה", ["i l dʒ aː m ʕ a"]), "جامعة#of": ("גַ'אמְעַת", ["dʒ aː m ʕ a t", "dʒ aː m ʕ i t"]), - "العبريّة": ("אֵלְעִבְּרִיֵّה", ["i l ʕ i b r i j j e"]), "الجامع": ("אֵלְגַ'אמֵע", ["i l dʒ aː m i ʕ"]), - "المحلّ": ("אֵלְמַחַלّ", ["i l m a ħ a l l"]), "اوتيل": ("אוֹתֵיל", ["ʔ oː t eː l"]), "البلكونة": ("(א)לְבַּלְכּוֹנֵה", ["i l b a l k oː n e", "l i b a l k oː n e"]), - "راس": ("רַאס", ["r aː s"]), "الجبل": ("אֵלְגַ'בַּל", ["i l dʒ a b a l"]), "الجنوب": ("אֵלְגַ'נוּבּ", ["i l dʒ a n uː b"]), - "فستان": ("פֻסְתַאן", ["f u s t aː n"]), "شوربة": ("שוֹרַבַּה", ["ʃ oː r a b a"]), "حمار": ("חְמַאר", ["ħ m aː r"]), - "عيلة#of": ("עֵילֵת", ["ʕ eː l e t", "ʕ eː l i t"]), "اصحابك": ("אַצְחַאבַּ-כּ", ["ʔ a sˤ ħ aː b a k"]), "لالهام": ("לַאִלְהַאם", ["l a ʔ i l h aː m"]), - "البدوي": ("אֵלְבַּדַוִי", ["i l b a d a w i"]), - "النّقب": ("אֵ(ל)נַّקַבּ", ["i n n a ʔ a b", "i n n a q a b"]), "عراد": ("עַרַאד", ["ʕ a r aː d"]), "العيساويّة": ("אֵלְעִיסַאוִיֵّה", ["i l ʕ iː s aː w i j j e"]), - "انجلترا": ("אִנְגְלַטֵרַה", ["ʔ i n g l a t e r a", "ʔ i n g i l t e r a"]), "اوكسفورد": ("אוֹקְסְפוֹרְד", ["ʔ oː k s f oː r d"]), - "لندن": ("לַנְדַן", ["l a n d a n"]), "اسبانيا": ("אִסְבַּאנְיַא", ["ʔ i s b aː n j a"]), "اوروبّا": ("אֻרוּבַּّא", ["ʔ u r uː b b a", "ʔ oː r oː b b a"]), - "ايلات": ("אֵילַאת", ["ʔ eː l aː t"]), "الاردن": ("אֵלְאֻרְדֻן", ["i l ʔ u r d u n"]), "طرابين": ("טַרַבִּין", ["tˤ a r a b iː n", "t a r a b iː n"]), - "اشرف": ("אַשְרַף", ["ʔ a ʃ r a f"]), "كامل": ("כַּאמֵל", ["k aː m i l"]), "نضال": ("נִדַֿאל", ["n i dˤ aː l"]), - "سليمان": ("סְלֵימַאן", ["s l eː m aː n"]), "سميرة": ("סַמִירַה", ["s a m iː r a"]), "حنين": ("חַנִין", ["ħ a n iː n"]), - # ---- lesson 10 ---- - # adjectives and their comparative (אַפְעַל); the ones missing here are earlier lessons' - "اكبر": ("אַכְּבַּר", ["ʔ a k b a r"]), "اصغر": ("אַצְעַ'ר", ["ʔ a z ɣ a r", "ʔ a sˤ ɣ a r"]), - "طويل": ("טַוִיל", ["tˤ a w iː l"]), "اطول": ("אַטְוַל", ["ʔ a tˤ w a l"]), - "قصير": ("קַצִיר", ["ʔ a sˤ iː r", "q a sˤ iː r"]), "اقصر": ("אַקְצַר", ["ʔ a ʔ sˤ a r", "ʔ a q sˤ a r"]), - "ناصح": ("נַאצֵח", ["n aː sˤ i ħ"]), "انصح": ("אַנְצַח", ["ʔ a n sˤ a ħ"]), - "ضعيف": ("דְֿעִיף", ["dˤ ʕ iː f"]), "اضعف": ("אַדְֿעַף", ["ʔ a dˤ ʕ a f"]), - "تقيل": ("תְקִיל", ["t ʔ iː l", "t q iː l"]), "تقيلة": ("תְקִילֵה", ["t ʔ iː l e", "t q iː l e"]), - "اتقل": ("אַתְקַל", ["ʔ a t ʔ a l", "ʔ a t q a l"]), "اكتر": ("אַכְּתַר", ["ʔ a k t a r"]), - "منيح": ("מְנִיח", ["m n iː ħ"]), "احسن": ("אַחְסַן", ["ʔ a ħ s a n"]), "ابعد": ("אַבְּעַד", ["ʔ a b ʕ a d"]), - "اقرب": ("אַקְרַבּ", ["ʔ a ʔ r a b", "ʔ a q r a b"]), "رخيص": ("רְחִ'יץ", ["r x iː sˤ"]), "ارخص": ("אַרְחַ'ץ", ["ʔ a r x a sˤ"]), - "نضيف": ("נְדִֿיף", ["n dˤ iː f"]), "انضف": ("אַנְדַֿף", ["ʔ a n dˤ a f"]), "وسخ": ("וִסֵח'", ["w i s i x"]), "اوسخ": ("אַוְסַח'", ["ʔ a w s a x"]), - "فقير": ("פַקִיר", ["f a ʔ iː r", "f a q iː r"]), "افقر": ("אַפְקַר", ["ʔ a f ʔ a r", "ʔ a f q a r"]), - "بشع": ("בִּשֵע", ["b i ʃ i ʕ"]), "ابشع": ("אַבְּשַע", ["ʔ a b ʃ a ʕ"]), "اطيب": ("אַטְיַבּ", ["ʔ a tˤ j a b"]), - "عاطل": ("עַאטֵל", ["ʕ aː tˤ i l"]), "اعطل": ("אַעְטַל", ["ʔ a ʕ tˤ a l"]), "شاطر": ("שַאטֵר", ["ʃ aː tˤ i r"]), "اشطر": ("אַשְטַר", ["ʔ a ʃ tˤ a r"]), - "قديم": ("קַדִים", ["ʔ a d iː m", "q a d iː m"]), "اقدم": ("אַקְדַם", ["ʔ a ʔ d a m", "ʔ a q d a m"]), - "جميل": ("גַ'מִיל", ["dʒ a m iː l"]), "جميلة": ("גַ'מִילֵה", ["dʒ a m iː l e"]), "اجمل": ("אַגְ'מַל", ["ʔ a dʒ m a l"]), - "احلى": ("אַחְלַא", ["ʔ a ħ l a"]), "غالي": ("עַ'אלִי", ["ɣ aː l i"]), "اغلى": ("אַעְ'לַא", ["ʔ a ɣ l a"]), "ازكى": ("אַזְכַּא", ["ʔ a z k a"]), - "غني": ("עַ'נִי", ["ɣ a n i"]), "اغنى": ("אַעְ'נַא", ["ʔ a ɣ n a"]), "قوي": ("קַוִי", ["ʔ a w i", "q a w i"]), "اقوى": ("אַקְוַא", ["ʔ a ʔ w a", "ʔ a q w a"]), - "رقيق": ("רַקִיק", ["r a ʔ iː ʔ", "r a q iː q"]), "ارقّ": ("אַרַקّ", ["ʔ a r a ʔ ʔ", "ʔ a r a q q"]), - "خفيف": ("חַ'פִיף", ["x a f iː f"]), "اخفّ": ("אַחַ'ףّ", ["ʔ a x a f f"]), "عزيز": ("עַזִיז", ["ʕ a z iː z"]), "اعزّ": ("אַעַזّ", ["ʔ a ʕ a z z"]), - "اهمّ": ("אַהַםّ", ["ʔ a h a m m"]), "مهمّة": ("מֻהִמֵّה", ["m u h i m m e"]), "قليل": ("קַלִיל", ["ʔ a l iː l", "q a l iː l"]), - "اقلّ": ("אַקַלّ", ["ʔ a ʔ a l l", "ʔ a q a l l"]), "صحيح": ("צַחִיח", ["sˤ a ħ iː ħ"]), "اصحّ": ("אַצַחّ", ["ʔ a sˤ a ħ ħ"]), - "اجدد": ("אַגְ'דַד", ["ʔ a dʒ d a d", "ʔ a dʒ a d d"]), - # ... "the most": with ال - "الاشطر": ("אֵלְאַשְטַר", ["i l ʔ a ʃ tˤ a r"]), "الاطول": ("אֵלְאַטְוַל", ["i l ʔ a tˤ w a l"]), "الاحلى": ("אֵלְאַחְלַא", ["i l ʔ a ħ l a"]), - "الاقوى": ("אֵלְאַקְוַא", ["i l ʔ a ʔ w a", "i l ʔ a q w a"]), "الاجمل": ("אֵלְאַגְ'מַל", ["i l ʔ a dʒ m a l"]), "الاهمّ": ("אֵלְאַהַםّ", ["i l ʔ a h a m m"]), - "الانضف": ("אֵלְאַנְדַֿף", ["i l ʔ a n dˤ a f"]), "الابشع": ("אֵלְאַבְּשַע", ["i l ʔ a b ʃ a ʕ"]), "الاغلى": ("אֵלְאַעְ'לַא", ["i l ʔ a ɣ l a"]), - "الانصح": ("אֵלְאַנְצַח", ["i l ʔ a n sˤ a ħ"]), "الاتقل": ("אֵלְאַתְקַל", ["i l ʔ a t ʔ a l", "i l ʔ a t q a l"]), - "الابعد": ("אֵלְאַבְּעַד", ["i l ʔ a b ʕ a d"]), "الارخص": ("אֵלְאַרְחַ'ץ", ["i l ʔ a r x a sˤ"]), - # verbs (גזרת פִעֵל): past tense, then the future's base form - "نزل": ("נִזֵל", ["n i z i l"]), "نزلت": ("נְזִלֵת", ["n z i l t", "n z i l i t"]), "نزلتي": ("נְזִלְתִי", ["n z i l t i"]), - "نزلتو": ("נְזִלְתוּ", ["n z i l t u"]), "نزلنا": ("נְזִלְנַא", ["n z i l n a"]), "نزلت#f": ("נִזְלַת", ["n i z l a t"]), - "نزلو": ("נִזְלוּ", ["n i z l u"]), "ينزل": ("יִנְזֵל", ["j i n z i l", "j i n z a l"]), - "وصل": ("וִצֵל", ["w i sˤ i l"]), "وصلت": ("וְצִלֵת", ["w sˤ i l t", "u sˤ i l t", "w sˤ i l i t"]), "وصلتي": ("וְצִלְתִי", ["w sˤ i l t i", "u sˤ i l t i"]), - "وصلتو": ("וְצִלְתוּ", ["w sˤ i l t u", "u sˤ i l t u"]), "وصلت#f": ("וִצְלַת", ["w i sˤ l a t"]), "وصلو": ("וִצְלוּ", ["w i sˤ l u"]), - "يوصل": ("יוֹצַל", ["j oː sˤ a l"]), - "ضحك": ("דִֿחֵכּ", ["dˤ i ħ i k"]), "ضحكت": ("דְֿחִכֵּת", ["dˤ ħ i k t", "dˤ ħ i k i t"]), "ضحكتي": ("דְֿחִכְּתִי", ["dˤ ħ i k t i"]), - "ضحكتو": ("דְֿחִכְּתוּ", ["dˤ ħ i k t u"]), "ضحكنا": ("דְֿחִכְּנַא", ["dˤ ħ i k n a"]), "ضحكت#f": ("דִֿחְכַּת", ["dˤ i ħ k a t"]), - "ضحكو": ("דִֿחְכּוּ", ["dˤ i ħ k u"]), "يضحك": ("יִדְֿחַכּ", ["j i dˤ ħ a k"]), - "لبس": ("לִבֵּס", ["l i b i s"]), "لبست": ("לְבִּסֵת", ["l b i s t", "l b i s i t"]), "لبستي": ("לְבִּסְתִי", ["l b i s t i"]), - "لبستو": ("לְבִּסְתוּ", ["l b i s t u"]), "لبسنا": ("לְבִּסְנַא", ["l b i s n a"]), "لبست#f": ("לִבְּסַת", ["l i b s a t"]), - "لبسو": ("לִבְּסוּ", ["l i b s u"]), "يلبس": ("יִלְבַּס", ["j i l b a s"]), - "لعب": ("לִעֵבּ", ["l i ʕ i b"]), "لعبت": ("לְעִבֵּת", ["l ʕ i b t", "l ʕ i b i t"]), "لعبتي": ("לְעִבְּתִי", ["l ʕ i b t i"]), - "لعبتو": ("לְעִבְּתוּ", ["l ʕ i b t u"]), "لعبنا": ("לְעִבְּנַא", ["l ʕ i b n a"]), "لعبت#f": ("לִעְבַּת", ["l i ʕ b a t"]), - "لعبو": ("לִעְבּוּ", ["l i ʕ b u"]), "يلعب": ("יִלְעַבּ", ["j i l ʕ a b"]), - # the future after بدّ, and other verb forms of the sentences - "تلعب": ("תִלְעַבּ", ["t i l ʕ a b"]), "تلعبي": ("תִלְעַבִּי", ["t i l ʕ a b i"]), "نلعب": ("נִלְעַבּ", ["n i l ʕ a b"]), - "تلعبو": ("תִלְעַבּוּ", ["t i l ʕ a b u"]), "يلعبو": ("יִלְעַבּוּ", ["j i l ʕ a b u"]), - "تضحك": ("תִדְֿחַכּ", ["t i dˤ ħ a k"]), "تضحكي": ("תִדְֿחַכִּי", ["t i dˤ ħ a k i"]), "نضحك": ("נִדְֿחַכּ", ["n i dˤ ħ a k"]), - "تضحكو": ("תִדְֿחַכּוּ", ["t i dˤ ħ a k u"]), "تلبس": ("תִלְבַּס", ["t i l b a s"]), - "البسه": ("אַלְבַּס-וֹ", ["ʔ a l b a s o"]), "تلبسيها": ("תִלְבַּסִי-הַא", ["t i l b a s iː h a"]), "تلبسوها": ("תִלְבַּסוּ-הַא", ["t i l b a s uː h a"]), - "توصل": ("תוֹצַל", ["t oː sˤ a l"]), "توصلي": ("תוֹצַלִי", ["t oː sˤ a l i"]), "نوصل": ("נוֹצַל", ["n oː sˤ a l"]), - "يوصلو": ("יוֹצַלוּ", ["j oː sˤ a l u"]), "تنزل": ("תִנְזֵל", ["t i n z i l", "t i n z a l"]), "تنزلو": ("תִנְזְלוּ", ["t i n z l u", "t i n z i l u", "t i n z a l u"]), - "ترجعو": ("תִרְגַ'עוּ", ["t i r dʒ a ʕ u"]), "يرجعو": ("יִרְגַ'עוּ", ["j i r dʒ a ʕ u"]), "تشوفو": ("תְשוּפוּ", ["t ʃ uː f u"]), - "اشوف": ("אַשוּף", ["ʔ a ʃ uː f"]), "اشوفه": ("אַשוּפ-וֹ", ["ʔ a ʃ uː f o"]), "اشوفهم": ("אַשוּפ-הֹםّ", ["ʔ a ʃ uː f h o m"]), - "يشوفني": ("יְשוּפ-נִי", ["j ʃ uː f n i", "i ʃ uː f n i"]), "نشتري": ("נִשְתְרִי", ["n i ʃ t r i", "n i ʃ t i r i"]), - "نشكره": ("נֻשְכֹּר-וֹ", ["n u ʃ k u r o"]), "افهمه": ("אַפְהַמ-וֹ", ["ʔ a f h a m o"]), "اعطي": ("אַעַטִי", ["ʔ a ʕ tˤ i", "ʔ a ʕ a tˤ i"]), - "شفتوني": ("שֻפְתוּ-נִי", ["ʃ u f t uː n i"]), "شافني": ("שַאף-נִי", ["ʃ aː f n i"]), "باسني": ("בַּאס-נִי", ["b aː s n i"]), - "باسته": ("בַּאסַת-וֹ", ["b aː s a t o"]), "سمعتها": ("סְמִעְת-הַא", ["s m i ʕ t h a"]), "قلتلي": ("קֻלְתִ-לִّי", ["ʔ u l t i l l i", "q u l t i l l i"]), - "اشتريتلي": ("אִשְתַרֵית-לִי", ["ʔ i ʃ t a r eː t l i", "ʔ i ʃ t a r eː t i l l i"]), - "اشتريتلك#f": ("אִשְתַרֵית-לֵכּ", ["ʔ i ʃ t a r eː t l i k", "ʔ i ʃ t a r eː t i l l i k"]), - "حكيتلكم": ("חַכֵּית-לְכֹּם", ["ħ a k eː t l k u m", "ħ a k eː t i l k u m", "ħ a k eː t i l k u"]), "خلّصنا": ("חַ'לַّצְנַא", ["x a l l a sˤ n a"]), - "بيحبّ": ("בִּחִבّ", ["b i ħ i b b", "b i j ħ i b b"]), "يسلمو": ("יִסְלַמוּ", ["j i s l a m u"]), - "تفهّمني": ("תְפַהֵّמְ-נִי", ["t f a h h i m n i"]), "تراعيني": ("תְרַאעִי-נִי", ["t r aː ʕ iː n i"]), "بتعلّم": ("בַּתְעַלַّם", ["b a t ʕ a l l a m"]), - "فاهم": ("פַאהֵם", ["f aː h i m"]), - # the demonstratives - "هدول": ("הַדוֹל", ["h a d oː l", "h a d oː l a"]), "هداك": ("הַדַאכּ", ["h a d aː k"]), "هديك": ("הַדִיכּ", ["h a d iː k"]), - "هدولاك": ("הַדוֹלַאכּ", ["h a d oː l aː k"]), - # expressions - "ولا": ("וַלַא", ["w a l a"]), "يهمّك": ("יְהִמַّ-כּ", ["j h i m m a k", "i h i m m a k"]), "يهمّك#f": ("יְהִמֵّ-כּ", ["j h i m m i k", "i h i m m i k"]), - "يمكن": ("יִמְכֵּן", ["j i m k i n"]), "ممكن": ("מֻמְכֵּן", ["m u m k i n"]), "طبعا": ("טַבַּעַן", ["tˤ a b ʕ a n", "tˤ a b a ʕ a n"]), - "طول": ("טוּל", ["tˤ uː l"]), "فكرة": ("פִכְּרַה", ["f i k r a"]), "المهمّ": ("אֵלְמֻהִםّ", ["i l m u h i m m"]), - "هسّا": ("הַסַّא", ["h a s s a", "ʔ i s s a", "h a l l a"]), "هلقيت": ("הַלְקֵית", ["h a l ʔ eː t", "h a l q eː t", "h a l l a"]), - "الّي": ("אִלִّי", ["ʔ i l l i"]), "منها": ("מִנְ-הַא", ["m i n h a"]), "عليّ": ("עַלַיְ", ["ʕ a l a j j", "ʕ a l a j"]), "عليك": ("עַלֵיכּ", ["ʕ a l eː k"]), - "الي": ("אִלִ-י", ["ʔ i l i"]), "الك#f": ("אִלֵ-כּ", ["ʔ i l i k"]), "لمين": ("לַמִין", ["l a m iː n"]), "بالاصل": ("בִּלְאַצֵל", ["b i l ʔ a sˤ i l"]), - "حقّه": ("חַקّ-וֹ", ["ħ a ʔ ʔ o", "ħ a q q o"]), "بتمانية": ("בְּתַמַאנְיֵה", ["b t a m aː n j e", "b i t a m aː n j e"]), - # the sentences' other words - "العالم": ("אֵלְעַאלַם", ["i l ʕ aː l a m"]), "الحياة": ("אֵלְחַיַאה", ["i l ħ a j aː"]), "حياتي": ("חַיַאתִ-י", ["ħ a j aː t i"]), - "غرفتي": ("עֻ'רְפְתִ-י", ["ɣ u r f t i", "ɣ u r f i t i"]), "بلوزتي": ("בְּלוּזְתִ-י", ["b l uː z t i"]), "بلوزة#of": ("בְּלוּזֵת", ["b l uː z e t", "b l uː z i t"]), - "شاطرين": ("שַאטְרִין", ["ʃ aː tˤ r iː n"]), "للحفلة": ("לַלְחַפְלֵה", ["l a l ħ a f l e", "l i l ħ a f l e"]), "البنفسجي": ("אֵלְבַּנַפְסַגִ'י", ["i l b a n a f s a dʒ i"]), - "شواكل": ("שַוַאכֵּל", ["ʃ a w aː k i l"]), "فوتبول": ("פוּתְבּוֹל", ["f uː t b oː l"]), "ربع": ("רֻבֵּע", ["r u b i ʕ", "r u b ʕ"]), - "شنطة": ("שַנְטַה", ["ʃ a n tˤ a"]), "الشّنطة": ("אֵ(ל)שַّנְטַה", ["i ʃ ʃ a n tˤ a"]), "الهديّة": ("אֵלְהַדִיֵّה", ["i l h a d i j j e"]), - "فستانين": ("פֻסְתַאנֵין", ["f u s t aː n eː n"]), "بليستيشن": ("בְּלֵיסְתֵישֵין", ["b l eː s t eː ʃ i n", "p l eː s t eː ʃ i n"]), - "ايديكي": ("אִידֵיכִּי", ["ʔ iː d eː k i"]), "اخو": ("אַח'וּ", ["ʔ a x u"]), "الزّلام": ("אֵ(ל)זְّלַאם", ["i z z l aː m"]), - "بصل": ("בַּצַל", ["b a sˤ a l"]), "زيتون": ("זַיְתוּן", ["z a j t uː n", "z eː t uː n"]), "شطّ": ("שַטّ", ["ʃ a tˤ tˤ"]), - "التّرين": ("אֵ(ל)תְّרֵין", ["i t t r eː n"]), "ترين": ("תְרֵין", ["t r eː n"]), "الخفيف": ("אֵלְחַ'פִיף", ["i l x a f iː f"]), - "الدّفتر": ("אֵ(ל)דַّפְתַר", ["i d d a f t a r"]), "الجاكيت": ("אֵלְגַ'אכֵּת", ["i l dʒ aː k eː t", "i l dʒ aː k i t"]), "عربي": ("עַרַבִּי", ["ʕ a r a b i"]), - "باجي": ("בַּאגִ'י", ["b aː dʒ i"]), "وينك": ("וֵינֵכּ", ["w eː n i k", "w eː n a k"]), - "المرّة": ("אֵלְמַרַّה", ["i l m a r r a"]), "لحالك": ("לַחַאלַ-כּ", ["l a ħ aː l a k"]), - "بلجيكا": ("בֵּלְגִ'יכַּה", ["b e l dʒ iː k a"]), "سعيد": ("סַעִיד", ["s a ʕ iː d"]), "بسّام": ("בַּסַّאם", ["b a s s aː m"]), - # ---- level 2, lesson 1 (the workbook's tenth) ---- - # the new verbs; in بعث, ث is taught and scored as t - "ركض": ("רַכַּדֿ", ["r a k a dˤ"]), "ركضت": ("רַכַּדֵֿת", ["r a k a dˤ t"]), "ركضت#f": ("רַכְּדַֿת", ["r a k dˤ a t"]), - "ركضتي": ("רַכַּדְֿתִי", ["r a k a dˤ t i"]), "ركضنا": ("רַכַּדְֿנַא", ["r a k a dˤ n a"]), "ركضتو": ("רַכַּדְֿתוּ", ["r a k a dˤ t u"]), - "ركضو": ("רַכַּדֿוּ", ["r a k a dˤ u", "r a k dˤ u"]), "يركض": ("יֻרְכֹּדֿ", ["j u r k u dˤ"]), - "دفع": ("דַפַע", ["d a f a ʕ"]), "دفعت": ("דַפַעֵת", ["d a f a ʕ t"]), "دفعت#f": ("דַפְעַת", ["d a f ʕ a t"]), - "دفعتي": ("דַפַעְתִי", ["d a f a ʕ t i"]), "دفعنا": ("דַפַעְנַא", ["d a f a ʕ n a"]), "دفعتو": ("דַפַעְתוּ", ["d a f a ʕ t u"]), - "دفعو": ("דַפַעוּ", ["d a f a ʕ u", "d a f ʕ u"]), "يدفع": ("יִדְפַע", ["j i d f a ʕ"]), - "بعث": ("בַּעַת'", ["b a ʕ a t"]), "بعثت": ("בַּעַתֵ'ת", ["b a ʕ a t t"]), - "بعثت#f": ("בַּעְתַ'ת", ["b a ʕ t a t"]), "بعثتي": ("בַּעַתְ'תִי", ["b a ʕ a t t i"]), - "بعثنا": ("בַּעַתְ'נַא", ["b a ʕ a t n a"]), "بعثتو": ("בַּעַתְ'תוּ", ["b a ʕ a t t u"]), - "بعثو": ("בַּעַת'וּ", ["b a ʕ a t u", "b a ʕ t u"]), "يبعث": ("יִבְּעַת'", ["j i b ʕ a t"]), - "كسر": ("כַּסַר", ["k a s a r"]), "كسرت": ("כַּסַרֵת", ["k a s a r t"]), "كسرت#f": ("כַּסְרַת", ["k a s r a t"]), - "كسرتي": ("כַּסַרְתִי", ["k a s a r t i"]), "كسرنا": ("כַּסַרְנַא", ["k a s a r n a"]), "كسرتو": ("כַּסַרְתוּ", ["k a s a r t u"]), - "كسرو": ("כַּסַרוּ", ["k a s a r u", "k a s r u"]), "يكسر": ("יִכְּסֵר", ["j i k s i r"]), - "ربط": ("רַבַּט", ["r a b a tˤ"]), "ربطت": ("רַבַּטֵת", ["r a b a tˤ t"]), "ربطت#f": ("רַבְּטַת", ["r a b tˤ a t"]), - "ربطتي": ("רַבַּטְתִי", ["r a b a tˤ t i"]), "ربطنا": ("רַבַּטְנַא", ["r a b a tˤ n a"]), "ربطتو": ("רַבַּטְתוּ", ["r a b a tˤ t u"]), - "ربطو": ("רַבַּטוּ", ["r a b a tˤ u", "r a b tˤ u"]), "يربط": ("יֻרְבֹּט", ["j u r b u tˤ"]), - # the future after لازم and رح - "رح": ("רַח", ["r a ħ"]), - "تشوف": ("תְשוּף", ["t ʃ uː f"]), "تشوفي": ("תְשוּפִי", ["t ʃ uː f i"]), "يشوفو": ("יִשוּפוּ", ["j ʃ uː f u", "i ʃ uː f u", "j i ʃ uː f u"]), - "اربط": ("אַרְבֹּט", ["ʔ a r b u tˤ"]), "تربط": ("תֻרְבֹּט", ["t u r b u tˤ"]), "تربطي": ("תֻרֻבְּטִי", ["t u r u b tˤ i", "t u r b u tˤ i"]), - "نربط": ("נֻרְבֹּט", ["n u r b u tˤ"]), "تربطو": ("תֻרֻבְּטוּ", ["t u r u b tˤ u", "t u r b u tˤ u"]), - "يربطو": ("יֻרֻבְּטוּ", ["j u r u b tˤ u", "j u r b u tˤ u"]), "اربط#imp": ("אֻרְבֹּט", ["ʔ u r b u tˤ"]), - "اركض": ("אַרְכֹּדֿ", ["ʔ a r k u dˤ"]), "تركض": ("תֻרְכֹּדֿ", ["t u r k u dˤ"]), "تركضي": ("תֻרֻכְּדִֿי", ["t u r u k dˤ i", "t u r k u dˤ i"]), - "نركض": ("נֻרְכֹּדֿ", ["n u r k u dˤ"]), "تركضو": ("תֻרֻכְּדֿוּ", ["t u r u k dˤ u", "t u r k u dˤ u"]), - "يركضو": ("יֻרֻכְּדֿוּ", ["j u r u k dˤ u", "j u r k u dˤ u"]), - "ابعث": ("אַבְּעַת'", ["ʔ a b ʕ a t"]), "تبعث": ("תִבְּעַת'", ["t i b ʕ a t"]), - "تبعثي": ("תִבְּעַתִ'י", ["t i b ʕ a t i"]), "نبعث": ("נִבְּעַת'", ["n i b ʕ a t"]), - "تبعثو": ("תִבְּעַת'וּ", ["t i b ʕ a t u"]), "يبعثو": ("יִבְּעַת'וּ", ["j i b ʕ a t u"]), - "تشربي": ("תִשְרַבִּי", ["t i ʃ r a b i"]), "يشربو": ("יִשְרַבּוּ", ["j i ʃ r a b u"]), - # the sentences - "تبعثني": ("תִבְּעַת'-נִי", ["t i b ʕ a t n i"]), "المول": ("אֵלְמוֹל", ["i l m oː l"]), - "ندفع": ("נִדְפַע", ["n i d f a ʕ"]), "تكون": ("תְכּוּן", ["t k uː n"]), "شفتك#f": ("שֻפְתֵ-כּ", ["ʃ u f t i k"]), - "اشوفك#f": ("אַשוּפֵ-כּ", ["ʔ a ʃ uː f i k"]), "يعطيني": ("יֵעַטִי-נִי", ["j a ʕ tˤ iː n i", "j i ʕ tˤ iː n i"]), - "ناخدها": ("נֹאחֵ'ד-הַא", ["n oː x i d h a", "n aː x u d h a", "n oː x u d h a"]), "ديري": ("דִירִי", ["d iː r i"]), - "بالك#f": ("בַּאלֵ-כּ", ["b aː l i k"]), "تكسري": ("תִכְּסְרִי", ["t i k s r i", "t i k s i r i"]), - "اسوقها": ("אַסוּק-הַא", ["ʔ a s uː ʔ h a", "ʔ a s uː q h a"]), "اسوق": ("אַסוּק", ["ʔ a s uː ʔ", "ʔ a s uː q"]), - "بعثلي": ("בַּעַת'-לִי", ["b a ʕ a t l i"]), "يبعثلي": ("יִבְּעַת'-לִי", ["j i b ʕ a t l i"]), - "اضحك": ("אַדְֿחַכּ", ["ʔ a dˤ ħ a k"]), "مرت": ("מַרְת", ["m a r t"]), "تبوس": ("תְבּוּס", ["t b uː s"]), - "لعبناش": ("לְעִבְּנַאש", ["l ʕ i b n aː ʃ"]), "احكيلك": ("אַחְכִּי-לַכּ", ["ʔ a ħ k iː l a k"]), "عنّه": ("עַנّוֹ", ["ʕ a n n o"]), - "افوت": ("אַפוּת", ["ʔ a f uː t"]), "كتاب": ("כְּתַאבּ", ["k t aː b"]), "بكّير": ("בַּכִּّיר", ["b a k k iː r"]), - "آخد": ("אַאחֹ'ד", ["ʔ aː x u d", "ʔ aː x i d"]), "كلامه": ("כַּלַאמ-וֹ", ["k a l aː m o"]), - "للبيّاع": ("לַלְבַּיַّאע", ["l a l b a j j aː ʕ"]), "تدفعله": ("תִדְפַע-לוֹ", ["t i d f a ʕ l o"]), - "ادفعله": ("אַדְפַע-לוֹ", ["ʔ a d f a ʕ l o"]), "دفعتله": ("דַפַעְתִ-לّוֹ", ["d a f a ʕ t i l l o"]), - "حماتي": ("חַמַאת-י", ["ħ a m aː t i"]), "معلّمك#f": ("מְעַלְّמֵ-כּ", ["m ʕ a l l m i k"]), - "تربطه": ("תֻרֻבְּט-וֹ", ["t u r u b tˤ o", "t u r b u tˤ o"]), "تزوره": ("תְזוּר-וֹ", ["t z uː r o"]), - "قلب": ("קַלְבּ", ["ʔ a l b", "q a l b"]), "لقلب": ("לַקַלְבּ", ["l a ʔ a l b", "l a q a l b"]), "قلبي": ("קַלְבּ-י", ["ʔ a l b i", "q a l b i"]), - "بعثتني#f": ("בַּעְתַ'ת-נִי", ["b a ʕ t a t n i"]), "مصطفى": ("מֻצְטַפַא", ["m u sˤ tˤ a f a"]), - "الصّبح": ("אֵ(ל)צֻّבֵּח", ["i sˤ sˤ u b i ħ", "i sˤ sˤ u b ħ"]), - # the grocery story - "الايّام": ("אֵלְאַיַאם", ["i l ʔ a j j aː m"]), "تقريبا": ("תַקְרִיבַּן", ["t a ʔ r iː b a n", "t a q r iː b a n"]), - "البقالة": ("אֵלְבַּקַאלֵה", ["i l b a ʔ aː l e", "i l b a q aː l e"]), "بقالة#of": ("בַּקַאלֵת", ["b a ʔ aː l i t", "b a q aː l i t"]), - "البقّال": ("אֵלְבַּקַّאל", ["i l b a ʔ ʔ aː l", "i l b a q q aː l"]), "حسن": ("חַסַן", ["ħ a s a n"]), - "لعنّا": ("לַעִ(ד)נַّא", ["l a ʕ i n n a"]), "ضيوف": ("דֻֿיוּף", ["dˤ j uː f", "dˤ u j uː f"]), "حاضر": ("חַאדֵֿר", ["ħ aː dˤ i r"]), - "تطبخي": ("תֻטְבֹּחִ'י", ["t u tˤ b u x i"]), "اطبخ": ("אַטְבֹּח'", ["ʔ a tˤ b u x"]), "الخمسة": ("אֵלְחַ'מְסֵה", ["i l x a m s e"]), - "كوسا": ("כּוּסַא", ["k uː s a"]), "بيتنجان": ("בֵּתִנְגַ'אן", ["b e t i n dʒ aː n", "b eː t i n dʒ aː n"]), "محشي": ("מַחְשִי", ["m a ħ ʃ i"]), - "فاخدت": ("פַ-אַחַ'דֵת", ["f a ʔ a x a d t"]), "الحمار": ("(א)לִחְמַאר", ["l i ħ m aː r", "i l ħ m aː r"]), - "مطرح": ("מַטְרַח", ["m a tˤ r a ħ"]), "يقولّك": ("יֵקוּל-לַّכּ", ["j ʔ uː l l a k", "i ʔ uː l l a k", "j q uː l l a k"]), - "هلا": ("הַלַא", ["h a l a"]), "صباح": ("צַבַּאח", ["sˤ a b aː ħ"]), "الخير": ("אֵלְחֵ'יר", ["i l x eː r"]), - "الياسمين": ("אֵלְיַאסְמִין", ["i l j aː s m iː n"]), "الورد": ("אֵלְוַרְד", ["i l w a r d"]), "الحال": ("אֵלְחַאל", ["i l ħ aː l"]), - "الوضع": ("אֵלְוַדֵֿע", ["i l w a dˤ i ʕ", "i l w a dˤ ʕ"]), "نشكر": ("נֻשְכֹּר", ["n u ʃ k u r"]), - "اجيبلك": ("אַגִ'יבּ-לַכּ", ["ʔ a dʒ iː b l a k", "ʔ a dʒ iː b i l a k"]), "روحي": ("רוּח-י", ["r uː ħ i"]), - "لحظة": ("לַחְזַֿה", ["l a ħ ðˤ a", "l a ħ z a"]), "لستة": ("לִסְתַה", ["l i s t a"]), "كتبتلي": ("כַּתְבַּת-לִי", ["k a t b a t l i"]), - "كيس": ("כִּיס", ["k iː s"]), "فلفل": ("פִלְפֵל", ["f i l f i l"]), "راسين": ("רַאסֵין", ["r aː s eː n"]), "توم": ("ת'וֹם", ["t oː m"]), - "حبيب": ("חַבִּיבּ", ["ħ a b iː b"]), "علبة#of": ("עִלְבֵּת", ["ʕ i l b i t"]), - "شوكولاطة": ("שֻכֹּלַאטַה", ["ʃ u k o l aː tˤ a", "ʃ o k o l aː tˤ a"]), "فراطة": ("פְרַאטַה", ["f r aː tˤ a"]), - "بخاطرك": ("בְּחַ'אטְרַכּ", ["b x aː tˤ r a k"]), - # ---- level 2, lesson 2 (the workbook's eleventh) ---- - # the days; the present itself (بسأل, بتشوف, منيجي) is not listed: _present_of makes it from the future form - "الاحد": ("אֵלְאַחַד", ["i l ʔ a ħ a d"]), "التّلاتا": ("אֵ(ל)תַّלַאתַא", ["i t t a l aː t a"]), - "الاربعا": ("אֵלְאַרְבַּע", ["i l ʔ a r b a ʕ", "i l ʔ a r b a ʕ a"]), "الخميس": ("אֵלְחַ'מִיס", ["i l x a m iː s"]), - "السّبت": ("אֵ(ל)סַّבְּת", ["i s s a b t", "i s s a b i t"]), - "اسأل#i": ("אַסְאַל", ["ʔ a s ʔ a l"]), "تسأل": ("תִסְאַל", ["t i s ʔ a l"]), "نسأل": ("נִסְאַל", ["n i s ʔ a l"]), - "تسألو": ("תִסְאַלוּ", ["t i s ʔ a l u"]), "يسألو": ("יִסְאַלוּ", ["j i s ʔ a l u"]), "نسأله": ("נִסְאַל-וֹ", ["n i s ʔ a l o"]), - "تاكل": ("תֹאכֹּל", ["t oː k u l", "t oː k i l", "t aː k u l"]), "تاكلي": ("תֹאכְּלִי", ["t oː k l i", "t aː k l i", "t oː k i l i"]), - "دايما": ("דַאיְמַן", ["d aː j m a n"]), "عادة": ("עַדַתַן", ["ʕ aː d a t a n"]), "اخوك": ("אַח'וּכּ", ["ʔ a x uː k"]), - "ناخده": ("נֹאחְ'ד-וֹ", ["n oː x d o", "n aː x d o", "n oː x u d o"]), "لعمّي": ("לַעַמّ-י", ["l a ʕ a m m i"]), - "تقوليلي": ("תְקוּלִי-לִי", ["t ʔ uː l iː l i", "t q uː l iː l i"]), "تلفّ": ("תְלִףّ", ["t l i f f"]), "دوالي": ("דַוַאלִי", ["d a w aː l i"]), - "نيجي": ("נִיגִ'י", ["n iː dʒ i"]), "عرسك#f": ("עֹרְסֵ-כּ", ["ʕ u r s i k"]), "تلبسي": ("תִלְבַּסִי", ["t i l b a s i"]), - "ادفع": ("אַדְפַע", ["ʔ a d f a ʕ"]), "الوقت": ("אֵלְוַקֵת", ["i l w a ʔ i t", "i l w a q i t", "i l w a ʔ t"]), - "بعثني": ("בַּעַת'-נִי", ["b a ʕ a t n i"]), "يبعثني": ("יִבְּעַת'-נִי", ["j i b ʕ a t n i"]), - "ترجعي": ("תִרְגַ'עִי", ["t i r dʒ a ʕ i"]), "المدير": ("אֵלְמֻדִיר", ["i l m u d iː r"]), "مدير": ("מֻדִיר", ["m u d iː r"]), - "عنّك": ("עַנַّ-כּ", ["ʕ a n n a k"]), "تسوق": ("תְסוּק", ["t s uː ʔ", "t s uː q"]), "ابوس": ("אַבּוּס", ["ʔ a b uː s"]), - "احفادي": ("אַחְפַאד-י", ["ʔ a ħ f aː d i"]), "معلّمتها": ("מְעַלְّמֵת-הַא", ["m ʕ a l l m i t h a"]), - "يروحو": ("יִרוּחוּ", ["j r uː ħ u", "i r uː ħ u", "j i r uː ħ u"]), "فهمتك#f": ("פְהִמְתֵ-כּ", ["f h i m t i k"]), - "افهمك#f": ("אַפְהַמֵ-כּ", ["ʔ a f h a m i k"]), "تعطي": ("תֵעַטִי", ["t a ʕ tˤ i", "t i ʕ tˤ i"]), "علبة": ("עִלְבֵּה", ["ʕ i l b e"]), - "تنقل": ("תֻנְקֹל", ["t u n ʔ u l", "t u n q u l"]), - # the falafel story - "الماضي": ("אֵלְמַאדִֿי", ["i l m aː dˤ i"]), "علي": ("עַלִי", ["ʕ a l i"]), "صاحب#of": ("צַאחֵבּ", ["sˤ aː ħ i b"]), - "الاخبار": ("אֵלְאַחְ'בַּאר", ["i l ʔ a x b aː r"]), "كيفك": ("כִּיפַ-כּ", ["k iː f a k"]), "نحمده": ("נִחֵמְד-וֹ", ["n i ħ m d o", "n i ħ i m d o"]), - "حبّة#of": ("חַבֵّת", ["ħ a b b i t"]), "احضّرلك": ("אַחַדֵֿّר-לַכּ", ["ʔ a ħ a dˤ dˤ i r l a k"]), "حضّرلي": ("חַדֵֿّר-לִי", ["ħ a dˤ dˤ i r l i"]), - "ساندويشة": ("סַנְדְוִישֵה", ["s a n d w iː ʃ e"]), "ساندويشة#of": ("סַנְדְוִישֵת", ["s a n d w iː ʃ i t"]), - "سلطة#of": ("סַלַטַת", ["s a l a tˤ a t"]), "ملفوف": ("מַלְפוּף", ["m a l f uː f"]), "مكابيس": ("מַכַּאבִּיס", ["m a k aː b iː s"]), - "شيبس": ("שִבְּס", ["ʃ i b s"]), "طحينة": ("טְחִינֵה", ["tˤ ħ iː n e"]), "شطّة": ("שַטַّה", ["ʃ a tˤ tˤ a"]), - "العمبة": ("אֵלְעַמְבַּה", ["i l ʕ a m b a"]), "راسي": ("רַאס-י", ["r aː s i"]), - "تاخدها": ("תֹאחְ'ד-הַא", ["t oː x d h a", "t aː x u d h a", "t oː x i d h a"]), - "قنّينة#of": ("קַנִّינֵת", ["ʔ a n n iː n i t", "q a n n iː n i t"]), "صحّة": ("צַחַّה", ["sˤ a ħ ħ a"]), - "قلبك": ("קַלְבַּ-כּ", ["ʔ a l b a k", "q a l b a k"]), "نطبخلك": ("נֻטְבֹּח'-לַכּ", ["n u tˤ b u x l a k"]), - "جيبلي": ("גִ'יבּ-לִי", ["dʒ iː b l i"]), "تومة": ("ת'וֹמֵה", ["t oː m e"]), "فرحنا": ("פַ-רֻחְנַא", ["f a r u ħ n a"]), - "تسألنيش": ("תִסְאַלְ-נִיש", ["t i s ʔ a l n iː ʃ"]), "حسب": ("חַסַבּ", ["ħ a s a b"]), "رايي": ("רַאיִ-י", ["r aː j i"]), - "يابا": ("יַאבַּّא", ["j aː b a", "j a b b a"]), "ازورك": ("אַזוּרַ-כּ", ["ʔ a z uː r a k"]), - # ---- level 2, lesson 3 (the workbook's twelfth) ---- - # the clock - "السّاعة": ("אֵ(ל)סֵّיעַה", ["i s s eː ʕ a", "i s s aː ʕ a"]), "تنتين": ("תִנְתֵין", ["t i n t eː n"]), - "تلت": ("תֻלְת", ["t u l t", "t u l i t", "s u l s"]), "ارباع": ("אַרְבַּאע", ["ʔ a r b aː ʕ"]), - "دقايق": ("דַקַאיֵק", ["d a ʔ aː j i ʔ", "d a q aː j i q"]), "ايّ": ("אַיְ", ["ʔ a j j", "ʔ a j"]), "لحدّ": ("לַחַדّ", ["l a ħ a d d"]), - "الّا": ("אִלַّא", ["ʔ i l l a"]), "تعالو": ("תַעַאלוּ", ["t a ʕ aː l u"]), "بسيدر": ("בְּסִידֵר", ["b s iː d e r", "b s e d e r"]), - "بالزّبط": ("בִּ(ל)זַّבֵּט", ["b i z z a b i tˤ", "b i z z a b tˤ", "b i ðˤ ðˤ a b tˤ"]), - "يخلّيك": ("יְחַ'לִّי-כּ", ["j x a l l iː k", "i x a l l iː k", "j i x a l l iː k"]), "عفوا": ("עַפְוַן", ["ʕ a f w a n"]), - "للجماعة": ("לַלְגַ'מַאעַה", ["l a l dʒ a m aː ʕ a"]), "شهرين": ("שַהְרֵין", ["ʃ a h r eː n"]), "اشهر": ("אַשְהֹר", ["ʔ a ʃ h u r"]), - "اشتريله": ("אַשְתְרִי-לוֹ", ["ʔ a ʃ t r iː l o"]), "البنطلون": ("אֵלְבַּנְטַלוֹן", ["i l b a n tˤ a l oː n"]), "انّي": ("אִנִّי", ["ʔ i n n i"]), - "شبّاك#of": ("שֻבַּّאכּ", ["ʃ u b b aː k"]), "يحكيلي": ("יִחְכִּי-לִי", ["j i ħ k iː l i"]), - "ياخدها": ("יֹאחְ'ד-הַא", ["j oː x d h a", "j aː x u d h a", "j oː x i d h a"]), "حفلة": ("חַפְלֵה", ["ħ a f l e"]), - "بالعالم": ("בִּלְעַאלַם", ["b i l ʕ aː l a m"]), "الطّريق": ("אֵ(ל)טַّרִיק", ["i tˤ tˤ a r iː ʔ", "i tˤ tˤ a r iː q"]), - "حضّرتي": ("חַדַֿّרְתִי", ["ħ a dˤ dˤ a r t i"]), "لابوكي": ("לַאַבּוּכִּי", ["l a ʔ a b uː k i"]), - "الممرّضة": ("אֵלְמֻמַרִّדַֿה", ["i l m u m a r r i dˤ a"]), "فاتت#f": ("פַאתַת", ["f aː t a t"]), "ايمتا": ("אֵימְתַא", ["ʔ eː m t a"]), - "مكتبك": ("מַכְּתַבַּ-כּ", ["m a k t a b a k"]), "الرّجل": ("אֵ(ל)רֻّגֻ'ל", ["i r r u dʒ u l", "i r r a dʒ u l"]), - "الفواكه": ("אֵלְפַוַאכֵּה", ["i l f a w aː k e", "i l f a w aː k i h"]), "للبيّاعة": ("לַלְבַּיַّאעַה", ["l a l b a j j aː ʕ a"]), - "البيّاعة": ("אֵלְבַּיַّאעַה", ["i l b a j j aː ʕ a"]), "صبح": ("צֻבֵּח", ["sˤ u b i ħ", "sˤ u b ħ"]), "الخضرة": ("אֵלְחֻ'דְֿרַה", ["i l x u dˤ r a"]), - "تحت": ("תַחְת", ["t a ħ t", "t a ħ i t"]), "سمعتني": ("סְמִעְתְ-נִי", ["s m i ʕ t n i", "s m i ʕ i t n i"]), - "المسا": ("אֵלְמַסַא", ["i l m a s a"]), "ماشي": ("מַאשִי", ["m aː ʃ i"]), "يالله": ("יַאללַّה", ["j a l l a", "j aː l l a"]), - "عيد": ("עִיד", ["ʕ iː d"]), "ميلاد": ("מִילַאד", ["m iː l aː d"]), "اوصل": ("אַוְצַל", ["ʔ a w sˤ a l", "ʔ oː sˤ a l"]), - "غدا": ("עַ'דַא", ["ɣ a d a"]), "مربوط": ("מַרְבּוּט", ["m a r b uː tˤ"]), "تسألك#f": ("תִסְאַלֵ-כּ", ["t i s ʔ a l i k"]), - "قوليلها": ("קוּלִי-לְהַא", ["ʔ uː l iː l h a", "q uː l iː l h a"]), "المكسور": ("אֵלְמַכְּסוּר", ["i l m a k s uː r"]), - "مكسور": ("מַכְּסוּר", ["m a k s uː r"]), "مكسورة": ("מַכְּסוּרַה", ["m a k s uː r a"]), "كسره": ("כַּסַר-וֹ", ["k a s a r o"]), - "شفناك": ("שֻפְנַא-כּ", ["ʃ u f n aː k"]), - # בניין שני, פַעַّל - "نزّل": ("נַזַّל", ["n a z z a l"]), "ينزّل": ("יְנַזֵّל", ["j n a z z i l", "i n a z z i l", "j i n a z z i l"]), - "وصّل": ("וַצַّל", ["w a sˤ sˤ a l"]), "يوصّل": ("יְוַצֵّל", ["j w a sˤ sˤ i l", "i w a sˤ sˤ i l", "j i w a sˤ sˤ i l"]), - "ضحّك": ("דַֿחַّכּ", ["dˤ a ħ ħ a k"]), "يضحّك": ("יְדַֿחֵّכּ", ["j dˤ a ħ ħ i k", "i dˤ a ħ ħ i k", "j i dˤ a ħ ħ i k"]), - "لبّس": ("לַבַּّס", ["l a b b a s"]), "يلبّس": ("יְלַבֵּّס", ["j l a b b i s", "i l a b b i s", "j i l a b b i s"]), - "سمّع": ("סַמַّע", ["s a m m a ʕ"]), "يسمّع": ("יְסַמֵّע", ["j s a m m i ʕ", "i s a m m i ʕ", "j i s a m m i ʕ"]), - "رجّع": ("רַגַّ'ע", ["r a dʒ dʒ a ʕ"]), "يرجّع": ("יְרַגֵّ'ע", ["j r a dʒ dʒ i ʕ", "i r a dʒ dʒ i ʕ", "j i r a dʒ dʒ i ʕ"]), - "فهّم": ("פַהַّם", ["f a h h a m"]), "يفهّم": ("יְפַהֵّם", ["j f a h h i m", "i f a h h i m", "j i f a h h i m"]), - "طلّع": ("טַלַّע", ["tˤ a l l a ʕ"]), "يطلّع": ("יְטַלֵّע", ["j tˤ a l l i ʕ", "i tˤ a l l i ʕ", "j i tˤ a l l i ʕ"]), - "شرّب": ("שַרַّבּ", ["ʃ a r r a b"]), "يشرّب": ("יְשַרֵّבּ", ["j ʃ a r r i b", "i ʃ a r r i b", "j i ʃ a r r i b"]), - "لبّست": ("לַבַּّסֵת", ["l a b b a s t", "l a b b a s i t"]), "لبّستي": ("לַבַּّסְתִי", ["l a b b a s t i"]), "لبّست#f": ("לַבַּّסַת", ["l a b b a s a t"]), - "لبّسنا": ("לַבַּّסְנַא", ["l a b b a s n a"]), "لبّستو": ("לַבַּّסְתוּ", ["l a b b a s t u"]), "لبّسو": ("לַבַּّסוּ", ["l a b b a s u"]), - "رجّعت": ("רַגַّ'עֵת", ["r a dʒ dʒ a ʕ t", "r a dʒ dʒ a ʕ i t"]), "رجّعت#f": ("רַגַّ'עַת", ["r a dʒ dʒ a ʕ a t"]), - "طلّعت": ("טַלַّעֵת", ["tˤ a l l a ʕ t", "tˤ a l l a ʕ i t"]), "طلّعتي": ("טַלַّעְתִי", ["tˤ a l l a ʕ t i"]), - "اطلّعها": ("אַטַלֵّע-הַא", ["ʔ a tˤ a l l i ʕ h a"]), "تطلّع": ("תְטַלֵّע", ["t tˤ a l l i ʕ", "tˤ tˤ a l l i ʕ", "t i tˤ a l l i ʕ"]), - "سمّعنا": ("סַמַّעְנַא", ["s a m m a ʕ n a"]), "سمّعت": ("סַמַّעֵת", ["s a m m a ʕ t", "s a m m a ʕ i t"]), "سمّعتي": ("סַמַّעְתִי", ["s a m m a ʕ t i"]), - "سمّعتها#f": ("סַמַّעַת-הַא", ["s a m m a ʕ a t h a"]), "نزّلت#f": ("נַזַّלַת", ["n a z z a l a t"]), "نزّلتي": ("נַזַّלְתִי", ["n a z z a l t i"]), - "نزّلها": ("נַזַّל-הַא", ["n a z z a l h a"]), "فهّمه": ("פַהַّמ-וֹ", ["f a h h a m o"]), "فهّمني": ("פַהַّמְ-נִי", ["f a h h a m n i"]), - "فهّمت": ("פַהַّמֵת", ["f a h h a m t", "f a h h a m i t"]), "فهّمتها#f": ("פַהַّמַת-הַא", ["f a h h a m a t h a"]), - "تفهّموني": ("תְפַהְّמוּ-נִי", ["t f a h h m uː n i", "t f a h h i m uː n i"]), "لبّستها#f": ("לַבַּّסַת-הַא", ["l a b b a s a t h a"]), - "لبّسته#f": ("לַבַּّסַת-וֹ", ["l a b b a s a t o"]), "شرّبت": ("שַרַّבֵּת", ["ʃ a r r a b t", "ʃ a r r a b i t"]), - "شرّبني": ("שַרַّבְּ-נִי", ["ʃ a r r a b n i"]), "فشرّبته": ("פַ-שַרַّבְּת-וֹ", ["f a ʃ a r r a b t o"]), - "شرّبناهم": ("שַרַّבְּנַא-הֹם", ["ʃ a r r a b n aː h o m", "ʃ a r r a b n aː h u m"]), "وصّلتي": ("וַצַّלְתִי", ["w a sˤ sˤ a l t i"]), - "وصّلت": ("וַצַّלֵת", ["w a sˤ sˤ a l t", "w a sˤ sˤ a l i t"]), "وصّلت#f": ("וַצַّלַת", ["w a sˤ sˤ a l a t"]), "وصّلني": ("וַצַّל-נִי", ["w a sˤ sˤ a l n i"]), - "ضحّكنا": ("דַֿחַّכּ-נַא", ["dˤ a ħ ħ a k n a"]), "يضحّكه": ("יְדַֿחְّכּ-וֹ", ["j dˤ a ħ ħ k o", "i dˤ a ħ ħ k o", "j dˤ a ħ ħ i k o"]), - "ضحّكتني": ("דַֿחַّכְּתְ-נִי", ["dˤ a ħ ħ a k t n i", "dˤ a ħ ħ a k i t n i"]), "ضحّكناها": ("דַֿחַّכְּנַא-הַא", ["dˤ a ħ ħ a k n aː h a"]), - "ضحّكتيني": ("דַֿחַّכְּתִי-נִי", ["dˤ a ħ ħ a k t iː n i"]), - "لاختي": ("לַאֹחְ'ת-י", ["l a ʔ u x t i"]), "البسكليت": ("אֵלְבִּסִכְּלֵית", ["i l b i s i k l eː t", "i l b i s k l eː t"]), - "الموبايل": ("אֵלְמוֹבַּיֵל", ["i l m oː b aː j i l", "i l m oː b aː j l"]), "غصبن": ("עַ'צְבֵּן", ["ɣ a sˤ b i n", "ɣ a sˤ b a n"]), - "عنّها": ("עַנְّ-הַא", ["ʕ a n n h a", "ʕ a n h a"]), "باردة": ("בַּארְדֵה", ["b aː r d e"]), "افهم": ("אַפְהַם", ["ʔ a f h a m"]), - "عالبيت": ("עַלְבֵּית", ["ʕ a l b eː t"]), "دير": ("דִיר", ["d iː r"]), "بالك": ("בַּאלַ-כּ", ["b aː l a k"]), - "هلّق": ("הַלַّק", ["h a l l a ʔ", "h a l l a q"]), "الكلبة": ("אֵלְכַּלְבֵּה", ["i l k a l b e"]), "البنّيّة": ("אֵלְבֻּנִّיֵّה", ["i l b u n n i j j e"]), - "اللّيلة": ("אֵ(ל)לֵّילֵה", ["i l l eː l e"]), "قالت#f": ("קַאלַת", ["ʔ aː l a t", "q aː l a t"]), "المديرة": ("אֵלְמֻדִירַה", ["i l m u d iː r a"]), - "البرتقاليّة": ("אֵלְבֻּרְתֻקַאלִיֵّה", ["i l b u r t u ʔ aː l i j j e", "i l b u r t u q aː l i j j e"]), - "باخدها": ("בַּאחֹ'ד-הַא", ["b aː x u d h a", "b oː x u d h a"]), "غنوتين": ("עִ'נְוְתֵין", ["ɣ i n w t eː n", "ɣ i n i w t eː n"]), - "اغاني": ("אַעַ'אנִי", ["ʔ a ɣ aː n i"]), "جزداني": ("גֻ'זְדַאנ-י", ["dʒ u z d aː n i"]), "فطلع": ("פַ-טִלֵע", ["f a tˤ i l i ʕ"]), - "الكاسة": ("אֵלְכַּאסֵה", ["i l k aː s e"]), "قلتلك#f": ("קֻלְתִ-לֵّכּ", ["ʔ u l t i l l i k", "q u l t i l l i k"]), - # the song شرّبني قهوة: a request is the future without its first letter (شرِّبني), not the past (شرَّبني) - "شرّبني#imp": ("שַרֵّבְּ-נִי", ["ʃ a r r i b n i"]), "فتّح#imp": ("פַתֵّח", ["f a t t i ħ"]), "رجّع#imp": ("רַגֵّ'ע", ["r a dʒ dʒ i ʕ"]), - "ولّع#imp": ("וַלֵّע", ["w a l l i ʕ"]), "عيّط#imp": ("עַיֵّט", ["ʕ a j j i tˤ"]), "النّخوة": ("אֵ(ל)נַّחְ'וֵה", ["i n n a x w e"]), - "احساسي": ("אִחְסַאס-י", ["ʔ i ħ s aː s i"]), "جيب": ("גִ'יבּ", ["dʒ iː b"]), "سيجارة": ("סִיגַארַה", ["s iː ɡ aː r a", "s iː dʒ aː r a"]), - "ايّاها": ("אִיַّאהַא", ["ʔ i j j aː h a"]), "للجارة": ("לַלְגַ'ארַה", ["l a l dʒ aː r a"]), "معاها": ("מַעַאהַא", ["m a ʕ aː h a"]), - "احكيلها": ("אַחְכִּי-לְהַא", ["ʔ a ħ k iː l h a"]), "ببالي": ("בִּבַּאל-י", ["b i b aː l i"]), "تيضلّ": ("תַ-יְדַֿלّ", ["t a j dˤ a l l", "t a i dˤ a l l"]), - "مرتاح": ("מִרְתַאח", ["m i r t aː ħ"]), "بسرعة": ("בְּסֻרְעַה", ["b s u r ʕ a", "b i s u r ʕ a"]), "قدرتي": ("קְדִרְתִי", ["ʔ d i r t i", "q d i r t i"]), - "تنسينا": ("תִנְסִי-נַא", ["t i n s iː n a"]), "بعلمي": ("בְּעִלְמ-י", ["b ʕ i l m i", "b i ʕ i l m i"]), "زمان": ("זַמַאן", ["z a m aː n"]), - "تحبّينا": ("תְחִבִּّי-נַא", ["t ħ i b b iː n a"]), "يالّي": ("יַלִّי", ["j a l l i"]), "غير": ("עֵ'יר", ["ɣ eː r"]), - "هالشي": ("הַ(א)לְשִי", ["h a ʃ ʃ i", "h a l ʃ i"]), "تقدر": ("תִקְדַר", ["t i ʔ d a r", "t i q d a r"]), - "تخلفها": ("תִחְ'לִפ-הַא", ["t i x l i f h a"]), "مهما": ("מַהְמַא", ["m a h m a"]), "جرى": ("גַ'רַה", ["dʒ a r a"]), - "تبعدنا": ("תִבְּעֵד-נַא", ["t i b ʕ i d n a"]), "الهوا": ("אֵלְהַוַא", ["i l h a w a"]), "عشقنا": ("עַשַקְנַא", ["ʕ a ʃ a ʔ n a", "ʕ a ʃ a q n a"]), - "نزعل": ("נִזְעַל", ["n i z ʕ a l"]), "له": ("לוֹ", ["l o"]), - # ---- level 2, lesson 4 (the workbook's thirteenth) ---- - # five more of בניין שני - "صلّح": ("צַלַّח", ["sˤ a l l a ħ"]), "يصلّح": ("יְצַלֵّח", ["j sˤ a l l i ħ", "i sˤ a l l i ħ", "j i sˤ a l l i ħ"]), - "خبّر": ("חַ'בַּّר", ["x a b b a r"]), "يخبّر": ("יְחַ'בֵּّר", ["j x a b b i r", "i x a b b i r", "j i x a b b i r"]), - "حضّر": ("חַדַֿّר", ["ħ a dˤ dˤ a r"]), "يحضّر": ("יְחַדֵֿّר", ["j ħ a dˤ dˤ i r", "i ħ a dˤ dˤ i r", "j i ħ a dˤ dˤ i r"]), - "غنّى": ("עַ'נַّא", ["ɣ a n n a"]), "يغنّي": ("יְעַ'נִّי", ["j ɣ a n n i", "i ɣ a n n i", "j i ɣ a n n i"]), - "فكّر": ("פַכַּّר", ["f a k k a r"]), "يفكّر": ("יְפַכֵּّר", ["j f a k k i r", "i f a k k i r", "j i f a k k i r"]), - "فكّرت": ("פַכַּّרֵת", ["f a k k a r t", "f a k k a r i t"]), "فكّرتي": ("פַכַּّרְתִי", ["f a k k a r t i"]), "فكّرت#f": ("פַכַּّרַת", ["f a k k a r a t"]), - "فكّرنا": ("פַכַּّרְנַא", ["f a k k a r n a"]), "فكّرتو": ("פַכַּّרְתוּ", ["f a k k a r t u"]), "فكّرو": ("פַכַּّרוּ", ["f a k k a r u"]), - "غنّيت": ("עַ'נֵّית", ["ɣ a n n eː t"]), "غنّيتي": ("עַ'נֵّיתִי", ["ɣ a n n eː t i"]), "غنّت": ("עַ'נַّת", ["ɣ a n n a t"]), - "غنّينا": ("עַ'נֵّינַא", ["ɣ a n n eː n a"]), "غنّيتو": ("עַ'נֵّיתוּ", ["ɣ a n n eː t u"]), "غنّو": ("עַ'נّוּ", ["ɣ a n n u"]), - "افكّر": ("אַפַכֵּّר", ["ʔ a f a k k i r"]), "تفكّر": ("תְפַכֵּّר", ["t f a k k i r", "t i f a k k i r"]), "فكّرتك": ("פַכַּّרְתַ-כּ", ["f a k k a r t a k"]), - "اغنّي": ("אַעַ'נִّי", ["ʔ a ɣ a n n i"]), "نغنّي": ("נְעַ'נִّי", ["n ɣ a n n i", "n i ɣ a n n i"]), "تغنّي": ("תְעַ'נִّי", ["t ɣ a n n i", "t i ɣ a n n i"]), - "مغنّي": ("מֻעַ'נִّי", ["m u ɣ a n n i"]), - "صلّحت": ("צַלַّחֵת", ["sˤ a l l a ħ t", "sˤ a l l a ħ i t"]), "صلّحها": ("צַלַّח-הַא", ["sˤ a l l a ħ h a"]), "صلّحته": ("צַלַّחְת-וֹ", ["sˤ a l l a ħ t o"]), - "نصلّح": ("נְצַלֵّח", ["n sˤ a l l i ħ", "n i sˤ a l l i ħ"]), "اصلّح": ("אַצַלֵّח", ["ʔ a sˤ a l l i ħ"]), "اصلّحها": ("אַצַלְّח-הַא", ["ʔ a sˤ a l l ħ a", "ʔ a sˤ a l l i ħ h a"]), - "يصلّحها": ("יְצַלְّח-הַא", ["j sˤ a l l ħ a", "i sˤ a l l i ħ h a", "j sˤ a l l i ħ h a"]), - "خبّرني": ("חַ'בַּّר-נִי", ["x a b b a r n i"]), "خبّرني#imp": ("חַ'בֵּّר-נִי", ["x a b b i r n i"]), "خبّرت": ("חַ'בַּّרֵת", ["x a b b a r t", "x a b b a r i t"]), - "خبّرنا": ("חַ'בַּّר-נַא", ["x a b b a r n a"]), "خبّرتك#f": ("חַ'בַּّרְתֵ-כּ", ["x a b b a r t i k"]), "خبّرك": ("חַ'בַּّרַ-כּ", ["x a b b a r a k"]), - "خبّره": ("חַ'בַּّר-וֹ", ["x a b b a r o"]), "خبّرتني": ("חַ'בַּّרְתְ-נִי", ["x a b b a r t n i", "x a b b a r i t n i"]), - "اخبّر": ("אַחַ'בֵּّר", ["ʔ a x a b b i r"]), "اخبّره": ("אַחַ'בְּّר-וֹ", ["ʔ a x a b b r o", "ʔ a x a b b i r o"]), - "اخبّرك": ("אַחַ'בְּّרַ-כּ", ["ʔ a x a b b r a k", "ʔ a x a b b i r a k"]), "تخبّرني": ("תְחַ'בֵּّר-נִי", ["t x a b b i r n i", "t i x a b b i r n i"]), - "حضّرت": ("חַדַֿّרֵת", ["ħ a dˤ dˤ a r t", "ħ a dˤ dˤ a r i t"]), "حضّرت#f": ("חַדַֿّרַת", ["ħ a dˤ dˤ a r a t"]), "حضّرنا": ("חַדַֿّרְנַא", ["ħ a dˤ dˤ a r n a"]), - "نحضّر": ("נְחַדֵֿّר", ["n ħ a dˤ dˤ i r", "n i ħ a dˤ dˤ i r"]), "احضّر": ("אַחַדֵֿّר", ["ʔ a ħ a dˤ dˤ i r"]), - "تحضّرلي": ("תְחַדֵֿّר-לִי", ["t ħ a dˤ dˤ i r l i", "t i ħ a dˤ dˤ i r l i"]), "حضّرتلك": ("חַדַֿّרְת-לַכּ", ["ħ a dˤ dˤ a r t i l l a k", "ħ a dˤ dˤ a r t l a k"]), - "حضّرتها": ("חַדַֿّרְת-הַא", ["ħ a dˤ dˤ a r t h a"]), - "شكرته#f": ("שַכְּרַת-וֹ", ["ʃ a k r a t o"]), "مروان": ("מַרְוַאן", ["m a r w aː n"]), "الاكل": ("אֵלְאַכֵּל", ["i l ʔ a k i l", "i l ʔ a k l"]), - "عالطّريق": ("עַ-טַّרִיק", ["ʕ a tˤ tˤ a r iː ʔ", "ʕ a tˤ tˤ a r iː q"]), "مشكلة": ("מֻשְכִּלֵה", ["m u ʃ k i l e"]), - "المشكلة": ("אֵלְמֻשְכִּלֵה", ["i l m u ʃ k i l e"]), "اجي": ("אַגִ'י", ["ʔ a dʒ i", "ʔ aː dʒ i"]), "خلاص": ("חַ'לַאץ", ["x a l aː sˤ"]), - "مبسوطين": ("מַבְּסוּטִין", ["m a b s uː tˤ iː n"]), "وضع": ("וַדֵֿע", ["w a dˤ i ʕ", "w a dˤ ʕ"]), "العب": ("אַלְעַבּ", ["ʔ a l ʕ a b"]), - "تدفعلي": ("תִדְפַע-לִי", ["t i d f a ʕ l i"]), "دفعتيله": ("דַפַעְתִי-לוֹ", ["d a f a ʕ t iː l o"]), "مريم": ("מַרְיַם", ["m a r j a m"]), - "البيضا": ("אֵלְבֵּידַֿא", ["i l b eː dˤ a"]), "المقهى": ("אֵלְמַקְהַא", ["i l m a ʔ h a", "i l m a q h a"]), - "اسمحيلي": ("אִסְמַחִי-לִי", ["ʔ i s m a ħ iː l i"]), "اسمحلي": ("אִסְמַח-לִי", ["ʔ i s m a ħ l i"]), "الجار": ("אֵלְגַ'אר", ["i l dʒ aː r"]), - "نطبخ": ("נֻטְבֹּח'", ["n u tˤ b u x"]), "مطبوخة": ("מַטְבּוּחַ'ה", ["m a tˤ b uː x a"]), "المصاري": ("אֵלְמַצַארִי", ["i l m a sˤ aː r i"]), - # انّه, ما, الّي - "موجودين": ("מַוְג'וּדִין", ["m a w dʒ uː d iː n"]), "كسرتها": ("כַּסַרְת-הַא", ["k a s a r t h a"]), "ليتر": ("לִיתֵר", ["l iː t i r", "l iː t e r"]), - "البلفون": ("אֵלְבֵּלֵפוֹן", ["i l b e l e f oː n", "i l b i l i f oː n"]), "كلامي": ("כַּלַאמ-י", ["k a l aː m i"]), - "اقولّك": ("אַקוּל-לַّכּ", ["ʔ a ʔ uː l l a k", "ʔ a q uː l l a k"]), "ناكلها": ("נֹאכֹּל-הַא", ["n oː k u l h a", "n aː k u l h a"]), - "تاكلو": ("תֹאכְּלוּ", ["t oː k l u", "t aː k l u", "t oː k u l u"]), "لسيّارة#of": ("לַסַיַّארֵת", ["l a s a j j aː r i t"]), - "قريبة": ("קַרִיבֵּה", ["ʔ a r iː b e", "q a r iː b e"]), "اشوفك": ("אַשוּפַ-כּ", ["ʔ a ʃ uː f a k"]), "اشتريته": ("אִשְתַרֵית-וֹ", ["ʔ i ʃ t a r eː t o"]), - "بنطلوني": ("בַּנְטַלוֹנ-י", ["b a n tˤ a l oː n i"]), "بنطلونك": ("בַּנְטַלוֹנַ-כּ", ["b a n tˤ a l oː n a k"]), "افتح": ("אַפְתַח", ["ʔ a f t a ħ"]), - "ناس": ("נַאס", ["n aː s"]), "زيّك": ("זַיַّ-כּ", ["z a j j a k"]), "بخير": ("בְּחֵ'יר", ["b x eː r", "b i x eː r"]), "خير": ("חֵ'יר", ["x eː r"]), - "يجيب": ("יְגִ'יבּ", ["j dʒ iː b", "i dʒ iː b", "j i dʒ iː b"]), "اجيب": ("אַגִ'יבּ", ["ʔ a dʒ iː b"]), "نجيب": ("נְגִ'יבּ", ["n dʒ iː b", "n i dʒ iː b"]), - "لنا": ("לַנַא", ["l a n a"]), "المكتوب": ("אֵלְמַכְּתוּבּ", ["i l m a k t uː b"]), "نبعثله": ("נִבְּעַת'-לוֹ", ["n i b ʕ a t l o"]), - "تصير": ("תְצִיר", ["t sˤ iː r", "t i sˤ iː r"]), "حدّ": ("חַדّ", ["ħ a d d"]), "كسرته#f": ("כַּסְרַת-וֹ", ["k a s r a t o"]), - "المثل": ("אֵלְמַתַ'ל", ["i l m a t a l", "i l m a s a l"]), "لجوزي": ("לַג'וֹז-י", ["l a dʒ oː z i"]), "تحبّ": ("תְחִבّ", ["t ħ i b b"]), - "اكون": ("אַכּוּן", ["ʔ a k uː n"]), "قالتلهم": ("קַאלַת-לְהֹם", ["ʔ aː l a t l h o m", "q aː l a t l h o m", "ʔ aː l a t i l h o m"]), - "قالّك": ("קַאל-לַّכּ", ["ʔ aː l l a k", "q aː l l a k"]), "زرتني": ("זֻרְתְ-נִי", ["z u r t n i", "z u r i t n i"]), "اعطتك": ("אַעְטַת-כּ", ["ʔ a ʕ tˤ a t a k"]), - "رهط": ("רַהַט", ["r a h a tˤ"]), "اخدته#f": ("אַחַ'דַת-וֹ", ["ʔ a x a d a t o"]), "شفناها": ("שֻפְנַא-הַא", ["ʃ u f n aː h a"]), - # the phone call - "ايّاه": ("אִיַّאה", ["ʔ i j j aː h"]), "نهار": ("נְהַאר", ["n h aː r"]), "نهارك": ("נְהַארַ-כּ", ["n h aː r a k"]), "خد": ("חֹ'ד", ["x u d", "x o d"]), - "راحة": ("רַאחַה", ["r aː ħ a"]), "راحتك": ("רַאחְתַ-כּ", ["r aː ħ t a k"]), "قلم": ("קַלַם", ["ʔ a l a m", "q a l a m"]), - "اقلام": ("אַקְלַאם", ["ʔ a ʔ l aː m", "ʔ a q l aː m"]), "ضروري": ("דַֿרוּרִי", ["dˤ a r uː r i"]), "عاش": ("עַאש", ["ʕ aː ʃ"]), - "صوتك": ("צוֹתַ-כּ", ["sˤ oː t a k"]), "الو": ("הַלוֹ", ["h a l o", "ʔ a l o"]), "رقمه": ("רַקְמ-וֹ", ["r a ʔ m o", "r a q m o"]), - "ايّام": ("אַיַّאם", ["ʔ a j j aː m"]), "لمحمّد": ("לַמְחַמַّד", ["l a m ħ a m m a d"]), "تعطيني": ("תַעְטִי-נִי", ["t a ʕ tˤ iː n i"]), - "فش": ("פִש", ["f i ʃ"]), "ثواني": ("תַ'וַאנִי", ["t a w aː n i", "s a w aː n i"]), "صفرين": ("צִפְרֵין", ["sˤ i f r eː n"]), - "آلاف": ("אַלַאף", ["ʔ aː l aː f", "t aː l aː f"]), "اي": ("אַיְ", ["ʔ a j", "ʔ eː"]), "نعم": ("נַעַם", ["n a ʕ a m"]), - "عزيزي": ("עַזִיז-י", ["ʕ a z iː z i"]), "شادي": ("שַאדִי", ["ʃ aː d i"]), "صحّ": ("צַחّ", ["sˤ a ħ ħ"]), - "بتغدّى": ("בַּתְעַ'דַّא", ["b a t ɣ a d d a"]), # "I": بـ on اتغدّى, which loses its ا before the ت - # ---- level 2, lesson 5 (the workbook's fourteenth) ---- - # גזרת ע"י - "زاد": ("זַאד", ["z aː d"]), "يزيد": ("יְזִיד", ["j z iː d", "i z iː d", "j i z iː d"]), "جاب": ("גַ'אבּ", ["dʒ aː b"]), - "باع": ("בַּאע", ["b aː ʕ"]), "يبيع": ("יְבִּיע", ["j b iː ʕ", "i b iː ʕ", "j i b iː ʕ"]), "صار": ("צַאר", ["sˤ aː r"]), - "يصير": ("יְצִיר", ["j sˤ iː r", "i sˤ iː r", "j i sˤ iː r"]), "وقّف": ("וַקַّף", ["w a ʔ ʔ a f", "w a q q a f"]), - "يوقّف": ("יְוַקֵّף", ["j w a ʔ ʔ i f", "i w a ʔ ʔ i f", "j w a q q i f", "i w a q q i f"]), "وقّف#imp": ("וַקֵّף", ["w a ʔ ʔ i f", "w a q q i f"]), - "وقّفت": ("וַקַّפֵת", ["w a ʔ ʔ a f t", "w a ʔ ʔ a f i t", "w a q q a f t", "w a q q a f i t"]), - "استنّى": ("אִסְתַנַّא", ["ʔ i s t a n n a", "s t a n n a"]), "يستنّى": ("יִסְתַנַّא", ["j i s t a n n a"]), "استنّى#i": ("אַסְתַנַّא", ["ʔ a s t a n n a"]), - "استنّى#imp": ("סְתַנַّא", ["s t a n n a", "ʔ i s t a n n a"]), "استنّيتك": ("אִסְתַנֵّיתַ-כּ", ["ʔ i s t a n n eː t a k", "s t a n n eː t a k"]), - "استنّيتك#f": ("סְתַנֵّיתֵ-כּ", ["s t a n n eː t i k", "ʔ i s t a n n eː t i k"]), "استنّيته": ("סְתַנֵّית-וֹ", ["s t a n n eː t o", "ʔ i s t a n n eː t o"]), - "استنّاك": ("אַסְתַנַّא-כּ", ["ʔ a s t a n n aː k"]), "استنّاكم": ("אַסְתַנַّא-כֹּם", ["ʔ a s t a n n aː k o m", "ʔ a s t a n n aː k u m"]), - "يستنّاني": ("יִסְתַנַّא-נִי", ["j i s t a n n aː n i"]), - "جبت": ("גִ'בֵּת", ["dʒ i b t", "dʒ i b i t"]), "جبتي": ("גִ'בְּתִי", ["dʒ i b t i"]), "جبنا": ("גִ'בְּנַא", ["dʒ i b n a"]), - "جبتو": ("גִ'בְּתוּ", ["dʒ i b t u"]), "جابو": ("גַ'אבּוּ", ["dʒ aː b u"]), - "صرت": ("צִרֵת", ["sˤ i r t", "sˤ i r i t"]), "صرتي": ("צִרְתִי", ["sˤ i r t i"]), "صارت": ("צַארַת", ["sˤ aː r a t"]), - "صرنا": ("צִרְנַא", ["sˤ i r n a"]), "صرتو": ("צִרְתוּ", ["sˤ i r t u"]), "صارو": ("צַארוּ", ["sˤ aː r u"]), - "تجيب": ("תְגִ'יבּ", ["t dʒ iː b", "t i dʒ iː b"]), "تجيبي": ("תְגִ'יבִּי", ["t dʒ iː b i", "t i dʒ iː b i"]), - "تجيبو": ("תְגִ'יבּוּ", ["t dʒ iː b u", "t i dʒ iː b u"]), "يجيبو": ("יְגִ'יבּוּ", ["j dʒ iː b u", "i dʒ iː b u", "j i dʒ iː b u"]), - "اصير": ("אַצִיר", ["ʔ a sˤ iː r"]), "تصيري": ("תְצִירִי", ["t sˤ iː r i", "t i sˤ iː r i"]), "نصير": ("נְצִיר", ["n sˤ iː r", "n i sˤ iː r"]), - "تصيرو": ("תְצִירוּ", ["t sˤ iː r u", "t i sˤ iː r u"]), "يصيرو": ("יְצִירוּ", ["j sˤ iː r u", "i sˤ iː r u", "j i sˤ iː r u"]), - "صارلك": ("צַאר-לַכּ", ["sˤ aː r l a k"]), "صارلك#f": ("צַאר-לֵכּ", ["sˤ aː r l i k"]), "صارلي": ("צַאר-לִי", ["sˤ aː r l i"]), - "صارله": ("צַאר-לוֹ", ["sˤ aː r l o"]), "موسى": ("מוּסַא", ["m uː s a"]), "مهندس": ("מֻהַנְדֵס", ["m u h a n d i s"]), - "يزيدلي": ("יְזִיד-לִי", ["j z iː d l i", "i z iː d l i", "j i z iː d l i"]), "اقولّه": ("אַקוּל-לّוֹ", ["ʔ a ʔ uː l l o", "ʔ a q uː l l o"]), - "بعت": ("בִּעֵת", ["b i ʕ t", "b i ʕ i t"]), "بعتي": ("בִּעְתִי", ["b i ʕ t i"]), "اخدتها": ("אַחַ'דְת-הַא", ["ʔ a x a d t h a", "ʔ a x a t t h a"]), - "ابيعها": ("אַבִּיע-הַא", ["ʔ a b iː ʕ h a"]), "يبيعها": ("יְבִּיע-הַא", ["j b iː ʕ h a", "i b iː ʕ h a", "j i b iː ʕ h a"]), - "تبيعي": ("תְבִּיעִי", ["t b iː ʕ i", "t i b iː ʕ i"]), "لاختها": ("לַאֹחְ'ת-הַא", ["l a ʔ u x t h a"]), - "تجيبلنا": ("תְגִ'יבּ-לְנַא", ["t dʒ iː b l n a", "t dʒ iː b i l n a"]), "هدايا": ("הַדַאיַא", ["h a d aː j a"]), - "زدت": ("זִדֵת", ["z i d t", "z i d i t", "z i t t"]), "زدتي": ("זִדְתִי", ["z i d t i", "z i t t i"]), "ازيد": ("אַזִיד", ["ʔ a z iː d"]), - "تزيد": ("תְזִיד", ["t z iː d", "d z iː d", "t i z iː d"]), "تزيدي": ("תְזִידִי", ["t z iː d i", "d z iː d i", "t i z iː d i"]), - "ملح": ("מִלֵח", ["m i l i ħ", "m i l ħ"]), "للاكل": ("לַלְאַכֵּל", ["l a l ʔ a k i l", "l a l ʔ a k l"]), "لاكلك#f": ("לַאַכְּלֵ-כּ", ["l a ʔ a k l i k"]), - "جبتيلي": ("גִ'בְּתִי-לִי", ["dʒ i b t iː l i"]), "الكتاب": ("(א)לִכְּתַאבּ", ["l i k t aː b", "i l k t aː b"]), - "حكيتيلي": ("חַכֵּיתִי-לִי", ["ħ a k eː t iː l i"]), "تجيبيلي": ("תְגִ'יבִּי-לִי", ["t dʒ iː b iː l i"]), "للفلافل": ("לַלְפַלַאפֵל", ["l a l f a l aː f i l"]), - "تجيبلي": ("תְגִ'יבּ-לִי", ["t dʒ iː b l i"]), "لابوك": ("לַאַבּוּכּ", ["l a ʔ a b uː k"]), "اواعي": ("אַוַאעִי", ["ʔ a w aː ʕ i"]), - "تزورني": ("תְזוּר-נִי", ["t z uː r n i", "d z uː r n i"]), "زيت": ("זֵית", ["z eː t"]), "للسّيّارة": ("לַ(ל)סַّיַّארַה", ["l a s s a j j aː r a"]), - "احسنلك#f": ("אַחְסַן-לֵכּ", ["ʔ a ħ s a n l i k"]), "بقدّيش": ("בְּקַדֵّיש", ["b ʔ a d d eː ʃ", "b q a d d eː ʃ", "b i ʔ a d d eː ʃ"]), - "للكوسا": ("לַלְכּוּסַא", ["l a l k uː s a"]), "المحشي": ("אֵלְמַחְשִי", ["i l m a ħ ʃ i"]), "يجيبلك": ("יְגִ'יבּ-לַכּ", ["j dʒ iː b l a k", "i dʒ iː b l a k"]), - "دكتورة": ("דֻכְּתוֹרַה", ["d u k t oː r a"]), "جبتلك#f": ("גִ'בְּת-לֵכּ", ["dʒ i b t i l l i k", "dʒ i b t l i k"]), "عنها": ("עַנְ-הַא", ["ʕ a n h a"]), - "اجيبلك#f": ("אַגִ'יבּ-לֵכּ", ["ʔ a dʒ iː b l i k"]), "اليونان": ("אֵלְיוּנַאן", ["i l j uː n aː n"]), - # the garage story - "صاحب": ("צַאחֵבּ", ["sˤ aː ħ i b"]), "اصحاب": ("אַצְחַאבּ", ["ʔ a sˤ ħ aː b"]), "الارض": ("אֵלְאַרְדֿ", ["i l ʔ a r dˤ"]), - "عريس": ("עַרִיס", ["ʕ a r iː s"]), "العريس": ("אֵלְעַרִיס", ["i l ʕ a r iː s"]), "عروسة": ("עַרוּסֵה", ["ʕ a r uː s e"]), - "العروس": ("אֵלְעַרוּס", ["i l ʕ a r uː s"]), "مشاكل": ("מַשַאכֵּל", ["m a ʃ aː k i l"]), - "الماتور": ("אֵלְמַאתוֹר", ["i l m aː t oː r"]), "بالماتور": ("בִּלְמַאתוֹר", ["b i l m aː t oː r"]), "لمبة": ("לַמְבַּה", ["l a m b a"]), - "لمبة#of": ("לַמְבֵּת", ["l a m b i t", "l a m b e t"]), "تعمل": ("תִעְמַל", ["t i ʕ m a l"]), "الجنب": ("אֵלְגַ'נְבּ", ["i l dʒ a n b", "i l dʒ a n i b"]), - "وسام": ("וִסַאם", ["w i s aː m"]), "كراج": ("כַּרַאג'", ["k a r aː dʒ"]), "كراجه": ("כַּרַאג'-וֹ", ["k a r aː dʒ o"]), "عنّا": ("עִנַّא", ["ʕ i n n a"]), - "اساعدك": ("אַסַאעְדַ-כּ", ["ʔ a s aː ʕ d a k", "ʔ a s aː ʕ i d a k"]), "بينفع": ("בִּינְפַע", ["b i n f a ʕ", "b j i n f a ʕ"]), - "اخدني": ("אַחַ'ד-נִי", ["ʔ a x a d n i"]), "اخدتني#f": ("אַחַ'דַת-נִי", ["ʔ a x a d a t n i"]), "اخده": ("אַחַ'ד-וֹ", ["ʔ a x a d o"]), - "منشانك": ("מִנְשַאנַ-כּ", ["m i n ʃ aː n a k"]), "وينتا": ("וֵינְתַא", ["w eː n t a"]), "يشتغل": ("יִשְתְעֵ'ל", ["j i ʃ t ɣ i l", "j i ʃ t i ɣ i l"]), - "الاحمر": ("אֵלְאַחְמַר", ["i l ʔ a ħ m a r"]), "ياخدك": ("יֹאחְ'דַ-כּ", ["j oː x d a k", "j aː x d a k", "j oː x u d a k"]), - "بسيّارتك": ("בְּסַיַّארְתַ-כּ", ["b s a j j aː r t a k", "b i s a j j aː r t a k"]), "الوجبة": ("אֵלְוַגְ'בֵּה", ["i l w a dʒ b e"]), - "تدفع": ("תִדְפַע", ["t i d f a ʕ"]), "محلّك": ("מַחַלַّ-כּ", ["m a ħ a l l a k"]), "احترام": ("אִחְתִרַאם", ["ʔ i ħ t i r aː m"]), - "دفعته": ("דַפַעְת-וֹ", ["d a f a ʕ t o"]), "البنشر": ("אֵלְבַּנְשַר", ["i l b a n ʃ a r"]), "اخدناها": ("אַחַ'דְנַא-הַא", ["ʔ a x a d n aː h a"]), - "رجّعلي": ("רַגַّ'ע-לִי", ["r a dʒ dʒ a ʕ l i"]), "قالولي": ("קַאלוּ-לִי", ["ʔ aː l uː l i", "q aː l uː l i"]), - "يصلّحه": ("יְצַלְّח-וֹ", ["j sˤ a l l ħ o", "i sˤ a l l ħ o", "j sˤ a l l i ħ o"]), - # an adjective's plural - "طوال": ("טְוַאל", ["tˤ w aː l"]), "قصار": ("קְצַאר", ["ʔ sˤ aː r", "q sˤ aː r"]), "كبار": ("כְּבַּאר", ["k b aː r"]), "صغار": ("צְעַ'אר", ["sˤ ɣ aː r"]), - "كتيرة": ("כְּתִירֵה", ["k t iː r e"]), "كتار": ("כְּתַאר", ["k t aː r"]), "جداد": ("גְ'דַאד", ["dʒ d aː d"]), - "وينك#m": ("וֵינַ-כּ", ["w eː n a k"]), "سألتك#2f": ("סַאַלְתֵ-כּ", ["s a ʔ a l t i k"]), # to a man; I asked her (سألتك#f is "she asked you") - "الطّاولات": ("אֵ(ל)טַّאוְלַאת", ["i tˤ tˤ aː w l aː t"]), "شقق": ("שֻקַק", ["ʃ u ʔ a ʔ", "ʃ u q a q"]), - # ---- level 2, lesson 6 (the workbook's fifteenth) ---- - # בניין שלישי - "ساعد": ("סַאעַד", ["s aː ʕ a d"]), "يساعد": ("יְסַאעֵד", ["j s aː ʕ i d", "i s aː ʕ i d", "j i s aː ʕ i d"]), - "جاوب": ("גַ'אוַבּ", ["dʒ aː w a b"]), "يجاوب": ("יְגַ'אוֵבּ", ["j dʒ aː w i b", "i dʒ aː w i b", "j i dʒ aː w i b"]), - "عالج": ("עַאלַג'", ["ʕ aː l a dʒ"]), "يعالج": ("יְעַאלֵג'", ["j ʕ aː l i dʒ", "i ʕ aː l i dʒ", "j i ʕ aː l i dʒ"]), - "لاقى": ("לַאקַא", ["l aː ʔ a", "l aː q a"]), "يلاقي": ("יְלַאקִי", ["j l aː ʔ i", "i l aː ʔ i", "j l aː q i", "i l aː q i"]), - "ساوى": ("סַאוַא", ["s aː w a"]), "يساوي": ("יְסַאוִי", ["j s aː w i", "i s aː w i", "j i s aː w i"]), - "ساعدت": ("סַאעַדֵת", ["s aː ʕ a d t", "s aː ʕ a d i t", "s aː ʕ a t t"]), "ساعدتي": ("סַאעַדְתִי", ["s aː ʕ a d t i", "s aː ʕ a t t i"]), - "ساعدت#f": ("סַאעַדַת", ["s aː ʕ a d a t"]), "ساعدنا": ("סַאעַדְנַא", ["s aː ʕ a d n a"]), "ساعدتو": ("סַאעַדְתוּ", ["s aː ʕ a d t u", "s aː ʕ a t t u"]), - "ساعدو": ("סַאעַדוּ", ["s aː ʕ a d u"]), - "لاقيت": ("לַאקֵית", ["l aː ʔ eː t", "l aː q eː t"]), "لاقيتي": ("לַאקֵיתִי", ["l aː ʔ eː t i", "l aː q eː t i"]), "لاقت": ("לַאקַת", ["l aː ʔ a t", "l aː q a t"]), - "لاقينا": ("לַאקֵינַא", ["l aː ʔ eː n a", "l aː q eː n a"]), "لاقيتو": ("לַאקֵיתוּ", ["l aː ʔ eː t u", "l aː q eː t u"]), "لاقو": ("לַאקוּ", ["l aː ʔ u", "l aː q u"]), - "لاقاها": ("לַאקַא-הַא", ["l aː ʔ aː h a", "l aː q aː h a"]), - "اساعد": ("אַסַאעֵד", ["ʔ a s aː ʕ i d"]), "تساعد": ("תְסַאעֵד", ["t s aː ʕ i d", "t i s aː ʕ i d"]), "تساعدي": ("תְסַאעְדִי", ["t s aː ʕ d i", "t s aː ʕ i d i"]), - "نساعد": ("נְסַאעֵד", ["n s aː ʕ i d", "n i s aː ʕ i d"]), "تساعدو": ("תְסַאעְדוּ", ["t s aː ʕ d u", "t s aː ʕ i d u"]), - "يساعدو": ("יְסַאעְדוּ", ["j s aː ʕ d u", "i s aː ʕ d u", "j s aː ʕ i d u"]), - "يساعدني": ("יְסַאעֵד-נִי", ["j s aː ʕ i d n i", "i s aː ʕ i d n i"]), "تساعدني": ("תְסַאעֵד-נִי", ["t s aː ʕ i d n i"]), - "يساعده": ("יְסַאעְד-וֹ", ["j s aː ʕ d o", "i s aː ʕ d o", "j s aː ʕ i d o"]), "تساعديها": ("תְסַאעְדִי-הַא", ["t s aː ʕ d iː h a", "t s aː ʕ i d iː h a"]), - "تساعدهم": ("תְסַאעֵד-הֹם", ["t s aː ʕ i d h o m", "t s aː ʕ i d h u m"]), "ساعدني#imp": ("סַאעֵד-נִי", ["s aː ʕ i d n i"]), - "الاقي": ("אַלַאקִי", ["ʔ a l aː ʔ i", "ʔ a l aː q i"]), "تلاقي": ("תְלַאקִי", ["t l aː ʔ i", "t l aː q i", "t i l aː ʔ i"]), - "نلاقي": ("נְלַאקִי", ["n l aː ʔ i", "n l aː q i", "n i l aː ʔ i"]), "تلاقو": ("תְלַאקוּ", ["t l aː ʔ u", "t l aː q u", "t i l aː ʔ u"]), - "يلاقو": ("יְלַאקוּ", ["j l aː ʔ u", "i l aː ʔ u", "j l aː q u", "i l aː q u"]), - "اعالج": ("אַעַאלֵג'", ["ʔ a ʕ aː l i dʒ"]), "عالجتني#f": ("עַאלַגַ'ת-נִי", ["ʕ aː l a dʒ a t n i"]), "تعالجني": ("תְעַאלֵג'-נִי", ["t ʕ aː l i dʒ n i"]), - "عالجتك#she": ("עַאלַגַ'תַ-כּ", ["ʕ aː l a dʒ a t a k"]), "تعالجك": ("תְעַאלְגַ'-כּ", ["t ʕ aː l dʒ a k", "t ʕ aː l i dʒ a k"]), - "عالجها": ("עַאלַג'-הַא", ["ʕ aː l a dʒ h a"]), "يعالجها": ("יְעַאלֵג'-הַא", ["j ʕ aː l i dʒ h a", "i ʕ aː l i dʒ h a"]), - "يعالجه": ("יְעַאלְג'-וֹ", ["j ʕ aː l dʒ o", "i ʕ aː l dʒ o", "j ʕ aː l i dʒ o"]), "يعالجو": ("יְעַאלְג'וּ", ["j ʕ aː l dʒ u", "i ʕ aː l dʒ u", "j ʕ aː l i dʒ u"]), - "ساوينا": ("סַאוֵינַא", ["s aː w eː n a"]), "ساويت": ("סַאוֵית", ["s aː w eː t"]), "ساوت": ("סַאוַת", ["s aː w a t"]), - "اساوي": ("אַסַאוִי", ["ʔ a s aː w i"]), "تساوي": ("תְסַאוִי", ["t s aː w i", "t i s aː w i"]), "نساوي": ("נְסַאוִי", ["n s aː w i", "n i s aː w i"]), - "جاوبتك#f": ("גַ'אוַבְּתֵ-כּ", ["dʒ aː w a b t i k"]), "اجاوبك#f": ("אַגַ'אוְבֵּ-כּ", ["ʔ a dʒ aː w b i k", "ʔ a dʒ aː w i b i k"]), - "جاوبتيني": ("גַ'אוַבְּתִי-נִי", ["dʒ aː w a b t iː n i"]), "تجاوبيني": ("תְגַ'אוְבִּי-נִי", ["t dʒ aː w b iː n i", "t dʒ aː w i b iː n i"]), - "جاوبته": ("גַ'אוַבְּת-וֹ", ["dʒ aː w a b t o"]), "اجاوبه": ("אַגַ'אוְבּ-וֹ", ["ʔ a dʒ aː w b o", "ʔ a dʒ aː w i b o"]), - "جاوبني": ("גַ'אוַבּ-נִי", ["dʒ aː w a b n i"]), "يجاوبني": ("יְגַ'אוֵבּ-נִי", ["j dʒ aː w i b n i", "i dʒ aː w i b n i"]), - "جاوبنا": ("גַ'אוַבְּנַא", ["dʒ aː w a b n a"]), "نجاوبها": ("נְגַ'אוֵבּ-הַא", ["n dʒ aː w i b h a"]), - "جاوبتني#f": ("גַ'אוַבַּת-נִי", ["dʒ aː w a b a t n i"]), "تجاوبني": ("תְגַ'אוֵבּ-נִי", ["t dʒ aː w i b n i"]), "جاوبه": ("גַ'אוַבּ-וֹ", ["dʒ aː w a b o"]), - "شكرني": ("שַכַּר-נִי", ["ʃ a k a r n i"]), "اسألك#f": ("אַסְאַלֵ-כּ", ["ʔ a s ʔ a l i k"]), "اسأله": ("אַסְאַל-וֹ", ["ʔ a s ʔ a l o"]), - "اسألها": ("אַסְאַל-הַא", ["ʔ a s ʔ a l h a"]), "تسألنا": ("תִסְאַל-נַא", ["t i s ʔ a l n a"]), - "بالآخر": ("בִּלְאַאחֵ'ר", ["b i l ʔ aː x i r"]), "معروف": ("מַעְרוּף", ["m a ʕ r uː f"]), "للمعلّم": ("לַלְמְעַלֵّם", ["l a l m ʕ a l l i m"]), - "شغل": ("שֻעֻ'ל", ["ʃ u ɣ u l", "ʃ u ɣ l"]), "اشتراها": ("אִשְתַרַא-הַא", ["ʔ i ʃ t a r aː h a"]), "الجواب": ("אֵלְגַ'וַאבּ", ["i l dʒ a w aː b"]), - "اسئلة": ("אַסְאִלֵה", ["ʔ a s ʔ i l e"]), "نصيبك": ("נַצִיבַּ-כּ", ["n a sˤ iː b a k"]), "البيطري": ("אֵלְבֵּיטַרִי", ["i l b eː tˤ a r i"]), - "هالاسبوع": ("הַלְאֻסְבּוּע", ["h a l ʔ u s b uː ʕ"]), "اكيد": ("אַכִּיד", ["ʔ a k iː d"]), "النّاس": ("אֵ(ל)נַّאס", ["i n n aː s"]), - # the old jar - "طريق": ("טַרִיק", ["tˤ a r iː ʔ", "tˤ a r iː q"]), "طرق": ("טֻרֻק", ["tˤ u r u ʔ", "tˤ u r u q"]), "رجعة": ("רַגְ'עַה", ["r a dʒ ʕ a"]), - "ارض": ("אַרְדֿ", ["ʔ a r dˤ"]), "صراحة": ("צַרַאחַה", ["sˤ a r aː ħ a"]), "بصراحة": ("בְּצַרַאחַה", ["b sˤ a r aː ħ a", "b i sˤ a r aː ħ a"]), - "صريح": ("צַרִיח", ["sˤ a r iː ħ"]), "غطا": ("עַ'טַא", ["ɣ a tˤ a"]), "غطاه": ("עַ'טַא-ה", ["ɣ a tˤ aː h"]), "غطاها": ("עַ'טַא-הַא", ["ɣ a tˤ aː h a"]), - "يعرف": ("יִעְרַף", ["j i ʕ r a f"]), "البير": ("אֵלְבִּיר", ["i l b iː r"]), "طنجرة": ("טַנְגַ'רַה", ["tˤ a n dʒ a r a"]), - "فرحت": ("פַ-רֻחֵת", ["f a r u ħ t", "f a r u ħ i t"]), "يقولوله": ("יְקוּלוּ-לוֹ", ["j ʔ uː l uː l o", "i ʔ uː l uː l o", "j q uː l uː l o"]), - "عين": ("עֵין", ["ʕ eː n"]), "شجرة#of": ("שַגַ'רֵת", ["ʃ a dʒ a r i t", "ʃ a dʒ a r e t"]), "التّمر": ("אֵ(ל)תַّמֵר", ["i t t a m i r", "i t t a m r"]), - "النّخلة": ("אֵ(ל)נַّחְ'לֵה", ["i n n a x l e"]), "لنفسه": ("לַנַפְס-וֹ", ["l a n a f s o"]), "حفر": ("חַפַר", ["ħ a f a r"]), - "جرّة": ("גַ'רַّה", ["dʒ a r r a"]), "الجرّة": ("אֵלְגַ'רַّה", ["i l dʒ a r r a"]), "قديمة": ("קַדִימֵה", ["ʔ a d iː m e", "q a d iː m e"]), - "اعمل": ("אַעְמַל", ["ʔ a ʕ m a l"]), "شفتها": ("שֻפְת-הַא", ["ʃ u f t h a"]), "قاله": ("קַאל-וֹ", ["ʔ aː l o", "q aː l o", "ʔ aː l l o"]), - "جيرانه": ("גִ'ירַאנ-וֹ", ["dʒ iː r aː n o"]), "فؤاد": ("פֻאַאד", ["f u ʔ aː d"]), "عرفات": ("עַרַפַאת", ["ʕ a r a f aː t"]), - "يشتريها": ("יִשְתְרִי-הַא", ["j i ʃ t r iː h a"]), "منّك": ("מִנַّ-כּ", ["m i n n a k"]), "فوصل": ("פַ-וִצֵל", ["f a w i sˤ i l"]), - "الحلوة": ("אֵלְחִלְוֵה", ["i l ħ i l w e"]), "عتيقة": ("עַתִיקַה", ["ʕ a t iː ʔ a", "ʕ a t iː q a"]), "رخيصة": ("רְחִ'יצַה", ["r x iː sˤ a"]), - "غالية": ("עַ'אלְיֵה", ["ɣ aː l j e"]), "لكن": ("לַכִּן", ["l a k i n", "l aː k i n"]), "يدفعله": ("יִדְפַע-לוֹ", ["j i d f a ʕ l o"]), - "دينار": ("דִינַאר", ["d iː n aː r"]), "يرجعله": ("יִרְגַ'ע-לוֹ", ["j i r dʒ a ʕ l o"]), "زوجته": ("זוֹגְ'ת-וֹ", ["z oː dʒ t o"]), - "المسكين": ("אֵלְמִסְכִּין", ["i l m i s k iː n"]), "بعثته#f": ("בַּעְתַ'ת-וֹ", ["b a ʕ t a t o"]), "دفعله": ("דַפַע-לוֹ", ["d a f a ʕ l o"]), - "للبيت": ("לַלְבֵּית", ["l a l b eː t"]), "ربّه": ("רַבّ-וֹ", ["r a b b o"]), "اعطاه": ("אַעְטַא-ה", ["ʔ a ʕ tˤ aː h"]), "لهم": ("לַהֹם", ["l a h o m", "l a h u m"]), - "القصّة": ("אֵלְקִצַّה", ["i l ʔ i sˤ sˤ a", "i l q i sˤ sˤ a"]), "يخلّيها": ("יְחַ'לִّי-הַא", ["j x a l l iː h a", "i x a l l iː h a"]), - "ياخدو": ("יֹאחְ'דוּ", ["j oː x d u", "j aː x d u", "j oː x u d u"]), "عمل": ("עִמֵל", ["ʕ i m i l"]), "لاهلها": ("לַאַהֵל-הַא", ["l a ʔ a h i l h a", "l a ʔ a h l h a"]), - # "him" after a vowel: a ه, and the stress moves to the end - "اكلتيها": ("אַכַּלְתִי-הַא", ["ʔ a k a l t iː h a"]), "شفتوها": ("שֻפְתוּ-הַא", ["ʃ u f t uː h a"]), "فتحناها": ("פַתַחְנַא-הַא", ["f a t a ħ n aː h a"]), - "شافوها": ("שַאפוּ-הַא", ["ʃ aː f uː h a"]), "اكلتيه": ("אַכַּלְתִי-ה", ["ʔ a k a l t iː h", "ʔ a k a l t iː"]), "شفتوه": ("שֻפְתוּ-ה", ["ʃ u f t uː h", "ʃ u f t uː"]), - "فتحناه": ("פַתַחְנַא-ה", ["f a t a ħ n aː h", "f a t a ħ n aː"]), "شافوه": ("שַאפוּ-ה", ["ʃ aː f uː h", "ʃ aː f uː"]), - "يخلّيلك": ("יְחַ'לִّי-לַכּ", ["j x a l l iː l a k", "i x a l l iː l a k"]), "اولادك": ("אַוְלַאדַ-כּ", ["ʔ a w l aː d a k"]), - "يخلّيه": ("יְחַ'לִّי-ה", ["j x a l l iː h", "i x a l l iː h", "j x a l l iː"]), - # ---- level 2, lesson 7 (the workbook's sixteenth) ---- - # the imperative: the future without its first letter, and a helping ا where two consonants would open the word - "فوت": ("פוּת", ["f uː t"]), "فكّر#imp": ("פַכֵּّר", ["f a k k i r"]), "فكّري#imp": ("פַכְּّרִי", ["f a k k r i", "f a k k i r i"]), - "اسمع#imp": ("אִסְמַע", ["ʔ i s m a ʕ"]), "اسمعي#imp": ("אִסְמַעִי", ["ʔ i s m a ʕ i"]), "اسمعو#imp": ("אִסְמַעוּ", ["ʔ i s m a ʕ u"]), - "اشرب#imp": ("אִשְרַבּ", ["ʔ i ʃ r a b"]), "اشربو#imp": ("אִשְרַבּוּ", ["ʔ i ʃ r a b u"]), "ادفع#imp": ("אִדְפַע", ["ʔ i d f a ʕ"]), - "ادفعي#imp": ("אִדְפַעִי", ["ʔ i d f a ʕ i"]), "ارجع#imp": ("אִרְגַ'ע", ["ʔ i r dʒ a ʕ"]), "اقعد#imp": ("אֻקְעֹד", ["ʔ u ʔ ʕ u d", "ʔ u q ʕ u d"]), - "احكي#imp": ("אִחְכִּי", ["ʔ i ħ k i"]), "احكو#imp": ("אִחְכּוּ", ["ʔ i ħ k u"]), "اشتري#imp": ("אִשְתְרִי", ["ʔ i ʃ t r i", "ʔ i ʃ t i r i"]), - "اركض#imp": ("אֻרְכֹּדֿ", ["ʔ u r k u dˤ"]), "اركضي#imp": ("אֻרֻכְּדִֿי", ["ʔ u r u k dˤ i", "ʔ u r k u dˤ i"]), - "اركضو#imp": ("אֻרֻכְּדֿוּ", ["ʔ u r u k dˤ u", "ʔ u r k u dˤ u"]), "خدي": ("חֹ'דִי", ["x u d i", "x o d i"]), "خدو": ("חֹ'דוּ", ["x u d u", "x o d u"]), - "كل": ("כֹּל", ["k u l", "k o l"]), "كلي": ("כֹּלִי", ["k u l i", "k o l i"]), "كلو": ("כֹּלוּ", ["k u l u", "k o l u"]), "زيرو": ("זִירוֹ", ["z iː r o"]), - "انجحي#imp": ("אִנְגַ'חִי", ["ʔ i n dʒ a ħ i"]), "انسى#imp": ("אִנְסַא", ["ʔ i n s a"]), "انسو#imp": ("אִנְסוּ", ["ʔ i n s u"]), - "اعمل#imp": ("אִעְמַל", ["ʔ i ʕ m a l"]), "العب#imp": ("אִלְעַבּ", ["ʔ i l ʕ a b"]), "ساعديني#imp": ("סַאעְדִי-נִי", ["s aː ʕ d iː n i", "s aː ʕ i d iː n i"]), - "ساعدوني#imp": ("סַאעְדוּ-נִי", ["s aː ʕ d uː n i", "s aː ʕ i d uː n i"]), "شوفوني": ("שוּפוּ-נִי", ["ʃ uː f uː n i"]), "سوقها": ("סוּק-הַא", ["s uː ʔ h a", "s uː q h a"]), - # more of פִעֵל, and the story's verbs - "نسي": ("נִסִי", ["n i s i"]), "ينسى": ("יִנְסַא", ["j i n s a"]), "نجح": ("נִגֵ'ח", ["n i dʒ i ħ"]), "ينجح": ("יִנְגַ'ח", ["j i n dʒ a ħ"]), - "يعمل": ("יִעְמַל", ["j i ʕ m a l"]), "زعل": ("זִעֵל", ["z i ʕ i l"]), "يزعل": ("יִזְעַל", ["j i z ʕ a l"]), "وقع": ("וִקֵע", ["w i ʔ i ʕ", "w i q i ʕ"]), - "يوقع": ("יוֹקַע", ["j oː ʔ a ʕ", "j oː q a ʕ"]), "ركب": ("רִכֵּבּ", ["r i k i b"]), "يركب": ("יִרְכַּבּ", ["j i r k a b"]), - "صرخ": ("צַרַח'", ["sˤ a r a x"]), "يصرخ": ("יֻצְרֹח'", ["j u sˤ r u x"]), "جرح": ("גַ'רַח", ["dʒ a r a ħ"]), "يجرح": ("יִגְ'רַח", ["j i dʒ r a ħ"]), - "بكى": ("בַּכַּא", ["b a k a"]), "يبكي": ("יִבְּכִּי", ["j i b k i"]), - "زعلت": ("זְעִלֵת", ["z ʕ i l t", "z ʕ i l i t"]), "زعلتي": ("זְעִלְתִי", ["z ʕ i l t i"]), "زعلت#f": ("זִעְלַת", ["z i ʕ l a t"]), - "زعلنا": ("זְעִלְנַא", ["z ʕ i l n a"]), "زعلتو": ("זְעִלְתוּ", ["z ʕ i l t u"]), "زعلو": ("זִעְלוּ", ["z i ʕ l u"]), - "نسيت": ("נְסִית", ["n s iː t"]), "نسيتي": ("נְסִיתִי", ["n s iː t i"]), "نسيت#f": ("נִסְיַת", ["n i s j a t"]), "نسينا": ("נְסִינַא", ["n s iː n a"]), - "نسيتو": ("נְסִיתוּ", ["n s iː t u"]), "نسيو": ("נִסְיוּ", ["n i s j u"]), - "ازعل": ("אַזְעַל", ["ʔ a z ʕ a l"]), "تزعل": ("תִזְעַל", ["t i z ʕ a l"]), "تزعلي": ("תִזְעַלִי", ["t i z ʕ a l i"]), "تزعلو": ("תִזְעַלוּ", ["t i z ʕ a l u"]), - "يزعلو": ("יִזְעַלוּ", ["j i z ʕ a l u"]), "تنسى": ("תִנְסַא", ["t i n s a"]), "تنسو": ("תִנְסוּ", ["t i n s u"]), "ينسو": ("יִנְסוּ", ["j i n s u"]), - "نجحتي": ("נְגִ'חְתִי", ["n dʒ i ħ t i"]), "تنجحي": ("תִנְגַ'חִי", ["t i n dʒ a ħ i"]), "وقعت": ("וְקִעֵת", ["w ʔ i ʕ t", "w ʔ i ʕ i t", "w q i ʕ t", "w q i ʕ i t"]), - "توقع": ("תוֹקַע", ["t oː ʔ a ʕ", "t oː q a ʕ"]), "عملت": ("עְמִלֵת", ["ʕ m i l t", "ʕ m i l i t"]), "عملتله": ("עְמִלְת-לוֹ", ["ʕ m i l t i l l o", "ʕ m i l t l o"]), - "عملو": ("עִמְלוּ", ["ʕ i m l u"]), "تعملي": ("תִעְמַלִי", ["t i ʕ m a l i"]), - "عارف": ("עַארֵף", ["ʕ aː r i f"]), "يخبّرها": ("יְחַ'בֵּّר-הַא", ["j x a b b i r h a", "i x a b b i r h a"]), "خبّرتني#f": ("חַ'בַּّרַת-נִי", ["x a b b a r a t n i"]), - "تخبّروني": ("תְחַ'בְּّרוּ-נִי", ["t x a b b r uː n i", "t x a b b i r uː n i"]), "لصاحبك": ("לַצַאחְבַּ-כּ", ["l a sˤ aː ħ b a k"]), - "احسنلك": ("אַחְסַן-לַכּ", ["ʔ a ħ s a n l a k"]), "بحياتك": ("בְּחַיַאתַ-כּ", ["b ħ a j aː t a k", "b i ħ a j aː t a k"]), "تقول": ("תְקוּל", ["t ʔ uː l", "t q uː l"]), - "انّك": ("אִנַّ-כּ", ["ʔ i n n a k"]), "كذّاب": ("כַּזַّאבּ", ["k a z z aː b", "k a ð ð aː b"]), "وحياتكم": ("וִחְיַאתְ-כֹּם", ["w i ħ j aː t k o m", "w i ħ j aː t k u m"]), - "الامتحان": ("אֵלְאִמְתִחַאן", ["i l ʔ i m t i ħ aː n"]), "تروحي": ("תְרוּחִי", ["t r uː ħ i"]), "الموضوع": ("אֵלְמַוְדֿוּע", ["i l m a w dˤ uː ʕ"]), - "بالموضوع": ("בִּלְמַוְדֿוּע", ["b i l m a w dˤ uː ʕ"]), "تيجيني": ("תִיגִ'י-נִי", ["t iː dʒ iː n i"]), "تحكو": ("תִחְכּוּ", ["t i ħ k u"]), - "تجاوبكم": ("תְגַ'אוֵבּ-כֹּם", ["t dʒ aː w i b k o m", "t dʒ aː w i b k u m"]), "تدفعي": ("תִדְפַעִי", ["t i d f a ʕ i"]), "تدفعو": ("תִדְפַעוּ", ["t i d f a ʕ u"]), - "خلّصت": ("חַ'לַّצֵת", ["x a l l a sˤ t", "x a l l a sˤ i t"]), "فضلك#f": ("פַדְֿלֵ-כּ", ["f a dˤ l i k"]), "راسك": ("רַאסַ-כּ", ["r aː s a k"]), - "شنطتها": ("שַנְטֵת-הַא", ["ʃ a n tˤ i t h a", "ʃ a n tˤ e t h a"]), "اكلك#f": ("אַכְּלֵ-כּ", ["ʔ a k l i k"]), "القديمة": ("אֵלְקַדִימֵה", ["i l ʔ a d iː m e", "i l q a d iː m e"]), - "لصاحب#of": ("לַצַאחֵבּ", ["l a sˤ aː ħ i b"]), "لولا": ("לַוַלַא", ["l a w a l a"]), "لجاري": ("לַגַ'אר-י", ["l a dʒ aː r i"]), - "تشوفوها": ("תְשוּפוּ-הַא", ["t ʃ uː f uː h a"]), "تشوفوني": ("תְשוּפוּ-נִי", ["t ʃ uː f uː n i"]), "تفكّري": ("תְפַכְּّרִי", ["t f a k k r i", "t f a k k i r i"]), - "تسوقها": ("תְסוּק-הַא", ["t s uː ʔ h a", "t s uː q h a"]), "تحكيلي": ("תִחְכִּי-לִי", ["t i ħ k iː l i"]), - # the bus story - "نفسي": ("נַפְס-י", ["n a f s i"]), "نفسه": ("נַפְס-וֹ", ["n a f s o"]), "انسان": ("אִנְסַאן", ["ʔ i n s aː n"]), "خطّ": ("חַ'טّ", ["x a tˤ tˤ"]), - "خطوط": ("חְ'טוּט", ["x tˤ uː tˤ"]), "مليان": ("מַלְיַאן", ["m a l j aː n"]), "مليانة": ("מַלְיַאנֵה", ["m a l j aː n e"]), - "عالآخر": ("עַלְאַאחֵ'ר", ["ʕ a l ʔ aː x i r"]), "محطّة": ("מַחַטַّה", ["m a ħ a tˤ tˤ a"]), "محطّة#of": ("מַחַטֵّת", ["m a ħ a tˤ tˤ i t", "m a ħ a tˤ tˤ e t"]), - "المحطّة": ("אֵלְמַחַטַّה", ["i l m a ħ a tˤ tˤ a"]), "بالمحطّة": ("בִּלְמַחַטַّה", ["b i l m a ħ a tˤ tˤ a"]), "البنزين": ("אֵלְבַּנְזִין", ["i l b a n z iː n"]), - "لبيتك": ("לַבֵּיתַ-כּ", ["l a b eː t a k"]), "مهندسة": ("מֻהַנְדִסֵה", ["m u h a n d i s e"]), "ازورك#f": ("אַזוּרֵ-כּ", ["ʔ a z uː r i k"]), - "دقيقتين": ("דַקִיקְתֵין", ["d a ʔ iː ʔ t eː n", "d a q iː q t eː n"]), "تستنّيني": ("תִסְתַנִّי-נִי", ["t i s t a n n iː n i"]), - "ادفعلها": ("אַדְפַע-לְהַא", ["ʔ a d f a ʕ l h a", "ʔ a d f a ʕ i l h a"]), "قويّة": ("קַוִיֵّה", ["ʔ a w i j j e", "q a w i j j e"]), "دمّ": ("דַםّ", ["d a m m"]), - "ايده": ("אִיד-וֹ", ["ʔ iː d o"]), "بسته": ("בֻּסְת-וֹ", ["b u s t o"]), "طبريّا": ("טַבַּרִיַّא", ["tˤ a b a r i j j a"]), - "يزوره": ("יְזוּר-וֹ", ["j z uː r o", "i z uː r o"]), "شافه": ("שַאפ-וֹ", ["ʃ aː f o"]), "يحكيله": ("יִחְכִּי-לוֹ", ["j i ħ k iː l o"]), - "يخبّره": ("יְחַ'בְּّר-וֹ", ["j x a b b r o", "i x a b b r o", "j x a b b i r o"]), "حياته": ("חַיַאת-וֹ", ["ħ a j aː t o"]), - "الكرمل": ("אֵלְכַּרְמֵל", ["i l k a r m e l", "i l k a r m i l"]), "باص": ("בַּאץ", ["b aː sˤ"]), "روحي#imp": ("רוּחִי", ["r uː ħ i"]), - "باصات": ("בַּאצַאת", ["b aː sˤ aː t"]), "عالباص": ("עַלְבַּאץ", ["ʕ a l b aː sˤ"]), "مفرق": ("מַפְרַק", ["m a f r a ʔ", "m a f r a q"]), - "فرديس": ("פַרְדִיס", ["f a r d iː s"]), "تسع": ("תִסְע", ["t i s i ʕ", "t i s ʕ"]), "باليوم": ("בִּלְיוֹם", ["b i l j oː m"]), - "بالنّاس": ("בִּ(ל)נַّאס", ["b i n n aː s"]), "اسوّي": ("אַסַוִّי", ["ʔ a s a w w i"]), "اسّا": ("אִסַّא", ["ʔ i s s a"]), "قبلي": ("קַבְּל-י", ["ʔ a b l i", "q a b l i"]), - "محلّه": ("מַחַלّ-וֹ", ["m a ħ a l l o"]), "كمجروح": ("כַּמַגְ'רוּח", ["k a m a dʒ r uː ħ"]), "كمجروحة": ("כַּמַגְ'רוּחַה", ["k a m a dʒ r uː ħ a"]), - "شباب": ("שַבַּאבּ", ["ʃ a b aː b"]), "احكيلكم": ("אַחְכִּי-לְכֹּם", ["ʔ a ħ k iː l k o m", "ʔ a ħ k iː l k u m"]), "الطّيّبين": ("אֵ(ל)טַّיְّבִּין", ["i tˤ tˤ a j j b iː n", "i tˤ tˤ a j j i b iː n"]), - "استنّو": ("סְתַנّוּ", ["s t a n n u", "ʔ i s t a n n u"]), "يفهمو": ("יִפְהַמוּ", ["j i f h a m u"]), "كذب": ("כַּזַבּ", ["k a z a b", "k a ð a b"]), - "عليهم": ("עַלֵי-הֹם", ["ʕ a l eː h o m", "ʕ a l eː h u m"]), "حبل": ("חַבֵּל", ["ħ a b i l", "ħ a b l"]), "الكذب": ("אֵלְכִּזֵבּ", ["i l k i z i b", "i l k i z b", "i l k i ð b"]), - "طلب": ("טַלַבּ", ["tˤ a l a b"]), "رايك": ("רַאיַ-כּ", ["r aː j a k"]), "قوليلي": ("קוּלִי-לִי", ["ʔ uː l iː l i", "q uː l iː l i"]), - "نساعدك#f": ("נְסַאעְדֵ-כּ", ["n s aː ʕ d i k", "n s aː ʕ i d i k"]), "تاخدني": ("תֹאחֵ'ד-נִי", ["t oː x i d n i", "t aː x u d n i", "t oː x u d n i"]), - "حكالك": ("חַכַּא-לַכּ", ["ħ a k aː l a k"]), "بينا": ("פִּינַא", ["p iː n a", "b iː n a"]), "تاخدي": ("תֹאחְ'דִי", ["t oː x d i", "t aː x d i", "t oː x u d i"]), - "تاكسي": ("תַאכְּסִי", ["t aː k s i", "t a k s i"]), "يسألوني": ("יִסְאַלוּ-נִי", ["j i s ʔ a l uː n i"]), "يافا": ("יַאפַא", ["j aː f a"]), - "ابوه": ("אַבּוּ-ה", ["ʔ a b uː h", "ʔ a b uː"]), "يزوروك": ("יְזוּרוּ-כּ", ["j z uː r uː k", "i z uː r uː k"]), - # ---- level 2, lesson 8 (the workbook's seventeenth) ---- - # גזרת הכפולים: in the past "I" and "you" take ـيت - "حطّ": ("חַטّ", ["ħ a tˤ tˤ"]), "يحطّ": ("יְחֻטّ", ["j ħ u tˤ tˤ", "i ħ u tˤ tˤ", "j i ħ u tˤ tˤ"]), "ردّ": ("רַדّ", ["r a d d"]), - "يردّ": ("יְרֻדّ", ["j r u d d", "i r u d d", "j i r u d d"]), "حبّ": ("חַבّ", ["ħ a b b"]), "يحبّ": ("יְחִבّ", ["j ħ i b b", "i ħ i b b", "j i ħ i b b"]), - "ضلّ": ("דַֿלّ", ["dˤ a l l", "ðˤ a l l"]), "يضلّ": ("יְדַֿלّ", ["j dˤ a l l", "i dˤ a l l", "j i dˤ a l l"]), "حسّ": ("חַסّ", ["ħ a s s"]), - "يحسّ": ("יְחִסّ", ["j ħ i s s", "i ħ i s s", "j i ħ i s s"]), "رنّ": ("רַןّ", ["r a n n"]), "يرنّ": ("יְרִןّ", ["j r i n n", "i r i n n", "j i r i n n"]), - "زعّل": ("זַעַّל", ["z a ʕ ʕ a l"]), "يزعّل": ("יְזַעֵّל", ["j z a ʕ ʕ i l", "i z a ʕ ʕ i l", "j i z a ʕ ʕ i l"]), - "ضلّيت": ("דַֿלֵّית", ["dˤ a l l eː t"]), "ضلّيتي": ("דַֿלֵّיתִי", ["dˤ a l l eː t i"]), "ضلّت": ("דַֿלַّת", ["dˤ a l l a t"]), - "ضلّينا": ("דַֿלֵّינַא", ["dˤ a l l eː n a"]), "ضلّيتو": ("דַֿלֵّיתוּ", ["dˤ a l l eː t u"]), "ضلّو": ("דַֿלّוּ", ["dˤ a l l u"]), - "ضلّيتني": ("דַֿלֵّית-נִי", ["dˤ a l l eː t n i"]), - "حطّيت": ("חַטֵّית", ["ħ a tˤ tˤ eː t"]), "حطّيتي": ("חַטֵّיתִי", ["ħ a tˤ tˤ eː t i"]), "حطّت": ("חַטַّת", ["ħ a tˤ tˤ a t"]), - "حطّينا": ("חַטֵّינַא", ["ħ a tˤ tˤ eː n a"]), "حطّيتو": ("חַטֵّיתוּ", ["ħ a tˤ tˤ eː t u"]), "حطّو": ("חַטّוּ", ["ħ a tˤ tˤ u"]), - "حطّيتها": ("חַטֵّית-הַא", ["ħ a tˤ tˤ eː t h a"]), "حطّيته": ("חַטֵّית-וֹ", ["ħ a tˤ tˤ eː t o"]), "حطّيتلي": ("חַטֵّית-לִי", ["ħ a tˤ tˤ eː t l i", "ħ a tˤ tˤ eː t i l l i"]), - "حطّيتله": ("חַטֵّית-לוֹ", ["ħ a tˤ tˤ eː t l o", "ħ a tˤ tˤ eː t i l l o"]), "حطّلي": ("חַטّ-לִי", ["ħ a tˤ tˤ l i", "ħ a tˤ tˤ i l i"]), - "حطّلي#imp": ("חֻטّ-לִי", ["ħ u tˤ tˤ l i", "ħ u tˤ tˤ i l i"]), "احطّها": ("אַחֻטّ-הַא", ["ʔ a ħ u tˤ tˤ h a"]), - "اضلّ": ("אַדַֿלّ", ["ʔ a dˤ a l l"]), "تضلّ": ("תְדַֿלّ", ["t dˤ a l l", "dˤ dˤ a l l", "t i dˤ a l l"]), "تضلّي": ("תְדַֿלִّי", ["t dˤ a l l i", "dˤ dˤ a l l i"]), - "نضلّ": ("נְדַֿלّ", ["n dˤ a l l", "n i dˤ a l l"]), "تضلّو": ("תְדַֿלّוּ", ["t dˤ a l l u", "dˤ dˤ a l l u"]), "يضلّو": ("יְדַֿלّוּ", ["j dˤ a l l u", "i dˤ a l l u"]), - "تحبّي": ("תְחִבִּّי", ["t ħ i b b i"]), "نحبّ": ("נְחִבّ", ["n ħ i b b", "n i ħ i b b"]), "تحبّو": ("תְחִבّוּ", ["t ħ i b b u"]), - "يحبّو": ("יְחִבّוּ", ["j ħ i b b u", "i ħ i b b u"]), "احبّها": ("אַחִבّ-הַא", ["ʔ a ħ i b b h a", "ʔ a ħ i b b a"]), "احبّك": ("אַחִבַּّ-כּ", ["ʔ a ħ i b b a k"]), - "نحبّه": ("נְחִבّ-וֹ", ["n ħ i b b o"]), "حبّت": ("חַבַּّת", ["ħ a b b a t"]), "حبّيتك": ("חַבֵּّיתַ-כּ", ["ħ a b b eː t a k"]), - "تحسّ": ("תְחִסّ", ["t ħ i s s"]), "تحسّي": ("תְחִסִّי", ["t ħ i s s i"]), "حسّيتي": ("חַסֵّיתִי", ["ħ a s s eː t i"]), "حسّيت": ("חַסֵّית", ["ħ a s s eː t"]), - "حسّت": ("חַסַّת", ["ħ a s s a t"]), "تردّ": ("תְרֻדّ", ["t r u d d"]), "تردّي": ("תְרֻדִّי", ["t r u d d i"]), "ردّيت": ("רַדֵّית", ["r a d d eː t"]), - "ردّت": ("רַדַּّת", ["r a d d a t"]), "ارنّ": ("אַרִןّ", ["ʔ a r i n n"]), "ترنّ": ("תְרִןّ", ["t r i n n"]), "رنّيتي": ("רַנֵّיתִי", ["r a n n eː t i"]), - "رنّيت": ("רַנֵّית", ["r a n n eː t"]), - "بوجع": ("בְּוַגַ'ע", ["b w a dʒ a ʕ", "b i w a dʒ a ʕ"]), "الوجع": ("אֵלְוַגַ'ע", ["i l w a dʒ a ʕ"]), "اسبرين": ("אַסְבִּירִין", ["ʔ a s b iː r iː n"]), - "زيّي": ("זַיִّ-י", ["z a j j i"]), "سألها": ("סַאַל-הַא", ["s a ʔ a l h a"]), "شرّبته": ("שַרַّבְּת-וֹ", ["ʃ a r r a b t o"]), - "خبّرتيني": ("חַ'בַּّרְתִי-נִי", ["x a b b a r t iː n i"]), "لسمير": ("לַסַמִיר", ["l a s a m iː r"]), "صارلنا": ("צַאר-לְנַא", ["sˤ aː r l n a", "sˤ aː r i l n a"]), - "صارلها": ("צַאר-לְהַא", ["sˤ aː r l h a", "sˤ aː r i l h a"]), "نستنّاكم": ("נִסְתַנַّא-כֹּם", ["n i s t a n n aː k o m", "n i s t a n n aː k u m"]), - "تستنّى": ("תִסְתַנַّא", ["t i s t a n n a"]), "تستنّو": ("תִסְתַנّוּ", ["t i s t a n n u"]), "لاقيته": ("לַאקֵית-וֹ", ["l aː ʔ eː t o", "l aː q eː t o"]), - "اخدوها": ("אַחַ'דוּ-הַא", ["ʔ a x a d uː h a"]), "بيني": ("בֵּינִ-י", ["b eː n i"]), "بينك": ("בֵּינַ-כּ", ["b eː n a k"]), "بينك#f": ("בֵּינֵ-כּ", ["b eː n i k"]), - "لحدا": ("לַחַדַא", ["l a ħ a d a"]), "منّك#f": ("מִנֵّ-כּ", ["m i n n i k"]), "عنّك#f": ("עַנֵّ-כּ", ["ʕ a n n i k"]), "حتّى": ("חַתַّא", ["ħ a t t a"]), - "كلمة": ("כִּלְמֵה", ["k i l m e"]), "كلمة#of": ("כִּלְמֵת", ["k i l m i t", "k i l m e t"]), "كلمات": ("כַּלִמַאת", ["k a l i m aː t"]), - "لعندك": ("לַעִנְדַ-כּ", ["l a ʕ i n d a k"]), "الاحترام": ("אֵלְאִחְתִרַאם", ["i l ʔ i ħ t i r aː m"]), "جنبه": ("גַ'נְבּ-וֹ", ["dʒ a n b o"]), - "ياخدني": ("יֹאחֵ'ד-נִי", ["j oː x i d n i", "j aː x u d n i", "j oː x u d n i"]), "وحياتك#f": ("וִחְיַאתֵ-כּ", ["w i ħ j aː t i k"]), - "اشتريتله": ("אִשְתַרֵית-לוֹ", ["ʔ i ʃ t a r eː t l o", "ʔ i ʃ t a r eː t i l l o"]), "بيتزا": ("בִּיתְזַא", ["b iː t z a", "p iː t s a"]), - "يجيبها": ("יְגִ'יבּ-הַא", ["j dʒ iː b h a", "i dʒ iː b h a"]), - # عم before the present: happening now - "عم": ("עַםְ", ["ʕ a m"]), "اشتغل": ("אַשְתְעֵ'ל", ["ʔ a ʃ t ɣ i l", "ʔ a ʃ t i ɣ i l"]), "قريبا": ("קַרִיבַּן", ["ʔ a r iː b a n", "q a r iː b a n"]), - "ناصر": ("נַאצֵר", ["n aː sˤ i r"]), "تدخل": ("תֻדְחֹ'ל", ["t u d x u l"]), "شافتنا": ("שַאפַת-נַא", ["ʃ aː f a t n a"]), - "سألتنا#f": ("סַאְלַת-נַא", ["s a ʔ l a t n a"]), "ارجعلك": ("אַרְגַ'ע-לַכּ", ["ʔ a r dʒ a ʕ l a k"]), "اسألك": ("אַסְאַלַ-כּ", ["ʔ a s ʔ a l a k"]), - "صاحبة#of": ("צַאחִבֵּת", ["sˤ aː ħ i b i t", "sˤ aː ħ b i t"]), "شافتها": ("שַאפַת-הַא", ["ʃ aː f a t h a"]), "يامّا": ("יַאמַّא", ["j a m m a", "j aː m m a"]), - "تحضّري": ("תְחַדְֿّרִי", ["t ħ a dˤ dˤ r i", "t ħ a dˤ dˤ i r i"]), "باستا": ("בַּאסְתַא", ["b aː s t a", "p aː s t a"]), - "صلصة#of": ("צַלְצֵת", ["sˤ a l sˤ i t", "sˤ a l sˤ e t"]), "خبّروني": ("חַ'בַּّרוּ-נִי", ["x a b b a r uː n i"]), "الصّحّة": ("אֵ(ל)צִّחַّה", ["i sˤ sˤ i ħ ħ a", "i sˤ sˤ a ħ ħ a"]), - "اركب": ("אַרְכַּבּ", ["ʔ a r k a b"]), "تعملو": ("תִעְמַלוּ", ["t i ʕ m a l u"]), - # the song شاطر شاطر - "يلّي": ("יַלִّי", ["j a l l i"]), "مات": ("מַאת", ["m aː t"]), "ثياب": ("תְ'יַאבּ", ["t j aː b", "t i j aː b", "s j aː b"]), - "ثيابه": ("תְ'יַאבּ-וֹ", ["t j aː b o", "t i j aː b o"]), "عاقل": ("עַאקֵל", ["ʕ aː ʔ i l", "ʕ aː q i l"]), "لعبة": ("לֻעְבֵּה", ["l u ʕ b e"]), - "العاب": ("אַלְעַאבּ", ["ʔ a l ʕ aː b"]), "العابه": ("אַלְעַאבּ-וֹ", ["ʔ a l ʕ aː b o"]), "رفيق": ("רַפִיק", ["r a f iː ʔ", "r a f iː q"]), - "رفقا": ("רֻפְקַא", ["r u f ʔ a", "r u f q a", "r u f a ʔ a"]), "رفقاته": ("רִפְקַאת-וֹ", ["r i f ʔ aː t o", "r i f q aː t o"]), - "يكذب": ("יִכְּזֵבּ", ["j i k z i b", "j i k ð i b"]), "قانون": ("קַאנוּן", ["ʔ aː n uː n", "q aː n uː n"]), "قوانين": ("קַוַאנִין", ["ʔ a w aː n iː n", "q a w aː n iː n"]), - "القوانين": ("אֵלְקַוַאנִין", ["i l ʔ a w aː n iː n", "i l q a w aː n iː n"]), "الحبّ": ("אֵלְחֻבّ", ["i l ħ u b b"]), - "غنية#of": ("עִ'נְיֵת", ["ɣ i n j i t", "ɣ i n j e t"]), "نقولّه": ("נְקוּל-לّוֹ", ["n ʔ uː l l o", "n q uː l l o"]), "وحده": ("וַחְד-וֹ", ["w a ħ d o"]), - "بمدرسته": ("בְּמַדְרַסְת-וֹ", ["b m a d r a s t o", "b i m a d r a s t o"]), "معلّمته": ("מְעַלְّמְת-וֹ", ["m ʕ a l l m t o", "m ʕ a l l i m t o"]), - "بيجو": ("בִּיג'וּ", ["b iː dʒ u"]), "بحياته": ("בְּחַיַאת-וֹ", ["b ħ a j aː t o", "b i ħ a j aː t o"]), "يجوّي": ("יְגַ'וִّי", ["j dʒ a w w i", "i dʒ a w w i"]), - # ---- level 2, lesson 9 (the workbook's eighteenth) ---- - # the active participle: ـا after the first letter; ة, ين, ات for her and them - "ساكنات": ("סַאכְּנַאת", ["s aː k n aː t"]), "سامع": ("סַאמֵע", ["s aː m i ʕ"]), "سامعة": ("סַאמְעַה", ["s aː m ʕ a"]), - "سامعين": ("סַאמְעִין", ["s aː m ʕ iː n"]), "سامعات": ("סַאמְעַאת", ["s aː m ʕ aː t"]), "سامعك": ("סַאמְעַ-כּ", ["s aː m ʕ a k"]), - "سامعني": ("סַאמֵע-נִי", ["s aː m i ʕ n i"]), - "رايح": ("רַאיֵח", ["r aː j i ħ"]), "رايحة": ("רַאיְחַה", ["r aː j ħ a"]), "رايحين": ("רַאיְחִין", ["r aː j ħ iː n"]), "رايحات": ("רַאיְחַאת", ["r aː j ħ aː t"]), - "فاهمة": ("פַאהְמֵה", ["f aː h m e"]), "فاهمين": ("פַאהְמִין", ["f aː h m iː n"]), "فاهمات": ("פַאהְמַאת", ["f aː h m aː t"]), - "طالع": ("טַאלֵע", ["tˤ aː l i ʕ"]), "طالعة": ("טַאלְעַה", ["tˤ aː l ʕ a"]), "طالعين": ("טַאלְעִין", ["tˤ aː l ʕ iː n"]), "طالعات": ("טַאלְעַאת", ["tˤ aː l ʕ aː t"]), - "راجع": ("רַאגֵ'ע", ["r aː dʒ i ʕ"]), "راجعة": ("רַאגְ'עַה", ["r aː dʒ ʕ a"]), "راجعين": ("רַאגְ'עִין", ["r aː dʒ ʕ iː n"]), "راجعات": ("רַאגְ'עַאת", ["r aː dʒ ʕ aː t"]), - "عارفة": ("עַארְפֵה", ["ʕ aː r f e"]), "عارفين": ("עַארְפִין", ["ʕ aː r f iː n"]), "عارفات": ("עַארְפַאת", ["ʕ aː r f aː t"]), - "نايم": ("נַאיֵם", ["n aː j i m"]), "نايمة": ("נַאיְמֵה", ["n aː j m e"]), "نايمين": ("נַאיְמִין", ["n aː j m iː n"]), "نايمات": ("נַאיְמַאת", ["n aː j m aː t"]), - "قاعد": ("קַאעֵד", ["ʔ aː ʕ i d", "q aː ʕ i d"]), "قاعدة": ("קַאעְדֵה", ["ʔ aː ʕ d e", "q aː ʕ d e"]), "قاعدين": ("קַאעְדִין", ["ʔ aː ʕ d iː n", "q aː ʕ d iː n"]), - "نازل": ("נַאזֵל", ["n aː z i l"]), "نازلة": ("נַאזְלֵה", ["n aː z l e"]), "نازلين": ("נַאזְלִין", ["n aː z l iː n"]), "عامل": ("עַאמֵל", ["ʕ aː m i l"]), - "قادر": ("קַאדֵר", ["ʔ aː d i r", "q aː d i r"]), "قادرة": ("קַאדְרֵה", ["ʔ aː d r e", "q aː d r e"]), "شايف": ("שַאיֵף", ["ʃ aː j i f"]), - "شايفين": ("שַאיְפִין", ["ʃ aː j f iː n"]), "شايفه": ("שַאיְפ-וֹ", ["ʃ aː j f o"]), "شايفني": ("שַאיֵף-נִי", ["ʃ aː j i f n i"]), - "فايت": ("פַאיֵת", ["f aː j i t"]), "جايب": ("גַ'איֵבּ", ["dʒ aː j i b"]), "جايبك": ("גַ'איְבַּ-כּ", ["dʒ aː j b a k"]), "عايش": ("עַאיֵש", ["ʕ aː j i ʃ"]), - "حابب": ("חַאבֵּבּ", ["ħ aː b i b"]), "حابّة": ("חַאבֵּّה", ["ħ aː b b e"]), "حاطط": ("חַאטֵט", ["ħ aː tˤ i tˤ"]), "واقف": ("וַאקֵף", ["w aː ʔ i f", "w aː q i f"]), - "حاسس": ("חַאסֵס", ["ħ aː s i s"]), "صاير": ("צַאיֵר", ["sˤ aː j i r"]), "جاية": ("גַ'איֵה", ["dʒ aː j e"]), "جايات": ("גַ'איַאת", ["dʒ aː j aː t"]), - "فاتحين": ("פַאתְחִין", ["f aː t ħ iː n"]), "صاحية": ("צַאחְיֵה", ["sˤ aː ħ j e"]), "مايت": ("מַאיֵת", ["m aː j i t"]), "راكب": ("רַאכֵּבּ", ["r aː k i b"]), - # five more verbs - "قدر": ("קִדֵר", ["ʔ i d i r", "q i d i r"]), "يقدر": ("יִקְדַר", ["j i ʔ d a r", "j i q d a r"]), "نقدر": ("נִקְדַר", ["n i ʔ d a r", "n i q d a r"]), - "مشي": ("מִשִי", ["m i ʃ i"]), "يمشي": ("יִמְשִי", ["j i m ʃ i"]), "وقف": ("וִקֵף", ["w i ʔ i f", "w i q i f"]), "عرف": ("עִרֵף", ["ʕ i r i f"]), - "اعرف": ("אַעְרַף", ["ʔ a ʕ r a f"]), "عرفتش": ("עְרִפְתֵש", ["ʕ r i f t i ʃ", "ʕ r i f t ʃ"]), "نام": ("נַאם", ["n aː m"]), - "ينام": ("יְנַאם", ["j n aː m", "i n aː m", "j i n aː m"]), "نامي": ("נַאמִי", ["n aː m i"]), "يعيش": ("יְעִיש", ["j ʕ iː ʃ", "i ʕ iː ʃ", "j i ʕ iː ʃ"]), - "عليكي": ("עַלֵיכִּי", ["ʕ a l eː k i"]), "عليه": ("עַלֵי-ה", ["ʕ a l eː h", "ʕ a l eː"]), "سمّاعات": ("סַמַّאעַאת", ["s a m m aː ʕ aː t"]), - "لايمتا": ("לַאֵימְתַא", ["l a ʔ eː m t a"]), "تصرخ": ("תֻצְרֹח'", ["t u sˤ r u x"]), "شافته#f": ("שַאפַת-וֹ", ["ʃ aː f a t o"]), "رنّت": ("רַנַّת", ["r a n n a t"]), - "شفتكم": ("שֻפְתְ-כֹּם", ["ʃ u f t k o m", "ʃ u f t k u m"]), "شافها": ("שַאפ-הַא", ["ʃ aː f h a"]), "التّين": ("אֵ(ל)תִّין", ["i t t iː n"]), - "زمّور": ("זַמּוּר", ["z a m m uː r"]), "اردّ": ("אַרֻדّ", ["ʔ a r u d d"]), "يخبّروني": ("יְחַ'בְּّרוּ-נִי", ["j x a b b r uː n i", "i x a b b r uː n i"]), - "بالاوّل": ("בִּלְאַוַّל", ["b i l ʔ a w w a l"]), "تبكي": ("תִבְּכִּי", ["t i b k i"]), "اساعدك#f": ("אַסַאעְדֵ-כּ", ["ʔ a s aː ʕ d i k", "ʔ a s aː ʕ i d i k"]), - "الحاسوب": ("אֵלְחַאסוּבּ", ["i l ħ aː s uː b"]), "ارجعلك#f": ("אַרְגַ'ע-לֵכּ", ["ʔ a r dʒ a ʕ l i k"]), - "استنّاني#imp": ("סְתַנַّא-נִי", ["s t a n n aː n i", "ʔ i s t a n n aː n i"]), "تيجو": ("תִיג'וּ", ["t iː dʒ u"]), - "نشرّبكم": ("נְשַרֵّבּ-כֹּם", ["n ʃ a r r i b k o m", "n ʃ a r r i b k u m"]), "اسمحولي": ("אִסְמַחוּ-לִי", ["ʔ i s m a ħ uː l i"]), - "المختار": ("אֵלְמֻחְ'תַאר", ["i l m u x t aː r"]), "اساعدكم": ("אַסַאעֵד-כֹּם", ["ʔ a s aː ʕ i d k o m", "ʔ a s aː ʕ i d k u m"]), - "الحمرا": ("אֵלְחַמְרַא", ["i l ħ a m r a"]), "استنّاكي": ("אַסְתַנַّאכִּי", ["ʔ a s t a n n aː k i"]), "عزبا": ("עַזְבַּא", ["ʕ a z b a"]), - "تفوت": ("תְפוּת", ["t f uː t"]), "السّنة": ("אֵ(ל)סַّנֵה", ["i s s a n e"]), "شفتك": ("שֻפְתַ-כּ", ["ʃ u f t a k"]), - "عالجت#f": ("עַאלַגַ'ת", ["ʕ aː l a dʒ a t"]), "اشتريلك#f": ("אַשְתְרִי-לֵכּ", ["ʔ a ʃ t r iː l i k"]), "كم": ("כַּם", ["k a m"]), - "جبتلك": ("גִ'בְּת-לַכּ", ["dʒ i b t i l l a k", "dʒ i b t l a k"]), - # the song انا عايش, in Egypt's Arabic - "منتش": ("מֻנְתַש", ["m u n t a ʃ"]), "ليالي": ("לַיַאלִי", ["l a j aː l i"]), "نهاية": ("נִהַאיֵה", ["n i h aː j e"]), "جرح#n": ("גַ'רֵח", ["dʒ a r i ħ", "dʒ u r ħ"]), - "شوق": ("שוֹק", ["ʃ oː ʔ", "ʃ oː q"]), "هوا": ("הַוַא", ["h a w a"]), "بكفّي": ("בִּכַּפִّי", ["b i k a f f i"]), "ارتاح": ("אִרְתַאח", ["ʔ i r t aː ħ"]), - "ارتاح#i": ("אַרְתַאח", ["ʔ a r t aː ħ"]), "يرتاح": ("יִרְתַאח", ["j i r t aː ħ"]), "بقي": ("בִּקִי", ["b i ʔ i", "b i q i"]), - "يبقى": ("יִבְּקַא", ["j i b ʔ a", "j i b q a"]), "ابقى": ("אַבְּקַא", ["ʔ a b ʔ a", "ʔ a b q a"]), "حلم": ("חִלֵם", ["ħ i l i m"]), "يحلم": ("יִחְלַם", ["j i ħ l a m"]), - "ببسي": ("בֵּבְּסִי", ["b e b s i", "p e p s i"]), "بنساك": ("בַּנְסַא-כּ", ["b a n s aː k"]), "ببعد": ("בַּבְּעֵד", ["b a b ʕ i d"]), - "معاك": ("מַעַא-כּ", ["m a ʕ aː k"]), "معايا": ("מַעַאיַא", ["m a ʕ aː j a"]), "بعدك": ("בֻּעְדַ-כּ", ["b u ʕ d a k"]), "بعدك#after": ("בַּעְדַ-כּ", ["b a ʕ d a k"]), "بينا#us": ("בֵּינַא", ["b eː n a"]), "انسى": ("אַנְסַא", ["ʔ a n s a"]), - "عايز": ("עַאיֵז", ["ʕ aː j i z"]), "جراحه": ("גְ'רַאח-וֹ", ["dʒ r aː ħ o"]), "حبّيتش": ("חַבֵּّית-ש", ["ħ a b b eː t ʃ", "ħ a b b eː t i ʃ"]), - "غيرك": ("עֵ'ירַ-כּ", ["ɣ eː r a k"]), "ايه": ("אֵי", ["ʔ eː", "ʔ eː h"]), "بشوقي": ("בְּשוֹק-י", ["b ʃ oː ʔ i", "b ʃ oː q i", "b i ʃ oː ʔ i"]), - "هوايا": ("הַוַאיַא", ["h a w aː j a"]), "ليلي": ("לֵיל-י", ["l eː l i"]), "نهاري": ("נְהַאר-י", ["n h aː r i"]), "كفاية": ("כִּפַאיַא", ["k i f aː j a"]), - # the father, the son and the donkey - "الفقير": ("אֵלְפַקִיר", ["i l f a ʔ iː r", "i l f a q iː r"]), "عالحمار": ("עַלְ-(א)לִחְמַאר", ["ʕ a l i ħ m aː r", "ʕ a l ħ m aː r"]), - "شافوهم": ("שַאפוּ-הֹם", ["ʃ aː f uː h o m", "ʃ aː f uː h u m"]), "شوفو": ("שוּפוּ", ["ʃ uː f u"]), "يرحم": ("יִרְחַם", ["j i r ħ a m"]), - "يرحمو": ("יִרְחַמוּ", ["j i r ħ a m u"]), "عيب": ("עֵיבּ", ["ʕ eː b"]), "الكلام": ("אֵלְכַּלַאם", ["i l k a l aː m"]), "ادب": ("אַדַבּ", ["ʔ a d a b"]), - "فقال": ("פַ-קַאל", ["f a ʔ aː l", "f a q aː l"]), "نركب": ("נִרְכַּבּ", ["n i r k a b"]), "يركبو": ("יִרְכַּבּוּ", ["j i r k a b u"]), - "انزل#imp": ("אִנְזַל", ["ʔ i n z a l", "ʔ i n z i l"]), "سلام": ("סַלַאם", ["s a l aː m"]), - "نرضّي": ("נְרַדִֿّי", ["n r a dˤ dˤ i", "n i r a dˤ dˤ i"]), "حملنا": ("חַמַלְנַא", ["ħ a m a l n a"]), "كتافنا": ("כְּתַאפְ-נַא", ["k t aː f n a"]), - "يحكو": ("יִחְכּוּ", ["j i ħ k u"]), -} -PUNCT = {"؟": "?", "،": ",", ".": ".", ":": ":", "!": "!"} - -# Hebrew that the prompt voice misreads without its points (פתח is read "petach", את is read "et") is -# written pointed: the page shows it unpointed and the voice gets the pointed text (`he_say`). -POINTS = dict.fromkeys(range(0x0591, 0x05C8)) - - -def _plain(he: str) -> str: - return he.translate(POINTS) - - -# words rewritten for the voice wherever they appear: unpointed דוד is read "David", חברי is read -# "chaverei" (my friends), שבוע is read "shvua" (oath), and ה' is said אלוהים -MISREAD = {"ה'": "אלוהים", "דוד": "דּוֹד", "הדוד": "הַדּוֹד", "דודי": "דּוֹדִי", "ודודי": "וְדוֹדִי", "דודנו": "דּוֹדֵנוּ", - "חברי": "חֲבֵרִי", "וחברי": "וַחֲבֵרִי", "שבוע": "שָׁבוּעַ", "רווק": "רַוָּק"} -_MISREAD = re.compile(rf"(? str: - return _MISREAD.sub(lambda m: MISREAD[m[1]], he) - - -HE_PRONOUNS = "אֲנִי אַתָּה אַתְּ הוּא הִיא אֲנַחְנוּ אַתֶּם הֵם".split() # in PRONOUNS order -AR_PRONOUNS = [alts[0][0] for _, _, _, alts in PRONOUNS] -# lesson 1's verbs (בניין ראשון, פַעַל): Hebrew past forms and LEX keys, one per pronoun in PRONOUNS order, then the future -VERBS_1 = [ - ("פָּתַחְתִּי פָּתַחְתָּ פָּתַחְתְּ פָּתַח פָּתְחָה פָּתַחְנוּ פְּתַחְתֶּם פָּתְחוּ", "فتحت فتحت فتحتي فتح فتحت#f فتحنا فتحتو فتحو", "يفتح"), - ("אָכַלְתִּי אָכַלְתָּ אָכַלְתְּ אָכַל אָכְלָה אָכַלְנוּ אֲכַלְתֶּם אָכְלוּ", "اكلت اكلت اكلتي اكل اكلت#f اكلنا اكلتو اكلو", "ياكل"), - ("שָׁאַלְתִּי שָׁאַלְתָּ שָׁאַלְתְּ שָׁאַל שָׁאֲלָה שָׁאַלְנוּ שְׁאַלְתֶּם שָׁאֲלוּ", "سألت سألت سألتي سأل سألت#f سألنا سألتو سألو", "يسأل"), - ("יָשַׁבְתִּי יָשַׁבְתָּ יָשַׁבְתְּ יָשַׁב יָשְׁבָה יָשַׁבְנוּ יְשַׁבְתֶּם יָשְׁבוּ", "قعدت قعدت قعدتي قعد قعدت#f قعدنا قعدتو قعدو", "يقعد"), -] -HUWWE = 3 # the pronoun whose past form names the verb - - -# lesson 3's; a fourth item is said after the Hebrew base form where that alone is unclear (עבר is also "past") -VERBS_3 = [ - ("נִכְנַסְתִּי נִכְנַסְתָּ נִכְנַסְתְּ נִכְנַס נִכְנְסָה נִכְנַסְנוּ נִכְנַסְתֶּם נִכְנְסוּ", "دخلت دخلت دخلتي دخل دخلت#f دخلنا دخلتو دخلو", "يدخل"), - ("שָׂחִיתִי שָׂחִיתָ שָׂחִית שָׂחָה שָׂחֲתָה שָׂחִינוּ שְׂחִיתֶם שָׂחוּ", "سبحت سبحت سبحتي سبح سبحت#f سبحنا سبحتو سبحو", "يسبح"), - ("בִּישַּׁלְתִּי בִּישַּׁלְתָּ בִּישַּׁלְתְּ בִּישֵּׁל בִּישְּׁלָה בִּישַּׁלְנוּ בִּישַּׁלְתֶּם בִּישְּׁלוּ", "طبخت طبخت طبختي طبخ طبخت#f طبخنا طبختو طبخو", "يطبخ"), - ("עָבַרְתִּי עָבַרְתָּ עָבַרְתְּ עָבַר עָבְרָה עָבַרְנוּ עֲבַרְתֶּם עָבְרוּ", "نقلت نقلت نقلتي نقل نقلت#f نقلنا نقلتو نقلو", "ينقل", "דירה"), -] -# lesson 4's (verbs that end in ا) -VERBS_4 = [ - ("בָּאתִי בָּאתָ בָּאת בָּא בָּאָה בָּאנוּ בָּאתֶם בָּאוּ", "اجيت اجيت اجيتي اجا اجت اجينا اجيتو اجو", "يجي"), - ("דִּיבַּרְתִּי דִּיבַּרְתָּ דִּיבַּרְתְּ דִּיבֵּר דִּיבְּרָה דִּיבַּרְנוּ דִּיבַּרְתֶּם דִּיבְּרוּ", "حكيت حكيت حكيتي حكى حكت حكينا حكيتو حكو", "يحكي"), - ("נָתַתִּי נָתַתָּ נָתַתְּ נָתַן נָתְנָה נָתַנּוּ נְתַתֶּם נָתְנוּ", "اعطيت اعطيت اعطيتي اعطى اعطت اعطينا اعطيتو اعطو", "يعطي"), - ("קָנִיתִי קָנִיתָ קָנִית קָנָה קָנְתָה קָנִינוּ קְנִיתֶם קָנוּ", "اشتريت اشتريت اشتريتي اشترى اشترت#f اشترينا اشتريتو اشترو", "يشتري"), -] -# lesson 5's (גזרת ע"ו) -VERBS_5 = [ - ("הָלַכְתִּי הָלַכְתָּ הָלַכְתְּ הָלַךְ הָלְכָה הָלַכְנוּ הֲלַכְתֶּם הָלְכוּ", "رحت رحت رحتي راح راحت رحنا رحتو راحو", "يروح"), - ("אָמַרְתִּי אָמַרְתָּ אָמַרְתְּ אָמַר אָמְרָה אָמַרְנוּ אֲמַרְתֶּם אָמְרוּ", "قلت قلت قلتي قال قالت قلنا قلتو قالو", "يقول"), - ("בִּיקַּרְתִּי בִּיקַּרְתָּ בִּיקַּרְתְּ בִּיקֵּר בִּיקְּרָה בִּיקַּרְנוּ בִּיקַּרְתֶּם בִּיקְּרוּ", "زرت زرت زرتي زار زارت زرنا زرتو زارو", "يزور"), - ("הָיִיתִי הָיִיתָ הָיִית הָיָה הָיְתָה הָיִינוּ הֱיִיתֶם הָיוּ", "كنت كنت كنتي كان كانت كنّا كنتو كانو", "يكون"), -] -# lesson 6's: more of גזרת ע"ו, and اخد, which the lesson's homework adds -VERBS_6 = [ - ("נִישַּׁקְתִּי נִישַּׁקְתָּ נִישַּׁקְתְּ נִישֵּׁק נִישְּׁקָה נִישַּׁקְנוּ נִישַּׁקְתֶּם נִישְּׁקוּ", "بست بست بستي باس باست بسنا بستو باسو", "يبوس"), - ("נָהַגְתִּי נָהַגְתָּ נָהַגְתְּ נָהַג נָהֲגָה נָהַגְנוּ נְהַגְתֶּם נָהֲגוּ", "سقت سقت سقتي ساق ساقت سقنا سقتو ساقو", "يسوق"), - ("רָאִיתִי רָאִיתָ רָאִית רָאָה רָאֲתָה רָאִינוּ רְאִיתֶם רָאוּ", "شفت شفت شفتي شاف شافت شفنا شفتو شافو", "يشوف"), - ("נִכְנַסְתִּי נִכְנַסְתָּ נִכְנַסְתְּ נִכְנַס נִכְנְסָה נִכְנַסְנוּ נִכְנַסְתֶּם נִכְנְסוּ", "فتت فتت فتّي فات فاتت فتنا فتّو فاتو", "يفوت", "ועבר"), - ("לָקַחְתִּי לָקַחְתָּ לָקַחְתְּ לָקַח לָקְחָה לָקַחְנוּ לְקַחְתֶּם לָקְחוּ", "اخدت اخدت اخدتي اخد اخدت#f اخدنا اخدتو اخدو", "ياخد"), -] -# the future (lesson 6): Hebrew and LEX keys, one per pronoun in PRONOUNS order -FUTURE_6 = [ - ("אֶשְׁמַע תִּשְׁמַע תִּשְׁמְעִי יִשְׁמַע תִּשְׁמַע נִשְׁמַע תִּשְׁמְעוּ יִשְׁמְעוּ", "اسمع#i تسمع تسمعي يسمع تسمع نسمع تسمعو يسمعو"), - ("אֵשֵׁב תֵּשֵׁב תֵּשְׁבִי יֵשֵׁב תֵּשֵׁב נֵשֵׁב תֵּשְׁבוּ יֵשְׁבוּ", "اقعد تقعد تقعدي يقعد تقعد نقعد تقعدو يقعدو"), - ("אֲבַקֵּר תְּבַקֵּר תְּבַקְּרִי יְבַקֵּר תְּבַקֵּר נְבַקֵּר תְּבַקְּרוּ יְבַקְּרוּ", "ازور تزور تزوري يزور تزور نزور تزورو يزورو"), -] -# lesson 8's (גזרת פִעֵל) -VERBS_8 = [ - ("שָׁתִיתִי שָׁתִיתָ שָׁתִית שָׁתָה שָׁתְתָה שָׁתִינוּ שְׁתִיתֶם שָׁתוּ", "شربت شربت شربتي شرب شربت#f شربنا شربتو شربو", "يشرب"), - ("יָצָאתִי יָצָאתָ יָצָאת יָצָא יָצְאָה יָצָאנוּ יְצָאתֶם יָצְאוּ", "طلعت طلعت طلعتي طلع طلعت#f طلعنا طلعتو طلعو", "يطلع", "ועלה"), - ("שָׁמַעְתִּי שָׁמַעְתָּ שָׁמַעְתְּ שָׁמַע שָׁמְעָה שָׁמַעְנוּ שְׁמַעְתֶּם שָׁמְעוּ", "سمعت سمعت سمعتي سمع سمعت#f سمعنا سمعتو سمعو", "يسمع"), - ("הֵבַנְתִּי הֵבַנְתָּ הֵבַנְתְּ הֵבִין הֵבִינָה הֵבַנּוּ הֲבַנְתֶּם הֵבִינוּ", "فهمت فهمت فهمتي فهم فهمت#f فهمنا فهمتو فهمو", "يفهم"), - ("חָזַרְתִּי חָזַרְתָּ חָזַרְתְּ חָזַר חָזְרָה חָזַרְנוּ חֲזַרְתֶּם חָזְרוּ", "رجعت رجعت رجعتي رجع رجعت#f رجعنا رجعتو رجعو", "يرجع"), -] -# lesson 9's; a fifth item is said after every past form as well ("to lunch" has no verb of its own in Hebrew) -VERBS_9 = [ - ("הוֹדֵיתִי הוֹדֵיתָ הוֹדֵית הוֹדָה הוֹדְתָה הוֹדֵינוּ הוֹדֵיתֶם הוֹדוּ", "شكرت شكرت شكرتي شكر شكرت#f شكرنا شكرتو شكرو", "يشكر"), - ("אָכַלְתִּי אָכַלְתָּ אָכַלְתְּ אָכַל אָכְלָה אָכַלְנוּ אֲכַלְתֶּם אָכְלוּ", "تغدّيت تغدّيت تغدّيتي تغدّى تغدّت تغدّينا تغدّيتو تغدّو", "يتغدّى", "צהריים", "צהריים"), -] -# lesson 10's (more of גזרת פִעֵל) -VERBS_10 = [ - ("יָרַדְתִּי יָרַדְתָּ יָרַדְתְּ יָרַד יָרְדָה יָרַדְנוּ יְרַדְתֶּם יָרְדוּ", "نزلت نزلت نزلتي نزل نزلت#f نزلنا نزلتو نزلو", "ينزل"), - ("הִגַּעְתִּי הִגַּעְתָּ הִגַּעְתְּ הִגִּיעַ הִגִּיעָה הִגַּעְנוּ הִגַּעְתֶּם הִגִּיעוּ", "وصلت وصلت وصلتي وصل وصلت#f وصلنا وصلتو وصلو", "يوصل"), - ("צָחַקְתִּי צָחַקְתָּ צָחַקְתְּ צָחַק צָחֲקָה צָחַקְנוּ צְחַקְתֶּם צָחֲקוּ", "ضحكت ضحكت ضحكتي ضحك ضحكت#f ضحكنا ضحكتو ضحكو", "يضحك", "וחייך"), - ("לָבַשְׁתִּי לָבַשְׁתָּ לָבַשְׁתְּ לָבַשׁ לָבְשָׁה לָבַשְׁנוּ לְבַשְׁתֶּם לָבְשׁוּ", "لبست لبست لبستي لبس لبست#f لبسنا لبستو لبسو", "يلبس"), - ("שִׂיחַקְתִּי שִׂיחַקְתָּ שִׂיחַקְתְּ שִׂיחֵק שִׂיחֲקָה שִׂיחַקְנוּ שִׂיחַקְתֶּם שִׂיחֲקוּ", "لعبت لعبت لعبتي لعب لعبت#f لعبنا لعبتو لعبو", "يلعب"), -] -# the future after بدّ (lesson 9): the Hebrew infinitive, and the verb's future form for each pronoun in PRONOUNS order -WANTS_9 = [ - ("לצאת", "اطلع تطلع تطلعي يطلع تطلع نطلع تطلعو يطلعو"), - ("לשמוע", "اسمع#i تسمع تسمعي يسمع تسمع نسمع تسمعو يسمعو"), - ("לאכול צהריים", "اتغدّى تتغدّى تتغدّي يتغدّى تتغدّى نتغدّى تتغدّو يتغدّو"), -] -# level 2, lesson 1's (the workbook's tenth lesson: five more of בניין ראשון, פַעַל) -VERBS_2_1 = [ - ("רַצְתִּי רַצְתָּ רַצְתְּ רָץ רָצָה רַצְנוּ רַצְתֶּם רָצוּ", "ركضت ركضت ركضتي ركض ركضت#f ركضنا ركضتو ركضو", "يركض"), - ("שִׁילַּמְתִּי שִׁילַּמְתָּ שִׁילַּמְתְּ שִׁילֵּם שִׁילְּמָה שִׁילַּמְנוּ שִׁילַּמְתֶּם שִׁילְּמוּ", "دفعت دفعت دفعتي دفع دفعت#f دفعنا دفعتو دفعو", "يدفع"), - ("שָׁלַחְתִּי שָׁלַחְתָּ שָׁלַחְתְּ שָׁלַח שָׁלְחָה שָׁלַחְנוּ שְׁלַחְתֶּם שָׁלְחוּ", "بعثت بعثت بعثتي بعث بعثت#f بعثنا بعثتو بعثو", "يبعث"), - ("שָׁבַרְתִּי שָׁבַרְתָּ שָׁבַרְתְּ שָׁבַר שָׁבְרָה שָׁבַרְנוּ שְׁבַרְתֶּם שָׁבְרוּ", "كسرت كسرت كسرتي كسر كسرت#f كسرنا كسرتو كسرو", "يكسر"), - ("קָשַׁרְתִּי קָשַׁרְתָּ קָשַׁרְתְּ קָשַׁר קָשְׁרָה קָשַׁרְנוּ קְשַׁרְתֶּם קָשְׁרוּ", "ربطت ربطت ربطتي ربط ربطت#f ربطنا ربطتو ربطو", "يربط"), -] -# the future after لازم and after رح (level 2, lesson 1): the Hebrew infinitive, and the verb's future form for each pronoun -MUST_2_1 = [ - ("לראות", "اشوف تشوف تشوفي يشوف تشوف نشوف تشوفو يشوفو"), - ("לבקר", "ازور تزور تزوري يزور تزور نزور تزورو يزورو"), - ("לקשור", "اربط تربط تربطي يربط تربط نربط تربطو يربطو"), -] -WILL_2_1 = [ - ("לרוץ", "اركض تركض تركضي يركض تركض نركض تركضو يركضو"), - ("לשלוח", "ابعث تبعث تبعثي يبعث تبعث نبعث تبعثو يبعثو"), - ("לשתות", "اشرب تشرب تشربي يشرب تشرب نشرب تشربو يشربو"), -] -# level 2, lesson 2 (the workbook's eleventh): the days of the week, said after يوم -DAYS = [("יום ראשון", "الاحد"), ("יום שני", "التّنين"), ("יום שלישי", "التّلاتا"), ("יום רביעי", "الاربعا"), ("יום חמישי", "الخميس"), - ("יום שישי", "الجمعة"), ("יום שבת", "السّبت")] -# the present (بـ before the future): the Hebrew present of "he", "she" and "they", and the verb's form for each pronoun -PRESENT_2_2 = [ - ("שׁוֹאֵל", "שׁוֹאֶלֶת", "שׁוֹאֲלִים", "بسأل بتسأل بتسألي بيسأل بتسأل بنسأل بتسألو بيسألو"), - ("רוֹאֶה", "רוֹאָה", "רוֹאִים", "بشوف بتشوف بتشوفي بيشوف بتشوف بنشوف بتشوفو بيشوفو"), - ("שׁוֹתֶה", "שׁוֹתָה", "שׁוֹתִים", "بشرب بتشرب بتشربي بيشرب بتشرب بنشرب بتشربو بيشربو"), -] -# level 2, lesson 3 (the workbook's twelfth): the hours, said after السّاعة; an hour is feminine (واحدة, تنتين) -HOURS = [("אחת", "واحدة"), ("שתיים", "تنتين"), ("שלוש", "تلاتة"), ("ארבע", "اربعة"), ("חמש", "خمسة"), ("שש", "ستّة"), ("שבע", "سبعة"), - ("שמונה", "تمانية"), ("תשע", "تسعة"), ("עשר", "عشرة"), ("אחת עשרה", "حدعش"), ("שתים עשרה", "تنعش")] -# בניין שני, פַעַّל: the nine verbs of פִעֵל, done to someone else; Hebrew, past and future of "he" -FAAAL_2_3 = [("הוריד", "نزّل", "ينزّل"), ("הוביל", "وصّل", "يوصّل"), ("הצחיק", "ضحّك", "يضحّك"), ("הלביש", "لبّس", "يلبّس"), ("השמיע", "سمّع", "يسمّع"), - ("החזיר", "رجّع", "يرجّع"), ("הסביר", "فهّم", "يفهّم"), ("הוציא", "طلّع", "يطلّع"), ("השקה", "شرّب", "يشرّب")] -VERBS_2_3 = [ - ("לָבַשְׁתִּי לָבַשְׁתָּ לָבַשְׁתְּ לָבַשׁ לָבְשָׁה לָבַשְׁנוּ לְבַשְׁתֶּם לָבְשׁוּ", "لبست لبست لبستي لبس لبست#f لبسنا لبستو لبسو", "يلبس"), - ("הִלְבַּשְׁתִּי הִלְבַּשְׁתָּ הִלְבַּשְׁתְּ הִלְבִּישׁ הִלְבִּישָׁה הִלְבַּשְׁנוּ הִלְבַּשְׁתֶּם הִלְבִּישׁוּ", "لبّست لبّست لبّستي لبّس لبّست#f لبّسنا لبّستو لبّسو", "يلبّس"), -] -# level 2, lesson 4 (the workbook's thirteenth): five more of בניין שני; Hebrew, past and future of "he" -FAAAL_2_4 = [("תיקן", "صلّح", "يصلّح"), ("עדכן", "خبّر", "يخبّر"), ("הכין", "حضّر", "يحضّر"), ("שר", "غنّى", "يغنّي"), ("חשב", "فكّر", "يفكّر")] -VERBS_2_4 = [ - ("חָשַׁבְתִּי חָשַׁבְתָּ חָשַׁבְתְּ חָשַׁב חָשְׁבָה חָשַׁבְנוּ חֲשַׁבְתֶּם חָשְׁבוּ", "فكّرت فكّرت فكّرتي فكّر فكّرت#f فكّرنا فكّرتو فكّرو", "يفكّر"), - ("שַׁרְתִּי שַׁרְתָּ שַׁרְתְּ שָׁר שָׁרָה שַׁרְנוּ שַׁרְתֶּם שָׁרוּ", "غنّيت غنّيت غنّيتي غنّى غنّت غنّينا غنّيتو غنّو", "يغنّي"), -] -# level 2, lesson 5 (the workbook's fourteenth): גזרת ע"י (the past's ا is the future's ي), and the story's two verbs -BASES_2_5 = [("הוסיף", "زاد", "يزيد"), ("הביא", "جاب", "يجيب"), ("מכר", "باع", "يبيع"), ("נהיה", "صار", "يصير"), ("עצר", "وقّف", "يوقّف"), - ("חיכה", "استنّى", "يستنّى")] -VERBS_2_5 = [ - ("הֵבֵאתִי הֵבֵאתָ הֵבֵאת הֵבִיא הֵבִיאָה הֵבֵאנוּ הֲבֵאתֶם הֵבִיאוּ", "جبت جبت جبتي جاب جابت جبنا جبتو جابو", "يجيب"), - ("נִהְיֵיתִי נִהְיֵיתָ נִהְיֵית נִהְיָה נִהְיְתָה נִהְיֵינוּ נִהְיֵיתֶם נִהְיוּ", "صرت صرت صرتي صار صارت صرنا صرتو صارو", "يصير"), -] -PRESENT_2_5 = [ - ("מֵבִיא", "מְבִיאָה", "מְבִיאִים", "بجيب بتجيب بتجيبي بيجيب بتجيب بنجيب بتجيبو بيجيبو"), - ("נִהְיֶה", "נִהְיֵית", "נִהְיִים", "بصير بتصير بتصيري بيصير بتصير بنصير بتصيرو بيصيرو"), -] -# level 2, lesson 6 (the workbook's fifteenth): בניין שלישי, four letters and the second is ا -BASES_2_6 = [("עזר", "ساعد", "يساعد"), ("ענה", "جاوب", "يجاوب"), ("טיפל", "عالج", "يعالج"), ("מצא, פגש", "لاقى", "يلاقي"), ("עשה", "ساوى", "يساوي")] -VERBS_2_6 = [ - ("עָזַרְתִּי עָזַרְתָּ עָזַרְתְּ עָזַר עָזְרָה עָזַרְנוּ עֲזַרְתֶּם עָזְרוּ", "ساعدت ساعدت ساعدتي ساعد ساعدت#f ساعدنا ساعدتو ساعدو", "يساعد"), - ("מָצָאתִי מָצָאתָ מָצָאת מָצָא מָצְאָה מָצָאנוּ מְצָאתֶם מָצְאוּ", "لاقيت لاقيت لاقيتي لاقى لاقت لاقينا لاقيتو لاقو", "يلاقي"), -] -PRESENT_2_6 = [ - ("עוֹזֵר", "עוֹזֶרֶת", "עוֹזְרִים", "بساعد بتساعد بتساعدي بيساعد بتساعد بنساعد بتساعدو بيساعدو"), - ("מוֹצֵא", "מוֹצֵאת", "מוֹצְאִים", "بلاقي بتلاقي بتلاقي بيلاقي بتلاقي بنلاقي بتلاقو بيلاقو"), -] -# level 2, lesson 7 (the workbook's sixteenth): more of פִעֵל, and the bus story's three verbs -BASES_2_7 = [("שכח", "نسي", "ينسى"), ("הצליח", "نجح", "ينجح"), ("עשה", "عمل", "يعمل"), ("כעס", "زعل", "يزعل"), ("נפל", "وقع", "يوقع"), - ("רכב, עלה על", "ركب", "يركب"), ("צרח", "صرخ", "يصرخ"), ("פצע", "جرح", "يجرح"), ("בכה", "بكى", "يبكي")] -VERBS_2_7 = [ - ("כָּעַסְתִּי כָּעַסְתָּ כָּעַסְתְּ כָּעַס כָּעֲסָה כָּעַסְנוּ כְּעַסְתֶּם כָּעֲסוּ", "زعلت زعلت زعلتي زعل زعلت#f زعلنا زعلتو زعلو", "يزعل"), - ("שָׁכַחְתִּי שָׁכַחְתָּ שָׁכַחְתְּ שָׁכַח שָׁכְחָה שָׁכַחְנוּ שְׁכַחְתֶּם שָׁכְחוּ", "نسيت نسيت نسيتي نسي نسيت#f نسينا نسيتو نسيو", "ينسى"), -] -PRESENT_2_7 = [ - ("רָץ", "רָצָה", "רָצִים", "بركض بتركض بتركضي بيركض بتركض بنركض بتركضو بيركضو"), - ("כּוֹעֵס", "כּוֹעֶסֶת", "כּוֹעֲסִים", "بزعل بتزعل بتزعلي بيزعل بتزعل بنزعل بتزعلو بيزعلو"), -] -# level 2, lesson 8 (the workbook's seventeenth): גזרת הכפולים, two letters and a shadda on the second -BASES_2_8 = [("שם, הניח", "حطّ", "يحطّ"), ("ענה", "ردّ", "يردّ"), ("אהב, רצה", "حبّ", "يحبّ"), ("נשאר, המשיך", "ضلّ", "يضلّ"), ("הרגיש", "حسّ", "يحسّ"), - ("צלצל", "رنّ", "يرنّ"), ("הכעיס", "زعّل", "يزعّل")] -VERBS_2_8 = [ - ("נִשְׁאַרְתִּי נִשְׁאַרְתָּ נִשְׁאַרְתְּ נִשְׁאַר נִשְׁאֲרָה נִשְׁאַרְנוּ נִשְׁאַרְתֶּם נִשְׁאֲרוּ", "ضلّيت ضلّيت ضلّيتي ضلّ ضلّت ضلّينا ضلّيتو ضلّو", "يضلّ"), - ("שַׂמְתִּי שַׂמְתָּ שַׂמְתְּ שָׂם שָׂמָה שַׂמְנוּ שַׂמְתֶּם שָׂמוּ", "حطّيت حطّيت حطّيتي حطّ حطّت حطّينا حطّيتو حطّو", "يحطّ"), -] -PRESENT_2_8 = [ - ("נִשְׁאָר", "נִשְׁאֶרֶת", "נִשְׁאָרִים", "بضلّ بتضلّ بتضلّي بيضلّ بتضلّ بنضلّ بتضلّو بيضلّو"), - ("אוֹהֵב", "אוֹהֶבֶת", "אוֹהֲבִים", "بحبّ بتحبّ بتحبّي بيحبّ بتحبّ بنحبّ بتحبّو بيحبّو"), -] -# level 2, lesson 9 (the workbook's eighteenth): the active participle (سامع): Hebrew stem, Arabic masculine, and the four -# Hebrew forms where they are not the stem and its endings; the Arabic ones always are -ACTIVE_2_9 = [("גר", "ساكن", ""), ("שומע", "سامع", "שומע, שומעת, שומעים, שומעות"), ("הולך", "رايح", "הולך, הולכת, הולכים, הולכות"), - ("מבין", "فاهم", "מבין, מבינה, מבינים, מבינות"), ("יוצא", "طالع", "יוצא, יוצאת, יוצאים, יוצאות"), ("חוזר", "راجع", "חוזר, חוזרת, חוזרים, חוזרות"), - ("יודע", "عارف", "יודע, יודעת, יודעים, יודעות"), ("ישן", "نايم", "ישן, ישנה, ישנים, ישנות")] -# a verb and its participle: Hebrew past and present, Arabic past of "he" and the participle -ACTIVE_PAIRS_2_9 = [("ישב", "יושב", "قعد", "قاعد"), ("ירד", "יורד", "نزل", "نازل"), ("בא", "בא (עכשיו)", "اجا", "جاي"), ("עשה", "עושה", "عمل", "عامل"), - ("יכול היה", "יכול", "قدر", "قادر"), ("הלך ברגל", "הולך, בסדר", "مشي", "ماشي"), ("ראה", "רואה", "شاف", "شايف"), - ("נכנס", "נכנס (עכשיו)", "فات", "فايت"), ("הביא", "מביא", "جاب", "جايب"), ("חי", "חי (עכשיו)", "عاش", "عايش"), - ("אהב, רצה", "אוהב, רוצה", "حبّ", "حابب"), ("שם", "שם (עכשיו)", "حطّ", "حاطط"), ("עמד", "עומד", "وقف", "واقف"), - ("הרגיש", "מרגיש", "حسّ", "حاسس"), ("פתח", "פותח, פתוח", "فتح", "فاتح"), ("נהיה", "קורה", "صار", "صاير")] -BASES_2_9 = [("ידע, הכיר", "عرف", "يعرف"), ("יכול היה", "قدر", "يقدر"), ("הלך ברגל", "مشي", "يمشي"), ("ישן", "نام", "ينام"), ("חי", "عاش", "يعيش")] -HE_MUST = "צריך צריך צריכה צריך צריכה צריכים צריכים צריכים".split() -HE_GOING = "הולך הולך הולכת הולך הולכת הולכים הולכים הולכים".split() -BIDD = "بدّي بدّك بدّك#f بدّه بدّها بدّنا بدّكم بدّهم".split() # "want", one per pronoun -HE_WANT = ["רוצה"] * 5 + ["רוצים"] * 3 - - -def _wants(verbs): - """Every pronoun's "wants to": אני רוצה לצאת -> بدّي اطلع. Arabic has no infinitive: the verb is in the future.""" - return [(_plain(f"{hp} {want} {inf}"), f"{bidd} {a}", f"{hp} {want} {inf}") for inf, ar in verbs - for hp, want, bidd, a in zip(HE_PRONOUNS, HE_WANT, BIDD, ar.split())] - - -def _before_future(word, he_words, verbs): - """Every pronoun's future after a word that does not change (لازم, رح): אני צריך לראות -> انا لازم اشوف.""" - return [(_plain(f"{hp} {hw} {inf}"), f"{ap} {word} {a}", f"{hp} {hw} {inf}") for inf, ar in verbs - for hp, ap, hw, a in zip(HE_PRONOUNS, AR_PRONOUNS, he_words, ar.split())] - - -def _present(verbs): - """Every pronoun's present: אני שואל -> انا بسأل. Hebrew's present has he's form, she's and theirs.""" - return [(_plain(f"{hp} {h}"), f"{ap} {a}", f"{hp} {h}") for m, f, pl, ar in verbs - for hp, ap, h, a in zip(HE_PRONOUNS, AR_PRONOUNS, (m, m, f, m, f, pl, pl, pl), ar.split())] - - -def _base_forms(verbs): - """A verb as the workbook lists it: past and future of "he".""" - out = [] - for he, ar, future, *note in verbs: - base, note = he.split()[HUWWE], "".join(f" {n}" for n in note[:1]) - out.append((f"{_plain(base)}{note} (עבר, עתיד)", f"{ar.split()[HUWWE]}، {future}", base + note)) - return out - - -def _past(verbs): - """Every pronoun with its form (the past's, or the future's of FUTURE_6): אני פתחתי -> انا فتحت.""" - return [(_plain(f"{hp} {h}") + note, f"{ap} {a}", f"{hp} {h}{note}") for he, ar, *rest in verbs - for note in ["".join(f" {n}" for n in rest[2:])] - for hp, ap, h, a in zip(HE_PRONOUNS, AR_PRONOUNS, he.split(), ar.split())] - - -# lesson 2's dual: Hebrew singular and dual, Arabic singular and dual; said together like a verb's past and future -DUALS = [ - ("שבוע", "שבועיים", "اسبوع", "اسبوعين"), ("כוס", "שתי כוסות", "كاسة", "كاستين"), ("יד", "ידיים", "ايد", "ايدين"), - ("דלת", "שתי דלתות", "باب", "بابين"), ("ספל קפה", "שני ספלי קפה", "فنجان", "فنجانين"), - ("חולצה", "שתי חולצות", "بلوزة", "بلوزتين"), ("שכונה", "שתי שכונות", "حارة", "حارتين"), - ("ילד", "שני ילדים", "ولد", "ولدين"), ("שאלה", "שתי שאלות", "سؤال", "سؤالين"), ("אח", "שני אחים", "اخ", "اخين"), - ("מטוס", "שני מטוסים", "طيّارة", "طيّارتين"), ("מכונית", "שתי מכוניות", "سيّارة", "سيّارتين"), - ("בת", "שתי בנות", "بنت", "بنتين"), ("יום", "יומיים", "يوم", "يومين"), ("שולחן", "שני שולחנות", "طاولة", "طاولتين"), - ("נכד", "שני נכדים", "حفيد", "حفيدين"), ("כלב", "שני כלבים", "كلب", "كلبين"), -] -FAMILY = [ - ("הורים, משפחה קרובה", "اهل"), ("אבא", "اب"), ("אמא", "امّ"), ("דוד (מצד האב)", "عمّ"), ("דודה (מצד האב)", "عمّة"), - ("דוד (מצד האם)", "خال"), ("דודה (מצד האם)", "خالة"), ("אח", "اخ"), ("אחות", "اخت"), ("בן", "ابن"), - ("ילד", "ولد"), ("ילדים", "اولاد"), ("בת", "بنت"), ("נכד", "حفيد"), ("נכדה", "حفيدة"), - ("סבא (שתי מילים)", "سيد، جدّ"), ("סבתא (שתי מילים)", "ستّ، جدّة"), ("בכור", "بكر"), ("בכורה", "بكرة"), - ("בן הזקונים", "آخر العنقود"), ("בעל", "جوز"), ("אישה", "مرة"), ("נשים", "نسوان"), ("גבר", "زلمة"), -] -# the words drilled with every owner's suffix: Hebrew, Arabic stem (see POSSESSED) -POSSESSIVE_DRILL = [("הבת", "بنت"), ("השולחן", "طاولت"), ("הבן", "ابن"), ("הכלב", "كلب"), ("הנכדה", "حفيدت"), ("הסבא", "جدّ")] - - -def _possessives(nouns): - """Every word with every owner: הבת שלי -> بنتي. שלך is pointed for the prompt voice.""" - return [(f"{he} {owner}", stem + suffix + tag, *([f"{he} {say}"] if say else [])) - for he, stem in nouns for owner, say, suffix, tag, *_ in OWNERS] - - -# lesson 3's numbers: digits, Hebrew, Arabic. The card shows the digits beside the Hebrew; the voice says the Hebrew. -NUMBERS = [ - ("0", "אפס", "صفر"), ("1", "אחד", "واحد"), ("2", "שתיים", "تنين"), ("3", "שלוש", "تلاتة"), ("4", "ארבע", "اربعة"), - ("5", "חמש", "خمسة"), ("6", "שש", "ستّة"), ("7", "שבע", "سبعة"), ("8", "שמונה", "تمانية"), ("9", "תשע", "تسعة"), - ("10", "עשר", "عشرة"), ("11", "אחת עשרה", "حدعش"), ("12", "שתים עשרה", "تنعش"), ("13", "שלוש עשרה", "تلاتعش"), - ("14", "ארבע עשרה", "اربعتعش"), ("15", "חמש עשרה", "خمستعش"), ("16", "שש עשרה", "ستّعش"), ("17", "שבע עשרה", "سبعتعش"), - ("18", "שמונה עשרה", "تمنتعش"), ("19", "תשע עשרה", "تسعتعش"), ("20", "עשרים", "عشرين"), ("30", "שלושים", "تلاتين"), - ("40", "ארבעים", "اربعين"), ("50", "חמישים", "خمسين"), ("60", "שישים", "ستّين"), ("70", "שבעים", "سبعين"), - ("80", "שמונים", "تمانين"), ("90", "תשעים", "تسعين"), ("100", "מאה", "ميّة"), ("200", "מאתיים", "ميتين"), - ("300", "שלוש מאות", "تلات ميّة"), ("1,000", "אלף", "الف"), ("2,000", "אלפיים", "الفين"), - ("3,000", "שלושת אלפים", "تلات الاف"), ("10,000", "עשרת אלפים", "عشرة#of الاف"), ("100,000", "מאה אלף", "ميّة#of الف"), - ("1,000,000", "מיליון", "مليون"), -] - -# lesson 7, a review: an adjective's masculine and feminine, said together (Hebrew and Arabic of each) -FEMININES = [ - ("גדול", "גדולה", "كبير", "كبيرة"), ("קטן", "קטנה", "صغير", "صغيرة"), ("חדש", "חדשה", "جديد", "جديدة"), - ("מתוק", "מתוקה", "حلو", "حلوة"), ("נשוי", "נשואה", "متجوّز", "متجوّزة"), ("רווק", "רווקה", "عزّابي", "عزّابيّة"), - ("שמח", "שמחה", "مبسوط", "مبسوطة"), ("רעב", "רעבה", "جوعان", "جوعانة"), -] -# ... its duals, as lesson 2's -DUALS_7 = [ - ("יום", "יומיים", "يوم", "يومين"), ("שנה", "שנתיים", "سنة", "سنتين"), ("ילד", "שני ילדים", "ولد", "ولدين"), - ("בת", "שתי בנות", "بنت", "بنتين"), ("מכונית", "שתי מכוניות", "سيّارة", "سيّارتين"), ("כלב", "שני כלבים", "كلب", "كلبين"), - ("שבוע", "שבועיים", "اسبوع", "اسبوعين"), ("פעם", "פעמיים", "مرّة", "مرّتين"), ("כוס", "שתי כוסות", "كاسة", "كاستين"), - ("שולחן", "שני שולחנות", "طاولة", "طاولتين"), ("בית", "שני בתים", "بيت", "بيتين"), ("שכונה", "שתי שכונות", "حارة", "حارتين"), - ("מיטה", "שתי מיטות", "تخت", "تختين"), -] -# ... and its numbers, as NUMBERS -NUMBERS_7 = [ - ("7", "שבע", "سبعة"), ("12", "שתים עשרה", "تنعش"), ("25", "עשרים וחמש", "خمسة وعشرين"), ("40", "ארבעים", "اربعين"), - ("100", "מאה", "ميّة"), ("175", "מאה שבעים וחמש", "ميّة خمسة وسبعين"), ("200", "מאתיים", "ميتين"), - ("202", "מאתיים ושתיים", "ميتين وتنين"), ("268", "מאתיים שישים ושמונה", "ميتين تمانية وستّين"), - ("365", "שלוש מאות שישים וחמש", "تلات ميّة خمسة وستّين"), ("1,000", "אלף", "الف"), ("2,026", "אלפיים עשרים ושש", "الفين ستّة وعشرين"), -] - -# lesson 8's colours, masculine and feminine said together (Hebrew and Arabic of each); a colour with two words has a -# hint after the second one -COLORS = [ - ("אדום", "אדומה", "احمر", "حمرا"), ("כחול", "כחולה", "ازرق", "زرقا"), ("ירוק", "ירוקה", "اخضر", "خضرا"), - ("לבן", "לבנה", "ابيض", "بيضا"), ("שחור", "שחורה", "اسود", "سودا"), ("צהוב", "צהובה", "اصفر", "صفرا"), - ("חום", "חומה", "بنّي", "بنّيّة"), ("ורוד", "ורודה", "زهري", "زهريّة"), ("ורוד", "ורודה (מהמילה ורד)", "وردي", "ورديّة"), - ("סגול", "סגולה", "بنفسجي", "بنفسجيّة"), ("כתום", "כתומה", "برتقالي", "برتقاليّة"), ("אפור", "אפורה", "سكني", "سكنيّة"), - ("אפור", "אפורה (מהמילה אפר)", "رمادي", "رماديّة"), ("כסף", "כסופה", "فضّة", "فضّيّة"), ("תכלת", "תכולה", "سماوي", "سماويّة"), - ("בהיר", "בהירה", "فاتح", "فاتحة"), ("כהה", "כהה (לנקבה)", "غامق", "غامقة"), -] - -# lesson 10's adjectives, each said with its comparative (Hebrew, Arabic, the comparative) -COMPARATIVES = [ - ("גדול", "كبير", "اكبر"), ("קטן", "صغير", "اصغر"), ("גבוה", "طويل", "اطول"), ("נמוך", "قصير", "اقصر"), ("שמן", "ناصح", "انصح"), - ("רזה", "ضعيف", "اضعف"), ("כבד", "تقيل", "اتقل"), ("הרבה", "كتير", "اكتر"), ("טוב", "منيح", "احسن"), ("רחוק", "بعيد", "ابعد"), - ("קרוב", "قريب", "اقرب"), ("זול", "رخيص", "ارخص"), ("נקי", "نضيف", "انضف"), ("מלוכלך", "وسخ", "اوسخ"), ("עני", "فقير", "افقر"), - ("מכוער", "بشع", "ابشع"), ("טעים", "طيّب", "اطيب"), ("רע", "عاطل", "اعطل"), ("חכם", "شاطر", "اشطر"), ("ישן", "قديم", "اقدم"), - ("יפה", "جميل", "اجمل"), ("מתוק", "حلو", "احلى"), ("יקר", "غالي", "اغلى"), ("טעים (לאוכל בלבד)", "زاكي", "ازكى"), ("עשיר", "غني", "اغنى"), - ("חזק", "قوي", "اقوى"), ("דק", "رقيق", "ارقّ"), ("קל", "خفيف", "اخفّ"), ("יקר (אדם אהוב)", "عزيز", "اعزّ"), ("חשוב", "مهمّ", "اهمّ"), - ("מעט", "قليل", "اقلّ"), ("נכון", "صحيح", "اصحّ"), ("חדש", "جديد", "اجدد"), -] - -# The course's levels: level -> (name, how many lessons it will have). A level's lessons that are not in LESSONS yet -# are shown on the path as "בקרוב". -LEVELS = {1: ("שלב א", 10), 2: ("שלב ב", 9)} # שלב ב ends at שיעור 18 (2.9): there is no שיעור 19 -# A level whose workbook goes on counting from the one before: the number its first lesson is shown with. The -# lessons' keys and ids still count from 1 within the level ((2, 1), "2.1"). -FIRST_LESSON = {2: 10} - -# (level, lesson) -> its topics: (id, name, kind, shown in order, [(Hebrew, Arabic) or (Hebrew, Arabic, Hebrew as read -# aloud)]); sentence numbering is the workbook's. Every lesson names its level. -LESSONS = { - (1, 1): [ - ("new", "מילים חדשות", "learn", True, [ - ("דלת", "باب"), ("מכונית", "سيّارة"), ("שאלה", "سؤال"), ("הרופא", "الدّكتور"), ("כלב", "كلب"), ("בית", "بيت"), - ("שולחן", "طاولة"), ("ילד", "ولد"), ("ילדה, בת", "بنت"), ("שכונה", "حارة"), ("כיסא", "كرسي"), ("מטוס", "طيّارة"), - ("חבר", "صاحب"), ("עם", "مع"), ("ב, בתוך, יש", "في"), ("איפה", "وين"), ("מי", "مين"), ("מדוע", "ليش"), ("מתי", "ايمتى"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("pron", "הגופים", "say", True, [(_plain(he) + " / הן" * (he == "הֵם"), ar, he) for he, ar in zip(HE_PRONOUNS, AR_PRONOUNS)]), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_1)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_1)), - ("say", "משפטים: עברית ← ערבית", "say", True, [ - ("אני שאלתי מדוע היא פתחה את הדלת היום?", "انا سألت ليش هيّ فتحت#f الباب اليوم؟"), - ("אנחנו שאלנו את הרופאה איפה הנגר גר, הוא גר בירושלים.", "احنا سألنا الدّكتورة وين النّجّار ساكن، هوّ ساكن في القدس."), - ("אני אכלתי עגבניות, אתם אכלתם מלפפונים.", "انا اكلت بندورة، انتو اكلتو خيار."), - ("מי שאל שאלה? אני שאלתי שאלה. אני שאלתי את חברי שאלה.", "مين سأل سؤال؟ انا سألت سؤال. انا سألت صاحبي سؤال."), - ("אנחנו פתחנו את דלת המכונית, הם פתחו את דלת המטוס.", "احنا فتحنا باب السّيّارة، همّ فتحو باب الطّيّارة."), - ("הרופא ישב בביתו עם הכלב, הילד שאל מדוע.", "الدّكتور قعد في بيته مع الكلب، الولد سأل ليش."), - ("מי פתח את הדלת? היא פתחה את דלת הבית ושאלה שאלה.", "مين فتح الباب؟ هيّ فتحت#f باب البيت وسألت#f سؤال."), - ("אמי שאלה את חברי: מי ישב במכונית? אנחנו ישבנו במכונית.", "امّي سألت#f صاحبي: مين قعد في السّيّارة؟ احنا قعدنا في السّيّارة."), - ("חברו שאל את חברי מי ישב ואכל בביתו פלאפל?", "صاحبه سأل صاحبي مين قعد واكل في بيته فلافل؟"), - ("אנחנו אכלנו כנאפה, היא אכלה בקלאווה, מה אתם אכלתם?", "احنا اكلنا كنافة، هيّ اكلت#f بقلاوة، شو انتو اكلتو؟"), - ]), - ("read", "משפטים: ערבית ← עברית", "read", True, [ - ("אתם אכלתם עם חבר שלי? לא, אנחנו אכלנו עם אחמד.", "انتو اكلتو مع صاحبي؟ لا، احنا اكلنا مع احمد."), - ("מה אתה שאלת את הילד? אני שאלתי אותו למה הוא ישב בשכונה.", "شو انت سألت الولد؟ انا سألته ليش هوّ قعد في الحارة."), - ("הדלת פתוחה, מי פתח את הדלת? אתם פתחתם את הדלת?", "الباب مفتوح، مين فتح الباب؟ انتو فتحتو الباب؟"), - ("אני שאלתי אותך שאלה ואתה שאלת את חבר שלי שאלה.", "انا سألتك سؤال وانت سألت صاحبي سؤال."), - ("אנחנו פתחנו את דלת המטוס, דלת המטוס פתוחה.", "احنا فتحنا باب الطّيّارة، باب الطّيّارة مفتوح."), - ("אתם שאלתם אותי שאלה? לא, אנחנו שאלנו את המורה שאלה.", "انتو سألتوني سؤال؟ لا، احنا سألنا المعلّم سؤال."), - ("מי אכל את האוכל? אנחנו אכלנו, ברוך השם.", "مين اكل الأكل؟ احنا اكلنا الحمد للّه."), - ("היא שאלה את פאטמה שאלה ואני שאלתי אותה שאלה גם.", "هيّ سألت#f فاطمة سؤال وانا سألتها سؤال كمان."), - ("הוא אכל פלאפל ואני אכלתי שווארמה. מה אתה אכלת היום?", "هوّ اكل فلافل وانا اكلت شاورما. شو انت اكلت اليوم؟"), - ("הילדה ישבה עם הילד, הם ישבו בבית ואכלו.", "البنت قعدت#f مع الولد، همّ قعدو في البيت واكلو."), - ("המורה ישב ליד השולחן ושאל אותנו שאלה.", "المعلّم قعد على الطّاولة وسألنا#us سؤال."), - ("אני שאלתי: למה הכלב ישב בבית?", "انا سألت: ليش الكلب قعد في البيت؟"), - ("הרופא שאל את חבר שלי: מי ישב על הכיסא?", "الدّكتور سأل صاحبي: مين قعد على الكرسي؟"), - ("אנחנו ישבנו בשכונה עם המורה.", "احنا قعدنا في الحارة مع المعلّم."), - ("היא שאלה את הנגר למה הוא פתח את דלת המכונית.", "هيّ سألت#f النّجّار ليش هوّ فتح باب السّيّارة."), - ("אתם ישבתם עם הבת שלי במטוס ואכלתם איתה?", "انتو قعدتو مع بنتي في الطّيّارة واكلتو معها؟"), - ("את ישבת על הכיסא ושאלת אותי שאלה גדולה.", "انتي قعدتي على الكرسي وسألتيني سؤال كبير."), - ("את, חבר שלי ואני ישבנו בבית שלי ואכלנו פיצה.", "انتي، صاحبي وانا قعدنا في بيتي واكلنا بيتسا."), - ("הרופאה שאלה אותך שאלה: למה את ישבת על השולחן?", "الدّكتورة سألتك#f سؤال: ليش انتي قعدتي على الطّاولة؟"), - ("אחי פתח את דלת הבית ושאל את אחותי שאלה.", "اخوي فتح باب البيت وسأل اختي سؤال."), - ("אני שאלתי את אבא שלי מי אכל את האוכל? אבא שלי: \"הכלב אכל את האוכל\".", "انا سألت ابوي مين اكل الأكل؟ ابوي: الكلب اكل الأكل."), - ("המורה שאלה את הילדה: \"חרוצה, למה ישבת על השולחן?\"", "المعلّمة سألت#f البنت: يا شاطرة ليش قعدتي على الطّاولة؟"), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 2): [ - ("new", "מילים חדשות", "learn", True, [ - ("לפני", "قبل"), ("אצל, יש ל", "عند"), ("שם", "اسم"), ("הים", "البحر"), ("אחר כך", "بعدين"), - ("אבל, רק", "بسّ"), ("נשוי", "متجوّز"), ("רווק", "عزّابي"), ("קטנה", "صغيرة"), ("חדש", "جديد"), - ("מסעדה", "المطعم"), ("חלון", "شبّاك"), ("חדר", "غرفة"), ("בית ספר", "المدرسة"), ("כאן", "هون"), - ("כלומר", "يعني"), ("שמח", "مبسوط"), ("טעים", "زاكي"), ("מטבח", "المطبخ"), ("הכפר, העיר", "البلد"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("dual", "הזוגי", "say", True, [(f"{he}, {he2}", f"{ar}، {ar2}") for he, he2, ar, ar2 in DUALS]), - ("family", "המשפחה", "say", True, FAMILY), - ("poss", "כינויי הקניין", "say", False, _possessives(POSSESSIVE_DRILL)), - ("say", "משפטים: עברית ← ערבית א'", "say", True, [ - ("אני ישבתי עם חברי לפני יומיים ואכלנו פיצה בסלון.", "انا قعدت مع صاحبي قبل يومين واكلنا بيتسا في الصّالون."), - ("יש לי שולחן גדול בבית, ישבתי עם אשתי לפני שבוע.", "عندي طاولة كبيرة في البيت، قعدت مع مرتي قبل اسبوع."), - ("אבי גר בחברון ואני גר עם אחי בשכם.", "ابوي ساكن في الخليل وانا ساكن مع اخوي في نابلس."), - ("את שאלת אותי איפה נכדי? נכדי עם נכדך.", "انتي سألتيني وين حفيدي؟ حفيدي مع حفيدك#f."), - ("איפה סבא שלך? סבא שלי ישב בביתו. (לגבר)", "وين سيدك؟ سيدي قعد في بيته."), - ("יש לי ילדים, שני בנים ובת אחת.", "عندي اولاد، ولدين وبنت واحدة."), - ("כלבי אצל אחי, הוא גר בירושלים בשכונה קטנה.", "كلبي عند اخوي، هوّ ساكن في القدس في حارة صغيرة."), - ("האחים שלי בבית אבי, איפה האחים שלך? (לגבר)", "اخوتي في بيت ابوي، وين اخوتك؟"), - ("איפה כלבכם? הוא אצל דודי (מצד אמא).", "وين كلبكم؟ هوّ عند خالي."), - ("שם בתי הבכורה מונא, מה שם בתך הבכורה? (לאישה)", "اسم بنتي البكرة مونا، شو اسم بنتك#f البكرة؟"), - ("אני גרה בחברון, נכדתי גרה עם נכדי בבית אמם.", "انا ساكنة في الخليل، حفيدتي ساكنة مع حفيدي في بيت امّهم."), - ("דודי (מצד אמא) ישב עם דודתי (מצד אמא), הם אכלו בקלאווה בבית.", "خالي قعد مع خالتي، همّ اكلو بقلاوة في البيت."), - ("יש לך ילדים? יש לי שתי בנות ובן אחד. (לאישה)", "عندك#f اولاد؟ عندي بنتين وولد واحد."), - ("אבי עם אחי, הם ישבו במוסך הקטן.", "ابوي مع اخوي، همّ قعدو في الكراج الصّغير."), - ("בני ובנך גרים בחיפה, הם אכלו בבית חברי. (לאישה)", "ابني وابنك#f ساكنين في حيفا، همّ اكلو في بيت صاحبي."), - ("בעלך אכל במסעדה עם בעלי לפני שבועיים.", "جوزك#f اكل في المطعم مع جوزي قبل اسبوعين."), - ("יש לי שני נכדים, אחד ישב בביתו ואחד ישב בביתי.", "عندي حفيدين، واحد قعد في بيته وواحد قعد في بيتي."), - ("שם בני הבכור שריף, מה שם בנך הבכור? שמו היתם. (לאישה)", "اسم ابني البكر شريف، شو اسم ابنك#f البكر؟ اسمه هيثم."), - ("לתושבי הכפר יש שיח' מצוין.", "اهل البلد عندهم شيخ ممتاز."), - ("דודנו (מצד אבא) ישב על הכיסא ושאל אותנו שתי שאלות.", "عمّنا قعد على الكرسي وسألنا#us سؤالين."), - ("בני הבכור שאל את בנך הבכור שאלה גדולה. (לאישה)", "ابني البكر سأل ابنك#f البكر سؤال كبير."), - ("אבי פתח את הדלת, אמי שאלה אותו: \"מדוע פתחת אותה?\"", "ابوي فتح الباب، امّي سألته#f: ليش فتحته؟"), - ("מי ישב בבית השיח'? אני וחברי ישבנו בבית השיח'.", "مين قعد في بيت الشّيخ؟ انا وصاحبي قعدنا في بيت الشّيخ."), - ]), - ("say2", "משפטים: עברית ← ערבית ב'", "say", True, [ - ("לפני שבועיים אכלתי אוכל טעים ודודי (מצד אמא) אכל איתי.", "قبل اسبوعين اكلت اكل#n زاكي وخالي اكل معي."), - ("ישבנו בים ואכלנו אבטיח, הם אכלו עגבניות.", "قعدنا في البحر واكلنا بطّيخة، همّ اكلو بندورة."), - ("אתה שאלת מי אכל את העוגה ומי אכל את השווארמה.", "انت سألت مين اكل الكعكة ومين اكل الشّاورما."), - ("החלון פתוח? לא, אני פתחתי את הדלת ואחר כך ישבתי בבית.", "الشّبّاك مفتوح؟ لا، انا فتحت الباب وبعدين قعدت في البيت."), - ("היא פתחה את דלת החדר בבית.", "هيّ فتحت#f باب الغرفة في البيت."), - ("לפני יומיים מחמוד ישב עם אחותו בים, הם אכלו פלאפל.", "قبل يومين محمود قعد مع اخته في البحر، همّ اكلو فلافل."), - ("אני שאלתי אותם שאלה, הם שאלו אותי שתי שאלות.", "انا سألتهم سؤال، همّ سألوني سؤالين."), - ("בני גר בירושלים, מה שם השכונה?", "ابني ساكن في القدس، شو اسم الحارة؟"), - ("היא ישבה במלון ושאלה איפה הטבח?", "هيّ قعدت#f في الاوتيل وسألت#f وين الطّبّاخ؟"), - ("אתם שאלתם אותי מי ישב בבית החדש? אני ישבתי בבית החדש.", "انتو سألتوني مين قعد في البيت الجديد؟ انا قعدت في البيت الجديد."), - ("למה שאלת את אחותך שתי שאלות? אמי שאלה את אחי. (לגבר)", "ليش سألت اختك سؤالين؟ امّي سألت#f اخوي."), - ("המורה שאלה אותנו מי פתח את דלת בית הספר.", "المعلّمة سألتنا مين فتح باب المدرسة."), - ("איפה ישב נכדך? נכדי ישב עם אחותו במכונית. (לאישה)", "وين قعد حفيدك#f؟ حفيدي قعد مع اخته في السّيّارة."), - ("הם ישבו עם סבי בביתו, סבתי שאלה: \"מה אכלתם היום?\"", "همّ قعدو مع سيدي في بيته، ستّي سألت#f: شو اكلتو اليوم؟"), - ("אבא של מחמוד ישב במסעדה ושאל אותי: \"איפה הכיסא הגדול?\"", "ابو محمود قعد في المطعم وسألني: وين الكرسي الكبير؟"), - ("אנחנו ישבנו עם סבתי, אחר כך אכלנו עם אמי אוכל טעים.", "احنا قعدنا مع ستّي، بعدين اكلنا مع امّي اكل#n زاكي."), - ("המורה שאלה אותה מתי היא ישבה בביתה החדש?", "المعلّمة سألتها#f ايمتى هيّ قعدت#f في بيتها الجديد؟"), - ("מה את שאלת את נכדי? שאלתי אותו מדוע הוא אכל אצל חברו.", "شو انتي سألتي حفيدي؟ سألته ليش هوّ اكل عند صاحبه."), - ("אמי ישבה בבית ושאלה את אחותי: \"מה את אכלת היום?\"", "امّي قعدت#f في البيت وسألت#f اختي: شو انتي اكلتي اليوم؟"), - ("המורה שאל אותי מתי פתחתי את דלת ביתי?", "المعلّم سألني ايمتى فتحت باب بيتي؟"), - ("איפה את אכלת היום? אני אכלתי בים עם אחותי הגדולה.", "وين انتي اكلتي اليوم؟ انا اكلت في البحر مع اختي الكبيرة."), - ("שכני גר בשכונה, הוא ואשתו ישבו כאן.", "جاري ساكن في الحارة، هوّ ومرته قعدو هون."), - ("ישבנו בים ואחר כך אכלנו אבטיח גדול.", "قعدنا في البحر وبعدين اكلنا بطّيخة كبيرة."), - ("דודתי (מצד אבא) ישבה בביתה עם בעלה ואכלה מקלובה.", "عمّتي قعدت#f في بيتها مع جوزها واكلت#f مقلوبة."), - ("אבי שאל את חברו מתי הוא ואשתו פתחו את דלת המכונית.", "ابوي سأل صاحبه ايمتى هوّ ومرته فتحو باب السّيّارة."), - ]), - ("read", "משפטים: ערבית ← עברית", "read", True, [ - ("סבא שלי ישב בבית ואכל עם סבתא שלי, הם אכלו עם אחותי.", "جدّي قعد في البيت واكل مع جدّتي، همّ اكلو مع اختي."), - ("הילד פתח היום שתי דלתות, אני שאלתי אותו: \"איפה אבא שלך, ילד?\"", "الولد فتح اليوم بابين، انا سألته: وين ابوك يا ولد؟"), - ("יש לי שתי מכוניות בבית, לחבר שלי יש מכונית אחת בבית שלו.", "عندي سيّارتين في البيت، صاحبي عنده سيّارة واحدة في بيته."), - ("אתם שאלתם את הילדה מי ישב במכונית.", "انتو سألتو البنت مين قعد في السّيّارة."), - ("סבתא שלי גרה בשכונת תלפיות בירושלים, איפה אתה גר?", "جدّتي ساكنة في حارة#of تلبيوت في القدس، وين انت ساكن؟"), - ("הבת הבכורה שלי, שמה רובה. מה שם הבת הבכורה שלך?", "بنتي البكرة اسمها روبا، شو اسم بنتك البكرة؟"), - ("יש לי כלב, שמו שוקו. לך יש כלב? יש לי שני כלבים.", "عندي كلب اسمه شوكو، انتي عندك#f كلب؟ عندي كلبين."), - ("לפני יומיים אנחנו אכלנו עם אבא שלי, מתי אתה אכלת איתו?", "قبل يومين احنا اكلنا مع ابوي، ايمتى انت اكلت معه؟"), - ("איפה הדוד שלך (מצד אמא)? הוא ישב עם אמא שלי ואכל בבית שלנו לפני שבועיים.", "وين خالك؟ هوّ قعد مع امّي واكل في بيتنا قبل اسبوعين."), - ("מה שם הבן הגדול שלך? זאת אומרת הבן הבכור שלך.", "شو اسم ابنك الكبير؟ يعني ابنك البكر."), - ("אני גרה עם אמא ואבא שלי בשכונה, איפה את גרה?", "انا ساكنة مع امّي وابوي في الحارة، وين انتي ساكنة؟"), - ("יש לי שתי כוסות בבית וכוס אחת במכונית שלי.", "عندي كاستين في البيت وكاسة واحدة في سيّارتي."), - ("אשתי פתחה את הדלת וישבה עם אבא שלה, אחר כך הם אכלו במטבח.", "مرتي فتحت#f الباب وقعدت#f مع ابوها، بعدين اكلو في المطبخ."), - ]), - ("talk", "שיחת היכרות", "say", True, [ - ("השלום עליכם", "السّلام عليكم"), - ("ועליכם השלום", "وعليكم السّلام"), - ("מה שלומך? (לגבר)", "كيف حالك؟"), - ("אני שמח, ברוך השם! איך אתה?", "انا مبسوط الحمد للّه! كيف انت؟"), - ("חיים, ברוך השם", "عايشين الحمد للّه"), - ("מה שמך? (לגבר)", "شو اسمك؟"), - ("אני, שמי ח'ליל. ומה שמך אתה?", "انا اسمي خليل، وشو اسمك انت؟"), - ("אני, שמי מחמוד", "انا اسمي محمود"), - ("נעים מאוד, מחמוד!", "تشرّفنا يا محمود!"), - ("יוסיף לך כבוד! (התשובה ל\"נעים מאוד\")", "يزيدك شرف!"), - ("בן כמה אתה, יקירי?", "قدّيش عمرك حبيبي؟"), - ("אני בן עשרים וחמש", "عمري خمسة وعشرين سنة"), - ("ה' יאריך את ימיך!", "الله يطوّل عمرك!"), - ("מאיפה אתה, ח'ליל?", "من وين انت يا خليل؟"), - ("אני במקור מטורעאן בצפון, אבל היום גר בירושלים", "انا اصلي من طرعان في الشّمال، بسّ اليوم ساكن في القدس"), - ("אתה נשוי או רווק?", "انت متجوّز ولّا عزّابي؟"), - ("אני נשוי ויש לי שני בנים ושתי בנות, זאת אומרת ארבעה ילדים, ברוך השם", "انا متجوّز وعندي ولدين وبنتين، يعني اربع اولاد الحمد للّه"), - ("ה' ישמור אותם!", "الله يخلّيهم!"), - ("ה' ישמור אותך! (לגבר)", "الله يحفظك!"), - ("במה אתה עובד?", "شو بتشتغل؟"), - ("אני עורך דין, ואתה?", "انا محامي، وانت؟"), - ("אני מלצר", "انا جرسون"), - ("יפה מאוד, יקירי", "حلو كتير حبيبي"), - ("טוב, אני ממהר קצת, ברשותך, אני צריך ללכת", "طيّب، انا مستعجل شويّ، عن اذنك، لازم اروح"), - ("בעזרת השם נתראה, יקירי", "انشالله نشوفك حبيبي"), - ("ה' איתך (לגבר)", "الله معك"), - ("עם השלום (להתראות)", "مع السّلامة"), - ]), - ("meet", "היכרות: עברית ← ערבית", "say", True, [ - ("אני במקור מתל אביב, איפה את גרה?", "انا اصلي من تلّ ابيب، وين انتي ساكنة؟"), - ("חבר שלי במקור מאשקלון אבל משפחתו גרה בירושלים.", "صاحبي اصله من عسقلان بسّ عيلته ساكنة في القدس."), - ("אני רווק, אחותי נשואה, יש לה שני בנים ובת קטנה.", "انا عزّابي، اختي متجوّزة، عندها ولدين وبنت صغيرة."), - ("אני עורך דין ואבי עורך דין גם.", "انا محامي وابوي محامي كمان."), - ("בן כמה אתה? אתה בן עשרים או בן שלושים? אני בן שלושים ואחת.", "قدّيش عمرك؟ عمرك عشرين ولّا تلاتين؟ عمري واحد وتلاتين."), - ("אני בן ארבעים ושש, בת כמה את? אני בת ארבעים ותשע.", "عمري ستّة واربعين، قدّيش عمرك#f؟ عمري تسعة واربعين."), - ("אני במקור מהצפון אבל לפני שבוע ישבתי פה.", "انا اصلي من الشّمال بسّ قبل اسبوع قعدت هون."), - ("שמי מחמד ומה שמך? שמי נור, נעים להכיר! (לגבר)", "اسمي محمّد وشو اسمك؟ اسمي نور، تشرّفنا!"), - ("אני בת עשרים ושש, יש לי שתי בנות, ה' ישמור עליהן.", "عمري ستّة وعشرين، عندي بنتين، الله يخلّيهم."), - ("יש לך ילדים? שלושה, ברוך השם. ואת? יש לי בן ובת, ה' ישמור אותם! (לגבר)", "عندك اولاد؟ تلاتة الحمد للّه. وانتي؟ عندي ولد وبنت، الله يخلّيهم!"), - ("במה אתה עובד? אני עורך דין! אני עורכת דין גם.", "شو بتشتغل؟ انا محامي! انا محاميّة كمان."), - ("שמי ח'אלד, מה שמך? שלום ח'אלד, שמי פאטמה, נעים להכיר! (לאישה)", "اسمي خالد، شو اسمك#f؟ مرحبا خالد، اسمي فاطمة، تشرّفنا!"), - ("דאוד פתח את דלת בית חברו ושאל את אמו האם חברו בבית.", "داود فتح باب بيت صاحبه وسأل امّه اذا صاحبه في البيت."), - ("מה שלומך, דאוד? אני שמח, ברוך השם! איך אתה?", "كيف حالك يا داود؟ انا مبسوط الحمد للّه! كيف انت؟"), - ("יש לי מכונית קטנה, אני וחברי ישבנו במכונית לפני יומיים.", "عندي سيّارة صغيرة، انا وصاحبي قعدنا في السّيّارة قبل يومين."), - ("מה אכלת היום, יקירי? היום ישבתי עם אחותי ואכלנו פלאפל.", "شو اكلت اليوم حبيبي؟ اليوم قعدت مع اختي واكلنا فلافل."), - ("דודי (מצד אמא) פתח את דלת הבית וישב בביתו עם אשתו.", "خالي فتح باب البيت وقعد في بيته مع مرته."), - ("אתה שאלת אותי מה אכלתי היום? אני שאלתי אותך האם אכלת היום?", "انت سألتني شو اكلت اليوم؟ انا سألتك اذا اكلت اليوم؟"), - ("בנותיי ישבו במסעדה ואני ישבתי במכונית.", "بناتي قعدو في المطعم وانا قعدت في السّيّارة."), - ("הוא שאל אותי בן כמה אני, אני גם שאלתי אותו בן כמה הוא.", "هوّ سألني قدّيش عمري، انا كمان سألته قدّيش عمره."), - ("שלום עליכם דודי (מצד אבא), ה' יאריך את ימיך!", "السّلام عليكم عمّي، الله يطوّل عمرك!"), - ("אני נשוי ויש לי שני בנים ושתי בנות, אחי נשוי ויש לו ילד גדול.", "انا متجوّز وعندي ولدين وبنتين، اخوي متجوّز وعنده ولد كبير."), - ("ה' ישמור אותם! אני גר בשכונה שלך עם אשתי ובתי. (לגבר)", "الله يخلّيهم! انا ساكن في حارتك مع مرتي وبنتي."), - ("אני במקור מירושלים אבל היום אני גר בצפון.", "انا اصلي من القدس بسّ اليوم انا ساكن في الشّمال."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 3): [ - ("new", "מילים חדשות", "learn", True, [ - ("קילו", "كيلو"), ("מספר (טלפון)", "رقم"), ("לכאן", "لهون"), ("אותו ה...", "نفس"), ("הבריכה", "البركة"), - ("חביתה", "عجّة"), ("תפוחים", "تفّاح"), ("בשר", "لحم"), ("שעה", "ساعة"), ("אתמול", "امبارح"), ("הר", "جبل"), - ("העיר", "المدينة"), ("שקל", "شيكل"), ("בחוץ", "برّا"), ("תפוחי אדמה", "بطاطا"), ("בתים", "بيوت"), - ("דלתות", "ابواب"), ("כיסאות", "كراسي"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_3)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_3)), - ("read", "משפטים: ערבית ← עברית", "read", True, [ - ("לפני יומיים סבא שלי נכנס לבית של חבר שלו ובישל איתו שקשוקה.", "قبل يومين جدّي دخل على بيت صاحبه وطبخ معه شكشوكة."), - ("הם שחו לפני שבועיים ואנחנו שחינו לפני יומיים.", "همّ سبحو قبل اسبوعين واحنا سبحنا قبل يومين."), - ("מאיפה עברת לשכם? עברתי לפה מבית לחם.", "من وين نقلت على نابلس؟ نقلت لهون من بيت لحم."), - ("למה את פתחת את חלון המכונית? סמיר פתח את החלון, סמיר פתח אותו.", "ليش انتي فتحتي شبّاك السّيّارة؟ سمير فتح الشّبّاك، سمير فتحه."), - ("אני נכנסתי לבית של דודה שלי (מצד אמא) והיא שאלה אותי מה שלומי היום.", "انا دخلت على بيت خالتي وهيّ سألتني#f كيف حالي اليوم."), - ("פאטמה בישלה אוכל טעים מאוד, אנחנו אכלנו אצלה היום.", "فاطمة طبخت#f اكل#n زاكي كتير، احنا اكلنا عندها اليوم."), - ("הבת שלי שחתה בים והבן שלי שחה גם, זאת אומרת הם שחו היום.", "بنتي سبحت#f في البحر وابني سبح كمان، يعني همّ سبحو اليوم."), - ("מי עבר לבית החדש? דוד שלי ודודה שלי (מצד אבא) עברו לפה.", "مين نقل على البيت الجديد؟ عمّي وعمّتي نقلو لهون."), - ("דוד שלי (מצד אמא) נכנס לבית של סבתא שלי והיא בישלה לו מקלובה טובה.", "خالي دخل على بيت جدّتي وهيّ طبختله#f مقلوبة طيّبة."), - ("למה אתם עברתם לשכונה? למה לא, יקירי?", "ليش انتو نقلتو على الحارة؟ ليش لا يا حبيبي؟"), - ("אח שלי נכנס לבית ואני שאלתי אותו מי בישל את האוכל.", "اخوي دخل على البيت وانا سألته مين طبخ الأكل."), - ("הרופאה שחתה עם בעלה בים, אחר כך הם אכלו שקשוקה.", "الدّكتورة سبحت#f مع جوزها في البحر بعدين همّ اكلو شكشوكة."), - ("אני שאלתי: מה בישלת היום, אמא? היום בישלתי מנסף.", "انا سألت: شو طبختي اليوم يمّا؟ اليوم طبخت منسف."), - ("יש לי שני כלבים, אחד ישב על הכיסא והשני ישב על השולחן.", "عندي كلبين، واحد قعد على الكرسي والتّاني قعد على الطّاولة."), - ("המורה פתחה את דלת בית הספר וישבה בבית הספר.", "المعلّمة فتحت#f باب المدرسة وقعدت#f في المدرسة."), - ("לחבר שלי יש שתי שאלות: מה שם אח שלי? ולמה הוא נכנס לשכונה היום?", "صاحبي عنده سؤالين: شو اسم اخوي؟ وليش هوّ دخل على الحارة اليوم؟"), - ("ההורים שלי עברו לבית חדש באותה השכונה לפני שנתיים.", "اهلي نقلو على بيت جديد في نفس الحارة قبل سنتين."), - ("הבת הבכורה שלך אכלה עם הבת שלי בבית שלנו לפני שעתיים.", "بنتك البكرة اكلت#f مع بنتي في بيتنا قبل ساعتين."), - ("השכן שלך שחה עם אשתו בים לפני יומיים.", "جارك سبح مع مرته في البحر قبل يومين."), - ("דודה שלי (מצד אבא) ישבה עם השכנה שלה בבית שלהם והבן שלה נכנס לבית.", "عمّتي قعدت#f مع جارتها في بيتهم وابنها دخل على البيت."), - ("שלום עליכם! שמי סאמי, היום אני עברתי לפה, אתה גר פה?", "السّلام عليكم! اسمي سامي، اليوم انا نقلت لهون، انت ساكن هون؟"), - ("אתם פתחתם את החלון היום? אנחנו פתחנו שני חלונות.", "انتو فتحتو الشّبّاك اليوم؟ احنا فتحنا شبّاكين."), - ("מי שחה איתך בבריכה? חבר שלי מחמוד שחה איתי.", "مين سبح معك في البركة؟ صاحبي محمود سبح معي."), - ("המורה שאל את הבת שלי: \"למה את שאלת אותי שתי שאלות היום?\"", "المعلّم سأل بنتي: ليش انتي سألتيني سؤالين اليوم؟"), - ("חברה שלי בת עשרים ושתיים, היא גרה בבית של אבא שלה.", "صاحبتي عمرها تنين وعشرين سنة، هيّ ساكنة في بيت ابوها."), - ("דוד שלי (מצד אמא) עבר לשכונה עם אשתו והבן שלו לפני שנתיים.", "خالي نقل على الحارة مع مرته وابنه قبل سنتين."), - ]), - ("say", "משפטים: עברית ← ערבית א'", "say", True, [ - ("לפני שבועיים בישלתי אוכל טעים ודודי (מצד אמא) בישל איתי.", "قبل اسبوعين طبخت اكل#n زاكي وخالي طبخ معي."), - ("שחינו בים ואכלנו שני אבטיחים, הם אכלו פלאפל.", "سبحنا في البحر واكلنا بطّيختين، همّ اكلو فلافل."), - ("אתה שאלת אותי מי בישל היום חביתה, אני בישלתי היום חביתה.", "انت سألتني مين طبخ اليوم عجّة، انا طبخت اليوم عجّة."), - ("החלון פתוח? כן, אני נכנסתי לבית ופתחתי אותו.", "الشّبّاك مفتوح؟ اه، انا دخلت على البيت وفتحته."), - ("היא גרה באותו הבית אבל עברה לחדר חדש.", "هيّ ساكنة في نفس البيت بسّ نقلت#f على غرفة جديدة."), - ("מחמוד שחה עם אחותו, הם שחו לפני יומיים בבריכה.", "محمود سبح مع اخته، همّ سبحو قبل يومين في البركة."), - ("הם נכנסו לשכונתנו ושאלו אותה שתי שאלות.", "همّ دخلو على حارتنا وسألوها سؤالين."), - ("בני עבר לירושלים לפני שבוע, מה שם השכונה? גם בני גר בירושלים.", "ابني نقل على القدس قبل اسبوع، شو اسم الحارة؟ كمان ابني ساكن في القدس."), - ("היא נכנסה למלון ושאלה איפה הטבח?", "هيّ دخلت#f على الاوتيل وسألت#f وين الطّبّاخ؟"), - ("מדוע אתם שאלתם את שכני מי עבר לבית החדש?", "ليش انتو سألتو جاري مين نقل على البيت الجديد؟"), - ("אמי שאלה את אחי: למה בישלת לאחותך?", "امّي سألت#f اخوي: ليش طبخت لاختك؟", "אמי שאלה את אחי: למה בִּשַּׁלְתָּ לַאֲחוֹתְךָ?"), - ("המורה שחתה בים, היא נכנסה לים עם חברה שלה.", "المعلّمة سبحت#f في البحر، هيّ دخلت#f على البحر مع صاحبتها."), - ]), - ("say2", "משפטים: עברית ← ערבית ב'", "say", True, [ - ("הרופא שאל את אבי מתי עברנו לשכונה?", "الدّكتور سأل ابوي ايمتى نقلنا على الحارة؟"), - ("סבתי נכנסה לבית ושאלה: \"מה אתם אכלתם היום?\"", "ستّي دخلت#f على البيت وسألت#f: شو انتو اكلتو اليوم؟"), - ("אבי נכנס למסעדה ושאל את המלצר איפה השולחן הגדול?", "ابوي دخل على المطعم وسأل الجرسون وين الطّاولة الكبيرة؟"), - ("אנחנו ישבנו עם דודי (מצד אבא), אחר כך סבי נכנס ושאל אותו: מה שלומך?", "احنا قعدنا مع عمّي، بعدين سيدي دخل وسأله: كيف حالك؟"), - ("מה את בישלת לנכדך? בישלתי לו מקלובה, הוא אכל עם חברו.", "شو انتي طبختي لحفيدك#f؟ طبختله مقلوبة، هوّ اكل مع صاحبه."), - ("אמי נכנסה לבית ושאלה את אחותי: \"מה את אכלת היום?\"", "امّي دخلت#f على البيت وسألت#f اختي: شو انتي اكلتي اليوم؟"), - ("אנחנו ישבנו אצל דודי (מצד אמא) ואכלנו איתו אוכל טעים.", "احنا قعدنا عند خالي واكلنا معه اكل#n زاكي."), - ("מחמוד נכנס לבית הספר ושאל את אחותו: \"מה שלומך היום?\"", "محمود دخل على المدرسة وسأل اخته: كيف حالك#f اليوم؟", "מחמוד נכנס לבית הספר ושאל את אחותו: \"מה שְׁלוֹמֵךְ היום?\""), - ("אבי פתח את דלת המכונית ונכנס למכונית לפני שעה.", "ابوي فتح باب السّيّارة ودخل على السّيّارة قبل ساعة."), - ("אתה שחית עם חברך בים היום? כן, שחיתי עם אברהים.", "انت سبحت مع صاحبك في البحر اليوم؟ اه، سبحت مع ابراهيم."), - ("דודך (מצד אבא) ישב עם דודתך (מצד אבא), זאת אומרת הוא ישב עם אשתו. (לגבר)", "عمّك قعد مع عمّتك، يعني هوّ قعد مع مرته.", "דּוֹדְךָ (מצד אבא) ישב עם דּוֹדָתְךָ (מצד אבא), זאת אומרת הוא ישב עם אשתו."), - ("אני עברתי לחברון ואחותי עברה לירושלים, היא גרה בשכונת ג'בל אלמוכבר.", "انا نقلت على الخليل واختي نقلت#f على القدس، هيّ ساكنة في حارة#of جبل المكبّر."), - ("סבתי שאלה אותי איפה אבי? אבי נכנס לחדר: \"אני פה!\"", "ستّي سألتني#f وين ابوي؟ ابوي دخل على الغرفة: انا هون!"), - ("אני גרה בבית גדול עם בעלי, בני עבר לשכם ובתי עברה לחיפה.", "انا ساكنة في بيت كبير مع جوزي، ابني نقل على نابلس وبنتي نقلت#f على حيفا."), - ]), - ("nums", "המספרים", "say", True, [(f"{he} ({digits})", ar, he) for digits, he, ar in NUMBERS]), - ("numread", "מספרים: ערבית ← עברית", "read", True, [ - ("יש לי ארבע מכוניות במוסך ועוד שתי מכוניות בבית.", "عندي اربع سيّارات في الكراج وكمان سيّارتين في البيت."), - ("לדוד שלי (מצד אמא) יש חמישה בנים ושלוש בנות, ה' ישמור אותם!", "خالي عنده خمس اولاد وتلات بنات، الله يخلّيهم!"), - ("חברה שלי פתחה ארבע דלתות ושישה חלונות אתמול בבית הספר.", "صاحبتي فتحت#f اربع ابواب وستّ شبابيك امبارح في المدرسة."), - ("סבתא שלי בישלה שלושה קילו מקלובה, כולנו אכלנו אצלה.", "جدّتي طبخت#f تلاتة كيلو مقلوبة، كلّنا اكلنا عندها."), - ("דודה שלי (מצד אמא) גרה פה בשכונה, בית מספר שלוש מאות שבעים וארבע.", "خالتي ساكنة هون في الحارة، بيت رقم تلات ميّة اربع وسبعين."), - ("אבא שלי שאל את אמא שלי איפה התפוחים, אמא שלי: במטבח יש אחד עשר קילו תפוחים.", "ابوي سأل امّي وين التّفّاح، امّي: في المطبخ في حدعشر كيلو تفّاح."), - ("את ישבת היום על חמישה כיסאות בבית הספר.", "انتي قعدتي اليوم على خمس كراسي في المدرسة."), - ("לדוד שלי (מצד אבא) יש שבעה ילדים, זאת אומרת בן גדול ועוד שש בנות.", "عمّي عنده سبع اولاد يعني ولد كبير وكمان ستّ بنات."), - ("אני שאלתי אותו: \"כמה הפלאפל?\" הוא: \"שבעה עשר שקלים, ועם קולה? עשרים וארבע\".", "انا سألته: قدّيش الفلافل؟ هوّ: سبعتعشر شيكل، ومع كولا؟ اربع وعشرين."), - ("אח שלי עבר לשלוש שכונות באותה העיר.", "اخوي نقل على تلات حارات في نفس المدينة."), - ("חברה שלי בת ארבעים וחמש, בעלה בן ארבעים ושש.", "صاحبتي عمرها خمسة واربعين سنة، جوزها عمره ستّة واربعين سنة."), - ]), - ("numsay", "מספרים: עברית ← ערבית", "say", True, [ - ("יש לי חמישה ילדים, זאת אומרת שלוש בנות ושני בנים, הם נכנסו לבית חברי סלים.", "عندي خمس اولاد، يعني تلات بنات وولدين، همّ دخلو على بيت صاحبي سليم."), - ("במוסך יש עשרים מכוניות ולאבי יש שלוש מכוניות.", "في الكراج في عشرين سيّارة وابوي عنده تلات سيّارات."), - ("חברי פתח שתי דלתות וארבעה חלונות, אתה פתחת גם חלונות?", "صاحبي فتح بابين واربع شبابيك، انت فتحت كمان شبابيك؟"), - ("לסבי יש שנים עשר נכדים, שישה נכדים ושש נכדות, הם ישבו בביתו.", "سيدي عنده تنعشر حفيد، ستّ احفاد وستّ حفيدات، همّ قعدو في بيته."), - ("אני גר בבית מספר מאה שמונים ושלוש, איפה את גרה? אני בבית מספר מאתיים חמישים וארבע.", "انا ساكن في بيت رقم ميّة تلاتة وتمانين، وين انتي ساكنة؟ انا في بيت رقم ميتين اربعة وخمسين."), - ("יש לנו אחד עשר דודים ושבע עשרה דודות (מצד אבא).", "عندنا حدعشر عمّ وسبعتعشر عمّة.", "יש לנו אחד עשר דּוֹדִים ושבע עשרה דּוֹדוֹת (מצד אבא)."), - ("בן כמה אתה? אני בן שלושים וחמש! ואת? אני בת עשרים ושמונה.", "قدّيش عمرك؟ عمري خمسة وتلاتين سنة! وانتي؟ عمري تمانية وعشرين."), - ("לאחי יש שני שולחנות בבית ועוד תשעה עשר שולחנות בחוץ.", "اخوي عنده طاولتين في البيت وكمان تسعتعشر طاولة برّا."), - ("הילד בן תשע, נכנס היום לארבעה בתים ופתח שני חלונות.", "الولد عمره تسعة، دخل اليوم على اربع بيوت وفتح شبّاكين."), - ("היא קנתה שלושה קילו תפוחים ושנים עשר קילו תפוחי אדמה.", "هيّ اشترت#f تلاتة كيلو تفّاح وتنعشر كيلو بطاطا."), - ("יש לי שתי אחיות וארבעה אחים, אחת נשואה לבן דודך (מצד אמא). (לאישה)", "عندي اختين واربع اخوة، واحدة متجوّزة لابن خالك#f.", "יש לי שתי אחיות וארבעה אחים, אחת נשואה לבן דּוֹדֵךְ (מצד אמא)."), - ]), - ("story", "סיפור: חברתי רובה", "say", True, [ - ("חברתי רובה עברה לבית חדש בשכונתנו.", "صاحبتي روبا نقلت#f على بيت جديد في حارتنا."), - ("היא גרה בבית מספר מאתיים שישים ושבע,", "هيّ ساكنة في بيت رقم ميتين سبعة وستّين،"), - ("יש לה שני אחים, זאת אומרת אחים שניים, וגם אחות גדולה נשואה לבן דודה (מצד אבא).", "عندها اخين، يعني اخوة التّنين، وكمان اخت كبيرة متجوّزة لابن عمّها.", "יש לה שני אחים, זאת אומרת אחים שניים, וגם אחות גדולה נשואה לבן דּוֹדָהּ (מצד אבא)."), - ("אחותה שמה ביאן והיא גרה עם בעלה בשכונה רחוקה.", "اختها اسمها بيان وهيّ ساكنة مع جوزها في حارة بعيدة."), - ("רובה בת שמונה עשרה, זאת אומרת בת שמונה עשרה שנה.", "روبا عمرها تمنتعش، يعني تمنتعشر سنة."), - ("יש לה מכונית חדשה וקטנה.", "عندها سيّارة جديدة وصغيرة."), - ("לפני יומיים רובה פתחה את דלת המכונית, נכנסה וישבה במכונית.", "قبل يومين روبا فتحت#f باب السّيّارة، دخلت#f وقعدت#f في السّيّارة."), - ("אמה שאלה אותה: \"בתי, מי נכנס למכוניתך היום?\"", "امّها سألتها#f: يا بنتي، مين دخل على سيّارتك#f اليوم؟", "אמה שאלה אותה: \"בתי, מי נכנס לִמְכוֹנִיתֵךְ היום?\""), - ("רובה: \"אמא, אני נכנסתי וישבתי במכונית היום\".", "روبا: ماما، انا دخلت وقعدت في السّيّارة اليوم."), - ("אמה: \"טוב בתי, בואי הביתה, אני בישלתי אוכל טעים מאוד!\"", "امّها: طيّب يا بنتي، يلّا تعالي على البيت، انا طبخت اكل#n زاكي كتير!"), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 4): [ - ("new", "מילים חדשות", "learn", True, [ - ("רחוק", "بعيد"), ("קרוב", "قريب"), ("לא (לפני שם ותואר)", "مش"), ("קפה", "قهوة"), ("המוכר", "البيّاع"), - ("סוכר", "سكّر"), ("אני רוצה", "بدّي"), ("עוד, שני", "تاني"), ("אם, אילו", "لو"), ("רעב", "جوعان"), - ("השוק", "السّوق"), ("הילדים", "الاولاد"), ("משהו", "اشي"), ("עוגה", "كعكة"), ("תה", "شاي"), - ("עייף", "تعبان"), ("בלי", "بدون"), ("כל", "كلّ"), ("שכן", "جار"), ("תודה", "شكرا"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_4)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_4)), - ("read", "משפטים: ערבית ← עברית א'", "read", True, [ - ("אח שלי שאל את חבר שלו מה הוא קנה היום, חבר שלו קנה שולחן וכיסא.", "اخوي سأل صاحبه شو هوّ اشترى اليوم، صاحبه اشترى طاولة وكرسي."), - ("אני באתי מרחוק וחברה שלי באה איתי.", "انا اجيت من بعيد وصاحبتي اجت معي."), - ("למה השכן שלך בא עם הבן שלו לבית שלי? אני שאלתי את דוד שלי (מצד אמא).", "ليش جارك اجا مع ابنه على بيتي؟ انا سألت خالي."), - ("השכן שלי בא ונתן לי שמונים ותשעה שקלים, אחר כך נכנס לבית שלו.", "جاري اجا واعطاني تسعة وتمانين شيكل بعدين دخل على بيته."), - ("אמא שלי ואבא שלי עברו לבית חדש בשכונה חדשה לפני שבועיים.", "امّي وابوي نقلو على بيت جديد في حارة جديدة قبل اسبوعين."), - ("את סיפרת לי שנכנסת לשוק וקנית עגבניות.", "انتي حكيتي لي انّه دخلتي على السّوق واشتريتي بندورة."), - ("הילדים ישבו עם דוד שלהם (מצד אבא) והוא נתן להם אוכל ואחר כך הם אכלו.", "الاولاد قعدو مع عمّهم وهوّ اعطاهم اكل#n وبعدين همّ اكلو."), - ("דודה שלי (מצד אמא) באה לבית שלנו ונתנה לבן שלי כלב קטן.", "خالتي اجت على بيتنا واعطت ابني كلب صغير."), - ("אני סיפרתי לאשתי שחבר שלי בא אליי ונתן לי ארבעים ושבעה שקלים.", "انا حكيت لمرتي انّه صاحبي اجا لعندي واعطاني سبعة واربعين شيكل."), - ("לפני שעה קניתי חולצה חדשה עם חברה שלי.", "قبل ساعة اشتريت بلوزة جديدة مع صاحبتي."), - ("אני קניתי ממנו חולצה, נתתי לו תשעים ותשעה שקלים.", "انا اشتريت منّه بلوزة، اعطيته تسعة وتسعين شيكل."), - ("אתה באת לבית של דוד שלי (מצד אמא) והוא נתן לך ספל קפה.", "انت اجيت على بيت خالي وهوّ اعطاك فنجان قهوة."), - ]), - ("read2", "משפטים: ערבית ← עברית ב'", "read", True, [ - ("אבא שלי בא הביתה וסיפר לאמא שלי שאני שחיתי בבריכה.", "ابوي اجا على البيت وحكى لامّي انّه انا سبحت في البركة."), - ("אנחנו עברנו לשכונה לפני יומיים, באנו לפה מרחוק.", "احنا نقلنا على الحارة قبل يومين، اجينا لهون من بعيد."), - ("מי ישב עם השכן שלך בבית שלך? דוד שלי (מצד אמא) ישב אצלו ודיבר איתו.", "مين قعد مع جارك في بيتك؟ خالي قعد عنده وحكى معه."), - ("למה את נתת לי שבעים ושלושה שקלים? אני שאלתי את דודה שלי (מצד אבא).", "ليش انتي اعطيتيني تلاتة وسبعين شيكل؟ انا سألت عمّتي."), - ("הנגר סיפר לאבא שלי שהוא קנה בית חדש בשכונה.", "النّجّار حكى لابوي انّه هوّ اشترى بيت جديد في الحارة."), - ("אתה נתת לחבר שלך את העגבניות? כן! אני נתתי לו.", "انت اعطيت صاحبك البندورة؟ ايوه! انا اعطيته."), - ("דוד שלי (מצד אמא) קנה לילדים שלו בריכה חדשה, הם שחו בבריכה.", "خالي اشترى لاولاده بركة جديدة، همّ سبحو في البركة."), - ("יש לי שני בנים וארבע בנות, ה' ישמור אותם, כולם באו אליי לפני שבוע.", "عندي ولدين واربع بنات الله يخلّيهم، كلّهم اجو لعندي قبل اسبوع."), - ("אני קניתי את החולצה עם אחותי, איך החולצה? יפה או לא?", "انا اشتريت البلوزة مع اختي، كيف البلوزة؟ حلوة ولّا لا؟"), - ("אני סיפרתי לך שבישלתי אוכל טעים מאוד, קדימה, בוא לבית שלי!", "انا حكيتلك انّه طبخت اكل#n زاكي كتير، يلّا تعال على بيتي!"), - ("סבא שלי בא אליי ושאל אותי איפה אחמד, אחמד הוא אח שלי הגדול.", "جدّي اجاني وسألني وين احمد، احمد هوّ اخوي الكبير."), - ("אנחנו ישבנו בבית הספר עם המורה, אבא שלי בא ונתן לנו אוכל טעים.", "احنا قعدنا في المدرسة مع المعلّم، ابوي اجا واعطانا اكل#n طيّب."), - ("הילדים שחו בים ואחר כך הם קנו פלאפל בשוק.", "الاولاد سبحو في البحر وبعدين همّ اشترو فلافل في السّوق."), - ("אתם פתחתם את החלון ודיברתם עם המורה, היא סיפרה לי הכול.", "انتو فتحتو الشّبّاك وحكيتو مع المعلّمة، هيّ حكتلي كلّ اشي."), - ]), - ("say", "משפטים: עברית ← ערבית א'", "say", True, [ - ("חברתי באה אליי לפני שבוע ושאלה אותי האם אני רעבה.", "صاحبتي اجت لعندي قبل اسبوع وسألتني#f اذا انا جوعانة."), - ("אתמול קניתי כיסא חדש וחברי קנה שולחן חדש.", "امبارح اشتريت كرسي جديد وصاحبي اشترى طاولة جديدة."), - ("שכנתי שאלה את בנה מדוע הוא בא לביתי ואכל איתי.", "جارتي سألت#f ابنها ليش هوّ اجا على بيتي واكل معي."), - ("לפני שבוע המוכר בא אליי ונתן לי חמישה עשר שקלים.", "قبل اسبوع البيّاع اجا لعندي واعطاني خمستعشر شيكل."), - ("אני שאלתי את אחותי: \"מדוע את נתת לבני עוגה?\"", "انا سألت اختي: ليش انتي اعطيتي ابني كعكة؟"), - ("אני סיפרתי לחברי שקניתי היום קפה, חברי שאל: \"איפה קנית אותו?\"", "انا حكيت لصاحبي انّه اشتريت اليوم قهوة، صاحبي سأل: وين اشتريتها؟", "אני סיפרתי לחברי שקניתי היום קפה, חברי שאל: \"איפה קָנִיתָ אותו?\""), - ("אני שחיתי היום בים ואחר כך באתי עם חברתי לפה.", "انا سبحت اليوم في البحر وبعدين اجيت مع صاحبتي لهون."), - ("דודי (מצד אבא) סיפר לי שחברו חמדאן עבר לשכונה שלנו.", "عمّي حكالي انّه صاحبه حمدان نقل على حارتنا."), - ("אני שאלתי את המוכר שתי שאלות, המוכר שאל אותי: \"מתי קנית פה?\"", "انا سألت البيّاع سؤالين، البيّاع سألني: ايمتى اشتريت هون؟", "אני שאלתי את המוכר שתי שאלות, המוכר שאל אותי: \"מתי קָנִיתָ פה?\""), - ("לפני שבועיים, כאשר ישבתי בביתי, חברי בא אליי.", "قبل اسبوعين، لمّا قعدت في بيتي، صاحبي اجا لعندي."), - ("היא נכנסה לבית של אמא שלה ושאלה אותה האם היא קנתה את האוכל.", "هيّ دخلت#f على بيت امّها وسألتها#f اذا هيّ اشترت#f الأكل."), - ("דודתי (מצד אבא) באה אליי ושאלה אותי בן כמה אני, אני בן שתים עשרה.", "عمّتي اجت لعندي وسألتني#f قدّيش عمري، عمري تنعشر سنة."), - ]), - ("say2", "משפטים: עברית ← ערבית ב'", "say", True, [ - ("לפני יומיים חברי קנה שני שולחנות וחמישה כיסאות.", "قبل يومين صاحبي اشترى طاولتين وخمس كراسي."), - ("אתה באת אליי ושאלת אותי שתי שאלות, אני שאלתי את המורה עשרים שאלות.", "انت اجيت لعندي وسألتني سؤالين، انا سألت المعلّمة عشرين سؤال.", "אתה בָּאתָ אליי וְשָׁאַלְתָּ אותי שתי שאלות, אני שאלתי את המורה עשרים שאלות."), - ("חברי סיפר לי שלפני שנה הוא קנה שתי מכוניות.", "صاحبي حكالي انّه قبل سنة هوّ اشترى سيّارتين."), - ("לדודי (מצד אמא) יש ילד בן שבע עשרה ובת בת שש עשרה.", "خالي عنده ولد عمره سبعتعش وبنت عمرها ستّعش.", "לְדוֹדִי (מצד אמא) יש ילד בן שבע עשרה ובת בת שש עשרה."), - ("אני קניתי כוס וחברתי קנתה אחת עשרה כוסות.", "انا اشتريت كاسة وصاحبتي اشترت#f حدعشر كاسة."), - ("שאלתי את המוכר כמה האבטיח? רק ארבעה עשר שקלים!", "سألت البيّاع قدّيش البطّيخة؟ بسّ اربعتعشر شيكل!"), - ("חברתי שאלה אותי האם דודי (מצד אמא) נשוי או רווק.", "صاحبتي سألتني#f اذا خالي متجوّز ولّا عزّابي."), - ("לפני ארבע שנים עברתי לפה, מתי את עברת לפה?", "قبل اربع سنين نقلت لهون، ايمتى انتي نقلتي لهون؟"), - ("אני סיפרתי לחברתי שאני עורכת דין ואני בת שלושים ושתיים.", "انا حكيت لصاحبتي انّه انا محاميّة وعمري تنين وتلاتين."), - ("המורה שאלה את הילד מדוע הוא בא לבית הספר היום.", "المعلّمة سألت#f الولد ليش هوّ اجا على المدرسة اليوم."), - ("אמי סיפרה לרופא שאני ילדה חרוצה, הוא נתן לי מכונית קטנה.", "امّي حكت للدّكتور انّه انا بنت شاطرة، هوّ اعطاني سيّارة صغيرة."), - ("חברי נכנס לבית אחותו וסיפר לה שהוא קנה את העוגה.", "صاحبي دخل على بيت اخته وحكالها انّه هوّ اشترى الكعكة."), - ]), - ("story", "סיפור: שכני החדש", "say", True, [ - ("לפני שבוע בא לשכונה שלנו שכן חדש.", "قبل اسبوع اجا على حارتنا جار جديد."), - ("אני אמרתי לאשתי: \"אני רוצה ללכת לשכננו החדש ואביא לו עוגה\".", "انا قلت لمرتي: بدّي اروح لعند جارنا الجديد واجيبله كعكة."), - ("אשתי אמרה: \"קדימה, למה לא! לך אליו וראה האם הוא נשוי או רווק, כמה ילדים יש לו ומה שלומו\".", "مرتي قالت: يلّا، ليش لا! روح لعنده وشوف اذا هوّ متجوّز ولّا عزّابي، اكم ولد عنده وكيف حاله."), - ("טוב, למחרת הלכתי אליו.", "طيّب، تاني يوم انا رحت لعنده."), - ("אני: \"שלום וברוכים הבאים, יקירי!\"", "انا: مرحبا واهلا وسهلا فيك، حبيبي!"), - ("הוא אמר: \"שלום שלום, מה שלומך?\"", "هوّ قال: اهلين! كيف حالك؟"), - ("אני: \"ברוך השם, טוב!\"", "انا: الحمد للّه تمام!"), - ("אני: \"שמי ראע'ב עבדאללה, ומה שמך?\"", "اسمي راغب عبدالله، وشو اسمك؟"), - ("הוא אמר: \"אני, שמי סמיר עטאללה, נעים להכיר!\"", "هوّ قال: انا اسمي سمير عطالله، تشرّفنا!"), - ("אני: \"יוסיף לך כבוד! מאיפה עברת לכאן? מרחוק?\"", "انا: يزيدك شرف! من وين نقلت لهون؟ من بعيد؟", "אני: \"יוסיף לך כבוד! מאיפה עָבַרְתָּ לכאן? מרחוק?\""), - ("הוא אמר: \"לא, לא מרחוק, מרמאללה, יקירי\".", "هوّ قال: لا، مش من بعيد، من رام الله حبيبي."), - ("אני: \"אתה נשוי או רווק?\"", "انا: انت متجوّز ولّا عزّابي؟"), - ("הוא אמר: \"אני לא נשוי, אני מאורס, ברוך השם! ארוסתי שמה פאטמה והיא ממשפחת אלקואסמה\".", "هوّ قال: انا مش متجوّز، انا خاطب الحمد للّه! خطيبتي اسمها فاطمة وهيّ من دار القواسمة."), - ("אני: \"מזל טוב, יקירי!\"", "انا: مبروك حبيبي!"), - ("הוא אמר: \"ה' יברך אותך! ואתה נשוי או רווק?\"", "هوّ قال: الله يبارك فيك، وانت متجوّز ولّا عزّابي؟"), - ("אני: \"אני נשוי ויש לי שני בנים ושתי בנות, ברוך השם!\"", "انا: انا متجوّز وعندي ولدين وبنتين الحمد للّه!"), - ("הוא אמר: \"ה' ישמור אותם!\"", "هوّ قال: الله يخلّيهم!"), - ("אני: \"ה' ישמור אותך!\"", "انا: الله يحفظك!"), - ("ואחר כך הוא שאל: \"סלח לי, איפה יש כאן מסעדה? אני רעב מאוד\".", "وبعدين هوّ سأل: لو سمحت، وين في هون مطعم؟ انا جوعان كتير."), - ("אני: \"אתה רעב? בוא אליי לביתי! אשתי בישלה מקלובה\".", "انا: انت جوعان؟ تعال لعندي على بيتي! مرتي طبخت#f مقلوبة."), - ]), - ("meet", "שכני החדש: עברית ← ערבית", "say", True, [ - ("אתמול חברי בא אליי לביתי ונתן לי חולצה חדשה.", "امبارح صاحبي اجا لعندي على بيتي واعطاني بلوزة جديدة."), - ("סלח לי, אתה באת עם סמיר אליי? כן! אני באתי איתו.", "لو سمحت، انت اجيت مع سمير لعندي؟ اه! انا اجيت معه.", "סלח לי, אתה בָּאתָ עם סמיר אליי? כן! אני באתי איתו."), - ("לפני שבועיים חבר שלי אכל פיצה עם דודי (מצד אמא), למחרת הוא אכל איתי.", "قبل اسبوعين صاحبي اكل بيتسا مع خالي، تاني يوم هوّ اكل معي."), - ("אני אכלתי פלאפל עם דודתך (מצד אמא), היא באה מרחוק. (לגבר)", "انا اكلت فلافل مع خالتك، هيّ اجت من بعيد.", "אני אכלתי פלאפל עם דּוֹדָתְךָ (מצד אמא), היא באה מרחוק."), - ("הוא נשוי וגר עם אשתו וילדיו בבית הגדול בשכונתנו.", "هوّ متجوّز وساكن مع مرته واولاده في البيت الكبير في حارتنا."), - ("היא עברה לבית חדש? לא, היא גרה באותו הבית.", "هيّ نقلت#f على بيت جديد؟ لا، هيّ ساكنة في نفس البيت."), - ("הוא שאל את שכנו מאיפה הוא במקור? שכנו: אני במקור מפה.", "هوّ سأل جاره من وين اصله؟ جاره: انا اصلي من هون."), - ("כמה ילדים יש לך? ה' ישמור אותם! לי יש שני בנים וארבע בנות. (לגבר)", "اكم ولد عندك؟ الله يخلّيهم! انا عندي ولدين واربع بنات.", "כמה ילדים יש לְךָ? ה' ישמור אותם! לי יש שני בנים וארבע בנות."), - ("יש לי חמישה ילדים, זאת אומרת שלוש בנות ושני בנים, אנחנו גרים לא רחוק משכם.", "عندي خمس اولاد، يعني تلات بنات وولدين، احنا ساكنين مش بعيد عن نابلس."), - ("ביתך רחוק מהמסעדה או קרוב אליה? ביתי רחוק מהמסעדה. (לגבר)", "بيتك بعيد عن المطعم ولّا قريب منّه؟ بيتي بعيد عن المطعم.", "בֵּיתְךָ רחוק מהמסעדה או קרוב אליה? ביתי רחוק מהמסעדה."), - ("ביתי לא רחוק מהעיר אבל בית חברי רחוק מהעיר.", "بيتي مش بعيد عن البلد بسّ بيت صاحبي بعيد عن البلد."), - ("שכני נכנס לביתי ואחר כך שאל איפה בנו? אמרתי לו: בנך לא כאן.", "جاري دخل على بيتي وبعدين سأل وين ابنه؟ قلتله: ابنك مش هون.", "שכני נכנס לביתי ואחר כך שאל איפה בנו? אמרתי לו: בִּנְךָ לא כאן."), - ("מי פתח את הדלת? אבא שלי פתח אותה והביא לנו משהו.", "مين فتح الباب؟ ابوي فتحه وجابلنا اشي."), - ("אתה רוצה קפה או תה? עם סוכר?", "بدّك قهوة ولّا شاي؟ مع سكّر؟"), - ("אני רוצה קפה עם סוכר, תודה!", "بدّي قهوة مع سكّر، شكرا!"), - ("אני רוצה תה, ללא סוכר, ה' ישמור אותך! (לגבר)", "بدّي شاي، بدون سكّر، الله يحفظك!", "אני רוצה תה, ללא סוכר, ה' ישמור אוֹתְךָ!"), - ("לפני שבועיים דודי (מצד אמא) עבר לכאן, למחרת הלכתי אליו.", "قبل اسبوعين خالي نقل لهون، تاني يوم رحت لعنده."), - ("סלחי לי, איפה גר אבו יוסף? הוא גר רחוק מאוד, לא בשכונה.", "لو سمحتي، وين ساكن ابو يوسف؟ هوّ ساكن بعيد كتير، مش في الحارة."), - ]), - ("meetread", "שכני החדש: ערבית ← עברית", "read", True, [ - ("מי דיבר עם דוד שלי (מצד אמא) היום? אני דיברתי איתו היום.", "مين حكى مع خالي اليوم؟ انا حكيت معه اليوم."), - ("אני אמרתי לאשתי: אני שחיתי בבריכת השחייה.", "انا قلت لمرتي: انا سبحت في بركة#of السّباحة."), - ("אשתי אמרה: \"למה לא, לך אליו וראה אם הוא נשוי או רווק\".", "مرتي قالت: ليش لا، روح لعنده وشوف اذا هوّ متجوّز ولّا عزّابي."), - ("חבר שלך שאל אותי מי אני, אני אמרתי: שמי סמיר ממשפחת אלקואסמה.", "صاحبك سألني مين انا، انا قلت: اسمي سمير من دار القواسمة."), - ("אני לא עייף, אני קצת רעב, יש לך אוכל?", "انا مش تعبان، انا جوعان شويّ، عندك اكل#n؟"), - ("אשתך בישלה מקלובה, אנחנו ישבנו ואחר כך אכלנו.", "مرتك طبخت#f مقلوبة، احنا قعدنا وبعدين اكلنا."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 5): [ - ("new", "מילים חדשות", "learn", True, [ - ("לא (לפני פועל)", "ما"), ("קצת, חלק מ", "بعض"), ("בערך", "حوالي"), ("מחיר", "حقّ"), ("בבקשה", "من فضلك"), - ("חצי", "نصّ"), ("מחר", "بكرا"), ("באמת, בחיי", "والله"), ("בית החולים", "المستشفى"), ("פעם", "مرّة"), - ("אחרי, עוד", "بعد"), ("חנות", "محلّ"), ("זמן", "وقت"), ("ירקות", "خضرة"), ("פירות", "فواكه"), - ("ממתקים", "حلويات"), ("כי, בשביל", "عشان"), ("המפתח", "المفتاح"), ("חודש", "شهر"), ("מתנה", "هديّة"), - ("שם (מקום)", "هناك"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("meet", "שכני החדש: עברית ← ערבית", "say", True, [ - ("סלח לי, מה סיפרת לאחי ומדוע נכנסת לבית הוריי?", "لو سمحت، شو حكيت لاخوي وليش دخلت على بيت اهلي؟", "סלח לי, מה סִפַּרְתָּ לאחי ומדוע נִכְנַסְתָּ לבית הוריי?"), - ("כאשר באתם לבית דודי (מצד אבא), אני ישבתי ודיברתי איתו.", "لمّا اجيتو على بيت عمّي، انا قعدت وحكيت معه."), - ("את רעבה? בואי אליי לביתי! אני בישלתי מקלובה.", "انتي جوعانة؟ تعالي لعندي على بيتي! انا طبخت مقلوبة."), - ("קנינו בית גדול בחברון, לא רחוק מבית לחם, אשתי שמחה.", "اشترينا بيت كبير في الخليل، مش بعيد عن بيت لحم، مرتي مبسوطة."), - ("מזל טוב, יקירי! תודה, ה' ישמור אותך!", "مبروك حبيبي! شكرا، الله يحفظك!", "מזל טוב, יקירי! תודה, ה' ישמור אוֹתְךָ!"), - ("סלח לי, מאיפה עברת לכאן? מרחוק? לא, לא מרחוק, מחיפה, יקירי.", "لو سمحت، من وين نقلت لهون؟ من بعيد؟ لا، مش من بعيد، من حيفا حبيبي.", "סלח לי, מאיפה עָבַרְתָּ לכאן? מרחוק? לא, לא מרחוק, מחיפה, יקירי."), - ("מי קנה לך את המכונית? אבי קנה לי, מזל טוב! ה' יברך אותך! (לגבר)", "مين اشترالك السّيّارة؟ ابوي اشترالي، مبروك! الله يبارك فيك!", "מי קנה לְךָ את המכונית? אבי קנה לי, מזל טוב! ה' יברך אוֹתְךָ!"), - ("לפני שבוע אני שחיתי בים עם חברתי, למחרת שחיתי עם אחותי.", "قبل اسبوع انا سبحت في البحر مع صاحبتي، تاني يوم سبحت مع اختي."), - ("דודי (מצד אבא) בא לביתי, אחר כך ישבתי איתו ואכלנו ביחד.", "عمّي اجا على بيتي، بعدين قعدت معه واكلنا مع بعض."), - ("חברי עבר לבית חדש בשכונה, אני אמרתי לו: \"ברוכים הבאים!\"", "صاحبي نقل على بيت جديد في الحارة، انا قلتله: اهلا وسهلا!"), - ("חברי ואני שחינו בים לפני שבועיים, למחרת הלכתי אליו.", "صاحبي وانا سبحنا في البحر قبل اسبوعين، تاني يوم رحت لعنده."), - ("שכנתי נכנסה לביתי ושאלה אותי האם אני רוצה להביא לה עוגה.", "جارتي دخلت#f على بيتي وسألتني#f اذا بدّي اجيبلها كعكة."), - ("מאיפה עברת לכאן, מרחוק? כן! מרחוק, מהצפון, יקירתי. (לאישה)", "من وين نقلتي لهون، من بعيد؟ اه! من بعيد، من الشّمال حبيبتي.", "מאיפה עָבַרְתְּ לכאן, מרחוק? כן! מרחוק, מהצפון, יקירתי."), - ]), - ("meetread", "שכני החדש: ערבית ← עברית", "read", True, [ - ("אנחנו הלכנו לאחותי, היא ישבה עם דודה שלי (מצד אמא), אנחנו ישבנו ביחד.", "احنا رحنا لعند اختي، هيّ قعدت#f مع خالتي، احنا قعدنا مع بعض."), - ("אני באתי לבית של חברה שלי יאסמין, היא ישבה במטבח.", "انا اجيت على بيت صاحبتي ياسمين، هيّ قعدت#f في المطبخ."), - ("אני רוצה ללכת לחבר שלי עאטף, הוא גר לא רחוק מרמאללה.", "بدّي اروح لعند صاحبي عاطف، هوّ ساكن مش بعيد عن رام الله."), - ("השכנה שלי נתנה לי את המכונית שלה ואמרה: \"ה' איתך, יקירתי!\"", "جارتي اعطتني سيّارتها وقالت: الله معك#f حبيبتي!"), - ("מתי שחיתם בים? אנחנו שחינו לפני יומיים.", "ايمتى سبحتو في البحر؟ احنا سبحنا قبل يومين."), - ("את באת לבית של אחותי? כן! באתי עם חברה שלי לבית שלה.", "انتي اجيتي على بيت اختي؟ اه! اجيت مع صاحبتي على بيتها."), - ("הבן של דודה שלי (מצד אמא) בא אליי והלכנו לאחותי, היא עברה לירושלים.", "ابن خالتي اجا لعندي ورحنا لعند اختي، هيّ نقلت#f على القدس."), - ("יש לי שני כלבים, אחד גדול והשני קטן, הם ישבו בבית.", "عندي كلبين واحد كبير والتّاني صغير، همّ قعدو في البيت."), - ("אבא שלי בישל פסטה, אמא שלי באה וישבה איתנו, אחר כך שאלה אותי מה שלומי היום.", "ابوي طبخ معكرونة، امّي اجت وقعدت#f معنا، بعدين سألتني#f كيف حالي اليوم."), - ("אח שלי קנה שתי חולצות, נתן לי אחת וגם נתן לאחותי אחת.", "اخوي اشترى بلوزتين، اعطاني واحدة وكمان اعطى اختي واحدة."), - ("דוד שלי (מצד אבא) אמר לבן שלו: \"יקירי, למה קנית את הספל?\"", "عمّي قال لابنه: يا حبيبي، ليش اشتريت الفنجان؟"), - ("אנחנו עברנו לשכונה חדשה באותה העיר.", "احنا نقلنا على حارة جديدة في نفس البلد."), - ]), - ("neg", "השלילה: ערבית ← עברית א'", "read", True, [ - ("הוא אמר לחבר שלו שהוא לא רעב.", "هوّ قال لصاحبه انّه هوّ مش جوعان."), - ("היא לא אמרה לחבר שלה שהיא עייפה.", "هيّ ما قالت لصاحبها انّه هيّ تعبانة."), - ("אני לא שאלתי אותך שאלה, אני רק שאלתי את המורה שאלה.", "انا ما سألتكش سؤال، انا بسّ سألت المعلّم سؤال."), - ("אתם לא נכנסתם לבית שלי, אתם נכנסתם לבית של השכנה שלי.", "انتو ما دخلتوش على بيتي، انتو دخلتو على بيت جارتي."), - ("אני שאלתי אותו אם הילד גדול, הוא אמר: \"לא, הילד לא גדול\".", "انا سألته اذا الولد كبير، هوّ قال: لا، الولد مش كبير."), - ("כאשר היא הלכה לבית שלה, היא לא שאלה אותי שאלה.", "لمّا هيّ راحت على بيتها، هيّ ما سألتنيش سؤال."), - ("אתם לא אמרתם לי שום דבר לפני שבוע, למחרת אתם שאלתם אותי למה אני באתי לפה?", "انتو ما قلتولي اشي قبل اسبوع، تاني يوم انتو سألتوني ليش انا اجيت لهون؟"), - ("אני לא עייף, אני רק קצת רעב, לא אכלתי מאתמול בלילה.", "انا مش تعبان، انا بسّ شويّة#of جوعان، ما اكلت من امبارح ليلة."), - ("אני לא ישבתי אצל השכן שלי, אני ישבתי אצל אח שלי.", "انا ما قعدتش عند جاري، انا قعدت عند اخوي."), - ("אנחנו לא שחינו בים, אנחנו שחינו בבריכה.", "احنا ما سبحنا في البحر، احنا سبحنا في البركة."), - ("אני רוצה ללכת היום, אני לא רוצה ללכת מחר. בסדר! לך מחר! למה לא.", "بدّي اروح اليوم، ما بدّي اروح بكرا. تمام! روح بكرا! ليش لا."), - ("הדלת לא פתוחה, את לא פתחת את הדלת? כן! אני פתחתי אותה לפני שעתיים.", "الباب مش مفتوح، انتي ما فتحتي الباب؟ اه! انا فتحته قبل ساعتين."), - ("אין לי כלב, לך יש כלב בבית שלך? כן, יש לי שני כלבים.", "انا ما عندي كلب، انت عندك كلب في بيتك؟ اه، عندي كلبين."), - ]), - ("neg2", "השלילה: ערבית ← עברית ב'", "read", True, [ - ("את לא קנית כנאפה, את קנית בקלאווה.", "انتي ما اشتريتي كنافة، انتي اشتريتي بقلاوة."), - ("הם לא עברו לשכם, הם עברו לשכונה לא גדולה בחיפה.", "همّ ما نقلو على نابلس، همّ نقلو على حارة مش كبيرة في حيفا."), - ("המורה שחה היום בבריכה, לא אני ולא חבר שלי.", "المعلّم سبح اليوم في البركة، مش انا ومش صاحبي."), - ("סלחי לי, שכנה שלי, אני לא סיפרתי לך שום דבר, אני רק דיברתי עם הבת שלך.", "لو سمحتي يا جارتي، انا ما حكيتلك#f اشي، انا بسّ حكيت مع بنتك#f."), - ("אני לא רוצה ללכת לדוד שלי (מצד אמא), אני רוצה ללכת הביתה.", "انا ما بدّي اروح لعند خالي، بدّي اروح على البيت."), - ("אנחנו אכלנו מקלובה, לא מנסף.", "احنا اكلنا مقلوبة مش منسف."), - ("השכן שלי אמר לי שהילדים באו לבית שלו והוא לא נתן להם את המפתח.", "جاري قالّي انّه الاولاد اجو على بيته وهوّ ما اعطاهم المفتاح."), - ("אבא שלי שאל אותנו מה קנינו היום, אמרתי לו: \"באמת לא קנינו שום דבר!\"", "ابوي سألنا#us شو اشترينا اليوم، قلتله: والله ما اشترينا اشي!"), - ("אתה לא חבר שלי ואני לא חבר שלך, אני האבא ואתה הילד.", "انت مش صاحبي وانا مش صاحبك، انا الاب وانت الولد."), - ("אני ישבתי עם אמא שלי ואחותה, אבל לא במסעדה, ישבנו אצל דודה שלי (מצד אמא).", "انا قعدت مع امّي واختها، بسّ مش في المطعم، قعدنا عند خالتي."), - ("מי פתח את הדלת? לא אתה? לא, לא אני! טוב, מי פתח אותה?", "مين فتح الباب؟ مش انت؟ لا، مش انا! طيّب مين فتحه؟"), - ("אני לא עברתי מירושלים, אני עברתי מחברון, לא עם אבא שלי, עם אח שלי.", "انا ما نقلتش من القدس، انا نقلت من الخليل، مش مع ابوي، مع اخوي."), - ("אני לא רוצה ללכת מחר לים, אני רוצה ללכת לבריכה.", "ما بدّي اروح بكرا على البحر، بدّي اروح على البركة."), - ]), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_5)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_5)), - ("read", "משפטים: ערבית ← עברית", "read", True, [ - ("הוא אמר לי משהו, אני אמרתי לדוד שלי (מצד אמא) משהו.", "هوّ قالّي اشي، انا قلت لخالي اشي."), - ("אני אמרתי לו: \"מתי ביקרת את אמא שלך?\"", "انا قلتله: ايمتى زرت امّك؟"), - ("דוד שלי (מצד אבא) אמר לי: \"אנחנו הלכנו למוסך לפני יומיים. לאן את הלכת לפני יומיים?\"", "عمّي قالّي: احنا رحنا على الكراج قبل يومين. لوين انتي رحتي قبل يومين؟"), - ("אתם הלכתם מהבית לבית החולים? כן, הלכנו לבית החולים!", "انتو رحتو من البيت على المستشفى؟ ايوه، رحنا على المستشفى!"), - ("היא ביקרה אותי אתמול, ואני לא הייתי בבית, אני הלכתי לבית הספר.", "هيّ زارتني امبارح، وانا ما كنت في البيت، انا رحت على المدرسة."), - ("אנחנו ביקרנו אותה לפני שנה, והיא ביקרה אותנו שלוש פעמים.", "احنا زرناها قبل سنة، وهيّ زارتنا تلات مرّات."), - ("איפה הייתם שלשום? אנחנו היינו בבית הספר, ואת איפה היית?", "وين كنتو اوّل امبارح؟ احنا كنّا في المدرسة، وانتي وين كنتي؟"), - ("היינו אצל חבר שלי ח'ליל, הוא השכן שלי בשכונה שלנו.", "كنّا عند صاحبي خليل، هوّ جاري في حارتنا."), - ("אני הייתי עם המשפחה בפריז, והיא איפה הייתה?", "انا كنت مع العيلة في باريس، وهيّ وين كانت؟"), - ("לכמה זמן נסעתם לפריז? נסענו לפריז לשבוע.", "لقدّيش وقت رحتو على باريس؟ رحنا على باريس لاسبوع."), - ("את ישבת במכונית ואחר כך היית בגן.", "انتي قعدتي في السّيّارة وبعدين كنتي في البستان."), - ("היא אמרה לי: \"למה לא ביקרת את סבתא שלך כאשר היית בשכונה?\"", "هيّ قالتلي: ليش ما زرت ستّك لمّا كنت في الحارة؟"), - ("אנחנו הלכנו עם דוד שלי (מצד אבא) לסופרמרקט וקנינו מלפפונים ועגבניות.", "احنا رحنا مع عمّي على السّوبرماركت واشترينا خيار وبندورة."), - ("דוד שלי (מצד אבא) אמר לנו: \"למה ישבתם פה בשכונה? קדימה, לכו לבית שלי!\"", "عمّي قالنا: ليش قعدتو هون في الحارة؟ يلّا روحو على بيتي!"), - ("נכנסנו לבית של דוד שלי (מצד אבא), ואשתו נכנסה איתנו והביאה לנו קפה.", "دخلنا على بيت عمّي، ومرته دخلت#f معنا وجابتلنا قهوة."), - ("לפני שבועיים אנחנו ביקרנו אותה, אבל היא לא ביקרה אותנו כאשר הייתה איתך.", "قبل اسبوعين احنا زرناها، بسّ هيّ ما زارتنا لمّا كانت معك."), - ("אני הייתי במכונית והלכתי לבית של דוד שלי (מצד אמא), הוא גר לא רחוק.", "انا كنت في السّيّارة ورحت على بيت خالي، هوّ ساكن مش بعيد."), - ("באמת, כאשר הלכתי לבית שלי לא היה לי זמן.", "والله لمّا رحت على بيتي ما كان معي وقت."), - ("אנחנו היינו בבית החולים עם סבתא שלי, היא שאלה אותנו מה שלומנו.", "احنا كنّا في المستشفى مع جدّتي، هيّ سألتنا كيف حالنا."), - ("כאשר היא ביקרה אותי בחודש רמדאן אני הייתי במכה.", "لمّا هيّ زارتني في شهر رمضان انا كنت في مكّة."), - ("איפה הייתם? אנחנו היינו בבית שלך לפני שעה, אבל את לאן הלכת?", "وين كنتو؟ احنا كنّا في بيتك#f قبل ساعة، بسّ انتي لوين رحتي؟"), - ("אח שלי שאל אותי: \"איפה אבא שלי?\" אני אמרתי: \"אבא שלי נכנס לאוטובוס, ולא הלך הביתה\".", "اخوي سألني: وين ابوي؟ انا قلت: ابوي دخل الباص، وما راح على البيت."), - ("אני ביקרתי את הנכד שלי ונתתי לו מכונית קטנה מתנה, הוא היה שמח מאוד.", "انا زرت حفيدي واعطيته سيّارة صغيرة هديّة، هوّ كان مبسوط كتير."), - ]), - ("story", "סיפור השוק", "say", True, [ - ("שמי נביל ואני בן עשרים ושבע.", "اسمي نبيل وعمري سبعة وعشرين سنة."), - ("פעם, חברי אבראהים דיבר איתי ושאל אותי האם אני רוצה ללכת איתו לשוק.", "مرّة، صاحبي ابراهيم حكى معي وسألني اذا بدّي اروح معه عالسّوق."), - ("אמר לי שהוא רוצה לקנות ירקות ופירות.", "قالّي انّه بدّه يشتري خضرة وفواكه."), - ("כעבור בערך שעה הוא הגיע עם המכונית ונסענו ביחד.", "بعد حوالي ساعة هوّ اجا مع السّيّارة ورحنا مع بعض."), - ("כאשר הגענו לשוק, אבראהים שאל את המוכר: מה מחיר המלפפונים?", "لمّا وصلنا على السّوق، ابراهيم سأل البيّاع، قدّيش حقّ الخيار؟"), - ("אמר לו: שישה שקלים לקילו, והתפוחים שבעה שקלים.", "قالّه: ستّة شيكل للكيلو، والتّفّاح سبعة شيكل."), - ("תן לי בבקשה קילו וחצי מלפפונים וקילו עגבניות.", "اعطيني من فضلك كيلو ونصّ خيار وكيلو بندورة."), - ("בבקשה, אחי!", "تفضّل اخوي!"), - ("תודה, יקירי!", "شكرا حبيبي!"), - ("ייתן לך בריאות!", "يعطيك العافية!"), - ("ה' ייתן לך בריאות!", "الله يعفيك!"), - ("אחר כך שאלתי את אבראהים האם הוא רעב או לא?", "بعدين سألت ابراهيم اذا هوّ جوعان ولّا لا؟"), - ("הוא אמר לי שהוא לא רעב, רק רוצה משהו מתוק.", "هوّ قالّي انّه هوّ مش جوعان بسّ بدّه اشي حلو."), - ("אמרתי לו: \"באמת בא לי כנאפה!\"", "قلتله: والله جاي على بالي كنافة!"), - ("נסענו לחנות ממתקים ואכלנו כנאפה.", "رحنا على محلّ حلويات واكلنا كنافة."), - ]), - ("aread", "סיפור השוק: ערבית ← עברית", "read", True, [ - ("כאשר אח שלי בא אכלנו כנאפה ביחד, אחר כך ביקרנו את אמא שלי בבית שלה.", "لمّا اخوي اجا اكلنا كنافة مع بعض، بعدين زرنا امّي في بيتها."), - ("כעבור בערך שעתיים הוא בא עם המכונית שלו ואני הלכתי איתו.", "بعد حوالي ساعتين هوّ اجا مع سيّارته وانا رحت معه."), - ("השכן שלי סיפר לי שהם קנו בית, אמרתי לו: מזל טוב! הוא אמר: \"ה' יברך אותך!\"", "جاري حكى لي انّه همّ اشترو بيت، قلتله: مبروك! هوّ قال: الله يبارك فيك!"), - ("הלכנו לבית של סבתא שלי, אחר כך היא בישלה אוכל טעים מאוד, אכלנו ביחד.", "رحنا على بيت ستّي، بعدين هيّ طبخت#f اكل#n كتير زاكي، اكلنا مع بعض."), - ("היא דיברה איתי ושאלה אותי אם עדנאן חבר שלי, אני אמרתי: לא, הוא לא חבר שלי.", "هيّ حكت معي وسألتني#f اذا عدنان صاحبي، انا قلت: لا، هوّ مش صاحبي."), - ("סלח לי, איפה ההורים שלך? זאת אומרת אבא שלך ואמא שלך, בבית או במסעדה?", "لو سمحت، وين اهلك؟ يعني ابوك وامّك، في البيت ولّا في المطعم؟"), - ("סלח לי, תן לי בבקשה קילו וחצי עגבניות, כמה העגבניות?", "لو سمحت، اعطيني من فضلك كيلو ونصّ بندورة، قدّيش البندورة؟"), - ("פעם חבר שלי אימן קנה מכונית חדשה, הבן שלו היה שמח.", "مرّة صاحبي ايمن اشترى سيّارة جديدة، ابنه كان مبسوط."), - ("אני קניתי שני קילו תפוחים אתמול, ואת, מה קנית?", "انا اشتريت تنين كيلو تفّاح امبارح، وانتي، شو اشتريتي؟"), - ("הבן של אח שלי אמר לי שהוא קנה קפה עם אשתו ואחר כך הם ביקרו את אחותי.", "ابن اخوي قالّي انّه هوّ اشترى قهوة مع مرته وبعدين همّ زارو اختي."), - ("אחרי חמש שעות באנו לשוק, אנחנו עייפים.", "بعد خمس ساعات اجينا على السّوق، احنا تعبانين."), - ("כאשר ביקרתי אותך אתמול, את ישבת עם סמיר ולא אמרת לי שלום!", "لمّا زرتك#f امبارح، انتي قعدتي مع سمير وما قلتيلي مرحبا!"), - ]), - ("say", "סיפור השוק: עברית ← ערבית א'", "say", True, [ - ("כאשר בן דודי (מצד אמא) דיבר איתי אני הייתי עם חברתי.", "لمّا ابن خالي حكى معي انا كنت مع صاحبتي."), - ("תן לי בבקשה חצי קילו עגבניות, כמה עולים המלפפונים?", "اعطيني من فضلك نصّ كيلو بندورة، قدّيش حقّ الخيار؟"), - ("דודי (מצד אבא) אמר לי שהוא רוצה לקנות ירקות אבל לא בשוק.", "عمّي قالّي انّه بدّه يشتري خضرة بسّ مش في السّوق."), - ("היא באה עם המכונית שלה ואני באתי באוטובוס.", "هيّ اجت مع سيّارتها وانا اجيت بالباص."), - ("דיברתי עם אבי ואמרתי שאני רוצה כנאפה, הוא אמר: \"בא לי גם!\"", "حكيت مع ابوي وقلت انّه بدّي كنافة، هوّ قال: جاي على بالي كمان!"), - ("בחייך, כמה עולה קילו וחצי תפוחים? תן לי בבקשה ארבעה קילו.", "وحياتك، قدّيش حقّ كيلو ونصّ تفّاح؟ اعطيني من فضلك اربعة كيلو."), - ("סלחי לי, איפה יש פה חנות ממתקים? לא רחוק, בערך מאה וחמישים מטר.", "لو سمحتي، وين في هون محلّ حلويات؟ مش بعيد، حوالي ميّة وخمسين متر."), - ("נתתי למוכר חמישים שקלים, הוא נתן לי ארבעים שקלים, כמה את נתת למוכר?", "اعطيت البيّاع خمسين شيكل، هوّ اعطاني اربعين شيكل، قدّيش انتي اعطيتي البيّاع؟"), - ("בת דודי (מצד אבא) שאלה אותי אם אני רוצה ללכת איתה לשוק.", "بنت عمّي سألتني#f اذا بدّي اروح معها على السّوق."), - ("הוא רוצה לקנות כנאפה בשוק, מה מחיר הכנאפה?", "بدّه يشتري كنافة في السّوق، قدّيش حقّ الكنافة؟"), - ("אחי אמר שהוא קנה פירות, שאלתי אותו: \"קנית בשוק או בסופר?\"", "اخوي قال انّه هوّ اشترى فواكه، سألته: اشتريت في السّوق ولّا في السّوبرماركت؟", "אחי אמר שהוא קנה פירות, שאלתי אותו: \"קָנִיתָ בשוק או בסופר?\""), - ("תן לי בבקשה קפה! תודה! ייתן לך בריאות!", "اعطيني من فضلك قهوة! شكرا! يعطيك العافية!"), - ("פעם אחת ח'דיג'ה ביקרה אותי ושאלה: \"כמה עולה המכונית?\"", "مرّة خديجة زارتني وسألت#f: قدّيش حقّ السّيّارة؟"), - ]), - ("say2", "סיפור השוק: עברית ← ערבית ב'", "say", True, [ - ("שאלנו את אחמד האם הוא רעב, הוא אמר שהוא הלך למסעדה ואכל שם.", "سألنا احمد اذا هوّ جوعان، هوّ قال انّه هوّ راح على المطعم واكل هناك."), - ("המוכר נכנס ואמר לחברי נביל: \"ה' ישמור אותך!\" ונביל אמר לו: \"תודה! ה' ישמור אותך!\"", "البيّاع دخل وقال لصاحبي نبيل: الله يحفظك! ونبيل قالّه: شكرا! الله يحفظك!", "המוכר נכנס ואמר לחברי נביל: \"ה' ישמור אוֹתְךָ!\" ונביל אמר לו: \"תודה! ה' ישמור אוֹתְךָ!\""), - ("אחר כך נסעתי לבית של אבי בבאר שבע, ישבנו ביחד בערך שעתיים.", "بعدين رحت على بيت ابوي في بير السّبع، قعدنا مع بعض حوالي ساعتين."), - ("כאשר באתי לבית, אמי שאלה אותי איך נכנסתי לכאן?", "لمّا اجيت على البيت، امّي سألتني#f كيف دخلت لهون؟"), - ("את ובעלך ביקרתם אותי לפני יומיים אבל אני לא הייתי בבית.", "انتي وجوزك#f زرتوني قبل يومين بسّ انا ما كنت في البيت.", "את וּבַעֲלֵךְ ביקרתם אותי לפני יומיים אבל אני לא הייתי בבית."), - ("סלחי לי, יש מסעדה בשכונה? כן, לא רחוק, בערך מאתיים מטר.", "لو سمحتي، في مطعم في الحارة؟ اه، مش بعيد، حوالي ميتين متر."), - ("את היית במכונית אתמול? לא, אחותי הייתה במכונית, לא אני.", "انتي كنتي في السّيّارة امبارح؟ لا، اختي كانت في السّيّارة، مش انا."), - ("אנחנו לא ביקרנו אותה לפני שבוע, אנחנו ביקרנו אותה לפני שבועיים.", "احنا ما زرناها قبل اسبوع، احنا زرناها قبل اسبوعين."), - ("אמי אמרה לי: \"אני רוצה ללכת לשוק, בוא איתי!\"", "امّي قالتلي: بدّي اروح على السّوق، تعال معي!"), - ("את ישבת עם אביך בשכונה כאשר אחיך נכנס למכוניתו.", "انتي قعدتي مع ابوكي في الحارة لمّا اخوكي دخل على سيّارته.", "את ישבת עם אָבִיךְ בשכונה כאשר אָחִיךְ נכנס למכוניתו."), - ("אחותי נתנה לי את החולצה ואמרה שהיא שמחה כעת.", "اختي اعطتني البلوزة وقالت انّه هيّ مبسوطة هلّا."), - ]), - ("say3", "סיפור השוק: עברית ← ערבית ג'", "say", True, [ - ("היא נכנסה לבית ואחר כך ישבה על הכיסא, שאלתי אותה איפה היא הייתה היום.", "هيّ دخلت#f على البيت وبعدين قعدت#f على الكرسي، سألتها وين هيّ كانت اليوم."), - ("היית אצל בן דודך (מצד אמא)? מה הוא אמר לך? הוא אמר שהוא רעב. (לאישה)", "كنتي عند ابن خالك#f؟ شو هوّ قالّك#f؟ هوّ قال انّه هوّ جوعان.", "הָיִית אצל בן דּוֹדֵךְ (מצד אמא)? מה הוא אמר לָךְ? הוא אמר שהוא רעב."), - ("לא ביקרתי אותך כיוון שלא הייתי בשכונתך, הייתי בשכונה אחרת. (לגבר)", "ما زرتك عشان ما كنت في حارتك، كنت في حارة تانية.", "לא ביקרתי אוֹתְךָ כיוון שלא הייתי בִּשְׁכוּנָתְךָ, הייתי בשכונה אחרת."), - ("למה לא ישבתם איתנו כאשר הייתם בים?", "ليش ما قعدتو معنا لمّا كنتو في البحر؟"), - ("הוא היה עם אשתו במסעדה, הם ישבו יחד עם ילדיהם.", "هوّ كان مع مرته في المطعم، همّ قعدو مع بعض مع اولادهم."), - ("היא לא ביקרה אותי אבל אני ביקרתי אותה הרבה פעמים.", "هيّ ما زارتني بسّ انا زرتها كتير مرّات."), - ("מי קנה אבטיח היום? אני לא קניתי כיוון שלא הייתי בשוק.", "مين اشترى بطّيخة اليوم؟ انا ما اشتريت عشان ما كنت في السّوق."), - ("ישבתי עם חברתי בחנות הממתקים, סיפרתי לה את סיפור השוק.", "قعدت مع صاحبتي في محلّ الحلويات، حكيتلها قصّة#of السّوق."), - ("אנחנו היינו במסעדה עם אבי, אכלנו חומוס ביחד.", "احنا كنّا في المطعم مع ابوي، اكلنا حمّص مع بعض."), - ("איפה היית לפני שעה וחצי? הייתי עם חברתי בשכונה. (לאישה)", "وين كنتي قبل ساعة ونصّ؟ كنت مع صاحبتي في الحارة.", "איפה הָיִית לפני שעה וחצי? הייתי עם חברתי בשכונה."), - ("חברי אמר לי: \"עכשיו בא לי חומוס!\" אמרתי לו: \"קדימה, למה לא!\"", "صاحبي قالّي: هلّا جاي على بالي حمّص! قلتله: يلّا، ليش لا!"), - ("מי קנה לך את המכונית הגדולה? אני קניתי אותה. (לאישה)", "مين اشترالك#f السّيّارة الكبيرة؟ انا اشتريتها.", "מי קנה לָךְ את המכונית הגדולה? אני קניתי אותה."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 6): [ - ("new", "מילים חדשות", "learn", True, [ - ("לבדי", "لحالي"), ("מיטה", "تخت"), ("המונית", "التّاكسي"), ("כמו", "زيّ"), ("המסיבה", "الحفلة"), - ("האיש", "الزّلمة"), ("פנוי, ריק", "فاضي"), ("קר", "بارد"), ("כתוב, גורל", "مكتوب"), ("זה", "هادا"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("fut", "הטיות בעתיד", "say", False, _past(FUTURE_6)), - ("futsay", "עבר ועתיד: עברית ← ערבית", "say", True, [ - ("אני לא נכנסתי לבית, אני אכנס בעוד שבוע.", "انا ما دخلت على البيت، انا ادخل كمان اسبوع."), - ("אשתי הלכה לבית של אמא שלה, מחר היא תלך לבית של סבתא שלה.", "مرتي راحت على بيت امّها، بكرا هيّ تروح على بيت ستّها."), - ("אנחנו שחינו בים, מחר נשחה בבריכה.", "احنا سبحنا في البحر، بكرا نسبح في البركة."), - ("אתה ישבת על המיטה, מחר תשב על הכיסא.", "انت قعدت على التّخت، بكرا تقعد على الكرسي.", "אתה יָשַׁבְתָּ על המיטה, מחר תֵּשֵׁב על הכיסא."), - ("הוא קנה פירות בשוק, מחר הוא יקנה ירקות בסופרמרקט.", "هوّ اشترى فواكه في السّوق، بكرا هوّ يشتري خضرة في السّوبرماركت."), - ("אני נתתי לך עשרים וארבעה שקלים, אני אתן לך עוד שישה עשר שקלים. (לגבר)", "انا اعطيتك اربعة وعشرين شيكل، انا اعطيك كمان ستّعشر شيكل.", "אני נתתי לְךָ עשרים וארבעה שקלים, אני אתן לְךָ עוד שישה עשר שקלים."), - ("הוא נכנס לביתו, מחר הוא ייכנס לחדרו.", "هوّ دخل على بيته، بكرا هوّ يدخل على غرفته."), - ("את שאלת אותי אתמול, מחר את תשאלי את בעלך שתי שאלות.", "انتي سألتيني امبارح، بكرا انتي تسألي جوزك#f سؤالين.", "את שאלת אותי אתמול, מחר את תשאלי את בַּעֲלֵךְ שתי שאלות."), - ("ישבנו עם אחמד ואכלנו יחד, מחר נשב עם יונס ונאכל יחד.", "قعدنا مع احمد واكلنا مع بعض، بكرا نقعد مع يونس وناكل مع بعض."), - ("את ביקרת את חברתך, מחר את תבקרי אותי.", "انتي زرتي صاحبتك#f، بكرا انتي تزوريني.", "את ביקרת את חֲבֶרְתֵּךְ, מחר את תבקרי אותי."), - ("הם באו במכונית, הם לא יבואו עם המונית.", "همّ اجو بالسّيّارة، همّ ما يجو مع التّاكسي."), - ("היא שחתה בים, מחר היא תשחה בבריכה.", "هيّ سبحت#f في البحر، بكرا هيّ تسبح في البركة."), - ("אני דיברתי איתך היום, אני אדבר עם אמא שלך מחר. (לאישה)", "انا حكيت معك#f اليوم، انا احكي مع امّك#f بكرا.", "אני דיברתי אִתָּךְ היום, אני אדבר עם אמא שֶׁלָּךְ מחר."), - ]), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_6)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_6)), - ("read", "משפטים: ערבית ← עברית א'", "read", True, [ - ("אתם נכנסתם למסעדה עם חבר שלי? לא, אנחנו נכנסנו לבית עם דוד שלי (מצד אמא).", "انتو فتّو على المطعم مع صاحبي؟ لا، احنا فتنا على البيت مع خالي."), - ("ראית לאן הלך הילד? לא, לא ראיתי שום דבר.", "شفتي لوين راح الولد؟ لا، ما شفتش اشي."), - ("הייתם בבית הספר? כן, היינו בו היום! מה אמרה המורה? לא אמרה שום דבר.", "كنتو في المدرسة؟ اه، كنّا فيها اليوم! شو قالت المعلّمة؟ ما قالت اشي."), - ("היא הלכה מהבית לשוק עם בעלה, זאת אומרת הלכו ביחד.", "هيّ راحت من البيت على السّوق مع جوزها، يعني راحو مع بعض."), - ("אנחנו ישבנו עם החברים שלנו, אבל חבר שלי לא ישב איתנו, הוא נהג במונית.", "احنا قعدنا مع اصحابنا، بسّ صاحبي ما قعد معنا، هوّ ساق التّاكسي."), - ("אתם הייתם בים? לא, אנחנו נכנסנו למכונית ואחר כך אני נהגתי הביתה.", "انتو كنتو في البحر؟ لا، احنا فتنا على السّيّارة وبعدين انا سقت على البيت."), - ("סלחי לי, כמה מכוניות יש לך? אין לי מכונית בכלל.", "لو سمحتي، اكم سيّارة عندك#f؟ ما عندي سيّارة بالمرّة."), - ("היא הלכה לרופאה ואני נהגתי במכונית והלכתי לבית של דודה שלי (מצד אמא).", "هيّ راحت لعند الدّكتورة وانا سقت السّيّارة ورحت على بيت خالتي."), - ("כאשר הוא בא לבית של ההורים שלו הוא לא אכל מקלובה, אמא שלו הביאה לו בקלאווה.", "لمّا هوّ اجا على بيت اهله هوّ ما اكل مقلوبة، امّه جابتله بقلاوة."), - ("הילדה ישבה בשכונה עם החברות שלה, ואחר כך הן נכנסו למסעדה.", "البنت قعدت#f في الحارة مع صاحباتها، وبعدين همّ فاتو على المطعم."), - ("כאשר הם ביקרו אותי הם לא ראו את אבא שלי, הוא אמר לי שהוא היה בבית של השכן שלנו.", "لمّا همّ زاروني همّ ما شافو ابوي، هوّ قالّي انّه هوّ كان في بيت جارنا."), - ]), - ("read2", "משפטים: ערבית ← עברית ב'", "read", True, [ - ("דוד שלי (מצד אבא) ואשתו נכנסו לבית הגדול שלי אבל לא ראו אותי, אתמול לא הייתי בבית.", "عمّي ومرته فاتو على بيتي الكبير بسّ ما شافونيش، امبارح ما كنت في البيت."), - ("סלח לי, לאן הלכת עם הבת שלך אחרי המסיבה? הלכנו לבית של אח שלי בשכונה שלנו.", "لو سمحت، لوين رحت مع بنتك بعد الحفلة؟ رحنا على بيت اخوي في حارتنا."), - ("כאשר אבא שלי היה בבית הוא נישק את הבן שלי, זאת אומרת הוא נישק את הנכד שלו.", "لمّا ابوي كان في البيت هوّ باس ابني، يعني هوّ باس حفيده."), - ("כמה בנים וכמה בנות יש לך? יש לי רק שתי בנות, ה' ישמור אותן, ואת? יש לי שני בנים ובת.", "اكم ولد واكم بنت عندك؟ عندي بسّ بنتين الله يخلّيهم، وانتي؟ عندي ولدين وبنت."), - ("אתמול ראית מישהו שנהג במכונית שלך? מי נהג בה? השכן שלי נהג בה.", "امبارح شفت واحد ساق سيّارتك؟ مين ساقها؟ جاري ساقها."), - ("ביקרנו את אמא שלי והיא נישקה אותנו שלוש פעמים, גם אנחנו נישקנו אותה!", "زرنا امّي وهيّ باستنا تلات مرّات، احنا كمان بسناها!"), - ("אבא שלי נכנס לחדר ושאל אותי מי פתח את החלון, הוא ראה שהוא פתוח.", "ابوي فات على الغرفة وسألني مين فتح الشّبّاك، هوّ شاف انّه هوّ مفتوح."), - ("את לא ראית אותי לפני יומיים במסעדה כיוון שאני לא הייתי שם.", "انتي ما شفتيني قبل يومين في المطعم عشان انا ما كنت هناك."), - ("דודה שלי (מצד אמא) הלכה ופתחה את החלון ואמרה שעכשיו היא שמחה מאוד, ברוך השם.", "خالتي راحت وفتحت#f الشّبّاك وقالت انّه هلّا هيّ مبسوطة كتير الحمد للّه."), - ("מי היה פה לפני קצת זמן? הבן של דוד שלך (מצד אבא) מחמוד ביקר אותי לפני בערך שעה.", "مين كان هون قبل شويّ؟ ابن عمّك محمود زارني قبل حوالي ساعة."), - ("אני נתתי לך ארבעים וחמישה שקלים, כמה אתה נתת לי?", "انا اعطيتك خمسة واربعين شيكل، قدّيش انت اعطيتني؟"), - ("מה בישלת היום? אני לא בישלתי, אמרתי לך שלא הייתי בבית היום.", "شو طبختي اليوم؟ انا ما طبختش، قلتلك انّه ما كنتش في البيت اليوم."), - ("אתם ראיתם את האיש פה? כן! הוא נכנס למכונית שלו ונהג לפני שעה וחצי.", "انتو شفتو الزّلمة هون؟ اه! هوّ فات على سيّارته وساق قبل ساعة ونصّ."), - ]), - ("story", "קפטן איוב ונסרין", "say", True, [ - ("איוב: נסרין יקירתי", "ايوب: نسرين حبيبتي"), - ("נסרין: יקירתך!?", "نسرين: حبيبتك!؟"), - ("איוב: שמעי!", "ايوب: اسمعي!"), - ("אני גרוש פעמיים", "انا مطلّق مرّتين"), - ("יש לי חמישה ילדים, ה' ישמור אותם", "عندي خمس اولاد الله يخلّيهم"), - ("אבל אני חוזר לבית ריק.", "بسّ انا برجع لبيت فاضي"), - ("בו מיטה גדולה וקרה.", "فيه تخت كبير وبارد"), - ("לבדי, כל יום לבדי.", "لحالي، كلّ يوم لحالي"), - ("לו הייתה לי אישה כמוך...", "لو كان عندي مرة زيّك#f..."), - ("נסרין: קפטן איוב, אם תסלח לי", "نسرين: كابتن ايوب، لو سمحت"), - ("כל אחד לוקח את גורלו בחיים.", "كلّ واحد بياخد نصيبه بالحياة"), - ("הכול כתוב מאת ה'.", "كلّه مكتوب من عند الله"), - ]), - ("meet", "קפטן איוב: עברית ← ערבית", "say", True, [ - ("חברי אמר שהוא גרוש ויש לו ארבעה ילדים, ה' ישמור אותם.", "صاحبي قال انّه هوّ مطلّق وعنده اربع اولاد، الله يخلّيهم."), - ("מחמוד היה לבדו בבית הספר, אני הייתי לבדי בבית, לבדי, כל יום לבדי.", "محمود كان لحاله في المدرسة، انا كنت لحالي في البيت، لحالي، كلّ يوم لحالي."), - ("שמע! אני רוצה ללכת לשכני החדש ואביא לו את המיטה הזו.", "اسمع! بدّي اروح لعند جاري الجديد واجيبله هادا التّخت.", "שְׁמַע! אני רוצה ללכת לשכני החדש ואביא לו את המיטה הזו."), - ("שמי חמודי, זאת אומרת מחמד, נעים להכיר! יוסיף לך כבוד!", "اسمي حمّودي، يعني محمّد، تشرّفنا! يزيدك شرف!"), - ("סלחי לי, איפה גר אבו יוסף ממשפחת טהה? הוא גר עם משפחתו פה בשכונה.", "لو سمحتي، وين ساكن ابو يوسف من دار طه؟ هوّ ساكن مع عيلته هون في الحارة."), - ("סלח לי דודי (מצד אבא), איפה יש מיטות? אני רוצה לקנות מיטה קטנה לבתי.", "لو سمحت عمّي، وين في تخوت؟ بدّي اشتري تخت صغير لبنتي."), - ("כמה עולה הבית? אני רוצה לקנות בית כמוהו ברמאללה.", "قدّيش حقّ البيت؟ بدّي اشتري بيت زيّه في رام الله."), - ("כאשר ח'אלד היה בבית אני ישבתי עם אחי, ישבנו לבדנו בבית.", "لمّا خالد كان في البيت انا قعدت مع اخوي، قعدنا لحالنا في البيت."), - ("הוא נישק את נכדתו כאשר היא ביקרה אותו בביתו.", "هوّ باس حفيدته لمّا هيّ زارته في بيته."), - ("ביקרנו את אחי בשכם, הוא גר לא רחוק מהשוק.", "زرنا اخوي في نابلس، هوّ ساكن مش بعيد عن السّوق."), - ("סלח לי, איפה יש מכונית כמו המכונית שלך? אני רוצה מכונית כמוה.", "لو سمحت، وين في سيّارة زيّ سيّارتك؟ بدّي سيّارة زيّها.", "סלח לי, איפה יש מכונית כמו המכונית שֶׁלְּךָ? אני רוצה מכונית כמוה."), - ("חברתי עברה לבית חדש, אני הלכתי אליה, ביקרתי אותה ונתתי לה מתנה.", "صاحبتي نقلت#f على بيت جديد، انا رحت لعندها، زرتها واعطيتها هديّة."), - ]), - ("meetread", "קפטן איוב: ערבית ← עברית", "read", True, [ - ("אני ביקרתי את אהובתי לפני שבוע וחצי, אחר כך היא ביקרה אותי, ישבנו בבית לבדנו.", "انا زرت حبيبتي قبل اسبوع ونصّ، بعدين هيّ زارتني، قعدنا في البيت لحالنا."), - ("אני לא נשואה ולא רווקה, אני לא גרה לבדי בבית שלי.", "انا مش متجوّزة ومش عزّابيّة، انا مش ساكنة لحالي في بيتي."), - ("יש לי חמישה ילדים, ה' ישמור אותם! כמו אחותי, גם לה יש חמישה.", "عندي خمس اولاد، الله يخلّيهم! زيّ اختي، كمان هيّ عندها خمسة."), - ("חנות הממתקים רחוקה, למה הלכת לבדך? אני לא הלכתי לבדי.", "محلّ الحلويات بعيد، ليش رحتي لحالك#f؟ انا ما رحتش لحالي."), - ("אני רוצה לקנות מכונית חדשה, לו הייתה לי מכונית חדשה אשתי הייתה שמחה.", "بدّي اشتري سيّارة جديدة، لو كانت عندي سيّارة جديدة مرتي كانت مبسوطة."), - ("כאשר הלכנו לשכונה, ראינו את חבר שלי עאדל, הוא היה עם אשתו.", "لمّا رحنا على الحارة، شفنا صاحبي عادل هوّ كان مع مرته."), - ("אחותי שאלה את המוכר מה מחיר העגבניות ומה מחיר המלפפונים?", "اختي سألت#f البيّاع قدّيش حقّ البندورة وقدّيش حقّ الخيار؟"), - ("היא הלכה לבדה לבית של חברה שלה, אני לא ראיתי אותה.", "هيّ راحت لحالها على بيت صاحبتها، انا ما شفتهاش."), - ("היא שאלה את הנגר מה מחיר השולחן ומה מחיר הכיסא.", "هيّ سألت#f النّجّار قدّيش حقّ الطّاولة وقدّيش حقّ الكرسي."), - ("דוד שלי (מצד אמא) בא אליי ואני הייתי שמח מאוד, אחר כך הלכנו לחנות הממתקים.", "خالي اجا لعندي وانا كنت مبسوط كتير، بعدين رحنا على محلّ الحلويات."), - ("כאשר נכנסנו לבית שלי אבא שלי ישב עם אמא שלי והיה שמח.", "لمّا دخلنا على بيتي ابوي قعد مع امّي وكان مبسوط."), - ("לאן הלכת עם הבן של השכן שלי? הלכנו לשוק ביחד ואחר כך ביקרנו את דוד שלי (מצד אמא).", "لوين رحت مع ابن جاري؟ رحنا على السّوق مع بعض وبعدين زرنا خالي."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 7): [ - ("fem", "זכר ונקבה", "say", True, [(f"{he}, {he2}", f"{ar}، {ar2}") for he, he2, ar, ar2 in FEMININES]), - ("dual", "הזוגי", "say", True, [(f"{he}, {he2}", f"{ar}، {ar2}") for he, he2, ar, ar2 in DUALS_7]), - ("nums", "המספרים", "say", True, [(f"{he} ({digits})", ar, he) for digits, he, ar in NUMBERS_7]), - ("count", "מספר ושם עצם", "say", True, [ - ("שלושה ילדים", "تلات اولاد"), ("חמש בנות", "خمس بنات"), ("שבעה ילדים", "سبع اولاد"), ("אחד עשר ילדים", "حدعشر ولد"), - ("שלושה עשר כיסאות", "تلاتعشر كرسي"), ("עשרים שקלים", "عشرين شيكل"), ("שתים עשרה בנות", "تنعشر بنت"), - ("מאה שקלים", "ميّة#of شيكل"), ("תשעה עשר ילדים", "تسعتعشر ولد"), ("תשעים וחמישה שקלים", "خمسة وتسعين شيكل"), - ("שישים וארבעה שקלים", "اربعة وستّين شيكل"), ("אלף מאה חמישים ושלושה שקלים", "الف ميّة تلاتة وخمسين شيكل"), - ("שלושת אלפים שמונה מאות שמונים ושניים שקלים", "تلات الاف تمن ميّة تنين وتمانين شيكل"), - ("מיליון ואחד", "مليون وواحد"), ("עשרה מיליון", "عشرة مليون"), - ]), - # every verb so far, in the workbook's order: פַעַל, the verbs that end in ا, גזרת ע"ו - ("verbs", "פעלים: חזרה", "say", True, _base_forms(VERBS_1 + VERBS_3 + VERBS_6[-1:] + VERBS_4 + VERBS_5 + VERBS_6[:-1])), - ("read", "משפטים: ערבית ← עברית", "read", True, [ - ("לפני שבוע הלכנו לשוק וקנינו תפוחים וכנאפה.", "قبل اسبوع رحنا على السّوق واشترينا تفّاح وكنافة."), - ("כאשר חברה שלי באה לבית שלי, אני בישלתי מקלובה ואכלנו ביחד.", "لمّا اجت صاحبتي على بيتي، انا طبخت مقلوبة واكلنا مع بعض."), - ("אבא שלי נהג במכונית ואנחנו נסענו לים.", "ابوي ساق السّيّارة واحنا رحنا على البحر."), - ("השכן החדש שלנו עבר לשכונה שלנו לפני יומיים, הוא לא נשוי.", "جارنا الجديد نقل على حارتنا قبل يومين، هوّ مش متجوّز."), - ("מתי ביקרת את דודה שלך (מצד אמא)? ביקרתי אותה אתמול ואחר כך ביקרתי גם את דודה שלי (מצד אבא).", "ايمتى زرتي خالتك#f؟ زرتها امبارح وبعدين زرت عمّتي كمان."), - ("אני ראיתי שהילד נכנס לבית ונישק את אמא שלו.", "انا شفت انّه الولد فات على البيت وباس امّه."), - ("ראיתם את הילדים בבריכה? לא, לא ראינו אותם.", "شفتو الاولاد في البركة؟ لا، ما شفناهم."), - ("מה מחיר התפוחים? התפוחים שבעה שקלים.", "قدّيش حقّ التّفّاح؟ التّفّاح سبعة شيكل."), - ("למה לא היית בבית הספר היום? כי הייתי אצל סבתא שלי.", "ليش ما كنتش في المدرسة اليوم؟ عشان كنت عند ستّي."), - ("אח שלי אמר לי שהוא רוצה לקנות מכונית חדשה.", "اخوي قالّي انّه بدّه يشتري سيّارة جديدة."), - ("היום אני לא רעב, אבל מחר אני אלך למסעדה עם חבר שלי.", "اليوم انا مش جوعان، بسّ بكرا انا اروح على المطعم مع صاحبي."), - ("דיברתי עם דוד שלי (מצד אמא) ואמרתי לו: \"ה' איתך\".", "حكيت مع خالي وقلتله: الله معك."), - ("אבא שלי נתן לי מאה שקלים, ואני נתתי לאחותי ארבעים שקלים.", "ابوي اعطاني ميّة#of شيكل، وانا اعطيت اختي اربعين شيكل."), - ("הוא ראה את הדלת פתוחה ונכנס לבית לבדו.", "هوّ شاف الباب مفتوح وفات على البيت لحاله."), - ("יש לי שלושה ילדים, שני בנים ובת, את, כמה ילדים יש לך?", "عندي تلات اولاد، ولدين وبنت، انتي، اكم ولد عندك#f؟"), - ("אתה ביקרת את סבתא שלך אתמול? לא, מחר אני אבקר את סבתא שלי.", "انت زرت ستّك امبارح؟ لا، بكرا انا ازور ستّي."), - ("אמא שלי קנתה שלושה קילו תפוחים וקילו עגבניות מהשוק.", "امّي اشترت#f تلاتة كيلو تفّاح وكيلو بندورة من السّوق."), - ("כאשר היינו בחברון, ביקרנו את דוד שלי (מצד אמא) וישבנו אצלו יומיים.", "لمّا كنّا في الخليل، زرنا خالي وقعدنا عنده يومين."), - ("למה לא באתם לבית הספר אתמול? כי היינו אצל הרופא.", "ليش ما اجيتو على المدرسة امبارح؟ عشان كنّا عند الدّكتور."), - ("מי דיבר איתך? חבר שלי דיבר איתי ושאל אותי איפה אבא שלי.", "مين حكى معك؟ صاحبي حكى معي وسألني وين ابوي."), - ("השכנה שלי אמרה לי שהיא לא שמחה בשכונה החדשה.", "جارتي قالتلي انّه هيّ مش مبسوطة في الحارة الجديدة."), - ("בת כמה הבת שלך? היא בת אחת עשרה.", "قدّيش عمر بنتك؟ عمرها حدعشر سنة."), - ("אתם נכנסתם לבית של סבתא שלכם? כן, נכנסנו ונישקנו אותה.", "انتو فتّو على بيت ستّكم؟ اه، فتنا وبسناها."), - ]), - ("say", "משפטים: עברית ← ערבית א'", "say", True, [ - ("לפני שבועיים ביקרתי את דודי (מצד אמא) בחברון, וישבנו אצלו יומיים.", "قبل اسبوعين زرت خالي في الخليل، وقعدنا عنده يومين."), - ("כשבאנו הביתה, אמא שלי בישלה מקלובה ואכלנו ביחד.", "لمّا اجينا على البيت، امّي طبخت#f مقلوبة واكلنا مع بعض."), - ("מי פתח את החלון? אני לא פתחתי אותו, אחי פתח אותו.", "مين فتح الشّبّاك؟ انا ما فتحته، اخوي فتحه."), - ("יש לי שלושה ילדים ושתי מכוניות, אבל אין לי כלב.", "عندي تلات اولاد وسيّارتين، بسّ ما عندي كلب."), - ("בת כמה אחותך? היא בת שבע עשרה, והיא לא נשואה. (לגבר)", "قدّيش عمر اختك؟ عمرها سبعتعش، وهيّ مش متجوّزة.", "בת כמה אֲחוֹתְךָ? היא בת שבע עשרה, והיא לא נשואה."), - ("אתמול הלכנו לשוק וקנינו שני קילו תפוחים ושלושה קילו עגבניות.", "امبارح رحنا على السّوق واشترينا تنين كيلو تفّاح وتلاتة كيلو بندورة."), - ("למה לא באתם לביתנו? כי הייתם בים? לא, לא היינו בים.", "ليش ما اجيتو على بيتنا؟ عشان كنتو في البحر؟ لا، ما كنّا في البحر."), - ("השכן החדש שלנו מרמאללה, הוא עבר לשכונה שלנו לפני חודש.", "جارنا الجديد من رام الله، هوّ نقل على حارتنا قبل شهر."), - ("מחר אלך אליו ואביא לו עוגה.", "بكرا اروح لعنده واجيبله كعكة."), - ("אשתי אמרה לי: \"לך אליו ותראה אם הוא נשוי או רווק\".", "مرتي قالتلي: روح لعنده وشوف اذا هوّ متجوّز ولّا عزّابي."), - ("היא נכנסה הביתה, נישקה את אמה וסיפרה לה סיפור ארוך.", "هيّ دخلت#f على البيت، باست امّها وحكتلها قصّة طويلة."), - ("אבי נתן לי חמישים שקלים, ואני קניתי כנאפה לאחי הקטן.", "ابوي اعطاني خمسين شيكل، وانا اشتريت كنافة لاخوي الصّغير."), - ("מתי ראית את חברך? ראיתי אותו לפני שעה ודיברתי איתו. (לגבר)", "ايمتى شفت صاحبك؟ شفته قبل ساعة وحكيت معه.", "מתי רָאִיתָ את חֲבֵרְךָ? ראיתי אותו לפני שעה ודיברתי איתו."), - ("אנחנו לא רוצים ללכת למסעדה היום, נשב בבית ונאכל ביחד.", "احنا ما بدّنا نروح على المطعم اليوم، نقعد في البيت وناكل مع بعض."), - ]), - ("say2", "משפטים: עברית ← ערבית ב'", "say", True, [ - ("איפה היית אתמול? הייתי אצל סבתי, היא גרה לא רחוק מכאן. (לגבר)", "وين كنت امبارح؟ كنت عند ستّي، هيّ ساكنة مش بعيد من هون.", "איפה הָיִיתָ אתמול? הייתי אצל סבתי, היא גרה לא רחוק מכאן."), - ("כמה עולה המכונית? אני רוצה לקנות מכונית כמו המכונית שלך. (לגבר)", "قدّيش حقّ السّيّارة؟ بدّي اشتري سيّارة زيّ سيّارتك.", "כמה עולה המכונית? אני רוצה לקנות מכונית כמו המכונית שֶׁלְּךָ."), - ("אני וההורים שלי נסענו לירושלים, ואבי נהג במכונית.", "انا واهلي رحنا على القدس، وابوي ساق السّيّارة."), - ("דודתי (מצד אבא) שחתה בבריכה, ואחר כך היא ישבה עם בתה.", "عمّتي سبحت#f في البركة، وبعدين هيّ قعدت#f مع بنتها."), - ("סלח לי, שאלת אותי מה שמי? שמי מחמוד, נעים להכיר!", "لو سمحت، سألتني شو اسمي؟ اسمي محمود، تشرّفنا!", "סלח לי, שָׁאַלְתָּ אותי מה שמי? שמי מחמוד, נעים להכיר!"), - ("כשהייתי קטן הייתי בירושלים, אבל היום אני גר בחיפה.", "لمّا كنت صغير كنت في القدس، بسّ اليوم انا ساكن في حيفا."), - ("לפני שבוע אבא ואמא של סמיר באו לבית של סמיר, ואשתו בישלה מקלובה.", "قبل اسبوع ابو سمير وامّ سمير اجو على بيت سمير، ومرته طبخت#f مقلوبة."), - ("שאלתי את המוכר כמה עולה הקפה, והוא אמר לי: \"אחד עשר שקלים\".", "سألت البيّاع قدّيش حقّ القهوة، وهوّ قالّي: حدعشر شيكل."), - ("אחותי הבכורה שמה רובא, היא בת עשרים ושתיים ויש לה בת קטנה.", "اختي البكرة اسمها روبا، عمرها تنين وعشرين وعندها بنت صغيرة."), - ("נכנסתי לבית של חברי ולא ראיתי אותו, הוא לא היה בבית.", "دخلت على بيت صاحبي وما شفته، هوّ ما كان في البيت."), - ("כמה כיסאות יש בבית? יש ארבעה עשר כיסאות ושני שולחנות.", "اكم كرسي في البيت؟ في اربعتعشر كرسي وطاولتين."), - ("איך אמא שלך? היא שמחה ברוך השם, היא באה אלינו אתמול. (לגבר)", "كيف امّك؟ هيّ مبسوطة الحمد للّه، هيّ اجت لعندنا امبارح.", "איך אמא שֶׁלְּךָ? היא שמחה ברוך השם, היא באה אלינו אתמול."), - ("שאלתי את חברתי כמה עולה המכונית שלה, היא אמרה ארבעים ושבעה אלף שקלים.", "سألت صاحبتي قدّيش حقّ سيّارتها، هيّ قالت سبعة واربعين الف شيكل."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 8): [ - ("new", "מילים חדשות", "learn", True, [ - ("נמצא, יש", "موجود"), ("ככה", "هيك"), ("צבע", "لون"), ("מוזיקה", "موسيقى"), ("מובן", "مفهوم"), - ("העבודה", "الشّغل"), ("חתונה", "عرس"), ("שיר", "غنوة"), ("השמש", "الشّمس"), ("מכנסיים", "بنطلون"), - ("העץ", "الشّجرة"), ("חשוב", "مهمّ"), ("כסף", "مصاري"), ("דקה", "دقيقة"), ("השכנים", "الجيران"), - ("השמלה", "الفستان"), ("מים", "ميّ"), ("המשרד", "المكتب"), ("גבינה", "جبنة"), ("פרח", "وردة"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("howmany", "אַכַּם וקַדֵّיש: עברית ← ערבית", "say", True, [ - ("לאחותך הגדולה, כמה ילדים יש לה? לאחותי יש שבעה ילדים. (לאישה)", "اختك#f الكبيرة، اكم ولد عندها؟ اختي عندها سبع اولاد.", "לַאֲחוֹתֵךְ הגדולה, כמה ילדים יש לה? לאחותי יש שבעה ילדים."), - ("כמה אנשים היו במכונית? במכונית היו גבר ושלושה ילדים.", "اكم واحد كان في السّيّارة؟ في السّيّارة كانو زلمة وتلات اولاد."), - ("חברי נתן לי קפה, אני שאלתי אותו עם כמה סוכר?", "صاحبي اعطاني قهوة، انا سألته مع قدّيش سكّر؟"), - ("כמה עולים התפוחים? תן לי בבקשה ארבעה קילו.", "قدّيش حقّ التّفّاح؟ اعطيني من فضلك اربعة كيلو."), - ("בתי בת שלושים, בן כמה אתה? אני בן שלושים ושבע.", "بنتي عمرها تلاتين سنة، قدّيش عمرك؟ عمري سبعة وتلاتين سنة."), - ("כמה אתה שמח? הרבה, ברוך השם, ואתה? חיים...", "قدّيش انت مبسوط؟ كتير الحمد للّه، وانت؟ عايشين..."), - ("כמה קילו תפוחי אדמה אתה רוצה? בבקשה! ייתן לך בריאות!", "اكم كيلو بطاطا بدّك؟ تفضّل! يعطيك العافية!"), - ("כמה שולחנות יש לך בבית? וכמה שולחנות יש לך בחוץ? (לאישה)", "اكم طاولة عندك#f في البيت؟ واكم طاولة عندك#f برّا؟", "כמה שולחנות יש לָךְ בבית? וכמה שולחנות יש לָךְ בחוץ?"), - ("כמה כסף הוא רוצה? לא הרבה, בערך שבעים ושישה שקלים.", "قدّيش مصاري بدّه؟ مش كتير، حوالي ستّة وسبعين شيكل."), - ("כמה פעמים אמרת לאשתך שאתה רעב? היום אמרתי לה שלוש פעמים.", "اكم مرّة قلت لمرتك انّه انت جوعان؟ اليوم قلتلها تلات مرّات.", "כמה פעמים אָמַרְתָּ לְאִשְׁתְּךָ שאתה רעב? היום אמרתי לה שלוש פעמים."), - ("כמה אחיות וכמה אחים יש לך? יש לי שלוש אחיות ושלושה אחים, אני בן הזקונים. (לגבר)", "اكم اخت واكم اخ عندك؟ عندي تلات اخوات وتلات اخوة، انا آخر العنقود.", "כמה אחיות וכמה אחים יש לְךָ? יש לי שלוש אחיות ושלושה אחים, אני בן הזקונים."), - ("כמה כיסאות יש אצלך בבית? אצלי בבית יש בערך עשרה כיסאות. (לגבר)", "اكم كرسي عندك في البيت؟ عندي في البيت في حوالي عشر كراسي.", "כמה כיסאות יש אֶצְלְךָ בבית? אצלי בבית יש בערך עשרה כיסאות."), - ("כמה זמן דיברת עם אחותך? דיברנו בערך שלושים וחמש דקות. (לאישה)", "قدّيش وقت حكيتي مع اختك#f؟ حكينا حوالي خمسة وتلاتين دقيقة.", "כמה זמן דִּבַּרְתְּ עם אֲחוֹתֵךְ? דיברנו בערך שלושים וחמש דקות."), - ]), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_8)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_8)), - ("read", "משפטים: ערבית ← עברית א'", "read", True, [ - ("מחמוד חזר מהעבודה ושמע מוזיקה בסלון הבית שלו.", "محمود رجع من الشّغل وسمع موسيقى في صالون بيته."), - ("אני חזרתי מהחתונה של חבר שלי והבן שלי חזר מחתונה אחרת.", "انا رجعت من عرس صاحبي وابني رجع من عرس تاني."), - ("את יצאת מהבית והילד לא יצא איתך.", "انتي طلعتي من البيت والولد ما طلع معك#f."), - ("אנחנו שמענו שיר יפה ואחר כך הבנו את השיר הזה.", "احنا سمعنا غنوة حلوة وبعدين فهمنا هادي الغنوة."), - ("אני שאלתי מי יצא מהבית ולקח איתו את הארנק?", "انا سألت مين طلع من البيت واخد معه الجزدان؟"), - ("הילד חזר אתמול מהמסיבה של השכנים, אני ראיתי אותו בבית.", "الولد رجع امبارح من حفلة#of الجيران، انا شفته في البيت."), - ("איך לא הבנת? לא היית בבית הספר? לא, לא הייתי אתמול בבית הספר.", "كيف ما فهمتي؟ ما كنتي في المدرسة؟ لا، ما كنتش امبارح في المدرسة."), - ("הבן שלי שמע מוזיקה עם הבת שלך, הם שמעו שיר חדש ביחד.", "ابني سمع موسيقى مع بنتك، همّ سمعو غنوة جديدة مع بعض."), - ("דודה שלי (מצד אמא) שמעה שאחותי קנתה את השמלה החדשה ואמרה: \"מזל טוב!\"", "خالتي سمعت#f انّه اختي اشترت#f الفستان الجديد وقالت: مبروك!"), - ("אני שתיתי נס קפה, אבא שלי לא שתה קפה אתמול איתי.", "انا شربت نسكافيه، ابوي ما شرب قهوة امبارح معي."), - ("מה שמעתם מאחמד? שמענו שהוא יצא עם הבן הקטן שלו והלכו לחברון.", "شو سمعتو من احمد؟ سمعنا انّه هوّ طلع مع ابنه الصّغير وراحو على الخليل."), - ("כאשר אני חזרתי מירושלים אני ראיתי שאתה חזרת משכם.", "لمّا انا رجعت من القدس انا شفت انّه انت رجعت من نابلس."), - ]), - ("read2", "משפטים: ערבית ← עברית ב'", "read", True, [ - ("אתם שתיתם תה עם חבר שלי? לא, אנחנו שתינו קפה עם אבא שלך.", "انتو شربتو شاي مع صاحبي؟ لا، احنا شربنا قهوة مع ابوك."), - ("שמעת לאן הלכה הילדה? לא, לא שמעתי שום דבר.", "سمعت لوين راحت البنت؟ لا، ما سمعتش اشي."), - ("השאלה לא מובנת, אתם הבנתם מה אמר המורה? כן! הבנו מה אמר.", "السّؤال مش مفهوم، انتو فهمتو شو قال الاستاذ؟ اه! فهمنا شو قال."), - ("היא יצאה מהבית והלכה לשוק עם בעלה, זאת אומרת הלכו ביחד.", "هيّ طلعت#f من البيت وراحت على السّوق مع جوزها، يعني راحو مع بعض."), - ("אנחנו ישבנו במסעדה, אבל חבר שלי סמיר לא ישב איתנו, הוא יצא עם אשתו.", "احنا قعدنا في المطعم، بسّ صاحبي سمير ما قعد معنا، هوّ طلع مع مرته."), - ("אתם שחיתם בים? לא, אנחנו שמענו מוזיקה במכונית.", "انتو سبحتو في البحر؟ لا، احنا سمعنا موسيقى بالسّيّارة."), - ("מי לא אכל את האוכל? אנחנו לא רעבים, לכן לא אכלנו, שתינו רק קפה.", "مين ما اكل الأكل؟ احنا مش جوعانين، عشان هيك ما اكلنا، شربنا بسّ قهوة."), - ("היא יצאה החוצה ואני עליתי למכונית ונהגתי לבית של דוד שלי (מצד אמא) יוסף.", "هيّ طلعت#f برّا وانا طلعت على السّيّارة وسقت على بيت خالي يوسف."), - ("כאשר הוא חזר לבית של ההורים שלו הוא לא אכל מקלובה אבל הוא אכל בקלאווה.", "لمّا هوّ رجع على بيت اهله هوّ ما اكل مقلوبة امّا هوّ اكل بقلاوة."), - ("הילדה יצאה לשכונה עם אח שלה ואחר כך הם נכנסו למסעדה.", "البنت طلعت#f على الحارة مع اخوها وبعدين همّ دخلو على المطعم."), - ("אבא שלי בא ושאל אותי למה חזרתי לבדי, אני אמרתי לו שלא חזרתי לבדי.", "ابوي اجا وسألني ليش رجعت لحالي، انا قلتله انّه ما رجعت لحالي."), - ("מה שתית כאשר היית אצל דודה שלך (מצד אבא)? שתינו קפה ומים.", "شو شربتي لمّا كنتي عند عمّتك#f؟ شربنا قهوة وميّ."), - ]), - ("read3", "משפטים: ערבית ← עברית ג'", "read", True, [ - ("אתמול בלילה אני חזרתי מהחתונה, שמעתי רעש מהמכונית.", "امبارح ليلة انا رجعت من العرس، سمعت ضجّة من السّيّارة."), - ("אני יצאתי מהמכונית ועליתי לבית שלי.", "انا طلعت من السّيّارة وطلعت على بيتي."), - ("היא ראתה אותו כאשר הוא ישב במשרד ואמרה לו שהיא יצאה מהבית.", "هيّ شافته لمّا هوّ قعد في المكتب وقالتله انّه هيّ طلعت#f من البيت."), - ("מה שתיתם שלשום אצל חבר שלי? אנחנו שתינו תה.", "شو شربتو اوّل امبارح عند صاحبي؟ احنا شربنا شاي."), - ("אנחנו הלכנו אליה והבנו מה היא שאלה אותנו, אחר כך חזרנו לעבודה.", "احنا رحنا لعندها وفهمنا شو سألتنا، بعدين رجعنا على الشّغل."), - ("שמעתי שהיא יצאה מהבית שלה אבל אני לא ראיתי לאן היא הלכה.", "سمعت انّه هيّ طلعت#f من بيتها بسّ انا ما شفتش لوين هيّ راحت."), - ("אנחנו במקור משכונה קטנה בשכם, לפני שבוע חזרנו לשכונה שלנו.", "احنا اصلنا من حارة صغيرة في نابلس، قبل اسبوع رجعنا على حارتنا."), - ("האיש היה פה ואחרי שעה יצא מהמכונית שלו ונכנס לבית הכחול.", "الزّلمة كان هون وبعد ساعة طلع من سيّارته وفات على البيت الازرق."), - ("היא גרה שם, לא רחוק, לכן היא חזרה לבדה.", "هيّ ساكنة هناك، مش بعيد، عشان هيك هيّ رجعت#f لحالها."), - ("כאשר ישבנו אצל השכן שלי ושתינו קפה ביחד אשתו יצאה מהבית.", "لمّا قعدنا عند جاري وشربنا قهوة مع بعض مرته طلعت#f من البيت."), - ("עכשיו אני הבנתי אותך, יקירי, אתה עברת לפה לפני שבוע, אתה חדש פה.", "هلّا انا فهمتك حبيبي، انت نقلت لهون قبل اسبوع، انت جديد هون."), - ("שמעת את הילד? לא, לא שמעתי אותו, למה? כי הוא יצא ולא אמר שום דבר.", "سمعتي الولد؟ لا، ما سمعتهوش، ليش؟ عشان هوّ طلع وما قال اشي."), - ("ראיתם מה שתתה הבת שלו? לא, לא ראינו מה היא שתתה, למה?", "شفتو ايش شربت#f بنته؟ لا، ما شفنا شو هيّ شربت#f، ليش؟"), - ]), - ("colors", "הצבעים", "say", True, [(f"{he}, {he2}", f"{ar}، {ar2}") for he, he2, ar, ar2 in COLORS]), - ("colread", "הצבעים: ערבית ← עברית", "read", True, [ - ("הירח לבן, השמש צהובה, הים כחול והדשא ירוק.", "القمر ابيض، الشّمس صفرا، البحر ازرق والحشيش اخضر."), - ("מחמוד סיפר לי שהוא חזר מהבית הלבן באמריקה.", "محمود حكى لي انّه هوّ رجع من البيت الابيض في امريكا."), - ("את קנית גבינה צהובה בשוק? מה המחיר שלה?", "انتي اشتريتي جبنة صفرا في السّوق؟ قدّيش حقّها؟"), - ("ראיתי שאתה שתית משהו שחור, מה זה היה? אני שתיתי קולה.", "شفت انّه انت شربت اشي اسود، شو هادا كان؟ انا شربت كولا."), - ("מי יצא מהבית עם החלון הירוק? אני יצאתי ממנו.", "مين طلع من البيت مع الشّبّاك الاخضر؟ انا طلعت منّه."), - ("אתם הבנתם שהכלב החום לא אכל את האוכל? כן! הבנו!", "انتو فهمتو انّه الكلب البنّي ما اكل الأكل؟ اه! فهمنا!"), - ("תראי את השמיים בזמן שקיעת השמש! הצבע שלהם סגול.", "شوفي السّما بوقت غروب الشّمس! لونها بنفسجي."), - ("ראיתי שיש לך שולחן אפור בבית שלך, איפה קנית אותו?", "شفت انّه عندك طاولة رماديّة في بيتك، وين اشتريتها؟"), - ("החולצה הראשונה ורודה והשנייה סגולה, היא לא אותה החולצה.", "البلوزة الاولى زهريّة والتّانية بنفسجيّة، هيّ مش نفس البلوزة."), - ("חבר שלי סיפר שהוא לקח ממני מכנסיים אבל לא שמעתי מה הצבע שלהם.", "صاحبي حكى انّه هوّ اخد منّي بنطلون بسّ ما سمعت شو لونه."), - ("אני שמעתי שקנית מכונית בצבע כחול, זאת אומרת יש לך מכונית כחולה.", "انا سمعت انّه اشتريت سيّارة بلون ازرق، يعني عندك سيّارة زرقا."), - ("החדר לבן, אני אוהב את הצבע הלבן, אבל אני רוצה צבע אחר.", "الغرفة بيضا، انا بحبّ اللّون الابيض، بسّ بدّي لون تاني."), - ("כאשר אני יצאתי, החתול השחור נכנס לבית שלי, לכן לא ראיתי אותו.", "لمّا انا طلعت، البسّ الاسود دخل على بيتي، عشان هيك ما شفته."), - ]), - ("story", "שיר הצבעים", "say", True, [ - ("אדום אדום אדום", "احمر احمر احمر"), - ("אני בתפוחים נמצא", "انا في التّفّاح موجود"), - ("אני בתותים נמצא", "انا في التّوتي موجود"), - ("ירוק ירוק ירוק", "اخضر اخضر اخضر"), - ("אני בעשב נמצא", "انا في العشبي موجود"), - ("אני בעצים נמצא", "انا في الشّجري موجود"), - ("צהוב צהוב צהוב", "اصفر اصفر اصفر"), - ("אני בשמש נמצא", "انا في الشّمسي موجود"), - ("אני בבננות נמצא", "انا في الموزي موجود"), - ("כחול כחול כחול", "ازرق ازرق ازرق"), - ("אני בים נמצא", "انا في البحري موجود"), - ("אני בשמיים נמצא", "انا في السّماي موجود"), - ("חום חום חום", "بنّي بنّي بنّي"), - ("אני בעץ נמצא", "انا في الخشبي موجود"), - ("אני בשוקולד נמצא", "انا في الشّوكولات موجود"), - ("צבעים צבעים צבעים צבעים", "الوان الوان الوان الوان"), - ("ורוד ורוד ורוד", "وردي وردي وردي"), - ("אני בפרחים נמצא", "انا في الزّهري موجود"), - ("אני בממתקים נמצא", "انا في الحلوى موجود"), - ("שחור שחור שחור", "اسود اسود اسود"), - ("אני בפחם נמצא", "انا في الفحمي موجود"), - ("אני בלילה נמצא", "انا في اللّيلي موجود"), - ("לבן לבן לבן", "ابيض ابيض ابيض"), - ("אני בביצים נמצא", "انا في البيضي موجود"), - ("אני בשלג נמצא", "انا في التّلجي موجود"), - ]), - ("colsay", "הצבעים: עברית ← ערבית א'", "say", True, [ - ("מה צבע המכונית שלו? צבע המכונית שלו אדום, זאת אומרת יש לו מכונית אדומה.", "شو لون سيّارته؟ لون سيّارته احمر، يعني عنده سيّارة حمرا."), - ("אני גר בבית לבן עם חלון שחור ודלת ירוקה.", "انا ساكن في بيت ابيض مع شبّاك اسود وباب اخضر."), - ("הכיסא לא צהוב ולא חום, הוא כחול, יא גבר.", "الكرسي مش اصفر ومش بنّي، هوّ ازرق يا زلمة."), - ("סלח לי, מה מחיר החולצה? יש בצבע אחר?", "لو سمحت، قدّيش حقّ البلوزة؟ في بلون تاني؟"), - ("בני קנה תפוחים בצבע אדום, אני אוהב תפוחים ירוקים.", "ابني اشترى تفّاح بلون احمر، انا بحبّ تفّاح اخضر."), - ("למה את שמחה? כיוון שבעלי נתן לי פרח ורוד.", "ليش انتي مبسوطة؟ عشان جوزي اعطاني وردة زهريّة."), - ("כמה מיטות יש לך בחדר? שתיים, אחת חומה ואחת שחורה. (לאישה)", "اكم تخت عندك#f في الغرفة؟ تنين، واحد بنّي وواحد اسود.", "כמה מיטות יש לָךְ בחדר? שתיים, אחת חומה ואחת שחורה."), - ("השולחן הגדול והירוק, יש עליו אגרטל כחול.", "الطّاولة الكبيرة والخضرا، في عليها مزهريّة زرقا."), - ("הילד היה עם מכנסיים שחורים וחולצה לבנה.", "الولد كان مع بنطلون اسود وبلوزة بيضا."), - ("סלחי לי, לאיזה בית בשכונה יש חלון ירוק ודלת צהובה?", "لو سمحتي، لايّ بيت في الحارة في شبّاك اخضر وباب اصفر؟"), - ]), - ("colsay2", "הצבעים: עברית ← ערבית ב'", "say", True, [ - ("לפני שבועיים שמעתי את שיר הצבעים עם בני, הוא שמע והיה שמח מאוד.", "قبل اسبوعين سمعت غنوة#of الالوان مع ابني، هوّ سمع وكان مبسوط كتير."), - ("שאלתי את המוכר כמה יצא החשבון ונתתי לו שמונים וחמישה שקלים.", "سألت البيّاع قدّيش طلع الحساب واعطيته خمسة وتمانين شيكل."), - ("השאלה לא הייתה מובנת, לכן הלכתי למורה ושאלתי אותה, עכשיו הבנתי.", "السّؤال ما كان مفهوم، عشان هيك رحت لعند المعلّمة وسألتها، هلّا فهمت."), - ("אשתי חזרה הביתה ושאלה אותי מי קנה את הדלת החומה?", "مرتي رجعت#f على البيت وسألتني#f مين اشترى الباب البنّي؟"), - ("כאשר בת דודי (מצד אמא) יצאה מהבית אחותי חזרה לבית.", "لمّا بنت خالي طلعت#f من البيت اختي رجعت#f على البيت."), - ("מחמוד ראה את החתולה על העץ, אחר כך הוא עלה על העץ ולקח אותה.", "محمود شاف البسّة على الشّجرة، بعدين هوّ طلع على الشّجرة واخدها."), - ("כאשר הם באו לשכונה שלנו שתינו יחד קפה ונתנו להם בקלאווה.", "لمّا همّ اجو على حارتنا شربنا مع بعض قهوة واعطيناهم بقلاوة."), - ("כאשר הם נכנסו למלון, הם שמעו שאביהם יצא לפני בערך שעתיים.", "لمّا همّ دخلو على الاوتيل، همّ سمعو انّه ابوهم طلع قبل حوالي ساعتين."), - ("אתם שאלתם אותי שאלה חשובה ואני הבנתי מדוע השאלה חשובה.", "انتو سألتوني سؤال مهمّ وانا فهمت ليش السّؤال مهمّ."), - ("מדוע אחותך הקטנה שתתה את הקפה? באמת לא הבנתי מדוע. (לאישה)", "ليش اختك#f الصّغيرة شربت#f القهوة؟ والله ما فهمت ليش.", "מדוע אֲחוֹתֵךְ הקטנה שתתה את הקפה? באמת לא הבנתי מדוע."), - ("מי אמר לך שאנחנו לא הבנו? אני הבנתי ואחר כך גם אחי הבין, הכול מובן. (לאישה)", "مين قالّك#f انّه احنا ما فهمنا؟ انا فهمت وبعدين كمان اخوي فهم، كلّه مفهوم.", "מי אמר לָךְ שאנחנו לא הבנו? אני הבנתי ואחר כך גם אחי הבין, הכול מובן."), - ("אני שמעתי אותו אבל הוא לא שמע אותי, לכן אמרתי לו פעמיים.", "انا سمعته بسّ هوّ ما سمعني، عشان هيك قلتله مرّتين."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 9): [ - ("new", "מילים חדשות", "learn", True, [ - ("ליד", "جنب"), ("בלי סוכר", "سادة"), ("חלב", "حليب"), ("שבוע", "جمعة"), ("יום שישי", "الجمعة"), - ("המסגד", "الجامع"), ("האוניברסיטה", "الجامعة"), ("המרפסת", "البلكونة"), ("מלון", "اوتيل"), ("שלם", "كامل"), - ("הדרום", "الجنوب"), ("ראש", "راس"), ("קצרה", "قصيرة"), ("מרק", "شوربة"), ("השבוע הבא", "الاسبوع الجاي"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("story", "סיפור הקפה", "say", True, [ - ("שלום! שמי אשרף, קוראים לי גם אבו כאמל, אני נשוי לאלהאם,", "مرحبا! اسمي اشرف، بيقولولي كمان ابو كامل، انا متجوّز لالهام،"), - ("יש לי שני בנים ושלוש בנות, ה' ישמור אותם!", "عندي ولدين وتلات بنات الله يخلّيهم!"), - ("לפני שבועיים נהגתי במכונית הכחולה שלי, נסעתי לנגב בדרום.", "قبل جمعتين سقت سيّارتي الزّرقا، رحت على النّقب في الجنوب."), - ("ביקרתי את חברי אבו נדאל הבדואי ממשפחת טרבין,", "زرت صاحبي ابو نضال البدوي من دار طرابين،"), - ("הוא גר לא רחוק מערד, זאת אומרת ליד ערד.", "هوّ ساكن مش بعيد عن عراد، يعني جنب عراد."), - ("כאשר הגעתי אליו הוא אמר לי: \"ברוכים הבאים, הארת את המקום!\"", "لمّا اجيت لعنده هوّ قالّي: اهلا وسهلا، نوّرت المحلّ!", "כאשר הגעתי אליו הוא אמר לי: \"ברוכים הבאים, הֵאַרְתָּ את המקום!\""), - ("אני אמרתי לו: \"בנוכחותכם!\"", "انا قلتله: بوجودكم!"), - ("שתינו קפה חלק, זאת אומרת קפה ללא סוכר וללא חלב.", "شربنا قهوة سادة، يعني قهوة بدون سكّر وبدون حليب."), - ("דיברנו על המשפחה ועל העבודה,", "حكينا عن العيلة وعن الشّغل،"), - ("אחרי ששאלתי אותו איפה בנו נדאל נמצא, אני הבנתי ממנו שבנו עבר לאנגליה", "بعد ما سألته وين ابنه نضال موجود، انا فهمت منّه انّه ابنه نقل على انجلترا"), - ("לפני שבוע ביום שישי, הוא הלך לאוניברסיטת אוקספורד, הוא רוצה ללמוד שם.", "قبل جمعة في يوم الجمعة، راح على جامعة#of اوكسفورد، بدّه يتعلّم هناك."), - ("אכלנו ארוחת צהריים ביחד, האוכל היה טעים מאוד.", "تغدّينا مع بعض، الأكل كان كتير طيّب."), - ("לפני שיצאתי מביתו, אמרתי לו תודה והודיתי לו מאוד מאוד.", "قبل ما اطلع من بيته، قلتله تسلم وشكرته كتير كتير."), - ("הוא אמר: \"עם השלום, אשרף!\"", "هوّ قال: مع السّلامة يا اشرف!"), - ("אני אמרתי לו: \"ה' ישמור אותך!\" וחזרתי הביתה.", "انا قلتله: الله يسلّمك! ورجعت على البيت."), - ]), - ("aread", "סיפור הקפה: ערבית ← עברית", "read", True, [ - ("דיברנו על העבודה ועל חבר שלי, אחר כך אח שלי בא וסיפר לנו שהוא בא מרחוק.", "حكينا عن الشّغل وعن صاحبي، بعدين اخوي اجا وحكالنا انّه هوّ اجا من بعيد."), - ("היא סיפרה לי שהיא גרה לא רחוק מתל אביב.", "هيّ حكت لي انّه هيّ ساكنة مش بعيد عن تلّ ابيب."), - ("אחרי שהוא אמר: \"עם השלום, יקירי\", אני אמרתי לו: \"ה' ישמור אותך!\"", "بعد ما هوّ قال: مع السّلامة يا حبيبي، انا قلتله: الله يسلّمك!"), - ("לפני שאכלנו צהריים, אשתו הביאה תה מתוק מאוד ושתינו ביחד.", "قبل ما نتغدّى، مرته جابت شاي حلو كتير وشربنا مع بعض."), - ("עם מי אכלתם צהריים אתמול? עם החברים שלך או עם המשפחה שלך?", "مع مين تغدّيتو امبارح؟ مع اصحابك ولّا مع عيلتك؟"), - ("כאשר בעלי בא לשכונה שלנו אמרנו לו: \"ברוכים הבאים\".", "لمّا جوزي اجا على حارتنا قلناله: اهلا وسهلا."), - ("אנחנו גרים לא רחוק מירושלים, זאת אומרת קרוב לבית לחם.", "احنا ساكنين مش بعيد عن القدس، يعني قريب من بيت لحم."), - ("הוא שאל: \"מה שלומך?\" אני אמרתי: \"ברוך השם, טוב\".", "هوّ سأل: كيف حالك#f؟ انا قلت: الحمد للّه منيحة."), - ("אנחנו ביקרנו את הבן של דוד שלי (מצד אבא), הוא גר בירושלים.", "احنا زرنا ابن عمّي، هوّ ساكن في القدس."), - ("הלכנו לאוניברסיטה העברית בירושלים, היא נמצאת ליד עיסאוויה.", "رحنا على الجامعة العبريّة في القدس، هيّ موجودة جنب العيساويّة."), - ("ביום שישי הייתי אצל דודה שלי (מצד אבא), לפני שחזרתי לבית שלי, שתיתי איתה תה.", "في يوم الجمعة كنت عند عمّتي، قبل ما ارجع على بيتي، شربت معها شاي."), - ("כאשר אני נהגתי במכונית שלי ראיתי את חבר שלי לבדו ולקחתי אותו לבית שלו.", "لمّا انا سقت سيّارتي شفت صاحبي لحاله واخدته على بيته."), - ("לפני שבועיים אנחנו הלכנו לאוניברסיטת חיפה, היא נמצאת על ראש ההר.", "قبل جمعتين احنا رحنا على جامعة#of حيفا، هيّ موجودة على راس الجبل."), - ]), - ("asay", "סיפור הקפה: עברית ← ערבית", "say", True, [ - ("ברוכים הבאים, הארת את המקום! תודה! בנוכחותכם! (לאישה)", "اهلا وسهلا، نوّرتي المحلّ! شكرا! بوجودكم!", "ברוכים הבאים, הֵאַרְתְּ את המקום! תודה! בנוכחותכם!"), - ("אחרי שראיתי את חברתי דיברנו על המשפחה ועל העבודה.", "بعد ما شفت صاحبتي حكينا عن العيلة وعن الشّغل."), - ("לפני שבועיים וחצי ביקרתי את אמי בביתה, גם אחי ביקר אותה.", "قبل اسبوعين ونصّ زرت امّي في بيتها، كمان اخوي زارها."), - ("הילד לא היה אצלי כאשר דיברנו, הוא בא אליי רק אחרי שדיברנו.", "الولد ما كان عندي لمّا حكينا، هوّ اجا لعندي بسّ بعد ما حكينا."), - ("אני ממשפחת טרבין ושם אבי אבו אחמד, זאת אומרת בנו הגדול שמו אחמד.", "انا من دار طرابين واسم ابوي ابو احمد، يعني ابنه الكبير اسمه احمد."), - ("שתינו קפה ללא סוכר וללא חלב, זאת אומרת קפה חלק.", "شربنا قهوة بدون سكّر وبدون حليب، يعني قهوة سادة."), - ("כמו שהבנתי היא חזרה ללונדון, אני אמרתי לה: \"ה' ישמור אותך!\"", "زيّ ما فهمت هيّ رجعت#f على لندن، انا قلتلها: الله يسلّمك#f!", "כמו שהבנתי היא חזרה ללונדון, אני אמרתי לה: \"ה' ישמור אוֹתָךְ!\""), - ("דודתי (מצד אמא) שמעה שהייתי כאן, לכן היא לקחה את בנה ובאה לכאן.", "خالتي سمعت#f انّه كنت هون، عشان هيك هيّ اخدت#f ابنها واجت لهون."), - ("כאשר באתי לבית של סבתי שתיתי תה מתוק מאוד.", "لمّا اجيت على بيت ستّي شربت شاي حلو كتير."), - ("אנחנו גרים לא רחוק מהמסגד, זאת אומרת ליד המסגד הגדול.", "احنا ساكنين مش بعيد عن الجامع، يعني جنب الجامع الكبير."), - ("כאשר משפחת אבו יוסף באה לבית של משפחת חנין הם דיברו על הילדה.", "لمّا عيلة#of ابو يوسف اجت على بيت عيلة#of حنين همّ حكو عن البنت."), - ("ליד הבית שלי ראיתי חמור גדול בצבע לבן, הוא היה לבדו.", "جنب بيتي شفت حمار كبير بلون ابيض، هوّ كان لحاله."), - ]), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_9)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_9)), - ("want", "העתיד עם בִּדّ", "say", False, _wants(WANTS_9)), - ("read", "עבר ועתיד: ערבית ← עברית א'", "read", True, [ - ("אני שתיתי נס קפה, מחר אני רוצה לשתות תה עם סוכר.", "انا شربت نسكافيه، بكرا بدّي اشرب شاي مع سكّر."), - ("אתה חזרת מהחתונה של אח שלי, מחר אתה רוצה לחזור מהחתונה של סלימאן.", "انت رجعت من عرس اخوي، بكرا بدّك ترجع من عرس سليمان."), - ("את יצאת מהבית ואחר כך את רוצה לצאת מהמוסך.", "انتي طلعتي من البيت وبعدين بدّك#f تطلعي من الكراج."), - ("אנחנו שמענו שיר ארוך, מחר אנחנו רוצים לשמוע שיר קצר.", "احنا سمعنا غنوة طويلة، بكرا بدّنا نسمع غنوة قصيرة."), - ("את יצאת מהבית, את רוצה לצאת מבית הספר.", "انتي طلعتي من البيت، انتي بدّك#f تطلعي من المدرسة."), - ("היא חזרה אתמול מהמסיבה של השכנים, מחר היא רוצה לחזור מבית הספר.", "هيّ رجعت#f امبارح من حفلة#of الجيران، بكرا بدّها ترجع من المدرسة."), - ("את הבנת את השאלה? את רוצה להבין את השאלה אחר כך.", "انتي فهمتي السّؤال؟ بدّك#f تفهمي السّؤال بعدين."), - ("אנחנו יצאנו מהחדר, אנחנו רוצים לצאת מהבית.", "احنا طلعنا من الغرفة، بدّنا نطلع من البيت."), - ("הבת שלי דיברה על הבת שלה אתמול, מחר היא רוצה לדבר על הבן שלה.", "بنتي حكت عن بنتها امبارح، بكرا بدّها تحكي عن ابنها."), - ("אחותי קנתה שמלה חדשה, היא רוצה לקנות מכנסיים חדשים.", "اختي اشترت#f فستان جديد، بدّها تشتري بنطلون جديد."), - ("הוא שמע מוזיקה עם חבר שלו, הוא רוצה לשמוע מוזיקה גם עם אמא שלו.", "هوّ سمع موسيقى مع صاحبه، بدّه يسمع موسيقى مع امّه كمان."), - ("למה לא נהגת במכונית היום? את רוצה לנהוג במכונית מחר?", "ليش ما سقتي السّيّارة اليوم؟ بدّك#f تسوقي السّيّارة بكرا؟"), - ("אתה שתית קפה חלק? אתה רוצה לשתות תה מתוק?", "انت شربت قهوة سادة؟ بدّك تشرب شاي حلو؟"), - ]), - ("read2", "עבר ועתיד: ערבית ← עברית ב'", "read", True, [ - ("אני אמרתי לאשתי שאבא שלה בא היום, אני רוצה לומר לאשתי שאני שמח.", "انا قلت لمرتي انّه ابوها اجا اليوم، بدّي اقول لمرتي انّه انا مبسوط."), - ("אנחנו הלכנו הביתה, מחר אנחנו רוצים ללכת לים ביחד.", "احنا رحنا على البيت، بكرا بدّنا نروح على البحر مع بعض."), - ("חבר שלו שאל אותי מי רוצה לצאת מחברון? אני אמרתי לו שהוא רוצה לצאת.", "صاحبه سألني مين بدّه يطلع من الخليل؟ انا قلتله انّه هوّ بدّه يطلع."), - ("אנחנו היינו בבית לפני שעה, מחרתיים אנחנו רוצים להיות בספרד.", "احنا كنّا في البيت قبل ساعة، بعد بكرا بدّنا نكون في اسبانيا."), - ("היא ישבה בסלון, אחר כך היא רוצה לשבת במרפסת ולשמוע מוזיקה.", "هيّ قعدت#f في الصّالون، بعدين بدّها تقعد في البلكونة وتسمع موسيقى."), - ("סמירה שאלה אותי מה אני רוצה לשתות, מחר היא רוצה לשאול אותי מה אני רוצה לאכול.", "سميرة سألتني#f شو بدّي اشرب، بكرا بدّها تسألني شو بدّي آكل."), - ("חבר שלי נכנס למסעדה, הוא רוצה להיכנס ולשאול אותי למה חלון המכונית פתוח.", "صاحبي دخل على المطعم، بدّه يدخل ويسألني ليش شبّاك السّيّارة مفتوح."), - ("אתם אמרתם לי שביקרתם את חבר שלי, אתם רוצים לבקר אותי אחר כך.", "انتو قلتولي انّه زرتو صاحبي، انتو بدّكم تزوروني بعدين."), - ("אבו יוסף ישב על כיסא בסלון הבית שלו, הוא לא רוצה לשבת במרפסת.", "ابو يوسف قعد على كرسي في صالون بيته، ما بدّه يقعد في البلكونة."), - ("היא לקחה מהשכנה שלה סוכר, היא רוצה לקחת את בעלה לבית של דודה שלה (מצד אמא).", "هيّ اخدت#f من جارتها سكّر، بدّها تاخد جوزها على بيت خالتها."), - ("דוד שלי (מצד אבא) יצא מהבית שלו אתמול, מחר הוא רוצה לצאת מהעבודה.", "عمّي طلع من بيته امبارح، بكرا بدّه يطلع من الشّغل."), - ("הלכנו למוסך אתמול, מחר אנחנו רוצים ללכת לחבר שלי מחמוד.", "رحنا على الكراج امبارح، بكرا بدّنا نروح لعند صاحبي محمود."), - ("אשתי בישלה מקלובה לפני שבוע וחצי, מחר היא רוצה לבשל מרק.", "مرتي طبخت#f مقلوبة قبل اسبوع ونصّ، بكرا بدّها تطبخ شوربة."), - ]), - ("say", "עבר ועתיד: עברית ← ערבית", "say", True, [ - ("אני שמעתי שיר חדש, אני רוצה לשמוע את שיר הצבעים, בעזרת ה'.", "انا سمعت غنوة جديدة، بدّي اسمع#i غنوة#of الالوان انشالله."), - ("אתמול נסענו לים, מחר אנחנו רוצים לנסוע עוד פעם.", "امبارح رحنا على البحر، بكرا بدّنا نروح كمان مرّة."), - ("חברי חזר מאירופה, בשבוע הבא הוא רוצה לחזור לאירופה.", "صاحبي رجع من اوروبّا، الاسبوع الجاي بدّه يرجع على اوروبّا."), - ("אני לא שמעתי אותך, אני רוצה לשמוע אותך. (לאישה)", "انا ما سمعتك#f، بدّي اسمعك#f.", "אני לא שמעתי אוֹתָךְ, אני רוצה לשמוע אוֹתָךְ."), - ("אנחנו נכנסנו לחדר, בת דודה שלי (מצד אמא) הגיעה ואמרה שניכנס לסלון.", "احنا دخلنا على الغرفة، بنت خالتي اجت وقالت انّه ندخل على الصّالون."), - ("מחמוד לא ראה את החתולה על העץ, הוא רוצה לראות את החתולה אחר כך.", "محمود ما شاف البسّة على الشّجرة، بدّه يشوف البسّة بعدين."), - ("היא באה לשכונה שלנו, כאשר היא תבוא לבית שלנו נביא לה קפה וכנאפה.", "هيّ اجت على حارتنا، لمّا تيجي على بيتنا نجيبلها قهوة وكنافة."), - ("אשתי הלכה למלון באילת, היא רוצה ללכת למלון בירדן.", "مرتي راحت على اوتيل في ايلات، بدّها تروح على اوتيل في الاردن."), - ("אתם שאלתם את המורה שאלה? אתם רוצים לשאול אותי שאלה חשובה?", "انتو سألتو المعلّمة سؤال؟ بدّكم تسألوني سؤال مهمّ؟"), - ("אמא שלי שתתה תה, עכשיו היא רוצה לשתות קפה ללא סוכר.", "امّي شربت#f شاي، هلّا بدّها تشرب قهوة بدون سكّر."), - ("היא ובעלה עברו לשכונה לפני שנתיים, הם רוצים לעבור לשכונה חדשה.", "هيّ وجوزها نقلو على الحارة قبل سنتين، بدّهم ينقلو على حارة جديدة."), - ("אנחנו אכלנו צהריים בבית אתמול, היום אנחנו רוצים לאכול צהריים במסעדה.", "احنا تغدّينا في البيت امبارح، اليوم بدّنا نتغدّى في المطعم."), - ]), - ("mix", "עוד תרגילים: עברית ← ערבית", "say", True, [ - ("שתינו קפה לפני שבועיים ליד השולחן הגדול והשחור, מחר אנחנו רוצים לשתות עוד פעם.", "شربنا قهوة قبل اسبوعين جنب الطّاولة الكبيرة والسّودا، بكرا بدّنا نشرب كمان مرّة."), - ("כאשר אכלנו ארוחת צהריים יחד לא סיפרתי לך על חברי, מחר אני רוצה לספר לך. (לאישה)", "لمّا تغدّينا مع بعض ما حكيتلك#f عن صاحبي، بكرا بدّي احكيلك#f.", "כאשר אכלנו ארוחת צהריים יחד לא סיפרתי לָךְ על חברי, מחר אני רוצה לספר לָךְ."), - ("כאשר הוא בא לבקר אותי, לא ראיתי אותו, אמרתי לו שיבוא מחר.", "لمّا هوّ اجا يزورني، ما شفته، قلتله انّه يجي بكرا."), - ("אבי שאל אותי האם אני נמצאת בבית? מחר הוא גם רוצה לשאול אותי את אותה השאלה.", "ابوي سألني اذا انا موجودة في البيت؟ بكرا هوّ كمان بدّه يسألني نفس السّؤال."), - ("סלח לי סמיר, אתה לא הבנת? אם אתה רוצה להבין תשאל את אביך.", "لو سمحت يا سمير، انت ما فهمت؟ اذا بدّك تفهم اسأل ابوك.", "סלח לי סמיר, אתה לא הֵבַנְתָּ? אם אתה רוצה להבין תשאל את אָבִיךָ."), - ("דודי (מצד אמא) קנה מיטה גדולה, הוא רוצה לקנות גם שישה כיסאות ושולחן חום.", "خالي اشترى تخت كبير، بدّه يشتري كمان ستّ كراسي وطاولة بنّيّة."), - ]), - ("mixread", "עוד תרגילים: ערבית ← עברית", "read", True, [ - ("אני הלכתי לבית לחם, מחר אני רוצה ללכת לדודה שלי (מצד אמא), היא גרה לבדה.", "انا رحت على بيت لحم، بكرا بدّي اروح لعند خالتي، هيّ ساكنة لحالها."), - ("אתם ביקרתם אותי בבית החולים לפני שבוע או לא? אתם רוצים לבקר אותי מחר?", "انتو زرتوني في المستشفى قبل جمعة ولّا لا؟ بدّكم تزوروني بكرا؟"), - ("אתם שתיתם קפה או תה? אתם רוצים לשתות קפה חלק? זאת אומרת קפה ללא סוכר.", "انتو شربتو قهوة ولّا شاي؟ بدّكم تشربو قهوة سادة؟ يعني قهوة بدون سكّر."), - ("יצאנו מהבית וביקרנו את סבא שלי. לפני שנצא מהבית שלו נדבר איתך.", "طلعنا من البيت وزرنا جدّي. قبل ما نطلع من بيته نحكي معك."), - ("את לא היית בעבודה היום, את רוצה להיות בעבודה מחרתיים.", "انتي ما كنتيش في الشّغل اليوم، بدّك#f تكوني في الشّغل بعد بكرا."), - ("כאשר ראינו את ההורים שלי, אכלנו איתם צהריים, מחר אנחנו רוצים לראות את דוד שלי (מצד אבא) ולאכול איתו צהריים.", "لمّا شفنا اهلي، تغدّينا معهم، بكرا بدّنا نشوف عمّي ونتغدّى معه."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (1, 10): [ - ("new", "מילים חדשות", "learn", True, [ - ("העולם, האנשים", "العالم"), ("בצל", "بصل"), ("כמובן", "طبعا"), ("זיתים", "زيتون"), ("המחברת", "الدّفتر"), - ("עכשיו", "هسّا"), ("כדורגל", "فوتبول"), ("תיק", "شنطة"), ("רבע", "ربع"), ("רעיון", "فكرة"), - ("החיים", "الحياة"), ("חוף", "شطّ"), ("רכבת", "ترين"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("adj", "ערך היתרון", "say", True, [(f"{base}, יותר {base}{sep}{note}", f"{ar}، {comp}") for he, ar, comp in COMPARATIVES - for base, sep, note in [he.partition(" (")]]), - ("compread", "ערך היתרון וההפלגה: ערבית ← עברית א'", "read", True, [ - ("הבת שלך חרוצה, יותר חרוצה מהבת שלי, הבת הכי חרוצה בבית הספר, היא החרוצה ביותר.", "بنتك شاطرة، اشطر من بنتي، اشطر بنت في المدرسة، هيّ الاشطر."), - ("המורה גבוה, יותר גבוה מאבא שלי, הוא הכי גבוה בשכונה, הוא הגבוה ביותר.", "المعلّم طويل، اطول من ابوي، هوّ اطول واحد في الحارة، هوّ الاطول."), - ("החולצה יפה, היא יותר יפה מהחולצה שלי, החולצה הכי יפה בבית, היא היפה ביותר.", "البلوزة حلوة، هيّ احلى من بلوزتي، احلى بلوزة في البيت، هيّ الاحلى."), - ("אבא שלי חזק, אבא שלי יותר חזק מדוד שלי (מצד אבא), אבא שלי האיש הכי חזק בעולם, אבא שלי הוא החזק ביותר.", "ابوي قوي، ابوي اقوى من عمّي، ابوي اقوى زلمة في العالم، ابوي هوّ الاقوى."), - ("אחותי יפה, אחותי יותר יפה ממני, היא הבת הכי יפה במשפחה, היא היפה ביותר.", "اختي جميلة، اختي اجمل منّي، هيّ اجمل بنت في العيلة، هيّ الاجمل."), - ("אמא שלי חשובה, היא יותר חשובה מהשכנה שלי, היא הכי חשובה בעולם, היא החשובה ביותר.", "امّي مهمّة، هيّ اهمّ من جارتي، هيّ اهمّ واحدة في العالم، هيّ الاهمّ."), - ("אשתי יפה, יותר יפה מאחותה, אשתי היא הכי יפה בעולם, היא היפה ביותר.", "مرتي حلوة، احلى من اختها، مرتي هيّ احلى واحدة في العالم، هيّ الاحلى."), - ("המסעדה נקייה, יותר נקייה מהמסעדה של דוד שלי (מצד אמא), המסעדה הכי נקייה בשכונה, היא הנקייה ביותר.", "المطعم نضيف، انضف من مطعم خالي، انضف مطعم في الحارة، هوّ الانضف."), - ]), - ("compread2", "ערך היתרון וההפלגה: ערבית ← עברית ב'", "read", True, [ - ("הכלב שלי מכוער, הוא יותר מכוער מהכלב של השכן שלי, הוא הכלב הכי מכוער בכפר, הוא המכוער ביותר.", "كلبي بشع، هوّ ابشع من كلب جاري، هوّ ابشع كلب في البلد، هوّ الابشع."), - ("הרכבת הקלה בירושלים ארוכה, היא יותר ארוכה מהאוטובוס, היא הרכבת הכי ארוכה בירושלים, היא הארוכה ביותר.", "التّرين الخفيف في القدس طويل، هوّ اطول من الباص، هوّ اطول ترين في القدس، هوّ الاطول."), - ("דוד שלי (מצד אבא) יקר לי, הוא יותר יקר מאשתו, הוא האיש הכי יקר בכפר, הוא היקר ביותר.", "عمّي غالي عليّ، هوّ اغلى من مرته، هوّ اغلى زلمة في البلد، هوّ الاغلى."), - ("השיר ארוך, יותר ארוך מהשיר \"באג'י וינכ\", הוא השיר הכי ארוך ששמעתי בחיים שלי, הוא הארוך ביותר.", "الغنوة طويلة، اطول من الغنوة باجي وينك، هيّ اطول غنوة الّي سمعتها في حياتي، هيّ الاطول."), - ("אני הייתי שמן, יותר שמן ממחמוד, הכי שמן בבית הספר, הייתי השמן ביותר.", "انا كنت ناصح، انصح من محمود، انصح واحد في المدرسة، كنت الانصح."), - ("השולחן כבד מאוד, יותר כבד משישה עשר כיסאות, השולחן הכי כבד בכפר, הוא הכבד ביותר.", "الطّاولة تقيلة كتير، اتقل من ستّعشر كرسي، اتقل طاولة في البلد، هيّ الاتقل."), - ("הבית שלך רחוק, יותר רחוק מהבית שלי, אבל לא הבית הכי רחוק בשכונה, הבית שלך לא הרחוק ביותר.", "بيتك بعيد، ابعد من بيتي، بسّ مش ابعد بيت في الحارة، بيتك مش الابعد."), - ("השמלה זולה, יותר זולה מהחולצה בשלושים ושמונה שקלים, היא השמלה הכי זולה בחנות, היא הזולה ביותר.", "الفستان رخيص، ارخص من البلوزة بتمانية وتلاتين شيكل، هوّ ارخص فستان في المحلّ، هوّ الارخص."), - ]), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_10)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_10)), - ("read", "עבר ועתיד: ערבית ← עברית א'", "read", True, [ - ("הילד שיחק עם הילדה, מחר הוא רוצה לשחק עם חבר שלו.", "الولد لعب مع البنت، بكرا بدّه يلعب مع صاحبه."), - ("כאשר ראית אותי במסיבה את צחקת, מחר את רוצה לצחוק עוד ועוד, בעזרת ה'.", "لمّا شفتيني في الحفلة انتي ضحكتي، بكرا بدّك#f تضحكي كمان كمان انشالله."), - ("אני לא לבשתי את השמלה הכחולה, אני רוצה ללבוש אותה מחרתיים למסיבה.", "انا ما لبست الفستان الازرق، بدّي البسه بعد بكرا للحفلة."), - ("אבא שלי הגיע הביתה בשעה חמש וחצי, מחר הוא רוצה להגיע בשלוש וחצי.", "ابوي وصل على البيت على ساعة خمسة ونصّ، بكرا بدّه يوصل على تلاتة ونصّ."), - ("מאיפה הגעת אתמול בלילה? את רוצה להגיע לחתונה של חברה שלי מחר?", "من وين وصلتي امبارح ليلة؟ بدّك#f توصلي على عرس صاحبتي بكرا؟"), - ("אנחנו שיחקנו בבית הספר, ביום שישי אנחנו רוצים לשחק בבית של סבתא שלי.", "احنا لعبنا في المدرسة، في يوم الجمعة بدّنا نلعب في بيت جدّتي."), - ("את לבשת את החולצה לבדך? את רוצה ללבוש אותה לבדך? כן! כל יום לבדי.", "انتي لبستي البلوزة لحالك#f؟ بدّك#f تلبسيها لحالك#f؟ اه! كلّ يوم لحالي."), - ("הילדה צחקה עם המורה, מחר היא לא רוצה לצחוק איתה.", "البنت ضحكت#f مع المعلّمة، بكرا ما بدّها تضحك معها."), - ("אנחנו שיחקנו שש בש, בעוד יומיים אנחנו רוצים לשחק כדורגל.", "احنا لعبنا طاولة، كمان يومين بدّنا نلعب فوتبول."), - ("דודה שלי (מצד אבא) יצאה מהבית שלה, בעוד שעה היא רוצה לצאת מהמכונית.", "عمّتي طلعت#f من بيتها، كمان ساعة بدّها تطلع من السّيّارة."), - ("אנחנו לא צחקנו עליך, אנחנו לא רוצים לצחוק עליך בכלל.", "احنا ما ضحكنا عليك، احنا ما بدّنا نضحك عليك بالمرّة."), - ("אתה לבשת חולצה שחורה ומכנסיים כחולים? מה אתה רוצה ללבוש מחר?", "انت لبست بلوزة سودا وبنطلون ازرق؟ شو بدّك تلبس بكرا؟"), - ]), - ("read2", "עבר ועתיד: ערבית ← עברית ב'", "read", True, [ - ("אנחנו לא היינו אתמול בבית הספר, אנחנו רוצים להיות בבית בעוד חצי שעה.", "احنا ما كنّا امبارح في المدرسة، بدّنا نكون في البيت كمان نصّ ساعة."), - ("אמא שלי נכנסה לחדר שלי ושאלה אותי מתי אחמד הגיע, אמרתי לה שהוא רוצה להגיע מחר, לא היום.", "امّي دخلت#f على غرفتي وسألتني#f ايمتى احمد وصل، قلتلها انّه بدّه يوصل بكرا، مش اليوم."), - ("היא צחקה כאשר הבינה שפאטמה היא אחותי, למה למה היא רוצה לצחוק עליי?", "هيّ ضحكت#f لمّا فهمت#f انّه فاطمة هيّ اختي، ليش ليش بدّها تضحك عليّ؟"), - ("מה שתיתם אתמול אחרי שביקרתם את חמודי? אתם רוצים לשתות איתי עכשיו?", "شو شربتو امبارح بعد ما زرتو حمّودي؟ بدّكم تشربو معي هسّا؟"), - ("המורה הגיעה היום בשעה עשר, מחר היא לא רוצה להגיע בכלל.", "المعلّمة وصلت#f اليوم على ساعة عشرة، بكرا ما بدّها توصل بالمرّة."), - ("אחרי שנתתי לך כסף, אמרת לי שאתה רוצה עוד אחד עשר שקלים, אני רוצה לתת לך לפני שנצא מכאן, בסדר?", "بعد ما اعطيتك مصاري، قلتلي انّه بدّك كمان حدعشر شيكل، بدّي اعطيك قبل ما نطلع من هون، تمام؟"), - ("אנחנו הודינו למורה ואנחנו רוצים להודות לו כל יום.", "احنا شكرنا المعلّم وبدّنا نشكره كلّ يوم."), - ("איך יצאת מהבית ולא ביקרת את סבא שלך? את רוצה לבקר את סבא שלך מחר?", "كيف طلعتي من البيت وما زرتي سيدك#f؟ بدّك#f تزوري سيدك#f بكرا؟"), - ("הלכתי לבית של דוד שלי (מצד אמא) וראיתי שהבית לא נקי, לפני שבועיים הוא היה הבית הכי נקי בשכונה, אני רוצה לראות אותו היום.", "رحت على بيت خالي وشفت انّه البيت مش نضيف، قبل جمعتين هوّ كان انضف بيت في الحارة، بدّي اشوفه اليوم."), - ]), - ("say", "עבר ועתיד: עברית ← ערבית א'", "say", True, [ - ("עם מי שיחקת אתמול? אתה רוצה לשחק איתי מחר?", "مع مين لعبت امبارح؟ بدّك تلعب معي بكرا؟", "עם מי שִׂיחַקְתָּ אתמול? אתה רוצה לשחק איתי מחר?"), - ("אתם צחקתם כאשר ראיתם אותי, אתם רוצים לצחוק גם כאשר תראו את אחי?", "انتو ضحكتو لمّا شفتوني، بدّكم تضحكو كمان لمّا تشوفو اخوي؟"), - ("את קנית את השמלה הירוקה אתמול, מה תקני מחר?", "انتي اشتريتي الفستان الاخضر امبارح، شو تشتري بكرا؟", "אַתְּ קָנִית את השמלה הירוקה אתמול, מה תקני מחר?"), - ("כאשר הגענו לשוק ראינו את מחמוד, אנחנו רוצים להגיע לביתו אחרי שנקנה פירות.", "لمّا وصلنا على السّوق شفنا محمود، بدّنا نوصل على بيته بعد ما نشتري فواكه."), - ("את שיחקת עם חברתך אתמול? את רוצה לשחק עם אחותך היום?", "انتي لعبتي مع صاحبتك#f امبارح؟ بدّك#f تلعبي مع اختك#f اليوم؟", "אַתְּ שִׂיחַקְתְּ עם חֲבֶרְתֵּךְ אתמול? אַתְּ רוצה לשחק עם אֲחוֹתֵךְ היום?"), - ("אנחנו יצאנו מהבית ואנחנו רוצים לעלות לבית חברתי.", "احنا طلعنا من البيت وبدّنا نطلع على بيت صاحبتي."), - ("ראיתי את בן דודי (מצד אמא) לבדו, מחר אני אראה אותו עם אימו.", "شفت ابن خالي لحاله، بكرا اشوفه مع امّه."), - ("המורה ירדה מבית הספר, היא רוצה לרדת מהמכונית אחרי שתגיע לביתה.", "المعلّمة نزلت#f من المدرسة، بدّها تنزل من السّيّارة بعد ما توصل على بيتها.", "המורָה ירדה מבית הספר, היא רוצה לרדת מהמכונית אחרי שתגיע לביתהּ."), - ("אתם חזרתם מבית חברי? אתם רוצים לחזור לבית היום או מחר?", "انتو رجعتو من بيت صاحبي؟ بدّكم ترجعو على البيت اليوم ولّا بكرا؟"), - ("בני הבכור חייך כאשר ראה אותי, הוא יצחק גם מחר כאשר יראה אותי.", "ابني البكر ضحك لمّا شافني، هوّ يضحك كمان بكرا لمّا يشوفني."), - ("איך הגעת לכאן? את רוצה להגיע לכאן בכל יום שישי? (לאישה)", "كيف وصلتي لهون؟ بدّك#f توصلي لهون كلّ يوم جمعة؟", "איך הִגַּעְתְּ לכאן? אַתְּ רוצה להגיע לכאן בכל יום שישי?"), - ("אני לא הבנתי את המורה, אני רוצה להבין אותו מחר.", "انا ما فهمت المعلّم، بدّي افهمه بكرا.", "אני לא הבנתי את המורֶה, אני רוצה להבין אותו מחר."), - ]), - ("say2", "עבר ועתיד: עברית ← ערבית ב'", "say", True, [ - ("מדוע לא לבשתם את חולצת בית הספר? אתם חרוצים, אתם תלבשו אותה מחר, כמו כל יום!", "ليش ما لبستو بلوزة#of المدرسة؟ انتو شاطرين، انتو تلبسوها بكرا، زيّ كلّ يوم!"), - ("אני סיפרתי לך על אחותי, אני רוצה לספר לך משהו אחר. (לאישה)", "انا حكيتلك#f عن اختي، بدّي احكيلك#f اشي تاني.", "אני סיפרתי לָךְ על אחותי, אני רוצה לספר לָךְ משהו אחר."), - ("חברי שאל האם לבשתי את השמלה הסגולה ואם אני רוצה ללבוש אותה למסיבה.", "صاحبي سأل اذا لبست الفستان البنفسجي واذا بدّي البسه للحفلة."), - ("אתם ירדתם מהבית? מתי אתם רוצים לרדת?", "انتو نزلتو من البيت؟ ايمتى بدّكم تنزلو؟"), - ("חברתי הגיעה היום לשוק, היא רוצה להגיע גם מחר לאותו המקום.", "صاحبتي وصلت#f اليوم على السّوق، بدّها توصل كمان بكرا على نفس المحلّ."), - ("אחרי שנתתי לחברי שלושים ושמונה שקלים, אמרתי לו שמחר אתן לשכני עוד חמישה שקלים.", "بعد ما اعطيت صاحبي تمانية وتلاتين شيكل، قلتله انّه بكرا اعطي جاري كمان خمس شواكل."), - ("דודתי (מצד אמא) אכלה איתי צהריים לפני שבוע, היא אמרה לי שהיא רוצה לאכול איתי צהריים גם היום.", "خالتي تغدّت معي قبل اسبوع، هيّ قالتلي انّه بدّها تتغدّى معي كمان اليوم."), - ("איך לא שיחקתם שש בש בשכונה? אתם רוצים לשחק כאן מחר?", "كيف ما لعبتو طاولة في الحارة؟ بدّكم تلعبو هون بكرا؟"), - ("אכלנו ארוחת צהריים ביחד, האוכל היה טעים, הכי טעים בעולם! גם מחר אנחנו רוצים לאכול ביחד באותה המסעדה.", "تغدّينا مع بعض، الأكل كان طيّب، اطيب اكل#n في العالم! كمان بكرا بدّنا ناكل مع بعض في نفس المطعم."), - ]), - ("dem", "כינויי הרמז", "say", True, [ - ("זה", "هادا"), ("זאת", "هادي"), ("אלה", "هدول"), ("ההוא", "هداك"), ("ההיא", "هديك"), ("ההם", "هدولاك"), - ("המתנה הזאת", "هادي الهديّة"), ("הפעם הזאת", "هادي المرّة"), ("הגברים האלה", "هدول الزّلام"), - ("סלח לי, אני רוצה את התיק הזה, יש עוד אחד? אל תדאג, יש הכול!", "لو سمحت، بدّي الشّنطة هادي، في كمان واحدة؟ ولا يهمّك، في كلّ اشي!"), - ("מי אלה? אלה הילדים של השכן שלך? כן, כמובן!", "مين هدول؟ هدول اولاد جارك؟ اه، طبعا!", "מי אלה? אלה הילדים של השכן שֶׁלְּךָ? כן, כמובן!"), - ("אולי ההוא יותר זול.", "يمكن هداك ارخص."), - ]), - ("story", "סיפור דודי סעיד", "say", True, [ - ("לפני בערך שנתיים, כאשר הייתי בת אחת עשרה, זה היה ביום שישי.", "قبل حوالي سنتين، لمّا عمري كان حدعشر سنة، هادا كان في يوم الجمعة."), - ("דיברה איתי חברתי רובה ואמרה לי שהיא רוצה לשחק איתי בחוץ.", "حكت معي صاحبتي روبا وقالتلي انّه بدّها تلعب معي برّا."), - ("יצאתי ושיחקנו ביחד, אחרי בערך רבע שעה אני ראיתי מרחוק מכונית צהובה,", "طلعت ولعبنا مع بعض، بعد حوالي ربع ساعة انا شفت من بعيد سيّارة صفرا،"), - ("ירד ממנה איש מבוגר, הוא לקח איתו תיק שחור ואמר לי: \"נור, בואי יקירתי!\"", "نزل منها زلمة كبير، هوّ اخد معه شنطة سودا وقالّي: يا نور، تعالي حبيبتي!"), - ("אני הייתי שמחה לראות שהוא דודי סעיד, אח של אמי.", "انا كنت مبسوطة اشوف انّه هوّ خالي سعيد، اخو امّي."), - ("הוא נישק אותי ואני ראיתי שהוא בא לבדו, ושאלתי אותו:", "هوّ باسني وانا شفت انّه هوّ اجا لحاله، وسألته:"), - ("\"דודי סעיד, איפה בנך יוסף? ואיפה כל המשפחה? מדוע באת לבדך?\"", "يا خالي سعيد، وين ابنك يوسف؟ ووين كلّ العيلة؟ ليش اجيت لحالك؟", "\"דודי סעיד, איפה בִּנְךָ יוסף? ואיפה כל המשפחה? מדוע בָּאתָ לְבַדְּךָ?\""), - ("הוא אמר לי: \"אל תדאגי, הם רוצים להגיע לכאן מחר, בעזרת ה',", "هوّ قالّي: ولا يهمّك#f، بدّهم يوصلو لهون بكرا انشالله،"), - ("קדימה, בואי נלך הביתה, אני רוצה לראות את הורייך ואת אחייך\", ועלינו הביתה.", "يلّا تعالي نروح على البيت، بدّي اشوف اهلك#f واخوتك#f، وطلعنا على البيت.", "קדימה, בואי נלך הביתה, אני רוצה לראות את הוֹרַיִךְ ואת אַחַיִךְ\", ועלינו הביתה."), - ("כאשר אמי ראתה אותו היא מיד צחקה ונישקה אותו ואמרה לו: \"ברוכים הבאים, הארת את הבית!\", הוא אמר: \"בנוכחותכם!\"", "لمّا امّي شافته على طول ضحكت#f وباسته وقالتله: اهلا وسهلا، نوّرت البيت! هوّ قال: بوجودكم!", "כאשר אמי ראתה אותו היא מיד צחקה ונישקה אותו ואמרה לו: \"ברוכים הבאים, הֵאַרְתָּ את הבית!\", הוא אמר: \"בנוכחותכם!\""), - ("הם ישבו ושתו קפה חלק ודיברו ביחד, אמי אמרה לו שהיא בישלה את המקלובה הכי טעימה, כמו שהוא אוהב,", "همّ قعدو وشربو قهوة سادة وحكو مع بعض، امّي قالتله انّه هيّ طبخت#f اطيب مقلوبة، زيّ ما هوّ بيحبّ،"), - ("הוא אמר: \"יבורכו ידייך, אום בסאם!\"", "هوّ قال: يسلمو ايديكي يا امّ بسّام!", "הוא אמר: \"יבורכו יָדַיִךְ, אוּם בסאם!\""), - ("אחר כך דודי סעיד נתן לאחיי בסאם ועאדל מתנה, פלייסטיישן חדש, הם הודו לדודי והלכו לשחק.", "بعدين خالي سعيد اعطى اخوتي بسّام وعادل هديّة، بليستيشن جديد، همّ شكرو خالي وراحو يلعبو.", "אחר כך דודי סעיד נתן לְאַחַי בסאם ועאדל מתנה, פלייסטיישן חדש, הם הודו לדודי והלכו לשחק."), - ("שאלתי אותו: \"דודי סעיד, אולי קנית גם לי מתנה?\"", "سألته: يا خالي سعيد، يمكن اشتريتلي كمان هديّة؟", "שאלתי אותו: \"דודי סעיד, אולי קָנִיתָ גם לי מתנה?\""), - ("הוא אמר: \"כן! כמובן! כיוון שאת הבכורה קניתי לך שתי שמלות, אחת ורודה ואחת לבנה!\"", "هوّ قال: اه! طبعا! عشان انتي البكرة اشتريتلك#f فستانين، واحد زهري وواحد ابيض!", "הוא אמר: \"כן! כמובן! כיוון שאַתְּ הבכורה קניתי לָךְ שתי שמלות, אחת ורודה ואחת לבנה!\""), - ("ונתן לי את המתנה ואני הודיתי לו מאוד.", "واعطاني الهديّة وانا شكرته كتير."), - ("אחרי שעה וחצי אבי חזר מהעבודה ואכלנו צהריים ביחד, כל המשפחה.", "بعد ساعة ونصّ ابوي رجع من الشّغل وتغدّينا مع بعض كلّ العيلة."), - ("למחרת הגיעה משפחת דודי סעיד, ואני ירדתי מהבית כדי לראות את בן דודי יוסף,", "تاني يوم وصلت#f عيلة#of خالي سعيد، وانا نزلت من البيت عشان اشوف ابن خالي يوسف،"), - ("דרך אגב, יוסף בן שלוש עשרה ואני גם בת שלוש עשרה, כמו שסיפרתי לכם קודם, והלכנו למסעדת פיצה.", "على فكرة، يوسف عمره تلاتعشر سنة وانا كمان عمري تلاتعش، زيّ ما حكيتلكم من قبل، ورحنا على مطعم بيتسا."), - ("המוכר שאל אותי האם אני רוצה עם בצל או עם זיתים כמו שיוסף לקח,", "البيّاع سألني اذا بدّي مع بصل ولّا مع زيتون زيّ ما يوسف اخد،"), - ("אמרתי לו שאני לא אוהבת בצל בכלל ואני רוצה ללא בצל וללא זיתים.", "قلتله انّه انا ما بحبّ بصل بالمرّة وبدّي بدون بصل وبدون زيتون."), - ("אכלנו ודיברנו על החיים, אחרי שסיימנו לאכול יצאנו מהמסעדה והלכנו לחוף הים.", "اكلنا وحكينا عن الحياة، بعد ما خلّصنا ناكل طلعنا من المطعم ورحنا على شطّ البحر."), - ("שמעתי שהם רוצים לחזור לבלגיה בעוד שבוע, לא הבנתי מדוע כך,", "سمعت انّه همّ بدّهم يرجعو على بلجيكا كمان اسبوع، ما فهمت ليش هيك،"), - ("אבל לא חשוב, מה שחשוב שאנחנו ביחד עכשיו.", "بسّ مش مهمّ، المهمّ انّه احنا مع بعض هلقيت."), - ("אחרי שבוע הם חזרו, ובעזרת ה' אראה אותם בעוד חצי שנה.", "بعد اسبوع همّ رجعو وانشالله اشوفهم كمان نصّ سنة."), - ]), - ("phrases", "משפטים שימושיים", "say", True, [ - ("אני מבין אותך! (לגבר)", "انا فاهم عليك!", "אני מבין אוֹתְךָ!"), - ("אני לא מבין, מה זאת אומרת?", "انا مش فاهم، شو يعني؟"), - ("האם תוכל להסביר לי קצת?", "ممكن تفهّمني شويّ؟"), - ("עוד פעם, בחייך! (לגבר)", "كمان مرّة وحياتك!", "עוד פעם, בְּחַיֶּיךָ!"), - ("אני לומד ערבית.", "انا بتعلّم عربي."), - ("זה, כמה הוא עולה?", "هادا، قدّيش حقّه؟"), - ("באמת יקר לי, אולי תבוא לקראתי קצת?", "والله غالي عليّ، ممكن تراعيني شويّ؟"), - ("אני רוצה קפה ללא סוכר.", "انا بدّي قهوة بدون سكّر."), - ("איפה את גרה?", "وين انتي ساكنة؟"), - ("אנחנו גרים בירושלים.", "احنا ساكنين في القدس."), - ("מאיפה אתה במקור?", "من وين انت بالاصل؟"), - ("אני במקור מבית לחם, אבל היום אני גר בחברון.", "اصلي من بيت لحم، بسّ اليوم انا ساكن في الخليل."), - ("יאריך ימיך! (לגבר)", "يطوّل عمرك!", "יאריך יָמֶיךָ!"), - ("אתה נשוי או רווק?", "انت متجوّز ولّا عزّابي؟"), - ("אני לא נשוי, זאת אומרת רווק.", "انا مش متجوّز، يعني عزّابي."), - ("אני נשוי ויש לי בן ובת, ברוך השם.", "انا متجوّز وعندي ولد وبنت الحمد للّه."), - ("ה' ישמור אותם!", "الله يخلّيهم!"), - ("של מי המחברת? המחברת שלי.", "لمين الدّفتر؟ الدّفتر الي."), - ("המעיל לא שלך. (לאישה)", "الجاكيت مش الك#f.", "המעיל לא שֶׁלָּךְ."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 1): [ # the second workbook's first lesson, which it numbers the tenth - ("new", "מילים חדשות", "learn", True, [ - ("מוקדם", "بكّير"), ("המכולת", "البقالة"), ("בעל המכולת", "البقّال"), ("בוקר", "صباح"), ("בבוקר", "الصّبح"), - ("הקניון", "المول"), ("ספר", "كتاب"), ("לב", "قلب"), ("בערך", "تقريبا"), ("אורחים", "ضيوف"), - ("בסדר, מוכן", "حاضر"), ("קישואים", "كوسا"), ("חצילים", "بيتنجان"), ("ממולא", "محشي"), ("רגע", "لحظة"), - ("רשימה", "لستة"), ("שקית", "كيس"), ("פלפל", "فلفل"), ("שוקולד", "شوكولاطة"), ("כסף קטן", "فراطة"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("verbs", "פעלים", "say", True, _base_forms(VERBS_2_1)), - ("past", "הטיות בעבר", "say", False, _past(VERBS_2_1)), - ("must", "העתיד עם לַאזֵם", "say", False, _before_future("لازم", HE_MUST, MUST_2_1)), - ("will", "העתיד עם רַח", "say", False, _before_future("رح", HE_GOING, WILL_2_1)), - ("read", "רַח ולַאזֵם: ערבית ← עברית א'", "read", True, [ - ("הילד ביקר אותי אתמול, מחר הוא הולך לבקר את סבתא שלו.", "الولد زارني امبارح، بكرا هوّ رح يزور جدّته."), - ("היא שלחה את הבת שלה לדודה שלה (מצד אמא), מחר היא הולכת לשלוח אותי לקניון.", "هيّ بعثت#f بنتها لعند خالتها، بكرا هيّ رح تبعثني على المول."), - ("אנחנו שילמנו אלפיים מאתיים שבעים ושלושה שקלים, אנחנו לא צריכים לשלם עוד.", "احنا دفعنا الفين ميتين تلاتة وسبعين شيكل، احنا مش لازم ندفع كمان."), - ("החברה שלי הייתה פה לפני שעה וחצי, היא הולכת להיות בבית בעוד רבע שעה.", "صاحبتي كانت هون قبل ساعة ونصّ، هيّ رح تكون في البيت كمان ربع ساعة."), - ("כאשר אני רצתי אני לא ראיתי אותך, אני צריך לראות אותך היום.", "لمّا انا ركضت انا ما شفتك#f، انا لازم اشوفك#f اليوم."), - ("אנחנו ישבנו במסעדה, אחר כך אנחנו הולכים לשבת בגן.", "احنا قعدنا في المطعم، بعدين احنا رح نقعد في البستان."), - ("בעלי שאל אותי אם בא לי לשתות קפה עם דודה שלו (מצד אמא), אני צריכה לשתות איתה.", "جوزي سألني اذا جاي على بالي اشرب قهوة مع خالته، انا لازم اشرب معها."), - ("המוכר נתן לי חולצה כחולה ואמר שמחרתיים הוא הולך לתת לי חולצה לבנה.", "البيّاع اعطاني بلوزة زرقا وقال انّه بعد بكرا هوّ رح يعطيني بلوزة بيضا."), - ("השיח' חזר מהמסגד בשעה ארבע ורבע, הוא הולך לחזור היום בחמש.", "الشّيخ رجع من الجامع على ساعة اربع وربع، هوّ رح يرجع اليوم على خمسة."), - ("אתמול לא לקחנו את המכונית מהמוסך, אנחנו צריכים לקחת אותה היום.", "امبارح ما اخدنا السّيّارة من الكراج، احنا لازم ناخدها اليوم."), - ("את שברת את השולחן, שימי לב! את הולכת לשבור גם את הכיסא.", "انتي كسرتي الطّاولة، ديري بالك#f! انتي رح تكسري الكرسي كمان."), - ("אתם לא יצאתם מהעבודה בשעה שתים עשרה? קדימה, אתם צריכים לצאת.", "انتو ما طلعتو من الشّغل على ساعة تنعش؟ يلّا انتو لازم تطلعو."), - ]), - ("read2", "רַח ולַאזֵם: ערבית ← עברית ב'", "read", True, [ - ("אני נהגתי במכונית הכי יפה בשכונה, אני הולך לנהוג בה גם מחר.", "انا سقت احلى سيّارة في الحارة، انا رح اسوقها كمان بكرا."), - ("למה לא היית בבית הספר? את צריכה להיות בעבודה היום.", "ليش ما كنتي في المدرسة؟ انتي لازم تكوني في الشّغل اليوم."), - ("אנחנו אכלנו צהריים ביחד, בעזרת ה' אנחנו הולכים לאכול צהריים גם בשבוע הבא.", "احنا تغدّينا مع بعض، انشالله احنا رح نتغدّى الاسبوع الجاي كمان."), - ("המורה שלח לי מכתב, הוא הולך לשלוח לי מכתב בכל יום שישי.", "الاستاذ بعثلي مكتوب، هوّ رح يبعثلي كلّ يوم الجمعة مكتوب."), - ("אני צחקתי כאשר ראיתי אותך, מחר אני לא הולך לצחוק בכלל.", "انا ضحكت لمّا شفتك#f، بكرا انا مش رح اضحك بالمرّة."), - ("אחרי שהגעתי קניתי שמלה אדומה, אני צריכה לקנות עוד אחת כמוה.", "بعد ما وصلت انا اشتريت فستان احمر، انا لازم اشتري كمان واحد زيّه."), - ("אשתו של דוד שלי (מצד אבא) נישקה את הבן שלי, היא הולכת לנשק גם את הבת שלי אחרי שהיא תגיע לכאן.", "مرت عمّي باست ابني، هيّ رح تبوس بنتي كمان بعد ما هيّ توصل لهون."), - ("היום אנחנו לא שיחקנו ביחד, מחר אנחנו צריכים לשחק ביחד.", "اليوم احنا ما لعبناش مع بعض، بكرا احنا لازم نلعب مع بعض."), - ("למה לא באת אליי לפני שעה? אתה צריך לבוא אליי בעוד חצי שעה!", "ليش ما اجيت لعندي قبل ساعة؟ انت لازم تيجي لعندي كمان نصّ ساعة!"), - ("אני לא סיפרתי לך על המורה, אני צריך לספר לך עליו!", "انا ما حكيتلك عن المعلّم، انا لازم احكيلك عنّه!"), - ("לפני שבוע נכנסתי לבית של חברה שלי, בעוד שבוע אני הולכת להיכנס לבית שלה עוד פעם.", "قبل جمعة فتت على بيت صاحبتي، بعد جمعة رح افوت على بيتها كمان مرّة."), - ]), - ("say", "רַח ולַאזֵם: עברית ← ערבית א'", "say", True, [ - ("הילדה קנתה שמלה ורודה, מחר היא הולכת לקנות ספר.", "البنت اشترت#f فستان زهري، بكرا هيّ رح تشتري كتاب."), - ("אתה לא יצאת מוקדם מהבית, אמרתי לך: אתה חייב לצאת מוקדם!", "انت ما طلعت بكّير من البيت، قلتلك: انت لازم تطلع بكّير!", "אתה לא יָצָאתָ מוקדם מהבית, אמרתי לְךָ: אתה חייב לצאת מוקדם!"), - ("קיבלתי מתנה, המתנה הכי יפה בעולם! מחר אני הולכת לקבל עוד אחת.", "اخدت هديّة، احلى هديّة في العالم! بكرا انا رح آخد كمان واحدة."), - ("לא הלכתי לבית הספר אתמול, אני צריך ללכת מחר. את רוצה לבוא איתי?", "ما رحت على المدرسة امبارح، انا لازم اروح بكرا. بدّك#f تيجي معي؟", "לא הלכתי לבית הספר אתמול, אני צריך ללכת מחר. אַתְּ רוצה לבוא איתי?"), - ("מה שמעת מאביך? אני לא שמעתי אותו. את צריכה לשמוע את דבריו שוב. (לאישה)", "شو سمعتي من ابوكي؟ انا ما سمعته. انتي لازم تسمعي كلامه كمان مرّة.", "מה שָׁמַעְתְּ מֵאָבִיךְ? אני לא שמעתי אותו. אַתְּ צריכה לשמוע את דבריו שוב."), - ("למה לא שילמת למוכר? אתה הולך לשלם לו מחר? כן, אני הולך לשלם לו מחר.", "ليش ما دفعت للبيّاع؟ انت رح تدفعله بكرا؟ اه، انا رح ادفعله بكرا.", "למה לא שִׁילַּמְתָּ למוכר? אתה הולך לשלם לו מחר? כן, אני הולך לשלם לו מחר."), - ("חמותי שאלה האם הלכתי לרופא ומתי אני צריך ללכת אליו שוב.", "حماتي سألت#f اذا رحت لعند الدّكتور وايمتى انا لازم اروح لعنده كمان مرّة."), - ("דודי (מצד אבא) קנה בית בכפר, הוא הולך לקנות בית יותר גדול, הבית הכי גדול בשכונה.", "عمّي اشترى بيت في البلد، هوّ رح يشتري بيت اكبر، اكبر بيت في الحارة."), - ("אשת השיח' ישבה לבדה בבית, היא הולכת לשבת עם בתה אחר כך.", "مرت الشّيخ قعدت#f لحالها في البيت، هيّ رح تقعد مع بنتها بعدين.", "אשת השיח' ישבה לבדהּ בבית, היא הולכת לשבת עם בִּתָּהּ אחר כך."), - ("את שמעת מה שאמר המורה? את חייבת לשמוע את המורה שלך.", "انتي سمعتي شو قال المعلّم؟ انتي لازم تسمعي معلّمك#f.", "אַתְּ שָׁמַעְתְּ מה שאמר המורֶה? אַתְּ חייבת לשמוע את המורֶה שֶׁלָּךְ."), - ("אני הלכתי לשכונה לבדי, אני ממהר מעט, אני צריך ללכת.", "انا رحت على الحارة لحالي، انا مستعجل شويّ، انا لازم اروح."), - ("מדוע לא קשרת את הכלב? אתה צריך לקשור אותו כאשר אתה נמצא כאן.", "ليش ما ربطت الكلب؟ انت لازم تربطه لمّا انت موجود هون.", "מדוע לא קָשַׁרְתָּ את הכלב? אתה צריך לקשור אותו כאשר אתה נמצא כאן."), - ]), - ("say2", "רַח ולַאזֵם: עברית ← ערבית ב'", "say", True, [ - ("אני נהגתי במכונית הכחולה, אני הולך לנהוג במכונית השחורה.", "انا سقت السّيّارة الزّرقا، انا رح اسوق السّيّارة السّودا."), - ("אנחנו ישבנו במסעדה לבדנו, מחר אנחנו הולכים לשבת בבית עם חברינו.", "احنا قعدنا في المطعم لحالنا، بكرا احنا رح نقعد في البيت مع اصحابنا.", "אנחנו ישבנו במסעדה לבדנו, מחר אנחנו הולכים לשבת בבית עם חֲבֵרֵינוּ."), - ("שילמתי למוכר ארבעים וחמישה שקלים, אני הולך לשלם לו עוד ארבעה שקלים.", "دفعت للبيّاع خمسة واربعين شيكل، انا رح ادفعله كمان اربع شواكل."), - ("לקחנו את הילדה מבית חברתה, אנחנו צריכים לקחת אותה לבית סבתה.", "اخدنا البنت من بيت صاحبتها، احنا لازم ناخدها على بيت ستّها.", "לקחנו את הילדה מבית חֲבֶרְתָּהּ, אנחנו צריכים לקחת אותה לבית סָבָתָהּ."), - ("הרופאה ביקרה את דודי (מצד אבא), בשבוע הבא היא צריכה לבקר אותו שוב.", "الدّكتورة زارت عمّي، الاسبوع الجاي هيّ لازم تزوره كمان مرّة."), - ("שכני עבר לכאן לפני שנתיים, הוא הולך לעבור לפריז בעוד שבועיים.", "جاري نقل لهون قبل سنتين، هوّ رح ينقل على باريس كمان اسبوعين."), - ("שאלתי את השיח' שאלה, אני הולך לשאול את דודי (מצד אבא) את אותה השאלה.", "سألت الشّيخ سؤال، انا رح اسأل عمّي نفس السّؤال."), - ("הילד יצא מביתו בלב הכפר, הוא רוצה לצאת מהכפר לבדו.", "الولد طلع من بيته في قلب البلد، هوّ بدّه يطلع من البلد لحاله."), - ("הלכנו לבית חברתי והבנתי שהגענו מוקדם, היינו צריכים להגיע בשש.", "رحنا على بيت صاحبتي وفهمت انّه وصلنا بكّير، كنّا لازم نوصل على ستّة."), - ("אמי שלחה אותי לכאן, אמי הולכת לשלוח אותי לבית מוצטפא.", "امّي بعثتني#f لهون، امّي رح تبعثني على بيت مصطفى."), - ("הילד לבש חולצה צהובה, מחר בבוקר הוא הולך ללבוש חולצה ירוקה.", "الولد لبس بلوزة صفرا، بكرا الصّبح هوّ رح يلبس بلوزة خضرا."), - ]), - ("story", "סיפור המכולת", "say", True, [ - ("ביום מן הימים, כאשר הייתי בערך בן תשע עשרה, אמי שלחה אותי למכולת,", "يوم من الايّام، لمّا كان عمري تقريبا تسعتعش، امّي بعثتني#f على البقالة،"), - ("היא אמרה לי: \"עאדל, בוא בני! אתה צריך ללכת לאבו חסן החנווני,", "هيّ قالتلي: يا عادل، تعال يا ابني! انت لازم تروح لعند ابو حسن البقّال،"), - ("אני רוצה כמה דברים ממנו, יבואו אלינו אורחים היום.\"", "بدّي اكم اشي منّه، رح يجو لعنّا ضيوف اليوم."), - ("אני אמרתי לה: \"מוכן, אמא!\" ושאלתי אותה: \"מי יבוא היום?", "انا قلتلها: حاضر يمّا! وسألتها: مين رح يجي اليوم؟"), - ("ומה בא לך לבשל היום, אמא?\"", "وشو جاي على بالك#f تطبخي اليوم يمّا؟", "ומה בא לָךְ לבשל היום, אמא?\""), - ("היא אמרה: \"דודך (מצד אבא) ח'אלד, אשתו וחמשת ילדיהם,", "هيّ قالت: عمّك خالد، مرته واولادهم الخمسة،", "היא אמרה: \"דּוֹדְךָ ח'אלד, אשתו וחמשת ילדיהם,"), - ("אני רוצה לבשל קישואים וחצילים ממולאים, האוכל הכי טעים בעולם, כמו שאביך אוהב!\"", "بدّي اطبخ كوسا وبيتنجان محشي، اطيب اكل#n في العالم، زيّ ما بيحبّ ابوك!", "אני רוצה לבשל קישואים וחצילים ממולאים, האוכל הכי טעים בעולם, כמו שאָבִיךָ אוהב!\""), - ("אז לקחתי את החמור הלבן והלכתי איתו ללב הכפר, ליד המסגד, למכולת של אבו חסן.", "فاخدت الحمار الابيض ورحت معه لقلب البلد، جنب الجامع، على بقالة#of ابو حسن."), - ("חסן, בנו הבכור, יצא מהמכולת ושאל אותי איפה אני רוצה לקשור את החמור.", "حسن ابنه البكر طلع من البقالة وسألني وين بدّي اربط الحمار."), - ("אני אמרתי לו: \"קשור את החמור במקום שאומר לך בעליו\", וצחקנו.", "انا قلتله: اربط#imp الحمار مطرح ما يقولّك صاحبه، وضحكنا.", "אני אמרתי לו: \"קְשׁוֹר את החמור במקום שאומר לְךָ בעליו\", וצחקנו."), - ("הוא קשר את החמור ואני נכנסתי למכולת.", "هوّ ربط الحمار وانا فتت على البقالة."), - ("השלום עליכם, אבו חסן!", "السّلام عليكم يا ابو حسن!"), - ("שלום שלום, ועליכם השלום! בוקר טוב ויסמין!", "هلا هلا، وعليكم السّلام! صباح الخير والياسمين!"), - ("בוקר של ורדים! מה שלומך?", "صباح الورد! كيف الحال؟"), - ("ברוך השם, מה המצב?", "الحمد للّه، شو الوضع؟"), - ("הכול טוב, נודה לה'!", "كلّ اشي تمام، نشكر الله!"), - ("מה להביא לך, נשמה?", "ايش اجيبلك يا روحي؟", "מה להביא לְךָ, נשמה?"), - ("רגע קט, אני קיבלתי רשימה, אני צריך לראות מה כתבה לי אמי.", "لحظة شويّ، انا اخدت لستة، لازم اشوف شو كتبتلي امّي."), - ("טוב, אני רוצה שקית חלב, גבינה צהובה, שני קילו מלפפונים, ארבעה קילו עגבניות,", "طيّب بدّي كيس حليب، جبنة صفرا، تنين كيلو خيار، اربع كيلو بندورة،"), - ("קילו וחצי פלפל ירוק ושני ראשי שום.", "كيلو ونصّ فلفل اخضر وراسين توم."), - ("בבקשה, יקיר ליבי, עוד משהו?", "تفضّل حبيب قلبي، كمان اشي؟"), - ("כן, באמת! בא לי משהו מתוק, סלח לי, תן לי גם חפיסת שוקולד קטנה,", "اه والله! جاي على بالي اشي حلو، لو سمحت، اعطيني كمان علبة#of شوكولاطة صغيرة،"), - ("לא, לא זו, תן לי אחת יותר קטנה ממנה, תודה! כמה החשבון?", "لا، لا مش هادي، اعطيني واحدة اصغر منها، شكرا! قدّيش الحساب؟"), - ("תשעים ושניים שקלים.", "تنين وتسعين شيكل."), - ("יש לך עודף? אני אתן לך מאתיים שקלים.", "معك فراطة؟ انا رح اعطيك ميتين شيكل.", "יש לְךָ עודף? אני אתן לְךָ מאתיים שקלים."), - ("כן, אל תדאג! בבקשה, מאה ושמונה שקלים.", "اه، ولا يهمّك! تفضّل ميّة وتمانية شيكل."), - ("שילמתי לו ואמרתי לו: \"תודה רבה, ברשותך!\"", "دفعتله وقلتله: شكرا كتير، بخاطرك!", "שילמתי לו ואמרתי לו: \"תודה רבה, בִּרְשׁוּתְךָ!\""), - ("הוא אמר: \"עם השלום, יקירי!\"", "هوّ قال: مع السّلامة حبيبي!"), - ("וחזרתי הביתה.", "ورجعت على البيت."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 2): [ # the workbook's eleventh lesson - ("new", "מילים חדשות", "learn", True, [ - ("תמיד", "دايما"), ("מנהל", "مدير"), ("בדרך כלל", "عادة"), ("העבר", "الماضي"), ("החדשות", "الاخبار"), - ("כריך", "ساندويشة"), ("כרוב", "ملفوف"), ("חמוצים", "مكابيس"), ("צ'יפס", "شيبس"), ("טחינה", "طحينة"), - ("רוטב חריף", "شطّة"), ("העמבה", "العمبة"), ("ממרח שום", "تومة"), ("לדעתי", "حسب رايي"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("days", "ימי השבוע", "say", True, [(he, f"يوم {ar}") for he, ar in DAYS] + [("שבוע", "اسبوع"), ("חודש", "شهر"), ("שנה", "سنة")]), - ("pres", "ההווה עם בּ", "say", False, _present(PRESENT_2_2)), - ("read", "ההווה: ערבית ← עברית א'", "read", True, [ - ("הוא שמע שיר עם אשתו, מחר הוא רוצה לשמוע עם דוד שלו (מצד אמא), כל יום הוא שומע עם אחותו.", "هوّ سمع غنوة مع مرته، بكرا بدّه يسمع مع خاله، كلّ يوم هوّ بيسمع مع اخته."), - ("הם חזרו אתמול, מחר הם רוצים לחזור מהמסעדה, כל יום חמישי הם חוזרים מהעבודה.", "همّ رجعو امبارح، بكرا بدّهم يرجعو من المطعم، كلّ يوم الخميس همّ بيرجعو من الشّغل."), - ("אתם יצאתם מהבית לפני שבוע, אתם רוצים לצאת בעוד חודש, אתם יוצאים כל יום.", "انتو طلعتو من البيت قبل اسبوع، بدّكم تطلعو كمان شهر، انتو بتطلعو كلّ يوم."), - ("אנחנו שאלנו את המורה שאלה, מחר אנחנו רוצים לשאול אותו עוד שאלה, כל יום אנחנו שואלים אותו שאלה.", "احنا سألنا المعلّم سؤال، بكرا بدّنا نسأله كمان سؤال، كلّ يوم بنسأله سؤال."), - ("את אכלת מקלובה, את רוצה לאכול בקלאווה? את אוכלת כנאפה בכל יום רביעי.", "انتي اكلتي مقلوبة، بدّك#f تاكلي بقلاوة؟ انتي بتاكلي كلّ يوم الاربعا كنافة."), - ("הילדים לא חזרו אתמול, הם רוצים לחזור בעוד שבוע, כל יום שני הם חוזרים בשעה חמש וחצי.", "الاولاد ما رجعو امبارح، بدّهم يرجعو بعد اسبوع، كلّ يوم التّنين همّ بيرجعو على ساعة خمسة ونصّ."), - ("את הבנת? את לא רוצה להבין? תמיד כאשר המורה מדבר את מבינה.", "انتي فهمتي؟ ما بدّك#f تفهمي؟ دايما لمّا الاستاذ بيحكي انتي بتفهمي."), - ("אתה ביקרת את דודה שלך (מצד אבא), מחר אתה רוצה לבקר את אח שלך, אתה מבקר את ההורים שלך הרבה או לא?", "انت زرت عمّتك، بكرا بدّك تزور اخوك، انت بتزور اهلك كتير ولّا لا؟"), - ]), - ("read2", "ההווה: ערבית ← עברית ב'", "read", True, [ - ("הבת שלי שמעה מוזיקה, מחר היא רוצה לשמוע מוזיקה, כל יום שני היא שומעת מוזיקה.", "بنتي سمعت#f موسيقى، بكرا بدّها تسمع موسيقى، كلّ يوم التّنين هيّ بتسمع موسيقى."), - ("אני לבשתי את השמלה החדשה, מחר אני רוצה ללבוש אותה, כל יום שישי אני לובשת אותה.", "انا لبست الفستان الجديد، بكرا بدّي البسه، كلّ يوم الجمعة انا بلبسه."), - ("אנחנו לקחנו את הילד מהגן, מחר אנחנו רוצים לקחת אותו מחבר שלו, בדרך כלל אנחנו לוקחים אותו מבית הספר.", "احنا اخدنا الولد من البستان، بكرا بدّنا ناخده من عند صاحبه، عادة احنا مناخده من المدرسة."), - ("אני אמרתי לדוד שלי (מצד אמא) שאני נמצא בבית, מחר אני רוצה לומר לדוד שלי (מצד אבא), אני תמיד אומר לאשתי שאני נמצא בבית.", "انا قلت لخالي انّه انا موجود في البيت، بكرا بدّي اقول لعمّي، انا دايما بقول لمرتي انّه انا موجود في البيت."), - ("סאמי הלך לים, מחרתיים הוא רוצה ללכת לחבר שלו. כל יום ראשון הוא הולך לסבתא שלו.", "سامي راح على البحر، بعد بكرا بدّه يروح لعند صاحبه. كلّ يوم الاحد بيروح لعند ستّه."), - ("אני יצאתי מהבית, בעוד שבוע אני רוצה לצאת מהכפר, אני יוצא כל יום מהעבודה.", "انا طلعت من البيت، كمان اسبوع بدّي اطلع من البلد، انا بطلع كلّ يوم من الشّغل."), - ("את אמרת שלא אכלת, מה את רוצה לומר לי? עכשיו את אומרת לי משהו אחר.", "انتي قلتي انّه ما اكلتي، شو بدّك#f تقوليلي؟ هلقيت بتقوليلي اشي تاني."), - ("היא ישבה אצל השכנה שלה, מחר היא רוצה לשבת אצל דודה שלה (מצד אמא), כל יום ראשון היא יושבת במרפסת ומגלגלת עלי גפן.", "هيّ قعدت#f عند جارتها، بكرا بدّها تقعد عند خالتها، كلّ يوم الاحد هيّ بتقعد في البلكونة وبتلفّ دوالي."), - ]), - ("read3", "ההווה: ערבית ← עברית ג'", "read", True, [ - ("אני שתיתי תה, מחר אני רוצה לשתות מים, כל יום שלישי בשעה ארבע אני שותה קפה.", "انا شربت شاي، بكرا بدّي اشرب ميّ، كلّ يوم التّلاتا على ساعة اربع انا بشرب قهوة."), - ("אני ודוד שלי (מצד אבא) באנו מרחוק, מחר אנחנו רוצים לבוא מחבר שלי, אנחנו באים כל יום לעבודה.", "انا وعمّي اجينا من بعيد، بكرا بدّنا نيجي من عند صاحبي، احنا منيجي كلّ يوم على الشّغل."), - ("את לבשת שמלה שחורה, בעזרת ה' בחתונה שלך את רוצה ללבוש שמלה לבנה, כל יום שבת את לובשת שמלה ירוקה.", "انتي لبستي فستان اسود، انشالله في عرسك#f بدّك#f تلبسي فستان ابيض، كلّ يوم السّبت بتلبسي فستان اخضر."), - ("אנחנו הגענו לשכונה, מחר אנחנו רוצים להגיע לעבודה, כל יום חמישי אנחנו מגיעים למסעדה.", "احنا وصلنا على الحارة، بكرا بدّنا نوصل على الشّغل، كلّ يوم الخميس احنا بنوصل على المطعم."), - ("את קנית חולצה כחולה, את רוצה לקנות חולצה אדומה? למה את לא קונה חולצה שחורה?", "انتي اشتريتي بلوزة زرقا، بدّك#f تشتري بلوزة حمرا؟ ليش انتي ما بتشتري بلوزة سودا؟"), - ("אני שילמתי שבעים ושלושה שקלים, אני צריך לשלם עוד חמישה עשר שקלים, אני משלם כל הזמן.", "انا دفعت تلاتة وسبعين شيكل، انا لازم ادفع كمان خمستعشر شيكل، انا بدفع كلّ الوقت."), - ("הוא שלח אותי לכאן, הוא רוצה לשלוח אותי לנגר, כל יום הוא שולח אותי למקום חדש.", "هوّ بعثني لهون، بدّه يبعثني لعند النّجّار، كلّ يوم هوّ بيبعثني على محلّ جديد."), - ("אני רצתי איתך אתמול, אני הולך לרוץ עם דודה שלי (מצד אמא), כל יום רביעי אני רץ איתה.", "انا ركضت معك#f امبارح، انا رح اركض مع خالتي، كلّ يوم الاربعا انا بركض معها."), - ]), - ("say", "ההווה: עברית ← ערבית א'", "say", True, [ - ("את חזרת מהעבודה, מחר תחזרי מחברתך, כל יום את חוזרת מאמך.", "انتي رجعتي من الشّغل، بكرا ترجعي من عند صاحبتك#f، كلّ يوم انتي بترجعي من عند امّك#f.", "אַתְּ חָזַרְתְּ מהעבודה, מחר תחזרי מֵחֲבֶרְתֵּךְ, כל יום אַתְּ חוזרת מֵאִמֵּךְ."), - ("אנחנו שתינו קפה ללא סוכר, מחרתיים נשתה קפה עם סוכר, כל יום ראשון אנחנו שותים תה עם המנהל.", "احنا شربنا قهوة بدون سكّر، بعد بكرا نشرب قهوة مع سكّر، كلّ يوم الاحد احنا بنشرب شاي مع المدير."), - ("ראיתי אותך אתמול, אני רוצה לראות אותך בעוד שעה, בכל יום אני רואה אותך פה. (לאישה)", "شفتك#f امبارح، بدّي اشوفك#f كمان ساعة، كلّ يوم انا بشوفك#f هون.", "ראיתי אוֹתָךְ אתמול, אני רוצה לראות אוֹתָךְ בעוד שעה, בכל יום אני רואה אוֹתָךְ פה."), - ("ביקרתי את חברי, מחר אני רוצה לבקר את אחי, כל שבוע אני מבקר את אבי.", "زرت صاحبي، بكرا بدّي ازور اخوي، كلّ اسبوع انا بزور ابوي."), - ("שילמתי עליך אתמול, אני רוצה לשלם עליך גם מחר, כל יום אני משלם עליך. (לגבר)", "دفعت عنّك امبارح، بدّي ادفع عنّك كمان بكرا، كلّ يوم انا بدفع عنّك.", "שילמתי עָלֶיךָ אתמול, אני רוצה לשלם עָלֶיךָ גם מחר, כל יום אני משלם עָלֶיךָ."), - ("נהגת במכונית? אתה לא צריך לנהוג היום? אתה נוהג במכונית בכל יום.", "سقت السّيّارة؟ انت مش لازم تسوق اليوم؟ انت بتسوق السّيّارة كلّ يوم.", "נָהַגְתָּ במכונית? אתה לא צריך לנהוג היום? אתה נוהג במכונית בכל יום."), - ("נישקתי את נכדי, אני רוצה לנשק את נכדתי, בכל יום שבת אני מנשק את נכדיי.", "بست حفيدي، بدّي ابوس حفيدتي، كلّ يوم السّبت انا ببوس احفادي.", "נישקתי את נֶכְדִּי, אני רוצה לנשק את נֶכְדָּתִי, בכל יום שבת אני מנשק את נְכָדַי."), - ("אכלת צהריים בבית? אתה רוצה לאכול צהריים במסעדה? בכל יום שישי אתה אוכל צהריים אצל סבתך.", "تغدّيت في البيت؟ بدّك تتغدّى في المطعم؟ كلّ يوم الجمعة انت بتتغدّى عند ستّك.", "אָכַלְתָּ צהריים בבית? אתה רוצה לאכול צהריים במסעדה? בכל יום שישי אתה אוכל צהריים אצל סָבָתְךָ."), - ]), - ("say2", "ההווה: עברית ← ערבית ב'", "say", True, [ - ("הילדה ביקרה את אחותי, היא צריכה לבקר את המורה שלה, היא מבקרת את דודתה (מצד אמא) בכל יום שני.", "البنت زارت اختي، هيّ لازم تزور معلّمتها، هيّ بتزور خالتها كلّ يوم التّنين.", "הילדה ביקרה את אחותי, היא צריכה לבקר את המורָה שלה, היא מבקרת את דּוֹדָתָהּ בכל יום שני."), - ("אחי ראה אותי אתמול, הוא רוצה לראות אותי גם מחר, בכל יום שלישי הוא רואה אותי.", "اخوي شافني امبارح، بدّه يشوفني كمان بكرا، كلّ يوم التّلاتا هوّ بيشوفني."), - ("הילדים הלכו לביתם, הם צריכים ללכת לחברם סמיר, הם הולכים בכל יום למכולת.", "الاولاد راحو على بيتهم، همّ لازم يروحو لعند صاحبهم سمير، همّ بيروحو كلّ يوم على البقالة."), - ("אני הבנתי אותך, אני רוצה להבין אותך גם מחר, אני מבין אותך תמיד. (לאישה)", "انا فهمتك#f، بدّي افهمك#f كمان بكرا، انا بفهمك#f دايما.", "אני הבנתי אוֹתָךְ, אני רוצה להבין אוֹתָךְ גם מחר, אני מבין אוֹתָךְ תמיד."), - ("אני לקחתי קפה, אני רוצה לקחת תה, אני לוקח בכל יום חמישי מקלובה.", "انا اخدت قهوة، بدّي آخد شاي، انا باخد كلّ يوم الخميس مقلوبة."), - ("יצאתי מהעבודה, אני רוצה לצאת עם בעלי, בכל יום ראשון אני יוצאת עם חברתי.", "طلعت من الشّغل، بدّي اطلع مع جوزي، كلّ يوم الاحد انا بطلع مع صاحبتي."), - ("אשתי נתנה לי חפיסת שוקולד, היא רוצה לתת לבני גם חפיסה אבל יותר קטנה ממנה, היא נותנת לחברה שלה בכל יום שלישי חציל ממולא.", "مرتي اعطتني علبة#of شوكولاطة، بدّها تعطي ابني كمان علبة بسّ اصغر منها، هيّ بتعطي صاحبتها كلّ يوم التّلاتا بيتنجان محشي."), - ("שכנתי עברה לשכונה לפני חצי שנה, היא רוצה לעבור לשכונה חדשה, היא עוברת בכל חצי שנה.", "جارتي نقلت#f على الحارة قبل نصّ سنة، بدّها تنقل على حارة جديدة، هيّ بتنقل كلّ نصّ سنة."), - ]), - ("story", "סיפור הפלאפל", "say", True, [ - ("ביום חמישי שעבר רציתי ללכת למסעדת פלאפל לאכול משהו,", "يوم الخميس الماضي كنت بدّي اروح على محلّ فلافل آكل اشي،"), - ("הלכתי לחברי עלי, הוא בעל מסעדת פלאפל לא רחוק מביתי.", "رحت لعند صاحبي علي، هوّ صاحب#of مطعم فلافل مش بعيد عن بيتي."), - ("השלום עליכם, עלי!", "السّلام عليكم يا علي!"), - ("ועליכם השלום! שלום שלום שלום!", "وعليكم السّلام! هلا هلا هلا!"), - ("מה החדשות? מה שלומך?", "شو الاخبار؟ كيف حالك؟", "מה החדשות? מה שְׁלוֹמְךָ?"), - ("ברוך השם, טוב. מה שלומך אתה? מה המצב?", "الحمد للّه تمام، كيفك انت؟ شو الوضع؟", "ברוך השם, טוב. מה שְׁלוֹמְךָ אתה? מה המצב?"), - ("באמת שמח, ברוך השם, נודה לה' ונשבח אותו.", "والله مبسوط الحمد للّه، نشكر الله ونحمده."), - ("דבר ראשון, בבקשה, כדור פלאפל! מה אתה רוצה שאכין לך, יקר?", "اوّل اشي تفضّل حبّة#of فلافل! ايش بدّك احضّرلك يا غالي؟", "דבר ראשון, בבקשה, כדור פלאפל! מה אתה רוצה שאכין לְךָ, יקר?"), - ("תכין לי מנת פלאפל כמו שאני אוהב!", "حضّرلي ساندويشة#of فلافل زيّ ما انا بحبّ!"), - ("מוכן, יקר! אני מכין לך את המנה הכי טעימה, מה אתה רוצה בה?", "حاضر يا غالي، انا بحضّرلك اطيب ساندويشة، شو بدّك فيها؟", "מוכן, יקר! אני מכין לְךָ את המנה הכי טעימה, מה אתה רוצה בה?"), - ("סלט ירקות, זאת אומרת מלפפון ועגבנייה, קצת בצל וחציל.", "سلطة#of خضرة، يعني خيار وبندورة، شويّة#of بصل وبيتنجان."), - ("כרוב אדום? חמוצים אתה רוצה?", "ملفوف احمر؟ مكابيس بدّك؟"), - ("לא, אבל בחייך אני רוצה צ'יפס, טחינה וגם חריף, הכי חשוב העמבה!", "لا، امّا وحياتك بدّي شيبس، طحينة وكمان شطّة، اهمّ اشي العمبة!", "לא, אבל בְּחַיֶּיךָ אני רוצה צ'יפס, טחינה וגם חריף, הכי חשוב העמבה!"), - ("בשמחה, אתה אוכל פה או לוקח אותה איתך?", "على راسي، بتاكل هون ولّا بتاخدها معك؟", "בשמחה, אתה אוכל פה או לוקח אותה אִיתְּךָ?"), - ("אני אוכל פה, יקירי, תן לי גם בקבוק קולה. כמה החשבון?", "باكل هون حبيبي، اعطيني كمان قنّينة#of كولا. قدّيش الحساب؟"), - ("שמונה עשרה, רק שמונה עשר שקלים ביחד.", "تمنتعش، بسّ تمنتعشر شيكل مع بعض."), - ("תודה, יקירי, באמת הפלאפל הכי טעים בעולם!", "شكرا حبيبي، والله اطيب فلافل في العالم!"), - ("בתיאבון!", "صحّة!"), - ("על ליבך! (התשובה ל\"בתיאבון\")", "على قلبك!", "על לִבְּךָ!"), - ]), - ("fread", "סיפור הפלאפל: ערבית ← עברית", "read", True, [ - ("סליחה, כמה החשבון? ארבעים ושבעה שקלים, באמת לא יקר!", "لو سمحتي، قدّيش الحساب؟ سبعة واربعين شيكل، والله مش غالي!"), - ("היא יצאה מהמכולת ושאלה אותי מה אני רוצה לקנות.", "هيّ طلعت#f من البقالة وسألتني#f ايش بدّي اشتري؟"), - ("אני קונה פה כל חודש! מחיר השווארמה ארבעים ושמונה שקלים.", "انا بشتري هون كلّ شهر! حقّ الشّاورما تمانية واربعين شيكل."), - ("אנחנו רוצים לבשל לך שקשוקה! לך תביא לי שני קילו עגבניות, חצי קילו פלפל אדום וראש שום.", "بدّنا نطبخلك شكشوكة! روح جيبلي تنين كيلو بندورة، نصّ كيلو فلفل احمر وتومة."), - ("אנחנו יושבים פה כל יום רביעי ושותים תה ביחד.", "احنا بنقعد هون كلّ يوم الاربعا وبنشرب شاي مع بعض."), - ("אשתי הייתה רעבה אז הלכנו לאכול במסעדת פלאפל.", "مرتي كانت جوعانة فرحنا ناكل في مطعم فلافل."), - ("נביל לא חזר לבית שלו, כמו שהבנתי הוא הולך לחזור בעוד רבע שעה.", "نبيل ما رجع على بيته، زيّ ما فهمت هوّ رح يرجع كمان ربع ساعة."), - ("בחייך תן לי מאה שקלים, ואל תשאל אותי אם יש לי עודף או לא!", "وحياتك اعطيني ميّة#of شيكل، وتسألنيش اذا معي فراطة ولّا لا!"), - ("מי רוצה להיות מנהל בית הספר? לפי דעתי הבן של השיח' רוצה להיות המנהל.", "مين بدّه يكون مدير المدرسة؟ حسب رايي ابن الشّيخ بدّه يكون المدير."), - ("אבא שלי אמר לי: \"בני, תכין לי כוס קפה\", אמרתי לו: \"מוכן, אבא!\"", "ابوي قالّي: يا ابني حضّرلي فنجان قهوة، قلتله: حاضر يابا!"), - ("רציתי לבקר אותך, אבל אמא שלך סיפרה לי שיצאת מהבית והלכת לעבודה.", "كان بدّي ازورك، امّا امّك حكتلي انّه انت طلعت من البيت ورحت على الشّغل."), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 3): [ # the workbook's twelfth lesson - ("new", "מילים חדשות", "learn", True, [ - ("בדיוק", "بالزّبط"), ("עכשיו", "هلّق"), ("הכוס", "الكاسة"), ("הדרך", "الطّريق"), - ("האחות (בבית חולים)", "الممرّضة"), ("מתחת", "تحت"), ("הערב", "المسا"), ("יום הולדת", "عيد ميلاد"), - ("שבור", "مكسور"), ("האופניים", "البسكليت"), ("הטלפון הנייד", "الموبايل"), ("קרה", "باردة"), ("מהר", "بسرعة"), - ("סיגריה", "سيجارة"), ("רגוע, נוח", "مرتاح"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("hours", "השעות", "say", True, [(f"השעה {he}", f"السّاعة {ar}") for he, ar in HOURS]), - ("clock", "השעון: רבע, שליש, חצי", "say", True, [ - ("רבע שעה", "ربع ساعة"), ("שליש שעה (עשרים דקות)", "تلت ساعة"), ("חצי שעה", "نصّ ساعة"), ("שלושת רבעי שעה", "تلات ارباع ساعة"), - ("דקה, דקות", "دقيقة، دقايق"), ("בעוד שעה", "كمان ساعة"), - ("באיזו שעה?", "على ايّ ساعة؟"), ("עד איזו שעה?", "لحدّ ايّ ساعة؟"), ("בשעה שלוש", "على ساعة تلاتة"), - ("השעה רבע לעשר", "السّاعة عشرة الّا ربع"), ("חמישה לארבע", "اربعة الّا خمسة"), - ("השעה אחת עשרה עשרים וחמש", "السّاعة حدعش ونصّ الّا خمسة"), ("השעה אחת עשרה שלושים וחמש", "السّاعة حدعش ونصّ وخمسة"), - ("ביום ראשון בעשרים לאחת", "في يوم الاحد على واحدة الّا تلت"), ("בואו בשעה ארבע וחצי", "تعالو ساعة اربع ونصّ"), - ]), - ("talk", "שיחה: מה השעה?", "say", True, [ - ("סליחה, מה השעה עכשיו?", "لو سمحت، قدّيش السّاعة هلقيت؟"), - ("אין לך שעון?", "ما في معك ساعة؟", "אין לְךָ שעון?"), - ("לא, דודי, אין לי שעון!", "لا عمّي، ما في معي ساعة!"), - ("טוב, בסדר, השעה חמש בדיוק.", "طيّب بسيدر، السّاعة خمسة تمام، يعني بالزّبط."), - ("תודה, דודי, ה' ישמור אותך!", "تسلم عمّي الله يخلّيك!", "תודה, דודי, ה' ישמור אוֹתְךָ!"), - ("על לא דבר, ה' ישמור עליך!", "عفوا، الله يحفظك!", "על לא דבר, ה' ישמור עָלֶיךָ!"), - ]), - ("read", "השעות: ערבית ← עברית א'", "read", True, [ - ("אנחנו ישבנו יחד אתמול מעשרים לארבע עד חמש שלושים וחמש, אני הולך לומר לחבר'ה שנשב עוד פעם בעוד יומיים.", "احنا قعدنا مع بعض امبارح على اربع الّا تلت لحدّ خمسة ونصّ وخمسة، انا رح احكي للجماعة انّه رح نقعد كمان مرّة كمان يومين."), - ("הילדה שיחקה עם הילד כל היום, מהשעה רבע לתשע עד השעה שתיים שלושים וחמש. הם רוצים לשחק יחד גם מחר, הם משחקים כל יום יחד.", "البنت لعبت#f مع الولد كلّ اليوم، من السّاعة تسعة الّا ربع لحدّ السّاعة تنتين ونصّ وخمسة. بدّهم يلعبو مع بعض بكرا كمان، همّ بيلعبو كلّ يوم مع بعض."), - ("אני חזרתי מהעבודה הביתה לפני שעה וחצי, כאשר נכנסתי הביתה ראיתי שהשעה הייתה שלוש ורבע, אני רוצה לחזור מחר ברבע לשלוש.", "انا رجعت من الشّغل على البيت قبل ساعة ونصّ، لمّا فتت على البيت شفت انّه السّاعة كانت تلاتة وربع، بدّي ارجع بكرا على تلاتة الّا ربع."), - ("לפני חודשיים וחצי לקחתי כסף מדוד שלי, הוא ראה שלבשתי מכנסיים שחורים ואמר לי שאני צריך לקנות לו את אותם המכנסיים, אמרתי לו שאני קונה לו מה שהוא רוצה.", "قبل شهرين ونصّ اخدت مصاري من عمّي، هوّ شاف انّه لبست بنطلون اسود وقالّي انّه انا لازم اشتريله نفس البنطلون، قلتله انّي بشتريله شو ما بدّه."), - ("אח שלי שבר את חלון המכונית שלי כאשר נהג בה, הוא בא לספר לי שהוא רוצה לקחת אותה למוסך מחר בבוקר ולחזור לכאן בעוד יומיים בשעה עשרה לשמונה.", "اخوي كسر شبّاك#of سيّارتي لمّا ساقها، هوّ اجا يحكيلي انّه بدّه ياخدها على الكراج بكرا الصّبح ويرجع لهون كمان يومين على ساعة تمانية الّا عشرة."), - ("מחמוד בן עשרים ותשע שנה ועשרה חודשים, בעוד חודשיים תהיה המסיבה הכי יפה בעולם, ביום חמישי בשעה רבע לשש במסעדת \"אלוורד\".", "محمود عمره تسعة وعشرين سنة وعشر اشهر، كمان شهرين رح تكون احلى حفلة بالعالم، يوم الخميس ساعة ستّة الّا ربع في مطعم الورد."), - ("שלשום אני נהגתי במכונית שלי והייתי לבדי, כאשר ראיתי את חברי עאטף, זה היה בערך בשעה רבע לשתים עשרה, הוא נכנס למכונית וצחקנו כל הדרך.", "اوّل امبارح انا سقت سيّارتي وكنت لحالي، لمّا شفت صاحبي عاطف، هادا كان حوالي ساعة تنعش الّا ربع، هوّ دخل على السّيّارة وضحكنا كلّ الطّريق."), - ]), - ("read2", "השעות: ערבית ← עברית ב'", "read", True, [ - ("את הכנת קישוא ממולא לאבא שלך? מצוין! אנחנו נגיע לבית שלך בעוד שעתיים בערך, זאת אומרת בשעה חמש ועשרים בעזרת ה'.", "انتي حضّرتي كوسا محشي لابوكي؟ ممتاز! احنا رح نوصل على بيتك#f كمان ساعتين تقريبا، يعني ساعة خمسة وتلت انشالله."), - ("האחות נכנסה לחדר שלי ושאלה מה ומתי אכלתי היום, אמרתי לה שאכלתי לצהריים רק סלט ירקות בשעה אחת שלושים וחמש בערך.", "الممرّضة فاتت#f على غرفتي وسألت#f شو وايمتا اكلت اليوم، قلتلها انّه تغدّيت بسّ سلطة#of خضرة على ساعة واحدة ونصّ وخمسة تقريبا."), - ("אתה הגעת לעבודה בשעה עשר וחצי, כאשר אני נכנסתי למשרד שלך ראיתי שלא ישבת לבדך, מי האיש הזה שהיה איתך?", "انت وصلت على الشّغل ساعة عشرة ونصّ، لمّا انا فتت على مكتبك، شفت انّه ما قعدت لحالك، مين هادا الرّجل الّي كان معك؟"), - ("למה לא באת לים היום? באמת שחינו ושיחקנו בערך שלוש שעות ורבע, זאת אומרת מהשעה ארבע עד השעה שבע ורבע.", "ليش ما اجيتي على البحر اليوم؟ والله سبحنا ولعبنا حوالي تلات ساعات وربع، يعني من السّاعة اربع لحدّ السّاعة سبعة وربع."), - ("כאשר קניתי את הפירות שילמתי למוכרת חמישים ושבעה שקלים, אמרתי לה שחברי סמיר יבוא לכאן בעוד ארבע שעות ועשר דקות, זאת אומרת בשעה שלוש בערך.", "لمّا اشتريت الفواكه دفعت للبيّاعة سبعة وخمسين شيكل، قلتلها انّه صاحبي سمير رح يجي لهون كمان اربع ساعات وعشر دقايق، يعني ساعة تلاتة تقريبا."), - ("סבא שלי בן שמונים ושלוש שנה בערך, הוא הכי חרוץ, כל בוקר הוא רץ חמישה קילומטר ואחר כך שוחה בערך חצי שעה וחוזר לביתו ואוכל.", "جدّي عمره تلاتة وتمانين سنة تقريبا، هوّ اشطر واحد، كلّ صبح هوّ بيركض خمسة كيلو وبعدين بيسبح حوالي نصّ ساعة وبيرجع على بيته وبياكل."), - ("בת דודי (מצד אמא) דיברה איתי ואמרה לי שהיא הבינה שאבא שלה יגיע הביתה אחרי שיקנה את הירקות בשוק, היא תבשל מקלובה כאשר יחזור בשעה ארבע ועשרים.", "بنت خالي حكت معي وقالتلي انّه هيّ فهمت#f انّه ابوها رح يوصل على البيت بعد ما يشتري الخضرة في السّوق، هيّ رح تطبخ مقلوبة لمّا يرجع ساعة اربع وتلت."), - ]), - ("say", "השעות: עברית ← ערבית א'", "say", True, [ - ("אתמול בשעה שתים עשרה וחצי בערך, חברי בא לבקר אותי בבית, הייתי שמח מאוד, אכלנו צהריים יחד.", "امبارح على ساعة تنعش ونصّ تقريبا، صاحبي اجا يزورني في البيت، كنت مبسوط كتير، تغدّينا مع بعض."), - ("באיזו שעה הוא רוצה לצאת למכולת? בשבע ועשרים, כל יום הוא יוצא באותה השעה.", "على ايّ ساعة بدّه يطلع على البقالة؟ على سبعة وتلت، كلّ يوم هوّ بيطلع على نفس السّاعة."), - ("לפני שבועיים וחצי ביום ראשון בשעה ארבע ורבע, יצאתי מהבית וראיתי את שכנתי, היא נכנסה למכוניתה והלכה לבקר את אמה.", "قبل اسبوعين ونصّ في يوم الاحد على ساعة اربعة وربع، طلعت من البيت وشفت جارتي، هيّ دخلت#f على سيّارتها وراحت تزور امّها."), - ("היום בשעה שש שלושים וחמש הייתי מתחת לבית שלך, מדוע לא ירדת? לא שמעת אותי?", "اليوم على ساعة ستّة ونصّ وخمسة كنت تحت بيتك، ليش ما نزلت؟ ما سمعتني؟", "היום בשעה שש שלושים וחמש הייתי מתחת לבית שֶׁלְּךָ, מדוע לא יָרַדְתָּ? לא שָׁמַעְתָּ אותי?"), - ("בעוד חודשיים ביום שלישי בערב, בערך בשעה תשע וחצי נדבר, בסדר? יאללה, מתאים!", "كمان شهرين في يوم التّلاتا المسا، تقريبا على ساعة تسعة ونصّ نحكي، ماشي؟ يالله تمام!"), - ("הבנתי שהיא באה אליי בשעה רבע לארבע אבל לא קשרתי את הכלב, כיוון שהייתי בעבודה.", "فهمت انّه هيّ اجت لعندي على ساعة اربعة الّا ربع بسّ ما ربطت الكلب، عشان كنت في الشّغل."), - ("אבי בן חמישים ותשע שנה ואחד עשר חודשים, בעוד חודש ביום שישי בשתים עשרה וחצי תהיה המסיבה הכי יפה.", "ابوي عمره تسعة وخمسين سنة وحدعشر شهر، كمان شهر في يوم الجمعة على تنعش ونصّ رح تكون احلى حفلة."), - ("לבן אחי היה יום הולדת, המסיבה הייתה ביום שלישי בשעה חמש בדיוק.", "ابن اخوي كان عنده عيد ميلاد، الحفلة كانت في يوم التّلاتا على ساعة خمسة بالزّبط."), - ("בני ביקר את אמי לפני שבוע ביום רביעי בשעה עשרים לארבע, הם שתו תה מתוק ואכלו עוגה.", "ابني زار امّي قبل اسبوع في يوم الاربعا على ساعة اربعة الّا تلت، همّ شربو شاي حلو واكلو كعكة."), - ]), - ("say2", "השעות: עברית ← ערבית ב'", "say", True, [ - ("ביקרנו את סבתי היום בשעה חמישה לשש, כאשר הגענו היא נישקה אותנו.", "زرنا ستّي اليوم على ساعة ستّة الّا خمسة، لمّا وصلنا هيّ باستنا."), - ("שמעתי שלא הלכת לעבודה היום? הכול בסדר? כמובן! אל תדאג, אגיע מחר בשעה עשרים לשמונה, נדבר מחר.", "سمعت انّه ما رحتي على الشّغل اليوم؟ كلّ اشي تمام؟ طبعا! ولا يهمّك، بوصل بكرا على ساعة تمانية الّا تلت، نحكي بكرا.", "שמעתי שלא הָלַכְתְּ לעבודה היום? הכול בסדר? כמובן! אל תדאג, אגיע מחר בשעה עשרים לשמונה, נדבר מחר."), - ("מהשעה אחת עשרה ורבע עד השעה חמישה לשתים עשרה הייתי בבית ובישלתי ארוחת צהריים, בשעה רבע לאחת אשתי הגיעה ואכלנו צהריים ביחד.", "من السّاعة حدعش وربع لحدّ السّاعة تنعش الّا خمسة كنت في البيت وطبخت غدا، على ساعة واحدة الّا ربع مرتي وصلت#f وتغدّينا مع بعض."), - ("אני לא קשרתי את הכלב, כאשר הגעתי לביתך בשעה שתיים ועשרים ראיתי שהוא היה קשור, נתתי לו מים ואוכל וחזרתי הביתה.", "انا ما ربطت الكلب، لمّا وصلت على بيتك على ساعة تنتين وتلت شفت انّه كان مربوط، اعطيته ميّ واكل#n ورجعت على البيت.", "אני לא קשרתי את הכלב, כאשר הגעתי לְבֵיתְךָ בשעה שתיים ועשרים ראיתי שהוא היה קשור, נתתי לו מים ואוכל וחזרתי הביתה."), - ("בעוד שמונה דקות בערך, כאשר אמי תדבר איתך, היא תשאל אותך איפה אני, תגידי לה שאני נמצאת כאן אבל אדבר איתה עוד מעט.", "كمان تمن دقايق تقريبا، لمّا امّي تحكي معك#f، هيّ رح تسألك#f وين انا، قوليلها انّه انا موجودة هون بسّ بحكي معها كمان شويّ.", "בעוד שמונה דקות בערך, כאשר אמי תדבר אִיתָּךְ, היא תשאל אוֹתָךְ איפה אני, תגידי לה שאני נמצאת כאן אבל אדבר איתה עוד מעט."), - ("ראית את החלון השבור? אני לא הבנתי מי שבר אותו ומתי. כאשר נכנסתי לבית הספר ראיתי אותו שבור.", "شفتي الشّبّاك المكسور؟ انا ما فهمت مين كسره وايمتا. لمّا دخلت على المدرسة شفته مكسور.", "רָאִית את החלון השבור? אני לא הבנתי מי שבר אותו ומתי. כאשר נכנסתי לבית הספר ראיתי אותו שבור."), - ("חברתי פאטמה לא יצאה מביתה, היא שלחה את בתה אליי ואמרה לי שהיא לא רוצה לצאת לפני שבעלה יחזור, הוא יחזור בערך בעוד שעה ורבע.", "صاحبتي فاطمة ما طلعت#f من بيتها، هيّ بعثت#f بنتها لعندي وقالتلي انّه هيّ ما بدّها تطلع قبل ما جوزها يرجع، هوّ رح يرجع تقريبا كمان ساعة وربع."), - ("אנחנו באנו לכאן אבל לא ראינו אותך, איפה היית? יא גבר! אמרתי לך שמהשעה תשע ורבע עד השעה רבע לאחת עשרה אני לא נמצא.", "احنا اجينا لهون بسّ ما شفناك، وين كنت؟ يا زلمة! قلتلك انّه من السّاعة تسعة وربع لحدّ السّاعة حدعش الّا ربع انا مش موجود.", "אנחנו באנו לכאן אבל לא ראינו אוֹתְךָ, איפה הָיִיתָ? יא גבר! אמרתי לְךָ שמהשעה תשע ורבע עד השעה רבע לאחת עשרה אני לא נמצא."), - ]), - ("verbs", "בניין שני: פַעַّל", "say", False, [(f"{he} (עבר, עתיד)", f"{past}، {future}") for he, past, future in FAAAL_2_3]), - ("past", "לבש והלביש בעבר", "say", False, _past(VERBS_2_3)), - ("vread", "פִעֵל מול פַעַّל: ערבית ← עברית א'", "read", True, [ - ("הוא שמע מוזיקה עם חבר שלו, אחר כך הוא השמיע לבן שלו מוזיקה בסלון הבית שלו.", "هوّ سمع موسيقى مع صاحبه، بعدين هوّ سمّع ابنه موسيقى في صالون بيته."), - ("אני חזרתי מהחתונה של חבר שלי והחזרתי את הבת שלי מחתונה אחרת.", "انا رجعت من عرس صاحبي ورجّعت بنتي من عرس تاني."), - ("אני הייתי בבית כאשר את יצאת מהבית והוצאת את הילד איתך.", "انا كنت في البيت لمّا انتي طلعتي من البيت وطلّعتي الولد معك#f."), - ("אנחנו שמענו שיר יפה ואחר כך השמענו את השיר הזה לאחותי.", "احنا سمعنا غنوة حلوة وبعدين سمّعنا هادي الغنوة لاختي."), - ("אני שאלתי מי ירד מהבית והוריד איתו את הארנק?", "انا سألت مين نزل من البيت ونزّل معه الجزدان؟"), - ("הילד לא חזר אתמול מהמסיבה, לכן הוא לא החזיר את האופניים.", "الولد ما رجع امبارح من الحفلة، عشان هيك هوّ ما رجّع البسكليت."), - ("איך הילד לא הבין? המורה לא הסביר לו? לא, הוא לא הסביר לו כמו שצריך.", "كيف الولد ما فهم؟ الاستاذ ما فهّمه؟ لا، هوّ ما فهّمه زيّ ما لازم."), - ("למה יצאת מהחדר ולא הוצאת את הטלפון הנייד?", "ليش طلعت من الغرفة وما طلّعت الموبايل؟"), - ("הבת שלי שמעה מוזיקה עם הבת שלך, הבת שלי השמיעה לה שיר חדש.", "بنتي سمعت#f موسيقى مع بنتك، بنتي سمّعتها#f غنوة جديدة."), - ("אחותי לא לבשה את השמלה החדשה, אמא שלה הלבישה אותה בעל כורחה.", "اختي ما لبست#f الفستان الجديد، امّها لبّستها#f غصبن عنّها."), - ("אני שתיתי נס קפה והשקיתי את השכן שלי קפה שחור אתמול כאשר הוא בא לבקר אותי.", "انا شربت نسكافيه وشرّبت جاري قهوة سادة امبارح لمّا هوّ اجا يزورني."), - ("אני לא הבנתי למה את הגעת לבדך? למה לא הבאת את הילד הקטן לכאן?", "انا ما فهمت ليش انتي وصلتي لحالك#f؟ ليش ما وصّلتي الولد الصّغير لهون؟"), - ]), - ("vread2", "פִעֵל מול פַעַّל: ערבית ← עברית ב'", "read", True, [ - ("הדלת לא הייתה פתוחה, אני שברתי את החלון כדי שיצאתי והוצאתי את התיק.", "الباب ما كان مفتوح، انا كسرت الشّبّاك عشان طلعت وطلّعت الشّنطة."), - ("אבא שלה לא הבין מה אני רוצה בדיוק, וגם הוא לא הסביר לי.", "ابوها ما فهم شو بدّي بالزّبط، وكمان هوّ ما فهّمني."), - ("דוד שלי הגיע לבית שלי, הביא אותי לים והשקה אותי קפה קר, שתינו יחד.", "عمّي وصل على بيتي ووصّلني على البحر وشرّبني قهوة باردة، شربنا مع بعض."), - ("אני רוצה להבין, למה את לבשת שמלה צהובה והלבשת את הבת שלי שמלה ורודה?", "انا بدّي افهم، ليش انتي لبستي فستان اصفر ولبّستي بنتي فستان زهري؟"), - ("אנחנו ישבנו במרפסת ושמענו שיר, אחר כך השמענו לשכנה שלי את אותו השיר.", "احنا قعدنا في البلكونة وسمعنا غنوة، بعدين سمّعنا جارتي نفس الغنوة."), - ("הבן של אחי חזר מהעבודה והחזיר את אחותו הקטנה הביתה.", "ابن اخوي رجع من الشّغل ورجّع اخته الصّغيرة عالبيت."), - ("חברה שלי נכנסה ושאלה אותי מי צחק עליה, אני אמרתי לה שהשכן שלי הצחיק אותנו, לכן צחקנו.", "صاحبتي دخلت#f وسألتني#f مين ضحك عليها، انا قلتلها انّه جاري ضحّكنا عشان هيك ضحكنا."), - ("אנחנו אמרנו: \"תיזהר! אל תצא החוצה עכשיו ואל תוציא את הילדים החוצה!\"", "احنا قلنا: دير بالك! ما تطلع برّا هلّق وما تطلّع الاولاد برّا!"), - ("כאשר יוסף היה גר עם הבן שלו הוא היה צוחק הרבה, הבן שלו רוצה להצחיק אותו עוד פעם.", "لمّا يوسف كان ساكن مع ابنه هوّ كان يضحك كتير، ابنه بدّه يضحّكه كمان مرّة."), - ("היא ירדה מהבית שלה והורידה את הכלבה החומה לגן.", "هيّ نزلت#f من بيتها ونزّلت#f الكلبة البنّيّة على البستان."), - ("הבן שלי לא הבין אבל דוד שלי בא והסביר לו בדיוק מה אמר המורה.", "ابني ما فهم بسّ عمّي اجا وفهّمه بالزّبط شو قال المعلّم."), - ("באמת אני צחקתי הרבה אתמול בלילה, כל הלילה אתה הצחקת אותי.", "والله انا ضحكت كتير امبارح ليلة، طول اللّيلة انت ضحّكتني."), - ]), - ("vsay", "פִעֵל מול פַעַّל: עברית ← ערבית א'", "say", True, [ - ("אני חזרתי מהעבודה והחזרתי את בני מהגן.", "انا رجعت من الشّغل ورجّعت ابني من البستان."), - ("מדוע שמעת מוזיקה לבדך? את לא השמעת לחברתך את השיר הזה?", "ليش سمعتي موسيقى لحالك#f؟ ما سمّعتي صاحبتك#f هادي الغنوة؟", "מדוע שָׁמַעְתְּ מוזיקה לְבַדֵּךְ? את לא הִשְׁמַעְתְּ לַחֲבֶרְתֵּךְ את השיר הזה?"), - ("הילד לבש את המכנסיים השחורים והלביש את חברו חולצה כחולה.", "الولد لبس البنطلون الاسود ولبّس صاحبه بلوزة زرقا."), - ("יצאתי מהבית אבל לא הוצאתי את התיק הירוק, אל תדאג! אני אוציא אותו אחר כך.", "طلعت من البيت بسّ ما طلّعت الشّنطة الخضرا، ولا يهمّك! انا رح اطلّعها بعدين."), - ("שאלתי את הילדה האם היא הבינה או לא, היא אמרה שהמורה לא הסבירה לה.", "سألت البنت اذا هيّ فهمت#f ولّا لا، هيّ قالت#f انّه المعلّمة ما فهّمتها#f.", "שאלתי את הילדה האם היא הבינה או לא, היא אמרה שהמוֹרָה לא הסבירה לה."), - ("כאשר שתיתי תה בביתי חברי נכנס אליי, אז השקיתי אותו תה.", "لمّا شربت شاي في بيتي صاحبي دخل لعندي فشرّبته شاي."), - ("את הגעת לכאן אחרי שביקרת את דודתך מצד אבא? הבאת גם את בנך?", "انتي وصلتي لهون بعد ما زرتي عمّتك#f؟ وصّلتي ابنك#f كمان؟", "אַתְּ הִגַּעְתְּ לכאן אחרי שֶׁבִּיקַּרְתְּ את דּוֹדָתֵךְ מצד אבא? הֵבֵאת גם את בְּנֵךְ?"), - ("המנהלת חזרה היום מהשוק אבל לא החזירה את בתה מבית הספר.", "المديرة رجعت#f اليوم من السّوق بسّ ما رجّعت#f بنتها من المدرسة."), - ("הבנתי שזו החולצה הכי יקרה פה, הסברתי למוכר שאני רוצה חולצה יותר זולה.", "فهمت انّه هادي اغلى بلوزة هون، فهّمت البيّاع انّه بدّي بلوزة ارخص."), - ("אני שמעתי שיר חדש ומיד השמעתי לחברי את השיר הזה.", "انا سمعت غنوة جديدة وعلى طول سمّعت صاحبي هادي الغنوة."), - ("כאשר ירדת מהבית הורדת את התיק הכתום? כמובן! תמיד אני לוקחת אותו איתי.", "لمّا نزلتي من البيت نزّلتي الشّنطة البرتقاليّة؟ طبعا! دايما انا باخدها معي.", "כאשר יָרַדְתְּ מהבית הוֹרַדְתְּ את התיק הכתום? כמובן! תמיד אני לוקחת אותו איתי."), - ("כאשר הגעתי הביתה, בעלי אמר לי: \"הארת את הבית!\" ושאל האם הבאתי את בתנו.", "لمّا وصلت على البيت، جوزي قالّي: نوّرتي البيت! وسأل اذا وصّلت بنتنا.", "כאשר הגעתי הביתה, בעלי אמר לי: \"הֵאַרְתְּ את הבית!\" ושאל האם הבאתי את בִּתֵּנוּ."), - ]), - ("vsay2", "פִעֵל מול פַעַّל: עברית ← ערבית ב'", "say", True, [ - ("לפני שבועיים שמעתי שני שירים והשמעתי לדודי (מצד אבא) שלושה שירים.", "قبل اسبوعين سمعت غنوتين وسمّعت عمّي تلات اغاني."), - ("שאלתי את המוכרת כמה החשבון, הוצאתי את ארנקי, נתתי לה כסף ויצאתי מהחנות.", "سألت البيّاعة قدّيش الحساب، طلّعت جزداني، اعطيتها مصاري وطلعت من المحلّ."), - ("אני לא הבנתי, השאלה לא מובנת, אולי תסביר לי?", "انا ما فهمت، السّؤال مش مفهوم، ممكن تفهّمني؟"), - ("חברי ואני צחקנו, כאשר חברתי נכנסה הצחקנו גם אותה.", "انا وصاحبي ضحكنا، لمّا صاحبتي دخلت#f ضحّكناها كمان."), - ("בת דודי מצד אמא הגיעה לביתי והביאה את בתה איתה.", "بنت خالي وصلت#f على بيتي ووصّلت#f بنتها معها."), - ("מחמוד ירד מביתו, ראה את החתולה על העץ והוריד אותה משם.", "محمود نزل من بيته، شاف البسّة على الشّجرة ونزّلها من هناك."), - ("כאשר הם הגיעו לשכונה שלנו, שתינו תה והשקינו אותם קפה.", "لمّا همّ وصلو على حارتنا، شربنا شاي وشرّبناهم قهوة."), - ("מנהל המלון שמע רעש, אז יצא מהחדר והוציא את הילדים החוצה.", "مدير الاوتيل سمع ضجّة فطلع من الغرفة وطلّع الاولاد برّا."), - ("אתם שאלתם אותי שאלה חשובה, אחרי שהבנתם, אולי תסבירו לי?", "انتو سألتوني سؤال مهمّ، بعد ما فهمتو، ممكن تفهّموني؟"), - ("למה אחותך הקטנה ירדה מהכיסא והורידה את הכוס מהשולחן? כעת הכוס שבורה.", "ليش اختك#f الصّغيرة نزلت#f عن الكرسي ونزّلت#f الكاسة عن الطّاولة؟ هلّق الكاسة مكسورة.", "למה אֲחוֹתֵךְ הקטנה ירדה מהכיסא והורידה את הכוס מהשולחן? כעת הכוס שבורה."), - ("הילד לבש חולצה לבנה, אמרתי לך שאמו לא הלבישה אותו חולצה שחורה.", "الولد لبس بلوزة بيضا، قلتلك#f انّه امّه ما لبّسته#f بلوزة سودا.", "הילד לבש חולצה לבנה, אמרתי לָךְ שאמו לא הלבישה אותו חולצה שחורה."), - ("אני לא צחקתי, את לא הצחקת אותי בכלל אתמול.", "انا ما ضحكت، انتي ما ضحّكتيني بالمرّة امبارح.", "אני לא צחקתי, אַתְּ לא הִצְחַקְתְּ אותי בכלל אתמול."), - ]), - ("story", "השיר: שַרֵّבְּ-נִי קַהְוֵה", "say", True, [ - ("השקה אותי קפה, פתח לי את ראשי!", "شرّبني#imp قهوة فتّح#imp لي راسي!"), - ("החזר לי את האצילות, החזר את רגשותיי!", "رجّع#imp لي النّخوة رجّع#imp احساسي!"), - ("תן לי סיגריה, הדלק לי אותה", "جيب لي سيجارة ولّع#imp لي ايّاها"), - ("קרא לשכנה, נדבר איתה", "عيّط#imp للجارة نحكي معاها"), - ("אני רוצה לומר לה כל אשר בראשי", "بدّي احكيلها كلّ اشي ببالي"), - ("בכדי שראשי יישאר, אני רגוע", "تيضلّ له راسي انا مرتاح"), - ("ראי, לו שבוע לא דיברנו", "شوفي لو جمعة ما حكينا"), - ("ובמהירות, ולא יכולת לשכוח אותנו", "وبسرعة ولا قدرتي تنسينا", "ובמהירות, ולא יָכֹלְתְּ לשכוח אותנו"), - ("לידיעתי הרבה זמן אהבת אותנו", "بعلمي زمان كنتي تحبّينا", "לידיעתי הרבה זמן אָהַבְתְּ אותנו"), - ("מה עוד יש מלבד זה?", "شو يالّي غير هالشي؟"), - ("אין שום אישה שיכולה להחליף אותה", "ما في مرة بتقدر تخلفها"), - ("לא משנה מה קרה, זה לא ירחיק אותנו", "مهما جرى مش رح تبعدنا"), - ("לו לא הייתה האהבה, לא היינו מתאהבים", "لو ما الهوا ما كنّا عشقنا"), - ("לא צריך שנכעס על שום דבר", "ما لازم نزعل على ايّ اشي"), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 4): [ # the workbook's thirteenth lesson - ("new", "מילים חדשות", "learn", True, [ - ("בעיה", "مشكلة"), ("בטוב", "بخير"), ("שניות", "ثواني"), ("די, נגמר", "خلاص"), ("בית הקפה", "المقهى"), - ("ליטר", "ليتر"), ("הטלפון", "البلفون"), ("הפתגם", "المثل"), ("מישהו", "حدّ"), ("אלפים", "آلاف"), - ("כן", "نعم"), ("טוב", "خير"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("verbs", "בניין שני: חמישה פעלים", "say", False, [(f"{he} (עבר, עתיד)", f"{past}، {future}") for he, past, future in FAAAL_2_4]), - ("past", "חשב ושר בעבר", "say", False, _past(VERBS_2_4)), - ("vread", "הבניין השני: ערבית ← עברית א'", "read", True, [ - ("אמא שלי שילמה למוכר והודתה לו, אחר כך היא חשבה ושאלה אותי: \"כמה שילמתי לו?\"", "امّي دفعت#f للبيّاع وشكرته#f، بعدين هيّ فكّرت#f وسألتني#f: قدّيش انا دفعتله؟"), - ("שלשום דיברתי עם חבר שלך מרואן, הוא אמר שהוא רוצה לשיר בחתונה.", "اوّل امبارح حكيت مع صاحبك مروان، هوّ قال انّه بدّه يغنّي في العرس."), - ("לפני שבוע תיקנתי את דלת הבית של דודה שלי, לא לקחתי ממנה כסף בכלל.", "قبل جمعة صلّحت باب بيت خالتي، ما اخدت منها مصاري بالمرّة."), - ("מחמוד עדכן אותי שהמכונית במוסך, בעל המוסך תיקן אותה.", "محمود خبّرني انّه السّيّارة في الكراج، صاحب#of الكراج صلّحها."), - ("סליחה, כמה החשבון? באמת לא יקר, חשבתי שייצא יקר יותר.", "لو سمحتي، قدّيش الحساب؟ والله مش غالي، فكّرت انّه رح يطلع اغلى."), - ("אני חשבתי קצת, בא לי לשיר בחתונה של דוד שלי מחמד.", "انا فكّرت شويّ، جاي على بالي اغنّي في عرس عمّي محمّد."), - ("רגע! מי הכין את האוכל? אחותי הכינה את האוכל! באמת האוכל הכי טעים בעולם!", "لحظة! مين حضّر الاكل؟ اختي حضّرت#f الاكل! والله اطيب اكل#n بالعالم!"), - ("אני צריך לחזור לבית שלי ולעדכן את אשתי שחבר שלי סמיר בדרך.", "انا لازم ارجع على بيتي واخبّر مرتي انّه صاحبي سمير عالطّريق."), - ("אני אמרתי לה שאנחנו רוצים לשתות קפה שחור לפני שנתקן את החלון.", "انا قلتلها انّه بدّنا نشرب قهوة سادة قبل ما نصلّح الشّبّاك."), - ("אתם רוצים לאכול צהריים איתי? היום הכנתי את הקישוא הממולא הכי טוב בעולם.", "بدّكم تتغدّو معي؟ اليوم حضّرت احلى كوسا محشي بالعالم."), - ("כאשר הגענו לאמא שלי, אני אמרתי לה שהייתה בעיה במכונית.", "لمّا وصلنا لعند امّي، انا قلتلها انّه كانت مشكلة في السّيّارة."), - ("אני לא שילמתי, אני רוצה לשלם אחרי שאראה שתיקנת את הבעיה.", "انا ما دفعت، بدّي ادفع بعد ما اشوف انّه صلّحت المشكلة."), - ("יש לי שאלה חשובה, השאלה הכי חשובה בעולם, למה יצאתם מהבית לפני שאבוא?", "عندي سؤال مهمّ، اهمّ سؤال بالعالم، ليش طلعتو من البيت قبل ما اجي؟"), - ]), - ("vread2", "הבניין השני: ערבית ← עברית ב'", "read", True, [ - ("אני תיקנתי את המכונית פעמיים, מספיק! צריך לקנות מכונית חדשה.", "انا صلّحت السّيّارة مرّتين، خلاص! لازم اشتري سيّارة جديدة."), - ("אל תדאג יקירי, אני אחשוב טוב מאוד לפני שאלך אליו מחר.", "ولا يهمّك حبيبي، انا رح افكّر كتير منيح قبل ما اروح لعنده بكرا."), - ("בעוד שעה בערך נגיע הביתה ונכין אוכל, בסדר?", "كمان تقريبا ساعة رح نوصل على البيت ونحضّر اكل#n، تمام؟"), - ("בעל הבית לא רוצה לתקן את הבעיה, לכן נצא ממנו בעוד יומיים.", "صاحب#of البيت ما بدّه يصلّح المشكلة، عشان هيك رح نطلع منّه كمان يومين."), - ("אנחנו שרנו בבית שלך כמו ששרנו במסיבה, היינו שמחים מאוד.", "احنا غنّينا في بيتك زيّ ما غنّينا في الحفلة، كنّا مبسوطين كتير."), - ("אני שאלתי את בעל המוסך מה מצב המכונית, הוא אמר שהוא תיקן אותה.", "انا سألت صاحب#of الكراج شو وضع السّيّارة، هوّ قال انّه هوّ صلّحها."), - ("אני רוצה לשיר את השיר \"הצבעים\" ואחר כך נשיר עוד שיר.", "بدّي اغنّي غنوة#of الالوان وبعدين رح نغنّي كمان غنوة."), - ("אמא שלי שלחה את אח שלי כדי שישאל אותי אם אני רוצה שהיא תכין לי קפה או תה.", "امّي بعثت#f اخوي عشان يسألني اذا بدّي انّه هيّ تحضّرلي قهوة ولّا شاي."), - ("חשבתי שבאת לבדך אבל אחר כך הבנתי שבאת עם אחותך.", "فكّرت انّه اجيتي لحالك#f بسّ بعدين فهمت انّه اجيتي مع اختك#f."), - ("אני אבקר את סבתא וסבא שלי, הם גרים בשכונה רחוקה, צבע הבית שלהם כחול.", "انا رح ازور جدّتي وجدّي، همّ ساكنين في حارة بعيدة، لون بيتهم ازرق."), - ("אני רוצה לחשוב לפני שאשחק איתך עוד פעם שש-בש, אתה הכי חרוץ בשכונה.", "بدّي افكّر قبل ما العب معك كمان مرّة طاولة، انت اشطر واحد في الحارة."), - ("אני תיקנתי אותו בערך ארבע עשרה פעמים, הבנתי שצריך לקנות אחד חדש יותר.", "انا صلّحته تقريبا اربعتعشر مرّة، فهمت انّه لازم اشتري واحد اجدد."), - ("אני עדכנתי את אבא שלי שהמוכר נתן לי ארבעים ושישה שקלים עודף.", "انا خبّرت ابوي انّه البيّاع اعطاني ستّة واربعين شيكل فراطة."), - ]), - ("vsay", "הבניין השני: עברית ← ערבית א'", "say", True, [ - ("חשבתי שאנחנו רוצים לדבר על העבודה, לא על המשפחה.", "فكّرت انّه بدّنا نحكي عن الشّغل، مش عن العيلة."), - ("מי רוצה לשיר איתי בחתונה? אני רוצה לשיר איתך כמו בכל יום שישי.", "مين بدّه يغنّي معي في العرس؟ انا بدّي اغنّي معك زيّ كلّ يوم الجمعة.", "מי רוצה לשיר איתי בחתונה? אני רוצה לשיר אִיתְּךָ כמו בכל יום שישי."), - ("דיברתי עם נור, היא אמרה לי: \"אל תדאג, אני תיקנתי את הכיסא, הוא לא שבור!\"", "حكيت مع نور، هيّ قالتلي: ولا يهمّك، انا صلّحت الكرسي، هوّ مش مكسور!"), - ("את רוצה לקנות ירקות בשוק? אני רוצה להכין פלפל ממולא.", "بدّك#f تشتري خضرة في السّوق؟ انا بدّي احضّر فلفل محشي.", "אַתְּ רוצה לקנות ירקות בשוק? אני רוצה להכין פלפל ממולא."), - ("בעל המסעדה דיבר עם בנו ושאל אותו מדוע הוא לא שר במסיבה.", "صاحب#of المطعم حكى مع ابنه وسأله ليش هوّ ما غنّى في الحفلة."), - ("המורה עדכן אותי שהוא לא יבוא מחר ולא מחרתיים לבית הספר.", "المعلّم خبّرني انّه هوّ مش رح يجي بكرا ولا بعد بكرا على المدرسة."), - ("אני שרתי שיר ארוך, השיר הכי ארוך בעולם, מה את שרת?", "انا غنّيت غنوة طويلة، اطول غنوة بالعالم، شو انتي غنّيتي؟", "אני שרתי שיר ארוך, השיר הכי ארוך בעולם, מה אַתְּ שַׁרְתְּ?"), - ("לפני שבוע תיקנתי את החלון, למחרת תיקנתי את הדלת.", "قبل اسبوع صلّحت الشّبّاك، تاني يوم صلّحت الباب."), - ("כאשר הייתי בבית, חמודי וסמיר ישבו בסלון ושרו יחד שיר יפה.", "لمّا كنت في البيت، حمّودي وسمير قعدو في الصّالون وغنّو مع بعض غنوة حلوة."), - ("אחרי שאתקן את הבעיה אתה תשלם לי, אל תדאג!", "بعد ما اصلّح المشكلة انت رح تدفعلي، ولا يهمّك!"), - ("כמו שחשבתי, הוא לא הגיע היום, הוא עדכן אותנו שהוא יבוא מחר בעזרת ה'.", "زيّ ما فكّرت، هوّ ما وصل اليوم، هوّ خبّرنا انّه رح يجي بكرا انشالله."), - ("אני עדכנתי אותך שאני לא אקנה היום שני קילו חציל, איך לא קנית?", "انا خبّرتك#f انّه انا مش رح اشتري اليوم تنين كيلو بيتنجان، كيف ما اشتريتي؟", "אני עדכנתי אוֹתָךְ שאני לא אקנה היום שני קילו חציל, איך לא קָנִית?"), - ("אמי הכינה בצל ממולא, האוכל הכי טעים בעולם.", "امّي حضّرت#f بصل محشي، اطيب اكل#n بالعالم."), - ]), - ("vsay2", "הבניין השני: עברית ← ערבית ב'", "say", True, [ - ("המוכר אמר שלא שילמת לו בכלל, אני חשבתי ששילמת לו.", "البيّاع قال انّه ما دفعتيله بالمرّة، انا فكّرت انّه دفعتيله.", "המוכר אמר שלא שִׁילַּמְתְּ לו בכלל, אני חשבתי שֶׁשִּׁילַּמְתְּ לו."), - ("מי שר איתך כאן? זה היה בן דודי מצד אבא, שמו עאטף, הוא נשוי למרים.", "مين غنّى معك هون؟ هادا كان ابن عمّي، اسمه عاطف، هوّ متجوّز مريم.", "מי שר אִיתְּךָ כאן? זה היה בן דודי מצד אבא, שמו עאטף, הוא נשוי למרים."), - ("סלח לי, אתה גר בשכונה? אני רוצה לתקן את המכונית הלבנה.", "لو سمحت، انت ساكن في الحارة؟ انا بدّي اصلّح السّيّارة البيضا."), - ("אני חשבתי שהילד רץ עם בת השיח', הבנתי שהם רצו ביחד למכולת.", "انا فكّرت انّه الولد ركض مع بنت الشّيخ، فهمت انّه همّ ركضو مع بعض على البقالة."), - ("אמרתי למוכר שבא לי לחשוב לפני שאקנה את השולחן.", "قلت للبيّاع انّه جاي على بالي افكّر قبل ما اشتري الطّاولة."), - ("בעלי שאל אותי האם אני רוצה ללכת לבית קפה, אמרתי: מתאים!", "جوزي سألني اذا بدّي اروح على المقهى، قلت: ماشي!"), - ("היא חשבה קצת ואחר כך אמרה: \"סלחי לי, אני צריכה לרוץ!\"", "هيّ فكّرت#f شويّ وبعدين قالت#f: اسمحيلي، انا لازم اركض!"), - ("הילדים היו אתמול בבית הספר ושמעו שהמורה חזרה לביתה.", "الاولاد كانو امبارح في المدرسة وسمعو انّه المعلّمة رجعت#f على بيتها.", "הילדים היו אתמול בבית הספר ושמעו שהמוֹרָה חזרה לביתה."), - ("בעל המוסך לא תיקן את המכונית היום, הוא צריך לתקן אותה מחר.", "صاحب#of الكراج ما صلّح السّيّارة اليوم، هوّ لازم يصلّحها بكرا."), - ("השכן עדכן אותך על העבודה החדשה? כן, הוא עדכן אותי.", "الجار خبّرك عن الشّغل الجديد؟ اه، هوّ خبّرني.", "השכן עדכן אוֹתְךָ על העבודה החדשה? כן, הוא עדכן אותי."), - ("אנחנו רוצים להגיע לבית דודתי מצד אמא כדי לבשל סלט מטבוחה.", "بدّنا نوصل على بيت خالتي عشان نطبخ سلطة#of مطبوخة."), - ("המורה שאלה אותי מה המצב, אמרתי לה: \"השבח לאל, הכול טוב!\"", "المعلّمة سألتني#f شو الوضع، قلتلها: الحمد للّه كلّه تمام!", "המוֹרָה שאלה אותי מה המצב, אמרתי לה: \"השבח לאל, הכול טוב!\""), - ("מדוע לא חשבת לפני שלקחת את הכסף איתך, אתה צריך לחשוב!", "ليش ما فكّرت قبل ما اخدت المصاري معك، انت لازم تفكّر!", "מדוע לא חָשַׁבְתָּ לפני שֶׁלָּקַחְתָּ את הכסף אִיתְּךָ, אתה צריך לחשוב!"), - ]), - ("rel", "ש: אִנّוֹ / מַא / אִלִّי", "say", True, [ - ("הוא אמר שהוא עייף", "هوّ قال انّه هوّ تعبان"), ("הוא שמע שבית הספר הזה טוב", "هوّ سمع انّه هادي المدرسة منيحة"), - ("הם הבינו שאנחנו לא נמצאים בבית", "همّ فهمو انّه احنا مش موجودين في البيت"), - ("המנהל ראה שאני לא הייתי מרוצה ממנו", "المدير شاف انّه انا ما كنتش مبسوط منّه"), - ("לפני שאצא מהבית שלו", "قبل ما اطلع من بيته"), - ("אני קניתי חולצה אחרי שאתה קנית את המכנסיים", "انا اشتريت بلوزة بعد ما انت اشتريت البنطلون"), - ("כמו שאני הבנתי, היא לא תבוא היום לעבודה", "زيّ ما انا فهمت هيّ مش رح تيجي اليوم على الشّغل"), - ("הילד שהלך", "الولد الّي راح"), ("המכונית שהייתה בחוץ", "السّيّارة الّي كانت برّا"), ("השולחן שאתה שברת", "الطّاولة الّي انت كسرتها"), - ("הילדה ששתתה חצי ליטר מים", "البنت الّي شربت#f نصّ ليتر ميّ"), ("הטלפון שאני קניתי יפה", "البلفون الّي انا اشتريت حلو"), - ("הכיסא שהיה שבור", "الكرسي الّي كان مكسور"), ("המורה שדיברה איתך בחמישה לחמש", "المعلّمة الّي حكت معك على ساعة خمسة الّا خمسة", "המוֹרָה שדיברה אִיתְּךָ בחמישה לחמש"), - ]), - ("rread", "אִנّוֹ / מַא / אִלִّי: ערבית ← עברית א'", "read", True, [ - ("כמו שחשבתי, אתה לא הבנת את הדברים שלי כאשר דיברנו ביום שבת, אני צריך לומר לך עוד פעם, מחר נדבר בשעה עשרים לאחת עשרה.", "زيّ ما فكّرت، انت ما فهمت كلامي لمّا حكينا في يوم السّبت، انا لازم اقولّك كمان مرّة، بكرا رح نحكي على ساعة حدعش الّا تلت."), - ("אנחנו הכנו שקשוקה, לפני שנאכל אותה נשתה תה בלי סוכר, כמו שאמר לנו הדוקטור נביל: \"חמש דקות לפני שתאכלו צריך לשתות תה בלי סוכר\".", "احنا حضّرنا شكشوكة، قبل ما ناكلها رح نشرب شاي بدون سكّر، زيّ ما قالنا الدّكتور نبيل: خمس دقايق قبل ما تاكلو لازم تشربو شاي بدون سكّر."), - ("הלכתי לתקן את המכונית שלי, בעל המוסך אמר לי שהבעיה היא אותה הבעיה שהייתה למכונית של חבר שלי מחמוד, ואמר לי שאני צריך לחזור בעוד שעתיים.", "رحت اصلّح سيّارتي، صاحب#of الكراج قالّي انّه المشكلة هيّ نفس المشكلة الّي كانت لسيّارة#of صاحبي محمود، وقالّي انّه لازم ارجع كمان ساعتين."), - ("עברתי לבית חדש בשכונה חדשה, היא קרובה לבית הספר שהבן שלי חושב לעבור אליו בעוד חודש.", "نقلت على بيت جديد في حارة جديدة، هيّ قريبة من المدرسة الّي ابني بيفكّر ينقل عليها كمان شهر."), - ("רציתי לקנות מכנסיים לפני שאראה אותך, תראה! אלה המכנסיים שקניתי, צבע המכנסיים שלי כחול, קצת יותר בהיר מהמכנסיים שלך.", "كان بدّي اشتري بنطلون قبل ما اشوفك، شوف! هادا البنطلون الّي اشتريته، لون بنطلوني ازرق، شويّة#of افتح من بنطلونك."), - ("ראיתי שאכלת צהריים לבדך, למה לא אמרת לי? הייתי אוכל צהריים איתך! אני אוהב לאכול צהריים עם אנשים כמוך.", "شفت انّه تغدّيت لحالك، ليش ما قلتلي؟ كنت بتغدّى معك! انا بحبّ اتغدّى مع ناس زيّك."), - ("המורה שמע שאני לא הגעתי אתמול וגם שלשום, לכן הוא דיבר עם אבא שלי, אבא שלי עדכן אותו שאני בסדר והכול טוב.", "المعلّم سمع انّه انا ما وصلت امبارح وكمان اوّل امبارح، عشان هيك هوّ حكى مع ابوي، ابوي خبّره انّه انا بخير وكلّ اشي تمام."), - ]), - ("rread2", "אִנّוֹ / מַא / אִלִّי: ערבית ← עברית ב'", "read", True, [ - ("השכן שלי לקח ממני קילו טחינה ואמר לי שאחרי שיכין את הפלאפל יבוא לכאן ויביא לנו פלאפל ונאכל יחד.", "جاري اخد منّي كيلو طحينة وقالّي انّه بعد ما يحضّر الفلافل يجي لهون ويجيب لنا فلافل وناكل مع بعض."), - ("אנחנו שלחנו את המכתב לאח שלי אחרי שדיברנו איתו, הבנו שצריך לשלוח לו לפני שתהיה בעיה.", "احنا بعثنا المكتوب لاخوي بعد ما حكينا معه، فهمنا انّه لازم نبعثله قبل ما تصير مشكلة."), - ("ליד הבית שלך היה איש בן שלושים וחמש בערך, הוא היה מתקן את החלון שבת דודה שלי שברה.", "حدّ بيتك كان زلمة عمره خمسة وتلاتين سنة تقريبا، هوّ كان بيصلّح الشّبّاك الّي بنت خالتي كسرته#f."), - ("אחרי ששמעתי איך אתה שר אני אמרתי לאשתי ששרת כמו הזמר הכי גדול בעולם.", "بعد ما سمعت كيف انت بتغنّي انا قلت لمرتي انّه غنّيت زيّ اكبر مغنّي بالعالم."), - ("\"קשור את החמור כמו שאומר לך בעליו\", זה הפתגם שאני אוהב לומר לאח הקטן שלי.", "اربط#imp الحمار زيّ ما بيقولّك صاحبه، هادا المثل الّي انا بحبّ اقول لاخوي الصّغير."), - ("המוכר בא אליי ואמר שאני צריכה לקנות לבעלי את החולצה הזאת, אחר כך אחותי באה ואמרה לי בדיוק כמו שאמר המוכר.", "البيّاع اجاني وقال انّه لازم اشتري لجوزي هادي البلوزة، بعدين اختي اجت وقالتلي بالزّبط زيّ ما قال البيّاع."), - ("הכנתי לך קפה כמו שאתה אוהב, קצת סוכר בלי חלב, אל תדאג, נשתה את הקפה במרפסת.", "حضّرتلك قهوة زيّ ما انت بتحبّ، شويّة#of سكّر بدون حليب، ولا يهمّك رح نشرب القهوة في البلكونة."), - ]), - ("rsay", "אִלִّי וְאִנّוֹ: עברית ← ערבית א'", "say", True, [ - ("הילדה שביקרה את אחותי אמרה שהיא לא רעבה אחרי שהיא אכלה מקלובה.", "البنت الّي زارت اختي قالت#f انّه هيّ مش جوعانة بعد ما هيّ اكلت#f مقلوبة."), - ("אחי אמר לי שהוא רוצה לראות אותי מחר, אמרתי לו שאני צריך להיות בעבודה.", "اخوي قالّي انّه بدّه يشوفني بكرا، قلتله انّه انا لازم اكون في الشّغل."), - ("הילדים שהיו אתמול בבית הספר שמעו שהמורה הלכה לביתה אחרי שהיא אמרה להם בוקר טוב.", "الاولاد الّي كانو امبارح في المدرسة سمعو انّه المعلّمة راحت على بيتها بعد ما هيّ قالتلهم صباح الخير.", "הילדים שהיו אתמול בבית הספר שמעו שהמוֹרָה הלכה לביתה אחרי שהיא אמרה להם בוקר טוב."), - ("אבי חשב שדיברתי עם דודי מצד אבא ואמר לי שאם אני רוצה משהו ממנו אני צריך לעדכן אותו לפני שאדבר עם דודי.", "ابوي فكّر انّه حكيت مع عمّي وقالّي انّه اذا بدّي اشي منّه انا لازم اخبّره قبل ما احكي مع عمّي."), - ("מי אמר לך שהוא שילם? בעל המסעדה לא אמר לי דבר, לכן חשבתי שהוא שילם.", "مين قالّك انّه هوّ دفع؟ صاحب#of المطعم ما قالّي اشي، عشان هيك فكّرت انّه هوّ دفع.", "מי אמר לְךָ שהוא שילם? בעל המסעדה לא אמר לי דבר, לכן חשבתי שהוא שילם."), - ("שמעתי שביקרת אצלי בעבודה, מדוע לא עדכנת אותי? המנהל אמר שאתה יצאת מהעבודה לפני חצי שעה והלכת הביתה.", "سمعت انّه زرتني في الشّغل، ليش ما خبّرتني؟ المدير قال انّه انت طلعت من الشّغل قبل نصّ ساعة ورحت على البيت.", "שמעתי שֶׁבִּיקַּרְתָּ אצלי בעבודה, מדוע לא עִדְכַּנְתָּ אותי? המנהל אמר שאתה יָצָאתָ מהעבודה לפני חצי שעה וְהָלַכְתָּ הביתה."), - ("אשתי אמרה לי שהיא נתנה לך את הכסף, לא קיבלת? כן, לפני שיצאתי היא נתנה לי חמישה עשר שקלים.", "مرتي قالتلي انّه هيّ اعطتك المصاري، ما اخدت؟ اه، قبل ما اطلع هيّ اعطتني خمستعشر شيكل.", "אשתי אמרה לי שהיא נתנה לְךָ את הכסף, לא קִיבַּלְתָּ? כן, לפני שיצאתי היא נתנה לי חמישה עשר שקלים."), - ]), - ("rsay2", "אִלִّי וְאִנّוֹ: עברית ← ערבית ב'", "say", True, [ - ("שכנתי חדשה בשכונה, היא אמרה לי שבעל הבית צריך לתקן את הדלת, אבל יש בעיה, הוא גר רחוק, יותר רחוק מרהט.", "جارتي جديدة في الحارة، هيّ قالتلي انّه صاحب#of البيت لازم يصلّح الباب، بسّ في مشكلة، هوّ ساكن بعيد، ابعد من رهط."), - ("דיברנו אתמול, חשבתי שאמרת שאת לא צריכה לדבר איתי היום. טוב, אפשר לדבר איתך אחרי השעה שמונה ורבע?", "حكينا امبارح، فكّرت انّه قلتي انّه انتي مش لازم تحكي معي اليوم. طيّب، ممكن احكي معك#f بعد السّاعة تمانية وربع؟", "דיברנו אתמול, חשבתי שֶׁאָמַרְתְּ שֶׁאַתְּ לא צריכה לדבר איתי היום. טוב, אפשר לדבר אִיתָּךְ אחרי השעה שמונה ורבע?"), - ("אמרתי לך בעבר, אני אומר לך כעת ואני אומר לך בכל יום, אני לא רוצה לשתות את הקפה שאתה הכנת.", "قلتلك زمان، انا بقولّك هلّق وانا بقولّك كلّ يوم، انا ما بدّي اشرب القهوة الّي انت حضّرتها.", "אמרתי לְךָ בעבר, אני אומר לְךָ כעת ואני אומר לְךָ בכל יום, אני לא רוצה לשתות את הקפה שאתה הֵכַנְתָּ."), - ("חברי שאל את אחותו מה צריך להביא לחתונה, היא מדברת עם בן דודה מצד אמא האם הוא רוצה לשיר בחתונה או לא, היא תעדכן אותי אחרי שתדבר איתו.", "صاحبي سأل اخته شو لازم نجيب على العرس، هيّ بتحكي مع ابن خالها اذا بدّه يغنّي في العرس ولّا لا، هيّ رح تخبّرني بعد ما تحكي معه."), - ("כאשר חזרתי הביתה ראיתי שאמי באה אליי, דיברתי איתה עשר דקות ונכנסנו הביתה, אחר כך היא נתנה לי את המפתח שקיבלה משכנתי.", "لمّا رجعت على البيت شفت انّه امّي اجت لعندي، حكيت معها عشر دقايق ودخلنا على البيت، بعدين هيّ اعطتني المفتاح الّي اخدته#f من جارتي."), - ("איפה היית? את שיחקת עם בת דודתך מצד אמא? לא, אני שיחקתי עם הילדה שעברה לשכונה לפני חודש.", "وين كنتي؟ انتي لعبتي مع بنت خالتك#f؟ لا، انا لعبت مع البنت الّي نقلت#f على الحارة قبل شهر.", "איפה הָיִית? אַתְּ שִׂיחַקְתְּ עם בת דּוֹדָתֵךְ מצד אמא? לא, אני שיחקתי עם הילדה שעברה לשכונה לפני חודש."), - ("אנחנו גרים לא רחוק מתל אביב, אני חושב שלא ראינו אותה יותר מחודשיים.", "احنا ساكنين مش بعيد عن تلّ ابيب، انا بفكّر انّه ما شفناها اكتر من شهرين."), - ]), - ("words", "מילים לשיחת טלפון", "say", True, [ - ("זמן עבר, תקופה", "زمان"), ("הרבה זמן שלא ראיתי אותך!", "زمان عنّك!", "הרבה זמן שלא ראיתי אוֹתְךָ!"), ("מצב", "وضع"), ("אותו, אותה", "ايّاه، ايّاها"), - ("יום (שעות האור)", "نهار"), ("יומך שמח!", "نهارك سعيد!", "יוֹמְךָ שמח!"), ("קח!", "خد!"), ("מנוחה", "راحة"), - ("קח את הזמן!", "خد راحتك!"), ("עט, עטים", "قلم، اقلام"), ("דחוף, הכרחי", "ضروري"), ("חי מי ששמע את קולך!", "عاش مين سمع صوتك!", "חי מי ששמע את קוֹלְךָ!"), - ]), - ("talk", "שיחת טלפון: שאדי ויוסף", "say", True, [ - ("הלו מחמד? מה שלומך? מה החדשות? הכול טוב?", "الو محمّد؟ كيفك؟ شو الاخبار؟ كلّه تمام؟", "הלו מחמד? מה שְׁלוֹמְךָ? מה החדשות? הכול טוב?"), - ("שלום שלום, מי איתי?", "هلا هلا، مين معي؟"), - ("סלח לי, זה לא המספר של מחמד?", "اسمحلي، هادا مش رقم محمّد؟"), - ("לא לא, דודי, אני לא מחמד אבל אתה צודק! זה המספר שלו, כלומר זה היה המספר שלו מזמן, אבל הוא נתן לי את המספר שלו, אני יוסף, הבן של מחמד, אתה רוצה שאתן לך את המספר החדש שלו?", "لا لا يا عمّي، انا مش محمّد بسّ معك حقّ! هادا رقمه، يعني كان رقمه ايّام زمان، بسّ هوّ اعطاني رقمه، انا يوسف ابنه لمحمّد، بدّك اعطيك رقمه الجديد؟", "לא לא, דודי, אני לא מחמד אבל אתה צודק! זה המספר שלו, כלומר זה היה המספר שלו מזמן, אבל הוא נתן לי את המספר שלו, אני יוסף, הבן של מחמד, אתה רוצה שאתן לְךָ את המספר החדש שלו?"), - ("שלום עליכם, יוסף! מה המצב?", "السّلام عليكم يا يوسف! شو الوضع؟"), - ("אני בסדר, השבח לאל!", "انا بخير الحمد للّه!"), - ("באמת חשבתי שאתה אבא שלך, הוא לא עדכן אותי שהוא נתן לך אותו. אפשר שתיתן לי את המספר החדש שלו? אני צריך לדבר איתו דחוף!", "والله فكّرتك ابوك، هوّ ما خبّرني انّه اعطاك ايّاه. ممكن تعطيني رقمه الجديد؟ انا لازم احكي معه ضروري!", "באמת חשבתי שאתה אבא שֶׁלְּךָ, הוא לא עדכן אותי שהוא נתן לְךָ אותו. אפשר שתיתן לי את המספר החדש שלו? אני צריך לדבר איתו דחוף!"), - ("כמובן! אין בעיה! אני נותן לך את המספר שלו, רק טוב בעזרת ה', הכול טוב?", "طبعا! فش مشكلة! بعطيك رقمه، خير انشالله، كلّه تمام؟", "כמובן! אין בעיה! אני נותן לְךָ את המספר שלו, רק טוב בעזרת ה', הכול טוב?"), - ("כן, השבח לאל, שניות בבקשה, שניות איתך, אני מביא עט.", "اه الحمد للّه، ثواني لو سمحت، ثواني معك، بجيب قلم.", "כן, השבח לאל, שניות בבקשה, שניות אִיתְּךָ, אני מביא עט."), - ("אל תדאג, לאט לאט, קח את הזמן יקירי, אני לא ממהר בכלל.", "ولا يهمّك شويّ شويّ، خد راحتك حبيبي، انا مش مستعجل بالمرّة."), - ("טוב, מה המספר שלו?", "تمام، شو رقمه؟"), - ("אפס חמש תשע, שתיים, שני אפסים, ששת אלפים ושמונה.", "صفر خمسة تسعة، تنين، صفرين، ستّ آلاف وتمانية."), - ("תודה, ה' ישמור אותך!", "شكرا الله يخلّيك!", "תודה, ה' ישמור אוֹתְךָ!"), - ("יקירי! ה' ישמור עליך! יומך שמח!", "حبيبي! الله يحفظك! نهارك سعيد!", "יקירי! ה' ישמור עָלֶיךָ! יוֹמְךָ שמח!"), - ]), - ("talk2", "שיחת טלפון: שאדי ומחמד", "say", True, [ - ("הלו מחמד?", "الو محمّد؟"), - ("כן! מי איתי? מי מדבר?", "اي نعم! مين معي؟ مين بيحكي؟"), - ("שלום יקירי! איתך שאדי אבראהים!", "مرحبا عزيزي! معك شادي ابراهيم!", "שלום יקירי! אִיתְּךָ שאדי אבראהים!"), - ("שלום שלום, אהוב לבי, חי מי ששמע את קולך! איפה אתה נמצא? בוא אליי הביתה!", "اهلين حبيب قلبي، عاش مين سمع صوتك! انت وين موجود؟ تعال لعندي على بيتي!", "שלום שלום, אהוב לבי, חי מי ששמע את קוֹלְךָ! איפה אתה נמצא? בוא אליי הביתה!"), - ("היום אני לא קרוב לבית שלך, אבל אני חושב שאהיה קרוב יותר מחר. אתה תהיה בבית בשתים עשרה וחצי בערך?", "اليوم انا مش قريب من بيتك، بسّ انا بفكّر انّه رح اكون اقرب بكرا. بتكون في البيت على تنعش ونصّ تقريبا؟", "היום אני לא קרוב לבית שֶׁלְּךָ, אבל אני חושב שאהיה קרוב יותר מחר. אתה תהיה בבית בשתים עשרה וחצי בערך?"), - ("כן! אהיה פה, אבל עדכן אותי לפני שתבוא כדי שאחזור מהעבודה.", "اه! بكون هون، بسّ خبّرني#imp قبل ما تيجي عشان ارجع من الشّغل."), - ("כמובן שאעדכן אותך, אל תדאג! אתה גר בבית שנמצא ליד המוסך הגדול, נכון? כי אני רוצה לקחת את המכונית לתקן אותה במוסך.", "طبعا بخبّرك، ولا يهمّك! انت ساكن في البيت الّي موجود جنب الكراج الكبير، صحّ؟ عشان بدّي آخد السّيّارة اصلّحها في الكراج.", "כמובן שאעדכן אוֹתְךָ, אל תדאג! אתה גר בבית שנמצא ליד המוסך הגדול, נכון? כי אני רוצה לקחת את המכונית לתקן אותה במוסך."), - ("נכון!", "صحّ!"), - ("טוב! בעזרת ה' נראה אותך מחר!", "تمام! انشالله نشوفك بكرا!", "טוב! בעזרת ה' נראה אוֹתְךָ מחר!"), - ("ה' איתך!", "الله معك!", "ה' אִיתְּךָ!"), - ("להתראות!", "مع السّلامة!"), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 5): [ # the workbook's fourteenth lesson - ("new", "מילים חדשות", "learn", True, [ - ("מלח", "ملح"), ("המנוע", "الماتور"), ("מהנדס", "مهندس"), ("שמן", "زيت"), ("מתנות", "هدايا"), - ("בגדים", "اواعي"), ("נורה", "لمبة"), ("כבוד", "احترام"), ("הפנצ'ריה", "البنشر"), ("החתן", "العريس"), - ("הכלה", "العروس"), ("הארוחה", "الوجبة"), ("בשבילך", "منشانك"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("verbs", "גזרת ע\"י ופעלי הסיפור", "say", False, [(f"{he} (עבר, עתיד)", f"{past}، {future}") for he, past, future in BASES_2_5]), - ("past", "הביא ונהיה בעבר", "say", False, _past(VERBS_2_5)), - ("pres", "מביא ונהיה בהווה", "say", False, _present(PRESENT_2_5)), - ("sar", "השימושים של צַאר", "say", True, [ - ("מה קרה לך, יקירי?", "شو صارلك حبيبي؟", "מה קרה לְךָ, יקירי?"), ("לא קרה לי דבר", "ما صارلي اشي"), - ("אתה כבר חצי שעה פה?", "صارلك نصّ ساعة هون؟"), ("היא נהייתה עורכת דין", "هيّ صارت محاميّة"), ("אני נהייתי עייף", "انا صرت تعبان"), - ("מוסא התחיל לצחוק", "موسى صار يضحك"), ("כמה זמן אתה כבר גר פה?", "قدّيش صارلك ساكن هون؟"), - ("כאשר אגדל אני רוצה להיות מהנדס", "لمّا اكبر بدّي اصير مهندس"), - ("בת כמה את וכמה זמן את כבר עורכת דין?", "قدّيش عمرك#f وقدّيش صارلك#f محاميّة؟", "בת כמה אַתְּ וכמה זמן אַתְּ כבר עורכת דין?"), - ("אני בת שלושים ותשע, אני כבר תשע עשרה שנה עורכת דין!", "عمري تسعة وتلاتين سنة، صارلي تسعتعشر سنة محاميّة!"), - ]), - ("vread", "גזרת ע\"י: ערבית ← עברית א'", "read", True, [ - ("השכן שלי הוסיף לי סוכר, אמרתי לו שלא יוסיף, תמיד הוא מוסיף לי ואני אומר לו: \"דודי, אני שותה בלי סוכר!\"", "جاري زاد لي سكّر، قلتله انّه ما يزيد، دايما هوّ بيزيدلي وانا بقولّه: يا عمّي، انا بشرب بدون سكّر!"), - ("אני לא מכרתי את המכונית שלקחתי מאח שלי, רציתי למכור אותה מזמן, מספיק! היום אני מוכר אותה דחוף.", "انا ما بعت السّيّارة الّي اخدتها من اخوي، كان بدّي ابيعها من زمان، خلاص! اليوم انا ببيعها ضروري."), - ("חברה שלי הביאה לי מתנה, היא תביא גם לאחותה, תמיד כאשר היא חוזרת מבלגיה היא מביאה לנו מתנות.", "صاحبتي جابت لي هديّة، هيّ رح تجيب كمان لاختها، دايما لمّا هيّ بترجع من بلجيكا هيّ بتجيبلنا هدايا."), - ("דוד שלי כבר יותר משעה פה, מה יקרה איתו? כל יום ראשון כאשר דוד שלי רואה אותי הוא נהיה שמח.", "صار لعمّي اكتر من ساعة هون، شو رح يصير معه؟ كلّ يوم الاحد لمّا عمّي بيشوفني هوّ بيصير مبسوط."), - ("למה הוספת מלח לאוכל? לא צריך להוסיף מלח, אם את מוסיפה מלח, תוסיפי רק לאוכל שלך, בסדר?", "ليش زدتي ملح للاكل؟ مش لازم تزيدي ملح، اذا بتزيدي ملح، بدّك#f تزيدي بسّ لاكلك#f، تمام؟"), - ("מתי אתה נהיית מהנדס? חשבתי שרצית להיות עורך דין, העיקר שאתה נהיה שמח.", "ايمتا انت صرت مهندس؟ فكّرت انّه كان بدّك تصير محامي، المهمّ انّه انت بتصير مبسوط."), - ("סליחה, את הבאת לי את הספר שסיפרת לי עליו? תביאי לי את הספר מחר? כל שבוע את מביאה לי ספר חדש.", "لو سمحتي، انتي جبتيلي الكتاب الّي حكيتيلي عنّه؟ رح تجيبيلي الكتاب بكرا؟ كلّ اسبوع انتي بتجيبيلي كتاب جديد."), - ("אני הוספתי טחינה לפלאפל, אני אוהב להוסיף טחינה, כל יום שלישי כאשר אני אוכל פה אני מוסיף טחינה.", "انا زدت طحينة للفلافل، انا بحبّ ازيد طحينة، كلّ يوم التّلاتا لمّا انا باكل هون انا بزيد طحينة."), - ]), - ("vread2", "גזרת ע\"י: ערבית ← עברית ב'", "read", True, [ - ("ילד! הבאת איתך כסף היום? אתה לא רוצה להביא לי את הכסף? אתה מביא לי כסף ואחר כך אשלח לאבא שלך שלושה קילו תפוחים ירוקים.", "يا ولد! جبت معك مصاري اليوم؟ ما بدّك تجيبلي المصاري؟ انت بتجيبلي مصاري وبعدين رح ابعث لابوك تلاتة كيلو تفّاح اخضر."), - ("מה קרה לך אתמול בחתונה? אל תדאג, יהיה טוב בעזרת ה', תמיד נהיה טוב.", "شو صارلك امبارح في العرس؟ ولا يهمّك رح يصير خير انشالله، دايما بيصير خير."), - ("המוכר מכר את החולצה שאני אמרתי לו שלא ימכור אותה, המוכר הזה בא לכאן ומוכר בגדים כל יום חמישי.", "البيّاع باع البلوزة الّي انا قلتله انّه ما يبيعها، هادا البيّاع بيجي لهون وبيبيع اواعي كلّ يوم الخميس."), - ("נהיית רעב? תהיה רעב אחרי שתגיע לבית שלי, אתה מתחיל לאכול כל פעם כאשר אתה מבקר אותי.", "صرت جوعان؟ رح تصير جوعان بعد ما توصل على بيتي، انت بتصير تاكل كلّ مرّة لمّا بتزورني."), - ("אתה הוספת שמן למכונית, אתה רוצה להוסיף גם מים? אני שמעתי שאתה מוסיף כל יומיים שמן ומים.", "انت زدت زيت للسّيّارة، بدّك تزيد ميّ كمان؟ انا سمعت انّه انت بتزيد كلّ يومين زيت وميّ."), - ("אתה נהיית חרוץ, מחר תהיה בן שמונה, יום אחרי יום אתה נהיה חרוץ יותר.", "انت صرت شاطر، بكرا بيصير عمرك تمانية، يوم بعد يوم انت بتصير اشطر."), - ("למה מכרת את הבית, אמרתי לך שעדיף לך שלא תמכרי את הבית, טוב, לא חשוב, בכמה את מוכרת את המכונית שלך?", "ليش بعتي البيت، قلتلك#f انّه احسنلك#f ما تبيعي البيت، طيّب مش مهمّ، بقدّيش بتبيعي سيّارتك#f؟"), - ]), - ("vsay", "גזרת ע\"י: עברית ← ערבית", "say", True, [ - ("דודי מצד אמא הוסיף לי מלח, אני אמרתי לו שלא יוסיף לי עוד מלח, תמיד הוא מוסיף מלח לקישוא הממולא.", "خالي زاد لي ملح، انا قلتله انّه ما يزيدلي كمان ملح، دايما هوّ بيزيد ملح للكوسا المحشي."), - ("אתה נהיית חזק, אתה רוצה להיות יותר חזק? יום אחרי יום אתה נהיה חזק יותר וגדול יותר.", "انت صرت قوي، بدّك تصير اقوى؟ يوم بعد يوم انت بتصير اقوى واكبر.", "אתה נִהְיֵיתָ חזק, אתה רוצה להיות יותר חזק? יום אחרי יום אתה נהיה חזק יותר וגדול יותר."), - ("המורה הביא לי ספר, הוא רוצה להביא לך מחר עוד ספר, בכל יום רביעי בשעה רבע לחמש הוא מביא לביתו ספר חדש.", "المعلّم جاب لي كتاب، بدّه يجيبلك بكرا كمان كتاب، كلّ يوم الاربعا على ساعة خمسة الّا ربع هوّ بيجيب على بيته كتاب جديد.", "המורה הביא לי ספר, הוא רוצה להביא לְךָ מחר עוד ספר, בכל יום רביעי בשעה רבע לחמש הוא מביא לביתו ספר חדש."), - ("אני כבר שנתיים רופאה, אני רוצה להיות הרופאה הכי חרוצה בבית החולים.", "صارلي سنتين دكتورة، بدّي اصير اشطر دكتورة في المستشفى."), - ("אני מכרתי את המשרד שהיה לי בחיפה, אני רוצה לקנות אחד חדש בירושלים.", "انا بعت المكتب الّي كان عندي في حيفا، بدّي اشتري واحد جديد في القدس."), - ("הבאתי לך את המתנה שסיפרתי לך עליה, אל תדאגי! מחר אני אביא לך עוד אחת, בכל חודש כאשר אני חוזר מיוון אני מביא לך מתנה.", "جبتلك#f الهديّة الّي حكيتلك#f عنها، ولا يهمّك#f! بكرا رح اجيبلك#f كمان واحدة، كلّ شهر لمّا برجع من اليونان بجيبلك#f هديّة.", "הבאתי לָךְ את המתנה שסיפרתי לָךְ עליה, אל תדאגי! מחר אני אביא לָךְ עוד אחת, בכל חודש כאשר אני חוזר מיוון אני מביא לָךְ מתנה."), - ("חברי אמר לי שהוא נהיה רעב ושהוא רוצה לאכול צהריים איתי, אמרתי לו שאני מחכה למחמוד, כאשר מחמוד יגיע אליי אני אעדכן אותו ונאכל צהריים ביחד.", "صاحبي قالّي انّه صار جوعان وانّه بدّه يتغدّى معي، قلتله انّه انا بستنّى محمود، لمّا محمود يوصل لعندي انا رح اخبّره ونتغدّى مع بعض."), - ]), - ("words", "מילים לסיפור המוסך", "say", True, [ - ("מיד", "على طول"), ("חבר או בעלים, חברים", "صاحب، اصحاب"), ("בעל הבית", "صاحب#of البيت"), ("בעלי האדמה", "اصحاب الارض"), - ("צד, שול הדרך", "جنب"), ("בערך (כך דבר)", "هيك اشي"), ("רעש", "ضجّة"), ("חתונה, חתן, כלה", "عرس، عريس، عروسة"), - ("בעיה, בעיות", "مشكلة، مشاكل"), ("חיכיתי לך", "استنّيتك", "חיכיתי לְךָ"), ("אני לא הולך לחכות לך היום", "مش رح استنّاك اليوم", "אני לא הולך לחכות לְךָ היום"), - ("אני כבר שעה מחכה לך פה", "صارلي ساعة بستنّاك هون", "אני כבר שעה מחכה לְךָ פה"), - ]), - ("story", "סיפור המוסך", "say", True, [ - ("ביום חמישי בלילה, בשעה אחת עשרה ועשרים בערך, כאשר חזרתי מהחתונה של חברי מרואן,", "يوم الخميس ليلة، ساعة حدعش وتلت تقريبا، لمّا انا رجعت من عرس صاحبي مروان،"), - ("שמעתי רעש מהמנוע, וראיתי שנורת המנוע נהייתה אדומה, והמכונית התחילה לעשות רעש.", "سمعت ضجّة من الماتور، وشفت انّه لمبة#of الماتور صارت حمرا، والسّيّارة صارت تعمل ضجّة."), - ("מיד עצרתי את המכונית בצד, דיברתי עם חברי אבו ויסאם,", "على طول وقّفت السّيّارة على الجنب، حكيت مع صاحبي ابو وسام،"), - ("הוא בעל מוסך אצלנו בכפר.", "هوّ صاحب#of كراج عنّا في البلد."), - ("הוא אמר לי: \"חכה שם, אני בא לעזור לך.\"", "هوّ قالّي: استنّى#imp هناك انا جاي اساعدك.", "הוא אמר לי: \"חכה שם, אני בא לעזור לְךָ.\""), - ("חיכיתי לו חצי שעה, והוא לא הגיע.", "استنّيته نصّ ساعة، وهوّ ما وصل."), - ("דיברתי איתו עוד פעם ואמרתי לו: \"דודי! אני כבר חצי שעה מחכה לך פה! מה קרה לך?", "حكيت معه كمان مرّة وقلتله: يا عمّي! صارلي نصّ ساعة بستنّاك هون! شو صارلك؟", "דיברתי איתו עוד פעם ואמרתי לו: \"דודי! אני כבר חצי שעה מחכה לְךָ פה! מה קרה לְךָ?"), - ("טוב, איפה אתה יקירי? באמת לא מתאים שכך אחכה לך פה כל הלילה!\"", "طيّب وينك#m حبيبي؟ والله ما بينفع هيك استنّاك هون طول اللّيلة!", "טוב, איפה אתה יקירי? באמת לא מתאים שכך אחכה לְךָ פה כל הלילה!\""), - ("הוא אמר: \"אל תדאג! בעוד רבע שעה אהיה אצלך.\"", "هوّ قال: ولا يهمّك! كمان ربع ساعة بكون عندك.", "הוא אמר: \"אל תדאג! בעוד רבע שעה אהיה אֶצְלְךָ.\""), - ("הוא בא ולקח את המכונית למוסך שלו ואחר כך לקח אותי לבית שלי.", "هوّ اجا واخد السّيّارة على كراجه وبعدين اخدني على بيتي."), - ("למחרת בשעה שלוש ורבע בערך, הוא דיבר איתי ואמר שהייתה בעיה קטנה במנוע, ותיקן אותה,", "تاني يوم على ساعة تلاتة وربع هيك اشي، هوّ حكى معي وقال انّه كانت مشكلة صغيرة بالماتور، وصلّحها،"), - ("אני שאלתי אותו: \"כמה החשבון?\"", "انا سألته: قدّيش الحساب؟"), - ("הוא אמר: \"אלף ומאתיים שקל, אבל בשבילך שבע מאות וחמישים וזהו.\"", "هوّ قال: الف وميتين شيكل، بسّ منشانك سبع ميّة وخمسين وبسّ.", "הוא אמר: \"אלף ומאתיים שקל, אבל בִּשְׁבִילְךָ שבע מאות וחמישים וזהו.\""), - ("בשעה עשרים לארבע אשתי לקחה אותי למוסך, אני שילמתי לו והודיתי לו מאוד ונהגתי הביתה.", "على ساعة اربع الّا تلت مرتي اخدتني#f على الكراج، انا دفعتله وشكرته كتير وسقت عالبيت."), - ]), - ("ask", "שאלות על סיפור המוסך", "read", True, [ - ("מתי חזר האיש ומאיפה חזר?", "وينتا رجع الزّلمة ومن وين رجع؟"), ("מה שמע האיש?", "شو سمع الزّلمة؟"), ("מה ראה האיש?", "شو شاف الزّلمة؟"), - ("במה עובד אבו ויסאם?", "شو بيشتغل ابو وسام؟"), ("איפה נמצא המוסך?", "وين موجود الكراج؟"), ("כמה זמן חיכה האיש?", "قدّيش وقت استنّى الزّلمة؟"), - ("מי לקח את האיש לבית שלו ומי לקח אותו למוסך?", "مين اخد الزّلمة على بيته ومين اخده على الكراج؟"), - ("מי אמר למי \"הייתה בעיה קטנה במנוע\"?", "مين قال لمين كانت مشكلة صغيرة بالماتور؟"), - ("כמה יצא החשבון?", "قدّيش طلع الحساب؟"), ("מי שילם? וכמה?", "مين دفع؟ وقدّيش؟"), - ]), - ("sread", "סיפור המוסך: ערבית ← עברית", "read", True, [ - ("סלח לי, מי בעל הבית? תראה! הדלת האדומה פתוחה.", "اسمحلي، مين صاحب#of البيت؟ شوف! الباب الاحمر مفتوح."), - ("היא דיברה איתי ואמרה לי שהייתה בעיה קטנה, לכן היא לא תגיע היום.", "هيّ حكت معي وقالتلي انّه كانت مشكلة صغيرة، عشان هيك مش رح توصل اليوم."), - ("אתמול אני שיחקתי עם אחי הבכור, מחר אני אשחק איתו, תמיד אני משחק איתו.", "امبارح انا لعبت مع اخوي البكر، بكرا انا رح العب معه، دايما بلعب معه."), - ("אתה רוצה שחבר שלך ייקח אותך או שאתה רוצה להגיע לבדך במכונית שלך?", "بدّك انّه صاحبك ياخدك ولّا بدّك توصل لحالك بسيّارتك؟"), - ("מי שילם על המנה? אני שילמתי אתמול, אתה רוצה לשלם היום?", "مين دفع حقّ الوجبة؟ انا دفعت امبارح، انت بدّك تدفع اليوم؟"), - ("לפני שתצא מהבית, תקשור את הכלב טוב ואחר כך תעדכן אותי.", "قبل ما تطلع من البيت، بدّك تربط الكلب منيح وبعدين بدّك تخبّرني."), - ("בחייך, כאשר אני מדבר איתך עמוד במקומך, קצת כבוד...", "وحياتك، لمّا انا بحكي معك وقّف#imp محلّك، شويّة#of احترام..."), - ("איפה את, אחותי? חיכיתי לך שעה וחצי ולא הגעת, למה לא באת לבקר אותי?", "وين انتي يا اختي؟ استنّيتك#f ساعة ونصّ وما وصلتي، ليش ما اجيتي تزوريني؟"), - ("שאלנו את אבא שלי מי שילם את החשבון, הוא אמר: \"אני שילמתי אותו, אל תדאג!\"", "سألنا ابوي مين دفع الحساب، هوّ قال: انا دفعته، ولا يهمّك!"), - ("הם לקחו את הילד לרופא כי הוא לא היה מרוצה בכלל.", "همّ اخدو الولد للدّكتور عشان هوّ كان مش مبسوط بالمرّة."), - ("כמה זמן הוא כבר מחכה לי במוסך? יותר משעתיים או לא?", "قدّيش صارله بيستنّاني في الكراج؟ اكتر من ساعتين ولّا لا؟"), - ("אני נתתי לבעל הבית את הכסף, הוא לקח את הכסף ממני שלשום.", "انا اعطيت صاحب#of البيت المصاري، هوّ اخد المصاري منّي اوّل امبارح."), - ]), - ("ssay", "סיפור המוסך: עברית ← ערבית", "say", True, [ - ("חברי תיקן את התקר ואחרי חצי שעה הגענו למכונית ולקחנו אותה.", "صاحبي صلّح البنشر وبعد نصّ ساعة وصلنا على السّيّارة واخدناها."), - ("לפני שיצאתי מהבית דיברתי עם בעל המסעדה, הוא אמר: \"אני מחכה לכם\".", "قبل ما اطلع من البيت حكيت مع صاحب#of المطعم، هوّ قال: انا بستنّاكم."), - ("אני לא אוהב לחכות לך כל יום שעתיים וחצי.", "انا ما بحبّ استنّاك كلّ يوم ساعتين ونصّ.", "אני לא אוהב לחכות לְךָ כל יום שעתיים וחצי."), - ("מחר בשעה שש אני רוצה לבקר אותך בבית, אני רוצה לראות מה המצב.", "بكرا على ساعة ستّة بدّي ازورك في البيت، بدّي اشوف شو الوضع.", "מחר בשעה שש אני רוצה לבקר אוֹתְךָ בבית, אני רוצה לראות מה המצב."), - ("אני שאלתי אותך ואת לא שמעת, הכול טוב, בעלך עדכן אותי לפני עשר דקות.", "انا سألتك#2f وانتي ما سمعتي، كلّه تمام، جوزك#f خبّرني قبل عشر دقايق.", "אני שאלתי אוֹתָךְ וְאַתְּ לא שָׁמַעְתְּ, הכול טוב, בַּעֲלֵךְ עדכן אותי לפני עשר דקות."), - ("החשבון הוא מאה ארבעים ושמונה שקלים, בשבילך רק מאה וארבעים.", "الحساب ميّة وتمانية واربعين شيكل، منشانك بسّ ميّة واربعين.", "החשבון הוא מאה ארבעים ושמונה שקלים, בִּשְׁבִילְךָ רק מאה וארבעים."), - ("סבי אמר לי שהוא רוצה לעצור בשול, מיד עצרתי ושאלתי אותו מה קרה?", "جدّي قالّي انّه بدّه يوقّف على الجنب، على طول وقّفت وسألته شو صار؟"), - ("סלח לי, כמה שילמת למוכר? אני נתתי לו שלוש מאות עשרים וארבעה שקלים והוא לא החזיר לי שקל.", "لو سمحت، قدّيش دفعت للبيّاع؟ انا اعطيته تلات ميّة واربعة وعشرين شيكل وهوّ ما رجّعلي شيكل.", "סלח לי, כמה שִׁילַּמְתָּ למוכר? אני נתתי לו שלוש מאות עשרים וארבעה שקלים והוא לא החזיר לי שקל."), - ("כאשר הגעתי לבית הוריי הם אמרו לי שהדלת שבורה ואין מי שיתקן אותה.", "لمّا وصلت على بيت اهلي همّ قالولي انّه الباب مكسور وما في مين يصلّحه."), - ("לקחת את המכתב משכנתך? לא, הלכתי אליה אבל היא לא הייתה בבית.", "اخدتي المكتوب من جارتك#f؟ لا، رحت لعندها بسّ هيّ ما كانت في البيت.", "לָקַחְתְּ את המכתב מִשְּׁכֶנְתֵּךְ? לא, הלכתי אליה אבל היא לא הייתה בבית."), - ("החתן חזר יחד עם הכלה לבית הוריו אחרי החתונה, חברי נהג במכונית.", "العريس رجع مع العروس على بيت اهله بعد العرس، صاحبي ساق السّيّارة."), - ("לסבתי יש בעיה במכונית, אני חושב שזה מהמנוע, שמעתי רעש וראיתי נורה אדומה.", "ستّي عندها مشكلة في السّيّارة، انا بفكّر انّه هادا من الماتور، سمعت ضجّة وشفت لمبة حمرا."), - ]), - ("adj", "תואר ברבים: יחיד, יחידה, רבים", "say", True, [ - ("גבוה, גבוהה, גבוהים", "طويل، طويلة، طوال"), ("קצר, קצרה, קצרים", "قصير، قصيرة، قصار"), ("גדול, גדולה, גדולים", "كبير، كبيرة، كبار"), - ("קטן, קטנה, קטנים", "صغير، صغيرة، صغار"), ("הרבה (יחיד, יחידה, רבים)", "كتير، كتيرة، كتار"), ("חדש, חדשה, חדשים", "جديد، جديدة، جداد"), - ("השולחנות גדולים", "الطّاولات كبيرة"), ("הילדים גדולים", "الاولاد كبار"), ("שלוש דירות גדולות", "تلات شقق كبار"), - ("השולחנות האלה גדולים", "هدول الطّاولات كبار"), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 6): [ # the workbook's fifteenth lesson - ("new", "מילים חדשות", "learn", True, [ - ("טובה, ידוע", "معروف"), ("ישנה", "قديمة"), ("אשתו", "زوجته"), ("הווטרינר", "البيطري"), ("שאלות", "اسئلة"), - ("בטוח", "اكيد"), ("האנשים", "النّاس"), ("עין", "عين"), ("התמרים", "التّمر"), ("הדקל", "النّخلة"), - ("עתיקה", "عتيقة"), ("זולה", "رخيصة"), ("יקרה", "غالية"), ("אבל", "لكن"), ("דינר", "دينار"), - ("המסכן", "المسكين"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("verbs", "הבניין השלישי: חמישה פעלים", "say", False, [(f"{he} (עבר, עתיד)", f"{past}، {future}") for he, past, future in BASES_2_6]), - ("past", "עזר ומצא בעבר", "say", False, _past(VERBS_2_6)), - ("pres", "עוזר ומוצא בהווה", "say", False, _present(PRESENT_2_6)), - ("vread", "הבניין השלישי: ערבית ← עברית א'", "read", True, [ - ("אתמול בלילה אני עזרתי לחברה שלי, היום אני צריך לעזור לאחותי, כל יום ראשון אני עוזר לחמותי.", "امبارح ليلة انا ساعدت صاحبتي، اليوم انا لازم اساعد اختي، كلّ يوم الاحد انا بساعد حماتي."), - ("ביום חמישי בשעה רבע לאחת אני מצאתי מאה שקלים, היום אני רוצה למצוא מאתיים שקלים, כל יום שישי אני פוגש את חבר שלי בבית הקפה.", "في يوم الخميس على ساعة واحدة الّا ربع انا لاقيت ميّة#of شيكل، اليوم بدّي الاقي ميتين شيكل، كلّ يوم الجمعة انا بلاقي صاحبي في المقهى."), - ("הרופאה טיפלה בי אתמול ואמרה שהיא תטפל בי מחר, היא מטפלת בי כל יום רביעי בשעה תשע וחצי בדיוק.", "الدّكتورة عالجتني#f امبارح وقالت#f انّه هيّ رح تعالجني بكرا، هيّ بتعالجني كلّ يوم الاربعا على ساعة تسعة ونصّ بالزّبط."), - ("שלשום בשעה שש וחצי עשינו את הממולאים הכי טובים, אנחנו צריכים לעשות עוד פעם ממולאים, אנחנו עושים ממולאים כל יום חמישי בשעה רבע לשמונה.", "اوّل امبارح على ساعة ستّة ونصّ ساوينا احلى محشي، احنا لازم نساوي كمان مرّة محشي، احنا منساوي محشي كلّ يوم الخميس على ساعة تمانية الّا ربع."), - ("אתה לא מצאת את המכנסיים בבית? אולי תמצא את המכנסיים מתחת למיטה? אתה תמיד מוצא את המכנסיים שם.", "انت ما لاقيت البنطلون في البيت؟ ممكن تلاقي البنطلون تحت التّخت؟ انت دايما بتلاقي البنطلون هناك."), - ("אני לא שמעתי, לכן לא עניתי לך, כמובן שאני רוצה לענות לך, אני עונה לך כל יום, יקירתי.", "انا ما سمعت عشان هيك ما جاوبتك#f، طبعا بدّي اجاوبك#f، انا بجاوبك#f كلّ يوم يا حبيبتي."), - ("האחות לא טיפלה בך כי היא לא הגיעה לבית החולים היום, אל תדאג! היא צריכה לטפל בך כאשר היא תגיע בעוד עשרים דקות, היא תמיד מטפלת בך.", "الممرّضة ما عالجتك#she عشان هيّ ما وصلت#f على المستشفى اليوم، ولا يهمّك! هيّ لازم تعالجك لمّا هيّ توصل كمان تلت ساعة، هيّ دايما بتعالجك."), - ("הילד הזה הוא הילד הכי חרוץ בבית הספר, הוא עזר לאחותו, הוא אמר שהוא רוצה לעזור גם לי, הוא בדרך כלל עוזר לי.", "هادا الولد هوّ اشطر ولد في المدرسة، هوّ ساعد اخته، هوّ قال انّه بدّه يساعدني كمان، هوّ عادة بيساعدني."), - ]), - ("vread2", "הבניין השלישי: ערבית ← עברית ב'", "read", True, [ - ("אתמול אני עזרתי לאחי הבכור והוא הודה לי, מחר אני צריך לעזור לדוד שלי, כל יום שישי בשעה שתיים ועשרים אני עוזר לאבא שלי.", "امبارح انا ساعدت اخوي البكر وهوّ شكرني، بكرا انا لازم اساعد عمّي، كلّ يوم الجمعة على ساعة تنتين وتلت انا بساعد ابوي."), - ("פאטמה עשתה תה, היא תעשה קפה, כל יום רביעי בשעה רבע לארבע היא עושה קפה שחור ושותה עם בת דודתה.", "فاطمة ساوت شاي، هيّ رح تساوي قهوة، كلّ يوم الاربعا على ساعة اربع الّا ربع هيّ بتساوي قهوة سادة وبتشرب مع بنت خالتها."), - ("את לא ענית לי, למה את לא רוצה לענות לי? בבקשה, כאשר אני שואל אותך שאלה את עונה לי, בסדר? אל תדאגי, אני אענה לך.", "انتي ما جاوبتيني، ليش ما بدّك#f تجاوبيني؟ لو سمحتي، لمّا بسألك#f سؤال انتي بتجاوبيني، تمام؟ ولا يهمّك#f انا رح اجاوبك#f."), - ("היא אמרה שהיא לא מצאה את המפתח, היא לא תמצא את המפתח כי הוא לא פה, אל תדאג, היא תמיד מוצאת את המפתח בסוף.", "هيّ قالت#f انّه هيّ ما لاقت المفتاح، هيّ مش رح تلاقي المفتاح عشان هوّ مش هون، ولا يهمّك، هيّ دايما بتلاقي المفتاح بالآخر."), - ("אתמול אני עשיתי לאח שלי טובה, אני אעשה לבן של אחותי טובה, כל יום אני עושה בעיה למורה.", "امبارح انا ساويت لاخوي معروف، انا رح اساوي لابن اختي معروف، كلّ يوم انا بساوي مشكلة للمعلّم."), - ("חבר שלך מצא עבודה חדשה? הוא רוצה למצוא עבודה חדשה או לא? אם מישהו רוצה הוא מוצא! נכון או לא? כן! נכון מאוד!", "صاحبك لاقى شغل جديد؟ بدّه يلاقي شغل جديد ولّا لا؟ اذا واحد بدّه هوّ بيلاقي! صحّ ولّا لا؟ اه! صحّ ونصّ!"), - ("בעל החנות שאל מי שילם על האוכל? אני עניתי לו שאני שילמתי, גם מחר אני אענה לו, ואם יש לו שאלה כלשהי רק שישאל אותי ואני עונה לו.", "صاحب#of المحلّ سأل مين دفع على الاكل؟ انا جاوبته انّه انا دفعت، كمان بكرا انا رح اجاوبه، واذا عنده ايّ سؤال بسّ يسألني وانا بجاوبه."), - ("את עזרת לאמא שלך? לפני שתצאי מהבית תעזרי לה, את תמיד עוזרת לה, נכון?", "انتي ساعدتي امّك#f؟ قبل ما تطلعي من البيت بدّك#f تساعديها، انتي دايما بتساعديها، صحّ؟"), - ]), - ("vread3", "הבניין השלישי: ערבית ← עברית ג'", "read", True, [ - ("אני שאלתי אותו אם הוא קנה לי את החולצה הכחולה? הוא ענה לי שהוא לא קנה אותה, והוא יענה לאחותי את אותה התשובה, תמיד כאשר אני שואל אותו הוא עונה לי כך.", "انا سألته اذا اشترالي البلوزة الزّرقا؟ هوّ جاوبني انّه هوّ ما اشتراها، ورح يجاوب اختي نفس الجواب، دايما لمّا انا بسأله هوّ بيجاوبني هيك."), - ("מי טיפל בה היום? הוא יטפל בה גם מחר? מצוין! אני שמחה כי הוא מטפל בה.", "مين عالجها اليوم؟ هوّ رح يعالجها كمان بكرا؟ ممتاز! انا مبسوطة عشان هوّ بيعالجها."), - ("אני עשיתי מקלובה, אני אעשה קפה, כל יום שני בשעה עשרים לשתיים אני עושה תה מתוק ושותה עם בת דודתי.", "انا ساويت مقلوبة، انا رح اساوي قهوة، كلّ يوم التّنين على ساعة تنتين الّا تلت انا بساوي شاي حلو وبشرب مع بنت عمّتي."), - ("אנחנו ענינו למורה, אנחנו צריכים לענות לה, תמיד כאשר היא שואלת אותנו שאלות אנחנו עונים לה.", "احنا جاوبنا المعلّمة، احنا لازم نجاوبها، دايما لمّا هيّ بتسألنا اسئلة احنا منجاوبها."), - ("אתה מצאת את הגורל שלך בחיים? אתה תמצא את הגורל שלך בחיים! אל תדאג, כל אחד מוצא את הגורל שלו בחיים, וגם אתה בעזרת ה'!", "انت لاقيت نصيبك في الحياة؟ انت رح تلاقي نصيبك في الحياة! ولا يهمّك كلّ واحد بيلاقي نصيبه في الحياة، وانت كمان انشالله!"), - ("הווטרינר טיפל בכלב, הוא יטפל בו עוד פעם השבוע, כל שנה הוא מטפל בכלב פעמיים.", "البيطري عالج الكلب، هوّ رح يعالجه كمان مرّة هالاسبوع، كلّ سنة هوّ بيعالج الكلب مرّتين."), - ("אשתך מצאה את הילד? היא תמצא את הילד בשוק! אני אומר לך, בטוח היא מוצאת את הילד בשוק.", "مرتك لاقت الولد؟ هيّ رح تلاقي الولد في السّوق! انا بقولّك، اكيد هيّ بتلاقي الولد في السّوق."), - ]), - ("vsay", "הבניין השלישי: עברית ← ערבית", "say", True, [ - ("אני יצאתי מהבית ועזרתי לאחותי לפני שעתיים, אני אצא מהעבודה ואעזור למחמוד בשעה שתים עשרה ורבע, בכל יום חמישי אני יוצא מהמוסך ועוזר לאבי.", "انا طلعت من البيت وساعدت اختي قبل ساعتين، انا رح اطلع من الشّغل واساعد محمود على ساعة تنعش وربع، كلّ يوم الخميس انا بطلع من الكراج وبساعد ابوي."), - ("אשתי לא ענתה לי כאשר שאלתי אותה איפה נמצא המפתח, אני חושב שהיא תענה לי בעוד חמש דקות, בכל יום אני שואל אותה את אותה השאלה והיא עונה לי.", "مرتي ما جاوبتني#f لمّا سألتها وين موجود المفتاح، انا بفكّر انّه هيّ رح تجاوبني كمان خمس دقايق، كلّ يوم انا بسألها نفس السّؤال وهيّ بتجاوبني."), - ("אני פגשתי את חברי בשוק, אני רוצה לפגוש גם את בת דודי מצד אמא בשוק, בכל יום רביעי כאשר אני קונה כאן אני מוצא תפוחים ירוקים.", "انا لاقيت صاحبي في السّوق، بدّي الاقي كمان بنت خالي في السّوق، كلّ يوم الاربعا لمّا انا بشتري هون انا بلاقي تفّاح اخضر."), - ("הווטרינר טיפל בחתולה, הוא צריך לטפל בה שוב כאשר בעלי יחזור מהעבודה, רק הווטרינר הזה מטפל בה.", "البيطري عالج البسّة، هوّ لازم يعالجها كمان مرّة لمّا جوزي يرجع من الشّغل، بسّ هادا البيطري بيعالجها."), - ("כיוון שאת שאלת אותי אני עניתי לך, אני לא רוצה לענות לך גם בעוד חצי שעה, אני עונה לך בכל יום אבל את לא שומעת.", "عشان انتي سألتيني انا جاوبتك#f، انا ما بدّي اجاوبك#f كمان بعد نصّ ساعة، انا بجاوبك#f كلّ يوم بسّ انتي ما بتسمعي.", "כיוון שֶׁאַתְּ שָׁאַלְתְּ אותי אני עניתי לָךְ, אני לא רוצה לענות לָךְ גם בעוד חצי שעה, אני עונה לָךְ בכל יום אבל אַתְּ לא שומעת."), - ("סלח לי, אתה עזרת לבני לפני שעה, בערך בשעה עשר ורבע? אתה רוצה לעזור לי? המוכר אמר לי שאתה תמיד עוזר לאנשים.", "لو سمحت، انت ساعدت ابني قبل ساعة، تقريبا على ساعة عشرة وربع؟ بدّك تساعدني؟ البيّاع قالّي انّه انت دايما بتساعد النّاس.", "סלח לי, אתה עָזַרְתָּ לבני לפני שעה, בערך בשעה עשר ורבע? אתה רוצה לעזור לי? המוכר אמר לי שאתה תמיד עוזר לאנשים."), - ("אני הכנתי קפה שחור, אני רוצה לעשות תה מתוק, בכל יום שני בשעה תשע שלושים וחמש אני עושה קפה ושותה עם חברתי.", "انا ساويت قهوة سادة، بدّي اساوي شاي حلو، كلّ يوم التّنين على ساعة تسعة ونصّ وخمسة انا بساوي قهوة وبشرب مع صاحبتي."), - ]), - ("words", "מילים ומשפטים לסיפור הכד", "say", True, [ - ("דרך, דרכים", "طريق، طرق"), ("חזרה", "رجعة"), ("אדמה, ארץ", "ارض"), ("כנות", "صراحة"), - ("אני מדבר איתך בכנות!", "انا بحكي معك بصراحة!", "אני מדבר אִיתְּךָ בכנות!"), ("אני כן איתך", "انا صريح معك", "אני כֵּן אִיתְּךָ"), ("אולי, ייתכן", "يمكن"), - ("כסף", "مصاري"), ("מכסה", "غطا"), ("מכיר את הבאר ואת המכסה שלה", "بيعرف البير وغطاه"), ("סיר, והיא מצאה את המכסה שלה", "طنجرة ولاقت غطاها"), - ("שלום, אפשר לעזור לך?", "مرحبا، ممكن اساعدك؟", "שלום, אפשר לעזור לְךָ?"), - ("אשתי שאלה אותי מה עשיתי היום", "مرتي سألتني#f شو ساويت اليوم"), - ("מצאתי את מחמוד עוזר לו, אז הלכתי לטפל באמא שלי", "لاقيت محمود بيساعده، فرحت اعالج امّي"), - ]), - ("story", "סיפור הכד העתיק", "say", True, [ - ("יום אחד יצא מחמוד, קוראים לו גם אבו עאטף, מהבית שלו והלך לקנות ירקות ופירות בשוק.", "يوم من الايّام طلع محمود، بيقولوله كمان ابو عاطف، من بيته وراح يشتري خضرة وفواكه في السّوق."), - ("באמצע הדרך ראה מקום יפה ובו מעיין, קשר את הכלב שלו וישב מתחת לעץ התמר, כלומר מתחת לדקל.", "نصّ الطّريق شاف محلّ حلو فيه عين، ربط كلبه وقعد تحت شجرة#of التّمر، يعني تحت النّخلة."), - ("אחרי בערך עשרים דקות, הוא חשב לעצמו שאולי יש משהו מתחת לאדמה.", "بعد حوالي تلت ساعة، هوّ فكّر لنفسه انّه يمكن في اشي تحت الارض."), - ("אחרי שחפר קצת, הוא מצא כד עתיק, חשב לעצמו:", "بعد ما حفر شويّ، هوّ لاقى جرّة قديمة، فكّر لحاله:"), - ("\"מה אעשה איתו, באמת זה הכד הכי עתיק שראיתי בחיי,", "شو بدّي اعمل معها، والله هيّ اقدم جرّة الّي شفتها في حياتي،"), - ("אני בחיים לא ראיתי כד יפה כמוהו.\" טוב, הוא לקח אותו לבית שלו.", "انا عمري ما شفت جرّة حلوة زيّها. طيّب، اخدها على بيته."), - ("אמר לו אחד מהשכנים שלו: \"לך לאדון פואד עראפאת, אולי יקנה אותו ממך.\"", "قاله واحد من جيرانه: روح عند الاستاذ فؤاد عرفات، يمكن يشتريها منّك.", "אמר לו אחד מהשכנים שלו: \"לך לאדון פואד עראפאת, אולי יקנה אותו מִמְּךָ.\""), - ("ענה לו מחמוד: \"באמת רעיון טוב! בוא תעזור לי בחייך, ניקח אותו יחד אליו.\"", "جاوبه محمود: والله فكرة منيحة! تعال ساعدني#imp وحياتك، ناخدها مع بعض لعنده.", "ענה לו מחמוד: \"באמת רעיון טוב! בוא תעזור לי בְּחַיֶּיךָ, ניקח אותו יחד אליו.\""), - ("ענה לו השכן שלו: \"מוכן! אל תדאג! אני רק רוצה לעדכן את אשתי.\"", "جاوبه جاره: حاضر! ولا يهمّك! بدّي بسّ اخبّر مرتي."), - ("אז הגיע השכן שלו עם הבן הקטן שלו ועזר למחמוד לקחת אותו לבית של האדון פואד.", "فوصل جاره مع ابنه الصّغير وساعد محمود ياخدها لبيت الاستاذ فؤاد."), - ("כאשר הגיעו לבית שלו, פואד ראה את הכד היפה ואמר לו שבכנות זה כד עתיק ויפה מאוד,", "لمّا وصلو على بيته، فؤاد شاف الجرّة الحلوة وقاله انّه بصراحة هيّ جرّة عتيقة وحلوة كتير،"), - ("וגם הוא חושב שהוא לא זול, כלומר הוא יקר,", "وكمان هوّ بيفكّر انّه هيّ مش رخيصة، يعني هيّ غالية،"), - ("אבל הוא לא ישלם לו יותר ממאה ושבעים דינר, כי הוא לא מצא את המכסה שלו.", "لكن هوّ مش رح يدفعله اكتر من ميّة وسبعين دينار، عشان هوّ ما لاقى غطاها."), - ("ענה לו מחמוד שהוא צריך לחשוב קצת ואחר כך יחזור אליו.", "جاوبه محمود انّه هوّ لازم يفكّر شويّ وبعدين يرجعله."), - ("אחרי שדיבר עם אשתו, הוא הבין שהכסף יעזור להם, כך הם יטפלו בבן המסכן שלהם, ולמחרת אשתו שלחה אותו אליו.", "بعد ما حكى مع زوجته، هوّ فهم انّه المصاري رح تساعدهم، هيك همّ يعالجو ابنهم المسكين، وتاني يوم زوجته بعثته#f لعنده."), - ("מחמוד לבש חולצה לבנה ומכנסיים כחולים, והלך לבדו.", "محمود لبس بلوزة بيضا وبنطلون ازرق، وراح لحاله."), - ("כאשר הגיע לאדון פואד, עדכן אותו שאשתו אמרה מזל טוב!", "لمّا وصل لعند الاستاذ فؤاد، خبّره انّه مرته قالت#f مبروك!"), - ("האדון אמר: \"ה' יברך אותך!\" והכין קפה שחור, הם שתו, אחר כך הוא שילם לו את הכסף וכך הוא קנה את הכד.", "الاستاذ قال: الله يبارك فيك! وحضّر قهوة سادة، همّ شربو، بعدين هوّ دفعله المصاري وهيك هوّ اشترى الجرّة.", "האדון אמר: \"ה' יברך אוֹתְךָ!\" והכין קפה שחור, הם שתו, אחר כך הוא שילם לו את הכסף וכך הוא קנה את הכד."), - ("כל הדרך חזרה הביתה הוא שר לעצמו והודה לאלוהיו על מה שנתן לו, כאשר הגיע לבית שלו לקח את אשתו ואת הילדים שלו למסעדה גדולה וסיפר להם את הסיפור.", "كلّ الطّريق رجعة للبيت هوّ كان بيغنّي لحاله وشكر ربّه على الّي اعطاه، لمّا وصل على بيته اخد مرته واولاده على مطعم كبير وحكى لهم القصّة."), - ("הבת הקטנה שלו, בת חמש, ה' ישמור אותה, אמרה שגם היא תמצא כד עתיק גדול יותר מהכד שמצא אבא שלה, הם צחקו והיו שמחים.", "بنته الصّغيرة، عمرها خمسة، الله يخلّيها، قالت#f انّه هيّ كمان رح تلاقي جرّة قديمة اكبر من الجرّة الّي لاقاها ابوها، همّ ضحكو وكانو مبسوطين."), - ]), - ("ask", "שאלות על סיפור הכד", "read", True, [ - ("למחמוד יש עוד שם, מה הוא?", "محمود عنده كمان اسم، شو هوّ؟"), ("איפה ישב מחמוד?", "وين قعد محمود؟"), ("מי אמר לו ללכת ולמה?", "مين قاله يروح وليش؟"), - ("מה השכן שלו רוצה לעדכן את אשתו?", "شو جاره بدّه يخبّر مرته؟"), ("כמה הוא רוצה לשלם לו ולמה לא יותר?", "قدّيش بدّه يدفعله وليش مش اكتر؟"), - ("למה מחמוד ואשתו אמרו שהם רוצים לקחת את הכסף?", "ليش محمود ومرته قالو انّه بدّهم ياخدو المصاري؟"), - ("מה לבש מחמוד ומי הלך איתו?", "شو لبس محمود ومين راح معه؟"), ("מה הכין האדון עראפאת?", "شو حضّر الاستاذ عرفات؟"), - ("מה עשה מחמוד בדרך חזרה?", "شو عمل محمود في الطّريق رجعة؟"), - ("בת כמה הבת הקטנה שלו ומה אמרה להורים שלה?", "قدّيش عمر بنته الصّغيرة وشو قالت#f لاهلها؟"), - ]), - ("him", "אותו ואותה אחרי תנועה", "say", True, [ - ("את אכלת אותה", "انتي اكلتيها", "אַתְּ אָכַלְתְּ אותה"), ("אתם ראיתם אותה", "انتو شفتوها"), ("אנחנו פתחנו אותה", "احنا فتحناها"), ("הם ראו אותה", "همّ شافوها"), - ("את אכלת אותו", "انتي اكلتيه", "אַתְּ אָכַלְתְּ אותו"), ("אתם ראיתם אותו", "انتو شفتوه"), ("אנחנו פתחנו אותו", "احنا فتحناه"), ("הם ראו אותו", "همّ شافوه"), - ("ה' ישמור לך את הילדים שלך!", "الله يخلّيلك اولادك!", "ה' ישמור לְךָ את הילדים שֶׁלְּךָ!"), ("ה' ישמור אותך", "الله يخلّيك", "ה' ישמור אוֹתְךָ"), - ("ה' ישמור אותה", "الله يخلّيها"), ("ה' ישמור אותו", "الله يخلّيه"), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 7): [ # the workbook's sixteenth lesson - ("new", "מילים חדשות", "learn", True, [ - ("התחנה", "المحطّة"), ("מלאה", "مليانة"), ("צומת", "مفرق"), ("שקרן", "كذّاب"), ("המבחן", "الامتحان"), - ("הנושא", "الموضوع"), ("הדלק", "البنزين"), ("מהנדסת", "مهندسة"), ("חזקה", "قويّة"), ("דם", "دمّ"), - ("צעירים", "شباب"), ("חבל", "حبل"), ("השקר", "الكذب"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("imp", "הציווי", "say", True, [ - ("כנס!", "فوت!"), ("הבא!", "جيب!"), ("חשוב!", "فكّر#imp!"), ("עדכן אותי!", "خبّرني#imp!"), ("עזור לי!", "ساعدني#imp!"), - ("ילד, שמע!", "يا ولد اسمع#imp!"), ("ילדה, שמעי!", "يا بنت اسمعي#imp!"), ("ילדים, שמעו!", "يا اولاد اسمعو#imp!"), - ("שתה!", "اشرب#imp!"), ("שלם!", "ادفع#imp!"), ("חזור!", "ارجع#imp!"), ("שב!", "اقعد#imp!"), ("דבר!", "احكي#imp!"), ("קנה!", "اشتري#imp!"), - ("רוץ! רוצי! רוצו!", "اركض#imp! اركضي#imp! اركضو#imp!"), ("קח! קחי! קחו!", "خد! خدي! خدو!"), ("אכול! אכלי! אכלו!", "كل! كلي! كلو!"), - ("לך אליו וראה!", "روح لعنده وشوف!"), ("אל תשתה קולה ואל תשתה זירו", "لا تشرب كولا وما تشرب زيرو"), - ]), - ("verbs", "פעלים נוספים בגזרת פִעֵל ופעלי הסיפור", "say", False, [(f"{he} (עבר, עתיד)", f"{past}، {future}") for he, past, future in BASES_2_7]), - ("past", "כעס ושכח בעבר", "say", False, _past(VERBS_2_7)), - ("pres", "רץ וכועס בהווה", "say", False, _present(PRESENT_2_7)), - ("ex", "משפטים עם הפעלים החדשים", "say", True, [ - ("הילד עשה את עצמו לא יודע", "الولد عمل حاله مش عارف"), - ("אני כעסתי עליו ואמרתי לו שהוא צריך לעדכן אותה", "انا زعلت منّه وقلتله انّه هوّ لازم يخبّرها"), - ("בעזרת ה' הוא יצליח כמו שצריך, בלי בעיות", "انشالله رح ينجح زيّ ما لازم، بدون مشاكل"), - ("אל תכעס עליי יקירי! שכחתי לעדכן אותך שאני לא אבוא איתך למסיבה", "لا تزعل منّي حبيبي! نسيت اخبّرك انّه انا مش رح اجي معك على الحفلة", "אל תכעס עליי יקירי! שכחתי לעדכן אוֹתְךָ שאני לא אבוא אִיתְּךָ למסיבה"), - ("אל תדאג, אשתך חשבה שאתה תשכח ולכן היא עדכנה אותי", "ولا يهمّك، مرتك فكّرت#f انّه رح تنسى وعشان هيك هيّ خبّرتني#f", "אל תדאג, אִשְׁתְּךָ חשבה שאתה תשכח ולכן היא עדכנה אותי"), - ]), - ("fill", "עבר, עתיד, הווה וציווי: ערבית ← עברית א'", "read", True, [ - ("למה אתה עשית כך לחבר שלך? עדיף לך שלא תעשה כך בחיים, אם אתה עושה כך, האנשים אומרים שאתה שקרן, בחייך אל תעשה כך עוד פעם!", "ليش انت عملت هيك لصاحبك؟ احسنلك ما تعمل هيك بحياتك، اذا انت بتعمل هيك، النّاس بتقول انّك كذّاب، وحياتك لا تعمل هيك كمان مرّة!"), - ("אתם כעסתם על המורה? אני אומר שלא צריך לכעוס עליו, אם אתם כועסים עליו תעדכנו אותי, בחייכם אל תכעסו!", "انتو زعلتو من المعلّم؟ انا بقول انّه مش لازم تزعلو منّه، اذا انتو بتزعلو منّه بدّكم تخبّروني، وحياتكم لا تزعلو!"), - ("את הצלחת אתמול במבחן? אל תדאגי, את תצליחי באוניברסיטה, את חרוצה ממני ואת תמיד מצליחה, קדימה, תצליחי!", "انتي نجحتي امبارح في الامتحان؟ ولا يهمّك#f رح تنجحي في الجامعة، انتي اشطر منّي وانتي دايما بتنجحي، يالله انجحي#imp!"), - ("אתה חשבת על המצב? אתה צריך לחשוב לפני שתדבר איתה. בדרך כלל אתה חושב ואחר כך מדבר, קדימה, תחשוב על זה קצת!", "انت فكّرت على الوضع؟ انت لازم تفكّر قبل ما تحكي معها. عادة انت بتفكّر وبعدين بتحكي، يالله فكّر#imp فيها شويّ!"), - ("את הלכת לבית החולים? את תלכי לסבתא שלך היום. כל יום ראשון את הולכת אליה ואוכלת צהריים איתה, קדימה, לכי ואחר כך בואי גם אליי!", "انتي رحتي على المستشفى؟ انتي رح تروحي عند جدّتك#f اليوم. كلّ يوم الاحد انتي بتروحي لعندها وبتتغدّي معها، يالله روحي#imp وبعدين تعالي لعندي كمان!"), - ("מה אתם שתיתם כאשר ישבתם במסעדה? אתם צריכים לשתות שלושה ליטר מים היום, אתם תמיד שותים מים לפני שאתם אוכלים, בחייכם שתו איתי קפה!", "شو انتو شربتو لمّا قعدتو في المطعم؟ انتو لازم تشربو تلاتة ليتر ميّ اليوم، انتو دايما بتشربو ميّ قبل ما تاكلو، وحياتكم اشربو#imp معي قهوة!"), - ]), - ("fill2", "עבר, עתיד, הווה וציווי: ערבית ← עברית ב'", "read", True, [ - ("אתה שכחת את התיק במכונית הכחולה? אתה צריך לשכוח מהנושא אחרי שתביא לי את הכסף. בדרך כלל אתה שוכח ואחר כך בא אליי ושואל אותי איפה הארנק, קדימה, תשכח ממנו!", "انت نسيت الشّنطة في السّيّارة الزّرقا؟ انت لازم تنسى الموضوع بعد ما تجيبلي المصاري. عادة انت بتنسى وبعدين بتيجيني وبتسألني وين الجزدان، يالله انسى#imp منّه!"), - ("אתה לקחת מאח שלך את החולצה הלבנה? עדיף לך שלא תיקח ממנו בגדים בכלל, כאשר אתה לוקח ממנו בגדים הוא כועס עליך, בבקשה, קח ממני מה שאתה רוצה.", "انت اخدت من اخوك البلوزة البيضا؟ احسنلك ما تاخد منّه اواعي بالمرّة، لمّا انت بتاخد منّه اواعي هوّ بيزعل منّك، تفضّل خد منّي شو ما بدّك."), - ("אתם דיברתם על המורה? המורה אומר שלא צריך לדבר עליה, אם אתם מדברים עליה היא לא שומעת ולא עונה לכם, בחייכם אל תדברו עליה, דברו איתה, בסדר?", "انتو حكيتو عن المعلّمة؟ المعلّم بيقول انّه مش لازم تحكو عنها، اذا انتو بتحكو عنها هيّ ما بتسمع وما بتجاوبكم، وحياتكم لا تحكو عنها، احكو#imp معها، تمام؟"), - ("את שילמת למוכרת מאה שמונים ושבעה שקלים? אל תדאגי, את תשלמי אחרי שתקני, את תמיד משלמת, טוב, אני סיימתי, קדימה, שלמי בבקשה!", "انتي دفعتي للبيّاعة ميّة وسبعة وتمانين شيكل؟ ولا يهمّك#f انتي رح تدفعي بعد ما تشتري، انتي دايما بتدفعي، طيّب انا خلّصت، يالله ادفعي#imp من فضلك#f!"), - ("אתה נפלת על הראש? שמע ממני! אתה לא רוצה ליפול בבית הספר! בדרך כלל אתה נופל ואחר כך מעדכן אותי, תיזהר שלא תיפול!", "انت وقعت على راسك؟ اسمع#imp منّي! انت ما بدّك توقع في المدرسة! عادة انت بتوقع وبعدين بتخبّرني، دير بالك لا توقع!"), - ("את עזרת לאחותך? את צריכה לעזור לסבא שלך. כל יום שישי את עוזרת למשפחה, קדימה, עזרי לי ואחר כך נשב וגם נדבר!", "انتي ساعدتي اختك#f؟ انتي لازم تساعدي جدّك#f. كلّ يوم الجمعة انتي بتساعدي العيلة، يالله ساعديني#imp وبعدين نقعد ونحكي كمان!"), - ]), - ("isay", "עבר, עתיד, הווה וציווי: עברית ← ערבית", "say", True, [ - ("אתה לקחת את הילדה מבית הספר? לאן אתה רוצה לקחת אותה? אתה לוקח אותה בכל יום מהגן לבית, קח גם את התיק שלה!", "انت اخدت البنت من المدرسة؟ لوين بدّك تاخدها؟ انت بتاخدها كلّ يوم من البستان على البيت، خد كمان شنطتها!", "אתה לָקַחְתָּ את הילדה מבית הספר? לאן אתה רוצה לקחת אותה? אתה לוקח אותה בכל יום מהגן לבית, קח גם את התיק שלה!"), - ("את אכלת עם בת דודתך מצד אמא? את הולכת לאכול במסעדה היום? בכל יום שלישי את אוכלת אצל סבתך, קדימה, אכלי את האוכל שלך!", "انتي اكلتي مع بنت خالتك#f؟ انتي رح تاكلي في المطعم اليوم؟ كلّ يوم التّلاتا انتي بتاكلي عند ستّك#f، يالله كلي اكلك#f!", "אַתְּ אָכַלְתְּ עם בת דּוֹדָתֵךְ מצד אמא? אַתְּ הולכת לאכול במסעדה היום? בכל יום שלישי אַתְּ אוכלת אצל סָבָתֵךְ, קדימה, אִכְלִי את האוכל שֶׁלָּךְ!"), - ("אתם שכחתם את מחמוד בעבודה? אתם צריכים לשכוח מהמכונית הישנה! בכל חודש אתם שוכחים לשלם לבעל המכולת, קדימה, תשכחו ממני!", "انتو نسيتو محمود في الشّغل؟ انتو لازم تنسو من السّيّارة القديمة! كلّ شهر انتو بتنسو تدفعو لصاحب#of البقالة، يالله انسو#imp منّي!"), - ("המנהל אמר שעשיתי לו בעיות בעבודה? אני לא רוצה לעשות בעיות לאף אחד! בכל יום ראשון אני עושה טובה לשכן שלי. קדימה, עשה לי טובה!", "المدير قال انّه عملتله مشاكل في الشّغل؟ انا ما بدّي اعمل مشاكل لولا واحد! كلّ يوم الاحد انا بعمل معروف لجاري. يالله اعمل#imp لي معروف!"), - ("אתם ראיתם אותה? אתם צריכים לראות אותה מחר בשעה תשע ורבע, בכל שבוע ביום שני בבוקר אתם רואים אותי, קדימה, ראו אותי, אנשים!", "انتو شفتوها؟ انتو لازم تشوفوها بكرا على ساعة تسعة وربع، كلّ اسبوع يوم التّنين الصّبح انتو بتشوفوني، يالله شوفوني يا ناس!"), - ("על מה חשבת? את רוצה לחשוב ולדבר עם בעלך? את תמיד חושבת לפני שאת קונה משהו, חשבי על זה קצת!", "على ايش فكّرتي؟ بدّك#f تفكّري وتحكي مع جوزك#f؟ انتي دايما بتفكّري قبل ما تشتري اشي، فكّري#imp فيها شويّ!", "על מה חָשַׁבְתְּ? אַתְּ רוצה לחשוב ולדבר עם בַּעֲלֵךְ? אַתְּ תמיד חושבת לפני שֶׁאַתְּ קונה משהו, חִשְׁבִי על זה קצת!"), - ("אתה נהגת במכונית? אתה רוצה לנהוג בה בעוד יומיים? כל שבוע אתה נוהג במכונית ואחר כך אתה מספר לי, קדימה, נהג בה היום!", "انت سقت السّيّارة؟ بدّك تسوقها كمان يومين؟ كلّ اسبوع انت بتسوق السّيّارة وبعدين بتحكيلي، يالله سوقها اليوم!", "אתה נָהַגְתָּ במכונית? אתה רוצה לנהוג בה בעוד יומיים? כל שבוע אתה נוהג במכונית ואחר כך אתה מספר לי, קדימה, נְהַג בה היום!"), - ]), - ("words", "מילים לסיפור יוסף והאוטובוס", "say", True, [ - ("נפש, אותו ה...", "نفس"), ("אני בעצמי", "انا نفسي"), ("איש, אנשים", "انسان، ناس"), ("קו, קווים", "خطّ، خطوط"), ("לכן", "عشان هيك"), - ("סוף, אחרון", "آخر"), ("בסוף האוטובוס", "في آخر الباص"), ("מלא עד הסוף", "مليان عالآخر"), ("הפעם האחרונה", "آخر مرّة"), ("תחנה", "محطّة"), - ("תחנת האוטובוס ליד תחנת הדלק", "محطّة#of الباص جنب محطّة#of البنزين"), - ("מפה לבית שלך בערך מאתיים מטר", "من هون لبيتك ابو ميتين متر", "מפה לבית שֶׁלְּךָ בערך מאתיים מטר"), - ("היא הייתה בת עשרים ושמונה כאשר היא נהייתה מהנדסת", "صار عمرها تمانية وعشرين لمّا هيّ صارت مهندسة"), - ("מוסא התחיל לדבר איתי ואחר כך התחיל לצחוק", "موسى صار يحكي معي وبعدين صار يضحك"), - ]), - ("pre", "לפני הסיפור: עברית ← ערבית", "say", True, [ - ("הילדה התחילה לצחוק אחרי ששמעה שמחר היא תהיה בת שמונה בדיוק.", "البنت صارت تضحك بعد ما سمعت#f انّه بكرا بيصير عمرها تمانية بالزّبط."), - ("הבנתי שנהיית שמחה אחרי שהבנת שמחרתיים אני רוצה לבקר אותך.", "فهمت انّه صرتي مبسوطة بعد ما فهمتي انّه بعد بكرا بدّي ازورك#f.", "הבנתי שֶׁנִּהְיֵית שמחה אחרי שֶׁהֵבַנְתְּ שמחרתיים אני רוצה לבקר אוֹתָךְ."), - ("אני כבר שתי דקות כאן לבדי. אל תדאגי יקירתי, את לא תחכי לי יותר מעשר דקות.", "صارلي دقيقتين هون لحالي. ولا يهمّك#f حبيبتي، انتي مش رح تستنّيني اكتر من عشر دقايق."), - ("שאלתי את המוכרת כמה אני צריך לשלם לה, היא אמרה \"בשבילך מאה תשעים ושבעה שקלים וזהו\".", "سألت البيّاعة قدّيش انا لازم ادفعلها، هيّ قالت#f: منشانك ميّة وسبعة وتسعين شيكل وبسّ.", "שאלתי את המוכרת כמה אני צריך לשלם לה, היא אמרה \"בִּשְׁבִילְךָ מאה תשעים ושבעה שקלים וזהו\"."), - ("לדודי מצד אבא יש בעיה במכונית, לפי דעתי זה מהמנוע, שמעתי רעש חזק.", "عمّي عنده مشكلة في السّيّارة، حسب رايي هادا من الماتور، سمعت ضجّة قويّة."), - ("כאשר תיקנתי את הדלת חברי נכנס ואמר שיש לו אותה הבעיה בדיוק.", "لمّا صلّحت الباب صاحبي دخل وقال انّه عنده نفس المشكلة بالزّبط."), - ("לקחת את המכתב מהשכן שלי? לא, הלכתי אליו אבל הוא לא נתן לי את המכתב.", "اخدت المكتوب من جاري؟ لا، رحت لعنده بسّ هوّ ما اعطاني المكتوب.", "לָקַחְתָּ את המכתב מהשכן שלי? לא, הלכתי אליו אבל הוא לא נתן לי את המכתב."), - ("הילד התחיל לבכות כאשר ראה דם ביד שלו, אחרי שנישקתי אותו הוא נהיה שמח.", "الولد صار يبكي لمّا شاف دمّ في ايده، بعد ما بسته هوّ صار مبسوط."), - ]), - ("story", "סיפור יוסף והאוטובוס", "say", True, [ - ("לפני חודשיים בערך, יוסף יצא מהעבודה ודיבר עם חבר שלו ח'ליל שגר בטבריה,", "قبل شهرين تقريبا، يوسف طلع من الشّغل وحكى مع صاحبه خليل الّي ساكن في طبريّا،"), - ("יוסף אמר לו שהוא חושב לבוא לבקר אותו כי הרבה זמן לא ראה אותו, וגם הוא רוצה לשבת איתו ולספר לו ולעדכן אותו קצת על החיים שלו.", "يوسف قاله انّه هوّ بيفكّر يجي يزوره عشان زمان ما شافه وكمان بدّه يقعد معه ويحكيله ويخبّره شويّ عن حياته."), - ("הוא עצמו גר בחיפה על הר הכרמל, כלומר קצת רחוק מטבריה,", "هوّ نفسه ساكن في حيفا على جبل الكرمل، يعني شويّة#of بعيد عن طبريّا،"), - ("לכן יוסף צריך לקחת שלושה אוטובוסים.", "عشان هيك يوسف لازم ياخد تلات باصات."), - ("האוטובוס הראשון יורד בצומת פרדיס, שם צריך לחכות לקו תשע מאות שמונים ושתיים,", "اوّل باص بينزل على مفرق فرديس، هناك لازم يستنّى خطّ تسع ميّة تنين وتمانين،"), - ("והאוטובוס מגיע רק פעם אחת ביום בשעה שמונה בדיוק. לכן יוסף צריך להיות בשעה עשרה לשמונה בערך בתחנה.", "والباص بيجي بسّ مرّة واحدة باليوم على ساعة تمانية بالزّبط. عشان هيك يوسف لازم يكون على ساعة تمانية الّا عشرة هيك اشي في المحطّة."), - ("יוסף הגיע לתחנה בשעה רבע לשמונה והתחנה הייתה מלאה באנשים.", "يوسف وصل على المحطّة على ساعة تمانية الّا ربع والمحطّة كانت مليانة بالنّاس."), - ("יוסף חשב ואמר: \"מה אעשה, מה אעשה עכשיו? יש פה בערך חמישים איש לפניי,", "يوسف فكّر وقال: شو بدّي اعمل، ايش بدّي اسوّي اسّا؟ في هون ابو خمسين واحد من قبلي،"), - ("מי ייתן לי לעלות על האוטובוס במקומו?\"", "مين بدّه يعطيني اطلع عالباص محلّه؟"), - ("וכך חשב עשר דקות, ובסוף אמר: \"אם אני רוצה לעלות על האוטובוס אני צריך לעשות את עצמי כפצוע.\"", "وهيك فكّر عشر دقايق، وبالآخر قال: اذا بدّي اطلع على الباص لازم اعمل حالي كمجروح."), - ("וכך עשה, רק כאשר ראה את האוטובוס, יוסף נפל והתחיל לצרוח והתחיל לבכות ואמר:", "وهيك عمل، بسّ لمّا شاف الباص، يوسف وقع وصار يصرخ وصار يبكي وقال:"), - ("\"חבר'ה, עזרו לי, באמת נפלתי, אני צריך להגיע לטבריה!\"", "يا شباب، ساعدوني#imp، والله وقعت، انا لازم اوصل على طبريّا!"), - ("ומה אספר לכם, האנשים הטובים עזרו ליוסף לעלות על האוטובוס.", "وشو احكيلكم، النّاس الطّيّبين ساعدو يوسف يطلع على الباص."), - ("יוסף הצליח והגיע לבית של חבר שלו ח'ליל, והרבה אנשים חיכו ליום למחרת.", "يوسف نجح ووصل على بيت صاحبه خليل، وكتير ناس استنّو تاني يوم."), - ("הבעיה היא שבסוף האנשים יבינו שהוא שיקר להם ויכעסו עליו,", "المشكلة هيّ انّه بالآخر النّاس رح يفهمو انّه هوّ كذب عليهم ورح يزعلو منّه،"), - ("כמו שאומר הפתגם: \"חבל השקר קצר\".", "زيّ ما بيقول المثل: حبل الكذب قصير."), - ("אבל אולי הם לא יבינו ולא יכעסו וישכחו מהסיפור לגמרי.", "بسّ يمكن همّ مش رح يفهمو ومش رح يزعلو ورح ينسو من القصّة عالآخر."), - ]), - ("ask", "שאלות על סיפור יוסף והאוטובוס", "read", True, [ - ("מתי יוסף רצה לבקר את חבר שלו?", "ايمتا يوسف كان بدّه يزور صاحبه؟"), ("איפה גר יוסף ואיפה גר חבר שלו?", "وين ساكن يوسف ووين ساكن صاحبه؟"), - ("כמה פעמים ביום מגיע קו תשע מאות שמונים ושתיים?", "اكم مرّة باليوم بيجي خطّ تسع ميّة تنين وتمانين؟"), - ("כמה אנשים היו בתחנה כאשר הגיע יוסף?", "اكم واحد كانو بالمحطّة لمّا وصل يوسف؟"), - ("מה חשב יוסף כאשר ראה שהתחנה מלאה באנשים?", "شو فكّر يوسف لمّا شاف انّه المحطّة مليانة بالنّاس؟"), - ("מה ביקש יוסף מהאנשים הטובים?", "ايش طلب يوسف من النّاس الطّيّبين؟"), ("לאן יוסף הגיע בסוף?", "لوين يوسف وصل بالآخر؟"), - ("מה עשו הרבה אנשים? ולמה?", "شو عملو كتير ناس؟ وليش؟"), ("מה הפתגם שכתוב בסוף הסיפור?", "شو المثل الّي مكتوب في آخر القصّة؟"), - ("מה דעתך על יוסף?", "شو رايك بيوسف؟"), - ]), - ("sread", "יוסף והאוטובוס: ערבית ← עברית", "read", True, [ - ("סלח לי, מי בעל הבית? אני חושב ששכחתם את הדלת פתוחה.", "اسمحلي، مين صاحب#of البيت؟ انا بفكّر انّه نسيتو الباب مفتوح."), - ("היא דיברה איתי ואמרה שהייתה בעיה קטנה, לכן היא לא הגיעה.", "هيّ حكت معي وقالت#f انّه كانت مشكلة صغيرة، عشان هيك ما وصلت#f."), - ("אני צריך להיות בשעה תשע בדיוק בעבודה.", "انا لازم اكون على ساعة تسعة بالزّبط في الشّغل."), - ("תחשוב טוב על הנושא ובסוף תספר לי מה אתה רוצה לעשות.", "بدّك تفكّر منيح بالموضوع وبالآخر تحكيلي شو بدّك تعمل."), - ("את לא צריכה לעשות את עצמך כפצועה, רק תגידי לי מה את רוצה ואנחנו נעזור לך.", "انتي مش لازم تعملي حالك#f كمجروحة، بسّ قوليلي شو بدّك#f واحنا نساعدك#f."), - ("לפני שתצא מהבית אתה צריך לדבר איתי, טוב?", "قبل ما تطلع من البيت انت لازم تحكي معي، طيّب؟"), - ("בחייך, תעזור לי קצת, האוטובוס מלא באנשים ואני רוצה ללכת לדוד שלי, אפשר שתיקח אותי איתך במכונית שלך?", "وحياتك، ساعدني#imp شويّ، الباص مليان بالنّاس وبدّي اروح لعند عمّي، ممكن تاخدني معك بسيّارتك؟"), - ("הם לקחו את הילד לרופא כי הוא התחיל לצרוח והתחיל לבכות.", "همّ اخدو الولد للدّكتور عشان هوّ صار يصرخ وصار يبكي."), - ("מה סיפר לך חבר שלך? הוא סיפר לי שהוא לקח את אשתו לבית של אמא שלה לפני שעה וחצי, ועכשיו היא רוצה לחזור לבית שלה.", "شو حكالك صاحبك؟ هوّ حكالي انّه هوّ اخد مرته على بيت امّها قبل ساعة ونصّ، واسّا بدّها ترجع على بيتها."), - ]), - ("ssay", "יוסף והאוטובוס: עברית ← ערבית", "say", True, [ - ("הוא בעצמו היה גר בבית לחם, אבל הוא עבר לחיפה לפני חודש.", "هوّ نفسه كان ساكن في بيت لحم، بسّ هوّ نقل على حيفا قبل شهر."), - ("האוטובוס הראשון מצומת ראש פינה בשעה רבע לאחת עשרה והשני מטבריה.", "اوّل باص من مفرق راس بينا على ساعة حدعش الّا ربع والتّاني من طبريّا."), - ("הילד התחיל לבכות כי הוא נפל בסלון הבית שלו, ה' יעזור לו.", "الولد صار يبكي عشان هوّ وقع في صالون بيته، الله يساعده."), - ("את צריכה לקחת שלושה אוטובוסים, עדיף לך שתיקחי מונית.", "انتي لازم تاخدي تلات باصات، احسنلك#f تاخدي تاكسي.", "אַתְּ צריכה לקחת שלושה אוטובוסים, עדיף לָךְ שתיקחי מונית."), - ("הרבה אנשים שואלים אותי מאיפה אני? אני תמיד אומר שבמקור אני מיפו אבל היום אני גר בירושלים.", "كتير ناس بيسألوني من وين انا؟ انا دايما بقول انّه انا بالاصل من يافا بسّ اليوم انا ساكن في القدس."), - ("הגעתי לתחנה בשעה עשרה לתשע, התחנה הייתה מלאה באנשים.", "وصلت على المحطّة على ساعة تسعة الّا عشرة، المحطّة كانت مليانة بالنّاس."), - ("לסבתי יש בעיה במכונית, היא צריכה לקחת אוטובוס.", "ستّي عندها مشكلة في السّيّارة، هيّ لازم تاخد باص."), - ("סלח לי, איזה קו מגיע לתל אביב מפה? אני חושב שקו מאתיים ושתים עשרה, האוטובוס הירוק.", "لو سمحت، ايّ خطّ بيوصل على تلّ ابيب من هون؟ انا بفكّر انّه خطّ ميتين وتنعش، الباص الاخضر."), - ("אני אומר שהדבר הכי חשוב הוא המשפחה, לכן כל יום חמישי אני נמצא בבית.", "انا بقول انّه اهمّ اشي هوّ العيلة، عشان هيك كلّ يوم الخميس انا موجود في البيت."), - ("חבר שלי חשב קצת ואחר כך אמר שהוא צריך לעזור לאבא שלו היום ולא מחר.", "صاحبي فكّر شويّ وبعدين قال انّه هوّ لازم يساعد ابوه اليوم ومش بكرا."), - ("הילד נפל והתחיל לבכות כאשר ראה שאחותו התחילה לצחוק.", "الولد وقع وصار يبكي لمّا شاف انّه اخته صارت تضحك."), - ("אם לא תשחק עם הילדים בשכונה הם לא יבואו לבקר אותך, קדימה, שחק איתם!", "اذا ما بتلعب مع الاولاد في الحارة همّ مش رح يجو يزوروك، يالله العب#imp معهم!", "אם לא תשחק עם הילדים בשכונה הם לא יבואו לבקר אוֹתְךָ, קדימה, שַׂחֵק איתם!"), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 8): [ # the workbook's seventeenth lesson - ("new", "מילים חדשות", "learn", True, [ - ("הכאב", "الوجع"), ("אספירין", "اسبرين"), ("הבריאות", "الصّحّة"), ("פיצה", "بيتزا"), ("פסטה", "باستا"), - ("החוקים", "القوانين"), ("האהבה", "الحبّ"), ("אפילו, עד", "حتّى"), ("בקרוב", "قريبا"), ("לבד", "وحده"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("verbs", "גזרת הכפולים", "say", False, [(f"{he} (עבר, עתיד)", f"{past}، {future}") for he, past, future in BASES_2_8]), - ("past", "נשאר ושם בעבר", "say", False, _past(VERBS_2_8)), - ("pres", "נשאר ואוהב בהווה", "say", False, _present(PRESENT_2_8)), - ("vread", "גזרת הכפולים: ערבית ← עברית א'", "read", True, [ - ("הוא הרגיש כאב ראש, הוא לא ירגיש את הכאב אחרי שישתה כדור אספירין, הוא מרגיש טוב היום, השבח לאל.", "هوّ حسّ بوجع راس، مش رح يحسّ الوجع بعد ما يشرب حبّة#of اسبرين، هوّ بيحسّ منيح اليوم الحمد للّه."), - ("עם הזמן התחלתי להבין שאני אוהב אותה יותר ויותר, אבל הבעיה היא שהיא לא עונה לי כאשר אני מצלצל אליה, אולי היא לא מרגישה כמוני. תעזור לי! מה אעשה?", "مع الوقت صرت افهم انّه انا بحبّها اكتر واكتر، بسّ المشكلة هيّ انّه هيّ ما بتردّ عليّ لمّا انا برنّ عليها، يمكن هيّ ما بتحسّ زيّي. ساعدني#imp! شو بدّي اعمل؟"), - ("דודה שלי רצתה ללכת עם בעלה לקניון, הוא שאל אותה מה היא אוהבת לקנות?", "خالتي حبّت تروح مع جوزها على المول، هوّ سألها شو هيّ بتحبّ تشتري؟"), - ("אני נשארתי בבית כי המשכתי לדבר עם הבן שלי לפני שיצא, השקיתי אותו תה ודיברנו.", "انا ضلّيت في البيت عشان ضلّيت احكي مع ابني قبل ما يطلع، شرّبته شاي وحكينا."), - ("סליחה, איפה שמת את התיק החום? אני שמתי אותו מתחת למיטה, תמיד אני שמה אותו שם.", "لو سمحتي، وين حطّيتي الشّنطة البنّيّة؟ انا حطّيتها تحت التّخت، دايما انا بحطّها هناك."), - ("איך הרגשת כאשר ראית את חבר שלך לפני יומיים בחתונה של אחי? באמת הרגשתי טוב, השבח לאל.", "كيف حسّيتي لمّا شفتي صاحبك#f قبل يومين في عرس اخوي؟ والله حسّيت منيح الحمد للّه."), - ("רציתי להישאר פה אבל לא נשאר לי זמן, ברשותך, אני ממהר קצת, צריך כבר ללכת.", "كان بدّي اضلّ هون بسّ ما ضلّ معي وقت، عن اذنك، انا مستعجل شويّ، صار لازم اروح."), - ("אני אמרתי למוכר: \"שים לי עוד טחינה וצ'יפס!\" הוא שם לי כמו שאמרתי לו.", "انا قلت للبيّاع: حطّلي#imp كمان طحينة وشيبس! هوّ حطّلي زيّ ما قلتله."), - ("אני נשארתי לבדי במסעדה והמשכתי לדבר עם המלצר על בעל המסעדה.", "انا ضلّيتني لحالي في المطعم وضلّيت احكي مع الجرسون عن صاحب#of المطعم."), - ("אני נהייתי שמח כאשר את צלצלת אליי ועדכנת אותי שאת מרגישה טוב יותר.", "انا صرت مبسوط لمّا انتي رنّيتي عليّ وخبّرتيني انّه انتي بتحسّي احسن."), - ]), - ("vread2", "גזרת הכפולים: ערבית ← עברית ב'", "read", True, [ - ("זה לא אני שעניתי לך, הבן הבכור שלי הוא שענה לך אתמול כאשר צלצלת אליי.", "مش انا الّي ردّيت عليك، ابني البكر هوّ الّي ردّ عليك امبارح لمّا رنّيت عليّ."), - ("אחרי שאני הבנתי שאתה בן הדוד של סמיר, אני אהבתי אותך יותר!", "بعد ما انا فهمت انّه انت ابن عمّه لسمير، انا حبّيتك اكتر!"), - ("למה נשארתם בעבודה? אנחנו כבר שעה מחכים לכם בבית הכחול.", "ليش ضلّيتو في الشّغل؟ صارلنا ساعة احنا منستنّاكم في البيت الازرق."), - ("אני שמתי את הספל על השולחן ואחר כך לא מצאתי אותו שם.", "انا حطّيت الفنجان على الطّاولة وبعدين ما لاقيته هناك."), - ("בת דודה שלי הרגישה לא טוב, לכן לקחו אותה לרופא כדי שיטפל בה.", "بنت خالتي حسّت مش منيح، عشان هيك اخدوها على الدّكتور عشان يعالجها."), - ("אבל זה נשאר ביני ובינך, אל תדאג, אני לא אספר לאף אחד.", "بسّ هادا بيضلّ بيني وبينك، ولا يهمّك مش رح احكي لحدا."), - ("טוב, איפה שמת לי את הכסף? אני שמתי אותו במכונית השחורה שהייתה פה.", "طيّب وين حطّيتلي المصاري؟ انا حطّيتها في السّيّارة السّودا الّي كانت هون."), - ("סבתא שלי שמעה שאת נשארת לבדך בבית והתחילה לכעוס עלייך, היא ממשיכה לשאול עלייך כל יום.", "جدّتي سمعت#f انّه انتي ضلّيتي لحالك#f في البيت وصارت تزعل منّك#f، هيّ بتضلّ تسأل عنّك#f كلّ يوم."), - ("המורה הרגישה שהילד לא היה כן איתה, היא חשבה לדבר איתו אבל בסוף לא אמרה אפילו מילה.", "المعلّمة حسّت انّه الولد ما كان صريح معها، هيّ فكّرت#f تحكي معه بسّ بالآخر ما قالت#f حتّى كلمة."), - ]), - ("vsay", "גזרת הכפולים: עברית ← ערבית א'", "say", True, [ - ("שלשום אני הרגשתי לא טוב, לכן יצאתי מהעבודה וחזרתי הביתה.", "اوّل امبارح انا حسّيت مش منيح، عشان هيك طلعت من الشّغل ورجعت على البيت."), - ("אני רוצה לבוא אליך, אבל אני לא אשאר לדבר איתך הרבה, אני צריך לאכול צהריים עם חברי.", "بدّي اجي لعندك، بسّ انا مش رح اضلّ احكي معك كتير، انا لازم اتغدّى مع صاحبي.", "אני רוצה לבוא אֵלֶיךָ, אבל אני לא אשאר לדבר אִיתְּךָ הרבה, אני צריך לאכול צהריים עם חברי."), - ("אני שמתי אותו ליד הבית שלך, איך לא מצאת אותו? הוא היה שם בטוח!", "انا حطّيته جنب بيتك، كيف ما لاقيته؟ هوّ كان هناك اكيد!", "אני שמתי אותו ליד הבית שֶׁלְּךָ, איך לא מָצָאתָ אותו? הוא היה שם בטוח!"), - ("אל תדאג יקירי, כמובן! אני אמשיך לאהוב אותך, אתה יקר, הכי יקר בעולם!", "ولا يهمّك حبيبي، طبعا! انا رح اضلّ احبّك، انت غالي، اغلى واحد بالعالم!", "אל תדאג יקירי, כמובן! אני אמשיך לאהוב אוֹתְךָ, אתה יקר, הכי יקר בעולם!"), - ("אני לא הבנתי מדוע הילדה לא ענתה לי כאשר שאלתי אותה מתי היא חזרה אתמול בלילה.", "انا ما فهمت ليش البنت ما ردّت عليّ لمّا سألتها ايمتا هيّ رجعت#f امبارح ليلة."), - ("סלח לי, בחייך! שים לי עוד חריף, אני אוהב לאכול הרבה חריף.", "لو سمحت، وحياتك! حطّلي#imp كمان شطّة، انا بحبّ آكل كتير شطّة.", "סלח לי, בְּחַיֶּיךָ! שים לי עוד חריף, אני אוהב לאכול הרבה חריף."), - ("אני חשבתי שאחרי שהילד נפל הוא לא ירצה לרכוב יותר, אבל נהייתי שמחה כאשר ראיתי שהוא ממשיך לרכוב על האופניים.", "انا فكّرت انّه بعد ما الولد وقع هوّ مش رح يحبّ يركب كمان، بسّ صرت مبسوطة لمّا شفت انّه هوّ بيضلّ يركب على البسكليت."), - ("את הרגשת שאני נמצא בבית ולכן באת לכאן, כל הכבוד! את הכי חרוצה בעולם.", "انتي حسّيتي انّه انا موجود في البيت وعشان هيك اجيتي لهون، كلّ الاحترام! انتي اشطر واحدة بالعالم.", "אַתְּ הִרְגַּשְׁתְּ שאני נמצא בבית ולכן בָּאת לכאן, כל הכבוד! אַתְּ הכי חרוצה בעולם."), - ("איפה שמת את התיק הלבן? שמתי אותו ליד השולחן הירוק במרפסת.", "وين حطّيت الشّنطة البيضا؟ حطّيتها جنب الطّاولة الخضرا في البلكونة.", "איפה שַׂמְתָּ את התיק הלבן? שמתי אותו ליד השולחן הירוק במרפסת."), - ("אבי נהיה שמח כאשר הוא שמע שאת נשארת איתנו.", "ابوي صار مبسوط لمّا هوّ سمع انّه انتي بتضلّي معنا.", "אבי נהיה שמח כאשר הוא שמע שֶׁאַתְּ נִשְׁאֶרֶת איתנו."), - ]), - ("vsay2", "גזרת הכפולים: עברית ← ערבית ב'", "say", True, [ - ("סלחי לי, אולי תעני לה? היא כבר רבע שעה מחכה פה.", "اسمحيلي، يمكن تردّي عليها؟ صارلها ربع ساعة بتستنّى هون."), - ("אנחנו אוהבים לשבת ולצחוק ביחד, כל יום חמישי בשעה עשרים לתשע הם באים אליי.", "احنا بنحبّ نقعد ونضحك مع بعض، كلّ يوم الخميس على ساعة تسعة الّا تلت همّ بيجو لعندي."), - ("אני נשארתי בעבודה לבדי, חברי הגיע אליי ויצאנו ביחד, הוא נהג ואני ישבתי לידו.", "انا ضلّيت في الشّغل لحالي، صاحبي وصل لعندي وطلعنا مع بعض، هوّ ساق وانا قعدت جنبه."), - ("איפה שמת את הספר? שמתי אותו על השולחן הכי קטן, הצבע שלו כתום.", "وين حطّيت الكتاب؟ حطّيته على اصغر طاولة، لونها برتقالي.", "איפה שַׂמְתָּ את הספר? שמתי אותו על השולחן הכי קטן, הצבע שלו כתום."), - ("כיוון שלא הרגשתי טוב חזרתי לבית שלי, דיברתי עם אבי, הוא בא לקחת אותי.", "عشان ما حسّيت منيح رجعت على بيتي، حكيت مع ابوي، هوّ اجا ياخدني."), - ("מה שסיפרתי לך צריך להישאר רק ביני ובינך, בחייך אל תספרי לאף אחד!", "الّي حكيتلك#f لازم يضلّ بسّ بيني وبينك#f، وحياتك#f لا تحكي لحدا!", "מה שסיפרתי לָךְ צריך להישאר רק ביני וּבֵינֵךְ, בְּחַיַּיִךְ אל תספרי לאף אחד!"), - ("אני שמתי לו חומוס אבל הוא לא אוהב חומוס, אז בסוף קניתי לו פיצה.", "انا حطّيتله حمّص بسّ هوّ ما بيحبّ حمّص، وبالآخر اشتريتله بيتزا."), - ("דודי מצד אבא לא אהב לשמוע שבתו נשארה לבדה בשוק, הוא כעס עליי ונסע להביא אותה.", "عمّي ما حبّ يسمع انّه بنته ضلّت لحالها في السّوق، هوّ زعل منّي وراح يجيبها."), - ("בעל המוסך הרגיש שהייתה בעיה קטנה במנוע, לכן הוא התקשר אליי ועדכן אותי שהוא ממשיך לתקן את המכונית גם היום.", "صاحب#of الكراج حسّ انّه كانت مشكلة صغيرة بالماتور، عشان هيك هوّ رنّ عليّ وخبّرني انّه هوّ بيضلّ يصلّح السّيّارة كمان اليوم."), - ]), - ("am", "עַםְ: פעולה שקורית עכשיו", "say", True, [ - ("אני מדבר איתך (כעת, עוד מעט, כל יום)", "انا بحكي معك", "אני מדבר אִיתְּךָ"), ("אני מדבר איתך כעת", "انا عم بحكي معك", "אני מדבר אִיתְּךָ כעת"), - ("מה אתה שותה?", "شو انت عم بتشرب؟"), ("אני שותה קפה עם חלב בלי סוכר!", "انا عم بشرب قهوة مع حليب بدون سكّر!"), - ("אני עובד כעת", "انا عم بشتغل"), ("בעלי, איפה אתה?", "يا جوزي، وينك#m؟"), ("לא, יקיר לבי, אני אוכל כעת", "لا حبيب قلبي، انا عم باكل"), - ("ראיתי אותו משחק עם אחותו", "شفته عم بيلعب مع اخته"), - ("סבי שמע רעש במטבח ושאל אותי: \"מה קורה?\"", "جدّي سمع ضجّة في المطبخ وسألني: شو عم بيصير؟"), - ]), - ("amsay", "עַםְ: עברית ← ערבית א'", "say", True, [ - ("אמרתי לך בעבר, כמו שאני אומר לך בכל יום, ואני אומר לך שוב פעם כעת, אני לא רוצה לאכול בקלאווה בכלל.", "قلتلك زمان، زيّ ما بقولّك كلّ يوم، وانا عم بقولّك كمان مرّة هلّق، انا ما بدّي آكل بقلاوة بالمرّة.", "אמרתי לְךָ בעבר, כמו שאני אומר לְךָ בכל יום, ואני אומר לְךָ שוב פעם כעת, אני לא רוצה לאכול בקלאווה בכלל."), - ("חברי שאל את אחותו מתי החתונה, כעת היא מצלצלת לחברה שלה, בקרוב היא תענה לה.", "صاحبي سأل اخته ايمتا العرس، هلّق هيّ عم بترنّ على صاحبتها، قريبا هيّ رح تردّ عليها."), - ("דיברנו עם נאצר אתמול, אנחנו מדברים איתו בכל יום, אנחנו מדברים איתו כעת.", "حكينا مع ناصر امبارح، احنا بنحكي معه كلّ يوم، احنا عم بنحكي معه هلّق."), - ("תמיד כשהיא נכנסת לבית היא אומרת שלום שלום, היום היא ראתה אותנו אוכלים צהריים ולכן לא שאלה אותנו דבר.", "دايما لمّا هيّ بتدخل على البيت هيّ بتقول مرحبا مرحبا، اليوم هيّ شافتنا عم بنتغدّى وعشان هيك ما سألتنا#f اشي."), - ("אני נמצאת בקניון, אני צוחקת כעת עם המוכרת, אני חוזרת אליך עוד מעט, אל תדאג!", "انا موجودة في المول، انا عم بضحك مع البيّاعة، انا برجعلك كمان شويّ، ولا يهمّك!", "אני נמצאת בקניון, אני צוחקת כעת עם המוכרת, אני חוזרת אֵלֶיךָ עוד מעט, אל תדאג!"), - ("אני מצאתי את הילד, הוא היה בבית של חבר שלו, אני אומר לך כעת, הכול טוב.", "انا لاقيت الولد، هوّ كان في بيت صاحبه، انا عم بقولّك هلّق، كلّه تمام.", "אני מצאתי את הילד, הוא היה בבית של חבר שלו, אני אומר לְךָ כעת, הכול טוב."), - ("סלח לי, איפה נמצאת המסעדה של אבו שריף? אתה שואל אותי? כן, אני שואל אותך כעת.", "لو سمحت، وين موجود مطعم ابو شريف؟ انت بتسألني؟ اه، انا عم بسألك هلّق.", "סלח לי, איפה נמצאת המסעדה של אבו שריף? אתה שואל אותי? כן, אני שואל אוֹתְךָ כעת."), - ("בדרך כלל אני לא עובד ביום חמישי, אבל כיוון שהמקום מלא עד הסוף אני עובד כעת.", "عادة انا ما بشتغل يوم الخميس، بسّ عشان المحلّ مليان عالآخر انا عم بشتغل هلّق."), - ("חברתי הלכה לראות איפה בעלת הבית נמצאת, היא ראתה אותה למטה ושאלה אותה: \"מה את עושה כאן כעת?\"", "صاحبتي راحت تشوف وين صاحبة#of البيت موجودة، هيّ شافتها تحت وسألتها#f: شو عم بتعملي هون هلّق؟"), - ]), - ("amsay2", "עַםְ: עברית ← ערבית ב'", "say", True, [ - ("למה אתה מחכה כעת? קדימה, בוא עזור לי למצוא את הילדה!", "ليش عم بتستنّى هلّق؟ يالله تعال ساعدني#imp الاقي البنت!"), - ("שמעתי מישהו שר כעת, אני חושבת שזה השכן שלי, כל יום בדרך חזרה מהעבודה בערך בשעה רבע לארבע הוא שר.", "سمعت واحد عم بيغنّي هلّق، انا بفكّر انّه هادا جاري، كلّ يوم في الطّريق رجعة من الشّغل تقريبا على ساعة اربعة الّا ربع هوّ بيغنّي."), - ("אמא, מה את מכינה כעת? אני מבשלת פסטה עם רוטב עגבניות.", "يامّا، شو عم بتحضّري هلّق؟ انا عم بطبخ باستا مع صلصة#of بندورة.", "אמא, מה אַתְּ מכינה כעת? אני מבשלת פסטה עם רוטב עגבניות."), - ("אני נמצא בבאר שבע, עדכנו אותי שקו ארבע מאות ושבעים, האוטובוס הירוק, מגיע לירושלים, אני מחכה כעת בתחנה.", "انا موجود في بير السّبع، خبّروني انّه خطّ اربع ميّة وسبعين، الباص الاخضر، بيوصل على القدس، انا عم بستنّى هلّق في المحطّة."), - ("אני וחברי רצים כעת, צריך לרוץ בכל שבוע שלוש פעמים בשביל הבריאות.", "انا وصاحبي عم بنركض هلّق، ضروري نركض كلّ اسبوع تلات مرّات عشان الصّحّة."), - ("על מה את חושבת כעת? אני חושבת על המצב, אני רוצה שיהיה יותר טוב בעזרת ה'.", "على ايش عم بتفكّري هلّق؟ انا عم بفكّر على الوضع، بدّي انّه يكون احسن انشالله.", "על מה אַתְּ חושבת כעת? אני חושבת על המצב, אני רוצה שיהיה יותר טוב בעזרת ה'."), - ("אני רוכב על האופניים כעת, אני רוצה להגיע אליך לפני השעה רבע לאחת, כאשר אגיע נשב יחד ונדבר.", "انا عم بركب على البسكليت هلّق، بدّي اوصل لعندك قبل السّاعة واحدة الّا ربع، لمّا اوصل نقعد مع بعض ونحكي.", "אני רוכב על האופניים כעת, אני רוצה להגיע אֵלֶיךָ לפני השעה רבע לאחת, כאשר אגיע נשב יחד ונדבר."), - ("מה קורה פה כעת? מה אתם עושים כעת? למה אתם מחכים כעת?", "شو عم بيصير هون هلّق؟ شو عم بتعملو هلّق؟ ليش عم بتستنّو هلّق؟"), - ]), - ("words", "מילים לשיר שַאטֵר שַאטֵר", "say", True, [ - ("שיר, שירים", "غنوة، اغاني"), ("מי ש, אשר", "يلّي"), ("מה שעבר מת", "الّي فات مات"), ("מילה, מילים", "كلمة، كلمات"), ("בגדים", "ثياب"), - ("נבון, בעל שכל", "عاقل"), ("משחק, משחקים", "لعبة، العاب"), ("חבר, חברים", "رفيق، رفقا"), ("כעס על, הכעיס", "زعل من، زعّل"), - ("שיקר ל (עבר, עתיד)", "كذب على، يكذب على"), ("חוק, חוקים", "قانون، قوانين"), ("האהבה לא יודעת את החוקים", "الحبّ لا يعرف القوانين"), - ]), - ("story", "השיר: שַאטֵר שַאטֵר", "say", True, [ - ("מי רוצה לשיר איתי את השיר \"חרוץ חרוץ\"?", "مين بدّه يغنّي معي غنية#of شاطر شاطر؟"), - ("אני אני אני!", "انا انا انا!"), - ("מי ששומע בקול ההורים שלו, מה נאמר לו? חרוץ חרוץ!", "يلّي بيسمع كلمة#of اهله شو منقولّه؟ شاطر شاطر!"), - ("מי שיושב יפה לבדו, מה נאמר לו? חרוץ חרוץ!", "يلّي بيقعد عاقل وحده شو منقولّه؟ شاطر شاطر!"), - ("מי שמצליח בבית הספר שלו ולא מכעיס את המורה שלו", "يلّي بينجح بمدرسته وما بيزعّل معلّمته"), - ("נאהב אותו ונמשיך לומר לו חרוץ חרוץ", "منحبّه ومنضلّ منقولّه شاطر شاطر"), - ("מי שעוזר לחברים שלו, מה נאמר לו? חרוץ חרוץ!", "يلّي بيساعد رفقاته شو منقولّه؟ شاطر شاطر!"), - ("מי שלא משקר בחיים שלו, מה נאמר לו? חרוץ חרוץ!", "يلّي ما بيكذب بحياته شو منقولّه؟ شاطر شاطر!"), - ("מי שלא שובר את המשחקים שלו, מה נאמר לו? חרוץ חרוץ!", "يلّي ما بيكسر العابه شو منقولّه؟ شاطر شاطر!"), - ("ומי שאוכל ולא מלכלך את הבגדים שלו, מה נאמר לו? חרוץ חרוץ!", "وبياكل ما بيجوّي ثيابه شو منقولّه؟ شاطر شاطر!"), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], - (2, 9): [ # the workbook's eighteenth lesson - ("new", "מילים חדשות", "learn", True, [ - ("בושה", "عيب"), ("העני", "الفقير"), ("אוזניות", "سمّاعات"), ("התאנים", "التّين"), ("צופר", "زمّور"), - ("המחשב", "الحاسوب"), ("המוכתר", "المختار"), ("השנה", "السّنة"), ("נימוס", "ادب"), ("מספיק", "كفاية"), - ("שלום", "سلام"), - ]), - ("pairs", "התאימו זוגות", "match", True, []), - ("part", "הבינוני הפועל: ארבע הצורות", "say", True, [(f"{he}, {he}ה, {he}ים, {he}ות" if not full else full, f"{ar}، {ar}ة، {ar}ين، {ar}ات") for he, ar, full in ACTIVE_2_9]), - ("part2", "פועל והבינוני שלו", "say", True, [(f"{past_he} – {he}", f"{past}، {ar}") for past_he, he, past, ar in ACTIVE_PAIRS_2_9]), - ("verbs", "חמישה פעלים חדשים", "say", False, [(f"{he} (עבר, עתיד)", f"{past}، {future}") for he, past, future in BASES_2_9]), - ("ex", "משפטים עם הבינוני הפועל", "say", True, [ - ("מה קורה?", "شو صاير؟"), ("השבוע הבא", "الاسبوع الجاي"), ("השמלה מתאימה לך", "الفستان جاي عليكي", "השמלה מתאימה לָךְ"), - ("אני רוצה לומר לך משהו", "انا حابب احكيلك اشي", "אני רוצה לומר לְךָ משהו"), ("אני אוהבת להכיר את האנשים", "انا حابّة اعرف النّاس"), - ("אני שם אוזניות ושר", "انا حاطط سمّاعات وعم بغنّي"), - ("כל יום שני אני הולך לחבר שלי", "كلّ يوم التّنين انا بروح لعند صاحبي"), ("אני הולך לחבר שלי עכשיו", "انا رايح لعند صاحبي هلّق"), - ("את מבינה אותי? כן! הבנתי אותך!", "انتي فاهمة عليّ؟ اه! فهمت عليكي!", "אַתְּ מבינה אותי? כן! הבנתי אוֹתָךְ!"), - ("איפה אבא שלך? אבא שלי ישן עכשיו", "وين ابوكي؟ ابوي نايم اسّا", "איפה אבא שֶׁלָּךְ? אבא שלי ישן עכשיו"), - ("הלכנו אליו לפני שבע שעות, אבל הוא היה ישן בסלון", "رحنا لعنده قبل سبع ساعات، بسّ هوّ كان نايم في الصّالون"), - ("אני הייתי גר בבאר שבע, הייתי חי שם שבע שנים", "انا كنت ساكن في بير السّبع، كنت عايش هناك سبع سنين"), - ("היום אני גר בירושלים", "اليوم انا ساكن في القدس"), ("אני לא גר כאן", "انا مش ساكن هون"), - ("את ישנה? לא, אני לא ישנה, אני ערה!", "انتي نايمة؟ لا، انا مش نايمة، انا صاحية!", "אַתְּ ישנה? לא, אני לא ישנה, אני ערה!"), - ("עד מתי אתם פתוחים היום?", "لايمتا فاتحين اليوم؟"), - ("אל תדאגי! אנחנו פתוחים עד השעה שש ורבע", "ولا يهمّك#f! احنا فاتحين لحدّ السّاعة ستّة وربع"), - ]), - ("pread", "הבינוני הפועל: ערבית ← עברית א'", "read", True, [ - ("אתמול בלילה, בעודי חוזר מהחתונה של חברי מרואן, שמעתי רעש מהמנוע.", "امبارح ليلة وانا راجع من عرس صاحبي مروان، سمعت ضجّة من الماتور."), - ("אתה שומע את עצמך איך אתה מדבר איתי? אני שומע אותך יקירי, לא צריך לצרוח עליי.", "انت سامع حالك كيف عم بتحكي معي؟ انا سامعك حبيبي، مش لازم تصرخ عليّ."), - ("היא ראתה אותו יוצא מהמשרד ואחר כך צלצלה אליו ואמרה לו שהוא צריך לחזור עכשיו.", "هيّ شافته#f طالع من المكتب وبعدين رنّت عليه وقالتله انّه لازم يرجع اسّا."), - ("איפה הייתם שלשום בשעה שש וחצי? ראיתי אתכם יוצאים מהמוסך.", "وين كنتو اوّل امبارح على ساعة ستّة ونصّ؟ شفتكم طالعين من الكراج."), - ("אנחנו הולכים אליה ואחר כך נדבר איתך, בסדר? טוב, בסדר!", "احنا رايحين لعندها وبعدين نحكي معك، ماشي؟ طيّب ماشي!"), - ("שמעתי שהיא ירדה מהבית שלה אבל אף אחד לא ראה אותה יורדת.", "سمعت انّه هيّ نزلت#f من بيتها بسّ ولا واحد شافها نازلة."), - ("אנחנו גרים בשכונת התאנה אבל עכשיו אנחנו הולכים לשכונה אחרת.", "احنا ساكنين في حارة#of التّين لكن هلّق احنا رايحين على حارة تانية."), - ("מי האיש שירד מפה? ראית אותו? לא, לא ראיתי אותו ואני לא יודע מי הוא.", "مين الزّلمة الّي كان نازل من هون؟ شفته؟ لا، ما شفته ومش عارف مين هوّ."), - ("בואו איתנו לבית של פאטמה, היא גרה שם, אתם רואים? כן, אנחנו רואים!", "تعالو معنا على بيت فاطمة، هيّ ساكنة هناك، انتو شايفين؟ اه، احنا شايفين!"), - ("לא צריך לעשות רעש עם צופר המכונית, אני יורדת מהבית שלי, יקירי.", "مش لازم تعمل ضجّة مع زمّور السّيّارة، انا نازلة من بيتي حبيبي."), - ]), - ("pread2", "הבינוני הפועל: ערבית ← עברית ב'", "read", True, [ - ("את שומעת את עצמך איך את מדברת? אני רוצה לענות לך אבל לא פה, כאשר נהיה לבד.", "انتي سامعة حالك#f كيف عم بتحكي؟ بدّي اردّ عليكي بسّ مش هون، لمّا نكون لحالنا."), - ("אני כבר שעתיים מחכה לרופא, אני לא מבין איך לא מעדכנים אותי מתי ניכנס אליו?", "صارلي ساعتين بستنّى الدّكتور، انا مش فاهم كيف ما بيخبّروني ايمتا ندخل عنده؟"), - ("חמודה! מה את עושה פה, למה את לא ישנה? השעה כבר עשרים לאחת עשרה, קדימה יקירתי, לכי לישון!", "يا شاطرة! شو عم بتعملي هون، ليش مش نايمة؟ صارت السّاعة حدعش الّا تلت، يالله حبيبتي روحي#imp نامي!"), - ("אני לא ידעתי איזה צבע את אוהבת יותר, בהתחלה חשבתי שוורוד, אבל עכשיו אני יודע שאת אוהבת יותר כתום.", "انا ما عرفتش ايّ لون انتي بتحبّي اكتر، بالاوّل فكّرت انّه زهري، بسّ هلّق انا عارف انّه بتحبّي اكتر برتقالي."), - ("בנות, אתן באות איתנו או לא? כן! אנחנו באות, אל תדאגי, אמא!", "يا بنات جايات معنا ولّا لا؟ اه! احنا جايات، ولا يهمّك#f يامّا!"), - ("אני הייתי מכין קפה וראיתי את הבן שלי יוצא מהבית, אבל אני לא יודע לאן הלך.", "انا كنت عم بحضّر قهوة وشفت ابني طالع من البيت، لكن انا مش عارف لوين راح."), - ("אתמול, בעודנו חוזרים מהקניון, דוד שלי נהג במכונית, ואנחנו שרנו.", "امبارح واحنا راجعين من المول، عمّي كان عم بيسوق السّيّارة، كنّا عم بنغنّي."), - ("אתה רוצה לעזור לי או שאתה רוצה להמשיך לבכות כך כל היום? סלחי לי דודה, אני לא יכול לעזור לך כי אני לא שמח בכלל.", "بدّك تساعدني ولّا بدّك تضلّ تبكي هيك كلّ اليوم؟ اسمحيلي خالتي، مش قادر اساعدك#f عشان انا مش مبسوط بالمرّة."), - ("יא גבר, איך לא מצאת את המחשב, אתה לא רואה שהוא נמצא פה? לא, באמת, לא ראיתי אותו, אבל עכשיו אני רואה אותו.", "يا زلمة كيف ما لاقيت الحاسوب، انت مش شايف انّه هوّ موجود هون؟ لا والله، ما شفته، لكن هسّا انا شايفه."), - ("שמעי יקירתי, אני נכנס לעבודה, אני לא יכול לדבר עכשיו, אחזור אלייך אחר כך, בסדר? כן! בסדר, אל תדאג! הכול טוב, ה' איתך!", "اسمعي#imp حبيبتي، انا فايت على الشّغل، مش قادر احكي هلّق، برجعلك#f بعدين، ماشي؟ اه! ماشي ولا يهمّك! كلّه تمام، الله معك!"), - ]), - ("psay", "הבינוני הפועל: עברית ← ערבית א'", "say", True, [ - ("אמא שלך ישנה? לא, אני ראיתי אותה פה לפני בערך רבע שעה.", "امّك نايمة؟ لا، انا شفتها هون قبل ربع ساعة تقريبا.", "אמא שֶׁלְּךָ ישנה? לא, אני ראיתי אותה פה לפני בערך רבע שעה."), - ("שמעתי מישהו יורד מהבית, זה בן דודך (מצד אמא)? אני לא ראיתי מי ירד מהבית.", "سمعت واحد نازل من البيت، هادا ابن خالك؟ انا ما شفت مين نزل من البيت.", "שמעתי מישהו יורד מהבית, זה בן דּוֹדְךָ? אני לא ראיתי מי ירד מהבית."), - ("לאן אתה הולך? אני לא הולך, אני מחכה לך פה כבר שעה, מהשעה רבע לתשע.", "لوين انت رايح؟ انا مش رايح، صارلي ساعة بستنّاك هون، من السّاعة تسعة الّا ربع.", "לאן אתה הולך? אני לא הולך, אני מחכה לְךָ פה כבר שעה, מהשעה רבע לתשע."), - ("אני בא, בחייך אל תצא מהמכולת, חכה לי עשר דקות, אל תדאג, אני מחכה לך!", "انا جاي، وحياتك لا تطلع من البقالة، استنّاني#imp عشر دقايق، ولا يهمّك انا بستنّاك!", "אני בא, בְּחַיֶּיךָ אל תצא מהמכולת, חכה לי עשר דקות, אל תדאג, אני מחכה לְךָ!"), - ("אנחנו יושבים בבית ואשתי יושבת במרפסת, כאשר תבואו נשקה אתכם קפה שחור.", "احنا قاعدين في البيت ومرتي قاعدة في البلكونة، لمّا تيجو نشرّبكم قهوة سادة."), - ("אנחנו יוצאים, אתה בא או לא? טוב, בסדר, אני בא.", "احنا طالعين، انت جاي ولّا لا؟ طيّب ماشي، انا جاي."), - ("את שומעת? אני מדבר איתך כעת! לא שמעתי כלום, הייתי עם אוזניות.", "انتي سامعة؟ انا عم بحكي معك#f هلّق! ما سمعت اشي، كنت مع سمّاعات.", "אַתְּ שומעת? אני מדבר אִיתָּךְ כעת! לא שמעתי כלום, הייתי עם אוזניות."), - ("סלחו לי, איפה הבית של המוכתר? אתם יודעים? לא, אנחנו לא יודעים, אנחנו לא מפה.", "اسمحولي، وين بيت المختار؟ انتو عارفين؟ لا، احنا مش عارفين، احنا مش من هون."), - ("אני יורד מהבית שלי, אתה רואה אותי? אני בא לדבר איתך.", "انا نازل من بيتي، انت شايفني؟ انا جاي احكي معك.", "אני יורד מהבית שלי, אתה רואה אותי? אני בא לדבר אִיתְּךָ."), - ("אתם חוזרים לעבודה או לא? אני חוזר אבל היא לא חוזרת, היא תחזור מחר.", "انتو راجعين على الشّغل ولّا لا؟ انا راجع بسّ هيّ مش راجعة، هيّ رح ترجع بكرا."), - ]), - ("psay2", "הבינוני הפועל: עברית ← ערבית ב'", "say", True, [ - ("מה קורה איתך? אני מבין שאת לא מוצאת את הדרך, אבל אל תדאגי, יהיה טוב בעזרת ה'!", "شو صاير معك#f؟ انا فاهم انّه انتي ما بتلاقي الطّريق، بسّ ولا يهمّك#f، رح يكون خير انشالله!", "מה קורה אִיתָּךְ? אני מבין שֶׁאַתְּ לא מוצאת את הדרך, אבל אל תדאגי, יהיה טוב בעזרת ה'!"), - ("רציתי לבוא אליך אבל חשבתי שאתה ישן, בסוף הלכתי לבדי לדודי מצד אמא.", "كان بدّي اجي لعندك بسّ فكّرت انّه انت نايم، بالآخر رحت لحالي لعند خالي.", "רציתי לבוא אֵלֶיךָ אבל חשבתי שאתה ישן, בסוף הלכתי לבדי לדודי מצד אמא."), - ("אני לא יכול ללכת איתך לשוק, הייתי בעבודה כל היום ונהייתי עייף.", "انا مش قادر اروح معك على السّوق، كنت في الشّغل كلّ اليوم وصرت تعبان.", "אני לא יכול ללכת אִיתְּךָ לשוק, הייתי בעבודה כל היום ונהייתי עייף."), - ("ראיתי אותו יושב במכונית שלו, הוא היה לבדו, בטח אחרי שהלכתי חבר שלו הגיע.", "شفته قاعد في سيّارته، هوّ كان لحاله، اكيد بعد ما رحت صاحبه وصل."), - ("הייתי חי בצפון אבל עברתי לכאן לפני חצי שנה.", "كنت عايش في الشّمال بسّ نقلت لهون قبل نصّ سنة."), - ("מה מביא אותך לפה יקירי? באתי לעזור לכם למצוא את התיק האדום.", "شو جايبك لهون حبيبي؟ اجيت اساعدكم تلاقو الشّنطة الحمرا.", "מה מביא אוֹתְךָ לפה יקירי? באתי לעזור לכם למצוא את התיק האדום."), - ("אני רוצה לדעת איפה היית? אני כבר שעתיים פה מחכה לך.", "انا حابب اعرف وين كنتي؟ صارلي ساعتين هون بستنّاكي.", "אני רוצה לדעת איפה הָיִית? אני כבר שעתיים פה מחכה לָךְ."), - ("אתה שומע אותי? ראיתי אותו יוצא מהבית שלו אבל אני לא יודעת לאן הוא הלך.", "انت سامعني؟ شفته طالع من بيته بسّ انا مش عارفة لوين هوّ راح."), - ("אתה יודע אם היא נשואה או רווקה? אני חושב שראיתי אותה לבדה במסיבה.", "انت عارف اذا هيّ متجوّزة ولّا عزبا؟ انا بفكّر انّه شفتها لحالها في الحفلة."), - ]), - ("psay3", "הבינוני הפועל: עברית ← ערבית ג'", "say", True, [ - ("אתה נכנס או לא? אני נכנס כעת, חשבתי שגם אתה רוצה להיכנס.", "انت فايت ولّا لا؟ انا فايت هلّق، فكّرت انّه انت كمان بدّك تفوت."), - ("אני לא יכולה לשחק איתך, רק עכשיו חזרתי אחרי היום הכי ארוך בשנה.", "انا مش قادرة العب معك، بسّ هلّق رجعت بعد اطول يوم في السّنة.", "אני לא יכולה לשחק אִיתְּךָ, רק עכשיו חזרתי אחרי היום הכי ארוך בשנה."), - ("אתמול, בעודי חוזרת מהשכנה שלי, ראיתי אותך יושב פה עם החברים שלך.", "امبارح وانا راجعة من عند جارتي شفتك قاعد هون مع اصحابك.", "אתמול, בעודי חוזרת מהשכנה שלי, ראיתי אוֹתְךָ יושב פה עם החברים שֶׁלְּךָ."), - ("אני לא הייתי ישנה כאשר הגעת אליי, שמעתי אותך אבל הייתי במיטה.", "انا ما كنت نايمة لمّا وصلتي لعندي، سمعتك#f بسّ كنت في التّخت.", "אני לא הייתי ישנה כאשר הִגַּעְתְּ אליי, שמעתי אוֹתָךְ אבל הייתי במיטה."), - ("שלשום, בעודנו יורדים מהעבודה, שמענו שהרופאה טיפלה בילדה, ה' ישמור אותה.", "اوّل امبارح واحنا نازلين من الشّغل سمعنا انّه الدّكتورة عالجت#f البنت، الله يخلّيها."), - ("אני לא יכול לראות אותך לא שמחה, אני רוצה לקנות לך מתנה גדולה!", "انا مش قادر اشوفك#f مش مبسوطة، انا حابب اشتريلك#f هديّة كبيرة!", "אני לא יכול לראות אוֹתָךְ לא שמחה, אני רוצה לקנות לָךְ מתנה גדולה!"), - ("אני באה בעוד כמה דקות, כמו שאני באה בכל יום! אל תדאג, אני באה.", "انا جاية كمان كم دقيقة، زيّ ما انا باجي كلّ يوم! ولا يهمّك انا جاية."), - ("אני חשבתי שאתה לא נמצא ולכן לא הבאתי לך אוכל, אתה מבין? כן, הבנתי אותך.", "انا فكّرت انّه انت مش موجود وعشان هيك ما جبتلك اكل#n، انت فاهم؟ اه، فهمت عليكي.", "אני חשבתי שאתה לא נמצא ולכן לא הבאתי לְךָ אוכל, אתה מבין? כן, הבנתי אוֹתָךְ."), - ("המכנסיים השחורים מתאימים לך, אלף מזל טוב!", "البنطلون الاسود جاي عليك، الف مبروك!", "המכנסיים השחורים מתאימים לְךָ, אלף מזל טוב!"), - ]), - ("words", "מילים לשיר אַנַא עַאיֵש", "say", True, [ - ("אופוריה", "منتش"), ("לילה, לילות", "ليلة، ليالي"), ("סוף", "نهاية"), ("אפילו", "ولا حتّى"), ("מלבד, אחר", "غير"), ("פצע", "جرح#n"), - ("געגוע", "شوق"), ("אהבה, אוויר", "هوا"), ("מספיק", "بكفّي"), ("נח, נרגע (עבר, עתיד)", "ارتاح، يرتاح"), ("נשאר (עבר, עתיד)", "بقي، يبقى"), - ("חלם (עבר, עתיד)", "حلم، يحلم"), - ("כשתבוא, תביא לי שווארמה ופפסי איתך!", "وانت جاي، جيب لي شاورما وببسي معك!", "כשתבוא, תביא לי שווארמה ופפסי אִיתְּךָ!"), - ("אני לא יכול למצוא אוכל כאן, אבל כאשר אני חוזר מהעבודה בטוח אני מוצא בדרך ומביא לך!", "انا مش قادر الاقي اكل#n هون بسّ وانا راجع من الشّغل اكيد بلاقي في الطّريق وبجيبلك!", "אני לא יכול למצוא אוכל כאן, אבל כאשר אני חוזר מהעבודה בטוח אני מוצא בדרך ומביא לְךָ!"), - ]), - ("song", "השיר: אַנַא עַאיֵש", "say", True, [ - ("לא נרגע בלילה ולא שוכח אותך ולא מצאתי סוף", "لا برتاح في ليلة ولا بنساك ولا لاقيت نهاية"), - ("ואפילו אם אני מתרחק, אני נשאר איתך והאופוריה איתי", "ولا حتّى ببعد ببقى معاك ومنتش معايا"), - ("אני חי, לא חי, ולא יכול עם המרחק ממך", "انا عايش مش عايش ومش قادر على بعدك"), - ("לא יודע יום אשכח, ולא צריך אהוב אחריך", "ولا عارف في يوم انسى ولا عايز حبيب بعدك#after"), - ("אני שואל ולבי חולם על היום שיירגע מהפצעים שלו", "وبسأل وقلبي بيحلم يوم يرتاح من جراحه"), - ("הלילות שהיו בינינו מזמן חוזרים ולא הלכו", "ليالي الّي كانو ما بينا#us زمان راجعين ولا راحو"), - ("לא אהבתי אף אחד מלבדך, ומה אעשה בגעגועיי ובאהבתי", "ما حبّيتش غيرك واعمل ايه بشوقي وهوايا"), - ("והלילה שלי והיום שלי אני חושב עליך, תחזור כבר, מספיק!", "وليلي ونهاري بفكّر فيك ما ترجع كفاية!"), - ]), - ("story", "סיפור האב, הבן והחמור", "say", True, [ - ("יום אחד האב העני היה רוכב על החמור והילד הולך לידו.", "في يوم من الايّام الاب الفقير كان راكب عالحمار والولد قاعد يمشي جنبه."), - ("ראו אותם האנשים ואמרו:", "شافوهم النّاس وقالو:"), - ("\"תראו איך האיש הזה לא מרחם על הבן שלו, אלוהים! בושה עליו!\"", "شوفو كيف هادا الرّجل ما يرحم ابنه، يا الله! عيب عليه!"), - ("והאב שמע את הדברים, ירד מהחמור ונתן לבן שלו לרכוב עליו.", "والاب سمع الكلام، نزل عن الحمار واعطى ابنه يركب عليه."), - ("ראו אותם האנשים ואמרו: \"תראו איך הילד הזה חסר נימוס, איך אבא שלו הולך והוא רוכב על החמור? אלוהים! בושה עליו!\"", "شافوهم النّاس وقالو: شوفو كيف هادا الولد قليل ادب، كيف ابوه يمشي وهوّ راكب عالحمار؟ يا الله! عيب عليه!"), - ("והאב שמע את הדברים, אז אמר לבן שלו:", "والاب سمع الكلام، فقال لابنه:"), - ("\"בוא בני, נרכב על החמור יחד.\"", "تعال يا ابني، نركب عالحمار مع بعض."), - ("ראו אותם האנשים ואמרו: \"תראו את אלה, איך הם לא מרחמים על החמור, אלוהים! בושה עליהם!\"", "شافوهم النّاس وقالو: شوفو هدول كيف ما يرحمو الحمار، يا الله! عيب عليهم!"), - ("האב שמע את הדברים ואמר לו: \"בני, רד מהחמור! אף אחד לא ירכב על החמור הזה.\"", "الاب سمع الكلام وقاله: يا ابني انزل#imp عن الحمار! ولا واحد رح يركب على هادا الحمار."), - ("ראו אותם האנשים ואמרו: \"איזה יופי! תראו איך יש להם חמור והם לא רוכבים עליו!\"", "شافوهم النّاس وقالو: يا سلام! شوفو كيف عندهم حمار وما يركبو عليه!"), - ("והאב שמע את הדברים, אמר האב העני לבן שלו:", "والاب سمع الكلام، قاله الاب الفقير لابنه:"), - ("\"שמע וקח ממני, בני!", "اسمع#imp وخد منّي يا ابني!"), - ("לא נוכל לרצות את כל האנשים, אפילו אם נישא את החמור על הכתפיים שלנו.", "مش رح نقدر نرضّي كلّ النّاس، حتّى لو حملنا الحمار على كتافنا."), - ("אתה חי או מת, יש בך טוב או לא, האנשים ידברו וידברו!\"", "انت عايش ولّا مايت، فيك خير ولّا لا، النّاس رح يحكو ويحكو!", "אתה חי או מת, יש בְּךָ טוב או לא, האנשים ידברו וידברו!\""), - ]), - ("hw", "כתיבת משפטים", "write", True, []), - ], -} -for _level, _n in LESSONS: - assert _level in LEVELS and 1 <= _n <= LEVELS[_level][1], f"lesson {_n} of level {_level} is not in LEVELS" - - -def lesson_id(level: int, n: int) -> str: - """Level 1's lessons keep their first ids ("1", and its topics "l1-..."): recorded attempts and saved classes name - them. Later levels' are "2.1" (topics "l2.1-...").""" - return str(n) if level == 1 else f"{level}.{n}" - - -LEVEL_TABS = [{"id": str(level), "name": name} for level, (name, _) in LEVELS.items()] -# every planned lesson, in the course's order; one not written yet is `soon` -LESSON_TABS = [{"id": lesson_id(level, n), "name": f"שיעור {shown}", "level": str(level), "n": shown, - **({} if (level, n) in LESSONS else {"soon": True})} - for level, (_, count) in LEVELS.items() for n in range(1, count + 1) - for shown in [n + FIRST_LESSON.get(level, 1) - 1]] -# Topics the page lays out as a story: a conversation, all its lines on the card, said one after the other. -# A conversation has its two speakers' voices, by name in the voice list: the odd lines are the first one's, the even the -# second's. A story has none: it is told in the voice the student chose. -STORIES = {"l2-talk": ("אמיר", "ראאף"), "l3-story": (), "l4-story": (), "l5-story": (), "l6-story": ("איוב", "נסרין"), "l8-story": (), "l9-story": (), "l10-story": (), "l2.1-story": (), "l2.2-story": (), "l2.3-talk": ("אמיר", "ראאף"), "l2.3-story": (), "l2.4-talk": ("אמיר", "ראאף"), "l2.4-talk2": ("אמיר", "ראאף"), "l2.5-story": (), "l2.6-story": (), "l2.7-story": (), "l2.8-story": (), "l2.9-song": (), "l2.9-story": ()} -# What the card calls a topic's items where they are not sentences ("פועל 1 מתוך 4"), by topic id in any lesson, -# and what it asks for where the answer is two forms said together. -# A dialogue whose lines open with their speaker ("איוב: ..."): topic -> {speaker: the voice that plays them, "" for Raef's}. -# The name is cut from what is said and scored and kept as the line's `speaker`, which the page writes before it; -# a line without a name goes on in the last speaker's voice. -DIALOGUES = {"l6-story": {"איוב": "", "נסרין": "חנאן"}} -SPEAKER = re.compile(r"^(\S+): ") -UNITS = {"days": "מילה", "hours": "שעה", "pron": "מילה", "verbs": "פועל", "dual": "מילה", "family": "מילה", "nums": "מילה", "fem": "מילה", "colors": "צבע", "adj": "תואר", "dem": "מילה"} -TASKS = {"verbs": "אמרו את הפועל בערבית מדוברת: בעבר ואז בעתיד", - "dual": "אמרו בערבית מדוברת: ביחיד ואז בזוגי", - "fem": "אמרו בערבית מדוברת: בזכר ואז בנקבה", "colors": "אמרו את הצבע בערבית מדוברת: בזכר ואז בנקבה", - "adj": "אמרו את התואר בערבית מדוברת, ואז את ערך היתרון שלו"} -# The topics the student writes in (kind "write"): the task as the workbook gives it, and how many sentences it asks for. -WRITING = {"l1-hw": ("כתבו חמישה משפטים בערבית בעבר עם הפעלים, הגופים, שמות העצם שלמדנו.\n" - "השתמשו במילות הדבק: מַע - עם, וֵין - איפה, מִין - מי, לֵיש - מדוע, פִי - ב, בתוך, יש, " - "אֵימְתַא / וֵיְנְתַא - מתי.", 5), - "l2-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור השני.\n" - "2. כתבו 5 משפטים פשוטים בהטיות הפעלים שלמדנו בכל הגופים.\n" - "השתמשו במילות הדבק: עַלַא – ל (מקום), אל, על. עִנְד – אצל (אדם), יש ל. פִי – יש, ב, בתוך, קַבֵּל – לפני. " - "מַע – עם, וֵין – איפה, מִין – מי, לֵיש – מדוע. בַּעַדֵין - אחר כך.\n" - "השתמשו רק בפעלים שלמדנו: אַכַּל / יֹאכֵּל – אכל, סַאַל / יִסְאַל – שאל, פַתַח / יִפְתַח – פתח, " - "קַעַד / יֻקְעֹד – ישב.", 5), - "l3-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור השלישי.\n" - "2. כתבו 5 משפטים הכוללים כמה שיותר פעלים בהטיות גופים ומילים חדשות שלמדנו.\n" - "השתמשו במילות הדבק: עַלַא – ל (מקום), אל, על. עִנְד – אצל (אדם), יש ל. פִי – יש, ב, בתוך, קַבֵּל – לפני. " - "מַע – עם, וֵין – איפה, מִין – מי, לֵיש – מדוע. בַּעַדֵין - אחר כך.\n" - "הפעלים עד כה: דַחַ'ל / יֻדְחֹ'ל – נכנס, סַבַּח / יִסְבַּח – שחה, טַבַּח' / יֻטְבֹּח' – בישל, " - "נַקַל / יֻנְקֹל – עבר (דירה, עבודה), אַכַּל / יֹאכֵּל – אכל, סַאַל / יִסְאַל – שאל, פַתַח / יִפְתַח – פתח, " - "קַעַד / יֻקְעֹד – ישב.\n" - "למשל: קַבֵּל אֻסְבּוּעֵין אֹחְ'תִי נַקְלַת עַלַא בֵּית גְ'דִיד מַע ג'וֹז-הַא – לפני שבועיים אחותי עברה לבית חדש עם בעלה.", 5), - "l4-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור הרביעי.\n" - "2. כתבו 5 משפטים בעבר בלבד, רק עם הפעלים ואוצר המילים שלמדנו, עם כמה שיותר מילות דבק.\n" - "מילות הדבק: עַלַא – ל (מקום), אל, על. עִנְד – אצל (אדם), יש ל. פִי – יש, ב, בתוך. קַבֵּל – לפני. מַע – עם. " - "וֵין – איפה. מִין – מי. לֵיש – מדוע. לַמַّא – כאשר, כש. אִנّוֹ – ש (אחרי פועל). בַּעֵד – אחרי. " - "מַע בַּעַדֿ – ביחד. אֵימְתַא / וֵינְתַא – מתי.\n" - "הפעלים החדשים: אַגַ'א / יִגִ'י – בא, חַכַּא / יִחְכִּי – דיבר, סיפר, אַעְטַא / יֵעַטִי – נתן ל, " - "אִשְתַרַא / יִשְתְרִי – קנה.\n" - "למשל: לַמַّא אַנַא קַעַדֵת פִי בֵּיתִי, צַאחְבִּי אַגַ'א לַעִנְדִ-י – כאשר אני ישבתי בביתי, חברי בא אליי.", 5), - "l5-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור החמישי.\n" - "2. קראו והקשיבו שוב ושוב לסיפור השוק.\n" - "3. כתבו חמישה משפטים פשוטים בעבר בלבד עם הפעלים שלמדנו.\n" - "מילות הדבק: עַלַא – ל (מקום), אל, על. עִנְד – אצל (אדם), יש ל. פִי – יש, ב, בתוך. קַבֵּל – לפני. מַע – עם. " - "וֵין – איפה. מִין – מי. לֵיש – מדוע. לַמַّא – כאשר, כש. אִנّוֹ – ש (אחרי פועל). בַּעֵד – אחרי. " - "מַע בַּעַדֿ – ביחד. עַשַאן – כיוון ש. אֵימְתַא / וֵינְתַא – מתי.\n" - "הפעלים החדשים: קַאל / יִקוּל – אמר, רַאח / יִרוּח – הלך, כַּאן / יִכּוּן – היה, זַאר / יִזוּר – ביקר.", 5), - "l6-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור השישי.\n" - "2. כתבו חמישה משפטים פשוטים באותו הגוף ובאותו הפועל, פעם אחת בעבר ופעם אחת בעתיד.\n" - "למשל: אַנַא זֻרֵת אֹחְ'תִ-י, אַנַא אַזוּר אַח'וּ-י – אני ביקרתי את אחותי, אני אבקר את אחי.\n" - "הפעלים החדשים: פַאת / יִפוּת – נכנס, עבר, בַּאס / יִבּוּס – נישק, שַאף / יִשוּף – ראה, " - "סַאק / יִסוּק – נהג ב, אַחַ'ד / יֹאחֵ'ד – לקח, קיבל.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אחר כך, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַאנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, עַשַאן – כיוון ש, " - "אֵימְתַא / וֵינְתַא – מתי.", 5), - "l7-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל התרגילים בשיעור השביעי.\n" - "2. כתבו חמישה משפטים פשוטים באותו הגוף ובאותו הפועל, פעם אחת בעבר ופעם אחת בעתיד.\n" - "למשל: אַנַא כֻּנֵת עִנְד אֹחְ'תִי, אַנַא אַכּוּן עִנְד אַח'וּי – אני הייתי אצל אחותי, אני אהיה אצל אחי.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אחר כך, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַאנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, עַשַאן – כיוון ש, " - "אֵימְתַא / וֵינְתַא – מתי.", 5), - "l8-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור השמיני.\n" - "2. האזינו שוב ושוב לשיר הצבעים.\n" - "3. כתבו חמישה משפטים פשוטים בעבר בלבד, השתמשו בכמה שיותר פעלים וצבעים שלמדנו.\n" - "למשל: אַנַא פְהִמֵת אִנّוֹ אֹחְ'תִי אִשְתַרַת פֻסְתַאן אַסְוַד וּקֻלְתִ-לְהַא: מַבְּרוּכּ! – אני הבנתי שאחותי קנתה שמלה שחורה " - "ואמרתי לה: מזל טוב!\n" - "הפעלים החדשים: שִרֵבּ / יִשְרַבּ – שתה, טִלֵע / יִטְלַע – יצא, עלה, סִמֵע / יִסְמַע – שמע, פִהֵם / יִפְהַם – הבין, " - "רִגֵ'ע / יִרְגַ'ע – חזר.\n" - "מילות דבק חדשות: עַשַאן הֵיכּ – לכן, אַמַّא – אבל.", 5), - "l9-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את סיפור הקפה בהנאה ואת כל השיעור התשיעי.\n" - "2. כתבו 5 משפטים פשוטים, עם אותו הפועל ואותו הגוף, אחד בעבר ואחד בעתיד. לא לשכוח את המילה בִּדّ בעתיד.\n" - "למשל: אִנְתִי טְלִעְתִי מִן אֵלְבֵּית אִמְבַּארֵח, בִּדֵّכּ תִטְלַעִי מִן אֵלְבֵּית בֻּכְּרַא.\n" - "הפעלים החדשים: שַכַּר / יֻשְכֹּר – הודה ל, תְעַ'דַّא / יִתְעַ'דַّא – אכל ארוחת צהריים.\n" - "מילות דבק חדשות: בַּעֵד מַא – אחרי ש, קַבֵּל מַא – לפני ש, זַיّ מַא – כמו ש, עַן – על אודות, גַ'נְבּ – ליד.", 5), - "l10-hw": ("אֵלְוַזַֿאיֵף לַקוֹרְס אֵלגַ'איְ - המשימות לקורס הבא\n" - "1. פתרו את כל השיעור העשירי.\n" - "2. כתבו 5 משפטים פשוטים, עם אותו הפועל ואותו הגוף, אחד בעבר ואחד בעתיד.\n" - "למשל: אַנַא וּאִמִّי לְעִבְּנַא מַע בַּעַדֿ עִנְד חַ'אלְתִ-י, בֻּכְּרַא בִּדְّנַא נִלְעַבּ פִי אֵלְבֵּית.\n" - "הפעלים החדשים: נִזֵל / יִנְזֵל – ירד, וִצֵל / יוֹצַל – הגיע, דִֿחֵכּ / יִדְֿחַכּ – צחק, חייך, " - "לִבֵּס / יִלְבַּס – לבש, התלבש, לִעֵבּ / יִלְעַבּ – שיחק.\n" - "אַהַםّ אִשִי – הכי חשוב: המשיכו ללמוד ערבית ונצלו כל הזדמנות לברך ולדבר בערבית!", 5), - "l2.1-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור העשירי.\n" - "2. כתבו 5 משפטים באותו גוף ובאותו הפועל בעבר ורק אז בעתיד בשימוש רַח ולַאזֵם.\n" - "למשל: אַנַא רְגִ'עֵת מִן אֵ(ל)שֻّעֻ'ל עַלַא סֵיעַה אַרְבַּע, אַנַא רַח אַרְגַ'ע עַלְ-בֵּית עַלַא סֵיעַה סִתֵّה – " - "אני חזרתי מהעבודה בשעה ארבע, אני אחזור לבית בשעה שש.\n" - "הפעלים החדשים: רַכַּדֿ / יֻרְכֹּדֿ – רץ, דַפַע / יִדְפַע – שילם, דחף, הניע ל, בַּעַת' / יִבְּעַת' – שלח, " - "כַּסַר / יִכְּסֵר – שבר, רַבַּט / יֻרְבֹּט – קשר.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש.", 5), - "l2.2-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור האחד עשר.\n" - "2. כתבו 5 משפטים בעבר, בעתיד בשילוב של בִּדّ, רַח ולַאזֵם ואז בהוו'ה, בכל משפט להיצמד לאותו הגוף ואותו הפועל.\n" - "למשל: אִנְתוּ פַתַחְתוּ בַּאבּ אֵלְבֵּית, בֻּכְּרַא רַח תִפְתַחוּ אֵלְמַטְעַם, כֻּלّ יוֹם אִנְתוּ בְּתִפְתַחוּ אֵלְבַּאבּ – " - "אתם פתחתם את דלת הבית, מחר תפתחו את המסעדה, כל יום אתם פותחים את הדלת.\n" - "3. לכו להזמין פלאפל בערבית בלבד, צַחַّה!\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש, אִלִّי – מי ש, ש (אחרי שם עצם).", 5), - "l2.3-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. פתרו את כל השיעור השנים עשר.\n" - "2. תִסְע אַפְעַאל גְ'דִידֵה, הבניין השני, אִחְפְזֿוּ עַן עֵ'יבּ: נַזַّל – הוריד, וַצַّל – הוביל, דַֿחַّכּ – הצחיק, לַבַּّס – הלביש, " - "סַמַّע – השמיע, רַגַّ'ע – החזיר, פַהַّם – הסביר, טַלַّע – הוציא / העלה, שַרַّבּ – השקה.\n" - "3. כתבו חמישה משפטים, השתמשו רק בתשעת הפעלים של גזרת פִעֵל והבניין השני, בכל משפט השתמשו באותו הגוף " - "ובאותו הזמן. שמרו על סדר הזמנים עבר – עתיד – הווה.\n" - "למשל: אַנַא שְרִבֵּת קַהְוֵה וּשַרַّבֵּת אִבְּנִי עַצִיר עִנַבּ. בֻּכְּרַא אַנַא רַח אַשְרַבּ מַיّ וּאַשַרֵّבּ אִבְּנִי כַּמַאן כַּאסְתֵין מַיّ. " - "כֻּלّ צֻבֵּח אַנַא בַּשְרַבּ לִיתֵר וּנֻצّ מַיّ וּבַּשַרֵّבּ אִבְּנִי כַּאסֵת שַאיְ – אני שתיתי קפה והשקיתי את בני מיץ ענבים. " - "מחר אני אשתה מים ואשקה את בני שתי כוסות מים. כל בוקר אני שותה ליטר וחצי מים ומשקה את בני כוס תה.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש, אִלִّי – מי ש, ש (אחרי שם עצם).", 5), - "l2.4-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. חַ'מְס אַפְעַאל אִחְפְזֿוּ עַן עֵ'יבּ: צַלַّח / יְצַלֵّח – תיקן, עַ'נַّא / יְעַ'נִّי – שר, חַ'בַּّר / יְחַ'בֵּّר – עדכן, " - "חַדַֿّר / יְחַדֵֿّר – הכין, פַכַּّר / יְפַכֵּّר – חשב.\n" - "2. פתרו את כל השיעור השלושה עשר.\n" - "3. כתבו חמישה משפטים, השתמשו במגוון הפעלים שלמדנו, בכל משפט השתמשו באותו הגוף ואותו הפועל. " - "שמרו על סדר הזמנים עבר – עתיד – הווה.\n" - "למשל: אַנַא עַ'נֵّית עִ'נְוֵה קַצִירֵה, בֻּכְּרַא אַנַא רַח אַעַ'נִّי מַע צַאחִבְּתִי, כֻּלّ יוֹם אֵלְחַ'מִיס אַנַא בַּעַ'נִّי מַע אַצְחַאבִּי – " - "אני שרתי שיר קצר, מחר אני אשיר עם חברתי, בכל יום חמישי אני שר עם חבריי.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש, אִלִّי – מי ש, ש (אחרי שם עצם).", 5), - "l2.5-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. אַרְבַּע אַפְעַאל אִחְפְזֿוּ עַן עֵ'יבּ, גזרת ע\"י: זַאד / יְזִיד – הוסיף, גַ'אבּ / יְגִ'יבּ – הביא, בַּאע / יְבִּיע – מכר, " - "צַאר / יְצִיר – נהיה, החל ל, הפך ל. וּכַּמַאן מִן אֵלְקִצַّה: אִסְתַנַّא / יִסְתַנַّא – חיכה ל, וַקַّף / יְוַקֵّף – עצר.\n" - "2. פתרו את כל השיעור הארבעה עשר.\n" - "3. כתבו חמישה משפטים בעבר, בעתיד (רַח / לַאזֵם / בִּדّ) ואז בהוו'ה, עם הפעלים שלמדנו ובגזרת ע\"י, בשילוב הימים " - "והשעות, בכל משפט אותו הגוף ואותו הפועל.\n" - "למשל: אִחְנַא גִ'בְּנַא לַאַחְמַד כְּנַאפֵה, בִּדְّנַא נְגִ'יבּ לַמַחְמוּד בַּקְלַאוַה אֵלְיוֹם עַלַא סֵיעַה תְנַעַש אִלַّא רֻבֵּע, " - "אִחְנַא מִנְגִ'יבּ לַנוּר כֻּלّ יוֹם (א)לִתְנֵין שַוַארְמַה עַלַא מַכְּתַבּ-הַא – אנחנו הבאנו לאחמד כנאפה, אנחנו רוצים להביא " - "למחמוד בקלאווה היום ברבע לשתים עשרה, אנחנו מביאים לנור כל יום שני שווארמה למשרד שלה.\n" - "4. כתבו סיפור המשלב את מילות הדבק ואת הפעלים שלמדנו.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש, אִלִّי – מי ש, ש (אחרי שם עצם).", 5), - "l2.6-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. חַ'מְס אַפְעַאל אִחְפְזֿוּ עַן עֵ'יבּ, בניין 3: סַאעַד / יְסַאעֵד – עזר ל, גַ'אוַבּ / יְגַ'אוֵבּ – ענה ל, " - "עַאלַג' / יְעַאלֵג' – טיפל ב, לַאקַא / יְלַאקִי – מצא, פגש את, סַאוַא / יְסַאוִי – עשה, הכין, תיקן.\n" - "2. פתרו את כל השיעור החמישה עשר.\n" - "3. כתבו חמישה משפטים בעבר, בעתיד (רַח / לַאזֵם / בִּדّ) ואז בהוו'ה, עם הפעלים בבניין השני ובבניין השלישי, " - "בשילוב הימים והשעות, בכל משפט אותו הגוף ואותו הפועל.\n" - "למשל: אִחְנַא סַאעַדְנַא אַחְמַד, בִּדְّנַא נְסַאעֵד מַחְמוּד אֵלְיוֹם עַלַא סֵיעַה חְדַעְש וּרֻבֵּע, אִחְנַא מִנְסַאעֵד נוּר כֻּלّ יוֹם " - "אֵלְאַחַד עַלַא סֵיעַה וַאחַדֵה אִלַّא תֻלְת – אנחנו עזרנו לאחמד, אנחנו רוצים לעזור למחמוד היום באחת עשרה ורבע, " - "אנחנו עוזרים לנור כל יום ראשון בעשרים לאחת.\n" - "4. כתבו סיפור המשלב את מילות הדבק ואת הפעלים שלמדנו.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש, אִלִّי – מי ש, ש (אחרי שם עצם).", 5), - "l2.7-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. חַ'מְס אַפְעַאל אִחְפְזֿוּ עַן עֵ'יבּ, גזרת פִעֵל: נִסִי / יִנְסַא – שכח, נִגֵ'ח / יִנְגַ'ח – הצליח, עִמֵל / יִעְמַל – עשה, " - "זִעֵל מִן / יִזְעַל מִן – כעס על, וִקֵע / יוֹקַע – נפל. וּכַּמַאן מִן קִצֵّת אֵלְבַּאץ: בַּכַּא / יִבְּכִּי – בכה, " - "צַרַח' / יֻצְרֹח' – צרח.\n" - "2. פתרו את כל השיעור השישה עשר.\n" - "3. כתבו חמישה משפטים בעבר, בעתיד (בִּדّ / רַח / לַאזֵם), בהוו'ה ואז בציווי, עם אותו הגוף ואותו הפועל בכל משפט.\n" - "למשל: אִנְתִי נְסִיתִי תִחְכִּי מַע אַחְמַד? אִנְשַאללַّה מִש רַח תִנְסִי תְגִ'יבִּי לַמַחְמוּד בַּקְלַאוַה פִי יוֹם אֵלְחַ'מִיס, " - "אִנְתִי דַאיְמַן בְּתִנְסִי תְחַ'בְּّרִי-נִי קַבֵּל מַא תִיגִ'י לַהוֹן – את שכחת לדבר עם אחמד? בעזרת ה' לא תשכחי להביא " - "למחמוד בקלאווה ביום חמישי, את תמיד שוכחת לעדכן אותי לפני שאת באה לכאן.\n" - "4. כתבו סיפור קצר ומרגש בעל מסר חיובי, השתמשו בכמה שיותר פעלים שלמדנו עד כה.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש, אִלִّי – מי ש, ש (אחרי שם עצם).", 5), - "l2.8-hw": ("אֵלְוַזַֿאיֵף לַדַרְס אֵלגַ'איְ - המשימות לשיעור הבא\n" - "1. סִתّ אַפְעַאל אִחְפְזֿוּ עַן עֵ'יבּ, גזרת הכפולים: חַטّ / יְחֻטّ – הניח, שׂם, רַדّ / יְרֻדّ – ענה, חַבّ / יְחִבّ – אהב, רצה ל, " - "דַֿלّ / יְדַֿלّ – נשאר, המשיך ל, חַסّ / יְחִסّ – הרגיש, רַןّ עַלַא / יְרִןّ עַלַא – צלצל ל.\n" - "2. פתרו את כל השיעור השבעה עשר.\n" - "3. כתבו חמישה משפטים בעבר, בעתיד (בִּדّ / רַח / לַאזֵם), בהוו'ה ורק אז בציווי, עם אותו הגוף ואותו הפועל בכל משפט.\n" - "למשל: לַמַّא אִנְתֵ סַאעַדֵת חַ'אלַכּ, קַדֵّיש וַקֵת אַחַ'ד מַעַ-כּ? הוּّ רַח יֹאחֹ'ד סַיַّארְתוֹ מִן אֵלְכַּרַאג'? אַכַּם מַרַّה " - "בִּ(א)לְיוֹם הוּّ בֹּאחֹ'ד חַבֵּّת אַסְבִּירִין? אַנַא קֻלְת-לוֹ: \"תְפַדַֿّל, חֹ'ד!\" – כאשר עזרת לדוד שלך, כמה זמן לקח לך? " - "הוא ייקח את המכונית שלו מהמוסך? כמה פעמים ביום הוא לוקח כדור אספירין? אני אמרתי לו: \"בבקשה, קח!\"\n" - "4. כתבו סיפור קצר ומרגש בעל מסר חיובי, השתמשו בכמה שיותר פעלים שלמדנו.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש, אִלִّי – מי ש, ש (אחרי שם עצם).", 5), - "l2.9-hw": ("אֵלְוַזַֿאיֵף לַ(א)לְקוּרְס אֵלגַ'איְ - המשימות לקורס הבא\n" - "1. חַ'מְס אַפְעַאל אִחְפְזֿוּ עַן עֵ'יבּ: קִדֵר / יִקְדַר – יכול, מִשִי / יִמְשִי – התהלך, נַאם / יְנַאם – ישן, " - "עַאש / יְעִיש – חי, עִרֵף / יִעְרַף – ידע, הכיר.\n" - "2. פתרו את כל השיעור השמונה עשר.\n" - "3. כתבו חמישה משפטים פשוטים, בכל אחד מהם משפט בעבר, בהוו'ה (פעולה שחוזרת על עצמה) ואז בבינוני הפועל.\n" - "למשל: אַנַא רֻחֵת עַלַא אֵלְמוֹל, כֻּלّ יוֹם אֵ(ל)תַّלַאתַה אַנַא בַּרוּח לַעִנְד סִתّ-י, אִסַّא אַנַא רַאיֵח עַלַא אֵ(ל)שֻّעֻ'ל – " - "אני הלכתי לקניון, כל יום שלישי אני הולך לסבתא שלי, עכשיו אני הולך לעבודה.\n" - "4. ספרו סיפור קצר בעל מסר חיובי, שלבו בו כמה שיותר פעלים, מילים ומושגים שלמדנו.\n" - "מילות הדבק: וֵין – איפה, מִין – מי, מִן וֵין – מאיפה, לֵיש – מדוע, עִנְד – אצל, יש ל, עַלַא – למקום, על, " - "מַע – עם, כִּיף – איך, שוּ / אֵיש – מה, קַבֵּל – לפני, בַּעַדֵין – אח\"כ, בַּעֵד – אחרי, מַע בַּעַדֿ – יחד, " - "תַ'אנִי יוֹם – למחרת, לַמַّא – כאשר, קַדֵّיש / אַכַּם – כמה, לַחַאלִי – לבדי, זַיّ – כמו, קַבֵּל מַא – לפני ש, " - "זַיّ מַא – כמו ש, בַּעֵד מַא – אחרי ש, אִלִّי – מי ש, ש (אחרי שם עצם).", 5)} -def _words_topic(key: tuple[int, int]) -> str: - """The lesson's "learn" topic, whose words its "match" game deals.""" - return next(f"l{lesson_id(*key)}-{tid}" for tid, _, kind, *_ in LESSONS[key] if kind == "learn") - - -CATEGORIES = [{"id": f"l{lesson_id(*key)}-{tid}", "name": name, "lesson": lesson_id(*key), - **({"write": dict(zip(("task", "lines"), WRITING[f"l{lesson_id(*key)}-{tid}"]))} if kind == "write" else {}), - **({"learn": True} if kind == "learn" else {}), - **({"match": _words_topic(key)} if kind == "match" else {}), - **({"story": True} if f"l{lesson_id(*key)}-{tid}" in STORIES else {}), - **({"voices": list(STORIES[f"l{lesson_id(*key)}-{tid}"])} if STORIES.get(f"l{lesson_id(*key)}-{tid}") else {}), - **({"unit": UNITS[tid]} if tid in UNITS else {}), **({"task": TASKS[tid]} if tid in TASKS else {})} - for key, topics in LESSONS.items() for tid, name, kind, *_ in topics] - -# Games live outside the lesson path. Their phrases use the same scoring bank, but they are not topics, -# do not affect lesson completion, and cannot be assigned as homework. -GAMES = [{"id": "game-yalla-verb", "name": "פועל זמני", "lesson": "1", "bubble": True, "hidden": False, - "unit": "פועל", "task": "אמרו את הפועל בערבית מדוברת: בעבר ואז בעתיד, לפני שהבועה נעלמת"}] - - -def _verb_hint(unlock: str, sentence: str) -> tuple[str, str]: - """The workbook's building and weak/stem class for a game's past + future pair.""" - defaults = { - "1": ("בניין ראשון", "גזרת פַעַל"), "3": ("בניין ראשון", "גזרת פַעַל"), - "5": ("בניין ראשון", "גזרת ע״ו"), "6": ("בניין ראשון", "גזרת ע״ו"), - "8": ("בניין ראשון", "גזרת פִעֵל"), "10": ("בניין ראשון", "גזרת פִעֵל"), - "2.1": ("בניין ראשון", "גזרת פַעַל"), "2.3": ("בניין שני", "גזרת השלמים"), - "2.4": ("בניין שני", "גזרת השלמים"), "2.6": ("בניין שלישי", "גזרת השלמים"), - "2.7": ("בניין ראשון", "גזרת פִעֵל"), "2.8": ("בניין ראשון", "גזרת הכפולים"), - "2.9": ("בניין ראשון", "גזרת פִעֵל"), - } - past = sentence.split("،", 1)[0] - special = { - "اجا": ("בניין ראשון", "גזרת ל״א"), "حكى": ("בניין ראשון", "גזרת ל״א"), - "اعطى": ("בניין רביעי", "גזרת ל״א"), "اشترى": ("בניין שמיני", "גזרת ל״א"), - "اخد": ("בניין ראשון", "גזרת פַעַל"), "شكر": ("בניין ראשון", "גזרת פַעַל"), - "تغدّى": ("בניין חמישי", "גזרת ל״א"), "غنّى": ("בניין שני", "גזרת ל״א"), - "زاد": ("בניין ראשון", "גזרת ע״י"), "جاب": ("בניין ראשון", "גזרת ע״י"), - "باع": ("בניין ראשון", "גזרת ע״י"), "صار": ("בניין ראשון", "גזרת ע״י"), - "وقّف": ("בניין שני", "גזרת השלמים"), "استنّى": ("בניין עשירי", "גזרת ל״א"), - "لاقى": ("בניין שלישי", "גזרת ל״א"), "ساوى": ("בניין שלישי", "גזרת ל״א"), - "نسي": ("בניין ראשון", "גזרת ל״א"), "بكى": ("בניין ראשון", "גזרת ל״א"), - "زعّل": ("בניין שני", "גזרת השלמים"), "مشي": ("בניין ראשון", "גזרת ל״א"), - "نام": ("בניין ראשון", "גזרת ע״ו"), "عاش": ("בניין ראשון", "גזרת ע״י"), - } - return special[past] if past in special else defaults[unlock] - - -def _game_verbs() -> list[tuple[str, tuple, str, str]]: - """Every verb when first taught: unlock lesson, exercise, building, and class. - - Later review topics repeat earlier verbs; keeping the first occurrence makes the game cumulative without - putting the same verb into its pool twice. - """ - out, seen = [], set() - for key, topics in LESSONS.items(): - for tid, _, _, _, items in topics: - if tid != "verbs": - continue - for item in items: - signature = item[1] # the Arabic past + future pair is the stable identity - if signature not in seen: - seen.add(signature) - unlock = lesson_id(*key) - out.append((unlock, item, *_verb_hint(unlock, signature))) - return out - - -GAME_ITEMS = {"game-yalla-verb": _game_verbs()} - - -def _present_of(key: str) -> tuple[str, list[str]] | None: - """The present of a future form that LEX has: بـ before it (بتسأل, بيشوف), and مـ as well before "we" (منيجي). "I" - loses its ا (اسأل -> بسأل). The prefix takes the vowel the form needs: בַּסְאַל, בְּתִסְאַל, בִּתְשוּף, בִּיסְאַל.""" - body, _, tag = key[1:].partition("#") - tag = "#" + tag if tag else "" - if key[:1] == "ب" and body[:1] not in "تني": # "I": the future's ا, or its آ (باكل) - for k in ("ا" + body + tag, "ا" + body + "#i", "آ" + body[1:] + tag): # اسأل alone is the imperative: the future is اسأل#i - if k in LEX and LEX[k][0].startswith("אַ"): - tr, ipas = LEX[k] - return "ב\u05b7\u05bc" + tr[2:], ["b " + i.removeprefix("ʔ ") for i in ipas] - return None - if not (key[:1] == "ب" and body[:1] in "تني" or key[:1] == "م" and body[:1] == "ن") or body + tag not in LEX: - return None - tr, ipas = LEX[body + tag] - he, dot, ipa = ("ב", "\u05bc", "b") if key[0] == "ب" else ("מ", "", "m") # the vowel goes under the letter, before its dagesh - if tr[:2] in ("יִ", "יְ"): # يسأل, يشوف: the י is only heard as the prefix's vowel - heard = [i.removeprefix("j i ").removeprefix("j ").removeprefix("i ") for i in ipas] - return he + "\u05b4" + dot + "י" + tr[2:], list(dict.fromkeys(f"{ipa} i {i}" for i in heard)) - if tr[1:2] == "ְ": # تشوف, نروح: two consonants open the form, so the prefix has a vowel - return he + "\u05b4" + dot + tr, [f"{ipa} i {i}" for i in ipas] - return he + "\u05b0" + dot + tr, [f"{ipa} {i}" for i in ipas] - - -def _word(token: str) -> tuple[str, str, list[str]]: - """(Arabic as shown, transliteration, IPA readings) of one word of a sentence.""" - key = token.rstrip("".join(PUNCT)) - tail = token[len(key):] - tr_tail = "".join(PUNCT[c] for c in tail) - if key in LEX: - tr, ipas = LEX[key] - elif _present_of(key): # بـ on a future form: the present - tr, ipas = _present_of(key) - elif key.startswith("و") and (key[1:] in LEX or _present_of(key[1:])): # "and" written on the word - tr, ipas = LEX.get(key[1:]) or _present_of(key[1:]) - tr, ipas = "וּ" + tr, ["u " + i for i in ipas] - else: - raise KeyError(f"workbook word not in LEX: {key}") - return key.partition("#")[0] + tail, tr + tr_tail, ipas - - -def _variants(words: list[tuple[str, str, list[str]]]) -> list[tuple[str, str]]: - """Every combination of the words' readings; when that is too many, the main reading throughout - and then each word's n-th reading together (a speaker who says ق as q says it in every word).""" - ar = " ".join(a for a, _, _ in words) - readings = [ipas for _, _, ipas in words] - count = 1 - for r in readings: - count *= len(r) - if count <= MAX_VARIANTS: - combos = itertools.product(*readings) - else: - combos = dict.fromkeys(tuple(r[min(n, len(r) - 1)] for r in readings) - for n in range(max(map(len, readings)))) - return [(ar, " | ".join(c)) for c in combos] - - -def workbook_phrases() -> list[dict]: - """The lessons' items as phrases (see phrases.py). `seq` keeps an ordered topic's order and - `read` marks the Arabic -> Hebrew ones, where the page shows `tr` and hides `he`; - `he_say` is the pointed Hebrew for the prompt voice, where the plain text is misread. - A phrase's id is its place in the bank and recorded attempts name it, so the "learn" cards, which came later, - are added at the end, after the games'.""" - out = [] - - def add(key, tid, kind, in_order, items, **extra): - speakers, who, said = DIALOGUES.get(f"l{lesson_id(*key)}-{tid}", {}), None, None - for seq, (he, sentence, *he_say) in enumerate(items, 1): - m = SPEAKER.match(he) - if m and m[1] in speakers: - who = said = m[1] - he, sentence = he[m.end():], SPEAKER.sub("", sentence) - words = [_word(t) for t in sentence.split()] - variants = _variants(words) - # These vocabulary prompts teach two synonymous words. Saying both remains valid, but unlike - # conjugation and singular/dual pairs, either synonym on its own is also a complete answer. - if "(שתי מילים)" in he: - singles = [(a.rstrip("".join(PUNCT)), tr.rstrip("".join(PUNCT.values())), ipas) for a, tr, ipas in words] - variants += [variant for word in singles for variant in _variants([word]) if variant not in variants] - phrase = dict(cat=f"l{lesson_id(*key)}-{tid}", he=he, tr=" ".join(t for _, t, _ in words), - variants=variants, read=kind == "read", **extra) - say = _voiced(he_say[0] if he_say else he) - if say != he: - phrase["he_say"] = say - if in_order: - phrase["seq"] = seq - if said: - phrase["speaker"], said = said, None - if speakers.get(who): - phrase["voice"] = speakers[who] - out.append(phrase) - - for key, topics in LESSONS.items(): - for tid, _, kind, in_order, items in topics: - if kind != "learn": - add(key, tid, kind, in_order, items) - for game in GAMES: - for unlock, (he, sentence, *he_say), binyan, gizra in GAME_ITEMS[game["id"]]: - words = [_word(t) for t in sentence.split()] - he = re.sub(r"\s*\(עבר, עתיד\)$", "", he) - phrase = dict(cat=game["id"], he=he, tr=" ".join(t for _, t, _ in words), - variants=_variants(words), unlock=unlock, binyan=binyan, gizra=gizra) - say = _voiced(he_say[0] if he_say else he) - if say != he: - phrase["he_say"] = say - out.append(phrase) - for key, topics in LESSONS.items(): - for tid, _, kind, in_order, items in topics: - if kind == "learn": - add(key, tid, kind, in_order, items, listed=False) - return out diff --git a/arabic_splash/README.md b/arabic_splash/README.md deleted file mode 100644 index c655c19e5c50e246799364ceba0b5998650b969a..0000000000000000000000000000000000000000 --- a/arabic_splash/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Animated splash for FastAPI / HTML - -Run locally: - -```bash -pip install -r requirements.txt -uvicorn app:app --host 0.0.0.0 --port 7860 -``` - -For Hugging Face Spaces Docker, run uvicorn on port 7860 and expose port 7860 in your Dockerfile. - -To integrate into an existing project: copy static/logo.png, splash.css, splash.js; add the stylesheet to your HTML head, the `#app-splash` div from index.html at the beginning of body, and the JS script at the end of body. Ensure your FastAPI app serves /static. The demo uses three clipped copies of the original image, preserving the logo exactly. It is a reveal animation, not true vector stroke tracing. diff --git a/arabic_splash/app.py b/arabic_splash/app.py deleted file mode 100644 index 67a953fd9b56190b6b61757f8379c01c0f8eeb57..0000000000000000000000000000000000000000 --- a/arabic_splash/app.py +++ /dev/null @@ -1,12 +0,0 @@ -from pathlib import Path -from fastapi import FastAPI -from fastapi.responses import FileResponse -from fastapi.staticfiles import StaticFiles - -BASE = Path(__file__).resolve().parent -app = FastAPI() -app.mount('/static', StaticFiles(directory=BASE / 'static'), name='static') - -@app.get('/') -def home(): - return FileResponse(BASE / 'index.html') diff --git a/arabic_splash/index.html b/arabic_splash/index.html deleted file mode 100644 index eb3bdc4ef43a8e73a724b8a94272cbc85d394197..0000000000000000000000000000000000000000 --- a/arabic_splash/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - Arabic School — Splash Demo - - - - -
- -
-

ברוכים הבאים

זהו מסך הדגמה. החליפו את התוכן הזה בממשק האפליקציה שלכם.

- - - diff --git a/arabic_splash/requirements.txt b/arabic_splash/requirements.txt deleted file mode 100644 index 97dc7cd8c1fd2f07d8ec79a1117664c5ebaf2842..0000000000000000000000000000000000000000 --- a/arabic_splash/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi -uvicorn diff --git a/arabic_splash/static/logo.png b/arabic_splash/static/logo.png deleted file mode 100644 index bf060daffc646ae359a3a62f309ccc30d7e110f0..0000000000000000000000000000000000000000 --- a/arabic_splash/static/logo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:35ce45096bf91d4a8ba094ea8a9857ed558736a54a8eb299eefd6e8fcbc82600 -size 49324 diff --git a/arabic_splash/static/splash.css b/arabic_splash/static/splash.css deleted file mode 100644 index 2c799b0a17310fff959e58dc567db0f0cbaa3562..0000000000000000000000000000000000000000 --- a/arabic_splash/static/splash.css +++ /dev/null @@ -1,12 +0,0 @@ -:root { --splash-bg: #fff; --splash-duration: 3100ms; } -#app-splash { position:fixed; inset:0; z-index:99999; display:grid; place-items:center; background:var(--splash-bg); transition:opacity .45s ease, visibility .45s ease; } -#app-splash.splash-done { opacity:0; visibility:hidden; pointer-events:none; } -.splash-art { position:relative; width:min(88vw, 560px); aspect-ratio:489/225; } -.splash-layer { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; } -.splash-symbol { clip-path:polygon(64% 0,100% 0,100% 100%,64% 100%); animation:symbol-in .95s ease-out both; transform-origin:82% 48%; } -.splash-arabic { clip-path:polygon(0 0,65% 0,65% 72%,0 72%); animation:arabic-in 1.05s .65s ease-out both; } -.splash-hebrew { clip-path:polygon(0 69%,65% 69%,65% 100%,0 100%); animation:hebrew-in .65s 1.6s ease-out both; } -@keyframes symbol-in { from {opacity:0; transform:scale(.78) rotate(-18deg)} to {opacity:1; transform:scale(1) rotate(0)} } -@keyframes arabic-in { from {opacity:0; clip-path:polygon(65% 0,65% 0,65% 72%,65% 72%)} to {opacity:1; clip-path:polygon(0 0,65% 0,65% 72%,0 72%)} } -@keyframes hebrew-in { from {opacity:0; transform:translateY(10px)} to {opacity:1; transform:translateY(0)} } -@media (prefers-reduced-motion:reduce) { .splash-layer {animation:none!important} #app-splash {transition:none} } diff --git a/arabic_splash/static/splash.js b/arabic_splash/static/splash.js deleted file mode 100644 index 5a9238d996b76832ccdbff0b39786bdc14059e48..0000000000000000000000000000000000000000 --- a/arabic_splash/static/splash.js +++ /dev/null @@ -1,9 +0,0 @@ -(() => { - const splash = document.getElementById('app-splash'); - if (!splash) return; - const hide = () => { splash.classList.add('splash-done'); window.setTimeout(() => splash.remove(), 500); }; - const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - const start = () => window.setTimeout(hide, reduced ? 300 : 3100); - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once:true }); - else start(); -})(); diff --git a/data/example.csv b/data/example.csv deleted file mode 100644 index f3208cf6e7a3db8ff60e837c09fce0ede9f0b365..0000000000000000000000000000000000000000 --- a/data/example.csv +++ /dev/null @@ -1,4 +0,0 @@ -audio,ipa -native/toilet_01.webm,w eː n | i l ħ a m m aː m -learners/toilet_dana_03.webm,w eː n | i l χ a m m aː m -native/coffee_01.webm,b i d d i | f i n dʒ aː n | ʔ a h w e diff --git a/data/minerva_metadata.json b/data/minerva_metadata.json deleted file mode 100644 index e0140f9f7a8c33fdeb5a372cfd201de76b04b3c5..0000000000000000000000000000000000000000 --- a/data/minerva_metadata.json +++ /dev/null @@ -1,27998 +0,0 @@ -[ - { - "file_name": "במכולת * פִי א-דֻכַּאן * في الدّكّان * קטע שיחה בערבית מדוברת [U8aBRd3-wlM].webm", - "youtube_id": "U8aBRd3-wlM", - "title_hebrew": "במכולת", - "segments": [ - { - "voice_time_seconds": { - "start": 9.0, - "end": 10.8 - }, - "arabic": "فيه عندك حليب طارة؟", - "hebrew_transliteration": "פִיה עִנְדַכּ חַלִיב טַארֶה?", - "hebrew": "יש לך חלב טרי?", - "expected_phonemes": "f iː h | ʕ i n d a k | ħ a l iː b | tˤ aː r e" - }, - { - "voice_time_seconds": { - "start": 10.8, - "end": 12.8 - }, - "arabic": "آه، جوّا في التلّاجة، على إيدك اليمين.", - "hebrew_transliteration": "אַה, גֻ'וַּא פִי ת-תַּלַּאגֶ'ה, עַלַא אִידֶכּ לְ-יַמִין.", - "hebrew": "כן, בפנים במקרר, מימינך.", - "expected_phonemes": "ʔ aː | dʒ u w a | f i | t t a l aː dʒ e | ʕ a l a | ʔ iː d e k | l j a m iː n" - }, - { - "voice_time_seconds": { - "start": 12.8, - "end": 14.2 - }, - "arabic": "ممتاز.", - "hebrew_transliteration": "מֻמְתַאז.", - "hebrew": "מצוין.", - "expected_phonemes": "m u m t aː z" - }, - { - "voice_time_seconds": { - "start": 14.2, - "end": 17.6 - }, - "arabic": "ممكن تحطّلي كمان وقيّة جبنة صفرا؟", - "hebrew_transliteration": "מֻמְכּן תְחֻטְלִי כַּמַאן וַקִיַּת גֻ'בְּנֶה צַפְרַא?", - "hebrew": "אפשר לשים לי גם חבילת גבינה צהובה?", - "expected_phonemes": "m u m k n | t ħ u tˤ l i | k a m aː n | w a ʔ i j a t | dʒ u b n e | sˤ a f r a" - }, - { - "voice_time_seconds": { - "start": 17.6, - "end": 21.8 - }, - "arabic": "آه طبعًا، بس ثواني أخلّص معاه الحساب...", - "hebrew_transliteration": "אַה טַבְּעַן, בַּס תְוַאנִי אַחְ'לַצ מַעַאה לְ-חִסַאב.", - "hebrew": "כן, כמובן, רק רגע שאסיים איתו את החשבון.", - "expected_phonemes": "ʔ aː | tˤ a b ʕ a n | b a s | t w aː n i | ʔ a x l a sˤ | m a ʕ aː h | l ħ i s aː b" - }, - { - "voice_time_seconds": { - "start": 21.8, - "end": 24.6 - }, - "arabic": "تفضّلي.", - "hebrew_transliteration": "תְפַדַّלִי.", - "hebrew": "בבקשה.", - "expected_phonemes": "t f a dˤ dˤ a l i" - }, - { - "voice_time_seconds": { - "start": 24.6, - "end": 27.6 - }, - "arabic": "هذا هو؟ أيوة، قديش الحساب؟", - "hebrew_transliteration": "הַאדַא הֻוֶּ? אַיוַה, קַדֵּיש לְ-חִסַאב?", - "hebrew": "זהו? כן, כמה החשבון?", - "expected_phonemes": "h aː d a | h u w e | ʔ a j w a | ʔ a d eː ʃ | l ħ i s aː b" - }, - { - "voice_time_seconds": { - "start": 27.6, - "end": 30.4 - }, - "arabic": "خمسة وعشرين شيقل. تفضّل.", - "hebrew_transliteration": "חַ'מְסֶה וְעִשְרִין שֵיקֶל. תְפַדַّל.", - "hebrew": "עשרים וחמישה שקל. בבקשה.", - "expected_phonemes": "x a m s e | u ʕ i ʃ r iː n | ʃ eː ʔ e l | t f a dˤ dˤ a l" - }, - { - "voice_time_seconds": { - "start": 30.4, - "end": 33.0 - }, - "arabic": "معكيش أصغر؟ معيش صرافة ميّة...", - "hebrew_transliteration": "מַעְכִּיש אַצְעַ'ר? מַעִיש צַרַאפֶת מִיֶּה.", - "hebrew": "אין לך קטן יותר? אין לי עודף ממאה.", - "expected_phonemes": "m a ʕ k iː ʃ | ʔ a z ɣ a r | m a ʕ iː ʃ | sˤ a r aː f e t | m i j e" - }, - { - "voice_time_seconds": { - "start": 33.0, - "end": 35.4 - }, - "arabic": "لحظة أفحص...", - "hebrew_transliteration": "לַחְטַ'ה אַפְחַצ.", - "hebrew": "רגע, אבדוק.", - "expected_phonemes": "l a ħ ðˤ a | ʔ a f ħ a sˤ" - }, - { - "voice_time_seconds": { - "start": 35.4, - "end": 39.2 - }, - "arabic": "لا والله معيش، فيه بس ٢٠ إذا بيفيدوك.", - "hebrew_transliteration": "לַא וַאללַה מַעִיש, פִיה בַּס עִשְרִין אִזַא בִּיפִידוּכּ.", - "hebrew": "לא, בחיי, אין לי; יש רק עשרים, אם זה עוזר לך.", - "expected_phonemes": "l aː | w aː l l a | m a ʕ iː ʃ | f iː h | b a s | ʕ i ʃ r iː n | ʔ i z a | b iː f iː d uː k" - }, - { - "voice_time_seconds": { - "start": 39.2, - "end": 42.8 - }, - "arabic": "لا معلش، مش مهم، بدبّرها.", - "hebrew_transliteration": "לַא מַעְלֵיש, מֻש מֻהִם, בַּדַבֶּרְהַא.", - "hebrew": "לא נורא, לא חשוב, אסתדר.", - "expected_phonemes": "l aː | m a ʕ l eː ʃ | m u ʃ | m u h i m | b a d a b e r h a" - }, - { - "voice_time_seconds": { - "start": 42.8, - "end": 44.6 - }, - "arabic": "تفضّلي الباقي، معوّضين.", - "hebrew_transliteration": "תְפַדַّלִי אִלְ-בַּאקִי, מְעַוַّדִין.", - "hebrew": "הנה העודף, תבואי שוב.", - "expected_phonemes": "t f a d d a l i | i l b aː ʔ i | m ʕ a w w a d iː n" - }, - { - "voice_time_seconds": { - "start": 44.6, - "end": 46.4 - }, - "arabic": "تسلم، يعطيك العافية!", - "hebrew_transliteration": "תִסְלַם, יַעְטִיכּ אִלְ-עַאפְיֶה!", - "hebrew": "תודה, שיהיה לך כוח ובריאות!", - "expected_phonemes": "t i s l a m | j a ʕ tˤ iː k | i l ʕ aː f j e" - }, - { - "voice_time_seconds": { - "start": 46.4, - "end": 48.4 - }, - "arabic": "الله يعافيك، مع السلامة!", - "hebrew_transliteration": "אַללַה יְעַאפִיכּ, מַע אִלְ-סַלַאמֶה!", - "hebrew": "שאלוהים ייתן לך בריאות, להתראות!", - "expected_phonemes": "i l l a | j ʕ aː f iː k | m a ʕ | i l s a l aː m e" - } - ] - }, - { - "file_name": "אסיפת הורים * אִגתִמַאע אַהַאלִי * إجتماع أهالي * קטע שיחה בערבית מדוברת [OuAwxXUZBU4].webm", - "youtube_id": "OuAwxXUZBU4", - "title_hebrew": "אסיפת הורים", - "segments": [ - { - "voice_time_seconds": { - "start": 8.8, - "end": 11.8 - }, - "arabic": "تمام، كيفك؟ شو صار معك؟", - "hebrew_transliteration": "תַמַאם, כִּיפֶכּ? שוּ צַאר מַעֶכּ?", - "hebrew": "בסדר, מה שלומך? מה קרה איתך?", - "expected_phonemes": "t a m aː m | k iː f e k | ʃ u | sˤ aː r | m a ʕ e k" - }, - { - "voice_time_seconds": { - "start": 11.8, - "end": 14.2 - }, - "arabic": "بسيدر، اسا طلعت من المدرسة.", - "hebrew_transliteration": "בְּסֵידֶר, אִסַּא טִלְעֵת מִן אִלְ-מַדְרַסֶה.", - "hebrew": "בסדר, עכשיו יצאתי מבית הספר.", - "expected_phonemes": "b s eː d e r | ʔ i s a | tˤ i l ʕ i t | m i n | i l m a d r a s e" - }, - { - "voice_time_seconds": { - "start": 14.2, - "end": 17.4 - }, - "arabic": "قعدت مع المربية وباقي لمعلمات.", - "hebrew_transliteration": "קַעַדֶת מַע אִלְ-מֻרַבִּיֶה וּבַּאקִי לְ-מֻעַלִּמַאת.", - "hebrew": "ישבתי עם המחנכת ועם שאר המורות.", - "expected_phonemes": "ʔ a ʕ a d e t | m a ʕ | i l m u r a b i j e | u b aː ʔ i | l m u ʕ a l i m aː t" - }, - { - "voice_time_seconds": { - "start": 17.4, - "end": 21.2 - }, - "arabic": "ميس نوال بتقول انو ربيع ولد بجنن وعقلو كبير.", - "hebrew_transliteration": "מִיס נַוַאל בְּתְקוּל אִנּוּ רַבִּיע וַלַד בִּגַ'נֵּן וְעַקְלוֹ כְּבִּיר.", - "hebrew": "מיס נוואל אומרת שרביע ילד מקסים ובוגר לגילו.", - "expected_phonemes": "m iː s | n a w aː l | b t ʔ uː l | ʔ i n u | r a b iː ʕ | w a l a d | b i dʒ a n i n | u ʕ a ʔ l o | k b iː r" - }, - { - "voice_time_seconds": { - "start": 21.2, - "end": 24.4 - }, - "arabic": "مأدّب، بحب يساعد غيرو، شاطر وفتح.", - "hebrew_transliteration": "מֻאַדַّבּ, בְּחִבּ יְסַאעֶד עֵ'ירוֹ, שַאטֶר וּפַתֶח.", - "hebrew": "הוא מנומס, אוהב לעזור לאחרים, חכם ופיקח.", - "expected_phonemes": "m u ʔ a d d a b | b ħ i b | j s aː ʕ e d | ɣ eː r o | ʃ aː tˤ e r | u f a t e ħ" - }, - { - "voice_time_seconds": { - "start": 24.4, - "end": 25.8 - }, - "arabic": "بس متل العادة.", - "hebrew_transliteration": "בַּס מִתְל אִלְ-עַאדֶה.", - "hebrew": "אבל כרגיל.", - "expected_phonemes": "b a s | m i t l | i l ʕ aː d e" - }, - { - "voice_time_seconds": { - "start": 25.8, - "end": 29.4 - }, - "arabic": "لازم يشتغل اكتر على مودوع التركيز.", - "hebrew_transliteration": "לַאזֶם יִשְתַעֵ'ל אַכְּתַר עַלַא מַוְדוּע אִלְ-תַרְכִּיז.", - "hebrew": "הוא צריך לעבוד יותר על נושא הריכוז.", - "expected_phonemes": "l aː z e m | j i ʃ t a ɣ i l | ʔ a k t a r | ʕ a l a | m a w d uː ʕ | i l t a r k iː z" - }, - { - "voice_time_seconds": { - "start": 29.4, - "end": 31.0 - }, - "arabic": "كتير بشت وبسرح.", - "hebrew_transliteration": "כְּתִיר בִּשַתּ וּבִּסְרַח.", - "hebrew": "הוא מוסח וחולם בהקיץ הרבה.", - "expected_phonemes": "k t iː r | b i ʃ a t | u b i s r a ħ" - }, - { - "voice_time_seconds": { - "start": 31.0, - "end": 35.8 - }, - "arabic": "وهادا بحليه يعمل اغلاط زغيرة في دراسة.", - "hebrew_transliteration": "וְהַאדַא בִּחַלִּיה יַעְמַל אַעְ'לַאט זְעִ'ירֶה פִי דִּרַאסֶה.", - "hebrew": "וזה גורם לו לעשות טעויות קטנות בלימודים.", - "expected_phonemes": "u h aː d a | b i ħ a l iː h | j a ʕ m a l | ʔ a ɣ l aː tˤ | z ɣ iː r e | f i | d i r aː s e" - }, - { - "voice_time_seconds": { - "start": 35.8, - "end": 37.4 - }, - "arabic": "هي وباقي لمعلمات منتبهات.", - "hebrew_transliteration": "הֵי וּבַּאקִי לְ-מֻעַלִּמַאת מִנְתַבְּהַאת.", - "hebrew": "היא ושאר המורות שמו לב.", - "expected_phonemes": "h eː | u b aː ʔ i | l m u ʕ a l i m aː t | m i n t a b h aː t" - }, - { - "voice_time_seconds": { - "start": 37.4, - "end": 39.2 - }, - "arabic": "انو مشكلتو مش عدم فهم.", - "hebrew_transliteration": "אִנּוּ מֻשְכִּלְתוֹ מֻש עַדַם פַהֶם.", - "hebrew": "שהבעיה שלו אינה חוסר הבנה.", - "expected_phonemes": "ʔ i n u | m u ʃ k i l t o | m u ʃ | ʕ a d a m | f a h e m" - }, - { - "voice_time_seconds": { - "start": 39.2, - "end": 41.0 - }, - "arabic": "بلعكس، عربيهن شاطر كتير.", - "hebrew_transliteration": "בִּלְ-עַכֶּס, עַרְבִּיהֶן שַאטֶר כְּתִיר.", - "hebrew": "להפך, בערבית הוא מצטיין מאוד.", - "expected_phonemes": "b i l ʕ a k e s | ʕ a r b iː h e n | ʃ aː tˤ e r | k t iː r" - }, - { - "voice_time_seconds": { - "start": 41.0, - "end": 46.0 - }, - "arabic": "بس مشكلتو قلت تركيز.", - "hebrew_transliteration": "בַּס מֻשְכִּלְתוֹ קִלֶּת תַרְכִּיז.", - "hebrew": "הבעיה שלו היא רק חוסר ריכוז.", - "expected_phonemes": "b a s | m u ʃ k i l t o | ʔ i l e t | t a r k iː z" - }, - { - "voice_time_seconds": { - "start": 46.0, - "end": 49.4 - }, - "arabic": "اضا هيك الودع فش على شو نقلق.", - "hebrew_transliteration": "אִדַ'א הֵיכּ אִלְ-וַדְע פִש עַלַא שוּ נִקְלַק.", - "hebrew": "אם זה המצב, אין לנו סיבה לדאוג.", - "expected_phonemes": "ʔ i dˤ a | h eː k | i l w a d ʕ | f i ʃ | ʕ a l a | ʃ u | n i ʔ l a ʔ" - }, - { - "voice_time_seconds": { - "start": 49.4, - "end": 52.6 - }, - "arabic": "هو متحسن كتير ومش لازم ندقق على كل اشي.", - "hebrew_transliteration": "הֻוֶּ מְתַחַסֵּן כְּתִיר וּמֻש לַאזֶם נְדַקֵּק עַלַא כֻּל אִשִי.", - "hebrew": "הוא השתפר מאוד ולא צריך להקפיד איתו על כל דבר.", - "expected_phonemes": "h u w e | m t a ħ a s i n | k t iː r | u m u ʃ | l aː z e m | n d a ʔ i ʔ | ʕ a l a | k u l | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 52.6, - "end": 56.4 - }, - "arabic": "راح يتغير مع الوقت ولجيل الو حقو.", - "hebrew_transliteration": "רַאח יִתְעַ'יַּר מַע אִלְ-וַקְת וִלְ-גִ'יל אִלוֹ חַקּוֹ.", - "hebrew": "הוא ישתנה עם הזמן, ולגיל יש את שלו.", - "expected_phonemes": "r aː ħ | j i t ɣ a j a r | m a ʕ | i l w a ʔ t | w i l dʒ iː l | i l o | ħ a ʔ o" - }, - { - "voice_time_seconds": { - "start": 56.4, - "end": 59.8 - }, - "arabic": "مدبوط. انا بقول لازم نجيبلو هدية زغيرة.", - "hebrew_transliteration": "מַדְבּוּט. אַנַא בַּקוּל לַאזֶם נְגִ'יבְלוֹ הַדִיֶּה זְעִ'ירֶה.", - "hebrew": "נכון. אני אומרת שצריך להביא לו מתנה קטנה.", - "expected_phonemes": "m a d b uː tˤ | ʔ a n a | b a ʔ uː l | l aː z e m | n dʒ iː b l o | h a d i j e | z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 59.8, - "end": 61.6 - }, - "arabic": "متل ما وعدناه.", - "hebrew_transliteration": "מִתְל מַא וַעַדְנַאה.", - "hebrew": "כמו שהבטחנו לו.", - "expected_phonemes": "m i t l | m a | w a ʕ a d n aː h" - }, - { - "voice_time_seconds": { - "start": 61.6, - "end": 64.2 - }, - "arabic": "اه اكيد، اليوم منطلع كلنا مشوار.", - "hebrew_transliteration": "אַה אַכִּיד, אִלְ-יוֹם מִנְטַלַע כֻּלְנַא מִשְוַאר.", - "hebrew": "כן, בטח. היום כולנו נצא לטיול.", - "expected_phonemes": "ʔ aː | ʔ a k iː d | i l j oː m | m i n tˤ a l a ʕ | k u l n a | m i ʃ w aː r" - }, - { - "voice_time_seconds": { - "start": 64.2, - "end": 65.2 - }, - "arabic": "ومنجيبلو شغلة.", - "hebrew_transliteration": "וּמְנְגִ'יבְלוֹ שַעְ'לֶה.", - "hebrew": "ונביא לו משהו.", - "expected_phonemes": "u m n dʒ iː b l o | ʃ a ɣ l e" - }, - { - "voice_time_seconds": { - "start": 65.2, - "end": 69.2 - }, - "arabic": "خليه يحس انو احنا مقدرين تعبو وجديتو.", - "hebrew_transliteration": "חַ'לִּיה יְחִס אִנּוּ אִחְנַא מְקַדְּרִין תַעַבּוֹ וְגִ'דִּיֶּתוֹ.", - "hebrew": "שירגיש שאנחנו מעריכים את המאמץ והרצינות שלו.", - "expected_phonemes": "x a l iː h | j ħ i s | ʔ i n u | ʔ i ħ n a | m ʔ a d r iː n | t a ʕ a b o | u dʒ i d i j e t o" - }, - { - "voice_time_seconds": { - "start": 69.2, - "end": 74.2 - }, - "arabic": "طيب ممتاز، بلا منحكي، باي.", - "hebrew_transliteration": "טַיֵּב מֻמְתַאז, בְּלַא מַנְחַכִּי, בַּאי.", - "hebrew": "טוב, מצוין. נדבר, ביי.", - "expected_phonemes": "tˤ a j i b | m u m t aː z | b l a | m a n ħ a k i | b aː i" - } - ] - }, - { - "file_name": "הקְדֵרָה הולידה קְדֵרָה * אִ-טֻנגָ'רָה וִלדַת טֻנגָ'רָה * الطّنجرة ولدت طنجرة * סיפור בערבית מדוברת [V2y6LqnoXTo].mkv", - "youtube_id": "V2y6LqnoXTo", - "title_hebrew": "הקדרה", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "الطنجرة ولدت طنجرة.", - "hebrew_transliteration": "אִלְ-טֻנְגַ'רַה וִלְדַת טֻנְגַ'רַה.", - "hebrew": "הקדרה ילדה קדרה.", - "expected_phonemes": "i l tˤ u n dʒ a r a | w i l d a t | tˤ u n dʒ a r a" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 10.2 - }, - "arabic": "مرة طلب جما من جاره يعطيه طنجرة كبيرة.", - "hebrew_transliteration": "מַרַּה טַלַבּ גַ'מַא מִן גַ'ארוֹ יַעְטִיה טֻנְגַ'רַה כְּבִּירֶה.", - "hebrew": "פעם ג'וחא ביקש משכנו לתת לו קדרה גדולה.", - "expected_phonemes": "m a r a | tˤ a l a b | dʒ a m a | m i n | dʒ aː r o | j a ʕ tˤ iː h | tˤ u n dʒ a r a | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 10.2, - "end": 14.2 - }, - "arabic": "عشان يقدر يطبح ويطعمي معازيمو لكتار.", - "hebrew_transliteration": "עַשַאן יִקְדַר יִטְבַּח וְיִטְעַמִּי מַעַאזִימוֹ לְ-כֻּתַאר.", - "hebrew": "כדי שיוכל לבשל ולהאכיל את אורחיו הרבים.", - "expected_phonemes": "ʕ a ʃ aː n | j i ʔ d a r | j i tˤ b a ħ | u j i tˤ ʕ a m i | m a ʕ aː z iː m o | l k u t aː r" - }, - { - "voice_time_seconds": { - "start": 14.2, - "end": 17.2 - }, - "arabic": "الي جايين عنده على الغدا.", - "hebrew_transliteration": "אִלִּי גַ'איִין עִנְדוֹ עַלַא אִלְ-עַ'דַא.", - "hebrew": "שבאו אליו לארוחת צהריים.", - "expected_phonemes": "ʔ i l i | dʒ aː j iː n | ʕ i n d o | ʕ a l a | i l ɣ a d a" - }, - { - "voice_time_seconds": { - "start": 17.2, - "end": 21.0 - }, - "arabic": "الجار وافق واعطاه طنجرة لجما.", - "hebrew_transliteration": "אִלְ-גַ'אר וַאפַק וְאַעְטַאה טֻנְגַ'רַה לְגַ'מַא.", - "hebrew": "השכן הסכים ונתן לג'וחא קדרה.", - "expected_phonemes": "i l dʒ aː r | w aː f a ʔ | u ʔ a ʕ tˤ aː h | tˤ u n dʒ a r a | l dʒ a m a" - }, - { - "voice_time_seconds": { - "start": 21.0, - "end": 25.0 - }, - "arabic": "بعد اكم يوم رجع جما الطنجرة لكبيرة.", - "hebrew_transliteration": "בַּעְד אַכַּם יוֹם רַגַ'ע גַ'מַא אִלְ-טֻנְגַ'רַה לְ-כְּבִּירֶה.", - "hebrew": "אחרי כמה ימים החזיר ג'וחא את הקדרה הגדולה.", - "expected_phonemes": "b a ʕ d | ʔ a k a m | j oː m | r a dʒ a ʕ | dʒ a m a | i l tˤ u n dʒ a r a | l k b iː r e" - }, - { - "voice_time_seconds": { - "start": 25.0, - "end": 28.6 - }, - "arabic": "وبقلبها طنجرة زغيرونة.", - "hebrew_transliteration": "וּבְּקַלְבְּהַא טֻנְגַ'רַה זְעִ'ירוֹנֶה.", - "hebrew": "ובתוכה קדרה קטנטנה.", - "expected_phonemes": "u b ʔ a l b h a | tˤ u n dʒ a r a | z ɣ iː r oː n e" - }, - { - "voice_time_seconds": { - "start": 28.6, - "end": 31.8 - }, - "arabic": "سأل الجار عن هاي طنجرة لزغيرة.", - "hebrew_transliteration": "סַאַל אִלְ-גַ'אר עַן הַאי טֻנְגַ'רַה לְ-זְעִ'ירֶה.", - "hebrew": "השכן שאל על הקדרה הקטנה הזאת.", - "expected_phonemes": "s a ʔ a l | i l dʒ aː r | ʕ a n | h aː i | tˤ u n dʒ a r a | l z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 31.8, - "end": 36.2 - }, - "arabic": "وجما فسرلو انها انولدت من الطنجرة لكبيرة.", - "hebrew_transliteration": "וְגַ'מַא פַסַּרְלוֹ אִנְהַא אִנְוַלַדַת מִן אִלְ-טֻנְגַ'רַה לְ-כְּבִּירֶה.", - "hebrew": "וג'וחא הסביר לו שהיא נולדה מהקדרה הגדולה.", - "expected_phonemes": "u dʒ a m a | f a s a r l o | ʔ i n h a | ʔ i n w a l a d a t | m i n | i l tˤ u n dʒ a r a | l k b iː r e" - }, - { - "voice_time_seconds": { - "start": 36.2, - "end": 40.4 - }, - "arabic": "في لفترة الي كانت الطنجرة لكبيرة عنده.", - "hebrew_transliteration": "פִי לְ-פַתְרַה אִלִּי כַּאנֶת אִלְ-טֻנְגַ'רַה לְ-כְּבִּירֶה עִנְדוֹ.", - "hebrew": "בתקופה שבה הקדרה הגדולה הייתה אצלו.", - "expected_phonemes": "f i | l f a t r a | ʔ i l i | k aː n e t | i l tˤ u n dʒ a r a | l k b iː r e | ʕ i n d o" - }, - { - "voice_time_seconds": { - "start": 40.4, - "end": 42.2 - }, - "arabic": "قبل الجار من جما لغبي.", - "hebrew_transliteration": "קִבֵּל אִלְ-גַ'אר מִן גַ'מַא לְ-עַ'בִּי.", - "hebrew": "השכן קיבל מג'וחא הטיפש.", - "expected_phonemes": "ʔ i b i l | i l dʒ aː r | m i n | dʒ a m a | l ɣ a b i" - }, - { - "voice_time_seconds": { - "start": 42.2, - "end": 45.8 - }, - "arabic": "هاي لبنت لزغيرة بكل فرح وسرور.", - "hebrew_transliteration": "הַאי לְ-בִּנְת לְ-זְעִ'ירֶה בְּכֻל פַרַח וּסֻרוּר.", - "hebrew": "את הבת הקטנה הזאת בשמחה ובאושר.", - "expected_phonemes": "h aː i | l b i n t | l z ɣ iː r e | b k u l | f a r a ħ | u s u r uː r" - }, - { - "voice_time_seconds": { - "start": 45.8, - "end": 48.4 - }, - "arabic": "وفي قلبو تحوت على جما.", - "hebrew_transliteration": "וּפִי קַלְבּוֹ תְחַוַּת עַלַא גַ'מַא.", - "hebrew": "ובליבו צחק על ג'וחא.", - "expected_phonemes": "u f i | ʔ a l b o | t ħ a w a t | ʕ a l a | dʒ a m a" - }, - { - "voice_time_seconds": { - "start": 48.4, - "end": 51.2 - }, - "arabic": "انو واحد اهبل معتوه.", - "hebrew_transliteration": "אִנּוּ וַאחַד אַהְבַּל מַעְתוֹה.", - "hebrew": "שהוא טיפש וחסר דעת.", - "expected_phonemes": "ʔ i n u | w aː ħ a d | ʔ a h b a l | m a ʕ t oː h" - }, - { - "voice_time_seconds": { - "start": 51.2, - "end": 54.2 - }, - "arabic": "بعد اكم يوم اجا جما عند جاره.", - "hebrew_transliteration": "בַּעְד אַכַּם יוֹם אַגַ'א גַ'מַא עִנְד גַ'ארוֹ.", - "hebrew": "אחרי כמה ימים בא ג'וחא אל שכנו.", - "expected_phonemes": "b a ʕ d | ʔ a k a m | j oː m | ʔ a dʒ a | dʒ a m a | ʕ i n d | dʒ aː r o" - }, - { - "voice_time_seconds": { - "start": 54.2, - "end": 58.4 - }, - "arabic": "يطلب منو يمسكو الطنجرة كمان مرة.", - "hebrew_transliteration": "יִטְלֻבּ מִנּוֹ יִמְסִכּוֹ אִלְ-טֻנְגַ'רַה כַּמַאן מַרַּה.", - "hebrew": "לבקש ממנו להשאיל לו שוב את הקדרה.", - "expected_phonemes": "j i tˤ l u b | m i n o | j i m s i k o | i l tˤ u n dʒ a r a | k a m aː n | m a r a" - }, - { - "voice_time_seconds": { - "start": 58.4, - "end": 61.0 - }, - "arabic": "فرح الجار وقال في قلبو.", - "hebrew_transliteration": "פְרִח אִלְ-גַ'אר וְקַאל פִי קַלְבּוֹ.", - "hebrew": "השכן שמח ואמר בליבו.", - "expected_phonemes": "f r i ħ | i l dʒ aː r | u ʔ aː l | f i | ʔ a l b o" - }, - { - "voice_time_seconds": { - "start": 61.0, - "end": 64.0 - }, - "arabic": "هلق بتولد الطنجرة كمان بنت.", - "hebrew_transliteration": "הַלַק בְּתוּלַד אִלְ-טֻנְגַ'רַה כַּמַאן בִּנְת.", - "hebrew": "עכשיו הקדרה תלד עוד בת.", - "expected_phonemes": "h a l a ʔ | b t uː l a d | i l tˤ u n dʒ a r a | k a m aː n | b i n t" - }, - { - "voice_time_seconds": { - "start": 64.0, - "end": 68.0 - }, - "arabic": "ومين بعرف، بلكي هاي لمرة ولدت توأم.", - "hebrew_transliteration": "וּמִין בִּעְרַף, בַּלְכִּי הַאי לְ-מַרַּה וַלַדַת תוֹאַם.", - "hebrew": "ומי יודע, אולי הפעם היא תלד תאומים.", - "expected_phonemes": "u m iː n | b i ʕ r a f | b a l k i | h aː i | l m a r a | w a l a d a t | t oː ʔ a m" - }, - { - "voice_time_seconds": { - "start": 68.0, - "end": 72.6 - }, - "arabic": "وهيك مسك الجار طنجرتو لجما.", - "hebrew_transliteration": "וְהֵיכּ מַסַכּ אִלְ-גַ'אר טֻנְגַ'רְתוֹ לְגַ'מַא.", - "hebrew": "וכך השאיל השכן את קדרתו לג'וחא.", - "expected_phonemes": "u h eː k | m a s a k | i l dʒ aː r | tˤ u n dʒ a r t o | l dʒ a m a" - }, - { - "voice_time_seconds": { - "start": 72.6, - "end": 76.4 - }, - "arabic": "مرقت عدت ايام وجما ما رجعش الطنجرة.", - "hebrew_transliteration": "מַרְקַת עִדֶּת אַיַּאם וְגַ'מַא מַא רַגַ'עְש אִלְ-טֻנְגַ'רַה.", - "hebrew": "עברו כמה ימים וג'וחא לא החזיר את הקדרה.", - "expected_phonemes": "m a r ʔ a t | ʕ i d e t | ʔ a j aː m | u dʒ a m a | m a | r a dʒ a ʕ ʃ | i l tˤ u n dʒ a r a" - }, - { - "voice_time_seconds": { - "start": 76.4, - "end": 79.4 - }, - "arabic": "عشان هيك راح الجار لعند جما.", - "hebrew_transliteration": "עַשַאן הֵיכּ רַאח אִלְ-גַ'אר לְעִנְד גַ'מַא.", - "hebrew": "לכן הלך השכן אל ג'וחא.", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | r aː ħ | i l dʒ aː r | l ʕ i n d | dʒ a m a" - }, - { - "voice_time_seconds": { - "start": 79.4, - "end": 81.8 - }, - "arabic": "وطلب منو يرجعها.", - "hebrew_transliteration": "וְטַלַבּ מִנּוֹ יִרַגְ'עְהַא.", - "hebrew": "וביקש ממנו להחזיר אותה.", - "expected_phonemes": "u tˤ a l a b | m i n o | j i r a dʒ ʕ h a" - }, - { - "voice_time_seconds": { - "start": 81.8, - "end": 85.6 - }, - "arabic": "بس جما قاللو انو الطنجرة ماتت.", - "hebrew_transliteration": "בַּס גַ'מַא קַאלְלוֹ אִנּוּ אִלְ-טֻנְגַ'רַה מַאתַת.", - "hebrew": "אבל ג'וחא אמר לו שהקדרה מתה.", - "expected_phonemes": "b a s | dʒ a m a | ʔ aː l l o | ʔ i n u | i l tˤ u n dʒ a r a | m aː t a t" - }, - { - "voice_time_seconds": { - "start": 85.6, - "end": 88.0 - }, - "arabic": "الجار تعصب كتير وقال.", - "hebrew_transliteration": "אִלְ-גַ'אר תְעַצַּב כְּתִיר וְקַאל.", - "hebrew": "השכן התרגז מאוד ואמר.", - "expected_phonemes": "i l dʒ aː r | t ʕ a sˤ a b | k t iː r | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 88.0, - "end": 90.6 - }, - "arabic": "كيف بتسمح لنفسك تتحوت عليّ؟", - "hebrew_transliteration": "כִּיף בְּתִסְמַח לְנַפְסַכּ תִתְחַוַּת עַלַיّ?", - "hebrew": "איך אתה מרשה לעצמך לצחוק עליי?", - "expected_phonemes": "k iː f | b t i s m a ħ | l n a f s a k | t i t ħ a w a t | ʕ a l a j j" - }, - { - "voice_time_seconds": { - "start": 90.6, - "end": 93.6 - }, - "arabic": "شو؟ الطنجرة كاينة كأن حيّ؟", - "hebrew_transliteration": "שוּ? אִלְ-טֻנְגַ'רַה כַּאיְנֶה כַּאִן חַיّ?", - "hebrew": "מה? הקדרה היא יצור חי?", - "expected_phonemes": "ʃ u | i l tˤ u n dʒ a r a | k aː j n e | k a ʔ i n | ħ a j j" - }, - { - "voice_time_seconds": { - "start": 93.6, - "end": 95.8 - }, - "arabic": "بتعيش وبتموت؟", - "hebrew_transliteration": "בְּתְעִיש וּבְּתְמוּת?", - "hebrew": "היא חיה ומתה?", - "expected_phonemes": "b t ʕ iː ʃ | u b t m uː t" - }, - { - "voice_time_seconds": { - "start": 95.8, - "end": 97.8 - }, - "arabic": "جما رد عليه وقال.", - "hebrew_transliteration": "גַ'מַא רַד עַלֵיה וְקַאל.", - "hebrew": "ג'וחא השיב לו ואמר.", - "expected_phonemes": "dʒ a m a | r a d | ʕ a l eː h | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 97.8, - "end": 101.0 - }, - "arabic": "لا سمح اللة، اتحوت على شخص متلك.", - "hebrew_transliteration": "לַא סַמַח אַללַה, אִתְחַוַּת עַלַא שַחְ'צ מִתְלַכּ.", - "hebrew": "חס וחלילה שאצחק על אדם כמוך.", - "expected_phonemes": "l aː | s a m a ħ | i l l a | ʔ i t ħ a w a t | ʕ a l a | ʃ a x sˤ | m i t l a k" - }, - { - "voice_time_seconds": { - "start": 101.0, - "end": 104.4 - }, - "arabic": "بس اضا ممكن الطنجرة تولد.", - "hebrew_transliteration": "בַּס אִדַ'א מֻמְכִּן אִלְ-טֻנְגַ'רַה תוּלַד.", - "hebrew": "אבל אם הקדרה יכולה ללדת.", - "expected_phonemes": "b a s | ʔ i dˤ a | m u m k i n | i l tˤ u n dʒ a r a | t uː l a d" - }, - { - "voice_time_seconds": { - "start": 104.4, - "end": 106.4 - }, - "arabic": "معناه ممكن انها تموت.", - "hebrew_transliteration": "מַעְנַאה מֻמְכִּן אִנְהַא תְמוּת.", - "hebrew": "פירוש הדבר שהיא יכולה גם למות.", - "expected_phonemes": "m a ʕ n aː h | m u m k i n | ʔ i n h a | t m uː t" - }, - { - "voice_time_seconds": { - "start": 106.4, - "end": 113.8 - }, - "arabic": "وهادي نهايت القصة.", - "hebrew_transliteration": "וְהַאדִי נִהַאיַת אִלְ-קִצַּה.", - "hebrew": "וזהו סוף הסיפור.", - "expected_phonemes": "u h aː d i | n i h aː j a t | i l ʔ i sˤ a" - } - ] - }, - { - "file_name": "או שימות הגמל או שימות המלך * יא אמא בימות אל-ג'מל... * يا إمّا بيموت الجمل... * סיפור בערבית מדוברת [1qMvkw5U1GU].mkv", - "youtube_id": "1qMvkw5U1GU", - "title_hebrew": "או שימות הגמל או שימות המלך", - "segments": [ - { - "voice_time_seconds": { - "start": 1.0, - "end": 5.4 - }, - "arabic": "يا إمّا بيموت الجمل، يا إمّا بيموت الملك!", - "hebrew_transliteration": "יַא אִמַּא בִּימוּת אִל-גַ'מַל, יַא אִמַּא בִּימוּת אִל-מַלֶכּ!", - "hebrew": "או שימות הגמל, או שימות המלך!", - "expected_phonemes": "j a | ʔ i m a | b iː m uː t | i l dʒ a m a l | j a | ʔ i m a | b iː m uː t | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 6.2, - "end": 9.2 - }, - "arabic": "كان يا ما كان، يا سامعين الكلام،", - "hebrew_transliteration": "כַּאן יַא מַא כַּאן, יַא סַאמְעִין אִל-כַּלַאם,", - "hebrew": "היה היה, הו שומעי הדברים,", - "expected_phonemes": "k aː n | j a | m a | k aː n | j a | s aː m ʕ iː n | i l k a l aː m" - }, - { - "voice_time_seconds": { - "start": 9.2, - "end": 12.8 - }, - "arabic": "فيه ملك إلّي عنده جمل أبيض،", - "hebrew_transliteration": "פִיה מַלֶכּ אִלִّי עִנדוֹ גַ'מַל אַבְּיַד',", - "hebrew": "היה מלך שהיה לו גמל לבן,", - "expected_phonemes": "f iː h | m a l e k | ʔ i l l i | ʕ i n d o | dʒ a m a l | ʔ a b j a dˤ" - }, - { - "voice_time_seconds": { - "start": 12.8, - "end": 16.4 - }, - "arabic": "كان يحبّه أكتر من ما بيحبّ ولاده.", - "hebrew_transliteration": "כַּאן יחִבּוֹ אַכְּתַר מִן מַא בִּיחֶבּ וְלַאדוֹ.", - "hebrew": "הוא אהב אותו יותר משאהב את ילדיו.", - "expected_phonemes": "k aː n | j ħ i b o | ʔ a k t a r | m i n | m a | b iː ħ e b | u l aː d o" - }, - { - "voice_time_seconds": { - "start": 16.4, - "end": 18.4 - }, - "arabic": "في يوم من الأيّام", - "hebrew_transliteration": "פִי יוֹם מִן אִל-אַיַّאם", - "hebrew": "יום אחד", - "expected_phonemes": "f i | j oː m | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 18.4, - "end": 23.0 - }, - "arabic": "قرّر الملك يعلّم جمله يقرا ويكتب.", - "hebrew_transliteration": "קַרַّר אִל-מַלֶכּ יעַלֶّם גַ'מַלוֹ יִקרַא וִיכתֶבּ.", - "hebrew": "החליט המלך ללמד את הגמל שלו לקרוא ולכתוב.", - "expected_phonemes": "ʔ a r r a r | i l m a l e k | j ʕ a l l e m | dʒ a m a l o | j i ʔ r a | w iː k t e b" - }, - { - "voice_time_seconds": { - "start": 23.0, - "end": 26.6 - }, - "arabic": "بس المشكلة إنّه ما لاقاش في مملكته", - "hebrew_transliteration": "בַּס אִל-מֻשכִּלֶה אִנּוֹ מַא לַאקַאש פִי מַמלַכּתוֹ", - "hebrew": "אבל הבעיה הייתה שהוא לא מצא בממלכתו", - "expected_phonemes": "b a s | i l m u ʃ k i l e | ʔ i n o | m a | l aː ʔ aː ʃ | f i | m a m l a k t o" - }, - { - "voice_time_seconds": { - "start": 26.6, - "end": 31.8 - }, - "arabic": "ولا واحد مستعدّ يعلّم الجمل القراءة والكتابة.", - "hebrew_transliteration": "וְלַא וַאחַד מֻסתַעֶד יעַלֶّם אִל-גַ'מַל אִל-קִרַאאַה וִל-כִּתַאבֶּה.", - "hebrew": "אף אחד שמוכן ללמד את הגמל קריאה וכתיבה.", - "expected_phonemes": "u l aː | w aː ħ a d | m u s t a ʕ e d | j ʕ a l l e m | i l dʒ a m a l | i l ʔ i r aː ʔ a | w i l k i t aː b e" - }, - { - "voice_time_seconds": { - "start": 31.8, - "end": 34.0 - }, - "arabic": "الملك زعل كتير،", - "hebrew_transliteration": "אִל-מַלֶכּ זִעֶל כְּתִיר,", - "hebrew": "המלך כעס מאוד,", - "expected_phonemes": "i l m a l e k | z i ʕ e l | k t iː r" - }, - { - "voice_time_seconds": { - "start": 34.0, - "end": 38.2 - }, - "arabic": "وقعد يفكّر كيف ممكن يلاقي حلّ لمشكلته.", - "hebrew_transliteration": "וּקַעַד יפַכַّר כִּיף מֻמכֶּן יְלַאקִי חַלّ לַמֻשכִּלתוֹ.", - "hebrew": "וישב לחשוב איך יוכל למצוא פתרון לבעיה שלו.", - "expected_phonemes": "u ʔ a ʕ a d | j f a k k a r | k iː f | m u m k e n | j l aː ʔ i | ħ a l l | l a m u ʃ k i l t o" - }, - { - "voice_time_seconds": { - "start": 38.2, - "end": 42.0 - }, - "arabic": "فكّر وفكّر بسّ ما طلعش بحلّ.", - "hebrew_transliteration": "פַכַّר וּפַכַّר בַּס מַא טִלֶעש בִּ-חַלّ.", - "hebrew": "חשב וחשב, אבל לא מצא פתרון.", - "expected_phonemes": "f a k k a r | u f a k k a r | b a s | m a | tˤ i l e ʕ ʃ | b i ħ a l l" - }, - { - "voice_time_seconds": { - "start": 42.0, - "end": 45.6 - }, - "arabic": "مرّة فات لعنده واحد من خدمه", - "hebrew_transliteration": "מַרַّה פַאת לַעִנדוֹ וַאחַד מִן חַ'דַמוֹ", - "hebrew": "פעם נכנס אליו אחד ממשרתיו", - "expected_phonemes": "m a r r a | f aː t | l a ʕ i n d o | w aː ħ a d | m i n | x a d a m o" - }, - { - "voice_time_seconds": { - "start": 45.6, - "end": 49.6 - }, - "arabic": "وقال-له إنّه فيه واحد ختيار طالب يشوفه.", - "hebrew_transliteration": "וּקַאל-לוֹ אִנּוֹ פִיה וַאחַד חְ'תִיַאר טַאלֶבּ יְשוּפוֹ.", - "hebrew": "ואמר לו שיש זקן אחד שמבקש לראות אותו.", - "expected_phonemes": "u ʔ aː l l o | ʔ i n o | f iː h | w aː ħ a d | x t i j aː r | tˤ aː l e b | j ʃ uː f o" - }, - { - "voice_time_seconds": { - "start": 49.6, - "end": 53.4 - }, - "arabic": "الملك استغرب بسّ ما رفضش الطّلب،", - "hebrew_transliteration": "אִל-מַלֶכּ אִסתַעְ'רַבּ בַּס מַא רַפַד'ש אִ-טַּלַבּ,", - "hebrew": "המלך הופתע, אבל לא סירב לבקשה,", - "expected_phonemes": "i l m a l e k | ʔ i s t a ɣ r a b | b a s | m a | r a f a dˤ ʃ | ʔ i tˤ a l a b" - }, - { - "voice_time_seconds": { - "start": 53.4, - "end": 57.4 - }, - "arabic": "وحكى للخادم يدخّل الختيار لعنده.", - "hebrew_transliteration": "וּחַכַא לִל-חַ'אדֶם יְדַחֶ'ל אִל-חְ'תִיַאר לַעִנדוֹ.", - "hebrew": "ואמר למשרת להכניס אליו את הזקן.", - "expected_phonemes": "u ħ a k a | l i l x aː d e m | j d a x e l | i l x t i j aː r | l a ʕ i n d o" - }, - { - "voice_time_seconds": { - "start": 57.4, - "end": 60.0 - }, - "arabic": "دخل هالختيار لعند الملك.", - "hebrew_transliteration": "דַחַ'ל הַל-חְ'תִיַאר לַעִנד אִל-מַלֶכּ.", - "hebrew": "הזקן הזה נכנס אל המלך.", - "expected_phonemes": "d a x a l | h a l x t i j aː r | l a ʕ i n d | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 60.0, - "end": 64.8 - }, - "arabic": "الملك اتطلّع عليه، لاقاه زلمة كبير في السّن،", - "hebrew_transliteration": "אִל-מַלֶכּ אִתטַלַّע עַלֵיה, לַאקַאה זַלַמֶה כְּבִּיר פִי אִ-סִّן,", - "hebrew": "המלך הביט בו ומצא שהוא אדם מבוגר בגיל,", - "expected_phonemes": "i l m a l e k | ʔ i t tˤ a l l a ʕ | ʕ a l eː h | l aː ʔ aː h | z a l a m e | k b iː r | f i | ʔ i s s i n" - }, - { - "voice_time_seconds": { - "start": 64.8, - "end": 69.0 - }, - "arabic": "أسمر اللّون، شكله مرتّب وأواعيه نضيفة.", - "hebrew_transliteration": "אַסמַר אִ-לּוֹן, שַכְּלוֹ מְרַתַّבּ וּאַוַאעִיה נַד'ִיפֶה.", - "hebrew": "שחום עור, מראהו מסודר ובגדיו נקיים.", - "expected_phonemes": "ʔ a s m a r | i l l oː n | ʃ a k l o | m r a t t a b | u ʔ a w aː ʕ iː h | n a dˤ iː f e" - }, - { - "voice_time_seconds": { - "start": 69.0, - "end": 73.0 - }, - "arabic": "الختيار طلب من الملك الإذن إنّه يحكي،", - "hebrew_transliteration": "אִל-חְ'תִיַאר טַלַבּ מִן אִל-מַלֶכּ אִל-אִד'ֶן אִנּוֹ יִחכִּי,", - "hebrew": "הזקן ביקש מהמלך רשות לדבר,", - "expected_phonemes": "i l x t i j aː r | tˤ a l a b | m i n | i l m a l e k | i l ʔ i dˤ e n | ʔ i n o | j i ħ k i" - }, - { - "voice_time_seconds": { - "start": 73.0, - "end": 75.0 - }, - "arabic": "والملك سمحله.", - "hebrew_transliteration": "וִל-מַלֶכּ סַמַח-לוֹ.", - "hebrew": "והמלך הרשה לו.", - "expected_phonemes": "w i l m a l e k | s a m a ħ l o" - }, - { - "voice_time_seconds": { - "start": 75.0, - "end": 76.8 - }, - "arabic": "الختيار قال:", - "hebrew_transliteration": "אִל-חְ'תִיַאר קַאל:", - "hebrew": "הזקן אמר:", - "expected_phonemes": "i l x t i j aː r | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 76.8, - "end": 78.4 - }, - "arabic": "يا جلالة الملك،", - "hebrew_transliteration": "יַא גַ'לַאלֶת אִל-מַלֶכּ,", - "hebrew": "הוד מלכותו,", - "expected_phonemes": "j a | dʒ a l aː l e t | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 78.4, - "end": 81.6 - }, - "arabic": "أنا سمعت إنّك عم بتدوّر على واحد", - "hebrew_transliteration": "אַנַא סמִעֶת אִנַّכּ עַם-בִּתדַוֶّר עַלַא וַאחַד", - "hebrew": "שמעתי שאתה מחפש מישהו", - "expected_phonemes": "ʔ a n a | s m i ʕ e t | ʔ i n n a k | ʕ a m b i t d a w w e r | ʕ a l a | w aː ħ a d" - }, - { - "voice_time_seconds": { - "start": 81.6, - "end": 84.2 - }, - "arabic": "يعلّم جملك القراءة والكتابة،", - "hebrew_transliteration": "יעַלֶّם גַ'מַלַכּ אִל-קִרַאאַה וִל-כִּתַאבֶּה,", - "hebrew": "שילמד את הגמל שלך קריאה וכתיבה,", - "expected_phonemes": "j ʕ a l l e m | dʒ a m a l a k | i l ʔ i r aː ʔ a | w i l k i t aː b e" - }, - { - "voice_time_seconds": { - "start": 84.2, - "end": 86.4 - }, - "arabic": "وأنا عند طلبك.", - "hebrew_transliteration": "וּאַנַא עִנד טַלַבַּכּ.", - "hebrew": "ואני לשירותך.", - "expected_phonemes": "u ʔ a n a | ʕ i n d | tˤ a l a b a k" - }, - { - "voice_time_seconds": { - "start": 86.4, - "end": 89.4 - }, - "arabic": "أنا مستعدّ أعلّملك الجمل.", - "hebrew_transliteration": "אַנַא מֻסתַעֶד אַעַלֶّמ-לַכּ אִל-גַ'מַל.", - "hebrew": "אני מוכן ללמד לך את הגמל.", - "expected_phonemes": "ʔ a n a | m u s t a ʕ e d | ʔ a ʕ a l l e m l a k | i l dʒ a m a l" - }, - { - "voice_time_seconds": { - "start": 89.4, - "end": 93.2 - }, - "arabic": "الملك انبسط كتير وصار يبتسم.", - "hebrew_transliteration": "אִל-מַלֶכּ אִנבַּסַט כְּתִיר וּצַאר יִבּתִסֶם.", - "hebrew": "המלך שמח מאוד והתחיל לחייך.", - "expected_phonemes": "i l m a l e k | ʔ i n b a s a tˤ | k t iː r | u sˤ aː r | j i b t i s e m" - }, - { - "voice_time_seconds": { - "start": 93.2, - "end": 95.6 - }, - "arabic": "كمّل الختيار حكي وقال:", - "hebrew_transliteration": "כַּמַّל אִל-חְ'תִיַאר חַכִּי וּקַאל:", - "hebrew": "הזקן המשיך לדבר ואמר:", - "expected_phonemes": "k a m m a l | i l x t i j aː r | ħ a k i | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 95.6, - "end": 99.6 - }, - "arabic": "بس يا جلالة الملك، عندي شرط.", - "hebrew_transliteration": "בַּס יַא גַ'לַאלֶת אִל-מַלֶכּ, עִנדִי שַרְט.", - "hebrew": "אבל, הוד מלכותו, יש לי תנאי.", - "expected_phonemes": "b a s | j a | dʒ a l aː l e t | i l m a l e k | ʕ i n d i | ʃ a r tˤ" - }, - { - "voice_time_seconds": { - "start": 99.6, - "end": 104.6 - }, - "arabic": "عشان أعلّم جملك، بدّي تدفعلي كل شهر خمسين دينار،", - "hebrew_transliteration": "עַשַאן אַעַלֶّם גַ'מַלַכּ, בַּדִّי תִדפַע-לִי כֻּל שַהֶר חַ'מסִין דִינַאר,", - "hebrew": "כדי ללמד את הגמל שלך, אני רוצה שתשלם לי כל חודש חמישים דינר,", - "expected_phonemes": "ʕ a ʃ aː n | ʔ a ʕ a l l e m | dʒ a m a l a k | b a d d i | t i d f a ʕ l i | k u l | ʃ a h e r | x a m s iː n | d iː n aː r" - }, - { - "voice_time_seconds": { - "start": 104.6, - "end": 109.6 - }, - "arabic": "وتعطيني مهلة عشر سنين أقدر أعلّمه.", - "hebrew_transliteration": "וּתַעטִינִי מֻהלֶה עַשֶר סְנִין עַשַאן אַקדַר אַעַלֶّמוֹ.", - "hebrew": "ותיתן לי עשר שנים כדי שאוכל ללמד אותו.", - "expected_phonemes": "u t a ʕ tˤ iː n i | m u h l e | ʕ a ʃ e r | s n iː n | ʕ a ʃ aː n | ʔ a ʔ d a r | ʔ a ʕ a l l e m o" - }, - { - "voice_time_seconds": { - "start": 109.6, - "end": 114.4 - }, - "arabic": "الملك فكّر شويّ وبعدين أعطى موافقته،", - "hebrew_transliteration": "אִל-מַלֶכּ פַכַّר שְוַיّ וּבַּעדֵין אַעטַא מוּאַפַקתוֹ,", - "hebrew": "המלך חשב קצת ואחר כך נתן את הסכמתו,", - "expected_phonemes": "i l m a l e k | f a k k a r | ʃ w a j j | u b a ʕ d eː n | ʔ a ʕ tˤ a | m uː ʔ a f a ʔ t o" - }, - { - "voice_time_seconds": { - "start": 114.4, - "end": 117.6 - }, - "arabic": "بسّ هوّ كمان كان عنده شرط للختيار،", - "hebrew_transliteration": "בַּס הֻוֶّ כַּמַאן כַּאן עִנדוֹ שַרְט לִל-חְ'תִיַאר,", - "hebrew": "אבל גם לו היה תנאי לזקן,", - "expected_phonemes": "b a s | h u w w e | k a m aː n | k aː n | ʕ i n d o | ʃ a r tˤ | l i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 117.6, - "end": 120.2 - }, - "arabic": "وعشان هيك قال-له:", - "hebrew_transliteration": "וּעַשַאן הֵיכּ קַאל-לוֹ:", - "hebrew": "ולכן אמר לו:", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 120.2, - "end": 124.4 - }, - "arabic": "أنا موافق، رح أدفعلك خمسين دينار كل شهر،", - "hebrew_transliteration": "אַנַא מוּאַפֶק, רַח-אַדפַע-לַכּ חַ'מסִין דִינַאר כֻּל שַהֶר,", - "hebrew": "אני מסכים, אשלם לך חמישים דינר כל חודש,", - "expected_phonemes": "ʔ a n a | m uː ʔ a f e ʔ | r a ħ ʔ a d f a ʕ l a k | x a m s iː n | d iː n aː r | k u l | ʃ a h e r" - }, - { - "voice_time_seconds": { - "start": 124.4, - "end": 127.2 - }, - "arabic": "ورح أعطيك مهلة لعشر سنين.", - "hebrew_transliteration": "וּרַח-אַעטִיכּ מֻהלֶה לַעַשֶר סְנִין.", - "hebrew": "ואתן לך עשר שנים.", - "expected_phonemes": "u r a ħ ʔ a ʕ tˤ iː k | m u h l e | l a ʕ a ʃ e r | s n iː n" - }, - { - "voice_time_seconds": { - "start": 127.2, - "end": 129.8 - }, - "arabic": "بسّ إن مرقوا العشر سنين", - "hebrew_transliteration": "בַּס אִן מַרַקוּ אִל-עַשֶר סְנִין", - "hebrew": "אבל אם יעברו עשר השנים", - "expected_phonemes": "b a s | ʔ i n | m a r a ʔ u | i l ʕ a ʃ e r | s n iː n" - }, - { - "voice_time_seconds": { - "start": 129.8, - "end": 133.2 - }, - "arabic": "وما نجحتش تعلّم جملي القراءة والكتابة", - "hebrew_transliteration": "וּמַא נַגַ'חתֶש תעַלֶّם גַ'מַלִי אִל-קִרַאאַה וִל-כִּתַאבֶּה", - "hebrew": "ולא תצליח ללמד את הגמל שלי קריאה וכתיבה", - "expected_phonemes": "u m a | n a dʒ a ħ t e ʃ | t ʕ a l l e m | dʒ a m a l i | i l ʔ i r aː ʔ a | w i l k i t aː b e" - }, - { - "voice_time_seconds": { - "start": 133.2, - "end": 135.4 - }, - "arabic": "رح أقطع راسك.", - "hebrew_transliteration": "רַח-אַקטַע רַאסַכּ.", - "hebrew": "אכרות את ראשך.", - "expected_phonemes": "r a ħ ʔ a ʔ tˤ a ʕ | r aː s a k" - }, - { - "voice_time_seconds": { - "start": 135.4, - "end": 139.8 - }, - "arabic": "الختيار ردّ على كلام الملك وقال:", - "hebrew_transliteration": "אִל-חְ'תִיַאר רַדّ עַלַא כַּלַאם אִל-מַלֶכּ וּקַאל:", - "hebrew": "הזקן השיב לדברי המלך ואמר:", - "expected_phonemes": "i l x t i j aː r | r a d d | ʕ a l a | k a l aː m | i l m a l e k | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 139.8, - "end": 141.4 - }, - "arabic": "موافق!", - "hebrew_transliteration": "מוּאַפֶק!", - "hebrew": "מסכים!", - "expected_phonemes": "m uː ʔ a f e ʔ" - }, - { - "voice_time_seconds": { - "start": 141.4, - "end": 143.6 - }, - "arabic": "طلع الختيار من عند الملك", - "hebrew_transliteration": "טִלֶע אִל-חְ'תִיַאר מִן עִנד אִל-מַלֶכּ", - "hebrew": "הזקן יצא מאצל המלך", - "expected_phonemes": "tˤ i l e ʕ | i l x t i j aː r | m i n | ʕ i n d | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 143.6, - "end": 145.8 - }, - "arabic": "وكان طاير من الفرح.", - "hebrew_transliteration": "וּכַּאן טַאיֶר מִן אִל-פַרַח.", - "hebrew": "והיה מאושר עד הגג.", - "expected_phonemes": "u k aː n | tˤ aː j e r | m i n | i l f a r a ħ" - }, - { - "voice_time_seconds": { - "start": 145.8, - "end": 149.6 - }, - "arabic": "صحابه إلّي عرفوا شو عمل حكوله:", - "hebrew_transliteration": "צְחַאבּוֹ אִלִّי עִרפוּ שוּ עִמֶל חַכוּ-לוֹ:", - "hebrew": "חבריו, שידעו מה עשה, אמרו לו:", - "expected_phonemes": "sˤ ħ aː b o | ʔ i l l i | ʕ i r f u | ʃ u | ʕ i m e l | ħ a k uː l o" - }, - { - "voice_time_seconds": { - "start": 149.6, - "end": 152.2 - }, - "arabic": "يا مجنون، شو أجا على بالك،", - "hebrew_transliteration": "יַא מַגְ'נוּן, שוּ אַגַ'א עַלַא בַּאלַכּ,", - "hebrew": "משוגע, מה עלה בדעתך,", - "expected_phonemes": "j a | m a dʒ n uː n | ʃ u | ʔ a dʒ a | ʕ a l a | b aː l a k" - }, - { - "voice_time_seconds": { - "start": 152.2, - "end": 154.6 - }, - "arabic": "وليش هيك رميت حالك للموت؟", - "hebrew_transliteration": "וּלֵיש הֵיכּ רַמֵית חַאלַכּ לַלמוֹת?", - "hebrew": "ולמה זרקת את עצמך כך אל המוות?", - "expected_phonemes": "u l eː ʃ | h eː k | r a m eː t | ħ aː l a k | l a l m oː t" - }, - { - "voice_time_seconds": { - "start": 154.6, - "end": 157.6 - }, - "arabic": "كيف يعني بدّك تعلّم جمل يقرا ويكتب؟", - "hebrew_transliteration": "כִּיף יַעְנִי בַּדַّכּ תְעַלֶّם גַ'מַל יִקרַא וִיכתֶבּ?", - "hebrew": "איך בדיוק אתה רוצה ללמד גמל לקרוא ולכתוב?", - "expected_phonemes": "k iː f | j a ʕ n i | b a d d a k | t ʕ a l l e m | dʒ a m a l | j i ʔ r a | w iː k t e b" - }, - { - "voice_time_seconds": { - "start": 157.6, - "end": 161.8 - }, - "arabic": "لو بتقعد مية سنة مش رح تقدر تعلّمه كلمة.", - "hebrew_transliteration": "לַו בְּתֻקעֹד מִית סַנֶה מֻש רַח-תִקדַר תְעַלְّמוֹ כִּלְמֶה.", - "hebrew": "גם אם תשב מאה שנה לא תוכל ללמד אותו מילה.", - "expected_phonemes": "l a w | b t u ʔ ʕ o d | m iː t | s a n e | m u ʃ | r a ħ t i ʔ d a r | t ʕ a l l m o | k i l m e" - }, - { - "voice_time_seconds": { - "start": 161.8, - "end": 164.2 - }, - "arabic": "الختيار ضحك وقال:", - "hebrew_transliteration": "אִל-חְ'תִיַאר דִ'חֶכּ וּקַאל:", - "hebrew": "הזקן צחק ואמר:", - "expected_phonemes": "i l x t i j aː r | dˤ i ħ e k | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 164.2, - "end": 165.8 - }, - "arabic": "ما تخافوش عليّ،", - "hebrew_transliteration": "מַא תְחַ'אפוּש עַלַיّ,", - "hebrew": "אל תדאגו לי,", - "expected_phonemes": "m a | t x aː f uː ʃ | ʕ a l a j j" - }, - { - "voice_time_seconds": { - "start": 165.8, - "end": 167.8 - }, - "arabic": "من هون لعشر سنين،", - "hebrew_transliteration": "מִן הוֹן לַעַשֶר סְנִין,", - "hebrew": "מעכשיו ועד עשר שנים,", - "expected_phonemes": "m i n | h oː n | l a ʕ a ʃ e r | s n iː n" - }, - { - "voice_time_seconds": { - "start": 167.8, - "end": 171.2 - }, - "arabic": "يا إمّا بيموت الجمل يا إمّا بيموت الملك!", - "hebrew_transliteration": "יַא אִמַּא בִּימוּת אִל-גַ'מַל יַא אִמַּא בִּימוּת אִל-מַלֶכּ!", - "hebrew": "או שימות הגמל או שימות המלך!", - "expected_phonemes": "j a | ʔ i m a | b iː m uː t | i l dʒ a m a l | j a | ʔ i m a | b iː m uː t | i l m a l e k" - } - ] - }, - { - "file_name": "אלאדין ומנורת הקסמים * עלאא א-דין ול-מסבאח א-סחרי * علاء الدّين والمصباح السّحري * סיפור [EN15l_wkRmw].mkv", - "youtube_id": "EN15l_wkRmw", - "title_hebrew": "אלאדין ומנורת הקסמים", - "segments": [ - { - "voice_time_seconds": { - "start": 1.2, - "end": 6.0 - }, - "arabic": "علاء الدّين والمصباح السّحري", - "hebrew_transliteration": "עַלַאא א-דִّין וִל-מִסבַּאח א-סִّחרִי", - "hebrew": "אלאדין ומנורת הקסמים", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | w i l m i s b aː ħ | ʔ s s i ħ r i" - }, - { - "voice_time_seconds": { - "start": 6.8, - "end": 9.4 - }, - "arabic": "كان ياما كان في قديم الزّمان،", - "hebrew_transliteration": "כַּאן יַא מַא כַּאן פִי קַדִים א-זַّמַאן,", - "hebrew": "היה היה בימים עברו,", - "expected_phonemes": "k aː n | j a | m a | k aː n | f i | ʔ a d iː m | ʔ z z a m aː n" - }, - { - "voice_time_seconds": { - "start": 9.4, - "end": 13.0 - }, - "arabic": "في بغداد، شاب أسمه علاء الدّين.", - "hebrew_transliteration": "פִי בַּעְ'דַאד, שַאבּ אִסמוֹ עַלַאא א-דִّין.", - "hebrew": "בבגדאד, בחור ששמו אלאדין.", - "expected_phonemes": "f i | b a ɣ d aː d | ʃ aː b | ʔ i s m o | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 13.0, - "end": 17.2 - }, - "arabic": "علاء الدّين كان شاب شجاع، قوي، وذكي،", - "hebrew_transliteration": "עַלַאא א-דִّין כַּאן שַאבּ שֻגַ'אע, קַוִי, וּזַכִּי,", - "hebrew": "אלאדין היה בחור אמיץ, חזק ונבון,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | k aː n | ʃ aː b | ʃ u dʒ aː ʕ | ʔ a w i | u z a k i" - }, - { - "voice_time_seconds": { - "start": 17.2, - "end": 20.2 - }, - "arabic": "بسّ مشكلته إنّه كان فقير،", - "hebrew_transliteration": "בַּס מֻשכִּלתוֹ אִנּוֹ כַּאן פַקִיר,", - "hebrew": "אבל הבעיה שלו הייתה שהוא היה עני,", - "expected_phonemes": "b a s | m u ʃ k i l t o | ʔ i n o | k aː n | f a ʔ iː r" - }, - { - "voice_time_seconds": { - "start": 20.2, - "end": 22.8 - }, - "arabic": "وما كانش معه لا مصاري", - "hebrew_transliteration": "וּמַא כַּאנֶש מַעוֹ לַא מַצַארִי", - "hebrew": "ולא היה לו לא כסף", - "expected_phonemes": "u m a | k aː n e ʃ | m a ʕ o | l aː | m a sˤ aː r i" - }, - { - "voice_time_seconds": { - "start": 22.8, - "end": 26.8 - }, - "arabic": "ولا بيت، ولا شغل، ولا حتّى عيلة،", - "hebrew_transliteration": "וְלַא בֵּית, וְלַא שֻעְ'ל, וְלַא חַתַّא עֵילֶה,", - "hebrew": "ולא בית, ולא עבודה, ואפילו לא משפחה,", - "expected_phonemes": "u l aː | b eː t | u l aː | ʃ u ɣ l | u l aː | ħ a t t a | ʕ eː l e" - }, - { - "voice_time_seconds": { - "start": 26.8, - "end": 28.6 - }, - "arabic": "كان عايش بخربة.", - "hebrew_transliteration": "כַּאן עַאיֶש בִּחִ'רבֶּה.", - "hebrew": "הוא גר בחורבה.", - "expected_phonemes": "k aː n | ʕ aː j e ʃ | b i x i r b e" - }, - { - "voice_time_seconds": { - "start": 28.6, - "end": 33.0 - }, - "arabic": "علاء الدّين كان دايما يحلم يكون واحد غني،", - "hebrew_transliteration": "עַלַאא א-דִّין כַּאן דַאיְמַן יִחלַם יְכוּן וַאחַד עַ'נִי,", - "hebrew": "אלאדין תמיד חלם להיות עשיר,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | k aː n | d aː j m a n | j i ħ l a m | j k uː n | w aː ħ a d | ɣ a n i" - }, - { - "voice_time_seconds": { - "start": 33.0, - "end": 37.0 - }, - "arabic": "كان يحلم بالقصور، بالذّهب، في العز", - "hebrew_transliteration": "כַּאן יִחלַם בִּל-קֻצוּר, בִּ-ד'ַّהַבּ, פִי אִל-עִזّ", - "hebrew": "הוא חלם על ארמונות, על זהב, על עושר", - "expected_phonemes": "k aː n | j i ħ l a m | b i l ʔ u sˤ uː r | b i dˤ dˤ a h a b | f i | i l ʕ i z z" - }, - { - "voice_time_seconds": { - "start": 37.0, - "end": 38.8 - }, - "arabic": "وحتّى في بنت السّلطان", - "hebrew_transliteration": "וּחַתַّא פִי בִּנת א-סֻّלטַאן", - "hebrew": "ואפילו על בת הסולטאן", - "expected_phonemes": "u ħ a t t a | f i | b i n t | ʔ s s u l tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 38.8, - "end": 41.6 - }, - "arabic": "إلّي جمالها فوق الخيال.", - "hebrew_transliteration": "אִלִّי גַ'מַאלהַא פוֹק אִל-חַ'יַאל.", - "hebrew": "שיופייה מעבר לדמיון.", - "expected_phonemes": "ʔ i l l i | dʒ a m aː l h a | f oː ʔ | i l x a j aː l" - }, - { - "voice_time_seconds": { - "start": 41.6, - "end": 43.0 - }, - "arabic": "يوم من الأيّام", - "hebrew_transliteration": "יוֹם מִן אִל-אַיַّאם", - "hebrew": "יום אחד", - "expected_phonemes": "j oː m | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 43.0, - "end": 46.4 - }, - "arabic": "لمّا كان علاء الدّين عم بتمشّى في السّوق،", - "hebrew_transliteration": "לַמַّא כַּאן עַלַאא א-דִّין עַם-בִּתמַשַّא פִי א-סּוּק,", - "hebrew": "כשאלאדין טייל בשוק,", - "expected_phonemes": "l a m m a | k aː n | ʕ a l aː a | ʔ d d iː n | ʕ a m b i t m a ʃ ʃ a | f i | i s s uː ʔ" - }, - { - "voice_time_seconds": { - "start": 46.4, - "end": 48.8 - }, - "arabic": "بيجي لعنده زلمة ختيار", - "hebrew_transliteration": "בִּיגִ'י לַעִנדוֹ זַלַמֶה חְ'תִיַאר", - "hebrew": "בא אליו איש זקן", - "expected_phonemes": "b iː dʒ i | l a ʕ i n d o | z a l a m e | x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 48.8, - "end": 51.8 - }, - "arabic": "بيعرض عليه فرصة ما بتتعوّضش.", - "hebrew_transliteration": "בִּיעְרֶד' עַלֵיה פֻרצַה מַא בִּתתְעַוַّד'ש.", - "hebrew": "ומציע לו הזדמנות שלא תחזור.", - "expected_phonemes": "b iː ʕ r e dˤ | ʕ a l eː h | f u r sˤ a | m a | b i t t ʕ a w w a dˤ ʃ" - }, - { - "voice_time_seconds": { - "start": 51.8, - "end": 54.6 - }, - "arabic": "هالختيار طلب من علاء الدّين", - "hebrew_transliteration": "הַל-חְ'תִיַאר טַלַבּ מִן עַלַאא א-דִّין", - "hebrew": "הזקן הזה ביקש מאלאדין", - "expected_phonemes": "h a l x t i j aː r | tˤ a l a b | m i n | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 54.6, - "end": 56.8 - }, - "arabic": "ييجي معه على الصّحرا،", - "hebrew_transliteration": "יִיגִ'י מַעוֹ עַלַא א-צַّחרַא,", - "hebrew": "לבוא איתו למדבר,", - "expected_phonemes": "j iː dʒ i | m a ʕ o | ʕ a l a | ʔ sˤ sˤ a ħ r a" - }, - { - "voice_time_seconds": { - "start": 56.8, - "end": 59.8 - }, - "arabic": "عشان يروحوا سوا على مغارة،", - "hebrew_transliteration": "עַשַאן יְרוּחוּ סַוַא עַלַא מַעַ'ארַה,", - "hebrew": "כדי ללכת יחד למערה,", - "expected_phonemes": "ʕ a ʃ aː n | j r uː ħ u | s a w a | ʕ a l a | m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 59.8, - "end": 61.6 - }, - "arabic": "ويجيبوا مصباح،", - "hebrew_transliteration": "וִיגִ'יבּוּ מִסבַּאח,", - "hebrew": "ולהביא מנורה,", - "expected_phonemes": "w iː dʒ iː b u | m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 61.6, - "end": 65.2 - }, - "arabic": "وبالمقابل حكى الختيار لعلاء الدّين", - "hebrew_transliteration": "וּבִּל-מֻקַאבֶּל חַכַא אִל-חְ'תִיַאר לַעַלַאא א-דִّין", - "hebrew": "ובתמורה אמר הזקן לאלאדין", - "expected_phonemes": "u b i l m u ʔ aː b e l | ħ a k a | i l x t i j aː r | l a ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 65.2, - "end": 68.4 - }, - "arabic": "إنّه راح يعطيه ذهب.", - "hebrew_transliteration": "אִנּוֹ רַאח יַעטִיה דַ'הַבּ.", - "hebrew": "שהוא ייתן לו זהב.", - "expected_phonemes": "ʔ i n o | r aː ħ | j a ʕ tˤ iː h | dˤ a h a b" - }, - { - "voice_time_seconds": { - "start": 68.4, - "end": 71.0 - }, - "arabic": "علاء الدّين إنبسط كتير،", - "hebrew_transliteration": "עַלַאא א-דִّין אִנבַּסַט כְּתִיר,", - "hebrew": "אלאדין שמח מאוד,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | ʔ i n b a s a tˤ | k t iː r" - }, - { - "voice_time_seconds": { - "start": 71.0, - "end": 75.0 - }, - "arabic": "وحسّ حاله إنّه قرّب من تحقيق أحلامه.", - "hebrew_transliteration": "וּחַסّ חַאלוֹ אִנּוֹ קַרַّבּ מִן תַחקִיק אַחלַאמוֹ.", - "hebrew": "והרגיש שהוא קרוב להגשמת חלומותיו.", - "expected_phonemes": "u ħ a s s | ħ aː l o | ʔ i n o | ʔ a r r a b | m i n | t a ħ ʔ iː ʔ | ʔ a ħ l aː m o" - }, - { - "voice_time_seconds": { - "start": 75.0, - "end": 80.0 - }, - "arabic": "عشان هيك، دغري وافق وراح مع الختيار.", - "hebrew_transliteration": "עַשַאן הֵיכּ, דֻעְ'רִי וַאפַק וּרַאח מַע אִל-חְ'תִיַאר.", - "hebrew": "לכן, מיד הסכים והלך עם הזקן.", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | d u ɣ r i | w aː f a ʔ | u r aː ħ | m a ʕ | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 80.0, - "end": 81.8 - }, - "arabic": "الختيار وعلاء الدّين،", - "hebrew_transliteration": "אִל-חְ'תִיַאר וּעַלַאא א-דִّין,", - "hebrew": "הזקן ואלאדין,", - "expected_phonemes": "i l x t i j aː r | u ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 81.8, - "end": 86.6 - }, - "arabic": "سافروا على الجمال لحدّ ما وصلوا الصّحرا،", - "hebrew_transliteration": "סַאפַרוּ עַלַא אִל-גְ'מַאל לַחַדّ מַא וְצֵלוּ א-צַّחרַא,", - "hebrew": "נסעו על הגמלים עד שהגיעו למדבר,", - "expected_phonemes": "s aː f a r u | ʕ a l a | i l dʒ m aː l | l a ħ a d d | m a | u sˤ i l u | ʔ sˤ sˤ a ħ r a" - }, - { - "voice_time_seconds": { - "start": 86.6, - "end": 91.6 - }, - "arabic": "هناك أشّر الختيار على باب مغارة صغيرة،", - "hebrew_transliteration": "הֻנַאכּ אַשַّר אִל-חְ'תִיַאר עַלַא בַּאבּ אִל-מַעַ'ארַה אִל-זְעִ'ירֶה,", - "hebrew": "שם הצביע הזקן על פתחה של מערה קטנה,", - "expected_phonemes": "h u n aː k | ʔ a ʃ ʃ a r | i l x t i j aː r | ʕ a l a | b aː b | i l m a ɣ aː r a | i l z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 91.6, - "end": 95.6 - }, - "arabic": "وقال لعلاء الدّين هاي هيّ المغارة،", - "hebrew_transliteration": "וּקַאל לַעַלַאא א-דִّין הַאי הִיֶّ אִל-מַעַ'ארַה,", - "hebrew": "ואמר לאלאדין: זו המערה,", - "expected_phonemes": "u ʔ aː l | l a ʕ a l aː a | ʔ d d iː n | h aː i | h i j j e | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 95.6, - "end": 100.6 - }, - "arabic": "بدّي منّك تفوت لجوّاها، وتدوّرلي على المصباح،", - "hebrew_transliteration": "בַּדִّי מִנַّכּ תְפוּת לַגֻ'וַّאהַא, וּתדַוֶّר-לִי עַלַא אִל-מִסבַּאח,", - "hebrew": "אני רוצה שתיכנס לתוכה ותחפש לי את המנורה,", - "expected_phonemes": "b a d d i | m i n n a k | t f uː t | l a dʒ u w w aː h a | u t d a w w e r l i | ʕ a l a | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 100.6, - "end": 102.6 - }, - "arabic": "وبعد ما تجيبلي إيّاه", - "hebrew_transliteration": "וּבַּעד מַא תְגִ'יבּ-לִי אִיַّאה", - "hebrew": "ואחרי שתביא לי אותה", - "expected_phonemes": "u b a ʕ d | m a | t dʒ iː b l i | ʔ i j j aː h" - }, - { - "voice_time_seconds": { - "start": 102.6, - "end": 107.2 - }, - "arabic": "راح أكافئك وأعطيك ذهب متل ما وعدتك.", - "hebrew_transliteration": "רַאח אַכַּאפְאַכּ וּאַעטִיכּ דַ'הַבּ מִתֶל מַא וַעַדתַכּ.", - "hebrew": "אגמול לך ואתן לך זהב כפי שהבטחתי לך.", - "expected_phonemes": "r aː ħ | ʔ a k aː f ʔ a k | u ʔ a ʕ tˤ iː k | dˤ a h a b | m i t e l | m a | w a ʕ a d t a k" - }, - { - "voice_time_seconds": { - "start": 107.2, - "end": 110.4 - }, - "arabic": "علاء الدّين نطّ من فوق الجمل،", - "hebrew_transliteration": "עַלַאא א-דִّין נַטّ מִן פוֹק אִל-גַ'מַל,", - "hebrew": "אלאדין קפץ מעל הגמל,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | n a tˤ tˤ | m i n | f oː ʔ | i l dʒ a m a l" - }, - { - "voice_time_seconds": { - "start": 110.4, - "end": 113.8 - }, - "arabic": "جهّز حاله وركض على باب المغارة.", - "hebrew_transliteration": "גַ'הַّז חַאלוֹ וּרַכַד' עַלַא בַּאבּ אִל-מַעַ'ארַה.", - "hebrew": "התכונן ורץ אל פתח המערה.", - "expected_phonemes": "dʒ a h h a z | ħ aː l o | u r a k a dˤ | ʕ a l a | b aː b | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 113.8, - "end": 119.6 - }, - "arabic": "وهوّ عم بركض وقفّه الختيار ونبّهه وقال:", - "hebrew_transliteration": "וּהֻוֶّ עַם-בִּרכֻּד' וַקַّפוֹ אִל-חְ'תִיַאר וּנַבַّהוֹ וּקַאל:", - "hebrew": "ובזמן שרץ, הזקן עצר אותו, הזהיר אותו ואמר:", - "expected_phonemes": "u h u w w e | ʕ a m b i r k u dˤ | w a ʔ ʔ a f o | i l x t i j aː r | u n a b b a h o | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 119.6, - "end": 123.0 - }, - "arabic": "لو شو ما شفت بقلب المغارة", - "hebrew_transliteration": "לַו שוּ מַא שֻפֶת בְּקַלבּ אִל-מַעַ'ארַה", - "hebrew": "לא משנה מה תראה בתוך המערה", - "expected_phonemes": "l a w | ʃ u | m a | ʃ u f e t | b ʔ a l b | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 123.0, - "end": 126.8 - }, - "arabic": "إوعك تلمس إشي غير المصباح.", - "hebrew_transliteration": "אוּעַכּ תִלמֶס אִשִי עֵ'יר אִל-מִסבַּאח.", - "hebrew": "היזהר מלגעת בשום דבר חוץ מהמנורה.", - "expected_phonemes": "ʔ uː ʕ a k | t i l m e s | ʔ i ʃ i | ɣ eː r | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 126.8, - "end": 130.2 - }, - "arabic": "إذا مسكت أي إشي غير المصباح", - "hebrew_transliteration": "אִד'ַא מִסִכּת אַיّ אִשִי עֵ'יר אִל-מִסבַּאח", - "hebrew": "אם תיגע בכל דבר אחר חוץ מהמנורה", - "expected_phonemes": "ʔ i dˤ a | m i s i k t | ʔ a j j | ʔ i ʃ i | ɣ eː r | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 130.2, - "end": 133.0 - }, - "arabic": "راح تسكّر عليك المغارة", - "hebrew_transliteration": "רַאח תְסַכַّר עַלֵיכּ אִל-מַעַ'ארַה", - "hebrew": "המערה תיסגר עליך", - "expected_phonemes": "r aː ħ | t s a k k a r | ʕ a l eː k | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 133.0, - "end": 136.8 - }, - "arabic": "وراح تعلق هناك لمدى الحياة.", - "hebrew_transliteration": "וּרַאח תִעלַק הֻנַאכּ לַמַדַא אִל-חַיַאה.", - "hebrew": "ותיתקע שם לכל החיים.", - "expected_phonemes": "u r aː ħ | t i ʕ l a ʔ | h u n aː k | l a m a d a | i l ħ a j aː" - }, - { - "voice_time_seconds": { - "start": 136.8, - "end": 140.2 - }, - "arabic": "علاء الدّين سمع كلام الختيار", - "hebrew_transliteration": "עַלַאא א-דִّין סִמֶע כַּלַאם אִל-חְ'תִיַאר", - "hebrew": "אלאדין שמע את דברי הזקן", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | s i m e ʕ | k a l aː m | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 140.2, - "end": 143.8 - }, - "arabic": "وبعدها كمّل ركض على باب المغارة.", - "hebrew_transliteration": "וּבַּעדְהַא כַּמַّל רַכֶּד' עַלַא בַּאבּ אִל-מַעַ'ארַה.", - "hebrew": "ואחר כך המשיך לרוץ אל פתח המערה.", - "expected_phonemes": "u b a ʕ d h a | k a m m a l | r a k e dˤ | ʕ a l a | b aː b | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 143.8, - "end": 149.0 - }, - "arabic": "فات على قلبها، وشاف دراج طويلة طويلة", - "hebrew_transliteration": "פַאת עַלַא קַלבְּהַא, וּשַאף דַרַאג' טַוִילֶה טַוִילֶה", - "hebrew": "הוא נכנס לתוכה וראה מדרגות ארוכות ארוכות", - "expected_phonemes": "f aː t | ʕ a l a | ʔ a l b h a | u ʃ aː f | d a r aː dʒ | tˤ a w iː l e | tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 149.0, - "end": 150.8 - }, - "arabic": "بتنزل لتحت.", - "hebrew_transliteration": "בְּתִנזַל לַתַחֶת.", - "hebrew": "שיורדות למטה.", - "expected_phonemes": "b t i n z a l | l a t a ħ e t" - }, - { - "voice_time_seconds": { - "start": 150.8, - "end": 154.2 - }, - "arabic": "بلّش علاء الدّين ينزل عالدراج", - "hebrew_transliteration": "בַּלַّש עַלַאא א-דִّין יִנזַל עַ-דַّרַאג'", - "hebrew": "אלאדין התחיל לרדת במדרגות", - "expected_phonemes": "b a l l a ʃ | ʕ a l aː a | ʔ d d iː n | j i n z a l | ʕ a d d a r aː dʒ" - }, - { - "voice_time_seconds": { - "start": 154.2, - "end": 158.4 - }, - "arabic": "وصار يبعد شوي شوي عن باب المغارة.", - "hebrew_transliteration": "וּצַאר יִבּעַד שְוַי שְוַי עַן בַּאבּ אִל-מַעַ'ארַה.", - "hebrew": "והתרחק לאט לאט מפתח המערה.", - "expected_phonemes": "u sˤ aː r | j i b ʕ a d | ʃ w a j | ʃ w a j | ʕ a n | b aː b | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 158.4, - "end": 162.6 - }, - "arabic": "نزل ونزل على الدّراج قريب النّصّ ساعة،", - "hebrew_transliteration": "נִזֶל וּנִזֶל עַלַא אִל-דַّרַאג' קַרִיבּ נֻצّ סַאעַה,", - "hebrew": "ירד וירד במדרגות כמעט חצי שעה,", - "expected_phonemes": "n i z e l | u n i z e l | ʕ a l a | i l d d a r aː dʒ | ʔ a r iː b | n u sˤ sˤ | s aː ʕ a" - }, - { - "voice_time_seconds": { - "start": 162.6, - "end": 165.2 - }, - "arabic": "لحدّ ما شاف إشي بلمع...", - "hebrew_transliteration": "לַחַדّ מַא שַאף אִשִי בִּלמַע...", - "hebrew": "עד שראה משהו נוצץ...", - "expected_phonemes": "l a ħ a d d | m a | ʃ aː f | ʔ i ʃ i | b i l m a ʕ" - }, - { - "voice_time_seconds": { - "start": 165.2, - "end": 168.4 - }, - "arabic": "قرّب ومشي وإلّا بيلاقي حاله", - "hebrew_transliteration": "קַרַّבּ וּמִשִי וְאִלַّא בִּילַאקִי חַאלוֹ", - "hebrew": "התקרב והלך, ופתאום מצא את עצמו", - "expected_phonemes": "ʔ a r r a b | u m i ʃ i | u ʔ i l l a | b iː l aː ʔ i | ħ aː l o" - }, - { - "voice_time_seconds": { - "start": 168.4, - "end": 171.0 - }, - "arabic": "بقلب غرفة مليانة ذهب،", - "hebrew_transliteration": "בְּקַלבּ עֻ'רפֶה מַליַאנֶה דַ'הַבּ,", - "hebrew": "בתוך חדר מלא זהב,", - "expected_phonemes": "b ʔ a l b | ɣ u r f e | m a l j aː n e | dˤ a h a b" - }, - { - "voice_time_seconds": { - "start": 171.0, - "end": 174.6 - }, - "arabic": "مجوهرات، ألماز، ومصاري.", - "hebrew_transliteration": "מְגַ'וְהַרַאת, אַלמַאז וּמַצַארִי.", - "hebrew": "תכשיטים, יהלומים וכסף.", - "expected_phonemes": "m dʒ a w h a r aː t | i l m aː z | u m a sˤ aː r i" - }, - { - "voice_time_seconds": { - "start": 174.6, - "end": 177.4 - }, - "arabic": "علاء الدّين ما صدّقش إلّي شايفه،", - "hebrew_transliteration": "עַלַאא א-דִّין מַא צַדַّקֶש אִלִّי שַאיְפוֹ,", - "hebrew": "אלאדין לא האמין למה שראה,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | m a | sˤ a d d a ʔ e ʃ | ʔ i l l i | ʃ aː j f o" - }, - { - "voice_time_seconds": { - "start": 177.4, - "end": 183.2 - }, - "arabic": "بسّ... وقبل ما يمسك أي إشي، تذكّر كلام الختيار،", - "hebrew_transliteration": "בַּס... וּקַבֶּל מַא יִמסַכּ אַי אִשִי, תְזַכַّר כַּלַאם אִל-חְ'תִיַאר,", - "hebrew": "אבל... לפני שנגע במשהו, נזכר בדברי הזקן,", - "expected_phonemes": "b a s | u ʔ a b e l | m a | j i m s a k | ʔ a j j | ʔ i ʃ i | t z a k k a r | k a l aː m | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 183.2, - "end": 187.8 - }, - "arabic": "وركّز بسّ على المهمّة إلّي أجا عشانها.", - "hebrew_transliteration": "וּרַכַّז בַּס עַלַא אִל-מֻהִמֶّה אִלִّי אַגַ'א עַשַאנְהַא.", - "hebrew": "והתרכז רק במשימה שבשבילה בא.", - "expected_phonemes": "u r a k k a z | b a s | ʕ a l a | i l m u h i m m e | ʔ i l l i | ʔ a dʒ a | ʕ a ʃ aː n h a" - }, - { - "voice_time_seconds": { - "start": 187.8, - "end": 190.4 - }, - "arabic": "دوّر علاء الدّين على المصباح،", - "hebrew_transliteration": "דַוַّר עַלַאא א-דִّין עַלַא אִל-מִסבַּאח,", - "hebrew": "אלאדין חיפש את המנורה,", - "expected_phonemes": "d a w w a r | ʕ a l aː a | ʔ d d iː n | ʕ a l a | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 190.4, - "end": 192.2 - }, - "arabic": "بسّ ما قدرش يلاقيه.", - "hebrew_transliteration": "בַּס מַא קִדְרֶש יְלַאקִיה.", - "hebrew": "אבל לא הצליח למצוא אותה.", - "expected_phonemes": "b a s | m a | ʔ i d r e ʃ | j l aː ʔ iː h" - }, - { - "voice_time_seconds": { - "start": 192.2, - "end": 196.8 - }, - "arabic": "دوّر من اليمين ودوّر من الشّمال، بسّ ما قدرش يلاقيه.", - "hebrew_transliteration": "דַוַّר מִן אִל-יַמִין וּדַוַّר מִן א-שַّמַאל, בַּס מַא קִדְרֶש יְלַאקִיה.", - "hebrew": "חיפש מימין וחיפש משמאל, אבל לא הצליח למצוא אותה.", - "expected_phonemes": "d a w w a r | m i n | i l j a m iː n | u d a w w a r | m i n | ʔ ʃ ʃ a m aː l | b a s | m a | ʔ i d r e ʃ | j l aː ʔ iː h" - }, - { - "voice_time_seconds": { - "start": 196.8, - "end": 200.8 - }, - "arabic": "لحدّ ما لمحت عينه إشي بلمع كتير", - "hebrew_transliteration": "לַחַדّ מַא לַמַחַת עֵינוֹ אִשִי בִּלמַע כְּתִיר", - "hebrew": "עד שעינו קלטה משהו שנוצץ מאוד", - "expected_phonemes": "l a ħ a d d | m a | l a m a ħ a t | ʕ eː n o | ʔ i ʃ i | b i l m a ʕ | k t iː r" - }, - { - "voice_time_seconds": { - "start": 200.8, - "end": 205.4 - }, - "arabic": "موجود فوق تلّة ذهب كبيرة.", - "hebrew_transliteration": "מַוְגּ'וּד פוֹק תַלֶّת דַ'הַבּ כְּבִּירֶה.", - "hebrew": "שנמצא על גבעת זהב גדולה.", - "expected_phonemes": "m a w dʒ uː d | f oː ʔ | t a l l e t | dˤ a h a b | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 205.4, - "end": 210.0 - }, - "arabic": "إتطلّع علاء الدّين لفوق وإلّا بشوف المصباح.", - "hebrew_transliteration": "אִתטַלַّע עַלַאא א-דִّין לַפוֹק וְאִלַّא בִּישוּף אִל-מִסבַּאח.", - "hebrew": "אלאדין הביט למעלה, והנה הוא רואה את המנורה.", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | ʕ a l aː a | ʔ d d iː n | l a f oː ʔ | u ʔ i l l a | b iː ʃ uː f | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 210.0, - "end": 213.8 - }, - "arabic": "بلّش يفكّر كيف ممكن يوصل للمصباح", - "hebrew_transliteration": "בַּלַّש יפַכַّר כִּיף מֻמכֶּן יוּצַל לַל-מִסבַּאח", - "hebrew": "התחיל לחשוב איך יוכל להגיע אל המנורה", - "expected_phonemes": "b a l l a ʃ | j f a k k a r | k iː f | m u m k e n | j uː sˤ a l | l a l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 213.8, - "end": 215.8 - }, - "arabic": "من غير ما يلمس أي إشي.", - "hebrew_transliteration": "מִן עֵ'יר מַא יִלמֶס אַי אִשִי.", - "hebrew": "בלי לגעת בשום דבר.", - "expected_phonemes": "m i n | ɣ eː r | m a | j i l m e s | ʔ a j j | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 216.0, - "end": 218.0 - }, - "arabic": "مستحيل قال لنفسه...", - "hebrew_transliteration": "מֻסתַחִיל קַאל לַנַפסוֹ...", - "hebrew": "בלתי אפשרי, אמר לעצמו...", - "expected_phonemes": "m u s t a ħ iː l | ʔ aː l | l a n a f s o" - }, - { - "voice_time_seconds": { - "start": 218.0, - "end": 221.4 - }, - "arabic": "المصباح موجود فوق تلّة الذّهب،", - "hebrew_transliteration": "אִל-מִסבַּאח מַוְגּ'וּד פוֹק תַלֶّת א-דַّהַבּ,", - "hebrew": "המנורה נמצאת על גבעת הזהב,", - "expected_phonemes": "i l m i s b aː ħ | m a w dʒ uː d | f oː ʔ | t a l l e t | ʔ d d a h a b" - }, - { - "voice_time_seconds": { - "start": 221.4, - "end": 227.0 - }, - "arabic": "وعشان أوصله مجبور أتسلّق وأمسك الإشياء إلّي حواليه.", - "hebrew_transliteration": "וּעַשַאן אוּצַלוֹ מַגְ'בּוּר אַתסַלַّק וּאַמסֶכּ אִל-אַשיַא אִלִّי חַוַאלֵי.", - "hebrew": "וכדי להגיע אליה אני חייב לטפס ולגעת בדברים שמסביבה.", - "expected_phonemes": "u ʕ a ʃ aː n | ʔ uː sˤ a l o | m a dʒ b uː r | ʔ a t s a l l a ʔ | u ʔ a m s e k | i l ʔ a ʃ j a | ʔ i l l i | ħ a w aː l eː" - }, - { - "voice_time_seconds": { - "start": 227.0, - "end": 230.4 - }, - "arabic": "علاء الدّين من خوفه إنّه يعلق في المغارة،", - "hebrew_transliteration": "עַלַאא א-דִّין מִן חוֹפוֹ אִנּוֹ יִעלַק פִי אִל-מַעַ'ארַה,", - "hebrew": "אלאדין, מפחדו להיתקע במערה,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | m i n | ħ oː f o | ʔ i n o | j i ʕ l a ʔ | f i | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 230.4, - "end": 233.6 - }, - "arabic": "فكّر إنّه يرجع لفوق عند باب المغارة", - "hebrew_transliteration": "פַכַّר אִנּוֹ יִרגַ'ע לַפוֹק עִנד בַּאבּ אִל-מַעַ'ארַה", - "hebrew": "חשב לחזור למעלה אל פתח המערה", - "expected_phonemes": "f a k k a r | ʔ i n o | j i r dʒ a ʕ | l a f oː ʔ | ʕ i n d | b aː b | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 233.6, - "end": 237.8 - }, - "arabic": "ويحكي للختيار إنّه مش راح يقدر يجيب المصباح.", - "hebrew_transliteration": "וִיחכִּי לַל-חְ'תִיַאר אִנּוֹ מֻש רַאח יִקדַר יְגִ'יבּ אִל-מִסבַּאח.", - "hebrew": "ולומר לזקן שהוא לא יוכל להביא את המנורה.", - "expected_phonemes": "w iː ħ k i | l a l x t i j aː r | ʔ i n o | m u ʃ | r aː ħ | j i ʔ d a r | j dʒ iː b | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 237.8, - "end": 241.8 - }, - "arabic": "وبالفعل بلّش يمشي لعند الدّراج", - "hebrew_transliteration": "וּבִּל-פִעֶל בַּלַّש יִמשִי לַעִנד אִל-דַّרַאג'", - "hebrew": "ואכן התחיל ללכת לעבר המדרגות", - "expected_phonemes": "u b i l f i ʕ e l | b a l l a ʃ | j i m ʃ i | l a ʕ i n d | i l d d a r aː dʒ" - }, - { - "voice_time_seconds": { - "start": 241.8, - "end": 246.6 - }, - "arabic": "لحدّ ما شاف بساط سحري عم بيطير حواليه.", - "hebrew_transliteration": "לַחַדّ מַא שַאף בְּסַאט סִחרִי עַם-בִּיטִיר חַוַאלֵיה.", - "hebrew": "עד שראה שטיח קסמים שעף סביבו.", - "expected_phonemes": "l a ħ a d d | m a | ʃ aː f | b s aː tˤ | s i ħ r i | ʕ a m b iː tˤ iː r | ħ a w aː l eː h" - }, - { - "voice_time_seconds": { - "start": 246.6, - "end": 249.8 - }, - "arabic": "خطرت في بال علاء الدّين فكرة،", - "hebrew_transliteration": "חַ'טְרַת פִי בַּאל עַלַאא א-דִّין פִכּרַה,", - "hebrew": "עלה במוחו של אלאדין רעיון,", - "expected_phonemes": "x a tˤ r a t | f i | b aː l | ʕ a l aː a | ʔ d d iː n | f i k r a" - }, - { - "voice_time_seconds": { - "start": 249.8, - "end": 254.8 - }, - "arabic": "إنّه يركب هذا البساط ويطير عليه لعند المصباح.", - "hebrew_transliteration": "אִנּוֹ יִרכַּבּ הַאדַא אִל-בְּסַאט וִיטִיר עַלֵיה לַעִנד אִל-מִסבַּאח.", - "hebrew": "לעלות על השטיח הזה ולעוף עליו אל המנורה.", - "expected_phonemes": "ʔ i n o | j i r k a b | h aː d a | i l b s aː tˤ | w iː tˤ iː r | ʕ a l eː h | l a ʕ i n d | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 254.8, - "end": 256.2 - }, - "arabic": "وهيك صار.", - "hebrew_transliteration": "וּהֵיכּ צַאר.", - "hebrew": "וכך היה.", - "expected_phonemes": "u h eː k | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 256.2, - "end": 257.8 - }, - "arabic": "طلع على البساط", - "hebrew_transliteration": "טִלֶע עַלַא אִל-בְּסַאט", - "hebrew": "עלה על השטיח", - "expected_phonemes": "tˤ i l e ʕ | ʕ a l a | i l b s aː tˤ" - }, - { - "voice_time_seconds": { - "start": 257.8, - "end": 262.6 - }, - "arabic": "وخلّاه يطير لعند المصباح عشان يوخده.", - "hebrew_transliteration": "וּחַ'לַّאה יטִיר לַעִנד אִל-מִסבַּאח עַשַאן יוֹחְ'דוֹ.", - "hebrew": "וגרם לו לעוף אל המנורה כדי לקחת אותה.", - "expected_phonemes": "u x a l l aː h | j tˤ iː r | l a ʕ i n d | i l m i s b aː ħ | ʕ a ʃ aː n | j oː x d o" - }, - { - "voice_time_seconds": { - "start": 262.6, - "end": 267.4 - }, - "arabic": "أخد علاء الدّين المصباح، بسّ بالغلط", - "hebrew_transliteration": "אַחַ'ד עַלַאא א-דִّין אִל-מִסבַּאח, בַּס בִּל-עַ'לַט", - "hebrew": "אלאדין לקח את המנורה, אבל בטעות", - "expected_phonemes": "ʔ a x a d | ʕ a l aː a | ʔ d d iː n | i l m i s b aː ħ | b a s | b i l ɣ a l a tˤ" - }, - { - "voice_time_seconds": { - "start": 267.4, - "end": 271.2 - }, - "arabic": "مسك شقفة ذهب كانت بجمب المصباح.", - "hebrew_transliteration": "מִסֶכּ שֻקפֶת דַ'הַבּ כַּאנַת בְּגַ'מבּ אִל-מִסבַּאח.", - "hebrew": "נגע בחתיכת זהב שהייתה ליד המנורה.", - "expected_phonemes": "m i s e k | ʃ u ʔ f e t | dˤ a h a b | k aː n a t | b dʒ a m b | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 271.2, - "end": 275.0 - }, - "arabic": "المغارة بسرعة بلّشت تغيّر بشكلها،", - "hebrew_transliteration": "אִל-מַעַ'ארַה בְּסֻרעַה בַּלַّשַת תְעַ'יֶّר בְּשַכְּלְהַא,", - "hebrew": "המערה התחילה מהר לשנות את צורתה,", - "expected_phonemes": "i l m a ɣ aː r a | b s u r ʕ a | b a l l a ʃ a t | t ɣ a j j e r | b ʃ a k l h a" - }, - { - "voice_time_seconds": { - "start": 275.0, - "end": 279.2 - }, - "arabic": "وكلّ شي بلّش يدوب ويتحوّل لنار.", - "hebrew_transliteration": "וּכֻּלّ שִי בַּלַّש יְדוּבּ וִיתחַוַّל לַנַאר.", - "hebrew": "והכול התחיל להימס ולהפוך לאש.", - "expected_phonemes": "u k u l l | ʃ i | b a l l a ʃ | j d uː b | w iː t ħ a w w a l | l a n aː r" - }, - { - "voice_time_seconds": { - "start": 279.2, - "end": 281.4 - }, - "arabic": "علاء الدّين خاف كتير،", - "hebrew_transliteration": "עַלַאא א-דִّין חַ'אף כְּתִיר,", - "hebrew": "אלאדין פחד מאוד,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | x aː f | k t iː r" - }, - { - "voice_time_seconds": { - "start": 281.4, - "end": 285.2 - }, - "arabic": "وطلب من البساط يطير بأعلى سرعة لفوق", - "hebrew_transliteration": "וּטַלַבּ מִן אִל-בְּסַאט יטִיר בְּאַעלַא סֻרעַה לַפוֹק", - "hebrew": "וביקש מהשטיח לעוף במהירות הגבוהה ביותר למעלה", - "expected_phonemes": "u tˤ a l a b | m i n | i l b s aː tˤ | j tˤ iː r | b ʔ a ʕ l a | s u r ʕ a | l a f oː ʔ" - }, - { - "voice_time_seconds": { - "start": 285.2, - "end": 287.2 - }, - "arabic": "لعند باب المغارة.", - "hebrew_transliteration": "לַעִנד בַּאבּ אִל-מַעַ'ארַה.", - "hebrew": "אל פתח המערה.", - "expected_phonemes": "l a ʕ i n d | b aː b | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 287.2, - "end": 289.6 - }, - "arabic": "حاول البساط يطير بسرعة", - "hebrew_transliteration": "חַאוַל אִל-בְּסַאט יטִיר בְּסֻרעַה", - "hebrew": "השטיח ניסה לעוף מהר", - "expected_phonemes": "ħ aː w a l | i l b s aː tˤ | j tˤ iː r | b s u r ʕ a" - }, - { - "voice_time_seconds": { - "start": 289.6, - "end": 292.6 - }, - "arabic": "وعالي أحسن ما يعلق بالنّار،", - "hebrew_transliteration": "וּעַאלִי אַחסַן מַא יִעלַק בִּ-נַّאר,", - "hebrew": "וגבוה, שלא ייתקע באש,", - "expected_phonemes": "u ʕ aː l i | ʔ a ħ s a n | m a | j i ʕ l a ʔ | b i n n aː r" - }, - { - "voice_time_seconds": { - "start": 292.6, - "end": 294.8 - }, - "arabic": "بسّ لمّا وصل باب المغارة", - "hebrew_transliteration": "בַּס לַמַّא וְצֶל בַּאבּ אִל-מַעַ'ארַה", - "hebrew": "אבל כשהגיע לפתח המערה", - "expected_phonemes": "b a s | l a m m a | u sˤ e l | b aː b | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 294.8, - "end": 298.4 - }, - "arabic": "لاقى الختيار عم بطلب منّه المصباح", - "hebrew_transliteration": "לַאקַא אִל-חְ'תִיַאר עַם-בִּטלֹבּ מִנּוֹ אִל-מִסבַּאח", - "hebrew": "מצא את הזקן מבקש ממנו את המנורה", - "expected_phonemes": "l aː ʔ a | i l x t i j aː r | ʕ a m b i tˤ l o b | m i n o | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 298.4, - "end": 301.4 - }, - "arabic": "قبل ما يساعده يطلع من المغارة.", - "hebrew_transliteration": "קַבֶּל מַא יְסַאעדוֹ יִטלַע מִן אִל-מַעַ'ארַה.", - "hebrew": "לפני שיעזור לו לצאת מהמערה.", - "expected_phonemes": "ʔ a b e l | m a | j s aː ʕ d o | j i tˤ l a ʕ | m i n | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 301.4, - "end": 303.6 - }, - "arabic": "علاء الدّين خاف", - "hebrew_transliteration": "עַלַאא א-דִّין חַ'אף", - "hebrew": "אלאדין פחד", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | x aː f" - }, - { - "voice_time_seconds": { - "start": 303.6, - "end": 306.6 - }, - "arabic": "وما قبل يعطيه المصباح قبل ما يساعده.", - "hebrew_transliteration": "וּמַא קִבֶּל יַעטִיה אִל-מִסבַּאח קַבֶּל מַא יְסַאעדוֹ.", - "hebrew": "ולא הסכים לתת לו את המנורה לפני שיעזור לו.", - "expected_phonemes": "u m a | ʔ i b e l | j a ʕ tˤ iː h | i l m i s b aː ħ | ʔ a b e l | m a | j s aː ʕ d o" - }, - { - "voice_time_seconds": { - "start": 306.6, - "end": 311.4 - }, - "arabic": "الختيار رفض، وفجأة سكّرت المغارة بابها", - "hebrew_transliteration": "אִל-חְ'תִיַאר רַפַד', וּפַגְ'אַה סַכַّרַת אִל-מַעַ'ארַה בַּאבְּהַא", - "hebrew": "הזקן סירב, ופתאום המערה סגרה את פתחה", - "expected_phonemes": "i l x t i j aː r | r a f a dˤ | u f a dʒ ʔ a | s a k k a r a t | i l m a ɣ aː r a | b aː b h a" - }, - { - "voice_time_seconds": { - "start": 311.4, - "end": 315.2 - }, - "arabic": "وعلاء الدّين بقي علقان بقلبها.", - "hebrew_transliteration": "וּעַלַאא א-דִّין בְּקִי עַלקַאן בְּקַלבְּהַא.", - "hebrew": "ואלאדין נשאר תקוע בתוכה.", - "expected_phonemes": "u ʕ a l aː a | ʔ d d iː n | b ʔ i | ʕ a l ʔ aː n | b ʔ a l b h a" - }, - { - "voice_time_seconds": { - "start": 315.2, - "end": 318.8 - }, - "arabic": "بطّل يعرف كيف ممكن يطلع من هناك،", - "hebrew_transliteration": "בַּטַّל יִערַף כִּיף מֻמכֶּן יִטלַע מִן הֻנַאכּ,", - "hebrew": "הוא כבר לא ידע איך יוכל לצאת משם,", - "expected_phonemes": "b a tˤ tˤ a l | j i ʕ r a f | k iː f | m u m k e n | j i tˤ l a ʕ | m i n | h u n aː k" - }, - { - "voice_time_seconds": { - "start": 318.8, - "end": 323.4 - }, - "arabic": "ومهما حاول إنّه يلاقي مخرج بقي علقان،", - "hebrew_transliteration": "וּמַהמַא חַאוַל אִנּוֹ יְלַאקִי מַחְ'רַג' בְּקִי עַלקַאן,", - "hebrew": "וככל שניסה למצוא יציאה, נשאר תקוע,", - "expected_phonemes": "u m a h m a | ħ aː w a l | ʔ i n o | j l aː ʔ i | m a x r a dʒ | b ʔ i | ʕ a l ʔ aː n" - }, - { - "voice_time_seconds": { - "start": 323.4, - "end": 326.2 - }, - "arabic": "لحدّ ما قعد على الأرض", - "hebrew_transliteration": "לַחַדّ מַא קַעַד עַלַא אִל-אַרְד'", - "hebrew": "עד שישב על הרצפה", - "expected_phonemes": "l a ħ a d d | m a | ʔ a ʕ a d | ʕ a l a | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 326.2, - "end": 330.8 - }, - "arabic": "وصار يتطلّع على المصباح إلّي بقي في إيده.", - "hebrew_transliteration": "וּצַאר יִתטַלַّע עַלַא אִל-מִסבַּאח אִלִّי בְּקִי פִי אִידוֹ.", - "hebrew": "והתחיל להסתכל על המנורה שנשארה בידו.", - "expected_phonemes": "u sˤ aː r | j i t tˤ a l l a ʕ | ʕ a l a | i l m i s b aː ħ | ʔ i l l i | b ʔ i | f i | ʔ iː d o" - }, - { - "voice_time_seconds": { - "start": 330.8, - "end": 336.0 - }, - "arabic": "مسح المصباح أكم مرّة وإلّا بطلع من قلبه،", - "hebrew_transliteration": "מַסַח אִל-מִסבַּאח אַכַּם מַרַّה וְאִלַّא בִּטלַע מִן קַלבּוֹ,", - "hebrew": "שפשף את המנורה כמה פעמים, והנה יוצא מתוכה", - "expected_phonemes": "m a s a ħ | i l m i s b aː ħ | ʔ a k a m | m a r r a | u ʔ i l l a | b i tˤ l a ʕ | m i n | ʔ a l b o" - }, - { - "voice_time_seconds": { - "start": 336.0, - "end": 339.6 - }, - "arabic": "مارد كبير، لونه أزرق.", - "hebrew_transliteration": "מַארֶד כְּבִּיר, לוֹנוֹ אַזרַק.", - "hebrew": "ג'יני ענק, כחול.", - "expected_phonemes": "m aː r e d | k b iː r | l oː n o | ʔ a z r a ʔ" - }, - { - "voice_time_seconds": { - "start": 339.6, - "end": 341.0 - }, - "arabic": "هالمارد قال:", - "hebrew_transliteration": "הַל-מַארֶד קַאל:", - "hebrew": "הג'יני הזה אמר:", - "expected_phonemes": "h a l m aː r e d | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 341.0, - "end": 346.0 - }, - "arabic": "شبّيك لبّيك، خادمك بين إيديك،", - "hebrew_transliteration": "שֻבַּיְכּ לֻבַּיְכּ, חַ'אדמַכּ בֵּין אִידֵיכּ,", - "hebrew": "שוביק לוביק, משרתך לפניך,", - "expected_phonemes": "ʃ u b a j k | l u b a j k | x aː d m a k | b eː n | ʔ iː d eː k" - }, - { - "voice_time_seconds": { - "start": 346.0, - "end": 350.8 - }, - "arabic": "أنا اسمي جيني، شو هي أمنيتك؟", - "hebrew_transliteration": "אַנַא אִסמִי גִ'ינִי, שוּ הִיֶّ אֻמנִיתַכּ?", - "hebrew": "שמי ג'יני, מהי משאלתך?", - "expected_phonemes": "ʔ a n a | ʔ i s m i | dʒ iː n i | ʃ u | h i j j e | ʔ u m n iː t a k" - }, - { - "voice_time_seconds": { - "start": 350.8, - "end": 355.8 - }, - "arabic": "علاء الدّين صفن وإتعجّب كتير وما فهمش شو إلّي عم بصير.", - "hebrew_transliteration": "עַלַאא א-דִّין צֻפֶן וּתעַגַّבּ כְּתִיר וּמַא פִהֶמש שוּ אִלִّי עַם-בִּיצִיר.", - "hebrew": "אלאדין נדהם, השתומם מאוד ולא הבין מה קורה.", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | sˤ u f e n | u t ʕ a g g a b | k t iː r | u m a | f i h e m ʃ | ʃ u | ʔ i l l i | ʕ a m b iː sˤ iː r" - }, - { - "voice_time_seconds": { - "start": 355.8, - "end": 357.8 - }, - "arabic": "فكّر حاله عم بحلم.", - "hebrew_transliteration": "פַכַّר חַאלוֹ עַם-בִּחלַם.", - "hebrew": "חשב שהוא חולם.", - "expected_phonemes": "f a k k a r | ħ aː l o | ʕ a m b i ħ l a m" - }, - { - "voice_time_seconds": { - "start": 357.8, - "end": 359.4 - }, - "arabic": "رجع المارد قال:", - "hebrew_transliteration": "רִגֶ'ע אִל-מַארֶד קַאל:", - "hebrew": "הג'יני חזר ואמר:", - "expected_phonemes": "r i dʒ e ʕ | i l m aː r e d | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 359.4, - "end": 364.0 - }, - "arabic": "شبّيك لبّيك، خادمك بين إيديك،", - "hebrew_transliteration": "שֻבַּיְכּ לֻבַּיְכּ, חַ'אדמַכּ בֵּין אִידֵיכּ,", - "hebrew": "שוביק לוביק, משרתך לפניך,", - "expected_phonemes": "ʃ u b a j k | l u b a j k | x aː d m a k | b eː n | ʔ iː d eː k" - }, - { - "voice_time_seconds": { - "start": 364.0, - "end": 368.2 - }, - "arabic": "أنا اسمي جيني، شو هي أمنيتك؟", - "hebrew_transliteration": "אַנַא אִסמִי גִ'ינִי, שוּ הִיֶّ אֻמנִיתַכּ?", - "hebrew": "שמי ג'יני, מהי משאלתך?", - "expected_phonemes": "ʔ a n a | ʔ i s m i | dʒ iː n i | ʃ u | h i j j e | ʔ u m n iː t a k" - }, - { - "voice_time_seconds": { - "start": 368.2, - "end": 371.8 - }, - "arabic": "علاء الدّين بصوت خايف قال:", - "hebrew_transliteration": "עַלַאא א-דִّין בְּצוֹת חַ'איֶף קַאל:", - "hebrew": "אלאדין אמר בקול מפוחד:", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | b sˤ oː t | x aː j e f | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 371.8, - "end": 373.8 - }, - "arabic": "طلّعني من هون!", - "hebrew_transliteration": "טַלַّעְנִי מִן הוֹן!", - "hebrew": "תוציא אותי מכאן!", - "expected_phonemes": "tˤ a l l a ʕ n i | m i n | h oː n" - }, - { - "voice_time_seconds": { - "start": 373.8, - "end": 376.8 - }, - "arabic": "وما لحقش يعد للتّلاتة", - "hebrew_transliteration": "וּמַא לִחְקֶש יְעֻדّ לַ-תַּלַאתֶה", - "hebrew": "ועוד לפני שהספיק לספור עד שלוש", - "expected_phonemes": "u m a | l i ħ ʔ e ʃ | j ʕ u d d | l a t a l aː t e" - }, - { - "voice_time_seconds": { - "start": 376.8, - "end": 381.2 - }, - "arabic": "لاقى حاله قاعد برّيت المغارة.", - "hebrew_transliteration": "לַאקַא חַאלוֹ קַאעֶד בַּרַّית אִל-מַעַ'ארַה.", - "hebrew": "מצא את עצמו יושב מחוץ למערה.", - "expected_phonemes": "l aː ʔ a | ħ aː l o | ʔ aː ʕ e d | b a r r a j t | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 381.2, - "end": 386.6 - }, - "arabic": "إنبسط علاء الدّين، رقص من الفرح، ما صدّقش حاله.", - "hebrew_transliteration": "אִנבַּסַט עַלַאא א-דִّין, רַקַץ מִן אִל-פַרַח, מַא צַדַّקֶש חַאלוֹ.", - "hebrew": "אלאדין שמח, רקד מרוב שמחה, לא האמין למזלו.", - "expected_phonemes": "ʔ i n b a s a tˤ | ʕ a l aː a | ʔ d d iː n | r a ʔ a sˤ | m i n | i l f a r a ħ | m a | sˤ a d d a ʔ e ʃ | ħ aː l o" - }, - { - "voice_time_seconds": { - "start": 386.6, - "end": 389.6 - }, - "arabic": "وبعد ما هدي وفهم إلّي عم بصير،", - "hebrew_transliteration": "וּבַּעד מַא הִדִי וּפִהֶם אִלִّי עַם בִּצִיר,", - "hebrew": "ואחרי שנרגע והבין מה קורה,", - "expected_phonemes": "u b a ʕ d | m a | h i d i | u f i h e m | ʔ i l l i | ʕ a m | b i sˤ iː r" - }, - { - "voice_time_seconds": { - "start": 389.6, - "end": 393.0 - }, - "arabic": "وإنّه المصباح هوّ مصباح سحري.", - "hebrew_transliteration": "וּאִנּוֹ אִל-מִסבַּאח הֻוֶّ מִסבַּאח סִחרִי.", - "hebrew": "ושהמנורה היא מנורת קסמים.", - "expected_phonemes": "u ʔ i n o | i l m i s b aː ħ | h u w w e | m i s b aː ħ | s i ħ r i" - }, - { - "voice_time_seconds": { - "start": 393.0, - "end": 398.2 - }, - "arabic": "سأل المارد إذا ممكن يطلب منّه كمان أمنية.", - "hebrew_transliteration": "סַאַל אִל-מַארֶד אִד'ַא מֻמכֶּן יֻטלֹבּ מִנּוֹ כַּמַאן אֻמנִיֶה.", - "hebrew": "שאל את הג'יני אם הוא יכול לבקש ממנו עוד משאלה.", - "expected_phonemes": "s a ʔ a l | i l m aː r e d | ʔ i dˤ a | m u m k e n | j u tˤ l o b | m i n o | k a m aː n | ʔ u m n i j e" - }, - { - "voice_time_seconds": { - "start": 398.2, - "end": 400.0 - }, - "arabic": "المارد قال له:", - "hebrew_transliteration": "אִל-מַארֶד קַאל-לוֹ:", - "hebrew": "הג'יני אמר לו:", - "expected_phonemes": "i l m aː r e d | ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 400.0, - "end": 405.8 - }, - "arabic": "طبعا ممكن، أنا الخادم تبعك، إيش بدّك بلبّيلك.", - "hebrew_transliteration": "טַבְּעַן מֻמכֶּן, אַנַא אִל-חַ'דַّאם תַבַּעַכּ, אֵיש בַּדַّכּ בַּלַבִּילַכּ.", - "hebrew": "כמובן שאפשר, אני המשרת שלך, מה שתרצה אמלא לך.", - "expected_phonemes": "tˤ a b ʕ a n | m u m k e n | ʔ a n a | i l x a d d aː m | t a b a ʕ a k | ʔ eː ʃ | b a d d a k | b a l a b iː l a k" - }, - { - "voice_time_seconds": { - "start": 406.0, - "end": 410.0 - }, - "arabic": "علاء الدّين طلب يصير غني،", - "hebrew_transliteration": "עַלַאא א-דִّין טַלַבּ יְצִיר עַ'נִי,", - "hebrew": "אלאדין ביקש להיות עשיר,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | tˤ a l a b | j sˤ iː r | ɣ a n i" - }, - { - "voice_time_seconds": { - "start": 410.0, - "end": 414.0 - }, - "arabic": "ويكون معه مصاري أكتر من مصاري السّلطان.", - "hebrew_transliteration": "וִיכוּן מַעוֹ מַצַארִי אַכְּתַר מִן מַצַארִי אִל-סֻّלטַאן.", - "hebrew": "ושיהיה לו יותר כסף מכספו של הסולטאן.", - "expected_phonemes": "w iː k uː n | m a ʕ o | m a sˤ aː r i | ʔ a k t a r | m i n | m a sˤ aː r i | i l s s u l tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 414.0, - "end": 418.0 - }, - "arabic": "وبلمح البصر لاقى حاله علاء الدّين", - "hebrew_transliteration": "וּבִּלַמחֶ אִל-בַּצַר לַאקַא חַאלוֹ עַלַאא א-דִّין", - "hebrew": "וכהרף עין מצא אלאדין את עצמו", - "expected_phonemes": "u b i l a m ħ e | i l b a sˤ a r | l aː ʔ a | ħ aː l o | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 418.0, - "end": 420.8 - }, - "arabic": "غرقان في بحر من الذّهب.", - "hebrew_transliteration": "עַ'רקַאן פִי בַּחַר מִן אִל-דַّהַבּ.", - "hebrew": "שקוע בים של זהב.", - "expected_phonemes": "ɣ a r ʔ aː n | f i | b a ħ a r | m i n | i l d d a h a b" - }, - { - "voice_time_seconds": { - "start": 420.8, - "end": 424.4 - }, - "arabic": "بلّش علاء الدّين يفكّر وقال لنفسه", - "hebrew_transliteration": "בַּלַّש עַלַאא א-דִّין יפַכַّר וּקַאל לַנַפסוֹ", - "hebrew": "אלאדין התחיל לחשוב ואמר לעצמו", - "expected_phonemes": "b a l l a ʃ | ʕ a l aː a | ʔ d d iː n | j f a k k a r | u ʔ aː l | l a n a f s o" - }, - { - "voice_time_seconds": { - "start": 424.4, - "end": 429.0 - }, - "arabic": "إنّه أجته الفرصة يحقّق أكبر حلم عنده في حياته", - "hebrew_transliteration": "אִנּוֹ אַגַ'תוֹ אִל-פֻרצַה יְחַקֶّק אַכְּבַּר חִלֶם עִנדוֹ פִי חַיַאתוֹ", - "hebrew": "שהגיעה ההזדמנות להגשים את החלום הגדול ביותר בחייו", - "expected_phonemes": "ʔ i n o | ʔ a dʒ a t o | i l f u r sˤ a | j ħ a ʔ ʔ e ʔ | ʔ a k b a r | ħ i l e m | ʕ i n d o | f i | ħ a j aː t o" - }, - { - "voice_time_seconds": { - "start": 429.0, - "end": 434.4 - }, - "arabic": "وهوّ إنّه يتجوّز ياسمينا بنت السّلطان.", - "hebrew_transliteration": "וּהֻוֶّ אִנּוֹ יִתגַ'וַّז יַאסמִינַא בִּנת אִל-סֻّלטַאן.", - "hebrew": "והוא להתחתן עם יסמינה בת הסולטאן.", - "expected_phonemes": "u h u w w e | ʔ i n o | j i t dʒ a w w a z | j aː s m iː n a | b i n t | i l s s u l tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 434.4, - "end": 438.0 - }, - "arabic": "وهيك رجع على بغداد،", - "hebrew_transliteration": "וּהֵיכּ רִגֶ'ע עַלַא בַּעְ'דַאד,", - "hebrew": "וכך חזר לבגדאד,", - "expected_phonemes": "u h eː k | r i dʒ e ʕ | ʕ a l a | b a ɣ d aː d" - }, - { - "voice_time_seconds": { - "start": 438.0, - "end": 442.2 - }, - "arabic": "لابس أحلى الأواعي، ومعه خدم وحشم.", - "hebrew_transliteration": "לַאבֶּס אַחלַא אִל-אַוַאעִי, וּמַעוֹ חַ'דַם וּחַשַם.", - "hebrew": "לבוש בבגדים היפים ביותר, ואיתו משרתים ופמליה.", - "expected_phonemes": "l aː b e s | ʔ a ħ l a | i l ʔ a w aː ʕ i | u m a ʕ o | x a d a m | u ħ a ʃ a m" - }, - { - "voice_time_seconds": { - "start": 442.2, - "end": 447.2 - }, - "arabic": "وراح على قصر السّلطان عشان يطلب إيد ياسمينا.", - "hebrew_transliteration": "וּרַאח עַלַא קַצֶר אִל-סֻّלטַאן עַשַאן יֻטלֹבּ אִיד יַאסמִינַא.", - "hebrew": "והלך לארמון הסולטאן כדי לבקש את ידה של יסמינה.", - "expected_phonemes": "u r aː ħ | ʕ a l a | ʔ a sˤ e r | i l s s u l tˤ aː n | ʕ a ʃ aː n | j u tˤ l o b | ʔ iː d | j aː s m iː n a" - }, - { - "voice_time_seconds": { - "start": 447.2, - "end": 451.8 - }, - "arabic": "السّلطان أعجب كتير في علاء الدّين،", - "hebrew_transliteration": "אִ-סֻّלטַאן, אֻעגִ'בּ כְּתִיר מִן עַלַאא א-דִّין,", - "hebrew": "הסולטאן התרשם מאוד מאלאדין,", - "expected_phonemes": "ʔ i s s u l tˤ aː n | ʔ u ʕ dʒ i b | k t iː r | m i n | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 451.8, - "end": 454.0 - }, - "arabic": "وحبّ إنّه يعطيه بنته،", - "hebrew_transliteration": "וּחַבّ אִנּוֹ יַעטִיה בִּנתוֹ,", - "hebrew": "ורצה לתת לו את בתו,", - "expected_phonemes": "u ħ a b b | ʔ i n o | j a ʕ tˤ iː h | b i n t o" - }, - { - "voice_time_seconds": { - "start": 454.0, - "end": 459.4 - }, - "arabic": "بسّ للأسف كان صار واعد إبن الوزير بإيد ياسمينا.", - "hebrew_transliteration": "בַּס לִל-אַסַף כַּאן צַאר וַאעֶד אִבּן אִל-וַזִיר, בְּאִיד יַאסמִינַא.", - "hebrew": "אבל לצערו הוא כבר הבטיח לבן הווזיר את ידה של יסמינה.", - "expected_phonemes": "b a s | l i l ʔ a s a f | k aː n | sˤ aː r | w aː ʕ e d | ʔ i b n | i l w a z iː r | b ʔ iː d | j aː s m iː n a" - }, - { - "voice_time_seconds": { - "start": 459.4, - "end": 462.4 - }, - "arabic": "عشان هيك قرّر علاء الدّين", - "hebrew_transliteration": "עַשַאן הֵיכּ קַרַّר עַלַאא א-דִّין", - "hebrew": "לכן החליט אלאדין", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | ʔ a r r a r | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 462.4, - "end": 468.2 - }, - "arabic": "إنّه يعمل خطّة تخلّي ياسمينا ترفض إبن الوزير.", - "hebrew_transliteration": "אִנּוֹ יַעמַל חֻ'טַّה תְחַלִّי יַאסמִינַא תֻרפֹּד' אִבּן אִל-וַזִיר.", - "hebrew": "לעשות תוכנית שתגרום ליסמינה לסרב לבן הווזיר.", - "expected_phonemes": "ʔ i n o | j a ʕ m a l | x u tˤ tˤ a | t ħ a l l i | j aː s m iː n a | t u r f o dˤ | ʔ i b n | i l w a z iː r" - }, - { - "voice_time_seconds": { - "start": 468.2, - "end": 469.6 - }, - "arabic": "وهيك صار.", - "hebrew_transliteration": "וּהֵיכּ צַאר.", - "hebrew": "וכך היה.", - "expected_phonemes": "u h eː k | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 469.6, - "end": 472.6 - }, - "arabic": "ياسمينا لمّا شافت علاء الدّين،", - "hebrew_transliteration": "יַאסמִינַא לַמַّא שַאפַת עַלַאא א-דִّין,", - "hebrew": "כשיסמינה ראתה את אלאדין,", - "expected_phonemes": "j aː s m iː n a | l a m m a | ʃ aː f a t | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 472.6, - "end": 477.2 - }, - "arabic": "وقدّيش إنّه هوّ شاب قوي، شجاع وشاطر،", - "hebrew_transliteration": "וּקַדֵّיש אִנּוֹ הֻוֶّ שַאבּ קַוִי, שֻגַ'אע וּשַאטֶר,", - "hebrew": "וכמה הוא בחור חזק, אמיץ ונבון,", - "expected_phonemes": "u ʔ a d d eː ʃ | ʔ i n o | h u w w e | ʃ aː b | ʔ a w i | ʃ u dʒ aː ʕ | u ʃ aː tˤ e r" - }, - { - "voice_time_seconds": { - "start": 477.2, - "end": 481.8 - }, - "arabic": "قرّرت ترفض إبن الوزير، وتتجوّز علاء الدّين.", - "hebrew_transliteration": "קַרַّרַת תֻרפֹּד' אִבּן אִל-וַזִיר, וּתִתגַ'וַّז עַלַאא א-דִّין.", - "hebrew": "החליטה לסרב לבן הווזיר ולהתחתן עם אלאדין.", - "expected_phonemes": "ʔ a r r a r a t | t u r f o dˤ | ʔ i b n | i l w a z iː r | u t i t dʒ a w w a z | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 481.8, - "end": 485.6 - }, - "arabic": "شرطه الوحيد للسّلطان عشان يتمّ الزّواج", - "hebrew_transliteration": "שַרְטוֹ אִל-וַחִיד לַל-סֻّלטַאן עַשַאן יִתֶּם אִל-זַّוַאג'", - "hebrew": "התנאי היחיד של הסולטאן כדי שהנישואים יתקיימו", - "expected_phonemes": "ʃ a r tˤ o | i l w a ħ iː d | l a l s s u l tˤ aː n | ʕ a ʃ aː n | j i t e m | i l z z a w aː dʒ" - }, - { - "voice_time_seconds": { - "start": 485.6, - "end": 488.2 - }, - "arabic": "كان إنّه يكون عند علاء الدّين", - "hebrew_transliteration": "כַּאן אִנּוֹ יְכוּן עִנד עַלַאא א-דִّין", - "hebrew": "היה שיהיה לאלאדין", - "expected_phonemes": "k aː n | ʔ i n o | j k uː n | ʕ i n d | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 488.2, - "end": 491.6 - }, - "arabic": "قصر كبير تعيش فيه ياسمينا.", - "hebrew_transliteration": "קַצֶר כְּבִּיר תְעִיש פִיה יַאסמִינַא.", - "hebrew": "ארמון גדול שבו תגור יסמינה.", - "expected_phonemes": "ʔ a sˤ e r | k b iː r | t ʕ iː ʃ | f iː h | j aː s m iː n a" - }, - { - "voice_time_seconds": { - "start": 491.6, - "end": 495.6 - }, - "arabic": "بالنّسبة لعلاء الدين هذا الشّرط كان كتير سهل.", - "hebrew_transliteration": "בִּ-נִّסבֶּה לַעַלַאא א-דִّין הַאדַא אִל-שַّרְט כַּאן כְּתִיר סַהֶל.", - "hebrew": "בשביל אלאדין התנאי הזה היה קל מאוד.", - "expected_phonemes": "b i n n i s b e | l a ʕ a l aː a | ʔ d d iː n | h aː d a | i l ʃ ʃ a r tˤ | k aː n | k t iː r | s a h e l" - }, - { - "voice_time_seconds": { - "start": 495.6, - "end": 498.0 - }, - "arabic": "بمجرّد إنّه طلب من جيني", - "hebrew_transliteration": "בִּמֻגַ'רַّד אִנּוֹ טַלַבּ מִן גִ'ינִי", - "hebrew": "ברגע שביקש מג'יני", - "expected_phonemes": "b i m u dʒ a r r a d | ʔ i n o | tˤ a l a b | m i n | dʒ iː n i" - }, - { - "voice_time_seconds": { - "start": 498.0, - "end": 501.6 - }, - "arabic": "يعملّه قصر أكبر من قصر السّلطان،", - "hebrew_transliteration": "יִעמִלּוֹ קַצֶר אַכְּבַּר מִן קַצֶר אִל-סֻّלטַאן,", - "hebrew": "לבנות לו ארמון גדול מארמון הסולטאן,", - "expected_phonemes": "j i ʕ m i l o | ʔ a sˤ e r | ʔ a k b a r | m i n | ʔ a sˤ e r | i l s s u l tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 501.6, - "end": 508.2 - }, - "arabic": "تحقّق الطّلب وصار عنده قصر من أجمل قصور العالم.", - "hebrew_transliteration": "תְחַקַّק אִל-טַּלַבּ וּצַאר עִנדוֹ קַצֶר מִן אַגְ'מַל קֻצוּר אִל-עַאלַם.", - "hebrew": "הבקשה התגשמה והיה לו ארמון מהיפים בעולם.", - "expected_phonemes": "t ħ a ʔ ʔ a ʔ | i l tˤ a l a b | u sˤ aː r | ʕ i n d o | ʔ a sˤ e r | m i n | ʔ a dʒ m a l | ʔ u sˤ uː r | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 508.2, - "end": 512.4 - }, - "arabic": "وعملوا عرس كبير، كان مدّته أربعين يوم وليلة،", - "hebrew_transliteration": "וּעִמלוּ עֻרֶס כְּבִּיר, כַּאן מֻדִّתוֹ אַרבְּעִין יוֹם וּלֵילֶה,", - "hebrew": "ועשו חתונה גדולה, שנמשכה ארבעים יום ולילה,", - "expected_phonemes": "u ʕ i m l u | ʕ u r e s | k b iː r | k aː n | m u d d i t o | ʔ a r b ʕ iː n | j oː m | u l eː l e" - }, - { - "voice_time_seconds": { - "start": 512.4, - "end": 517.4 - }, - "arabic": "ورقصوا وأكلوا وفرحوا وإنبسطوا.", - "hebrew_transliteration": "וּרַקַצוּ וּאַכַּלוּ וּפִרחוּ וּאִנבַּסַטוּ.", - "hebrew": "ורקדו ואכלו ושמחו ונהנו.", - "expected_phonemes": "u r a ʔ a sˤ u | u ʔ a k a l u | u f i r ħ u | u ʔ i n b a s a tˤ u" - }, - { - "voice_time_seconds": { - "start": 517.4, - "end": 521.0 - }, - "arabic": "يوم من الأيّام طلع علاء الدّين", - "hebrew_transliteration": "יוֹם מִן אִל-אַיַّאם טִלֶע עַלַאא א-דִّין", - "hebrew": "יום אחד יצא אלאדין", - "expected_phonemes": "j oː m | m i n | i l ʔ a j j aː m | tˤ i l e ʕ | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 521.0, - "end": 523.4 - }, - "arabic": "وخلّى ياسمينا لحالها.", - "hebrew_transliteration": "וּחַ'לַّא יַאסמִינַא לַחַאלְהַא.", - "hebrew": "והשאיר את יסמינה לבדה.", - "expected_phonemes": "u x a l l a | j aː s m iː n a | l a ħ aː l h a" - }, - { - "voice_time_seconds": { - "start": 523.4, - "end": 527.0 - }, - "arabic": "الختيار السّاحر إلّي كان بدّه المصباح", - "hebrew_transliteration": "אִל-חְ'תִיַאר אִל-סַّאחֶר אִלִّי כַּאן בַּדּוֹ אִל-מִסבַּאח", - "hebrew": "הזקן הקוסם שרצה את המנורה", - "expected_phonemes": "i l x t i j aː r | i l s s aː ħ e r | ʔ i l l i | k aː n | b a d o | i l m i s b aː ħ" - }, - { - "voice_time_seconds": { - "start": 527.0, - "end": 530.8 - }, - "arabic": "كان صار سامع عن واحد اسمه علاء الدّين،", - "hebrew_transliteration": "כַּאן צַאר סַאמֶע עַן וַאחַד אִסמוֹ עַלַאא א-דִّין,", - "hebrew": "כבר שמע על מישהו בשם אלאדין,", - "expected_phonemes": "k aː n | sˤ aː r | s aː m e ʕ | ʕ a n | w aː ħ a d | ʔ i s m o | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 530.8, - "end": 534.8 - }, - "arabic": "شاب غني إلّي تجوّز بنت السّلطان.", - "hebrew_transliteration": "שַאבּ עַ'נִי אִלִّי תְגַ'וַّז בִּנת אִל-סֻّלטַאן.", - "hebrew": "בחור עשיר שהתחתן עם בת הסולטאן.", - "expected_phonemes": "ʃ aː b | ɣ a n i | ʔ i l l i | t dʒ a w w a z | b i n t | i l s s u l tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 534.8, - "end": 540.2 - }, - "arabic": "عرف السّاحر إنّه هذا هوّ نفسه علاء الدّين الفقير", - "hebrew_transliteration": "עִרֶף אִ-סַّאחֶר אִנּוֹ הַאדַא הֻוֶّ נַפסוֹ עַלַאא א-דִّין אִל-פַקִיר", - "hebrew": "הקוסם הבין שזה אותו אלאדין העני", - "expected_phonemes": "ʕ i r e f | ʔ i s s aː ħ e r | ʔ i n o | h aː d a | h u w w e | n a f s o | ʕ a l aː a | ʔ d d iː n | i l f a ʔ iː r" - }, - { - "voice_time_seconds": { - "start": 540.2, - "end": 542.8 - }, - "arabic": "إلّي أخده معاه على الصّحرا", - "hebrew_transliteration": "אִלִّי אַחַ'דוֹ מַעַאה עַלַא צַחרַא", - "hebrew": "שלקח איתו למדבר", - "expected_phonemes": "ʔ i l l i | ʔ a x a d o | m a ʕ aː h | ʕ a l a | sˤ a ħ r a" - }, - { - "voice_time_seconds": { - "start": 542.8, - "end": 546.0 - }, - "arabic": "عشان يجيبله المصباح من المغارة.", - "hebrew_transliteration": "עַשַאן יְגִ'יבּ-לוֹ אִל-מִסבַּאח מִן אִל-מַעַ'ארַה.", - "hebrew": "כדי שיביא לו את המנורה מהמערה.", - "expected_phonemes": "ʕ a ʃ aː n | j dʒ iː b l o | i l m i s b aː ħ | m i n | i l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 546.0, - "end": 549.0 - }, - "arabic": "وعشان هيك ضلّ يراقب القصر", - "hebrew_transliteration": "וּעַשַאן הֵיכּ דַ'ל יְרַאקֶבּ אִל-קַצֶר", - "hebrew": "ולכן המשיך לעקוב אחרי הארמון", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | dˤ a l | j r aː ʔ e b | i l ʔ a sˤ e r" - }, - { - "voice_time_seconds": { - "start": 549.0, - "end": 553.6 - }, - "arabic": "لحدّ ما شاف إنّه ياسمينا بقيت لحالها.", - "hebrew_transliteration": "לַחַדّ מַא שַאף אִנּוֹ יַאסמִינַא בְּקִית לַחַאלְהַא.", - "hebrew": "עד שראה שיסמינה נשארה לבדה.", - "expected_phonemes": "l a ħ a d d | m a | ʃ aː f | ʔ i n o | j aː s m iː n a | b ʔ iː t | l a ħ aː l h a" - }, - { - "voice_time_seconds": { - "start": 553.6, - "end": 560.4 - }, - "arabic": "بعدها تنكّر ولبس أواعي تاجر بيبيع مصابيح جديدة.", - "hebrew_transliteration": "בַּעֶדהַא תְנַכַّר וּלִבֶּס אַוַאעִי תַאגֶ'ר בִּיבִּיע מַצַאבִּיח גְ'דִידֶה.", - "hebrew": "אחר כך התחפש ולבש בגדי סוחר שמוכר מנורות חדשות.", - "expected_phonemes": "b a ʕ e d h a | t n a k k a r | u l i b e s | ʔ a w aː ʕ i | t aː dʒ e r | b iː b iː ʕ | m a sˤ aː b iː ħ | dʒ d iː d e" - }, - { - "voice_time_seconds": { - "start": 560.4, - "end": 565.8 - }, - "arabic": "ودقّ على باب القصر، وطلب يشوف بنت السّلطان.", - "hebrew_transliteration": "וּדַקّ עַלַא בַּאבּ אִל-קַצֶר, וּטַלַבּ יְשוּף בִּנת אִל-סֻّלטַאן.", - "hebrew": "ודפק על דלת הארמון וביקש לראות את בת הסולטאן.", - "expected_phonemes": "u d a ʔ ʔ | ʕ a l a | b aː b | i l ʔ a sˤ e r | u tˤ a l a b | j ʃ uː f | b i n t | i l s s u l tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 565.8, - "end": 568.6 - }, - "arabic": "ياسمينا سمعت الختيار", - "hebrew_transliteration": "יַאסמִינַא סִמעַת אִל-חְ'תִיַאר", - "hebrew": "יסמינה שמעה את הזקן", - "expected_phonemes": "j aː s m iː n a | s i m ʕ a t | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 568.6, - "end": 572.4 - }, - "arabic": "عم بطلب من الخادم الإذن يشوفها،", - "hebrew_transliteration": "עַם-בִּטלֹבּ מִן אִל-חַ'אדֶם אִל-אִד'ֶן יְשוּפְהַא,", - "hebrew": "מבקש מהמשרת רשות לראות אותה,", - "expected_phonemes": "ʕ a m b i tˤ l o b | m i n | i l x aː d e m | i l ʔ i dˤ e n | j ʃ uː f h a" - }, - { - "voice_time_seconds": { - "start": 572.4, - "end": 576.8 - }, - "arabic": "وعشان شافته عجوز كبير، ما حبّت تكسفه،", - "hebrew_transliteration": "וּעַשַאן שַאפַתוֹ עַגוּז כְּבִּיר, מַא חַבַּת תְכַּסְפוֹ,", - "hebrew": "ומכיוון שראתה שהוא זקן מבוגר, לא רצתה לבייש אותו,", - "expected_phonemes": "u ʕ a ʃ aː n | ʃ aː f a t o | ʕ a g uː z | k b iː r | m a | ħ a b a t | t k a s f o" - }, - { - "voice_time_seconds": { - "start": 576.8, - "end": 579.6 - }, - "arabic": "وراحت وطلعت لعنده.", - "hebrew_transliteration": "וּרַאחַת וּטִלעַת לַעִנדוֹ.", - "hebrew": "והלכה ויצאה אליו.", - "expected_phonemes": "u r aː ħ a t | u tˤ i l ʕ a t | l a ʕ i n d o" - }, - { - "voice_time_seconds": { - "start": 579.6, - "end": 584.2 - }, - "arabic": "الختيار لما شافها، لمعوا عينيه وإنبسط،", - "hebrew_transliteration": "אִל-חְ'תִיַאר לַמַّא שַאפְהַא לַמַעוּ עֵינֵיה וּאִנבַּסַט,", - "hebrew": "כשהזקן ראה אותה, עיניו נצצו והוא שמח,", - "expected_phonemes": "i l x t i j aː r | l a m m a | ʃ aː f h a | l a m a ʕ u | ʕ eː n eː h | u ʔ i n b a s a tˤ" - }, - { - "voice_time_seconds": { - "start": 584.2, - "end": 588.4 - }, - "arabic": "وحكالها إنّه هوّ بشتري مصابيح قديمة", - "hebrew_transliteration": "וּחַכַא-לְהַא אִנּוֹ הֻוֶّ בִּשתְרִי מַצַאבִּיח קַדִימֶה", - "hebrew": "ואמר לה שהוא קונה מנורות ישנות", - "expected_phonemes": "u ħ a k aː l h a | ʔ i n o | h u w w e | b i ʃ t r i | m a sˤ aː b iː ħ | ʔ a d iː m e" - }, - { - "voice_time_seconds": { - "start": 588.4, - "end": 591.6 - }, - "arabic": "وبيعطي محلّها مصابيح جديدة.", - "hebrew_transliteration": "וּבִּיעטִי מַחַלַّהַא מַצַאבִּיח גְ'דִידֶה.", - "hebrew": "ונותן במקומן מנורות חדשות.", - "expected_phonemes": "u b iː ʕ tˤ i | m a ħ a l l a h a | m a sˤ aː b iː ħ | dʒ d iː d e" - }, - { - "voice_time_seconds": { - "start": 591.6, - "end": 595.6 - }, - "arabic": "وسألها إذا عندها مصباح قديم للبيع.", - "hebrew_transliteration": "וּסַאַלְהַא אִד'ַא עִנדְהַא מִסבַּאח קַדִים לַל-בֵּיע.", - "hebrew": "ושאל אותה אם יש לה מנורה ישנה למכירה.", - "expected_phonemes": "u s a ʔ a l h a | ʔ i dˤ a | ʕ i n d h a | m i s b aː ħ | ʔ a d iː m | l a l b eː ʕ" - }, - { - "voice_time_seconds": { - "start": 595.6, - "end": 598.2 - }, - "arabic": "ياسمينا فكّرت شوي،", - "hebrew_transliteration": "יַאסמִינַא פַכַّרַת שְוַי,", - "hebrew": "יסמינה חשבה קצת,", - "expected_phonemes": "j aː s m iː n a | f a k k a r a t | ʃ w a j" - }, - { - "voice_time_seconds": { - "start": 598.2, - "end": 603.4 - }, - "arabic": "وتذكّرت مصباح علاء الدّين، إلّي كان قديم كتير،", - "hebrew_transliteration": "וּתזַכַّרַת מִסבַּאח עַלַאא א-דִّין, אִלִّי כַּאן קַדִים כְּתִיר,", - "hebrew": "ונזכרה במנורה של אלאדין, שהייתה ישנה מאוד,", - "expected_phonemes": "u t z a k k a r a t | m i s b aː ħ | ʕ a l aː a | ʔ d d iː n | ʔ i l l i | k aː n | ʔ a d iː m | k t iː r" - }, - { - "voice_time_seconds": { - "start": 603.4, - "end": 606.8 - }, - "arabic": "وتوقّعت إنّه جوزها راح ينبسط كتير", - "hebrew_transliteration": "וּתוַקַّעַת אִנּוֹ גוֹזְהַא רַאח יִנבְּסֶט כְּתִיר", - "hebrew": "וחשבה שבעלה ישמח מאוד", - "expected_phonemes": "u t w a ʔ ʔ a ʕ a t | ʔ i n o | g oː z h a | r aː ħ | j i n b s e tˤ | k t iː r" - }, - { - "voice_time_seconds": { - "start": 606.8, - "end": 612.2 - }, - "arabic": "لمّا يعرف إنّها بدّلت المصباح القديم بواحد جديد.", - "hebrew_transliteration": "לַמַّא יִערַף אִנְّהַא בַּדַّלַת אִל-מִסבַּאח אִל-קַדִים בְּוַאחַד גְ'דִיד.", - "hebrew": "כשיידע שהחליפה את המנורה הישנה בחדשה.", - "expected_phonemes": "l a m m a | j i ʕ r a f | ʔ i n n h a | b a d d a l a t | i l m i s b aː ħ | i l ʔ a d iː m | b w aː ħ a d | dʒ d iː d" - }, - { - "voice_time_seconds": { - "start": 612.2, - "end": 616.2 - }, - "arabic": "عشان هيك، راحت جابته للمصباح القديم", - "hebrew_transliteration": "עַשַאן הֵיכּ, רַאחַת גַ'אבַּתוֹ לַל-מִסבַּאח אִל-קַדִים", - "hebrew": "לכן הלכה והביאה את המנורה הישנה", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | r aː ħ a t | dʒ aː b a t o | l a l m i s b aː ħ | i l ʔ a d iː m" - }, - { - "voice_time_seconds": { - "start": 616.2, - "end": 618.0 - }, - "arabic": "وأعطته للختيار،", - "hebrew_transliteration": "וּאַעטַתוֹ לַל-חְ'תִיַאר,", - "hebrew": "ונתנה אותה לזקן,", - "expected_phonemes": "u ʔ a ʕ tˤ a t o | l a l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 618.0, - "end": 622.6 - }, - "arabic": "وبالمقابل أخدت منّه مصباح جديد.", - "hebrew_transliteration": "וּבִּל-מֻקַאבֶּל אַחַ'דַת מִנּוֹ מִסבַּאח גְ'דִיד.", - "hebrew": "ובתמורה לקחה ממנו מנורה חדשה.", - "expected_phonemes": "u b i l m u ʔ aː b e l | ʔ a x a d a t | m i n o | m i s b aː ħ | dʒ d iː d" - }, - { - "voice_time_seconds": { - "start": 622.6, - "end": 625.4 - }, - "arabic": "لمّا رجع علاء الدّين على القصر", - "hebrew_transliteration": "לַמַّא רִגֶ'ע עַלַאא א-דִّין עַלַא אִל-קַצֶר", - "hebrew": "כשאלאדין חזר לארמון", - "expected_phonemes": "l a m m a | r i dʒ e ʕ | ʕ a l aː a | ʔ d d iː n | ʕ a l a | i l ʔ a sˤ e r" - }, - { - "voice_time_seconds": { - "start": 625.4, - "end": 628.8 - }, - "arabic": "حكتله ياسمينا على إلّي عملته.", - "hebrew_transliteration": "חַכַּת-לוֹ יַאסמִינַא עַלַא אִלִّי עִמלַתוֹ.", - "hebrew": "יסמינה סיפרה לו על מה שעשתה.", - "expected_phonemes": "ħ a k a t l o | j aː s m iː n a | ʕ a l a | ʔ i l l i | ʕ i m l a t o" - }, - { - "voice_time_seconds": { - "start": 628.8, - "end": 630.8 - }, - "arabic": "علاء الدّين إنجنّ", - "hebrew_transliteration": "עַלַאא א-דִّין אִנגַ'ן", - "hebrew": "אלאדין השתגע", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | ʔ i n dʒ a n" - }, - { - "voice_time_seconds": { - "start": 630.8, - "end": 635.8 - }, - "arabic": "وقالها إنّها بدّلت مصباح سحري بمصباح عادي،", - "hebrew_transliteration": "וּקַאלְהַא אִנְّהַא בַּדַّלַת מִסבַּאח סִחרִי בְּמִסבַּאח עַאדִי,", - "hebrew": "ואמר לה שהיא החליפה מנורת קסמים במנורה רגילה,", - "expected_phonemes": "u ʔ aː l h a | ʔ i n n h a | b a d d a l a t | m i s b aː ħ | s i ħ r i | b m i s b aː ħ | ʕ aː d i" - }, - { - "voice_time_seconds": { - "start": 635.8, - "end": 639.4 - }, - "arabic": "وإنّه هذا الختيار هوّ ساحر.", - "hebrew_transliteration": "וּאִנּוֹ הַאדַא אִל-חְ'תִיַאר הֻוֶّ סַאחֶר.", - "hebrew": "ושהזקן הזה הוא קוסם.", - "expected_phonemes": "u ʔ i n o | h aː d a | i l x t i j aː r | h u w w e | s aː ħ e r" - }, - { - "voice_time_seconds": { - "start": 639.4, - "end": 643.0 - }, - "arabic": "بلّش علاء الدّين يفتّش على الختيار،", - "hebrew_transliteration": "בַּלַّש עַלַאא א-דִّין יפַתֶّש עַלַא אִל-חְ'תִיַאר,", - "hebrew": "אלאדין התחיל לחפש את הזקן,", - "expected_phonemes": "b a l l a ʃ | ʕ a l aː a | ʔ d d iː n | j f a t t e ʃ | ʕ a l a | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 643.0, - "end": 647.6 - }, - "arabic": "عشان يرجّع مصباحه السّحري إلّي فيه المارد.", - "hebrew_transliteration": "עַשַאן יְרַגַّע מִסבַּאחוֹ אִ-סִّחרִי אִלִّי פִיה אִל-מַארֶד.", - "hebrew": "כדי להחזיר את מנורת הקסמים שלו שבה נמצא הג'יני.", - "expected_phonemes": "ʕ a ʃ aː n | j r a g g a ʕ | m i s b aː ħ o | ʔ i s s i ħ r i | ʔ i l l i | f iː h | i l m aː r e d" - }, - { - "voice_time_seconds": { - "start": 647.6, - "end": 650.2 - }, - "arabic": "فتّش وما لاقاهوش،", - "hebrew_transliteration": "פַתַّש וּמַא לַאקַאהוּש,", - "hebrew": "חיפש ולא מצא אותו,", - "expected_phonemes": "f a t t a ʃ | u m a | l aː ʔ aː h uː ʃ" - }, - { - "voice_time_seconds": { - "start": 650.2, - "end": 656.2 - }, - "arabic": "لحد ما سمع إنّه فيه حدا عم بحاول يقتل السّلطان.", - "hebrew_transliteration": "לַחַדّ מַא סִמֶע אִנּוֹ פִיה חַדַא עַם-בִּחַאוֶל יֻקתֹל אִל-סֻّלטַאן.", - "hebrew": "עד ששמע שמישהו מנסה להרוג את הסולטאן.", - "expected_phonemes": "l a ħ a d d | m a | s i m e ʕ | ʔ i n o | f iː h | ħ a d a | ʕ a m b i ħ aː w e l | j u ʔ t o l | i l s s u l tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 656.2, - "end": 659.8 - }, - "arabic": "ركض لهناك، وشاف جيني المارد،", - "hebrew_transliteration": "רַכַד' לַהֻנַאכּ, וּשַאף גִ'ינִי אִל-מַארֶד,", - "hebrew": "רץ לשם, וראה את ג'יני הענק,", - "expected_phonemes": "r a k a dˤ | l a h u n aː k | u ʃ aː f | dʒ iː n i | i l m aː r e d" - }, - { - "voice_time_seconds": { - "start": 659.8, - "end": 662.8 - }, - "arabic": "متغيّر لونه وصاير أحمر.", - "hebrew_transliteration": "מִתעַ'יֶّר לוֹנוֹ וּצַאיֶר אַחמַר.", - "hebrew": "שצבעו השתנה והפך לאדום.", - "expected_phonemes": "m i t ɣ a j j e r | l oː n o | u sˤ aː j e r | ʔ a ħ m a r" - }, - { - "voice_time_seconds": { - "start": 662.8, - "end": 666.0 - }, - "arabic": "جيني كان تحت سلطة الختيار", - "hebrew_transliteration": "גִ'ינִי כַּאן תַחת סֻלטֶת אִל-חְ'תִיַאר", - "hebrew": "ג'יני היה תחת שליטת הזקן", - "expected_phonemes": "dʒ iː n i | k aː n | t a ħ t | s u l tˤ e t | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 666.0, - "end": 668.8 - }, - "arabic": "إلّي تبيّن إنّه ساحر شرّير", - "hebrew_transliteration": "אִלִّי תְבַּיַّן אִנּוֹ סַאחֶר שִרִّיר", - "hebrew": "שהתברר שהוא קוסם רשע", - "expected_phonemes": "ʔ i l l i | t b a j j a n | ʔ i n o | s aː ħ e r | ʃ i r r iː r" - }, - { - "voice_time_seconds": { - "start": 668.8, - "end": 673.8 - }, - "arabic": "هدفه يقتل السّلطان ويقعد محلّه على كرسي الحكم.", - "hebrew_transliteration": "הַדַפוֹ יֻקתֹל אִל-סֻّלטַאן וִיֻקעֹד מַחַלّוֹ עַלַא כֻּרסִי אִל-חֻכֶּם.", - "hebrew": "שמטרתו להרוג את הסולטאן ולשבת במקומו על כס השלטון.", - "expected_phonemes": "h a d a f o | j u ʔ t o l | i l s s u l tˤ aː n | w i j u ʔ ʕ o d | m a ħ a l l o | ʕ a l a | k u r s i | i l ħ u k e m" - }, - { - "voice_time_seconds": { - "start": 673.8, - "end": 678.6 - }, - "arabic": "علاء الدّين حاول ينقذ السّلطان من إيدين جيني،", - "hebrew_transliteration": "עַלַאא א-דִّין חַאוַל יִנקֶד' אִל-סֻّלטַאן מִן אִידֵין גִ'ינִי,", - "hebrew": "אלאדין ניסה להציל את הסולטאן מידי ג'יני,", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | ħ aː w a l | j i n ʔ e dˤ | i l s s u l tˤ aː n | m i n | ʔ iː d eː n | dʒ iː n i" - }, - { - "voice_time_seconds": { - "start": 678.6, - "end": 682.6 - }, - "arabic": "بسّ ما قدرش، جيني كان كتير قوي.", - "hebrew_transliteration": "בַּס מַא קִדְרֶש, גִ'ינִי כַּאן כְּתִיר קַוִי.", - "hebrew": "אבל לא הצליח, ג'יני היה חזק מאוד.", - "expected_phonemes": "b a s | m a | ʔ i d r e ʃ | dʒ iː n i | k aː n | k t iː r | ʔ a w i" - }, - { - "voice_time_seconds": { - "start": 682.6, - "end": 687.0 - }, - "arabic": "عشان هيك راح علاء الدّين وتواجه مع الختيار،", - "hebrew_transliteration": "עַשַאן הֵיכּ רַאח עַלַאא א-דִّין וּתוַאגַ'ה מַע אִל-חְ'תִיַאר,", - "hebrew": "לכן הלך אלאדין והתעמת עם הזקן,", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | r aː ħ | ʕ a l aː a | ʔ d d iː n | u t w aː dʒ a | m a ʕ | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 687.0, - "end": 690.0 - }, - "arabic": "وصار إشتباك عنيف بيناتهم", - "hebrew_transliteration": "וּצַאר אִשתִבַּאכּ עַנִיף בֵּינַאתְהֹם", - "hebrew": "ופרץ ביניהם עימות אלים", - "expected_phonemes": "u sˤ aː r | ʔ i ʃ t i b aː k | ʕ a n iː f | b eː n aː t h o m" - }, - { - "voice_time_seconds": { - "start": 690.0, - "end": 693.8 - }, - "arabic": "إلّي في نهايته قدر يربح علاء الدّين", - "hebrew_transliteration": "אִלִّי פִי נִהַאיְתוֹ קִדֶר יִרבַּח עַלַאא א-דִّין", - "hebrew": "שבסופו אלאדין הצליח לנצח", - "expected_phonemes": "ʔ i l l i | f i | n i h aː j t o | ʔ i d e r | j i r b a ħ | ʕ a l aː a | ʔ d d iː n" - }, - { - "voice_time_seconds": { - "start": 693.8, - "end": 696.4 - }, - "arabic": "ويقتل السّاحر العجوز.", - "hebrew_transliteration": "וִיֻקתֹל אִל-סַّאחֶר אִל-עַגוּז.", - "hebrew": "ולהרוג את הקוסם הזקן.", - "expected_phonemes": "w i j u ʔ t o l | i l s s aː ħ e r | i l ʕ a g uː z" - }, - { - "voice_time_seconds": { - "start": 696.4, - "end": 702.0 - }, - "arabic": "جيني رجع لطبيعته، وزعل كتير إنّه حاول يقتل السّلطان،", - "hebrew_transliteration": "גִ'ינִי רִגֶ'ע לַטַבִּיעתוֹ, וּזִעֶל כְּתִיר אִנּוֹ חַאוַל יֻקתֹל אִל-סֻّלטַאן,", - "hebrew": "ג'יני חזר לטבעו, והצטער מאוד שניסה להרוג את הסולטאן,", - "expected_phonemes": "dʒ iː n i | r i dʒ e ʕ | l a tˤ a b iː ʕ t o | u z i ʕ e l | k t iː r | ʔ i n o | ħ aː w a l | j u ʔ t o l | i l s s u l tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 702.0, - "end": 707.4 - }, - "arabic": "بسّ السّلطان سامحه وفهّمه إنّه هو مش مذنب.", - "hebrew_transliteration": "בַּס אִל-סֻّלטַאן סַאמַחוֹ וּפַהַّמוֹ אִנּוֹ הֻוֶّ מֻש מֻד'נֶבּ.", - "hebrew": "אבל הסולטאן סלח לו והסביר לו שהוא לא אשם.", - "expected_phonemes": "b a s | i l s s u l tˤ aː n | s aː m a ħ o | u f a h h a m o | ʔ i n o | h u w w e | m u ʃ | m u dˤ n e b" - }, - { - "voice_time_seconds": { - "start": 707.4, - "end": 712.2 - }, - "arabic": "علاء الدّين زعل كتير على وضع جيني وعشان هيك حرّره", - "hebrew_transliteration": "עַלַאא א-דִّין זִעֶל כְּתִיר עַלַא וַדֶע גִ'ינִי וּעַשַאן הֵיכּ חַרַّרוֹ", - "hebrew": "אלאדין הצטער מאוד על מצבו של ג'יני ולכן שחרר אותו", - "expected_phonemes": "ʕ a l aː a | ʔ d d iː n | z i ʕ e l | k t iː r | ʕ a l a | w a d e ʕ | dʒ iː n i | u ʕ a ʃ aː n | h eː k | ħ a r r a r o" - }, - { - "voice_time_seconds": { - "start": 712.2, - "end": 715.2 - }, - "arabic": "وقال-له: إنت حرّ يا جيني.", - "hebrew_transliteration": "וּקַאל-לוֹ: אִנתֶ חֻרّ יַא גִ'ינִי.", - "hebrew": "ואמר לו: אתה חופשי, ג'יני.", - "expected_phonemes": "u ʔ aː l l o | ʔ i n t e | ħ u r r | j a | dʒ iː n i" - }, - { - "voice_time_seconds": { - "start": 715.2, - "end": 717.2 - }, - "arabic": "بسّ جيني كان وفي كتير،", - "hebrew_transliteration": "בַּס גִ'ינִי כַּאן וַפִי כְּתִיר,", - "hebrew": "אבל ג'יני היה נאמן מאוד,", - "expected_phonemes": "b a s | dʒ iː n i | k aː n | w a f i | k t iː r" - }, - { - "voice_time_seconds": { - "start": 717.2, - "end": 720.6 - }, - "arabic": "وقال لعلاء الدّين إنّه هو راح يبقى بجمبه", - "hebrew_transliteration": "וּקַאל לַעַלַאא א-דִّין אִנּוֹ הֻוֶّ רַאח יִבְקַא בְּגַ'מבּוֹ", - "hebrew": "ואמר לאלאדין שהוא יישאר לצדו", - "expected_phonemes": "u ʔ aː l | l a ʕ a l aː a | ʔ d d iː n | ʔ i n o | h u w w e | r aː ħ | j i b ʔ a | b dʒ a m b o" - }, - { - "voice_time_seconds": { - "start": 720.6, - "end": 724.4 - }, - "arabic": "مدى الحياة، ويحقّقله كل أمنياته.", - "hebrew_transliteration": "מַדַא אִל-חַיַאה, וִיחַקֶّק-לוֹ כֻּלّ אֻמנִיַאתוֹ.", - "hebrew": "כל החיים, ויגשים לו את כל משאלותיו.", - "expected_phonemes": "m a d a | i l ħ a j aː | w iː ħ a ʔ ʔ e ʔ l o | k u l l | ʔ u m n i j aː t o" - }, - { - "voice_time_seconds": { - "start": 724.4, - "end": 729.2 - }, - "arabic": "وهيك عاشوا علاء الدّين وياسمينا،", - "hebrew_transliteration": "וּהֵיכּ עַאשוּ עַלַאא א-דִّין וּיַאסמִינַא,", - "hebrew": "וכך חיו אלאדין ויסמינה,", - "expected_phonemes": "u h eː k | ʕ aː ʃ u | ʕ a l aː a | ʔ d d iː n | u j aː s m iː n a" - }, - { - "voice_time_seconds": { - "start": 729.2, - "end": 733.0 - }, - "arabic": "بثبات ونبات وخلّفوا ولاد وبنات،", - "hebrew_transliteration": "בְּתַבַּאת וּנַבַּאת וּחַ'לַّפוּ וְלַאד וּבַּנַאת,", - "hebrew": "באושר ועושר, והולידו בנים ובנות,", - "expected_phonemes": "b t a b aː t | u n a b aː t | u x a l l a f u | u l aː d | u b a n aː t" - }, - { - "voice_time_seconds": { - "start": 733.0, - "end": 736.6 - }, - "arabic": "ومعاهم كانوا السّلطان وجيني.", - "hebrew_transliteration": "וּמַעַאהֹם כַּאנוּ אִ-סֻّלטַאן וּגִ'ינִי.", - "hebrew": "ואיתם היו הסולטאן וג'יני.", - "expected_phonemes": "u m a ʕ aː h o m | k aː n u | ʔ i s s u l tˤ aː n | u dʒ iː n i" - }, - { - "voice_time_seconds": { - "start": 736.6, - "end": 740.2 - }, - "arabic": "وتوتا توتا خلصت الحدّوتة.", - "hebrew_transliteration": "וּתוּתַא תוּתַא חִ'לסַת אִל-חַדּוּתֶה.", - "hebrew": "ותותה תותה, נגמר הסיפור.", - "expected_phonemes": "u t uː t a | t uː t a | x i l s a t | i l ħ a d uː t e" - } - ] - }, - { - "file_name": "אצבעות זֵינַבּ (גרסה שניה) * אָסָאבֶּע זֵינַבּ * أصابع زينب * קטע בערבית מדוברת [cQNP77IGm1o].webm", - "youtube_id": "cQNP77IGm1o", - "title_hebrew": "אצבעות זינב (גרסה שניה)", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "أصابع زينب", - "hebrew_transliteration": "אַצַאבֶּע זֵינַבּ", - "hebrew": "אצבעות זינב", - "expected_phonemes": "ʔ a sˤ aː b e ʕ | z eː n a b" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 8.4 - }, - "arabic": "مين منّا ما أكلش", - "hebrew_transliteration": "מִין מִנַّא מַא אַכַּלֶש", - "hebrew": "מי מאיתנו לא אכל", - "expected_phonemes": "m iː n | m i n n a | m a | ʔ a k a l e ʃ" - }, - { - "voice_time_seconds": { - "start": 8.4, - "end": 10.2 - }, - "arabic": "أو على القليلة ما سمعش", - "hebrew_transliteration": "אַו עַלַא אִל-קַלִילֶה מַא סִמעֶש", - "hebrew": "או לפחות לא שמע", - "expected_phonemes": "ʔ a w | ʕ a l a | i l ʔ a l iː l e | m a | s i m ʕ e ʃ" - }, - { - "voice_time_seconds": { - "start": 10.2, - "end": 12.4 - }, - "arabic": "عن أصابع زينب؟", - "hebrew_transliteration": "עַן אַצַאבֶּע זֵינַבּ?", - "hebrew": "על אצבעות זינב?", - "expected_phonemes": "ʕ a n | ʔ a sˤ aː b e ʕ | z eː n a b" - }, - { - "voice_time_seconds": { - "start": 12.4, - "end": 17.4 - }, - "arabic": "نوع حلو شرقي، مقلي، معمول من سميد وطحين،", - "hebrew_transliteration": "נוֹע חִלוּ שַרקִי, מַקלִי, מַעמוּל מִן סְמִיד וּטְחִין,", - "hebrew": "סוג של מתוק מזרחי, מטוגן, עשוי מסולת וקמח,", - "expected_phonemes": "n oː ʕ | ħ i l u | ʃ a r ʔ i | m a ʔ l i | m a ʕ m uː l | m i n | s m iː d | u tˤ ħ iː n" - }, - { - "voice_time_seconds": { - "start": 17.4, - "end": 19.4 - }, - "arabic": "ومحلّى بقطر.", - "hebrew_transliteration": "וּמְחַלַّא בְּקַטֶר.", - "hebrew": "וממותק בסירופ.", - "expected_phonemes": "u m ħ a l l a | b ʔ a tˤ e r" - }, - { - "voice_time_seconds": { - "start": 19.4, - "end": 23.0 - }, - "arabic": "طعمه بيشهّي وريحته أشهى.", - "hebrew_transliteration": "טַעמוֹ בִּישַהִّי וּרִיחתוֹ אַשהַא.", - "hebrew": "טעמו מגרה וריחו מגרה עוד יותר.", - "expected_phonemes": "tˤ a ʕ m o | b iː ʃ a h h i | u r iː ħ t o | ʔ a ʃ h a" - }, - { - "voice_time_seconds": { - "start": 23.0, - "end": 24.8 - }, - "arabic": "وبمصر تحديدا،", - "hebrew_transliteration": "וּבְּמַצֶר תַחדִידַן,", - "hebrew": "ובמצרים במיוחד,", - "expected_phonemes": "u b m a sˤ e r | t a ħ d iː d a n" - }, - { - "voice_time_seconds": { - "start": 24.8, - "end": 29.2 - }, - "arabic": "يعتبر من الحلويّات الشّعبيّة المشهورة كتير.", - "hebrew_transliteration": "יֻעתַבַּר מִן אִל-חַלַוִיַّאת אִל-שַّעבִּיֶّה אִל-מַשהוּרַה כְּתִיר.", - "hebrew": "הוא נחשב לאחד הממתקים העממיים המפורסמים מאוד.", - "expected_phonemes": "j u ʕ t a b a r | m i n | i l ħ a l a w i j j aː t | i l ʃ ʃ a ʕ b i j j e | i l m a ʃ h uː r a | k t iː r" - }, - { - "voice_time_seconds": { - "start": 29.2, - "end": 34.0 - }, - "arabic": "بسّ، هل بشي مرّة سمعتوا عن قصّة زينب،", - "hebrew_transliteration": "בַּס, הַל בִּשִי מַרַّה סִמעִתוּ עַן קִצֶّת זֵינַבּ,", - "hebrew": "אבל, האם שמעתם פעם על הסיפור של זינב,", - "expected_phonemes": "b a s | h a l | b i ʃ i | m a r r a | s i m ʕ i t u | ʕ a n | ʔ i sˤ sˤ e t | z eː n a b" - }, - { - "voice_time_seconds": { - "start": 34.0, - "end": 38.4 - }, - "arabic": "إلّي الحلو مسمّى على أسمها؟", - "hebrew_transliteration": "אִלִّי אִל-חִלוּ מְסַמַّא עַלַא אִסמְהַא?", - "hebrew": "שהממתק נקרא על שמה?", - "expected_phonemes": "ʔ i l l i | i l ħ i l u | m s a m m a | ʕ a l a | ʔ i s m h a" - }, - { - "voice_time_seconds": { - "start": 38.4, - "end": 40.8 - }, - "arabic": "على قدّ ما الحلو هاد زاكي،", - "hebrew_transliteration": "עַלַא קַדّ מַא אִל-חִלוּ הַאד זַאכִּי,", - "hebrew": "עד כמה שהממתק הזה טעים,", - "expected_phonemes": "ʕ a l a | ʔ a d d | m a | i l ħ i l u | h aː d | z aː k i" - }, - { - "voice_time_seconds": { - "start": 40.8, - "end": 44.2 - }, - "arabic": "إلّا إنّه القصّة من وراه حزينة.", - "hebrew_transliteration": "אִלַّא אִנּוֹ אִל-קִצַّה מִן וַרַאה חַזִינֶה.", - "hebrew": "הסיפור שמאחוריו עצוב.", - "expected_phonemes": "ʔ i l l a | ʔ i n o | i l ʔ i sˤ sˤ a | m i n | w a r aː h | ħ a z iː n e" - }, - { - "voice_time_seconds": { - "start": 44.2, - "end": 47.0 - }, - "arabic": "بيقولوا إنّه زينب المقصودة،", - "hebrew_transliteration": "בִּיקוּלוּ אִנּוֹ זֵינַבּ אִל-מַקצוּדֶה,", - "hebrew": "אומרים שזינב שמדובר בה,", - "expected_phonemes": "b iː ʔ uː l u | ʔ i n o | z eː n a b | i l m a ʔ sˤ uː d e" - }, - { - "voice_time_seconds": { - "start": 47.0, - "end": 50.6 - }, - "arabic": "كانت زينب بنت علي إبن أبي طالب", - "hebrew_transliteration": "כַּאנַת זֵינַבּ בִּנת עַלִי אִבְּן אַבִּי טַאלֶבּ", - "hebrew": "הייתה זינב בת עלי בן אבי טאלב", - "expected_phonemes": "k aː n a t | z eː n a b | b i n t | ʕ a l i | ʔ i b n | ʔ a b i | tˤ aː l e b" - }, - { - "voice_time_seconds": { - "start": 50.6, - "end": 53.6 - }, - "arabic": "وفاطمة الزّهراء بنت النّبي محمّد", - "hebrew_transliteration": "וּפַאטְמֶה אִ-זַّהרַאא בִּנת אִל-נַّבִּי מֻחַמַّד", - "hebrew": "ופאטמה א-זהראא בת הנביא מוחמד", - "expected_phonemes": "u f aː tˤ m e | ʔ i z z a h r aː a | b i n t | i l n n a b i | m u ħ a m m a d" - }, - { - "voice_time_seconds": { - "start": 53.6, - "end": 56.6 - }, - "arabic": "صلّى الله عليه وسلّم.", - "hebrew_transliteration": "צַלַّא אַללַה עַלֵיה וַסַלַّם.", - "hebrew": "עליו השלום.", - "expected_phonemes": "sˤ a l l a | i l l a | ʕ a l eː h | w a s a l l a m" - }, - { - "voice_time_seconds": { - "start": 56.6, - "end": 60.0 - }, - "arabic": "لزينب كان فيه دور كبير وأهمّيّة كبيرة", - "hebrew_transliteration": "לַזֵינַבּ כַּאן פִיה דוֹר כְּבִּיר וּאַהַמִّיֶּה כְּבִּירֶה", - "hebrew": "לזינב היה תפקיד גדול וחשיבות רבה", - "expected_phonemes": "l a z eː n a b | k aː n | f iː h | d oː r | k b iː r | u ʔ a h a m m i j e | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 60.0, - "end": 61.8 - }, - "arabic": "في معركة كربلاء", - "hebrew_transliteration": "פִי מַערַכֶּת כַּרבַּלַאא", - "hebrew": "בקרב כרבלאא", - "expected_phonemes": "f i | m a ʕ r a k e t | k a r b a l aː a" - }, - { - "voice_time_seconds": { - "start": 61.8, - "end": 64.8 - }, - "arabic": "إلّي طلعت عليها وشاركت فيها", - "hebrew_transliteration": "אִלִّי טִלעַת עַלֵיהַא וּשַארַכַּת פִיהַא", - "hebrew": "שיצאה אליו והשתתפה בו", - "expected_phonemes": "ʔ i l l i | tˤ i l ʕ a t | ʕ a l eː h a | u ʃ aː r a k a t | f iː h a" - }, - { - "voice_time_seconds": { - "start": 64.8, - "end": 67.2 - }, - "arabic": "مع أخوها الحسين بن علي،", - "hebrew_transliteration": "מַע אַחוּהַא אִל-חֻסֵין בִּן עַלִי,", - "hebrew": "עם אחיה אל-חוסיין בן עלי,", - "expected_phonemes": "m a ʕ | ʔ a ħ uː h a | i l ħ u s eː n | b i n | ʕ a l i" - }, - { - "voice_time_seconds": { - "start": 67.2, - "end": 70.0 - }, - "arabic": "إلّي خلال هاي المعركة الطّاحنة", - "hebrew_transliteration": "אִלִّי חִ'לַאל הַאי אִל-מַערַכֶּה אִ-טַّאחנֶה", - "hebrew": "שבמהלך הקרב העז הזה", - "expected_phonemes": "ʔ i l l i | x i l aː l | h aː i | i l m a ʕ r a k e | ʔ i tˤ tˤ aː ħ n e" - }, - { - "voice_time_seconds": { - "start": 70.0, - "end": 74.0 - }, - "arabic": "سقط في إيد العدو وإنقتل.", - "hebrew_transliteration": "סַקַט פִי אִיד אִל-עַדוּ וְאִנקַתַל.", - "hebrew": "נפל בידי האויב ונהרג.", - "expected_phonemes": "s a ʔ a tˤ | f i | ʔ iː d | i l ʕ a d u | u ʔ i n ʔ a t a l" - }, - { - "voice_time_seconds": { - "start": 74.0, - "end": 78.2 - }, - "arabic": "لمّا شافت زينب بنت علي أخوها الحسين مقتول،", - "hebrew_transliteration": "לַמַّא שַאפַת זֵינַבּ בִּנת עַלִי אַחוּהַא אִל-חֻסֵין מַקתוּל,", - "hebrew": "כשזינב בת עלי ראתה את אחיה אל-חוסיין הרוג,", - "expected_phonemes": "l a m m a | ʃ aː f a t | z eː n a b | b i n t | ʕ a l i | ʔ a ħ uː h a | i l ħ u s eː n | m a ʔ t uː l" - }, - { - "voice_time_seconds": { - "start": 78.2, - "end": 80.8 - }, - "arabic": "إنصدمت وإنفعلت كتير،", - "hebrew_transliteration": "אִנצַדַמַת וּאִנפַעַלַת כְּתִיר,", - "hebrew": "היא הזדעזעה ונסערה מאוד,", - "expected_phonemes": "ʔ i n sˤ a d a m a t | u ʔ i n f a ʕ a l a t | k t iː r" - }, - { - "voice_time_seconds": { - "start": 80.8, - "end": 84.0 - }, - "arabic": "وبقيت ضامّمته وحاطّة إيديها عليه،", - "hebrew_transliteration": "וּבְּקִית דַ'אמְמִתוֹ וּחַאטַّה אִידֵיהַא עַלֵיה,", - "hebrew": "ונשארה מחבקת אותו ומניחה עליו את ידיה,", - "expected_phonemes": "u b ʔ iː t | dˤ aː m m i t o | u ħ aː tˤ tˤ a | ʔ iː d eː h a | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 84.0, - "end": 86.2 - }, - "arabic": "وهيّ عم تبكي.", - "hebrew_transliteration": "וּהִיֶّ עַם-תִבכִּי.", - "hebrew": "והיא בוכה.", - "expected_phonemes": "u h i j j e | ʕ a m t i b k i" - }, - { - "voice_time_seconds": { - "start": 86.2, - "end": 88.6 - }, - "arabic": "لمّا أجوا جنود العدو،", - "hebrew_transliteration": "לַמַّא אַגוּ גְ'נוּד אִל-עַדוּ,", - "hebrew": "כשבאו חיילי האויב,", - "expected_phonemes": "l a m m a | ʔ a g u | dʒ n uː d | i l ʕ a d u" - }, - { - "voice_time_seconds": { - "start": 88.6, - "end": 91.6 - }, - "arabic": "يعني جنود عبيد الله بن زياد،", - "hebrew_transliteration": "יַעְנִי גְ'נוּד עֻבֵּיד אַללַה בִּן זִיַאד,", - "hebrew": "כלומר חיילי עוביד אללה בן זיאד,", - "expected_phonemes": "j a ʕ n i | dʒ n uː d | ʕ u b eː d | i l l a | b i n | z i j aː d" - }, - { - "voice_time_seconds": { - "start": 91.6, - "end": 93.8 - }, - "arabic": "يوخدوا جثّة الحسين،", - "hebrew_transliteration": "יוֹחְ'דוּ גֻ'תֶّת אִל-חֻסֵין,", - "hebrew": "לקחת את גופת אל-חוסיין,", - "expected_phonemes": "j oː x d u | dʒ u t t e t | i l ħ u s eː n" - }, - { - "voice_time_seconds": { - "start": 93.8, - "end": 96.8 - }, - "arabic": "ما قدروا يفرّقوا زينب عن الحسين.", - "hebrew_transliteration": "מַא קִדְרוּ יפַרְקוּ זֵינַבּ עַן אִל-חֻסֵין.", - "hebrew": "לא הצליחו להפריד את זינב מאל-חוסיין.", - "expected_phonemes": "m a | ʔ i d r u | j f a r ʔ u | z eː n a b | ʕ a n | i l ħ u s eː n" - }, - { - "voice_time_seconds": { - "start": 96.8, - "end": 101.0 - }, - "arabic": "وما كان في قدّامهم طريقة لتفريقها عنّه،", - "hebrew_transliteration": "וּמַא כַּאן פִי קֻדַّאמְהֹם טַרִיקַה לַתַפרִיקְהַא עַנּוֹ,", - "hebrew": "ולא הייתה לפניהם דרך להפריד אותה ממנו,", - "expected_phonemes": "u m a | k aː n | f i | ʔ u d d aː m h o m | tˤ a r iː ʔ a | l a t a f r iː ʔ h a | ʕ a n o" - }, - { - "voice_time_seconds": { - "start": 101.0, - "end": 103.2 - }, - "arabic": "غير إنّه يقطعوا أصابعها", - "hebrew_transliteration": "עֵ'יר אִנּוֹ יִקטַעוּ אַצַאבְּעְהַא", - "hebrew": "מלבד לקצוץ את אצבעותיה", - "expected_phonemes": "ɣ eː r | ʔ i n o | j i ʔ tˤ a ʕ u | ʔ a sˤ aː b ʕ h a" - }, - { - "voice_time_seconds": { - "start": 103.2, - "end": 106.2 - }, - "arabic": "إلّي كانت معلّقة في أخوها.", - "hebrew_transliteration": "אִלִّי כַּאנַת מְעַלַّקַה פִי אַחוּהַא.", - "hebrew": "שהיו נאחזות באחיה.", - "expected_phonemes": "ʔ i l l i | k aː n a t | m ʕ a l l a ʔ a | f i | ʔ a ħ uː h a" - }, - { - "voice_time_seconds": { - "start": 106.2, - "end": 110.6 - }, - "arabic": "وهيك وبسبب حبّ المصريّين لزينب بنت علي،", - "hebrew_transliteration": "וּהֵיכּ וּבִּסַבַּבּ חֻבּ אִל-מַצרִיִّין לַזֵינַבּ בִּנת עַלִי,", - "hebrew": "וכך, בגלל אהבת המצרים לזינב בת עלי,", - "expected_phonemes": "u h eː k | u b i s a b a b | ħ u b | i l m a sˤ r i j j iː n | l a z eː n a b | b i n t | ʕ a l i" - }, - { - "voice_time_seconds": { - "start": 110.6, - "end": 112.8 - }, - "arabic": "سمّوا الحلو على أسمها.", - "hebrew_transliteration": "סַמַّוּ אִל-חִלוּ עַלַא אִסמְהַא.", - "hebrew": "קראו לממתק על שמה.", - "expected_phonemes": "s a m m a w | i l ħ i l u | ʕ a l a | ʔ i s m h a" - } - ] - }, - { - "file_name": "בין חאנא ומאנא אבד זקננו * בֵּין חַאנַא וּמַאנַא דָאעַת לִחיַאנַא * بين حانا ومانا ضاعت لحيانا [fA2Ta1saWaA].mkv", - "youtube_id": "fA2Ta1saWaA", - "title_hebrew": "בין חאנא ומאנא", - "segments": [ - { - "voice_time_seconds": { - "start": 1.4, - "end": 5.6 - }, - "arabic": "بين حانا ومانا ضاعت لحيانا", - "hebrew_transliteration": "בֵּין חַאנַא וּמַאנַא דַ'אעַת לִחיַאנַא", - "hebrew": "בין חאנא ומאנא אבד זקננו", - "expected_phonemes": "b eː n | ħ aː n a | u m aː n a | dˤ aː ʕ a t | l i ħ j aː n a" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 9.6 - }, - "arabic": "كان يا ما كان، في قديم الزّمان،", - "hebrew_transliteration": "כַּאן יַא מַא כַּאן, פִי קַדִים אִ-זַّמַאן,", - "hebrew": "היה היה, בימים עברו,", - "expected_phonemes": "k aː n | j a | m a | k aː n | f i | ʔ a d iː m | ʔ i z z a m aː n" - }, - { - "voice_time_seconds": { - "start": 9.6, - "end": 11.8 - }, - "arabic": "زلمة في نصّ حياته،", - "hebrew_transliteration": "זַלַמֶה פִי נֻצّ חַיַאתוֹ,", - "hebrew": "איש באמצע חייו,", - "expected_phonemes": "z a l a m e | f i | n u sˤ sˤ | ħ a j aː t o" - }, - { - "voice_time_seconds": { - "start": 11.8, - "end": 16.4 - }, - "arabic": "يعني، عمره تقريبا شي خمسين سنة.", - "hebrew_transliteration": "יַעְנִי, עֻמרוֹ תַקרִיבַּן שִי חַ'מסִין סַנֶה.", - "hebrew": "כלומר, בן חמישים בערך.", - "expected_phonemes": "j a ʕ n i | ʕ u m r o | t a ʔ r iː b a n | ʃ i | x a m s iː n | s a n e" - }, - { - "voice_time_seconds": { - "start": 16.4, - "end": 19.4 - }, - "arabic": "هالزّلمة كان متجوّز تنتين", - "hebrew_transliteration": "הַ-זַّלַמֶה כַּאן מִתגַ'וֶّז תִנתֵין", - "hebrew": "האיש הזה היה נשוי לשתיים", - "expected_phonemes": "h a z z a l a m e | k aː n | m i t dʒ a w w e z | t i n t eː n" - }, - { - "voice_time_seconds": { - "start": 19.4, - "end": 22.8 - }, - "arabic": "وعايش معاهم في بيت واحد.", - "hebrew_transliteration": "וּעַאיֶש מַעַאהֹם פִי בֵּית וַאחַד.", - "hebrew": "וגר איתן בבית אחד.", - "expected_phonemes": "u ʕ aː j e ʃ | m a ʕ aː h o m | f i | b eː t | w aː ħ a d" - }, - { - "voice_time_seconds": { - "start": 22.8, - "end": 26.4 - }, - "arabic": "مرته الأولى إلّي هيّ المرة الكبيرة،", - "hebrew_transliteration": "מַרְתוֹ אִל-אוּלַה אִלִّי הִיֶّ אִל-מַרַה לֶ-כְּבִּירֶה,", - "hebrew": "אשתו הראשונה, שהיא האישה המבוגרת,", - "expected_phonemes": "m a r t o | i l aː uː l a | ʔ i l l i | h i j j e | i l m a r a | l e k b iː r e" - }, - { - "voice_time_seconds": { - "start": 26.4, - "end": 29.0 - }, - "arabic": "كانت بجياله لجوزها،", - "hebrew_transliteration": "כַּאנַת בְּגִ'יַאלוֹ לַגוֹזְהַא,", - "hebrew": "הייתה בגילו של בעלה,", - "expected_phonemes": "k aː n a t | b dʒ i j aː l o | l a g oː z h a" - }, - { - "voice_time_seconds": { - "start": 29.0, - "end": 31.8 - }, - "arabic": "وكان أسمها حانا.", - "hebrew_transliteration": "וּכַּאן אִסמְהַא חַאנַא.", - "hebrew": "ושמה היה חאנא.", - "expected_phonemes": "u k aː n | ʔ i s m h a | ħ aː n a" - }, - { - "voice_time_seconds": { - "start": 31.8, - "end": 35.2 - }, - "arabic": "أمّا مرته التّانية كانت صبيّة صغيرة", - "hebrew_transliteration": "אַמַّא מַרְתוֹ אִ-תַّאנְיֶה כַּאנַת צַבִּיֶّה זְעִ'ירֶה", - "hebrew": "ואילו אשתו השנייה הייתה צעירה", - "expected_phonemes": "ʔ a m m a | m a r t o | ʔ i t t aː n j e | k aː n a t | sˤ a b i j j e | z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 35.2, - "end": 37.8 - }, - "arabic": "وكان أسمها مانا.", - "hebrew_transliteration": "וּכַּאן אִסמְהַא מַאנַא.", - "hebrew": "ושמה היה מאנא.", - "expected_phonemes": "u k aː n | ʔ i s m h a | m aː n a" - }, - { - "voice_time_seconds": { - "start": 37.8, - "end": 40.2 - }, - "arabic": "مانا، المرة الصغيرة،", - "hebrew_transliteration": "מַאנַא, אִל-מַרַה לֶ-זְעִ'ירֶה,", - "hebrew": "מאנא, האישה הצעירה,", - "expected_phonemes": "m aː n a | i l m a r a | l e z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 40.2, - "end": 43.4 - }, - "arabic": "ما كانتش تحبّ تشوف علامات الكبر", - "hebrew_transliteration": "מַא כַּאנַת-ש תְחֶבّ תְשוּף עַלַאמַאת אִל-כִּבַּר", - "hebrew": "לא אהבה לראות סימני זקנה", - "expected_phonemes": "m a | k aː n a t ʃ | t ħ e b b | t ʃ uː f | ʕ a l aː m aː t | i l k i b a r" - }, - { - "voice_time_seconds": { - "start": 43.4, - "end": 46.0 - }, - "arabic": "إلّي بلّشت تبيّن على جوزها،", - "hebrew_transliteration": "אִלִّי בַּלַّשַת תְבַּיֶّן עַלַא גוֹזְהַא,", - "hebrew": "שהתחילו להופיע על בעלה,", - "expected_phonemes": "ʔ i l l i | b a l l a ʃ a t | t b a j j e n | ʕ a l a | g oː z h a" - }, - { - "voice_time_seconds": { - "start": 46.0, - "end": 50.2 - }, - "arabic": "وعشان هيك صارت كلّ ما يطلعله شعر على دقنه،", - "hebrew_transliteration": "וּעַשַאן הֵיכּ צַארַת כֻּלّ מַא יִטלַע-לוֹ שַעַר עַלַא דַקְנוֹ,", - "hebrew": "ולכן, בכל פעם שצמחה לו שערה על זקנו,", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | sˤ aː r a t | k u l l | m a | j i tˤ l a ʕ l o | ʃ a ʕ a r | ʕ a l a | d a ʔ n o" - }, - { - "voice_time_seconds": { - "start": 50.2, - "end": 54.2 - }, - "arabic": "تجيب هلملقط وتنتّفله الشّعرات البيض.", - "hebrew_transliteration": "תְגִ'יבּ הַל-מַלקַט וּתנַתֶّפ-לוֹ אִ-שַّעְרַאת אִל-בִּיד'.", - "hebrew": "היא הביאה את הפינצטה ומרטה לו את השערות הלבנות.", - "expected_phonemes": "t dʒ iː b | h a l m a l ʔ a tˤ | u t n a t t e f l o | ʔ i ʃ ʃ a ʕ r aː t | i l b iː dˤ" - }, - { - "voice_time_seconds": { - "start": 54.2, - "end": 57.8 - }, - "arabic": "لمّا انتبهت حانا لهالموضوع خافت،", - "hebrew_transliteration": "לַמַّא נִתבְּהַת חַאנַא לַ-הַל-מַוְדוּע חַ'אפַת,", - "hebrew": "כשחאנא שמה לב לעניין הזה, היא פחדה,", - "expected_phonemes": "l a m m a | n i t b h a t | ħ aː n a | l a h a l m a w d uː ʕ | x aː f a t" - }, - { - "voice_time_seconds": { - "start": 57.8, - "end": 60.6 - }, - "arabic": "لإنّها فهمت إنّه عمل مانا هادا", - "hebrew_transliteration": "לַאִנְّהַא פִהמַת אִנּוֹ עַמַל מַאנַא הַאדַא", - "hebrew": "כי הבינה שהמעשה הזה של מאנא", - "expected_phonemes": "l a ʔ i n n h a | f i h m a t | ʔ i n o | ʕ a m a l | m aː n a | h aː d a" - }, - { - "voice_time_seconds": { - "start": 60.6, - "end": 62.8 - }, - "arabic": "ممكن يخرّب بيتها", - "hebrew_transliteration": "מֻמכֶּן יְחַ'רֶّבּ בֵּיתְהַא", - "hebrew": "עלול להרוס את ביתה", - "expected_phonemes": "m u m k e n | j x a r r e b | b eː t h a" - }, - { - "voice_time_seconds": { - "start": 62.8, - "end": 65.2 - }, - "arabic": "ويهدّد حياتها مع جوزها،", - "hebrew_transliteration": "וִיהַדֶّד חַיַאתְהַא מַע גוֹזְהַא,", - "hebrew": "ולאיים על חייה עם בעלה,", - "expected_phonemes": "w iː h a d d e d | ħ a j aː t h a | m a ʕ | g oː z h a" - }, - { - "voice_time_seconds": { - "start": 65.2, - "end": 69.2 - }, - "arabic": "وهاد لإنّه هوّ رح يبقى مبيّن متل الشّباب،", - "hebrew_transliteration": "וּהַאד לַאִנּוֹ הֻוֶّ רַח-יִבְקַא מְבַּיֶّן מִתל אִ-שַّבַּאבּ,", - "hebrew": "וזה מפני שהוא ייראה צעיר,", - "expected_phonemes": "u h aː d | l a ʔ i n o | h u w w e | r a ħ j i b ʔ a | m b a j j e n | m i t l | ʔ i ʃ ʃ a b aː b" - }, - { - "voice_time_seconds": { - "start": 69.2, - "end": 73.2 - }, - "arabic": "وهيّ عم بتكبر وشكلها عم بتغيّر.", - "hebrew_transliteration": "וּהִיֶّ עַם-בְּתִכבַּר וּשַכְּלְהַא עַם-בִּתעַ'יַّר.", - "hebrew": "והיא מזדקנת ומראה משתנה.", - "expected_phonemes": "u h i j j e | ʕ a m b t i k b a r | u ʃ a k l h a | ʕ a m b i t ɣ a j j a r" - }, - { - "voice_time_seconds": { - "start": 73.2, - "end": 77.6 - }, - "arabic": "عشان هيك صارت هيّ كمان تنتّف شعرات دقن جوزها.", - "hebrew_transliteration": "עַשַאן הֵיכּ צַארַת הִיֶّ כַּמַאן תְנַתֶّף שַעְרַאת דַקֶן גוֹזְהַא.", - "hebrew": "לכן גם היא התחילה למרוט שערות מזקנו של בעלה.", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | sˤ aː r a t | h i j j e | k a m aː n | t n a t t e f | ʃ a ʕ r aː t | d a ʔ e n | g oː z h a" - }, - { - "voice_time_seconds": { - "start": 77.6, - "end": 81.2 - }, - "arabic": "بسّ هيّ كانت تنتّف الشّعرات السّود.", - "hebrew_transliteration": "בַּס הִיֶّ כַּאנַת תְנַתֶّף אִ-שַּׁעְרַאת אִ-סּוּד.", - "hebrew": "אבל היא מרטה את השערות השחורות.", - "expected_phonemes": "b a s | h i j j e | k aː n a t | t n a t t e f | ʔ i ʃ a ʕ r aː t | ʔ i s uː d" - }, - { - "voice_time_seconds": { - "start": 81.2, - "end": 83.0 - }, - "arabic": "مرقوا أكمّ أسبوع", - "hebrew_transliteration": "מַרַקוּ אַכַּם אֻסבּוּע", - "hebrew": "עברו כמה שבועות", - "expected_phonemes": "m a r a ʔ u | ʔ a k a m | ʔ u s b uː ʕ" - }, - { - "voice_time_seconds": { - "start": 83.0, - "end": 86.6 - }, - "arabic": "والتّنتين مكمّلات على نفس الحالة،", - "hebrew_transliteration": "וּאִ-תִּנתֵין מְכַּמְّלַאת עַלַא נַפֶס אִל-חַאלֶה,", - "hebrew": "ושתיהן המשיכו באותו מצב,", - "expected_phonemes": "u ʔ i t i n t eː n | m k a m m l aː t | ʕ a l a | n a f e s | i l ħ aː l e" - }, - { - "voice_time_seconds": { - "start": 86.6, - "end": 88.6 - }, - "arabic": "واحدة تنتّف الشّعر الأسود،", - "hebrew_transliteration": "וַאחדֶה תְנַתֶّף אִ-שַּׁעַר אִל-אַסוַד,", - "hebrew": "אחת מורטת את השיער השחור,", - "expected_phonemes": "w aː ħ d e | t n a t t e f | ʔ i ʃ a ʕ a r | i l ʔ a s w a d" - }, - { - "voice_time_seconds": { - "start": 88.6, - "end": 90.8 - }, - "arabic": "وواحدة تنتّف الشّعر الأبيض،", - "hebrew_transliteration": "וּוַאחדֶה תְנַתֶّף אִ-שַּׁעַר אִל-אַבְּיַד',", - "hebrew": "ואחת מורטת את השיער הלבן,", - "expected_phonemes": "u w aː ħ d e | t n a t t e f | ʔ i ʃ a ʕ a r | i l ʔ a b j a dˤ" - }, - { - "voice_time_seconds": { - "start": 90.8, - "end": 95.4 - }, - "arabic": "لحدّ ما بطّل فيه دقن بالمرّة عند هالزّلمة.", - "hebrew_transliteration": "לַחַדّ מַא בַּטַّל פִיה דַקֶן בִּ-לְ-מַרַّה עִנד הַ-זַّלַמֶה.", - "hebrew": "עד שלא נשאר לאיש הזה זקן בכלל.", - "expected_phonemes": "l a ħ a d d | m a | b a tˤ tˤ a l | f iː h | d a ʔ e n | b i l m a r r a | ʕ i n d | h a z z a l a m e" - }, - { - "voice_time_seconds": { - "start": 95.4, - "end": 96.4 - }, - "arabic": "مسكين،", - "hebrew_transliteration": "מַסכִּין,", - "hebrew": "מסכן,", - "expected_phonemes": "m a s k iː n" - }, - { - "voice_time_seconds": { - "start": 96.4, - "end": 100.8 - }, - "arabic": "نتّفوله كلّ شعرات دقنوا وبطّل يطلعله شعر.", - "hebrew_transliteration": "נַתַّפוּלוֹ כֻּלّ שַעְרַאת דַקְנוֹ וּבַּטַّל יִטלַע-לוֹ שַעַר.", - "hebrew": "מרטו לו את כל שערות זקנו והפסיק לצמוח לו שיער.", - "expected_phonemes": "n a t t a f uː l o | k u l l | ʃ a ʕ r aː t | d a ʔ n o | u b a tˤ tˤ a l | j i tˤ l a ʕ l o | ʃ a ʕ a r" - }, - { - "voice_time_seconds": { - "start": 100.8, - "end": 102.4 - }, - "arabic": "وعلى قولة المتل:", - "hebrew_transliteration": "וּעַלַא קוֹלֶת אִל-מַתַל:", - "hebrew": "וכמו שאומר הפתגם:", - "expected_phonemes": "u ʕ a l a | ʔ oː l e t | i l m a t a l" - }, - { - "voice_time_seconds": { - "start": 102.4, - "end": 107.6 - }, - "arabic": "بين حانا ومانا ضاعت لحيانا.", - "hebrew_transliteration": "בֵּין חַאנַא וּמַאנַא דַ'אעַת לִחיַאנַא.", - "hebrew": "בין חאנא ומאנא אבד זקננו.", - "expected_phonemes": "b eː n | ħ aː n a | u m aː n a | dˤ aː ʕ a t | l i ħ j aː n a" - } - ] - }, - { - "file_name": "אצבעות זֵינַבּ (גרסה ראשונה) * אָסָאבֶּע זֵינַבּ * أصابع زينب * סיפור בערבית מדוברת [G-C00-9KKlE].webm", - "youtube_id": "G-C00-9KKlE", - "title_hebrew": "אצבעות זינב (גרסה ראשונה)", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "أصابع زينب", - "hebrew_transliteration": "אַצַאבֶּע זֵינַבּ", - "hebrew": "אצבעות זינב", - "expected_phonemes": "ʔ a sˤ aː b e ʕ | z eː n a b" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 9.2 - }, - "arabic": "فيه كتير قصص بتحكي عن أصل", - "hebrew_transliteration": "פִיה כְּתִיר קִצַץ בְּתִחכִּי עַן אַצֶל", - "hebrew": "יש הרבה סיפורים שמספרים על מקור", - "expected_phonemes": "f iː h | k t iː r | ʔ i sˤ a sˤ | b t i ħ k i | ʕ a n | ʔ a sˤ e l" - }, - { - "voice_time_seconds": { - "start": 9.2, - "end": 11.6 - }, - "arabic": "تسمية الحلو الشّرقي الزّاكي", - "hebrew_transliteration": "תַסמִיֶת אִל-חִלוּ אִל-שַّרקִי אִ-זַّאכִּי", - "hebrew": "השם של הממתק המזרחי הטעים", - "expected_phonemes": "t a s m i j e t | i l ħ i l u | i l ʃ ʃ a r ʔ i | ʔ i z z aː k i" - }, - { - "voice_time_seconds": { - "start": 11.6, - "end": 14.0 - }, - "arabic": "إلّي أسمه أصابع زينب.", - "hebrew_transliteration": "אִלִّי אִסמוֹ אַצַאבֶּע זֵינַבּ.", - "hebrew": "ששמו אצבעות זינב.", - "expected_phonemes": "ʔ i l l i | ʔ i s m o | ʔ a sˤ aː b e ʕ | z eː n a b" - }, - { - "voice_time_seconds": { - "start": 14.0, - "end": 17.4 - }, - "arabic": "إلّي بيدوّر، بيلاقي قصص متنوّعة،", - "hebrew_transliteration": "אִלִّי בִּידַוֶّר, בִּילַאקִי קִצַץ מִתנַוְּעַה,", - "hebrew": "מי שמחפש, מוצא סיפורים מגוונים,", - "expected_phonemes": "ʔ i l l i | b iː d a w w e r | b iː l aː ʔ i | ʔ i sˤ a sˤ | m i t n a w ʕ a" - }, - { - "voice_time_seconds": { - "start": 17.4, - "end": 19.0 - }, - "arabic": "منها قصّة رومانسيّة", - "hebrew_transliteration": "מִנְהַא קִצַّה רוֹמַנסִיֶּה", - "hebrew": "ביניהם סיפור רומנטי", - "expected_phonemes": "m i n h a | ʔ i sˤ sˤ a | r oː m a n s i j e" - }, - { - "voice_time_seconds": { - "start": 19.0, - "end": 21.8 - }, - "arabic": "إلّي راح نحكي عنها اليوم.", - "hebrew_transliteration": "אִלִّי רַח נִחכִּי עַנְהַא אִל-יוֹם.", - "hebrew": "שעליו נספר היום.", - "expected_phonemes": "ʔ i l l i | r a ħ | n i ħ k i | ʕ a n h a | i l j oː m" - }, - { - "voice_time_seconds": { - "start": 21.8, - "end": 25.4 - }, - "arabic": "بيقولوا إنّه في سنة ألف وميتين وستّين،", - "hebrew_transliteration": "בִּיקוּלוּ אִנּוֹ פִי סַנֶת אַלְף וּמִיתֵין וּסִתִّין,", - "hebrew": "אומרים שבשנת אלף מאתיים ושישים,", - "expected_phonemes": "b iː ʔ uː l u | ʔ i n o | f i | s a n e t | ʔ a l f | u m iː t eː n | u s i t t iː n" - }, - { - "voice_time_seconds": { - "start": 25.4, - "end": 28.6 - }, - "arabic": "لمّا رجع السّلطان الظّاهر بيبرس", - "hebrew_transliteration": "לַמַّא רִגֶ'ע אִ-סֻّלטַאן אִ-טַّאהֶר בֵּיבַּרס", - "hebrew": "כשחזר הסולטאן א-טאהר ביברס", - "expected_phonemes": "l a m m a | r i dʒ e ʕ | ʔ i s s u l tˤ aː n | ʔ i tˤ tˤ aː h e r | b eː b a r s" - }, - { - "voice_time_seconds": { - "start": 28.6, - "end": 30.6 - }, - "arabic": "مرفوع الرّاس بعد ما إنتصر", - "hebrew_transliteration": "מַרפוּעַ אִל-רַّאס בַּעד מַא אִנתַצַר", - "hebrew": "בראש מורם אחרי שניצח", - "expected_phonemes": "m a r f uː ʕ a | i l r r aː s | b a ʕ d | m a | ʔ i n t a sˤ a r" - }, - { - "voice_time_seconds": { - "start": 30.6, - "end": 32.2 - }, - "arabic": "في معركة عين جالوت،", - "hebrew_transliteration": "פִי מַערַכֶּת עֵין גַ'אלוּת,", - "hebrew": "בקרב עין ג'אלות,", - "expected_phonemes": "f i | m a ʕ r a k e t | ʕ eː n | dʒ aː l uː t" - }, - { - "voice_time_seconds": { - "start": 32.2, - "end": 35.0 - }, - "arabic": "إلّي كانت بين المسلمين والمغول،", - "hebrew_transliteration": "אִלִّי כַּאנַת בֵּין אִל-מֻסלִמִין וִל-מַעְ'וּל,", - "hebrew": "שהיה בין המוסלמים למונגולים,", - "expected_phonemes": "ʔ i l l i | k aː n a t | b eː n | i l m u s l i m iː n | w i l m a ɣ uː l" - }, - { - "voice_time_seconds": { - "start": 35.0, - "end": 38.0 - }, - "arabic": "كان فرحان ومبسوط كتير.", - "hebrew_transliteration": "כַּאן פַרחַאן וּמַבְּסוּט כְּתִיר.", - "hebrew": "הוא היה שמח ומרוצה מאוד.", - "expected_phonemes": "k aː n | f a r ħ aː n | u m a b s uː tˤ | k t iː r" - }, - { - "voice_time_seconds": { - "start": 38.0, - "end": 41.6 - }, - "arabic": "من فرحته وفخره طلب الظّاهر بيبرس", - "hebrew_transliteration": "מִן פַרחתוֹ וּפַחֶ'רוֹ טַלַבּ אִ-טַّאהֶר בֵּיבַּרס", - "hebrew": "מרוב שמחה וגאווה ביקש א-טאהר ביברס", - "expected_phonemes": "m i n | f a r ħ t o | u f a x e r o | tˤ a l a b | ʔ i tˤ tˤ aː h e r | b eː b a r s" - }, - { - "voice_time_seconds": { - "start": 41.6, - "end": 43.2 - }, - "arabic": "من طبّاخين القصر", - "hebrew_transliteration": "מִן טַבַּאחִ'ין אִל-קַצֶר", - "hebrew": "מטבחי הארמון", - "expected_phonemes": "m i n | tˤ a b aː x iː n | i l ʔ a sˤ e r" - }, - { - "voice_time_seconds": { - "start": 43.2, - "end": 45.8 - }, - "arabic": "يحضّروا حلو على أنواعه،", - "hebrew_transliteration": "יְחַדְّרוּ חִלוּ עַלַא אַנוַאעוֹ,", - "hebrew": "להכין ממתקים מכל הסוגים,", - "expected_phonemes": "j ħ a d d r u | ħ i l u | ʕ a l a | ʔ a n w aː ʕ o" - }, - { - "voice_time_seconds": { - "start": 45.8, - "end": 47.8 - }, - "arabic": "ويوزّعوه على الشّعب.", - "hebrew_transliteration": "וִיוַזְّעוּה עַלַא אִ-שַّעֶבּ.", - "hebrew": "ולחלק אותם לעם.", - "expected_phonemes": "w iː w a z z ʕ uː h | ʕ a l a | ʔ i ʃ ʃ a ʕ e b" - }, - { - "voice_time_seconds": { - "start": 47.8, - "end": 52.2 - }, - "arabic": "وهيك، بالفعل بلّشوا كلّ طبّاخين المطبخ", - "hebrew_transliteration": "וּהֵיכּ, בִּל-פִעֶל בַּלַّשוּ כֻּלّ טַבַּאחִ'ין אִל-מַטבַּח'", - "hebrew": "וכך, אכן התחילו כל טבחי המטבח", - "expected_phonemes": "u h eː k | b i l f i ʕ e l | b a l l a ʃ u | k u l l | tˤ a b aː x iː n | i l m a tˤ b a x" - }, - { - "voice_time_seconds": { - "start": 52.2, - "end": 57.0 - }, - "arabic": "يعجنوا ويخبزوا ويعملوا كلّ أنواع الحلو.", - "hebrew_transliteration": "יִעגְ'נוּ וִיחְ'בְּזוּ וִיעמְלוּ כֻּלّ אַנוַאע אִל-חִלוּ.", - "hebrew": "ללוש, לאפות ולהכין את כל סוגי הממתקים.", - "expected_phonemes": "j i ʕ dʒ n u | w iː x b z u | w iː ʕ m l u | k u l l | ʔ a n w aː ʕ | i l ħ i l u" - }, - { - "voice_time_seconds": { - "start": 57.0, - "end": 59.4 - }, - "arabic": "في اللّيل على وقت العشا،", - "hebrew_transliteration": "פִי אִל-לֵּיל עַלַא וַקת אִל-עַשַא,", - "hebrew": "בלילה, בשעת ארוחת הערב,", - "expected_phonemes": "f i | i l l eː l | ʕ a l a | w a ʔ t | i l ʕ a ʃ a" - }, - { - "voice_time_seconds": { - "start": 59.4, - "end": 61.6 - }, - "arabic": "بعد ما تعشّى الظّاهر بيبرس،", - "hebrew_transliteration": "בַּעד מַא תְעַשַّא אִ-טַّאהֶר בֵּיבַּרס,", - "hebrew": "אחרי שא-טאהר ביברס אכל ארוחת ערב,", - "expected_phonemes": "b a ʕ d | m a | t ʕ a ʃ ʃ a | ʔ i tˤ tˤ aː h e r | b eː b a r s" - }, - { - "voice_time_seconds": { - "start": 61.6, - "end": 64.0 - }, - "arabic": "هوّ وحاشيته كلّها،", - "hebrew_transliteration": "הֻוֶّ וּחַאשִיתוֹ כֻּלְّהַא,", - "hebrew": "הוא וכל פמלייתו,", - "expected_phonemes": "h u w w e | u ħ aː ʃ iː t o | k u l l h a" - }, - { - "voice_time_seconds": { - "start": 64.0, - "end": 66.2 - }, - "arabic": "طلب يجيبولهم من الحلو", - "hebrew_transliteration": "טַלַבּ יְגִ'יבּוּלְהֹם מִן אִל-חִלוּ", - "hebrew": "ביקש שיביאו להם מהממתקים", - "expected_phonemes": "tˤ a l a b | j dʒ iː b uː l h o m | m i n | i l ħ i l u" - }, - { - "voice_time_seconds": { - "start": 66.2, - "end": 70.2 - }, - "arabic": "إلّي تحضّر على شرف الإنتصار في المعركة.", - "hebrew_transliteration": "אִלִّי תְחַדַّר עַלַא שַרַף אִל-אִנתִצַאר פִי אִל-מַערַכֶּה.", - "hebrew": "שהוכנו לכבוד הניצחון בקרב.", - "expected_phonemes": "ʔ i l l i | t ħ a d d a r | ʕ a l a | ʃ a r a f | i l ʔ i n t i sˤ aː r | f i | i l m a ʕ r a k e" - }, - { - "voice_time_seconds": { - "start": 70.2, - "end": 74.2 - }, - "arabic": "جابوا الطّبّاخين الحلو على أشكاله وألوانه", - "hebrew_transliteration": "גַ'אבּוּ אִ-טַּבַּאחִ'ין אִל-חִלוּ עַלַא אַשכַּאלוֹ וּאַלוַאנוֹ", - "hebrew": "הטבחים הביאו ממתקים מכל הצורות והצבעים", - "expected_phonemes": "dʒ aː b u | ʔ i tˤ a b aː x iː n | i l ħ i l u | ʕ a l a | ʔ a ʃ k aː l o | u i l w aː n o" - }, - { - "voice_time_seconds": { - "start": 74.2, - "end": 77.8 - }, - "arabic": "وبلّشوا يوزّعوا على كلّ الموجودين.", - "hebrew_transliteration": "וּבַּלַّשוּ יוַזְּעוּ עַלַא כֻּלّ אִל-מַוְגּ'וּדִין.", - "hebrew": "והתחילו לחלק לכל הנוכחים.", - "expected_phonemes": "u b a l l a ʃ u | j w a z ʕ u | ʕ a l a | k u l l | i l m a w dʒ uː d iː n" - }, - { - "voice_time_seconds": { - "start": 77.8, - "end": 80.4 - }, - "arabic": "الظّاهر بيبرس إنتبه لحلو", - "hebrew_transliteration": "אִ-טַّאהֶר בֵּיבַּרס אִנתַבַּה לַחִלוּ", - "hebrew": "א-טאהר ביברס שם לב לממתק", - "expected_phonemes": "ʔ i tˤ tˤ aː h e r | b eː b a r s | ʔ i n t a b a | l a ħ i l u" - }, - { - "voice_time_seconds": { - "start": 80.4, - "end": 83.6 - }, - "arabic": "على شكل أصابع لونه ذهبي.", - "hebrew_transliteration": "עַלַא שַכֶּל אַצַאבֶּע, לוֹנוֹ דַ'הַבִּי.", - "hebrew": "בצורת אצבעות, בצבע זהוב.", - "expected_phonemes": "ʕ a l a | ʃ a k e l | ʔ a sˤ aː b e ʕ | l oː n o | dˤ a h a b i" - }, - { - "voice_time_seconds": { - "start": 83.6, - "end": 87.8 - }, - "arabic": "ذاقه وإنبسط كتير من طعمه وشكله،", - "hebrew_transliteration": "דַ'אקוֹ וּאִנבַּסַט כְּתִיר מִן טַעמוֹ וּשַכְּלוֹ,", - "hebrew": "טעם אותו ומאוד נהנה מטעמו וממראהו,", - "expected_phonemes": "dˤ aː ʔ o | u ʔ i n b a s a tˤ | k t iː r | m i n | tˤ a ʕ m o | u ʃ a k l o" - }, - { - "voice_time_seconds": { - "start": 87.8, - "end": 90.4 - }, - "arabic": "فنادى على الطّبّاخ الرّئيسي", - "hebrew_transliteration": "פַנַאדַא עַלַא אִ-טַּבַּאחַ' אִ-רַּאִיסִי", - "hebrew": "אז קרא לטבח הראשי", - "expected_phonemes": "f a n aː d a | ʕ a l a | ʔ i tˤ a b aː x a | ʔ i r a ʔ iː s i" - }, - { - "voice_time_seconds": { - "start": 90.4, - "end": 94.0 - }, - "arabic": "وسأله عن أسم هاد النّوع من الحلو.", - "hebrew_transliteration": "וּסַאַלוֹ עַן אִסֶם הַאד אִל-נַّוְע מִן אִל-חִלוּ.", - "hebrew": "ושאל אותו מה שמו של סוג הממתק הזה.", - "expected_phonemes": "u s a ʔ a l o | ʕ a n | ʔ i s e m | h aː d | i l n n a w ʕ | m i n | i l ħ i l u" - }, - { - "voice_time_seconds": { - "start": 94.0, - "end": 96.8 - }, - "arabic": "الطّبّاخ ما عرفش شو يجاوب", - "hebrew_transliteration": "אִ-טַּבַּאחַ' מַא עִרְפֶש שוּ יְגַ'אוֶבּ", - "hebrew": "הטבח לא ידע מה לענות", - "expected_phonemes": "ʔ i tˤ a b aː x a | m a | ʕ i r f e ʃ | ʃ u | j dʒ aː w e b" - }, - { - "voice_time_seconds": { - "start": 96.8, - "end": 99.4 - }, - "arabic": "لإنّه كان هاد نوع جديد", - "hebrew_transliteration": "לַאִנּוֹ כַּאן הַאד נוֹע גְ'דִיד", - "hebrew": "כי זה היה סוג חדש", - "expected_phonemes": "l a ʔ i n o | k aː n | h aː d | n oː ʕ | dʒ d iː d" - }, - { - "voice_time_seconds": { - "start": 99.4, - "end": 102.2 - }, - "arabic": "أوّل مرّة بينعمل في مطبخه.", - "hebrew_transliteration": "אַוַّל מַרַّה בִּינעַמַל פִי מַטבַּחוֹ.", - "hebrew": "שהוכן לראשונה במטבח שלו.", - "expected_phonemes": "ʔ a w w a l | m a r r a | b iː n ʕ a m a l | f i | m a tˤ b a ħ o" - }, - { - "voice_time_seconds": { - "start": 102.2, - "end": 104.8 - }, - "arabic": "بسّ كان يعرف إنّه إلّي عملته", - "hebrew_transliteration": "בַּס כַּאן יִערַף אִנּוֹ אִלִّי עִמלַתוֹ", - "hebrew": "אבל הוא ידע שמי שהכינה אותו", - "expected_phonemes": "b a s | k aː n | j i ʕ r a f | ʔ i n o | ʔ i l l i | ʕ i m l a t o" - }, - { - "voice_time_seconds": { - "start": 104.8, - "end": 108.4 - }, - "arabic": "هيّ واحدة صبيّة أسمها زينب،", - "hebrew_transliteration": "הִיֶّ וַאחדֶה צַבִּיֶّה אִסמְהַא זֵינַבּ,", - "hebrew": "היא בחורה צעירה בשם זינב,", - "expected_phonemes": "h i j j e | w aː ħ d e | sˤ a b i j j e | ʔ i s m h a | z eː n a b" - }, - { - "voice_time_seconds": { - "start": 108.4, - "end": 112.8 - }, - "arabic": "كانت طبّاخة جديدة جاي تشتغل في القصر.", - "hebrew_transliteration": "כַּאנַת טַבַּאחַ'ה גְ'דִידֶה גַ'אי תִשתְעֶ'ל פִי אִל-קַצֶר.", - "hebrew": "טבחית חדשה שבאה לעבוד בארמון.", - "expected_phonemes": "k aː n a t | tˤ a b aː x a | dʒ d iː d e | dʒ aː j | t i ʃ t ɣ e l | f i | i l ʔ a sˤ e r" - }, - { - "voice_time_seconds": { - "start": 112.8, - "end": 116.0 - }, - "arabic": "الطّبّاخ إستحى يقول لظّاهر بيبرس", - "hebrew_transliteration": "אִ-טַּבַּאחַ' אִסתַחַא יְקוּל לַ-טַּאהֶר בֵּיבַּרס", - "hebrew": "הטבח התבייש לומר לא-טאהר ביברס", - "expected_phonemes": "ʔ i tˤ a b aː x a | ʔ i s t a ħ a | j ʔ uː l | l a tˤ aː h e r | b eː b a r s" - }, - { - "voice_time_seconds": { - "start": 116.0, - "end": 118.2 - }, - "arabic": "إنّه ما بيعرف أسم الحلو،", - "hebrew_transliteration": "אִנּוֹ מַא בִּיעְרַף אִסֶם אִל-חִלוּ,", - "hebrew": "שהוא לא יודע את שם הממתק,", - "expected_phonemes": "ʔ i n o | m a | b iː ʕ r a f | ʔ i s e m | i l ħ i l u" - }, - { - "voice_time_seconds": { - "start": 118.2, - "end": 121.6 - }, - "arabic": "وعشان هيك إخترع أسم على السّريع", - "hebrew_transliteration": "וּעַשַאן הֵיכּ אִחְ'תַרַע אִסֶם עַלַא אִ-סַּרִיע", - "hebrew": "ולכן המציא שם במהירות", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | ʔ i x t a r a ʕ | ʔ i s e m | ʕ a l a | ʔ i s a r iː ʕ" - }, - { - "voice_time_seconds": { - "start": 121.6, - "end": 126.4 - }, - "arabic": "وقال لظّاهر بيبرس إنّه أسمه أصابع زينب،", - "hebrew_transliteration": "וּקַאל לַ-טַּאהֶר בֵּיבַּרס אִנּוֹ אִסמוֹ אַצַאבֶּע זֵינַבּ,", - "hebrew": "ואמר לא-טאהר ביברס ששמו אצבעות זינב,", - "expected_phonemes": "u ʔ aː l | l a tˤ aː h e r | b eː b a r s | ʔ i n o | ʔ i s m o | ʔ a sˤ aː b e ʕ | z eː n a b" - }, - { - "voice_time_seconds": { - "start": 126.4, - "end": 130.0 - }, - "arabic": "بما إنّه زينب هيّ إلّي عملته.", - "hebrew_transliteration": "בִּמַא אִנּוֹ זֵינַבּ הִיֶّ אִלִّי עִמלַתוֹ.", - "hebrew": "מכיוון שזינב היא שהכינה אותו.", - "expected_phonemes": "b i m a | ʔ i n o | z eː n a b | h i j j e | ʔ i l l i | ʕ i m l a t o" - }, - { - "voice_time_seconds": { - "start": 130.0, - "end": 133.2 - }, - "arabic": "من كتر ما كان الظّاهر بيبرس مبسوط،", - "hebrew_transliteration": "מִן כֻּתְר מַא כַּאן אִ-טַّאהֶר בֵּיבַּרס מַבְּסוּט,", - "hebrew": "מרוב שא-טאהר ביברס היה מרוצה,", - "expected_phonemes": "m i n | k u t r | m a | k aː n | ʔ i tˤ tˤ aː h e r | b eː b a r s | m a b s uː tˤ" - }, - { - "voice_time_seconds": { - "start": 133.2, - "end": 135.0 - }, - "arabic": "طلب يشوف زينب.", - "hebrew_transliteration": "טַלַבּ יְשוּף זֵינַבּ.", - "hebrew": "ביקש לראות את זינב.", - "expected_phonemes": "tˤ a l a b | j ʃ uː f | z eː n a b" - }, - { - "voice_time_seconds": { - "start": 135.0, - "end": 139.8 - }, - "arabic": "عشان هيك راحوا الخدم نادوها من المطبخ.", - "hebrew_transliteration": "עַשַאן הֵיכּ רַאחוּ אִל-חַ'דַם נַאדוּהַא מִן אִל-מַטבַּח'.", - "hebrew": "לכן המשרתים הלכו וקראו לה מהמטבח.", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | r aː ħ u | i l x a d a m | n aː d uː h a | m i n | i l m a tˤ b a x" - }, - { - "voice_time_seconds": { - "start": 139.8, - "end": 143.2 - }, - "arabic": "زينب أجت خايفة لعند الظّاهر بيبرس،", - "hebrew_transliteration": "זֵינַבּ אַגַ'ת חַ'איְפֶה לַעִנד אִ-טַّאהֶר בֵּיבַּרס,", - "hebrew": "זינב באה מפוחדת אל א-טאהר ביברס,", - "expected_phonemes": "z eː n a b | ʔ a dʒ a t | x aː j f e | l a ʕ i n d | ʔ i tˤ tˤ aː h e r | b eː b a r s" - }, - { - "voice_time_seconds": { - "start": 143.2, - "end": 146.2 - }, - "arabic": "كانت مفكّرة إنّه بدّه يعاقبها.", - "hebrew_transliteration": "כַּאנַת מְפַכְّרַה אִנּוֹ בַּדּוֹ יְעַאקֶבְּהַא.", - "hebrew": "היא חשבה שהוא רוצה להעניש אותה.", - "expected_phonemes": "k aː n a t | m f a k k r a | ʔ i n o | b a d o | j ʕ aː ʔ e b h a" - }, - { - "voice_time_seconds": { - "start": 146.2, - "end": 149.0 - }, - "arabic": "بسّ على عكس توقّعاتها", - "hebrew_transliteration": "בַּס עַלַא עַכֶּס תַוַקֻּעַאתְהַא", - "hebrew": "אבל בניגוד לציפיותיה", - "expected_phonemes": "b a s | ʕ a l a | ʕ a k e s | t a w a ʔ u ʕ aː t h a" - }, - { - "voice_time_seconds": { - "start": 149.0, - "end": 150.6 - }, - "arabic": "الظّاهر بيبرس شكرها", - "hebrew_transliteration": "אִ-טַّאהֶר בֵּיבַּרס שַכַּרְהַא", - "hebrew": "א-טאהר ביברס הודה לה", - "expected_phonemes": "ʔ i tˤ tˤ aː h e r | b eː b a r s | ʃ a k a r h a" - }, - { - "voice_time_seconds": { - "start": 150.6, - "end": 153.0 - }, - "arabic": "على الحلو الطّيّب إلّي عملته،", - "hebrew_transliteration": "עַלַא אִל-חִלוּ אִ-טַّיֶּבּ אִלִّי עִמלַתוֹ,", - "hebrew": "על הממתק הטעים שהכינה,", - "expected_phonemes": "ʕ a l a | i l ħ i l u | ʔ i tˤ tˤ a j e b | ʔ i l l i | ʕ i m l a t o" - }, - { - "voice_time_seconds": { - "start": 153.0, - "end": 156.6 - }, - "arabic": "وصار يحكي ويوخد ويعطي معاها.", - "hebrew_transliteration": "וּצַאר יִחכִּי וִיוֹחֹ'ד וִיַעטִי מַעַאהַא.", - "hebrew": "והתחיל לדבר ולשוחח איתה.", - "expected_phonemes": "u sˤ aː r | j i ħ k i | w iː oː x o d | w i j a ʕ tˤ i | m a ʕ aː h a" - }, - { - "voice_time_seconds": { - "start": 156.6, - "end": 160.4 - }, - "arabic": "حكي جاب حكي وبلّش الظّاهر بيبرس يشعر", - "hebrew_transliteration": "חַכִּי גַ'אבּ חַכִּי וּבַּלַّש אִ-טַّאהֶר בֵּיבַּרס יִשעֹר", - "hebrew": "מילה הביאה מילה, וא-טאהר ביברס התחיל להרגיש", - "expected_phonemes": "ħ a k i | dʒ aː b | ħ a k i | u b a l l a ʃ | ʔ i tˤ tˤ aː h e r | b eː b a r s | j i ʃ ʕ o r" - }, - { - "voice_time_seconds": { - "start": 160.4, - "end": 162.2 - }, - "arabic": "إنّه قلبه إرتاحلها.", - "hebrew_transliteration": "אִנּוֹ קַלבּוֹ אִרתַאח-לְהַא.", - "hebrew": "שליבו נפתח אליה.", - "expected_phonemes": "ʔ i n o | ʔ a l b o | ʔ i r t aː ħ l h a" - }, - { - "voice_time_seconds": { - "start": 162.2, - "end": 165.4 - }, - "arabic": "فتجوّزوا وجابوا ولاد وبنات", - "hebrew_transliteration": "פַתגַ'וַّזוּ וּגַ'אבּוּ וְלַאד וּבַּנַאת", - "hebrew": "אז התחתנו והביאו בנים ובנות", - "expected_phonemes": "f a t dʒ a w w a z u | u dʒ aː b u | u l aː d | u b a n aː t" - }, - { - "voice_time_seconds": { - "start": 165.4, - "end": 167.8 - }, - "arabic": "وعاشوا في ثبات ونبات.", - "hebrew_transliteration": "וּעַאשוּ פִי תַבַּאת וּנַבַּאת.", - "hebrew": "וחיו באושר ועושר.", - "expected_phonemes": "u ʕ aː ʃ u | f i | t a b aː t | u n a b aː t" - }, - { - "voice_time_seconds": { - "start": 167.8, - "end": 171.2 - }, - "arabic": "والحلو هاد صار اسمه أصابع زينب.", - "hebrew_transliteration": "וִל-חִלוּ הַאד צַאר אִסמוֹ אַצַאבֶּע זֵינַבּ.", - "hebrew": "והממתק הזה נקרא מאז אצבעות זינב.", - "expected_phonemes": "w i l ħ i l u | h aː d | sˤ aː r | ʔ i s m o | ʔ a sˤ aː b e ʕ | z eː n a b" - } - ] - }, - { - "file_name": "דברי האדמה * כַּלַאמֶ ל-אָרד * كلام الأرض * סיפור בערבית מדוברת [jf782k4Eb-g].webm", - "youtube_id": "jf782k4Eb-g", - "title_hebrew": "דברי האדמה", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "كلام الأرض", - "hebrew_transliteration": "כַּלַאם אִל-אַרְד'", - "hebrew": "דברי האדמה", - "expected_phonemes": "k a l aː m | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 9.8 - }, - "arabic": "كان في قديم الزّمان فلّاحين تنين", - "hebrew_transliteration": "כַּאן פִי קַדִים אִ-זַّמַאן פַלַّאחִין תְנֵין", - "hebrew": "היו פעם בימים עברו שני איכרים", - "expected_phonemes": "k aː n | f i | ʔ a d iː m | ʔ i z z a m aː n | f a l l aː ħ iː n | t n eː n" - }, - { - "voice_time_seconds": { - "start": 9.8, - "end": 13.0 - }, - "arabic": "إلّي كانوا يفلحوا أراضهم حدّ بعض.", - "hebrew_transliteration": "אִלִّי כַּאנוּ יִפלְחוּ אַרַאד'הֹם חַדّ בַּעְד'", - "hebrew": "שעיבדו את אדמותיהם זו לצד זו.", - "expected_phonemes": "ʔ i l l i | k aː n u | j i f l ħ u | ʔ a r aː dˤ h o m | ħ a d d | b a ʕ dˤ" - }, - { - "voice_time_seconds": { - "start": 13.0, - "end": 14.6 - }, - "arabic": "في يوم من الأيّام،", - "hebrew_transliteration": "פִי יוֹם מִן אִל-אַיַّאם,", - "hebrew": "יום אחד,", - "expected_phonemes": "f i | j oː m | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 14.6, - "end": 16.2 - }, - "arabic": "صار بينهم جدال كبير", - "hebrew_transliteration": "צַאר בֵּינְהֹם גְ'דַאל כְּבִּיר", - "hebrew": "פרץ ביניהם ויכוח גדול", - "expected_phonemes": "sˤ aː r | b eː n h o m | dʒ d aː l | k b iː r" - }, - { - "voice_time_seconds": { - "start": 16.2, - "end": 18.0 - }, - "arabic": "على قطعة أرض.", - "hebrew_transliteration": "עַלַא קִטעֶת אַרְד'.", - "hebrew": "על חלקת אדמה.", - "expected_phonemes": "ʕ a l a | ʔ i tˤ ʕ e t | ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 18.0, - "end": 19.4 - }, - "arabic": "واحد بقول:", - "hebrew_transliteration": "וַאחַד בִּיקוּל:", - "hebrew": "אחד אומר:", - "expected_phonemes": "w aː ħ a d | b iː ʔ uː l" - }, - { - "voice_time_seconds": { - "start": 19.4, - "end": 20.6 - }, - "arabic": "هاي إلي!", - "hebrew_transliteration": "הַאי אִלִי!", - "hebrew": "זו שלי!", - "expected_phonemes": "h aː i | ʔ i l i" - }, - { - "voice_time_seconds": { - "start": 20.6, - "end": 21.8 - }, - "arabic": "والتّاني بقول:", - "hebrew_transliteration": "וִ-תַّאנִי בִּיקוּל:", - "hebrew": "והשני אומר:", - "expected_phonemes": "w i t t aː n i | b iː ʔ uː l" - }, - { - "voice_time_seconds": { - "start": 21.8, - "end": 24.6 - }, - "arabic": "والله مش إلك، هاي إلي!", - "hebrew_transliteration": "וַאללַה מִש אִלַכּ, הַאי אִלִי!", - "hebrew": "בחיי שהיא לא שלך, היא שלי!", - "expected_phonemes": "w aː l l a | m i ʃ | ʔ i l a k | h aː i | ʔ i l i" - }, - { - "voice_time_seconds": { - "start": 24.6, - "end": 27.6 - }, - "arabic": "فتحت الأرض تمّها وحكت:", - "hebrew_transliteration": "פַתחַת אִל-אַרְד' תֻמְّהַא וּחַכַּת:", - "hebrew": "האדמה פתחה את פיה ואמרה:", - "expected_phonemes": "f a t ħ a t | i l ʔ a r dˤ | t u m m h a | u ħ a k a t" - }, - { - "voice_time_seconds": { - "start": 27.6, - "end": 32.4 - }, - "arabic": "لا إلك ولا إلك، إنتو تنيناتكو إلي!", - "hebrew_transliteration": "לַא אִלַכּ וְלַא אִלַכּ, אִנתוּ תִנֵינַאתְכּוּ אִלִי!", - "hebrew": "לא שלך ולא שלך, שניכם שלי!", - "expected_phonemes": "l aː | ʔ i l a k | u l aː | ʔ i l a k | ʔ i n t u | t i n eː n aː t k u | ʔ i l i" - } - ] - }, - { - "file_name": "האיכר והחמור * אִל-פַלַאח וִלֶ-חמַאר * الفلّاح والحمار * סיפור בערבית מדוברת [AZd5OJm-QOA].webm", - "youtube_id": "AZd5OJm-QOA", - "title_hebrew": "האיכר והחמור", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "الفلّاح والحمار", - "hebrew_transliteration": "אִל-פַלַّאח וִלֶ-חמַאר", - "hebrew": "האיכר והחמור", - "expected_phonemes": "i l f a l l aː ħ | w i l e ħ m aː r" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 9.6 - }, - "arabic": "كان يا ما كان، في قديم الزّمان،", - "hebrew_transliteration": "כַּאן יַא מַא כַּאן, פִי קַדִים אִ-זַّמַאן,", - "hebrew": "היה היה, בימים עברו,", - "expected_phonemes": "k aː n | j a | m a | k aː n | f i | ʔ a d iː m | ʔ i z z a m aː n" - }, - { - "voice_time_seconds": { - "start": 9.6, - "end": 11.6 - }, - "arabic": "واحد فلّاح.", - "hebrew_transliteration": "וַאחַד פַלַّאח.", - "hebrew": "איכר אחד.", - "expected_phonemes": "w aː ħ a d | f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 11.6, - "end": 15.0 - }, - "arabic": "كان لهدا الفلّاح حمار.", - "hebrew_transliteration": "כַּאן לַהַאדַא אִל-פַלַّאח חמַאר.", - "hebrew": "לאיכר הזה היה חמור.", - "expected_phonemes": "k aː n | l a h aː d a | i l f a l l aː ħ | ħ m aː r" - }, - { - "voice_time_seconds": { - "start": 15.0, - "end": 18.8 - }, - "arabic": "هالحمار كان ختيار تعبان،", - "hebrew_transliteration": "הַלֶ-חמַאר כַּאן חְ'תִיַאר תַעבַּאן,", - "hebrew": "החמור הזה היה זקן ועייף,", - "expected_phonemes": "h a l e ħ m aː r | k aː n | x t i j aː r | t a ʕ b aː n" - }, - { - "voice_time_seconds": { - "start": 18.8, - "end": 22.6 - }, - "arabic": "كلّ حياته كان يشتغل عند الفلّاح.", - "hebrew_transliteration": "כֻּלّ חַיַאתוֹ כַּאן יִשתְעֶ'ל עִנד אִל-פַלַّאח.", - "hebrew": "כל חייו עבד אצל האיכר.", - "expected_phonemes": "k u l l | ħ a j aː t o | k aː n | j i ʃ t ɣ e l | ʕ i n d | i l f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 22.6, - "end": 25.6 - }, - "arabic": "وهيك، بيوم من الأيّام،", - "hebrew_transliteration": "וּהֵיכּ, בְּיוֹם מִן אִל-אַיַّאם,", - "hebrew": "וכך, יום אחד,", - "expected_phonemes": "u h eː k | b j oː m | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 25.6, - "end": 27.0 - }, - "arabic": "مات الحمار،", - "hebrew_transliteration": "מַאת לֶ-חמַאר,", - "hebrew": "החמור מת,", - "expected_phonemes": "m aː t | l e ħ m aː r" - }, - { - "voice_time_seconds": { - "start": 27.0, - "end": 30.4 - }, - "arabic": "وبطّل فيه مين يساعد الفلّاح.", - "hebrew_transliteration": "וּבַּטַّל פִיה מִין יְסַאעֶד אִל-פַלַّאח.", - "hebrew": "ולא נשאר מי שיעזור לאיכר.", - "expected_phonemes": "u b a tˤ tˤ a l | f iː h | m iː n | j s aː ʕ e d | i l f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 30.4, - "end": 35.4 - }, - "arabic": "مشان هيك قرّر الفلّاح يشتري حمار جديد.", - "hebrew_transliteration": "מִשַאן הֵיכּ קַרַّר אִל-פַלַّאח יִשתְרִי חמַאר גְ'דִיד.", - "hebrew": "לכן החליט האיכר לקנות חמור חדש.", - "expected_phonemes": "m i ʃ aː n | h eː k | ʔ a r r a r | i l f a l l aː ħ | j i ʃ t r i | ħ m aː r | dʒ d iː d" - }, - { - "voice_time_seconds": { - "start": 35.4, - "end": 39.6 - }, - "arabic": "وبالفعل، بلّش الفلّاح يدوّر في القرية", - "hebrew_transliteration": "וּבִּל-פִעֶל, בַּלַّש אִל-פַלַّאח יְדַוֶّר פִי אִל-קַרְיֶה", - "hebrew": "ואכן, האיכר התחיל לחפש בכפר", - "expected_phonemes": "u b i l f i ʕ e l | b a l l a ʃ | i l f a l l aː ħ | j d a w w e r | f i | i l ʔ a r j e" - }, - { - "voice_time_seconds": { - "start": 39.6, - "end": 42.8 - }, - "arabic": "ويسأل مين عنده حمار للبيع.", - "hebrew_transliteration": "וִיסַאַל מִין עִנדוֹ חמַאר לַל-בֵּיע.", - "hebrew": "ולשאול למי יש חמור למכירה.", - "expected_phonemes": "w iː s a ʔ a l | m iː n | ʕ i n d o | ħ m aː r | l a l b eː ʕ" - }, - { - "voice_time_seconds": { - "start": 42.8, - "end": 46.0 - }, - "arabic": "مرق من بيت لبيت ومن حارة لحارة،", - "hebrew_transliteration": "מַרַק מִן בֵּית לַבֵּית וּמִן חַארַה לַחַארַה,", - "hebrew": "עבר מבית לבית ומשכונה לשכונה,", - "expected_phonemes": "m a r a ʔ | m i n | b eː t | l a b eː t | u m i n | ħ aː r a | l a ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 46.0, - "end": 49.8 - }, - "arabic": "يستفسر ويسأل عن حمار للبيع.", - "hebrew_transliteration": "יִסתַפסֶר וִיסַאַל עַן חמַאר לַל-בֵּיע.", - "hebrew": "מברר ושואל על חמור למכירה.", - "expected_phonemes": "j i s t a f s e r | w iː s a ʔ a l | ʕ a n | ħ m aː r | l a l b eː ʕ" - }, - { - "voice_time_seconds": { - "start": 49.8, - "end": 52.8 - }, - "arabic": "لحدّ ما التقى بختيار.", - "hebrew_transliteration": "לַחַדّ מַא אִלתַקַא בְּחְ'תִיַאר.", - "hebrew": "עד שפגש זקן אחד.", - "expected_phonemes": "l a ħ a d d | m a | i l t a ʔ a | b x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 52.8, - "end": 55.0 - }, - "arabic": "هالختيار سأله:", - "hebrew_transliteration": "הַל-חְ'תִיַאר סַאַלוֹ:", - "hebrew": "הזקן הזה שאל אותו:", - "expected_phonemes": "h a l x t i j aː r | s a ʔ a l o" - }, - { - "voice_time_seconds": { - "start": 55.0, - "end": 56.8 - }, - "arabic": "شو بدّك؟", - "hebrew_transliteration": "שוּ בַּדַّכּ?", - "hebrew": "מה אתה רוצה?", - "expected_phonemes": "ʃ u | b a d d a k" - }, - { - "voice_time_seconds": { - "start": 56.8, - "end": 58.8 - }, - "arabic": "ردّ عليه:", - "hebrew_transliteration": "רַדّ עַלֵיה:", - "hebrew": "הוא ענה לו:", - "expected_phonemes": "r a d d | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 58.8, - "end": 62.0 - }, - "arabic": "بدّي أشتري حمار، بلاقي عندك؟", - "hebrew_transliteration": "בַּדִّי אַשתְרִי חמַאר, בַּלַאקִי עִנדַכּ?", - "hebrew": "אני רוצה לקנות חמור, יש לך?", - "expected_phonemes": "b a d d i | ʔ a ʃ t r i | ħ m aː r | b a l aː ʔ i | ʕ i n d a k" - }, - { - "voice_time_seconds": { - "start": 62.0, - "end": 64.0 - }, - "arabic": "الختيار جاوب بسرعة:", - "hebrew_transliteration": "אִל-חְ'תִיַאר גַ'אוַבּ בְּסֻרעַה:", - "hebrew": "הזקן ענה מהר:", - "expected_phonemes": "i l x t i j aː r | dʒ aː w a b | b s u r ʕ a" - }, - { - "voice_time_seconds": { - "start": 64.0, - "end": 67.2 - }, - "arabic": "آه، فيه عندي، تشتري؟", - "hebrew_transliteration": "אַה, פִיה עִנדִי, תִשתְרִי?", - "hebrew": "כן, יש לי, תקנה?", - "expected_phonemes": "ʔ aː | f iː h | ʕ i n d i | t i ʃ t r i" - }, - { - "voice_time_seconds": { - "start": 67.2, - "end": 69.6 - }, - "arabic": "طبعا! قال الفلّاح.", - "hebrew_transliteration": "טַבְּעַן! קַאל אִל-פַלַّאח.", - "hebrew": "בטח! אמר האיכר.", - "expected_phonemes": "tˤ a b ʕ a n | ʔ aː l | i l f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 69.6, - "end": 74.0 - }, - "arabic": "الختيار كمّل حكي وقال للفلّاح:", - "hebrew_transliteration": "אִל-חְ'תִיַאר כַּמַّל חַכִּי וּקַאל לַל-פַלַّאח:", - "hebrew": "הזקן המשיך לדבר ואמר לאיכר:", - "expected_phonemes": "i l x t i j aː r | k a m m a l | ħ a k i | u ʔ aː l | l a l f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 74.0, - "end": 75.8 - }, - "arabic": "بسّ لازم تعرف،", - "hebrew_transliteration": "בַּס לַאזֶם תִערַף,", - "hebrew": "אבל אתה צריך לדעת,", - "expected_phonemes": "b a s | l aː z e m | t i ʕ r a f" - }, - { - "voice_time_seconds": { - "start": 75.8, - "end": 79.0 - }, - "arabic": "هدا الحمار مش حمار عادي،", - "hebrew_transliteration": "הַאדַא לֶ-חמַאר מִש חמַאר עַאדִי,", - "hebrew": "החמור הזה הוא לא חמור רגיל,", - "expected_phonemes": "h aː d a | l e ħ m aː r | m i ʃ | ħ m aː r | ʕ aː d i" - }, - { - "voice_time_seconds": { - "start": 79.0, - "end": 81.4 - }, - "arabic": "هوّ مش متل كلّ الحمير.", - "hebrew_transliteration": "הֻוֶّ מִש מִתֶל כֻּלّ אִל-חַמִיר.", - "hebrew": "הוא לא כמו כל החמורים.", - "expected_phonemes": "h u w w e | m i ʃ | m i t e l | k u l l | i l ħ a m iː r" - }, - { - "voice_time_seconds": { - "start": 81.4, - "end": 85.0 - }, - "arabic": "بيمشي بسّ إذا بتقولّه «الحمدلله»", - "hebrew_transliteration": "בִּימשִי בַּס אִזַא בְּתְקוּל-לוֹ \"אִל-חַמדֻלִלַّה\"", - "hebrew": "הוא הולך רק אם אתה אומר לו \"אל-חמדוללה\"", - "expected_phonemes": "b iː m ʃ i | b a s | ʔ i z a | b t ʔ uː l l o | i l ħ a m d u l i l l a" - }, - { - "voice_time_seconds": { - "start": 85.0, - "end": 89.4 - }, - "arabic": "وبيوقّف بسّ لمّا بتقولّه «سمسم».", - "hebrew_transliteration": "וּבִּיוַקֶّף בַּס לַמַّא בְּתְקוּל-לוֹ \"סִמסֶם\".", - "hebrew": "ועוצר רק כשאתה אומר לו \"סמסם\".", - "expected_phonemes": "u b iː w a ʔ ʔ e f | b a s | l a m m a | b t ʔ uː l l o | s i m s e m" - }, - { - "voice_time_seconds": { - "start": 89.4, - "end": 92.0 - }, - "arabic": "الفلّاح تعجّب وقال:", - "hebrew_transliteration": "אִל-פַלַّאח תְעַגַّבּ וּקַאל:", - "hebrew": "האיכר השתומם ואמר:", - "expected_phonemes": "i l f a l l aː ħ | t ʕ a g g a b | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 92.0, - "end": 95.4 - }, - "arabic": "شكله هدا الحمار حمار غريب،", - "hebrew_transliteration": "שַכְּלוֹ הַאדַא לֶ-חמַאר חמַאר עַ'רִיבּ,", - "hebrew": "נראה שהחמור הזה הוא חמור מוזר,", - "expected_phonemes": "ʃ a k l o | h aː d a | l e ħ m aː r | ħ m aː r | ɣ a r iː b" - }, - { - "voice_time_seconds": { - "start": 95.4, - "end": 99.6 - }, - "arabic": "بسّ معليش، بدّي إيّاه، بدّي أشتريه.", - "hebrew_transliteration": "בַּס מַעלֵיש, בַּדִّי אִיַّאה, בַּדִّי אַשתְרִיה.", - "hebrew": "אבל לא נורא, אני רוצה אותו, אני רוצה לקנות אותו.", - "expected_phonemes": "b a s | m a ʕ l eː ʃ | b a d d i | ʔ i j j aː h | b a d d i | ʔ a ʃ t r iː h" - }, - { - "voice_time_seconds": { - "start": 99.6, - "end": 101.6 - }, - "arabic": "إشترى الفلّاح الحمار", - "hebrew_transliteration": "אִשתַרַא אִל-פַלַّאח לֶ-חמַאר", - "hebrew": "האיכר קנה את החמור", - "expected_phonemes": "ʔ i ʃ t a r a | i l f a l l aː ħ | l e ħ m aː r" - }, - { - "voice_time_seconds": { - "start": 101.6, - "end": 103.4 - }, - "arabic": "ورجع على بيته مبسوط إنّه", - "hebrew_transliteration": "וּרִגֶ'ע עַלַא בֵּיתוֹ מַבְּסוּט אִנּוֹ", - "hebrew": "וחזר לביתו שמח על כך", - "expected_phonemes": "u r i dʒ e ʕ | ʕ a l a | b eː t o | m a b s uː tˤ | ʔ i n o" - }, - { - "voice_time_seconds": { - "start": 103.4, - "end": 107.2 - }, - "arabic": "نجح يلاقي حمار يساعده بشغله.", - "hebrew_transliteration": "נִגֶ'ח יְלַאקִי חמַאר יְסַאעדוֹ בְּשֻעְ'לוֹ.", - "hebrew": "שהצליח למצוא חמור שיעזור לו בעבודתו.", - "expected_phonemes": "n i dʒ e ħ | j l aː ʔ i | ħ m aː r | j s aː ʕ d o | b ʃ u ɣ l o" - }, - { - "voice_time_seconds": { - "start": 107.2, - "end": 110.6 - }, - "arabic": "وصار يعمل متل ما حكاله الختيار.", - "hebrew_transliteration": "וּצַאר יַעמַל מִתֶל מַא חַכַּא-לוֹ אִל-חְ'תִיַאר.", - "hebrew": "והתחיל לעשות כמו שאמר לו הזקן.", - "expected_phonemes": "u sˤ aː r | j a ʕ m a l | m i t e l | m a | ħ a k aː l o | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 110.6, - "end": 112.4 - }, - "arabic": "لمّا بدّه الحمار يمشي", - "hebrew_transliteration": "לַמַّא בַּדּוֹ לֶ-חמַאר יִמשִי", - "hebrew": "כשרצה שהחמור ילך", - "expected_phonemes": "l a m m a | b a d o | l e ħ m aː r | j i m ʃ i" - }, - { - "voice_time_seconds": { - "start": 112.4, - "end": 114.4 - }, - "arabic": "كان يقولّه «الحمدلله»", - "hebrew_transliteration": "כַּאן יְקוּל-לוֹ \"אִל-חַמדֻלִלַّה\"", - "hebrew": "היה אומר לו \"אל-חמדוללה\"", - "expected_phonemes": "k aː n | j ʔ uː l l o | i l ħ a m d u l i l l a" - }, - { - "voice_time_seconds": { - "start": 114.4, - "end": 116.6 - }, - "arabic": "ولمّا بدّه إيّاه يوقّف،", - "hebrew_transliteration": "וּלַמַّא בַּדּוֹ אִיַّאה יוַקֶّף,", - "hebrew": "וכשרצה שיעצור,", - "expected_phonemes": "u l a m m a | b a d o | ʔ i j j aː h | j w a ʔ ʔ e f" - }, - { - "voice_time_seconds": { - "start": 116.6, - "end": 119.0 - }, - "arabic": "يقولّه «سمسم».", - "hebrew_transliteration": "יְקוּל-לוֹ \"סִמסֶם\".", - "hebrew": "היה אומר לו \"סמסם\".", - "expected_phonemes": "j ʔ uː l l o | s i m s e m" - }, - { - "voice_time_seconds": { - "start": 119.0, - "end": 122.2 - }, - "arabic": "بيوم من الأيّام كان هالفلّاح", - "hebrew_transliteration": "בְּיוֹם מִן אִל-אַיַّאם כַּאן הַל-פַלַّאח", - "hebrew": "יום אחד, האיכר הזה", - "expected_phonemes": "b j oː m | m i n | i l ʔ a j j aː m | k aː n | h a l f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 122.2, - "end": 125.8 - }, - "arabic": "بدّه يوصل لواحدة من القرى البعيدة.", - "hebrew_transliteration": "בַּדּוֹ יוּצַל לַוַאחדֶה מִן אִל-קֻרַא לֶ-בְּעִידֶה.", - "hebrew": "רצה להגיע לאחד הכפרים הרחוקים.", - "expected_phonemes": "b a d o | j uː sˤ a l | l a w aː ħ d e | m i n | i l ʔ u r a | l e b ʕ iː d e" - }, - { - "voice_time_seconds": { - "start": 125.8, - "end": 128.6 - }, - "arabic": "ركب حماره ومتل العادة", - "hebrew_transliteration": "רִכֶּבּ חמַארוֹ וּמִתֶל אִל-עַאדֶה", - "hebrew": "רכב על חמורו וכרגיל", - "expected_phonemes": "r i k e b | ħ m aː r o | u m i t e l | i l ʕ aː d e" - }, - { - "voice_time_seconds": { - "start": 128.6, - "end": 130.6 - }, - "arabic": "قالّه «الحمدلله»", - "hebrew_transliteration": "קַאל-לוֹ \"אִל-חַמדֻלִלַّה\"", - "hebrew": "אמר לו \"אל-חמדוללה\"", - "expected_phonemes": "ʔ aː l l o | i l ħ a m d u l i l l a" - }, - { - "voice_time_seconds": { - "start": 130.6, - "end": 132.2 - }, - "arabic": "مشان يمشي.", - "hebrew_transliteration": "מִשַאן יִמשִי.", - "hebrew": "כדי שילך.", - "expected_phonemes": "m i ʃ aː n | j i m ʃ i" - }, - { - "voice_time_seconds": { - "start": 132.2, - "end": 135.4 - }, - "arabic": "الحمار بلّش يمشي.", - "hebrew_transliteration": "לֶ-חמַאר בַּלַّש יִמשִי.", - "hebrew": "החמור התחיל ללכת.", - "expected_phonemes": "l e ħ m aː r | b a l l a ʃ | j i m ʃ i" - }, - { - "voice_time_seconds": { - "start": 135.4, - "end": 139.8 - }, - "arabic": "بعد فترة وصلوا الحمار وصاحبه على القرية.", - "hebrew_transliteration": "בַּעֶד פַתרַה וְצְלוּ לֶ-חמַאר וּצַאחבּוֹ עַלַא אִל-קַרְיֶה.", - "hebrew": "אחרי זמן מה החמור ובעליו הגיעו לכפר.", - "expected_phonemes": "b a ʕ e d | f a t r a | w i sˤ l u | l e ħ m aː r | u sˤ aː ħ b o | ʕ a l a | i l ʔ a r j e" - }, - { - "voice_time_seconds": { - "start": 139.8, - "end": 143.6 - }, - "arabic": "بسّ، الفلّاح ما عرفش يوقّف الحمار.", - "hebrew_transliteration": "בַּס, אִל-פַלַّאח מַא עִרְפֶש יוַקֶّף לֶ-חמַאר.", - "hebrew": "אבל, האיכר לא ידע איך לעצור את החמור.", - "expected_phonemes": "b a s | i l f a l l aː ħ | m a | ʕ i r f e ʃ | j w a ʔ ʔ e f | l e ħ m aː r" - }, - { - "voice_time_seconds": { - "start": 143.6, - "end": 145.8 - }, - "arabic": "طلع ناسي كلمة السّرّ.", - "hebrew_transliteration": "טִלֶע נַאסִי כִּלמֶת אִ-סִّר.", - "hebrew": "התברר ששכח את מילת הסוד.", - "expected_phonemes": "tˤ i l e ʕ | n aː s i | k i l m e t | ʔ i s s i r" - }, - { - "voice_time_seconds": { - "start": 145.8, - "end": 149.2 - }, - "arabic": "وهيك ضلّ الحمار يمشي ويمشي،", - "hebrew_transliteration": "וּהֵיכּ דַ'לّ לֶ-חמַאר יִמשִי וִימשִי,", - "hebrew": "וכך החמור המשיך ללכת וללכת,", - "expected_phonemes": "u h eː k | dˤ a l l | l e ħ m aː r | j i m ʃ i | w iː m ʃ i" - }, - { - "voice_time_seconds": { - "start": 149.2, - "end": 151.6 - }, - "arabic": "وقطع القرية وكمّل مشي", - "hebrew_transliteration": "וּקַטַע אִל-קַרְיֶה וּכַּמַّל מַשִי", - "hebrew": "וחצה את הכפר והמשיך ללכת", - "expected_phonemes": "u ʔ a tˤ a ʕ | i l ʔ a r j e | u k a m m a l | m a ʃ i" - }, - { - "voice_time_seconds": { - "start": 151.6, - "end": 153.4 - }, - "arabic": "بدون ما يوقّف.", - "hebrew_transliteration": "בְּדוּן מַא יוַקֶّף.", - "hebrew": "בלי לעצור.", - "expected_phonemes": "b i d uː n | m a | j w a ʔ ʔ e f" - }, - { - "voice_time_seconds": { - "start": 153.4, - "end": 155.6 - }, - "arabic": "الفلّاح خاف كتير", - "hebrew_transliteration": "אִל-פַלַّאח חַ'אף כְּתִיר", - "hebrew": "האיכר פחד מאוד", - "expected_phonemes": "i l f a l l aː ħ | x aː f | k t iː r" - }, - { - "voice_time_seconds": { - "start": 155.6, - "end": 158.2 - }, - "arabic": "لإنّه شاف إنّهم قرّبوا على وادي،", - "hebrew_transliteration": "לַאִנּוֹ שַאף אִנְّהֹם קַרַّבּוּ עַלַא וַאדִי,", - "hebrew": "כי ראה שהם התקרבו לוואדי,", - "expected_phonemes": "l a ʔ i n o | ʃ aː f | ʔ i n n h o m | ʔ a r r a b u | ʕ a l a | w aː d i" - }, - { - "voice_time_seconds": { - "start": 158.2, - "end": 159.8 - }, - "arabic": "وخاف يوقعوا فيه.", - "hebrew_transliteration": "וּחַ'אף יוּקַעוּ פִיה.", - "hebrew": "ופחד שייפלו לתוכו.", - "expected_phonemes": "u x aː f | j uː ʔ a ʕ u | f iː h" - }, - { - "voice_time_seconds": { - "start": 159.8, - "end": 163.6 - }, - "arabic": "بسّ الله هداه متر واحد قبل ضفّة الوادي", - "hebrew_transliteration": "בַּס אַללַה הַדַאה מִתֶר וַאחַד קַבֶּל דַ'פֶّת אִל-וַאדִי", - "hebrew": "אבל אלוהים הדריך אותו מטר אחד לפני שפת הוואדי", - "expected_phonemes": "b a s | i l l a | h a d aː h | m i t e r | w aː ħ a d | ʔ a b e l | dˤ a f f e t | i l w aː d i" - }, - { - "voice_time_seconds": { - "start": 163.6, - "end": 165.2 - }, - "arabic": "وتذكّر الكلمة.", - "hebrew_transliteration": "וּתזַכַّר אִל-כִּלמֶה.", - "hebrew": "ונזכר במילה.", - "expected_phonemes": "u t z a k k a r | i l k i l m e" - }, - { - "voice_time_seconds": { - "start": 165.2, - "end": 169.6 - }, - "arabic": "وبسرعة وبصوت عالي قالّه لحماره", - "hebrew_transliteration": "וּבְּסֻרעַה וּבְּצוֹת עַאלִי קַאל-לוֹ לַחמַארוֹ", - "hebrew": "ובמהירות ובקול רם אמר לחמורו", - "expected_phonemes": "u b s u r ʕ a | u b sˤ oː t | ʕ aː l i | ʔ aː l l o | l a ħ m aː r o" - }, - { - "voice_time_seconds": { - "start": 169.6, - "end": 171.0 - }, - "arabic": "«سمسم!»", - "hebrew_transliteration": "סִמסֶם!", - "hebrew": "סמסם!", - "expected_phonemes": "s i m s e m" - }, - { - "voice_time_seconds": { - "start": 171.0, - "end": 173.0 - }, - "arabic": "الحمار وقّف.", - "hebrew_transliteration": "לֶ-חמַאר וַקַّף.", - "hebrew": "החמור עצר.", - "expected_phonemes": "l e ħ m aː r | w a ʔ ʔ a f" - }, - { - "voice_time_seconds": { - "start": 173.0, - "end": 176.2 - }, - "arabic": "الفلّاح انبسط وتنهّد بإرتياح.", - "hebrew_transliteration": "אִל-פַלַّאח אִנבַּסַט וּתנַהַّד בְּאִרתִיַאח.", - "hebrew": "האיכר שמח ונאנח בהקלה.", - "expected_phonemes": "i l f a l l aː ħ | ʔ i n b a s a tˤ | u t n a h h a d | b ʔ i r t i j aː ħ" - }, - { - "voice_time_seconds": { - "start": 176.2, - "end": 178.0 - }, - "arabic": "ومن فرحته قال", - "hebrew_transliteration": "וּמִן פַרחתוֹ קַאל", - "hebrew": "ומרוב שמחה אמר", - "expected_phonemes": "u m i n | f a r ħ t o | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 178.0, - "end": 180.6 - }, - "arabic": "«الحمدلله»...", - "hebrew_transliteration": "אִל-חַמדֻלִלַّה...", - "hebrew": "אל-חמדוללה...", - "expected_phonemes": "i l ħ a m d u l i l l a" - } - ] - }, - { - "file_name": "ג'חא והשופט * גֻ'חַא וִל-קָאדִי * جحا والقاضي * סיפור בערבית מדוברת [UnlJBvvWBGA].webm", - "youtube_id": "UnlJBvvWBGA", - "title_hebrew": "ג'חא והשופט", - "segments": [ - { - "voice_time_seconds": { - "start": 2.2, - "end": 7.4 - }, - "arabic": "جحا والقاضي", - "hebrew_transliteration": "גֻ'חַא וִל-קַאדִי", - "hebrew": "ג'חא והשופט", - "expected_phonemes": "dʒ u ħ a | w i l ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 7.4, - "end": 9.4 - }, - "arabic": "بواحدة من البلدات لصغرة", - "hebrew_transliteration": "בְּוַאחדֶה מִן אִל-בַּלַדַאת לֶ-זְעִ'ירֶה", - "hebrew": "באחת העיירות הקטנות", - "expected_phonemes": "b w aː ħ d e | m i n | i l b a l a d aː t | l e z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 9.4, - "end": 11.0 - }, - "arabic": "كان في قاضي ختيار", - "hebrew_transliteration": "כַּאן פִיה קַאדִי חְ'תִיַאר", - "hebrew": "היה שופט זקן", - "expected_phonemes": "k aː n | f iː h | ʔ aː d i | x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 11.0, - "end": 14.2 - }, - "arabic": "إلّي كان يحكم بالعدالة بين النّاس،", - "hebrew_transliteration": "אִלִّי כַּאן יִחכֹּם בִּל-עַדַאלֶה בֵּין אִ-נַّאס,", - "hebrew": "ששפט בצדק בין האנשים,", - "expected_phonemes": "ʔ i l l i | k aː n | j i ħ k o m | b i l ʕ a d aː l e | b eː n | i n n aː s" - }, - { - "voice_time_seconds": { - "start": 14.2, - "end": 18.6 - }, - "arabic": "بذكاء، بنزاهة وبطولة بال.", - "hebrew_transliteration": "בִּדַ'כַּאא, בִּנַזַאהַה וּבִּטוּלֶת בַּאל.", - "hebrew": "בתבונה, ביושר ובסבלנות.", - "expected_phonemes": "b i dˤ a k aː a | b i n a z aː h a | u b i tˤ uː l e t | b aː l" - }, - { - "voice_time_seconds": { - "start": 18.6, - "end": 20.4 - }, - "arabic": "بيوم من الأيّام مرض", - "hebrew_transliteration": "בְּיוֹם מִן אִל-אַיַّאם מִרֶד'", - "hebrew": "יום אחד הוא חלה", - "expected_phonemes": "b j oː m | m i n | i l ʔ a j j aː m | m i r e dˤ" - }, - { - "voice_time_seconds": { - "start": 20.4, - "end": 22.6 - }, - "arabic": "وبعد فترة قصيرة توفّى", - "hebrew_transliteration": "וּבַּעֶד פַתרַה קַצִירֶה תְוַפַّא", - "hebrew": "ואחרי זמן קצר נפטר", - "expected_phonemes": "u b a ʕ e d | f a t r a | ʔ a sˤ iː r e | t w a f f a" - }, - { - "voice_time_seconds": { - "start": 22.6, - "end": 25.4 - }, - "arabic": "ورجّع روحه لعند الخالق.", - "hebrew_transliteration": "וּרַגַّע רוּחוֹ לַעִנד אִל-חַ'אלֶק.", - "hebrew": "והשיב את נשמתו לבורא.", - "expected_phonemes": "u r a g g a ʕ | r uː ħ o | l a ʕ i n d | i l x aː l e ʔ" - }, - { - "voice_time_seconds": { - "start": 25.4, - "end": 27.0 - }, - "arabic": "ما مرقش أسبوع", - "hebrew_transliteration": "מַא מַרַקש אֻסבּוּע", - "hebrew": "לא עבר שבוע", - "expected_phonemes": "m a | m a r a ʔ ʃ | ʔ u s b uː ʕ" - }, - { - "voice_time_seconds": { - "start": 27.0, - "end": 30.0 - }, - "arabic": "وإلّا أجا قاضي جديد على البلد،", - "hebrew_transliteration": "וְאִלַّא אַגַ'א קַאדִי גְ'דִיד עַלַא אִל-בַּלַד,", - "hebrew": "והנה הגיע שופט חדש לעיירה,", - "expected_phonemes": "u ʔ i l l a | ʔ a dʒ a | ʔ aː d i | dʒ d iː d | ʕ a l a | i l b a l a d" - }, - { - "voice_time_seconds": { - "start": 30.0, - "end": 32.4 - }, - "arabic": "قعد على كرسيه في المحكمة،", - "hebrew_transliteration": "קַעַד עַלַא כֻּרסִיה פִי אִל-מַחכַּמֶה,", - "hebrew": "ישב על כיסאו בבית המשפט,", - "expected_phonemes": "ʔ a ʕ a d | ʕ a l a | k u r s iː h | f i | i l m a ħ k a m e" - }, - { - "voice_time_seconds": { - "start": 32.4, - "end": 34.4 - }, - "arabic": "وبلّش يحكم بين النّاس", - "hebrew_transliteration": "וּבַּלַّש יִחכֹּם בֵּין אִ-נַّאס", - "hebrew": "והתחיל לשפוט בין האנשים", - "expected_phonemes": "u b a l l a ʃ | j i ħ k o m | b eː n | i n n aː s" - }, - { - "voice_time_seconds": { - "start": 34.4, - "end": 37.6 - }, - "arabic": "في القضايا إلّي جابوها لعنده.", - "hebrew_transliteration": "פִי אִל-קַדַאיַא אִלִّי גַ'אבּוּהַא לַעִנדוֹ.", - "hebrew": "בתיקים שהביאו אליו.", - "expected_phonemes": "f i | i l ʔ a d aː j a | ʔ i l l i | dʒ aː b uː h a | l a ʕ i n d o" - }, - { - "voice_time_seconds": { - "start": 37.6, - "end": 40.8 - }, - "arabic": "كان هدا القاضي شخص صعب قاسي،", - "hebrew_transliteration": "כַּאן הַאדַא אִל-קַאדִי שַחְ'ץ צַעֶבּ וּקַאסִי,", - "hebrew": "השופט הזה היה אדם קשה ואכזר,", - "expected_phonemes": "k aː n | h aː d a | i l ʔ aː d i | ʃ a x sˤ | sˤ a ʕ e b | u ʔ aː s i" - }, - { - "voice_time_seconds": { - "start": 40.8, - "end": 44.4 - }, - "arabic": "عنده كشرة وقلبه بعرفش الرّحمة.", - "hebrew_transliteration": "עִנדוֹ כַּשרַה וּקַלבּוֹ בִּיעְרַפש אִ-רַّחמֶה.", - "hebrew": "היה לו פרצוף זועף וליבו לא ידע רחמים.", - "expected_phonemes": "ʕ i n d o | k a ʃ r a | u ʔ a l b o | b iː ʕ r a f ʃ | ʔ i r r a ħ m e" - }, - { - "voice_time_seconds": { - "start": 44.4, - "end": 47.6 - }, - "arabic": "لا بعرف الله ولا رسوله.", - "hebrew_transliteration": "לַא בִּיעְרַף אַללַה וְלַא רַסוּלוֹ.", - "hebrew": "לא הכיר את אלוהים ולא את שליחו.", - "expected_phonemes": "l aː | b iː ʕ r a f | i l l a | u l aː | r a s uː l o" - }, - { - "voice_time_seconds": { - "start": 47.6, - "end": 49.8 - }, - "arabic": "كان قاضي إلّي حكمه شديد،", - "hebrew_transliteration": "כַּאן קַאדִי אִלִّי חֻכְּמוֹ שַדִיד,", - "hebrew": "הוא היה שופט שפסקי הדין שלו חמורים,", - "expected_phonemes": "k aː n | ʔ aː d i | ʔ i l l i | ħ u k m o | ʃ a d iː d" - }, - { - "voice_time_seconds": { - "start": 49.8, - "end": 52.0 - }, - "arabic": "وسكّان البلد ما كانوش عارفين", - "hebrew_transliteration": "וּסֻכַּאן אִל-בַּלַד מַא כַּאנוּש עַארְפִין", - "hebrew": "ותושבי העיירה לא ידעו", - "expected_phonemes": "u s u k aː n | i l b a l a d | m a | k aː n uː ʃ | ʕ aː r f iː n" - }, - { - "voice_time_seconds": { - "start": 52.0, - "end": 54.8 - }, - "arabic": "كيف يخلصوا من عقابه.", - "hebrew_transliteration": "כִּיף יִחְ'לַצוּ מִן עִקַאבּוֹ.", - "hebrew": "איך להימלט מהעונש שלו.", - "expected_phonemes": "k iː f | j i x l a sˤ u | m i n | ʕ i ʔ aː b o" - }, - { - "voice_time_seconds": { - "start": 54.8, - "end": 58.0 - }, - "arabic": "قام جحا وقال: «ما تقلقوش.", - "hebrew_transliteration": "קַאם גֻ'חַא וּקַאל: מַא תְקַלקְלוּש.", - "hebrew": "ג'חא קם ואמר: אל תדאגו.", - "expected_phonemes": "ʔ aː m | dʒ u ħ a | u ʔ aː l | m a | t ʔ a l ʔ l uː ʃ" - }, - { - "voice_time_seconds": { - "start": 58.0, - "end": 59.4 - }, - "arabic": "أنا بدبّر الأمور", - "hebrew_transliteration": "אַנַא בַּדַבֶּר אִל-אֻמוּר", - "hebrew": "אני אסדר את העניינים", - "expected_phonemes": "ʔ a n a | b a d a b e r | i l ʔ u m uː r" - }, - { - "voice_time_seconds": { - "start": 59.4, - "end": 62.4 - }, - "arabic": "مع هاد القاضي ابن الحرام.»", - "hebrew_transliteration": "מַע הַאד אִל-קַאדִי אִבְּן אִל-חַרַאם.", - "hebrew": "עם השופט הממזר הזה.", - "expected_phonemes": "m a ʕ | h aː d | i l ʔ aː d i | ʔ i b n | i l ħ a r aː m" - }, - { - "voice_time_seconds": { - "start": 62.4, - "end": 65.6 - }, - "arabic": "النّاس مسكوا ضحكتهم وسألوه:", - "hebrew_transliteration": "אִ-נַّאס מַסְכּוּ דִ'חְכִּתְהֹם וּסַאַלוּה:", - "hebrew": "האנשים התאפקו מלצחוק ושאלו אותו:", - "expected_phonemes": "i n n aː s | m a s k u | dˤ i ħ k i t h o m | u s a ʔ a l uː h" - }, - { - "voice_time_seconds": { - "start": 65.6, - "end": 68.4 - }, - "arabic": "«ليش شو طالع في راسك تعمل؟»", - "hebrew_transliteration": "לֵיש שוּ טַאלֶע פִי רַאסַכּ תַעמַל?", - "hebrew": "מה עלה בראשך לעשות?", - "expected_phonemes": "l eː ʃ | ʃ u | tˤ aː l e ʕ | f i | r aː s a k | t a ʕ m a l" - }, - { - "voice_time_seconds": { - "start": 68.4, - "end": 72.2 - }, - "arabic": "جحا قاللهم: «استنّوا وشوفوا.", - "hebrew_transliteration": "גֻ'חַא קַאל-לְהֹם: אִסתַנּוּ וּשוּפוּ.", - "hebrew": "ג'חא אמר להם: חכו ותראו.", - "expected_phonemes": "dʒ u ħ a | ʔ aː l l h o m | ʔ i s t a n u | u ʃ uː f u" - }, - { - "voice_time_seconds": { - "start": 72.2, - "end": 76.0 - }, - "arabic": "نقّولكوا وفد، من عشرين تلاتين زلمة محترم", - "hebrew_transliteration": "נַקּוּלְכּוּ וַפְד, מִן עִשרִין תַלַאתִין זַלַמֶה מֻחתַרַם", - "hebrew": "תבחרו לכם משלחת, של עשרים-שלושים אנשים מכובדים", - "expected_phonemes": "n a ʔ uː l k u | w a f d | m i n | ʕ i ʃ r iː n | t a l aː t iː n | z a l a m e | m u ħ t a r a m" - }, - { - "voice_time_seconds": { - "start": 76.0, - "end": 79.2 - }, - "arabic": "إلّي ييجوا يوم الأحد معي على المحكمة", - "hebrew_transliteration": "אִלִّי יִיגוּ יוֹם אִל-אַחַד מַעִי עַלַא אִל-מַחכַּמֶה", - "hebrew": "שיבואו איתי ביום ראשון לבית המשפט", - "expected_phonemes": "ʔ i l l i | j iː g u | j oː m | i l ʔ a ħ a d | m a ʕ i | ʕ a l a | i l m a ħ k a m e" - }, - { - "voice_time_seconds": { - "start": 79.2, - "end": 81.4 - }, - "arabic": "ويكونوا حاضرين في القاعة", - "hebrew_transliteration": "וִיכוּנוּ חַאדְ'רִין פִי אִל-קַאעַה", - "hebrew": "ויהיו נוכחים באולם", - "expected_phonemes": "w iː k uː n u | ħ aː dˤ r iː n | f i | i l ʔ aː ʕ a" - }, - { - "voice_time_seconds": { - "start": 81.4, - "end": 85.2 - }, - "arabic": "لمّا رح أقدّم دعوتي قدّامه.»", - "hebrew_transliteration": "לַמַّא רַח-אַקַדֶّם דַעוִתִי קֻדַّאמוֹ.", - "hebrew": "כשאגיש את תביעתי בפניו.", - "expected_phonemes": "l a m m a | r a ħ ʔ a ʔ a d d e m | d a ʕ w i t i | ʔ u d d aː m o" - }, - { - "voice_time_seconds": { - "start": 85.2, - "end": 86.4 - }, - "arabic": "في يوم المحكمة", - "hebrew_transliteration": "פִי יוֹם אִל-מַחכַּמֶה", - "hebrew": "ביום המשפט", - "expected_phonemes": "f i | j oː m | i l m a ħ k a m e" - }, - { - "voice_time_seconds": { - "start": 86.4, - "end": 89.6 - }, - "arabic": "حضر جحا قدّام القاضي في القاعة", - "hebrew_transliteration": "חַדַ'ר גֻ'חַא קֻדַّאם אִל-קַאדִי פִי אִל-קַאעַה", - "hebrew": "ג'חא הופיע בפני השופט באולם", - "expected_phonemes": "ħ a dˤ a r | dʒ u ħ a | ʔ u d d aː m | i l ʔ aː d i | f i | i l ʔ aː ʕ a" - }, - { - "voice_time_seconds": { - "start": 89.6, - "end": 92.2 - }, - "arabic": "وقال: «سيدي القاضي،", - "hebrew_transliteration": "וּקַאל: סִידִי אִל-קַאדִי,", - "hebrew": "ואמר: אדוני השופט,", - "expected_phonemes": "u ʔ aː l | s iː d i | i l ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 92.2, - "end": 94.4 - }, - "arabic": "بدّي أقدّم عندك دعوى،", - "hebrew_transliteration": "בַּדִّי אַקַדֶّם עִנדַכּ דַעוַה,", - "hebrew": "אני רוצה להגיש אצלך תביעה,", - "expected_phonemes": "b a d d i | ʔ a ʔ a d d e m | ʕ i n d a k | d a ʕ w a" - }, - { - "voice_time_seconds": { - "start": 94.4, - "end": 97.6 - }, - "arabic": "وأطلب منّك تعمل العدالة!»", - "hebrew_transliteration": "וּאַטלֹבּ מִנַّכּ תַעמַל אִל-עַדַאלֶה!", - "hebrew": "ואני מבקש ממך לעשות צדק!", - "expected_phonemes": "u ʔ a tˤ l o b | m i n n a k | t a ʕ m a l | i l ʕ a d aː l e" - }, - { - "voice_time_seconds": { - "start": 97.6, - "end": 101.2 - }, - "arabic": "أتطلّع القاضي على جحا وهو متعصّب وسأل:", - "hebrew_transliteration": "אִתטַלַّע אִל-קַאדִי עַלַא גֻ'חַא וּהֻוֶّ מִתעַצֶّבּ וּסַאַל:", - "hebrew": "השופט הסתכל על ג'חא בכעס ושאל:", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | i l ʔ aː d i | ʕ a l a | dʒ u ħ a | u h u w w e | m i t ʕ a sˤ sˤ e b | u s a ʔ a l" - }, - { - "voice_time_seconds": { - "start": 101.2, - "end": 104.0 - }, - "arabic": "«ضدّ مين بدّك تقدّم دعوى؟»", - "hebrew_transliteration": "דִ'דّ מִין בַּדַّכּ תְקַדֶّם דַעוַה?", - "hebrew": "נגד מי אתה רוצה להגיש תביעה?", - "expected_phonemes": "dˤ i d d | m iː n | b a d d a k | t ʔ a d d e m | d a ʕ w a" - }, - { - "voice_time_seconds": { - "start": 104.0, - "end": 108.0 - }, - "arabic": "ردّ جحا: «بدّي أقاضي ربّ العالمين،", - "hebrew_transliteration": "רַדّ גֻ'חַא: בַּדִّי אַקַאדִי רַבّ אִל-עַאלַמִין,", - "hebrew": "ג'חא ענה: אני רוצה לתבוע את ריבון העולמים,", - "expected_phonemes": "r a d d | dʒ u ħ a | b a d d i | ʔ a ʔ aː d i | r a b b | i l ʕ aː l a m iː n" - }, - { - "voice_time_seconds": { - "start": 108.0, - "end": 110.8 - }, - "arabic": "الله سبحانه وتعالى.»", - "hebrew_transliteration": "אַללַה סֻבְּחַאנוֹ וּתַעַאלַא.", - "hebrew": "אלוהים, ישתבח ויתעלה.", - "expected_phonemes": "i l l a | s u b ħ aː n o | u t a ʕ aː l a" - }, - { - "voice_time_seconds": { - "start": 110.8, - "end": 114.2 - }, - "arabic": "القاضي انصدم وعلّى صوته بغضب:", - "hebrew_transliteration": "אִל-קַאדִי אִנצַדַם וּעַלַّא צוֹתוֹ בִּעַ'דַבּ:", - "hebrew": "השופט הזדעזע והרים את קולו בכעס:", - "expected_phonemes": "i l ʔ aː d i | ʔ i n sˤ a d a m | u ʕ a l l a | sˤ oː t o | b i ɣ a d a b" - }, - { - "voice_time_seconds": { - "start": 114.2, - "end": 116.4 - }, - "arabic": "«مين بدّك تقاضي؟!»", - "hebrew_transliteration": "מִין בַּדַّכּ תְקַאדִי?!", - "hebrew": "את מי אתה רוצה לתבוע?!", - "expected_phonemes": "m iː n | b a d d a k | t ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 116.4, - "end": 119.4 - }, - "arabic": "جاوب جحا: «متل ما سمعت.", - "hebrew_transliteration": "גַ'אוַבּ גֻ'חַא: מִתֶל מַא סִמעֶת.", - "hebrew": "ג'חא ענה: כמו ששמעת.", - "expected_phonemes": "dʒ aː w a b | dʒ u ħ a | m i t e l | m a | s i m ʕ e t" - }, - { - "voice_time_seconds": { - "start": 119.4, - "end": 123.2 - }, - "arabic": "بدّي أقاضي عندك ربّنا للعدالة!»", - "hebrew_transliteration": "בַּדִّי אַקַאדִי עִנדַכּ רַבַּנַא לִל-עַדַאלֶה!", - "hebrew": "אני רוצה לתבוע אצלך את אלוהינו לדין!", - "expected_phonemes": "b a d d i | ʔ a ʔ aː d i | ʕ i n d a k | r a b a n a | l i l ʕ a d aː l e" - }, - { - "voice_time_seconds": { - "start": 123.2, - "end": 125.8 - }, - "arabic": "«وشو الموضوع؟» سأل القاضي.", - "hebrew_transliteration": "וּשוּ אִל-מַוְדוּע? סַאַל אִל-קַאדִי.", - "hebrew": "ומה העניין? שאל השופט.", - "expected_phonemes": "u ʃ u | i l m a w dˤ uː ʕ | s a ʔ a l | i l ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 125.8, - "end": 129.2 - }, - "arabic": "ردّ جحا: «عندي اخّين،", - "hebrew_transliteration": "רַדّ גֻ'חַא: עִנדִי אַחַّיִן,", - "hebrew": "ג'חא ענה: יש לי שני אחים,", - "expected_phonemes": "r a d d | dʒ u ħ a | ʕ i n d i | ʔ a ħ ħ a j i n" - }, - { - "voice_time_seconds": { - "start": 129.2, - "end": 132.2 - }, - "arabic": "واحد تاجر سجّاد، رزقته وفيرة", - "hebrew_transliteration": "וַאחַד תַאגֶ'ר סַגַّאד, רִזְקתוֹ וַפִירֶה", - "hebrew": "אחד סוחר שטיחים, פרנסתו שופעת", - "expected_phonemes": "w aː ħ a d | t aː dʒ e r | s a g g aː d | r i z ʔ t o | w a f iː r e" - }, - { - "voice_time_seconds": { - "start": 132.2, - "end": 136.0 - }, - "arabic": "وأولاده معافيين بسمعوا الكلام.", - "hebrew_transliteration": "וּאוֹלַאדוֹ מְעַאפַיִין בִּסמְעוּ אִל-כַּלַאם.", - "hebrew": "וילדיו בריאים ושומעים בקולו.", - "expected_phonemes": "u ʔ oː l aː d o | m ʕ aː f a j iː n | b i s m ʕ u | i l k a l aː m" - }, - { - "voice_time_seconds": { - "start": 136.0, - "end": 139.0 - }, - "arabic": "التاني صاحب مطحنة قمح،", - "hebrew_transliteration": "אִ-תַّאנִי צַאחֶבּ מַטחַנֶת קַמֶח,", - "hebrew": "השני בעל טחנת חיטה,", - "expected_phonemes": "i t t aː n i | sˤ aː ħ e b | m a tˤ ħ a n e t | ʔ a m e ħ" - }, - { - "voice_time_seconds": { - "start": 139.0, - "end": 142.6 - }, - "arabic": "دواليبها بتلفّ من الفجر للغروب.", - "hebrew_transliteration": "דַוַאלִיבְּהַא בִּתלֶף מִן אִל-פַגְ'ר לַל-עֻ'רוּבּ.", - "hebrew": "גלגליה מסתובבים מהשחר ועד השקיעה.", - "expected_phonemes": "d a w aː l iː b h a | b i t l e f | m i n | i l f a dʒ r | l a l ɣ u r uː b" - }, - { - "voice_time_seconds": { - "start": 142.6, - "end": 146.0 - }, - "arabic": "جاري من الجهة الأولى أرضه منيحة وخصبة،", - "hebrew_transliteration": "גַ'ארִי מִן אִל-גִ'הַה אִל-אוּלַא, אַרְד'וֹ מַנִיחַה וּחַ'סְבֶּה,", - "hebrew": "השכן שלי מצד אחד, אדמתו טובה ופורייה,", - "expected_phonemes": "dʒ aː r i | m i n | i l dʒ i h a | i l ʔ uː l a | ʔ a r dˤ o | m a n iː ħ a | u x a s b e" - }, - { - "voice_time_seconds": { - "start": 146.0, - "end": 149.0 - }, - "arabic": "وشفت بعيني كيف البندورة بتكبر عنده", - "hebrew_transliteration": "וּשֻפֶת בְּעֵינִי כִּיף אִל-בַּנדוֹרַה בְּתִכבַּר עִנדוֹ", - "hebrew": "וראיתי במו עיניי איך העגבניות גדלות אצלו", - "expected_phonemes": "u ʃ u f e t | b ʕ eː n i | k iː f | i l b a n d oː r a | b t i k b a r | ʕ i n d o" - }, - { - "voice_time_seconds": { - "start": 149.0, - "end": 152.0 - }, - "arabic": "متل الشّمّام والبطيخ الضّخم،", - "hebrew_transliteration": "מִתֶל אִ-שַّמַّאם וִל-בַּטִּיחַ' אִ-דַّחֶ'ם,", - "hebrew": "כמו מלונים ואבטיחים ענקיים,", - "expected_phonemes": "m i t e l | ʔ i ʃ ʃ a m m aː m | w i l b a tˤ iː x a | ʔ i d d a x e m" - }, - { - "voice_time_seconds": { - "start": 152.0, - "end": 156.4 - }, - "arabic": "لدرجة إنّه السّكينة ممكن توقع وتختفي جوّاها.", - "hebrew_transliteration": "לַדַרַגֶ'ה אִנּוֹ אִ-סִّכִּינֶה מֻמכֶּן תוּקַע וּתְחְ'תַפִי גֻ'וַّאהַא.", - "hebrew": "עד כדי כך שסכין יכולה ליפול ולהיעלם בתוכן.", - "expected_phonemes": "l a d a r a dʒ e | ʔ i n o | ʔ i s s i k iː n e | m u m k e n | t uː ʔ a ʕ | u t x t a f i | dʒ u w w aː h a" - }, - { - "voice_time_seconds": { - "start": 156.4, - "end": 158.2 - }, - "arabic": "وجاري من الجّهة التانية،", - "hebrew_transliteration": "וּגַ'ארִי מִן גִ'הַה אִ-תַّאנְיֶה,", - "hebrew": "ושכני מהצד השני,", - "expected_phonemes": "u dʒ aː r i | m i n | dʒ i h a | ʔ i t t aː n j e" - }, - { - "voice_time_seconds": { - "start": 158.2, - "end": 162.0 - }, - "arabic": "ورث من أبوه بستان شجر فواكه مثمرة،", - "hebrew_transliteration": "וִרֶת מִן אַבּוּה בֻּסתַאן שַגַ'ר פַוַאכֶּה מֻתמִרַה,", - "hebrew": "ירש מאביו פרדס של עצי פרי נושאי פרי,", - "expected_phonemes": "w i r e t | m i n | ʔ a b uː h | b u s t aː n | ʃ a dʒ a r | f a w aː k e | m u t m i r a" - }, - { - "voice_time_seconds": { - "start": 162.0, - "end": 164.4 - }, - "arabic": "وما بقيش عليه غير إنّه يقطف", - "hebrew_transliteration": "וּמַא בִּקִיש עַלֵיה עֵ'יר אִנּוֹ יֻקטֹף", - "hebrew": "ולא נשאר לו אלא לקטוף", - "expected_phonemes": "u m a | b i ʔ iː ʃ | ʕ a l eː h | ɣ eː r | ʔ i n o | j u ʔ tˤ o f" - }, - { - "voice_time_seconds": { - "start": 164.4, - "end": 166.4 - }, - "arabic": "الفواكه في موسمها.", - "hebrew_transliteration": "אִל-פַוַאכֶּה פִי מוֹסִמְהַא.", - "hebrew": "את הפירות בעונתם.", - "expected_phonemes": "i l f a w aː k e | f i | m oː s i m h a" - }, - { - "voice_time_seconds": { - "start": 166.4, - "end": 171.0 - }, - "arabic": "وأنا؟ أبوي وإمّي رجّعوله أرواحهم وأنا صغير،", - "hebrew_transliteration": "וּאַנַא? אַבּוּי וּאִמִّי רַגַّעוּ-לוֹ אַרוַאחְהֹם וּאַנַא זְעִ'יר,", - "hebrew": "ואני? אבי ואמי השיבו לו את נשמותיהם כשהייתי קטן,", - "expected_phonemes": "u ʔ a n a | ʔ a b uː j | u ʔ i m m i | r a g g a ʕ uː l o | ʔ a r w aː ħ h o m | u ʔ a n a | z ɣ iː r" - }, - { - "voice_time_seconds": { - "start": 171.0, - "end": 175.0 - }, - "arabic": "شقفة الأرض إلّي خلّولي إيّاها صخريّة ناشفة،", - "hebrew_transliteration": "שֻקפֶת אִל-אַרְד' אִלִّי חַלַّוּ-לִי אִיַّאהַא צַחְ'רִיֶּה נַאשְפֶה,", - "hebrew": "חלקת האדמה שהשאירו לי סלעית ויבשה,", - "expected_phonemes": "ʃ u ʔ f e t | i l ʔ a r dˤ | ʔ i l l i | ħ a l l a w l i | ʔ i j j aː h a | sˤ a x r i j e | n aː ʃ f e" - }, - { - "voice_time_seconds": { - "start": 175.0, - "end": 176.8 - }, - "arabic": "ومش نافعة لإشي.", - "hebrew_transliteration": "וּמִש נַאפְעַה לַאִשִי.", - "hebrew": "ולא שווה כלום.", - "expected_phonemes": "u m i ʃ | n aː f ʕ a | l a ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 176.8, - "end": 179.2 - }, - "arabic": "عندي عنزة واحدة عجوزة،", - "hebrew_transliteration": "עִנדִי עַנְזֶה וַאחדֶה עַגוּזֶה,", - "hebrew": "יש לי עז אחת זקנה,", - "expected_phonemes": "ʕ i n d i | ʕ a n z e | w aː ħ d e | ʕ a g uː z e" - }, - { - "voice_time_seconds": { - "start": 179.2, - "end": 181.8 - }, - "arabic": "ما بتعطيش ولا نقطة حليب.", - "hebrew_transliteration": "מַא בְּתַעטִיש וְלַא נֻקטֶת חַלִיבּ.", - "hebrew": "שלא נותנת אף טיפת חלב.", - "expected_phonemes": "m a | b t a ʕ tˤ iː ʃ | u l aː | n u ʔ tˤ e t | ħ a l iː b" - }, - { - "voice_time_seconds": { - "start": 181.8, - "end": 184.4 - }, - "arabic": "وبيتي بدّه يوقع.", - "hebrew_transliteration": "וּבֵּיתִי בַּדּוֹ יוּקַע.", - "hebrew": "וביתי עומד ליפול.", - "expected_phonemes": "u b eː t i | b a d o | j uː ʔ a ʕ" - }, - { - "voice_time_seconds": { - "start": 184.4, - "end": 186.8 - }, - "arabic": "وليش أعطى لكلّ الباقي بوفرة،", - "hebrew_transliteration": "וּלֵיש אַעטַא לַכֻּלّ אִל-בַּאקִי בְּוַפרַה,", - "hebrew": "ולמה הוא נתן לכל השאר בשפע,", - "expected_phonemes": "u l eː ʃ | ʔ a ʕ tˤ a | l a k u l l | i l b aː ʔ i | b w a f r a" - }, - { - "voice_time_seconds": { - "start": 186.8, - "end": 189.4 - }, - "arabic": "بسّ أنا خلّاني فقير جوعان؟", - "hebrew_transliteration": "בַּס אַנַא חַלַّאנִי פַקִיר גוּעַאן?", - "hebrew": "ורק אותי השאיר עני ורעב?", - "expected_phonemes": "b a s | ʔ a n a | ħ a l l aː n i | f a ʔ iː r | g uː ʕ aː n" - }, - { - "voice_time_seconds": { - "start": 189.4, - "end": 191.4 - }, - "arabic": "هدا عدل؟", - "hebrew_transliteration": "הַאדַא עַדֶל?", - "hebrew": "זה צדק?", - "expected_phonemes": "h aː d a | ʕ a d e l" - }, - { - "voice_time_seconds": { - "start": 191.4, - "end": 195.8 - }, - "arabic": "القاضي سكّر عينيه شويّ وصعّب الأمور:", - "hebrew_transliteration": "אִל-קַאדִי סַכַּר עֵינֵיה שְוַיّ וּצַעַّבּ אִל-אֻמוּר:", - "hebrew": "השופט עצם את עיניו לרגע והקשה:", - "expected_phonemes": "i l ʔ aː d i | s a k a r | ʕ eː n eː h | ʃ w a j j | u sˤ a ʕ ʕ a b | i l ʔ u m uː r" - }, - { - "voice_time_seconds": { - "start": 195.8, - "end": 197.4 - }, - "arabic": "«بسّ كلّ إشي عم تحكيه،", - "hebrew_transliteration": "בַּס כֻּלّ אִשִי עַם-תִחכִּיה,", - "hebrew": "אבל כל מה שאתה מספר,", - "expected_phonemes": "b a s | k u l l | ʔ i ʃ i | ʕ a m t i ħ k iː h" - }, - { - "voice_time_seconds": { - "start": 197.4, - "end": 199.4 - }, - "arabic": "صار قبل كتير وقت،", - "hebrew_transliteration": "צַאר קַבֶּל כְּתִיר וַקֶת,", - "hebrew": "קרה לפני הרבה זמן,", - "expected_phonemes": "sˤ aː r | ʔ a b e l | k t iː r | w a ʔ e t" - }, - { - "voice_time_seconds": { - "start": 199.4, - "end": 201.4 - }, - "arabic": "ليش استنّيت لهلّق؟", - "hebrew_transliteration": "לֵיש אִסתַנֵّית לַהַלַّק?", - "hebrew": "למה חיכית עד עכשיו?", - "expected_phonemes": "l eː ʃ | ʔ i s t a n n eː t | l a h a l l a ʔ" - }, - { - "voice_time_seconds": { - "start": 201.4, - "end": 205.8 - }, - "arabic": "ليش ما قدّمت دعوتك لمّا صارت الأمور؟»", - "hebrew_transliteration": "לֵיש מַא קַדַّמֶת דַעוִתַכּ לַמַّא צַארַת אִל-אֻמוּר?", - "hebrew": "למה לא הגשת את תביעתך כשהדברים קרו?", - "expected_phonemes": "l eː ʃ | m a | ʔ a d d a m e t | d a ʕ w i t a k | l a m m a | sˤ aː r a t | i l ʔ u m uː r" - }, - { - "voice_time_seconds": { - "start": 205.8, - "end": 207.4 - }, - "arabic": "ردّ جحا:", - "hebrew_transliteration": "רַדّ גֻ'חַא:", - "hebrew": "ג'חא ענה:", - "expected_phonemes": "r a d d | dʒ u ħ a" - }, - { - "voice_time_seconds": { - "start": 207.4, - "end": 210.6 - }, - "arabic": "«وكيف كان بقدر أقدّم دعوتي قبل؟", - "hebrew_transliteration": "וּכִּיף כַּאן בַּקדַר אַקַדֶّם דַעוִתִי קַבֶּל?", - "hebrew": "ואיך יכולתי להגיש את תביעתי קודם?", - "expected_phonemes": "u k iː f | k aː n | b a ʔ d a r | ʔ a ʔ a d d e m | d a ʕ w i t i | ʔ a b e l" - }, - { - "voice_time_seconds": { - "start": 210.6, - "end": 212.6 - }, - "arabic": "كلّ القضاة إلّي كانوا قبلك", - "hebrew_transliteration": "כֻּלّ אִל-קֻדַ'אה אִלִّי כַּאנוּ קַבְּלַכּ", - "hebrew": "כל השופטים שהיו לפניך", - "expected_phonemes": "k u l l | i l ʔ u dˤ aː h | ʔ i l l i | k aː n u | ʔ a b l a k" - }, - { - "voice_time_seconds": { - "start": 212.6, - "end": 215.0 - }, - "arabic": "ما كانوش بيقدروا يساعدوني،", - "hebrew_transliteration": "מַא כַּאנוּש בִּיקדְרוּ יְסַאעדוּנִי,", - "hebrew": "לא יכלו לעזור לי,", - "expected_phonemes": "m a | k aː n uː ʃ | b iː ʔ d r u | j s aː ʕ d uː n i" - }, - { - "voice_time_seconds": { - "start": 215.0, - "end": 218.2 - }, - "arabic": "عشان كلّهم كانوا يخافوا منّه!", - "hebrew_transliteration": "עַשַאן כֻּלְّהֹם כַּאנוּ יְחַ'אפוּ מִנּוֹ!", - "hebrew": "כי כולם פחדו ממנו!", - "expected_phonemes": "ʕ a ʃ aː n | k u l l h o m | k aː n u | j x aː f u | m i n o" - }, - { - "voice_time_seconds": { - "start": 218.2, - "end": 220.4 - }, - "arabic": "بسّ من وقت ما إنت أجيت،", - "hebrew_transliteration": "בַּס מִן וַקת מַא אִנתֶ אִגִ'ית,", - "hebrew": "אבל מאז שאתה הגעת,", - "expected_phonemes": "b a s | m i n | w a ʔ t | m a | ʔ i n t e | ʔ i dʒ iː t" - }, - { - "voice_time_seconds": { - "start": 220.4, - "end": 223.6 - }, - "arabic": "بلّشت أسمع كلّ السّكّان بحكوا", - "hebrew_transliteration": "בַּלַّשֶת אַסמַע כֻּלّ אִ-סֻّכַּאן בִּחכּוּ", - "hebrew": "התחלתי לשמוע את כל התושבים אומרים", - "expected_phonemes": "b a l l a ʃ e t | ʔ a s m a ʕ | k u l l | ʔ i s s u k aː n | b i ħ k u" - }, - { - "voice_time_seconds": { - "start": 223.6, - "end": 227.4 - }, - "arabic": "إنّه وصل على البلد قاضي ما بخاف الله،", - "hebrew_transliteration": "אִנּוֹ וְצֶל עַלַא אִל-בַּלַד קַאדִי מַא בִּחַ'אף אַללַה,", - "hebrew": "שהגיע לעיירה שופט שלא מפחד מאלוהים,", - "expected_phonemes": "ʔ i n o | u sˤ e l | ʕ a l a | i l b a l a d | ʔ aː d i | m a | b i x aː f | i l l a" - }, - { - "voice_time_seconds": { - "start": 227.4, - "end": 230.2 - }, - "arabic": "وعشان هيك استعجلت أجي", - "hebrew_transliteration": "וּעַשַאן הֵיכּ אִסתַעגַלֶת אַגִ'י", - "hebrew": "ולכן מיהרתי לבוא", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | ʔ i s t a ʕ g a l e t | ʔ a dʒ i" - }, - { - "voice_time_seconds": { - "start": 230.2, - "end": 235.2 - }, - "arabic": "وأقدّم دعوتي.", - "hebrew_transliteration": "וּאַקַדֶّם דַעוִתִי.", - "hebrew": "ולהגיש את תביעתי.", - "expected_phonemes": "u ʔ a ʔ a d d e m | d a ʕ w i t i" - } - ] - }, - { - "file_name": "איך הגיע הקפה לעולם? * כִּיף אַגַ'ת אל-קַהוֶה עַלַא אל-עַאלַם? * كيف أجت القهوة على العالم? [mZJDfuABhz4].webm", - "youtube_id": "mZJDfuABhz4", - "title_hebrew": "איך הגיע הקפה לעולם", - "segments": [ - { - "voice_time_seconds": { - "start": 2.0, - "end": 8.2 - }, - "arabic": "كيف أجت القهوة على العالم", - "hebrew_transliteration": "כִּיף אַגַ'ת אִל-קַהוֶה עַלַא אִל-עַאלַם", - "hebrew": "איך הגיע הקפה לעולם", - "expected_phonemes": "k iː f | ʔ a dʒ a t | i l ʔ a h w e | ʕ a l a | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 8.2, - "end": 12.4 - }, - "arabic": "بحكوا، بسّ الله بعلم إذا حقيقة أو لا،", - "hebrew_transliteration": "בִּחכּוּ, בַּס אַללַה בִּעלַם אִזַא חַקִיקַה אַו לַא,", - "hebrew": "מספרים, אבל רק אלוהים יודע אם זה נכון או לא,", - "expected_phonemes": "b i ħ k u | b a s | i l l a | b i ʕ l a m | ʔ i z a | ħ a ʔ iː ʔ a | ʔ a w | l aː" - }, - { - "voice_time_seconds": { - "start": 12.4, - "end": 14.4 - }, - "arabic": "إنّه بواحد من الأيّام", - "hebrew_transliteration": "אִנּוֹ בְּוַאחַד מִן אִל-אַיַّאם", - "hebrew": "שיום אחד", - "expected_phonemes": "ʔ i n o | b w aː ħ a d | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 14.4, - "end": 18.2 - }, - "arabic": "كان الملك سليمان قاعد على عرشه في القدس.", - "hebrew_transliteration": "כַּאן אִל-מַלֶכּ סֻלֵימַאן קַאעֶד עַלַא עַרשוֹ פִי אִל-קֻדֶס.", - "hebrew": "המלך שלמה ישב על כס מלכותו בירושלים.", - "expected_phonemes": "k aː n | i l m a l e k | s u l eː m aː n | ʔ aː ʕ e d | ʕ a l a | ʕ a r ʃ o | f i | i l ʔ u d e s" - }, - { - "voice_time_seconds": { - "start": 18.2, - "end": 22.2 - }, - "arabic": "لمّا فجأة ظهر قدّامه الملاك جبرائيل،", - "hebrew_transliteration": "לַמַّא פַגְ'אַה טַ'הַר קֻדַّאמוֹ אִל-מַלַאכּ גִ'בְּרַאאִיל,", - "hebrew": "כשלפתע הופיע לפניו המלאך גבריאל,", - "expected_phonemes": "l a m m a | f a dʒ ʔ a | ðˤ a h a r | ʔ u d d aː m o | i l m a l aː k | dʒ i b r aː ʔ iː l" - }, - { - "voice_time_seconds": { - "start": 22.2, - "end": 25.6 - }, - "arabic": "فتح قدّامه كتاب وقالّه:", - "hebrew_transliteration": "פַתַח קֻדַّאמוֹ כְּתַאבּ וּקַאל-לוֹ:", - "hebrew": "פתח לפניו ספר ואמר לו:", - "expected_phonemes": "f a t a ħ | ʔ u d d aː m o | k t aː b | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 25.6, - "end": 30.2 - }, - "arabic": "«سيدي الملك، كون على علم إنّك بعد أسبوعين", - "hebrew_transliteration": "סִידִי אִל-מַלֶכּ, כּוּן עַלַא עִלֶם אִנַّכּ בַּעֶד אֻסבּוּעֵין", - "hebrew": "אדוני המלך, דע לך שבעוד שבועיים", - "expected_phonemes": "s iː d i | i l m a l e k | k uː n | ʕ a l a | ʕ i l e m | ʔ i n n a k | b a ʕ e d | ʔ u s b uː ʕ eː n" - }, - { - "voice_time_seconds": { - "start": 30.2, - "end": 33.0 - }, - "arabic": "إنت مطلوب للعالم إلّي فوق.", - "hebrew_transliteration": "אִנתֶ מַטלוּבּ לַל-עַאלַם אִלִّי פוֹק.", - "hebrew": "אתה נקרא לעולם העליון.", - "expected_phonemes": "ʔ i n t e | m a tˤ l uː b | l a l ʕ aː l a m | ʔ i l l i | f oː ʔ" - }, - { - "voice_time_seconds": { - "start": 33.0, - "end": 36.0 - }, - "arabic": "خلصت أيّامك على الأرض!»", - "hebrew_transliteration": "חִ'לצַת אַיַّאמַכּ עַלַא אִל-אַרְד'!", - "hebrew": "ימיך על האדמה תמו!", - "expected_phonemes": "x i l sˤ a t | ʔ a j j aː m a k | ʕ a l a | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 36.0, - "end": 41.0 - }, - "arabic": "قالّه: «مين؟ أنا؟ الملك سليمان؟", - "hebrew_transliteration": "קַאל-לוֹ: מִין? אַנַא? אִל-מַלֶכּ סֻלֵימַאן?", - "hebrew": "הוא אמר לו: מי? אני? המלך שלמה?", - "expected_phonemes": "ʔ aː l l o | m iː n | ʔ a n a | i l m a l e k | s u l eː m aː n" - }, - { - "voice_time_seconds": { - "start": 41.0, - "end": 43.4 - }, - "arabic": "الأذكى من كلّ البشر؟", - "hebrew_transliteration": "אִל-אַד'כַּא מִן כֻּלّ אִל-בַּשַר?", - "hebrew": "החכם מכל בני האדם?", - "expected_phonemes": "i l ʔ a dˤ k a | m i n | k u l l | i l b a ʃ a r" - }, - { - "voice_time_seconds": { - "start": 43.4, - "end": 45.8 - }, - "arabic": "أنا لازم أترك هدا العالم", - "hebrew_transliteration": "אַנַא לַאזֶם אַתרֻכּ הַאדַא אִל-עַאלַם", - "hebrew": "אני צריך לעזוב את העולם הזה", - "expected_phonemes": "ʔ a n a | l aː z e m | ʔ a t r u k | h aː d a | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 45.8, - "end": 49.0 - }, - "arabic": "وأنتقل على العالم إلّي كلّه خير؟»", - "hebrew_transliteration": "וּאַנתַקֶל עַלַא אִל-עַאלַם אִלִّי כֻּלّוֹ חֵ'יר?", - "hebrew": "ולעבור לעולם שכולו טוב?", - "expected_phonemes": "u ʔ a n t a ʔ e l | ʕ a l a | i l ʕ aː l a m | ʔ i l l i | k u l l o | x eː r" - }, - { - "voice_time_seconds": { - "start": 49.0, - "end": 54.0 - }, - "arabic": "ردّ عليه: «ملك أو مسكين، ذكي أو غبي،", - "hebrew_transliteration": "רַדّ עַלֵיה: מַלֶכּ אַו מַסכִּין, דַ'כִּי אַו עַ'בִּי,", - "hebrew": "הוא ענה לו: מלך או מסכן, חכם או טיפש,", - "expected_phonemes": "r a d d | ʕ a l eː h | m a l e k | ʔ a w | m a s k iː n | dˤ a k i | ʔ a w | ɣ a b i" - }, - { - "voice_time_seconds": { - "start": 54.0, - "end": 58.2 - }, - "arabic": "غني أو فقير - هدا مصير كلّ بني آدم.", - "hebrew_transliteration": "עַ'נִי אַו פַקִיר - הַאדַא מַצִיר כֻּלّ בַּנִי אַאדַם.", - "hebrew": "עשיר או עני - זה גורלו של כל אדם.", - "expected_phonemes": "ɣ a n i | ʔ a w | f a ʔ iː r | h aː d a | m a sˤ iː r | k u l l | b a n i | ʔ aː d a m" - }, - { - "voice_time_seconds": { - "start": 58.2, - "end": 61.4 - }, - "arabic": "إنت ملك، إحنا منعرف إنّه مش سهل عليك", - "hebrew_transliteration": "אִנתֶ מַלֶכּ, אִחנַא מִנִערַף אִנּוֹ מִש סַהֶל עַלֵיכּ", - "hebrew": "אתה מלך, אנחנו יודעים שלא קל לך", - "expected_phonemes": "ʔ i n t e | m a l e k | ʔ i ħ n a | m i n i ʕ r a f | ʔ i n o | m i ʃ | s a h e l | ʕ a l eː k" - }, - { - "voice_time_seconds": { - "start": 61.4, - "end": 63.0 - }, - "arabic": "تترك هيك فجأة،", - "hebrew_transliteration": "תִתרֻכּ הֵיכּ פַגְ'אַה,", - "hebrew": "לעזוב כך פתאום,", - "expected_phonemes": "t i t r u k | h eː k | f a dʒ ʔ a" - }, - { - "voice_time_seconds": { - "start": 63.0, - "end": 65.4 - }, - "arabic": "فيه عندك كتير أمور تخلّصها،", - "hebrew_transliteration": "פִיה עִנדַכּ כְּתִיר אֻמוּר תְחַ'לֶّצְהַא,", - "hebrew": "יש לך הרבה עניינים לסיים,", - "expected_phonemes": "f iː h | ʕ i n d a k | k t iː r | ʔ u m uː r | t x a l l e sˤ h a" - }, - { - "voice_time_seconds": { - "start": 65.4, - "end": 69.0 - }, - "arabic": "لهيك عمنعطيك تحذير قبل بأسبوعين.", - "hebrew_transliteration": "לַהֵיכּ עַם-נַעטִיכּ תַחדִ'יר קַבֶּל בְּאֻסבּוּעֵין.", - "hebrew": "לכן אנחנו נותנים לך התראה שבועיים מראש.", - "expected_phonemes": "l a h eː k | ʕ a m n a ʕ tˤ iː k | t a ħ dˤ iː r | ʔ a b e l | b ʔ u s b uː ʕ eː n" - }, - { - "voice_time_seconds": { - "start": 69.0, - "end": 71.0 - }, - "arabic": "تقبّل الموضوع بتفهّم", - "hebrew_transliteration": "תְקַבַّל אִל-מַוְדוּע בְּתַפַהֻّם", - "hebrew": "קבל את העניין בהבנה", - "expected_phonemes": "t ʔ a b b a l | i l m a w dˤ uː ʕ | b t a f a h h u m" - }, - { - "voice_time_seconds": { - "start": 71.0, - "end": 74.2 - }, - "arabic": "عشان هاي هيّ طريقة العالم.»", - "hebrew_transliteration": "עַשַאן הַאי הִיֶّ טַרִיקֶת אִל-עַאלַם.", - "hebrew": "כי זו דרכו של עולם.", - "expected_phonemes": "ʕ a ʃ aː n | h aː i | h i j j e | tˤ a r iː ʔ e t | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 74.2, - "end": 77.8 - }, - "arabic": "بعدها سكّر الكتاب وقالّه:", - "hebrew_transliteration": "בַּעֶדהַא סַכַּר לֶ-כְּתַאבּ וּקַאל-לוֹ:", - "hebrew": "אחר כך סגר את הספר ואמר לו:", - "expected_phonemes": "b a ʕ e d h a | s a k a r | l e k t aː b | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 77.8, - "end": 82.0 - }, - "arabic": "«بيني وبينك، إذا هالقدّ مهمّ إلك تكمّل تعيش -", - "hebrew_transliteration": "בֵּינִי וּבֵּינַכּ, אִזַא הַל-קַדّ מֻהֶם אִלַכּ תְכַּמֶّל תְעִיש -", - "hebrew": "בינינו, אם כל כך חשוב לך להמשיך לחיות -", - "expected_phonemes": "b eː n i | u b eː n a k | ʔ i z a | h a l ʔ a d d | m u h e m | ʔ i l a k | t k a m m e l | t ʕ iː ʃ" - }, - { - "voice_time_seconds": { - "start": 82.0, - "end": 85.0 - }, - "arabic": "فيه بالجنّة نبعة ميّة الحياة.", - "hebrew_transliteration": "פִיה בִּ-גַ'נֶّה נַבְּעֶת מַיֶּת אִל-חַיַאה.", - "hebrew": "יש בגן העדן מעיין מי החיים.", - "expected_phonemes": "f iː h | b i dʒ a n n e | n a b ʕ e t | m a j e t | i l ħ a j aː" - }, - { - "voice_time_seconds": { - "start": 85.0, - "end": 88.2 - }, - "arabic": "كلّ مين بشرب منها بعيش للأبد.", - "hebrew_transliteration": "כֻּלّ מִין בִּשרַבּ מִנְהַא בִּעִיש לַל-אַבַּד.", - "hebrew": "כל מי ששותה ממנו חי לנצח.", - "expected_phonemes": "k u l l | m iː n | b i ʃ r a b | m i n h a | b i ʕ iː ʃ | l a l ʔ a b a d" - }, - { - "voice_time_seconds": { - "start": 88.2, - "end": 90.2 - }, - "arabic": "إذا هدا إلّي بدّك إيّاه،", - "hebrew_transliteration": "אִזַא הַאדַא אִלִّי בַּדַّכּ אִיַّאה,", - "hebrew": "אם זה מה שאתה רוצה,", - "expected_phonemes": "ʔ i z a | h aː d a | ʔ i l l i | b a d d a k | ʔ i j j aː h" - }, - { - "voice_time_seconds": { - "start": 90.2, - "end": 93.4 - }, - "arabic": "لاقي من هاي الميّ وإشربها.»", - "hebrew_transliteration": "לַאקִי מִן הַאי אִל-מַיّ וּאִשרַבְּהַא.", - "hebrew": "מצא מהמים האלה ושתה אותם.", - "expected_phonemes": "l aː ʔ i | m i n | h aː i | i l m a j j | u ʔ i ʃ r a b h a" - }, - { - "voice_time_seconds": { - "start": 93.4, - "end": 97.0 - }, - "arabic": "هيك قال وراح بطريقه.", - "hebrew_transliteration": "הֵיכּ קַאל וּרַאח בְּטַרִיקוֹ.", - "hebrew": "כך אמר והלך לדרכו.", - "expected_phonemes": "h eː k | ʔ aː l | u r aː ħ | b tˤ a r iː ʔ o" - }, - { - "voice_time_seconds": { - "start": 97.0, - "end": 101.0 - }, - "arabic": "الملك سليمان ما ضيّعش ولا حتّى لحظة.", - "hebrew_transliteration": "אִל-מַלֶכּ סֻלֵימַאן מַא דַ'יַّעש וְלַא חַתַّא לַחזַה.", - "hebrew": "המלך שלמה לא בזבז אפילו רגע.", - "expected_phonemes": "i l m a l e k | s u l eː m aː n | m a | dˤ a j j a ʕ ʃ | u l aː | ħ a t t a | l a ħ ðˤ a" - }, - { - "voice_time_seconds": { - "start": 101.0, - "end": 103.4 - }, - "arabic": "ومعروف عنّه إنّه كان يعرف", - "hebrew_transliteration": "וּמַערוּף עַנּוֹ אִנּוֹ כַּאן יִערַף", - "hebrew": "וידוע עליו שהוא ידע", - "expected_phonemes": "u m a ʕ r uː f | ʕ a n o | ʔ i n o | k aː n | j i ʕ r a f" - }, - { - "voice_time_seconds": { - "start": 103.4, - "end": 105.8 - }, - "arabic": "لغة الحيوانات والطّيور.", - "hebrew_transliteration": "לֻעַ'ת אִל-חַיַוַאנַאת וִ-טֻّיוּר.", - "hebrew": "את שפת החיות והציפורים.", - "expected_phonemes": "l u ɣ a t | i l ħ a j a w aː n aː t | w i tˤ tˤ u j uː r" - }, - { - "voice_time_seconds": { - "start": 105.8, - "end": 107.6 - }, - "arabic": "فدغري عمل إجتماع", - "hebrew_transliteration": "פַדֻעְ'רִי עִמֶל אִגְ'תִמַאע", - "hebrew": "אז מיד ערך אסיפה", - "expected_phonemes": "f a d u ɣ r i | ʕ i m e l | ʔ i dʒ t i m aː ʕ" - }, - { - "voice_time_seconds": { - "start": 107.6, - "end": 110.8 - }, - "arabic": "لكلّ الكائنات الحيّة في العالم:", - "hebrew_transliteration": "לַכֻּלّ אִל-כַּאאִנַאת אִל-חַיֶّה פִי אִל-עַאלַם:", - "hebrew": "לכל היצורים החיים בעולם:", - "expected_phonemes": "l a k u l l | i l k aː ʔ i n aː t | i l ħ a j j e | f i | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 110.8, - "end": 114.6 - }, - "arabic": "فيلة وأسود، دبّبة وحمير الوحش،", - "hebrew_transliteration": "פִילֶה וּאֻסוּד, דִבַּבֶּה וּחַמִיר אִל-וַחש,", - "hebrew": "פילים ואריות, דובים וזברות,", - "expected_phonemes": "f iː l e | u ʔ u s uː d | d i b a b e | u ħ a m iː r | i l w a ħ ʃ" - }, - { - "voice_time_seconds": { - "start": 114.6, - "end": 118.8 - }, - "arabic": "نمور وبقر، حصونة وجمال، خرفان وبطّ -", - "hebrew_transliteration": "נֻמוּר וּבַּקַר, חְסוּנֶה וּגְ'מַאל, חִ'רפַאן וּבַּטّ -", - "hebrew": "נמרים ופרות, סוסים וגמלים, כבשים וברווזים -", - "expected_phonemes": "n u m uː r | u b a ʔ a r | ħ s uː n e | u dʒ m aː l | x i r f aː n | u b a tˤ tˤ" - }, - { - "voice_time_seconds": { - "start": 118.8, - "end": 121.0 - }, - "arabic": "الكلّ تجمّع وأجا،", - "hebrew_transliteration": "אִל-כֻּלّ תְגַ'מַّע וּאַגַ'א,", - "hebrew": "כולם התאספו ובאו,", - "expected_phonemes": "i l k u l l | t dʒ a m m a ʕ | u ʔ a dʒ a" - }, - { - "voice_time_seconds": { - "start": 121.0, - "end": 124.8 - }, - "arabic": "ووقّفوا قدّامه على شكل نصّ دائرة.", - "hebrew_transliteration": "וּוַקַّפוּ קֻדַّאמוֹ עַלַא שַכֶּל נֻצّ דַאאִירַה.", - "hebrew": "ועמדו לפניו בצורת חצי עיגול.", - "expected_phonemes": "u w a ʔ ʔ a f u | ʔ u d d aː m o | ʕ a l a | ʃ a k e l | n u sˤ sˤ | d aː ʔ iː r a" - }, - { - "voice_time_seconds": { - "start": 124.8, - "end": 128.6 - }, - "arabic": "قالّهم: «مين منكم يا حيوانات،", - "hebrew_transliteration": "קַאל-לְהֹם: מִין מִנְכֹּם יַא חַיַוַאנַאת,", - "hebrew": "הוא אמר להם: מי מכם, חיות,", - "expected_phonemes": "ʔ aː l l h o m | m iː n | m i n k o m | j a | ħ a j a w aː n aː t" - }, - { - "voice_time_seconds": { - "start": 128.6, - "end": 130.8 - }, - "arabic": "بعمل معروف مع ملكه", - "hebrew_transliteration": "בִּעמַל מַערוּף מַע מַלְכּוֹ", - "hebrew": "יעשה טובה למלכו", - "expected_phonemes": "b i ʕ m a l | m a ʕ r uː f | m a ʕ | m a l k o" - }, - { - "voice_time_seconds": { - "start": 130.8, - "end": 135.2 - }, - "arabic": "وبجيبله من ميّة الحياة الموجودة في الجنّة؟»", - "hebrew_transliteration": "וּבִּגִ'יבּ-לוֹ מִן מַיֶּת אִל-חַיַאה אִל-מַוְגּ'וּדֶה פִי אִ-גַّנֶّה?", - "hebrew": "ויביא לו ממי החיים שנמצאים בגן העדן?", - "expected_phonemes": "u b i dʒ iː b l o | m i n | m a j e t | i l ħ a j aː | i l m a w dʒ uː d e | f i | ʔ i g g a n n e" - }, - { - "voice_time_seconds": { - "start": 135.2, - "end": 137.2 - }, - "arabic": "قام النّسر الكبير وقال:", - "hebrew_transliteration": "קַאם אִ-נַّסֶר לֶ-כְּבִּיר וּקַאל:", - "hebrew": "הנשר הגדול קם ואמר:", - "expected_phonemes": "ʔ aː m | ʔ i n n a s e r | l e k b iː r | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 137.2, - "end": 140.8 - }, - "arabic": "«سيدي الملك، أنا بجيبلك منها!»", - "hebrew_transliteration": "סִידִי אִל-מַלֶכּ, אַנַא בַּגִ'יבּ-לַכּ מִנְהַא!", - "hebrew": "אדוני המלך, אני אביא לך מהם!", - "expected_phonemes": "s iː d i | i l m a l e k | ʔ a n a | b a dʒ iː b l a k | m i n h a" - }, - { - "voice_time_seconds": { - "start": 140.8, - "end": 144.6 - }, - "arabic": "أعطاه الملك كاس كبير فخم من الذّهب.", - "hebrew_transliteration": "אַעטַאה אִל-מַלֶכּ כַּאס כְּבִּיר פַחֶ'ם מִן אִ-דַّהַבּ.", - "hebrew": "המלך נתן לו גביע גדול ומפואר מזהב.", - "expected_phonemes": "ʔ a ʕ tˤ aː h | i l m a l e k | k aː s | k b iː r | f a x e m | m i n | ʔ i d d a h a b" - }, - { - "voice_time_seconds": { - "start": 144.6, - "end": 149.4 - }, - "arabic": "تمسّك النّسر فيه بمخالبه وحلّق للسّما.", - "hebrew_transliteration": "תְמַסַّכּ אִ-נַّסֶר פִיה בְּמַחַ'אלבּוֹ וּחַלַّק לַ-סַّמַא.", - "hebrew": "הנשר אחז בו בטפריו והמריא לשמיים.", - "expected_phonemes": "t m a s s a k | ʔ i n n a s e r | f iː h | b m a x aː l b o | u ħ a l l a ʔ | l a s s a m a" - }, - { - "voice_time_seconds": { - "start": 149.4, - "end": 154.2 - }, - "arabic": "النّسر طار تلت أيام وتلت ليالي،", - "hebrew_transliteration": "אִ-נַّסֶר טַאר תַלַת אַיַּאם וּתַלַת לַיַאלִי,", - "hebrew": "הנשר עף שלושה ימים ושלושה לילות,", - "expected_phonemes": "ʔ i n n a s e r | tˤ aː r | t a l a t | ʔ a j aː m | u t a l a t | l a j aː l i" - }, - { - "voice_time_seconds": { - "start": 154.2, - "end": 157.0 - }, - "arabic": "فوق الجبال وفوق البقاع،", - "hebrew_transliteration": "פוֹק לֶ-גְ'בַּאל וּפוֹק אִל-בְּקַאע,", - "hebrew": "מעל ההרים ומעל הבקעות,", - "expected_phonemes": "f oː ʔ | l e dʒ b aː l | u f oː ʔ | i l b ʔ aː ʕ" - }, - { - "voice_time_seconds": { - "start": 157.0, - "end": 159.4 - }, - "arabic": "فوق القرى وفوق الوديان،", - "hebrew_transliteration": "פוֹק אִל-קֻרַא וּפוֹק אִל-וִדיַאן,", - "hebrew": "מעל הכפרים ומעל העמקים,", - "expected_phonemes": "f oː ʔ | i l ʔ u r a | u f oː ʔ | i l w i d j aː n" - }, - { - "voice_time_seconds": { - "start": 159.4, - "end": 162.8 - }, - "arabic": "لحدّ ما وصل لبواب الجنّة.", - "hebrew_transliteration": "לַחַדّ מַא וְצֶל לַבְּוַאבּ אִ-גַّנֶّה.", - "hebrew": "עד שהגיע לשערי גן העדן.", - "expected_phonemes": "l a ħ a d d | m a | u sˤ e l | l a b w aː b | ʔ i g g a n n e" - }, - { - "voice_time_seconds": { - "start": 162.8, - "end": 165.2 - }, - "arabic": "شفطله من ميّة النّبعة،", - "hebrew_transliteration": "שַפַט-לוֹ מִן מַיֶּת אִ-נַّבְּעַה,", - "hebrew": "שאב לו ממי המעיין,", - "expected_phonemes": "ʃ a f a tˤ l o | m i n | m a j e t | ʔ i n n a b ʕ a" - }, - { - "voice_time_seconds": { - "start": 165.2, - "end": 169.0 - }, - "arabic": "وبعد تلت أيام جاب معه الكاس،", - "hebrew_transliteration": "וּבַּעֶד תַלַת אַיַּאם גַ'אבּ מַעוֹ אִל-כַּאס,", - "hebrew": "ואחרי שלושה ימים הביא איתו את הגביע,", - "expected_phonemes": "u b a ʕ e d | t a l a t | ʔ a j aː m | dʒ aː b | m a ʕ o | i l k aː s" - }, - { - "voice_time_seconds": { - "start": 169.0, - "end": 171.8 - }, - "arabic": "حطّه قدّام الملك وقالّه:", - "hebrew_transliteration": "חַטّוֹ קֻדַّאם אִל-מַלֶכּ וּקַאל-לוֹ:", - "hebrew": "הניח אותו לפני המלך ואמר לו:", - "expected_phonemes": "ħ a tˤ tˤ o | ʔ u d d aː m | i l m a l e k | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 171.8, - "end": 175.4 - }, - "arabic": "«يعيش الملك! تفضّل، إشرب،", - "hebrew_transliteration": "יְעִיש אִל-מַלֶכּ! תְפַדַّל, אִשרַבּ,", - "hebrew": "יחי המלך! בבקשה, שתה,", - "expected_phonemes": "j ʕ iː ʃ | i l m a l e k | t f a dˤ dˤ a l | ʔ i ʃ r a b" - }, - { - "voice_time_seconds": { - "start": 175.4, - "end": 179.0 - }, - "arabic": "إشرب ومدّ أيّامك على مملكتك!»", - "hebrew_transliteration": "אִשרַבּ וּמֻדّ אַיַّאמַכּ עַלַא מַמלַכּתַכּ!", - "hebrew": "שתה והארך את ימיך על ממלכתך!", - "expected_phonemes": "ʔ i ʃ r a b | u m u d d | ʔ a j j aː m a k | ʕ a l a | m a m l a k t a k" - }, - { - "voice_time_seconds": { - "start": 179.0, - "end": 183.2 - }, - "arabic": "الملك سليمان إتطلّع في الكاس المليان ميّ،", - "hebrew_transliteration": "אִל-מַלֶכּ סֻלֵימַאן אִתטַלַّע פִי אִל-כַּאס אִל-מַליַאן מַיّ,", - "hebrew": "המלך שלמה הסתכל בגביע המלא מים,", - "expected_phonemes": "i l m a l e k | s u l eː m aː n | ʔ i t tˤ a l l a ʕ | f i | i l k aː s | i l m a l j aː n | m a j j" - }, - { - "voice_time_seconds": { - "start": 183.2, - "end": 189.2 - }, - "arabic": "وفجأة بلّش يتردّد: أشرب أو ما أشربش؟", - "hebrew_transliteration": "וּפַגְ'אַה בַּלַّש יִתרַדַّד: אַשרַבּ אַו מַא אַשרַבֶּש?", - "hebrew": "ופתאום התחיל להסס: לשתות או לא לשתות?", - "expected_phonemes": "u f a dʒ ʔ a | b a l l a ʃ | j i t r a d d a d | ʔ a ʃ r a b | ʔ a w | m a | ʔ a ʃ r a b e ʃ" - }, - { - "voice_time_seconds": { - "start": 189.2, - "end": 194.6 - }, - "arabic": "فكّر: هاد مش موضوع سهل. لازم أستشير.", - "hebrew_transliteration": "פַכַּר: הַאד מַוְדוּע מִש סַהֶל. לַאזֶם אַסתַשִיר.", - "hebrew": "הוא חשב: זה לא עניין פשוט. אני צריך להתייעץ.", - "expected_phonemes": "f a k a r | h aː d | m a w d uː ʕ | m i ʃ | s a h e l | l aː z e m | ʔ a s t a ʃ iː r" - }, - { - "voice_time_seconds": { - "start": 194.6, - "end": 197.6 - }, - "arabic": "وكمان مرّة عمل إجتماع", - "hebrew_transliteration": "וּכַּמַאן מַרַّה עִמֶל אִגְ'תִמַאע", - "hebrew": "ושוב ערך אסיפה", - "expected_phonemes": "u k a m aː n | m a r r a | ʕ i m e l | ʔ i dʒ t i m aː ʕ" - }, - { - "voice_time_seconds": { - "start": 197.6, - "end": 200.6 - }, - "arabic": "مع كلّ الكائنات الحيّة في العالم،", - "hebrew_transliteration": "מַע כֻּלّ אִל-כַּאאִנַאת אִל-חַיֶּה פִי אִל-עַאלַם,", - "hebrew": "עם כל היצורים החיים בעולם,", - "expected_phonemes": "m a ʕ | k u l l | i l k aː ʔ i n aː t | i l ħ a j e | f i | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 200.6, - "end": 204.8 - }, - "arabic": "وحيد القرن، النّمور، الدّببة، الأسود،", - "hebrew_transliteration": "וַחִיד אִל-קַרֶן, אִ-נֻّמוּר, אִ-דִّבַּבֶּה, אִל-אֻסוּד,", - "hebrew": "הקרנף, הנמרים, הדובים, האריות,", - "expected_phonemes": "w a ħ iː d | i l ʔ a r e n | ʔ i n n u m uː r | ʔ i d d i b a b e | i l ʔ u s uː d" - }, - { - "voice_time_seconds": { - "start": 204.8, - "end": 209.0 - }, - "arabic": "النّمل، الحجل، السّمك والغزلان.", - "hebrew_transliteration": "אִ-נַّמֶל, אִל-חַגַ'ל, אִ-סַّמַכּ וִל-עִ'זלַאן.", - "hebrew": "הנמלים, החוגלות, הדגים והצבאים.", - "expected_phonemes": "ʔ i n n a m e l | i l ħ a dʒ a l | ʔ i s s a m a k | w i l ɣ i z l aː n" - }, - { - "voice_time_seconds": { - "start": 209.0, - "end": 212.2 - }, - "arabic": "قالّهم: «عندي هون ميّة الحياة.", - "hebrew_transliteration": "קַאל-לְהֹם: עִנדִי הוֹן מַיֶּת אִל-חַיַאה.", - "hebrew": "הוא אמר להם: יש לי כאן את מי החיים.", - "expected_phonemes": "ʔ aː l l h o m | ʕ i n d i | h oː n | m a j e t | i l ħ a j aː" - }, - { - "voice_time_seconds": { - "start": 212.2, - "end": 214.6 - }, - "arabic": "شو رأيكوا؟ شو بتقولوا؟", - "hebrew_transliteration": "שוּ רַאיְכּוּ? שוּ בִּתקוּלוּ?", - "hebrew": "מה דעתכם? מה אתם אומרים?", - "expected_phonemes": "ʃ u | r aː j k u | ʃ u | b i t ʔ uː l u" - }, - { - "voice_time_seconds": { - "start": 214.6, - "end": 217.0 - }, - "arabic": "أشرب أو ما أشربش؟»", - "hebrew_transliteration": "אַשרַבּ אַו מַא אַשרַבֶּש?", - "hebrew": "לשתות או לא לשתות?", - "expected_phonemes": "ʔ a ʃ r a b | ʔ a w | m a | ʔ a ʃ r a b e ʃ" - }, - { - "voice_time_seconds": { - "start": 217.0, - "end": 221.4 - }, - "arabic": "كلّ الحيوانات جاوبت دغري بصوت واحد:", - "hebrew_transliteration": "כֻּלّ אִל-חַיַוַאנַאת גַ'אוַבַּת דֻעְ'רִי בְּצוֹת וַאחַד:", - "hebrew": "כל החיות ענו מיד בקול אחד:", - "expected_phonemes": "k u l l | i l ħ a j a w aː n aː t | dʒ aː w a b a t | d u ɣ r i | b sˤ oː t | w aː ħ a d" - }, - { - "voice_time_seconds": { - "start": 221.4, - "end": 223.6 - }, - "arabic": "«نعم! إشرب!»", - "hebrew_transliteration": "נַעַם! אִשרַבּ!", - "hebrew": "כן! שתה!", - "expected_phonemes": "n a ʕ a m | ʔ i ʃ r a b" - }, - { - "voice_time_seconds": { - "start": 223.6, - "end": 225.6 - }, - "arabic": "«الكلّ عنده نفس الرّأي؟»", - "hebrew_transliteration": "אִל-כֻּלّ עִנדוֹ נַפֶס אִ-רַّאִי?", - "hebrew": "לכולם אותה דעה?", - "expected_phonemes": "i l k u l l | ʕ i n d o | n a f e s | ʔ i r r a ʔ i" - }, - { - "voice_time_seconds": { - "start": 225.6, - "end": 230.4 - }, - "arabic": "جاوبوه: «نعم!»", - "hebrew_transliteration": "גַ'אוַבּוּה: נַעַם!", - "hebrew": "הם ענו לו: כן!", - "expected_phonemes": "dʒ aː w a b uː h | n a ʕ a m" - }, - { - "voice_time_seconds": { - "start": 230.4, - "end": 234.4 - }, - "arabic": "إتطلّعوا حواليهم وشافوا - التّعلب ناقص!", - "hebrew_transliteration": "אִתטַלַّעוּ חַוַאלֵיהֹם וּשַאפוּ - אִ-תַّעלַבּ נַאקֶץ!", - "hebrew": "הם הסתכלו סביבם וראו - השועל חסר!", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ u | ħ a w aː l eː h o m | u ʃ aː f u | ʔ i t t a ʕ l a b | n aː ʔ e sˤ" - }, - { - "voice_time_seconds": { - "start": 234.4, - "end": 237.0 - }, - "arabic": "«آه، وين إبن الحرام هدا؟", - "hebrew_transliteration": "אַאה, וֵין אִבְּן אִל-חַרַאם הַאדַא?", - "hebrew": "אה, איפה הממזר הזה?", - "expected_phonemes": "ʔ aː h | w eː n | ʔ i b n | i l ħ a r aː m | h aː d a" - }, - { - "voice_time_seconds": { - "start": 237.0, - "end": 239.4 - }, - "arabic": "إستعجلوا جيبوه لعندي!»", - "hebrew_transliteration": "אִסתַעגְ'לוּ גִ'יבּוּה לַעִנדִי!", - "hebrew": "מהרו, הביאו אותו אליי!", - "expected_phonemes": "ʔ i s t a ʕ dʒ l u | dʒ iː b uː h | l a ʕ i n d i" - }, - { - "voice_time_seconds": { - "start": 239.4, - "end": 243.4 - }, - "arabic": "دغري طلعوا الرّسل يجيبوا التّعلب لعند الملك.", - "hebrew_transliteration": "דֻעְ'רִי טִלעוּ אִ-רֻّסֻל יְגִ'יבּוּ אִ-תַّעלַבּ לַעִנד אִל-מַלֶכּ.", - "hebrew": "מיד יצאו השליחים להביא את השועל אל המלך.", - "expected_phonemes": "d u ɣ r i | tˤ i l ʕ u | ʔ i r r u s u l | j dʒ iː b u | ʔ i t t a ʕ l a b | l a ʕ i n d | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 243.4, - "end": 246.6 - }, - "arabic": "أجا التّعلب، وقّف قدّام الملك -", - "hebrew_transliteration": "אַגַ'א אִ-תַּעלַבּ, וַקַّף קֻדַّאם אִל-מַלֶכּ -", - "hebrew": "השועל בא, עמד לפני המלך -", - "expected_phonemes": "ʔ a dʒ a | ʔ i t a ʕ l a b | w a ʔ ʔ a f | ʔ u d d aː m | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 246.6, - "end": 252.2 - }, - "arabic": "راسه في الأرض، ذيله بين إجريه برجّ من الخوف.", - "hebrew_transliteration": "רַאסוֹ פִי אִל-אַרְד', דֵילוֹ בֵּין אִגְ'רֵיה בִּרְגֶّ מִן אִל-חוֹף.", - "hebrew": "ראשו באדמה, זנבו בין רגליו, רועד מפחד.", - "expected_phonemes": "r aː s o | f i | i l ʔ a r dˤ | d eː l o | b eː n | ʔ i dʒ r eː h | b i r g g e | m i n | i l ħ oː f" - }, - { - "voice_time_seconds": { - "start": 252.2, - "end": 254.8 - }, - "arabic": "إتطلّع عليه الملك سليمان وقال:", - "hebrew_transliteration": "אִתטַלַّע עַלֵיה אִל-מַלֶכּ סֻלֵימַאן וּקַאל:", - "hebrew": "המלך שלמה הסתכל עליו ואמר:", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | ʕ a l eː h | i l m a l e k | s u l eː m aː n | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 254.8, - "end": 257.0 - }, - "arabic": "«أنا الملك سليمان،", - "hebrew_transliteration": "אַנַא אִל-מַלֶכּ סֻלֵימַאן,", - "hebrew": "אני המלך שלמה,", - "expected_phonemes": "ʔ a n a | i l m a l e k | s u l eː m aː n" - }, - { - "voice_time_seconds": { - "start": 257.0, - "end": 260.0 - }, - "arabic": "أمرت كلّ الحيوانات تيجي لعندي -", - "hebrew_transliteration": "אַמַרֶת כֻּלّ אִל-חַיַוַאנַאת תִיגִ'י לַעִנדִי -", - "hebrew": "ציוויתי על כל החיות לבוא אליי -", - "expected_phonemes": "ʔ a m a r e t | k u l l | i l ħ a j a w aː n aː t | t iː dʒ i | l a ʕ i n d i" - }, - { - "voice_time_seconds": { - "start": 260.0, - "end": 262.6 - }, - "arabic": "وإنت ما أجيتش!»", - "hebrew_transliteration": "וּאִנתֶ מַא אִגִ'יתֶש!", - "hebrew": "ואתה לא באת!", - "expected_phonemes": "u ʔ i n t e | m a | ʔ i dʒ iː t e ʃ" - }, - { - "voice_time_seconds": { - "start": 262.6, - "end": 265.4 - }, - "arabic": "قالّه: «صحّ سيدي الملك.", - "hebrew_transliteration": "קַאל-לוֹ: צַח סִידִי אִל-מַלֶכּ.", - "hebrew": "הוא אמר לו: נכון, אדוני המלך.", - "expected_phonemes": "ʔ aː l l o | sˤ a ħ | s iː d i | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 265.4, - "end": 269.4 - }, - "arabic": "إنت بتعرف عمبتمرق عليّ أيّام صعبة...", - "hebrew_transliteration": "אִנתֶ בְּתִערַף עַם-בִּתֻמרֹק עַלַיّ אַיַّאם צַעבֶּה...", - "hebrew": "אתה יודע, אני עובר ימים קשים...", - "expected_phonemes": "ʔ i n t e | b t i ʕ r a f | ʕ a m b i t u m r o ʔ | ʕ a l a j j | ʔ a j j aː m | sˤ a ʕ b e" - }, - { - "voice_time_seconds": { - "start": 269.4, - "end": 273.2 - }, - "arabic": "عندي مرة في البيت، ولاد مرضى...»", - "hebrew_transliteration": "עִנדִי מַרַה פִי אִל-בֵּית, וּלַאד מַרְדַ'א...", - "hebrew": "יש לי אישה בבית, ילדים חולים...", - "expected_phonemes": "ʕ i n d i | m a r a | f i | i l b eː t | u l aː d | m a r dˤ a" - }, - { - "voice_time_seconds": { - "start": 273.2, - "end": 276.2 - }, - "arabic": "«طيّب، طيّب! سمعنا وغفرنا.", - "hebrew_transliteration": "טַיֶּבּ, טַיֶּבּ! סִמִענַא וּעַ'פַרנַא.", - "hebrew": "טוב, טוב! שמענו וסלחנו.", - "expected_phonemes": "tˤ a j e b | tˤ a j e b | s i m i ʕ n a | u ɣ a f a r n a" - }, - { - "voice_time_seconds": { - "start": 276.2, - "end": 279.2 - }, - "arabic": "هلّق قولّي يا صديقي التّعلب،", - "hebrew_transliteration": "הַלַّק קוּל-לִי יַא צַדִיקִי אִ-תַّעלַבּ,", - "hebrew": "עכשיו תגיד לי, ידידי השועל,", - "expected_phonemes": "h a l l a ʔ | ʔ uː l l i | j a | sˤ a d iː ʔ i | ʔ i t t a ʕ l a b" - }, - { - "voice_time_seconds": { - "start": 279.2, - "end": 282.0 - }, - "arabic": "فيه هون قدّامي ميّة الحياة", - "hebrew_transliteration": "פִיה הוֹן קֻדַّאמִי מַיֶּת אִל-חַיַאה", - "hebrew": "יש כאן לפניי את מי החיים", - "expected_phonemes": "f iː h | h oː n | ʔ u d d aː m i | m a j e t | i l ħ a j aː" - }, - { - "voice_time_seconds": { - "start": 282.0, - "end": 284.6 - }, - "arabic": "إلّي جابلي إيّاها النّسر من الجنّة.", - "hebrew_transliteration": "אִלִّי גַ'אבּ-לִי אִיַّאהַא אִ-נַّסֶר מִן אִ-גַّנֶّה.", - "hebrew": "שהנשר הביא לי מגן העדן.", - "expected_phonemes": "ʔ i l l i | dʒ aː b l i | ʔ i j j aː h a | ʔ i n n a s e r | m i n | ʔ i g g a n n e" - }, - { - "voice_time_seconds": { - "start": 284.6, - "end": 288.4 - }, - "arabic": "شو بتقول - أشرب أو ما أشربش؟»", - "hebrew_transliteration": "שוּ בִּתקוּל - אַשרַבּ אַו מַא אַשרַבֶּש?", - "hebrew": "מה אתה אומר - לשתות או לא לשתות?", - "expected_phonemes": "ʃ u | b i t ʔ uː l | ʔ a ʃ r a b | ʔ a w | m a | ʔ a ʃ r a b e ʃ" - }, - { - "voice_time_seconds": { - "start": 288.4, - "end": 291.8 - }, - "arabic": "التّعلب فكّر للحظة، رفع راسه،", - "hebrew_transliteration": "אִ-תַّעלַבּ פַכַּר לַלַחזַה, רַפַע רַאסוֹ,", - "hebrew": "השועל חשב לרגע, הרים את ראשו,", - "expected_phonemes": "ʔ i t t a ʕ l a b | f a k a r | l a l a ħ z a | r a f a ʕ | r aː s o" - }, - { - "voice_time_seconds": { - "start": 291.8, - "end": 296.4 - }, - "arabic": "إتطلّع على اليمين، إتطلّع على الشّمال وسأل:", - "hebrew_transliteration": "אִתטַלַّע עַלַא אִל-יַמִין, אִתטַלַّע עַלַא אִ-שַּׁמַאל וּסַאַל:", - "hebrew": "הסתכל ימינה, הסתכל שמאלה ושאל:", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | ʕ a l a | i l j a m iː n | ʔ i t tˤ a l l a ʕ | ʕ a l a | ʔ i ʃ a m aː l | u s a ʔ a l" - }, - { - "voice_time_seconds": { - "start": 296.4, - "end": 299.0 - }, - "arabic": "«شو بتقول باقي الحيوانات؟»", - "hebrew_transliteration": "שוּ בִּתקוּל בַּאקִי אִל-חַיַוַאנַאת?", - "hebrew": "מה אומרות שאר החיות?", - "expected_phonemes": "ʃ u | b i t ʔ uː l | b aː ʔ i | i l ħ a j a w aː n aː t" - }, - { - "voice_time_seconds": { - "start": 299.0, - "end": 301.2 - }, - "arabic": "«الكلّ بقولّي أشرب.»", - "hebrew_transliteration": "אִל-כֻּלّ בִּקוּל-לִי אִשרַבּ.", - "hebrew": "כולם אומרים לי לשתות.", - "expected_phonemes": "i l k u l l | b i ʔ uː l l i | ʔ i ʃ r a b" - }, - { - "voice_time_seconds": { - "start": 301.2, - "end": 303.8 - }, - "arabic": "«طيّب، تمام، إشرب سيدي الملك،", - "hebrew_transliteration": "טַיֶּבּ, תַמַאם, אִשרַבּ סִידִי אִל-מַלֶכּ,", - "hebrew": "טוב, בסדר, שתה אדוני המלך,", - "expected_phonemes": "tˤ a j e b | t a m aː m | ʔ i ʃ r a b | s iː d i | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 303.8, - "end": 307.0 - }, - "arabic": "إشرب ومدّ أيّامك على مملكتك!»", - "hebrew_transliteration": "אִשרַבּ וּמֻדّ אַיַّאמַכּ עַלַא מַמלַכּתַכּ!", - "hebrew": "שתה והארך את ימיך על ממלכתך!", - "expected_phonemes": "ʔ i ʃ r a b | u m u d d | ʔ a j j aː m a k | ʕ a l a | m a m l a k t a k" - }, - { - "voice_time_seconds": { - "start": 307.0, - "end": 310.6 - }, - "arabic": "قالّه: «لحظة، لحظة، إستنّى شويّ.", - "hebrew_transliteration": "קַאל-לוֹ: לַחזַה, לַחזַה, אִסתַנַّא שְוַיّ.", - "hebrew": "הוא אמר לו: רגע, רגע, חכה קצת.", - "expected_phonemes": "ʔ aː l l o | l a ħ ðˤ a | l a ħ ðˤ a | ʔ i s t a n n a | ʃ w a j j" - }, - { - "voice_time_seconds": { - "start": 310.6, - "end": 314.0 - }, - "arabic": "إنت ثعلب، أمكر الحيوانات وأذكاهم -", - "hebrew_transliteration": "אִנתֶ תַעלַבּ, אַמכַּר אִל-חַיַוַאנַאת וּאַד'כַּאהֹם -", - "hebrew": "אתה שועל, הערמומי והחכם מכל החיות -", - "expected_phonemes": "ʔ i n t e | t a ʕ l a b | ʔ a m k a r | i l ħ a j a w aː n aː t | u ʔ a dˤ k aː h o m" - }, - { - "voice_time_seconds": { - "start": 314.0, - "end": 315.8 - }, - "arabic": "ما تقوليش شو الكلّ بقولّي.", - "hebrew_transliteration": "מַא תְקוּלִיש שוּ אִל-כֻּלّ בִּקוּל-לִי.", - "hebrew": "אל תגיד לי מה כולם אומרים לי.", - "expected_phonemes": "m a | t ʔ uː l iː ʃ | ʃ u | i l k u l l | b i ʔ uː l l i" - }, - { - "voice_time_seconds": { - "start": 315.8, - "end": 317.0 - }, - "arabic": "قولّي الحقيقة -", - "hebrew_transliteration": "קוּל-לִי אִל-חַקִיקַה -", - "hebrew": "תגיד לי את האמת -", - "expected_phonemes": "ʔ uː l l i | i l ħ a ʔ iː ʔ a" - }, - { - "voice_time_seconds": { - "start": 317.0, - "end": 320.8 - }, - "arabic": "شو إنت بتفكّر؟ قول وما تخافش.»", - "hebrew_transliteration": "שוּ אִנתֶ בִּתפַכַּר? קוּל וּמַא תְחַ'אפֶש.", - "hebrew": "מה אתה חושב? תגיד ואל תפחד.", - "expected_phonemes": "ʃ u | ʔ i n t e | b i t f a k a r | ʔ uː l | u m a | t x aː f e ʃ" - }, - { - "voice_time_seconds": { - "start": 320.8, - "end": 323.2 - }, - "arabic": "التّعلب سمع حكي الملك،", - "hebrew_transliteration": "אִ-תַּעלַבּ סִמֶע חַכִּי אִל-מַלֶכּ,", - "hebrew": "השועל שמע את דברי המלך,", - "expected_phonemes": "ʔ i t a ʕ l a b | s i m e ʕ | ħ a k i | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 323.2, - "end": 326.8 - }, - "arabic": "سكت فترة طويلة وبعدين قالّه:", - "hebrew_transliteration": "סַכַּת פַתרַה טַוִילֶה וּבַּעדֵין קַאל-לוֹ:", - "hebrew": "שתק זמן רב ואחר כך אמר לו:", - "expected_phonemes": "s a k a t | f a t r a | tˤ a w iː l e | u b a ʕ d eː n | ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 326.8, - "end": 329.6 - }, - "arabic": "«سيدي الملك، إذا إنت بتسألني هيك،", - "hebrew_transliteration": "סִידִי אִל-מַלֶכּ, אִזַא אִנתֶ בִּתסַאַלנִי הֵיכּ,", - "hebrew": "אדוני המלך, אם אתה שואל אותי כך,", - "expected_phonemes": "s iː d i | i l m a l e k | ʔ i z a | ʔ i n t e | b i t s a ʔ a l n i | h eː k" - }, - { - "voice_time_seconds": { - "start": 329.6, - "end": 331.4 - }, - "arabic": "عنجدّ نصيحتي إلك:", - "hebrew_transliteration": "עַנגַ'דّ נַצִיחתִי אִלַכּ:", - "hebrew": "באמת, העצה שלי אליך:", - "expected_phonemes": "ʕ a n dʒ a d d | n a sˤ iː ħ t i | ʔ i l a k" - }, - { - "voice_time_seconds": { - "start": 331.4, - "end": 335.0 - }, - "arabic": "ما تشربش! وليش؟ شوف:", - "hebrew_transliteration": "מַא תִשרַבֶּש! וּלֵיש? שוּף:", - "hebrew": "אל תשתה! ולמה? תראה:", - "expected_phonemes": "m a | t i ʃ r a b e ʃ | u l eː ʃ | ʃ uː f" - }, - { - "voice_time_seconds": { - "start": 335.0, - "end": 338.0 - }, - "arabic": "اليوم إنت ملك فخم على مملكتك،", - "hebrew_transliteration": "אִל-יוֹם אִנתֶ מַלֶכּ פַחֶ'ם עַלַא מַמלַכּתַכּ,", - "hebrew": "היום אתה מלך מפואר על ממלכתך,", - "expected_phonemes": "i l j oː m | ʔ i n t e | m a l e k | f a x e m | ʕ a l a | m a m l a k t a k" - }, - { - "voice_time_seconds": { - "start": 338.0, - "end": 340.4 - }, - "arabic": "محاط بالصحاب والأصدقاء.", - "hebrew_transliteration": "מֻחַאט בִּל-צְחַאבּ וִל-אַצדִקַאא.", - "hebrew": "מוקף בחברים וברעים.", - "expected_phonemes": "m u ħ aː tˤ | b i l sˤ ħ aː b | w i l ʔ a sˤ d i ʔ aː a" - }, - { - "voice_time_seconds": { - "start": 340.4, - "end": 343.8 - }, - "arabic": "بكرا - كلّ أصدقائك رح يموتوا قدّامك،", - "hebrew_transliteration": "בֻּכּרַא - כֻּלّ אַצדִקַאאַכּ רַח יְמוּתוּ קֻדַّאמַכּ,", - "hebrew": "מחר - כל חבריך ימותו לנגד עיניך,", - "expected_phonemes": "b u k r a | k u l l | ʔ a sˤ d i ʔ aː ʔ a k | r a ħ | j m uː t u | ʔ u d d aː m a k" - }, - { - "voice_time_seconds": { - "start": 343.8, - "end": 346.4 - }, - "arabic": "وإنت بتبقى وحيد في العالم،", - "hebrew_transliteration": "וּאִנתֶ בְּתִבְקַא וַחִיד פִי אִל-עַאלַם,", - "hebrew": "ואתה תישאר לבד בעולם,", - "expected_phonemes": "u ʔ i n t e | b t i b ʔ a | w a ħ iː d | f i | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 346.4, - "end": 349.6 - }, - "arabic": "وعدا عن هيك بقولوا: الزّمن دوّار.", - "hebrew_transliteration": "וּעַדַא עַן הֵיכּ בִּקוּלוּ: אִ-זַّמַן דַוַّאר.", - "hebrew": "וחוץ מזה אומרים: הזמן מסתובב.", - "expected_phonemes": "u ʕ a d a | ʕ a n | h eː k | b i ʔ uː l u | ʔ i z z a m a n | d a w w aː r" - }, - { - "voice_time_seconds": { - "start": 349.6, - "end": 351.4 - }, - "arabic": "اليوم إنت فوق،", - "hebrew_transliteration": "אִל-יוֹם אִנתֶ פוֹק,", - "hebrew": "היום אתה למעלה,", - "expected_phonemes": "i l j oː m | ʔ i n t e | f oː ʔ" - }, - { - "voice_time_seconds": { - "start": 351.4, - "end": 354.4 - }, - "arabic": "بسّ بكرا ممكن كلّ إشي ينقلب", - "hebrew_transliteration": "בַּס בֻּכּרַא מֻמכֶּן כֻּלّ אִשִי יִנקְלֶבּ", - "hebrew": "אבל מחר הכול עלול להתהפך", - "expected_phonemes": "b a s | b u k r a | m u m k e n | k u l l | ʔ i ʃ i | j i n ʔ l e b" - }, - { - "voice_time_seconds": { - "start": 354.4, - "end": 358.2 - }, - "arabic": "وتلاقي حالك بأوطى مكانة، من دون صحاب،", - "hebrew_transliteration": "וּתְלַאקִי חַאלַכּ בְּאַוְטַא מַכַּאנֶה, מִן דוּן צְחַאבּ,", - "hebrew": "ותמצא את עצמך במעמד הנמוך ביותר, בלי חברים,", - "expected_phonemes": "u t l aː ʔ i | ħ aː l a k | b ʔ a w tˤ a | m a k aː n e | m i n | d uː n | sˤ ħ aː b" - }, - { - "voice_time_seconds": { - "start": 358.2, - "end": 362.4 - }, - "arabic": "من دون عيلة، بدون غنى، بدون شرف -", - "hebrew_transliteration": "מִן דוּן עֵילֶה, בְּדוּן עִ'נַא, בְּדוּן שַרַף -", - "hebrew": "בלי משפחה, בלי עושר, בלי כבוד -", - "expected_phonemes": "m i n | d uː n | ʕ eː l e | b i d uː n | ɣ i n a | b i d uː n | ʃ a r a f" - }, - { - "voice_time_seconds": { - "start": 362.4, - "end": 365.4 - }, - "arabic": "مين بعرف؟ بلكي بيجي ملك أكبر منّك", - "hebrew_transliteration": "מִין בִּיעְרַף? בַּלְכִּי בִּיגִ'י מַלֶכּ אַכְּבַּר מִנַّכּ", - "hebrew": "מי יודע? אולי יבוא מלך גדול ממך", - "expected_phonemes": "m iː n | b iː ʕ r a f | b a l k i | b iː dʒ i | m a l e k | ʔ a k b a r | m i n n a k" - }, - { - "voice_time_seconds": { - "start": 365.4, - "end": 367.8 - }, - "arabic": "وبنزّلك من مكانتك وعظمتك.", - "hebrew_transliteration": "וּבִּנַזְّלַכּ מִן מַכַּאנתַכּ וּעַטַ'מתַכּ.", - "hebrew": "ויוריד אותך ממעמדך ומגדולתך.", - "expected_phonemes": "u b i n a z z l a k | m i n | m a k aː n t a k | u ʕ a ðˤ a m t a k" - }, - { - "voice_time_seconds": { - "start": 367.8, - "end": 371.4 - }, - "arabic": "تلاقي حالك في أسفل السّافلين. لشو؟", - "hebrew_transliteration": "תְלַאקִי חַאלַכּ פִי אַספַל אִ-סַّאפְלִין. לַשוּ?", - "hebrew": "תמצא את עצמך בשפל המדרגה. בשביל מה?", - "expected_phonemes": "t l aː ʔ i | ħ aː l a k | f i | ʔ a s f a l | ʔ i s s aː f l iː n | l a ʃ u" - }, - { - "voice_time_seconds": { - "start": 371.4, - "end": 374.4 - }, - "arabic": "إترك العالم اليوم، وإنت في عزّك", - "hebrew_transliteration": "אִתרֻכּ אִל-עַאלַם אִל-יוֹם, וּאִנתֶ פִי עִזַّכּ", - "hebrew": "עזוב את העולם היום, כשאתה בשיא תפארתך", - "expected_phonemes": "ʔ i t r u k | i l ʕ aː l a m | i l j oː m | u ʔ i n t e | f i | ʕ i z z a k" - }, - { - "voice_time_seconds": { - "start": 374.4, - "end": 378.6 - }, - "arabic": "بتذكّروك لأبد الآبدين كأكبر ملوك الزّمان.", - "hebrew_transliteration": "בִּתזַכַּרוּכּ לַאַבַּד אִל-אַאבְּדִין כַּאַכְּבַּר מֻלוּכּ אִ-זַّמַאן.", - "hebrew": "יזכרו אותך לעולמי עד כגדול מלכי הזמן.", - "expected_phonemes": "b i t z a k a r uː k | l a ʔ a b a d | i l ʔ aː b d iː n | k a ʔ a k b a r | m u l uː k | ʔ i z z a m aː n" - }, - { - "voice_time_seconds": { - "start": 378.6, - "end": 382.0 - }, - "arabic": "نصيحتي إلك - ما تشربش!»", - "hebrew_transliteration": "נַצִיחתִי אִלַכּ - מַא תִשרַבֶּש!", - "hebrew": "העצה שלי אליך - אל תשתה!", - "expected_phonemes": "n a sˤ iː ħ t i | ʔ i l a k | m a | t i ʃ r a b e ʃ" - }, - { - "voice_time_seconds": { - "start": 382.0, - "end": 386.6 - }, - "arabic": "الملك سمع هدا الكلام، فكّر شويّ وقال:", - "hebrew_transliteration": "אִל-מַלֶכּ סִמֶע הַאדַא אִל-כַּלַאם, פַכַּר שְוַיّ וּקַאל:", - "hebrew": "המלך שמע את הדברים האלה, חשב קצת ואמר:", - "expected_phonemes": "i l m a l e k | s i m e ʕ | h aː d a | i l k a l aː m | f a k a r | ʃ w a j j | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 386.6, - "end": 388.4 - }, - "arabic": "«معاك حقّ!»", - "hebrew_transliteration": "מַעַאכּ חַקّ!", - "hebrew": "אתה צודק!", - "expected_phonemes": "m a ʕ aː k | ħ a ʔ ʔ" - }, - { - "voice_time_seconds": { - "start": 388.4, - "end": 390.4 - }, - "arabic": "توجّه للنّسر وقالّه:", - "hebrew_transliteration": "תְוַגַّה לַ-נַּסֶר וּקַאל-לוֹ:", - "hebrew": "פנה אל הנשר ואמר לו:", - "expected_phonemes": "t w a g g a | l a n a s e r | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 390.4, - "end": 394.2 - }, - "arabic": "«خدّ ميّة هدا الكاس رجّعها على الجنّة!»", - "hebrew_transliteration": "חֻ'ד מַיֶּת הַאדַא אִל-כַּאס רַגַّעְהַא עַלַא אִ-גַּנֶّה!", - "hebrew": "קח את המים של הגביע הזה, החזר אותם לגן העדן!", - "expected_phonemes": "x u d | m a j e t | h aː d a | i l k aː s | r a g g a ʕ h a | ʕ a l a | ʔ i g a n n e" - }, - { - "voice_time_seconds": { - "start": 394.2, - "end": 396.4 - }, - "arabic": "سمع النّسر أمر الملك،", - "hebrew_transliteration": "סִמֶע אִ-נַّסֶר אַמֶר אִל-מַלֶכּ,", - "hebrew": "הנשר שמע את פקודת המלך,", - "expected_phonemes": "s i m e ʕ | ʔ i n n a s e r | ʔ a m e r | i l m a l e k" - }, - { - "voice_time_seconds": { - "start": 396.4, - "end": 400.0 - }, - "arabic": "مسك الكاس في مخالبه، حلّق للأعالي -", - "hebrew_transliteration": "מִסֶכּ אִל-כַּאס פִי מַחַ'אלבּוֹ, חַלַّק לַל-אַעַאלִי -", - "hebrew": "אחז את הגביע בטפריו, המריא למרומים -", - "expected_phonemes": "m i s e k | i l k aː s | f i | m a x aː l b o | ħ a l l a ʔ | l a l ʔ a ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 400.0, - "end": 404.6 - }, - "arabic": "تلت أيام وتلت ليالي، فوق الجبال والوديان،", - "hebrew_transliteration": "תַלַת אַיַּאם וּתַלַת לַיַאלִי, פוֹק לֶ-גְ'בַּאל וִל-וִדיַאן,", - "hebrew": "שלושה ימים ושלושה לילות, מעל ההרים והעמקים,", - "expected_phonemes": "t a l a t | ʔ a j aː m | u t a l a t | l a j aː l i | f oː ʔ | l e dʒ b aː l | w i l w i d j aː n" - }, - { - "voice_time_seconds": { - "start": 404.6, - "end": 407.6 - }, - "arabic": "لحدّ ما وصل لبوّابة الجنّة", - "hebrew_transliteration": "לַחַדّ מַא וְצֶל לַבַּוַּאבֶּת אִ-גַּנֶّה", - "hebrew": "עד שהגיע לשער גן העדן", - "expected_phonemes": "l a ħ a d d | m a | u sˤ e l | l a b a w aː b e t | ʔ i g a n n e" - }, - { - "voice_time_seconds": { - "start": 407.6, - "end": 412.6 - }, - "arabic": "ورجّع الميّ إلّي بقلب الكاس للنّبعة.", - "hebrew_transliteration": "וּרַגַّע אִל-מַיّ אִלִّי בְּקַלבּ אִל-כַּאס לַ-נַּבְּעַה.", - "hebrew": "והחזיר את המים שבתוך הגביע למעיין.", - "expected_phonemes": "u r a g g a ʕ | i l m a j j | ʔ i l l i | b ʔ a l b | i l k aː s | l a n a b ʕ a" - }, - { - "voice_time_seconds": { - "start": 412.6, - "end": 415.0 - }, - "arabic": "بسّ لمّا كان بطريقه على الجنّة،", - "hebrew_transliteration": "בַּס לַמַّא כַּאן בְּטַרִיקוֹ עַלַא אִ-גַّנֶّה,", - "hebrew": "אבל כשהיה בדרכו לגן העדן,", - "expected_phonemes": "b a s | l a m m a | k aː n | b tˤ a r iː ʔ o | ʕ a l a | ʔ i g g a n n e" - }, - { - "voice_time_seconds": { - "start": 415.0, - "end": 417.2 - }, - "arabic": "وهوّ طاير فوق اليمن،", - "hebrew_transliteration": "וּהֻוֶّ טַאיֶר פוֹק אִל-יַמַן,", - "hebrew": "בזמן שעף מעל תימן,", - "expected_phonemes": "u h u w w e | tˤ aː j e r | f oː ʔ | i l j a m a n" - }, - { - "voice_time_seconds": { - "start": 417.2, - "end": 419.8 - }, - "arabic": "هبّت عاصفة قويّة في السّما،", - "hebrew_transliteration": "הַבַּת עַאצְפֶה קַוִיֶּה פִי אִ-סַّמַא,", - "hebrew": "נשבה סערה חזקה בשמיים,", - "expected_phonemes": "h a b a t | ʕ aː sˤ f e | ʔ a w i j e | f i | i s s a m a" - }, - { - "voice_time_seconds": { - "start": 419.8, - "end": 421.6 - }, - "arabic": "أجنحة النّسر رجّت،", - "hebrew_transliteration": "אַגְ'נִחַת אִ-נַّסֶר רַגַّת,", - "hebrew": "כנפי הנשר רעדו,", - "expected_phonemes": "ʔ a dʒ n i ħ a t | ʔ i n n a s e r | r a g g a t" - }, - { - "voice_time_seconds": { - "start": 421.6, - "end": 424.0 - }, - "arabic": "وأكم نقطة من ميّة الحياة", - "hebrew_transliteration": "וּאַכַּם נֻקטַה מִן מַיֶּת אִל-חַיַאה", - "hebrew": "וכמה טיפות ממי החיים", - "expected_phonemes": "u ʔ a k a m | n u ʔ tˤ a | m i n | m a j e t | i l ħ a j aː" - }, - { - "voice_time_seconds": { - "start": 424.0, - "end": 425.4 - }, - "arabic": "إلّي كانت في الكاس،", - "hebrew_transliteration": "אִלִّי כַּאנַת פִי אִל-כַּאס,", - "hebrew": "שהיו בגביע,", - "expected_phonemes": "ʔ i l l i | k aː n a t | f i | i l k aː s" - }, - { - "voice_time_seconds": { - "start": 425.4, - "end": 428.4 - }, - "arabic": "وقعت على أرض اليمن.", - "hebrew_transliteration": "וִקעַת עַלַא אַרְד' אִל-יַמַן.", - "hebrew": "נפלו על אדמת תימן.", - "expected_phonemes": "w i ʔ ʕ a t | ʕ a l a | ʔ a r dˤ | i l j a m a n" - }, - { - "voice_time_seconds": { - "start": 428.4, - "end": 431.2 - }, - "arabic": "بكلّ محلّ وقعت فيه نقطة،", - "hebrew_transliteration": "בְּכֻּלّ מַחַלّ וִקעַת פִיה נֻקטַה,", - "hebrew": "בכל מקום שנפלה בו טיפה,", - "expected_phonemes": "b k u l l | m a ħ a l l | w i ʔ ʕ a t | f iː h | n u ʔ tˤ a" - }, - { - "voice_time_seconds": { - "start": 431.2, - "end": 433.4 - }, - "arabic": "طلعت شتلة قهوة.", - "hebrew_transliteration": "טִלעַת שַתלֶת קַהוֶה.", - "hebrew": "צמח שתיל קפה.", - "expected_phonemes": "tˤ i l ʕ a t | ʃ a t l e t | ʔ a h w e" - }, - { - "voice_time_seconds": { - "start": 433.4, - "end": 436.6 - }, - "arabic": "وهيك أجت القهوة على العالم،", - "hebrew_transliteration": "וּהֵיכּ אַגַ'ת אִל-קַהוֶה עַלַא אִל-עַאלַם,", - "hebrew": "וכך הגיע הקפה לעולם,", - "expected_phonemes": "u h eː k | ʔ a dʒ a t | i l ʔ a h w e | ʕ a l a | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 436.6, - "end": 441.4 - }, - "arabic": "ميّة الحياة.", - "hebrew_transliteration": "מַיֶּת אִל-חַיַאה.", - "hebrew": "מי החיים.", - "expected_phonemes": "m a j e t | i l ħ a j aː" - } - ] - }, - { - "file_name": "האמיר מבסרה והנער הגנב * אַמִיר אִל-בָּסרָה וא-שַאבּ אִל-חַרָאמִי * أمير البصرة والشاب الحرامي [T8BC3S8ltqM].webm", - "youtube_id": "T8BC3S8ltqM", - "title_hebrew": "האמיר מבסרה והנער הגנב", - "segments": [ - { - "voice_time_seconds": { - "start": 8.2, - "end": 11.6 - }, - "arabic": "مرّة جابت جماعة لأمير البصرة", - "hebrew_transliteration": "מַרַّה גַ'אבַּת גַ'מַאעַה לַ-אַמִיר אִל-בַּצרַה", - "hebrew": "פעם הביאה קבוצת אנשים לאמיר בצרה", - "expected_phonemes": "m a r r a | dʒ aː b a t | dʒ a m aː ʕ a | l a ʔ a m iː r | i l b a sˤ r a" - }, - { - "voice_time_seconds": { - "start": 11.6, - "end": 14.6 - }, - "arabic": "شابّ وسيم كتير وقالتله:", - "hebrew_transliteration": "שַאבّ וַסִים כְּתִיר וּקַאלַת-לוֹ:", - "hebrew": "בחור יפה תואר מאוד ואמרה לו:", - "expected_phonemes": "ʃ aː b b | w a s iː m | k t iː r | u ʔ aː l a t l o" - }, - { - "voice_time_seconds": { - "start": 14.6, - "end": 17.2 - }, - "arabic": "«يا أمير، هدا حرامي،", - "hebrew_transliteration": "יַא אַמִיר, הַאדַא חַרַאמִי,", - "hebrew": "אמיר, זה גנב,", - "expected_phonemes": "j a | ʔ a m iː r | h aː d a | ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 17.2, - "end": 20.0 - }, - "arabic": "مسكناه في بيتنا مبارح.»", - "hebrew_transliteration": "מַסַכְּנַאה פִי בֵּיתְנַא מְבַּארֶח.", - "hebrew": "תפסנו אותו בביתנו אתמול.", - "expected_phonemes": "m a s a k n aː h | f i | b eː t n a | m b aː r e ħ" - }, - { - "voice_time_seconds": { - "start": 20.0, - "end": 22.0 - }, - "arabic": "إتطلّع الأمير عالشّابّ،", - "hebrew_transliteration": "אִתטַלַّע אִל-אַמִיר עַ-שַّאבּ,", - "hebrew": "האמיר הסתכל על הבחור,", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | i l ʔ a m iː r | ʕ a ʃ ʃ aː b" - }, - { - "voice_time_seconds": { - "start": 22.0, - "end": 23.8 - }, - "arabic": "تعجّب من جماله", - "hebrew_transliteration": "תְעַגַّבּ מִן גַ'מַאלוֹ", - "hebrew": "התפעל מיופיו", - "expected_phonemes": "t ʕ a g g a b | m i n | dʒ a m aː l o" - }, - { - "voice_time_seconds": { - "start": 23.8, - "end": 27.0 - }, - "arabic": "وإستغرب إنّه واحد مثله حرامي.", - "hebrew_transliteration": "וּאִסתַעְ'רַבּ אִנּוֹ וַאחַד מִתלוֹ חַרַאמִי.", - "hebrew": "והתפלא שאדם כמותו הוא גנב.", - "expected_phonemes": "u ʔ i s t a ɣ r a b | ʔ i n o | w aː ħ a d | m i t l o | ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 27.0, - "end": 29.0 - }, - "arabic": "لهيك قال للجماعة:", - "hebrew_transliteration": "לַהֵיכּ קַאל לַל-גַ'מַאעַה:", - "hebrew": "לכן אמר לקבוצה:", - "expected_phonemes": "l a h eː k | ʔ aː l | l a l dʒ a m aː ʕ a" - }, - { - "voice_time_seconds": { - "start": 29.0, - "end": 31.6 - }, - "arabic": "«طيّب، إتركوه.»", - "hebrew_transliteration": "טַיֶּבּ, אִתרֻכּוּה.", - "hebrew": "טוב, עזבו אותו.", - "expected_phonemes": "tˤ a j e b | ʔ i t r u k uː h" - }, - { - "voice_time_seconds": { - "start": 31.6, - "end": 34.6 - }, - "arabic": "بعدين اتطلّع على الشاب وقالّه:", - "hebrew_transliteration": "בַּעדֵין, אִתטַלַّע עַלַא אִ-שַּׁאבּ וּקַאל-לוֹ:", - "hebrew": "אחר כך הסתכל על הבחור ואמר לו:", - "expected_phonemes": "b a ʕ d eː n | ʔ i t tˤ a l l a ʕ | ʕ a l a | ʔ i ʃ aː b | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 34.6, - "end": 38.2 - }, - "arabic": "«تعال، إحكيلي شو صار معك؟»", - "hebrew_transliteration": "תַעַאל, אִחכִּי-לִי שוּ צַאר מַעַכּ?", - "hebrew": "בוא, ספר לי מה קרה לך?", - "expected_phonemes": "t a ʕ aː l | ʔ i ħ k iː l i | ʃ u | sˤ aː r | m a ʕ a k" - }, - { - "voice_time_seconds": { - "start": 38.2, - "end": 39.4 - }, - "arabic": "الشّابّ قال:", - "hebrew_transliteration": "אִ-שַّאבּ קַאל:", - "hebrew": "הבחור אמר:", - "expected_phonemes": "ʔ i ʃ ʃ aː b | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 39.4, - "end": 42.8 - }, - "arabic": "«متل ما حكولك، كلامهم مزبوط.»", - "hebrew_transliteration": "מִתֶל מַא חַכוּ-לַכּ, כַּלַאמְהֹם מַזבּוּט.", - "hebrew": "כמו שהם אמרו לך, דבריהם נכונים.", - "expected_phonemes": "m i t e l | m a | ħ a k uː l a k | k a l aː m h o m | m a z b uː tˤ" - }, - { - "voice_time_seconds": { - "start": 42.8, - "end": 44.4 - }, - "arabic": "الأمير سأل:", - "hebrew_transliteration": "אִל-אַמִיר סַאַל:", - "hebrew": "האמיר שאל:", - "expected_phonemes": "i l ʔ a m iː r | s a ʔ a l" - }, - { - "voice_time_seconds": { - "start": 44.4, - "end": 45.6 - }, - "arabic": "«طيّب، ليش عملت هيك", - "hebrew_transliteration": "טַיֶּבּ, לֵיש עִמִלֶת הֵיכּ", - "hebrew": "טוב, למה עשית כך", - "expected_phonemes": "tˤ a j e b | l eː ʃ | ʕ i m i l e t | h eː k" - }, - { - "voice_time_seconds": { - "start": 45.6, - "end": 48.6 - }, - "arabic": "وشكلك بيدلّش إنّك حرامي.»", - "hebrew_transliteration": "וּשַכְּלַכּ בִּידַלֶّש אִנַّכּ חַרַאמִי.", - "hebrew": "כשמראך לא מעיד שאתה גנב?", - "expected_phonemes": "u ʃ a k l a k | b iː d a l l e ʃ | ʔ i n n a k | ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 48.6, - "end": 49.8 - }, - "arabic": "الشّابّ ردّ:", - "hebrew_transliteration": "אִ-שַّאבּ רַדّ:", - "hebrew": "הבחור ענה:", - "expected_phonemes": "ʔ i ʃ ʃ aː b | r a d d" - }, - { - "voice_time_seconds": { - "start": 49.8, - "end": 53.8 - }, - "arabic": "«حبّ المال وطمع الدّنيا دفعني أسرق.", - "hebrew_transliteration": "חֻבּ אִל-מַאל וּטַמַע אִ-דֻّנְיַא דַפַעְנִי אַסרֻק.", - "hebrew": "אהבת הכסף ותאוות העולם דחפו אותי לגנוב.", - "expected_phonemes": "ħ u b | i l m aː l | u tˤ a m a ʕ | ʔ i d d u n j a | d a f a ʕ n i | ʔ a s r u ʔ" - }, - { - "voice_time_seconds": { - "start": 53.8, - "end": 56.0 - }, - "arabic": "أنا ما بطلب منّك تسامحني", - "hebrew_transliteration": "אַנַא מַא בַּטלֹבּ מִנַّכּ תְסַאמֶחנִי", - "hebrew": "אני לא מבקש ממך לסלוח לי", - "expected_phonemes": "ʔ a n a | m a | b a tˤ l o b | m i n n a k | t s aː m e ħ n i" - }, - { - "voice_time_seconds": { - "start": 56.0, - "end": 59.6 - }, - "arabic": "وبطلب تعمل معي، متل ما قال الله ورسوله.", - "hebrew_transliteration": "וּבַּטלֹבּ תַעמַל מַעִי מִתֶל מַא קַאל אַללַה וּרַסוּלוֹ.", - "hebrew": "ואני מבקש שתעשה בי כמו שאמרו אלוהים ושליחו.", - "expected_phonemes": "u b a tˤ l o b | t a ʕ m a l | m a ʕ i | m i t e l | m a | ʔ aː l | i l l a | u r a s uː l o" - }, - { - "voice_time_seconds": { - "start": 59.6, - "end": 62.6 - }, - "arabic": "منشان يتعلّم غيري.»", - "hebrew_transliteration": "מִנשַאן יִתעַלַّם עֵ'ירִי.", - "hebrew": "כדי שאחרים ילמדו.", - "expected_phonemes": "m i n ʃ aː n | j i t ʕ a l l a m | ɣ eː r i" - }, - { - "voice_time_seconds": { - "start": 62.6, - "end": 64.6 - }, - "arabic": "تعجّب الأمير وقال:", - "hebrew_transliteration": "תְעַגַّבּ אִל-אַמִיר וּקַאל:", - "hebrew": "האמיר השתומם ואמר:", - "expected_phonemes": "t ʕ a g g a b | i l ʔ a m iː r | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 64.6, - "end": 67.2 - }, - "arabic": "«إعترافك حيّرني!", - "hebrew_transliteration": "אִעתִרַאפַכּ חַיַّרְנִי!", - "hebrew": "ההודאה שלך מבלבלת אותי!", - "expected_phonemes": "ʔ i ʕ t i r aː f a k | ħ a j j a r n i" - }, - { - "voice_time_seconds": { - "start": 67.2, - "end": 69.6 - }, - "arabic": "حسب رأيي إنت مش حرامي،", - "hebrew_transliteration": "חַסַבּ רַאִיִי אִנתֶ מִש חַרַאמִי,", - "hebrew": "לדעתי אתה לא גנב,", - "expected_phonemes": "ħ a s a b | r a ʔ i j i | ʔ i n t e | m i ʃ | ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 69.6, - "end": 72.6 - }, - "arabic": "بسّ على كلّ حال عندك أقوال تانية؟", - "hebrew_transliteration": "בַּס עַלַא כֻּלّ חַאל עִנדַכּ אַקוַאל תַאנְיֶה?", - "hebrew": "אבל בכל מקרה, יש לך דברים נוספים לומר?", - "expected_phonemes": "b a s | ʕ a l a | k u l l | ħ aː l | ʕ i n d a k | ʔ a ʔ w aː l | t aː n j e" - }, - { - "voice_time_seconds": { - "start": 72.6, - "end": 75.4 - }, - "arabic": "بتحبّ تغيّر قصتك؟»", - "hebrew_transliteration": "בִּתחִבּ תְעַ'יֶّר קִצְّתַכּ?", - "hebrew": "אתה רוצה לשנות את הסיפור שלך?", - "expected_phonemes": "b i t ħ i b | t ɣ a j j e r | ʔ i sˤ sˤ t a k" - }, - { - "voice_time_seconds": { - "start": 75.4, - "end": 77.6 - }, - "arabic": "«أبدا،» قال الشّابّ،", - "hebrew_transliteration": "אַבַּדַן, קַאל אִ-שַּׁאבּ,", - "hebrew": "בשום אופן, אמר הבחור,", - "expected_phonemes": "ʔ a b a d a n | ʔ aː l | ʔ i ʃ aː b" - }, - { - "voice_time_seconds": { - "start": 77.6, - "end": 80.4 - }, - "arabic": "«ما عنديش غير إلّي سمعته!»", - "hebrew_transliteration": "מַא עִנדִיש עֵ'יר אִלִّי סִמִעתוֹ!", - "hebrew": "אין לי אלא מה ששמעת!", - "expected_phonemes": "m a | ʕ i n d iː ʃ | ɣ eː r | ʔ i l l i | s i m i ʕ t o" - }, - { - "voice_time_seconds": { - "start": 80.4, - "end": 83.6 - }, - "arabic": "الأمير زعل كتير وقال لخدمه:", - "hebrew_transliteration": "אִל-אַמִיר זִעֶל כְּתִיר וּקַאל לַחַ'דַמוֹ:", - "hebrew": "האמיר כעס מאוד ואמר למשרתיו:", - "expected_phonemes": "i l ʔ a m iː r | z i ʕ e l | k t iː r | u ʔ aː l | l a x a d a m o" - }, - { - "voice_time_seconds": { - "start": 83.6, - "end": 85.4 - }, - "arabic": "«خدوه عالسّجن!»", - "hebrew_transliteration": "חֻ'דוּה עַ-סִّגֶ'ן!", - "hebrew": "קחו אותו לכלא!", - "expected_phonemes": "x u d uː h | ʕ a s s i dʒ e n" - }, - { - "voice_time_seconds": { - "start": 85.4, - "end": 87.8 - }, - "arabic": "وأمر واحد ينادي في البصرة:", - "hebrew_transliteration": "וּאַמַר וַאחַד יְנַאדִי פִי אִל-בַּצרַה:", - "hebrew": "וציווה על מישהו להכריז בבצרה:", - "expected_phonemes": "u ʔ a m a r | w aː ħ a d | j n aː d i | f i | i l b a sˤ r a" - }, - { - "voice_time_seconds": { - "start": 87.8, - "end": 92.0 - }, - "arabic": "«إلّي بيحبّ يشوف المحاكمة للحرامي فلان،", - "hebrew_transliteration": "אִלִّי בִּיחִבּ יְשוּף אִל-מֻחַאכַּמֶה לַל-חַרַאמִי פֻלַאן,", - "hebrew": "מי שרוצה לראות את משפטו של הגנב פלוני,", - "expected_phonemes": "ʔ i l l i | b iː ħ i b | j ʃ uː f | i l m u ħ aː k a m e | l a l ħ a r aː m i | f u l aː n" - }, - { - "voice_time_seconds": { - "start": 92.0, - "end": 94.6 - }, - "arabic": "ييجي بكرا على المحكمة!»", - "hebrew_transliteration": "יִיגִ'י בֻּכּרַא עַל-מַחכַּמֶה!", - "hebrew": "שיבוא מחר לבית המשפט!", - "expected_phonemes": "j iː dʒ i | b u k r a | ʕ a l m a ħ k a m e" - }, - { - "voice_time_seconds": { - "start": 94.6, - "end": 97.6 - }, - "arabic": "تاني يوم، أجوا ناس كتير عالمحكمة", - "hebrew_transliteration": "תַאנִי יוֹם, אַגוּ נַאס כְּתִיר עַל-מַחכַּמֶה", - "hebrew": "למחרת, הרבה אנשים באו לבית המשפט", - "expected_phonemes": "t aː n i | j oː m | ʔ a g u | n aː s | k t iː r | ʕ a l m a ħ k a m e" - }, - { - "voice_time_seconds": { - "start": 97.6, - "end": 101.0 - }, - "arabic": "عشان يشوفوا ويحضروا عقاب الحرامي.", - "hebrew_transliteration": "עַשַאן יְשוּפוּ וִיחְדַ'רוּ עִקַאבּ אִל-חַרַאמִי.", - "hebrew": "כדי לראות ולהיות נוכחים בענישת הגנב.", - "expected_phonemes": "ʕ a ʃ aː n | j ʃ uː f u | w iː ħ dˤ a r u | ʕ i ʔ aː b | i l ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 101.0, - "end": 103.4 - }, - "arabic": "بعد شوي جابوا المتّهم،", - "hebrew_transliteration": "בַּעֶד שְוַיّ גַ'אבּוּ אִל-מֻתַّהַם,", - "hebrew": "אחרי זמן קצר הביאו את הנאשם,", - "expected_phonemes": "b a ʕ e d | ʃ w a j j | dʒ aː b u | i l m u t t a h a m" - }, - { - "voice_time_seconds": { - "start": 103.4, - "end": 106.4 - }, - "arabic": "ووقّف قدّام القاضي.", - "hebrew_transliteration": "וּוַקַّף קֻדַّאם אִל-קַאדִי.", - "hebrew": "והוא עמד לפני השופט.", - "expected_phonemes": "u w a ʔ ʔ a f | ʔ u d d aː m | i l ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 106.4, - "end": 108.6 - }, - "arabic": "القاضي اتطلّع عليه وسأله:", - "hebrew_transliteration": "אִל-קַאדִי אִתטַלַّע עַלֵיה וּסַאַלוֹ:", - "hebrew": "השופט הסתכל עליו ושאל אותו:", - "expected_phonemes": "i l ʔ aː d i | ʔ i t tˤ a l l a ʕ | ʕ a l eː h | u s a ʔ a l o" - }, - { - "voice_time_seconds": { - "start": 108.6, - "end": 110.4 - }, - "arabic": "«إنت مذنب؟»", - "hebrew_transliteration": "אִנתֶ מֻד'נֶבּ?", - "hebrew": "אתה אשם?", - "expected_phonemes": "ʔ i n t e | m u dˤ n e b" - }, - { - "voice_time_seconds": { - "start": 110.4, - "end": 114.6 - }, - "arabic": "الشّاب المتّهم جاوب بسرعة: «نعم.»", - "hebrew_transliteration": "אִ-שַّאבּ אִל-מֻתַّהַם גַ'אוַבּ בְּסֻרעַה: נַעַם.", - "hebrew": "הבחור הנאשם ענה מהר: כן.", - "expected_phonemes": "ʔ i ʃ ʃ aː b | i l m u t t a h a m | dʒ aː w a b | b s u r ʕ a | n a ʕ a m" - }, - { - "voice_time_seconds": { - "start": 114.6, - "end": 116.6 - }, - "arabic": "إستغرب القاضي وقال:", - "hebrew_transliteration": "אִסתַעְ'רַבּ אִל-קַאדִי וּקַאל:", - "hebrew": "השופט התפלא ואמר:", - "expected_phonemes": "ʔ i s t a ɣ r a b | i l ʔ aː d i | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 116.6, - "end": 119.4 - }, - "arabic": "«ممكن فيه معك شريك في إلّي سرقته؟»", - "hebrew_transliteration": "מֻמכֶּן פִיה מַעַכּ שַרִיכּ פִי אִלִّי סַרַקתוֹ?", - "hebrew": "אולי יש לך שותף במה שגנבת?", - "expected_phonemes": "m u m k e n | f iː h | m a ʕ a k | ʃ a r iː k | f i | ʔ i l l i | s a r a ʔ t o" - }, - { - "voice_time_seconds": { - "start": 119.4, - "end": 122.0 - }, - "arabic": "«أبدا،» قال المتّهم.", - "hebrew_transliteration": "אַבַּדַן, קַאל אִל-מֻתַّהַם.", - "hebrew": "בשום אופן, אמר הנאשם.", - "expected_phonemes": "ʔ a b a d a n | ʔ aː l | i l m u t t a h a m" - }, - { - "voice_time_seconds": { - "start": 122.0, - "end": 124.4 - }, - "arabic": "القاضي اتعصّب وقال:", - "hebrew_transliteration": "אִל-קַאדִי תְעַצַّבּ וּקַאל:", - "hebrew": "השופט כעס ואמר:", - "expected_phonemes": "i l ʔ aː d i | t ʕ a sˤ sˤ a b | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 124.4, - "end": 126.2 - }, - "arabic": "«إذا إنت بتعترف،", - "hebrew_transliteration": "אִדַ'ן אִנתֶ בִּתעתַרֶף,", - "hebrew": "אם כן אתה מודה,", - "expected_phonemes": "ʔ i dˤ a n | ʔ i n t e | b i t ʕ t a r e f" - }, - { - "voice_time_seconds": { - "start": 126.2, - "end": 127.6 - }, - "arabic": "وواجب عليك العقاب.", - "hebrew_transliteration": "וּוַאגֶ'בּ עַלֵיכּ אִל-עִקַאבּ.", - "hebrew": "ומגיע לך העונש.", - "expected_phonemes": "u w aː dʒ e b | ʕ a l eː k | i l ʕ i ʔ aː b" - }, - { - "voice_time_seconds": { - "start": 127.6, - "end": 131.2 - }, - "arabic": "يا جلّاد، إستعدّ تقطع إيده!»", - "hebrew_transliteration": "יַא גַ'לַّאד, אִסתַעֶדّ תֻקטַע אִידוֹ!", - "hebrew": "תליין, היכון לקטוע את ידו!", - "expected_phonemes": "j a | dʒ a l l aː d | ʔ i s t a ʕ e d d | t u ʔ tˤ a ʕ | ʔ iː d o" - }, - { - "voice_time_seconds": { - "start": 131.2, - "end": 133.6 - }, - "arabic": "لمّا سمعوا النّاس حكم القاضي،", - "hebrew_transliteration": "לַמַّא סִמעוּ אִ-נַّאס חֻכֶּם אִל-קַאדִי,", - "hebrew": "כשהאנשים שמעו את פסק הדין של השופט,", - "expected_phonemes": "l a m m a | s i m ʕ u | i n n aː s | ħ u k e m | i l ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 133.6, - "end": 135.6 - }, - "arabic": "صيّحوا وعملوا ضجّة.", - "hebrew_transliteration": "צַיַّחוּ וּעִמלוּ דַ'גַّה.", - "hebrew": "הם צעקו ועשו רעש.", - "expected_phonemes": "sˤ a j j a ħ u | u ʕ i m l u | dˤ a g g a" - }, - { - "voice_time_seconds": { - "start": 135.6, - "end": 138.0 - }, - "arabic": "ومن بين النّاس طلعت بنت", - "hebrew_transliteration": "וּמִן בֵּין אִ-נַّאס טִלעַת בִּנְת", - "hebrew": "ומבין האנשים יצאה נערה", - "expected_phonemes": "u m i n | b eː n | i n n aː s | tˤ i l ʕ a t | b i n t" - }, - { - "voice_time_seconds": { - "start": 138.0, - "end": 140.0 - }, - "arabic": "لابسة ملابس ممزّعة،", - "hebrew_transliteration": "לַאבְּסֶה מַלַאבֶּס מְמַזַّעַה,", - "hebrew": "לבושה בגדים קרועים,", - "expected_phonemes": "l aː b s e | m a l aː b e s | m m a z z a ʕ a" - }, - { - "voice_time_seconds": { - "start": 140.0, - "end": 143.0 - }, - "arabic": "ووجهها أحلى من القمر.", - "hebrew_transliteration": "וּוִגְ'הְהַא אַחלַא מִן אִל-קַמַר.", - "hebrew": "ופניה יפות מהירח.", - "expected_phonemes": "u w i dʒ h h a | ʔ a ħ l a | m i n | i l ʔ a m a r" - }, - { - "voice_time_seconds": { - "start": 143.0, - "end": 145.8 - }, - "arabic": "البنت رمت حالها عالشّابّ", - "hebrew_transliteration": "אִל-בִּנְת רַמַת חַאלְהַא עַ-שַּׁאבּ", - "hebrew": "הנערה השליכה את עצמה על הבחור", - "expected_phonemes": "i l b i n t | r a m a t | ħ aː l h a | ʕ a ʃ aː b" - }, - { - "voice_time_seconds": { - "start": 145.8, - "end": 148.4 - }, - "arabic": "صيّحت بصوت عالي وقالت:", - "hebrew_transliteration": "צַיַّחַת בְּצוֹת עַאלִי וּקַאלַת:", - "hebrew": "צעקה בקול רם ואמרה:", - "expected_phonemes": "sˤ a j j a ħ a t | b sˤ oː t | ʕ aː l i | u ʔ aː l a t" - }, - { - "voice_time_seconds": { - "start": 148.4, - "end": 150.2 - }, - "arabic": "«يا أمير، لا!", - "hebrew_transliteration": "יַא אַמִיר, לַא!", - "hebrew": "אמיר, לא!", - "expected_phonemes": "j a | ʔ a m iː r | l aː" - }, - { - "voice_time_seconds": { - "start": 150.2, - "end": 153.2 - }, - "arabic": "تقطعش إيده قبل ما تقرا هالورقة!»", - "hebrew_transliteration": "תֻקטַעֶש אִידוֹ קַבֶּל מַא תִקרַא הַל-וַרַקַה!", - "hebrew": "אל תקטע את ידו לפני שתקרא את הפתק הזה!", - "expected_phonemes": "t u ʔ tˤ a ʕ e ʃ | ʔ iː d o | ʔ a b e l | m a | t i ʔ r a | h a l w a r a ʔ a" - }, - { - "voice_time_seconds": { - "start": 153.2, - "end": 155.4 - }, - "arabic": "ورمت الورقة للأمير.", - "hebrew_transliteration": "וּרַמַת אִל-וַרַקַה לַל-אַמִיר.", - "hebrew": "וזרקה את הפתק לאמיר.", - "expected_phonemes": "u r a m a t | i l w a r a ʔ a | l a l ʔ a m iː r" - }, - { - "voice_time_seconds": { - "start": 155.4, - "end": 157.8 - }, - "arabic": "الأمير أخد الورقة وقراها.", - "hebrew_transliteration": "אִל-אַמִיר אַחַ'ד אִל-וַרַקַה וּקַרַאהַא.", - "hebrew": "האמיר לקח את הפתק וקרא אותו.", - "expected_phonemes": "i l ʔ a m iː r | ʔ a x a d | i l w a r a ʔ a | u ʔ a r aː h a" - }, - { - "voice_time_seconds": { - "start": 157.8, - "end": 159.8 - }, - "arabic": "وبعدها، نادى البنت", - "hebrew_transliteration": "וּבַּעדְהַא, נַאדַא אִל-בִּנְת", - "hebrew": "ואחר כך קרא לנערה", - "expected_phonemes": "u b a ʕ d h a | n aː d a | i l b i n t" - }, - { - "voice_time_seconds": { - "start": 159.8, - "end": 162.6 - }, - "arabic": "وطلب منها تفهّمه القصّة.", - "hebrew_transliteration": "וּטַלַבּ מִנְהַא תְפַהֶّמוֹ אִל-קִצַّה.", - "hebrew": "וביקש ממנה להסביר לו את הסיפור.", - "expected_phonemes": "u tˤ a l a b | m i n h a | t f a h h e m o | i l ʔ i sˤ sˤ a" - }, - { - "voice_time_seconds": { - "start": 162.6, - "end": 165.4 - }, - "arabic": "حكت البنت: «يا أمير،", - "hebrew_transliteration": "חַכַּת אִל-בִּנְת: יַא אַמִיר,", - "hebrew": "הנערה סיפרה: אמיר,", - "expected_phonemes": "ħ a k a t | i l b i n t | j a | ʔ a m iː r" - }, - { - "voice_time_seconds": { - "start": 165.4, - "end": 168.6 - }, - "arabic": "هدا الشّابّ بحبّني وأنا بحبّه.", - "hebrew_transliteration": "הַאדַא אִ-שַّאבּ בִּיחִבַּנִי וּאַנַא בַּחִבּוֹ.", - "hebrew": "הבחור הזה אוהב אותי ואני אוהבת אותו.", - "expected_phonemes": "h aː d a | ʔ i ʃ ʃ aː b | b iː ħ i b a n i | u ʔ a n a | b a ħ i b o" - }, - { - "voice_time_seconds": { - "start": 168.6, - "end": 171.6 - }, - "arabic": "هديك الليلة أجا عدارنا عشان يقابلني.", - "hebrew_transliteration": "הַדִיכּ אִ-לֵּילֶה אַגַ'א עַ-דַארְנַא עַשַאן יְקַאבֶּלנִי.", - "hebrew": "באותו לילה הוא בא לביתנו כדי לפגוש אותי.", - "expected_phonemes": "h a d iː k | ʔ i l eː l e | ʔ a dʒ a | ʕ a d aː r n a | ʕ a ʃ aː n | j ʔ aː b e l n i" - }, - { - "voice_time_seconds": { - "start": 171.6, - "end": 173.6 - }, - "arabic": "بسّ أهلي صحيوا عليه.", - "hebrew_transliteration": "בַּס אַהלִי צְחִיוּ עַלֵיה.", - "hebrew": "אבל משפחתי התעוררה עליו.", - "expected_phonemes": "b a s | ʔ a h l i | sˤ ħ iː u | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 173.6, - "end": 177.4 - }, - "arabic": "ومن خوف ما يفضحني عمل حاله حرامي.", - "hebrew_transliteration": "וּמִן חוֹף מַא יִפדַחנִי עִמֶל חַאלוֹ חַרַאמִי.", - "hebrew": "ומפחד שיבייש אותי, העמיד פנים שהוא גנב.", - "expected_phonemes": "u m i n | ħ oː f | m a | j i f d a ħ n i | ʕ i m e l | ħ aː l o | ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 177.4, - "end": 180.0 - }, - "arabic": "هدا كلّ إلّي صار.»", - "hebrew_transliteration": "הַאדַא כֻּלّ אִלִّי צַאר.", - "hebrew": "זה כל מה שקרה.", - "expected_phonemes": "h aː d a | k u l l | ʔ i l l i | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 180.0, - "end": 183.2 - }, - "arabic": "الأمير إنبسط كتير من الشّابّ وموقفه،", - "hebrew_transliteration": "אִל-אַמִיר אִנבַּסַט כְּתִיר מִן אִ-שַּׁאבּ וּמַוְקֶפוֹ,", - "hebrew": "האמיר מאוד התרשם מהבחור ומעמדתו,", - "expected_phonemes": "i l ʔ a m iː r | ʔ i n b a s a tˤ | k t iː r | m i n | ʔ i ʃ aː b | u m a w ʔ e f o" - }, - { - "voice_time_seconds": { - "start": 183.2, - "end": 185.6 - }, - "arabic": "وأمرله بعشر الاف دينار،", - "hebrew_transliteration": "וּאַמַר-לוֹ בְּעַשֶר תַאלַאף דִינַאר,", - "hebrew": "ונתן לו עשרת אלפים דינר,", - "expected_phonemes": "u ʔ a m a r l o | b ʕ a ʃ e r | t aː l aː f | d iː n aː r" - }, - { - "voice_time_seconds": { - "start": 185.6, - "end": 187.2 - }, - "arabic": "وطلب من أبو البنت", - "hebrew_transliteration": "וּטַלַבּ מִן אַבּוּ אִל-בִּנְת", - "hebrew": "וביקש מאבי הנערה", - "expected_phonemes": "u tˤ a l a b | m i n | ʔ a b u | i l b i n t" - }, - { - "voice_time_seconds": { - "start": 187.2, - "end": 190.2 - }, - "arabic": "إنّه يسمحلهم يتجوّزوا.", - "hebrew_transliteration": "אִנּוֹ יִסמַח-לְהֹם יִתגַ'וַّזוּ.", - "hebrew": "שירשה להם להתחתן.", - "expected_phonemes": "ʔ i n o | j i s m a ħ l h o m | j i t dʒ a w w a z u" - }, - { - "voice_time_seconds": { - "start": 190.2, - "end": 192.4 - }, - "arabic": "أبو البنت وافق", - "hebrew_transliteration": "אַבּוּ אִל-בִּנְת וַאפַק", - "hebrew": "אבי הנערה הסכים", - "expected_phonemes": "ʔ a b u | i l b i n t | w aː f a ʔ" - }, - { - "voice_time_seconds": { - "start": 192.4, - "end": 195.8 - }, - "arabic": "وهيك، تجوّز الشّابّ حبيبته", - "hebrew_transliteration": "וּהֵיכּ, תְגַ'וַّז אִ-שַּׁאבּ חַבִּיבְּתוֹ", - "hebrew": "וכך, הבחור התחתן עם אהובתו", - "expected_phonemes": "u h eː k | t dʒ a w w a z | ʔ i ʃ aː b | ħ a b iː b t o" - }, - { - "voice_time_seconds": { - "start": 195.8, - "end": 199.0 - }, - "arabic": "وعاشوا بثبات ونبات", - "hebrew_transliteration": "וּעַאשוּ בִּתַבַּאת וּנַבַּאת", - "hebrew": "וחיו באושר ועושר", - "expected_phonemes": "u ʕ aː ʃ u | b i t a b aː t | u n a b aː t" - }, - { - "voice_time_seconds": { - "start": 199.0, - "end": 205.2 - }, - "arabic": "وخلّفوا صبيان وبنات.", - "hebrew_transliteration": "וּחַ'לַّפוּ צִבְּיַאן וּבַּנַאת.", - "hebrew": "והולידו בנים ובנות.", - "expected_phonemes": "u x a l l a f u | sˤ i b j aː n | u b a n aː t" - } - ] - }, - { - "file_name": "החתולה * אִל-בִּסֶה * البسّة * סיפור בערבית מדוברת [iF-0_XBc-hI].webm", - "youtube_id": "iF-0_XBc-hI", - "title_hebrew": "החתולה", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "البسّة", - "hebrew_transliteration": "אִל-בִּסֶّה", - "hebrew": "החתולה", - "expected_phonemes": "i l b i s s e" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 10.4 - }, - "arabic": "مرّة كان فيه واحد جاي على باله", - "hebrew_transliteration": "מַרַّה כַּאן פִיה וַאחַד גַ'אי עַלַא בַּאלוֹ", - "hebrew": "פעם היה מישהו שהתחשק לו", - "expected_phonemes": "m a r r a | k aː n | f iː h | w aː ħ a d | dʒ aː j | ʕ a l a | b aː l o" - }, - { - "voice_time_seconds": { - "start": 10.4, - "end": 12.8 - }, - "arabic": "يوكل لحمة خروف.", - "hebrew_transliteration": "יוֹכֹּל לַחמֶת חַ'רוּף.", - "hebrew": "לאכול בשר כבש.", - "expected_phonemes": "j oː k o l | l a ħ m e t | x a r uː f" - }, - { - "voice_time_seconds": { - "start": 12.8, - "end": 15.8 - }, - "arabic": "منشان هيك، نزل عالسّوق", - "hebrew_transliteration": "מִנשַאן הֵיכּ, נִזֶל עַ-סּוּק", - "hebrew": "לכן ירד לשוק", - "expected_phonemes": "m i n ʃ aː n | h eː k | n i z e l | ʕ a s uː ʔ" - }, - { - "voice_time_seconds": { - "start": 15.8, - "end": 19.6 - }, - "arabic": "واشترى تلاتة كيلو لحمة خروف.", - "hebrew_transliteration": "וּאִשתַרַא תַלַאתֶה כִּילוֹ לַחמֶת חַ'רוּף.", - "hebrew": "וקנה שלושה קילו בשר כבש.", - "expected_phonemes": "u ʔ i ʃ t a r a | t a l aː t e | k iː l o | l a ħ m e t | x a r uː f" - }, - { - "voice_time_seconds": { - "start": 19.6, - "end": 24.2 - }, - "arabic": "روّح عبيته وحطّ اللّحمة في التّلّاجة،", - "hebrew_transliteration": "רַוַّח עַ-בֵּיתוֹ וּחַטّ אִ-לַّחמֶה פִי אִ-תַّלַّאגֶ'ה,", - "hebrew": "חזר הביתה ושם את הבשר במקרר,", - "expected_phonemes": "r a w w a ħ | ʕ a b eː t o | u ħ a tˤ tˤ | ʔ i l l a ħ m e | f i | ʔ i t t a l l aː dʒ e" - }, - { - "voice_time_seconds": { - "start": 24.2, - "end": 27.6 - }, - "arabic": "ووصّى مرته تطبخها للعشا.", - "hebrew_transliteration": "וּוַצַّא מַרְתוֹ תֻטבֻּחְ'הַא לַל-עַשַא.", - "hebrew": "והורה לאשתו לבשל אותו לארוחת הערב.", - "expected_phonemes": "u w a sˤ sˤ a | m a r t o | t u tˤ b u x h a | l a l ʕ a ʃ a" - }, - { - "voice_time_seconds": { - "start": 27.6, - "end": 32.0 - }, - "arabic": "بعدين طلع من البيت وراح عشغله.", - "hebrew_transliteration": "בַּעדֵין טִלֶע מִן אִל-בֵּית וּרַאח עַ-שֻעְ'לוֹ.", - "hebrew": "אחר כך יצא מהבית והלך לעבודה.", - "expected_phonemes": "b a ʕ d eː n | tˤ i l e ʕ | m i n | i l b eː t | u r aː ħ | ʕ a ʃ u ɣ l o" - }, - { - "voice_time_seconds": { - "start": 32.0, - "end": 35.6 - }, - "arabic": "مرته، إلّي كانت مشتاقة توكل لحمة،", - "hebrew_transliteration": "מַרְתוֹ, אִלִّי כַּאנַת מִשתַאקַה תוֹכֹּל לַחמֶה,", - "hebrew": "אשתו, שהשתוקקה לאכול בשר,", - "expected_phonemes": "m a r t o | ʔ i l l i | k aː n a t | m i ʃ t aː ʔ a | t oː k o l | l a ħ m e" - }, - { - "voice_time_seconds": { - "start": 35.6, - "end": 38.2 - }, - "arabic": "ما قدرتش تمسك حالها للعشا،", - "hebrew_transliteration": "מַא קִדְרַת-ש תְמַסֶּכּ חַאלְהַא לַל-עַשַא,", - "hebrew": "לא הצליחה להתאפק עד ארוחת הערב,", - "expected_phonemes": "m a | ʔ i d r a t ʃ | t m a s e k | ħ aː l h a | l a l ʕ a ʃ a" - }, - { - "voice_time_seconds": { - "start": 38.2, - "end": 41.0 - }, - "arabic": "وقرّرت تعزم صاحباتها", - "hebrew_transliteration": "וּקַרַّרַת תִעזֶם צַאחְבַּאתְהַא", - "hebrew": "והחליטה להזמין את חברותיה", - "expected_phonemes": "u ʔ a r r a r a t | t i ʕ z e m | sˤ aː ħ b aː t h a" - }, - { - "voice_time_seconds": { - "start": 41.0, - "end": 44.8 - }, - "arabic": "وتوكل اللّحمات هيّ وايّاهم.", - "hebrew_transliteration": "וּתוֹכֹּל אִ-לַّחמַאת הִיֶّ וִיַّאהֹם.", - "hebrew": "ולאכול את הבשר יחד איתן.", - "expected_phonemes": "u t oː k o l | ʔ i l l a ħ m aː t | h i j j e | w i j j aː h o m" - }, - { - "voice_time_seconds": { - "start": 44.8, - "end": 50.0 - }, - "arabic": "وهيك صار: حضّرت اللّحمة، بهّرتها، شوتها", - "hebrew_transliteration": "וּהֵיכּ צַאר: חַדַّרַת אִ-לַّחמֶה, בְּהַרַתְהַא, שַוַתְהַא,", - "hebrew": "וכך היה: היא הכינה את הבשר, תיבלה אותו, צלתה אותו,", - "expected_phonemes": "u h eː k | sˤ aː r | ħ a dˤ dˤ a r a t | ʔ i l l a ħ m e | b h a r a t h a | ʃ a w a t h a" - }, - { - "voice_time_seconds": { - "start": 50.0, - "end": 53.8 - }, - "arabic": "وأكلتها كلّها هيّ وصاحباتها.", - "hebrew_transliteration": "וּאַכַּלַתְהַא כֻּלְّהַא הִיֶّ וּצַאחְבַּאתְהַא.", - "hebrew": "ואכלה את כולו היא וחברותיה.", - "expected_phonemes": "u ʔ a k a l a t h a | k u l l h a | h i j j e | u sˤ aː ħ b aː t h a" - }, - { - "voice_time_seconds": { - "start": 53.8, - "end": 57.8 - }, - "arabic": "المسا، لمّا رجع جوزها على البيت،", - "hebrew_transliteration": "אִל-מַסַא, לַמַّא רִגֶ'ע גוֹזְהַא עַלַא אִל-בֵּית,", - "hebrew": "בערב, כשבעלה חזר הביתה,", - "expected_phonemes": "i l m a s a | l a m m a | r i dʒ e ʕ | g oː z h a | ʕ a l a | i l b eː t" - }, - { - "voice_time_seconds": { - "start": 57.8, - "end": 60.4 - }, - "arabic": "طلب منها تحطّله العشا.", - "hebrew_transliteration": "טַלַבּ מִנְהַא תְחֻטّ-לוֹ אִל-עַשַא.", - "hebrew": "ביקש ממנה להגיש לו את ארוחת הערב.", - "expected_phonemes": "tˤ a l a b | m i n h a | t ħ u tˤ tˤ l o | i l ʕ a ʃ a" - }, - { - "voice_time_seconds": { - "start": 60.4, - "end": 64.2 - }, - "arabic": "كان جوعان وعم بستنّى يوكل من اللّحمة", - "hebrew_transliteration": "כַּאן גוּעַאן וּעַם-בִּיסתַנַّא יוֹכֹּל מִן אִ-לַّחמֶה", - "hebrew": "הוא היה רעב וחיכה לאכול מהבשר", - "expected_phonemes": "k aː n | g uː ʕ aː n | u ʕ a m b iː s t a n n a | j oː k o l | m i n | ʔ i l l a ħ m e" - }, - { - "voice_time_seconds": { - "start": 64.2, - "end": 66.8 - }, - "arabic": "إلّي شراها الصّبح.", - "hebrew_transliteration": "אִלִّי שַרַאהַא אִ-צֻّבֻּח.", - "hebrew": "שקנה בבוקר.", - "expected_phonemes": "ʔ i l l i | ʃ a r aː h a | ʔ i sˤ sˤ u b u ħ" - }, - { - "voice_time_seconds": { - "start": 66.8, - "end": 70.0 - }, - "arabic": "قعد على الطّاولة وحضّر نفسه،", - "hebrew_transliteration": "קַעַד עַלַא אִ-טַّאוְלֶה וּחַדַّר נַפסוֹ,", - "hebrew": "התיישב ליד השולחן והתכונן,", - "expected_phonemes": "ʔ a ʕ a d | ʕ a l a | ʔ i tˤ tˤ aː w l e | u ħ a dˤ dˤ a r | n a f s o" - }, - { - "voice_time_seconds": { - "start": 70.0, - "end": 74.4 - }, - "arabic": "وإلّا مرته جاي بإيدها صحن رز.", - "hebrew_transliteration": "וּאִלַّא מַרְתוֹ גַ'אי בְּאִידְהַא צַחֶן רֻז.", - "hebrew": "והנה אשתו באה ובידה צלחת אורז.", - "expected_phonemes": "u ʔ i l l a | m a r t o | dʒ aː j | b ʔ iː d h a | sˤ a ħ e n | r u z" - }, - { - "voice_time_seconds": { - "start": 74.4, - "end": 77.8 - }, - "arabic": "صفن في الصّحن، إتطلّع على مرته", - "hebrew_transliteration": "צֻפֶן פִי אִ-צַّחֶן, אִתטַלַّע עַלַא מַרְתוֹ", - "hebrew": "הוא בהה בצלחת, הסתכל על אשתו", - "expected_phonemes": "sˤ u f e n | f i | ʔ i sˤ sˤ a ħ e n | ʔ i t tˤ a l l a ʕ | ʕ a l a | m a r t o" - }, - { - "voice_time_seconds": { - "start": 77.8, - "end": 80.2 - }, - "arabic": "ورجع صفن في الصّحن.", - "hebrew_transliteration": "וּרִגֶ'ע צֻפֶן פִי אִ-צַّחֶן.", - "hebrew": "וחזר לבהות בצלחת.", - "expected_phonemes": "u r i dʒ e ʕ | sˤ u f e n | f i | ʔ i sˤ sˤ a ħ e n" - }, - { - "voice_time_seconds": { - "start": 80.2, - "end": 84.8 - }, - "arabic": "بعد دقيقة طويلة سألها: «شو هدا؟»", - "hebrew_transliteration": "בַּעֶד דַקִיקַה טַוִילֶה סַאַלְהַא: שוּ הַאדַא?", - "hebrew": "אחרי דקה ארוכה שאל אותה: מה זה?", - "expected_phonemes": "b a ʕ e d | d a ʔ iː ʔ a | tˤ a w iː l e | s a ʔ a l h a | ʃ u | h aː d a" - }, - { - "voice_time_seconds": { - "start": 84.8, - "end": 89.6 - }, - "arabic": "مرته ردّت عليه وقالت: «هدا رز!»", - "hebrew_transliteration": "מַרְתוֹ רַדַّת עַלֵיה וּקַאלַת: הַאדַא רֻז!", - "hebrew": "אשתו ענתה לו ואמרה: זה אורז!", - "expected_phonemes": "m a r t o | r a d d a t | ʕ a l eː h | u ʔ aː l a t | h aː d a | r u z" - }, - { - "voice_time_seconds": { - "start": 89.6, - "end": 92.8 - }, - "arabic": "قاللها: «عارف إنّه هدا رز...", - "hebrew_transliteration": "קַאל-לְהַא: עַארֶף אִנּוֹ הַאדַא רֻז...", - "hebrew": "הוא אמר לה: אני יודע שזה אורז...", - "expected_phonemes": "ʔ aː l l h a | ʕ aː r e f | ʔ i n o | h aː d a | r u z" - }, - { - "voice_time_seconds": { - "start": 92.8, - "end": 96.6 - }, - "arabic": "بسّ وين لحمة الخروف إلّي اشتريتها الصّبح؟!", - "hebrew_transliteration": "בַּס וֵין לַחמֶת אִל-חַ'רוּף אִלִّי שתַרֵיתְהַא אִ-צֻּבֻּח?!", - "hebrew": "אבל איפה בשר הכבש שקניתי בבוקר?!", - "expected_phonemes": "b a s | w eː n | l a ħ m e t | i l x a r uː f | ʔ i l l i | ʃ t a r eː t h a | ʔ i sˤ u b u ħ" - }, - { - "voice_time_seconds": { - "start": 96.6, - "end": 100.2 - }, - "arabic": "مش طلبت منّك تطبخيها للعشا؟»", - "hebrew_transliteration": "מִש טַלַבֶּת מִנֶּכּ תֻטבֻּחִ'יהַא לַל-עַשַא?", - "hebrew": "לא ביקשתי ממך לבשל אותו לארוחת הערב?", - "expected_phonemes": "m i ʃ | tˤ a l a b e t | m i n e k | t u tˤ b u x iː h a | l a l ʕ a ʃ a" - }, - { - "voice_time_seconds": { - "start": 100.2, - "end": 105.0 - }, - "arabic": "قالتله: «مزبوط، بسّ إنت بتعرفش شو صار!»", - "hebrew_transliteration": "קַאלַת-לוֹ: מַזבּוּט, בַּס אִנתֶ בִּתִערַפֶש שוּ צַאר!", - "hebrew": "היא אמרה לו: נכון, אבל אתה לא יודע מה קרה!", - "expected_phonemes": "ʔ aː l a t l o | m a z b uː tˤ | b a s | ʔ i n t e | b i t i ʕ r a f e ʃ | ʃ u | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 105.0, - "end": 107.2 - }, - "arabic": "«شو صار؟» سألها.", - "hebrew_transliteration": "שוּ צַאר? סַאַלְהַא.", - "hebrew": "מה קרה? שאל אותה.", - "expected_phonemes": "ʃ u | sˤ aː r | s a ʔ a l h a" - }, - { - "voice_time_seconds": { - "start": 107.2, - "end": 110.6 - }, - "arabic": "قالت: «بعد ما طلعت على الشّغل،", - "hebrew_transliteration": "קַאלַת: בַּעֶד מַא טִלֶעת עַלַא אִ-שֻּׁעְ'ל,", - "hebrew": "היא אמרה: אחרי שיצאת לעבודה,", - "expected_phonemes": "ʔ aː l a t | b a ʕ e d | m a | tˤ i l e ʕ t | ʕ a l a | ʔ i ʃ u ɣ l" - }, - { - "voice_time_seconds": { - "start": 110.6, - "end": 112.8 - }, - "arabic": "بلّشت أجهّز الأكل:", - "hebrew_transliteration": "בַּלַّשֶת אַגַ'הֶّז אִל-אַכֶּל:", - "hebrew": "התחלתי להכין את האוכל:", - "expected_phonemes": "b a l l a ʃ e t | ʔ a dʒ a h h e z | i l ʔ a k e l" - }, - { - "voice_time_seconds": { - "start": 112.8, - "end": 114.8 - }, - "arabic": "طلّعت اللّحمة من التّلّاجة", - "hebrew_transliteration": "טַלַّעֶת אִ-לַّחמֶה מִן אִ-תַּלַّאגֶ'ה", - "hebrew": "הוצאתי את הבשר מהמקרר", - "expected_phonemes": "tˤ a l l a ʕ e t | ʔ i l l a ħ m e | m i n | ʔ i t a l l aː dʒ e" - }, - { - "voice_time_seconds": { - "start": 114.8, - "end": 118.0 - }, - "arabic": "ودرت ضهري عنها وبلّشت أشتغل.", - "hebrew_transliteration": "וּדַרֶת דַ'הרִי עַנְהַא וּבַּלַّשֶת אַשתְעֶ'ל.", - "hebrew": "והפניתי לו את הגב והתחלתי לעבוד.", - "expected_phonemes": "u d a r e t | dˤ a h r i | ʕ a n h a | u b a l l a ʃ e t | ʔ a ʃ t ɣ e l" - }, - { - "voice_time_seconds": { - "start": 118.0, - "end": 120.4 - }, - "arabic": "لمّا رجعتلها، كانت البسّة", - "hebrew_transliteration": "לַמַّא רְגִ'עֶת-לְהַא, כַּאנַת אִל-בִּסֶّה", - "hebrew": "כשחזרתי אליו, החתולה", - "expected_phonemes": "l a m m a | r dʒ i ʕ e t l h a | k aː n a t | i l b i s s e" - }, - { - "voice_time_seconds": { - "start": 120.4, - "end": 123.0 - }, - "arabic": "صارت داخلة على البيت من الشّبّاك", - "hebrew_transliteration": "צַארַת דַאחְ'לֶה עַלַא אִל-בֵּית מִן אִ-שֻּׁבַּّאכּ", - "hebrew": "כבר נכנסה הביתה מהחלון", - "expected_phonemes": "sˤ aː r a t | d aː x l e | ʕ a l a | i l b eː t | m i n | ʔ i ʃ u b b aː k" - }, - { - "voice_time_seconds": { - "start": 123.0, - "end": 126.2 - }, - "arabic": "وماكليتها ومش مخلّية إشي.", - "hebrew_transliteration": "וּמַאכְּלִיתְהַא וּמִש מְחַ'לְّיֶה אִשִי.", - "hebrew": "ואכלה אותו ולא השאירה כלום.", - "expected_phonemes": "u m aː k l iː t h a | u m i ʃ | m x a l l j e | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 126.2, - "end": 130.0 - }, - "arabic": "لحقتها بسّ بعد ما خلّصت كلّ إشي.»", - "hebrew_transliteration": "לַחִקתְהַא בַּס בַּעֶד מַא חַ'לַّצַת כֻּלّ אִשִי.", - "hebrew": "הדבקתי אותה רק אחרי שגמרה הכול.", - "expected_phonemes": "l a ħ i ʔ t h a | b a s | b a ʕ e d | m a | x a l l a sˤ a t | k u l l | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 130.0, - "end": 133.2 - }, - "arabic": "الزّلمة تعجّب كتير من هدا الحكي", - "hebrew_transliteration": "אִ-זַّלַמֶה תְעַגַّבּ כְּתִיר מִן הַאדַא אִל-חַכִּי", - "hebrew": "האיש השתומם מאוד מהסיפור הזה", - "expected_phonemes": "ʔ i z z a l a m e | t ʕ a g g a b | k t iː r | m i n | h aː d a | i l ħ a k i" - }, - { - "voice_time_seconds": { - "start": 133.2, - "end": 135.6 - }, - "arabic": "وقرّر يفحص الموضوع.", - "hebrew_transliteration": "וּקַרַّר יִפחַץ אִל-מַוְדוּע.", - "hebrew": "והחליט לבדוק את העניין.", - "expected_phonemes": "u ʔ a r r a r | j i f ħ a sˤ | i l m a w dˤ uː ʕ" - }, - { - "voice_time_seconds": { - "start": 135.6, - "end": 139.4 - }, - "arabic": "راح جاب البسّة وجاب ميزان.", - "hebrew_transliteration": "רַאח גַ'אבּ אִל-בִּסֶّה וּגַ'אבּ מִיזַאן.", - "hebrew": "הלך, הביא את החתולה והביא משקל.", - "expected_phonemes": "r aː ħ | dʒ aː b | i l b i s s e | u dʒ aː b | m iː z aː n" - }, - { - "voice_time_seconds": { - "start": 139.4, - "end": 142.8 - }, - "arabic": "حطّ البسّة على الميزان ووزّنها.", - "hebrew_transliteration": "חַטّ אִל-בִּסֶّה עַלַא אִל-מִיזַאן וּוַזַّנְהַא.", - "hebrew": "שם את החתולה על המשקל ושקל אותה.", - "expected_phonemes": "ħ a tˤ tˤ | i l b i s s e | ʕ a l a | i l m iː z aː n | u w a z z a n h a" - }, - { - "voice_time_seconds": { - "start": 142.8, - "end": 146.6 - }, - "arabic": "طلع ميزانها تلاتة كيلو بالزّبط!", - "hebrew_transliteration": "טִלֶע מִיזַאנְהַא תַלַאתֶה כִּילוֹ בִּ-זַّבֶּט!", - "hebrew": "התברר שהיא שוקלת שלושה קילו בדיוק!", - "expected_phonemes": "tˤ i l e ʕ | m iː z aː n h a | t a l aː t e | k iː l o | b i z z a b e tˤ" - }, - { - "voice_time_seconds": { - "start": 146.6, - "end": 149.8 - }, - "arabic": "الزّلمة إستغرب كتير من وزن البسّة،", - "hebrew_transliteration": "אִ-זַّלַמֶה סְתַעְ'רַבּ כְּתִיר מִן וַזְן אִל-בִּסֶّה,", - "hebrew": "האיש התפלא מאוד ממשקלה של החתולה,", - "expected_phonemes": "ʔ i z z a l a m e | s t a ɣ r a b | k t iː r | m i n | w a z n | i l b i s s e" - }, - { - "voice_time_seconds": { - "start": 149.8, - "end": 151.6 - }, - "arabic": "وقعد يحكي مع نفسه:", - "hebrew_transliteration": "וּקַעַד יִחכִּי מַע נַפסוֹ:", - "hebrew": "וישב ודיבר עם עצמו:", - "expected_phonemes": "u ʔ a ʕ a d | j i ħ k i | m a ʕ | n a f s o" - }, - { - "voice_time_seconds": { - "start": 151.6, - "end": 155.2 - }, - "arabic": "طيّب، إذا وزن البسّة تلاتة كيلو،", - "hebrew_transliteration": "טַיֶּבּ, אִזַא וַזְן אִל-בִּסֶّה תַלַאתֶה כִּילוֹ,", - "hebrew": "טוב, אם החתולה שוקלת שלושה קילו,", - "expected_phonemes": "tˤ a j e b | ʔ i z a | w a z n | i l b i s s e | t a l aː t e | k iː l o" - }, - { - "voice_time_seconds": { - "start": 155.2, - "end": 157.4 - }, - "arabic": "شو مع اللّحمة إلّي أكلتها؟", - "hebrew_transliteration": "שוּ מַע אִ-לַّחמֶה אִלִّי אַכַּלַתְהַא?", - "hebrew": "מה עם הבשר שהיא אכלה?", - "expected_phonemes": "ʃ u | m a ʕ | ʔ i l l a ħ m e | ʔ i l l i | ʔ a k a l a t h a" - }, - { - "voice_time_seconds": { - "start": 157.4, - "end": 159.8 - }, - "arabic": "وين راح وزن اللّحمة؟", - "hebrew_transliteration": "וֵין רַאח וַזְן אִ-לַّחמֶה?", - "hebrew": "לאן הלך משקל הבשר?", - "expected_phonemes": "w eː n | r aː ħ | w a z n | ʔ i l l a ħ m e" - }, - { - "voice_time_seconds": { - "start": 159.8, - "end": 163.4 - }, - "arabic": "وإذا هدول التّلاتة كيلو همّ وزن اللّحمة،", - "hebrew_transliteration": "וּאִזַא הַדוֹל אִ-תַּלַאתֶה כִּילוֹ הֻמّ וַזְן אִ-לַّחמֶה,", - "hebrew": "ואם שלושת הקילו האלה הם משקל הבשר,", - "expected_phonemes": "u ʔ i z a | h a d oː l | ʔ i t a l aː t e | k iː l o | h u m m | w a z n | ʔ i l l a ħ m e" - }, - { - "voice_time_seconds": { - "start": 163.4, - "end": 166.2 - }, - "arabic": "شو مع البسّة؟ وين راحت؟", - "hebrew_transliteration": "שוּ מַע אִל-בִּסֶّה? וֵין רַאחַת?", - "hebrew": "מה עם החתולה? לאן היא הלכה?", - "expected_phonemes": "ʃ u | m a ʕ | i l b i s s e | w eː n | r aː ħ a t" - } - ] - }, - { - "file_name": "המזל הרע * אִל-חָז אִל-עָאטֶל * الحظ العاطل * סיפור בערבית מדוברת [6QubRNusq_g].webm", - "youtube_id": "6QubRNusq_g", - "title_hebrew": "המזל הרע", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "الحظ العاطل", - "hebrew_transliteration": "אִל-חַט' אִל-עַאטֶל", - "hebrew": "המזל הרע", - "expected_phonemes": "i l ħ a ðˤ | i l ʕ aː tˤ e l" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 9.8 - }, - "arabic": "بحكوا عن حاكم إلّي كان قاعد بشي صبح", - "hebrew_transliteration": "בִּיחכּוּ עַן חַאכֶּם אִלִّי כַּאן קַאעֶד בְּשִי צֻבֶּח", - "hebrew": "מספרים על שליט שישב בוקר אחד", - "expected_phonemes": "b iː ħ k u | ʕ a n | ħ aː k e m | ʔ i l l i | k aː n | ʔ aː ʕ e d | b ʃ i | sˤ u b e ħ" - }, - { - "voice_time_seconds": { - "start": 9.8, - "end": 12.4 - }, - "arabic": "على بلكونه عم بفطر.", - "hebrew_transliteration": "עַלַא בַּלכּוֹנוֹ עַם-בִּפטַר.", - "hebrew": "במרפסת שלו ואכל ארוחת בוקר.", - "expected_phonemes": "ʕ a l a | b a l k oː n o | ʕ a m b i f tˤ a r" - }, - { - "voice_time_seconds": { - "start": 12.4, - "end": 16.6 - }, - "arabic": "في نفس الوقت مرق شخص من جنب بلكونه.", - "hebrew_transliteration": "פִי נַפֶס אִל-וַקת מַרַק שַחְ'ץ מִן גַ'נבּ בַּלכּוֹנוֹ.", - "hebrew": "באותו זמן עבר אדם ליד המרפסת שלו.", - "expected_phonemes": "f i | n a f e s | i l w a ʔ t | m a r a ʔ | ʃ a x sˤ | m i n | dʒ a n b | b a l k oː n o" - }, - { - "voice_time_seconds": { - "start": 16.6, - "end": 19.0 - }, - "arabic": "لمّا شاف الحاكم هدا الشّخص", - "hebrew_transliteration": "לַמַّא שַאף אִל-חַאכֶּם הַאדַא אִ-שַּׁחְ'ץ", - "hebrew": "כשהשליט ראה את האדם הזה", - "expected_phonemes": "l a m m a | ʃ aː f | i l ħ aː k e m | h aː d a | ʔ i ʃ a x sˤ" - }, - { - "voice_time_seconds": { - "start": 19.0, - "end": 23.2 - }, - "arabic": "وقعت شقفة خبز من إيده الممدودة لتمّه.", - "hebrew_transliteration": "וִקעַת שֻקפֶת חֻ'בֶּז מִן אִידוֹ אִל-מַמדוּדֶה לַתֻמּוֹ.", - "hebrew": "נפלה חתיכת לחם מידו שהושטה אל פיו.", - "expected_phonemes": "w i ʔ ʕ a t | ʃ u ʔ f e t | x u b e z | m i n | ʔ iː d o | i l m a m d uː d e | l a t u m o" - }, - { - "voice_time_seconds": { - "start": 23.2, - "end": 25.0 - }, - "arabic": "الحاكم تعصّب", - "hebrew_transliteration": "אִל-חַאכֶּם תְעַצַّבּ", - "hebrew": "השליט כעס", - "expected_phonemes": "i l ħ aː k e m | t ʕ a sˤ sˤ a b" - }, - { - "voice_time_seconds": { - "start": 25.0, - "end": 27.8 - }, - "arabic": "وأمر حرّاسه يجيبوا هدا الزّلمة،", - "hebrew_transliteration": "וּאַמַר חֻרַّאסוֹ יְגִ'יבּוּ הַאדַא אִ-זַّלַמֶה,", - "hebrew": "וציווה על שומריו להביא את האיש הזה,", - "expected_phonemes": "u ʔ a m a r | ħ u r r aː s o | j dʒ iː b u | h aː d a | ʔ i z z a l a m e" - }, - { - "voice_time_seconds": { - "start": 27.8, - "end": 30.8 - }, - "arabic": "وأمرهم يحبسوه ويموّتوه.", - "hebrew_transliteration": "וּאַמַרְהֹם יִחבְּסוּה וִימַוְّתוּה.", - "hebrew": "וציווה עליהם לכלוא אותו ולהמית אותו.", - "expected_phonemes": "u ʔ a m a r h o m | j i ħ b s uː h | w iː m a w w t uː h" - }, - { - "voice_time_seconds": { - "start": 30.8, - "end": 34.0 - }, - "arabic": "لمّا جابوا الشّخص عشان يعدموه، قالوله:", - "hebrew_transliteration": "לַמַّא גַ'אבּוּ אִ-שַּׁחְ'ץ עַשַאן יִעדְמוּה, קַאלוּ-לוֹ:", - "hebrew": "כשהביאו את האדם כדי להוציא אותו להורג, אמרו לו:", - "expected_phonemes": "l a m m a | dʒ aː b u | ʔ i ʃ a x sˤ | ʕ a ʃ aː n | j i ʕ d m uː h | ʔ aː l uː l o" - }, - { - "voice_time_seconds": { - "start": 34.0, - "end": 36.6 - }, - "arabic": "«بحسب تقاليد الإعدام،", - "hebrew_transliteration": "בְּחַסַבּ תַקַאלִיד אִל-אִעדַאם,", - "hebrew": "לפי מנהגי ההוצאה להורג,", - "expected_phonemes": "b ħ a s a b | t a ʔ aː l iː d | i l ʔ i ʕ d aː m" - }, - { - "voice_time_seconds": { - "start": 36.6, - "end": 39.8 - }, - "arabic": "بتقدر إنّه تطلب أمنية واحدة.»", - "hebrew_transliteration": "בְּתִקדַר אִנּוֹ תֻטלֹבּ אֻמנִיֶה וַאחדֶה.", - "hebrew": "אתה יכול לבקש משאלה אחת.", - "expected_phonemes": "b t i ʔ d a r | ʔ i n o | t u tˤ l o b | ʔ u m n i j e | w aː ħ d e" - }, - { - "voice_time_seconds": { - "start": 39.8, - "end": 41.4 - }, - "arabic": "الشّخص قال:", - "hebrew_transliteration": "אִ-שַּׁחְ'ץ קַאל:", - "hebrew": "האדם אמר:", - "expected_phonemes": "ʔ i ʃ a x sˤ | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 41.4, - "end": 45.6 - }, - "arabic": "«نفسي أشوف الحاكم وأسأله سؤال.»", - "hebrew_transliteration": "נִפסִי אַשוּף אִל-חַאכֶּם וּאַסאַלוֹ סֻאַאל.", - "hebrew": "אני רוצה לראות את השליט ולשאול אותו שאלה.", - "expected_phonemes": "n i f s i | ʔ a ʃ uː f | i l ħ aː k e m | u ʔ a s ʔ a l o | s u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 45.6, - "end": 48.2 - }, - "arabic": "جابوا الحرّاس الشّخص المحكوم بالإعدام", - "hebrew_transliteration": "גַ'אבּוּ אִל-חֻרַّאס אִ-שַּׁחְ'ץ אִל-מַחכּוּם בִּל-אִעדַאם", - "hebrew": "השומרים הביאו את הנידון למוות", - "expected_phonemes": "dʒ aː b u | i l ħ u r r aː s | ʔ i ʃ a x sˤ | i l m a ħ k uː m | b i l ʔ i ʕ d aː m" - }, - { - "voice_time_seconds": { - "start": 48.2, - "end": 50.4 - }, - "arabic": "لعند الحاكم.", - "hebrew_transliteration": "לַעִנד אִל-חַאכֶּם.", - "hebrew": "אל השליט.", - "expected_phonemes": "l a ʕ i n d | i l ħ aː k e m" - }, - { - "voice_time_seconds": { - "start": 50.4, - "end": 52.0 - }, - "arabic": "الشّخص قال:", - "hebrew_transliteration": "אִ-שַּׁחְ'ץ קַאל:", - "hebrew": "האדם אמר:", - "expected_phonemes": "ʔ i ʃ a x sˤ | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 52.0, - "end": 55.8 - }, - "arabic": "«سيدي حاكم الأرض، الله يطوّل في عمرك،", - "hebrew_transliteration": "סִידִי חַאכֶּם אִל-אַרְד', אַללַה יְטַוֶّל פִי עֻמרַכּ,", - "hebrew": "אדוני שליט הארץ, שאלוהים יאריך את ימיך,", - "expected_phonemes": "s iː d i | ħ aː k e m | i l ʔ a r dˤ | i l l a | j tˤ a w w e l | f i | ʕ u m r a k" - }, - { - "voice_time_seconds": { - "start": 55.8, - "end": 59.2 - }, - "arabic": "شو ذنبي عشان تحكم عليّ بالإعدام؟»", - "hebrew_transliteration": "שוּ דַ'נבִּי עַשַאן תֻחכֹּם עַלַיّ בִּל-אִעדַאם?", - "hebrew": "מה חטאתי שתגזור עליי מוות?", - "expected_phonemes": "ʃ u | dˤ a n b i | ʕ a ʃ aː n | t u ħ k o m | ʕ a l a j j | b i l ʔ i ʕ d aː m" - }, - { - "voice_time_seconds": { - "start": 59.2, - "end": 60.6 - }, - "arabic": "الحاكم قال:", - "hebrew_transliteration": "אִל-חַאכֶּם קַאל:", - "hebrew": "השליט אמר:", - "expected_phonemes": "i l ħ aː k e m | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 60.6, - "end": 63.4 - }, - "arabic": "«عشانك بتجيب العين العاطلة.", - "hebrew_transliteration": "עַשַאנַכּ בִּתגִ'יבּ אִל-עֵין אִל-עַאטְלֶה.", - "hebrew": "כי אתה מביא עין רעה.", - "expected_phonemes": "ʕ a ʃ aː n a k | b i t dʒ iː b | i l ʕ eː n | i l ʕ aː tˤ l e" - }, - { - "voice_time_seconds": { - "start": 63.4, - "end": 66.4 - }, - "arabic": "في الوقت إلّي كنت قاعد على البلكون", - "hebrew_transliteration": "פִי אִל-וַקת אִלִّי כֻּנֶת קַאעֶד עַלַא אִל-בַּלכּוֹן", - "hebrew": "בזמן שישבתי במרפסת", - "expected_phonemes": "f i | i l w a ʔ t | ʔ i l l i | k u n e t | ʔ aː ʕ e d | ʕ a l a | i l b a l k oː n" - }, - { - "voice_time_seconds": { - "start": 66.4, - "end": 71.2 - }, - "arabic": "باكل فطوري، شفتك وشقفة الخبز وقعت منّي.»", - "hebrew_transliteration": "בַּאכֻּל פֻטוּרִי, שֻפתַכּ וּשֻקפֶת אִל-חֻ'בֶּז וִקעַת מִנִّי.", - "hebrew": "ואכלתי את ארוחת הבוקר שלי, ראיתי אותך וחתיכת הלחם נפלה ממני.", - "expected_phonemes": "b aː k u l | f u tˤ uː r i | ʃ u f t a k | u ʃ u ʔ f e t | i l x u b e z | w i ʔ ʕ a t | m i n n i" - }, - { - "voice_time_seconds": { - "start": 71.2, - "end": 72.8 - }, - "arabic": "الشّخص قال:", - "hebrew_transliteration": "אִ-שַּׁחְ'ץ קַאל:", - "hebrew": "האדם אמר:", - "expected_phonemes": "ʔ i ʃ a x sˤ | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 72.8, - "end": 76.0 - }, - "arabic": "«إنت يا سيدي حكمت عليّ أموت", - "hebrew_transliteration": "אִנתֶ יַא סִידִי חַכַּמֶת עַלַיّ אַמוּת", - "hebrew": "אתה, אדוני, גזרת עליי למות", - "expected_phonemes": "ʔ i n t e | j a | s iː d i | ħ a k a m e t | ʕ a l a j j | ʔ a m uː t" - }, - { - "voice_time_seconds": { - "start": 76.0, - "end": 78.2 - }, - "arabic": "وإذا حكمي الموت، بدّي أقولّك،", - "hebrew_transliteration": "וּאִד'ַא חֻכּמִי לַ-מוֹת, בַּדִّי אַקוּל-לַכּ,", - "hebrew": "ואם דיני הוא מוות, אני רוצה לומר לך,", - "expected_phonemes": "u ʔ i dˤ a | ħ u k m i | l a m oː t | b a d d i | ʔ a ʔ uː l l a k" - }, - { - "voice_time_seconds": { - "start": 78.2, - "end": 80.8 - }, - "arabic": "إنّه إنت إلّي بتجيب حظ عاطل.", - "hebrew_transliteration": "אִנּוֹ אִנתֶ אִלִّי בִּתגִ'יבּ חַט' עַאטֶל.", - "hebrew": "שאתה הוא שמביא מזל רע.", - "expected_phonemes": "ʔ i n o | ʔ i n t e | ʔ i l l i | b i t dʒ iː b | ħ a ðˤ | ʕ aː tˤ e l" - }, - { - "voice_time_seconds": { - "start": 80.8, - "end": 84.0 - }, - "arabic": "لمّا أنا شفتك، إنحكم عليّ الموت.»", - "hebrew_transliteration": "לַמַّא אַנַא שֻפתַכּ, אִנחַכַּם עַלַיّ לַ-מוֹת.", - "hebrew": "כשאני ראיתי אותך, נגזר עליי מוות.", - "expected_phonemes": "l a m m a | ʔ a n a | ʃ u f t a k | ʔ i n ħ a k a m | ʕ a l a j j | l a m oː t" - }, - { - "voice_time_seconds": { - "start": 84.0, - "end": 86.0 - }, - "arabic": "الحاكم ضحك وقال:", - "hebrew_transliteration": "אִל-חַאכֶּם דִ'חֶכּ וּקַאל:", - "hebrew": "השליט צחק ואמר:", - "expected_phonemes": "i l ħ aː k e m | dˤ i ħ e k | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 86.0, - "end": 89.6 - }, - "arabic": "«روح لطريقك بسلام، عشان الله رحيم،", - "hebrew_transliteration": "רוּח לַטַרִיקַכּ בִּסַלַאם, עַשַאן אַללַה רַחִים,", - "hebrew": "לך לדרכך בשלום, כי אלוהים רחום,", - "expected_phonemes": "r uː ħ | l a tˤ a r iː ʔ a k | b i s a l aː m | ʕ a ʃ aː n | i l l a | r a ħ iː m" - }, - { - "voice_time_seconds": { - "start": 89.6, - "end": 91.8 - }, - "arabic": "هوّ إلّي رح يغفرلك.»", - "hebrew_transliteration": "הֻוֶّ אִלִّי רַח יִעְ'פִר-לַכּ.", - "hebrew": "הוא שיסלח לך.", - "expected_phonemes": "h u w w e | ʔ i l l i | r a ħ | j i ɣ f i r l a k" - } - ] - }, - { - "file_name": "המשורר ואשת הח'ליף * אִ-שַאעֶר וזַוּגַ'ת אִל-חַלִיפֶה * الشّاعر وزوجة الخليفة * סיפור בערבית מדוברת [kvsYU5k1nvk].webm", - "youtube_id": "kvsYU5k1nvk", - "title_hebrew": "המשורר ואשת הח'ליף", - "segments": [ - { - "voice_time_seconds": { - "start": 8.2, - "end": 10.8 - }, - "arabic": "كان للخليفة الوليد إبن عبد الملك", - "hebrew_transliteration": "כַּאן לַל-חַ'לִיפֶה אִל-וַלִיד אִבְּן עַבְּד אִל-מַלִכּ", - "hebrew": "לח'ליף אל-ואליד בן עבד אל-מלכ היו", - "expected_phonemes": "k aː n | l a l x a l iː f e | i l w a l iː d | ʔ i b n | ʕ a b d | i l m a l i k" - }, - { - "voice_time_seconds": { - "start": 10.8, - "end": 12.4 - }, - "arabic": "كتير نساء.", - "hebrew_transliteration": "כְּתִיר נִסַאא.", - "hebrew": "הרבה נשים.", - "expected_phonemes": "k t iː r | n i s aː a" - }, - { - "voice_time_seconds": { - "start": 12.4, - "end": 15.0 - }, - "arabic": "بسّ أحلى وأحبّ واحدة على قلبه", - "hebrew_transliteration": "בַּס אַחלַא וּאַחַבّ וַאחדֶה עַלַא קַלבּוֹ", - "hebrew": "אבל היפה והאהובה ביותר על ליבו", - "expected_phonemes": "b a s | ʔ a ħ l a | u ʔ a ħ a b b | w aː ħ d e | ʕ a l a | ʔ a l b o" - }, - { - "voice_time_seconds": { - "start": 15.0, - "end": 16.8 - }, - "arabic": "كانت روضة.", - "hebrew_transliteration": "כַּאנַת רַוְדַ'ה.", - "hebrew": "הייתה רווידה.", - "expected_phonemes": "k aː n a t | r a w dˤ a" - }, - { - "voice_time_seconds": { - "start": 16.8, - "end": 20.2 - }, - "arabic": "لمّا راحت روضة على الحجّ في مكّة المكرّمة،", - "hebrew_transliteration": "לַמַّא רַאחַת רַוְדַ'ה עַלַא אִל-חַגّ פִי מַכֶּّה אִל-מֻכַּרַّמֶה,", - "hebrew": "כשרווידה הלכה לעלייה לרגל במכה הקדושה,", - "expected_phonemes": "l a m m a | r aː ħ a t | r a w dˤ a | ʕ a l a | i l ħ a g g | f i | m a k k e | i l m u k a r r a m e" - }, - { - "voice_time_seconds": { - "start": 20.2, - "end": 22.0 - }, - "arabic": "شافها واحد من الشّعراء", - "hebrew_transliteration": "שַאפְהַא וַאחַד מִן אִ-שֻּׁעַרַאא", - "hebrew": "ראה אותה אחד המשוררים", - "expected_phonemes": "ʃ aː f h a | w aː ħ a d | m i n | ʔ i ʃ u ʕ a r aː a" - }, - { - "voice_time_seconds": { - "start": 22.0, - "end": 24.6 - }, - "arabic": "والحبّ اشتعل بين لتنين.", - "hebrew_transliteration": "וִל-חֻבّ אִשתַעַל בֵּין לֶ-תְנֵין.", - "hebrew": "והאהבה בערה בין השניים.", - "expected_phonemes": "w i l ħ u b b | ʔ i ʃ t a ʕ a l | b eː n | l e t n eː n" - }, - { - "voice_time_seconds": { - "start": 24.6, - "end": 28.6 - }, - "arabic": "من يوم ما شافها، بطّل يوكل ولا ينام", - "hebrew_transliteration": "מִן יוֹם מַא שַאפְהַא, בַּטַّל יוֹכֹּל וְלַא יְנַאם", - "hebrew": "מיום שראה אותה, הפסיק לאכול ולישון", - "expected_phonemes": "m i n | j oː m | m a | ʃ aː f h a | b a tˤ tˤ a l | j oː k o l | u l aː | j n aː m" - }, - { - "voice_time_seconds": { - "start": 28.6, - "end": 32.0 - }, - "arabic": "وكلّ أفكاره وعقله كانوا عليها.", - "hebrew_transliteration": "וּכֻּלّ אַפכַּארוֹ וּעַקְלוֹ כַּאנוּ עַלֵיהַא.", - "hebrew": "וכל מחשבותיו ושכלו היו נתונים לה.", - "expected_phonemes": "u k u l l | ʔ a f k aː r o | u ʕ a ʔ l o | k aː n u | ʕ a l eː h a" - }, - { - "voice_time_seconds": { - "start": 32.0, - "end": 34.2 - }, - "arabic": "لمّا رجعت السّيّدة على دمشق،", - "hebrew_transliteration": "לַמַّא רִגְעַת אִ-סַّיֶּדֶה עַלַא דִמַשק,", - "hebrew": "כשהגבירה חזרה לדמשק,", - "expected_phonemes": "l a m m a | r i g ʕ a t | ʔ i s s a j e d e | ʕ a l a | d i m a ʃ ʔ" - }, - { - "voice_time_seconds": { - "start": 34.2, - "end": 38.0 - }, - "arabic": "صار الشّاعر يبعتلها أشعار بالسّرّ.", - "hebrew_transliteration": "צַאר אִ-שַּׁאעֶר יִבְּעַת-לְהַא אַשעַאר בִּ-סִّר.", - "hebrew": "המשורר התחיל לשלוח לה שירים בסתר.", - "expected_phonemes": "sˤ aː r | ʔ i ʃ aː ʕ e r | j i b ʕ a t l h a | ʔ a ʃ ʕ aː r | b i s s i r" - }, - { - "voice_time_seconds": { - "start": 38.0, - "end": 41.4 - }, - "arabic": "بواحدة من اللّيالي عمل الشّاعر عملة", - "hebrew_transliteration": "בְּוַאחדֶה מִן אִ-לַّיַאלִי עִמֶל אִ-שַּׁאעֶר עַמלֶה", - "hebrew": "באחד הלילות המשורר עשה מעשה", - "expected_phonemes": "b w aː ħ d e | m i n | ʔ i l l a j aː l i | ʕ i m e l | ʔ i ʃ aː ʕ e r | ʕ a m l e" - }, - { - "voice_time_seconds": { - "start": 41.4, - "end": 45.0 - }, - "arabic": "وبوجه الصّبح فات بالخفية على القصر.", - "hebrew_transliteration": "וּבִּוִגְ'הּ אִ-צֻּבֻּח פַאת בִּל-חֻ'פְיֶה עַלַא אִל-קַצֶר.", - "hebrew": "ועם שחר נכנס בחשאי לארמון.", - "expected_phonemes": "u b i w i dʒ h | ʔ i sˤ u b u ħ | f aː t | b i l x u f j e | ʕ a l a | i l ʔ a sˤ e r" - }, - { - "voice_time_seconds": { - "start": 45.0, - "end": 46.8 - }, - "arabic": "هرب من الحرّاس", - "hebrew_transliteration": "הַרַבּ מִן אִל-חֻרַّאס", - "hebrew": "חמק מהשומרים", - "expected_phonemes": "h a r a b | m i n | i l ħ u r r aː s" - }, - { - "voice_time_seconds": { - "start": 46.8, - "end": 49.6 - }, - "arabic": "ولاقى طريقه لغرفة روضة.", - "hebrew_transliteration": "וּלַאקַא טַרִיקוֹ לַעֻ'רְפֶת רַוְדַ'ה.", - "hebrew": "ומצא את דרכו לחדרה של רווידה.", - "expected_phonemes": "u l aː ʔ a | tˤ a r iː ʔ o | l a ɣ u r f e t | r a w dˤ a" - }, - { - "voice_time_seconds": { - "start": 49.6, - "end": 52.8 - }, - "arabic": "سوا قضّوا لتنين أوقات حلوة،", - "hebrew_transliteration": "סַוַא קַדַّוּ לֶ-תְנֵין אַוְקַאת חִלוֶה,", - "hebrew": "השניים בילו יחד זמנים יפים,", - "expected_phonemes": "s a w a | ʔ a d d a w | l e t n eː n | ʔ a w ʔ aː t | ħ i l w e" - }, - { - "voice_time_seconds": { - "start": 52.8, - "end": 55.6 - }, - "arabic": "وما كان فيه حدود لفرحتهم.", - "hebrew_transliteration": "וּמַא כַּאן פִיה חֻדוּד לַפַרחְתְהֹם.", - "hebrew": "ולא היה גבול לשמחתם.", - "expected_phonemes": "u m a | k aː n | f iː h | ħ u d uː d | l a f a r ħ t h o m" - }, - { - "voice_time_seconds": { - "start": 55.6, - "end": 59.4 - }, - "arabic": "بسّ فجأة سمعوا صوت دقّ على الباب:", - "hebrew_transliteration": "בַּס פַגְ'אַה סִמעוּ צוֹת דַקّ עַלַא אִל-בַּאבּ:", - "hebrew": "אבל פתאום שמעו קול דפיקה על הדלת:", - "expected_phonemes": "b a s | f a dʒ ʔ a | s i m ʕ u | sˤ oː t | d a ʔ ʔ | ʕ a l a | i l b aː b" - }, - { - "voice_time_seconds": { - "start": 59.4, - "end": 65.0 - }, - "arabic": "«إفتحي يا حلوتي! هدا أنا زوجك الخليفة».", - "hebrew_transliteration": "אִפתַחִי יַא חִלוִתִי! הַאדַא אַנַא זוֹגֶכּ אִל-חַ'לִיפֶה.", - "hebrew": "פתחי, יפתי! זה אני, בעלך הח'ליף.", - "expected_phonemes": "ʔ i f t a ħ i | j a | ħ i l w i t i | h aː d a | ʔ a n a | z oː g e k | i l x a l iː f e" - }, - { - "voice_time_seconds": { - "start": 65.0, - "end": 66.8 - }, - "arabic": "ردّت عليه روضة:", - "hebrew_transliteration": "רַדַّת עַלֵיה רַוְדַ'ה:", - "hebrew": "רווידה ענתה לו:", - "expected_phonemes": "r a d d a t | ʕ a l eː h | r a w dˤ a" - }, - { - "voice_time_seconds": { - "start": 66.8, - "end": 69.2 - }, - "arabic": "«لحظة صغيرة بسّ ألبس قميص النّوم", - "hebrew_transliteration": "לַחזַה זְעִ'ירֶה בַּס אַלבַּס קַמִיץ אִ-נּוֹם", - "hebrew": "רגע קטן, רק אלבש כתונת לילה", - "expected_phonemes": "l a ħ ðˤ a | z ɣ iː r e | b a s | i l b a s | ʔ a m iː sˤ | ʔ i n oː m" - }, - { - "voice_time_seconds": { - "start": 69.2, - "end": 71.0 - }, - "arabic": "وبفتحلك الباب.»", - "hebrew_transliteration": "וּבַּפתַח-לַכּ אִל-בַּאבּ.", - "hebrew": "ואפתח לך את הדלת.", - "expected_phonemes": "u b a f t a ħ l a k | i l b aː b" - }, - { - "voice_time_seconds": { - "start": 71.0, - "end": 73.6 - }, - "arabic": "وفي إيدها أشّرت للشاعر يفوت", - "hebrew_transliteration": "וּפִי אִידְהַא אַשַّרַת לַ-שַּׁאעֶר יְפוּת", - "hebrew": "ובידה סימנה למשורר להיכנס", - "expected_phonemes": "u f i | ʔ iː d h a | ʔ a ʃ ʃ a r a t | l a ʃ aː ʕ e r | j f uː t" - }, - { - "voice_time_seconds": { - "start": 73.6, - "end": 75.6 - }, - "arabic": "على صندوق الأواعي الكبير", - "hebrew_transliteration": "עַלַא צַנדוּק אִל-אַוַאעִי אִל-כְּבִּיר", - "hebrew": "לתוך ארגז הבגדים הגדול", - "expected_phonemes": "ʕ a l a | sˤ a n d uː ʔ | i l ʔ a w aː ʕ i | i l k b iː r" - }, - { - "voice_time_seconds": { - "start": 75.6, - "end": 77.8 - }, - "arabic": "إلّي كان في الغرفة.", - "hebrew_transliteration": "אִלִّי כַּאן פִי אִל-עֻ'רְפֶה.", - "hebrew": "שהיה בחדר.", - "expected_phonemes": "ʔ i l l i | k aː n | f i | i l ɣ u r f e" - }, - { - "voice_time_seconds": { - "start": 77.8, - "end": 79.6 - }, - "arabic": "فات الخليفة على الغرفة،", - "hebrew_transliteration": "פַאת אִל-חַ'לִיפֶה עַלַא אִל-עֻ'רְפֶה,", - "hebrew": "הח'ליף נכנס לחדר,", - "expected_phonemes": "f aː t | i l x a l iː f e | ʕ a l a | i l ɣ u r f e" - }, - { - "voice_time_seconds": { - "start": 79.6, - "end": 82.0 - }, - "arabic": "وعينه وقعت على الصّندوق", - "hebrew_transliteration": "וּעֵינוֹ וִקעַת עַלַא אִ-צַّנדוּק", - "hebrew": "ועינו נפלה על הארגז", - "expected_phonemes": "u ʕ eː n o | w i ʔ ʕ a t | ʕ a l a | ʔ i sˤ sˤ a n d uː ʔ" - }, - { - "voice_time_seconds": { - "start": 82.0, - "end": 84.4 - }, - "arabic": "والشّكّ دخل على قلبه.", - "hebrew_transliteration": "וִ-שַّכّ דַחַ'ל עַלַא קַלבּוֹ.", - "hebrew": "והחשד נכנס לליבו.", - "expected_phonemes": "w i ʃ ʃ a k k | d a x a l | ʕ a l a | ʔ a l b o" - }, - { - "voice_time_seconds": { - "start": 84.4, - "end": 87.0 - }, - "arabic": "قالّها: «روضة!»", - "hebrew_transliteration": "קַאל-לְהַא: רַוְדַ'ה!", - "hebrew": "הוא אמר לה: רווידה!", - "expected_phonemes": "ʔ aː l l h a | r a w dˤ a" - }, - { - "voice_time_seconds": { - "start": 87.0, - "end": 89.8 - }, - "arabic": "«نعم زوجي؟» ردّت عليه.", - "hebrew_transliteration": "נַעַם זוֹגִ'י? רַדַّת עַלֵיה.", - "hebrew": "כן, בעלי? ענתה לו.", - "expected_phonemes": "n a ʕ a m | z oː dʒ i | r a d d a t | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 89.8, - "end": 94.0 - }, - "arabic": "«روضة يا بؤبؤ عيني، بدّي تعطيني هديّة!»", - "hebrew_transliteration": "רַוְדַ'ה יַא בֻּאבֻּא עֵינִי, בַּדִّי תַעטִינִי הַדִיֶّה!", - "hebrew": "רווידה, בבת עיני, אני רוצה שתתני לי מתנה!", - "expected_phonemes": "r a w dˤ a | j a | b aː b a | ʕ eː n i | b a d d i | t a ʕ tˤ iː n i | h a d i j j e" - }, - { - "voice_time_seconds": { - "start": 94.0, - "end": 98.4 - }, - "arabic": "جاوبته: «كلّ إشي إلي - لإلك.»", - "hebrew_transliteration": "גַ'אוַבַּתוֹ: כֻּלّ אִשִי אִלִי - לַאִלַכּ.", - "hebrew": "היא ענתה לו: כל מה שלי - שלך.", - "expected_phonemes": "dʒ aː w a b a t o | k u l l | ʔ i ʃ i | ʔ i l i | l a ʔ i l a k" - }, - { - "voice_time_seconds": { - "start": 98.4, - "end": 99.8 - }, - "arabic": "زقّف الخليفة", - "hebrew_transliteration": "זַקַّף אִל-חַ'לִיפֶה", - "hebrew": "הח'ליף מחא כף", - "expected_phonemes": "z a ʔ ʔ a f | i l x a l iː f e" - }, - { - "voice_time_seconds": { - "start": 99.8, - "end": 103.0 - }, - "arabic": "ودغري فاتوا على الغرفة تلاتة من خدمه.", - "hebrew_transliteration": "וּדֻעְ'רִי פַאתוּ עַלַא אִל-עֻ'רְפֶה תַלַאתֶה מִן חַ'דַמוֹ.", - "hebrew": "ומיד נכנסו לחדר שלושה ממשרתיו.", - "expected_phonemes": "u d u ɣ r i | f aː t u | ʕ a l a | i l ɣ u r f e | t a l aː t e | m i n | x a d a m o" - }, - { - "voice_time_seconds": { - "start": 103.0, - "end": 107.0 - }, - "arabic": "قالّهم: «أحفروا حفرة في أرض الغرفة!»", - "hebrew_transliteration": "קַאל-לְהֹם: אִחפְרוּ חֻפְרַה פִי אַרְד' אִל-עֻ'רְפֶה!", - "hebrew": "הוא אמר להם: חפרו בור ברצפת החדר!", - "expected_phonemes": "ʔ aː l l h o m | ʔ i ħ f r u | ħ u f r a | f i | ʔ a r dˤ | i l ɣ u r f e" - }, - { - "voice_time_seconds": { - "start": 107.0, - "end": 108.4 - }, - "arabic": "حفروا.", - "hebrew_transliteration": "חַפַרוּ.", - "hebrew": "הם חפרו.", - "expected_phonemes": "ħ a f a r u" - }, - { - "voice_time_seconds": { - "start": 108.4, - "end": 111.0 - }, - "arabic": "قالّهم: «خدوا هذا الصّندوق", - "hebrew_transliteration": "קַאל-לְהֹם: חֻ'דוּ הַאדַא אִ-צַּנדוּק", - "hebrew": "הוא אמר להם: קחו את הארגז הזה", - "expected_phonemes": "ʔ aː l l h o m | x u d u | h aː d a | ʔ i sˤ a n d uː ʔ" - }, - { - "voice_time_seconds": { - "start": 111.0, - "end": 113.2 - }, - "arabic": "وادفنوه في الحفرة!»", - "hebrew_transliteration": "וּאִדפְנוּה פִי אִל-חֻפְרַה!", - "hebrew": "וקברו אותו בבור!", - "expected_phonemes": "u ʔ i d f n uː h | f i | i l ħ u f r a" - }, - { - "voice_time_seconds": { - "start": 113.2, - "end": 114.6 - }, - "arabic": "دفنوا.", - "hebrew_transliteration": "דַפַנוּ.", - "hebrew": "הם קברו.", - "expected_phonemes": "d a f a n u" - }, - { - "voice_time_seconds": { - "start": 114.6, - "end": 117.8 - }, - "arabic": "قالّهم: «رجّعوا البلاط على محلّه", - "hebrew_transliteration": "קַאל-לְהֹם: רַגַّעוּ אִל-בְּלַאט עַלַא מַחַלّוֹ", - "hebrew": "הוא אמר להם: החזירו את המרצפות למקומן", - "expected_phonemes": "ʔ aː l l h o m | r a g g a ʕ u | i l b l aː tˤ | ʕ a l a | m a ħ a l l o" - }, - { - "voice_time_seconds": { - "start": 117.8, - "end": 119.8 - }, - "arabic": "وغطّوه في السّجّادة.»", - "hebrew_transliteration": "וּעַ'טּוּה פִי אִ-סֻّגַّאדֶה.", - "hebrew": "וכסו אותו בשטיח.", - "expected_phonemes": "u ɣ a tˤ uː h | f i | ʔ i s s u g g aː d e" - }, - { - "voice_time_seconds": { - "start": 119.8, - "end": 124.2 - }, - "arabic": "الخدم نفذوا أوامره، وكلّ هدا الوقت", - "hebrew_transliteration": "אִל-חַ'דַם נַפַّדוּ אַוַאמְרוֹ, וּכֻּלّ הַאדַא אִל-וַקת", - "hebrew": "המשרתים ביצעו את פקודותיו, וכל הזמן הזה", - "expected_phonemes": "i l x a d a m | n a f f a d u | ʔ a w aː m r o | u k u l l | h aː d a | i l w a ʔ t" - }, - { - "voice_time_seconds": { - "start": 124.2, - "end": 128.4 - }, - "arabic": "الخليفة ما نزّل عينه من روضة.", - "hebrew_transliteration": "אִל-חַ'לִיפֶה מַא נַזַّל עֵינוֹ מִן רַוְדַ'ה.", - "hebrew": "הח'ליף לא הוריד את עיניו מרווידה.", - "expected_phonemes": "i l x a l iː f e | m a | n a z z a l | ʕ eː n o | m i n | r a w dˤ a" - }, - { - "voice_time_seconds": { - "start": 128.4, - "end": 131.2 - }, - "arabic": "لمّا خلّص كلّ إشي قالّها:", - "hebrew_transliteration": "לַמַّא חַ'לַّץ כֻּלّ אִשִי קַאל-לְהַא:", - "hebrew": "כשהכול נגמר הוא אמר לה:", - "expected_phonemes": "l a m m a | x a l l a sˤ | k u l l | ʔ i ʃ i | ʔ aː l l h a" - }, - { - "voice_time_seconds": { - "start": 131.2, - "end": 134.4 - }, - "arabic": "«سمعت إشي وما بعرف إذا مضبوط أو لأ.", - "hebrew_transliteration": "סִמִעֶת אִשִי וּמַא בַּערַף אִזַא מַזבּוּט אַו לַא.", - "hebrew": "שמעתי משהו ואני לא יודע אם זה נכון או לא.", - "expected_phonemes": "s i m i ʕ e t | ʔ i ʃ i | u m a | b a ʕ r a f | ʔ i z a | m a z b uː tˤ | ʔ a w | l aː" - }, - { - "voice_time_seconds": { - "start": 134.4, - "end": 137.0 - }, - "arabic": "إذا مضبوط بكون عمبدفن هلّق", - "hebrew_transliteration": "אִזַא מַזבּוּט בַּכּוּן עַם-בַּדפֻן הַלַّק", - "hebrew": "אם זה נכון, אני קובר עכשיו", - "expected_phonemes": "ʔ i z a | m a z b uː tˤ | b a k uː n | ʕ a m b a d f u n | h a l l a ʔ" - }, - { - "voice_time_seconds": { - "start": 137.0, - "end": 140.8 - }, - "arabic": "مرّة واحدة وللأبد إلّي في قلبي،", - "hebrew_transliteration": "מַרַّה וַאחדֶה וּלִל-אַבַּד אִלִّי פִי קַלבִּי,", - "hebrew": "אחת ולתמיד את מה שבליבי,", - "expected_phonemes": "m a r r a | w aː ħ d e | u l i l ʔ a b a d | ʔ i l l i | f i | ʔ a l b i" - }, - { - "voice_time_seconds": { - "start": 140.8, - "end": 149.8 - }, - "arabic": "وإذا مش مضبوط بكون بسّ دفنت صندوق...»", - "hebrew_transliteration": "וּאִזַא מִש מַזבּוּט בַּכּוּן בַּס דַפַנֶת צַנדוּק...", - "hebrew": "ואם זה לא נכון, רק קברתי ארגז...", - "expected_phonemes": "u ʔ i z a | m i ʃ | m a z b uː tˤ | b a k uː n | b a s | d a f a n e t | sˤ a n d uː ʔ" - } - ] - }, - { - "file_name": "הנער היתום והזקן החכם * אִס-סָבִּי אִל-יַתִים ול-חֶ'תיָאר אִז-זַכִּי * الصبي اليتيم والختيار الذكي [ToA75d6hWbI].webm", - "youtube_id": "ToA75d6hWbI", - "title_hebrew": "הנער היתום והזקן החכם", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "الصّبي اليتيم والختيار الذكي", - "hebrew_transliteration": "אִ-צַّבִּי אִל-יַתִים וִל-חְ'תִיַאר אִ-דַّכִּי", - "hebrew": "הנער היתום והזקן החכם", - "expected_phonemes": "ʔ i sˤ sˤ a b i | i l j a t iː m | w i l x t i j aː r | ʔ i d d a k i" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 9.8 - }, - "arabic": "صبي يتيم قاعد في زاوية الشّارع،", - "hebrew_transliteration": "צַבִּי יַתִים קַאעֶד פִי זַאוְיֶת אִ-שַּׁארֶע,", - "hebrew": "נער יתום יושב בפינת הרחוב,", - "expected_phonemes": "sˤ a b i | j a t iː m | ʔ aː ʕ e d | f i | z aː w j e t | ʔ i ʃ aː r e ʕ" - }, - { - "voice_time_seconds": { - "start": 9.8, - "end": 12.6 - }, - "arabic": "ببكي على نصيبه وبشحد.", - "hebrew_transliteration": "בִּבכִּי עַלַא נַצִיבּוֹ וּבִּשחַד.", - "hebrew": "בוכה על גורלו ומקבץ נדבות.", - "expected_phonemes": "b i b k i | ʕ a l a | n a sˤ iː b o | u b i ʃ ħ a d" - }, - { - "voice_time_seconds": { - "start": 12.6, - "end": 16.2 - }, - "arabic": "وإلّا بمرق ختيار ذكي وبسأله:", - "hebrew_transliteration": "וּאִלַّא בִּמרֹק חְ'תִיַאר דַכִּי וּבִּסאַלוֹ:", - "hebrew": "והנה עובר זקן חכם ושואל אותו:", - "expected_phonemes": "u ʔ i l l a | b i m r o ʔ | x t i j aː r | d a k i | u b i s ʔ a l o" - }, - { - "voice_time_seconds": { - "start": 16.2, - "end": 18.2 - }, - "arabic": "«ليش عم تبكي يا صبي؟»", - "hebrew_transliteration": "לֵיש עַם-תִבכִּי יַא צַבִּי?", - "hebrew": "למה אתה בוכה, נער?", - "expected_phonemes": "l eː ʃ | ʕ a m t i b k i | j a | sˤ a b i" - }, - { - "voice_time_seconds": { - "start": 18.2, - "end": 20.0 - }, - "arabic": "جاوبه الصّبي:", - "hebrew_transliteration": "גַ'אוַבּוֹ אִ-צַّבִּי:", - "hebrew": "הנער ענה לו:", - "expected_phonemes": "dʒ aː w a b o | ʔ i sˤ sˤ a b i" - }, - { - "voice_time_seconds": { - "start": 20.0, - "end": 21.6 - }, - "arabic": "«أنا يتيم،", - "hebrew_transliteration": "אַנַא יַתִים,", - "hebrew": "אני יתום,", - "expected_phonemes": "ʔ a n a | j a t iː m" - }, - { - "voice_time_seconds": { - "start": 21.6, - "end": 23.2 - }, - "arabic": "ما عنديش أبو،", - "hebrew_transliteration": "מַא עִנדִיש אַבּוּ,", - "hebrew": "אין לי אבא,", - "expected_phonemes": "m a | ʕ i n d iː ʃ | ʔ a b u" - }, - { - "voice_time_seconds": { - "start": 23.2, - "end": 24.8 - }, - "arabic": "ما عنديش إم،", - "hebrew_transliteration": "מַא עִנדִיש אִם,", - "hebrew": "אין לי אמא,", - "expected_phonemes": "m a | ʕ i n d iː ʃ | ʔ i m" - }, - { - "voice_time_seconds": { - "start": 24.8, - "end": 27.4 - }, - "arabic": "فشّ إلي إشي في العالم،", - "hebrew_transliteration": "פִשّ אִלִי אִשִי פִי אִל-עַאלַם,", - "hebrew": "אין לי כלום בעולם,", - "expected_phonemes": "f i ʃ ʃ | ʔ i l i | ʔ i ʃ i | f i | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 27.4, - "end": 31.2 - }, - "arabic": "ولا حتّى شقفة خبزة آكلها.»", - "hebrew_transliteration": "וְלַא חַתַّא שֻקפֶת חֻ'בְּזֶה אַאכֻּלְהַא.", - "hebrew": "אפילו לא חתיכת לחם לאכול.", - "expected_phonemes": "u l aː | ħ a t t a | ʃ u ʔ f e t | x u b z e | ʔ aː k u l h a" - }, - { - "voice_time_seconds": { - "start": 31.2, - "end": 34.8 - }, - "arabic": "إتطلّع الختيار، جعّد صباحه وقال:", - "hebrew_transliteration": "אִתטַלַّע אִל-חְ'תִיַאר, גַ'עַّד צְבַּאחוֹ וּקַאל:", - "hebrew": "הזקן הביט, קימט את מצחו ואמר:", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | i l x t i j aː r | dʒ a ʕ ʕ a d | sˤ b aː ħ o | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 34.8, - "end": 37.4 - }, - "arabic": "«عنجد فشّ عندك إشي؟»", - "hebrew_transliteration": "עַנגַ'ד פִשّ עִנדַכּ אִשִי?", - "hebrew": "באמת אין לך כלום?", - "expected_phonemes": "ʕ a n dʒ a d | f i ʃ ʃ | ʕ i n d a k | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 37.4, - "end": 39.0 - }, - "arabic": "«ولا إشي من مرّة،»", - "hebrew_transliteration": "וְלַא אִשִי מִן מַרַّה,", - "hebrew": "שום דבר בכלל,", - "expected_phonemes": "u l aː | ʔ i ʃ i | m i n | m a r r a" - }, - { - "voice_time_seconds": { - "start": 39.0, - "end": 42.0 - }, - "arabic": "جاوب الصّبي بصوت حزين.", - "hebrew_transliteration": "גַ'אוַבּ אִ-צַّבִּי בְּצוֹת חַזִין.", - "hebrew": "ענה הנער בקול עצוב.", - "expected_phonemes": "dʒ aː w a b | ʔ i sˤ sˤ a b i | b sˤ oː t | ħ a z iː n" - }, - { - "voice_time_seconds": { - "start": 42.0, - "end": 43.6 - }, - "arabic": "قالّه الختيار:", - "hebrew_transliteration": "קַאל-לוֹ אִל-חְ'תִיַאר:", - "hebrew": "הזקן אמר לו:", - "expected_phonemes": "ʔ aː l l o | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 43.6, - "end": 47.4 - }, - "arabic": "«إذا هيك، معناه بدّي أشتري إيديك.", - "hebrew_transliteration": "אִזַא הֵיכּ, מַעְנַאה בַּדִّי אַשתְרִי אִידֵיכּ.", - "hebrew": "אם כך, אני רוצה לקנות את הידיים שלך.", - "expected_phonemes": "ʔ i z a | h eː k | m a ʕ n aː h | b a d d i | ʔ a ʃ t r i | ʔ iː d eː k" - }, - { - "voice_time_seconds": { - "start": 47.4, - "end": 50.2 - }, - "arabic": "شكلهم صحيّات وقوايا،", - "hebrew_transliteration": "שַכְּלְהֹם צַחִّיַّאת וּקַוַאיַא,", - "hebrew": "הן נראות בריאות וחזקות,", - "expected_phonemes": "ʃ a k l h o m | sˤ a ħ ħ i j j aː t | u ʔ a w aː j a" - }, - { - "voice_time_seconds": { - "start": 50.2, - "end": 54.4 - }, - "arabic": "مستعدّ أدفعلك فيهم مية دينار.»", - "hebrew_transliteration": "מִסתַעֶד אַדפַע-לַכּ פִיהֹם מִית דִינַאר.", - "hebrew": "אני מוכן לשלם לך עליהן מאה דינר.", - "expected_phonemes": "m i s t a ʕ e d | ʔ a d f a ʕ l a k | f iː h o m | m iː t | d iː n aː r" - }, - { - "voice_time_seconds": { - "start": 54.4, - "end": 56.6 - }, - "arabic": "«ولا حتّى مقابل ألف دينار", - "hebrew_transliteration": "וְלַא חַתַّא מֻקַאבֶּל אַלְף דִינַאר", - "hebrew": "אפילו לא תמורת אלף דינר", - "expected_phonemes": "u l aː | ħ a t t a | m u ʔ aː b e l | ʔ a l f | d iː n aː r" - }, - { - "voice_time_seconds": { - "start": 56.6, - "end": 59.0 - }, - "arabic": "مش مستعدّ أبيع إيديّ،»", - "hebrew_transliteration": "מִש מִסתַעֶד אַבִּיע אִידַיّ,", - "hebrew": "אני לא מוכן למכור את הידיים שלי,", - "expected_phonemes": "m i ʃ | m i s t a ʕ e d | ʔ a b iː ʕ | ʔ iː d a j j" - }, - { - "voice_time_seconds": { - "start": 59.0, - "end": 61.8 - }, - "arabic": "جاوبه الصّبي دغري.", - "hebrew_transliteration": "גַ'אוַבּוֹ אִ-צַّבִּי דֻעְ'רִי.", - "hebrew": "ענה לו הנער מיד.", - "expected_phonemes": "dʒ aː w a b o | ʔ i sˤ sˤ a b i | d u ɣ r i" - }, - { - "voice_time_seconds": { - "start": 61.8, - "end": 67.2 - }, - "arabic": "«طيّب، إذا هيك بيعني عينيك التّنتين.", - "hebrew_transliteration": "טַיֶּבּ, אִזַא הֵיכּ בִּיעְנִי עֵינֵיכּ אִ-תִּנתֵין.", - "hebrew": "טוב, אם כך מכור לי את שתי עיניך.", - "expected_phonemes": "tˤ a j e b | ʔ i z a | h eː k | b iː ʕ n i | ʕ eː n eː k | ʔ i t i n t eː n" - }, - { - "voice_time_seconds": { - "start": 67.2, - "end": 70.6 - }, - "arabic": "عينين الصّغار في السّنّ بيشوفوا منيح،", - "hebrew_transliteration": "עֵינֵין לֶ-זְעַ'אר פִי אִ-סִّן בִּישוּפוּ מְנִיח,", - "hebrew": "עיניהם של צעירים בגיל רואות היטב,", - "expected_phonemes": "ʕ eː n eː n | l e z ɣ aː r | f i | ʔ i s s i n | b iː ʃ uː f u | m n iː ħ" - }, - { - "voice_time_seconds": { - "start": 70.6, - "end": 74.8 - }, - "arabic": "وبكل سرور بدفعلك فيهم مية دينار.»", - "hebrew_transliteration": "וּבִּכֻּלّ סֻרוּר בַּדפַע-לַכּ פִיהֹם מִית דִינַאר.", - "hebrew": "ובשמחה אשלם לך עליהן מאה דינר.", - "expected_phonemes": "u b i k u l l | s u r uː r | b a d f a ʕ l a k | f iː h o m | m iː t | d iː n aː r" - }, - { - "voice_time_seconds": { - "start": 74.8, - "end": 79.2 - }, - "arabic": "«ولا حتّى مقابل كلّ ذهب العالم ببيعك عينيّ!»", - "hebrew_transliteration": "וְלַא חַתַّא מֻקַאבֶּל כֻּלّ דַ'הַבּ אִל-עַאלַם בַּבִּיעַכּ עֵינַיّ!", - "hebrew": "אפילו לא תמורת כל הזהב שבעולם אמכור לך את עיניי!", - "expected_phonemes": "u l aː | ħ a t t a | m u ʔ aː b e l | k u l l | dˤ a h a b | i l ʕ aː l a m | b a b iː ʕ a k | ʕ eː n a j j" - }, - { - "voice_time_seconds": { - "start": 79.2, - "end": 83.0 - }, - "arabic": "جاوبه كمان مرّة بصوت قوي.", - "hebrew_transliteration": "גַ'אוַבּוֹ כַּמַאן מַרַّה בְּצוֹת קַוִי.", - "hebrew": "ענה לו שוב בקול חזק.", - "expected_phonemes": "dʒ aː w a b o | k a m aː n | m a r r a | b sˤ oː t | ʔ a w i" - }, - { - "voice_time_seconds": { - "start": 83.0, - "end": 86.0 - }, - "arabic": "قالّه الختيار: «هيّاه،", - "hebrew_transliteration": "קַאל-לוֹ אִל-חְ'תִיַאר: הִיַّאה,", - "hebrew": "הזקן אמר לו: הנה,", - "expected_phonemes": "ʔ aː l l o | i l x t i j aː r | h i j j aː h" - }, - { - "voice_time_seconds": { - "start": 86.0, - "end": 89.6 - }, - "arabic": "هدول إشيين عندك إيّاهم في هادا العالم:", - "hebrew_transliteration": "הַדוֹל אִשְיֵין עִנדַכּ אִיַּאהֹם פִי הַאדַא אִל-עַאלַם:", - "hebrew": "אלה שני דברים שיש לך בעולם הזה:", - "expected_phonemes": "h a d oː l | ʔ i ʃ j eː n | ʕ i n d a k | ʔ i j aː h o m | f i | h aː d a | i l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 89.6, - "end": 92.6 - }, - "arabic": "قوّة ذراعاتك وضو عينيك.", - "hebrew_transliteration": "קֻוֶّת דִ'רַאעַאתַכּ וּדַוּ עֵינֵיכּ.", - "hebrew": "כוח זרועותיך ואור עיניך.", - "expected_phonemes": "ʔ u w w e t | dˤ i r aː ʕ aː t a k | u d a w | ʕ eː n eː k" - }, - { - "voice_time_seconds": { - "start": 92.6, - "end": 97.6 - }, - "arabic": "استعملهم ومش رح تحتاج تشحد صدقات.»", - "hebrew_transliteration": "אִסתַעמִלְהֹם וּמִש רַח-תִחתַאג' תִשחַד צַדַקַאת.", - "hebrew": "השתמש בהם ולא תצטרך לקבץ נדבות.", - "expected_phonemes": "ʔ i s t a ʕ m i l h o m | u m i ʃ | r a ħ t i ħ t aː dʒ | t i ʃ ħ a d | sˤ a d a ʔ aː t" - } - ] - }, - { - "file_name": "התאים שַן לטבקה * וַאפַק שָנֻן טָבָּקָה * وافق شن طبقة * סיפור עממי בערבית מדוברת [u59nxkwLJ9k].mkv", - "youtube_id": "u59nxkwLJ9k", - "title_hebrew": "התאים שן לטבקה", - "segments": [ - { - "voice_time_seconds": { - "start": 1.8, - "end": 5.8 - }, - "arabic": "وافق شنّ طبقة", - "hebrew_transliteration": "וַאפַק שַןّ טַבַּקַה", - "hebrew": "התאים שן לטבקה", - "expected_phonemes": "w aː f a ʔ | ʃ a n n | tˤ a b a ʔ a" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 8.8 - }, - "arabic": "كان ياما كان في قديم الزّمان،", - "hebrew_transliteration": "כַּאן יַאמַא כַּאן פִי קַדִים אִ-זַّמַאן,", - "hebrew": "היה היה בימים עברו,", - "expected_phonemes": "k aː n | j aː m a | k aː n | f i | ʔ a d iː m | ʔ i z z a m aː n" - }, - { - "voice_time_seconds": { - "start": 8.8, - "end": 11.6 - }, - "arabic": "زلمة ختيار ماشي في الطّريق.", - "hebrew_transliteration": "זַלַמֶה חְ'תִיַאר מַאשִי פִי אִ-טַّרִיק.", - "hebrew": "איש זקן הלך בדרך.", - "expected_phonemes": "z a l a m e | x t i j aː r | m aː ʃ i | f i | i tˤ tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 11.6, - "end": 15.2 - }, - "arabic": "وهوّ في طريقه لاقى شبّ اسمه شنّ.", - "hebrew_transliteration": "וּהֻוֶّ פִי טַרִיקוֹ לַאקַא שַבּ אִסמוֹ שַןّ.", - "hebrew": "ובדרכו פגש בחור ששמו שן.", - "expected_phonemes": "u h u w w e | f i | tˤ a r iː ʔ o | l aː ʔ a | ʃ a b | ʔ i s m o | ʃ a n n" - }, - { - "voice_time_seconds": { - "start": 15.2, - "end": 17.8 - }, - "arabic": "التّنين صاروا يمشوا سوا.", - "hebrew_transliteration": "אִ-תְּנֵין צַארוּ יִמשוּ סַוַא.", - "hebrew": "השניים התחילו ללכת יחד.", - "expected_phonemes": "ʔ i t n eː n | sˤ aː r u | j i m ʃ u | s a w a" - }, - { - "voice_time_seconds": { - "start": 17.8, - "end": 20.8 - }, - "arabic": "مرّة واحدة وإلّا شنّ بسأل الختيار:", - "hebrew_transliteration": "מַרַّה וַאחדֶה וְאִלַّא שַןّ בִּסאַל אִל-חְ'תִיַאר:", - "hebrew": "פתאום שן שואל את הזקן:", - "expected_phonemes": "m a r r a | w aː ħ d e | u ʔ i l l a | ʃ a n n | b i s ʔ a l | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 20.8, - "end": 23.6 - }, - "arabic": "«بدّك تحملني أو أحملك أنا؟»", - "hebrew_transliteration": "בַּדַّכּ תִחמִלנִי אַו אַחמִלַכּ אַנַא?", - "hebrew": "אתה רוצה לשאת אותי או שאני אשא אותך?", - "expected_phonemes": "b a d d a k | t i ħ m i l n i | ʔ a w | ʔ a ħ m i l a k | ʔ a n a" - }, - { - "voice_time_seconds": { - "start": 23.6, - "end": 26.4 - }, - "arabic": "الختيار تعجّب كتير من هاد السّؤال،", - "hebrew_transliteration": "אִל-חְ'תִיַאר אִתעַגַّבּ כְּתִיר מִן הַאד אִ-סֻّאַאל,", - "hebrew": "הזקן השתומם מאוד מהשאלה הזאת,", - "expected_phonemes": "i l x t i j aː r | ʔ i t ʕ a g g a b | k t iː r | m i n | h aː d | i s s u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 26.4, - "end": 28.6 - }, - "arabic": "وما عرفش شو يجاوب،", - "hebrew_transliteration": "וּמַא עִרְפֶש שוּ יְגַ'אוֶבּ,", - "hebrew": "ולא ידע מה לענות,", - "expected_phonemes": "u m a | ʕ i r f e ʃ | ʃ u | j dʒ aː w e b" - }, - { - "voice_time_seconds": { - "start": 28.6, - "end": 31.6 - }, - "arabic": "فتجاهل السّؤال وكمّل طريقه.", - "hebrew_transliteration": "פַ-תְגַ'אהַל אִ-סֻّאַאל וּכַּמַّל טַרִיקוֹ.", - "hebrew": "אז התעלם מהשאלה והמשיך בדרכו.", - "expected_phonemes": "f a t dʒ aː h a l | i s s u ʔ aː l | u k a m m a l | tˤ a r iː ʔ o" - }, - { - "voice_time_seconds": { - "start": 31.6, - "end": 34.6 - }, - "arabic": "وهمّ ماشيين مرقوا من جنب حقل،", - "hebrew_transliteration": "וּהֻמّ מַאשְיִין מַרַקוּ מִן גַ'נבּ חַקֶל,", - "hebrew": "ובזמן שהלכו עברו ליד שדה,", - "expected_phonemes": "u h u m m | m aː ʃ j iː n | m a r a ʔ u | m i n | dʒ a n b | ħ a ʔ e l" - }, - { - "voice_time_seconds": { - "start": 34.6, - "end": 37.2 - }, - "arabic": "هون شنّ سأل الختيار:", - "hebrew_transliteration": "הוֹן שַןّ סַאַל אִל-חְ'תִיַאר:", - "hebrew": "כאן שן שאל את הזקן:", - "expected_phonemes": "h oː n | ʃ a n n | s a ʔ a l | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 37.2, - "end": 40.6 - }, - "arabic": "«فكرك هذا الزّرع إنّاكل ولّا لا؟»", - "hebrew_transliteration": "פִכְרַכּ הַאדַא אִ-זַּרֶע אִנּתַאכַּל וַלַّא לַא?", - "hebrew": "מה דעתך, היבול הזה נאכל או לא?", - "expected_phonemes": "f i k r a k | h aː d a | ʔ i z a r e ʕ | ʔ i n t aː k a l | w a l l a | l aː" - }, - { - "voice_time_seconds": { - "start": 40.6, - "end": 43.4 - }, - "arabic": "كمان هاي المرّة، الختيار تعجّب", - "hebrew_transliteration": "כַּמַאן הַאי אִל-מַרַّה אִל-חְ'תִיַאר תְעַגַّבּ", - "hebrew": "גם הפעם הזקן השתומם", - "expected_phonemes": "k a m aː n | h aː i | i l m a r r a | i l x t i j aː r | t ʕ a g g a b" - }, - { - "voice_time_seconds": { - "start": 43.4, - "end": 45.6 - }, - "arabic": "وما جاوب وكمّل مشي.", - "hebrew_transliteration": "וּמַא גַ'אוַבּ וּכַּמַّל מַשִי.", - "hebrew": "ולא ענה והמשיך ללכת.", - "expected_phonemes": "u m a | dʒ aː w a b | u k a m m a l | m a ʃ i" - }, - { - "voice_time_seconds": { - "start": 45.6, - "end": 49.0 - }, - "arabic": "بعدها بشوي مرقوا التّنين بجنب جنازة،", - "hebrew_transliteration": "בַּעֶדהַא בִּשוַי מַרַקוּ אִ-תְּנֵין בִּגַ'נבּ גַ'נַאזֶה,", - "hebrew": "קצת אחר כך עברו השניים ליד הלוויה,", - "expected_phonemes": "b a ʕ e d h a | b i ʃ w a j | m a r a ʔ u | ʔ i t n eː n | b i dʒ a n b | dʒ a n aː z e" - }, - { - "voice_time_seconds": { - "start": 49.0, - "end": 51.2 - }, - "arabic": "وإلّا الختيار بسمع شنّ", - "hebrew_transliteration": "וְאִלַّא אִל-חְ'תִיַאר בִּסמַע שַןّ", - "hebrew": "והנה הזקן שומע את שן", - "expected_phonemes": "u ʔ i l l a | i l x t i j aː r | b i s m a ʕ | ʃ a n n" - }, - { - "voice_time_seconds": { - "start": 51.2, - "end": 53.2 - }, - "arabic": "بسأل واحد من الرّجال", - "hebrew_transliteration": "בִּסאַל וַאחַד מִן אִ-רְّגַ'אל", - "hebrew": "שואל את אחד הגברים", - "expected_phonemes": "b i s ʔ a l | w aː ħ a d | m i n | ʔ i r r dʒ aː l" - }, - { - "voice_time_seconds": { - "start": 53.2, - "end": 56.6 - }, - "arabic": "إذا صاحب النعش ميّت أو عايش.", - "hebrew_transliteration": "אִזַא צַאחֶבּ אִ-נַّעֶש מַיֶּת אַו עַאיֶש.", - "hebrew": "אם בעל הארון מת או חי.", - "expected_phonemes": "ʔ i z a | sˤ aː ħ e b | ʔ i n n a ʕ e ʃ | m a j e t | ʔ a w | ʕ aː j e ʃ" - }, - { - "voice_time_seconds": { - "start": 56.6, - "end": 59.0 - }, - "arabic": "هون إنصدم كتير الختيار", - "hebrew_transliteration": "הוֹן אִנצַדַם כְּתִיר אִל-חְ'תִיַאר", - "hebrew": "כאן הזקן הזדעזע מאוד", - "expected_phonemes": "h oː n | ʔ i n sˤ a d a m | k t iː r | i l x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 59.0, - "end": 62.4 - }, - "arabic": "وحتّى إنّه فكّر شنّ واحد أهبل،", - "hebrew_transliteration": "וּחַתַّא אִנּוֹ פַכַّר שַןّ וַאחַד אַהבַּל,", - "hebrew": "ואפילו חשב ששן טיפש,", - "expected_phonemes": "u ħ a t t a | ʔ i n o | f a k k a r | ʃ a n n | w aː ħ a d | ʔ a h b a l" - }, - { - "voice_time_seconds": { - "start": 62.4, - "end": 65.8 - }, - "arabic": "بسّ برضه ضلّ ساكت وما جاوب.", - "hebrew_transliteration": "בַּס בִּרְדוֹ דַ'לّ סַאכֶּת וּמַא גַ'אוַבּ.", - "hebrew": "אבל בכל זאת נשאר שותק ולא ענה.", - "expected_phonemes": "b a s | b i r d o | dˤ a l l | s aː k e t | u m a | dʒ aː w a b" - }, - { - "voice_time_seconds": { - "start": 65.8, - "end": 69.4 - }, - "arabic": "بعد شوي وصلوا التّنين للبلد إلّي قاصدينها.", - "hebrew_transliteration": "בַּעֶד שְוַי וְצְלוּ אִ-תְּנֵין לַל-בַּלַד אִלִّי קַאצְדִינְהַא.", - "hebrew": "קצת אחר כך השניים הגיעו לעיר שאליה פנו.", - "expected_phonemes": "b a ʕ e d | ʃ w a j | w i sˤ l u | ʔ i t n eː n | l a l b a l a d | ʔ i l l i | ʔ aː sˤ d iː n h a" - }, - { - "voice_time_seconds": { - "start": 69.4, - "end": 74.4 - }, - "arabic": "الختيار عزم شنّ يبات عنده في البيت وشنّ وافق.", - "hebrew_transliteration": "אִל-חְ'תִיַאר עַזַם שַןّ יִבַּאת עִנדוֹ פִי אִל-בֵּית וּשַןّ וַאפַק.", - "hebrew": "הזקן הזמין את שן ללון אצלו בבית ושן הסכים.", - "expected_phonemes": "i l x t i j aː r | ʕ a z a m | ʃ a n n | j i b aː t | ʕ i n d o | f i | i l b eː t | u ʃ a n n | w aː f a ʔ" - }, - { - "voice_time_seconds": { - "start": 74.4, - "end": 77.4 - }, - "arabic": "فات الختيار على بيته وحكى لبنته", - "hebrew_transliteration": "פַאת אִל-חְ'תִיַאר עַלַא בֵּיתוֹ וּחַכַא לַבִּנתוֹ", - "hebrew": "הזקן נכנס לביתו וסיפר לבתו", - "expected_phonemes": "f aː t | i l x t i j aː r | ʕ a l a | b eː t o | u ħ a k a | l a b i n t o" - }, - { - "voice_time_seconds": { - "start": 77.4, - "end": 79.4 - }, - "arabic": "إلّي كان أسمها طبقة،", - "hebrew_transliteration": "אִלִّי כַּאן אִסמְהַא טַבַּקַה,", - "hebrew": "ששמה היה טבקה,", - "expected_phonemes": "ʔ i l l i | k aː n | ʔ i s m h a | tˤ a b a ʔ a" - }, - { - "voice_time_seconds": { - "start": 79.4, - "end": 82.4 - }, - "arabic": "إنّه جاب معاه ضيف غريب عجيب", - "hebrew_transliteration": "אִנּוֹ גַ'אבּ מַעַאה דֵ'יף עַ'רִיבּ עַגִ'יבּ", - "hebrew": "שהביא איתו אורח מוזר ומשונה", - "expected_phonemes": "ʔ i n o | dʒ aː b | m a ʕ aː h | dˤ eː f | ɣ a r iː b | ʕ a dʒ iː b" - }, - { - "voice_time_seconds": { - "start": 82.4, - "end": 85.8 - }, - "arabic": "بسأل أسئلة غبيّة وحتّى جاهلة.", - "hebrew_transliteration": "בִּסאַל אַסאִלֶה עַ'בִּיֶّה וּחַתַّא גַ'אהְלֶה.", - "hebrew": "ששואל שאלות טיפשיות ואפילו בורות.", - "expected_phonemes": "b i s ʔ a l | ʔ a s ʔ i l e | ɣ a b i j j e | u ħ a t t a | dʒ aː h l e" - }, - { - "voice_time_seconds": { - "start": 85.8, - "end": 88.6 - }, - "arabic": "بنت الختيار صار عندها فضول", - "hebrew_transliteration": "בִּנְת אִל-חְ'תִיַאר צַאר עִנדְהַא פֻדוּל", - "hebrew": "אצל בת הזקן התעוררה סקרנות", - "expected_phonemes": "b i n t | i l x t i j aː r | sˤ aː r | ʕ i n d h a | f u d uː l" - }, - { - "voice_time_seconds": { - "start": 88.6, - "end": 91.0 - }, - "arabic": "تعرف شو الأسئلة هاي،", - "hebrew_transliteration": "תִערַף שוּ אִל-אַסאִלֶה הַאי,", - "hebrew": "לדעת מה הן השאלות האלה,", - "expected_phonemes": "t i ʕ r a f | ʃ u | i l ʔ a s ʔ i l e | h aː i" - }, - { - "voice_time_seconds": { - "start": 91.0, - "end": 93.2 - }, - "arabic": "وطلبت من أبوها يحكيلها.", - "hebrew_transliteration": "וּטַלְבַּת מִן אַבּוּהַא יִחכִּי-לְהַא.", - "hebrew": "וביקשה מאביה שיספר לה.", - "expected_phonemes": "u tˤ a l b a t | m i n | ʔ a b uː h a | j i ħ k iː l h a" - }, - { - "voice_time_seconds": { - "start": 93.2, - "end": 96.4 - }, - "arabic": "الختيار قال لبنته كلّ أسئلة شنّ،", - "hebrew_transliteration": "אִל-חְ'תִיַאר קַאל לַבִּנתוֹ כֻּלّ אַסאִלֶת שַןّ,", - "hebrew": "הזקן סיפר לבתו את כל השאלות של שן,", - "expected_phonemes": "i l x t i j aː r | ʔ aː l | l a b i n t o | k u l l | ʔ a s ʔ i l e t | ʃ a n n" - }, - { - "voice_time_seconds": { - "start": 96.4, - "end": 99.4 - }, - "arabic": "بسّ البنت ما استغربتش بالمرّة", - "hebrew_transliteration": "בַּס אִל-בִּנְת מַא סתַעְ'רַבַּתֶש בִּל-מַרַّה", - "hebrew": "אבל הבת לא התפלאה בכלל", - "expected_phonemes": "b a s | i l b i n t | m a | s t a ɣ r a b a t e ʃ | b i l m a r r a" - }, - { - "voice_time_seconds": { - "start": 99.4, - "end": 103.0 - }, - "arabic": "وقالت لأبوها إنّه من ورا كلّ هاي الأسئلة", - "hebrew_transliteration": "וּקַאלַת לַאַבּוּהַא אִנּוֹ מִן וַרַא כֻּלّ הַאי אִל-אַסאִלֶה", - "hebrew": "ואמרה לאביה שמאחורי כל השאלות האלה", - "expected_phonemes": "u ʔ aː l a t | l a ʔ a b uː h a | ʔ i n o | m i n | w a r a | k u l l | h aː i | i l ʔ a s ʔ i l e" - }, - { - "voice_time_seconds": { - "start": 103.0, - "end": 104.8 - }, - "arabic": "فيه مغزى ومعنى،", - "hebrew_transliteration": "פִיה מַעְ'זַא וּמַעְנַא,", - "hebrew": "יש מוסר השכל ומשמעות,", - "expected_phonemes": "f iː h | m a ɣ z a | u m a ʕ n a" - }, - { - "voice_time_seconds": { - "start": 104.8, - "end": 107.6 - }, - "arabic": "وإنّها بالمرّة ما بتدلّ على جهل.", - "hebrew_transliteration": "וּאִנְّהַא בִּל-מַרַّה מַא בִּתדֻלّ עַלַא גַ'הֶל.", - "hebrew": "ושהן בכלל לא מעידות על בורות.", - "expected_phonemes": "u ʔ i n n h a | b i l m a r r a | m a | b i t d u l l | ʕ a l a | dʒ a h e l" - }, - { - "voice_time_seconds": { - "start": 107.6, - "end": 110.0 - }, - "arabic": "الختيار سأل: «كيف يعني؟»", - "hebrew_transliteration": "אִל-חְ'תִיַאר סַאַל: כִּיף יַעְנִי?", - "hebrew": "הזקן שאל: מה זאת אומרת?", - "expected_phonemes": "i l x t i j aː r | s a ʔ a l | k iː f | j a ʕ n i" - }, - { - "voice_time_seconds": { - "start": 110.0, - "end": 112.4 - }, - "arabic": "البنت قالتله: «بسيطة،", - "hebrew_transliteration": "אִל-בִּנְת קַאלַת-לוֹ: בַּסִיטַה,", - "hebrew": "הבת אמרה לו: פשוט,", - "expected_phonemes": "i l b i n t | ʔ aː l a t l o | b a s iː tˤ a" - }, - { - "voice_time_seconds": { - "start": 112.4, - "end": 115.4 - }, - "arabic": "تحملني أو أحملك معناها بدّك تحكي معي", - "hebrew_transliteration": "תִחמִלנִי אַו אַחמִלַכּ מַעְנַאהַא בַּדַّכּ תִחכִּי מַעִי", - "hebrew": "תישא אותי או אשא אותך פירושו: אתה רוצה לדבר איתי", - "expected_phonemes": "t i ħ m i l n i | ʔ a w | ʔ a ħ m i l a k | m a ʕ n aː h a | b a d d a k | t i ħ k i | m a ʕ i" - }, - { - "voice_time_seconds": { - "start": 115.4, - "end": 116.8 - }, - "arabic": "أو أنا إلّي أحكي معك", - "hebrew_transliteration": "אַו אַנַא אִלִّי אַחכִּי מַעַכּ", - "hebrew": "או שאני אדבר איתך", - "expected_phonemes": "ʔ a w | ʔ a n a | ʔ i l l i | ʔ a ħ k i | m a ʕ a k" - }, - { - "voice_time_seconds": { - "start": 116.8, - "end": 119.2 - }, - "arabic": "عشان ننسى تعب المشي الطّويل.", - "hebrew_transliteration": "עַשַאן נִנסַא תַעַבּ אִל-מַשִי אִ-טַّוִיל.", - "hebrew": "כדי שנשכח את עייפות ההליכה הארוכה.", - "expected_phonemes": "ʕ a ʃ aː n | n i n s a | t a ʕ a b | i l m a ʃ i | ʔ i tˤ tˤ a w iː l" - }, - { - "voice_time_seconds": { - "start": 119.2, - "end": 122.8 - }, - "arabic": "وبالنّسبة لسؤاله عن الزّرع إذا إنّاكل أو لا،", - "hebrew_transliteration": "וּבִּ-נִّסבֶּה לַסֻאַאלוֹ עַן אִ-זַּרֶע אִזַא אִנּתַאכַּל אַו לַא,", - "hebrew": "ולגבי שאלתו על היבול אם נאכל או לא,", - "expected_phonemes": "u b i n n i s b e | l a s u ʔ aː l o | ʕ a n | ʔ i z a r e ʕ | ʔ i z a | ʔ i n t aː k a l | ʔ a w | l aː" - }, - { - "voice_time_seconds": { - "start": 122.8, - "end": 125.8 - }, - "arabic": "المقصد إذا صاحب الزّرع باع الزّرع", - "hebrew_transliteration": "אִל-מַקצַד אִזַא צַאחֶבּ אִ-זַּרֶע בַּאע אִ-זַּרֶע", - "hebrew": "הכוונה היא אם בעל היבול מכר את היבול", - "expected_phonemes": "i l m a ʔ sˤ a d | ʔ i z a | sˤ aː ħ e b | ʔ i z a r e ʕ | b aː ʕ | ʔ i z a r e ʕ" - }, - { - "voice_time_seconds": { - "start": 125.8, - "end": 128.8 - }, - "arabic": "وجاب أكل من سعره أو لا.", - "hebrew_transliteration": "וּגַ'אבּ אַכֶּל מִן סִעְרוֹ אַו לַא.", - "hebrew": "והביא אוכל מתמורתו או לא.", - "expected_phonemes": "u dʒ aː b | ʔ a k e l | m i n | s i ʕ r o | ʔ a w | l aː" - }, - { - "voice_time_seconds": { - "start": 128.8, - "end": 132.4 - }, - "arabic": "أمّا بالنّسبة للسّؤال الثّالث وقت الجنازة", - "hebrew_transliteration": "אַמַّא בִּ-נִّסבֶּה לַ-סֻּאַאל אִ-תַּאלֶת וַקת אִל-גַ'נַאזֶה", - "hebrew": "ואילו לגבי השאלה השלישית בזמן ההלוויה", - "expected_phonemes": "ʔ a m m a | b i n n i s b e | l a s u ʔ aː l | ʔ i t aː l e t | w a ʔ t | i l dʒ a n aː z e" - }, - { - "voice_time_seconds": { - "start": 132.4, - "end": 136.8 - }, - "arabic": "فالمقصد إنّه الميّت بكون عايش لو خلّف وراه ولاد.", - "hebrew_transliteration": "פַ-לְ-מַקצַד אִנּוֹ אִל-מַיֶּת בִּכּוּן עַאיֶש לַו חַ'לַّף וַרַאה וְלַאד.", - "hebrew": "הכוונה היא שהמת נשאר חי אם השאיר אחריו ילדים.", - "expected_phonemes": "f a l m a ʔ sˤ a d | ʔ i n o | i l m a j e t | b i k uː n | ʕ aː j e ʃ | l a w | x a l l a f | w a r aː h | u l aː d" - }, - { - "voice_time_seconds": { - "start": 136.8, - "end": 139.6 - }, - "arabic": "مش بيقولوا إلّي خلّف ما مات؟!»", - "hebrew_transliteration": "מִש בִּיקוּלוּ אִלִّי חַ'לַّף מַא מַאת?!", - "hebrew": "הרי אומרים: מי שהוליד לא מת?!", - "expected_phonemes": "m i ʃ | b iː ʔ uː l u | ʔ i l l i | x a l l a f | m a | m aː t" - }, - { - "voice_time_seconds": { - "start": 139.6, - "end": 142.2 - }, - "arabic": "الختيار اتعجّب من ذكاء بنته", - "hebrew_transliteration": "אִל-חְ'תִיַאר אִתעַגַّבּ מִן דַ'כַּאא בִּנתוֹ", - "hebrew": "הזקן השתומם מחוכמת בתו", - "expected_phonemes": "i l x t i j aː r | ʔ i t ʕ a g g a b | m i n | dˤ a k aː a | b i n t o" - }, - { - "voice_time_seconds": { - "start": 142.2, - "end": 144.6 - }, - "arabic": "وفرح كتير إنّه فهم أسئلة شنّ،", - "hebrew_transliteration": "וּפִרֶח כְּתִיר אִנּוֹ פִהֶם אַסאִלֶת שַןّ,", - "hebrew": "ושמח מאוד שהבין את שאלותיו של שן,", - "expected_phonemes": "u f i r e ħ | k t iː r | ʔ i n o | f i h e m | ʔ a s ʔ i l e t | ʃ a n n" - }, - { - "voice_time_seconds": { - "start": 144.6, - "end": 146.4 - }, - "arabic": "وطلع لعنده وقال-له:", - "hebrew_transliteration": "וּטִלֶע לַעִנדוֹ וּקַאל-לוֹ:", - "hebrew": "ויצא אליו ואמר לו:", - "expected_phonemes": "u tˤ i l e ʕ | l a ʕ i n d o | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 146.4, - "end": 149.2 - }, - "arabic": "«أنا هلّق فهمت شو معنى أسئلتك.»", - "hebrew_transliteration": "אַנַא הַלַّק פִהִמֶת שוּ מַעְנַא אַסאִלתַכּ.", - "hebrew": "עכשיו הבנתי מה פירוש שאלותיך.", - "expected_phonemes": "ʔ a n a | h a l l a ʔ | f i h i m e t | ʃ u | m a ʕ n a | ʔ a s ʔ i l t a k" - }, - { - "voice_time_seconds": { - "start": 149.2, - "end": 151.2 - }, - "arabic": "شنّ إستغرب وقال:", - "hebrew_transliteration": "שַןّ אִסתַעְ'רַבּ וּקַאל:", - "hebrew": "שן התפלא ואמר:", - "expected_phonemes": "ʃ a n n | ʔ i s t a ɣ r a b | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 151.2, - "end": 153.8 - }, - "arabic": "«معقول؟ شو فهمت؟»", - "hebrew_transliteration": "מַעקוּל? שוּ פִהִמֶת?", - "hebrew": "באמת? מה הבנת?", - "expected_phonemes": "m a ʕ ʔ uː l | ʃ u | f i h i m e t" - }, - { - "voice_time_seconds": { - "start": 153.8, - "end": 156.8 - }, - "arabic": "الختيار حكاله كلّ إلّي بنته قالته.", - "hebrew_transliteration": "אִל-חְ'תִיַאר חַכַא-לוֹ כֻּלّ אִלִّי בִּנתוֹ קַאלַתוֹ.", - "hebrew": "הזקן סיפר לו את כל מה שבתו אמרה.", - "expected_phonemes": "i l x t i j aː r | ħ a k aː l o | k u l l | ʔ i l l i | b i n t o | ʔ aː l a t o" - }, - { - "voice_time_seconds": { - "start": 156.8, - "end": 160.0 - }, - "arabic": "شنّ انبسط إنه الختيار فهم عليه وسأله:", - "hebrew_transliteration": "שַןّ אִנבַּסַט אִנּוֹ אִל-חְ'תִיַאר פִהֶם עַלֵיה וּסַאַלוֹ:", - "hebrew": "שן שמח שהזקן הבין אותו ושאל אותו:", - "expected_phonemes": "ʃ a n n | ʔ i n b a s a tˤ | ʔ i n o | i l x t i j aː r | f i h e m | ʕ a l eː h | u s a ʔ a l o" - }, - { - "voice_time_seconds": { - "start": 160.0, - "end": 162.0 - }, - "arabic": "«شو إلّي خلّاك تفهم قصدي؟»", - "hebrew_transliteration": "שוּ אִלִّי חַ'לַّאכּ תִפהַם קַצדִי?", - "hebrew": "מה גרם לך להבין את כוונתי?", - "expected_phonemes": "ʃ u | ʔ i l l i | x a l l aː k | t i f h a m | ʔ a sˤ d i" - }, - { - "voice_time_seconds": { - "start": 162.0, - "end": 163.2 - }, - "arabic": "الختيار قال:", - "hebrew_transliteration": "אִל-חְ'תִיַאר קַאל:", - "hebrew": "הזקן אמר:", - "expected_phonemes": "i l x t i j aː r | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 163.2, - "end": 167.0 - }, - "arabic": "«هاي بنتي الذّكية طبقة، هيّ إلّي فهمت عليك.»", - "hebrew_transliteration": "הַאי בִּנתִי אִ-דַّכִּיֶּה טַבַּקַה, הִיֶّ אִלִّי פִהמַת עַלֵיכּ.", - "hebrew": "זו בתי החכמה טבקה, היא שהבינה אותך.", - "expected_phonemes": "h aː i | b i n t i | ʔ i d d a k i j e | tˤ a b a ʔ a | h i j j e | ʔ i l l i | f i h m a t | ʕ a l eː k" - }, - { - "voice_time_seconds": { - "start": 167.0, - "end": 171.4 - }, - "arabic": "شنّ شعر إنّه وأخيرًا لاقى الواحدة إلّي تفهم عليه،", - "hebrew_transliteration": "שַןّ שִעֶר אִנּוֹ וּאַחִ'ירַן לַאקַא אִל-וַאחדֶה אִלִّי תִפהַם עַלֵיה,", - "hebrew": "שן הרגיש שסוף סוף מצא את זו שמבינה אותו,", - "expected_phonemes": "ʃ a n n | ʃ i ʕ e r | ʔ i n o | u ʔ a x iː r a n | l aː ʔ a | i l w aː ħ d e | ʔ i l l i | t i f h a m | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 171.4, - "end": 173.6 - }, - "arabic": "وطلب إيد طبقة من أبوها.", - "hebrew_transliteration": "וּטַלַבּ אִיד טַבַּקַה מִן אַבּוּהַא.", - "hebrew": "וביקש את ידה של טבקה מאביה.", - "expected_phonemes": "u tˤ a l a b | ʔ iː d | tˤ a b a ʔ a | m i n | ʔ a b uː h a" - }, - { - "voice_time_seconds": { - "start": 173.6, - "end": 179.0 - }, - "arabic": "الختيار وطبقة وافقوا، وهيك شنّ وطبقة اتزوّجوا.", - "hebrew_transliteration": "אִל-חְ'תִיַאר וּטַבַּקַה וַאפַקוּ, וּהֵיכּ שַןّ וּטַבַּקַה אִתגַ'וַّזוּ.", - "hebrew": "הזקן וטבקה הסכימו, וכך שן וטבקה התחתנו.", - "expected_phonemes": "i l x t i j aː r | u tˤ a b a ʔ a | w aː f a ʔ u | u h eː k | ʃ a n n | u tˤ a b a ʔ a | ʔ i t dʒ a w w a z u" - }, - { - "voice_time_seconds": { - "start": 179.0, - "end": 180.6 - }, - "arabic": "وصار المتل يقول:", - "hebrew_transliteration": "וּצַאר אִל-מַתַל יְקוּל:", - "hebrew": "והפתגם התחיל לומר:", - "expected_phonemes": "u sˤ aː r | i l m a t a l | j ʔ uː l" - }, - { - "voice_time_seconds": { - "start": 180.6, - "end": 184.0 - }, - "arabic": "وافق شنّ طبقة.", - "hebrew_transliteration": "וַאפַק שַןّ טַבַּקַה.", - "hebrew": "התאים שן לטבקה.", - "expected_phonemes": "w aː f a ʔ | ʃ a n n | tˤ a b a ʔ a" - } - ] - }, - { - "file_name": "העכבר והדלעת * אִל-פָאר וִל-קָרעָה * الفار والقرعة * סיפור בערבית מדוברת [HttMrNDwhjE].webm", - "youtube_id": "HttMrNDwhjE", - "title_hebrew": "העכבר והדלעת", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "الفار والقرعة", - "hebrew_transliteration": "אִל-פַאר וִל-קַרעַה", - "hebrew": "העכבר והדלעת", - "expected_phonemes": "i l f aː r | w i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 8.4 - }, - "arabic": "في واحدة من سنوات القحط", - "hebrew_transliteration": "פִי וַאחדֶה מִן סַנַוַאת אִל-קַחְט", - "hebrew": "באחת משנות הבצורת", - "expected_phonemes": "f i | w aː ħ d e | m i n | s a n a w aː t | i l ʔ a ħ tˤ" - }, - { - "voice_time_seconds": { - "start": 8.4, - "end": 10.4 - }, - "arabic": "إلّي كانت في البلاد،", - "hebrew_transliteration": "אִלִّי כַּאנַת פִי לֶ-בְּלַאד,", - "hebrew": "שהיו בארץ,", - "expected_phonemes": "ʔ i l l i | k aː n a t | f i | l e b l aː d" - }, - { - "voice_time_seconds": { - "start": 10.4, - "end": 13.4 - }, - "arabic": "كان فيه نقص كبير في الغذاء.", - "hebrew_transliteration": "כַּאן פִיה נַקֶץ כְּבִּיר פִי אִל-עִ'דַ'אא.", - "hebrew": "היה מחסור גדול במזון.", - "expected_phonemes": "k aː n | f iː h | n a ʔ e sˤ | k b iː r | f i | i l ɣ i dˤ aː a" - }, - { - "voice_time_seconds": { - "start": 13.4, - "end": 18.0 - }, - "arabic": "الخضرا ما كانتش تنمو ولا القمح والشّعير.", - "hebrew_transliteration": "אִל-חֻ'דְרַא מַא כַּאנַתֶש תִנמוּ וְלַא אִל-קַמֶח וִל-שַעִיר.", - "hebrew": "הירקות לא צמחו, ולא החיטה והשעורה.", - "expected_phonemes": "i l x u d r a | m a | k aː n a t e ʃ | t i n m u | u l aː | i l ʔ a m e ħ | w i l ʃ a ʕ iː r" - }, - { - "voice_time_seconds": { - "start": 18.0, - "end": 22.6 - }, - "arabic": "الإسطبلات فضيت ومطاحن القمح أضربت.", - "hebrew_transliteration": "אִל-אִסטַבְלַאת פִדְיַת וּמַטַאחֶן אִל-קַמֶח אַדְ'רַבַּת.", - "hebrew": "האורוות התרוקנו וטחנות החיטה שבתו.", - "expected_phonemes": "i l ʔ i s tˤ a b l aː t | f i d j a t | u m a tˤ aː ħ e n | i l ʔ a m e ħ | ʔ a dˤ r a b a t" - }, - { - "voice_time_seconds": { - "start": 22.6, - "end": 25.6 - }, - "arabic": "في واحد من الحقول كان فيه فار قاعد", - "hebrew_transliteration": "פִי וַאחַד מִן אִל-חֻקוּל כַּאן פִיה פַאר קַאעֶד", - "hebrew": "באחד השדות ישב עכבר", - "expected_phonemes": "f i | w aː ħ a d | m i n | i l ħ u ʔ uː l | k aː n | f iː h | f aː r | ʔ aː ʕ e d" - }, - { - "voice_time_seconds": { - "start": 25.6, - "end": 28.8 - }, - "arabic": "بلفلف وبدوّر على حبّة خضرا يوكلها،", - "hebrew_transliteration": "בִּילַפלֶף וּבִּידַוֶّר עַלַא חַבֶּת חֻ'דְרַא יוֹכֻּלְהַא,", - "hebrew": "מסתובב ומחפש ירק לאכול,", - "expected_phonemes": "b iː l a f l e f | u b iː d a w w e r | ʕ a l a | ħ a b e t | x u d r a | j oː k u l h a" - }, - { - "voice_time_seconds": { - "start": 28.8, - "end": 31.2 - }, - "arabic": "يكسر فيها جوعه.", - "hebrew_transliteration": "יִכסֶר פִיהַא גוּעוֹ.", - "hebrew": "שישבור בו את רעבונו.", - "expected_phonemes": "j i k s e r | f iː h a | g uː ʕ o" - }, - { - "voice_time_seconds": { - "start": 31.2, - "end": 33.8 - }, - "arabic": "مشي هالفار بطول الحقل وعرضه", - "hebrew_transliteration": "מִשִי הַל-פַאר בְּטוּל אִל-חַקֶל וּעַרְדוֹ", - "hebrew": "העכבר הזה הלך לאורך השדה ולרוחבו", - "expected_phonemes": "m i ʃ i | h a l f aː r | b tˤ uː l | i l ħ a ʔ e l | u ʕ a r d o" - }, - { - "voice_time_seconds": { - "start": 33.8, - "end": 36.0 - }, - "arabic": "وما لاقاش إشي.", - "hebrew_transliteration": "וּמַא לַאקַאש אִשִי.", - "hebrew": "ולא מצא כלום.", - "expected_phonemes": "u m a | l aː ʔ aː ʃ | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 36.0, - "end": 39.0 - }, - "arabic": "فجأة شاف سدّ صغير.", - "hebrew_transliteration": "פַגְ'אַה שַאף סַדّ זְעִ'יר.", - "hebrew": "פתאום ראה סכר קטן.", - "expected_phonemes": "f a dʒ ʔ a | ʃ aː f | s a d d | z ɣ iː r" - }, - { - "voice_time_seconds": { - "start": 39.0, - "end": 42.4 - }, - "arabic": "ركض الفار لهناك ولاقى شويّة ميّ", - "hebrew_transliteration": "רַכַד' אִל-פַאר לַהֻנַאכּ וּלַאקַא שְוַיֶּת מַיّ", - "hebrew": "העכבר רץ לשם ומצא קצת מים", - "expected_phonemes": "r a k a dˤ | i l f aː r | l a h u n aː k | u l aː ʔ a | ʃ w a j e t | m a j j" - }, - { - "voice_time_seconds": { - "start": 42.4, - "end": 44.6 - }, - "arabic": "وحدّها نتفة خضرا،", - "hebrew_transliteration": "וּחַדַّהַא נִתפֶת חֻ'דְרַא,", - "hebrew": "ולידם מעט ירק,", - "expected_phonemes": "u ħ a d d a h a | n i t f e t | x u d r a" - }, - { - "voice_time_seconds": { - "start": 44.6, - "end": 48.6 - }, - "arabic": "وبقلب الخضرا لاقى قرعة واحدة.", - "hebrew_transliteration": "וּבְּקַלבּ אִל-חֻ'דְרַא לַאקַא קַרעַה וַאחדֶה.", - "hebrew": "ובתוך הירק מצא דלעת אחת.", - "expected_phonemes": "u b ʔ a l b | i l x u d r a | l aː ʔ a | ʔ a r ʕ a | w aː ħ d e" - }, - { - "voice_time_seconds": { - "start": 48.6, - "end": 52.4 - }, - "arabic": "قال لنفسه: «الله بعتلي هاي القرعة إلي،", - "hebrew_transliteration": "קַאל לַנַפסוֹ: אַללַה בַּעַת-לִי הַאי אִל-קַרעַה אִלִי,", - "hebrew": "אמר לעצמו: אלוהים שלח לי את הדלעת הזאת,", - "expected_phonemes": "ʔ aː l | l a n a f s o | i l l a | b a ʕ a t l i | h aː i | i l ʔ a r ʕ a | ʔ i l i" - }, - { - "voice_time_seconds": { - "start": 52.4, - "end": 57.0 - }, - "arabic": "منها أكل ومنها شرب ومنها بيت لإلي.»", - "hebrew_transliteration": "מִנְהַא אַכֶּל וּמִנְהַא שֻרבּ וּמִנְהַא בֵּית לַאִלִי.", - "hebrew": "ממנה אוכל וממנה שתייה וממנה בית בשבילי.", - "expected_phonemes": "m i n h a | ʔ a k e l | u m i n h a | ʃ u r b | u m i n h a | b eː t | l a ʔ i l i" - }, - { - "voice_time_seconds": { - "start": 57.0, - "end": 59.0 - }, - "arabic": "قرّب الفار من القرعة", - "hebrew_transliteration": "קַרַّבּ אִל-פַאר מִן אִל-קַרעַה", - "hebrew": "העכבר התקרב לדלעת", - "expected_phonemes": "ʔ a r r a b | i l f aː r | m i n | i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 59.0, - "end": 62.8 - }, - "arabic": "وبلّش يقرقط فيها وعمل فيها خزق.", - "hebrew_transliteration": "וּבַּלַّש יְקַרקֶט פִיהַא וּעִמֶל פִיהַא חֻ'זֻק.", - "hebrew": "והתחיל לכרסם בה ועשה בה חור.", - "expected_phonemes": "u b a l l a ʃ | j ʔ a r ʔ e tˤ | f iː h a | u ʕ i m e l | f iː h a | x u z u ʔ" - }, - { - "voice_time_seconds": { - "start": 62.8, - "end": 65.4 - }, - "arabic": "كمّل الفار يقرقط ويوسّع في الخزق،", - "hebrew_transliteration": "כַּמַّל אִל-פַאר יְקַרקֶט וִיוַסֶّע פִי אִל-חֻ'זֻק,", - "hebrew": "העכבר המשיך לכרסם ולהרחיב את החור,", - "expected_phonemes": "k a m m a l | i l f aː r | j ʔ a r ʔ e tˤ | w iː w a s s e ʕ | f i | i l x u z u ʔ" - }, - { - "voice_time_seconds": { - "start": 65.4, - "end": 68.6 - }, - "arabic": "عشان يقدر يفوت على قلب القرعة،", - "hebrew_transliteration": "עַשַאן יִקדַר יְפוּת עַלַא קַלבּ אִל-קַרעַה,", - "hebrew": "כדי שיוכל להיכנס לתוך הדלעת,", - "expected_phonemes": "ʕ a ʃ aː n | j i ʔ d a r | j f uː t | ʕ a l a | ʔ a l b | i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 68.6, - "end": 72.0 - }, - "arabic": "يوكل يشرب ويرتاح.", - "hebrew_transliteration": "יוֹכֹּל, יִשרַבּ וִירתַאח.", - "hebrew": "לאכול, לשתות ולנוח.", - "expected_phonemes": "j oː k o l | j i ʃ r a b | w iː r t aː ħ" - }, - { - "voice_time_seconds": { - "start": 72.0, - "end": 74.2 - }, - "arabic": "فات الفار لقلب القرعة", - "hebrew_transliteration": "פַאת אִל-פַאר לַקַלבּ אִל-קַרעַה", - "hebrew": "העכבר נכנס לתוך הדלעת", - "expected_phonemes": "f aː t | i l f aː r | l a ʔ a l b | i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 74.2, - "end": 77.6 - }, - "arabic": "وإنبسط من كلّ هدا الخير إلّي لاقاه،", - "hebrew_transliteration": "וּאִנבַּסַט מִן כֻּלّ הַאדַא אִל-חֵ'יר אִלִّי לַאקַאה,", - "hebrew": "ושמח מכל הטוב הזה שמצא,", - "expected_phonemes": "u ʔ i n b a s a tˤ | m i n | k u l l | h aː d a | i l x eː r | ʔ i l l i | l aː ʔ aː h" - }, - { - "voice_time_seconds": { - "start": 77.6, - "end": 79.8 - }, - "arabic": "أكل منها وشبع.", - "hebrew_transliteration": "אַכַּל מִנְהַא וּשִבֶּע.", - "hebrew": "אכל ממנה ושבע.", - "expected_phonemes": "ʔ a k a l | m i n h a | u ʃ i b e ʕ" - }, - { - "voice_time_seconds": { - "start": 79.8, - "end": 82.8 - }, - "arabic": "مع الوقت نشفت القرعة", - "hebrew_transliteration": "מַע אִל-וַקת נִשפַת אִל-קַרעַה", - "hebrew": "עם הזמן הדלעת התייבשה", - "expected_phonemes": "m a ʕ | i l w a ʔ t | n i ʃ f a t | i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 82.8, - "end": 86.2 - }, - "arabic": "وقشرتها صارت متل لوح الخشب.", - "hebrew_transliteration": "וּקִשרִתְהַא צַארַת מִתֶל לוֹח אִל-חַ'שַבּ.", - "hebrew": "וקליפתה נעשתה כמו לוח עץ.", - "expected_phonemes": "u ʔ i ʃ r i t h a | sˤ aː r a t | m i t e l | l oː ħ | i l x a ʃ a b" - }, - { - "voice_time_seconds": { - "start": 86.2, - "end": 88.2 - }, - "arabic": "الفار إلّي صار ناصح،", - "hebrew_transliteration": "אִל-פַאר אִלִّי צַאר נַאצֶח,", - "hebrew": "העכבר, שהשמין,", - "expected_phonemes": "i l f aː r | ʔ i l l i | sˤ aː r | n aː sˤ e ħ" - }, - { - "voice_time_seconds": { - "start": 88.2, - "end": 90.2 - }, - "arabic": "صار بدّه يطلع من القرعة،", - "hebrew_transliteration": "צַאר בַּדּוֹ יִטלַע מִן אִל-קַרעַה,", - "hebrew": "רצה לצאת מהדלעת,", - "expected_phonemes": "sˤ aː r | b a d o | j i tˤ l a ʕ | m i n | i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 90.2, - "end": 92.8 - }, - "arabic": "بسّ ما قدرش يطلّع حاله منها.", - "hebrew_transliteration": "בַּס מַא קִדְרֶש יְטַלֶּע חַאלוֹ מִנְהַא.", - "hebrew": "אבל לא הצליח לחלץ את עצמו ממנה.", - "expected_phonemes": "b a s | m a | ʔ i d r e ʃ | j tˤ a l e ʕ | ħ aː l o | m i n h a" - }, - { - "voice_time_seconds": { - "start": 92.8, - "end": 95.0 - }, - "arabic": "حاول يوسّع الفتحة،", - "hebrew_transliteration": "חַאוַל יְוַסֶّע אִל-פַתחַה,", - "hebrew": "ניסה להרחיב את הפתח,", - "expected_phonemes": "ħ aː w a l | j w a s s e ʕ | i l f a t ħ a" - }, - { - "voice_time_seconds": { - "start": 95.0, - "end": 98.4 - }, - "arabic": "بسّ سنانه ما قدرتش تقوم في المهمّة.", - "hebrew_transliteration": "בַּס סְנַאנוֹ מַא קִדְרַתֶש תְקוּם פִי אִל-מֻהִמֶّה.", - "hebrew": "אבל שיניו לא הצליחו לעמוד במשימה.", - "expected_phonemes": "b a s | s n aː n o | m a | ʔ i d r a t e ʃ | t ʔ uː m | f i | i l m u h i m m e" - }, - { - "voice_time_seconds": { - "start": 98.4, - "end": 102.8 - }, - "arabic": "قال لنفسه: «هلّق رح ييجي الفلّاح ويقتلني.»", - "hebrew_transliteration": "קַאל לַנַפסוֹ: הַלַّק רַח יִיגִ'י אִל-פַלַّאח וִיקַתֶּלנִי.", - "hebrew": "אמר לעצמו: עכשיו יבוא האיכר ויהרוג אותי.", - "expected_phonemes": "ʔ aː l | l a n a f s o | h a l l a ʔ | r a ħ | j iː dʒ i | i l f a l l aː ħ | w iː ʔ a t e l n i" - }, - { - "voice_time_seconds": { - "start": 102.8, - "end": 104.8 - }, - "arabic": "بقي الفار في قلب القرعة،", - "hebrew_transliteration": "בְּקִי אִל-פַאר פִי קַלבּ אִל-קַרעַה,", - "hebrew": "העכבר נשאר בתוך הדלעת,", - "expected_phonemes": "b ʔ i | i l f aː r | f i | ʔ a l b | i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 104.8, - "end": 106.0 - }, - "arabic": "من دون أكل وشرب،", - "hebrew_transliteration": "מִן דוּן אַכֶּל וּשֻרבּ,", - "hebrew": "בלי אוכל ושתייה,", - "expected_phonemes": "m i n | d uː n | ʔ a k e l | u ʃ u r b" - }, - { - "voice_time_seconds": { - "start": 106.0, - "end": 108.2 - }, - "arabic": "وما عرفش كيف يطلع منها.", - "hebrew_transliteration": "וּמַא עִרְפֶש כִּיף יִטלַע מִנְהַא.", - "hebrew": "ולא ידע איך לצאת ממנה.", - "expected_phonemes": "u m a | ʕ i r f e ʃ | k iː f | j i tˤ l a ʕ | m i n h a" - }, - { - "voice_time_seconds": { - "start": 108.2, - "end": 112.0 - }, - "arabic": "كان عقله مشغول يلاقي حلّ لهاي المشكلة،", - "hebrew_transliteration": "כַּאן עַקְלוֹ מַשעֻ'ול יְלַאקִי חַלّ לַהַאי אִל-מֻשכִּלֶה,", - "hebrew": "שכלו היה עסוק במציאת פתרון לבעיה הזאת,", - "expected_phonemes": "k aː n | ʕ a ʔ l o | m a ʃ ɣ u w l | j l aː ʔ i | ħ a l l | l a h aː i | i l m u ʃ k i l e" - }, - { - "voice_time_seconds": { - "start": 112.0, - "end": 114.8 - }, - "arabic": "والخوف والجوع تعّبوه كتير", - "hebrew_transliteration": "וִל-חוֹף וִל-גוּע תַעַّבּוּה כְּתִיר", - "hebrew": "והפחד והרעב התישו אותו מאוד", - "expected_phonemes": "w i l ħ oː f | w i l g uː ʕ | t a ʕ ʕ a b uː h | k t iː r" - }, - { - "voice_time_seconds": { - "start": 114.8, - "end": 118.4 - }, - "arabic": "لحدّ ما رجع جسمه كيف ما كان بالأوّل،", - "hebrew_transliteration": "לַחַדّ מַא רִגֶ'ע גִ'סמוֹ כִּיף מַא כַּאן בִּל-אַוַّל,", - "hebrew": "עד שגופו חזר להיות כמו שהיה בהתחלה,", - "expected_phonemes": "l a ħ a d d | m a | r i dʒ e ʕ | dʒ i s m o | k iː f | m a | k aː n | b i l ʔ a w w a l" - }, - { - "voice_time_seconds": { - "start": 118.4, - "end": 120.8 - }, - "arabic": "قبل ما يفوت على القرعة.", - "hebrew_transliteration": "קַבֶּל מַא יְפוּת עַלַא אִל-קַרעַה.", - "hebrew": "לפני שנכנס לדלעת.", - "expected_phonemes": "ʔ a b e l | m a | j f uː t | ʕ a l a | i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 120.8, - "end": 124.8 - }, - "arabic": "وهيك نجح يخلّص حاله من قلب القرعة.", - "hebrew_transliteration": "וּהֵיכּ נִגֶ'ח יְחַ'לֶّץ חַאלוֹ מִן קַלבּ אִל-קַרעַה.", - "hebrew": "וכך הצליח לחלץ את עצמו מתוך הדלעת.", - "expected_phonemes": "u h eː k | n i dʒ e ħ | j x a l l e sˤ | ħ aː l o | m i n | ʔ a l b | i l ʔ a r ʕ a" - }, - { - "voice_time_seconds": { - "start": 124.8, - "end": 127.2 - }, - "arabic": "على هدا الإشي قال المثل:", - "hebrew_transliteration": "עַלַא הַאדַא אִל-אִשִי קַאל אִל-מַתַ'ל:", - "hebrew": "על כך אמר הפתגם:", - "expected_phonemes": "ʕ a l a | h aː d a | i l ʔ i ʃ i | ʔ aː l | i l m a t a l" - }, - { - "voice_time_seconds": { - "start": 127.2, - "end": 131.6 - }, - "arabic": "«متل فار القرعة: متل ما فات - طلع».", - "hebrew_transliteration": "מִתֶל פַאר אִל-קַרעַה: מִתֶל מַא פַאת - טִלֶע.", - "hebrew": "כמו עכבר הדלעת: כמו שנכנס - יצא.", - "expected_phonemes": "m i t e l | f aː r | i l ʔ a r ʕ a | m i t e l | m a | f aː t | tˤ i l e ʕ" - } - ] - }, - { - "file_name": "התנצלות גרועה מחטא * עֻד'ר אַקבָּח מִן דַ'נבּ * عذر أقبح من ذنب * סיפור בערבית מדוברת [hFxI_jmgzVk].webm", - "youtube_id": "hFxI_jmgzVk", - "title_hebrew": "התנצלות גרועה מחטא", - "segments": [ - { - "voice_time_seconds": { - "start": 2.0, - "end": 8.2 - }, - "arabic": "عذر أقبح من ذنب", - "hebrew_transliteration": "עֻד'ר אַקבַּח מִן דַ'נבּ", - "hebrew": "התנצלות גרועה מחטא", - "expected_phonemes": "ʕ u dˤ r | ʔ a ʔ b a ħ | m i n | dˤ a n b" - }, - { - "voice_time_seconds": { - "start": 8.2, - "end": 11.4 - }, - "arabic": "بسّ قبل لحظات خلّص هارون الرّشيد", - "hebrew_transliteration": "בַּס קַבֶּל לַחַטַאת חַ'לַّץ הַארוּן אִ-רַّשִיד", - "hebrew": "רק לפני רגעים סיימו הארון א-רשיד", - "expected_phonemes": "b a s | ʔ a b e l | l a ħ a tˤ aː t | x a l l a sˤ | h aː r uː n | ʔ i r r a ʃ iː d" - }, - { - "voice_time_seconds": { - "start": 11.4, - "end": 15.4 - }, - "arabic": "ووزير البلاط أبو نوّاس وجبة الإفطار.", - "hebrew_transliteration": "וּוַזִיר אִל-בְּלַאט אַבּוּ נַוַّאס וַגְ'בֶּת אִל-אִפטַאר.", - "hebrew": "ווזיר החצר אבו נואס את ארוחת הבוקר.", - "expected_phonemes": "u w a z iː r | i l b l aː tˤ | ʔ a b u | n a w w aː s | w a dʒ b e t | i l ʔ i f tˤ aː r" - }, - { - "voice_time_seconds": { - "start": 15.4, - "end": 19.8 - }, - "arabic": "بوقت شرب القهوة، سأل الملك سؤال:", - "hebrew_transliteration": "בְּוַקת שֻרבּ אִל-קַהוֶה, סַאַל אִל-מַלִכּ סֻאַאל:", - "hebrew": "בזמן שתיית הקפה, המלך שאל שאלה:", - "expected_phonemes": "b w a ʔ t | ʃ u r b | i l ʔ a h w e | s a ʔ a l | i l m a l i k | s u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 19.8, - "end": 22.6 - }, - "arabic": "«قولّي يا أبو نوّاس، شو هوّ المثل", - "hebrew_transliteration": "קוּל-לִי יַא אַבּוּ נַוַّאס, שוּ הֻוֶّ אִל-מַתַל", - "hebrew": "תגיד לי, אבו נואס, מהו הפתגם", - "expected_phonemes": "ʔ uː l l i | j a | ʔ a b u | n a w w aː s | ʃ u | h u w w e | i l m a t a l" - }, - { - "voice_time_seconds": { - "start": 22.6, - "end": 26.2 - }, - "arabic": "إلّي بقول إنّه العذر أقبح من الذّنب؟»", - "hebrew_transliteration": "אִלִّי בִּקוּל אִנּוֹ אִל-עֻד'ר אַקבַּח מִן אִ-דַּנבּ?", - "hebrew": "שאומר שהתירוץ גרוע מהחטא?", - "expected_phonemes": "ʔ i l l i | b i ʔ uː l | ʔ i n o | i l ʕ u dˤ r | ʔ a ʔ b a ħ | m i n | ʔ i d a n b" - }, - { - "voice_time_seconds": { - "start": 26.2, - "end": 28.4 - }, - "arabic": "ردّ أبو نوّاس وقال:", - "hebrew_transliteration": "רַדّ אַבּוּ נַוַّאס וּקַאל:", - "hebrew": "אבו נואס השיב ואמר:", - "expected_phonemes": "r a d d | ʔ a b u | n a w w aː s | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 28.4, - "end": 30.4 - }, - "arabic": "«فيه واحد بعمل إشي عاطل", - "hebrew_transliteration": "פִיה וַאחַד בִּעמֶל אִשִי עַאטֶל", - "hebrew": "יש מישהו שעושה משהו רע", - "expected_phonemes": "f iː h | w aː ħ a d | b i ʕ m e l | ʔ i ʃ i | ʕ aː tˤ e l" - }, - { - "voice_time_seconds": { - "start": 30.4, - "end": 33.8 - }, - "arabic": "وبعتذر عن إلّي عمله، بسّ إعتذاره", - "hebrew_transliteration": "וּבִּעתַדֶר עַן אִלִّי עִמלוֹ, בַּס אִעתִדַארוֹ", - "hebrew": "ומתנצל על מה שעשה, אבל ההתנצלות שלו", - "expected_phonemes": "u b i ʕ t a d e r | ʕ a n | ʔ i l l i | ʕ i m l o | b a s | ʔ i ʕ t i d aː r o" - }, - { - "voice_time_seconds": { - "start": 33.8, - "end": 36.6 - }, - "arabic": "أعطل من العمل الغلط إلّي عمله.»", - "hebrew_transliteration": "אַעטַל מִן אִל-עַמַל אִל-עַ'לַט אִלִّי עִמלוֹ.", - "hebrew": "גרועה מהמעשה השגוי שעשה.", - "expected_phonemes": "ʔ a ʕ tˤ a l | m i n | i l ʕ a m a l | i l ɣ a l a tˤ | ʔ i l l i | ʕ i m l o" - }, - { - "voice_time_seconds": { - "start": 36.6, - "end": 39.0 - }, - "arabic": "«كيف يعني؟» تعجّب الملك،", - "hebrew_transliteration": "כִּיף יַעְנִי? תְעַגַّבּ אִל-מַלִכּ,", - "hebrew": "מה זאת אומרת? תמה המלך,", - "expected_phonemes": "k iː f | j a ʕ n i | t ʕ a g g a b | i l m a l i k" - }, - { - "voice_time_seconds": { - "start": 39.0, - "end": 42.4 - }, - "arabic": "«شخص أخطأ وبحاول يتأسّف،", - "hebrew_transliteration": "שַחְ'ץ אַחטַא וּבִּחַאוֶל יִתאַסַّף,", - "hebrew": "אדם חטא ומנסה להצטער,", - "expected_phonemes": "ʃ a x sˤ | ʔ a ħ tˤ a | u b i ħ aː w e l | j i t ʔ a s s a f" - }, - { - "voice_time_seconds": { - "start": 42.4, - "end": 45.0 - }, - "arabic": "يطلب المغفرة على خطأه،", - "hebrew_transliteration": "יִטלֹבּ אִל-מַעְ'פִרַה עַלַא חַטַאוֹ.", - "hebrew": "לבקש מחילה על חטאו.", - "expected_phonemes": "j i tˤ l o b | i l m a ɣ f i r a | ʕ a l a | ħ a tˤ aː o" - }, - { - "voice_time_seconds": { - "start": 45.0, - "end": 46.6 - }, - "arabic": "معقول يكون الأسف،", - "hebrew_transliteration": "מַעקוּל יְכוּן אִל-אַסַף,", - "hebrew": "ייתכן שהצער,", - "expected_phonemes": "m a ʕ ʔ uː l | j k uː n | i l ʔ a s a f" - }, - { - "voice_time_seconds": { - "start": 46.6, - "end": 49.6 - }, - "arabic": "إلّي أجا عشان يكفّر عن الذّنب،", - "hebrew_transliteration": "אִלִّי אַגַ'א עַשַאן יְכַפֶّר עַן אִ-דַّנבּ,", - "hebrew": "שבא כדי לכפר על החטא,", - "expected_phonemes": "ʔ i l l i | ʔ a dʒ a | ʕ a ʃ aː n | j k a f f e r | ʕ a n | ʔ i d d a n b" - }, - { - "voice_time_seconds": { - "start": 49.6, - "end": 52.6 - }, - "arabic": "أعطل من الذّنب نفسه؟»", - "hebrew_transliteration": "אַעטַל מִן אִ-דַّנבּ נַפסוֹ?", - "hebrew": "גרוע מהחטא עצמו?", - "expected_phonemes": "ʔ a ʕ tˤ a l | m i n | ʔ i d d a n b | n a f s o" - }, - { - "voice_time_seconds": { - "start": 52.6, - "end": 54.4 - }, - "arabic": "«هدا هوّ المثل يا سيدي،", - "hebrew_transliteration": "הַאדַא הֻוֶّ אִל-מַתַל יַא סִידִי,", - "hebrew": "זה הפתגם, אדוני,", - "expected_phonemes": "h aː d a | h u w w e | i l m a t a l | j a | s iː d i" - }, - { - "voice_time_seconds": { - "start": 54.4, - "end": 58.4 - }, - "arabic": "ومثل كلّ الأمثلة أكيد إله معنى.»", - "hebrew_transliteration": "וּמִתֶל כֻּלّ אִל-אַמתִלֶה אַכִּיד אִלוֹ מַעְנַא.", - "hebrew": "וכמו כל הפתגמים, בוודאי יש לו משמעות.", - "expected_phonemes": "u m i t e l | k u l l | i l ʔ a m t i l e | ʔ a k iː d | i l o | m a ʕ n a" - }, - { - "voice_time_seconds": { - "start": 58.4, - "end": 60.2 - }, - "arabic": "«أنا عمبسأل عن هدا المعنى،»", - "hebrew_transliteration": "אַנַא עַם-בַּסאַל עַן הַאדַא אִל-מַעְנַא,", - "hebrew": "אני שואל על המשמעות הזאת,", - "expected_phonemes": "ʔ a n a | ʕ a m b a s ʔ a l | ʕ a n | h aː d a | i l m a ʕ n a" - }, - { - "voice_time_seconds": { - "start": 60.2, - "end": 63.0 - }, - "arabic": "كمّل الملك كلامه، «بلكي بتقدر", - "hebrew_transliteration": "כַּמַّל אִל-מַלִכּ כַּלַאמוֹ, בַּלְכִּי בְּתִקדַר", - "hebrew": "המלך המשיך את דבריו, אולי תוכל", - "expected_phonemes": "k a m m a l | i l m a l i k | k a l aː m o | b a l k i | b t i ʔ d a r" - }, - { - "voice_time_seconds": { - "start": 63.0, - "end": 66.6 - }, - "arabic": "تعطيني مثال إلّي بثبت المثل؟»", - "hebrew_transliteration": "תַעטִינִי מִתַאל אִלִّי בִּתבֶּת אִל-מַתַל?", - "hebrew": "לתת לי דוגמה שמוכיחה את הפתגם?", - "expected_phonemes": "t a ʕ tˤ iː n i | m i t aː l | ʔ i l l i | b i t b e t | i l m a t a l" - }, - { - "voice_time_seconds": { - "start": 66.6, - "end": 70.0 - }, - "arabic": "جاوب أبو نوّاس: «هلّق مش متذكّر،", - "hebrew_transliteration": "גַ'אוַבּ אַבּוּ נַוַّאס: הַלַّק מִש מִתזַכַּר,", - "hebrew": "אבו נואס ענה: עכשיו אני לא נזכר,", - "expected_phonemes": "dʒ aː w a b | ʔ a b u | n a w w aː s | h a l l a ʔ | m i ʃ | m i t z a k a r" - }, - { - "voice_time_seconds": { - "start": 70.0, - "end": 73.4 - }, - "arabic": "بسّ خلال النهار أكيد بطلع معي إشي.»", - "hebrew_transliteration": "בַּס חִ'לַאל אִ-נַّהַאר אַכִּיד בִּטלַע מַעִי אִשִי.", - "hebrew": "אבל במהלך היום בטוח יעלה לי משהו.", - "expected_phonemes": "b a s | x i l aː l | ʔ i n n a h aː r | ʔ a k iː d | b i tˤ l a ʕ | m a ʕ i | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 73.4, - "end": 75.4 - }, - "arabic": "وهيك خلّصوا يشربوا القهوة", - "hebrew_transliteration": "וּהֵיכּ חַ'לַّצוּ יִשרַבּוּ אִל-קַהוֶה", - "hebrew": "וכך סיימו לשתות את הקפה", - "expected_phonemes": "u h eː k | x a l l a sˤ u | j i ʃ r a b u | i l ʔ a h w e" - }, - { - "voice_time_seconds": { - "start": 75.4, - "end": 79.6 - }, - "arabic": "وتوجّهوا للصّالة لكبيرة لاجتماع الوزراء.", - "hebrew_transliteration": "וּתוַגַّהוּ לַ-צַּאלַה לֶ-כְּבִּירֶה לַאִגְ'תִמַאע אִל-וֻזַרַא.", - "hebrew": "ופנו לאולם הגדול לישיבת השרים.", - "expected_phonemes": "u t w a g g a h u | l a sˤ aː l a | l e k b iː r e | l a ʔ i dʒ t i m aː ʕ | i l w u z a r a" - }, - { - "voice_time_seconds": { - "start": 79.6, - "end": 82.4 - }, - "arabic": "طلع الملك على دراج الشّايش العريضة", - "hebrew_transliteration": "טִלֶע אִל-מַלִכּ עַלַא דַרַאג' אִ-שַּׁאיִש אִל-עַרִידַ'ה", - "hebrew": "המלך עלה במדרגות השיש הרחבות", - "expected_phonemes": "tˤ i l e ʕ | i l m a l i k | ʕ a l a | d a r aː dʒ | ʔ i ʃ aː j i ʃ | i l ʕ a r iː dˤ a" - }, - { - "voice_time_seconds": { - "start": 82.4, - "end": 84.4 - }, - "arabic": "وأبو نوّاس بمشي وراه.", - "hebrew_transliteration": "וּאַבּוּ נַוַّאס בִּמשִי וַרַאה.", - "hebrew": "ואבו נואס הולך אחריו.", - "expected_phonemes": "u ʔ a b u | n a w w aː s | b i m ʃ i | w a r aː h" - }, - { - "voice_time_seconds": { - "start": 84.4, - "end": 88.6 - }, - "arabic": "وفجأة مدّ أبو نوّاس إيده لمؤخرة الملك", - "hebrew_transliteration": "וּפַגְ'אַה מַדّ אַבּוּ נַוַّאס אִידוֹ לַמֻאַחְ'רֶת אִל-מַלִכּ", - "hebrew": "ופתאום אבו נואס הושיט את ידו לאחוריו של המלך", - "expected_phonemes": "u f a dʒ ʔ a | m a d d | ʔ a b u | n a w w aː s | ʔ iː d o | l a m u ʔ a x r e t | i l m a l i k" - }, - { - "voice_time_seconds": { - "start": 88.6, - "end": 90.6 - }, - "arabic": "ومسك أردافه.", - "hebrew_transliteration": "וּמִסֶכּ אַרדַאפוֹ.", - "hebrew": "ותפס את ישבנו.", - "expected_phonemes": "u m i s e k | ʔ a r d aː f o" - }, - { - "voice_time_seconds": { - "start": 90.6, - "end": 93.6 - }, - "arabic": "«إنجنّيت؟» سأل الملك بصدمة.", - "hebrew_transliteration": "אִנגַ'נַّית? סַאַל אִל-מַלִכּ בִּצַדמֶה.", - "hebrew": "השתגעת? שאל המלך בהלם.", - "expected_phonemes": "ʔ i n dʒ a n n a j t | s a ʔ a l | i l m a l i k | b i sˤ a d m e" - }, - { - "voice_time_seconds": { - "start": 93.6, - "end": 97.0 - }, - "arabic": "«سامحني سيدي» جاوب أبو نوّاس،", - "hebrew_transliteration": "סַאמֶחנִי סִידִי, גַ'אוַבּ אַבּוּ נַוַّאס,", - "hebrew": "סלח לי אדוני, ענה אבו נואס,", - "expected_phonemes": "s aː m e ħ n i | s iː d i | dʒ aː w a b | ʔ a b u | n a w w aː s" - }, - { - "voice_time_seconds": { - "start": 97.0, - "end": 101.2 - }, - "arabic": "«كنت مفكّر إنّه هاي السّيدة زبيدة الملكة.»", - "hebrew_transliteration": "כֻּנֶת מְפַכַּר אִנּוֹ הַאי אִ-סַּיֶּדֶה זֻבֵּידֶה אִל-מַלכֶּה.", - "hebrew": "חשבתי שזו הגבירה זובידה המלכה.", - "expected_phonemes": "k u n e t | m f a k a r | ʔ i n o | h aː i | ʔ i s a j e d e | z u b eː d e | i l m a l k e" - }, - { - "voice_time_seconds": { - "start": 101.2, - "end": 105.0 - }, - "arabic": "«قصدت تعمل هيك للملكة؟ حقير ابن حقير!", - "hebrew_transliteration": "קַצַדֶת תַעמֶל הֵיכּ לַל-מַלכֶּה? חַקִיר אִבְּן חַקִיר!", - "hebrew": "התכוונת לעשות כך למלכה? נבזה בן נבזה!", - "expected_phonemes": "ʔ a sˤ a d e t | t a ʕ m e l | h eː k | l a l m a l k e | ħ a ʔ iː r | ʔ i b n | ħ a ʔ iː r" - }, - { - "voice_time_seconds": { - "start": 105.0, - "end": 107.6 - }, - "arabic": "اليوم رح آمر بشنقك، يا كلب!»", - "hebrew_transliteration": "אִל-יוֹם רַח-אַאמֹר בְּשַנקַכּ, יַא כַּלבּ!", - "hebrew": "היום אצווה לתלות אותך, כלב!", - "expected_phonemes": "i l j oː m | r a ħ ʔ aː m o r | b ʃ a n ʔ a k | j a | k a l b" - }, - { - "voice_time_seconds": { - "start": 107.6, - "end": 110.4 - }, - "arabic": "«لا يا سيدي، ما قصدت الملكة،", - "hebrew_transliteration": "לַא יַא סִידִי, מַא קַצַדֶת אִל-מַלכֶּה,", - "hebrew": "לא אדוני, לא התכוונתי למלכה,", - "expected_phonemes": "l aː | j a | s iː d i | m a | ʔ a sˤ a d e t | i l m a l k e" - }, - { - "voice_time_seconds": { - "start": 110.4, - "end": 112.4 - }, - "arabic": "بسّ هدا المثال إلّي طلبته", - "hebrew_transliteration": "בַּס הַאדַא אִל-מִתַאל אִלִّי טַלַבְּתוֹ", - "hebrew": "אלא זו הדוגמה שביקשת", - "expected_phonemes": "b a s | h aː d a | i l m i t aː l | ʔ i l l i | tˤ a l a b t o" - }, - { - "voice_time_seconds": { - "start": 112.4, - "end": 119.4 - }, - "arabic": "وإنّه فيه عذر أقبح من ذنب.»", - "hebrew_transliteration": "וּאִנּוֹ פִיה עֻד'ר אַקבַּח מִן דַ'נבּ.", - "hebrew": "לכך שיש תירוץ גרוע מחטא.", - "expected_phonemes": "u ʔ i n o | f iː h | ʕ u dˤ r | ʔ a ʔ b a ħ | m i n | dˤ a n b" - } - ] - }, - { - "file_name": "חזר בנעלי חנין * רִגֶ'ע בִּחֻ'פַי חַנִין * رجع بخفي حنين * סיפור עממי בערבית מדוברת [dApgffubK4c].mkv", - "youtube_id": "dApgffubK4c", - "title_hebrew": "חזר בנעלי חנין", - "segments": [ - { - "voice_time_seconds": { - "start": 2.0, - "end": 5.6 - }, - "arabic": "رجع بخفّي حنين", - "hebrew_transliteration": "רִגֶ'ע בִּחֻ'פַّי חַנִין", - "hebrew": "חזר בנעלי חנין", - "expected_phonemes": "r i dʒ e ʕ | b i x u f f a j | ħ a n iː n" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 7.8 - }, - "arabic": "كان يا ما كان", - "hebrew_transliteration": "כַּאן יַא מַא כַּאן", - "hebrew": "היה היה", - "expected_phonemes": "k aː n | j a | m a | k aː n" - }, - { - "voice_time_seconds": { - "start": 7.8, - "end": 12.0 - }, - "arabic": "واحد كندرجي شاطر أسمه حنين.", - "hebrew_transliteration": "וַאחַד כֻּנדַרגִ'י שַאטֶר אִסמוֹ חַנִין.", - "hebrew": "סנדלר מוכשר אחד ששמו חנין.", - "expected_phonemes": "w aː ħ a d | k u n d a r dʒ i | ʃ aː tˤ e r | ʔ i s m o | ħ a n iː n" - }, - { - "voice_time_seconds": { - "start": 12.0, - "end": 15.4 - }, - "arabic": "حنين هاد كان زلمة مبدع فنّان", - "hebrew_transliteration": "חַנִין הַאד כַּאן זַלַמֶה מֻבדֶע פַנַّאן", - "hebrew": "חנין הזה היה אדם יצירתי ואמן", - "expected_phonemes": "ħ a n iː n | h aː d | k aː n | z a l a m e | m u b d e ʕ | f a n n aː n" - }, - { - "voice_time_seconds": { - "start": 15.4, - "end": 19.0 - }, - "arabic": "وكان يصمّم ويعمل كنادر حلوة", - "hebrew_transliteration": "וּכַּאן יְצַמֶّם וִיעמַל כַּנַאדֶר חִלוֶה", - "hebrew": "והיה מעצב ומכין נעליים יפות", - "expected_phonemes": "u k aː n | j sˤ a m m e m | w iː ʕ m a l | k a n aː d e r | ħ i l w e" - }, - { - "voice_time_seconds": { - "start": 19.0, - "end": 21.4 - }, - "arabic": "مع جودة ممتازة.", - "hebrew_transliteration": "מַע גוּדֶה מֻמתַאזֶה.", - "hebrew": "באיכות מעולה.", - "expected_phonemes": "m a ʕ | g uː d e | m u m t aː z e" - }, - { - "voice_time_seconds": { - "start": 21.4, - "end": 22.4 - }, - "arabic": "مشان هيك", - "hebrew_transliteration": "מִשַאן הֵיכּ", - "hebrew": "לכן", - "expected_phonemes": "m i ʃ aː n | h eː k" - }, - { - "voice_time_seconds": { - "start": 22.4, - "end": 26.6 - }, - "arabic": "كان بيجي لعنده زباين من كلّ الأنحاء،", - "hebrew_transliteration": "כַּאן יִיגִ'י לַעִנדוֹ זְבַאיֶן מִן כֻּלّ אִל-אַנחַאא,", - "hebrew": "היו באים אליו לקוחות מכל הסביבה,", - "expected_phonemes": "k aː n | j iː dʒ i | l a ʕ i n d o | z b aː j e n | m i n | k u l l | i l ʔ a n ħ aː a" - }, - { - "voice_time_seconds": { - "start": 26.6, - "end": 31.0 - }, - "arabic": "مع انّه كان معروف ببضاعته الغالية.", - "hebrew_transliteration": "מַע אִנּוֹ כַּאן מַערוּף בִּבְּדַאעתוֹ אִל-עַ'אלְיֶה.", - "hebrew": "אף על פי שהיה ידוע בסחורתו היקרה.", - "expected_phonemes": "m a ʕ | ʔ i n o | k aː n | m a ʕ r uː f | b i b d aː ʕ t o | i l ɣ aː l j e" - }, - { - "voice_time_seconds": { - "start": 31.0, - "end": 32.4 - }, - "arabic": "يوم من الأيّام،", - "hebrew_transliteration": "יוֹם מִן אִל-אַיַّאם,", - "hebrew": "יום אחד,", - "expected_phonemes": "j oː m | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 32.4, - "end": 35.6 - }, - "arabic": "كان فيه هالبدوي راجع من السّفر", - "hebrew_transliteration": "כַּאן פִיה הַל-בַּדַוִי רַאגֶ'ע מִן אִ-סַّפַר", - "hebrew": "היה בדואי אחד שחזר ממסע", - "expected_phonemes": "k aː n | f iː h | h a l b a d a w i | r aː dʒ e ʕ | m i n | ʔ i s s a f a r" - }, - { - "voice_time_seconds": { - "start": 35.6, - "end": 38.6 - }, - "arabic": "ومحمّل معاه أغراض على جمله،", - "hebrew_transliteration": "וּמְחַמֶّל מַעַאה אַעְ'רַאד' עַלַא גַ'מַלוֹ,", - "hebrew": "ונשא איתו חפצים על גמלו,", - "expected_phonemes": "u m ħ a m m e l | m a ʕ aː h | ʔ a ɣ r aː dˤ | ʕ a l a | dʒ a m a l o" - }, - { - "voice_time_seconds": { - "start": 38.6, - "end": 40.6 - }, - "arabic": "وهوّ ماشي في الطّريق،", - "hebrew_transliteration": "וּהֻוֶّ מַאשִי פִי אִ-טַّרִיק,", - "hebrew": "ובזמן שהלך בדרך,", - "expected_phonemes": "u h u w w e | m aː ʃ i | f i | i tˤ tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 40.6, - "end": 46.2 - }, - "arabic": "شاف متجر حنين ملان ناس ومليان بضاعة حلوة.", - "hebrew_transliteration": "שַאף מַתגַ'ר חַנִין מַלַאן נַאס וּמַליַאן בְּדַאעַה חִלוֶה.", - "hebrew": "ראה את החנות של חנין מלאה אנשים ומלאה סחורה יפה.", - "expected_phonemes": "ʃ aː f | m a t dʒ a r | ħ a n iː n | m a l aː n | n aː s | u m a l j aː n | b d aː ʕ a | ħ i l w e" - }, - { - "voice_time_seconds": { - "start": 46.2, - "end": 47.8 - }, - "arabic": "فدخل على المتجر", - "hebrew_transliteration": "פַ-דַחַ'ל עַלַא אִל-מַתגַ'ר", - "hebrew": "אז נכנס לחנות", - "expected_phonemes": "f a d a x a l | ʕ a l a | i l m a t dʒ a r" - }, - { - "voice_time_seconds": { - "start": 47.8, - "end": 50.6 - }, - "arabic": "وانبسط كتير من البضاعة المعروضة،", - "hebrew_transliteration": "וִנבַּסַט כְּתִיר מִן לִ-בְּדַאעַה אִל-מַערוּדַ'ה,", - "hebrew": "ומאוד נהנה מהסחורה המוצגת,", - "expected_phonemes": "w i n b a s a tˤ | k t iː r | m i n | l i b d aː ʕ a | i l m a ʕ r uː dˤ a" - }, - { - "voice_time_seconds": { - "start": 50.6, - "end": 53.8 - }, - "arabic": "وصار بدّه يشتري كندرة.", - "hebrew_transliteration": "וּצַאר בַּדּוֹ יִשתְרִי כֻּנדַרַה.", - "hebrew": "ורצה לקנות נעליים.", - "expected_phonemes": "u sˤ aː r | b a d o | j i ʃ t r i | k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 53.8, - "end": 57.6 - }, - "arabic": "لمّا سمع هالبدوي سعر الكندرة إنصدم", - "hebrew_transliteration": "לַמַّא סִמֶע הַל-בַּדַוִי סִעְר אִל-כֻּנדַרַה אִנצַדַם", - "hebrew": "כשהבדואי הזה שמע את מחיר הנעליים הוא הזדעזע", - "expected_phonemes": "l a m m a | s i m e ʕ | h a l b a d a w i | s i ʕ r | i l k u n d a r a | ʔ i n sˤ a d a m" - }, - { - "voice_time_seconds": { - "start": 57.6, - "end": 61.4 - }, - "arabic": "وبلّش يفاصل بحنين لمدّة طويلة،", - "hebrew_transliteration": "וּבַּלַّש יפַאצֶל בְּחַנִין לַמֻדֶّה טַוִילֶה,", - "hebrew": "והתחיל להתמקח עם חנין זמן רב,", - "expected_phonemes": "u b a l l a ʃ | j f aː sˤ e l | b ħ a n iː n | l a m u d d e | tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 61.4, - "end": 64.0 - }, - "arabic": "ومرّة يحكيله هاي سعرها غالي", - "hebrew_transliteration": "וּמַרַّה יִחכִּי-לוֹ הַאי סִעְרְהַא עַ'אלִי", - "hebrew": "פעם אומר לו שזה מחירה יקר", - "expected_phonemes": "u m a r r a | j i ħ k iː l o | h aː i | s i ʕ r h a | ɣ aː l i" - }, - { - "voice_time_seconds": { - "start": 64.0, - "end": 68.2 - }, - "arabic": "ومرّة يقول له انت زلمة ما بدّك تبيع.", - "hebrew_transliteration": "וּמַרַّה יְקוּל-לוֹ אִנתֶ זַלַמֶה מַא בַּדַّכּ תְבִּיע.", - "hebrew": "ופעם אומר לו אתה אדם שלא רוצה למכור.", - "expected_phonemes": "u m a r r a | j ʔ uː l l o | ʔ i n t e | z a l a m e | m a | b a d d a k | t b iː ʕ" - }, - { - "voice_time_seconds": { - "start": 68.2, - "end": 73.4 - }, - "arabic": "وهيك بقيوا حنين وهالزّلمة يفاصلوا بعض كتير وقت،", - "hebrew_transliteration": "וּהֵיכּ בְּקְיוּ חַנִין וּהַ-זַّלַמֶה יפַאצְלוּ בַּעְד' כְּתִיר וַקֶת,", - "hebrew": "וכך חנין והאיש הזה המשיכו להתמקח זה עם זה הרבה זמן,", - "expected_phonemes": "u h eː k | b ʔ iː u | ħ a n iː n | u h a z z a l a m e | j f aː sˤ l u | b a ʕ dˤ | k t iː r | w a ʔ e t" - }, - { - "voice_time_seconds": { - "start": 73.4, - "end": 78.0 - }, - "arabic": "وكلّ هاد الوقت حنين ما كان معطي إنتباهه", - "hebrew_transliteration": "וּכֻּלّ הַאדַא אִל-וַקת חַנִין מַא כַּאן מַעטִי אִנתִבַּאהוֹ", - "hebrew": "וכל הזמן הזה חנין לא שם לב", - "expected_phonemes": "u k u l l | h aː d a | i l w a ʔ t | ħ a n iː n | m a | k aː n | m a ʕ tˤ i | ʔ i n t i b aː h o" - }, - { - "voice_time_seconds": { - "start": 78.0, - "end": 80.0 - }, - "arabic": "لولا حدا من زباينه", - "hebrew_transliteration": "לַוְלַא חַדַא מִן זְבַאינוֹ", - "hebrew": "לאף אחד מלקוחותיו", - "expected_phonemes": "l a w l a | ħ a d a | m i n | z b aː j n o" - }, - { - "voice_time_seconds": { - "start": 80.0, - "end": 83.2 - }, - "arabic": "إلّي أجوا على متجره عشان يشتروا.", - "hebrew_transliteration": "אִלִّי אַגוּ עַלַא מַתגַ'רוֹ עַשַאן יִשתְרוּ.", - "hebrew": "שבאו לחנות שלו כדי לקנות.", - "expected_phonemes": "ʔ i l l i | ʔ a g u | ʕ a l a | m a t dʒ a r o | ʕ a ʃ aː n | j i ʃ t r u" - }, - { - "voice_time_seconds": { - "start": 83.2, - "end": 86.0 - }, - "arabic": "وهيك ضيّع عليه مبلغ كبير", - "hebrew_transliteration": "וּהֵיכּ דַ'יַّע עַלֵיה מַבלַעְ' כְּבִּיר", - "hebrew": "וכך הפסיד סכום גדול", - "expected_phonemes": "u h eː k | dˤ a j j a ʕ | ʕ a l eː h | m a b l a ɣ | k b iː r" - }, - { - "voice_time_seconds": { - "start": 86.0, - "end": 90.0 - }, - "arabic": "كان ممكن يربحه في هداك النّهار.", - "hebrew_transliteration": "כַּאן מֻמכֶּן יִרבַּחוֹ פִי הַדַאכּ אִ-נַّהַאר.", - "hebrew": "שיכול היה להרוויח באותו יום.", - "expected_phonemes": "k aː n | m u m k e n | j i r b a ħ o | f i | h a d aː k | ʔ i n n a h aː r" - }, - { - "voice_time_seconds": { - "start": 90.0, - "end": 91.2 - }, - "arabic": "المهمّ،", - "hebrew_transliteration": "אִל-מֻהֶם,", - "hebrew": "העיקר,", - "expected_phonemes": "i l m u h e m" - }, - { - "voice_time_seconds": { - "start": 91.2, - "end": 94.8 - }, - "arabic": "بعد شي ساعة من الحكي والرّغي والمفاصلة،", - "hebrew_transliteration": "בַּעֶד שִי סַאעַה מִן אִל-חַכִּי וִ-רַّעְ'י וִל-מְפַאצַלֶה,", - "hebrew": "אחרי בערך שעה של דיבורים, פטפוטים ומיקוח,", - "expected_phonemes": "b a ʕ e d | ʃ i | s aː ʕ a | m i n | i l ħ a k i | w i r r a ɣ i | w i l m f aː sˤ a l e" - }, - { - "voice_time_seconds": { - "start": 94.8, - "end": 98.6 - }, - "arabic": "إتّفقوا التّنين على سعر بيرضي الطّرفين.", - "hebrew_transliteration": "אִתַّפַקוּ אִ-תְּנֵין עַלַא סִעְר בִּירְדִ'י אִ-טַّרַפֵין.", - "hebrew": "השניים הסכימו על מחיר שמספק את שני הצדדים.", - "expected_phonemes": "ʔ i t t a f a ʔ u | ʔ i t n eː n | ʕ a l a | s i ʕ r | b iː r dˤ i | ʔ i tˤ tˤ a r a f eː n" - }, - { - "voice_time_seconds": { - "start": 98.6, - "end": 100.6 - }, - "arabic": "فراح حنين يجهّز الكندرة", - "hebrew_transliteration": "פַ-רַאח חַנִין יְגַ'הֶّז אִל-כֻּנדַרַה", - "hebrew": "אז חנין הלך להכין את הנעליים", - "expected_phonemes": "f a r aː ħ | ħ a n iː n | j dʒ a h h e z | i l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 100.6, - "end": 103.4 - }, - "arabic": "إلّي كان بدّه إيّاها البدوي.", - "hebrew_transliteration": "אִלִّי כַּאן בַּדּוֹ אִיַּאהַא אִל-בַּדַוִי.", - "hebrew": "שהבדואי רצה.", - "expected_phonemes": "ʔ i l l i | k aː n | b a d o | ʔ i j aː h a | i l b a d a w i" - }, - { - "voice_time_seconds": { - "start": 103.4, - "end": 106.6 - }, - "arabic": "وإلّا بشوف انّه الزّلمة مسك حاله", - "hebrew_transliteration": "וְאִלַّא בִּשוּף אִנּוֹ אִ-זַّלַמֶה מִסֶכּ חַאלוֹ", - "hebrew": "והנה הוא רואה שהאיש קם", - "expected_phonemes": "u ʔ i l l a | b i ʃ uː f | ʔ i n o | ʔ i z z a l a m e | m i s e k | ħ aː l o" - }, - { - "voice_time_seconds": { - "start": 106.6, - "end": 110.2 - }, - "arabic": "وطلع من المتجر من غير ما يوخد الكندرة", - "hebrew_transliteration": "וּטִלֶע מִן אִל-מַתגַ'ר מִן עֵ'יר מַא יוֹחֹ'ד אִל-כֻּנדַרַה", - "hebrew": "ויצא מהחנות בלי לקחת את הנעליים", - "expected_phonemes": "u tˤ i l e ʕ | m i n | i l m a t dʒ a r | m i n | ɣ eː r | m a | j oː x o d | i l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 110.2, - "end": 112.6 - }, - "arabic": "ولا يدفع حقّها.", - "hebrew_transliteration": "וְלַא יִדפַע חַקְّהַא.", - "hebrew": "ובלי לשלם את מחירן.", - "expected_phonemes": "u l aː | j i d f a ʕ | ħ a ʔ ʔ h a" - }, - { - "voice_time_seconds": { - "start": 112.6, - "end": 116.2 - }, - "arabic": "حنين إنجنّ وعصّب كتير،", - "hebrew_transliteration": "חַנִין אִנגַ'ן וּעַצַّבּ כְּתִיר,", - "hebrew": "חנין השתגע וכעס מאוד,", - "expected_phonemes": "ħ a n iː n | ʔ i n dʒ a n | u ʕ a sˤ sˤ a b | k t iː r" - }, - { - "voice_time_seconds": { - "start": 116.2, - "end": 120.8 - }, - "arabic": "لإنّ بسبب هاد الزّلمة ضيّع وقت ومصاري كتير.", - "hebrew_transliteration": "לַאִנّ בִּסַבַּבּ הַאד אִ-זַּלַמֶה דַ'יַّע וַקֶת וּמַצַארִי כְּתִיר.", - "hebrew": "כי בגלל האיש הזה הפסיד הרבה זמן וכסף.", - "expected_phonemes": "l a ʔ i n n | b i s a b a b | h aː d | ʔ i z a l a m e | dˤ a j j a ʕ | w a ʔ e t | u m a sˤ aː r i | k t iː r" - }, - { - "voice_time_seconds": { - "start": 120.8, - "end": 123.4 - }, - "arabic": "فقرّر إنّه ينتقم منّه،", - "hebrew_transliteration": "פַ-קַרַّר אִנּוֹ יִנתַקֶם מִנּוֹ,", - "hebrew": "אז החליט לנקום בו,", - "expected_phonemes": "f a ʔ a r r a r | ʔ i n o | j i n t a ʔ e m | m i n o" - }, - { - "voice_time_seconds": { - "start": 123.4, - "end": 125.8 - }, - "arabic": "ولهيك طلع من متجره", - "hebrew_transliteration": "וּלַהֵיכּ טִלֶע מִן מַתגַ'רוֹ", - "hebrew": "ולכן יצא מהחנות שלו", - "expected_phonemes": "u l a h eː k | tˤ i l e ʕ | m i n | m a t dʒ a r o" - }, - { - "voice_time_seconds": { - "start": 125.8, - "end": 129.4 - }, - "arabic": "ومعاه الكندرة إلّي كان بدّه إيّاها البدوي،", - "hebrew_transliteration": "וּמַעַאה אִל-כֻּנדַרַה אִלִّי כַּאן בַּדּוֹ אִיַּאהַא אִל-בַּדַוִי,", - "hebrew": "ואיתו הנעליים שהבדואי רצה,", - "expected_phonemes": "u m a ʕ aː h | i l k u n d a r a | ʔ i l l i | k aː n | b a d o | ʔ i j aː h a | i l b a d a w i" - }, - { - "voice_time_seconds": { - "start": 129.4, - "end": 133.6 - }, - "arabic": "وصار يلحق فيه من غير ما هداك ينتبهله.", - "hebrew_transliteration": "וּצַאר יִלחַק פִיה מִן עֵ'יר מַא הַדַאכּ יִנתַבֶּה-לוֹ.", - "hebrew": "והתחיל לעקוב אחריו בלי שההוא ישים לב אליו.", - "expected_phonemes": "u sˤ aː r | j i l ħ a ʔ | f iː h | m i n | ɣ eː r | m a | h a d aː k | j i n t a b e h l o" - }, - { - "voice_time_seconds": { - "start": 133.6, - "end": 137.0 - }, - "arabic": "وبعد شويّ حنين سبق البدوي", - "hebrew_transliteration": "וּבַּעֶד שְוַיّ חַנִין סַבַק אִל-בַּדַוִי", - "hebrew": "ואחרי זמן קצר חנין הקדים את הבדואי", - "expected_phonemes": "u b a ʕ e d | ʃ w a j j | ħ a n iː n | s a b a ʔ | i l b a d a w i" - }, - { - "voice_time_seconds": { - "start": 137.0, - "end": 141.0 - }, - "arabic": "وحطّ في نصّ الطّريق فردة واحدة من الكندرة", - "hebrew_transliteration": "וּחַטّ פִי נֻצّ אִ-טַּרִיק פַרדֶה וַאחדֶה מִן אִל-כֻּנדַרַה", - "hebrew": "ושם באמצע הדרך נעל אחת מהזוג", - "expected_phonemes": "u ħ a tˤ tˤ | f i | n u sˤ sˤ | ʔ i tˤ a r iː ʔ | f a r d e | w aː ħ d e | m i n | i l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 141.0, - "end": 142.8 - }, - "arabic": "وراح يتخبّى.", - "hebrew_transliteration": "וּרַאח יִתחַ'בַّא.", - "hebrew": "והלך להתחבא.", - "expected_phonemes": "u r aː ħ | j i t x a b b a" - }, - { - "voice_time_seconds": { - "start": 142.8, - "end": 145.0 - }, - "arabic": "لمّا وصل البدوي لهناك", - "hebrew_transliteration": "לַמַّא וְצֶל אִל-בַּדַוִי לַהֻנַאכּ", - "hebrew": "כשהבדואי הגיע לשם", - "expected_phonemes": "l a m m a | u sˤ e l | i l b a d a w i | l a h u n aː k" - }, - { - "voice_time_seconds": { - "start": 145.0, - "end": 147.8 - }, - "arabic": "وشاف فردة الكندرة في نصّ الطّريق،", - "hebrew_transliteration": "וּשַאף פַרדֶת אִל-כֻּנדַרַה פִי נֻצّ אִ-טַּרִיק,", - "hebrew": "וראה את הנעל באמצע הדרך,", - "expected_phonemes": "u ʃ aː f | f a r d e t | i l k u n d a r a | f i | n u sˤ sˤ | ʔ i tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 147.8, - "end": 150.6 - }, - "arabic": "استغرب كتير وانبسط،", - "hebrew_transliteration": "אִסתַעְ'רַבּ כְּתִיר וּאִנבַּסַט,", - "hebrew": "התפלא מאוד ושמח,", - "expected_phonemes": "ʔ i s t a ɣ r a b | k t iː r | u ʔ i n b a s a tˤ" - }, - { - "voice_time_seconds": { - "start": 150.6, - "end": 152.0 - }, - "arabic": "وقال لنفسه:", - "hebrew_transliteration": "וּקַאל לַנַפסוֹ:", - "hebrew": "ואמר לעצמו:", - "expected_phonemes": "u ʔ aː l | l a n a f s o" - }, - { - "voice_time_seconds": { - "start": 152.0, - "end": 154.6 - }, - "arabic": "«هاي واحدة من كنادر حنين،", - "hebrew_transliteration": "הַאי וַאחדֶה מִן כַּנַאדֶר חַנִין,", - "hebrew": "זו אחת מהנעליים של חנין,", - "expected_phonemes": "h aː i | w aː ħ d e | m i n | k a n aː d e r | ħ a n iː n" - }, - { - "voice_time_seconds": { - "start": 154.6, - "end": 157.0 - }, - "arabic": "متل اللّي كان بدّي ايّاها", - "hebrew_transliteration": "מִתֶל אִלִّי כַּאן בַּדִّי אִיַּאהַא", - "hebrew": "כמו זו שרציתי", - "expected_phonemes": "m i t e l | ʔ i l l i | k aː n | b a d d i | ʔ i j aː h a" - }, - { - "voice_time_seconds": { - "start": 157.0, - "end": 158.8 - }, - "arabic": "خلّيني آخدها.»", - "hebrew_transliteration": "חַ'לִّינִי אַאחֹ'דְהַא.", - "hebrew": "תן לי לקחת אותה.", - "expected_phonemes": "x a l l iː n i | ʔ aː x o d h a" - }, - { - "voice_time_seconds": { - "start": 158.8, - "end": 161.4 - }, - "arabic": "بسّ بعدين راجع حاله،", - "hebrew_transliteration": "בַּס בַּעדֵין רַאגַ'ע חַאלוֹ,", - "hebrew": "אבל אחר כך חזר בו,", - "expected_phonemes": "b a s | b a ʕ d eː n | r aː dʒ a ʕ | ħ aː l o" - }, - { - "voice_time_seconds": { - "start": 161.4, - "end": 163.4 - }, - "arabic": "وفكّر إنّه مش راح يستفيد", - "hebrew_transliteration": "וּפַכַּר אִנּוֹ מִש רַאח יִסתַפִיד", - "hebrew": "וחשב שלא ירוויח כלום", - "expected_phonemes": "u f a k a r | ʔ i n o | m i ʃ | r aː ħ | j i s t a f iː d" - }, - { - "voice_time_seconds": { - "start": 163.4, - "end": 165.6 - }, - "arabic": "بفردة واحدة من الكندرة.", - "hebrew_transliteration": "בְּפַרדֶה וַאחדֶה מִן אִל-כֻּנדַרַה.", - "hebrew": "מנעל אחת מהזוג.", - "expected_phonemes": "b f a r d e | w aː ħ d e | m i n | i l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 165.6, - "end": 170.0 - }, - "arabic": "عشان هيك تركها وكمّل طريقه.", - "hebrew_transliteration": "עַשַאן הֵיכּ תַרַכְּהַא וּכַּמַّל טַרִיקוֹ.", - "hebrew": "לכן השאיר אותה והמשיך בדרכו.", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | t a r a k h a | u k a m m a l | tˤ a r iː ʔ o" - }, - { - "voice_time_seconds": { - "start": 170.0, - "end": 172.0 - }, - "arabic": "بعد مية متر لقدّام", - "hebrew_transliteration": "בַּעֶד מִית מִתֶר לַקֻדַّאם", - "hebrew": "אחרי מאה מטר קדימה", - "expected_phonemes": "b a ʕ e d | m iː t | m i t e r | l a ʔ u d d aː m" - }, - { - "voice_time_seconds": { - "start": 172.0, - "end": 176.4 - }, - "arabic": "شاف البدوي الفردة التّانية مرميّة بنصّ الطّريق.", - "hebrew_transliteration": "שַאף אִל-בַּדַוִי אִל-פַרדֶה אִ-תַּאנְיֶה מַרמִיֶّה בְּנֻצّ אִ-טַּרִיק.", - "hebrew": "הבדואי ראה את הנעל השנייה זרוקה באמצע הדרך.", - "expected_phonemes": "ʃ aː f | i l b a d a w i | i l f a r d e | ʔ i t aː n j e | m a r m i j j e | b n u sˤ sˤ | ʔ i tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 176.4, - "end": 178.8 - }, - "arabic": "ففكّر وقال لنفسه:", - "hebrew_transliteration": "פַ-פַכַّר וּקַאל לַנַפסוֹ:", - "hebrew": "אז חשב ואמר לעצמו:", - "expected_phonemes": "f a f a k k a r | u ʔ aː l | l a n a f s o" - }, - { - "voice_time_seconds": { - "start": 178.8, - "end": 181.0 - }, - "arabic": "«أحسن لي أرجع لورا", - "hebrew_transliteration": "אַחסַן לִי אַרגַ'ע לַוַרַא", - "hebrew": "עדיף לי לחזור אחורה", - "expected_phonemes": "ʔ a ħ s a n | l i | ʔ a r dʒ a ʕ | l a w a r a" - }, - { - "voice_time_seconds": { - "start": 181.0, - "end": 183.2 - }, - "arabic": "وين ما كانت الفردة الأولى", - "hebrew_transliteration": "וֵין מַא כַּאנַת אִל-פַרדֶה אִל-אוּלַא", - "hebrew": "למקום שבו הייתה הנעל הראשונה", - "expected_phonemes": "w eː n | m a | k aː n a t | i l f a r d e | i l ʔ uː l a" - }, - { - "voice_time_seconds": { - "start": 183.2, - "end": 187.4 - }, - "arabic": "عشان آخدها وتصير معي الكندرة كاملة.»", - "hebrew_transliteration": "עַשַאן אַאחֹ'דְהַא וּתְצִיר מַעִי אִל-כֻּנדַרַה כַּאמְלֶה.", - "hebrew": "כדי לקחת אותה ויהיה לי זוג נעליים שלם.", - "expected_phonemes": "ʕ a ʃ aː n | ʔ aː x o d h a | u t sˤ iː r | m a ʕ i | i l k u n d a r a | k aː m l e" - }, - { - "voice_time_seconds": { - "start": 187.4, - "end": 190.8 - }, - "arabic": "وبالفعل أخد البدوي الفردة التّانية،", - "hebrew_transliteration": "וּבִּל-פִעֶל אַחַ'ד אִל-בַּדַוִי אִל-פַרדֶה אִ-תַּאנְיֶה,", - "hebrew": "ואכן הבדואי לקח את הנעל השנייה,", - "expected_phonemes": "u b i l f i ʕ e l | ʔ a x a d | i l b a d a w i | i l f a r d e | ʔ i t aː n j e" - }, - { - "voice_time_seconds": { - "start": 190.8, - "end": 195.6 - }, - "arabic": "خلّى جمله وبضاعته وكلّ أغراضه هناك في نصّ الطّريق", - "hebrew_transliteration": "חַ'לַّא גַ'מַלוֹ וּבְּדַאעתוֹ וּכֻּלّ אַעְ'רַאדוֹ הֻנַאכּ פִי נֻצّ אִ-טַּרִיק", - "hebrew": "השאיר את גמלו, את סחורתו ואת כל חפציו שם באמצע הדרך", - "expected_phonemes": "x a l l a | dʒ a m a l o | u b d aː ʕ t o | u k u l l | ʔ a ɣ r aː d o | h u n aː k | f i | n u sˤ sˤ | ʔ i tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 195.6, - "end": 199.0 - }, - "arabic": "ورجع لورا يجيب الفردة الأولى.", - "hebrew_transliteration": "וּרִגֶ'ע לַוַרַא יְגִ'יבּ אִל-פַרדֶה אִל-אוּלַא.", - "hebrew": "וחזר אחורה להביא את הנעל הראשונה.", - "expected_phonemes": "u r i dʒ e ʕ | l a w a r a | j dʒ iː b | i l f a r d e | i l ʔ uː l a" - }, - { - "voice_time_seconds": { - "start": 199.0, - "end": 203.2 - }, - "arabic": "حنين الّي كان متخبّي وعم بيراقب البدوي،", - "hebrew_transliteration": "חַנִין אִלִّי כַּאן מִתחַ'בִّי וּעַם-בִּירַאקֶבּ אִל-בַּדַוִי,", - "hebrew": "חנין, שהתחבא ועקב אחרי הבדואי,", - "expected_phonemes": "ħ a n iː n | ʔ i l l i | k aː n | m i t x a b b i | u ʕ a m b iː r aː ʔ e b | i l b a d a w i" - }, - { - "voice_time_seconds": { - "start": 203.2, - "end": 206.0 - }, - "arabic": "انبسط إنّه قدر يخدع البدوي.", - "hebrew_transliteration": "אִנבַּסַט אִנּוֹ קִדֶר יִחְ'דַע אִל-בַּדַוִי.", - "hebrew": "שמח שהצליח לרמות את הבדואי.", - "expected_phonemes": "ʔ i n b a s a tˤ | ʔ i n o | ʔ i d e r | j i x d a ʕ | i l b a d a w i" - }, - { - "voice_time_seconds": { - "start": 206.0, - "end": 208.6 - }, - "arabic": "ودغري بسّ راح البدوي يمشي", - "hebrew_transliteration": "וּדֻעְ'רִי בַּס רַאח אִל-בַּדַוִי יִמשִי", - "hebrew": "ומיד כשהבדואי הלך", - "expected_phonemes": "u d u ɣ r i | b a s | r aː ħ | i l b a d a w i | j i m ʃ i" - }, - { - "voice_time_seconds": { - "start": 208.6, - "end": 210.8 - }, - "arabic": "بطريقه للفردة الأولى،", - "hebrew_transliteration": "בְּטַרִיקוֹ לַל-פַרדֶה אִל-אוּלַא,", - "hebrew": "בדרכו אל הנעל הראשונה,", - "expected_phonemes": "b tˤ a r iː ʔ o | l a l f a r d e | i l ʔ uː l a" - }, - { - "voice_time_seconds": { - "start": 210.8, - "end": 214.0 - }, - "arabic": "راح حنين وأخد كلّ أغراض هالبدوي،", - "hebrew_transliteration": "רַאח חַנִין וּאַחַ'ד כֻּלّ אַעְ'רַאד' הַל-בַּדַוִי,", - "hebrew": "חנין הלך ולקח את כל החפצים של הבדואי,", - "expected_phonemes": "r aː ħ | ħ a n iː n | u ʔ a x a d | k u l l | ʔ a ɣ r aː dˤ | h a l b a d a w i" - }, - { - "voice_time_seconds": { - "start": 214.0, - "end": 219.4 - }, - "arabic": "وحتّى انّه أخدله جمله، وما خلّالوش إشي.", - "hebrew_transliteration": "וּחַתַّא אִנּוֹ אַחַ'ד-לוֹ גַ'מַלוֹ, וּמַא חַ'לַّא-לוֹש אִשִי.", - "hebrew": "ואפילו לקח לו את הגמל, ולא השאיר לו כלום.", - "expected_phonemes": "u ħ a t t a | ʔ i n o | ʔ a x a d l o | dʒ a m a l o | u m a | x a l l aː l oː ʃ | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 219.4, - "end": 223.0 - }, - "arabic": "لمّا رجع البدوي يوخد غراضه من نصّ الطّريق،", - "hebrew_transliteration": "לַמַّא רִגֶ'ע אִל-בַּדַוִי יוֹחֹ'ד עַ'רַאדוֹ מִן נֻצّ אִ-טַּרִיק,", - "hebrew": "כשהבדואי חזר לקחת את חפציו מאמצע הדרך,", - "expected_phonemes": "l a m m a | r i dʒ e ʕ | i l b a d a w i | j oː x o d | ɣ a r aː d o | m i n | n u sˤ sˤ | ʔ i tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 223.0, - "end": 227.2 - }, - "arabic": "ما لاقاش إشي وفهم إنّه تعرّض للسّرقة.", - "hebrew_transliteration": "מַא לַאקַאש אִשִי וּפִהֶם אִנּוֹ תְעַרַّד' לַ-סַּרִקַה.", - "hebrew": "לא מצא כלום והבין שנשדד.", - "expected_phonemes": "m a | l aː ʔ aː ʃ | ʔ i ʃ i | u f i h e m | ʔ i n o | t ʕ a r r a dˤ | l a s a r i ʔ a" - }, - { - "voice_time_seconds": { - "start": 227.2, - "end": 231.0 - }, - "arabic": "وهيك رجع على بيته بدون إشي،", - "hebrew_transliteration": "וּהֵיכּ רִגֶ'ע עַלַא בֵּיתוֹ בְּדוּן אִשִי,", - "hebrew": "וכך חזר לביתו בלי כלום,", - "expected_phonemes": "u h eː k | r i dʒ e ʕ | ʕ a l a | b eː t o | b i d uː n | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 231.0, - "end": 234.2 - }, - "arabic": "بيمشي إيد لقدّام وإيد لورا،", - "hebrew_transliteration": "בִּימשִי אִיד לַקֻדַّאם וּאִיד לַוַרַא,", - "hebrew": "הולך יד קדימה ויד אחורה,", - "expected_phonemes": "b iː m ʃ i | ʔ iː d | l a ʔ u d d aː m | u ʔ iː d | l a w a r a" - }, - { - "voice_time_seconds": { - "start": 234.2, - "end": 237.4 - }, - "arabic": "بسّ كان فيه بإجره كندرة حنين.", - "hebrew_transliteration": "בַּס כַּאן פִיה בִּאִגְ'רוֹ כֻּנדַרֶת חַנִין.", - "hebrew": "אבל על רגליו היו הנעליים של חנין.", - "expected_phonemes": "b a s | k aː n | f iː h | b i ʔ i dʒ r o | k u n d a r e t | ħ a n iː n" - }, - { - "voice_time_seconds": { - "start": 237.4, - "end": 239.8 - }, - "arabic": "وهيك صار يضرب المثل", - "hebrew_transliteration": "וּהֵיכּ צַאר יִדְ'רַבּ אִל-מַתַ'ל", - "hebrew": "וכך התחילו להשתמש בפתגם", - "expected_phonemes": "u h eː k | sˤ aː r | j i dˤ r a b | i l m a t a l" - }, - { - "voice_time_seconds": { - "start": 239.8, - "end": 244.6 - }, - "arabic": "بكلّ واحد بيرجع بخيبة أمل وما معه إشي،", - "hebrew_transliteration": "בְּכֻּלّ וַאחַד בִּירגַ'ע בִּחַ'יבֶּת אַמַל וּמַא מַעוֹ אִשִי,", - "hebrew": "על כל מי שחוזר באכזבה ובלי כלום,", - "expected_phonemes": "b k u l l | w aː ħ a d | b iː r dʒ a ʕ | b i x a j b e t | ʔ a m a l | u m a | m a ʕ o | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 244.6, - "end": 249.8 - }, - "arabic": "وبيقولوا عنّه زيّ الّي رجع بخفّي حنين.", - "hebrew_transliteration": "וּבִּיקוּלוּ עַנּוֹ זַיّ אִלִّי רִגֶ'ע בִּחֻ'פַّי חַנִין.", - "hebrew": "ואומרים עליו: כמו מי שחזר בנעלי חנין.", - "expected_phonemes": "u b iː ʔ uː l u | ʕ a n o | z a j j | ʔ i l l i | r i dʒ e ʕ | b i x u f f a j | ħ a n iː n" - } - ] - }, - { - "file_name": "כיפה אדומה * לֵילַא אִל-חַמרָא * ليلى الحمرا * סיפור בערבית מדוברת [Yy-SUZDn0cE].mkv", - "youtube_id": "Yy-SUZDn0cE", - "title_hebrew": "כיפה אדומה", - "segments": [ - { - "voice_time_seconds": { - "start": 1.2, - "end": 5.6 - }, - "arabic": "ليلى الحمرا", - "hebrew_transliteration": "לֵילַא אִל-חַמרַא", - "hebrew": "כיפה אדומה", - "expected_phonemes": "l eː l a | i l ħ a m r a" - }, - { - "voice_time_seconds": { - "start": 6.2, - "end": 9.2 - }, - "arabic": "كان يا مكان في قديم الزّمان", - "hebrew_transliteration": "כַּאן יַא מַכַּאן פִי קַדִים אִ-זַّמַאן", - "hebrew": "היה היה בימים עברו", - "expected_phonemes": "k aː n | j a | m a k aː n | f i | ʔ a d iː m | ʔ i z z a m aː n" - }, - { - "voice_time_seconds": { - "start": 9.2, - "end": 12.2 - }, - "arabic": "بنت صغيرة أسمها ليلى.", - "hebrew_transliteration": "בִּנֶת זְעִ'ירֶה אִסמְהַא לֵילַא.", - "hebrew": "ילדה קטנה ששמה לילא.", - "expected_phonemes": "b i n e t | z ɣ iː r e | ʔ i s m h a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 12.2, - "end": 15.6 - }, - "arabic": "ليلى كانت تحبّ اللّون الأحمر،", - "hebrew_transliteration": "לֵילַא כַּאנַת תְחֶבّ אִל-לוֹן אִל-אַחמַר,", - "hebrew": "לילא אהבה את הצבע האדום,", - "expected_phonemes": "l eː l a | k aː n a t | t ħ e b b | i l l oː n | i l ʔ a ħ m a r" - }, - { - "voice_time_seconds": { - "start": 15.6, - "end": 19.0 - }, - "arabic": "وعشان هيك كانت كلّ أواعيها حمرا.", - "hebrew_transliteration": "וּעַשַאן הֵיכּ כַּאנַת כֻּלّ אַוַאעִיהַא חַמרַא.", - "hebrew": "ולכן כל הבגדים שלה היו אדומים.", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | k aː n a t | k u l l | ʔ a w aː ʕ iː h a | ħ a m r a" - }, - { - "voice_time_seconds": { - "start": 19.0, - "end": 22.2 - }, - "arabic": "فستانها كان لونه أحمر...", - "hebrew_transliteration": "פֻסתַאנְהַא כַּאן לוֹנוֹ אַחמַר...", - "hebrew": "השמלה שלה הייתה אדומה...", - "expected_phonemes": "f u s t aː n h a | k aː n | l oː n o | ʔ a ħ m a r" - }, - { - "voice_time_seconds": { - "start": 22.2, - "end": 24.8 - }, - "arabic": "جاكيتها كان لونه أحمر...", - "hebrew_transliteration": "גַ'אכֵּיתְהַא כַּאן לוֹנוֹ אַחמַר...", - "hebrew": "הז'קט שלה היה אדום...", - "expected_phonemes": "dʒ aː k eː t h a | k aː n | l oː n o | ʔ a ħ m a r" - }, - { - "voice_time_seconds": { - "start": 24.8, - "end": 27.6 - }, - "arabic": "طقيّتها كانت حمرا...", - "hebrew_transliteration": "טַקִיֶّתְהַא כַּאנַת חַמרַא...", - "hebrew": "הכובע שלה היה אדום...", - "expected_phonemes": "tˤ a ʔ i j j e t h a | k aː n a t | ħ a m r a" - }, - { - "voice_time_seconds": { - "start": 27.6, - "end": 29.8 - }, - "arabic": "كفوفها كانوا حمر...", - "hebrew_transliteration": "כְּפוּפְהַא כַּאנוּ חֻמֻר...", - "hebrew": "הכפפות שלה היו אדומות...", - "expected_phonemes": "k f uː f h a | k aː n u | ħ u m u r" - }, - { - "voice_time_seconds": { - "start": 29.8, - "end": 34.0 - }, - "arabic": "وحتّى كلساتها وجزمتها كان لونهم أحمر.", - "hebrew_transliteration": "וּחַתַّא כַּלסַאתְהַא וּגַ'זמִתְהַא כַּאן לוֹנְהֹם אַחמַר.", - "hebrew": "ואפילו הגרביים והמגפיים שלה היו אדומים.", - "expected_phonemes": "u ħ a t t a | k a l s aː t h a | u dʒ a z m i t h a | k aː n | l oː n h o m | ʔ a ħ m a r" - }, - { - "voice_time_seconds": { - "start": 34.0, - "end": 39.2 - }, - "arabic": "وعشان هيك الكلّ كان يناديها ليلى الحمرا.", - "hebrew_transliteration": "וּעַשַאן הֵיכּ אִל-כֻּלّ כַּאן יְנַאדִיהַא לֵילַא אִל-חַמרַא.", - "hebrew": "ולכן כולם קראו לה לילא האדומה.", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | i l k u l l | k aː n | j n aː d iː h a | l eː l a | i l ħ a m r a" - }, - { - "voice_time_seconds": { - "start": 39.2, - "end": 42.6 - }, - "arabic": "في يوم من الأيّام، إمّ ليلى الحمرا", - "hebrew_transliteration": "פִי יוֹם מִן אִל-אַיַّאם, אִםّ לֵילַא אִל-חַמרַא", - "hebrew": "יום אחד, אמא של לילא האדומה", - "expected_phonemes": "f i | j oː m | m i n | i l ʔ a j j aː m | ʔ i m m | l eː l a | i l ħ a m r a" - }, - { - "voice_time_seconds": { - "start": 42.6, - "end": 44.8 - }, - "arabic": "طلبت منها تروح عند ستّها", - "hebrew_transliteration": "טַלְבַּת מִנְהַא תְרוּח עִנד סִתַّהַא", - "hebrew": "ביקשה ממנה ללכת אל סבתה", - "expected_phonemes": "tˤ a l b a t | m i n h a | t r uː ħ | ʕ i n d | s i t t a h a" - }, - { - "voice_time_seconds": { - "start": 44.8, - "end": 48.8 - }, - "arabic": "وتوديلها سلّة فيها شويّة أكل وأغراض", - "hebrew_transliteration": "וּתוַדִّי-לְהַא סַלֶّה פִיהַא שְוַיֶּת אַכֶּל וּאַעְ'רַאד'", - "hebrew": "ולהביא לה סל ובו קצת אוכל ודברים", - "expected_phonemes": "u t w a d d iː l h a | s a l l e | f iː h a | ʃ w a j e t | ʔ a k e l | u ʔ a ɣ r aː dˤ" - }, - { - "voice_time_seconds": { - "start": 48.8, - "end": 51.4 - }, - "arabic": "عشان كانت مريضة.", - "hebrew_transliteration": "עַשַאן כַּאנַת מַרִידַ'ה.", - "hebrew": "כי היא הייתה חולה.", - "expected_phonemes": "ʕ a ʃ aː n | k aː n a t | m a r iː dˤ a" - }, - { - "voice_time_seconds": { - "start": 51.4, - "end": 56.0 - }, - "arabic": "ليلى إنبسطت كتير عشان راح تروح عند ستّها.", - "hebrew_transliteration": "לֵילַא אִנבַּסטַת כְּתִיר עַשַאן רַאחַת תְרוּח עִנד סִתַّהַא.", - "hebrew": "לילא שמחה מאוד כי היא תלך אל סבתה.", - "expected_phonemes": "l eː l a | ʔ i n b a s tˤ a t | k t iː r | ʕ a ʃ aː n | r aː ħ a t | t r uː ħ | ʕ i n d | s i t t a h a" - }, - { - "voice_time_seconds": { - "start": 56.0, - "end": 60.4 - }, - "arabic": "أخدت السّلّة من إمّها وأجت بدّها تطلع.", - "hebrew_transliteration": "אַחַ'דַת אִ-סַּלֶّה מִן אִמّהַא וּאַגַ'ת בַּדַّהַא תִטלַע.", - "hebrew": "לקחה את הסל מאמה ועמדה לצאת.", - "expected_phonemes": "ʔ a x a d a t | ʔ i s a l l e | m i n | ʔ i m m h a | u ʔ a dʒ a t | b a d d a h a | t i tˤ l a ʕ" - }, - { - "voice_time_seconds": { - "start": 60.4, - "end": 63.2 - }, - "arabic": "إمّها وقّفتها ونبّهتها.", - "hebrew_transliteration": "אִמַّהַא וַקַّפַתְהַא וּנַבַּהַתְהַא.", - "hebrew": "אמה עצרה אותה והזהירה אותה.", - "expected_phonemes": "ʔ i m m a h a | w a ʔ ʔ a f a t h a | u n a b a h a t h a" - }, - { - "voice_time_seconds": { - "start": 63.2, - "end": 67.8 - }, - "arabic": "قالتلها: «يا ليلى بتروحي لعند ستّك", - "hebrew_transliteration": "קַאלַת-לְהַא: יַא לֵילַא בִּתרוּחִי לַעִנד סִתַّכּ", - "hebrew": "היא אמרה לה: לילא, תלכי אל סבתך", - "expected_phonemes": "ʔ aː l a t l h a | j a | l eː l a | b i t r uː ħ i | l a ʕ i n d | s i t t a k" - }, - { - "voice_time_seconds": { - "start": 67.8, - "end": 69.4 - }, - "arabic": "من الطّريق الطّويلة،", - "hebrew_transliteration": "מִן אִ-טַּרִיק אִ-טַּוִילֶה,", - "hebrew": "בדרך הארוכה,", - "expected_phonemes": "m i n | ʔ i tˤ a r iː ʔ | ʔ i tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 69.4, - "end": 72.2 - }, - "arabic": "ما تروحيش من الطّريق القصيرة.", - "hebrew_transliteration": "מַא תְרוּחִי-ש מִן אִ-טַּרִיק אִל-קַצִירֶה.", - "hebrew": "אל תלכי בדרך הקצרה.", - "expected_phonemes": "m a | t r uː ħ iː ʃ | m i n | ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e" - }, - { - "voice_time_seconds": { - "start": 72.2, - "end": 77.4 - }, - "arabic": "الطّريق القصيرة خطرة وممكن يطلعلك فيها وحش،", - "hebrew_transliteration": "אִ-טַּרִיק אִל-קַצִירֶה חַ'טְרַה וּמֻמכֶּן יִטלַע-לַכּ פִיהַא וַחֶש,", - "hebrew": "הדרך הקצרה מסוכנת ועלולה לצוץ לך בה חיית פרא,", - "expected_phonemes": "ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e | x a tˤ r a | u m u m k e n | j i tˤ l a ʕ l a k | f iː h a | w a ħ e ʃ" - }, - { - "voice_time_seconds": { - "start": 77.4, - "end": 80.8 - }, - "arabic": "إمشي بسّ من الطّريق الطّويلة.»", - "hebrew_transliteration": "אִמשִי בַּס מִן אִ-טַּרִיק אִ-טַּוִילֶה.", - "hebrew": "לכי רק בדרך הארוכה.", - "expected_phonemes": "ʔ i m ʃ i | b a s | m i n | ʔ i tˤ a r iː ʔ | ʔ i tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 80.8, - "end": 83.0 - }, - "arabic": "ليلى قالت لإمّها:", - "hebrew_transliteration": "לֵילַא קַאלַת לַאִמַّהַא:", - "hebrew": "לילא אמרה לאמה:", - "expected_phonemes": "l eː l a | ʔ aː l a t | l a ʔ i m m a h a" - }, - { - "voice_time_seconds": { - "start": 83.0, - "end": 86.8 - }, - "arabic": "«طيّب ماما، أنا راح أمشي من الطّريق الطّويلة", - "hebrew_transliteration": "טַיֶّבּ מַאמַא, אַנַא רַאח אַמשִי מִן אִ-טַּרִיק אִ-טַּוִילֶה", - "hebrew": "טוב אמא, אני אלך בדרך הארוכה", - "expected_phonemes": "tˤ a j j e b | m aː m a | ʔ a n a | r aː ħ | ʔ a m ʃ i | m i n | ʔ i tˤ a r iː ʔ | ʔ i tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 86.8, - "end": 88.6 - }, - "arabic": "مش من الطّريق القصيرة،", - "hebrew_transliteration": "מִש מִן אִ-טַּרִיק אִל-קַצִירֶה,", - "hebrew": "לא בדרך הקצרה,", - "expected_phonemes": "m i ʃ | m i n | ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e" - }, - { - "voice_time_seconds": { - "start": 88.6, - "end": 93.0 - }, - "arabic": "وراح أروح عند تيتا أزورها وأتطمّن عليها.»", - "hebrew_transliteration": "וּרַאח אַרוּח עִנד תֵיתַא אַזוּרְהַא וּאַתטַמַّן עַלֵיהַא.", - "hebrew": "ואלך אל סבתא לבקר אותה ולוודא שהיא בסדר.", - "expected_phonemes": "u r aː ħ | ʔ a r uː ħ | ʕ i n d | t eː t a | ʔ a z uː r h a | u ʔ a t tˤ a m m a n | ʕ a l eː h a" - }, - { - "voice_time_seconds": { - "start": 93.0, - "end": 97.0 - }, - "arabic": "ليلى طلعت من البيت، وبلّشت تمشي،", - "hebrew_transliteration": "לֵילַא טִלעַת מִן אִל-בֵּית, וּבַּלַّשַת תִמשִי,", - "hebrew": "לילא יצאה מהבית, והתחילה ללכת,", - "expected_phonemes": "l eː l a | tˤ i l ʕ a t | m i n | i l b eː t | u b a l l a ʃ a t | t i m ʃ i" - }, - { - "voice_time_seconds": { - "start": 97.0, - "end": 102.0 - }, - "arabic": "وصلت لمفرق، على اليمين كانت الطّريق القصيرة،", - "hebrew_transliteration": "וִצלַת לַמַפרַק, עַלַא אִל-יַמִין כַּאנַת אִ-טַּרִיק אִל-קַצִירֶה,", - "hebrew": "הגיעה לצומת, מימין הייתה הדרך הקצרה,", - "expected_phonemes": "w i sˤ l a t | l a m a f r a ʔ | ʕ a l a | i l j a m iː n | k aː n a t | ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e" - }, - { - "voice_time_seconds": { - "start": 102.0, - "end": 105.6 - }, - "arabic": "وعلى الشمال كانت الطّريق الطّويلة.", - "hebrew_transliteration": "וּעַלַא לֶ-שְמַאל כַּאנַת אִ-טַּרִיק אִ-טַּוִילֶה.", - "hebrew": "ומשמאל הייתה הדרך הארוכה.", - "expected_phonemes": "u ʕ a l a | l e ʃ m aː l | k aː n a t | ʔ i tˤ a r iː ʔ | ʔ i tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 105.6, - "end": 109.0 - }, - "arabic": "وقّفت ليلى وفكّرت بقلبها:", - "hebrew_transliteration": "וַקַّפַת לֵילַא וּפַכַּרַת בְּקַלבְּהַא:", - "hebrew": "לילא עצרה וחשבה בליבה:", - "expected_phonemes": "w a ʔ ʔ a f a t | l eː l a | u f a k a r a t | b ʔ a l b h a" - }, - { - "voice_time_seconds": { - "start": 109.0, - "end": 111.2 - }, - "arabic": "«إذا بروح من الطّريق القصيرة", - "hebrew_transliteration": "אִזַא בַּרוּח מִן אִ-טַּרִיק אִל-קַצִירֶה", - "hebrew": "אם אלך בדרך הקצרה", - "expected_phonemes": "ʔ i z a | b a r uː ħ | m i n | ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e" - }, - { - "voice_time_seconds": { - "start": 111.2, - "end": 115.8 - }, - "arabic": "راح أكسب وقت وراح أقضّي وقت كتير عند تيتا،", - "hebrew_transliteration": "רַאח אַכּסַבּ וַקת וּרַאח אַקַדִّי וַקת כְּתִיר עִנד תֵיתַא,", - "hebrew": "ארוויח זמן ואבלה הרבה זמן אצל סבתא,", - "expected_phonemes": "r aː ħ | ʔ a k s a b | w a ʔ t | u r aː ħ | ʔ a ʔ a d d i | w a ʔ t | k t iː r | ʕ i n d | t eː t a" - }, - { - "voice_time_seconds": { - "start": 115.8, - "end": 119.0 - }, - "arabic": "أمّا إذا بروح من الطّريق الطّويلة", - "hebrew_transliteration": "אַמַّא אִזַא בַּרוּח מִן אִ-טַּרִיק אִ-טַּוִילֶה", - "hebrew": "ואילו אם אלך בדרך הארוכה", - "expected_phonemes": "ʔ a m m a | ʔ i z a | b a r uː ħ | m i n | ʔ i tˤ a r iː ʔ | ʔ i tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 119.0, - "end": 123.4 - }, - "arabic": "راح تعتّم الدّنيا بسرعة ومش راح ألحق ألعب.", - "hebrew_transliteration": "רַאחַת תְעַתֶّם אִ-דֻּנְיַא בְּסֻרעַה וּמִש רַאח אַלחַק אַלעַבּ.", - "hebrew": "יחשיך מהר ולא אספיק לשחק.", - "expected_phonemes": "r aː ħ a t | t ʕ a t t e m | ʔ i d u n j a | b s u r ʕ a | u m i ʃ | r aː ħ | i l ħ a ʔ | ʔ a l ʕ a b" - }, - { - "voice_time_seconds": { - "start": 123.4, - "end": 126.8 - }, - "arabic": "خلص بدّي أروح من الطّريق القصيرة...", - "hebrew_transliteration": "חַ'לַץ בַּדִّי אַרוּח מִן אִ-טַּרִיק אִל-קַצִירֶה...", - "hebrew": "זהו, אני רוצה ללכת בדרך הקצרה...", - "expected_phonemes": "x a l a sˤ | b a d d i | ʔ a r uː ħ | m i n | ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e" - }, - { - "voice_time_seconds": { - "start": 126.8, - "end": 129.4 - }, - "arabic": "وماما مش راح تعرف...", - "hebrew_transliteration": "וּמַאמַא מִש רַאח תִערַף...", - "hebrew": "ואמא לא תדע...", - "expected_phonemes": "u m aː m a | m i ʃ | r aː ħ | t i ʕ r a f" - }, - { - "voice_time_seconds": { - "start": 129.4, - "end": 131.8 - }, - "arabic": "بعدين أنا هاي مش أوّل مرّة", - "hebrew_transliteration": "בַּעדֵין, אַנַא הַאי מִש אַוַّל מַרַّה", - "hebrew": "וחוץ מזה, זו לא הפעם הראשונה", - "expected_phonemes": "b a ʕ d eː n | ʔ a n a | h aː i | m i ʃ | ʔ a w w a l | m a r r a" - }, - { - "voice_time_seconds": { - "start": 131.8, - "end": 133.8 - }, - "arabic": "بمشي من الطّريق القصيرة،", - "hebrew_transliteration": "בַּמשִי מִן אִ-טַּרִיק אִל-קַצִירֶה,", - "hebrew": "שאני הולכת בדרך הקצרה,", - "expected_phonemes": "b a m ʃ i | m i n | ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e" - }, - { - "voice_time_seconds": { - "start": 133.8, - "end": 136.6 - }, - "arabic": "وولا مرّة طلعولي وحوش.»", - "hebrew_transliteration": "וּוַלַא מַרַّה טִלעוּ-לִי וְחוּש.", - "hebrew": "ואף פעם לא צצו לי חיות פרא.", - "expected_phonemes": "u w a l a | m a r r a | tˤ i l ʕ uː l i | u ħ uː ʃ" - }, - { - "voice_time_seconds": { - "start": 136.6, - "end": 140.8 - }, - "arabic": "وهيك ليلى بلّشت تمشي في الطّريق القصيرة.", - "hebrew_transliteration": "וּהֵיכּ, לֵילַא בַּלַّשַת תִמשִי פִי אִ-טַּרִיק אִל-קַצִירֶה.", - "hebrew": "וכך, לילא התחילה ללכת בדרך הקצרה.", - "expected_phonemes": "u h eː k | l eː l a | b a l l a ʃ a t | t i m ʃ i | f i | ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e" - }, - { - "voice_time_seconds": { - "start": 140.8, - "end": 145.2 - }, - "arabic": "وهيّ ماشية، فجأة طلعلها الدّيب،", - "hebrew_transliteration": "וּהִיֶּ מַאשְיֶה, פַגְ'אַה טִלֶע-לְהַא אִ-דִّיבּ,", - "hebrew": "ובזמן שהלכה, פתאום הופיע לה הזאב,", - "expected_phonemes": "u h i j e | m aː ʃ j e | f a dʒ ʔ a | tˤ i l e ʕ l h a | ʔ i d d iː b" - }, - { - "voice_time_seconds": { - "start": 145.2, - "end": 148.2 - }, - "arabic": "بلّش ينطّ حدّها ويمشي معها،", - "hebrew_transliteration": "בַּלַّש יְנֻטّ חַדّהַא וִימשִי מַעַהַא,", - "hebrew": "התחיל לקפץ לידה וללכת איתה,", - "expected_phonemes": "b a l l a ʃ | j n u tˤ tˤ | ħ a d d h a | w iː m ʃ i | m a ʕ a h a" - }, - { - "voice_time_seconds": { - "start": 148.2, - "end": 153.2 - }, - "arabic": "وسألها: «ليلى يا ليلى لوين رايحة؟»", - "hebrew_transliteration": "וּסַאַלְהַא: לֵילַא יַא לֵילַא לַוֵין רַאיְחַה?", - "hebrew": "ושאל אותה: לילא, לילא, לאן את הולכת?", - "expected_phonemes": "u s a ʔ a l h a | l eː l a | j a | l eː l a | l a w eː n | r aː j ħ a" - }, - { - "voice_time_seconds": { - "start": 153.2, - "end": 156.8 - }, - "arabic": "قالتله: «أنا رايحة لعند التّيتا،", - "hebrew_transliteration": "קַאלַת-לוֹ: אַנַא רַאיְחַה לַעִנד אִ-תֵּיתַא,", - "hebrew": "היא אמרה לו: אני הולכת אל סבתא,", - "expected_phonemes": "ʔ aː l a t l o | ʔ a n a | r aː j ħ a | l a ʕ i n d | ʔ i t eː t a" - }, - { - "voice_time_seconds": { - "start": 156.8, - "end": 161.8 - }, - "arabic": "هيّ مريضة وماما بعتتلها معي شويّة أكل.»", - "hebrew_transliteration": "הִיֶּ מַרִידַ'ה וּמַאמַא בַּעַתַת-לְהַא מַעִי שְוַיֶּת אַכֶּל.", - "hebrew": "היא חולה ואמא שלחה לה איתי קצת אוכל.", - "expected_phonemes": "h i j e | m a r iː dˤ a | u m aː m a | b a ʕ a t a t l h a | m a ʕ i | ʃ w a j e t | ʔ a k e l" - }, - { - "voice_time_seconds": { - "start": 161.8, - "end": 167.2 - }, - "arabic": "الدّيب قال: «حرام، مسكينة التّيتا... سلامتها.»", - "hebrew_transliteration": "אִ-דִّיבּ קַאל: חַרַאם, מַסכִּינֶה אִ-תֵּיתַא... סַלַאמִתְהַא.", - "hebrew": "הזאב אמר: חבל, סבתא המסכנה... שתהיה בריאה.", - "expected_phonemes": "ʔ i d d iː b | ʔ aː l | ħ a r aː m | m a s k iː n e | ʔ i t eː t a | s a l aː m i t h a" - }, - { - "voice_time_seconds": { - "start": 167.2, - "end": 172.2 - }, - "arabic": "ليلى ردّت وقالت: «الله يسلّمك، يالّا باي.»", - "hebrew_transliteration": "לֵילַא רַדַّת וּקַאלַת: אַללַה יְסַלְّמַכּ, יַאלַّא בַּאי.", - "hebrew": "לילא ענתה ואמרה: תודה, יאללה ביי.", - "expected_phonemes": "l eː l a | r a d d a t | u ʔ aː l a t | i l l a | j s a l l m a k | j aː l l a | b aː i" - }, - { - "voice_time_seconds": { - "start": 172.2, - "end": 175.6 - }, - "arabic": "الدّيب ترك ليلى وأبعد عنها.", - "hebrew_transliteration": "אִ-דִّיבּ תַרַכּ לֵילַא וּאִבּעַד עַנְהַא.", - "hebrew": "הזאב עזב את לילא והתרחק ממנה.", - "expected_phonemes": "ʔ i d d iː b | t a r a k | l eː l a | u ʔ i b ʕ a d | ʕ a n h a" - }, - { - "voice_time_seconds": { - "start": 175.6, - "end": 179.2 - }, - "arabic": "وبسرعة فكّر بخطّة خبيثة.", - "hebrew_transliteration": "וּבְּסֻרעַה פַכַּר בְּחֻ'טַّה חַ'בִּיתֶה.", - "hebrew": "ובמהירות חשב על תוכנית מרושעת.", - "expected_phonemes": "u b s u r ʕ a | f a k a r | b x u tˤ tˤ a | x a b iː t e" - }, - { - "voice_time_seconds": { - "start": 179.2, - "end": 183.6 - }, - "arabic": "قال في نفسه: «لازم أسبق ليلى لعند ستّها،", - "hebrew_transliteration": "וּקַאל פִי נַפסוֹ: לַאזֶם אַסבֻּק לֵילַא לַעִנד סִתַّהַא,", - "hebrew": "ואמר בליבו: אני חייב להקדים את לילא אל סבתה,", - "expected_phonemes": "u ʔ aː l | f i | n a f s o | l aː z e m | ʔ a s b u ʔ | l eː l a | l a ʕ i n d | s i t t a h a" - }, - { - "voice_time_seconds": { - "start": 183.6, - "end": 187.4 - }, - "arabic": "آكلها وأستنّى ليلى تنها تيجي.»", - "hebrew_transliteration": "אַאכֻּלְהַא וּאַסתַנַّא לֵילַא תַנְהַא תִיגִ'י.", - "hebrew": "לאכול אותה ולחכות ללילא עד שתבוא.", - "expected_phonemes": "ʔ aː k u l h a | u ʔ a s t a n n a | l eː l a | t a n h a | t iː dʒ i" - }, - { - "voice_time_seconds": { - "start": 187.4, - "end": 190.2 - }, - "arabic": "وبالفعل، هيك صار.", - "hebrew_transliteration": "וּבִּל-פִעֶל, הֵיכּ צַאר.", - "hebrew": "ואכן, כך היה.", - "expected_phonemes": "u b i l f i ʕ e l | h eː k | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 190.2, - "end": 192.8 - }, - "arabic": "الدّيب ركض بسرعة بسرعة،", - "hebrew_transliteration": "אִ-דִّיבּ רַכַד' בְּסֻרעַה בְּסֻרעַה,", - "hebrew": "הזאב רץ מהר מהר,", - "expected_phonemes": "ʔ i d d iː b | r a k a dˤ | b s u r ʕ a | b s u r ʕ a" - }, - { - "voice_time_seconds": { - "start": 192.8, - "end": 196.6 - }, - "arabic": "ووصل لبيت التّيتا، بيت ستّها لليلى.", - "hebrew_transliteration": "וּוִצֶל לַבֵּית אִ-תֵּיתַא, בֵּית סִתַّהַא לַלֵילַא.", - "hebrew": "והגיע לבית של סבתא, בית סבתה של לילא.", - "expected_phonemes": "u w i sˤ e l | l a b eː t | ʔ i t eː t a | b eː t | s i t t a h a | l a l eː l a" - }, - { - "voice_time_seconds": { - "start": 196.6, - "end": 198.6 - }, - "arabic": "دقّ على الباب...", - "hebrew_transliteration": "דַקّ עַלַא אִל-בַּאבּ...", - "hebrew": "דפק על הדלת...", - "expected_phonemes": "d a ʔ ʔ | ʕ a l a | i l b aː b" - }, - { - "voice_time_seconds": { - "start": 198.6, - "end": 201.2 - }, - "arabic": "السّتّ سألت: «مين؟»", - "hebrew_transliteration": "אִ-סִّתّ סַאַלַת: מִין?", - "hebrew": "הסבתא שאלה: מי?", - "expected_phonemes": "ʔ i s s i t t | s a ʔ a l a t | m iː n" - }, - { - "voice_time_seconds": { - "start": 201.2, - "end": 205.0 - }, - "arabic": "الدّيب قال: «تيتا هاي أنا، ليلى.", - "hebrew_transliteration": "אִ-דִّיבּ קַאל: תֵיתַא, הַאי אַנַא, לֵילַא.", - "hebrew": "הזאב אמר: סבתא, זו אני, לילא.", - "expected_phonemes": "ʔ i d d iː b | ʔ aː l | t eː t a | h aː i | ʔ a n a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 205.0, - "end": 207.4 - }, - "arabic": "ماما بعتتني لعندك.»", - "hebrew_transliteration": "מַאמַא בַּעַתַתנִי לַעִנדֶכּ.", - "hebrew": "אמא שלחה אותי אלייך.", - "expected_phonemes": "m aː m a | b a ʕ a t a t n i | l a ʕ i n d e k" - }, - { - "voice_time_seconds": { - "start": 207.4, - "end": 211.0 - }, - "arabic": "التّيتا قالت: «ليلى حبيبتي فوتي،", - "hebrew_transliteration": "אִ-תֵּיתַא קַאלַת: לֵילַא חַבִּיבְּתִי פוּתִי,", - "hebrew": "הסבתא אמרה: לילא יקירתי, היכנסי,", - "expected_phonemes": "ʔ i t eː t a | ʔ aː l a t | l eː l a | ħ a b iː b t i | f uː t i" - }, - { - "voice_time_seconds": { - "start": 211.0, - "end": 214.4 - }, - "arabic": "تعالي، الباب مفتوح.»", - "hebrew_transliteration": "תַעַאלִי, אִל-בַּאבּ מַפתוּח.", - "hebrew": "בואי, הדלת פתוחה.", - "expected_phonemes": "t a ʕ aː l i | i l b aː b | m a f t uː ħ" - }, - { - "voice_time_seconds": { - "start": 214.4, - "end": 220.2 - }, - "arabic": "الدّيب فات، ومرّة وحدة هجم على التّيتا وأكلها.", - "hebrew_transliteration": "אִ-דִّיבּ פַאת, וּמַרַّה וַחדֶה הַגַ'ם עַלַא אִ-תֵּיתַא וּאַכַּלְהַא.", - "hebrew": "הזאב נכנס, ובבת אחת התנפל על הסבתא ואכל אותה.", - "expected_phonemes": "ʔ i d d iː b | f aː t | u m a r r a | w a ħ d e | h a dʒ a m | ʕ a l a | ʔ i t eː t a | u ʔ a k a l h a" - }, - { - "voice_time_seconds": { - "start": 220.2, - "end": 223.0 - }, - "arabic": "لبس أواعيها، نام على التّخت", - "hebrew_transliteration": "לִבֶּס אַוַאעִיהַא, נַאם עַלַא אִ-תַּחֶת", - "hebrew": "לבש את בגדיה, שכב על המיטה", - "expected_phonemes": "l i b e s | ʔ a w aː ʕ iː h a | n aː m | ʕ a l a | ʔ i t a ħ e t" - }, - { - "voice_time_seconds": { - "start": 223.0, - "end": 225.6 - }, - "arabic": "وصار يستنّى بليلى.", - "hebrew_transliteration": "וּצַאר יִסתַנַّא בְּלֵילַא.", - "hebrew": "והתחיל לחכות ללילא.", - "expected_phonemes": "u sˤ aː r | j i s t a n n a | b l eː l a" - }, - { - "voice_time_seconds": { - "start": 225.6, - "end": 230.8 - }, - "arabic": "بعد شوي، وصلت ليلى. دقّت على الباب وقالت:", - "hebrew_transliteration": "בַּעֶד שְוַי, וִצלַת לֵילַא. דַקַّת עַלַא אִל-בַּאבּ וּקַאלַת:", - "hebrew": "אחרי זמן קצר, לילא הגיעה. דפקה על הדלת ואמרה:", - "expected_phonemes": "b a ʕ e d | ʃ w a j | w i sˤ l a t | l eː l a | d a ʔ ʔ a t | ʕ a l a | i l b aː b | u ʔ aː l a t" - }, - { - "voice_time_seconds": { - "start": 230.8, - "end": 234.4 - }, - "arabic": "«تيتا إفتحيلي، هاي أنا ليلى.»", - "hebrew_transliteration": "תֵיתַא אִפתַחִי-לִי, הַאי אַנַא, לֵילַא.", - "hebrew": "סבתא, תפתחי לי, זו אני, לילא.", - "expected_phonemes": "t eː t a | ʔ i f t a ħ iː l i | h aː i | ʔ a n a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 234.4, - "end": 237.6 - }, - "arabic": "الدّيب غيّر بصوته وقال بنعومة:", - "hebrew_transliteration": "אִ-דִّיבּ עַ'יַّר בְּצוֹתוֹ וּקַאל בְּנֻעוּמֶה:", - "hebrew": "הזאב שינה את קולו ואמר ברכות:", - "expected_phonemes": "ʔ i d d iː b | ɣ a j j a r | b sˤ oː t o | u ʔ aː l | b n u ʕ uː m e" - }, - { - "voice_time_seconds": { - "start": 237.6, - "end": 242.6 - }, - "arabic": "«ليلى فوتي، أنا بالتّخت مريضة بقدرش أقوم.»", - "hebrew_transliteration": "לֵילַא פוּתִי, אַנַא בִּ-תַּחֶת מַרִידַ'ה בַּקדַרֶש אַקוּם.", - "hebrew": "לילא, היכנסי, אני במיטה חולה ולא יכולה לקום.", - "expected_phonemes": "l eː l a | f uː t i | ʔ a n a | b i t a ħ e t | m a r iː dˤ a | b a ʔ d a r e ʃ | ʔ a ʔ uː m" - }, - { - "voice_time_seconds": { - "start": 242.6, - "end": 247.8 - }, - "arabic": "فاتت ليلى ومشيت لعند التّخت وقالت:", - "hebrew_transliteration": "פַאתַת לֵילַא וּמִשיַת לַעִנד אִ-תַּחֶת וּקַאלַת:", - "hebrew": "לילא נכנסה, הלכה אל המיטה ואמרה:", - "expected_phonemes": "f aː t a t | l eː l a | u m i ʃ j a t | l a ʕ i n d | ʔ i t a ħ e t | u ʔ aː l a t" - }, - { - "voice_time_seconds": { - "start": 247.8, - "end": 251.0 - }, - "arabic": "«تيتا يا تيتا، بدّي أسألك سؤال.»", - "hebrew_transliteration": "תֵיתַא יַא תֵיתַא, בַּדִّי אַסאַלֶכּ סֻאַאל.", - "hebrew": "סבתא, סבתא, אני רוצה לשאול אותך שאלה.", - "expected_phonemes": "t eː t a | j a | t eː t a | b a d d i | ʔ a s ʔ a l e k | s u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 251.0, - "end": 254.0 - }, - "arabic": "الدّيب قال: «تفضّلي يا ليلى.»", - "hebrew_transliteration": "אִ-דִّיבּ קַאל: תְפַדַّלִי יַא לֵילַא.", - "hebrew": "הזאב אמר: בבקשה, לילא.", - "expected_phonemes": "ʔ i d d iː b | ʔ aː l | t f a d d a l i | j a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 254.0, - "end": 259.2 - }, - "arabic": "ليلى قالت: «تيتا يا تيتا، ليش عيونك كبار؟»", - "hebrew_transliteration": "לֵילַא קַאלַת: תֵיתַא יַא תֵיתַא, לֵיש עְיוּנֶכּ כְּבַּאר?", - "hebrew": "לילא אמרה: סבתא, סבתא, למה העיניים שלך גדולות?", - "expected_phonemes": "l eː l a | ʔ aː l a t | t eː t a | j a | t eː t a | l eː ʃ | ʕ j uː n e k | k b aː r" - }, - { - "voice_time_seconds": { - "start": 259.2, - "end": 262.4 - }, - "arabic": "«عشان أشوفك منيح يا ليلى.»", - "hebrew_transliteration": "עַשַאן אַשוּפֶכּ מְנִיח יַא לֵילַא.", - "hebrew": "כדי לראות אותך טוב, לילא.", - "expected_phonemes": "ʕ a ʃ aː n | ʔ a ʃ uː f e k | m n iː ħ | j a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 262.4, - "end": 264.4 - }, - "arabic": "«وليش دينيكي كبار؟»", - "hebrew_transliteration": "וּלֵיש דִינֵיכִּי כְּבַּאר?", - "hebrew": "ולמה האוזניים שלך גדולות?", - "expected_phonemes": "u l eː ʃ | d iː n eː k i | k b aː r" - }, - { - "voice_time_seconds": { - "start": 264.4, - "end": 267.4 - }, - "arabic": "«عشان أسمعك منيح يا ليلى.»", - "hebrew_transliteration": "עַשַאן אַסמַעֶכּ מְנִיח יַא לֵילַא.", - "hebrew": "כדי לשמוע אותך טוב, לילא.", - "expected_phonemes": "ʕ a ʃ aː n | ʔ a s m a ʕ e k | m n iː ħ | j a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 267.4, - "end": 269.6 - }, - "arabic": "«طب وليش إيديكي كبار؟»", - "hebrew_transliteration": "טַבּ וּלֵיש אִידֵיכִּי כְּבַּאר?", - "hebrew": "טוב, ולמה הידיים שלך גדולות?", - "expected_phonemes": "tˤ a b | u l eː ʃ | ʔ iː d eː k i | k b aː r" - }, - { - "voice_time_seconds": { - "start": 269.6, - "end": 272.4 - }, - "arabic": "«عشان أعبطك منيح يا ليلى.»", - "hebrew_transliteration": "עַשַאן אַעבֻּטֶכּ מְנִיח יַא לֵילַא.", - "hebrew": "כדי לחבק אותך טוב, לילא.", - "expected_phonemes": "ʕ a ʃ aː n | ʔ a ʕ b u tˤ e k | m n iː ħ | j a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 272.4, - "end": 276.2 - }, - "arabic": "«طيّب، تمّك... تمّك يا تيتا ليش كبير؟»", - "hebrew_transliteration": "טַיֶّבּ, תֻמֶّכּ... תֻמֶّכּ יַא תֵיתַא לֵיש כְּבִּיר?", - "hebrew": "טוב, הפה שלך... הפה שלך, סבתא, למה הוא גדול?", - "expected_phonemes": "tˤ a j j e b | t u m m e k | t u m m e k | j a | t eː t a | l eː ʃ | k b iː r" - }, - { - "voice_time_seconds": { - "start": 276.2, - "end": 278.6 - }, - "arabic": "«عشان آكلك بسرعة يا ليلى!!»", - "hebrew_transliteration": "עַשַאן אַאכֻּלֶכּ בְּסֻרעַה יַא לֵילַא!", - "hebrew": "כדי לאכול אותך מהר, לילא!", - "expected_phonemes": "ʕ a ʃ aː n | ʔ aː k u l e k | b s u r ʕ a | j a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 278.6, - "end": 281.2 - }, - "arabic": "ومرّة وحدة، قام الدّيب من التّخت،", - "hebrew_transliteration": "וּמַרַّה וַחדֶה, קַאם אִ-דִّיבּ מִן אִ-תַּחֶת,", - "hebrew": "ובבת אחת, הזאב קם מהמיטה,", - "expected_phonemes": "u m a r r a | w a ħ d e | ʔ aː m | ʔ i d d iː b | m i n | ʔ i t a ħ e t" - }, - { - "voice_time_seconds": { - "start": 281.2, - "end": 283.4 - }, - "arabic": "وصار يهجم على ليلى...", - "hebrew_transliteration": "וּצַאר יִהגֻ'ם עַלַא לֵילַא...", - "hebrew": "והתחיל להתנפל על לילא...", - "expected_phonemes": "u sˤ aː r | j i h dʒ u m | ʕ a l a | l eː l a" - }, - { - "voice_time_seconds": { - "start": 283.4, - "end": 286.2 - }, - "arabic": "ليلى صارت تصرخ بصوت عالي،", - "hebrew_transliteration": "לֵילַא צַארַת תֻצרֻחַ' בְּצוֹת עַאלִי,", - "hebrew": "לילא התחילה לצעוק בקול רם,", - "expected_phonemes": "l eː l a | sˤ aː r a t | t u sˤ r u x a | b sˤ oː t | ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 286.2, - "end": 288.4 - }, - "arabic": "وتنادي على حدا يساعدها.", - "hebrew_transliteration": "וּתנַאדִי עַלַא חַדַא יְסַאעֶדְהַא.", - "hebrew": "ולקרוא למישהו שיעזור לה.", - "expected_phonemes": "u t n aː d i | ʕ a l a | ħ a d a | j s aː ʕ e d h a" - }, - { - "voice_time_seconds": { - "start": 288.4, - "end": 291.8 - }, - "arabic": "الصّيّاد سمع صراخها من بعيد", - "hebrew_transliteration": "אִ-צַّיַّאד סִמֶע צֻרַאחְ'הַא מִן בְּעִיד", - "hebrew": "הצייד שמע את צעקותיה מרחוק", - "expected_phonemes": "ʔ i sˤ sˤ a j j aː d | s i m e ʕ | sˤ u r aː x h a | m i n | b ʕ iː d" - }, - { - "voice_time_seconds": { - "start": 291.8, - "end": 296.8 - }, - "arabic": "وركض عشان ينجّيها، بسّ ما لحقش.", - "hebrew_transliteration": "וּרַכַד' עַשַאן יְנַגִّ'יהַא, בַּס מַא לִחֶק-ש.", - "hebrew": "ורץ כדי להציל אותה, אבל לא הספיק.", - "expected_phonemes": "u r a k a dˤ | ʕ a ʃ aː n | j n a dʒ dʒ iː h a | b a s | m a | l i ħ e ʔ ʃ" - }, - { - "voice_time_seconds": { - "start": 296.8, - "end": 299.8 - }, - "arabic": "الدّيب أكلها لليلى.", - "hebrew_transliteration": "אִ-דִّיבּ אַכַּלְהַא לַלֵילַא.", - "hebrew": "הזאב אכל את לילא.", - "expected_phonemes": "ʔ i d d iː b | ʔ a k a l h a | l a l eː l a" - }, - { - "voice_time_seconds": { - "start": 299.8, - "end": 304.2 - }, - "arabic": "لمّا وصل الصّيّاد شاف الدّيب بطنه كبير", - "hebrew_transliteration": "לַמַّא וִצֶל אִ-צַّיַّאד שַאף אִ-דִّיבּ בַּטנוֹ כְּבִּיר", - "hebrew": "כשהצייד הגיע ראה שבטנו של הזאב גדולה", - "expected_phonemes": "l a m m a | w i sˤ e l | ʔ i sˤ sˤ a j j aː d | ʃ aː f | ʔ i d d iː b | b a tˤ n o | k b iː r" - }, - { - "voice_time_seconds": { - "start": 304.2, - "end": 306.6 - }, - "arabic": "ومش قادر يمشي من التّقل.", - "hebrew_transliteration": "וּמִש קַאדֶר יִמשִי מִן אִ-תִּקֶל.", - "hebrew": "ושהוא לא מסוגל ללכת מרוב כובד.", - "expected_phonemes": "u m i ʃ | ʔ aː d e r | j i m ʃ i | m i n | ʔ i t i ʔ e l" - }, - { - "voice_time_seconds": { - "start": 306.6, - "end": 311.4 - }, - "arabic": "قام الصّيّاد بسرعة رفع بارودته وطخّه...", - "hebrew_transliteration": "קַאם אִ-צַّיַّאד בְּסֻרעַה רַפַע בַּארוּדתוֹ וּטַחַّוֹ...", - "hebrew": "הצייד מיהר, הרים את רובהו וירה בו...", - "expected_phonemes": "ʔ aː m | ʔ i sˤ sˤ a j j aː d | b s u r ʕ a | r a f a ʕ | b aː r uː d t o | u tˤ a ħ ħ a w o" - }, - { - "voice_time_seconds": { - "start": 311.4, - "end": 313.6 - }, - "arabic": "الدّيب وقع على الأرض.", - "hebrew_transliteration": "אִ-דִّיבּ וִקֶע עַלַא אִל-אַרְד'.", - "hebrew": "הזאב נפל על האדמה.", - "expected_phonemes": "ʔ i d d iː b | w i ʔ e ʕ | ʕ a l a | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 313.6, - "end": 317.8 - }, - "arabic": "والصّيّاد جاب سكّينته وفتح بطنه للدّيب.", - "hebrew_transliteration": "וּ-צַّיַّאד גַ'אבּ סִכִּינתוֹ וּפַתַח בַּטנוֹ לַ-דִּיבּ.", - "hebrew": "והצייד הביא את סכינו ופתח את בטנו של הזאב.", - "expected_phonemes": "u sˤ sˤ a j j aː d | dʒ aː b | s i k iː n t o | u f a t a ħ | b a tˤ n o | l a d iː b" - }, - { - "voice_time_seconds": { - "start": 317.8, - "end": 322.2 - }, - "arabic": "وطلّع ليلى وستها إلّي كانوا في بطنه للدّيب.", - "hebrew_transliteration": "וּטַלַّע לֵילַא וּסִתַّהַא אִלִّי כַּאנוּ פִי בַּטנוֹ לַ-דִּיבּ.", - "hebrew": "והוציא את לילא ואת סבתה שהיו בבטנו של הזאב.", - "expected_phonemes": "u tˤ a l l a ʕ | l eː l a | u s i t t a h a | ʔ i l l i | k aː n u | f i | b a tˤ n o | l a d iː b" - }, - { - "voice_time_seconds": { - "start": 322.2, - "end": 325.4 - }, - "arabic": "بعدين، راح جاب حجار كبيرة", - "hebrew_transliteration": "בַּעדֵין, רַאח גַ'אבּ חְגַ'אר כְּבִּירֶה", - "hebrew": "אחר כך, הלך והביא אבנים גדולות", - "expected_phonemes": "b a ʕ d eː n | r aː ħ | dʒ aː b | ħ dʒ aː r | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 325.4, - "end": 329.2 - }, - "arabic": "وعبّى بطنه للدّيب وخيّطله إيّاه.", - "hebrew_transliteration": "וּעַבַّא בַּטנוֹ לַ-דִּיבּ וּחַיַّט-לוֹ אִיַّאה.", - "hebrew": "ומילא את בטנו של הזאב ותפר לו אותה.", - "expected_phonemes": "u ʕ a b b a | b a tˤ n o | l a d iː b | u ħ a j j a tˤ l o | ʔ i j j aː h" - }, - { - "voice_time_seconds": { - "start": 329.2, - "end": 334.0 - }, - "arabic": "وهوّ وليلى والتّيتا حملوا الدّيب سوا", - "hebrew_transliteration": "וּהֻוֶّ וּלֵילַא וּ-תֵּיתַא, חַמַלוּ אִ-דִّיבּ סַוַא", - "hebrew": "והוא, לילא וסבתא סחבו את הזאב יחד", - "expected_phonemes": "u h u w w e | u l eː l a | u t eː t a | ħ a m a l u | ʔ i d d iː b | s a w a" - }, - { - "voice_time_seconds": { - "start": 334.0, - "end": 338.0 - }, - "arabic": "ورموه في النّهر وخلصوا من شرّه.", - "hebrew_transliteration": "וּרַמוּה פִי אִ-נַּהֶר וּחִ'לצוּ מִן שַרّוֹ.", - "hebrew": "וזרקו אותו לנהר ונפטרו מרשעותו.", - "expected_phonemes": "u r a m uː h | f i | ʔ i n a h e r | u x i l sˤ u | m i n | ʃ a r r o" - }, - { - "voice_time_seconds": { - "start": 338.0, - "end": 340.4 - }, - "arabic": "ليلى روّحت على بيتها", - "hebrew_transliteration": "לֵילַא רַוַّחַת עַלַא בֵּיתְהַא", - "hebrew": "לילא חזרה לביתה", - "expected_phonemes": "l eː l a | r a w w a ħ a t | ʕ a l a | b eː t h a" - }, - { - "voice_time_seconds": { - "start": 340.4, - "end": 344.2 - }, - "arabic": "وتأسّفت لإمّها عشان ما سمعت كلامها", - "hebrew_transliteration": "וּתאַסַّפַת לַאִמַّהַא עַשַאן מַא סִמעַת כַּלַאמְהַא", - "hebrew": "והתנצלה בפני אמה על כך שלא שמעה בקולה", - "expected_phonemes": "u t ʔ a s s a f a t | l a ʔ i m m a h a | ʕ a ʃ aː n | m a | s i m ʕ a t | k a l aː m h a" - }, - { - "voice_time_seconds": { - "start": 344.2, - "end": 346.4 - }, - "arabic": "ومرقت من الطّريق القصيرة.", - "hebrew_transliteration": "וּמַרקַת מִן אִ-טַּרִיק אִל-קַצִירֶה.", - "hebrew": "ועברה בדרך הקצרה.", - "expected_phonemes": "u m a r ʔ a t | m i n | ʔ i tˤ a r iː ʔ | i l ʔ a sˤ iː r e" - }, - { - "voice_time_seconds": { - "start": 346.4, - "end": 352.0 - }, - "arabic": "ووعدت إمّها إنّه من يوم ورايح تسمع كلّ كلامها.", - "hebrew_transliteration": "וּוַעַדַת אִמַّהַא אִנּוֹ מִן יוֹם וּרַאיֶח תִסמַע כֻּלّ כַּלַאמְהַא.", - "hebrew": "והבטיחה לאמה שמהיום והלאה תשמע בקולה תמיד.", - "expected_phonemes": "u w a ʕ a d a t | ʔ i m m a h a | ʔ i n o | m i n | j oː m | u r aː j e ħ | t i s m a ʕ | k u l l | k a l aː m h a" - }, - { - "voice_time_seconds": { - "start": 352.0, - "end": 357.8 - }, - "arabic": "وهيك خلصت قصّتنا، وتوتا توتا خلصت الحدّوتة.", - "hebrew_transliteration": "וּהֵיכּ חִ'לצַת קִצַّתנַא, וּתוּתַא תוּתַא חִ'לצַת אִל-חַדּוּתֶה.", - "hebrew": "וכך נגמר הסיפור שלנו, ותותה תותה, נגמרה המעשייה.", - "expected_phonemes": "u h eː k | x i l sˤ a t | ʔ i sˤ sˤ a t n a | u t uː t a | t uː t a | x i l sˤ a t | i l ħ a d uː t e" - } - ] - }, - { - "file_name": "כבוד הגמל * אִחתִרָאם אִל-גָ'מָל * إحترام الجمل * סיפור בערבית מדוברת [nra9gZ3kmoo].webm", - "youtube_id": "nra9gZ3kmoo", - "title_hebrew": "כבוד הגמל", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "إحترام الجمّل", - "hebrew_transliteration": "אִחתִרַאם אִ-גַّמַל", - "hebrew": "כבוד הגמל", - "expected_phonemes": "ʔ i ħ t i r aː m | ʔ i g g a m a l" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 7.8 - }, - "arabic": "كان ياما كان", - "hebrew_transliteration": "כַּאן יַאמַא כַּאן", - "hebrew": "היה היה", - "expected_phonemes": "k aː n | j aː m a | k aː n" - }, - { - "voice_time_seconds": { - "start": 7.8, - "end": 11.2 - }, - "arabic": "تاجر عنده أعمال تجارة كتيرة.", - "hebrew_transliteration": "תַאגֶ'ר עִנדוֹ אַעמַאל תִגַ'ארַה כְּתִירֶה.", - "hebrew": "סוחר שהיו לו עסקי מסחר רבים.", - "expected_phonemes": "t aː dʒ e r | ʕ i n d o | ʔ a ʕ m aː l | t i dʒ aː r a | k t iː r e" - }, - { - "voice_time_seconds": { - "start": 11.2, - "end": 14.8 - }, - "arabic": "كان يمشّي قوافل جمال محمّلة بضاعة", - "hebrew_transliteration": "כַּאן יְמַשִّי קַוַאפֶל גְ'מַאל מְחַמַّלֶה בְּדַאעַה", - "hebrew": "הוא היה מוביל שיירות גמלים עמוסות סחורה", - "expected_phonemes": "k aː n | j m a ʃ ʃ i | ʔ a w aː f e l | dʒ m aː l | m ħ a m m a l e | b d aː ʕ a" - }, - { - "voice_time_seconds": { - "start": 14.8, - "end": 18.8 - }, - "arabic": "من الشّرق للغرب ومن الشّمال للجّنوب،", - "hebrew_transliteration": "מִן אִ-שַּׁרֶק לַל-עַ'רבּ וּמִן אִ-שַּׁמַאל לַ-גַּנוּבּ,", - "hebrew": "מהמזרח למערב ומהצפון לדרום,", - "expected_phonemes": "m i n | ʔ i ʃ a r e ʔ | l a l ɣ a r b | u m i n | ʔ i ʃ a m aː l | l a g a n uː b" - }, - { - "voice_time_seconds": { - "start": 18.8, - "end": 20.8 - }, - "arabic": "عشان يتاجر.", - "hebrew_transliteration": "עַשַאן יִתַאגֶ'ר.", - "hebrew": "כדי לסחור.", - "expected_phonemes": "ʕ a ʃ aː n | j i t aː dʒ e r" - }, - { - "voice_time_seconds": { - "start": 20.8, - "end": 25.0 - }, - "arabic": "والله أحسن إليه ونجّحه بطريقه.", - "hebrew_transliteration": "וּאַללַה אַחסַן אִלוֹ וּנַגַّחוֹ בְּטַרִיקוֹ.", - "hebrew": "ואלוהים היטיב עמו והצליח את דרכו.", - "expected_phonemes": "u i l l a | ʔ a ħ s a n | i l o | u n a g g a ħ o | b tˤ a r iː ʔ o" - }, - { - "voice_time_seconds": { - "start": 25.0, - "end": 28.8 - }, - "arabic": "كان لهدا الزّلمة أراضي وحيوانات", - "hebrew_transliteration": "כַּאן לַהַאדַא אִ-זַּלַמֶה אַרַאדִי וּחַיַוַאנַאת", - "hebrew": "לאיש הזה היו אדמות ובעלי חיים", - "expected_phonemes": "k aː n | l a h aː d a | ʔ i z a l a m e | ʔ a r aː d i | u ħ a j a w aː n aː t" - }, - { - "voice_time_seconds": { - "start": 28.8, - "end": 30.8 - }, - "arabic": "وولاده الأوفياء كانوا يساعدوه", - "hebrew_transliteration": "וּוְלַאדוֹ אִל-אַוּפִיַאא כַּאנוּ יְסַאעדוּה", - "hebrew": "וילדיו הנאמנים עזרו לו", - "expected_phonemes": "u w l aː d o | i l ʔ a w f i j aː a | k aː n u | j s aː ʕ d uː h" - }, - { - "voice_time_seconds": { - "start": 30.8, - "end": 33.6 - }, - "arabic": "في التّجارة وفي حرث الأرض.", - "hebrew_transliteration": "פִי אִ-תִּגַ'ארַה וּפִי חַרֶת אִל-אַרְד'.", - "hebrew": "במסחר ובחריש האדמה.", - "expected_phonemes": "f i | ʔ i t i dʒ aː r a | u f i | ħ a r e t | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 33.6, - "end": 37.6 - }, - "arabic": "عاش هدا التّاجر عيشة هنيّة غنيّة.", - "hebrew_transliteration": "עַאש הַאדַא אִ-תַּאגֶ'ר עִישֶה הַנִיֶּה עַ'נִיֶّה.", - "hebrew": "הסוחר הזה חי חיים טובים ועשירים.", - "expected_phonemes": "ʕ aː ʃ | h aː d a | ʔ i t aː dʒ e r | ʕ iː ʃ e | h a n i j e | ɣ a n i j j e" - }, - { - "voice_time_seconds": { - "start": 37.6, - "end": 39.8 - }, - "arabic": "كان كريم ودغري", - "hebrew_transliteration": "כַּאן כַּרִים וּדֻעְ'רִי", - "hebrew": "הוא היה נדיב וישר", - "expected_phonemes": "k aː n | k a r iː m | u d u ɣ r i" - }, - { - "voice_time_seconds": { - "start": 39.8, - "end": 43.8 - }, - "arabic": "وصحابه التّجّار كانوا يحترموه كتير.", - "hebrew_transliteration": "וּצְחַאבּוֹ אִ-תֻּגַّאר כַּאנוּ יִחתַרְמוּה כְּתִיר.", - "hebrew": "וחבריו הסוחרים כיבדו אותו מאוד.", - "expected_phonemes": "u sˤ ħ aː b o | ʔ i t u g g aː r | k aː n u | j i ħ t a r m uː h | k t iː r" - }, - { - "voice_time_seconds": { - "start": 43.8, - "end": 47.4 - }, - "arabic": "لمّا ختير وكبر وجسمه ضعف،", - "hebrew_transliteration": "לַמַّא חְ'תִיַר וּכִּבֶר וּגִ'סמוֹ דִ'עֶף,", - "hebrew": "כשהזדקן ובגר וגופו נחלש,", - "expected_phonemes": "l a m m a | x t i j a r | u k i b e r | u dʒ i s m o | dˤ i ʕ e f" - }, - { - "voice_time_seconds": { - "start": 47.4, - "end": 50.0 - }, - "arabic": "فهم إنّه نهايته قرّبت،", - "hebrew_transliteration": "פִהֶם אִנּוֹ נִהַאיתוֹ קַרַّבַת,", - "hebrew": "הבין שסופו קרב,", - "expected_phonemes": "f i h e m | ʔ i n o | n i h aː j t o | ʔ a r r a b a t" - }, - { - "voice_time_seconds": { - "start": 50.0, - "end": 52.6 - }, - "arabic": "فوقّف شغله وطلب من ولاده", - "hebrew_transliteration": "פַוַקַّף שֻעְ'לוֹ וּטַלַבּ מִן וְלַאדוֹ", - "hebrew": "אז הפסיק את עבודתו וביקש מילדיו", - "expected_phonemes": "f a w a ʔ ʔ a f | ʃ u ɣ l o | u tˤ a l a b | m i n | u l aː d o" - }, - { - "voice_time_seconds": { - "start": 52.6, - "end": 55.4 - }, - "arabic": "ييجوا عنده وقال:", - "hebrew_transliteration": "יִיגוּ עִנדוֹ וּקַאל:", - "hebrew": "לבוא אליו ואמר:", - "expected_phonemes": "j iː g u | ʕ i n d o | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 55.4, - "end": 59.4 - }, - "arabic": "«يا مرتي، يا ولادي ويا أحفادي!", - "hebrew_transliteration": "יַא מַרְתִי, יַא וְלַאדִי וּיַא אַחפַאדִי!", - "hebrew": "אשתי, ילדיי ונכדיי!", - "expected_phonemes": "j a | m a r t i | j a | u l aː d i | u j a | ʔ a ħ f aː d i" - }, - { - "voice_time_seconds": { - "start": 59.4, - "end": 61.6 - }, - "arabic": "إنتو شايفين إنّي ختيرت", - "hebrew_transliteration": "אִנתוּ שַאיְפִין אִנִّי חְ'תִיַרֶת", - "hebrew": "אתם רואים שהזדקנתי", - "expected_phonemes": "ʔ i n t u | ʃ aː j f iː n | ʔ i n n i | x t i j a r e t" - }, - { - "voice_time_seconds": { - "start": 61.6, - "end": 65.0 - }, - "arabic": "وبطّل عندي قوّة أكمّل شغل.", - "hebrew_transliteration": "וּבַּטַّל עִנדִי קֻוֶّה אַכַּמֶّל שֻעְ'ל.", - "hebrew": "ואין לי עוד כוח להמשיך לעבוד.", - "expected_phonemes": "u b a tˤ tˤ a l | ʕ i n d i | ʔ u w w e | ʔ a k a m m e l | ʃ u ɣ l" - }, - { - "voice_time_seconds": { - "start": 65.0, - "end": 68.2 - }, - "arabic": "نشكر الله ونحمده أعطاني حياة مديدة،", - "hebrew_transliteration": "נֻשכֹּר אַללַה וּנִחמַדוֹ אַעטַאנִי חַיַאה מַדִידֶה,", - "hebrew": "נודה לאלוהים ונהלל אותו שנתן לי חיים ארוכים,", - "expected_phonemes": "n u ʃ k u r | i l l a | u n i ħ m a d o | ʔ a ʕ tˤ aː n i | ħ a j aː h | m a d iː d e" - }, - { - "voice_time_seconds": { - "start": 68.2, - "end": 72.0 - }, - "arabic": "وعشت معكو بإحترام، بنغنغة وراحة.", - "hebrew_transliteration": "וּעִשֶת מַעְכּוּ בְּאִחתִרַאם, בְּנַעְ'נַעַ'ה וּרַאחַה.", - "hebrew": "וחייתי איתכם בכבוד, בפינוק ובנוחות.", - "expected_phonemes": "u ʕ i ʃ e t | m a ʕ k u | b ʔ i ħ t i r aː m | b n a ɣ n a ɣ a | u r aː ħ a" - }, - { - "voice_time_seconds": { - "start": 72.0, - "end": 76.4 - }, - "arabic": "يمكن يومي قرّب ولازم أرجّع روحي للّي خلقني،", - "hebrew_transliteration": "יֻמכֶּן יוֹמִי קַרַّבּ וּלַאזֶם אַרַגֶّע רוּחִי לַלִّי חַ'לַקנִי,", - "hebrew": "אולי יומי קרב ועליי להשיב את נשמתי למי שברא אותי,", - "expected_phonemes": "j u m k e n | j oː m i | ʔ a r r a b | u l aː z e m | ʔ a r a g g e ʕ | r uː ħ i | l a l l i | x a l a ʔ n i" - }, - { - "voice_time_seconds": { - "start": 76.4, - "end": 79.8 - }, - "arabic": "عشان هيك بطلب منكو تسامحوني،", - "hebrew_transliteration": "עַשַאן הֵיכּ בַּטלֹבּ מִנְכּוּ תְסַאמְחוּנִי,", - "hebrew": "לכן אני מבקש מכם לסלוח לי,", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | b a tˤ l o b | m i n k u | t s aː m ħ uː n i" - }, - { - "voice_time_seconds": { - "start": 79.8, - "end": 82.0 - }, - "arabic": "لإن يمكن بيوم من الأيّام", - "hebrew_transliteration": "לַאִנּ יֻמכֶּן בְּיוֹם מִן אִל-אַיַّאם", - "hebrew": "כי אולי ביום מן הימים", - "expected_phonemes": "l a ʔ i n | j u m k e n | b j oː m | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 82.0, - "end": 85.2 - }, - "arabic": "أذيت حدا فيكو بمعاملتي.»", - "hebrew_transliteration": "אַדֵ'ית חַדַא פִיכּוּ בְּמֻעַאמַלתִי.", - "hebrew": "פגעתי במישהו מכם ביחסי אליו.", - "expected_phonemes": "ʔ a dˤ eː t | ħ a d a | f iː k u | b m u ʕ aː m a l t i" - }, - { - "voice_time_seconds": { - "start": 85.2, - "end": 88.2 - }, - "arabic": "أفراد العيلة تأثّروا كتير من حكي الأب،", - "hebrew_transliteration": "אַפרַאד אִל-עֵילֶה תַאַתַّרוּ כְּתִיר מִן חַכִּי אִל-אַבּ,", - "hebrew": "בני המשפחה התרגשו מאוד מדברי האב,", - "expected_phonemes": "ʔ a f r aː d | i l ʕ eː l e | t a ʔ a t t a r u | k t iː r | m i n | ħ a k i | i l ʔ a b" - }, - { - "voice_time_seconds": { - "start": 88.2, - "end": 90.8 - }, - "arabic": "حزنوا كتير وقالوا:", - "hebrew_transliteration": "חִזנוּ כְּתִיר וּקַאלוּ:", - "hebrew": "התעצבו מאוד ואמרו:", - "expected_phonemes": "ħ i z n u | k t iː r | u ʔ aː l u" - }, - { - "voice_time_seconds": { - "start": 90.8, - "end": 94.8 - }, - "arabic": "«إحنا مسامحينك، وكمان الله رح يسامحك.", - "hebrew_transliteration": "אִחנַא מְסַאמְחִינַכּ, וּכַּמַאן אַללַה רַח-יְסַאמחַכּ.", - "hebrew": "אנחנו סולחים לך, וגם אלוהים יסלח לך.", - "expected_phonemes": "ʔ i ħ n a | m s aː m ħ iː n a k | u k a m aː n | i l l a | r a ħ j s aː m ħ a k" - }, - { - "voice_time_seconds": { - "start": 94.8, - "end": 96.8 - }, - "arabic": "ما غلطت معنا من مرّة،", - "hebrew_transliteration": "מַא עִ'לִטֶת מַעְנַא מִן מַרַّה,", - "hebrew": "לא טעית כלפינו אף פעם,", - "expected_phonemes": "m a | ɣ i l i tˤ e t | m a ʕ n a | m i n | m a r r a" - }, - { - "voice_time_seconds": { - "start": 96.8, - "end": 100.4 - }, - "arabic": "وأعطيتنا وغمرتنا بسّ بالحبّ والإحسان.", - "hebrew_transliteration": "וּאַעטֵיתְנַא וּעַ'מַרתְנַא בַּס בִּל-חֻבّ וִל-אִחסַאן.", - "hebrew": "ונתת לנו והצפת אותנו רק באהבה ובטוב.", - "expected_phonemes": "u ʔ a ʕ tˤ eː t n a | u ɣ a m a r t n a | b a s | b i l ħ u b b | w i l ʔ i ħ s aː n" - }, - { - "voice_time_seconds": { - "start": 100.4, - "end": 104.4 - }, - "arabic": "الله يطوّل في عمرك ويكتّر من أمثالك.»", - "hebrew_transliteration": "אַללַה יְטַוֶّל פִי עֻמרַכּ, וִיכַּתֶّר מִן אַמתַאלַכּ.", - "hebrew": "אלוהים יאריך את ימיך וירבה כמותך.", - "expected_phonemes": "i l l a | j tˤ a w w e l | f i | ʕ u m r a k | w iː k a t t e r | m i n | ʔ a m t aː l a k" - }, - { - "voice_time_seconds": { - "start": 104.4, - "end": 108.4 - }, - "arabic": "قال إلهم: «جيبولي بهايم الدار.»", - "hebrew_transliteration": "קַאל אִלְהֹם: גִ'יבּוּ-לִי בַּהַאיֶם אִ-דַּאר.", - "hebrew": "הוא אמר להם: הביאו לי את בהמות הבית.", - "expected_phonemes": "ʔ aː l | i l h o m | dʒ iː b uː l i | b a h aː j e m | ʔ i d aː r" - }, - { - "voice_time_seconds": { - "start": 108.4, - "end": 110.4 - }, - "arabic": "أجا لحمار.", - "hebrew_transliteration": "אַגַ'א לֶ-חמַאר.", - "hebrew": "בא החמור.", - "expected_phonemes": "ʔ a dʒ a | l e ħ m aː r" - }, - { - "voice_time_seconds": { - "start": 110.4, - "end": 112.6 - }, - "arabic": "إتطلّع عليه الختيار وقالّه:", - "hebrew_transliteration": "אִתטַלַّע עַלֵיה אִל-חְ'תִיַאר וּקַאל-לוֹ:", - "hebrew": "הזקן הביט בו ואמר לו:", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | ʕ a l eː h | i l x t i j aː r | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 112.6, - "end": 114.2 - }, - "arabic": "«سامحني يا حمار،", - "hebrew_transliteration": "סַאמֶחנִי יַא חמַאר,", - "hebrew": "סלח לי, חמור,", - "expected_phonemes": "s aː m e ħ n i | j a | ħ m aː r" - }, - { - "voice_time_seconds": { - "start": 114.2, - "end": 116.8 - }, - "arabic": "شغّلتك بأعمال صعبة،", - "hebrew_transliteration": "שַעַ'לְתַכּ בְּאַעמַאל צַעבֶּה,", - "hebrew": "העבדתי אותך בעבודות קשות,", - "expected_phonemes": "ʃ a ɣ a l t a k | b ʔ a ʕ m aː l | sˤ a ʕ b e" - }, - { - "voice_time_seconds": { - "start": 116.8, - "end": 120.8 - }, - "arabic": "حمّلتك الميّ والخشب من بعيد لحدّ بيتنا،", - "hebrew_transliteration": "חַמַّלתַכּ אִל-מַיّ וִל-חַ'שַבּ מִן בְּעִיד לַחַדّ בֵּיתְנַא,", - "hebrew": "העמסתי עליך מים ועצים מרחוק עד ביתנו,", - "expected_phonemes": "ħ a m m a l t a k | i l m a j j | w i l x a ʃ a b | m i n | b ʕ iː d | l a ħ a d d | b eː t n a" - }, - { - "voice_time_seconds": { - "start": 120.8, - "end": 123.8 - }, - "arabic": "ضربتك بالعصاي عشان أخلّيك تسرع،", - "hebrew_transliteration": "דַ'רַבְּתַכּ בִּל-עַצַאי עַשַאן אַחַלִّיכּ תִסרַע,", - "hebrew": "היכיתי אותך במקל כדי שתמהר,", - "expected_phonemes": "dˤ a r a b t a k | b i l ʕ a sˤ aː i | ʕ a ʃ aː n | ʔ a ħ a l l iː k | t i s r a ʕ" - }, - { - "voice_time_seconds": { - "start": 123.8, - "end": 127.0 - }, - "arabic": "ومرّات ما ذقتش طعم الأكل.", - "hebrew_transliteration": "וּמַרַّאת מַא דֻקתֶש טַעֶם אִל-אַכֶּל.", - "hebrew": "ולפעמים לא טעמת את טעם האוכל.", - "expected_phonemes": "u m a r r aː t | m a | d u ʔ t e ʃ | tˤ a ʕ e m | i l ʔ a k e l" - }, - { - "voice_time_seconds": { - "start": 127.0, - "end": 130.4 - }, - "arabic": "كلّ هاد مش عشان إستهنت فيك", - "hebrew_transliteration": "כֻּלّ הַאד מִש עַשַאן אִסתַהַנֶת פִיכּ", - "hebrew": "כל זה לא מפני שזלזלתי בך", - "expected_phonemes": "k u l l | h aː d | m i ʃ | ʕ a ʃ aː n | ʔ i s t a h a n e t | f iː k" - }, - { - "voice_time_seconds": { - "start": 130.4, - "end": 132.6 - }, - "arabic": "ومش من بخل عليك،", - "hebrew_transliteration": "וּמִש מִן בֻּחֶ'ל עַלֵיכּ,", - "hebrew": "ולא מקמצנות כלפיך,", - "expected_phonemes": "u m i ʃ | m i n | b u x e l | ʕ a l eː k" - }, - { - "voice_time_seconds": { - "start": 132.6, - "end": 135.4 - }, - "arabic": "بسّ إنت بتعرف إنّه عيلتي كبيرة", - "hebrew_transliteration": "בַּס אִנתֶ בְּתִערַף אִנּוֹ עֵילְתִי כְּבִּירֶה", - "hebrew": "אבל אתה יודע שהמשפחה שלי גדולה", - "expected_phonemes": "b a s | ʔ i n t e | b t i ʕ r a f | ʔ i n o | ʕ eː l t i | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 135.4, - "end": 137.6 - }, - "arabic": "وكان لازم أشتغل كتير صعب", - "hebrew_transliteration": "וּכַּאן לַאזֶם אַשתְעֶ'ל כְּתִיר צַעֶבּ", - "hebrew": "והייתי צריך לעבוד קשה מאוד", - "expected_phonemes": "u k aː n | l aː z e m | ʔ a ʃ t ɣ e l | k t iː r | sˤ a ʕ e b" - }, - { - "voice_time_seconds": { - "start": 137.6, - "end": 140.2 - }, - "arabic": "عشان أصرف عليها.»", - "hebrew_transliteration": "עַשַאן אַצרֻף עַלֵיהַא.", - "hebrew": "כדי לפרנס אותה.", - "expected_phonemes": "ʕ a ʃ aː n | ʔ a sˤ r u f | ʕ a l eː h a" - }, - { - "voice_time_seconds": { - "start": 140.2, - "end": 141.6 - }, - "arabic": "لحمار قال:", - "hebrew_transliteration": "לֶ-חמַאר קַאל:", - "hebrew": "החמור אמר:", - "expected_phonemes": "l e ħ m aː r | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 141.6, - "end": 145.2 - }, - "arabic": "«سامحتك يا سيدي، والله يسامحك.»", - "hebrew_transliteration": "סַאמַחתַכּ יַא סִידִי, וּאַללַה יְסַאמחַכּ.", - "hebrew": "סלחתי לך, אדוני, ואלוהים יסלח לך.", - "expected_phonemes": "s aː m a ħ t a k | j a | s iː d i | u i l l a | j s aː m ħ a k" - }, - { - "voice_time_seconds": { - "start": 145.2, - "end": 148.0 - }, - "arabic": "بعد لحمار أجا الثّور.", - "hebrew_transliteration": "בַּעֶד לֶ-חמַאר אַגַ'א אִ-תּוֹר.", - "hebrew": "אחרי החמור בא השור.", - "expected_phonemes": "b a ʕ e d | l e ħ m aː r | ʔ a dʒ a | ʔ i t oː r" - }, - { - "voice_time_seconds": { - "start": 148.0, - "end": 149.8 - }, - "arabic": "الختيار قالّه:", - "hebrew_transliteration": "אִל-חְ'תִיַאר קַאל-לוֹ:", - "hebrew": "הזקן אמר לו:", - "expected_phonemes": "i l x t i j aː r | ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 149.8, - "end": 153.6 - }, - "arabic": "«إنت يا ثور، كتير عزيز، حرثت أرضي،", - "hebrew_transliteration": "אִנתֶ יַא תוֹר, כְּתִיר עַזִיז, חַרַתֶת אַרְדִ'י,", - "hebrew": "אתה, שור, יקר מאוד, חרשת את אדמתי,", - "expected_phonemes": "ʔ i n t e | j a | t oː r | k t iː r | ʕ a z iː z | ħ a r a t e t | ʔ a r dˤ i" - }, - { - "voice_time_seconds": { - "start": 153.6, - "end": 157.6 - }, - "arabic": "وشغلك صعب، كان من الفجر للمسا،", - "hebrew_transliteration": "וּשֻעְ'לַכּ צַעֶבּ, כַּאן מִן אִל-פַגְ'ר לַל-מַסַא,", - "hebrew": "ועבודתך קשה, מהשחר ועד הערב,", - "expected_phonemes": "u ʃ u ɣ l a k | sˤ a ʕ e b | k aː n | m i n | i l f a dʒ r | l a l m a s a" - }, - { - "voice_time_seconds": { - "start": 157.6, - "end": 161.0 - }, - "arabic": "في البرد في الحمّ وفي الشّرد.", - "hebrew_transliteration": "פִי אִל-בַּרְד, פִי אִל-חַםّ וּפִי אִ-שַּׁרַד.", - "hebrew": "בקור, בחום ובשרב.", - "expected_phonemes": "f i | i l b a r d | f i | i l ħ a m m | u f i | ʔ i ʃ a r a d" - }, - { - "voice_time_seconds": { - "start": 161.0, - "end": 164.8 - }, - "arabic": "بسّ دايما كنت جمبك، كتفي على كتفك.", - "hebrew_transliteration": "בַּס דַאיְמַן כֻּנֶת גַ'מבַּכּ, כִּתפִי עַלַא כִּתפַכּ.", - "hebrew": "אבל תמיד הייתי לצדך, כתף אל כתף.", - "expected_phonemes": "b a s | d aː j m a n | k u n e t | dʒ a m b a k | k i t f i | ʕ a l a | k i t f a k" - }, - { - "voice_time_seconds": { - "start": 164.8, - "end": 168.2 - }, - "arabic": "ما كان عندي غاية أستغلّك أو أجور عليك،", - "hebrew_transliteration": "מַא כַּאן עִנדִי עַ'איֶה אַסתַעְ'לַّכּ אַו אַגוּר עַלֵיכּ,", - "hebrew": "לא הייתה לי כוונה לנצל אותך או להתעמר בך,", - "expected_phonemes": "m a | k aː n | ʕ i n d i | ɣ aː j e | ʔ a s t a ɣ l l a k | ʔ a w | ʔ a g uː r | ʕ a l eː k" - }, - { - "voice_time_seconds": { - "start": 168.2, - "end": 170.6 - }, - "arabic": "وكلّ معاملتي إلك كانت من همّي", - "hebrew_transliteration": "וּכֻּלّ מֻעַאמַלתִי אִלַכּ כַּאנַת מִן הַמִّי", - "hebrew": "וכל היחס שלי אליך נבע מדאגתי", - "expected_phonemes": "u k u l l | m u ʕ aː m a l t i | ʔ i l a k | k aː n a t | m i n | h a m m i" - }, - { - "voice_time_seconds": { - "start": 170.6, - "end": 173.8 - }, - "arabic": "على رزق عيلتي. سامحني.»", - "hebrew_transliteration": "עַלַא רִזֶק עֵילְתִי. סַאמֶחנִי.", - "hebrew": "לפרנסת משפחתי. סלח לי.", - "expected_phonemes": "ʕ a l a | r i z e ʔ | ʕ eː l t i | s aː m e ħ n i" - }, - { - "voice_time_seconds": { - "start": 173.8, - "end": 175.0 - }, - "arabic": "الثّور قال:", - "hebrew_transliteration": "אִ-תּוֹר קַאל:", - "hebrew": "השור אמר:", - "expected_phonemes": "ʔ i t oː r | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 175.0, - "end": 178.2 - }, - "arabic": "«أنا مسامحك والله يسامحك.»", - "hebrew_transliteration": "אַנַא מְסַאמְחַכּ וּאַללַה יְסַאמחַכּ.", - "hebrew": "אני סולח לך ואלוהים יסלח לך.", - "expected_phonemes": "ʔ a n a | m s aː m ħ a k | u i l l a | j s aː m ħ a k" - }, - { - "voice_time_seconds": { - "start": 178.2, - "end": 181.8 - }, - "arabic": "بعد الثّور أجوا زملاؤه التّجّار،", - "hebrew_transliteration": "בַּעֶד אִ-תּוֹר אַגוּ זֻמַלַאאוֹ אִ-תֻּגַّאר,", - "hebrew": "אחרי השור באו עמיתיו הסוחרים,", - "expected_phonemes": "b a ʕ e d | ʔ i t oː r | ʔ a g u | z u m a l aː aː o | ʔ i t u g g aː r" - }, - { - "voice_time_seconds": { - "start": 181.8, - "end": 184.0 - }, - "arabic": "لصحاب والجّيران.", - "hebrew_transliteration": "לֶ-צְחַאבּ וִ-גִّירַאן.", - "hebrew": "החברים והשכנים.", - "expected_phonemes": "l e sˤ ħ aː b | w i g g iː r aː n" - }, - { - "voice_time_seconds": { - "start": 184.0, - "end": 187.2 - }, - "arabic": "كلّهم مدحوه للختيار على طيبة قلبه", - "hebrew_transliteration": "כֻּלְّהֹם מַדַחוּה לַל-חְ'תִיַאר עַלַא טִיבֶּת קַלבּוֹ", - "hebrew": "כולם שיבחו את הזקן על טוב ליבו", - "expected_phonemes": "k u l l h o m | m a d a ħ uː h | l a l x t i j aː r | ʕ a l a | tˤ iː b e t | ʔ a l b o" - }, - { - "voice_time_seconds": { - "start": 187.2, - "end": 190.4 - }, - "arabic": "وعلى إنّه إنسان دغري محترم.", - "hebrew_transliteration": "וּעַלַא אִנּוֹ אִנסַאן דֻעְ'רִי מֻחתַרַם.", - "hebrew": "ועל היותו אדם ישר ומכובד.", - "expected_phonemes": "u ʕ a l a | ʔ i n o | ʔ i n s aː n | d u ɣ r i | m u ħ t a r a m" - }, - { - "voice_time_seconds": { - "start": 190.4, - "end": 194.4 - }, - "arabic": "في النّهاي أجوا لجمال، قال إلهم:", - "hebrew_transliteration": "פִי אִ-נַּהַאי אַגוּ לֶ-גְ'מַאל, קַאל אִלְהֹם:", - "hebrew": "בסוף באו הגמלים, הוא אמר להם:", - "expected_phonemes": "f i | ʔ i n a h aː i | ʔ a g u | l e dʒ m aː l | ʔ aː l | i l h o m" - }, - { - "voice_time_seconds": { - "start": 194.4, - "end": 196.2 - }, - "arabic": "«أعزّائي لجمال!", - "hebrew_transliteration": "אַעִזַّאאִי לֶ-גְ'מַאל!", - "hebrew": "גמלים יקרים שלי!", - "expected_phonemes": "ʔ a ʕ i z z aː ʔ i | l e dʒ m aː l" - }, - { - "voice_time_seconds": { - "start": 196.2, - "end": 199.4 - }, - "arabic": "فش عندي كلام أوصف وفائكو،", - "hebrew_transliteration": "פִש עִנדִי כַּלַאם אַוצֻף וַפַאאכּוּ,", - "hebrew": "אין לי מילים לתאר את נאמנותכם,", - "expected_phonemes": "f i ʃ | ʕ i n d i | k a l aː m | ʔ a w sˤ u f | w a f aː aː k u" - }, - { - "voice_time_seconds": { - "start": 199.4, - "end": 203.2 - }, - "arabic": "نشاطكو وخدمتكو إلي ولعيلتي.", - "hebrew_transliteration": "נַשַאטְכּוּ וּחִ'דמִתְכּוּ אִלִי וּלַעֵילְתִי.", - "hebrew": "את חריצותכם ואת שירותכם לי ולמשפחתי.", - "expected_phonemes": "n a ʃ aː tˤ k u | u x i d m i t k u | ʔ i l i | u l a ʕ eː l t i" - }, - { - "voice_time_seconds": { - "start": 203.2, - "end": 207.2 - }, - "arabic": "مشّيتكو في الصّحرا، في الحمّ وإنتو عطشانين.", - "hebrew_transliteration": "מַשַّיתְכּוּ פִי אִ-צַּחרַא, פִי אִל-חַםّ וּאִנתוּ עַטשַאנִין.", - "hebrew": "הובלתי אתכם במדבר, בחום כשאתם צמאים.", - "expected_phonemes": "m a ʃ ʃ a j t k u | f i | ʔ i sˤ a ħ r a | f i | i l ħ a m m | u ʔ i n t u | ʕ a tˤ ʃ aː n iː n" - }, - { - "voice_time_seconds": { - "start": 207.2, - "end": 209.8 - }, - "arabic": "حمّلت عليكو بضايع كتيرة،", - "hebrew_transliteration": "חַמַّלֶת עַלֵיכּוּ בְּדַאיֶע כְּתִירֶה,", - "hebrew": "העמסתי עליכם סחורות רבות,", - "expected_phonemes": "ħ a m m a l e t | ʕ a l eː k u | b d aː j e ʕ | k t iː r e" - }, - { - "voice_time_seconds": { - "start": 209.8, - "end": 213.6 - }, - "arabic": "ضربتكو في السّوط وما سمعتش منكو شكوة.", - "hebrew_transliteration": "דַ'רַבְּתְכּוּ פִי אִ-סּוֹט וּמַא סִמעֶתֶש מִנְכּוּ שַכְּוֶה.", - "hebrew": "היכיתי אתכם בשוט ולא שמעתי מכם תלונה.", - "expected_phonemes": "dˤ a r a b t k u | f i | ʔ i s oː tˤ | u m a | s i m ʕ e t e ʃ | m i n k u | ʃ a k w e" - }, - { - "voice_time_seconds": { - "start": 213.6, - "end": 216.2 - }, - "arabic": "إعملوا معروف سامحوني،", - "hebrew_transliteration": "אִעמְלוּ מַערוּף סַאמְחוּנִי,", - "hebrew": "עשו טובה, סלחו לי,", - "expected_phonemes": "ʔ i ʕ m l u | m a ʕ r uː f | s aː m ħ uː n i" - }, - { - "voice_time_seconds": { - "start": 216.2, - "end": 219.8 - }, - "arabic": "أنا ما عملتش هيك عشاني لئيم أو عاطل،", - "hebrew_transliteration": "אַנַא מַא עִמִלתֶש הֵיכּ עַשַאנִי לַאִים אַו עַאטֶל,", - "hebrew": "לא עשיתי זאת כי אני מרושע או רע,", - "expected_phonemes": "ʔ a n a | m a | ʕ i m i l t e ʃ | h eː k | ʕ a ʃ aː n i | l a ʔ iː m | ʔ a w | ʕ aː tˤ e l" - }, - { - "voice_time_seconds": { - "start": 219.8, - "end": 223.4 - }, - "arabic": "إنّما بسّ من همّي على رزق عيلتي.»", - "hebrew_transliteration": "אִנַّמַא בַּס מִן הַמִّי עַלַא רִזֶק עֵילְתִי.", - "hebrew": "אלא רק מדאגתי לפרנסת משפחתי.", - "expected_phonemes": "ʔ i n n a m a | b a s | m i n | h a m m i | ʕ a l a | r i z e ʔ | ʕ eː l t i" - }, - { - "voice_time_seconds": { - "start": 223.4, - "end": 226.8 - }, - "arabic": "الجّمل لكبير إتطلّع عليه وقالّه:", - "hebrew_transliteration": "אִ-גַّמַל לֶ-כְּבִּיר אִתטַלַّע עַלֵיה וּקַאל-לוֹ:", - "hebrew": "הגמל הגדול הביט בו ואמר לו:", - "expected_phonemes": "ʔ i g g a m a l | l e k b iː r | ʔ i t tˤ a l l a ʕ | ʕ a l eː h | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 226.8, - "end": 230.6 - }, - "arabic": "«إحنا مسامحينك على كلّ إشي عملته معنا،", - "hebrew_transliteration": "אִחנַא מְסַאמְחִינַכּ עַלַא כֻּלّ אִשִי עִמִלתוֹ מַעְנַא,", - "hebrew": "אנחנו סולחים לך על כל מה שעשית לנו,", - "expected_phonemes": "ʔ i ħ n a | m s aː m ħ iː n a k | ʕ a l a | k u l l | ʔ i ʃ i | ʕ i m i l t o | m a ʕ n a" - }, - { - "voice_time_seconds": { - "start": 230.6, - "end": 232.8 - }, - "arabic": "عشان هدا كان شغلنا.", - "hebrew_transliteration": "עַשַאן הַאדַא כַּאן שֻעְ'לְנַא.", - "hebrew": "כי זו הייתה עבודתנו.", - "expected_phonemes": "ʕ a ʃ aː n | h aː d a | k aː n | ʃ u ɣ l n a" - }, - { - "voice_time_seconds": { - "start": 232.8, - "end": 237.0 - }, - "arabic": "بسّ على إشي واحد منقدرش نسامحك.", - "hebrew_transliteration": "בַּס עַלַא אִשִי וַאחַד מַנִקדַרֶש נְסַאמחַכּ.", - "hebrew": "אבל על דבר אחד איננו יכולים לסלוח לך.", - "expected_phonemes": "b a s | ʕ a l a | ʔ i ʃ i | w aː ħ a d | m a n i ʔ d a r e ʃ | n s aː m ħ a k" - }, - { - "voice_time_seconds": { - "start": 237.0, - "end": 239.2 - }, - "arabic": "لمّا كنّا نمشي في قافلة", - "hebrew_transliteration": "לַמַّא כֻּנַّא נִמשִי פִי קַאפְלֶה", - "hebrew": "כשהלכנו בשיירה", - "expected_phonemes": "l a m m a | k u n n a | n i m ʃ i | f i | ʔ aː f l e" - }, - { - "voice_time_seconds": { - "start": 239.2, - "end": 241.2 - }, - "arabic": "عشان ننقل بضاعتك،", - "hebrew_transliteration": "עַשַאן נִנקֹל בְּדַאעתַכּ,", - "hebrew": "כדי להוביל את סחורתך,", - "expected_phonemes": "ʕ a ʃ aː n | n i n ʔ o l | b d aː ʕ t a k" - }, - { - "voice_time_seconds": { - "start": 241.2, - "end": 245.4 - }, - "arabic": "وكنت حاطط على راسنا حمار يمشّينا.»", - "hebrew_transliteration": "וּכֻּנֶת חַאטֶט עַלַא רַאסְנַא חמַאר יְמַשִّינַא.", - "hebrew": "שמת בראשנו חמור שיוביל אותנו.", - "expected_phonemes": "u k u n e t | ħ aː tˤ e tˤ | ʕ a l a | r aː s n a | ħ m aː r | j m a ʃ ʃ iː n a" - } - ] - }, - { - "file_name": "כמידת עורו של השור * קַד, קַד גִ'לד אִ-תוֹר * قدّ, قدّ جلد التّور * סיפור בערבית מדוברת [MuNGHEgm-fg].webm", - "youtube_id": "MuNGHEgm-fg", - "title_hebrew": "כמידת עורו של השור", - "segments": [ - { - "voice_time_seconds": { - "start": 2.2, - "end": 6.8 - }, - "arabic": "قدّ، قدّ جلد التّور", - "hebrew_transliteration": "קַדّ, קַדّ גִ'לד אִ-תּוֹר", - "hebrew": "כמידת, כמידת עורו של השור", - "expected_phonemes": "ʔ a d d | ʔ a d d | dʒ i l d | ʔ i t oː r" - }, - { - "voice_time_seconds": { - "start": 8.2, - "end": 11.8 - }, - "arabic": "لمّا سيدنا إبراهيم، خليل الله،", - "hebrew_transliteration": "לַמַّא סִידְנַא אִבּרַאהִים, חַלִיל אַללַה,", - "hebrew": "כשאדוננו אברהם, ידיד האל,", - "expected_phonemes": "l a m m a | s iː d n a | ʔ i b r aː h iː m | ħ a l iː l | i l l a" - }, - { - "voice_time_seconds": { - "start": 11.8, - "end": 13.6 - }, - "arabic": "أجا على الخليل،", - "hebrew_transliteration": "אַגַ'א עַלַא אִל-חַלִיל,", - "hebrew": "הגיע לחברון,", - "expected_phonemes": "ʔ a dʒ a | ʕ a l a | i l ħ a l iː l" - }, - { - "voice_time_seconds": { - "start": 13.6, - "end": 16.4 - }, - "arabic": "راح لعند الشّيخ وقال:", - "hebrew_transliteration": "רַאח לַעִנד אִ-שֵׁّיחַ' וּקַאל:", - "hebrew": "הלך אל השייח' ואמר:", - "expected_phonemes": "r aː ħ | l a ʕ i n d | ʔ i ʃ ʃ eː x a | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 16.4, - "end": 19.0 - }, - "arabic": "«بدّي شقفة أرض من عندك،", - "hebrew_transliteration": "בַּדִّי שֻקפֶת אַרְד' מִן עִנדַכּ,", - "hebrew": "אני רוצה חלקת אדמה ממך,", - "expected_phonemes": "b a d d i | ʃ u ʔ f e t | ʔ a r dˤ | m i n | ʕ i n d a k" - }, - { - "voice_time_seconds": { - "start": 19.0, - "end": 20.6 - }, - "arabic": "يا شيخ الخليل،", - "hebrew_transliteration": "יַא שֵיחַ' אִל-חַלִיל,", - "hebrew": "שייח' חברון,", - "expected_phonemes": "j a | ʃ eː x a | i l ħ a l iː l" - }, - { - "voice_time_seconds": { - "start": 20.6, - "end": 23.0 - }, - "arabic": "منشان أدفن زوجتي فيها.", - "hebrew_transliteration": "מִנשַאן אַדפֻן זוֹגְ'תִי פִיהַא.", - "hebrew": "כדי לקבור בה את אשתי.", - "expected_phonemes": "m i n ʃ aː n | ʔ a d f u n | z oː dʒ t i | f iː h a" - }, - { - "voice_time_seconds": { - "start": 23.0, - "end": 27.0 - }, - "arabic": "أعطيني شقفة قدّ جلد التّور.»", - "hebrew_transliteration": "אַעטִינִי שֻקפֶה קַדّ גִ'לד אִ-תּוֹר.", - "hebrew": "תן לי חלקה כמידת עורו של השור.", - "expected_phonemes": "ʔ a ʕ tˤ iː n i | ʃ u ʔ f e | ʔ a d d | dʒ i l d | ʔ i t oː r" - }, - { - "voice_time_seconds": { - "start": 27.0, - "end": 29.2 - }, - "arabic": "الشّيخ وافق.", - "hebrew_transliteration": "אִ-שֵׁّיחַ' וַאפַק.", - "hebrew": "השייח' הסכים.", - "expected_phonemes": "ʔ i ʃ ʃ eː x a | w aː f a ʔ" - }, - { - "voice_time_seconds": { - "start": 29.2, - "end": 32.4 - }, - "arabic": "سيدنا إبراهيم كان ذكي كتير،", - "hebrew_transliteration": "סִידְנַא אִבּרַאהִים כַּאן זַכִּי כְּתִיר,", - "hebrew": "אדוננו אברהם היה חכם מאוד,", - "expected_phonemes": "s iː d n a | ʔ i b r aː h iː m | k aː n | z a k i | k t iː r" - }, - { - "voice_time_seconds": { - "start": 32.4, - "end": 36.6 - }, - "arabic": "أخذ جلد التّور وقدّه، قدّه رفيع.", - "hebrew_transliteration": "אַחַ'ד גִ'לד אִ-תּוֹר וּקַדّוֹ, קַדّוֹ רַפִיע.", - "hebrew": "לקח את עור השור וחתך אותו, חתך אותו דק.", - "expected_phonemes": "ʔ a x a d | dʒ i l d | ʔ i t oː r | u ʔ a d d o | ʔ a d d o | r a f iː ʕ" - }, - { - "voice_time_seconds": { - "start": 36.6, - "end": 40.2 - }, - "arabic": "رفيع متل خيطان غربال رفيع.", - "hebrew_transliteration": "רַפִיע מִתֶל חֵיטַאן עִ'רבַּאל רַפִיע.", - "hebrew": "דק כמו חוטים של נפה דקה.", - "expected_phonemes": "r a f iː ʕ | m i t e l | ħ eː tˤ aː n | ɣ i r b aː l | r a f iː ʕ" - }, - { - "voice_time_seconds": { - "start": 40.2, - "end": 43.0 - }, - "arabic": "بعدين قاسه حوالي الأرض،", - "hebrew_transliteration": "בַּעדֵין קַאסוֹ חַוַאלֵי אִל-אַרְד',", - "hebrew": "אחר כך מדד אותו סביב האדמה,", - "expected_phonemes": "b a ʕ d eː n | ʔ aː s o | ħ a w aː l eː | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 43.0, - "end": 45.6 - }, - "arabic": "وطوّق مساحة كبيرة.", - "hebrew_transliteration": "וּטַוַّק מַסַאחַה כְּבִּירֶה.", - "hebrew": "והקיף שטח גדול.", - "expected_phonemes": "u tˤ a w w a ʔ | m a s aː ħ a | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 45.6, - "end": 48.2 - }, - "arabic": "شيخ الخليل كان معصّب", - "hebrew_transliteration": "שֵיחַ' אִל-חַלִיל כַּאן מְעַצֶّבּ", - "hebrew": "שייח' חברון כעס", - "expected_phonemes": "ʃ eː x a | i l ħ a l iː l | k aː n | m ʕ a sˤ sˤ e b" - }, - { - "voice_time_seconds": { - "start": 48.2, - "end": 51.6 - }, - "arabic": "فوق كلّ تصوّر وصاح وقال:", - "hebrew_transliteration": "פוֹק כֻּלّ תַצַוֻّר וּצַאח וּקַאל:", - "hebrew": "מעבר לכל דמיון, צעק ואמר:", - "expected_phonemes": "f oː ʔ | k u l l | t a sˤ a w w u r | u sˤ aː ħ | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 51.6, - "end": 54.2 - }, - "arabic": "«مستحيل، هدا مش صحيح!»", - "hebrew_transliteration": "מֻסתַחִיל, הַאדַא מִש צַחִיח!", - "hebrew": "בלתי אפשרי, זה לא נכון!", - "expected_phonemes": "m u s t a ħ iː l | h aː d a | m i ʃ | sˤ a ħ iː ħ" - }, - { - "voice_time_seconds": { - "start": 54.2, - "end": 56.4 - }, - "arabic": "لكن سيدنا إبراهيم قال:", - "hebrew_transliteration": "לַאכֶּן סִידְנַא אִבּרַאהִים קַאל:", - "hebrew": "אבל אדוננו אברהם אמר:", - "expected_phonemes": "l aː k e n | s iː d n a | ʔ i b r aː h iː m | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 56.4, - "end": 59.4 - }, - "arabic": "«لا، هدا كان إتّفاقنا.", - "hebrew_transliteration": "לַא, הַאדַא כַּאן אִתִّפַאקְנַא.", - "hebrew": "לא, זה היה ההסכם שלנו.", - "expected_phonemes": "l aː | h aː d a | k aː n | ʔ i t t i f aː ʔ n a" - }, - { - "voice_time_seconds": { - "start": 59.4, - "end": 62.8 - }, - "arabic": "إتذكّر، أنا قلتلك قدّ جلد التّور", - "hebrew_transliteration": "אִתזַכַّר, אַנַא קֻלתֶ-לַכּ קַדّ גִ'לד אִ-תּוֹר", - "hebrew": "תיזכר, אמרתי לך כמידת עורו של השור", - "expected_phonemes": "ʔ i t z a k k a r | ʔ a n a | ʔ u l t e l a k | ʔ a d d | dʒ i l d | ʔ i t oː r" - }, - { - "voice_time_seconds": { - "start": 62.8, - "end": 65.0 - }, - "arabic": "وأنا قدّيته.»", - "hebrew_transliteration": "וּאַנַא קַדַّיתוֹ.", - "hebrew": "ואני חתכתי אותו.", - "expected_phonemes": "u ʔ a n a | ʔ a d d a j t o" - }, - { - "voice_time_seconds": { - "start": 65.0, - "end": 67.8 - }, - "arabic": "هدول إلّي كانوا حوالي الشّيخ", - "hebrew_transliteration": "הַדוֹל אִלִّי כַּאנוּ חַוַאלֵי אִ-שֵׁّיחַ'", - "hebrew": "אלה שהיו סביב השייח'", - "expected_phonemes": "h a d oː l | ʔ i l l i | k aː n u | ħ a w aː l eː | ʔ i ʃ ʃ eː x a" - }, - { - "voice_time_seconds": { - "start": 67.8, - "end": 70.8 - }, - "arabic": "وافقوا مع إبراهيم وقالوا:", - "hebrew_transliteration": "וַאפַקוּ מַע אִבּרַאהִים וּקַאלוּ:", - "hebrew": "הסכימו עם אברהם ואמרו:", - "expected_phonemes": "w aː f a ʔ u | m a ʕ | ʔ i b r aː h iː m | u ʔ aː l u" - }, - { - "voice_time_seconds": { - "start": 70.8, - "end": 74.2 - }, - "arabic": "«يا شيخ، هدا كان الإتّفاق.»", - "hebrew_transliteration": "יַא שֵיחַ', הַאדַא כַּאן אִל-אִתִّפַאק.", - "hebrew": "שייח', זה היה ההסכם.", - "expected_phonemes": "j a | ʃ eː x a | h aː d a | k aː n | i l ʔ i t t i f aː ʔ" - }, - { - "voice_time_seconds": { - "start": 74.2, - "end": 77.4 - }, - "arabic": "شيخ الخليل صار يغلي من الغضب", - "hebrew_transliteration": "שֵיחַ' אִל-חַלִיל צַאר יִעְ'לִי מִן אִל-עַ'דַבּ", - "hebrew": "שייח' חברון התחיל לרתוח מכעס", - "expected_phonemes": "ʃ eː x a | i l ħ a l iː l | sˤ aː r | j i ɣ l i | m i n | i l ɣ a d a b" - }, - { - "voice_time_seconds": { - "start": 77.4, - "end": 79.4 - }, - "arabic": "لإنّ أرضه راحت.", - "hebrew_transliteration": "לַאִנّ אַרְדוֹ רַאחַת.", - "hebrew": "כי אדמתו הלכה.", - "expected_phonemes": "l a ʔ i n n | ʔ a r d o | r aː ħ a t" - }, - { - "voice_time_seconds": { - "start": 79.4, - "end": 81.4 - }, - "arabic": "قال لهدول إلّي شاورهم:", - "hebrew_transliteration": "קַאל לַהַדוֹל אִלִّי שַאוַרְהֹם:", - "hebrew": "הוא אמר לאלה שהתייעץ איתם:", - "expected_phonemes": "ʔ aː l | l a h a d oː l | ʔ i l l i | ʃ aː w a r h o m" - }, - { - "voice_time_seconds": { - "start": 81.4, - "end": 83.2 - }, - "arabic": "«رح أقطع روسكو.»", - "hebrew_transliteration": "רַח-אַקטַע רוּסְכּוּ.", - "hebrew": "אכרות את ראשיכם.", - "expected_phonemes": "r a ħ ʔ a ʔ tˤ a ʕ | r uː s k u" - }, - { - "voice_time_seconds": { - "start": 83.2, - "end": 85.8 - }, - "arabic": "وأخدهم لتلّة بحدّ هالبلد،", - "hebrew_transliteration": "וּאַחַ'דְהֹם לַתַלֶّה בְּחַדّ הַל-בַּלַד,", - "hebrew": "ולקח אותם לגבעה ליד העיר,", - "expected_phonemes": "u ʔ a x a d h o m | l a t a l l e | b ħ a d d | h a l b a l a d" - }, - { - "voice_time_seconds": { - "start": 85.8, - "end": 89.2 - }, - "arabic": "محلّ معروف لليوم، وقطع روسهم.", - "hebrew_transliteration": "מַחַלّ מַערוּף לַל-יוֹם, וּקַטַע רוּסְהֹם.", - "hebrew": "מקום שידוע עד היום, וכרת את ראשיהם.", - "expected_phonemes": "m a ħ a l l | m a ʕ r uː f | l a l j oː m | u ʔ a tˤ a ʕ | r uː s h o m" - }, - { - "voice_time_seconds": { - "start": 89.2, - "end": 93.0 - }, - "arabic": "والرّوس تتدحدل وتقول:", - "hebrew_transliteration": "וִ-רּוּס תִתדַחדַל וּתקוּל:", - "hebrew": "והראשים מתגלגלים ואומרים:", - "expected_phonemes": "w i r uː s | t i t d a ħ d a l | u t ʔ uː l" - }, - { - "voice_time_seconds": { - "start": 93.0, - "end": 95.8 - }, - "arabic": "«قدّ، قدّ جلد التّور.»", - "hebrew_transliteration": "קַדّ, קַדّ גִ'לד אִ-תּוֹר.", - "hebrew": "כמידת, כמידת עורו של השור.", - "expected_phonemes": "ʔ a d d | ʔ a d d | dʒ i l d | ʔ i t oː r" - }, - { - "voice_time_seconds": { - "start": 95.8, - "end": 98.6 - }, - "arabic": "وهيك شهدوا لسيّدنا إبراهيم", - "hebrew_transliteration": "וּהֵיכּ שַהַדוּ לַסַיֶّדְנַא אִבּרַאהִים", - "hebrew": "וכך העידו לטובת אדוננו אברהם", - "expected_phonemes": "u h eː k | ʃ a h a d u | l a s a j j e d n a | ʔ i b r aː h iː m" - }, - { - "voice_time_seconds": { - "start": 98.6, - "end": 100.6 - }, - "arabic": "إنّه الحقّ معاه.", - "hebrew_transliteration": "אִנּוֹ אִל-חַקّ מַעַאה.", - "hebrew": "שהצדק איתו.", - "expected_phonemes": "ʔ i n o | i l ħ a ʔ ʔ | m a ʕ aː h" - }, - { - "voice_time_seconds": { - "start": 100.6, - "end": 106.4 - }, - "arabic": "وهيك أخد الأرض.", - "hebrew_transliteration": "וּהֵיכּ אַחַ'ד אִל-אַרְד'.", - "hebrew": "וכך לקח את האדמה.", - "expected_phonemes": "u h eː k | ʔ a x a d | i l ʔ a r dˤ" - } - ] - }, - { - "file_name": "למען הדורות הבאים * עַשַאן אִל-אַג'יַאל אִ-גָ'אי * عشان الأجيال الجّاي * סיפור בערבית מדוברת [I3dibv1zXac].webm", - "youtube_id": "I3dibv1zXac", - "title_hebrew": "למען הדורות הבאים", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "عشان الأجيال الجّاي", - "hebrew_transliteration": "עַשַאן אִל-אַגְ'יַאל אִ-גַّאי", - "hebrew": "למען הדורות הבאים", - "expected_phonemes": "ʕ a ʃ aː n | i l ʔ a dʒ j aː l | ʔ i g g aː i" - }, - { - "voice_time_seconds": { - "start": 5.6, - "end": 8.4 - }, - "arabic": "في يوم من الأيّام،", - "hebrew_transliteration": "פִי יוֹם מִן אִל-אַיַّאם,", - "hebrew": "יום אחד,", - "expected_phonemes": "f i | j oː m | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 8.4, - "end": 10.6 - }, - "arabic": "كان واحد عم بمشي في الطريق", - "hebrew_transliteration": "כַּאן וַאחַד עַם-בִּמשִי פִי אִ-טַּרִיק", - "hebrew": "מישהו הלך בדרך", - "expected_phonemes": "k aː n | w aː ħ a d | ʕ a m b i m ʃ i | f i | ʔ i tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 10.6, - "end": 14.0 - }, - "arabic": "إلّي بتمرق من أطراف القرية الصغيرة.", - "hebrew_transliteration": "אִלִّי בְּתֻמרֹק מִן אַטרַאף אִל-קַרְיֶה לֶ-זְעִ'ירֶה.", - "hebrew": "שעוברת בשולי הכפר הקטן.", - "expected_phonemes": "ʔ i l l i | b t u m r o ʔ | m i n | ʔ a tˤ r aː f | i l ʔ a r j e | l e z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 14.0, - "end": 16.4 - }, - "arabic": "وهوّ مارق من جمب بستان،", - "hebrew_transliteration": "וּהֻוֶّ מַארֶק מִן גַ'מבּ בֻּסתַאן,", - "hebrew": "ובזמן שעבר ליד פרדס,", - "expected_phonemes": "u h u w w e | m aː r e ʔ | m i n | dʒ a m b | b u s t aː n" - }, - { - "voice_time_seconds": { - "start": 16.4, - "end": 18.6 - }, - "arabic": "شاف فلّاح ختيار،", - "hebrew_transliteration": "שַאף פַלַّאח חְ'תִיַאר,", - "hebrew": "ראה איכר זקן,", - "expected_phonemes": "ʃ aː f | f a l l aː ħ | x t i j aː r" - }, - { - "voice_time_seconds": { - "start": 18.6, - "end": 22.0 - }, - "arabic": "وجهه مليان تجاعيد وشعره شايب،", - "hebrew_transliteration": "וִגְ'הוֹ מַליַאן תַגַ'אעִיד וּשַעְרוֹ שַאיֶבּ,", - "hebrew": "פניו מלאות קמטים ושערו שׂב,", - "expected_phonemes": "w i dʒ h o | m a l j aː n | t a dʒ aː ʕ iː d | u ʃ a ʕ r o | ʃ aː j e b" - }, - { - "voice_time_seconds": { - "start": 22.0, - "end": 23.6 - }, - "arabic": "مقرفص على الأرض", - "hebrew_transliteration": "מְקַרְפֶץ עַלַא אִל-אַרְד'", - "hebrew": "כורע על האדמה", - "expected_phonemes": "m ʔ a r f e sˤ | ʕ a l a | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 23.6, - "end": 27.2 - }, - "arabic": "وبزرع شتلة شجرة فواكه.", - "hebrew_transliteration": "וּבִּזרַע שַתלֶת שַגַ'רַת פַוַאכֶּה.", - "hebrew": "ושותל שתיל של עץ פרי.", - "expected_phonemes": "u b i z r a ʕ | ʃ a t l e t | ʃ a dʒ a r a t | f a w aː k e" - }, - { - "voice_time_seconds": { - "start": 27.2, - "end": 31.2 - }, - "arabic": "الزّلمة قرّب على الفلّاح وسلّم عليه.", - "hebrew_transliteration": "אִ-זַّלַמֶה קַרַّבּ עַלַא אִל-פַלַّאח וּסַלַّם עַלֵיה.", - "hebrew": "האיש התקרב אל האיכר ובירך אותו לשלום.", - "expected_phonemes": "ʔ i z z a l a m e | ʔ a r r a b | ʕ a l a | i l f a l l aː ħ | u s a l l a m | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 31.2, - "end": 33.2 - }, - "arabic": "وبعد ما تبادلوا التّحيّات", - "hebrew_transliteration": "וּבַּעֶד מַא תְבַּאדַלוּ אִ-תַּחִיַّאת", - "hebrew": "ואחרי שהחליפו ברכות", - "expected_phonemes": "u b a ʕ e d | m a | t b aː d a l u | ʔ i t a ħ i j j aː t" - }, - { - "voice_time_seconds": { - "start": 33.2, - "end": 35.0 - }, - "arabic": "وكلمات المجاملة،", - "hebrew_transliteration": "וּכַּלִמַאת אִל-מֻגַ'אמַלֶה,", - "hebrew": "ומילות נימוס,", - "expected_phonemes": "u k a l i m aː t | i l m u dʒ aː m a l e" - }, - { - "voice_time_seconds": { - "start": 35.0, - "end": 37.6 - }, - "arabic": "سأل الزّلمة الفلّاح:", - "hebrew_transliteration": "סַאַל אִ-זַּלַמֶה אִל-פַלַّאח:", - "hebrew": "האיש שאל את האיכר:", - "expected_phonemes": "s a ʔ a l | ʔ i z a l a m e | i l f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 37.6, - "end": 41.2 - }, - "arabic": "«شايفك عم تبذل جهد بزراعة الشّجرة.»", - "hebrew_transliteration": "שַאיְפַכּ עַם-תִבדֻ'ל גֻ'הֶד בִּזרַאעַת אִ-שַּׁגַ'רַה.", - "hebrew": "אני רואה שאתה משקיע מאמץ בשתילת העץ.", - "expected_phonemes": "ʃ aː j f a k | ʕ a m t i b dˤ u l | dʒ u h e d | b i z r aː ʕ a t | ʔ i ʃ a dʒ a r a" - }, - { - "voice_time_seconds": { - "start": 41.2, - "end": 42.6 - }, - "arabic": "ردّ الفلّاح:", - "hebrew_transliteration": "רַדّ אִל-פַלַّאח:", - "hebrew": "האיכר ענה:", - "expected_phonemes": "r a d d | i l f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 42.6, - "end": 45.0 - }, - "arabic": "«أه، هلّق خلّصت زراعة", - "hebrew_transliteration": "אַה, הַלַّק חַ'לַّצֶת זְרַאעַת", - "hebrew": "כן, עכשיו סיימתי לשתול", - "expected_phonemes": "ʔ aː | h a l l a ʔ | x a l l a sˤ e t | z r aː ʕ a t" - }, - { - "voice_time_seconds": { - "start": 45.0, - "end": 48.0 - }, - "arabic": "شتلة صغيرة من شجرة اللّوز.»", - "hebrew_transliteration": "שַתלֶה זְעִ'ירֶה מִן שַגַ'רַת אִ-לּוֹז.", - "hebrew": "שתיל קטן של עץ שקד.", - "expected_phonemes": "ʃ a t l e | z ɣ iː r e | m i n | ʃ a dʒ a r a t | i l oː z" - }, - { - "voice_time_seconds": { - "start": 48.0, - "end": 49.6 - }, - "arabic": "الزّلمة سأل:", - "hebrew_transliteration": "אִ-זַّלַמֶה סַאַל:", - "hebrew": "האיש שאל:", - "expected_phonemes": "ʔ i z z a l a m e | s a ʔ a l" - }, - { - "voice_time_seconds": { - "start": 49.6, - "end": 54.0 - }, - "arabic": "«طيّب ولشو تتعّب حالك بزراعة شجرة لوز؟", - "hebrew_transliteration": "טַיֶّבּ וּלַשוּ תְתַעֶّבּ חַאלַכּ בִּזְרַאעַת שַגַ'רַת לוֹז?", - "hebrew": "טוב, ולמה אתה מתאמץ לשתול עץ שקד?", - "expected_phonemes": "tˤ a j j e b | u l a ʃ u | t t a ʕ ʕ e b | ħ aː l a k | b i z r aː ʕ a t | ʃ a dʒ a r a t | l oː z" - }, - { - "voice_time_seconds": { - "start": 54.0, - "end": 57.4 - }, - "arabic": "ما إنت ختيار، وهاي الشّتلة إلّي زرعتها", - "hebrew_transliteration": "מַא אִנתֶ חְ'תִיַאר, וּהַאי אִ-שַּׁתלֶה אִלִّי זַרַעתְהַא", - "hebrew": "הרי אתה זקן, והשתיל הזה ששתלת", - "expected_phonemes": "m a | ʔ i n t e | x t i j aː r | u h aː i | ʔ i ʃ a t l e | ʔ i l l i | z a r a ʕ t h a" - }, - { - "voice_time_seconds": { - "start": 57.4, - "end": 60.8 - }, - "arabic": "رح تعطي ثمار بعد كتير سنين!»", - "hebrew_transliteration": "רַח-תַעטִי תְ'מַאר בַּעֶד כְּתִיר סְנִין!", - "hebrew": "ייתן פירות רק אחרי הרבה שנים!", - "expected_phonemes": "r a ħ t a ʕ tˤ i | t m aː r | b a ʕ e d | k t iː r | s n iː n" - }, - { - "voice_time_seconds": { - "start": 60.8, - "end": 62.8 - }, - "arabic": "ردّ عليه الفلّاح:", - "hebrew_transliteration": "רַדّ עַלֵיה אִל-פַלַّאח:", - "hebrew": "האיכר ענה לו:", - "expected_phonemes": "r a d d | ʕ a l eː h | i l f a l l aː ħ" - }, - { - "voice_time_seconds": { - "start": 62.8, - "end": 65.4 - }, - "arabic": "«زرعت هاي الشّتلة عشان ولادي،", - "hebrew_transliteration": "זַרַעֶת הַאי אִ-שַּׁתלֶה עַשַאן וְלַאדִי,", - "hebrew": "שתלתי את השתיל הזה בשביל ילדיי,", - "expected_phonemes": "z a r a ʕ e t | h aː i | ʔ i ʃ a t l e | ʕ a ʃ aː n | u l aː d i" - }, - { - "voice_time_seconds": { - "start": 65.4, - "end": 68.8 - }, - "arabic": "أحفادي، وأولادهم من بعدهم.", - "hebrew_transliteration": "אַחפַאדִי, וּאוֹלַאדְהֹם מִן בַּעֶדְהֹם.", - "hebrew": "נכדיי, וילדיהם אחריהם.", - "expected_phonemes": "ʔ a ħ f aː d i | u ʔ oː l aː d h o m | m i n | b a ʕ e d h o m" - }, - { - "voice_time_seconds": { - "start": 68.8, - "end": 71.6 - }, - "arabic": "أسيادي زرعوا هدا الشّجر الكبير", - "hebrew_transliteration": "אַסיַאדִי זַרַעוּ הַאדַא אִ-שַּׁגַ'ר לֶ-כְּבִּיר", - "hebrew": "אבותיי שתלו את העצים הגדולים האלה", - "expected_phonemes": "ʔ a s j aː d i | z a r a ʕ u | h aː d a | ʔ i ʃ a dʒ a r | l e k b iː r" - }, - { - "voice_time_seconds": { - "start": 71.6, - "end": 75.0 - }, - "arabic": "إلّي شايفه من وراي، قبل كتير سنين.", - "hebrew_transliteration": "אִלִّי שַאיְפוֹ מִן וַרַאי, קַבֶּל כְּתִיר סְנִין.", - "hebrew": "שאתה רואה מאחוריי, לפני הרבה שנים.", - "expected_phonemes": "ʔ i l l i | ʃ aː j f o | m i n | w a r aː i | ʔ a b e l | k t iː r | s n iː n" - }, - { - "voice_time_seconds": { - "start": 75.0, - "end": 78.6 - }, - "arabic": "وأنا استمتعت وبعدني بستمتع بثماره.", - "hebrew_transliteration": "וּאַנַא אִסתַמתַעֶת וּבַּעדְנִי בַּסתַמתֶע בִּתְ'מַארוֹ.", - "hebrew": "ואני נהניתי ועדיין נהנה מפירותיהם.", - "expected_phonemes": "u ʔ a n a | ʔ i s t a m t a ʕ e t | u b a ʕ d n i | b a s t a m t e ʕ | b i t m aː r o" - }, - { - "voice_time_seconds": { - "start": 78.6, - "end": 81.8 - }, - "arabic": "وهلّق أنا عم بكمّل طريقهم", - "hebrew_transliteration": "וּהַלַّק אַנַא עַם-בַּכַּמֶّל טַרִיקְהֹם", - "hebrew": "ועכשיו אני ממשיך את דרכם", - "expected_phonemes": "u h a l l a ʔ | ʔ a n a | ʕ a m b a k a m m e l | tˤ a r iː ʔ h o m" - }, - { - "voice_time_seconds": { - "start": 81.8, - "end": 85.0 - }, - "arabic": "وعم بزرع عشان الأجيال الجّاي.»", - "hebrew_transliteration": "וּעַם-בַּזרַע עַשַאן אִל-אַגְ'יַאל אִ-גַّאי.", - "hebrew": "ושותל למען הדורות הבאים.", - "expected_phonemes": "u ʕ a m b a z r a ʕ | ʕ a ʃ aː n | i l ʔ a dʒ j aː l | ʔ i g g aː i" - } - ] - }, - { - "file_name": "מי שיודע יודע * אלי בערף בערף * إلّي بعرف بعرف * סיפור עממי בערבית [ycEZG5Qoth8].mkv", - "youtube_id": "ycEZG5Qoth8", - "title_hebrew": "מי שיודע יודע", - "segments": [ - { - "voice_time_seconds": { - "start": 1.2, - "end": 4.0 - }, - "arabic": "إلّي بعرف بعرف", - "hebrew_transliteration": "אִלִّי בִּיעְרַף בִּיעְרַף", - "hebrew": "מי שיודע יודע", - "expected_phonemes": "ʔ i l l i | b iː ʕ r a f | b iː ʕ r a f" - }, - { - "voice_time_seconds": { - "start": 5.0, - "end": 8.0 - }, - "arabic": "كان يا ما كان، في قديم الزّمان", - "hebrew_transliteration": "כַּאן יַא מַא כַּאן, פִי קַדִים אִ-זַّמַאן", - "hebrew": "היה היה, בימים עברו", - "expected_phonemes": "k aː n | j a | m a | k aː n | f i | ʔ a d iː m | ʔ i z z a m aː n" - }, - { - "voice_time_seconds": { - "start": 8.0, - "end": 10.4 - }, - "arabic": "في سالف العصر والأوان", - "hebrew_transliteration": "פִי סַאלֶף אִל-עַצֶר וִל-אַוַאן", - "hebrew": "בזמנים עברו", - "expected_phonemes": "f i | s aː l e f | i l ʕ a sˤ e r | w i l ʔ a w aː n" - }, - { - "voice_time_seconds": { - "start": 10.4, - "end": 13.8 - }, - "arabic": "واحد شابّ، شابّ جدع", - "hebrew_transliteration": "וַאחַד שַאבّ, שַאבّ גַ'דַע", - "hebrew": "בחור אחד, בחור אמיץ", - "expected_phonemes": "w aː ħ a d | ʃ aː b b | ʃ aː b b | dʒ a d a ʕ" - }, - { - "voice_time_seconds": { - "start": 13.8, - "end": 17.8 - }, - "arabic": "هالشّابّ كان متجوّز وكان إله شغل", - "hebrew_transliteration": "הַ-שַּׁאבّ כַּאן מִתגַ'וֶّז וּכַּאן אִלוֹ שֻעְ'ל", - "hebrew": "הבחור הזה היה נשוי והייתה לו עבודה", - "expected_phonemes": "h a ʃ aː b b | k aː n | m i t dʒ a w w e z | u k aː n | i l o | ʃ u ɣ l" - }, - { - "voice_time_seconds": { - "start": 17.8, - "end": 22.0 - }, - "arabic": "كان يطلع على شغله كل يوم الصّبح بكّير", - "hebrew_transliteration": "כַּאן יִטלַע עַלַא שֻעְ'לוֹ כֻּלّ יוֹם אִ-צֻּבֻּח בַּכִּיר", - "hebrew": "הוא יצא לעבודה כל יום מוקדם בבוקר", - "expected_phonemes": "k aː n | j i tˤ l a ʕ | ʕ a l a | ʃ u ɣ l o | k u l l | j oː m | ʔ i sˤ u b u ħ | b a k iː r" - }, - { - "voice_time_seconds": { - "start": 22.0, - "end": 24.6 - }, - "arabic": "وما يرجعش قبل المسا", - "hebrew_transliteration": "וּמַא יִרגַ'ע-ש קַבֶּל אִל-מַסַא", - "hebrew": "ולא חזר לפני הערב", - "expected_phonemes": "u m a | j i r dʒ a ʕ ʃ | ʔ a b e l | i l m a s a" - }, - { - "voice_time_seconds": { - "start": 24.6, - "end": 31.2 - }, - "arabic": "يوم من الأيّام صدفت إنّه خلّص شغله بكّير وقرّر يروّح", - "hebrew_transliteration": "יוֹם מִן אִל-אַיַّאם צַדַפַת אִנּוֹ חַ'לַّץ שֻעְ'לוֹ בַּכִּיר וּקַרַّר יְרַוֶّח", - "hebrew": "יום אחד קרה שסיים את עבודתו מוקדם והחליט לחזור הביתה", - "expected_phonemes": "j oː m | m i n | i l ʔ a j j aː m | sˤ a d a f a t | ʔ i n o | x a l l a sˤ | ʃ u ɣ l o | b a k iː r | u ʔ a r r a r | j r a w w e ħ" - }, - { - "voice_time_seconds": { - "start": 31.2, - "end": 35.2 - }, - "arabic": "فرجع عبيته السّاعة تنتين الضّهر", - "hebrew_transliteration": "פַ-רִגֶ'ע עַ-בֵּיתוֹ אִ-סַּאעַה תִנתֵין אִ-דֻּהֹר", - "hebrew": "אז חזר לביתו בשתיים בצהריים", - "expected_phonemes": "f a r i dʒ e ʕ | ʕ a b eː t o | ʔ i s aː ʕ a | t i n t eː n | ʔ i d u h o r" - }, - { - "voice_time_seconds": { - "start": 35.2, - "end": 39.6 - }, - "arabic": "لمّا فات عالبيت، لاقى مرته مع واحد", - "hebrew_transliteration": "לַמַّא פַאת עַ-לְ-בֵּית, לַאקַא מַרְתוֹ מַע וַאחַד", - "hebrew": "כשנכנס הביתה, מצא את אשתו עם מישהו", - "expected_phonemes": "l a m m a | f aː t | ʕ a l b eː t | l aː ʔ a | m a r t o | m a ʕ | w aː ħ a d" - }, - { - "voice_time_seconds": { - "start": 39.6, - "end": 43.6 - }, - "arabic": "إنجنّ هالشّابّ وصار يلحق فيه بدّه يموّته", - "hebrew_transliteration": "אִנגַ'ן הַ-שַּׁאבّ וּצַאר יִלחַק פִיה בַּדּוֹ יְמַוְּתוֹ", - "hebrew": "הבחור השתגע והתחיל לרדוף אחריו כדי להרוג אותו", - "expected_phonemes": "ʔ i n dʒ a n | h a ʃ aː b b | u sˤ aː r | j i l ħ a ʔ | f iː h | b a d o | j m a w t o" - }, - { - "voice_time_seconds": { - "start": 43.6, - "end": 46.8 - }, - "arabic": "هاداك، الزّلمة، صاحب مرته", - "hebrew_transliteration": "הַאדַאכּ, אִ-זַּלַמֶה, צַאחֶבּ מַרְתוֹ", - "hebrew": "ההוא, האיש, המאהב של אשתו", - "expected_phonemes": "h aː d aː k | ʔ i z a l a m e | sˤ aː ħ e b | m a r t o" - }, - { - "voice_time_seconds": { - "start": 46.8, - "end": 51.2 - }, - "arabic": "دغري نط من الشّبّاك وهرب من طريق الحقل", - "hebrew_transliteration": "דֻעְ'רִי נַטّ מִן אִ-שֻּׁבַּאכּ וּהִרֶבּ מִן טַרִיק אִל-חַקֶל", - "hebrew": "מיד קפץ מהחלון וברח בדרך השדה", - "expected_phonemes": "d u ɣ r i | n a tˤ tˤ | m i n | ʔ i ʃ u b aː k | u h i r e b | m i n | tˤ a r iː ʔ | i l ħ a ʔ e l" - }, - { - "voice_time_seconds": { - "start": 51.2, - "end": 56.4 - }, - "arabic": "وهوّ هارب شاف بطريقه كومة عدس على الأرض", - "hebrew_transliteration": "וּהֻוֶّ הַארֶבּ שַאף בְּטַרִיקוֹ כּוֹמֶת עַדַס עַלַא אִל-אַרְד'", - "hebrew": "ובזמן שברח ראה בדרכו ערימת עדשים על האדמה", - "expected_phonemes": "u h u w w e | h aː r e b | ʃ aː f | b tˤ a r iː ʔ o | k oː m e t | ʕ a d a s | ʕ a l a | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 56.4, - "end": 62.8 - }, - "arabic": "وقّف شوي وأخد كفّ عدس بإيديه ورجع كمّل يركض", - "hebrew_transliteration": "וַקַّף שְוַיّ וּאַחַ'ד כַּףّ עַדַס בְּאִידֵיה וּרִגֶ'ע כַּמַّל יִרכֹּד'", - "hebrew": "עצר רגע, לקח חופן עדשים בידיו וחזר להמשיך לרוץ", - "expected_phonemes": "w a ʔ ʔ a f | ʃ w a j j | u ʔ a x a d | k a f f | ʕ a d a s | b ʔ iː d eː h | u r i dʒ e ʕ | k a m m a l | j i r k o dˤ" - }, - { - "voice_time_seconds": { - "start": 62.8, - "end": 67.6 - }, - "arabic": "هوّ كان يركض والشّابّ يركض وراه بدّه يمسكه", - "hebrew_transliteration": "הֻוֶّ כַּאן יִרכֹּד' וִ-שַּׁאבّ יִרכֹּד' וַרַאה בַּדּוֹ יִמסְכּוֹ", - "hebrew": "הוא רץ והבחור רץ אחריו כדי לתפוס אותו", - "expected_phonemes": "h u w w e | k aː n | j i r k o dˤ | w i ʃ aː b b | j i r k o dˤ | w a r aː h | b a d o | j i m s k o" - }, - { - "voice_time_seconds": { - "start": 67.6, - "end": 70.2 - }, - "arabic": "هادا يركض وهاداك يركض", - "hebrew_transliteration": "הַאדַא יִרכֹּד' וּהַאדַאכּ יִרכֹּד'", - "hebrew": "זה רץ וההוא רץ", - "expected_phonemes": "h aː d a | j i r k o dˤ | u h aː d aː k | j i r k o dˤ" - }, - { - "voice_time_seconds": { - "start": 70.2, - "end": 73.0 - }, - "arabic": "هادا يهرب وهاداك يلحق", - "hebrew_transliteration": "הַאדַא יִהרֻבּ וּהַאדַאכּ יִלחַק", - "hebrew": "זה בורח וההוא רודף", - "expected_phonemes": "h aː d a | j i h r u b | u h aː d aː k | j i l ħ a ʔ" - }, - { - "voice_time_seconds": { - "start": 73.0, - "end": 76.4 - }, - "arabic": "الشّابّ إنتبه إنّه هادا الزّلمة", - "hebrew_transliteration": "אִ-שַּׁאבّ אִנתַבַּה אִנּוֹ הַאדַא אִ-זַּלַמֶה", - "hebrew": "הבחור שם לב שהאיש הזה", - "expected_phonemes": "ʔ i ʃ aː b b | ʔ i n t a b a | ʔ i n o | h aː d a | ʔ i z a l a m e" - }, - { - "voice_time_seconds": { - "start": 76.4, - "end": 79.2 - }, - "arabic": "بلّش يركض بين بيوت القرية", - "hebrew_transliteration": "בַּלַّש יִרכֹּד' בֵּין בְּיוּת אִל-קַרְיֶה", - "hebrew": "התחיל לרוץ בין בתי הכפר", - "expected_phonemes": "b a l l a ʃ | j i r k o dˤ | b eː n | b j uː t | i l ʔ a r j e" - }, - { - "voice_time_seconds": { - "start": 79.2, - "end": 82.8 - }, - "arabic": "وعشان هيك صار يصرّخ بأعلى صوت:", - "hebrew_transliteration": "וּעַשַאן הֵיכּ צַאר יְצַרֶّח בְּאַעלַא צוֹת:", - "hebrew": "ולכן התחיל לצעוק בקול רם:", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | sˤ aː r | j sˤ a r r e ħ | b ʔ a ʕ l a | sˤ oː t" - }, - { - "voice_time_seconds": { - "start": 82.8, - "end": 88.4 - }, - "arabic": "«يا ناس، يا عالم، إلّي بيخاف من ربّه، يمسكلي هالزّلمة...", - "hebrew_transliteration": "יַא נַאס, יַא עַאלַם, אִלִّי בִּיחַ'אף מִן רַבּוֹ, יִמסְכּ-לִי הַ-זַּלַמֶה...", - "hebrew": "אנשים, עולם, מי שירא את אלוהיו, שיתפוס לי את האיש הזה...", - "expected_phonemes": "j a | n aː s | j a | ʕ aː l a m | ʔ i l l i | b iː x aː f | m i n | r a b o | j i m s k l i | h a z a l a m e" - }, - { - "voice_time_seconds": { - "start": 88.4, - "end": 93.8 - }, - "arabic": "يا ناس... إلّي بيخاف ربّه يمسكه وما يتركه!!»", - "hebrew_transliteration": "יַא נַאס... אִלִّי בִּיחַ'אף רַבּוֹ יִמסְכּוֹ וּמַא יִתרֻכּוֹ!!", - "hebrew": "אנשים... מי שירא את אלוהיו, שיתפוס אותו ולא יעזוב אותו!!", - "expected_phonemes": "j a | n aː s | ʔ i l l i | b iː x aː f | r a b o | j i m s k o | u m a | j i t r u k o" - }, - { - "voice_time_seconds": { - "start": 93.8, - "end": 98.0 - }, - "arabic": "النّاس كلّها طلعت من بيوتها على صوت الشّابّ", - "hebrew_transliteration": "אִ-נַּאס כֻּלְّהַא טִלעַת מִן בְּיוּתְהַא עַלַא צוֹת אִ-שַּׁאבّ", - "hebrew": "כל האנשים יצאו מבתיהם לקול הבחור", - "expected_phonemes": "ʔ i n aː s | k u l l h a | tˤ i l ʕ a t | m i n | b j uː t h a | ʕ a l a | sˤ oː t | ʔ i ʃ aː b b" - }, - { - "voice_time_seconds": { - "start": 98.0, - "end": 105.2 - }, - "arabic": "وإلّا همّ شايفينه لزّلمة، لحقوه...، مسكوه... وسألوه:", - "hebrew_transliteration": "וּאִלַّא הֻמّ שַאיְפִינוֹ לַ-זַּלַמֶה, לַחקוּה..., מַסַכּוּה... וּסַאַלוּה:", - "hebrew": "והנה הם רואים את האיש, רדפו אחריו..., תפסו אותו... ושאלו אותו:", - "expected_phonemes": "u ʔ i l l a | h u m m | ʃ aː j f iː n o | l a z a l a m e | l a ħ ʔ uː h | m a s a k uː h | u s a ʔ a l uː h" - }, - { - "voice_time_seconds": { - "start": 105.2, - "end": 110.2 - }, - "arabic": "«شو السّيرة؟ شو عملت وليش عم بيلحقك هالشّابّ؟»", - "hebrew_transliteration": "שוּ אִ-סִּירַה? שוּ עִמִלֶת וּלֵיש עַם-בִּילחַקַכּ הַ-שַּׁאבّ?", - "hebrew": "מה העניין? מה עשית ולמה הבחור הזה רודף אחריך?", - "expected_phonemes": "ʃ u | ʔ i s iː r a | ʃ u | ʕ i m i l e t | u l eː ʃ | ʕ a m b iː l ħ a ʔ a k | h a ʃ aː b b" - }, - { - "voice_time_seconds": { - "start": 110.2, - "end": 112.2 - }, - "arabic": "ردّ عليهم وقال:", - "hebrew_transliteration": "רַדّ עַלֵיהֹם וּקַאל:", - "hebrew": "הוא ענה להם ואמר:", - "expected_phonemes": "r a d d | ʕ a l eː h o m | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 112.2, - "end": 116.6 - }, - "arabic": "«والله ولا إشي، أنا بسّ كنت مارق من طريق الحقل", - "hebrew_transliteration": "וַאללַה וְלַא אִשִי, אַנַא בַּס כֻּנֶת מַארֶק מִן טַרִיק אִל-חַקֶל", - "hebrew": "בחיי, כלום, רק עברתי בדרך השדה", - "expected_phonemes": "w aː l l a | u l aː | ʔ i ʃ i | ʔ a n a | b a s | k u n e t | m aː r e ʔ | m i n | tˤ a r iː ʔ | i l ħ a ʔ e l" - }, - { - "voice_time_seconds": { - "start": 116.6, - "end": 119.0 - }, - "arabic": "وشفت كومة عدس على الأرض", - "hebrew_transliteration": "וּשֻפֶת כּוֹמֶת עַדַס עַלַא אִל-אַרְד'", - "hebrew": "וראיתי ערימת עדשים על האדמה", - "expected_phonemes": "u ʃ u f e t | k oː m e t | ʕ a d a s | ʕ a l a | i l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 119.0, - "end": 123.2 - }, - "arabic": "أخدت منها كف واحد عشان أطعمي ولادي الجوعانين...", - "hebrew_transliteration": "אַחַ'דֶת מִנְהַא כַּףّ וַאחַד עַשַאן אַטַעְמִי וְלַאדִי אִל-גוּעַאנִין...", - "hebrew": "לקחתי ממנה חופן אחד כדי להאכיל את ילדיי הרעבים...", - "expected_phonemes": "ʔ a x a d e t | m i n h a | k a f f | w aː ħ a d | ʕ a ʃ aː n | ʔ a tˤ a ʕ m i | u l aː d i | i l g uː ʕ aː n iː n" - }, - { - "voice_time_seconds": { - "start": 123.2, - "end": 127.6 - }, - "arabic": "والله ما عملتش إشي، بسّ هوّ عم بيلاحقني", - "hebrew_transliteration": "וַאללַה מַא עִמִלתֶ-ש אִשִי, בַּס הֻוֶّ עַם-בִּילַאחִקנִי", - "hebrew": "בחיי שלא עשיתי כלום, אבל הוא רודף אחריי", - "expected_phonemes": "w aː l l a | m a | ʕ i m i l t e ʃ | ʔ i ʃ i | b a s | h u w w e | ʕ a m b iː l aː ħ i ʔ n i" - }, - { - "voice_time_seconds": { - "start": 127.6, - "end": 130.4 - }, - "arabic": "بدّه يموّتني عشان أخدت عدس...", - "hebrew_transliteration": "בַּדּוֹ יְמַוְּתנִי עַשַאן אַחַ'דֶת עַדַס...", - "hebrew": "רוצה להרוג אותי כי לקחתי עדשים...", - "expected_phonemes": "b a d o | j m a w t n i | ʕ a ʃ aː n | ʔ a x a d e t | ʕ a d a s" - }, - { - "voice_time_seconds": { - "start": 130.4, - "end": 133.6 - }, - "arabic": "والله فش عندي أيش أطعمي ولادي", - "hebrew_transliteration": "וַאללַה פִש עִנדִי אֵיש אַטַעְמִי וְלַאדִי", - "hebrew": "בחיי אין לי במה להאכיל את ילדיי", - "expected_phonemes": "w aː l l a | f i ʃ | ʕ i n d i | ʔ eː ʃ | ʔ a tˤ a ʕ m i | u l aː d i" - }, - { - "voice_time_seconds": { - "start": 133.6, - "end": 140.0 - }, - "arabic": "ولولا الحاجة ما كان مدّيت إيدي، أنا آسف حقكو عليّ.»", - "hebrew_transliteration": "וּלוֹלַא אִל-חַאגֶ'ה מַא כַּאן מַדֵّית אִידִי, אַנַא אַאסֶף חַקְّכּוּ עַלַיّ.", - "hebrew": "ולולא הצורך לא הייתי שולח יד, אני מצטער, סלחו לי.", - "expected_phonemes": "u l oː l a | i l ħ aː dʒ e | m a | k aː n | m a d d eː t | ʔ iː d i | ʔ a n a | ʔ aː s e f | ħ a ʔ ʔ k u | ʕ a l a j j" - }, - { - "voice_time_seconds": { - "start": 140.0, - "end": 144.4 - }, - "arabic": "أهل القرية بعرفوا الشّبّ وأطباعه المنيحة", - "hebrew_transliteration": "אַהֶל אִל-קַרְיֶה בִּיעִרפוּ אִ-שַּׁבّ וּאַטבַּאעוֹ אִל-מְנִיחַה", - "hebrew": "אנשי הכפר הכירו את הבחור ואת מידותיו הטובות", - "expected_phonemes": "ʔ a h e l | i l ʔ a r j e | b iː ʕ i r f u | ʔ i ʃ a b b | u ʔ a tˤ b aː ʕ o | i l m n iː ħ a" - }, - { - "voice_time_seconds": { - "start": 144.4, - "end": 147.4 - }, - "arabic": "وبعرفوه إنّه أصيل إبن أصيل", - "hebrew_transliteration": "וּבִּיעִרפוּה אִנּוֹ אַצִיל אִבְּן אַצִיל", - "hebrew": "וידעו שהוא אדם הגון בן הגונים", - "expected_phonemes": "u b iː ʕ i r f uː h | ʔ i n o | ʔ a sˤ iː l | ʔ i b n | ʔ a sˤ iː l" - }, - { - "voice_time_seconds": { - "start": 147.4, - "end": 151.2 - }, - "arabic": "عشان هيك إستغربوا كتير من حكي الزّلمة", - "hebrew_transliteration": "עַשַאן הֵיכּ אִסתַעְ'רַבּוּ כְּתִיר מִן חַכִּי אִ-זַּלַמֶה", - "hebrew": "לכן התפלאו מאוד מדברי האיש", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | ʔ i s t a ɣ r a b u | k t iː r | m i n | ħ a k i | ʔ i z a l a m e" - }, - { - "voice_time_seconds": { - "start": 151.2, - "end": 156.6 - }, - "arabic": "وحتّى زعلوا من الشّابّ على قلّة كرمه. وقالوا له:", - "hebrew_transliteration": "וּחַתַّא זִעלוּ מִן אִ-שַּׁאבّ עַלַא קִלֶّת כַּרַמוֹ. וּקַאלוּ-לוֹ:", - "hebrew": "ואפילו כעסו על הבחור על חוסר נדיבותו. ואמרו לו:", - "expected_phonemes": "u ħ a t t a | z i ʕ l u | m i n | ʔ i ʃ aː b b | ʕ a l a | ʔ i l l e t | k a r a m o | u ʔ aː l uː l o" - }, - { - "voice_time_seconds": { - "start": 156.6, - "end": 161.6 - }, - "arabic": "«إنت بتخافش ربنا؟ بدّك تموّت الزّلمة عشان كف عدس؟", - "hebrew_transliteration": "אִנתֶ בִּתחַ'אפֶ-ש רַבְּנַא? בַּדַّכּ תְמַוֶّת אִ-זַּלַמֶה עַשַאן כַּףّ עַדַס?", - "hebrew": "אינך ירא את אלוהינו? אתה רוצה להרוג את האיש בגלל חופן עדשים?", - "expected_phonemes": "ʔ i n t e | b i t x aː f e ʃ | r a b n a | b a d d a k | t m a w w e t | ʔ i z a l a m e | ʕ a ʃ aː n | k a f f | ʕ a d a s" - }, - { - "voice_time_seconds": { - "start": 161.6, - "end": 164.6 - }, - "arabic": "يا عيب الشّوم، مش حرام عليك؟", - "hebrew_transliteration": "יַא עֵיבּ אִ-שּׁוּם, מִש חַרַאם עַלֵיכּ?", - "hebrew": "איזו בושה, אין לך אלוהים?", - "expected_phonemes": "j a | ʕ eː b | ʔ i ʃ uː m | m i ʃ | ħ a r aː m | ʕ a l eː k" - }, - { - "voice_time_seconds": { - "start": 164.6, - "end": 170.4 - }, - "arabic": "ولاده جوعانين وفش عنده شو يطعميهم... عيب، عيب عليك!»", - "hebrew_transliteration": "וְלַאדוֹ גוּעַאנִין וּפִש עִנדוֹ שוּ יְטַעְמִיהֹם... עֵיבּ, עֵיבּ עַלֵיכּ!", - "hebrew": "ילדיו רעבים ואין לו במה להאכיל אותם... בושה, בושה עליך!", - "expected_phonemes": "u l aː d o | g uː ʕ aː n iː n | u f i ʃ | ʕ i n d o | ʃ u | j tˤ a ʕ m iː h o m | ʕ eː b | ʕ eː b | ʕ a l eː k" - }, - { - "voice_time_seconds": { - "start": 170.4, - "end": 176.4 - }, - "arabic": "الشّابّ وطّى راسه وما عرفش شو يقول. شو يعني بدّه يقول؟...", - "hebrew_transliteration": "אִ-שַּׁאבّ וַטַّא רַאסוֹ וּמַא עִרֶפ-ש שוּ יְקוּל. שוּ יַעְנִי בַּדּוֹ יְקוּל?...", - "hebrew": "הבחור הרכין את ראשו ולא ידע מה לומר. מה בעצם יכול היה לומר?...", - "expected_phonemes": "ʔ i ʃ aː b b | w a tˤ tˤ a | r aː s o | u m a | ʕ i r e f ʃ | ʃ u | j ʔ uː l | ʃ u | j a ʕ n i | b a d o | j ʔ uː l" - }, - { - "voice_time_seconds": { - "start": 176.4, - "end": 181.2 - }, - "arabic": "لاقيته لهالزّلمة مع مرتي... فتت على بيتي ولاقيتهم سوا؟", - "hebrew_transliteration": "לַאקֵיתוֹ לַ-הַ-זַּלַמֶה מַע מַרְתִי, פֻתֶת עַלַא בֵּיתִי וּלַאקֵיתְהֹם סַוַא?", - "hebrew": "מצאתי את האיש הזה עם אשתי... נכנסתי לביתי ומצאתי אותם יחד?", - "expected_phonemes": "l aː ʔ eː t o | l a h a z a l a m e | m a ʕ | m a r t i | f u t e t | ʕ a l a | b eː t i | u l aː ʔ eː t h o m | s a w a" - }, - { - "voice_time_seconds": { - "start": 181.2, - "end": 187.0 - }, - "arabic": "عشان هيك سكت، هزّ بكتافه وقال بصوت واطي:", - "hebrew_transliteration": "עַשַאן הֵיכּ סַכַּת, הַזّ בִּכְּתַאפוֹ וּקַאל בְּצוֹת וַאטִי:", - "hebrew": "לכן שתק, משך בכתפיו ואמר בקול שקט:", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | s a k a t | h a z z | b i k t aː f o | u ʔ aː l | b sˤ oː t | w aː tˤ i" - }, - { - "voice_time_seconds": { - "start": 187.0, - "end": 193.4 - }, - "arabic": "«إلّي بعرف بعرف، وإلّي ما بعرف بيقول كف عدس...»", - "hebrew_transliteration": "אִלִّי בִּיעְרַף בִּיעְרַף, וּאִלִّי מַא בִּיעְרַף בִּיקוּל כַּףّ עַדַס...", - "hebrew": "מי שיודע יודע, ומי שלא יודע אומר חופן עדשים...", - "expected_phonemes": "ʔ i l l i | b iː ʕ r a f | b iː ʕ r a f | u ʔ i l l i | m a | b iː ʕ r a f | b iː ʔ uː l | k a f f | ʕ a d a s" - } - ] - }, - { - "file_name": "מצנפת השיח' * עמאמת א-שיח' * عمامة الشيخ * סיפור עממי בערבית מדוברת [YwB2fUlghxk].mkv", - "youtube_id": "YwB2fUlghxk", - "title_hebrew": "מצנפת השיח'", - "segments": [ - { - "voice_time_seconds": { - "start": 1.2, - "end": 5.6 - }, - "arabic": "عمامة الشيخ", - "hebrew_transliteration": "עַמַאמֶת אִ-שֵׁّיחַ'", - "hebrew": "מצנפת השייח'", - "expected_phonemes": "ʕ a m aː m e t | ʔ i ʃ ʃ eː x a" - }, - { - "voice_time_seconds": { - "start": 6.2, - "end": 8.4 - }, - "arabic": "الدّنيا كانت خريف،", - "hebrew_transliteration": "אִ-דֻּנְיַא כַּאנַת חַ'רִיף,", - "hebrew": "היה סתיו,", - "expected_phonemes": "ʔ i d u n j a | k aː n a t | x a r iː f" - }, - { - "voice_time_seconds": { - "start": 8.4, - "end": 12.6 - }, - "arabic": "والأيّام أيّام نهاية موسم جدّ الزّتون.", - "hebrew_transliteration": "וִל-אַיַّאם, אַיַّאם נִהַאיֶת מוֹסֶם גַ'דّ אִ-זֵּיתוּן.", - "hebrew": "והימים היו ימי סוף עונת מסיק הזיתים.", - "expected_phonemes": "w i l ʔ a j j aː m | ʔ a j j aː m | n i h aː j e t | m oː s e m | dʒ a d d | ʔ i z eː t uː n" - }, - { - "voice_time_seconds": { - "start": 12.6, - "end": 17.2 - }, - "arabic": "الشّيخ حاجّ أبو عمر كان لابس عباية جديدة،", - "hebrew_transliteration": "אִ-שֵׁّיחַ' חַאגّ אַבּוּ עֻמַר כַּאן לַאבֶּס עַבַּאיֶה גְ'דִידֶה,", - "hebrew": "השייח' חאג' אבו עומר לבש גלימה חדשה,", - "expected_phonemes": "ʔ i ʃ ʃ eː x a | ħ aː g g | ʔ a b u | ʕ u m a r | k aː n | l aː b e s | ʕ a b aː j e | dʒ d iː d e" - }, - { - "voice_time_seconds": { - "start": 17.2, - "end": 19.4 - }, - "arabic": "ودقنه كانت طويلة بيضا", - "hebrew_transliteration": "וּדַקנוֹ כַּאנַת טַוִילֶה בֵּידַא", - "hebrew": "וזקנו היה ארוך ולבן", - "expected_phonemes": "u d a ʔ n o | k aː n a t | tˤ a w iː l e | b eː dˤ a" - }, - { - "voice_time_seconds": { - "start": 19.4, - "end": 23.6 - }, - "arabic": "متل لون عمامته الكبيرة إلّي على راسه.", - "hebrew_transliteration": "מִתֶל לוֹן עַמַאמתוֹ לֶ-כְּבִּירֶה אִלִّי עַלַא רַאסוֹ.", - "hebrew": "כמו צבע המצנפת הגדולה שעל ראשו.", - "expected_phonemes": "m i t e l | l oː n | ʕ a m aː m t o | l e k b iː r e | ʔ i l l i | ʕ a l a | r aː s o" - }, - { - "voice_time_seconds": { - "start": 23.6, - "end": 27.2 - }, - "arabic": "ركب حماره واتوجّه لعكّا.", - "hebrew_transliteration": "רִכֶּבּ חמַארוֹ וּתוַגַּّה לַעַכַּא.", - "hebrew": "רכב על חמורו ופנה לעכו.", - "expected_phonemes": "r i k e b | ħ m aː r o | u t w a g g a | l a ʕ a k a" - }, - { - "voice_time_seconds": { - "start": 27.2, - "end": 29.8 - }, - "arabic": "وصل عكّا بنصّ النّهار،", - "hebrew_transliteration": "וִצֶל עַכַּא בְּנֻצّ אִ-נַّהַאר,", - "hebrew": "הגיע לעכו בצהרי היום,", - "expected_phonemes": "w i sˤ e l | ʕ a k a | b n u sˤ sˤ | ʔ i n n a h aː r" - }, - { - "voice_time_seconds": { - "start": 29.8, - "end": 33.8 - }, - "arabic": "وكان ناوي يبيع بضاعته هناك ويترزّق.", - "hebrew_transliteration": "וּכַּאן נַאוִי יְבִּיע בְּדַאעתוֹ הֻנַאכּ וִיתרַזַّק.", - "hebrew": "והתכוון למכור שם את סחורתו ולהתפרנס.", - "expected_phonemes": "u k aː n | n aː w i | j b iː ʕ | b d aː ʕ t o | h u n aː k | w iː t r a z z a ʔ" - }, - { - "voice_time_seconds": { - "start": 33.8, - "end": 38.6 - }, - "arabic": "الشّيخ كان بدّه يبيع زيت وزتون من خير السّنة.", - "hebrew_transliteration": "אִ-שֵׁّיחַ' כַּאן בַּדּוֹ יְבִּיע זֵית וּזֵיתוּן מִן חֵ'יר אִ-סַּנֶה.", - "hebrew": "השייח' רצה למכור שמן וזיתים מיבול השנה.", - "expected_phonemes": "ʔ i ʃ ʃ eː x a | k aː n | b a d o | j b iː ʕ | z eː t | u z eː t uː n | m i n | x eː r | ʔ i s a n e" - }, - { - "voice_time_seconds": { - "start": 38.6, - "end": 41.4 - }, - "arabic": "وهيك صار يدوّر على حدا", - "hebrew_transliteration": "וּהֵיכּ צַאר יְדַוֶּר עַלַא חַדַא", - "hebrew": "וכך התחיל לחפש מישהו", - "expected_phonemes": "u h eː k | sˤ aː r | j d a w e r | ʕ a l a | ħ a d a" - }, - { - "voice_time_seconds": { - "start": 41.4, - "end": 44.4 - }, - "arabic": "يشتري منّه في سوق المينا.", - "hebrew_transliteration": "יִשתְרִי מִנּוֹ פִי סוּק אִל-מִינַא.", - "hebrew": "שיקנה ממנו בשוק הנמל.", - "expected_phonemes": "j i ʃ t r i | m i n o | f i | s uː ʔ | i l m iː n a" - }, - { - "voice_time_seconds": { - "start": 44.4, - "end": 50.2 - }, - "arabic": "فجأة أجا لعنده فلّاح، منظره فقير مسكين،", - "hebrew_transliteration": "פַגְ'אַה אַגַ'א לַעִנדוֹ פַלַّאח, מַנזַרוֹ פַקִיר מִסכִּין,", - "hebrew": "פתאום בא אליו איכר, שנראה עני ומסכן,", - "expected_phonemes": "f a dʒ ʔ a | ʔ a dʒ a | l a ʕ i n d o | f a l l aː ħ | m a n z a r o | f a ʔ iː r | m i s k iː n" - }, - { - "voice_time_seconds": { - "start": 50.2, - "end": 52.4 - }, - "arabic": "كان ماسك بإيده مكتوب.", - "hebrew_transliteration": "כַּאן מַאסֶכּ בְּאִידוֹ מַכּתוּבּ.", - "hebrew": "שהחזיק בידו מכתב.", - "expected_phonemes": "k aː n | m aː s e k | b ʔ iː d o | m a k t uː b" - }, - { - "voice_time_seconds": { - "start": 52.4, - "end": 56.0 - }, - "arabic": "هالفلّاح اتطلّع على الشّيخ وقال:", - "hebrew_transliteration": "הַל-פַלַّאח אִתטַלַّע עַלַא אִ-שֵׁّיחַ' וּקַאל:", - "hebrew": "האיכר הזה הביט בשייח' ואמר:", - "expected_phonemes": "h a l f a l l aː ħ | ʔ i t tˤ a l l a ʕ | ʕ a l a | ʔ i ʃ ʃ eː x a | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 56.0, - "end": 60.2 - }, - "arabic": "«السّلام عليكم يا شيخ، بدّي منّك هالمعروف،", - "hebrew_transliteration": "אִ-סַּלַאמֻ עַלֵיכֻּם יַא שֵיחַ', בַּדִّי מִנַّכּ הַל-מַערוּף,", - "hebrew": "שלום עליכם, שייח', אני מבקש ממך טובה,", - "expected_phonemes": "ʔ i s a l aː m u | ʕ a l eː k u m | j a | ʃ eː x a | b a d d i | m i n n a k | h a l m a ʕ r uː f" - }, - { - "voice_time_seconds": { - "start": 60.2, - "end": 63.2 - }, - "arabic": "بعتولي مكتوب من مكتب الحاكم،", - "hebrew_transliteration": "בַּעַתוּ-לִי מַכּתוּבּ מִן מַכּתַבּ אִל-חַאכֶּם,", - "hebrew": "שלחו לי מכתב ממשרד המושל,", - "expected_phonemes": "b a ʕ a t uː l i | m a k t uː b | m i n | m a k t a b | i l ħ aː k e m" - }, - { - "voice_time_seconds": { - "start": 63.2, - "end": 67.4 - }, - "arabic": "وأنا يا شيخ لا بعرف أقرا ولا بعرف أكتب،", - "hebrew_transliteration": "וּאַנַא יַא שֵיחַ' לַא בַּערַף אַקרַא וְלַא בַּערַף אַכּתֶבּ,", - "hebrew": "ואני, שייח', לא יודע לקרוא ולא יודע לכתוב,", - "expected_phonemes": "u ʔ a n a | j a | ʃ eː x a | l aː | b a ʕ r a f | ʔ a ʔ r a | u l aː | b a ʕ r a f | ʔ a k t e b" - }, - { - "voice_time_seconds": { - "start": 67.4, - "end": 69.6 - }, - "arabic": "ومش ملاقي حدا يقرالي إيّاه،", - "hebrew_transliteration": "וּמִש מְלַאקִי חַדַא יִקרַא-לִי אִיַّאה,", - "hebrew": "ולא מוצא מי שיקרא לי אותו,", - "expected_phonemes": "u m i ʃ | m l aː ʔ i | ħ a d a | j i ʔ r aː l i | ʔ i j j aː h" - }, - { - "voice_time_seconds": { - "start": 69.6, - "end": 73.8 - }, - "arabic": "ممكن تساعدني وتقرالي هالمكتوب؟»", - "hebrew_transliteration": "מֻמכֶּן תְסַאעֶדנִי וּתִקרַא-לִי הַל-מַכּתוּבּ?", - "hebrew": "אתה יכול לעזור לי ולקרוא לי את המכתב הזה?", - "expected_phonemes": "m u m k e n | t s aː ʕ e d n i | u t i ʔ r aː l i | h a l m a k t uː b" - }, - { - "voice_time_seconds": { - "start": 73.8, - "end": 77.0 - }, - "arabic": "الشّيخ أبو عمر مسك المكتوب،", - "hebrew_transliteration": "אִ-שֵׁّיחַ' אַבּוּ עֻמַר מִסֶכּ אִל-מַכּתוּבּ,", - "hebrew": "השייח' אבו עומר אחז במכתב,", - "expected_phonemes": "ʔ i ʃ ʃ eː x a | ʔ a b u | ʕ u m a r | m i s e k | i l m a k t uː b" - }, - { - "voice_time_seconds": { - "start": 77.0, - "end": 79.4 - }, - "arabic": "وقلبه بإيده أكمّ مرّة.", - "hebrew_transliteration": "וּקַלַבּוֹ בְּאִידוֹ אַכַּם מַרַّה.", - "hebrew": "והפך אותו בידו כמה פעמים.", - "expected_phonemes": "u ʔ a l a b o | b ʔ iː d o | ʔ a k a m | m a r r a" - }, - { - "voice_time_seconds": { - "start": 79.4, - "end": 83.2 - }, - "arabic": "بعدين اتطلّع على الفلّاح وقال:", - "hebrew_transliteration": "בַּעדֵין אִתטַלַّע עַלַא אִל-פַלַّאח וּקַאל:", - "hebrew": "אחר כך הביט באיכר ואמר:", - "expected_phonemes": "b a ʕ d eː n | ʔ i t tˤ a l l a ʕ | ʕ a l a | i l f a l l aː ħ | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 83.2, - "end": 85.0 - }, - "arabic": "«والله متأسّف يا أخوي،", - "hebrew_transliteration": "וַאללַה מִתאַסֶّף יַא אַחוּי,", - "hebrew": "בחיי, אני מצטער, אחי,", - "expected_phonemes": "w aː l l a | m i t ʔ a s s e f | j a | ʔ a ħ uː i" - }, - { - "voice_time_seconds": { - "start": 85.0, - "end": 86.4 - }, - "arabic": "ما بقدرش أساعدك،", - "hebrew_transliteration": "מַא בַּקדַר-ש אַסַאעדַכּ,", - "hebrew": "אני לא יכול לעזור לך,", - "expected_phonemes": "m a | b a ʔ d a r ʃ | ʔ a s aː ʕ d a k" - }, - { - "voice_time_seconds": { - "start": 86.4, - "end": 89.8 - }, - "arabic": "أنا متلي متلك بعرفش أقرا.»", - "hebrew_transliteration": "אַנַא מִתלִי מִתלַכּ בַּערַפ-ש אַקרַא.", - "hebrew": "אני כמוך, לא יודע לקרוא.", - "expected_phonemes": "ʔ a n a | m i t l i | m i t l a k | b a ʕ r a f ʃ | ʔ a ʔ r a" - }, - { - "voice_time_seconds": { - "start": 89.8, - "end": 91.8 - }, - "arabic": "تعجّب الفلّاح وقال:", - "hebrew_transliteration": "תְעַגַّבּ אִל-פַלַّאח וּקַאל:", - "hebrew": "האיכר השתומם ואמר:", - "expected_phonemes": "t ʕ a g g a b | i l f a l l aː ħ | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 91.8, - "end": 95.0 - }, - "arabic": "«كيف يعني بتعرفش تقرا، كيف ممكن هيك", - "hebrew_transliteration": "כִּיף יַעְנִי בְּתִערַפ-ש תִקרַא, כִּיף מֻמכֶּן הֵיכּ", - "hebrew": "מה זאת אומרת אתה לא יודע לקרוא, איך זה ייתכן", - "expected_phonemes": "k iː f | j a ʕ n i | b t i ʕ r a f ʃ | t i ʔ r a | k iː f | m u m k e n | h eː k" - }, - { - "voice_time_seconds": { - "start": 95.0, - "end": 97.8 - }, - "arabic": "وفيه عمامة كبيرة على راسك؟»", - "hebrew_transliteration": "וּפִיה עַמַאמֶה כְּבִּירֶה עַלַא רַאסַכּ?", - "hebrew": "כשיש מצנפת גדולה על ראשך?", - "expected_phonemes": "u f iː h | ʕ a m aː m e | k b iː r e | ʕ a l a | r aː s a k" - }, - { - "voice_time_seconds": { - "start": 97.8, - "end": 101.2 - }, - "arabic": "الشّيخ أبو عمر زعل وانحرج،", - "hebrew_transliteration": "אִ-שֵׁّיחַ' אַבּוּ עֻמַר זִעֶל וּאִנחַרַג',", - "hebrew": "השייח' אבו עומר כעס והתבייש,", - "expected_phonemes": "ʔ i ʃ ʃ eː x a | ʔ a b u | ʕ u m a r | z i ʕ e l | u ʔ i n ħ a r a dʒ" - }, - { - "voice_time_seconds": { - "start": 101.2, - "end": 103.0 - }, - "arabic": "وقام العمامة عن راسه", - "hebrew_transliteration": "וּקַאם אִל-עַמַאמֶה עַן רַאסוֹ", - "hebrew": "והסיר את המצנפת מעל ראשו", - "expected_phonemes": "u ʔ aː m | i l ʕ a m aː m e | ʕ a n | r aː s o" - }, - { - "voice_time_seconds": { - "start": 103.0, - "end": 106.6 - }, - "arabic": "وحطّها على راس الفلّاح وقال بعصبيّة:", - "hebrew_transliteration": "וּחַטַّהַא עַלַא רַאס אִל-פַלַّאח וּקַאל בְּעַצַבִּיֶּה:", - "hebrew": "ושם אותה על ראש האיכר ואמר בכעס:", - "expected_phonemes": "u ħ a tˤ tˤ a h a | ʕ a l a | r aː s | i l f a l l aː ħ | u ʔ aː l | b ʕ a sˤ a b i j e" - }, - { - "voice_time_seconds": { - "start": 106.6, - "end": 109.6 - }, - "arabic": "«إذا العمامة هيّ إلّي بتعلّم النّاس القراءة،", - "hebrew_transliteration": "אִזַא אִל-עַמַאמֶה הִיֶّ אִלִّי בִּתעַלֶّם אִ-נַّאס אִל-קִרַאאַה,", - "hebrew": "אם המצנפת היא שמלמדת אנשים לקרוא,", - "expected_phonemes": "ʔ i z a | i l ʕ a m aː m e | h i j j e | ʔ i l l i | b i t ʕ a l l e m | i n n aː s | i l ʔ i r aː ʔ a" - }, - { - "voice_time_seconds": { - "start": 109.6, - "end": 112.2 - }, - "arabic": "تفضّل، هاي العمامة على راسك.»", - "hebrew_transliteration": "תְפַדַّל, הַאי אִל-עַמַאמֶה עַלַא רַאסַכּ.", - "hebrew": "בבקשה, הנה המצנפת על ראשך.", - "expected_phonemes": "t f a dˤ dˤ a l | h aː i | i l ʕ a m aː m e | ʕ a l a | r aː s a k" - } - ] - }, - { - "file_name": "משל האבנים * מת'ל ל-חג'ארה * مثل الحجارة * סיפור עממי בערבית מדוברת [4JfFhuQosMg].mkv", - "youtube_id": "4JfFhuQosMg", - "title_hebrew": "משל האבנים", - "segments": [ - { - "voice_time_seconds": { - "start": 2.0, - "end": 5.4 - }, - "arabic": "مثل الحجارة", - "hebrew_transliteration": "מַתַ'ל לֶ-חְגַ'ארַה", - "hebrew": "משל האבנים", - "expected_phonemes": "m a t a l | l e ħ dʒ aː r a" - }, - { - "voice_time_seconds": { - "start": 6.6, - "end": 9.8 - }, - "arabic": "كان القاضي عبدالله قاضي مشهور،", - "hebrew_transliteration": "כַּאן אִל-קַאדִי עַבְּדַאללַّה קַאדִי מַשהוּר,", - "hebrew": "השופט עבדאללה היה שופט מפורסם,", - "expected_phonemes": "k aː n | i l ʔ aː d i | ʕ a b d a l l a | ʔ aː d i | m a ʃ h uː r" - }, - { - "voice_time_seconds": { - "start": 9.8, - "end": 14.8 - }, - "arabic": "معروف بفطنته وذكائه في كلّ البلاد.", - "hebrew_transliteration": "מַערוּף בִּפִטְנִתוֹ וּדַכַּאאוֹ פִי כֻּלّ לֶ-בְּלַאד.", - "hebrew": "ידוע בפיקחותו ובחוכמתו בכל הארץ.", - "expected_phonemes": "m a ʕ r uː f | b i f i tˤ n i t o | u d a k aː aː o | f i | k u l l | l e b l aː d" - }, - { - "voice_time_seconds": { - "start": 14.8, - "end": 19.2 - }, - "arabic": "بيت القاضي عبدالله كان دايما مفتوح لكلّ البشر.", - "hebrew_transliteration": "בֵּית אִל-קַאדִי עַבְּדַאללַّה כַּאן דַאיְמַן מַפתוּח לַכֻּלّ אִל-בַּשַר.", - "hebrew": "ביתו של השופט עבדאללה היה תמיד פתוח לכל בני האדם.", - "expected_phonemes": "b eː t | i l ʔ aː d i | ʕ a b d a l l a | k aː n | d aː j m a n | m a f t uː ħ | l a k u l l | i l b a ʃ a r" - }, - { - "voice_time_seconds": { - "start": 19.2, - "end": 23.4 - }, - "arabic": "للكبير والصّغير، للغني والفقير،", - "hebrew_transliteration": "לַלְ-כְּבִּיר וִלְ-זְעִ'יר, לַל-עַ'נִי וִל-פַקִיר,", - "hebrew": "לגדול ולקטן, לעשיר ולעני,", - "expected_phonemes": "l a l k b iː r | w i l z ɣ iː r | l a l ɣ a n i | w i l f a ʔ iː r" - }, - { - "voice_time_seconds": { - "start": 23.4, - "end": 27.8 - }, - "arabic": "من الشّمال ومن الجّنوب، من الشّرق ومن الغرب.", - "hebrew_transliteration": "מִן אִ-שַּׁמַאל וּמִן אִ-גַּנוּבּ, מִן אִ-שַּׁרֶק וּמִן אִל-עַ'רבּ.", - "hebrew": "מהצפון ומהדרום, מהמזרח ומהמערב.", - "expected_phonemes": "m i n | ʔ i ʃ a m aː l | u m i n | ʔ i g a n uː b | m i n | ʔ i ʃ a r e ʔ | u m i n | i l ɣ a r b" - }, - { - "voice_time_seconds": { - "start": 27.8, - "end": 29.4 - }, - "arabic": "في يوم من الأيام", - "hebrew_transliteration": "פִי יוֹם מִן אִל-אַיַّאם", - "hebrew": "יום אחד", - "expected_phonemes": "f i | j oː m | m i n | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 29.4, - "end": 34.6 - }, - "arabic": "أجوا على بيت هالقاضي زلمتين طالبين مساعدته.", - "hebrew_transliteration": "אַגוּ עַלַא בֵּית הַל-קַאדִי זַלַמְתֵין טַאלְבִּין מֻסַאעַדתוֹ.", - "hebrew": "באו לבית השופט הזה שני אנשים שביקשו את עזרתו.", - "expected_phonemes": "ʔ a g u | ʕ a l a | b eː t | h a l ʔ aː d i | z a l a m t eː n | tˤ aː l b iː n | m u s aː ʕ a d t o" - }, - { - "voice_time_seconds": { - "start": 34.6, - "end": 40.2 - }, - "arabic": "قعدوا في بيته، شربوا قهوة وبلّشوا يحكوا مع القاضي.", - "hebrew_transliteration": "קַעַדוּ פִי בֵּיתוֹ, שִרבּוּ קַהוֶה וּבַּלַّשוּ יִחכּוּ מַע אִל-קַאדִי.", - "hebrew": "ישבו בביתו, שתו קפה והתחילו לדבר עם השופט.", - "expected_phonemes": "ʔ a ʕ a d u | f i | b eː t o | ʃ i r b u | ʔ a h w e | u b a l l a ʃ u | j i ħ k u | m a ʕ | i l ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 40.2, - "end": 42.4 - }, - "arabic": "الزّلمة الأوّل بلّش وقال:", - "hebrew_transliteration": "אִ-זַּלַמֶה לֶ-אַוַّל בַּלַّש וּקַאל:", - "hebrew": "האיש הראשון פתח ואמר:", - "expected_phonemes": "ʔ i z a l a m e | l e ʔ a w w a l | b a l l a ʃ | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 42.4, - "end": 46.2 - }, - "arabic": "«يا قاضي، أنا اليوم زلمة كبير،", - "hebrew_transliteration": "יַא קַאדִי, אַנַא אִל-יוֹם זַלַמֶה כְּבִּיר,", - "hebrew": "שופט, אני היום אדם זקן,", - "expected_phonemes": "j a | ʔ aː d i | ʔ a n a | i l j oː m | z a l a m e | k b iː r" - }, - { - "voice_time_seconds": { - "start": 46.2, - "end": 48.4 - }, - "arabic": "عملت في حياتي كتير،", - "hebrew_transliteration": "עִמִלֶת פִי חַיַאתִי כְּתִיר,", - "hebrew": "עשיתי הרבה בחיי,", - "expected_phonemes": "ʕ i m i l e t | f i | ħ a j aː t i | k t iː r" - }, - { - "voice_time_seconds": { - "start": 48.4, - "end": 52.2 - }, - "arabic": "ربّيت ولادي وعيّشتهم عيشة كريمة،", - "hebrew_transliteration": "רַבַּיֶּת וְלַאדִי וּעַיַّשתְהֹם עִישֶה כַּרִימֶה,", - "hebrew": "גידלתי את ילדיי ונתתי להם חיים מכובדים,", - "expected_phonemes": "r a b a j e t | u l aː d i | u ʕ a j j a ʃ t h o m | ʕ iː ʃ e | k a r iː m e" - }, - { - "voice_time_seconds": { - "start": 52.2, - "end": 56.4 - }, - "arabic": "بسّ خلال السّنين عملت خطايا كتيرة،", - "hebrew_transliteration": "בַּס חִ'לַאל אִ-סְּנִין עִמִלֶת חַטַאיַא כְּתִירֶה,", - "hebrew": "אבל במהלך השנים חטאתי חטאים רבים,", - "expected_phonemes": "b a s | x i l aː l | ʔ i s n iː n | ʕ i m i l e t | ħ a tˤ aː j a | k t iː r e" - }, - { - "voice_time_seconds": { - "start": 56.4, - "end": 59.6 - }, - "arabic": "كتيرة بسّ صغيرة.»", - "hebrew_transliteration": "כְּתִירֶה בַּס זְעִ'ירֶה.", - "hebrew": "רבים אבל קטנים.", - "expected_phonemes": "k t iː r e | b a s | z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 59.6, - "end": 64.8 - }, - "arabic": "وبعدها بلّش الزّلمة التّاني يحكي وقال:", - "hebrew_transliteration": "וּבַּעֶדהַא בַּלַّש אִ-זַּלַמֶה אִ-תַּאנִי יִחכִּי וּקַאל:", - "hebrew": "ואחר כך האיש השני התחיל לדבר ואמר:", - "expected_phonemes": "u b a ʕ e d h a | b a l l a ʃ | ʔ i z a l a m e | ʔ i t aː n i | j i ħ k i | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 64.8, - "end": 69.4 - }, - "arabic": "«أنا، يا سيدي، كلّ حياتي عشت في سلام مع النّاس.", - "hebrew_transliteration": "אַנַא, יַא סִידִי, כֻּלّ חַיַאתִי עִשֶת פִי סַלַאם מַע אִ-נַּאס.", - "hebrew": "אני, אדוני, כל חיי חייתי בשלום עם האנשים.", - "expected_phonemes": "ʔ a n a | j a | s iː d i | k u l l | ħ a j aː t i | ʕ i ʃ e t | f i | s a l aː m | m a ʕ | ʔ i n aː s" - }, - { - "voice_time_seconds": { - "start": 69.4, - "end": 72.4 - }, - "arabic": "دايما تصرّفت بأمانة وضمير،", - "hebrew_transliteration": "דַאיְמַן תְצַרַّפֶת בְּאַמַאנֶה וּדַ'מִיר,", - "hebrew": "תמיד התנהגתי ביושר ובמצפון,", - "expected_phonemes": "d aː j m a n | t sˤ a r r a f e t | b ʔ a m aː n e | u dˤ a m iː r" - }, - { - "voice_time_seconds": { - "start": 72.4, - "end": 76.4 - }, - "arabic": "إن كان في شغلي وإن كان في حياتي الخاصّة.", - "hebrew_transliteration": "אִן כַּאן פִי שֻעְ'לִי וּאִן כַּאן פִי חַיַאתִי אִל-חַ'אצַّה.", - "hebrew": "בין בעבודתי ובין בחיי הפרטיים.", - "expected_phonemes": "ʔ i n | k aː n | f i | ʃ u ɣ l i | u ʔ i n | k aː n | f i | ħ a j aː t i | i l x aː sˤ sˤ a" - }, - { - "voice_time_seconds": { - "start": 76.4, - "end": 81.8 - }, - "arabic": "بسّ مرّة بساعة ديق عملت خطيّة كبيرة كتير.»", - "hebrew_transliteration": "בַּס מַרַّה בְּסַאעַת דִיק עִמִלֶת חַ'טִיֶّה כְּבִּירֶה כְּתִיר.", - "hebrew": "אבל פעם, בשעת מצוקה, חטאתי חטא גדול מאוד.", - "expected_phonemes": "b a s | m a r r a | b s aː ʕ a t | d iː ʔ | ʕ i m i l e t | x a tˤ i j j e | k b iː r e | k t iː r" - }, - { - "voice_time_seconds": { - "start": 81.8, - "end": 84.2 - }, - "arabic": "وقالوا الزّلمتين للقاضي:", - "hebrew_transliteration": "וּקַאלוּ אִ-זַּלַמְתֵין לַל-קַאדִי:", - "hebrew": "ושני האנשים אמרו לשופט:", - "expected_phonemes": "u ʔ aː l u | ʔ i z a l a m t eː n | l a l ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 84.2, - "end": 86.6 - }, - "arabic": "«لهيك جينا لعندك يا سيدنا،", - "hebrew_transliteration": "לַהֵיכּ גִ'ינַא לַעִנדַכּ יַא סִידְנַא,", - "hebrew": "לכן באנו אליך, אדוננו,", - "expected_phonemes": "l a h eː k | dʒ iː n a | l a ʕ i n d a k | j a | s iː d n a" - }, - { - "voice_time_seconds": { - "start": 86.6, - "end": 91.8 - }, - "arabic": "بدّنا تساعدنا وتحكيلنا لمين منّا السّماح والغفران", - "hebrew_transliteration": "בַּדְّנַא תְסַאעֶדְנַא וּתחכִּי-לְנַא לַמִין מִנַّא אִ-סַּמַאח וִל-עֻ'פרַאן", - "hebrew": "אנחנו רוצים שתעזור לנו ותגיד לנו למי מאיתנו הסליחה והמחילה", - "expected_phonemes": "b a d d n a | t s aː ʕ e d n a | u t ħ k iː l n a | l a m iː n | m i n n a | ʔ i s a m aː ħ | w i l ɣ u f r aː n" - }, - { - "voice_time_seconds": { - "start": 91.8, - "end": 94.2 - }, - "arabic": "في يوم الدّين.»", - "hebrew_transliteration": "פִי יוֹם אִ-דִּין.", - "hebrew": "ביום הדין.", - "expected_phonemes": "f i | j oː m | ʔ i d iː n" - }, - { - "voice_time_seconds": { - "start": 94.2, - "end": 96.4 - }, - "arabic": "القاضي قالّهم:", - "hebrew_transliteration": "אִל-קַאדִי קַאל-לְהֹם:", - "hebrew": "השופט אמר להם:", - "expected_phonemes": "i l ʔ aː d i | ʔ aː l l h o m" - }, - { - "voice_time_seconds": { - "start": 96.4, - "end": 98.8 - }, - "arabic": "«بدّي منكم تطلعوا برّيت البيت", - "hebrew_transliteration": "בַּדִّי מִנְכֹּם תִטלַעוּ בַּרַّית אִל-בֵּית", - "hebrew": "אני רוצה שתצאו מחוץ לבית", - "expected_phonemes": "b a d d i | m i n k o m | t i tˤ l a ʕ u | b a r r a j t | i l b eː t" - }, - { - "voice_time_seconds": { - "start": 98.8, - "end": 102.8 - }, - "arabic": "وتجيبولي حجار بحجم خطاياكم...»", - "hebrew_transliteration": "וּתגִ'יבּוּ-לִי חְגַ'אר בְּחַגְ'ם חַטַאיַאכֹּם...", - "hebrew": "ותביאו לי אבנים בגודל חטאיכם...", - "expected_phonemes": "u t dʒ iː b uː l i | ħ dʒ aː r | b ħ a dʒ m | ħ a tˤ aː j aː k o m" - }, - { - "voice_time_seconds": { - "start": 102.8, - "end": 107.8 - }, - "arabic": "راح الأوّل جاب كتير حجار صغيرة،", - "hebrew_transliteration": "רַאח לֶ-אַוַّל גַ'אבּ כְּתִיר חְגַ'אר זְעִ'ירֶה,", - "hebrew": "הראשון הלך והביא הרבה אבנים קטנות,", - "expected_phonemes": "r aː ħ | l e ʔ a w w a l | dʒ aː b | k t iː r | ħ dʒ aː r | z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 107.8, - "end": 112.8 - }, - "arabic": "وأمّا التّاني جاب حجر واحد كبير كتير.", - "hebrew_transliteration": "וּאַמַّא אִ-תַּאנִי גַ'אבּ חַגַ'ר וַאחַד כְּבִּיר כְּתִיר.", - "hebrew": "ואילו השני הביא אבן אחת גדולה מאוד.", - "expected_phonemes": "u ʔ a m m a | ʔ i t aː n i | dʒ aː b | ħ a dʒ a r | w aː ħ a d | k b iː r | k t iː r" - }, - { - "voice_time_seconds": { - "start": 112.8, - "end": 116.0 - }, - "arabic": "بعد ما شاف القاضي شو جاب كلّ واحد،", - "hebrew_transliteration": "בַּעֶד מַא שַאף אִל-קַאדִי שוּ גַ'אבּ כֻּלّ וַאחַד,", - "hebrew": "אחרי שהשופט ראה מה הביא כל אחד,", - "expected_phonemes": "b a ʕ e d | m a | ʃ aː f | i l ʔ aː d i | ʃ u | dʒ aː b | k u l l | w aː ħ a d" - }, - { - "voice_time_seconds": { - "start": 116.0, - "end": 119.4 - }, - "arabic": "طلب منهم يروحوا ويرجّعوا الحجار", - "hebrew_transliteration": "טַלַבּ מִנְהֹם יְרוּחוּ וִירַגְّעוּ לֶ-חְגַ'אר", - "hebrew": "ביקש מהם ללכת ולהחזיר את האבנים", - "expected_phonemes": "tˤ a l a b | m i n h o m | j r uː ħ u | w iː r a g g ʕ u | l e ħ dʒ aː r" - }, - { - "voice_time_seconds": { - "start": 119.4, - "end": 123.2 - }, - "arabic": "على نفس المحل إلّي أخدوها منّه.", - "hebrew_transliteration": "עַלַא נַפֶס אִל-מַחַלّ אִלִّי אַחַ'דוּהַא מִנּוֹ.", - "hebrew": "לאותו מקום שממנו לקחו אותן.", - "expected_phonemes": "ʕ a l a | n a f e s | i l m a ħ a l l | ʔ i l l i | ʔ a x a d uː h a | m i n o" - }, - { - "voice_time_seconds": { - "start": 123.2, - "end": 126.6 - }, - "arabic": "الزّلمة إلّي كان معه حجر واحد كبير،", - "hebrew_transliteration": "אִ-זַּלַמֶה אִלִّי כַּאן מַעוֹ חַגַ'ר וַאחַד כְּבִּיר,", - "hebrew": "האיש שהייתה לו אבן אחת גדולה,", - "expected_phonemes": "ʔ i z a l a m e | ʔ i l l i | k aː n | m a ʕ o | ħ a dʒ a r | w aː ħ a d | k b iː r" - }, - { - "voice_time_seconds": { - "start": 126.6, - "end": 132.0 - }, - "arabic": "قدر بكلّ سهولة يلاقي محل الحجر ورجّعوا محلّه.", - "hebrew_transliteration": "קִדֶר בְּכֻּלّ סְהוּלֶה יְלַאקִי מַחַלّ אִל-חַגַ'ר וּרַגַّעוֹ מַחַלּוֹ.", - "hebrew": "הצליח בקלות למצוא את מקום האבן והחזיר אותה למקומה.", - "expected_phonemes": "ʔ i d e r | b k u l l | s h uː l e | j l aː ʔ i | m a ħ a l l | i l ħ a dʒ a r | u r a g g a ʕ o | m a ħ a l o" - }, - { - "voice_time_seconds": { - "start": 132.0, - "end": 135.2 - }, - "arabic": "أمّا هداك إلّي كان معه ملان حجار،", - "hebrew_transliteration": "אַמַّא הַדַאכּ אִלִّי כַּאן מַעוֹ מַלַאן חְגַ'אר,", - "hebrew": "ואילו ההוא שהיו לו המון אבנים,", - "expected_phonemes": "ʔ a m m a | h a d aː k | ʔ i l l i | k aː n | m a ʕ o | m a l aː n | ħ dʒ aː r" - }, - { - "voice_time_seconds": { - "start": 135.2, - "end": 139.8 - }, - "arabic": "ما عرف لوين يرجّع الحجار وشو يعمل.", - "hebrew_transliteration": "מַא עִרֶף לַוֵין יְרַגֶّע לֶ-חְגַ'אר וּשוּ יַעמַל.", - "hebrew": "לא ידע לאן להחזיר את האבנים ומה לעשות.", - "expected_phonemes": "m a | ʕ i r e f | l a w eː n | j r a g g e ʕ | l e ħ dʒ aː r | u ʃ u | j a ʕ m a l" - }, - { - "voice_time_seconds": { - "start": 139.8, - "end": 143.8 - }, - "arabic": "القاضي شاف هاد الإشي وحكالهم:", - "hebrew_transliteration": "אִל-קַאדִי שַאף הַאד אִל-אִשִי וּחַכַא-לְהֹם:", - "hebrew": "השופט ראה את הדבר הזה ואמר להם:", - "expected_phonemes": "i l ʔ aː d i | ʃ aː f | h aː d | i l ʔ i ʃ i | u ħ a k aː l h o m" - }, - { - "voice_time_seconds": { - "start": 143.8, - "end": 147.4 - }, - "arabic": "«الحجار إلّي جبتوها هيّ خطاياكم.", - "hebrew_transliteration": "לֶ-חְגַ'אר אִלִّי גִ'בְּתוּהַא הִיֶّ חַטַאיַאכֹּם.", - "hebrew": "האבנים שהבאתם הן חטאיכם.", - "expected_phonemes": "l e ħ dʒ aː r | ʔ i l l i | dʒ i b t uː h a | h i j j e | ħ a tˤ aː j aː k o m" - }, - { - "voice_time_seconds": { - "start": 147.4, - "end": 150.4 - }, - "arabic": "إلّي خطاياه معدودة بسّ كبيرة", - "hebrew_transliteration": "אִלִّי חַטַאיַאה מַעדוּדֶה בַּס כְּבִּירֶה", - "hebrew": "מי שחטאיו מעטים אבל גדולים", - "expected_phonemes": "ʔ i l l i | ħ a tˤ aː j aː h | m a ʕ d uː d e | b a s | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 150.4, - "end": 153.8 - }, - "arabic": "بعرف منيح كيف يكفّر عنها.", - "hebrew_transliteration": "בִּיעְרַף מְנִיח כִּיף יְכַפֶّר עַנְהַא.", - "hebrew": "יודע היטב איך לכפר עליהם.", - "expected_phonemes": "b iː ʕ r a f | m n iː ħ | k iː f | j k a f f e r | ʕ a n h a" - }, - { - "voice_time_seconds": { - "start": 153.8, - "end": 157.6 - }, - "arabic": "بسّ إلّي خطاياه كتيرة وصغيرة", - "hebrew_transliteration": "בַּס אִלִّי חַטַאיַאה כְּתִירֶה וּזְעִ'ירֶה", - "hebrew": "אבל מי שחטאיו רבים וקטנים", - "expected_phonemes": "b a s | ʔ i l l i | ħ a tˤ aː j aː h | k t iː r e | u z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 157.6, - "end": 161.8 - }, - "arabic": "من مين ومين بدّه الكفّارة والغفران؟»", - "hebrew_transliteration": "מִן מִין וּמִין בַּדּוֹ אִל-כַּפַّארַה וִל-עֻ'פרַאן?", - "hebrew": "ממי וממי יבקש כפרה ומחילה?", - "expected_phonemes": "m i n | m iː n | u m iː n | b a d o | i l k a f f aː r a | w i l ɣ u f r aː n" - } - ] - }, - { - "file_name": "נוח מביט אל מחוץ לתיבה * נוּח יִתטָלָע בַּרִית אִ-סַפִינֶה * نوح يتطلّع برّيت السّفينة [zW0_KoHjUHQ].webm", - "youtube_id": "zW0_KoHjUHQ", - "title_hebrew": "נוח מביט אל מחוץ לתיבה", - "segments": [ - { - "voice_time_seconds": { - "start": 2.0, - "end": 8.2 - }, - "arabic": "نوح يتطلّع برّيت السّفينة", - "hebrew_transliteration": "נוּח יִתטַלַّע בַּרַّית אִ-סַּפִינֶה", - "hebrew": "נוח מביט אל מחוץ לתיבה", - "expected_phonemes": "n uː ħ | j i t tˤ a l l a ʕ | b a r r a j t | ʔ i s a f iː n e" - }, - { - "voice_time_seconds": { - "start": 8.2, - "end": 11.2 - }, - "arabic": "النّبي نوح كان بالسّفينة.", - "hebrew_transliteration": "אִ-נַּבִּי נוּח כַּאן בִּ-סַּפִינֶה.", - "hebrew": "הנביא נוח היה בתיבה.", - "expected_phonemes": "ʔ i n a b i | n uː ħ | k aː n | b i s a f iː n e" - }, - { - "voice_time_seconds": { - "start": 11.2, - "end": 13.0 - }, - "arabic": "إتطلّع من الشّبّاك", - "hebrew_transliteration": "אִתטַלַّע מִן אִ-שֻּׁבַּאכּ", - "hebrew": "הביט מהחלון", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | m i n | ʔ i ʃ u b aː k" - }, - { - "voice_time_seconds": { - "start": 13.0, - "end": 16.2 - }, - "arabic": "وشاف إنّه كلّ شي برّا هادي.", - "hebrew_transliteration": "וּשַאף אִנּוֹ כֻּלّ שִי בַּרַّא הַאדִי.", - "hebrew": "וראה שהכול בחוץ שקט.", - "expected_phonemes": "u ʃ aː f | ʔ i n o | k u l l | ʃ i | b a r r a | h aː d i" - }, - { - "voice_time_seconds": { - "start": 16.2, - "end": 19.6 - }, - "arabic": "وبعدها جاب الغراب وقالّه:", - "hebrew_transliteration": "וּבַּעֶדהַא גַ'אבּ אִל-עֻ'רַאבּ וּקַאל-לוֹ:", - "hebrew": "ואחר כך הביא את העורב ואמר לו:", - "expected_phonemes": "u b a ʕ e d h a | dʒ aː b | i l ɣ u r aː b | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 19.6, - "end": 21.2 - }, - "arabic": "«روح يا غراب،", - "hebrew_transliteration": "רוּח יַא עֻ'רַאבּ,", - "hebrew": "לך, עורב,", - "expected_phonemes": "r uː ħ | j a | ɣ u r aː b" - }, - { - "voice_time_seconds": { - "start": 21.2, - "end": 24.0 - }, - "arabic": "وجيبلي إشي أخضر من برّا.", - "hebrew_transliteration": "וּגִ'יבּ-לִי אִשִי אַחְ'דַ'ר מִן בַּרַّא.", - "hebrew": "והבא לי משהו ירוק מבחוץ.", - "expected_phonemes": "u dʒ iː b l i | ʔ i ʃ i | ʔ a x dˤ a r | m i n | b a r r a" - }, - { - "voice_time_seconds": { - "start": 24.0, - "end": 27.4 - }, - "arabic": "إذا لقيت نبتة، جيبلي إيّاها.»", - "hebrew_transliteration": "אִזַא לַקֵית נַבְּתֶה, גִ'יבּ-לִי אִיַّאהַא.", - "hebrew": "אם תמצא צמח, הבא לי אותו.", - "expected_phonemes": "ʔ i z a | l a ʔ eː t | n a b t e | dʒ iː b l i | ʔ i j j aː h a" - }, - { - "voice_time_seconds": { - "start": 27.4, - "end": 31.0 - }, - "arabic": "الغراب غاب لفترة طويلة،", - "hebrew_transliteration": "אִל-עֻ'רַאבּ עַ'אבּ לַפַתרַה טַוִילֶה,", - "hebrew": "העורב נעדר לזמן רב,", - "expected_phonemes": "i l ɣ u r aː b | ɣ aː b | l a f a t r a | tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 31.0, - "end": 32.8 - }, - "arabic": "ولمّا رجع أخيرا", - "hebrew_transliteration": "וּלַמַّא רִגֶ'ע אַחִ'ירַן", - "hebrew": "וכשסוף סוף חזר", - "expected_phonemes": "u l a m m a | r i dʒ e ʕ | ʔ a x iː r a n" - }, - { - "voice_time_seconds": { - "start": 32.8, - "end": 35.2 - }, - "arabic": "وكان منقاره فاضي،", - "hebrew_transliteration": "וּכַּאן מִנקַארוֹ פַאדִ'י,", - "hebrew": "מקורו היה ריק,", - "expected_phonemes": "u k aː n | m i n ʔ aː r o | f aː dˤ i" - }, - { - "voice_time_seconds": { - "start": 35.2, - "end": 36.8 - }, - "arabic": "قالّه النّبي نوح:", - "hebrew_transliteration": "קַאל-לוֹ אִ-נַּבִּי נוּח:", - "hebrew": "הנביא נוח אמר לו:", - "expected_phonemes": "ʔ aː l l o | ʔ i n a b i | n uː ħ" - }, - { - "voice_time_seconds": { - "start": 36.8, - "end": 39.2 - }, - "arabic": "«ريته الله يجعلك أسود", - "hebrew_transliteration": "רֵיתוֹ אַללַה יִגְ'עַלַכּ אַסוַד", - "hebrew": "הלוואי שאלוהים יעשה אותך שחור", - "expected_phonemes": "r eː t o | i l l a | j i dʒ ʕ a l a k | ʔ a s w a d" - }, - { - "voice_time_seconds": { - "start": 39.2, - "end": 41.2 - }, - "arabic": "متل سواد اللّيل،", - "hebrew_transliteration": "מִתֶל סַוַאד אִ-לֵّיל,", - "hebrew": "כמו שחור הלילה,", - "expected_phonemes": "m i t e l | s a w aː d | ʔ i l l eː l" - }, - { - "voice_time_seconds": { - "start": 41.2, - "end": 44.4 - }, - "arabic": "ورح تضلّ تعبّي ميّ بهالغربال", - "hebrew_transliteration": "וּרַח-תְדַ'לّ תְעַבִّי מַיّ בְּהַל-עֻ'רבַּאל", - "hebrew": "ותמשיך למלא מים בנפה הזאת", - "expected_phonemes": "u r a ħ t dˤ a l l | t ʕ a b b i | m a j j | b h a l ɣ u r b aː l" - }, - { - "voice_time_seconds": { - "start": 44.4, - "end": 46.2 - }, - "arabic": "لحتّى يتملّى.»", - "hebrew_transliteration": "לַחַתַّא יִתמַלַّא.", - "hebrew": "עד שתתמלא.", - "expected_phonemes": "l a ħ a t t a | j i t m a l l a" - }, - { - "voice_time_seconds": { - "start": 46.2, - "end": 50.0 - }, - "arabic": "وهيك، الغراب بعده بعبّي ميّ", - "hebrew_transliteration": "וּהֵיכּ, אִל-עֻ'רַאבּ בַּעדוֹ בִּיעַבִّי מַיّ", - "hebrew": "וכך, העורב עדיין ממלא מים", - "expected_phonemes": "u h eː k | i l ɣ u r aː b | b a ʕ d o | b iː ʕ a b b i | m a j j" - }, - { - "voice_time_seconds": { - "start": 50.0, - "end": 53.0 - }, - "arabic": "لحتّى يملّي الغربال، وبقول:", - "hebrew_transliteration": "לַחַתַّא יִמַלִّי אִל-עֻ'רבַּאל, וּבִּיקוּל:", - "hebrew": "כדי למלא את הנפה, ואומר:", - "expected_phonemes": "l a ħ a t t a | j i m a l l i | i l ɣ u r b aː l | u b iː ʔ uː l" - }, - { - "voice_time_seconds": { - "start": 53.0, - "end": 56.6 - }, - "arabic": "«كواك، كواك، مليّت، مليّت.»", - "hebrew_transliteration": "כְּוַאכּ, כְּוַאכּ, מַלֵّית, מַלֵّית.", - "hebrew": "קוואכ, קוואכ, מילאתי, מילאתי.", - "expected_phonemes": "k w aː k | k w aː k | m a l l eː t | m a l l eː t" - }, - { - "voice_time_seconds": { - "start": 56.6, - "end": 60.6 - }, - "arabic": "بسّ ما ببقى إشي في الغربال.", - "hebrew_transliteration": "בַּס מַא בִּיבְקַא אִשִי פִי אִל-עֻ'רבַּאל.", - "hebrew": "אבל לא נשאר דבר בנפה.", - "expected_phonemes": "b a s | m a | b iː b ʔ a | ʔ i ʃ i | f i | i l ɣ u r b aː l" - }, - { - "voice_time_seconds": { - "start": 60.6, - "end": 64.4 - }, - "arabic": "بعدها النّبي نوح بعث الحمامة.", - "hebrew_transliteration": "בַּעֶדהַא אִ-נַּבִּי נוּח בַּעַת אִל-חַמַאמֶה.", - "hebrew": "אחר כך הנביא נוח שלח את היונה.", - "expected_phonemes": "b a ʕ e d h a | ʔ i n a b i | n uː ħ | b a ʕ a t | i l ħ a m aː m e" - }, - { - "voice_time_seconds": { - "start": 64.4, - "end": 69.2 - }, - "arabic": "راحت ورجعت وبمنقارها حشيش أخضر.", - "hebrew_transliteration": "רַאחַת וּרִגְ'עַת וּבְּמִנקַארְהַא חַשִיש אַחְ'דַ'ר.", - "hebrew": "היא הלכה וחזרה ובמקורה עשב ירוק.", - "expected_phonemes": "r aː ħ a t | u r i dʒ ʕ a t | u b m i n ʔ aː r h a | ħ a ʃ iː ʃ | ʔ a x dˤ a r" - }, - { - "voice_time_seconds": { - "start": 69.2, - "end": 70.6 - }, - "arabic": "النّبي قالّها:", - "hebrew_transliteration": "אִ-נַּבִּי קַאל-לְהַא:", - "hebrew": "הנביא אמר לה:", - "expected_phonemes": "ʔ i n a b i | ʔ aː l l h a" - }, - { - "voice_time_seconds": { - "start": 70.6, - "end": 73.2 - }, - "arabic": "«ريته الله يبيّض وجهك،", - "hebrew_transliteration": "רֵיתוֹ אַללַה יְבַּיֶّד' וִגְ'הַכּ,", - "hebrew": "הלוואי שאלוהים ילבין את פנייך,", - "expected_phonemes": "r eː t o | i l l a | j b a j j e dˤ | w i dʒ h a k" - }, - { - "voice_time_seconds": { - "start": 73.2, - "end": 77.2 - }, - "arabic": "ويرزقك بزوج زغاليل كلّ شهر!»", - "hebrew_transliteration": "וִירזְקַכּ בְּזוֹג' זַעַ'אלִיל כֻּלّ שַהֶר!", - "hebrew": "ויעניק לך זוג גוזלים בכל חודש!", - "expected_phonemes": "w iː r z ʔ a k | b z oː dʒ | z a ɣ aː l iː l | k u l l | ʃ a h e r" - }, - { - "voice_time_seconds": { - "start": 77.2, - "end": 81.0 - }, - "arabic": "وبعدها حطّ النّبي حنّا على إجرها،", - "hebrew_transliteration": "וּבַּעֶדהַא חַטّ אִ-נַּבִּי חִנַّא עַלַא אִגְ'רְהַא,", - "hebrew": "ואחר כך הנביא שם חינה על רגלה,", - "expected_phonemes": "u b a ʕ e d h a | ħ a tˤ tˤ | ʔ i n a b i | ħ i n n a | ʕ a l a | ʔ i dʒ r h a" - }, - { - "voice_time_seconds": { - "start": 81.0, - "end": 89.4 - }, - "arabic": "ولهاليوم الحنّا موجودة على إجر الحمامة.", - "hebrew_transliteration": "וּלַהַל-יוֹם אִל-חִנַّא מַוְגּ'וּדֶה עַלַא אִגְ'ר אִל-חַמַאמֶה.", - "hebrew": "ועד היום החינה נמצאת על רגל היונה.", - "expected_phonemes": "u l a h a l j oː m | i l ħ i n n a | m a w dʒ uː d e | ʕ a l a | ʔ i dʒ r | i l ħ a m aː m e" - } - ] - }, - { - "file_name": "שיחה בין בני זוג * מֻחַאדתַ'ה בֵּין זוֹג' * محادثة بين زوج * שיחה בערבית מדוברת [T44fUdx1O90].webm", - "youtube_id": "T44fUdx1O90", - "title_hebrew": "שיחה בין בני זוג", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "محادثة بين زوج", - "hebrew_transliteration": "מֻחַאדַתַ'ה בֵּין זוֹג'", - "hebrew": "שיחה בין בני זוג", - "expected_phonemes": "m u ħ aː d a t a | b eː n | z oː dʒ" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 8.8 - }, - "arabic": "شو يا روحي كيفك؟", - "hebrew_transliteration": "שוּ יַא רוּחִי כִּיפַכּ?", - "hebrew": "מה נשמע, נשמתי, מה שלומך?", - "expected_phonemes": "ʃ u | j a | r uː ħ i | k iː f a k" - }, - { - "voice_time_seconds": { - "start": 8.8, - "end": 12.0 - }, - "arabic": "بسيدر حبيبي، عادي، شو الوضع عندك؟", - "hebrew_transliteration": "בְּסֵידֶר חַבִּיבִּי, עַאדִי, שוּ אִל-וַדְע עִנדַכּ?", - "hebrew": "בסדר, אהובי, רגיל, מה המצב אצלך?", - "expected_phonemes": "b s eː d e r | ħ a b iː b i | ʕ aː d i | ʃ u | i l w a d ʕ | ʕ i n d a k" - }, - { - "voice_time_seconds": { - "start": 12.0, - "end": 16.4 - }, - "arabic": "بالشّغل، كتير مضغوط.", - "hebrew_transliteration": "בִּ-שֻּׁעְ'ל, כְּתִיר מַדְעְ'וּט.", - "hebrew": "בעבודה, לחוץ מאוד.", - "expected_phonemes": "b i ʃ u ɣ l | k t iː r | m a d ɣ uː tˤ" - }, - { - "voice_time_seconds": { - "start": 16.4, - "end": 19.2 - }, - "arabic": "مش قادر أرفع راسي من كتر الشّغل.", - "hebrew_transliteration": "מִש קַאדֶר אַרפַע רַאסִי מִן כֻּתְר אִ-שֻּׁעְ'ל.", - "hebrew": "לא מסוגל להרים את הראש מרוב עבודה.", - "expected_phonemes": "m i ʃ | ʔ aː d e r | ʔ a r f a ʕ | r aː s i | m i n | k u t r | ʔ i ʃ u ɣ l" - }, - { - "voice_time_seconds": { - "start": 19.2, - "end": 20.4 - }, - "arabic": "والمشكلة إنّه هدا الوضع", - "hebrew_transliteration": "וִל-מֻשכִּלֶה אִנּוֹ הַאדַא אִל-וַדְע", - "hebrew": "והבעיה היא שהמצב הזה", - "expected_phonemes": "w i l m u ʃ k i l e | ʔ i n o | h aː d a | i l w a d ʕ" - }, - { - "voice_time_seconds": { - "start": 20.4, - "end": 23.2 - }, - "arabic": "مش رح يخفّ قبل آخر الشّهر...", - "hebrew_transliteration": "מִש רַח-יְחִ'ףّ קַבֶּל אַאחַ'ר אִ-שַּׁהֶר...", - "hebrew": "לא יוקל לפני סוף החודש...", - "expected_phonemes": "m i ʃ | r a ħ j x i f f | ʔ a b e l | ʔ aː x a r | ʔ i ʃ a h e r" - }, - { - "voice_time_seconds": { - "start": 23.2, - "end": 27.0 - }, - "arabic": "يلّا معليش هانت، كلّها كمان جمعتين.", - "hebrew_transliteration": "יַלַّא מַעלֵיש הַאנַת, כֻּלְّהַא כַּמַאן גֻ'מעַתֵין.", - "hebrew": "יאללה, לא נורא, נשאר מעט, עוד שבועיים בסך הכול.", - "expected_phonemes": "j a l l a | m a ʕ l eː ʃ | h aː n a t | k u l l h a | k a m aː n | dʒ u m ʕ a t eː n" - }, - { - "voice_time_seconds": { - "start": 27.0, - "end": 31.2 - }, - "arabic": "آه، بهالجمعتين رح تطلع روحي.", - "hebrew_transliteration": "אַה, בְּהַל-גֻ'מעַתֵין רַח-תִטלַע רוּחִי.", - "hebrew": "כן, בשבועיים האלה תצא נשמתי.", - "expected_phonemes": "ʔ aː | b h a l dʒ u m ʕ a t eː n | r a ħ t i tˤ l a ʕ | r uː ħ i" - }, - { - "voice_time_seconds": { - "start": 31.2, - "end": 33.6 - }, - "arabic": "شو غيره؟ شو عليكي اليوم؟", - "hebrew_transliteration": "שוּ עֵ'ירוֹ? שוּ עַלֵיכִּי אִל-יוֹם?", - "hebrew": "מה עוד? מה יש לך היום?", - "expected_phonemes": "ʃ u | ɣ eː r o | ʃ u | ʕ a l eː k i | i l j oː m" - }, - { - "voice_time_seconds": { - "start": 33.6, - "end": 35.6 - }, - "arabic": "شو كنّك نسيت؟", - "hebrew_transliteration": "שוּ כֻּנַّכּ נְסִית?", - "hebrew": "מה, שכחת?", - "expected_phonemes": "ʃ u | k u n n a k | n s iː t" - }, - { - "voice_time_seconds": { - "start": 35.6, - "end": 38.2 - }, - "arabic": "كمان شويّ طالعة على دكتور لسنان.", - "hebrew_transliteration": "כַּמַאן שְוַיّ טַאלְעַה עַלַא דַכּתוֹר לֶ-סְנַאן.", - "hebrew": "עוד מעט אני יוצאת לרופא השיניים.", - "expected_phonemes": "k a m aː n | ʃ w a j j | tˤ aː l ʕ a | ʕ a l a | d a k t oː r | l e s n aː n" - }, - { - "voice_time_seconds": { - "start": 38.2, - "end": 40.8 - }, - "arabic": "عشان هيك أخدت خوفش اليوم.", - "hebrew_transliteration": "עַשַאן הֵיכּ אַחַ'דֶת חוֹפֶש אִל-יוֹם.", - "hebrew": "לכן לקחתי היום חופש.", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | ʔ a x a d e t | ħ oː f e ʃ | i l j oː m" - }, - { - "voice_time_seconds": { - "start": 40.8, - "end": 43.2 - }, - "arabic": "آه مزبوط راح عن بالي.", - "hebrew_transliteration": "אַה מַזבּוּט רַאח עַן בַּאלִי.", - "hebrew": "אה נכון, שכחתי.", - "expected_phonemes": "ʔ aː | m a z b uː tˤ | r aː ħ | ʕ a n | b aː l i" - }, - { - "voice_time_seconds": { - "start": 43.2, - "end": 45.8 - }, - "arabic": "وبعد الدّكتور لازم أمرق عند ستّي.", - "hebrew_transliteration": "וּבַּעֶד אִ-דַּכּתוֹר לַאזֶם אַמרֹק עִנד סִתִّי.", - "hebrew": "ואחרי הרופא אני צריכה לקפוץ אל סבתא שלי.", - "expected_phonemes": "u b a ʕ e d | ʔ i d a k t oː r | l aː z e m | ʔ a m r o ʔ | ʕ i n d | s i t t i" - }, - { - "voice_time_seconds": { - "start": 45.8, - "end": 46.8 - }, - "arabic": "حكت معي مبارح،", - "hebrew_transliteration": "חַכַּת מַעִי מְבַּארֶח,", - "hebrew": "היא דיברה איתי אתמול,", - "expected_phonemes": "ħ a k a t | m a ʕ i | m b aː r e ħ" - }, - { - "voice_time_seconds": { - "start": 46.8, - "end": 49.0 - }, - "arabic": "عاملة قراص بسلق وبسبانخ،", - "hebrew_transliteration": "עַאמְלֶה קְרַאץ בְּסִלֶק וּבְּסַבַּאנֶחַ',", - "hebrew": "היא הכינה מאפים במנגולד ובתרד,", - "expected_phonemes": "ʕ aː m l e | ʔ r aː sˤ | b s i l e ʔ | u b s a b aː n e x a" - }, - { - "voice_time_seconds": { - "start": 49.0, - "end": 51.2 - }, - "arabic": "وضبّبتلنا حصّتنا.", - "hebrew_transliteration": "וּדַ'בַּבַּת-לְנַא חִצַّתנַא.", - "hebrew": "ושמרה לנו את החלק שלנו.", - "expected_phonemes": "u dˤ a b a b a t l n a | ħ i sˤ sˤ a t n a" - }, - { - "voice_time_seconds": { - "start": 51.2, - "end": 52.2 - }, - "arabic": "بدّي أمرق عليها،", - "hebrew_transliteration": "בַּדִّי אַמרֹק עַלֵיהַא,", - "hebrew": "אני רוצה לקפוץ אליה,", - "expected_phonemes": "b a d d i | ʔ a m r o ʔ | ʕ a l eː h a" - }, - { - "voice_time_seconds": { - "start": 52.2, - "end": 54.0 - }, - "arabic": "أشرب معها فنجان قهوة،", - "hebrew_transliteration": "אַשרַבּ מַעַהַא פִנגַ'אן קַהוֶה,", - "hebrew": "לשתות איתה כוס קפה,", - "expected_phonemes": "ʔ a ʃ r a b | m a ʕ a h a | f i n dʒ aː n | ʔ a h w e" - }, - { - "voice_time_seconds": { - "start": 54.0, - "end": 56.0 - }, - "arabic": "وآخد لقراص.", - "hebrew_transliteration": "וּאַאחֹ'ד לֶ-קְרַאץ.", - "hebrew": "ולקחת את המאפים.", - "expected_phonemes": "u ʔ aː x u d | l e ʔ r aː sˤ" - }, - { - "voice_time_seconds": { - "start": 56.0, - "end": 58.6 - }, - "arabic": "الله يخلّيها، سلّميلي عليها.", - "hebrew_transliteration": "אַללַה יְחַלִّיהַא, סַלְّמִי-לִי עַלֵיהַא.", - "hebrew": "שאלוהים ישמור עליה, תמסרי לה דרישת שלום ממני.", - "expected_phonemes": "i l l a | j ħ a l l iː h a | s a l l m iː l i | ʕ a l eː h a" - }, - { - "voice_time_seconds": { - "start": 58.6, - "end": 61.0 - }, - "arabic": "سلمت. وبعدين بدّي أروح أجيب", - "hebrew_transliteration": "סַלֶמֶת. וּבַּעדֵין בַּדִّי אַרוּח אַגִ'יבּ", - "hebrew": "תודה. ואחר כך אני רוצה ללכת להביא", - "expected_phonemes": "s a l e m e t | u b a ʕ d eː n | b a d d i | ʔ a r uː ħ | ʔ a dʒ iː b" - }, - { - "voice_time_seconds": { - "start": 61.0, - "end": 63.6 - }, - "arabic": "الحرامات من المغسلة.", - "hebrew_transliteration": "לֶ-חְרַאמַאת מִן אִל-מַעְ'סַלֶה.", - "hebrew": "את השמיכות מהמכבסה.", - "expected_phonemes": "l e ħ r aː m aː t | m i n | i l m a ɣ s a l e" - }, - { - "voice_time_seconds": { - "start": 63.6, - "end": 66.4 - }, - "arabic": "طيّب وإسأليه وينتا بخلّص جاكيتي.", - "hebrew_transliteration": "טַיֶّבּ וּאִסאַלִיה וֵינתַא בִּיחְ'לַץ גַ'אכֵּיתִי.", - "hebrew": "טוב, ותשאלי אותו מתי הז'קט שלי יהיה מוכן.", - "expected_phonemes": "tˤ a j j e b | u ʔ i s ʔ a l iː h | w eː n t a | b iː x l a sˤ | dʒ aː k eː t i" - }, - { - "voice_time_seconds": { - "start": 66.4, - "end": 68.6 - }, - "arabic": "أفكّر رح يكون جاهز مع الحرامات،", - "hebrew_transliteration": "אַפַכַּר רַח-יְכוּן גַ'אהֶז מַע לֶ-חְרַאמַאת,", - "hebrew": "אני חושבת שהוא יהיה מוכן עם השמיכות,", - "expected_phonemes": "ʔ a f a k a r | r a ħ j k uː n | dʒ aː h e z | m a ʕ | l e ħ r aː m aː t" - }, - { - "voice_time_seconds": { - "start": 68.6, - "end": 71.4 - }, - "arabic": "على كلّ حال بشوف.", - "hebrew_transliteration": "עַלַא כֻּלّ חַאל בַּשוּף.", - "hebrew": "בכל מקרה אראה.", - "expected_phonemes": "ʕ a l a | k u l l | ħ aː l | b a ʃ uː f" - }, - { - "voice_time_seconds": { - "start": 71.4, - "end": 73.2 - }, - "arabic": "طيّب. أه، قوليلي،", - "hebrew_transliteration": "טַיֶّבּ. אַה, קוּלִי-לִי,", - "hebrew": "טוב. אה, תגידי לי,", - "expected_phonemes": "tˤ a j j e b | ʔ aː | ʔ uː l iː l i" - }, - { - "voice_time_seconds": { - "start": 73.2, - "end": 75.6 - }, - "arabic": "شو صار بالنّسبة لحفلة الرّوضة؟", - "hebrew_transliteration": "שוּ צַאר בִּ-נִّסבֶּה לַחַפלֶת אִ-רַּוְדַ'ה?", - "hebrew": "מה קרה בנוגע למסיבה בגן?", - "expected_phonemes": "ʃ u | sˤ aː r | b i n n i s b e | l a ħ a f l e t | ʔ i r a w dˤ a" - }, - { - "voice_time_seconds": { - "start": 75.6, - "end": 77.4 - }, - "arabic": "بالنّسبة لشو؟", - "hebrew_transliteration": "בִּ-נִّסבֶּה לַשוּ?", - "hebrew": "בנוגע למה?", - "expected_phonemes": "b i n n i s b e | l a ʃ u" - }, - { - "voice_time_seconds": { - "start": 77.4, - "end": 79.2 - }, - "arabic": "الأواعي إلّي طلبوها.", - "hebrew_transliteration": "אִל-אַוַאעִי אִלִّי טַלַבּוּהַא.", - "hebrew": "הבגדים שביקשו.", - "expected_phonemes": "i l ʔ a w aː ʕ i | ʔ i l l i | tˤ a l a b uː h a" - }, - { - "voice_time_seconds": { - "start": 79.2, - "end": 82.8 - }, - "arabic": "آهه، قميص أبيض وبنطلون أسود فيه عنّا،", - "hebrew_transliteration": "אַהַה, קַמִיץ אַבְּיַד' וּבַּנטַלוֹן אַסוַד פִיה עִנַّא,", - "hebrew": "אה, חולצה לבנה ומכנסיים שחורים יש לנו,", - "expected_phonemes": "ʔ a h a | ʔ a m iː sˤ | ʔ a b j a dˤ | u b a n tˤ a l oː n | ʔ a s w a d | f iː h | ʕ i n n a" - }, - { - "voice_time_seconds": { - "start": 82.8, - "end": 84.6 - }, - "arabic": "بسّ طالبين كمان كندرة بيضا،", - "hebrew_transliteration": "בַּס טַאלְבִּין כַּמַאן כֻּנדַרַה בֵּידַא,", - "hebrew": "אבל מבקשים גם נעליים לבנות,", - "expected_phonemes": "b a s | tˤ aː l b iː n | k a m aː n | k u n d a r a | b eː dˤ a" - }, - { - "voice_time_seconds": { - "start": 84.6, - "end": 86.2 - }, - "arabic": "ولحدّ إسّا ما لقيتش،", - "hebrew_transliteration": "וּלַחַדّ אִסַّא מַא לַקֵיתֶש,", - "hebrew": "ועד עכשיו לא מצאתי,", - "expected_phonemes": "u l a ħ a d d | ʔ i s s a | m a | l a ʔ eː t e ʃ" - }, - { - "voice_time_seconds": { - "start": 86.2, - "end": 89.6 - }, - "arabic": "بلكي مرقت على الكنيون بعد الدّكتور.", - "hebrew_transliteration": "בַּלְכִּי מַרַקֶת עַלַא אִל-קַניוֹן בַּעֶד אִ-דַּכּתוֹר.", - "hebrew": "אולי אקפוץ לקניון אחרי הרופא.", - "expected_phonemes": "b a l k i | m a r a ʔ e t | ʕ a l a | i l ʔ a n j oː n | b a ʕ e d | ʔ i d a k t oː r" - }, - { - "voice_time_seconds": { - "start": 89.6, - "end": 91.8 - }, - "arabic": "قوليلي شو بصير معك.", - "hebrew_transliteration": "קוּלִי-לִי שוּ בִּיצִיר מַעֶכּ.", - "hebrew": "תגידי לי מה קורה איתך.", - "expected_phonemes": "ʔ uː l iː l i | ʃ u | b iː sˤ iː r | m a ʕ e k" - }, - { - "voice_time_seconds": { - "start": 91.8, - "end": 93.8 - }, - "arabic": "بدنا إشي اليوم للدّار؟", - "hebrew_transliteration": "בַּדְנַא אִשִי אִל-יוֹם לַ-דַּאר?", - "hebrew": "צריך משהו היום לבית?", - "expected_phonemes": "b a d n a | ʔ i ʃ i | i l j oː m | l a d aː r" - }, - { - "voice_time_seconds": { - "start": 93.8, - "end": 96.0 - }, - "arabic": "خبز، حليب وبيض...", - "hebrew_transliteration": "חֻ'בֶּז, חַלִיבּ וּבֵּיד'...", - "hebrew": "לחם, חלב וביצים...", - "expected_phonemes": "x u b e z | ħ a l iː b | u b eː dˤ" - }, - { - "voice_time_seconds": { - "start": 96.0, - "end": 98.6 - }, - "arabic": "بسّ إذا رح تتأخّر، أنا بجيب معي.", - "hebrew_transliteration": "בַּס אִזַא רַח-תִתאַחַّר, אַנַא בַּגִ'יבּ מַעִי.", - "hebrew": "אבל אם תתאחר, אני אביא איתי.", - "expected_phonemes": "b a s | ʔ i z a | r a ħ t i t ʔ a ħ ħ a r | ʔ a n a | b a dʒ iː b | m a ʕ i" - }, - { - "voice_time_seconds": { - "start": 98.6, - "end": 101.8 - }, - "arabic": "بشوف شو بصير معي وبحكيكي.", - "hebrew_transliteration": "בַּשוּף שוּ בִּיצִיר מַעִי וּבַּחכִּיכִּי.", - "hebrew": "אראה מה קורה איתי ואדבר איתך.", - "expected_phonemes": "b a ʃ uː f | ʃ u | b iː sˤ iː r | m a ʕ i | u b a ħ k iː k i" - }, - { - "voice_time_seconds": { - "start": 101.8, - "end": 104.2 - }, - "arabic": "طيّب، يلّا باي! - باي!", - "hebrew_transliteration": "טַיֶּבּ, יַלַّא בַּאי! - בַּאי!", - "hebrew": "טוב, יאללה ביי! - ביי!", - "expected_phonemes": "tˤ a j e b | j a l l a | b aː i | b aː i" - } - ] - }, - { - "file_name": "קנה את העולם הבא בגרושים * שתָרָא אִל-עַאלַם אִ-גַ'אי פִי קרוּש * إشترى العالم الجاي في قروش * סיפור [7M-ZkiDszcQ].webm", - "youtube_id": "7M-ZkiDszcQ", - "title_hebrew": "קנה את העולם הבא בגרושים", - "segments": [ - { - "voice_time_seconds": { - "start": 2.2, - "end": 6.8 - }, - "arabic": "الغني إلّي إشترى العالم الجّاي في قروش", - "hebrew_transliteration": "אִל-עַ'נִי אִלִّי שתַרַא לְ-עַאלַם אִ-גַّאי פִי קרוּש", - "hebrew": "העשיר שקנה את העולם הבא בגרושים", - "expected_phonemes": "i l ɣ a n i | ʔ i l l i | ʃ t a r a | l ʕ aː l a m | ʔ i g g aː i | f i | ʔ r uː ʃ" - }, - { - "voice_time_seconds": { - "start": 8.2, - "end": 11.8 - }, - "arabic": "شحّاد فقير لابس خرق، ضعيف وتعبان", - "hebrew_transliteration": "שַחַّאד פַקִיר לַאבֶּס חִ'רַק, דַ'עִיף וּתַעבַּאן", - "hebrew": "קבצן עני לבוש סמרטוטים, חלש ועייף", - "expected_phonemes": "ʃ a ħ ħ aː d | f a ʔ iː r | l aː b e s | x i r a ʔ | dˤ a ʕ iː f | u t a ʕ b aː n" - }, - { - "voice_time_seconds": { - "start": 11.8, - "end": 15.6 - }, - "arabic": "رايح من بلد لبلد ومن قرية لقرية،", - "hebrew_transliteration": "רַאיֶח מִן בַּלַד לַבַּלַד וּמִן קַרְיֶה לַקַרְיֶה,", - "hebrew": "הולך מעיר לעיר ומכפר לכפר,", - "expected_phonemes": "r aː j e ħ | m i n | b a l a d | l a b a l a d | u m i n | ʔ a r j e | l a ʔ a r j e" - }, - { - "voice_time_seconds": { - "start": 15.6, - "end": 19.0 - }, - "arabic": "بدقّ على أبواب البيوت وبطلب صدقة.", - "hebrew_transliteration": "בִּדֻקّ עַלַא בְּוַאבּ לֶ-בְּיוּת וּבִּטלֹבּ צַדַקַה.", - "hebrew": "דופק על דלתות הבתים ומבקש נדבה.", - "expected_phonemes": "b i d u ʔ ʔ | ʕ a l a | b w aː b | l e b j uː t | u b i tˤ l o b | sˤ a d a ʔ a" - }, - { - "voice_time_seconds": { - "start": 19.0, - "end": 22.2 - }, - "arabic": "لمّا كان يوصل هالزّلمة لبلدة جديدة،", - "hebrew_transliteration": "לַמַّא כַּאן יוּצַל הַ-זַּלַמֶה לַבַּלדֶה גְ'דִידֶה,", - "hebrew": "כשהאיש הזה היה מגיע לעיירה חדשה,", - "expected_phonemes": "l a m m a | k aː n | j uː sˤ a l | h a z a l a m e | l a b a l d e | dʒ d iː d e" - }, - { - "voice_time_seconds": { - "start": 22.2, - "end": 24.8 - }, - "arabic": "كان يلفلف بين الشّوارع والأزقّة،", - "hebrew_transliteration": "כַּאן יְלַפלֶף בֵּין אִ-שַּׁוַארֶע וִל-אַזִקַّה,", - "hebrew": "הוא היה מסתובב בין הרחובות והסמטאות,", - "expected_phonemes": "k aː n | j l a f l e f | b eː n | ʔ i ʃ a w aː r e ʕ | w i l ʔ a z i ʔ ʔ a" - }, - { - "voice_time_seconds": { - "start": 24.8, - "end": 27.2 - }, - "arabic": "يمدّ إيده لإلّي رايح وإلّي جاي", - "hebrew_transliteration": "יִמֻדّ אִידוֹ לַאִלִّי רַאיֶח וּאִלִّי גַ'אי", - "hebrew": "מושיט את ידו לכל ההולכים והבאים", - "expected_phonemes": "j i m u d d | ʔ iː d o | l a ʔ i l l i | r aː j e ħ | u ʔ i l l i | dʒ aː j" - }, - { - "voice_time_seconds": { - "start": 27.2, - "end": 29.0 - }, - "arabic": "ويلمّ قرش على قرش", - "hebrew_transliteration": "וִילֻםّ קִרש עַלַא קִרש", - "hebrew": "ואוסף גרוש על גרוש", - "expected_phonemes": "w iː l u m m | ʔ i r ʃ | ʕ a l a | ʔ i r ʃ" - }, - { - "voice_time_seconds": { - "start": 29.0, - "end": 33.2 - }, - "arabic": "على أمل يشتري كفاي أكل يسكّت جوعه.", - "hebrew_transliteration": "עַלַא אַמַל יִשתְרִי כְּפַאי אַכֶּל יְסַכֶּת גוּעוֹ.", - "hebrew": "בתקווה לקנות מספיק אוכל להשקיט את רעבונו.", - "expected_phonemes": "ʕ a l a | ʔ a m a l | j i ʃ t r i | k f aː i | ʔ a k e l | j s a k e t | g uː ʕ o" - }, - { - "voice_time_seconds": { - "start": 33.2, - "end": 38.4 - }, - "arabic": "وبعدها يلاقيله شي زاوية دافية ينام فيها.", - "hebrew_transliteration": "וּבַּעדְהַא יְלַאקִי-לוֹ שִי זַאוְיֶה דַאפְיֶה יְנַאם פִיהַא.", - "hebrew": "ואחר כך למצוא לו איזו פינה חמה לישון בה.", - "expected_phonemes": "u b a ʕ d h a | j l aː ʔ iː l o | ʃ i | z aː w j e | d aː f j e | j n aː m | f iː h a" - }, - { - "voice_time_seconds": { - "start": 38.4, - "end": 40.6 - }, - "arabic": "في يوم من الأيّام وصل الزّلمة", - "hebrew_transliteration": "פִי יוֹם מִן אִל-אַיַّאם וִצֶל אִ-זַּלַמֶה", - "hebrew": "יום אחד הגיע האיש", - "expected_phonemes": "f i | j oː m | m i n | i l ʔ a j j aː m | w i sˤ e l | ʔ i z a l a m e" - }, - { - "voice_time_seconds": { - "start": 40.6, - "end": 43.0 - }, - "arabic": "لبلد ما كانش فيها في الماضي،", - "hebrew_transliteration": "לַבַּלַד מַא כַּאנֶש פִיהַא פִי אִל-מַאדִ'י,", - "hebrew": "לעיר שלא היה בה בעבר,", - "expected_phonemes": "l a b a l a d | m a | k aː n e ʃ | f iː h a | f i | i l m aː dˤ i" - }, - { - "voice_time_seconds": { - "start": 43.0, - "end": 45.4 - }, - "arabic": "وصار يلفّ في شوارعها.", - "hebrew_transliteration": "וּצַאר יְלֻףّ פִי שַוַארֶעְהַא.", - "hebrew": "והתחיל להסתובב ברחובותיה.", - "expected_phonemes": "u sˤ aː r | j l u f f | f i | ʃ a w aː r e ʕ h a" - }, - { - "voice_time_seconds": { - "start": 45.4, - "end": 48.0 - }, - "arabic": "فجأة شاف قصر كبير فخم", - "hebrew_transliteration": "פַגְ'אַה שַאף קַצֶר כְּבִּיר פַחֶ'ם", - "hebrew": "פתאום ראה ארמון גדול ומפואר", - "expected_phonemes": "f a dʒ ʔ a | ʃ aː f | ʔ a sˤ e r | k b iː r | f a x e m" - }, - { - "voice_time_seconds": { - "start": 48.0, - "end": 50.8 - }, - "arabic": "على راس تلّة عالية حادّة.", - "hebrew_transliteration": "עַלַא רַאס תַלֶّה עַאלְיֶה חַאדֶّה.", - "hebrew": "על ראש גבעה גבוהה ותלולה.", - "expected_phonemes": "ʕ a l a | r aː s | t a l l e | ʕ aː l j e | ħ aː d d e" - }, - { - "voice_time_seconds": { - "start": 50.8, - "end": 53.0 - }, - "arabic": "قال لنفسه الشّحّاد:", - "hebrew_transliteration": "קַאל לַנַפסוֹ אִ-שַּׁחַّאד:", - "hebrew": "הקבצן אמר לעצמו:", - "expected_phonemes": "ʔ aː l | l a n a f s o | ʔ i ʃ a ħ ħ aː d" - }, - { - "voice_time_seconds": { - "start": 53.0, - "end": 54.8 - }, - "arabic": "«صحاب البيت الضّخم هاد", - "hebrew_transliteration": "צְחַאבּ אִל-בֵּית אִ-דַّחֶ'ם הַאד", - "hebrew": "בעלי הבית הענק הזה", - "expected_phonemes": "sˤ ħ aː b | i l b eː t | ʔ i d d a x e m | h aː d" - }, - { - "voice_time_seconds": { - "start": 54.8, - "end": 58.6 - }, - "arabic": "أكيد ناس أغنياء كتير، وكرماء كتير،", - "hebrew_transliteration": "אַכִּיד נַאס אַעְ'נִיַא כְּתִיר, וּכֻּרַמַא כְּתִיר,", - "hebrew": "בטח אנשים עשירים מאוד, ונדיבים מאוד,", - "expected_phonemes": "ʔ a k iː d | n aː s | ʔ a ɣ n i j a | k t iː r | u k u r a m a | k t iː r" - }, - { - "voice_time_seconds": { - "start": 58.6, - "end": 61.6 - }, - "arabic": "رح أروح عندهم وأطلب مصاري.", - "hebrew_transliteration": "רַח-אַרוּח עִנדְהֹם וּאַטלֹבּ מַצַארִי.", - "hebrew": "אלך אליהם ואבקש כסף.", - "expected_phonemes": "r a ħ ʔ a r uː ħ | ʕ i n d h o m | u ʔ a tˤ l o b | m a sˤ aː r i" - }, - { - "voice_time_seconds": { - "start": 61.6, - "end": 65.2 - }, - "arabic": "وهمّ أكيد رح يتبرّعولي بإيد سخيّة", - "hebrew_transliteration": "וּהֻמّ אַכִּיד רַח-יִתבַּרַעוּ-לִי בְּאִיד סַחִיֶّה", - "hebrew": "והם בטח יתרמו לי ביד נדיבה", - "expected_phonemes": "u h u m m | ʔ a k iː d | r a ħ j i t b a r a ʕ uː l i | b ʔ iː d | s a ħ i j j e" - }, - { - "voice_time_seconds": { - "start": 65.2, - "end": 67.8 - }, - "arabic": "ورح أقدر آكل حتى الشّبع", - "hebrew_transliteration": "וּרַח-אַקדַר אַאכֻּל חַתַּא אִ-שַּׁבַּע", - "hebrew": "ואוכל לאכול עד שובע", - "expected_phonemes": "u r a ħ ʔ a ʔ d a r | ʔ aː k u l | ħ a t a | ʔ i ʃ a b a ʕ" - }, - { - "voice_time_seconds": { - "start": 67.8, - "end": 71.2 - }, - "arabic": "وما أشحدش يوم أو يومين.»", - "hebrew_transliteration": "וּמַא אַשחַדֶש יוֹם אַו יוֹמֵין.", - "hebrew": "ולא אקבץ נדבות יום או יומיים.", - "expected_phonemes": "u m a | ʔ a ʃ ħ a d e ʃ | j oː m | ʔ a w | j oː m eː n" - }, - { - "voice_time_seconds": { - "start": 71.2, - "end": 74.6 - }, - "arabic": "في جهد كبير تسلّق هالشّحّاد على التّلّة،", - "hebrew_transliteration": "פִי גֻ'הֶד כְּבִּיר תְסַלַّק הַ-שַּׁחַّאד עַלַא אִ-תַּלֶّה,", - "hebrew": "במאמץ רב טיפס הקבצן על הגבעה,", - "expected_phonemes": "f i | dʒ u h e d | k b iː r | t s a l l a ʔ | h a ʃ a ħ ħ aː d | ʕ a l a | ʔ i t a l l e" - }, - { - "voice_time_seconds": { - "start": 74.6, - "end": 79.2 - }, - "arabic": "ووصل للقصر وهوّ تعبان، هلكان وكلّه عرق.", - "hebrew_transliteration": "וּוִצֶל לַל-קַצֶר וּהֻוֶّ תַעבַּאן, הַלכַּאן וּכֻּלّוֹ עַרַק.", - "hebrew": "והגיע לארמון כשהוא עייף, מותש וכולו מזיע.", - "expected_phonemes": "u w i sˤ e l | l a l ʔ a sˤ e r | u h u w w e | t a ʕ b aː n | h a l k aː n | u k u l l o | ʕ a r a ʔ" - }, - { - "voice_time_seconds": { - "start": 79.2, - "end": 81.4 - }, - "arabic": "رنّ على جرس المدخل،", - "hebrew_transliteration": "רַןّ עַלַא גַ'רַס אִל-מַדחַל,", - "hebrew": "צלצל בפעמון הכניסה,", - "expected_phonemes": "r a n n | ʕ a l a | dʒ a r a s | i l m a d ħ a l" - }, - { - "voice_time_seconds": { - "start": 81.4, - "end": 83.6 - }, - "arabic": "وبسرعة طلع صاحب البيت", - "hebrew_transliteration": "וּבְּסֻרעַה טִלֶע צַאחֶבּ אִל-בֵּית", - "hebrew": "ובמהירות יצא בעל הבית", - "expected_phonemes": "u b s u r ʕ a | tˤ i l e ʕ | sˤ aː ħ e b | i l b eː t" - }, - { - "voice_time_seconds": { - "start": 83.6, - "end": 86.0 - }, - "arabic": "على واحدة من البرندات.", - "hebrew_transliteration": "עַלַא וַאחדֶה מִן אִל-בַּרַנדַאת.", - "hebrew": "לאחת המרפסות.", - "expected_phonemes": "ʕ a l a | w aː ħ d e | m i n | i l b a r a n d aː t" - }, - { - "voice_time_seconds": { - "start": 86.0, - "end": 90.8 - }, - "arabic": "«صدقة لفقير، مشان الله!» قال الشّحّاد.", - "hebrew_transliteration": "צַדַקַה לַפַקִיר, מִשַאן אַללַّה! קַאל אִ-שַּׁחַّאד.", - "hebrew": "נדבה לעני, למען השם! אמר הקבצן.", - "expected_phonemes": "sˤ a d a ʔ a | l a f a ʔ iː r | m i ʃ aː n | ʔ a l l a | ʔ aː l | ʔ i ʃ a ħ ħ aː d" - }, - { - "voice_time_seconds": { - "start": 90.8, - "end": 95.0 - }, - "arabic": "رمى صاحب البيت لجهة الشّحّاد شوية قروش.", - "hebrew_transliteration": "רַמַא צַאחֶבּ אִל-בֵּית לַגִ'הֶת אִ-שַּׁחַّאד שְוַיֶּת קרוּש.", - "hebrew": "בעל הבית זרק לכיוון הקבצן כמה גרושים.", - "expected_phonemes": "r a m a | sˤ aː ħ e b | i l b eː t | l a dʒ i h e t | ʔ i ʃ a ħ ħ aː d | ʃ w a j e t | ʔ r uː ʃ" - }, - { - "voice_time_seconds": { - "start": 95.0, - "end": 98.2 - }, - "arabic": "لمّ الشّحّاد، إلّي خاب أمله، لقروش القليلة،", - "hebrew_transliteration": "לַםّ אִ-שַּׁחַّאד, אִלִّי חַ'אבּ אַמַלוֹ, לֶ-קרוּש אִל-קַלִילֶה,", - "hebrew": "הקבצן, שהתאכזב, אסף את הגרושים המעטים,", - "expected_phonemes": "l a m m | ʔ i ʃ a ħ ħ aː d | ʔ i l l i | x aː b | ʔ a m a l o | l e ʔ r uː ʃ | i l ʔ a l iː l e" - }, - { - "voice_time_seconds": { - "start": 98.2, - "end": 101.6 - }, - "arabic": "وكان الغضب بقلبه كبير على المبلغ القليل", - "hebrew_transliteration": "וּכַּאן אִל-עַ'דַבּ בְּקַלבּוֹ כְּבִּיר עַלַא אִל-מַבלַעְ' אִל-קַלִיל", - "hebrew": "והכעס בליבו היה גדול על הסכום הקטן", - "expected_phonemes": "u k aː n | i l ɣ a d a b | b ʔ a l b o | k b iː r | ʕ a l a | i l m a b l a ɣ | i l ʔ a l iː l" - }, - { - "voice_time_seconds": { - "start": 101.6, - "end": 104.2 - }, - "arabic": "إلّي أخده من شخص غني كتير.", - "hebrew_transliteration": "אִלִّי אַחַ'דוֹ מִן שַחְ'ץ עַ'נִי כְּתִיר.", - "hebrew": "שקיבל מאדם עשיר מאוד.", - "expected_phonemes": "ʔ i l l i | ʔ a x a d o | m i n | ʃ a x sˤ | ɣ a n i | k t iː r" - }, - { - "voice_time_seconds": { - "start": 104.2, - "end": 106.0 - }, - "arabic": "بعد ما لمّ لقروش،", - "hebrew_transliteration": "בַּעֶד מַא לַםّ לֶ-קרוּש,", - "hebrew": "אחרי שאסף את הגרושים,", - "expected_phonemes": "b a ʕ e d | m a | l a m m | l e ʔ r uː ʃ" - }, - { - "voice_time_seconds": { - "start": 106.0, - "end": 109.0 - }, - "arabic": "رفع راسه لجهة صاحب القصر وسأله:", - "hebrew_transliteration": "רַפַע רַאסוֹ לַגִ'הֶת צַאחֶבּ אִל-קַצֶר וּסַאַלוֹ:", - "hebrew": "הרים את ראשו לעבר בעל הארמון ושאל אותו:", - "expected_phonemes": "r a f a ʕ | r aː s o | l a dʒ i h e t | sˤ aː ħ e b | i l ʔ a sˤ e r | u s a ʔ a l o" - }, - { - "voice_time_seconds": { - "start": 109.0, - "end": 113.4 - }, - "arabic": "«سيدي! قدّيش كلّفك تبني هدا القصر الفخم؟»", - "hebrew_transliteration": "סִידִי! קַדֵّיש כַּלַّפַכּ תִבנִי הַאדַא אִל-קַצֶר אִל-פַחֶ'ם?", - "hebrew": "אדוני! כמה עלה לך לבנות את הארמון המפואר הזה?", - "expected_phonemes": "s iː d i | ʔ a d d eː ʃ | k a l l a f a k | t i b n i | h aː d a | i l ʔ a sˤ e r | i l f a x e m" - }, - { - "voice_time_seconds": { - "start": 113.4, - "end": 117.0 - }, - "arabic": "ردّ صاحب البيت: «مية ألف دينار.»", - "hebrew_transliteration": "רַדّ צַאחֶבּ אִל-בֵּית: מִית אַלְף דִינַאר.", - "hebrew": "בעל הבית ענה: מאה אלף דינר.", - "expected_phonemes": "r a d d | sˤ aː ħ e b | i l b eː t | m iː t | ʔ a l f | d iː n aː r" - }, - { - "voice_time_seconds": { - "start": 117.0, - "end": 121.0 - }, - "arabic": "قالّه الشّحّاد بزعل: «سيدي المحترم،", - "hebrew_transliteration": "קַאל-לוֹ אִ-שַּׁחַّאד בִּזַעַל: סִידִי אִל-מֻחתַרַם,", - "hebrew": "הקבצן אמר לו בכעס: אדוני הנכבד,", - "expected_phonemes": "ʔ aː l l o | ʔ i ʃ a ħ ħ aː d | b i z a ʕ a l | s iː d i | i l m u ħ t a r a m" - }, - { - "voice_time_seconds": { - "start": 121.0, - "end": 124.2 - }, - "arabic": "هدا العالم اشتريت بمية ألف دينار،", - "hebrew_transliteration": "הַאדַא אִל-עַאלַם אִשתַרֵית בְּמִית אַלְף דִינַאר,", - "hebrew": "את העולם הזה קנית במאה אלף דינר,", - "expected_phonemes": "h aː d a | i l ʕ aː l a m | ʔ i ʃ t a r eː t | b m iː t | ʔ a l f | d iː n aː r" - }, - { - "voice_time_seconds": { - "start": 124.2, - "end": 125.8 - }, - "arabic": "بسّ الدّخول للعالم الجّاي", - "hebrew_transliteration": "בַּס אִ-דֻّחוּל לַל-עַאלַם אִ-גַّאי", - "hebrew": "אבל את הכניסה לעולם הבא", - "expected_phonemes": "b a s | ʔ i d d u ħ uː l | l a l ʕ aː l a m | ʔ i g g aː i" - }, - { - "voice_time_seconds": { - "start": 125.8, - "end": 132.2 - }, - "arabic": "عم تطلب تشتري بقروش!؟»", - "hebrew_transliteration": "עַם-תֻטלֹבּ תִשתְרִי בִּקרוּש?!", - "hebrew": "אתה מבקש לקנות בגרושים?!", - "expected_phonemes": "ʕ a m t u tˤ l o b | t i ʃ t r i | b i ʔ r uː ʃ" - } - ] - }, - { - "file_name": "הנסיך הקטן – הפרק השני | אִל-אַמִיר לֶ-זעִ'יר – אִ-גֻזא אִ-תַאנִי | الأمير لصغير – الجّزء التّاني [sOXJfZJqQ3o].webm", - "youtube_id": "sOXJfZJqQ3o", - "title_hebrew": "הנסיך הקטן – הפרק השני", - "segments": [ - { - "voice_time_seconds": { - "start": 2.0, - "end": 6.8 - }, - "arabic": "الأمير لصغير – الجّزء التّاني", - "hebrew_transliteration": "אִל-אַמִיר לֶ-זְעִ'יר – אִ-גֻّזְא אִ-תַּאנִי", - "hebrew": "הנסיך הקטן – הפרק השני", - "expected_phonemes": "i l ʔ a m iː r | l e z ɣ iː r | ʔ i g g u z a | ʔ i t aː n i" - }, - { - "voice_time_seconds": { - "start": 8.2, - "end": 12.4 - }, - "arabic": "هيك عشت لحالي بدون حدا أحكي معه،", - "hebrew_transliteration": "הֵיכּ עִשֶת לַחַאלִי בְּדוּן חַדַא אַחכִּי מַעוֹ,", - "hebrew": "כך חייתי לבדי בלי מישהו לדבר איתו,", - "expected_phonemes": "h eː k | ʕ i ʃ e t | l a ħ aː l i | b i d uː n | ħ a d a | ʔ a ħ k i | m a ʕ o" - }, - { - "voice_time_seconds": { - "start": 12.4, - "end": 15.4 - }, - "arabic": "وبهدا الشّكل مرقوا الأيّام", - "hebrew_transliteration": "וּבִּהַאדַא אִ-שַּׁכֶּל מַרַקוּ אִל-אַיַّאם", - "hebrew": "וכך עברו הימים", - "expected_phonemes": "u b i h aː d a | ʔ i ʃ a k e l | m a r a ʔ u | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 15.4, - "end": 19.4 - }, - "arabic": "لحدّ ما خربت طيّارتي بحادث بالصّحرا.", - "hebrew_transliteration": "לַחַדّ מַא חִ'רבַּת טַיַّארְתִי בְּחַאדֶת בִּ-צַּחרַא.", - "hebrew": "עד שהמטוס שלי התקלקל בתאונה במדבר.", - "expected_phonemes": "l a ħ a d d | m a | x i r b a t | tˤ a j j aː r t i | b ħ aː d e t | b i sˤ a ħ r a" - }, - { - "voice_time_seconds": { - "start": 19.4, - "end": 22.6 - }, - "arabic": "هدا الحادث صار قبل ستّ سنين،", - "hebrew_transliteration": "הַאדַא אִל-חַאדֶת צַאר קַבֶּל סִתّ סְנִין,", - "hebrew": "התאונה הזאת קרתה לפני שש שנים,", - "expected_phonemes": "h aː d a | i l ħ aː d e t | sˤ aː r | ʔ a b e l | s i t t | s n iː n" - }, - { - "voice_time_seconds": { - "start": 22.6, - "end": 26.0 - }, - "arabic": "لمّا صار فيه خراب بماطور طيّارتي،", - "hebrew_transliteration": "לַמַّא צַאר פִיה חַ'רַאבּ בְּמַאטוֹר טַיַّארְתִי,", - "hebrew": "כשהייתה תקלה במנוע המטוס שלי,", - "expected_phonemes": "l a m m a | sˤ aː r | f iː h | x a r aː b | b m aː tˤ oː r | tˤ a j j aː r t i" - }, - { - "voice_time_seconds": { - "start": 26.0, - "end": 29.0 - }, - "arabic": "وعشان ما كانش معاي ميكانيكي ولا ركّاب،", - "hebrew_transliteration": "וּעַשַאן מַא כַּאנֶ-ש מַעַאי מִיכַּאנִיכִּי וְלַא רַכַּّאבּ,", - "hebrew": "ומכיוון שלא היו איתי מכונאי ולא נוסעים,", - "expected_phonemes": "u ʕ a ʃ aː n | m a | k aː n e ʃ | m a ʕ aː i | m iː k aː n iː k i | u l aː | r a k k aː b" - }, - { - "voice_time_seconds": { - "start": 29.0, - "end": 32.4 - }, - "arabic": "حاولت أصلّح هدا الخراب الصّعب بنفسي،", - "hebrew_transliteration": "חַאוַלֶת אַצַלֶّח הַאדַא אִל-חַ'רַאבּ אִ-צַּעֶבּ בִּנַפסִי,", - "hebrew": "ניסיתי לתקן את התקלה הקשה הזאת בעצמי,", - "expected_phonemes": "ħ aː w a l e t | ʔ a sˤ a l l e ħ | h aː d a | i l x a r aː b | ʔ i sˤ a ʕ e b | b i n a f s i" - }, - { - "voice_time_seconds": { - "start": 32.4, - "end": 34.4 - }, - "arabic": "لإنّه الموضوع كان بالنّسبة إلي", - "hebrew_transliteration": "לַאִנּוֹ אִל-מַוְדוּע כַּאן בִּ-נִّסבֶּה אִלִי", - "hebrew": "כי העניין היה בשבילי", - "expected_phonemes": "l a ʔ i n o | i l m a w dˤ uː ʕ | k aː n | b i n n i s b e | ʔ i l i" - }, - { - "voice_time_seconds": { - "start": 34.4, - "end": 36.6 - }, - "arabic": "موضوع حياة أو موت،", - "hebrew_transliteration": "מַוְדוּע חַיַאה אַו מוֹת,", - "hebrew": "עניין של חיים או מוות,", - "expected_phonemes": "m a w d uː ʕ | ħ a j aː h | ʔ a w | m oː t" - }, - { - "voice_time_seconds": { - "start": 36.6, - "end": 40.2 - }, - "arabic": "وهدا عشان كميّة الميّ إلّي كانت معاي", - "hebrew_transliteration": "וּהַאדַא עַשַאן כַּמִّיֶّת אִל-מַיّ אִלִّי כַּאנַת מַעַאי", - "hebrew": "וזה מפני שכמות המים שהייתה איתי", - "expected_phonemes": "u h aː d a | ʕ a ʃ aː n | k a m m i j j e t | i l m a j j | ʔ i l l i | k aː n a t | m a ʕ aː i" - }, - { - "voice_time_seconds": { - "start": 40.2, - "end": 43.0 - }, - "arabic": "بالكاد كانت تكفّي لأسبوع.", - "hebrew_transliteration": "בִּל-כַּאד כַּאנַת תְכַּפִّי לַאֻסבּוּע.", - "hebrew": "בקושי הספיקה לשבוע.", - "expected_phonemes": "b i l k aː d | k aː n a t | t k a f f i | l a ʔ u s b uː ʕ" - }, - { - "voice_time_seconds": { - "start": 43.0, - "end": 46.0 - }, - "arabic": "أوّل ليلة نمت على الرّمل بمحلّ", - "hebrew_transliteration": "אַוַّל לֵילֶה נִמֶת עַלַא אִ-רַּמֶל בְּמַחַלّ", - "hebrew": "בלילה הראשון ישנתי על החול במקום", - "expected_phonemes": "ʔ a w w a l | l eː l e | n i m e t | ʕ a l a | ʔ i r a m e l | b m a ħ a l l" - }, - { - "voice_time_seconds": { - "start": 46.0, - "end": 49.4 - }, - "arabic": "بيبعد حوالي ألف ميل عن أقرب بلد.", - "hebrew_transliteration": "בִּיבְּעַד חַוַאלֵי אַלְף מִיל עַן אַקרַבּ בַּלַד.", - "hebrew": "שמרוחק כאלף מייל מהעיר הקרובה ביותר.", - "expected_phonemes": "b iː b ʕ a d | ħ a w aː l eː | ʔ a l f | m iː l | ʕ a n | ʔ a ʔ r a b | b a l a d" - }, - { - "voice_time_seconds": { - "start": 49.4, - "end": 53.4 - }, - "arabic": "كنت وحيد أكتر من بحّار إلّي سفينته غرقت", - "hebrew_transliteration": "כֻּנֶת וַחִיד אַכְּתַר מִן בַּחַّאר אִלִّי סַפִינתוֹ עִ'רקַת", - "hebrew": "הייתי בודד יותר ממלח שספינתו טבעה", - "expected_phonemes": "k u n e t | w a ħ iː d | ʔ a k t a r | m i n | b a ħ ħ aː r | ʔ i l l i | s a f iː n t o | ɣ i r ʔ a t" - }, - { - "voice_time_seconds": { - "start": 53.4, - "end": 56.8 - }, - "arabic": "وهوّ طايش على خشبة بقلب المحيط.", - "hebrew_transliteration": "וּהֻוֶّ טַאיֶש עַלַא חַ'שַבֶּה בְּקַלבּ אִל-מֻחִיט.", - "hebrew": "והוא צף על קרש בלב האוקיינוס.", - "expected_phonemes": "u h u w w e | tˤ aː j e ʃ | ʕ a l a | x a ʃ a b e | b ʔ a l b | i l m u ħ iː tˤ" - }, - { - "voice_time_seconds": { - "start": 56.8, - "end": 61.0 - }, - "arabic": "ومنشان هيك أكيد بتقدروا تتصوّروا دهشتي", - "hebrew_transliteration": "וּמִנשַאן הֵיכּ אַכִּיד בְּתִקדַרוּ תִתצַוַّרוּ דַהשִתִי", - "hebrew": "ולכן בטח תוכלו לדמיין את תדהמתי", - "expected_phonemes": "u m i n ʃ aː n | h eː k | ʔ a k iː d | b t i ʔ d a r u | t i t sˤ a w w a r u | d a h ʃ i t i" - }, - { - "voice_time_seconds": { - "start": 61.0, - "end": 63.0 - }, - "arabic": "لمّا سمعت صوت رفيع", - "hebrew_transliteration": "לַמַّא סִמעֶת צוֹת רַפִיע", - "hebrew": "כששמעתי קול דק", - "expected_phonemes": "l a m m a | s i m ʕ e t | sˤ oː t | r a f iː ʕ" - }, - { - "voice_time_seconds": { - "start": 63.0, - "end": 66.4 - }, - "arabic": "عم بيصحّيني بساعة الفجر وبيقولّي:", - "hebrew_transliteration": "עַם-בִּיצַחִّינִי בְּסַאעַת אִל-פַגְ'ר וּבִּיקוּל-לִי:", - "hebrew": "מעיר אותי בשעת השחר ואומר לי:", - "expected_phonemes": "ʕ a m b iː sˤ a ħ ħ iː n i | b s aː ʕ a t | i l f a dʒ r | u b iː ʔ uː l l i" - }, - { - "voice_time_seconds": { - "start": 66.4, - "end": 69.6 - }, - "arabic": "«إعمل معروف... إرسملي خروف!»", - "hebrew_transliteration": "אִעמֶל מַערוּף... אִרסֶמ-לִי חַ'רוּף!", - "hebrew": "בבקשה... צייר לי כבשה!", - "expected_phonemes": "ʔ i ʕ m e l | m a ʕ r uː f | ʔ i r s e m l i | x a r uː f" - }, - { - "voice_time_seconds": { - "start": 69.6, - "end": 71.0 - }, - "arabic": "«شو؟!»", - "hebrew_transliteration": "שוּ?!", - "hebrew": "מה?!", - "expected_phonemes": "ʃ u" - }, - { - "voice_time_seconds": { - "start": 71.0, - "end": 72.6 - }, - "arabic": "«إرسملي خروف...»", - "hebrew_transliteration": "אִרסֶמ-לִי חַ'רוּף...", - "hebrew": "צייר לי כבשה...", - "expected_phonemes": "ʔ i r s e m l i | x a r uː f" - }, - { - "voice_time_seconds": { - "start": 72.6, - "end": 76.2 - }, - "arabic": "نطّيت من محلّي مصدوم، فركت عينيّ،", - "hebrew_transliteration": "נַטֵّית מִן מַחַלִّי מַצדוּם, פַרַכֶת עֵינַיّ,", - "hebrew": "קפצתי ממקומי המום, שפשפתי את עיניי,", - "expected_phonemes": "n a tˤ tˤ eː t | m i n | m a ħ a l l i | m a sˤ d uː m | f a r a k e t | ʕ eː n a j j" - }, - { - "voice_time_seconds": { - "start": 76.2, - "end": 80.6 - }, - "arabic": "إتطلّعت منيح وشفت قدّامي ولد شكله غريب،", - "hebrew_transliteration": "אִתטַלַّעֶת מְנִיח וּשֻפֶת קֻדַّאמִי וַלַד שַכְּלוֹ עַ'רִיבּ,", - "hebrew": "הסתכלתי היטב וראיתי לפניי ילד שנראה מוזר,", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ e t | m n iː ħ | u ʃ u f e t | ʔ u d d aː m i | w a l a d | ʃ a k l o | ɣ a r iː b" - }, - { - "voice_time_seconds": { - "start": 80.6, - "end": 82.6 - }, - "arabic": "كان يبحّر فيّ.", - "hebrew_transliteration": "כַּאן יִבַּחַّר פִיִّ.", - "hebrew": "שבהה בי.", - "expected_phonemes": "k aː n | j i b a ħ ħ a r | f i j j i" - }, - { - "voice_time_seconds": { - "start": 82.6, - "end": 86.0 - }, - "arabic": "وهاي الرّسمة هيّ أكتر رسمة موفّقة", - "hebrew_transliteration": "וּהַאי אִ-רַּסמֶה הִיֶּ אַכְּתַר רַסמֶה מוּפַّקַה", - "hebrew": "והציור הזה הוא הציור הכי מוצלח", - "expected_phonemes": "u h aː i | ʔ i r a s m e | h i j e | ʔ a k t a r | r a s m e | m uː f f a ʔ a" - }, - { - "voice_time_seconds": { - "start": 86.0, - "end": 88.6 - }, - "arabic": "قدرت أرسمها بعدين.", - "hebrew_transliteration": "קִדְרֶת אַרסֻמְהַא בַּעדֵין.", - "hebrew": "שהצלחתי לצייר אחר כך.", - "expected_phonemes": "ʔ i d r e t | ʔ a r s u m h a | b a ʕ d eː n" - }, - { - "voice_time_seconds": { - "start": 88.6, - "end": 90.8 - }, - "arabic": "بسّ رسمتي هاي مش حلوة", - "hebrew_transliteration": "בַּס רַסמִתִי הַאי מִש חִלוֶה", - "hebrew": "אבל הציור שלי הזה לא יפה", - "expected_phonemes": "b a s | r a s m i t i | h aː i | m i ʃ | ħ i l w e" - }, - { - "voice_time_seconds": { - "start": 90.8, - "end": 93.4 - }, - "arabic": "قدّ الولد الصغير إلّي كان قدّامي،", - "hebrew_transliteration": "קַדّ אִל-וַלַד לֶ-זְעִ'יר אִלִّי כַּאן קֻדַّאמִי,", - "hebrew": "כמו הילד הקטן שהיה לפניי,", - "expected_phonemes": "ʔ a d d | i l w a l a d | l e z ɣ iː r | ʔ i l l i | k aː n | ʔ u d d aː m i" - }, - { - "voice_time_seconds": { - "start": 93.4, - "end": 95.4 - }, - "arabic": "وهدا طبعا مش ذنبي", - "hebrew_transliteration": "וּהַאדַא טַבְּעַן מִש דַ'נבִּי", - "hebrew": "וזו כמובן לא אשמתי", - "expected_phonemes": "u h aː d a | tˤ a b ʕ a n | m i ʃ | dˤ a n b i" - }, - { - "voice_time_seconds": { - "start": 95.4, - "end": 97.8 - }, - "arabic": "لإنه الكبار همّ إلّي هبّطوا عزيمتي", - "hebrew_transliteration": "לַאִנּוֹ לֶ-כְּבַּאר הֻמّ אִלִّי הַבַּטוּ עַזִימתִי", - "hebrew": "כי המבוגרים הם שריפו את ידיי", - "expected_phonemes": "l a ʔ i n o | l e k b aː r | h u m m | ʔ i l l i | h a b a tˤ u | ʕ a z iː m t i" - }, - { - "voice_time_seconds": { - "start": 97.8, - "end": 101.6 - }, - "arabic": "عن موضوع الرّسم لمّا كنت إبن ستّ سنين", - "hebrew_transliteration": "עַן מַוְדוּע אִ-רַּסֶם לַמַّא כֻּנֶת אִבְּן סִתّ סְנִין", - "hebrew": "בנושא הציור כשהייתי בן שש", - "expected_phonemes": "ʕ a n | m a w d uː ʕ | ʔ i r a s e m | l a m m a | k u n e t | ʔ i b n | s i t t | s n iː n" - }, - { - "voice_time_seconds": { - "start": 101.6, - "end": 104.0 - }, - "arabic": "وبسببهم تنازلت عنّه،", - "hebrew_transliteration": "וּבְּסַבַּבְּהֹם תְנַאזַלֶת עַנּוֹ,", - "hebrew": "ובגללם ויתרתי עליו,", - "expected_phonemes": "u b s a b a b h o m | t n aː z a l e t | ʕ a n o" - }, - { - "voice_time_seconds": { - "start": 104.0, - "end": 105.2 - }, - "arabic": "وما تعلّمتش أرسم", - "hebrew_transliteration": "וּמַא תְעַלַّמתֶ-ש אַרסֻם", - "hebrew": "ולא למדתי לצייר", - "expected_phonemes": "u m a | t ʕ a l l a m t e ʃ | ʔ a r s u m" - }, - { - "voice_time_seconds": { - "start": 105.2, - "end": 109.2 - }, - "arabic": "غير حيايا البوا من برّا ومن جوّا.", - "hebrew_transliteration": "עֵ'יר חַיַאיַא אִל-בֻּוַא מִן בַּרַّא וּמִן גֻ'וַّא.", - "hebrew": "מלבד נחשי בואה מבחוץ ומבפנים.", - "expected_phonemes": "ɣ eː r | ħ a j aː j a | i l b u w a | m i n | b a r r a | u m i n | dʒ u w w a" - }, - { - "voice_time_seconds": { - "start": 109.2, - "end": 111.0 - }, - "arabic": "إتطلّعت على هدا الشّكل الغريب", - "hebrew_transliteration": "אִתטַלַّעֶת עַלַא הַאדַא אִ-שַּׁכֶּל אִל-עַ'רִיבּ", - "hebrew": "הסתכלתי על הדמות המוזרה הזאת", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ e t | ʕ a l a | h aː d a | ʔ i ʃ a k e l | i l ɣ a r iː b" - }, - { - "voice_time_seconds": { - "start": 111.0, - "end": 115.0 - }, - "arabic": "إلّي وقّف قدّامي وكنت مدهوش وحيران.", - "hebrew_transliteration": "אִלִّי וַקַّף קֻדַّאמִי וּכֻּנֶת מַדהוּש וּחַירַאן.", - "hebrew": "שעמדה לפניי והייתי נדהם ומבולבל.", - "expected_phonemes": "ʔ i l l i | w a ʔ ʔ a f | ʔ u d d aː m i | u k u n e t | m a d h uː ʃ | u ħ a j r aː n" - }, - { - "voice_time_seconds": { - "start": 115.0, - "end": 117.8 - }, - "arabic": "وما تنسوا إنّي بهديك السّاعة", - "hebrew_transliteration": "וּמַא תִנסוּ אִנִّי בְּהַדִיכּ אִ-סַּאעַה", - "hebrew": "ואל תשכחו שבאותה שעה", - "expected_phonemes": "u m a | t i n s u | ʔ i n n i | b h a d iː k | ʔ i s aː ʕ a" - }, - { - "voice_time_seconds": { - "start": 117.8, - "end": 119.8 - }, - "arabic": "كنت بقلب الصّحرا،", - "hebrew_transliteration": "כֻּנֶת בְּקַלבּ אִ-צַּחרַא,", - "hebrew": "הייתי בלב המדבר,", - "expected_phonemes": "k u n e t | b ʔ a l b | ʔ i sˤ a ħ r a" - }, - { - "voice_time_seconds": { - "start": 119.8, - "end": 122.8 - }, - "arabic": "ببعد ألف ميل من أيّ بلد.", - "hebrew_transliteration": "בִּבְּעַד אַלְף מִיל מִן אַיّ בַּלַד.", - "hebrew": "במרחק אלף מייל מכל עיר.", - "expected_phonemes": "b i b ʕ a d | ʔ a l f | m iː l | m i n | ʔ a j j | b a l a d" - }, - { - "voice_time_seconds": { - "start": 122.8, - "end": 127.4 - }, - "arabic": "وبرضه ما بيّن هدا الولد إنّه ضايع بالصّحرا.", - "hebrew_transliteration": "וּבַּרְדוֹ מַא בַּיַّן הַאדַא אִל-וַלַד אִנּוֹ דַ'איֶע בִּ-צַּחרַא.", - "hebrew": "ובכל זאת הילד הזה לא נראה אבוד במדבר.", - "expected_phonemes": "u b a r d o | m a | b a j j a n | h aː d a | i l w a l a d | ʔ i n o | dˤ aː j e ʕ | b i sˤ a ħ r a" - }, - { - "voice_time_seconds": { - "start": 127.4, - "end": 130.2 - }, - "arabic": "شكله بالمرّة ما كان شكل ولد ضايع،", - "hebrew_transliteration": "שַכְּלוֹ בִּל-מַרַّה מַא כַּאן שַכֶּל וַלַד דַ'איֶע,", - "hebrew": "הוא בכלל לא נראה כמו ילד אבוד,", - "expected_phonemes": "ʃ a k l o | b i l m a r r a | m a | k aː n | ʃ a k e l | w a l a d | dˤ aː j e ʕ" - }, - { - "voice_time_seconds": { - "start": 130.2, - "end": 133.8 - }, - "arabic": "ببعد ألف ميل من أيّ بلد مأهول.", - "hebrew_transliteration": "בִּבְּעַד אַלְף מִיל מִן אַיّ בַּלַד מַאהוּל.", - "hebrew": "במרחק אלף מייל מכל עיר מיושבת.", - "expected_phonemes": "b i b ʕ a d | ʔ a l f | m iː l | m i n | ʔ a j j | b a l a d | m aː h uː l" - }, - { - "voice_time_seconds": { - "start": 133.8, - "end": 137.2 - }, - "arabic": "بالنّهاي لمّا قدرت أطلّع كلمة من تمّي", - "hebrew_transliteration": "בִּ-נַּהַאי לַמַّא קִדְרֶת אַטַלֶּע כִּלמֶה מִן תֻמִّי", - "hebrew": "בסוף, כשהצלחתי להוציא מילה מפי", - "expected_phonemes": "b i n a h aː i | l a m m a | ʔ i d r e t | ʔ a tˤ a l e ʕ | k i l m e | m i n | t u m m i" - }, - { - "voice_time_seconds": { - "start": 137.2, - "end": 139.0 - }, - "arabic": "سألته بتعجّب:", - "hebrew_transliteration": "סַאַלתוֹ בִּתעַגֻّבּ:", - "hebrew": "שאלתי אותו בתמיהה:", - "expected_phonemes": "s a ʔ a l t o | b i t ʕ a g g u b" - }, - { - "voice_time_seconds": { - "start": 139.0, - "end": 140.6 - }, - "arabic": "«شو بتعمل هون؟»", - "hebrew_transliteration": "שוּ בְּתַעמֶל הוֹן?", - "hebrew": "מה אתה עושה כאן?", - "expected_phonemes": "ʃ u | b t a ʕ m e l | h oː n" - }, - { - "voice_time_seconds": { - "start": 140.6, - "end": 143.4 - }, - "arabic": "بسّ هوّ رجع بهدوء كأنّه عم بيحكي", - "hebrew_transliteration": "בַּס הֻוֶّ רִגֶ'ע בְּהֻדוּא כַּאִנּוֹ עַם-בִּיחכִּי", - "hebrew": "אבל הוא חזר בשקט כאילו הוא מדבר", - "expected_phonemes": "b a s | h u w w e | r i dʒ e ʕ | b h u d uː a | k a ʔ i n o | ʕ a m b iː ħ k i" - }, - { - "voice_time_seconds": { - "start": 143.4, - "end": 145.6 - }, - "arabic": "عن موضوع جدّي كتير:", - "hebrew_transliteration": "עַן מַוְדוּע גִ'דִّי כְּתִיר:", - "hebrew": "על נושא רציני מאוד:", - "expected_phonemes": "ʕ a n | m a w d uː ʕ | dʒ i d d i | k t iː r" - }, - { - "voice_time_seconds": { - "start": 145.6, - "end": 148.2 - }, - "arabic": "«إعمل معروف... إرسملي خروف...»", - "hebrew_transliteration": "אִעמֶל מַערוּף... אִרסֶמ-לִי חַ'רוּף...", - "hebrew": "בבקשה... צייר לי כבשה...", - "expected_phonemes": "ʔ i ʕ m e l | m a ʕ r uː f | ʔ i r s e m l i | x a r uː f" - }, - { - "voice_time_seconds": { - "start": 148.2, - "end": 150.6 - }, - "arabic": "لمّا منكون قدّام لغز كبير", - "hebrew_transliteration": "לַמַّא מִנכּוּן קֻדַّאם לֻעְ'ז כְּבִּיר", - "hebrew": "כשאנחנו עומדים מול חידה גדולה", - "expected_phonemes": "l a m m a | m i n k uː n | ʔ u d d aː m | l u ɣ z | k b iː r" - }, - { - "voice_time_seconds": { - "start": 150.6, - "end": 154.4 - }, - "arabic": "ما منسترجي نرفض بالرّغم من غرابة الموضوع،", - "hebrew_transliteration": "מַא מִנִסתַרגִ'י נֻרפֹּד' בִּ-רַّעְ'ם מִן עַ'רַאבֶּת אִל-מַוְדוּע,", - "hebrew": "איננו מעזים לסרב למרות מוזרות העניין,", - "expected_phonemes": "m a | m i n i s t a r dʒ i | n u r f o dˤ | b i r r a ɣ m | m i n | ɣ a r aː b e t | i l m a w dˤ uː ʕ" - }, - { - "voice_time_seconds": { - "start": 154.4, - "end": 157.4 - }, - "arabic": "ومش لازم ننسى إنّي كنت وقتها", - "hebrew_transliteration": "וּמִש לַאזֶם נִנסַא אִנִّי כֻּנֶת וַקְתְהַא", - "hebrew": "ואסור לשכוח שהייתי אז", - "expected_phonemes": "u m i ʃ | l aː z e m | n i n s a | ʔ i n n i | k u n e t | w a ʔ t h a" - }, - { - "voice_time_seconds": { - "start": 157.4, - "end": 160.6 - }, - "arabic": "بعيد ألف ميل عن أيّ بلد مأهول،", - "hebrew_transliteration": "בְּעִיד אַלְף מִיל עַן אַיّ בַּלַד מַאהוּל,", - "hebrew": "רחוק אלף מייל מכל עיר מיושבת,", - "expected_phonemes": "b ʕ iː d | ʔ a l f | m iː l | ʕ a n | ʔ a j j | b a l a d | m aː h uː l" - }, - { - "voice_time_seconds": { - "start": 160.6, - "end": 163.2 - }, - "arabic": "وحياتي محاطة بأخطار.", - "hebrew_transliteration": "וּחַיַאתִי מְחַאטַה בְּאַחְ'טַאר.", - "hebrew": "וחיי מוקפים בסכנות.", - "expected_phonemes": "u ħ a j aː t i | m ħ aː tˤ a | b ʔ a x tˤ aː r" - }, - { - "voice_time_seconds": { - "start": 163.2, - "end": 166.8 - }, - "arabic": "طلّعت من جيبتي شقفة ورقة وقلم حبر.", - "hebrew_transliteration": "טַלַّעֶת מִן גֵ'יבְּתִי שֻקפֶת וַרַקַה וּקַלַם חִבֶּר.", - "hebrew": "הוצאתי מכיסי פיסת נייר ועט.", - "expected_phonemes": "tˤ a l l a ʕ e t | m i n | dʒ eː b t i | ʃ u ʔ f e t | w a r a ʔ a | u ʔ a l a m | ħ i b e r" - }, - { - "voice_time_seconds": { - "start": 166.8, - "end": 170.0 - }, - "arabic": "وتذكّرت إنّه أغلب وقتي كان مخصّص", - "hebrew_transliteration": "וּתזַכַּרֶת אִנּוֹ אַעְ'לַבּ וַקתִי כַּאן מְחַ'צַّץ", - "hebrew": "ונזכרתי שרוב זמני הוקדש", - "expected_phonemes": "u t z a k a r e t | ʔ i n o | ʔ a ɣ l a b | w a ʔ t i | k aː n | m x a sˤ sˤ a sˤ" - }, - { - "voice_time_seconds": { - "start": 170.0, - "end": 173.4 - }, - "arabic": "للجّغرافيا، التّاريخ، لحساب والقواعد.", - "hebrew_transliteration": "לַ-גֻّעְ'רַאפְיַא, אִ-תַּארִיחַ', לֶ-חְסַאבּ וִל-קַוַאעֶד.", - "hebrew": "לגאוגרפיה, להיסטוריה, לחשבון ולדקדוק.", - "expected_phonemes": "l a g g u ɣ r aː f j a | ʔ i t aː r iː x a | l e ħ s aː b | w i l ʔ a w aː ʕ e d" - }, - { - "voice_time_seconds": { - "start": 173.4, - "end": 177.0 - }, - "arabic": "وعشان هيك قلت للولد وأنا معصّب شويّ،", - "hebrew_transliteration": "וּעַשַאן הֵיכּ קֻלֶת לַל-וַלַד וּאַנַא מְעַצֶّבּ שְוַיّ,", - "hebrew": "ולכן אמרתי לילד כשאני קצת עצבני,", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | ʔ u l e t | l a l w a l a d | u ʔ a n a | m ʕ a sˤ sˤ e b | ʃ w a j j" - }, - { - "voice_time_seconds": { - "start": 177.0, - "end": 178.8 - }, - "arabic": "إنّي بعرفش أرسم.", - "hebrew_transliteration": "אִנִّי בַּערַפ-ש אַרסֻם.", - "hebrew": "שאני לא יודע לצייר.", - "expected_phonemes": "ʔ i n n i | b a ʕ r a f ʃ | ʔ a r s u m" - }, - { - "voice_time_seconds": { - "start": 178.8, - "end": 181.4 - }, - "arabic": "بسّ هوّ ردّ عليّ وقال:", - "hebrew_transliteration": "בַּס הֻוֶّ רַדّ עַלַיّ וּקַאל:", - "hebrew": "אבל הוא ענה לי ואמר:", - "expected_phonemes": "b a s | h u w w e | r a d d | ʕ a l a j j | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 181.4, - "end": 183.8 - }, - "arabic": "«معليش، إرسملي خروف!»", - "hebrew_transliteration": "מַעלֵיש, אִרסֶמ-לִי חַ'רוּף!", - "hebrew": "לא נורא, צייר לי כבשה!", - "expected_phonemes": "m a ʕ l eː ʃ | ʔ i r s e m l i | x a r uː f" - }, - { - "voice_time_seconds": { - "start": 183.8, - "end": 186.0 - }, - "arabic": "بحياتي ما رسمت خرفان،", - "hebrew_transliteration": "בְּחַיַאתִי מַא רַסַמֶת חִ'רפַאן,", - "hebrew": "בחיים לא ציירתי כבשים,", - "expected_phonemes": "b ħ a j aː t i | m a | r a s a m e t | x i r f aː n" - }, - { - "voice_time_seconds": { - "start": 186.0, - "end": 188.2 - }, - "arabic": "وعشان هيك رسمت وحدة", - "hebrew_transliteration": "וּעַשַאן הֵיכּ רַסַמֶת וַחדֶה", - "hebrew": "ולכן ציירתי אחד", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | r a s a m e t | w a ħ d e" - }, - { - "voice_time_seconds": { - "start": 188.2, - "end": 191.8 - }, - "arabic": "من الرّسمتين إلّي كانت إيدي متعوّدة ترسمهم،", - "hebrew_transliteration": "מִן אִ-רַּסמִתֵין אִלִّי כַּאנַת אִידִי מִתעַוְּדֶה תִרסִמְהֹם,", - "hebrew": "משני הציורים שידי הייתה רגילה לצייר,", - "expected_phonemes": "m i n | ʔ i r a s m i t eː n | ʔ i l l i | k aː n a t | ʔ iː d i | m i t ʕ a w d e | t i r s i m h o m" - }, - { - "voice_time_seconds": { - "start": 191.8, - "end": 194.4 - }, - "arabic": "رسمت حيّة البوا من برّا.", - "hebrew_transliteration": "רַסַמֶת חַיֶّת אִל-בֻּוַא מִן בַּרַّא.", - "hebrew": "ציירתי את נחש הבואה מבחוץ.", - "expected_phonemes": "r a s a m e t | ħ a j j e t | i l b u w a | m i n | b a r r a" - }, - { - "voice_time_seconds": { - "start": 194.4, - "end": 197.6 - }, - "arabic": "وإنصدمت كتير لمّا الولد قال:", - "hebrew_transliteration": "וּאִנצַדַמֶת כְּתִיר לַמַّא אִל-וַלַד קַאל:", - "hebrew": "והזדעזעתי מאוד כשהילד אמר:", - "expected_phonemes": "u ʔ i n sˤ a d a m e t | k t iː r | l a m m a | i l w a l a d | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 197.6, - "end": 201.4 - }, - "arabic": "«لا، لا، بدّيش رسمة فيل بقلب حيّة بوا.", - "hebrew_transliteration": "לַא, לַא, בַּדִّי-ש רַסמֶת פִיל בְּקַלבּ חַיֶّת בֻּוַא.", - "hebrew": "לא, לא, אני לא רוצה ציור של פיל בתוך נחש בואה.", - "expected_phonemes": "l aː | l aː | b a d d iː ʃ | r a s m e t | f iː l | b ʔ a l b | ħ a j j e t | b u w a" - }, - { - "voice_time_seconds": { - "start": 201.4, - "end": 205.0 - }, - "arabic": "حيّة البوا مخلوق خطير والفيل تقيل.", - "hebrew_transliteration": "חַיֶّת אִל-בֻּוַא מַחְ'לוּק חַ'טִיר וִל-פִיל תְקִיל.", - "hebrew": "נחש הבואה הוא יצור מסוכן והפיל כבד.", - "expected_phonemes": "ħ a j j e t | i l b u w a | m a x l uː ʔ | x a tˤ iː r | w i l f iː l | t ʔ iː l" - }, - { - "voice_time_seconds": { - "start": 205.0, - "end": 207.6 - }, - "arabic": "وأنا عندي كلّ إشي صغير كتير.", - "hebrew_transliteration": "וּאַנַא עִנדִי כֻּלّ אִשִי זְעִ'יר כְּתִיר.", - "hebrew": "ואצלי הכול קטן מאוד.", - "expected_phonemes": "u ʔ a n a | ʕ i n d i | k u l l | ʔ i ʃ i | z ɣ iː r | k t iː r" - }, - { - "voice_time_seconds": { - "start": 207.6, - "end": 211.4 - }, - "arabic": "أنا بحاجة لخروف، إرسملي خروف!»", - "hebrew_transliteration": "אַנַא בְּחַאגֶ'ה לַחַ'רוּף, אִרסֶמ-לִי חַ'רוּף!", - "hebrew": "אני צריך כבשה, צייר לי כבשה!", - "expected_phonemes": "ʔ a n a | b ħ aː dʒ e | l a x a r uː f | ʔ i r s e m l i | x a r uː f" - }, - { - "voice_time_seconds": { - "start": 211.4, - "end": 213.0 - }, - "arabic": "رسمت.", - "hebrew_transliteration": "רַסַמֶת.", - "hebrew": "ציירתי.", - "expected_phonemes": "r a s a m e t" - }, - { - "voice_time_seconds": { - "start": 213.0, - "end": 216.8 - }, - "arabic": "هوّ إتطلّع على الرّسمة بإنتباه كبير وقال:", - "hebrew_transliteration": "הֻוֶّ תְטַלַّע עַלַא אִ-רַּסמֶה בְּאִנתִבַּאה כְּבִּיר וּקַאל:", - "hebrew": "הוא הסתכל על הציור בתשומת לב רבה ואמר:", - "expected_phonemes": "h u w w e | t tˤ a l l a ʕ | ʕ a l a | ʔ i r a s m e | b ʔ i n t i b aː h | k b iː r | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 216.8, - "end": 220.8 - }, - "arabic": "«لا! هدا الخروف شكله مريض كتير.", - "hebrew_transliteration": "לַא! הַאדַא אִל-חַ'רוּף שַכְּלוֹ מַרִיד' כְּתִיר.", - "hebrew": "לא! הכבשה הזאת נראית חולה מאוד.", - "expected_phonemes": "l aː | h aː d a | i l x a r uː f | ʃ a k l o | m a r iː dˤ | k t iː r" - }, - { - "voice_time_seconds": { - "start": 220.8, - "end": 222.8 - }, - "arabic": "إرسملي خروف تاني.»", - "hebrew_transliteration": "אִרסֶמ-לִי חַ'רוּף תַאנִי.", - "hebrew": "צייר לי כבשה אחרת.", - "expected_phonemes": "ʔ i r s e m l i | x a r uː f | t aː n i" - }, - { - "voice_time_seconds": { - "start": 222.8, - "end": 224.4 - }, - "arabic": "رسمت كمان مرّة.", - "hebrew_transliteration": "רַסַמֶת כַּמַאן מַרַّה.", - "hebrew": "ציירתי עוד פעם.", - "expected_phonemes": "r a s a m e t | k a m aː n | m a r r a" - }, - { - "voice_time_seconds": { - "start": 224.4, - "end": 227.0 - }, - "arabic": "صاحبي لصغير إبتسم وقال:", - "hebrew_transliteration": "צַאחְבִּי לֶ-זְעִ'יר אִבּתַסַם וּקַאל:", - "hebrew": "חברי הקטן חייך ואמר:", - "expected_phonemes": "sˤ aː ħ b i | l e z ɣ iː r | ʔ i b t a s a m | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 227.0, - "end": 230.0 - }, - "arabic": "«أكيد إنت شايف إنّه هدا مش خروف", - "hebrew_transliteration": "אַכִּיד אִנתֶ שַאיֶף אִנּוֹ הַאדַא מִש חַ'רוּף", - "hebrew": "בטח אתה רואה שזו לא כבשה", - "expected_phonemes": "ʔ a k iː d | ʔ i n t e | ʃ aː j e f | ʔ i n o | h aː d a | m i ʃ | x a r uː f" - }, - { - "voice_time_seconds": { - "start": 230.0, - "end": 233.2 - }, - "arabic": "وإنّما كبش. فيه عنده قرون...»", - "hebrew_transliteration": "וּאִנַّמַא כַּבֶּש. פִיה עִנדוֹ קְרוּן...", - "hebrew": "אלא איל. יש לו קרניים...", - "expected_phonemes": "u ʔ i n n a m a | k a b e ʃ | f iː h | ʕ i n d o | ʔ r uː n" - }, - { - "voice_time_seconds": { - "start": 233.2, - "end": 235.4 - }, - "arabic": "رجعت ورسمت من جديد،", - "hebrew_transliteration": "רְגִ'עֶת וּרַסַמֶת מִן גְ'דִיד,", - "hebrew": "חזרתי וציירתי מחדש,", - "expected_phonemes": "r dʒ i ʕ e t | u r a s a m e t | m i n | dʒ d iː d" - }, - { - "voice_time_seconds": { - "start": 235.4, - "end": 238.8 - }, - "arabic": "بسّ برضه رسمتي هاي ما عجبته", - "hebrew_transliteration": "בַּס בַּרְדוֹ רַסמִתִי הַאי מַא עַגַ'בַּתוֹ", - "hebrew": "אבל גם הציור הזה לא מצא חן בעיניו", - "expected_phonemes": "b a s | b a r d o | r a s m i t i | h aː i | m a | ʕ a dʒ a b a t o" - }, - { - "voice_time_seconds": { - "start": 238.8, - "end": 241.8 - }, - "arabic": "متل الرّسمتين الأوّلنيّات.", - "hebrew_transliteration": "מִתֶל אִ-רַּסמִתֵין אִל-אַוַّלַנִיַّאת.", - "hebrew": "כמו שני הציורים הראשונים.", - "expected_phonemes": "m i t e l | ʔ i r a s m i t eː n | i l ʔ a w w a l a n i j j aː t" - }, - { - "voice_time_seconds": { - "start": 241.8, - "end": 245.4 - }, - "arabic": "«هدا الخروف كتير ختيار،» هوّ قال،", - "hebrew_transliteration": "הַאדַא אִל-חַ'רוּף כְּתִיר חְ'תִיַאר, הֻוֶّ קַאל,", - "hebrew": "הכבשה הזאת זקנה מאוד, הוא אמר,", - "expected_phonemes": "h aː d a | i l x a r uː f | k t iː r | x t i j aː r | h u w w e | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 245.4, - "end": 248.2 - }, - "arabic": "«بدّي خروف يعيش كتير.»", - "hebrew_transliteration": "בַּדִّי חַ'רוּף יְעִיש כְּתִיר.", - "hebrew": "אני רוצה כבשה שתחיה הרבה.", - "expected_phonemes": "b a d d i | x a r uː f | j ʕ iː ʃ | k t iː r" - }, - { - "voice_time_seconds": { - "start": 248.2, - "end": 250.0 - }, - "arabic": "بطّل عندي صبر،", - "hebrew_transliteration": "בַּטַّל עִנדִי צַבֶּר,", - "hebrew": "נגמרה לי הסבלנות,", - "expected_phonemes": "b a tˤ tˤ a l | ʕ i n d i | sˤ a b e r" - }, - { - "voice_time_seconds": { - "start": 250.0, - "end": 252.8 - }, - "arabic": "عشان كان بدّي أبلّش أفكّ الماطور،", - "hebrew_transliteration": "עַשַאן כַּאן בַּדִّי אַבַּלֶّש אַפֻכّ אִל-מַאטוֹר,", - "hebrew": "כי רציתי להתחיל לפרק את המנוע,", - "expected_phonemes": "ʕ a ʃ aː n | k aː n | b a d d i | ʔ a b a l l e ʃ | ʔ a f u k k | i l m aː tˤ oː r" - }, - { - "voice_time_seconds": { - "start": 252.8, - "end": 255.8 - }, - "arabic": "ولهيك رسمت بخطوط دفشة،", - "hebrew_transliteration": "וּלַהֵיכּ רַסַמֶת בְּחֻ'טוּט דַפשֶה,", - "hebrew": "ולכן ציירתי בקווים גסים,", - "expected_phonemes": "u l a h eː k | r a s a m e t | b x u tˤ uː tˤ | d a f ʃ e" - }, - { - "voice_time_seconds": { - "start": 255.8, - "end": 257.8 - }, - "arabic": "هاي الرّسمة وقلت:", - "hebrew_transliteration": "הַאי אִ-רַּסמֶה וּקֻלֶת:", - "hebrew": "את הציור הזה ואמרתי:", - "expected_phonemes": "h aː i | ʔ i r a s m e | u ʔ u l e t" - }, - { - "voice_time_seconds": { - "start": 257.8, - "end": 261.0 - }, - "arabic": "«هدا صندوق، الخروف إلّي بدّك إيّاه", - "hebrew_transliteration": "הַאדַא צַנדוּק, אִל-חַ'רוּף אִלִّי בַּדַّכּ אִיַّאה", - "hebrew": "זו קופסה, הכבשה שאתה רוצה", - "expected_phonemes": "h aː d a | sˤ a n d uː ʔ | i l x a r uː f | ʔ i l l i | b a d d a k | ʔ i j j aː h" - }, - { - "voice_time_seconds": { - "start": 261.0, - "end": 263.2 - }, - "arabic": "موجود جوّا الصّندوق.»", - "hebrew_transliteration": "מַוְגּ'וּד גֻ'וַّא אִ-צַּנדוּק.", - "hebrew": "נמצאת בתוך הקופסה.", - "expected_phonemes": "m a w dʒ uː d | dʒ u w w a | ʔ i sˤ a n d uː ʔ" - }, - { - "voice_time_seconds": { - "start": 263.2, - "end": 267.2 - }, - "arabic": "تعجّبت إنّه حاكمي لصغير إنبسط وقال:", - "hebrew_transliteration": "תְעַגַّבֶּת אִנּוֹ חַאכְּמִי לֶ-זְעִ'יר אִנבַּסַט וּקַאל:", - "hebrew": "התפלאתי שהשליט הקטן שלי שמח ואמר:", - "expected_phonemes": "t ʕ a g g a b e t | ʔ i n o | ħ aː k m i | l e z ɣ iː r | ʔ i n b a s a tˤ | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 267.2, - "end": 269.6 - }, - "arabic": "«هدا بالضّبط إلّي بدّي إيّاه!", - "hebrew_transliteration": "הַאדַא בִּ-דַּבֶּט אִלִّי בַּדִّי אִיַّאה!", - "hebrew": "זה בדיוק מה שאני רוצה!", - "expected_phonemes": "h aː d a | b i d a b e tˤ | ʔ i l l i | b a d d i | ʔ i j j aː h" - }, - { - "voice_time_seconds": { - "start": 269.6, - "end": 274.2 - }, - "arabic": "بسّ فكرك هدا الخروف بحاجة لكتير عشب؟»", - "hebrew_transliteration": "בַּס פִכְרַכּ הַאדַא אִל-חַ'רוּף בְּחַאגֶ'ה לַכְּתִיר עֻשֶבּ?", - "hebrew": "אבל לדעתך הכבשה הזאת צריכה הרבה עשב?", - "expected_phonemes": "b a s | f i k r a k | h aː d a | i l x a r uː f | b ħ aː dʒ e | l a k t iː r | ʕ u ʃ e b" - }, - { - "voice_time_seconds": { - "start": 274.2, - "end": 276.8 - }, - "arabic": "«ليش بتسألني هدا السّؤال؟»", - "hebrew_transliteration": "לֵיש בְּתִסאַלנִי הַאדַא אִ-סֻּאַאל?", - "hebrew": "למה אתה שואל אותי את השאלה הזאת?", - "expected_phonemes": "l eː ʃ | b t i s ʔ a l n i | h aː d a | ʔ i s u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 276.8, - "end": 280.2 - }, - "arabic": "«منشان عندي أنا كلّ إشي صغير...»", - "hebrew_transliteration": "מִנשַאן עִנדִי אַנַא כֻּלّ אִשִי זְעִ'יר...", - "hebrew": "כי אצלי הכול קטן...", - "expected_phonemes": "m i n ʃ aː n | ʕ i n d i | ʔ a n a | k u l l | ʔ i ʃ i | z ɣ iː r" - }, - { - "voice_time_seconds": { - "start": 280.2, - "end": 283.6 - }, - "arabic": "«العشب رح يكفّيه أكيد،» أنا قلت،", - "hebrew_transliteration": "אִל-עֻשֶבּ רַח-יְכַפִّיה אַכִּיד, אַנַא קֻלֶת,", - "hebrew": "העשב בטח יספיק לה, אמרתי,", - "expected_phonemes": "i l ʕ u ʃ e b | r a ħ j k a f f iː h | ʔ a k iː d | ʔ a n a | ʔ u l e t" - }, - { - "voice_time_seconds": { - "start": 283.6, - "end": 286.6 - }, - "arabic": "«لإنّي أعطيتك خروف صغير صغنّون.»", - "hebrew_transliteration": "לַאִנִّי אַעטֵיתַכּ חַ'רוּף זְעִ'יר זְעַ'נּוּן.", - "hebrew": "כי נתתי לך כבשה קטנה וקטנטנה.", - "expected_phonemes": "l a ʔ i n n i | ʔ a ʕ tˤ eː t a k | x a r uː f | z ɣ iː r | z ɣ a n uː n" - }, - { - "voice_time_seconds": { - "start": 286.6, - "end": 289.6 - }, - "arabic": "هوّ حنا راسه فوق الرّسمة وقال:", - "hebrew_transliteration": "הֻוֶّ חַנַא רַאסוֹ פוֹק אִ-רַּסמֶה וּקַאל:", - "hebrew": "הוא הרכין את ראשו מעל הציור ואמר:", - "expected_phonemes": "h u w w e | ħ a n a | r aː s o | f oː ʔ | ʔ i r a s m e | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 289.6, - "end": 293.6 - }, - "arabic": "«لأ، مش صغير هالقدّ... شوف، هيّاه نام!...»", - "hebrew_transliteration": "לַא, מִש זְעִ'יר הַל-קַדّ... שוּף, הִיַّאה נַאם!...", - "hebrew": "לא, לא כל כך קטנה... תראה, היא נרדמה!...", - "expected_phonemes": "l aː | m i ʃ | z ɣ iː r | h a l ʔ a d d | ʃ uː f | h i j j aː h | n aː m" - }, - { - "voice_time_seconds": { - "start": 293.6, - "end": 299.8 - }, - "arabic": "وهيك إتعرّفت على الأمير لصغير.", - "hebrew_transliteration": "וּהֵיכּ אִתעַרַّפֶת עַלַא אִל-אַמִיר לֶ-זְעִ'יר.", - "hebrew": "וכך הכרתי את הנסיך הקטן.", - "expected_phonemes": "u h eː k | ʔ i t ʕ a r r a f e t | ʕ a l a | i l ʔ a m iː r | l e z ɣ iː r" - } - ] - }, - { - "file_name": "סיפור התאנה * קֻסת חַבֶּתֶ א-תִין * قصة حبّة التّين * סיפור בערבית מדוברת [mDdcOiORSMc].webm", - "youtube_id": "mDdcOiORSMc", - "title_hebrew": "סיפור התאנה", - "segments": [ - { - "voice_time_seconds": { - "start": 8.2, - "end": 12.2 - }, - "arabic": "كانت الدّنيا صبح والسّوق ملان ناس.", - "hebrew_transliteration": "כַּאנַת אִ-דֻּנְיַא צֻבֶּח וִ-סּוּק מַלַאן נַאס.", - "hebrew": "היה בוקר והשוק היה מלא אנשים.", - "expected_phonemes": "k aː n a t | ʔ i d u n j a | sˤ u b e ħ | w i s uː ʔ | m a l aː n | n aː s" - }, - { - "voice_time_seconds": { - "start": 12.2, - "end": 14.6 - }, - "arabic": "بسّ على عكس كلّ يوم تاني،", - "hebrew_transliteration": "בַּס עַלַא עַכֶּס כֻּלّ יוֹם תַאנִי,", - "hebrew": "אבל בניגוד לכל יום אחר,", - "expected_phonemes": "b a s | ʕ a l a | ʕ a k e s | k u l l | j oː m | t aː n i" - }, - { - "voice_time_seconds": { - "start": 14.6, - "end": 17.0 - }, - "arabic": "التّجّار ما فتحوش المحلّات", - "hebrew_transliteration": "אִ-תֻּגַّאר מַא פַתַחוּש אִל-מַחַלַّאת", - "hebrew": "הסוחרים לא פתחו את החנויות", - "expected_phonemes": "ʔ i t u g g aː r | m a | f a t a ħ uː ʃ | i l m a ħ a l l aː t" - }, - { - "voice_time_seconds": { - "start": 17.0, - "end": 19.4 - }, - "arabic": "والنّاس ما أجتش تشتري.", - "hebrew_transliteration": "וִ-נַּאס מַא אַגַ'תֶש תִשתְרִי.", - "hebrew": "והאנשים לא באו לקנות.", - "expected_phonemes": "w i n aː s | m a | ʔ a dʒ a t e ʃ | t i ʃ t r i" - }, - { - "voice_time_seconds": { - "start": 19.4, - "end": 22.6 - }, - "arabic": "الكلّ تجمّع بسّ لهدف واحد:", - "hebrew_transliteration": "אִל-כֻּלّ תְגַ'מַّע בַּס לַהַדַף וַאחַד:", - "hebrew": "כולם התאספו רק למטרה אחת:", - "expected_phonemes": "i l k u l l | t dʒ a m m a ʕ | b a s | l a h a d a f | w aː ħ a d" - }, - { - "voice_time_seconds": { - "start": 22.6, - "end": 26.2 - }, - "arabic": "يحضروا إعدام حسن بن سليمان،", - "hebrew_transliteration": "יִחְדַ'רוּ אִעדַאם חַסַן בִּן סֻלֵימַאן,", - "hebrew": "לנכוח בהוצאתו להורג של חסן בן סולימאן,", - "expected_phonemes": "j i ħ dˤ a r u | ʔ i ʕ d aː m | ħ a s a n | b i n | s u l eː m aː n" - }, - { - "voice_time_seconds": { - "start": 26.2, - "end": 28.4 - }, - "arabic": "الحرامي المشهور.", - "hebrew_transliteration": "אִל-חַרַאמִי לֶ-מַשהוּר.", - "hebrew": "הגנב המפורסם.", - "expected_phonemes": "i l ħ a r aː m i | l e m a ʃ h uː r" - }, - { - "voice_time_seconds": { - "start": 28.4, - "end": 31.2 - }, - "arabic": "الإشاعة بتقول إنّه طلع حكمه.", - "hebrew_transliteration": "אִל-אִשַאעַה בִּתקוּל אִנּוֹ טִלֶע חֻכְּמוֹ.", - "hebrew": "השמועה אומרת שגזר דינו נקבע.", - "expected_phonemes": "i l ʔ i ʃ aː ʕ a | b i t ʔ uː l | ʔ i n o | tˤ i l e ʕ | ħ u k m o" - }, - { - "voice_time_seconds": { - "start": 31.2, - "end": 34.2 - }, - "arabic": "الكلّ كان يعرف إنّه حرامي وسرّاق،", - "hebrew_transliteration": "אִל-כֻּלّ כַּאן יִערַף אִנּוֹ חַרַאמִי וּסַרַّאק,", - "hebrew": "כולם ידעו שהוא גנב ושודד,", - "expected_phonemes": "i l k u l l | k aː n | j i ʕ r a f | ʔ i n o | ħ a r aː m i | u s a r r aː ʔ" - }, - { - "voice_time_seconds": { - "start": 34.2, - "end": 38.2 - }, - "arabic": "بسّ كتير اتعجّبوا من إنّه رح يتعاقب بالفعل.", - "hebrew_transliteration": "בַּס כְּתִיר אִתעַגַّבּוּ מִן אִנּוֹ רַח-יִתעַאקַבּ בִּל-פִעֶל.", - "hebrew": "אבל רבים השתוממו מכך שהוא באמת ייענש.", - "expected_phonemes": "b a s | k t iː r | ʔ i t ʕ a g g a b u | m i n | ʔ i n o | r a ħ j i t ʕ aː ʔ a b | b i l f i ʕ e l" - }, - { - "voice_time_seconds": { - "start": 38.2, - "end": 41.4 - }, - "arabic": "لإنّه حسن جاي من عيلة غنيّة محترمة،", - "hebrew_transliteration": "לַאִנּוֹ חַסַן גַ'אי מִן עֵילֶה עַ'נִיֶّה מֻחתַרַמֶה,", - "hebrew": "כי חסן בא ממשפחה עשירה ומכובדת,", - "expected_phonemes": "l a ʔ i n o | ħ a s a n | dʒ aː j | m i n | ʕ eː l e | ɣ a n i j j e | m u ħ t a r a m e" - }, - { - "voice_time_seconds": { - "start": 41.4, - "end": 45.2 - }, - "arabic": "إلّي كتير من ولادها كانوا مجرمين مش صغار،", - "hebrew_transliteration": "אִלִّי כְּתִיר מִן וְלַאדְהַא כַּאנוּ מֻגְ'רִמִין מִש זְעַ'אר,", - "hebrew": "שרבים מבניה היו פושעים לא קטנים,", - "expected_phonemes": "ʔ i l l i | k t iː r | m i n | u l aː d h a | k aː n u | m u dʒ r i m iː n | m i ʃ | z ɣ aː r" - }, - { - "voice_time_seconds": { - "start": 45.2, - "end": 48.6 - }, - "arabic": "وبحياته ما واحد منهم إنشنق.", - "hebrew_transliteration": "וּבִּחַיַאתוֹ מַא וַאחַד מִנְהֹם אִנשַנַק.", - "hebrew": "ומעולם אף אחד מהם לא נתלה.", - "expected_phonemes": "u b i ħ a j aː t o | m a | w aː ħ a d | m i n h o m | ʔ i n ʃ a n a ʔ" - }, - { - "voice_time_seconds": { - "start": 48.6, - "end": 51.8 - }, - "arabic": "هل هاي المرّة المصاري والعلاقات", - "hebrew_transliteration": "הַל הַאי אִל-מַרַّה אִל-מַצַארִי וִל-עַלַאקַאת", - "hebrew": "האם הפעם הכסף והקשרים", - "expected_phonemes": "h a l | h aː i | i l m a r r a | i l m a sˤ aː r i | w i l ʕ a l aː ʔ aː t" - }, - { - "voice_time_seconds": { - "start": 51.8, - "end": 53.6 - }, - "arabic": "مش رح تساعد؟", - "hebrew_transliteration": "מִש רַח-תְסַאעֶד?", - "hebrew": "לא יעזרו?", - "expected_phonemes": "m i ʃ | r a ħ t s aː ʕ e d" - }, - { - "voice_time_seconds": { - "start": 53.6, - "end": 56.6 - }, - "arabic": "شكله لأ. الملك قرّر:", - "hebrew_transliteration": "שַכְּלוֹ לַא. אִל-מַלִכּ קַרַّר:", - "hebrew": "נראה שלא. המלך החליט:", - "expected_phonemes": "ʃ a k l o | l aː | i l m a l i k | ʔ a r r a r" - }, - { - "voice_time_seconds": { - "start": 56.6, - "end": 61.2 - }, - "arabic": "اليوم حسن رح ينشنق في دوّار البلد.", - "hebrew_transliteration": "אִל-יוֹם חַסַן רַח-יִנשְנֶק פִי דַוַّאר אִל-בַּלַד.", - "hebrew": "היום חסן ייתלה בכיכר העיר.", - "expected_phonemes": "i l j oː m | ħ a s a n | r a ħ j i n ʃ n e ʔ | f i | d a w w aː r | i l b a l a d" - }, - { - "voice_time_seconds": { - "start": 61.2, - "end": 63.8 - }, - "arabic": "الملك أجا مع المستشارين تاعونه،", - "hebrew_transliteration": "אִל-מַלִכּ אַגַ'א מַע אִל-מֻסתַשַארִין תַאעוּנוֹ,", - "hebrew": "המלך בא עם יועציו,", - "expected_phonemes": "i l m a l i k | ʔ a dʒ a | m a ʕ | i l m u s t a ʃ aː r iː n | t aː ʕ uː n o" - }, - { - "voice_time_seconds": { - "start": 63.8, - "end": 66.0 - }, - "arabic": "مع الوزراء والقضاة.", - "hebrew_transliteration": "מַע אִל-וֻזַרַא וִל-קֻדַ'אה.", - "hebrew": "עם השרים והשופטים.", - "expected_phonemes": "m a ʕ | i l w u z a r a | w i l ʔ u dˤ aː h" - }, - { - "voice_time_seconds": { - "start": 66.0, - "end": 67.8 - }, - "arabic": "وقعد على المنصّة المحترمة", - "hebrew_transliteration": "וּקַעַד עַלַא לֶ-מַנַצַّה אִל-מֻחתַרַמֶה", - "hebrew": "וישב על הבמה המכובדת", - "expected_phonemes": "u ʔ a ʕ a d | ʕ a l a | l e m a n a sˤ sˤ a | i l m u ħ t a r a m e" - }, - { - "voice_time_seconds": { - "start": 67.8, - "end": 70.4 - }, - "arabic": "إلّي عملوها عشانه.", - "hebrew_transliteration": "אִלִّי עִמלוּהַא עַשַאנוֹ.", - "hebrew": "שבנו בשבילו.", - "expected_phonemes": "ʔ i l l i | ʕ i m l uː h a | ʕ a ʃ aː n o" - }, - { - "voice_time_seconds": { - "start": 70.4, - "end": 73.8 - }, - "arabic": "جابوا حسن لعنده، مكبّل بالقيود.", - "hebrew_transliteration": "גַ'אבּוּ חַסַן לַעִנדוֹ, מְכַּבַּל בִּל-קְיוּד.", - "hebrew": "הביאו אליו את חסן, כבול באזיקים.", - "expected_phonemes": "dʒ aː b u | ħ a s a n | l a ʕ i n d o | m k a b a l | b i l ʔ j uː d" - }, - { - "voice_time_seconds": { - "start": 73.8, - "end": 78.2 - }, - "arabic": "«حسن،» قال الملك، «مصيرك تقرّر.", - "hebrew_transliteration": "חַסַן, קַאל אִל-מַלִכּ, מַצִירַכּ תְקַרַّר.", - "hebrew": "חסן, אמר המלך, גורלך נחרץ.", - "expected_phonemes": "ħ a s a n | ʔ aː l | i l m a l i k | m a sˤ iː r a k | t ʔ a r r a r" - }, - { - "voice_time_seconds": { - "start": 78.2, - "end": 81.8 - }, - "arabic": "فيه عندك طلب أخير قبل موتك؟»", - "hebrew_transliteration": "פִיה עִנדַכּ טַלַבּ אַחִ'יר קַבֶּל מוֹתַכּ?", - "hebrew": "יש לך בקשה אחרונה לפני מותך?", - "expected_phonemes": "f iː h | ʕ i n d a k | tˤ a l a b | ʔ a x iː r | ʔ a b e l | m oː t a k" - }, - { - "voice_time_seconds": { - "start": 81.8, - "end": 85.8 - }, - "arabic": "حسن اترجّى: «الرّحمة يا سيدي الملك!»", - "hebrew_transliteration": "חַסַן אִתרַגַّא: אִ-רַּחמֶה יַא סִידִי אִל-מַלִכּ!", - "hebrew": "חסן התחנן: רחמים, אדוני המלך!", - "expected_phonemes": "ħ a s a n | ʔ i t r a g g a | ʔ i r a ħ m e | j a | s iː d i | i l m a l i k" - }, - { - "voice_time_seconds": { - "start": 85.8, - "end": 90.4 - }, - "arabic": "الملك قرّر بصرامة: «فش رحمة للحراميّة.»", - "hebrew_transliteration": "אִל-מַלִכּ קַרַّר בְּצַרַאמֶה: פִש רַחמֶה לַל-חַרַאמִיֶّה.", - "hebrew": "המלך החליט בתקיפות: אין רחמים לגנבים.", - "expected_phonemes": "i l m a l i k | ʔ a r r a r | b sˤ a r aː m e | f i ʃ | r a ħ m e | l a l ħ a r aː m i j j e" - }, - { - "voice_time_seconds": { - "start": 90.4, - "end": 92.2 - }, - "arabic": "حسن قال:", - "hebrew_transliteration": "חַסַן קַאל:", - "hebrew": "חסן אמר:", - "expected_phonemes": "ħ a s a n | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 92.2, - "end": 96.4 - }, - "arabic": "«إذا هيك، فيه عندي بسّ طلب واحد.", - "hebrew_transliteration": "אִזַא הֵיכּ, פִיה עִנדִי בַּס טַלַבּ וַאחַד.", - "hebrew": "אם כך, יש לי רק בקשה אחת.", - "expected_phonemes": "ʔ i z a | h eː k | f iː h | ʕ i n d i | b a s | tˤ a l a b | w aː ħ a d" - }, - { - "voice_time_seconds": { - "start": 96.4, - "end": 98.8 - }, - "arabic": "فيه معي سرّ كبير،", - "hebrew_transliteration": "פִיה מַעִי סִרّ כְּבִּיר,", - "hebrew": "יש לי סוד גדול,", - "expected_phonemes": "f iː h | m a ʕ i | s i r r | k b iː r" - }, - { - "voice_time_seconds": { - "start": 98.8, - "end": 103.2 - }, - "arabic": "هديّة إلّي بتقدر تنقذ الفقراء من الجوع.", - "hebrew_transliteration": "הַדִיֶّה אִלִّי בְּתִקדַר תִנקֶד' אִל-פֻקַרַאא מִן אִ-גּוּע.", - "hebrew": "מתנה שיכולה להציל את העניים מרעב.", - "expected_phonemes": "h a d i j j e | ʔ i l l i | b t i ʔ d a r | t i n ʔ e dˤ | i l f u ʔ a r aː a | m i n | ʔ i g uː ʕ" - }, - { - "voice_time_seconds": { - "start": 103.2, - "end": 105.8 - }, - "arabic": "تقبل الملك يوخدها؟»", - "hebrew_transliteration": "תִקבַּל אִל-מַלִכּ יוֹחְ'דְהַא?", - "hebrew": "האם המלך מסכים לקבל אותה?", - "expected_phonemes": "t i ʔ b a l | i l m a l i k | j oː x d h a" - }, - { - "voice_time_seconds": { - "start": 105.8, - "end": 109.4 - }, - "arabic": "الملك صار عنده فضول وسأل:", - "hebrew_transliteration": "אִל-מַלִכּ צַאר עִנדוֹ פֻדוּל וּסַאַל:", - "hebrew": "אצל המלך התעוררה סקרנות והוא שאל:", - "expected_phonemes": "i l m a l i k | sˤ aː r | ʕ i n d o | f u d uː l | u s a ʔ a l" - }, - { - "voice_time_seconds": { - "start": 109.4, - "end": 111.6 - }, - "arabic": "«عن شو الحكي؟»", - "hebrew_transliteration": "עַן שוּ אִל-חַכִּי?", - "hebrew": "על מה מדובר?", - "expected_phonemes": "ʕ a n | ʃ u | i l ħ a k i" - }, - { - "voice_time_seconds": { - "start": 111.6, - "end": 113.4 - }, - "arabic": "حسن حكى:", - "hebrew_transliteration": "חַסַן חַכַא:", - "hebrew": "חסן אמר:", - "expected_phonemes": "ħ a s a n | ħ a k a" - }, - { - "voice_time_seconds": { - "start": 113.4, - "end": 115.2 - }, - "arabic": "«خلال سفراتي", - "hebrew_transliteration": "חִ'לַאל סַפְרַאתִי", - "hebrew": "במהלך מסעותיי", - "expected_phonemes": "x i l aː l | s a f r aː t i" - }, - { - "voice_time_seconds": { - "start": 115.2, - "end": 118.8 - }, - "arabic": "تعلّمت أسرار الحكماء القدامى.", - "hebrew_transliteration": "תְעַלַّמֶת אַסרַאר אִל-חֻכַּמַאא אִל-קֻדַמַא.", - "hebrew": "למדתי את סודות החכמים הקדמונים.", - "expected_phonemes": "t ʕ a l l a m e t | ʔ a s r aː r | i l ħ u k a m aː a | i l ʔ u d a m a" - }, - { - "voice_time_seconds": { - "start": 118.8, - "end": 124.0 - }, - "arabic": "أنا بعرف أكتب تعويذة خاصّة على حبّة التّين،", - "hebrew_transliteration": "אַנַא בַּערַף אַכּתֶבּ תַעוִידֶ'ה חַ'אצַّה עַלַא חַבֶּת אִ-תִּין,", - "hebrew": "אני יודע לכתוב לחש מיוחד על תאנה,", - "expected_phonemes": "ʔ a n a | b a ʕ r a f | ʔ a k t e b | t a ʕ w iː dˤ e | x aː sˤ sˤ a | ʕ a l a | ħ a b e t | ʔ i t iː n" - }, - { - "voice_time_seconds": { - "start": 124.0, - "end": 126.0 - }, - "arabic": "وإذا منزرعها في الأرض،", - "hebrew_transliteration": "וּאִזַא מִנִזרַעְהַא פִי לְ-אַרְד',", - "hebrew": "ואם נשתול אותה באדמה,", - "expected_phonemes": "u ʔ i z a | m i n i z r a ʕ h a | f i | l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 126.0, - "end": 130.8 - }, - "arabic": "تتحوّل دغري لشجرة عملاقة معبّية فواكه", - "hebrew_transliteration": "תִתחַוַّל דֻעְ'רִי לַשַגַ'רַה עִמלַאקַה מְעַבַּיֶה פַוַאכֶּה", - "hebrew": "היא תהפוך מיד לעץ ענק מלא פירות", - "expected_phonemes": "t i t ħ a w w a l | d u ɣ r i | l a ʃ a dʒ a r a | ʕ i m l aː ʔ a | m ʕ a b a j e | f a w aː k e" - }, - { - "voice_time_seconds": { - "start": 130.8, - "end": 134.4 - }, - "arabic": "إلّي بتقدر تشبّع الحشود.»", - "hebrew_transliteration": "אִלִّי בְּתִקדַר תְשַבֶּע אִל-חֻשוּד.", - "hebrew": "שיכול להשביע את ההמונים.", - "expected_phonemes": "ʔ i l l i | b t i ʔ d a r | t ʃ a b e ʕ | i l ħ u ʃ uː d" - }, - { - "voice_time_seconds": { - "start": 134.4, - "end": 136.2 - }, - "arabic": "الملك اتفاجئ", - "hebrew_transliteration": "אִל-מַלִכּ אִתפַאגַא", - "hebrew": "המלך הופתע", - "expected_phonemes": "i l m a l i k | ʔ i t f aː g a" - }, - { - "voice_time_seconds": { - "start": 136.2, - "end": 138.6 - }, - "arabic": "وقبل يفحص الموضوع.", - "hebrew_transliteration": "וּקִבֶל יִפחַץ אִל-מַוְדוּע.", - "hebrew": "והסכים לבדוק את העניין.", - "expected_phonemes": "u ʔ i b e l | j i f ħ a sˤ | i l m a w dˤ uː ʕ" - }, - { - "voice_time_seconds": { - "start": 138.6, - "end": 143.2 - }, - "arabic": "جابوا لحسن حبّة تين، حبر وريشة.", - "hebrew_transliteration": "גַ'אבּוּ לַחַסַן חַבֶּת תִין, חִבֶּר וּרִישֶה.", - "hebrew": "הביאו לחסן תאנה, דיו ונוצה.", - "expected_phonemes": "dʒ aː b u | l a ħ a s a n | ħ a b e t | t iː n | ħ i b e r | u r iː ʃ e" - }, - { - "voice_time_seconds": { - "start": 143.2, - "end": 144.6 - }, - "arabic": "حسن كتب بحذر", - "hebrew_transliteration": "חַסַן כַּתַבּ בְּחַדַ'ר", - "hebrew": "חסן כתב בזהירות", - "expected_phonemes": "ħ a s a n | k a t a b | b ħ a dˤ a r" - }, - { - "voice_time_seconds": { - "start": 144.6, - "end": 147.8 - }, - "arabic": "كلمات مرصوصة على قشرة التّين،", - "hebrew_transliteration": "כַּלִמַאת מַרצוּצַה עַלַא קִשרֶת אִ-תִּין,", - "hebrew": "מילים צפופות על קליפת התאנה,", - "expected_phonemes": "k a l i m aː t | m a r sˤ uː sˤ a | ʕ a l a | ʔ i ʃ r e t | ʔ i t iː n" - }, - { - "voice_time_seconds": { - "start": 147.8, - "end": 151.0 - }, - "arabic": "ونفخ عليها عشان ينشف الحبر", - "hebrew_transliteration": "וּנַפַח' עַלֵיהַא עַשַאן יִנשַף אִל-חִבֶּר", - "hebrew": "ונשף עליה כדי שהדיו יתייבש", - "expected_phonemes": "u n a f a x | ʕ a l eː h a | ʕ a ʃ aː n | j i n ʃ a f | i l ħ i b e r" - }, - { - "voice_time_seconds": { - "start": 151.0, - "end": 153.2 - }, - "arabic": "وقدّمها للملك.", - "hebrew_transliteration": "וּקַדַّמְהַא לַל-מַלִכּ.", - "hebrew": "והגיש אותה למלך.", - "expected_phonemes": "u ʔ a d d a m h a | l a l m a l i k" - }, - { - "voice_time_seconds": { - "start": 153.2, - "end": 156.4 - }, - "arabic": "«اتفضّل سيدي،» قال حسن.", - "hebrew_transliteration": "אִתפַדַّל סִידִי, קַאל חַסַן.", - "hebrew": "בבקשה, אדוני, אמר חסן.", - "expected_phonemes": "ʔ i t f a d d a l | s iː d i | ʔ aː l | ħ a s a n" - }, - { - "voice_time_seconds": { - "start": 156.4, - "end": 159.8 - }, - "arabic": "«هلّق، بقي بسّ نزرعها في الأرض", - "hebrew_transliteration": "הַלַّק בְּקִי בַּס נִזרַעְהַא פִי לְ-אַרְד'", - "hebrew": "עכשיו נשאר רק לשתול אותה באדמה", - "expected_phonemes": "h a l l a ʔ | b ʔ i | b a s | n i z r a ʕ h a | f i | l ʔ a r dˤ" - }, - { - "voice_time_seconds": { - "start": 159.8, - "end": 163.0 - }, - "arabic": "والعجيبة بتتحقّق.»", - "hebrew_transliteration": "וִל-עַגִ'יבֶּה בְּתִתחַקַّק.", - "hebrew": "והנס יתגשם.", - "expected_phonemes": "w i l ʕ a dʒ iː b e | b t i t ħ a ʔ ʔ a ʔ" - }, - { - "voice_time_seconds": { - "start": 163.0, - "end": 167.6 - }, - "arabic": "الملك قام، حفر حفرة صغيرة بمساعدة سيفه", - "hebrew_transliteration": "אִל-מַלִכּ קַאם, חַפַר חֻפְרַה זְעִ'ירֶה בְּמֻסַאעַדֶת סֵיפוֹ", - "hebrew": "המלך קם, חפר בור קטן בעזרת חרבו", - "expected_phonemes": "i l m a l i k | ʔ aː m | ħ a f a r | ħ u f r a | z ɣ iː r e | b m u s aː ʕ a d e t | s eː f o" - }, - { - "voice_time_seconds": { - "start": 167.6, - "end": 170.2 - }, - "arabic": "وانحنى يزرع التّينة.", - "hebrew_transliteration": "וּנחַנַא יִזרַע אִ-תִּינֶה.", - "hebrew": "והתכופף לשתול את התאנה.", - "expected_phonemes": "u n ħ a n a | j i z r a ʕ | ʔ i t iː n e" - }, - { - "voice_time_seconds": { - "start": 170.2, - "end": 173.6 - }, - "arabic": "«لحظة واحدة!» وقّفه حسن بخوف.", - "hebrew_transliteration": "לַחזַה וַאחדֶה! וַקַّפוֹ חַסַן בְּחוֹף.", - "hebrew": "רגע אחד! עצר אותו חסן בפחד.", - "expected_phonemes": "l a ħ ðˤ a | w aː ħ d e | w a ʔ ʔ a f o | ħ a s a n | b ħ oː f" - }, - { - "voice_time_seconds": { - "start": 173.6, - "end": 176.8 - }, - "arabic": "«نسيت تفصيل صغير بسّ مهمّ.", - "hebrew_transliteration": "נְסִית תַפצִיל זְעִ'יר בַּס מֻהֶם.", - "hebrew": "שכחתי פרט קטן אבל חשוב.", - "expected_phonemes": "n s iː t | t a f sˤ iː l | z ɣ iː r | b a s | m u h e m" - }, - { - "voice_time_seconds": { - "start": 176.8, - "end": 179.2 - }, - "arabic": "أنا حرامي ومجرم،", - "hebrew_transliteration": "אַנַא חַרַאמִי וּמֻגְ'רִם,", - "hebrew": "אני גנב ופושע,", - "expected_phonemes": "ʔ a n a | ħ a r aː m i | u m u dʒ r i m" - }, - { - "voice_time_seconds": { - "start": 179.2, - "end": 181.8 - }, - "arabic": "وعشان هيك ممنوع أزرعها.", - "hebrew_transliteration": "וּעַשַאן הֵיכּ מַמנוּע אַזרַעְהַא.", - "hebrew": "ולכן אסור לי לשתול אותה.", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | m a m n uː ʕ | ʔ a z r a ʕ h a" - }, - { - "voice_time_seconds": { - "start": 181.8, - "end": 185.0 - }, - "arabic": "السّحر بشتغل بسّ إذا بزرعها", - "hebrew_transliteration": "אִ-סִّחֶר בִּשתְעֶ'ל בַּס אִזַא בִּזרַעְהַא", - "hebrew": "הקסם עובד רק אם שותל אותה", - "expected_phonemes": "ʔ i s s i ħ e r | b i ʃ t ɣ e l | b a s | ʔ i z a | b i z r a ʕ h a" - }, - { - "voice_time_seconds": { - "start": 185.0, - "end": 187.8 - }, - "arabic": "شخص طاهر على الآخر", - "hebrew_transliteration": "שַחְ'ץ טַאהֶר עַלַא לְ-אַאחֶ'ר", - "hebrew": "אדם טהור לחלוטין", - "expected_phonemes": "ʃ a x sˤ | tˤ aː h e r | ʕ a l a | l ʔ aː x e r" - }, - { - "voice_time_seconds": { - "start": 187.8, - "end": 189.6 - }, - "arabic": "إلّي إيديه نظاف", - "hebrew_transliteration": "אִלִّי אִידֵיה נְדַ'אף", - "hebrew": "שידיו נקיות", - "expected_phonemes": "ʔ i l l i | ʔ iː d eː h | n dˤ aː f" - }, - { - "voice_time_seconds": { - "start": 189.6, - "end": 194.0 - }, - "arabic": "وبحياته ما سرق أو أخد إشي مش إله.»", - "hebrew_transliteration": "וּבִּחַיַאתוֹ מַא סַרַק אַו אַחַ'ד אִשִי מִש אִלוֹ.", - "hebrew": "ושמעולם לא גנב או לקח דבר שאינו שלו.", - "expected_phonemes": "u b i ħ a j aː t o | m a | s a r a ʔ | ʔ a w | ʔ a x a d | ʔ i ʃ i | m i ʃ | i l o" - }, - { - "voice_time_seconds": { - "start": 194.0, - "end": 196.4 - }, - "arabic": "الملك اتجمّد في محلّه.", - "hebrew_transliteration": "אִל-מַלִכּ תְגַ'מַّד פִי מַחַלّוֹ.", - "hebrew": "המלך קפא במקומו.", - "expected_phonemes": "i l m a l i k | t dʒ a m m a d | f i | m a ħ a l l o" - }, - { - "voice_time_seconds": { - "start": 196.4, - "end": 198.4 - }, - "arabic": "تذكّر أيّام شبابه،", - "hebrew_transliteration": "תְזַכַّר אַיַّאם שַבַּאבּוֹ,", - "hebrew": "נזכר בימי נעוריו,", - "expected_phonemes": "t z a k k a r | ʔ a j j aː m | ʃ a b aː b o" - }, - { - "voice_time_seconds": { - "start": 198.4, - "end": 202.4 - }, - "arabic": "كيف خطف صبيّة من عيلتها من دون إرادتها", - "hebrew_transliteration": "כִּיף חַ'טַף צַבִּיֶّה מִן עֵילִתְהַא מִן דוּן אִרַאדִתְהַא", - "hebrew": "איך חטף נערה ממשפחתה בניגוד לרצונה", - "expected_phonemes": "k iː f | x a tˤ a f | sˤ a b i j j e | m i n | ʕ eː l i t h a | m i n | d uː n | ʔ i r aː d i t h a" - }, - { - "voice_time_seconds": { - "start": 202.4, - "end": 205.0 - }, - "arabic": "بسّ عشان اشتهاها.", - "hebrew_transliteration": "בַּס עַשַאן אִשתַהַאהַא.", - "hebrew": "רק כי חשק בה.", - "expected_phonemes": "b a s | ʕ a ʃ aː n | ʔ i ʃ t a h aː h a" - }, - { - "voice_time_seconds": { - "start": 205.0, - "end": 207.2 - }, - "arabic": "وقّف محرج وقال:", - "hebrew_transliteration": "וַקַّף מִחרֶג' וּקַאל:", - "hebrew": "נעמד נבוך ואמר:", - "expected_phonemes": "w a ʔ ʔ a f | m i ħ r e dʒ | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 207.2, - "end": 211.6 - }, - "arabic": "«طيّب، يمكن مش مناسب إنّي أنا أزرع التّينة.", - "hebrew_transliteration": "טַיֶّבּ, יֻמכֶּן מִש מְנַאסֶבּ אִנִّי אַנַא אַזרַע אִ-תִּינֶה.", - "hebrew": "טוב, אולי לא מתאים שאני אשתול את התאנה.", - "expected_phonemes": "tˤ a j j e b | j u m k e n | m i ʃ | m n aː s e b | ʔ i n n i | ʔ a n a | ʔ a z r a ʕ | ʔ i t iː n e" - }, - { - "voice_time_seconds": { - "start": 211.6, - "end": 213.6 - }, - "arabic": "ما سرقتش ممتلكات،", - "hebrew_transliteration": "מַא סַרַקתֶש מֻמתַלַכַּאת,", - "hebrew": "לא גנבתי רכוש,", - "expected_phonemes": "m a | s a r a ʔ t e ʃ | m u m t a l a k aː t" - }, - { - "voice_time_seconds": { - "start": 213.6, - "end": 217.8 - }, - "arabic": "بسّ ضميري مش مرتاح على الآخر.»", - "hebrew_transliteration": "בַּס דַ'מִירִי מִש מִרתַאח עַלַא לְ-אַאחֶ'ר.", - "hebrew": "אבל מצפוני לא שקט לגמרי.", - "expected_phonemes": "b a s | dˤ a m iː r i | m i ʃ | m i r t aː ħ | ʕ a l a | l ʔ aː x e r" - }, - { - "voice_time_seconds": { - "start": 217.8, - "end": 220.8 - }, - "arabic": "اتطلّع على وزيره لكبير وقال:", - "hebrew_transliteration": "אִתטַלַّע עַלַא וַזִירוֹ לֶ-כְּבִּיר וּקַאל:", - "hebrew": "הביט בווזיר הגדול שלו ואמר:", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | ʕ a l a | w a z iː r o | l e k b iː r | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 220.8, - "end": 224.2 - }, - "arabic": "«خدها يا وزير وإزرع التّينة.»", - "hebrew_transliteration": "חֻ'דְהַא יַא וַזִיר וּאִזרַע אִ-תִּינֶה.", - "hebrew": "קח אותה, וזיר, ושתול את התאנה.", - "expected_phonemes": "x u d h a | j a | w a z iː r | u ʔ i z r a ʕ | ʔ i t iː n e" - }, - { - "voice_time_seconds": { - "start": 224.2, - "end": 227.6 - }, - "arabic": "الوزير العجوز صار لونه شاحب.", - "hebrew_transliteration": "אִל-וַזִיר אִל-עַגוּז צַאר לוֹנוֹ שַאחֶבּ.", - "hebrew": "פני הווזיר הזקן החווירו.", - "expected_phonemes": "i l w a z iː r | i l ʕ a g uː z | sˤ aː r | l oː n o | ʃ aː ħ e b" - }, - { - "voice_time_seconds": { - "start": 227.6, - "end": 231.2 - }, - "arabic": "تذكّر فرس أصيل اشتهاها في الماضي،", - "hebrew_transliteration": "תְזַכַّר פַרַס אַצִיל אִשתַהַאהַא פִי לְ-מַאדִ'י,", - "hebrew": "נזכר בסוסה אצילה שחשק בה בעבר,", - "expected_phonemes": "t z a k k a r | f a r a s | ʔ a sˤ iː l | ʔ i ʃ t a h aː h a | f i | l m aː dˤ i" - }, - { - "voice_time_seconds": { - "start": 231.2, - "end": 233.8 - }, - "arabic": "ولمّا صاحبها رفض يبيعها،", - "hebrew_transliteration": "וּלַמַّא צַאחֶבְּהַא רַפַד' יְבִּיעְהַא,", - "hebrew": "וכשבעליה סירב למכור אותה,", - "expected_phonemes": "u l a m m a | sˤ aː ħ e b h a | r a f a dˤ | j b iː ʕ h a" - }, - { - "voice_time_seconds": { - "start": 233.8, - "end": 237.0 - }, - "arabic": "بعت ناس في اللّيل تسرقها.", - "hebrew_transliteration": "בַּעַת נַאס פִי אִ-לֵّיל תֻסרֻקְהַא.", - "hebrew": "שלח אנשים בלילה לגנוב אותה.", - "expected_phonemes": "b a ʕ a t | n aː s | f i | ʔ i l l eː l | t u s r u ʔ h a" - }, - { - "voice_time_seconds": { - "start": 237.0, - "end": 240.2 - }, - "arabic": "«سيدي الملك،» تأتأ الوزير،", - "hebrew_transliteration": "סִידִי לְ-מַלִכּ, תַאתַא אִל-וַזִיר,", - "hebrew": "אדוני המלך, גמגם הווזיר,", - "expected_phonemes": "s iː d i | l m a l i k | t aː t a | i l w a z iː r" - }, - { - "voice_time_seconds": { - "start": 240.2, - "end": 245.4 - }, - "arabic": "«أنا رجل جيش، على إيدي حروبات وغنائم.", - "hebrew_transliteration": "אַנַא רַגֻ'ל גֵ'יש, עַלַא אִידַי חֻרוּבַּאת וּעַ'נַאאֶם.", - "hebrew": "אני איש צבא, על ידיי מלחמות ושלל.", - "expected_phonemes": "ʔ a n a | r a dʒ u l | dʒ eː ʃ | ʕ a l a | ʔ iː d a j | ħ u r uː b aː t | u ɣ a n aː ʔ e m" - }, - { - "voice_time_seconds": { - "start": 245.4, - "end": 249.6 - }, - "arabic": "يمكن أحسن إنّه رجل دين يزرعها.»", - "hebrew_transliteration": "יֻמכֶּן אַחסַן אִנּוֹ רַגֻ'ל דִין יִזרַעְהַא.", - "hebrew": "אולי עדיף שאיש דת ישתול אותה.", - "expected_phonemes": "j u m k e n | ʔ a ħ s a n | ʔ i n o | r a dʒ u l | d iː n | j i z r a ʕ h a" - }, - { - "voice_time_seconds": { - "start": 249.6, - "end": 253.4 - }, - "arabic": "وقدّم حبّة التّين للمفتي الرّئيسي.", - "hebrew_transliteration": "וּקַדַّם חַבֶּת אִ-תִּין לַל-מֻפתִי לְ-רַאִיסִי.", - "hebrew": "והגיש את התאנה למופתי הראשי.", - "expected_phonemes": "u ʔ a d d a m | ħ a b e t | ʔ i t iː n | l a l m u f t i | l r a ʔ iː s i" - }, - { - "voice_time_seconds": { - "start": 253.4, - "end": 257.4 - }, - "arabic": "المفتي أخد التّينة، قرّب لعند الحفرة", - "hebrew_transliteration": "אִל-מֻפתִי אַחַ'ד אִ-תִּינֶה, קַרַّבּ לַעִנד לְ-חֻפְרַה", - "hebrew": "המופתי לקח את התאנה, התקרב אל הבור", - "expected_phonemes": "i l m u f t i | ʔ a x a d | ʔ i t iː n e | ʔ a r r a b | l a ʕ i n d | l ħ u f r a" - }, - { - "voice_time_seconds": { - "start": 257.4, - "end": 259.6 - }, - "arabic": "بسّ فجأة وقّف.", - "hebrew_transliteration": "בַּס פַגְ'אַה וַקַّף.", - "hebrew": "אבל פתאום עצר.", - "expected_phonemes": "b a s | f a dʒ ʔ a | w a ʔ ʔ a f" - }, - { - "voice_time_seconds": { - "start": 259.6, - "end": 264.2 - }, - "arabic": "تذكّر كيف لمّا كان صغير وكان يدير دكّانة أبوه،", - "hebrew_transliteration": "תְזַכַّר כִּיף לַמַّא כַּאן זְעִ'יר וּכַּאן יְדִיר דֻכַּאנֶת אַבּוּה,", - "hebrew": "נזכר איך כשהיה צעיר וניהל את החנות של אביו,", - "expected_phonemes": "t z a k k a r | k iː f | l a m m a | k aː n | z ɣ iː r | u k aː n | j d iː r | d u k aː n e t | ʔ a b uː h" - }, - { - "voice_time_seconds": { - "start": 264.2, - "end": 268.6 - }, - "arabic": "كان يوخد مصاري من دون ما يطلب إذن.", - "hebrew_transliteration": "כַּאן יוֹחֹ'ד מַצַארִי מִן דוּן מַא יֻטלֹבּ אִדֶ'ן.", - "hebrew": "היה לוקח כסף בלי לבקש רשות.", - "expected_phonemes": "k aː n | j oː x o d | m a sˤ aː r i | m i n | d uː n | m a | j u tˤ l o b | ʔ i dˤ e n" - }, - { - "voice_time_seconds": { - "start": 268.6, - "end": 271.8 - }, - "arabic": "‹مضبوط أبوي كان غني،› قال لنفسه،", - "hebrew_transliteration": "מַדבּוּט אַבּוּי כַּאן עַ'נִי, קַאל לַנַפסוֹ,", - "hebrew": "נכון שאבי היה עשיר, אמר לעצמו,", - "expected_phonemes": "m a d b uː tˤ | ʔ a b uː j | k aː n | ɣ a n i | ʔ aː l | l a n a f s o" - }, - { - "voice_time_seconds": { - "start": 271.8, - "end": 275.0 - }, - "arabic": "‹بسّ أخدت مصاري من دون علمه.›", - "hebrew_transliteration": "בַּס אַחַ'דֶת מַצַארִי מִן דוּן עִלְמוֹ.", - "hebrew": "אבל לקחתי כסף בלי ידיעתו.", - "expected_phonemes": "b a s | ʔ a x a d e t | m a sˤ aː r i | m i n | d uː n | ʕ i l m o" - }, - { - "voice_time_seconds": { - "start": 275.0, - "end": 276.8 - }, - "arabic": "لفّ وقال:", - "hebrew_transliteration": "לַףّ וּקַאל:", - "hebrew": "הסתובב ואמר:", - "expected_phonemes": "l a f f | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 276.8, - "end": 280.8 - }, - "arabic": "«إيدي عم بترجّ من هدا الموقف المؤثّر.", - "hebrew_transliteration": "אִידַי עַם-בִּתרֻגّ מִן הַאדַא לְ-מַוְקֶף אִל-מֻאַתֶّר.", - "hebrew": "ידיי רועדות מהמעמד המרגש הזה.", - "expected_phonemes": "ʔ iː d a j | ʕ a m b i t r u g g | m i n | h aː d a | l m a w ʔ e f | i l m u ʔ a t t e r" - }, - { - "voice_time_seconds": { - "start": 280.8, - "end": 284.4 - }, - "arabic": "بلكي القاضي يزرعها محلّي؟»", - "hebrew_transliteration": "בַּלְכִּי לְ-קַאדִי יִזרַעְהַא מַחַלִّי?", - "hebrew": "אולי השופט ישתול אותה במקומי?", - "expected_phonemes": "b a l k i | l ʔ aː d i | j i z r a ʕ h a | m a ħ a l l i" - }, - { - "voice_time_seconds": { - "start": 284.4, - "end": 288.8 - }, - "arabic": "وهيك مرقت التّينة من إيد لإيد", - "hebrew_transliteration": "וּהֵיכּ מַרקַת אִ-תִּינֶה מִן אִיד לַאִיד", - "hebrew": "וכך עברה התאנה מיד ליד", - "expected_phonemes": "u h eː k | m a r ʔ a t | ʔ i t iː n e | m i n | ʔ iː d | l a ʔ iː d" - }, - { - "voice_time_seconds": { - "start": 288.8, - "end": 292.6 - }, - "arabic": "من وزير لقاضي ومن مستشار لموظّف.", - "hebrew_transliteration": "מִן וַזִיר לַקַאדִי וּמִן מֻסתַשַאר לַמֻוַדַّ'ף.", - "hebrew": "מווזיר לשופט ומיועץ לפקיד.", - "expected_phonemes": "m i n | w a z iː r | l a ʔ aː d i | u m i n | m u s t a ʃ aː r | l a m u w a dˤ dˤ a f" - }, - { - "voice_time_seconds": { - "start": 292.6, - "end": 296.2 - }, - "arabic": "الشّمس صارت عاموديّة في نصّ السّما،", - "hebrew_transliteration": "אִ-שַּׁמֶס צַארַת עַאמוּדִיֶّה פִי נֻצّ אִ-סַּמַא,", - "hebrew": "השמש עמדה באמצע השמיים,", - "expected_phonemes": "ʔ i ʃ a m e s | sˤ aː r a t | ʕ aː m uː d i j j e | f i | n u sˤ sˤ | ʔ i s a m a" - }, - { - "voice_time_seconds": { - "start": 296.2, - "end": 298.6 - }, - "arabic": "والجّمهور بلّش يتفرّق،", - "hebrew_transliteration": "וִ-גֻّמהוּר בַּלַّש יִתפַרַّק,", - "hebrew": "והקהל התחיל להתפזר,", - "expected_phonemes": "w i g g u m h uː r | b a l l a ʃ | j i t f a r r a ʔ" - }, - { - "voice_time_seconds": { - "start": 298.6, - "end": 301.2 - }, - "arabic": "والتّينة بقيت لحالها.", - "hebrew_transliteration": "וִ-תִּינֶה בְּקִית לַחַאלְהַא.", - "hebrew": "והתאנה נשארה לבדה.", - "expected_phonemes": "w i t iː n e | b ʔ iː t | l a ħ aː l h a" - }, - { - "voice_time_seconds": { - "start": 301.2, - "end": 303.0 - }, - "arabic": "ما كانش فيه ولا واحد", - "hebrew_transliteration": "מַא כַּאנֶש פִיה וַלַא וַאחַד", - "hebrew": "לא היה אף אחד", - "expected_phonemes": "m a | k aː n e ʃ | f iː h | w a l a | w aː ħ a d" - }, - { - "voice_time_seconds": { - "start": 303.0, - "end": 305.4 - }, - "arabic": "من المحترمين إلّي في المملكة", - "hebrew_transliteration": "מִן אִל-מֻחתַרַמִין אִלִّי פִי לְ-מַמלַכֶּה", - "hebrew": "מבין המכובדים שבממלכה", - "expected_phonemes": "m i n | i l m u ħ t a r a m iː n | ʔ i l l i | f i | l m a m l a k e" - }, - { - "voice_time_seconds": { - "start": 305.4, - "end": 308.6 - }, - "arabic": "إلّي بحياته ما مدّ إيده لممتلكات", - "hebrew_transliteration": "אִלִّי בְּחַיַאתוֹ מַא מַדّ אִידוֹ לַמֻמתַלַכַּאת", - "hebrew": "שמעולם לא שלח את ידו לרכוש", - "expected_phonemes": "ʔ i l l i | b ħ a j aː t o | m a | m a d d | ʔ iː d o | l a m u m t a l a k aː t" - }, - { - "voice_time_seconds": { - "start": 308.6, - "end": 313.0 - }, - "arabic": "أو لشخص تاني بشكل غير قانوني.", - "hebrew_transliteration": "אַו לַשַחְ'ץ תַאנִי בְּשַכֶּל עֵ'יר קַאנוּנִי.", - "hebrew": "או לאדם אחר באופן לא חוקי.", - "expected_phonemes": "ʔ a w | l a ʃ a x sˤ | t aː n i | b ʃ a k e l | ɣ eː r | ʔ aː n uː n i" - }, - { - "voice_time_seconds": { - "start": 313.0, - "end": 316.4 - }, - "arabic": "في النّهاية رجعت التّينة لإيد الملك.", - "hebrew_transliteration": "פִי לְ-נִהַאיֶה רִגְעַת אִ-תִּינֶה לַאִיד אִל-מַלִכּ.", - "hebrew": "בסוף חזרה התאנה לידי המלך.", - "expected_phonemes": "f i | l n i h aː j e | r i g ʕ a t | ʔ i t iː n e | l a ʔ iː d | i l m a l i k" - }, - { - "voice_time_seconds": { - "start": 316.4, - "end": 319.0 - }, - "arabic": "الملك تطلّع على المستشارين تعونه", - "hebrew_transliteration": "אִל-מַלִכּ תְטַלַّע עַלַא לְ-מֻסתַשַארִין תַאעוּנוֹ", - "hebrew": "המלך הביט ביועציו", - "expected_phonemes": "i l m a l i k | t tˤ a l l a ʕ | ʕ a l a | l m u s t a ʃ aː r iː n | t aː ʕ uː n o" - }, - { - "voice_time_seconds": { - "start": 319.0, - "end": 323.0 - }, - "arabic": "إلّي كانوا خجلانين وفرط من الضّحك:", - "hebrew_transliteration": "אִלִّי כַּאנוּ חַ'גְ'לַאנִין וּפִרֶט מִן אִ-דִּחֶכּ:", - "hebrew": "שהיו נבוכים, ופרץ בצחוק:", - "expected_phonemes": "ʔ i l l i | k aː n u | x a dʒ l aː n iː n | u f i r e tˤ | m i n | ʔ i d i ħ e k" - }, - { - "voice_time_seconds": { - "start": 323.0, - "end": 326.0 - }, - "arabic": "«أيّ محاكمة عادلة عملنا هون!", - "hebrew_transliteration": "אַיّ מֻחַאכַּמֶה עַאדְלֶה עִמִלנַא הוֹן!", - "hebrew": "איזה משפט צודק עשינו כאן!", - "expected_phonemes": "ʔ a j j | m u ħ aː k a m e | ʕ aː d l e | ʕ i m i l n a | h oː n" - }, - { - "voice_time_seconds": { - "start": 326.0, - "end": 330.2 - }, - "arabic": "تبيّن إنّه كلّنا حراميّة ومذنبين مش صغار،", - "hebrew_transliteration": "תְבַּיַّן אִנּוֹ כֻּלְّנַא חַרַאמִיֶّה וּמֻד'נִבִּין מִש זְעַ'אר,", - "hebrew": "התברר שכולנו גנבים ואשמים לא קטנים,", - "expected_phonemes": "t b a j j a n | ʔ i n o | k u l l n a | ħ a r aː m i j j e | u m u dˤ n i b iː n | m i ʃ | z ɣ aː r" - }, - { - "voice_time_seconds": { - "start": 330.2, - "end": 334.8 - }, - "arabic": "وبسّ كان بدّنا نشنق حسن؟ حرّروه فورًا!", - "hebrew_transliteration": "וּבַּס כַּאן בַּדְّנַא נִשנֹק חַסַן? חַרְّרוּה פַוְרַן!", - "hebrew": "ורק את חסן רצינו לתלות? שחררו אותו מיד!", - "expected_phonemes": "u b a s | k aː n | b a d d n a | n i ʃ n o ʔ | ħ a s a n | ħ a r r r uː h | f a w r a n" - }, - { - "voice_time_seconds": { - "start": 334.8, - "end": 342.6 - }, - "arabic": "مضبوط هوّ حرامي، بسّ أذكى منّا كلّنا سوا!»", - "hebrew_transliteration": "מַדבּוּט הֻוֶّ חַרַאמִי, בַּס אַדְ'כַּא מִנַّא כֻּלְّנַא סַוַא!", - "hebrew": "נכון שהוא גנב, אבל חכם מכולנו יחד!", - "expected_phonemes": "m a d b uː tˤ | h u w w e | ħ a r aː m i | b a s | ʔ a dˤ k a | m i n n a | k u l l n a | s a w a" - } - ] - }, - { - "file_name": "מרק כפתורים * שוֹרָבּתֶ זרָאר * شوربة زرار * סיפור בערבית מדוברת [nvLmH0u57Kc].webm", - "youtube_id": "nvLmH0u57Kc", - "title_hebrew": "מרק כפתורים", - "segments": [ - { - "voice_time_seconds": { - "start": 7.4, - "end": 10.8 - }, - "arabic": "كان فيه واحد عمبمشي بطريق طويلة", - "hebrew_transliteration": "כַּאן פִיה וַאחַד עַם-בִּמשִי בְּטַרִיק טַוִילֶה", - "hebrew": "היה מישהו שהלך בדרך ארוכה", - "expected_phonemes": "k aː n | f iː h | w aː ħ a d | ʕ a m b i m ʃ i | b tˤ a r iː ʔ | tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 10.8, - "end": 13.4 - }, - "arabic": "متوجّه لبلدة بعيدة.", - "hebrew_transliteration": "מִתוַגֶّה לַבַּלדֶה בְּעִידֶה.", - "hebrew": "בדרכו לעיירה רחוקה.", - "expected_phonemes": "m i t w a g g e | l a b a l d e | b ʕ iː d e" - }, - { - "voice_time_seconds": { - "start": 13.4, - "end": 15.6 - }, - "arabic": "لمّا بلّشت تليّل،", - "hebrew_transliteration": "לַמַّא בַּלַّשַת תְלַיֶّל,", - "hebrew": "כשהתחיל להחשיך,", - "expected_phonemes": "l a m m a | b a l l a ʃ a t | t l a j j e l" - }, - { - "voice_time_seconds": { - "start": 15.6, - "end": 18.6 - }, - "arabic": "ولاقى حاله على طراف قرية صغيرة،", - "hebrew_transliteration": "וּלַאקַא חַאלוֹ עַלַא טַרַף קַרְיֶה זְעִ'ירֶה,", - "hebrew": "ומצא את עצמו בקצה כפר קטן,", - "expected_phonemes": "u l aː ʔ a | ħ aː l o | ʕ a l a | tˤ a r a f | ʔ a r j e | z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 18.6, - "end": 21.2 - }, - "arabic": "إلّي سمعة ناسها إنّهم بخلا،", - "hebrew_transliteration": "אִלִّי סִמעַת נַאסְהַא אִנְّהֹם בֻּחַ'לַא,", - "hebrew": "שאנשיו ידועים כקמצנים,", - "expected_phonemes": "ʔ i l l i | s i m ʕ a t | n aː s h a | ʔ i n n h o m | b u x a l a" - }, - { - "voice_time_seconds": { - "start": 21.2, - "end": 23.8 - }, - "arabic": "كانت واصلة لبعيد.", - "hebrew_transliteration": "כַּאנַת וַאצְלֶה לַבְּעִיד.", - "hebrew": "השמועה הגיעה למרחוק.", - "expected_phonemes": "k aː n a t | w aː sˤ l e | l a b ʕ iː d" - }, - { - "voice_time_seconds": { - "start": 23.8, - "end": 26.2 - }, - "arabic": "بلّش هالزّلمة يمشي بين لبيوت", - "hebrew_transliteration": "בַּלַّש הַ-זַּלַמֶה יִמשִי בֵּין לֶ-בְּיוּת", - "hebrew": "האיש הזה התחיל ללכת בין הבתים", - "expected_phonemes": "b a l l a ʃ | h a z a l a m e | j i m ʃ i | b eː n | l e b j uː t" - }, - { - "voice_time_seconds": { - "start": 26.2, - "end": 29.4 - }, - "arabic": "عشان يلاقي محلّ يبات فيه.", - "hebrew_transliteration": "עַשַאן יְלַאקִי מַחַלّ יְבַּאת פִיה.", - "hebrew": "כדי למצוא מקום ללון בו.", - "expected_phonemes": "ʕ a ʃ aː n | j l aː ʔ i | m a ħ a l l | j b aː t | f iː h" - }, - { - "voice_time_seconds": { - "start": 29.4, - "end": 32.8 - }, - "arabic": "دقّ على بواب لبيوت واحد ورا التّاني،", - "hebrew_transliteration": "דַקّ עַלַא בְּוַאבּ לֶ-בְּיוּת וַאחַד וַרַא אִ-תַּאנִי,", - "hebrew": "דפק על דלתות הבתים אחת אחרי השנייה,", - "expected_phonemes": "d a ʔ ʔ | ʕ a l a | b w aː b | l e b j uː t | w aː ħ a d | w a r a | ʔ i t aː n i" - }, - { - "voice_time_seconds": { - "start": 32.8, - "end": 35.6 - }, - "arabic": "بسّ ولا حدا فتحله الباب.", - "hebrew_transliteration": "בַּס וְלַא חַדַא פַתַח-לוֹ לְ-בַּאבּ.", - "hebrew": "אבל אף אחד לא פתח לו את הדלת.", - "expected_phonemes": "b a s | u l aː | ħ a d a | f a t a ħ l o | l b aː b" - }, - { - "voice_time_seconds": { - "start": 35.6, - "end": 38.6 - }, - "arabic": "في النّهاي، كان فيه زوج عصبي", - "hebrew_transliteration": "פִי אִ-נַّהַאי, כַּאן פִיה זוֹג' עַצַבִּי", - "hebrew": "בסוף, היו זוג עצבני", - "expected_phonemes": "f i | ʔ i n n a h aː i | k aː n | f iː h | z oː dʒ | ʕ a sˤ a b i" - }, - { - "voice_time_seconds": { - "start": 38.6, - "end": 41.8 - }, - "arabic": "إلّي عملوا معروف وفتحوله الباب.", - "hebrew_transliteration": "אִלִّי עִמלוּ מַערוּף וּפַתַחוּ-לוֹ אִל-בַּאבּ.", - "hebrew": "שעשו טובה ופתחו לו את הדלת.", - "expected_phonemes": "ʔ i l l i | ʕ i m l u | m a ʕ r uː f | u f a t a ħ uː l o | i l b aː b" - }, - { - "voice_time_seconds": { - "start": 41.8, - "end": 44.8 - }, - "arabic": "وبالرّغم من إنّه الموضوع ما عجبهم،", - "hebrew_transliteration": "וּבִּ-רַّעְ'ם מִן אִנּוֹ לְ-מַוְדוּע מַא עַגַ'בְּהֹם,", - "hebrew": "ולמרות שהעניין לא מצא חן בעיניהם,", - "expected_phonemes": "u b i r r a ɣ m | m i n | ʔ i n o | l m a w d uː ʕ | m a | ʕ a dʒ a b h o m" - }, - { - "voice_time_seconds": { - "start": 44.8, - "end": 47.2 - }, - "arabic": "قبلوا يخلّوه يبات في الحظيرة", - "hebrew_transliteration": "קִבְלוּ יְחַ'לּוּה יְבַּאת פִי לְ-חַטִ'ירַה", - "hebrew": "הסכימו לתת לו ללון בדיר", - "expected_phonemes": "ʔ i b l u | j x a l uː h | j b aː t | f i | l ħ a ðˤ iː r a" - }, - { - "voice_time_seconds": { - "start": 47.2, - "end": 49.6 - }, - "arabic": "مع الغنم والخرفان.", - "hebrew_transliteration": "מַע אִל-עַ'נַם וִל-חִ'רפַאן.", - "hebrew": "עם הצאן והכבשים.", - "expected_phonemes": "m a ʕ | i l ɣ a n a m | w i l x i r f aː n" - }, - { - "voice_time_seconds": { - "start": 49.8, - "end": 53.0 - }, - "arabic": "الزّلمة شكرهم على استضافتهم إله،", - "hebrew_transliteration": "אִ-זַּלַמֶה שַכַּרְהֹם עַלַא סְתִדַ'אפִתְהֹם אִלוֹ,", - "hebrew": "האיש הודה להם על שאירחו אותו,", - "expected_phonemes": "ʔ i z a l a m e | ʃ a k a r h o m | ʕ a l a | s t i dˤ aː f i t h o m | i l o" - }, - { - "voice_time_seconds": { - "start": 53.0, - "end": 54.4 - }, - "arabic": "وبعد ما فهم إنّهم", - "hebrew_transliteration": "וּבַּעֶד מַא פִהֶם אִנְّהֹם", - "hebrew": "ואחרי שהבין שהם", - "expected_phonemes": "u b a ʕ e d | m a | f i h e m | ʔ i n n h o m" - }, - { - "voice_time_seconds": { - "start": 54.4, - "end": 57.4 - }, - "arabic": "مش معنيين يعرضوا عليه وجبة عشا،", - "hebrew_transliteration": "מִש מַעְנִיִّין יִערְדוּ עַלֵיה וַגְ'בֶּת עַשַא,", - "hebrew": "לא מעוניינים להציע לו ארוחת ערב,", - "expected_phonemes": "m i ʃ | m a ʕ n i j j iː n | j i ʕ r d u | ʕ a l eː h | w a dʒ b e t | ʕ a ʃ a" - }, - { - "voice_time_seconds": { - "start": 57.4, - "end": 58.6 - }, - "arabic": "قال إلهم:", - "hebrew_transliteration": "קַאל אִלְהֹם:", - "hebrew": "אמר להם:", - "expected_phonemes": "ʔ aː l | i l h o m" - }, - { - "voice_time_seconds": { - "start": 58.6, - "end": 62.0 - }, - "arabic": "«أنا متعوّد أسافر لمحلّات بعيدة", - "hebrew_transliteration": "אַנַא מִתעַוֶّד אַסַאפֶר לַמַחַלַّאת בְּעִידֶה", - "hebrew": "אני רגיל לנסוע למקומות רחוקים", - "expected_phonemes": "ʔ a n a | m i t ʕ a w w e d | ʔ a s aː f e r | l a m a ħ a l l aː t | b ʕ iː d e" - }, - { - "voice_time_seconds": { - "start": 62.0, - "end": 65.4 - }, - "arabic": "وعلى مدار لسنين تعلّمت كتير إشيا", - "hebrew_transliteration": "וּעַלַא מַדַאר לֶ-סְנִין תְעַלַّמֶת כְּתִיר אִשְיַא", - "hebrew": "ובמשך השנים למדתי הרבה דברים", - "expected_phonemes": "u ʕ a l a | m a d aː r | l e s n iː n | t ʕ a l l a m e t | k t iː r | ʔ i ʃ j a" - }, - { - "voice_time_seconds": { - "start": 65.4, - "end": 68.0 - }, - "arabic": "إلّي منيح الواحد يعرفها.", - "hebrew_transliteration": "אִלִّי מְנִיח אִל-וַאחַד יִערֶפְהַא.", - "hebrew": "שטוב לדעת.", - "expected_phonemes": "ʔ i l l i | m n iː ħ | i l w aː ħ a d | j i ʕ r e f h a" - }, - { - "voice_time_seconds": { - "start": 68.0, - "end": 71.0 - }, - "arabic": "قبل أكمّ سنة، وأنا ماشي في الطّريق،", - "hebrew_transliteration": "קַבֶּל אַכַּם סַנֶה, וּאַנַא מַאשִי פִי אִ-טַּרִיק,", - "hebrew": "לפני כמה שנים, כשהלכתי בדרך,", - "expected_phonemes": "ʔ a b e l | ʔ a k a m | s a n e | u ʔ a n a | m aː ʃ i | f i | ʔ i tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 71.0, - "end": 74.2 - }, - "arabic": "قابلت ختيار إلّي علّمني أحضّر", - "hebrew_transliteration": "קַאבַּלֶת חְ'תִיַאר אִלִّי עַלַّמְנִי אַחַדֶّר", - "hebrew": "פגשתי זקן שלימד אותי להכין", - "expected_phonemes": "ʔ aː b a l e t | x t i j aː r | ʔ i l l i | ʕ a l l a m n i | ʔ a ħ a d d e r" - }, - { - "voice_time_seconds": { - "start": 74.2, - "end": 76.2 - }, - "arabic": "شوربة زرار!»", - "hebrew_transliteration": "שוֹרַבֶּת זְרַאר!", - "hebrew": "מרק כפתורים!", - "expected_phonemes": "ʃ oː r a b e t | z r aː r" - }, - { - "voice_time_seconds": { - "start": 76.2, - "end": 79.0 - }, - "arabic": "«شوربة زرار؟!» تعجّبوا الزّوج.", - "hebrew_transliteration": "שוֹרַבֶּת זְרַאר?! תְעַגַّבּוּ אִ-זַּוְג'.", - "hebrew": "מרק כפתורים?! השתוממו בני הזוג.", - "expected_phonemes": "ʃ oː r a b e t | z r aː r | t ʕ a g g a b u | ʔ i z a w dʒ" - }, - { - "voice_time_seconds": { - "start": 79.0, - "end": 83.4 - }, - "arabic": "جاوبهم: «أه، شوربة على أصولها،", - "hebrew_transliteration": "גַ'אוַבְּהֹם: אַה, שוֹרַבֶּה עַלַא אֻצוּלְהַא,", - "hebrew": "הוא ענה להם: כן, מרק כמו שצריך,", - "expected_phonemes": "dʒ aː w a b h o m | ʔ aː | ʃ oː r a b e | ʕ a l a | ʔ u sˤ uː l h a" - }, - { - "voice_time_seconds": { - "start": 83.4, - "end": 84.8 - }, - "arabic": "زاكية وبتشبّع،", - "hebrew_transliteration": "זַאכְּיֶה וּבִּתשַבֶּע,", - "hebrew": "טעים ומשביע,", - "expected_phonemes": "z aː k j e | u b i t ʃ a b e ʕ" - }, - { - "voice_time_seconds": { - "start": 84.8, - "end": 89.6 - }, - "arabic": "والمكوّنات اللّازمة بسّ ميّ وزرّ.»", - "hebrew_transliteration": "וִל-מֻכַּוִّנַאת לַּאזְמֶה בַּס מַיّ וּזִרّ.", - "hebrew": "והמרכיבים הנחוצים הם רק מים וכפתור.", - "expected_phonemes": "w i l m u k a w w i n aː t | l aː z m e | b a s | m a j j | u z i r r" - }, - { - "voice_time_seconds": { - "start": 89.6, - "end": 93.2 - }, - "arabic": "الزّوج اتطلّعوا في بعضهم باستغراب:", - "hebrew_transliteration": "אִ-זַּוְג' תְטַלַّעוּ פִי בַּעְדְ'הֹם בִּסתִעְ'רַאבּ:", - "hebrew": "בני הזוג הביטו זה בזה בתמיהה:", - "expected_phonemes": "ʔ i z a w dʒ | t tˤ a l l a ʕ u | f i | b a ʕ dˤ h o m | b i s t i ɣ r aː b" - }, - { - "voice_time_seconds": { - "start": 93.2, - "end": 96.0 - }, - "arabic": "«من ميّ وزرّ رح تعرف تحضّر", - "hebrew_transliteration": "מִן מַיّ וּזִרّ רַח-תִערַף תְחַדֶّר", - "hebrew": "ממים וכפתור תדע להכין", - "expected_phonemes": "m i n | m a j j | u z i r r | r a ħ t i ʕ r a f | t ħ a d d e r" - }, - { - "voice_time_seconds": { - "start": 96.0, - "end": 98.6 - }, - "arabic": "شوربة زاكية وبتشبّع؟", - "hebrew_transliteration": "שוֹרַבֶּה זַאכְּיֶה וּבִּתשַבֶּע?", - "hebrew": "מרק טעים ומשביע?", - "expected_phonemes": "ʃ oː r a b e | z aː k j e | u b i t ʃ a b e ʕ" - }, - { - "voice_time_seconds": { - "start": 98.6, - "end": 100.6 - }, - "arabic": "عنجد عجيبة!", - "hebrew_transliteration": "עַנגַ'ד עַגִ'יבֶּה!", - "hebrew": "באמת פלא!", - "expected_phonemes": "ʕ a n dʒ a d | ʕ a dʒ iː b e" - }, - { - "voice_time_seconds": { - "start": 100.6, - "end": 102.0 - }, - "arabic": "إحنا فقرا", - "hebrew_transliteration": "אִחנַא פֻקַרַא", - "hebrew": "אנחנו עניים", - "expected_phonemes": "ʔ i ħ n a | f u ʔ a r a" - }, - { - "voice_time_seconds": { - "start": 102.0, - "end": 104.6 - }, - "arabic": "وما عنّا إشي نوكله في البيت.", - "hebrew_transliteration": "וּמַא עִנַّא אִשִי נוֹכְּלוֹ פִי לְ-בֵּית.", - "hebrew": "ואין לנו מה לאכול בבית.", - "expected_phonemes": "u m a | ʕ i n n a | ʔ i ʃ i | n oː k l o | f i | l b eː t" - }, - { - "voice_time_seconds": { - "start": 104.6, - "end": 108.8 - }, - "arabic": "بسّ – بقيت عنّا جرّة ميّ كاملة...»", - "hebrew_transliteration": "בַּס – בְּקִית עִנַّא גַ'רַّת מַיّ כַּאמְלֶה...", - "hebrew": "אבל – נשאר לנו כד מים מלא...", - "expected_phonemes": "b a s | b ʔ iː t | ʕ i n n a | dʒ a r r a t | m a j j | k aː m l e" - }, - { - "voice_time_seconds": { - "start": 108.8, - "end": 111.4 - }, - "arabic": "الزّلمة قال: «ممتاز!", - "hebrew_transliteration": "אִ-זַּלַמֶה קַאל: מֻמתַאז!", - "hebrew": "האיש אמר: מצוין!", - "expected_phonemes": "ʔ i z a l a m e | ʔ aː l | m u m t aː z" - }, - { - "voice_time_seconds": { - "start": 111.4, - "end": 114.6 - }, - "arabic": "جيبوا طنجرة، عبّوها ميّ،", - "hebrew_transliteration": "גִ'יבּוּ טֻנגַ'רַה, עַבּוּהַא מַיّ,", - "hebrew": "הביאו סיר, מלאו אותו מים,", - "expected_phonemes": "dʒ iː b u | tˤ u n dʒ a r a | ʕ a b uː h a | m a j j" - }, - { - "voice_time_seconds": { - "start": 114.6, - "end": 117.2 - }, - "arabic": "وحطّوها على النّار.»", - "hebrew_transliteration": "וּחֻטּוּהַא עַלַא נַּאר.", - "hebrew": "ושימו אותו על האש.", - "expected_phonemes": "u ħ u tˤ uː h a | ʕ a l a | n aː r" - }, - { - "voice_time_seconds": { - "start": 117.2, - "end": 120.6 - }, - "arabic": "بسرعة، جابوا الزّوج الجّرّة،", - "hebrew_transliteration": "בְּסֻרעַה, גַ'אבּוּ אִ-זַּוְג' אִ-גַّרַّה,", - "hebrew": "מהר, בני הזוג הביאו את הכד,", - "expected_phonemes": "b s u r ʕ a | dʒ aː b u | ʔ i z a w dʒ | ʔ i g g a r r a" - }, - { - "voice_time_seconds": { - "start": 120.6, - "end": 122.6 - }, - "arabic": "عبّوا الطّنجرة بالميّ،", - "hebrew_transliteration": "עַבּוּ אִ-טֻּנגַ'רַה בִּל-מַיّ,", - "hebrew": "מילאו את הסיר במים,", - "expected_phonemes": "ʕ a b u | ʔ i tˤ u n dʒ a r a | b i l m a j j" - }, - { - "voice_time_seconds": { - "start": 122.6, - "end": 124.4 - }, - "arabic": "وحطّوها على النّار.", - "hebrew_transliteration": "וּחַטּוּהַא עַלַא נַּאר.", - "hebrew": "ושמו אותו על האש.", - "expected_phonemes": "u ħ a tˤ uː h a | ʕ a l a | n aː r" - }, - { - "voice_time_seconds": { - "start": 124.4, - "end": 126.4 - }, - "arabic": "وبحركة بسيطة", - "hebrew_transliteration": "וּבְּחַרַכֶּה בַּסִיטַה", - "hebrew": "ובתנועה פשוטה", - "expected_phonemes": "u b ħ a r a k e | b a s iː tˤ a" - }, - { - "voice_time_seconds": { - "start": 126.4, - "end": 128.8 - }, - "arabic": "قبع الزّلمة زرّ من جاكيته", - "hebrew_transliteration": "קַבַּע אִ-זַּלַמֶה זִרّ מִן גַ'אכֵּיתוֹ", - "hebrew": "האיש תלש כפתור מהז'קט שלו", - "expected_phonemes": "ʔ a b a ʕ | ʔ i z a l a m e | z i r r | m i n | dʒ aː k eː t o" - }, - { - "voice_time_seconds": { - "start": 128.8, - "end": 131.4 - }, - "arabic": "ورماه بقلب الطّنجرة.", - "hebrew_transliteration": "וּרַמַאה בְּקַלבּ טֻנגַ'רַה.", - "hebrew": "וזרק אותו לתוך הסיר.", - "expected_phonemes": "u r a m aː h | b ʔ a l b | tˤ u n dʒ a r a" - }, - { - "voice_time_seconds": { - "start": 131.4, - "end": 133.4 - }, - "arabic": "بعد أكمّ من دقيقة،", - "hebrew_transliteration": "בַּעֶד אַכַּם מִן דַקִיקַה,", - "hebrew": "אחרי כמה דקות,", - "expected_phonemes": "b a ʕ e d | ʔ a k a m | m i n | d a ʔ iː ʔ a" - }, - { - "voice_time_seconds": { - "start": 133.4, - "end": 135.6 - }, - "arabic": "أخد معلقة خشب كبيرة،", - "hebrew_transliteration": "אַחַ'ד מִעלַקֶת חַ'שַבּ כְּבִּירֶה,", - "hebrew": "לקח כף עץ גדולה,", - "expected_phonemes": "ʔ a x a d | m i ʕ l a ʔ e t | x a ʃ a b | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 135.6, - "end": 138.8 - }, - "arabic": "وصار يحرّك في الطّنجرة.", - "hebrew_transliteration": "וּצַאר יְחַרֶّכּ פִי טֻּנגַ'רַה.", - "hebrew": "והתחיל לבחוש בסיר.", - "expected_phonemes": "u sˤ aː r | j ħ a r r e k | f i | tˤ u n dʒ a r a" - }, - { - "voice_time_seconds": { - "start": 138.8, - "end": 140.8 - }, - "arabic": "ذاق من الشّوربة", - "hebrew_transliteration": "דַ'אק מִן שּׁוֹרַבֶּה", - "hebrew": "טעם מהמרק", - "expected_phonemes": "dˤ aː ʔ | m i n | ʃ oː r a b e" - }, - { - "voice_time_seconds": { - "start": 140.8, - "end": 143.6 - }, - "arabic": "وضحك ضحكة كبيرة وقال:", - "hebrew_transliteration": "וּדִ'חֶכּ דִ'חְכֶּה כְּבִּירֶה וּקַאל:", - "hebrew": "וצחק צחוק גדול ואמר:", - "expected_phonemes": "u dˤ i ħ e k | dˤ i ħ k e | k b iː r e | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 143.6, - "end": 148.2 - }, - "arabic": "«روعة! هاي الشّوربة رح تكون زاكية كتير.", - "hebrew_transliteration": "רַוְעַה! הַאי שּׁוֹרַבֶּה רַח-תְכּוּן זַאכְּיֶה כְּתִיר.", - "hebrew": "נפלא! המרק הזה יהיה טעים מאוד.", - "expected_phonemes": "r a w ʕ a | h aː i | ʃ oː r a b e | r a ħ t k uː n | z aː k j e | k t iː r" - }, - { - "voice_time_seconds": { - "start": 148.2, - "end": 150.4 - }, - "arabic": "بسّ ممكن تكون أحسن", - "hebrew_transliteration": "בַּס מֻמכֶּן תְכּוּן אַחסַן", - "hebrew": "אבל הוא יכול להיות טוב יותר", - "expected_phonemes": "b a s | m u m k e n | t k uː n | ʔ a ħ s a n" - }, - { - "voice_time_seconds": { - "start": 150.4, - "end": 154.0 - }, - "arabic": "إذا منضيفلها شويّة ملح.»", - "hebrew_transliteration": "אִזַא מִנדִ'יפ-לְהַא שְוַיֶّת מִלֶח.", - "hebrew": "אם נוסיף לו קצת מלח.", - "expected_phonemes": "ʔ i z a | m i n dˤ iː f l h a | ʃ w a j j e t | m i l e ħ" - }, - { - "voice_time_seconds": { - "start": 154.0, - "end": 157.0 - }, - "arabic": "الزّوج إتطلّعوا في بعض وقالوا:", - "hebrew_transliteration": "אִ-זַּוְג' תְטַלַّעוּ פִי בַּעְד' וּקַאלוּ:", - "hebrew": "בני הזוג הביטו זה בזה ואמרו:", - "expected_phonemes": "ʔ i z a w dʒ | t tˤ a l l a ʕ u | f i | b a ʕ dˤ | u ʔ aː l u" - }, - { - "voice_time_seconds": { - "start": 157.0, - "end": 159.2 - }, - "arabic": "«فشّ عنّا إشي في البيت،", - "hebrew_transliteration": "פִשّ עִנַّא אִשִי פִי לְ-בֵּית,", - "hebrew": "אין לנו כלום בבית,", - "expected_phonemes": "f i ʃ ʃ | ʕ i n n a | ʔ i ʃ i | f i | l b eː t" - }, - { - "voice_time_seconds": { - "start": 159.2, - "end": 163.0 - }, - "arabic": "بسّ – ممكن بواحدة من الخزاين...", - "hebrew_transliteration": "בַּס – מֻמכֶּן בְּוַאחדֶה מִן לְ-חַ'זַאיֶן...", - "hebrew": "אבל – אולי באחד הארונות...", - "expected_phonemes": "b a s | m u m k e n | b w aː ħ d e | m i n | l x a z aː j e n" - }, - { - "voice_time_seconds": { - "start": 163.0, - "end": 165.2 - }, - "arabic": "نلاقي شويّة ملح.»", - "hebrew_transliteration": "נְלַאקִי שְוַיֶّת מִלֶח.", - "hebrew": "נמצא קצת מלח.", - "expected_phonemes": "n l aː ʔ i | ʃ w a j j e t | m i l e ħ" - }, - { - "voice_time_seconds": { - "start": 165.2, - "end": 167.6 - }, - "arabic": "راحت المرة بسرعة للمطبخ", - "hebrew_transliteration": "רַאחַת אִל-מַרַה בְּסֻרעַה לַל-מַטבַּח'", - "hebrew": "האישה הלכה מהר למטבח", - "expected_phonemes": "r aː ħ a t | i l m a r a | b s u r ʕ a | l a l m a tˤ b a x" - }, - { - "voice_time_seconds": { - "start": 167.6, - "end": 170.8 - }, - "arabic": "ورجعت حاملة بإيدها معلقة غميقة", - "hebrew_transliteration": "וּרִגְ'עַת חַאמְלֶה בְּאִידְהַא מִעלַקַה עַ'מִיקַה", - "hebrew": "וחזרה ובידה כף עמוקה", - "expected_phonemes": "u r i dʒ ʕ a t | ħ aː m l e | b ʔ iː d h a | m i ʕ l a ʔ a | ɣ a m iː ʔ a" - }, - { - "voice_time_seconds": { - "start": 170.8, - "end": 172.6 - }, - "arabic": "معبّية ملح.", - "hebrew_transliteration": "מְעַבַּיֶּה מִלֶח.", - "hebrew": "מלאה מלח.", - "expected_phonemes": "m ʕ a b a j e | m i l e ħ" - }, - { - "voice_time_seconds": { - "start": 172.6, - "end": 175.6 - }, - "arabic": "انبسط الضّيف، ضاف الملح،", - "hebrew_transliteration": "אִנבַּסַט דֵּ'יף, דַ'אף לְ-מִלֶח,", - "hebrew": "האורח שמח, הוסיף את המלח,", - "expected_phonemes": "ʔ i n b a s a tˤ | dˤ eː f | dˤ aː f | l m i l e ħ" - }, - { - "voice_time_seconds": { - "start": 175.6, - "end": 181.0 - }, - "arabic": "وكمّل يحرّك شوربة لزرار لكمان عشر دقايق.", - "hebrew_transliteration": "וּכַּמַّל יְחַרֶّכּ שוֹרַבֶּת לֶ-זְרַאר לַכַּמַאן עַשֶר דַקַאיֶק.", - "hebrew": "והמשיך לבחוש את מרק הכפתורים עוד עשר דקות.", - "expected_phonemes": "u k a m m a l | j ħ a r r e k | ʃ oː r a b e t | l e z r aː r | l a k a m aː n | ʕ a ʃ e r | d a ʔ aː j e ʔ" - }, - { - "voice_time_seconds": { - "start": 181.0, - "end": 186.0 - }, - "arabic": "بعدين، أخد المعلقة، ذاق الشّوربة وقال:", - "hebrew_transliteration": "בַּעדֵין, אַחַ'ד אִל-מִעלַקַה, דַ'אק אִ-שּׁוֹרַבֶּה וּקַאל:", - "hebrew": "אחר כך, לקח את הכף, טעם את המרק ואמר:", - "expected_phonemes": "b a ʕ d eː n | ʔ a x a d | i l m i ʕ l a ʔ a | dˤ aː ʔ | ʔ i ʃ oː r a b e | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 186.0, - "end": 189.0 - }, - "arabic": "«شوربة شهيّة! بسّ فشّ شكّ", - "hebrew_transliteration": "שוֹרַבֶּה שַהִיֶّה! בַּס פִשّ שַכּ", - "hebrew": "מרק טעים! אבל אין ספק", - "expected_phonemes": "ʃ oː r a b e | ʃ a h i j j e | b a s | f i ʃ ʃ | ʃ a k" - }, - { - "voice_time_seconds": { - "start": 189.0, - "end": 192.6 - }, - "arabic": "إنّه منقدر نحوّلها لأكلة شهيّة", - "hebrew_transliteration": "אִנּוֹ מִנִקדַר נְחַוֶּלְהַא לַאַכְּלֶה שַהִיֶّה", - "hebrew": "שנוכל להפוך אותו למאכל טעים", - "expected_phonemes": "ʔ i n o | m i n i ʔ d a r | n ħ a w e l h a | l a ʔ a k l e | ʃ a h i j j e" - }, - { - "voice_time_seconds": { - "start": 192.6, - "end": 196.0 - }, - "arabic": "إذا منضيفلها راسين بصل.»", - "hebrew_transliteration": "אִזַא מִנדִ'יפ-לְהַא רַאסֵין בַּצַל.", - "hebrew": "אם נוסיף לו שני ראשי בצל.", - "expected_phonemes": "ʔ i z a | m i n dˤ iː f l h a | r aː s eː n | b a sˤ a l" - }, - { - "voice_time_seconds": { - "start": 196.0, - "end": 199.4 - }, - "arabic": "الزّوج إتطلّعوا في بعض وقالوا للضّيف:", - "hebrew_transliteration": "אִ-זַּוְג' תְטַלַّעוּ פִי בַּעְד' וּקַאלוּ לַ-דֵּ'יף:", - "hebrew": "בני הזוג הביטו זה בזה ואמרו לאורח:", - "expected_phonemes": "ʔ i z a w dʒ | t tˤ a l l a ʕ u | f i | b a ʕ dˤ | u ʔ aː l u | l a dˤ eː f" - }, - { - "voice_time_seconds": { - "start": 199.4, - "end": 204.0 - }, - "arabic": "«بيت المونة فاضي، فشّ عنّا إشي في البيت.", - "hebrew_transliteration": "בֵּית אִל-מוֹנֶה פַאדִ'י, פִשّ עִנַّא אִשִי פִי לְ-בֵּית.", - "hebrew": "המזווה ריק, אין לנו כלום בבית.", - "expected_phonemes": "b eː t | i l m oː n e | f aː dˤ i | f i ʃ ʃ | ʕ i n n a | ʔ i ʃ i | f i | l b eː t" - }, - { - "voice_time_seconds": { - "start": 204.0, - "end": 207.8 - }, - "arabic": "بسّ – بلكي إذا طلعنا على الكرسي...", - "hebrew_transliteration": "בַּס – בַּלְכִּי אִזַא טְלִעְנַא עַלַא לְ-כֻּרסִי...", - "hebrew": "אבל – אולי אם נעלה על הכיסא...", - "expected_phonemes": "b a s | b a l k i | ʔ i z a | tˤ l i ʕ n a | ʕ a l a | l k u r s i" - }, - { - "voice_time_seconds": { - "start": 207.8, - "end": 210.6 - }, - "arabic": "ممكن نلاقي على الرّفوف إلّي من فوق", - "hebrew_transliteration": "מֻמכֶּן נְלַאקִי עַלַא רְּפוּף אִלִّי מִן פוֹק", - "hebrew": "אולי נמצא על המדפים העליונים", - "expected_phonemes": "m u m k e n | n l aː ʔ i | ʕ a l a | r f uː f | ʔ i l l i | m i n | f oː ʔ" - }, - { - "voice_time_seconds": { - "start": 210.6, - "end": 213.6 - }, - "arabic": "راس بصل أو راسين.»", - "hebrew_transliteration": "רַאס בַּצַל אַו רַאסֵין.", - "hebrew": "ראש בצל או שניים.", - "expected_phonemes": "r aː s | b a sˤ a l | ʔ a w | r aː s eː n" - }, - { - "voice_time_seconds": { - "start": 213.6, - "end": 216.6 - }, - "arabic": "أسرع صاحب الدّار على بيت المونة", - "hebrew_transliteration": "אַסרַע צַאחֶבּ דַּאר עַלַא בֵּית אִל-מוֹנֶה", - "hebrew": "בעל הבית מיהר למזווה", - "expected_phonemes": "ʔ a s r a ʕ | sˤ aː ħ e b | d aː r | ʕ a l a | b eː t | i l m oː n e" - }, - { - "voice_time_seconds": { - "start": 216.6, - "end": 221.0 - }, - "arabic": "ورجع بإيده راسين بصل كبار.", - "hebrew_transliteration": "וּרִגֶ'ע בְּאִידוֹ רַאסֵין בַּצַל כְּבַּאר.", - "hebrew": "וחזר ובידו שני ראשי בצל גדולים.", - "expected_phonemes": "u r i dʒ e ʕ | b ʔ iː d o | r aː s eː n | b a sˤ a l | k b aː r" - }, - { - "voice_time_seconds": { - "start": 221.0, - "end": 224.4 - }, - "arabic": "فرح كتير الضّيف، ضاف البصل،", - "hebrew_transliteration": "פִרֶח כְּתִיר אִ-דֵּ'יף, דַ'אף לְ-בַּצַל,", - "hebrew": "האורח שמח מאוד, הוסיף את הבצל,", - "expected_phonemes": "f i r e ħ | k t iː r | ʔ i dˤ eː f | dˤ aː f | l b a sˤ a l" - }, - { - "voice_time_seconds": { - "start": 224.4, - "end": 228.0 - }, - "arabic": "وأكّد للزّوج إنّه شوربة لزرار هاي", - "hebrew_transliteration": "וּאַכַּّד לַ-זַּוְג' אִנּוֹ שוֹרַבֶּת לֶ-זְרַאר הַאי", - "hebrew": "והבטיח לבני הזוג שאת מרק הכפתורים הזה", - "expected_phonemes": "u ʔ a k k a d | l a z a w dʒ | ʔ i n o | ʃ oː r a b e t | l e z r aː r | h aː i" - }, - { - "voice_time_seconds": { - "start": 228.0, - "end": 231.6 - }, - "arabic": "مش رح ينسوها لآخر حياتهم.", - "hebrew_transliteration": "מִש רַח-יִנסוּהַא לַאַאחַ'ר חַיַאתְהֹם.", - "hebrew": "הם לא ישכחו עד סוף חייהם.", - "expected_phonemes": "m i ʃ | r a ħ j i n s uː h a | l a ʔ aː x a r | ħ a j aː t h o m" - }, - { - "voice_time_seconds": { - "start": 231.6, - "end": 236.4 - }, - "arabic": "كمّل يحرّك الشّوربة إلّي كانت عمتغلي وبتبقبق،", - "hebrew_transliteration": "כַּמַّל יְחַרֶّכּ שּׁוֹרַבֶּה אִלִّי כַּאנַת עַם-תִעְ'לִי וּבִּתבַּקבֶּק,", - "hebrew": "המשיך לבחוש את המרק שרתח ובעבע,", - "expected_phonemes": "k a m m a l | j ħ a r r e k | ʃ oː r a b e | ʔ i l l i | k aː n a t | ʕ a m t i ɣ l i | u b i t b a ʔ b e ʔ" - }, - { - "voice_time_seconds": { - "start": 236.4, - "end": 240.2 - }, - "arabic": "ذاقها وقال بصوت عمبفكّر:", - "hebrew_transliteration": "דַ'אקְהַא וּקַאל בְּצוֹת עַם-בִּיפַכַּר:", - "hebrew": "טעם אותו ואמר בקול מהורהר:", - "expected_phonemes": "dˤ aː ʔ h a | u ʔ aː l | b sˤ oː t | ʕ a m b iː f a k a r" - }, - { - "voice_time_seconds": { - "start": 240.2, - "end": 245.4 - }, - "arabic": "«خسارة، بسّ لو كان فيه عنّا ضمّة بقدونس...»", - "hebrew_transliteration": "חַ'סַארַה, בַּס לַו כַּאן פִיה עִנַّא דַ'מֶّת בַּקדוֹנֶס...", - "hebrew": "חבל, אבל אילו הייתה לנו צרור פטרוזיליה...", - "expected_phonemes": "x a s aː r a | b a s | l a w | k aː n | f iː h | ʕ i n n a | dˤ a m m e t | b a ʔ d oː n e s" - }, - { - "voice_time_seconds": { - "start": 245.4, - "end": 248.2 - }, - "arabic": "«ضمّة بقدونس؟» سأل الزّلمة.", - "hebrew_transliteration": "דַ'מֶّת בַּקדוֹנֶס? סַאַל אִ-זַּלַמֶה.", - "hebrew": "צרור פטרוזיליה? שאל האיש.", - "expected_phonemes": "dˤ a m m e t | b a ʔ d oː n e s | s a ʔ a l | ʔ i z a l a m e" - }, - { - "voice_time_seconds": { - "start": 248.2, - "end": 253.0 - }, - "arabic": "«أه.» قال الضّيف، «شوربة لزرار كتير شهيّة،", - "hebrew_transliteration": "אַה. קַאל אִ-דֵּ'יף, שוֹרַבֶּת לֶ-זְרַאר כְּתִיר שַהִיֶّה,", - "hebrew": "כן. אמר האורח, מרק הכפתורים טעים מאוד,", - "expected_phonemes": "ʔ aː | ʔ aː l | ʔ i dˤ eː f | ʃ oː r a b e t | l e z r aː r | k t iː r | ʃ a h i j j e" - }, - { - "voice_time_seconds": { - "start": 253.0, - "end": 257.8 - }, - "arabic": "بسّ ضمّة بقدونس بتعملها أكلة ملوكيّة.»", - "hebrew_transliteration": "בַּס דַ'מֶّת בַּקדוֹנֶס בִּתעַמַל-הַא אַכְּלֶה מֻלוּכִּיֶّה.", - "hebrew": "אבל צרור פטרוזיליה יהפוך אותו למאכל מלכים.", - "expected_phonemes": "b a s | dˤ a m m e t | b a ʔ d oː n e s | b i t ʕ a m a l h a | ʔ a k l e | m u l uː k i j j e" - }, - { - "voice_time_seconds": { - "start": 257.8, - "end": 259.2 - }, - "arabic": "المرة قالت:", - "hebrew_transliteration": "אִל-מַרַה קַאלַת:", - "hebrew": "האישה אמרה:", - "expected_phonemes": "i l m a r a | ʔ aː l a t" - }, - { - "voice_time_seconds": { - "start": 259.2, - "end": 262.0 - }, - "arabic": "«رفوف المطبخ عنّا مليانة غبرة.", - "hebrew_transliteration": "רְפוּף אִל-מַטבַּח' עִנַّא מַליַאנֶה עֻ'בְּרַה.", - "hebrew": "מדפי המטבח שלנו מלאים אבק.", - "expected_phonemes": "r f uː f | i l m a tˤ b a x | ʕ i n n a | m a l j aː n e | ɣ u b r a" - }, - { - "voice_time_seconds": { - "start": 262.0, - "end": 263.8 - }, - "arabic": "فشّ عنّا إشي.", - "hebrew_transliteration": "פִשّ עִנַّא אִשִי.", - "hebrew": "אין לנו כלום.", - "expected_phonemes": "f i ʃ ʃ | ʕ i n n a | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 263.8, - "end": 268.0 - }, - "arabic": "بسّ – بشقفة الأرض لصغيرة إلّي ورا البيت...", - "hebrew_transliteration": "בַּס – בְּשֻקפֶת אִל-אַרְד' לֶ-זְעִ'ירֶה אִלִّי וַרַא לְ-בֵּית...", - "hebrew": "אבל – בחלקת האדמה הקטנה שמאחורי הבית...", - "expected_phonemes": "b a s | b ʃ u ʔ f e t | i l ʔ a r dˤ | l e z ɣ iː r e | ʔ i l l i | w a r a | l b eː t" - }, - { - "voice_time_seconds": { - "start": 268.0, - "end": 270.4 - }, - "arabic": "بقي أكمّ غصن بقدونس،", - "hebrew_transliteration": "בְּקִי אַכַּם עֻ'צֻן בַּקדוֹנֶס,", - "hebrew": "נשארו כמה ענפי פטרוזיליה,", - "expected_phonemes": "b ʔ i | ʔ a k a m | ɣ u sˤ u n | b a ʔ d oː n e s" - }, - { - "voice_time_seconds": { - "start": 270.4, - "end": 272.8 - }, - "arabic": "هلّق بروح وبفحص.»", - "hebrew_transliteration": "הַלַّק בַּרוּח וּבַּפחַץ.", - "hebrew": "עכשיו אלך ואבדוק.", - "expected_phonemes": "h a l l a ʔ | b a r uː ħ | u b a f ħ a sˤ" - }, - { - "voice_time_seconds": { - "start": 272.8, - "end": 275.6 - }, - "arabic": "أسرعت صاحبة الدّار للجنينة", - "hebrew_transliteration": "אַסרַעַת צַאחְבֶּת דַּאר לַל-גְ'נֵינֶה", - "hebrew": "בעלת הבית מיהרה לגינה", - "expected_phonemes": "ʔ a s r a ʕ a t | sˤ aː ħ b e t | d aː r | l a l dʒ n eː n e" - }, - { - "voice_time_seconds": { - "start": 275.6, - "end": 280.0 - }, - "arabic": "ورجعت حاملة بإيدها ضمّة بقدونس طازة", - "hebrew_transliteration": "וּרִגְ'עַת חַאמְלֶה בְּאִידְהַא דַ'מֶّת בַּקדוֹנֶס טַאזַה", - "hebrew": "וחזרה ובידה צרור פטרוזיליה טרייה", - "expected_phonemes": "u r i dʒ ʕ a t | ħ aː m l e | b ʔ iː d h a | dˤ a m m e t | b a ʔ d oː n e s | tˤ aː z a" - }, - { - "voice_time_seconds": { - "start": 280.0, - "end": 282.2 - }, - "arabic": "إلها ريحة حلوة.", - "hebrew_transliteration": "אִלְהַא רִיחַה חִלוֶה.", - "hebrew": "שיש לה ריח טוב.", - "expected_phonemes": "i l h a | r iː ħ a | ħ i l w e" - }, - { - "voice_time_seconds": { - "start": 282.2, - "end": 284.4 - }, - "arabic": "الضّيف أخد الضّمّة من إيدها،", - "hebrew_transliteration": "אִ-דֵּ'יף אַחַ'ד אִ-דַּ'מֶּה מִן אִידְהַא,", - "hebrew": "האורח לקח את הצרור מידה,", - "expected_phonemes": "ʔ i dˤ eː f | ʔ a x a d | ʔ i dˤ a m e | m i n | ʔ iː d h a" - }, - { - "voice_time_seconds": { - "start": 284.4, - "end": 287.6 - }, - "arabic": "وبسرعة ضافها لشوربة لزرار.", - "hebrew_transliteration": "וּבְּסֻרעַה דַ'אפְהַא לַשוֹרַבֶּת לֶ-זְרַאר.", - "hebrew": "ומהר הוסיף אותו למרק הכפתורים.", - "expected_phonemes": "u b s u r ʕ a | dˤ aː f h a | l a ʃ oː r a b e t | l e z r aː r" - }, - { - "voice_time_seconds": { - "start": 287.6, - "end": 290.4 - }, - "arabic": "بلّش البيت يتعبّى بريحة طيبة", - "hebrew_transliteration": "בַּלַّש אִל-בֵּית יִתעַבַּא בְּרִיחַה טַיְּבֶּה", - "hebrew": "הבית התחיל להתמלא בריח טוב", - "expected_phonemes": "b a l l a ʃ | i l b eː t | j i t ʕ a b a | b r iː ħ a | tˤ a j b e" - }, - { - "voice_time_seconds": { - "start": 290.4, - "end": 294.2 - }, - "arabic": "والضّيف ذاق كمان مرّة من الشّوربة وقال:", - "hebrew_transliteration": "וּ-דֵּ'יף דַ'אק כַּמַאן מַרַّה מִן אִ-שּׁוֹרַבֶּה וּקַאל:", - "hebrew": "והאורח טעם שוב מהמרק ואמר:", - "expected_phonemes": "u dˤ eː f | dˤ aː ʔ | k a m aː n | m a r r a | m i n | ʔ i ʃ oː r a b e | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 294.2, - "end": 297.4 - }, - "arabic": "«كمان شويّ منقدر نقعد نوكل.", - "hebrew_transliteration": "כַּמַאן שְוַיّ מִנִקדַר נֻקעֹד נוֹכֹּל.", - "hebrew": "עוד מעט נוכל לשבת לאכול.", - "expected_phonemes": "k a m aː n | ʃ w a j j | m i n i ʔ d a r | n u ʔ ʕ u d | n oː k o l" - }, - { - "voice_time_seconds": { - "start": 297.4, - "end": 300.0 - }, - "arabic": "فشّ أحسن من شوربة زرار سخنة", - "hebrew_transliteration": "פִשّ אַחסַן מִן שוֹרַבֶּת זְרַאר סֻחְ'נֶה", - "hebrew": "אין טוב יותר ממרק כפתורים חם", - "expected_phonemes": "f i ʃ ʃ | ʔ a ħ s a n | m i n | ʃ oː r a b e t | z r aː r | s u x n e" - }, - { - "voice_time_seconds": { - "start": 300.0, - "end": 302.6 - }, - "arabic": "بهيك مسا بارد ورطب.", - "hebrew_transliteration": "בְּהֵיכּ מַסַא בַּארֶד וּרַטֶבּ.", - "hebrew": "בערב קר ולח כזה.", - "expected_phonemes": "b h eː k | m a s a | b aː r e d | u r a tˤ e b" - }, - { - "voice_time_seconds": { - "start": 302.6, - "end": 305.2 - }, - "arabic": "بسّ إذا منلاقي طريقة", - "hebrew_transliteration": "בַּס אִזַא מִנלַאקִי טַרִיקַה", - "hebrew": "אבל אם נמצא דרך", - "expected_phonemes": "b a s | ʔ i z a | m i n l aː ʔ i | tˤ a r iː ʔ a" - }, - { - "voice_time_seconds": { - "start": 305.2, - "end": 309.0 - }, - "arabic": "نضيف للشّوربة ثلاث حبّات بطاطا،", - "hebrew_transliteration": "נְדִ'יף לַ-שּׁוֹרַבֶּה תַלַת חַבַּאת בַּטַאטַא,", - "hebrew": "להוסיף למרק שלושה תפוחי אדמה,", - "expected_phonemes": "n dˤ iː f | l a ʃ oː r a b e | t a l a t | ħ a b aː t | b a tˤ aː tˤ a" - }, - { - "voice_time_seconds": { - "start": 309.0, - "end": 313.6 - }, - "arabic": "ساعتها منوكل ومنشبع لمدّة يومين كاملين.»", - "hebrew_transliteration": "סַאעִתְהַא מִנוֹכֹּל וּמִנִשבַּע לַמֻדֶّת יוֹמֵין כַּאמְלִין.", - "hebrew": "אז נאכל ונשבע ליומיים שלמים.", - "expected_phonemes": "s aː ʕ i t h a | m i n oː k o l | u m i n i ʃ b a ʕ | l a m u d d e t | j oː m eː n | k aː m l iː n" - }, - { - "voice_time_seconds": { - "start": 313.6, - "end": 317.4 - }, - "arabic": "«أيّام صعبة عمتمرق علينا والبيت فاضي.", - "hebrew_transliteration": "אַיַّאם צַעבֶּה עַם-תֻמרֹק עַלֵינַא וִל-בֵּית פַאדִ'י.", - "hebrew": "ימים קשים עוברים עלינו והבית ריק.", - "expected_phonemes": "ʔ a j j aː m | sˤ a ʕ b e | ʕ a m t u m r o ʔ | ʕ a l eː n a | w i l b eː t | f aː dˤ i" - }, - { - "voice_time_seconds": { - "start": 317.4, - "end": 320.8 - }, - "arabic": "بسّ – يمكن بقي أكمّ حبّة بطاطا أخيرات...", - "hebrew_transliteration": "בַּס – יֻמכֶּן בְּקִי אַכַּם חַבֶּת בַּטַאטַא אַחִ'ירַאת...", - "hebrew": "אבל – אולי נשארו כמה תפוחי אדמה אחרונים...", - "expected_phonemes": "b a s | j u m k e n | b ʔ i | ʔ a k a m | ħ a b e t | b a tˤ aː tˤ a | ʔ a x iː r aː t" - }, - { - "voice_time_seconds": { - "start": 320.8, - "end": 323.0 - }, - "arabic": "في السّلّة إلّي في الخزانة.»", - "hebrew_transliteration": "פִי סַּלֶּה אִלִّי פִי לְ-חַ'זַאנֶה.", - "hebrew": "בסל שבארון.", - "expected_phonemes": "f i | s a l e | ʔ i l l i | f i | l x a z aː n e" - }, - { - "voice_time_seconds": { - "start": 323.0, - "end": 324.8 - }, - "arabic": "قال صاحب الدّار،", - "hebrew_transliteration": "קַאל צַאחֶבּ דַּאר,", - "hebrew": "אמר בעל הבית,", - "expected_phonemes": "ʔ aː l | sˤ aː ħ e b | d aː r" - }, - { - "voice_time_seconds": { - "start": 324.8, - "end": 328.8 - }, - "arabic": "وبعد فترة قصيرة رجع وهوّ عمبيضحك", - "hebrew_transliteration": "וּבַּעֶד פַתרַה קַצִירֶה רִגֶ'ע וּהֻוֶّ עַם-בִּידִ'חַכּ", - "hebrew": "ואחרי זמן קצר חזר והוא צוחק", - "expected_phonemes": "u b a ʕ e d | f a t r a | ʔ a sˤ iː r e | r i dʒ e ʕ | u h u w w e | ʕ a m b iː dˤ i ħ a k" - }, - { - "voice_time_seconds": { - "start": 328.8, - "end": 332.8 - }, - "arabic": "وقدّم للضّيف ثلاث حبّات بطاطا كبار.", - "hebrew_transliteration": "וּקַדַّם לַ-דֵּ'יף תַלַת חַבַּאת בַּטַאטַא כְּבַּאר.", - "hebrew": "והגיש לאורח שלושה תפוחי אדמה גדולים.", - "expected_phonemes": "u ʔ a d d a m | l a dˤ eː f | t a l a t | ħ a b aː t | b a tˤ aː tˤ a | k b aː r" - }, - { - "voice_time_seconds": { - "start": 332.8, - "end": 335.6 - }, - "arabic": "الضّيف ضاف البطاطا للطّنجرة،", - "hebrew_transliteration": "אִ-דֵּ'יף דַ'אף אִל-בַּטַאטַא לַ-טֻּנגַ'רַה,", - "hebrew": "האורח הוסיף את תפוחי האדמה לסיר,", - "expected_phonemes": "ʔ i dˤ eː f | dˤ aː f | i l b a tˤ aː tˤ a | l a tˤ u n dʒ a r a" - }, - { - "voice_time_seconds": { - "start": 335.6, - "end": 337.6 - }, - "arabic": "إلّي كانت عمتغلي،", - "hebrew_transliteration": "אִלִّי כַּאנַת עַם-תִעְ'לִי,", - "hebrew": "שרתח,", - "expected_phonemes": "ʔ i l l i | k aː n a t | ʕ a m t i ɣ l i" - }, - { - "voice_time_seconds": { - "start": 337.6, - "end": 340.8 - }, - "arabic": "وخلال نصّ ساعة أعلن لصحاب الدّار", - "hebrew_transliteration": "וּחִ'לַאל נֻצّ סַאעַה אַעלַן לַצְחַאבּ דַּאר", - "hebrew": "ותוך חצי שעה הודיע לבעלי הבית", - "expected_phonemes": "u x i l aː l | n u sˤ sˤ | s aː ʕ a | ʔ a ʕ l a n | l a sˤ ħ aː b | d aː r" - }, - { - "voice_time_seconds": { - "start": 340.8, - "end": 344.0 - }, - "arabic": "إنّه شوربة لزرار جاهزة.", - "hebrew_transliteration": "אִנּוֹ שוֹרַבֶּת לֶ-זְרַאר גַ'אהְזֶה.", - "hebrew": "שמרק הכפתורים מוכן.", - "expected_phonemes": "ʔ i n o | ʃ oː r a b e t | l e z r aː r | dʒ aː h z e" - }, - { - "voice_time_seconds": { - "start": 344.0, - "end": 346.2 - }, - "arabic": "قعدوا حوالين الطّاولة،", - "hebrew_transliteration": "קַעַדוּ חַוַאלֵין טַּאוְלֶה,", - "hebrew": "ישבו סביב השולחן,", - "expected_phonemes": "ʔ a ʕ a d u | ħ a w aː l eː n | tˤ aː w l e" - }, - { - "voice_time_seconds": { - "start": 346.2, - "end": 350.6 - }, - "arabic": "صبّوا شوربة لزرار بصحون وبلّشوا يوكلوا.", - "hebrew_transliteration": "צַבּוּ שוֹרַבֶּת לֶ-זְרַאר בְּצְחוּן וּבַּלַّשוּ יוֹכְּלוּ.", - "hebrew": "יצקו את מרק הכפתורים לצלחות והתחילו לאכול.", - "expected_phonemes": "sˤ a b u | ʃ oː r a b e t | l e z r aː r | b sˤ ħ uː n | u b a l l a ʃ u | j oː k l u" - }, - { - "voice_time_seconds": { - "start": 350.6, - "end": 354.4 - }, - "arabic": "«قدّيش الطّعم زاكي!» قال صاحب الدّار.", - "hebrew_transliteration": "קַדֵّיש אִ-טַּעֶם זַאכִּי! קַאל צַאחֶבּ דַּאר.", - "hebrew": "כמה הטעם טוב! אמר בעל הבית.", - "expected_phonemes": "ʔ a d d eː ʃ | ʔ i tˤ a ʕ e m | z aː k i | ʔ aː l | sˤ aː ħ e b | d aː r" - }, - { - "voice_time_seconds": { - "start": 354.4, - "end": 358.6 - }, - "arabic": "«أكل بليق في الملوك!» قالت صاحبة الدّار.", - "hebrew_transliteration": "אַכֶּל בִּלִיק פִי לְ-מֻלוּכּ! קַאלַת צַאחְבֶּת דַּאר.", - "hebrew": "אוכל ראוי למלכים! אמרה בעלת הבית.", - "expected_phonemes": "ʔ a k e l | b i l iː ʔ | f i | l m u l uː k | ʔ aː l a t | sˤ aː ħ b e t | d aː r" - }, - { - "voice_time_seconds": { - "start": 358.6, - "end": 363.2 - }, - "arabic": "الضّيف قال: «نعم، متل ما قولتلكو.", - "hebrew_transliteration": "אִ-דֵּ'יף קַאל: נַעַם, מִתֶל מַא קֻלתוּ-לְכּוּ.", - "hebrew": "האורח אמר: כן, כמו שאמרתי לכם.", - "expected_phonemes": "ʔ i dˤ eː f | ʔ aː l | n a ʕ a m | m i t e l | m a | ʔ u l t uː l k u" - }, - { - "voice_time_seconds": { - "start": 363.2, - "end": 366.6 - }, - "arabic": "شوربة الزرار هيّ عجيبة بالفعل!", - "hebrew_transliteration": "שוֹרַבֶּת לֶ-זְרַאר הִיֶّ עַגִ'יבֶּה בִּל-פִעֶל!", - "hebrew": "מרק הכפתורים הוא באמת פלא!", - "expected_phonemes": "ʃ oː r a b e t | l e z r aː r | h i j j e | ʕ a dʒ iː b e | b i l f i ʕ e l" - }, - { - "voice_time_seconds": { - "start": 366.6, - "end": 369.8 - }, - "arabic": "ومين كان يتوقّع إنّه ممكن نحضّر شوربة", - "hebrew_transliteration": "וּמִין כַּאן יִתוַקַّע אִנּוֹ מֻמכֶּן נְחַדֶّר שוֹרַבֶּה", - "hebrew": "ומי היה מצפה שאפשר להכין מרק", - "expected_phonemes": "u m iː n | k aː n | j i t w a ʔ ʔ a ʕ | ʔ i n o | m u m k e n | n ħ a d d e r | ʃ oː r a b e" - }, - { - "voice_time_seconds": { - "start": 369.8, - "end": 374.8 - }, - "arabic": "من ميّ وزرّ؟»", - "hebrew_transliteration": "מִן מַיّ וּזִרّ?", - "hebrew": "ממים וכפתור?", - "expected_phonemes": "m i n | m a j j | u z i r r" - } - ] - }, - { - "file_name": "החתול במגפיים * אִל-בֶּס אַבּוֹ א-גַזמֶה * البسّ أبو الجّزمة * סיפור בערבית מדוברת [YPyL5zTutD4].webm", - "youtube_id": "YPyL5zTutD4", - "title_hebrew": "החתול במגפיים", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "البسّ أبو الجّزمة", - "hebrew_transliteration": "אִל-בֶּסّ אַבּוּ גַּזמֶה", - "hebrew": "החתול במגפיים", - "expected_phonemes": "i l b e s s | ʔ a b u | g a z m e" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 9.2 - }, - "arabic": "كان ياما كان، في قديم الزّمان،", - "hebrew_transliteration": "כַּאן יַאמַא כַּאן פִי קַדִים זַּמַאן,", - "hebrew": "היה היה בימים עברו,", - "expected_phonemes": "k aː n | j aː m a | k aː n | f i | ʔ a d iː m | z a m aː n" - }, - { - "voice_time_seconds": { - "start": 9.2, - "end": 11.2 - }, - "arabic": "طحّان غني وناجح،", - "hebrew_transliteration": "טַחַّאן עַ'נִי וּנַאגֶ'ח,", - "hebrew": "טוחן עשיר ומצליח,", - "expected_phonemes": "tˤ a ħ ħ aː n | ɣ a n i | u n aː dʒ e ħ" - }, - { - "voice_time_seconds": { - "start": 11.2, - "end": 13.6 - }, - "arabic": "ملّاك أراضي وبيوت،", - "hebrew_transliteration": "מַלַّאכּ אַרַאדִי וּבְּיוּת,", - "hebrew": "בעל אדמות ובתים,", - "expected_phonemes": "m a l l aː k | ʔ a r aː d i | u b j uː t" - }, - { - "voice_time_seconds": { - "start": 13.6, - "end": 16.2 - }, - "arabic": "وعنده تلت ولاد.", - "hebrew_transliteration": "וּעִנדוֹ תַלַת וְלַאד.", - "hebrew": "והיו לו שלושה בנים.", - "expected_phonemes": "u ʕ i n d o | t a l a t | u l aː d" - }, - { - "voice_time_seconds": { - "start": 16.2, - "end": 17.8 - }, - "arabic": "يوم من الأيّام،", - "hebrew_transliteration": "יוֹם מִן לְ-אַיַّאם,", - "hebrew": "יום אחד,", - "expected_phonemes": "j oː m | m i n | l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 17.8, - "end": 22.0 - }, - "arabic": "مرض هالطّحّان بمرض خطير ومات.", - "hebrew_transliteration": "מִרֶד' הַ-טַּחַّאן בְּמַרַד' חַ'טִיר וּמַאת.", - "hebrew": "הטוחן הזה חלה במחלה קשה ומת.", - "expected_phonemes": "m i r e dˤ | h a tˤ a ħ ħ aː n | b m a r a dˤ | x a tˤ iː r | u m aː t" - }, - { - "voice_time_seconds": { - "start": 22.0, - "end": 27.2 - }, - "arabic": "في وصيته ترك الطّحّان كلّ أملاكه لولاده التّلاتة.", - "hebrew_transliteration": "פִי וַצִיתוֹ תַרַכּ טַּחַّאן כֻּלّ אַמלַאכּוֹ לַוְלַאדוֹ תַּלַאתֶה.", - "hebrew": "בצוואתו השאיר הטוחן את כל רכושו לשלושת בניו.", - "expected_phonemes": "f i | w a sˤ iː t o | t a r a k | tˤ a ħ ħ aː n | k u l l | ʔ a m l aː k o | l a w l aː d o | t a l aː t e" - }, - { - "voice_time_seconds": { - "start": 27.2, - "end": 32.6 - }, - "arabic": "للتّنين لكبار ترك المطحنة، لبيوت والأراضي.", - "hebrew_transliteration": "לַ-תְּנֵין לֶ-כְּבַּאר תַרַכּ אִל-מַטחַנֶה, לֶ-בְּיוּת וִל-אַרַאדִי.", - "hebrew": "לשני הגדולים השאיר את הטחנה, את הבתים ואת האדמות.", - "expected_phonemes": "l a t n eː n | l e k b aː r | t a r a k | i l m a tˤ ħ a n e | l e b j uː t | w i l ʔ a r aː d i" - }, - { - "voice_time_seconds": { - "start": 32.6, - "end": 36.2 - }, - "arabic": "وللولد لصغير تركله البسّ.", - "hebrew_transliteration": "וִלַל-וַלַד לֶ-זְעִ'יר תַרַכּ-לוֹ לְ-בֶּסّ.", - "hebrew": "ולבן הקטן השאיר את החתול.", - "expected_phonemes": "w i l a l w a l a d | l e z ɣ iː r | t a r a k l o | l b e s s" - }, - { - "voice_time_seconds": { - "start": 36.2, - "end": 38.8 - }, - "arabic": "الإبن لصغير زعل كتير،", - "hebrew_transliteration": "אִל-אִבְּן לֶ-זְעִ'יר זִעֶל כְּתִיר,", - "hebrew": "הבן הקטן כעס מאוד,", - "expected_phonemes": "i l ʔ i b n | l e z ɣ iː r | z i ʕ e l | k t iː r" - }, - { - "voice_time_seconds": { - "start": 38.8, - "end": 41.2 - }, - "arabic": "انقهر وقال:", - "hebrew_transliteration": "אִנקַהַר וּקַאל:", - "hebrew": "התמרמר ואמר:", - "expected_phonemes": "ʔ i n ʔ a h a r | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 41.2, - "end": 44.2 - }, - "arabic": "«إخوتي لكبار أخدوا كلّ الأملاك،", - "hebrew_transliteration": "אִחְ'וְתִי לֶ-כְּבַּאר אַחַ'דוּ כֻּלّ לְ-אַמלַאכּ,", - "hebrew": "אחיי הגדולים לקחו את כל הרכוש,", - "expected_phonemes": "ʔ i x w t i | l e k b aː r | ʔ a x a d u | k u l l | l ʔ a m l aː k" - }, - { - "voice_time_seconds": { - "start": 44.2, - "end": 47.2 - }, - "arabic": "وعايشين حياة غنى وترف،", - "hebrew_transliteration": "וּעַאיְשִין חַיַאת עִ'נַא וּתַרַף,", - "hebrew": "וחיים חיי עושר ומותרות,", - "expected_phonemes": "u ʕ aː j ʃ iː n | ħ a j aː t | ɣ i n a | u t a r a f" - }, - { - "voice_time_seconds": { - "start": 47.2, - "end": 50.2 - }, - "arabic": "أمّا أنا فقير معيش نكلة،", - "hebrew_transliteration": "אַמַّא אַנַא פַקִיר מַעִיש נַכְּלֶה,", - "hebrew": "ואילו אני עני ואין לי פרוטה,", - "expected_phonemes": "ʔ a m m a | ʔ a n a | f a ʔ iː r | m a ʕ iː ʃ | n a k l e" - }, - { - "voice_time_seconds": { - "start": 50.2, - "end": 53.4 - }, - "arabic": "وأكيد راح أموت من الجوع.»", - "hebrew_transliteration": "וּאַכִּיד רַח-אַמוּת מִן גּוּע.", - "hebrew": "ובטוח אמות מרעב.", - "expected_phonemes": "u ʔ a k iː d | r a ħ ʔ a m uː t | m i n | g uː ʕ" - }, - { - "voice_time_seconds": { - "start": 53.4, - "end": 57.8 - }, - "arabic": "البسّ إلّي كان نايم في الصّندوق جمب الفرن،", - "hebrew_transliteration": "אִל-בֶּסّ אִלִّי כַּאן נַאיֶם פִי צַּנדוּק גַ'מבּ לְ-פֻרֶן,", - "hebrew": "החתול, שישן בארגז ליד התנור,", - "expected_phonemes": "i l b e s s | ʔ i l l i | k aː n | n aː j e m | f i | sˤ a n d uː ʔ | dʒ a m b | l f u r e n" - }, - { - "voice_time_seconds": { - "start": 57.8, - "end": 59.4 - }, - "arabic": "فاق وقال:", - "hebrew_transliteration": "פַאק וּקַאל:", - "hebrew": "התעורר ואמר:", - "expected_phonemes": "f aː ʔ | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 59.4, - "end": 61.2 - }, - "arabic": "«تقلقش، يا سيدي.", - "hebrew_transliteration": "תְקַלקַ-ש, יַא סִידִי.", - "hebrew": "אל תדאג, אדוני.", - "expected_phonemes": "t ʔ a l ʔ a ʃ | j a | s iː d i" - }, - { - "voice_time_seconds": { - "start": 61.2, - "end": 65.4 - }, - "arabic": "إنت بسّ أعطيني كيس وجزمة على مقاسي،", - "hebrew_transliteration": "אִנתֶ בַּס אַעטִינִי כִּיס וּגַזמֶה עַלַא מַקַאסִי,", - "hebrew": "אתה רק תן לי שק ומגפיים במידה שלי,", - "expected_phonemes": "ʔ i n t e | b a s | ʔ a ʕ tˤ iː n i | k iː s | u g a z m e | ʕ a l a | m a ʔ aː s i" - }, - { - "voice_time_seconds": { - "start": 65.4, - "end": 69.0 - }, - "arabic": "وراح تشوف إنّه نصيبك من ورثة أبوك", - "hebrew_transliteration": "וּרַח-תְשוּף אִנּוֹ נַצִיבַּכּ מִן וַרתֶ'ת אַבּוּכּ", - "hebrew": "ותראה שחלקך בירושת אביך", - "expected_phonemes": "u r a ħ t ʃ uː f | ʔ i n o | n a sˤ iː b a k | m i n | w a r t e t | ʔ a b uː k" - }, - { - "voice_time_seconds": { - "start": 69.0, - "end": 72.0 - }, - "arabic": "مش قليل كيف ما إنت مفكّر.»", - "hebrew_transliteration": "מִש קַלִיל כִּיף מַא אִנתֶ מְפַכַּר.", - "hebrew": "לא קטן כמו שאתה חושב.", - "expected_phonemes": "m i ʃ | ʔ a l iː l | k iː f | m a | ʔ i n t e | m f a k a r" - }, - { - "voice_time_seconds": { - "start": 72.0, - "end": 76.0 - }, - "arabic": "الإبن لصغير عمل متل ما حكاله البسّ،", - "hebrew_transliteration": "אִל-אִבְּן לֶ-זְעִ'יר עִמֶל מִתֶל מַא חַכַא-לוֹ לְ-בֶּסّ,", - "hebrew": "הבן הקטן עשה כמו שאמר לו החתול,", - "expected_phonemes": "i l ʔ i b n | l e z ɣ iː r | ʕ i m e l | m i t e l | m a | ħ a k aː l o | l b e s s" - }, - { - "voice_time_seconds": { - "start": 76.0, - "end": 79.0 - }, - "arabic": "وجابله جزمة وكيس.", - "hebrew_transliteration": "וּגַ'אבּ-לוֹ גַזמֶה וּכִּיס.", - "hebrew": "והביא לו מגפיים ושק.", - "expected_phonemes": "u dʒ aː b l o | g a z m e | u k iː s" - }, - { - "voice_time_seconds": { - "start": 79.0, - "end": 81.4 - }, - "arabic": "إتطلّع على البسّ وكان مصدوم", - "hebrew_transliteration": "אִתטַלַّע עַלַא לְ-בֶּסّ וּכַּאן מַצדוּם", - "hebrew": "הביט בחתול והיה המום", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | ʕ a l a | l b e s s | u k aː n | m a sˤ d uː m" - }, - { - "voice_time_seconds": { - "start": 81.4, - "end": 84.6 - }, - "arabic": "كيف شافه عم بلبس الجّزمة على إجريه،", - "hebrew_transliteration": "כִּיף שַאפוֹ עַם-בִּילבַּס גַּזמֶה עַלַא אִגְ'רֵיה,", - "hebrew": "כשראה אותו נועל את המגפיים על רגליו,", - "expected_phonemes": "k iː f | ʃ aː f o | ʕ a m b iː l b a s | g a z m e | ʕ a l a | ʔ i dʒ r eː h" - }, - { - "voice_time_seconds": { - "start": 84.6, - "end": 89.4 - }, - "arabic": "وبيرمي على أكتافه الكيس، وطلع لطريقه.", - "hebrew_transliteration": "וּבִּירמִי עַלַא כְּתַאפוֹ לְ-כִּיס, וּטִלֶע לַטַרִיקוֹ.", - "hebrew": "זורק על כתפיו את השק, ויוצא לדרכו.", - "expected_phonemes": "u b iː r m i | ʕ a l a | k t aː f o | l k iː s | u tˤ i l e ʕ | l a tˤ a r iː ʔ o" - }, - { - "voice_time_seconds": { - "start": 89.4, - "end": 92.0 - }, - "arabic": "البسّ طلع على البريّة،", - "hebrew_transliteration": "אִל-בֶּסّ טִלֶע עַלַא לְ-בַּרִיֶّה,", - "hebrew": "החתול יצא לשדה הפתוח,", - "expected_phonemes": "i l b e s s | tˤ i l e ʕ | ʕ a l a | l b a r i j j e" - }, - { - "voice_time_seconds": { - "start": 92.0, - "end": 94.4 - }, - "arabic": "وين عاشت أرانب كتيرة.", - "hebrew_transliteration": "וֵין עַאשַת אַרַאנֶבּ כְּתִירֶה.", - "hebrew": "שם חיו ארנבים רבים.", - "expected_phonemes": "w eː n | ʕ aː ʃ a t | ʔ a r aː n e b | k t iː r e" - }, - { - "voice_time_seconds": { - "start": 94.4, - "end": 98.6 - }, - "arabic": "رمى بقلب كيسه شويّة قشر فواكه ووراق خضرا،", - "hebrew_transliteration": "רַמַא בְּקַלבּ כִּיסוֹ שְוַיֶّת קִשֶר פַוַאכֶּה וּוְרַאק חֻ'דְרַא,", - "hebrew": "זרק לתוך השק שלו מעט קליפות פירות ועלים ירוקים,", - "expected_phonemes": "r a m a | b ʔ a l b | k iː s o | ʃ w a j j e t | ʔ i ʃ e r | f a w aː k e | u w r aː ʔ | x u d r a" - }, - { - "voice_time_seconds": { - "start": 98.6, - "end": 101.6 - }, - "arabic": "وقعد يستنّى أرنب متسرّع", - "hebrew_transliteration": "וּקַעַד יִסתַנַّא אַרנַבּ מִתסַרֶّע", - "hebrew": "וישב לחכות לארנב פזיז", - "expected_phonemes": "u ʔ a ʕ a d | j i s t a n n a | ʔ a r n a b | m i t s a r r e ʕ" - }, - { - "voice_time_seconds": { - "start": 101.6, - "end": 105.6 - }, - "arabic": "ينجرّ للكيس وييجي يوكل.", - "hebrew_transliteration": "יִנגַ'רّ לַל-כִּיס וִיגִ'י יוֹכֹּל.", - "hebrew": "שיימשך לשק ויבוא לאכול.", - "expected_phonemes": "j i n dʒ a r r | l a l k iː s | w iː dʒ i | j oː k o l" - }, - { - "voice_time_seconds": { - "start": 105.6, - "end": 107.2 - }, - "arabic": "خلال وقت قصير،", - "hebrew_transliteration": "חִ'לַאל וַקת קַצִיר,", - "hebrew": "תוך זמן קצר,", - "expected_phonemes": "x i l aː l | w a ʔ t | ʔ a sˤ iː r" - }, - { - "voice_time_seconds": { - "start": 107.2, - "end": 109.8 - }, - "arabic": "نطّوا تلت أرانب لقلب الكيس،", - "hebrew_transliteration": "נַטּוּ תַלַת אַרַאנֶבּ לַקַלבּ לְ-כִּיס,", - "hebrew": "קפצו שלושה ארנבים לתוך השק,", - "expected_phonemes": "n a tˤ u | t a l a t | ʔ a r aː n e b | l a ʔ a l b | l k iː s" - }, - { - "voice_time_seconds": { - "start": 109.8, - "end": 113.6 - }, - "arabic": "والبسّ طوّالي ربط الكيس حواليهم", - "hebrew_transliteration": "וִל-בֶּסّ טַוַّאלִי רַבַּט אִל-כִּיס חַוַאלֵיהֹם", - "hebrew": "והחתול מיד קשר את השק סביבם", - "expected_phonemes": "w i l b e s s | tˤ a w w aː l i | r a b a tˤ | i l k iː s | ħ a w aː l eː h o m" - }, - { - "voice_time_seconds": { - "start": 113.6, - "end": 115.8 - }, - "arabic": "وحبسهم في قلبه.", - "hebrew_transliteration": "וּחַבַּסְהֹם פִי קַלבּוֹ.", - "hebrew": "וכלא אותם בתוכו.", - "expected_phonemes": "u ħ a b a s h o m | f i | ʔ a l b o" - }, - { - "voice_time_seconds": { - "start": 115.8, - "end": 119.2 - }, - "arabic": "البسّ أبو الجّزمة كان كتير فخور في نفسه", - "hebrew_transliteration": "אִל-בֶּסّ אַבּוּ גַּזמֶה כַּאן כְּתִיר פַחוּר בְּנַפסוֹ", - "hebrew": "החתול במגפיים היה גאה מאוד בעצמו", - "expected_phonemes": "i l b e s s | ʔ a b u | g a z m e | k aː n | k t iː r | f a ħ uː r | b n a f s o" - }, - { - "voice_time_seconds": { - "start": 119.2, - "end": 121.4 - }, - "arabic": "وراح بسرعة على القصر،", - "hebrew_transliteration": "וּרַאח בְּסֻרעַה עַלַא לְ-קַצֶר,", - "hebrew": "והלך מהר לארמון,", - "expected_phonemes": "u r aː ħ | b s u r ʕ a | ʕ a l a | l ʔ a sˤ e r" - }, - { - "voice_time_seconds": { - "start": 121.4, - "end": 124.0 - }, - "arabic": "وطلب يحكي مع الملك.", - "hebrew_transliteration": "וּטַלַבּ יִחכִּי מַע לְ-מַלִכּ.", - "hebrew": "וביקש לדבר עם המלך.", - "expected_phonemes": "u tˤ a l a b | j i ħ k i | m a ʕ | l m a l i k" - }, - { - "voice_time_seconds": { - "start": 124.0, - "end": 126.6 - }, - "arabic": "انحنى للملك وقال:", - "hebrew_transliteration": "אִנחַנַא לַל-מַלִכּ וּקַאל:", - "hebrew": "השתחווה למלך ואמר:", - "expected_phonemes": "ʔ i n ħ a n a | l a l m a l i k | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 126.6, - "end": 131.2 - }, - "arabic": "«مولاي، جبتلّك أرانب من سيدي النّبيل،", - "hebrew_transliteration": "מוֹלַאי, גִ'בֶּת-לַכּ אַרַאנֶבּ מִן סִידִי נַּבִּיל,", - "hebrew": "אדוני, הבאתי לך ארנבים מאדוני האציל,", - "expected_phonemes": "m oː l aː i | dʒ i b e t l a k | ʔ a r aː n e b | m i n | s iː d i | n a b iː l" - }, - { - "voice_time_seconds": { - "start": 131.2, - "end": 133.6 - }, - "arabic": "الماركيز من كرابس", - "hebrew_transliteration": "אִל-מַארְכִּיז מִן כַּרַאבַּס", - "hebrew": "המרקיז מקרבס", - "expected_phonemes": "i l m aː r k iː z | m i n | k a r aː b a s" - }, - { - "voice_time_seconds": { - "start": 133.6, - "end": 135.4 - }, - "arabic": "(هيك البسّ سمّى سيده)،", - "hebrew_transliteration": "(הֵיכּ לְ-בֶּסّ סַמַّא סִידוֹ),", - "hebrew": "(כך קרא החתול לאדונו),", - "expected_phonemes": "h eː k | l b e s s | s a m m a | s iː d o" - }, - { - "voice_time_seconds": { - "start": 135.4, - "end": 139.2 - }, - "arabic": "وأمرني أعطيك إيّاهم هديّة.»", - "hebrew_transliteration": "וּאַמַרְנִי אַעטִיכּ יַּאהֹם הַדִיֶّה.", - "hebrew": "וציווה עליי לתת לך אותם במתנה.", - "expected_phonemes": "u ʔ a m a r n i | ʔ a ʕ tˤ iː k | j aː h o m | h a d i j j e" - }, - { - "voice_time_seconds": { - "start": 139.2, - "end": 141.2 - }, - "arabic": "الملك كان كتير مبسوط", - "hebrew_transliteration": "אִל-מַלִכּ כַּאן כְּתִיר מַבְּסוּט", - "hebrew": "המלך שמח מאוד", - "expected_phonemes": "i l m a l i k | k aː n | k t iː r | m a b s uː tˤ" - }, - { - "voice_time_seconds": { - "start": 141.2, - "end": 144.2 - }, - "arabic": "لمّا سمع هدا الكلام وقال:", - "hebrew_transliteration": "לַמַّא סִמֶע הַאדַא לְ-כַּלַאם וּקַאל:", - "hebrew": "כששמע את הדברים האלה ואמר:", - "expected_phonemes": "l a m m a | s i m e ʕ | h aː d a | l k a l aː m | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 144.2, - "end": 147.0 - }, - "arabic": "«قول لسيدك الماركيز من كرابس", - "hebrew_transliteration": "קוּל לַסִידַכּ אִל-מַארְכִּיז מִן כַּרַאבַּס", - "hebrew": "אמור לאדונך המרקיז מקרבס", - "expected_phonemes": "ʔ uː l | l a s iː d a k | i l m aː r k iː z | m i n | k a r aː b a s" - }, - { - "voice_time_seconds": { - "start": 147.0, - "end": 151.0 - }, - "arabic": "إنّي بقبل هديّته بكلّ سرور.»", - "hebrew_transliteration": "אִנִّי בַּקבַּל אִל-הַדִיֶّה בְּכֻלّ סֻרוּר.", - "hebrew": "שאני מקבל את מתנתו בשמחה רבה.", - "expected_phonemes": "ʔ i n n i | b a ʔ b a l | i l h a d i j j e | b k u l l | s u r uː r" - }, - { - "voice_time_seconds": { - "start": 151.0, - "end": 152.6 - }, - "arabic": "وهيك صار،", - "hebrew_transliteration": "וּהֵיכּ צַאר,", - "hebrew": "וכך היה,", - "expected_phonemes": "u h eː k | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 152.6, - "end": 157.4 - }, - "arabic": "البسّ بجيب لقصر الملك هدايا كان يصيدها،", - "hebrew_transliteration": "אִל-בֶּסّ בִּיגִ'יבּ לַקַצֶר אִל-מַלִכּ הַדַאיַא כַּאן יְצִידְהַא,", - "hebrew": "החתול הביא לארמון המלך מתנות שצד,", - "expected_phonemes": "i l b e s s | b iː dʒ iː b | l a ʔ a sˤ e r | i l m a l i k | h a d aː j a | k aː n | j sˤ iː d h a" - }, - { - "voice_time_seconds": { - "start": 157.4, - "end": 159.8 - }, - "arabic": "مرّة أو مرّتين في الأسبوع،", - "hebrew_transliteration": "מַרַّה אַו מַרְתֵין פִי לְ-אֻסבּוּע,", - "hebrew": "פעם או פעמיים בשבוע,", - "expected_phonemes": "m a r r a | ʔ a w | m a r t eː n | f i | l ʔ u s b uː ʕ" - }, - { - "voice_time_seconds": { - "start": 159.8, - "end": 161.8 - }, - "arabic": "لمدّة شهرين تلاتة.", - "hebrew_transliteration": "לַמֻדֶּת שַהרֵין תַלַאתֶה.", - "hebrew": "במשך חודשיים-שלושה.", - "expected_phonemes": "l a m u d e t | ʃ a h r eː n | t a l aː t e" - }, - { - "voice_time_seconds": { - "start": 161.8, - "end": 165.8 - }, - "arabic": "وفي يوم، قال البسّ أبو الجّزمة لسيده:", - "hebrew_transliteration": "וּפִי יוֹם, קַאל לְ-בֶּסّ אַבּוּ גַּזמֶה לַסִידוֹ:", - "hebrew": "ויום אחד, אמר החתול במגפיים לאדונו:", - "expected_phonemes": "u f i | j oː m | ʔ aː l | l b e s s | ʔ a b u | g a z m e | l a s iː d o" - }, - { - "voice_time_seconds": { - "start": 165.8, - "end": 168.2 - }, - "arabic": "«إذا بتسمع نصيحتي،", - "hebrew_transliteration": "אִזַא בְּתִסמַע נַצִיחתִי,", - "hebrew": "אם תשמע לעצתי,", - "expected_phonemes": "ʔ i z a | b t i s m a ʕ | n a sˤ iː ħ t i" - }, - { - "voice_time_seconds": { - "start": 168.2, - "end": 170.6 - }, - "arabic": "أجرك راح يوصل لعندك.", - "hebrew_transliteration": "אַגְ'רַכּ רַח-יוּצַל לַעִנדַכּ.", - "hebrew": "שכרך יגיע אליך.", - "expected_phonemes": "ʔ a dʒ r a k | r a ħ j uː sˤ a l | l a ʕ i n d a k" - }, - { - "voice_time_seconds": { - "start": 170.6, - "end": 172.4 - }, - "arabic": "روح على النّهر،", - "hebrew_transliteration": "רוּח עַלַא נַּהֶר,", - "hebrew": "לך אל הנהר,", - "expected_phonemes": "r uː ħ | ʕ a l a | n a h e r" - }, - { - "voice_time_seconds": { - "start": 172.4, - "end": 176.0 - }, - "arabic": "اتحمّم في المنطقة إلّي راح أدلّك عليها.»", - "hebrew_transliteration": "תְחַמַّם פִי לְ-מַנטִקַה אִלִّי רַח-אַדִלַّכּ עַלֵיהַא.", - "hebrew": "תתרחץ במקום שאראה לך.", - "expected_phonemes": "t ħ a m m a m | f i | l m a n tˤ i ʔ a | ʔ i l l i | r a ħ ʔ a d i l l a k | ʕ a l eː h a" - }, - { - "voice_time_seconds": { - "start": 176.0, - "end": 179.0 - }, - "arabic": "الماركيز من كرابس عمل بالزّبط", - "hebrew_transliteration": "אִל-מַארְכִּיז מִן כַּרַאבַּס עִמֶל בִּ-זַּבֶּט", - "hebrew": "המרקיז מקרבס עשה בדיוק", - "expected_phonemes": "i l m aː r k iː z | m i n | k a r aː b a s | ʕ i m e l | b i z a b e tˤ" - }, - { - "voice_time_seconds": { - "start": 179.0, - "end": 181.2 - }, - "arabic": "متل ما حكاله البسّ،", - "hebrew_transliteration": "מִתֶל מַא חַכַא-לוֹ אִל-בֶּסّ,", - "hebrew": "כמו שאמר לו החתול,", - "expected_phonemes": "m i t e l | m a | ħ a k aː l o | i l b e s s" - }, - { - "voice_time_seconds": { - "start": 181.2, - "end": 185.0 - }, - "arabic": "وهوّ عم بتحمّم، مرق من هناك الملك،", - "hebrew_transliteration": "וּהֻוֶّ עַם-בִּיתחַמַّם, מַרַק מִן הֻנַאכּ אִל-מַלִכּ,", - "hebrew": "ובזמן שהתרחץ, עבר שם המלך,", - "expected_phonemes": "u h u w w e | ʕ a m b iː t ħ a m m a m | m a r a ʔ | m i n | h u n aː k | i l m a l i k" - }, - { - "voice_time_seconds": { - "start": 185.0, - "end": 187.8 - }, - "arabic": "راكب عربايته ومعاه بنته،", - "hebrew_transliteration": "רַאכֶּבּ עַרַבַּאיתוֹ וּמַעַאה בִּנתוֹ,", - "hebrew": "רוכב בכרכרתו ואיתו בתו,", - "expected_phonemes": "r aː k e b | ʕ a r a b aː j t o | u m a ʕ aː h | b i n t o" - }, - { - "voice_time_seconds": { - "start": 187.8, - "end": 190.4 - }, - "arabic": "أحلى أميرة في العالم.", - "hebrew_transliteration": "אַחלַא אַמִירַה פִי לְ-עַאלַם.", - "hebrew": "הנסיכה היפה בעולם.", - "expected_phonemes": "ʔ a ħ l a | ʔ a m iː r a | f i | l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 190.4, - "end": 194.8 - }, - "arabic": "ساعتها بلّش البسّ يموّي ويصرّخ:", - "hebrew_transliteration": "סַאעִתְהַא בַּלַّש לְ-בֶּסّ יְמַוִّי וִיצַרֶّח':", - "hebrew": "אז התחיל החתול ליילל ולצעוק:", - "expected_phonemes": "s aː ʕ i t h a | b a l l a ʃ | l b e s s | j m a w w i | w iː sˤ a r r e x" - }, - { - "voice_time_seconds": { - "start": 194.8, - "end": 197.4 - }, - "arabic": "«ساعدوني! ساعدوني!", - "hebrew_transliteration": "סַאעְדוּנִי! סַאעְדוּנִי!", - "hebrew": "הצילו! הצילו!", - "expected_phonemes": "s aː ʕ d uː n i | s aː ʕ d uː n i" - }, - { - "voice_time_seconds": { - "start": 197.4, - "end": 200.6 - }, - "arabic": "سيدي الماركيز من كرابس عم بغرق!»", - "hebrew_transliteration": "סִידִי לְ-מַארְכִּיז מִן כַּרַאבַּס עַם-בִּיעְ'רַק!", - "hebrew": "אדוני המרקיז מקרבס טובע!", - "expected_phonemes": "s iː d i | l m aː r k iː z | m i n | k a r aː b a s | ʕ a m b iː ɣ r a ʔ" - }, - { - "voice_time_seconds": { - "start": 200.6, - "end": 203.6 - }, - "arabic": "الملك سمع لصراخ العالي", - "hebrew_transliteration": "אִל-מַלִכּ סִמֶע לֶ-צְרַאחַ' אִל-עַאלִי", - "hebrew": "המלך שמע את הצעקות הרמות", - "expected_phonemes": "i l m a l i k | s i m e ʕ | l e sˤ r aː x a | i l ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 203.6, - "end": 207.0 - }, - "arabic": "وأمر خدمه ينقذوا الماركيز.", - "hebrew_transliteration": "וּאַמַר חַ'דַמוֹ יִנקְדוּ לְ-מַארְכִּיז.", - "hebrew": "וציווה על משרתיו להציל את המרקיז.", - "expected_phonemes": "u ʔ a m a r | x a d a m o | j i n ʔ d u | l m aː r k iː z" - }, - { - "voice_time_seconds": { - "start": 207.0, - "end": 208.8 - }, - "arabic": "في اللّحظة اللّي كانوا الخدم", - "hebrew_transliteration": "פִי לַחזַה אִלִّי כַּאנוּ לְ-חַ'דַם", - "hebrew": "ברגע שהמשרתים", - "expected_phonemes": "f i | l a ħ ðˤ a | ʔ i l l i | k aː n u | l x a d a m" - }, - { - "voice_time_seconds": { - "start": 208.8, - "end": 212.6 - }, - "arabic": "عم بسحبوا الماركيز من كرابس من النّهر،", - "hebrew_transliteration": "עַם-בִּיסחַבּוּ לְ-מַארְכִּיז מִן כַּרַאבַּס מִן נַּהֶר,", - "hebrew": "משו את המרקיז מקרבס מהנהר,", - "expected_phonemes": "ʕ a m b iː s ħ a b u | l m aː r k iː z | m i n | k a r aː b a s | m i n | n a h e r" - }, - { - "voice_time_seconds": { - "start": 212.6, - "end": 215.4 - }, - "arabic": "قرّب البسّ أبو الجّزمة من العرباي", - "hebrew_transliteration": "קַרַّבּ אִל-בֶּסّ אַבּוּ גַּזמֶה מִן לְ-עַרַבַּאי", - "hebrew": "החתול במגפיים התקרב לכרכרה", - "expected_phonemes": "ʔ a r r a b | i l b e s s | ʔ a b u | g a z m e | m i n | l ʕ a r a b aː i" - }, - { - "voice_time_seconds": { - "start": 215.4, - "end": 218.6 - }, - "arabic": "وحكى للملك إنّه الحراميّة معاهم", - "hebrew_transliteration": "וּחַכַא לַל-מַלִכּ אִנּוֹ אִל-חַרַאמִיֶּה מַעַאהֹם", - "hebrew": "ואמר למלך שהגנבים לקחו איתם", - "expected_phonemes": "u ħ a k a | l a l m a l i k | ʔ i n o | i l ħ a r aː m i j e | m a ʕ aː h o m" - }, - { - "voice_time_seconds": { - "start": 218.6, - "end": 220.6 - }, - "arabic": "أواعي سيده الماركيز،", - "hebrew_transliteration": "אַוַאעִי סִידוֹ אִל-מַארְכִּיז,", - "hebrew": "את בגדי אדונו המרקיז,", - "expected_phonemes": "ʔ a w aː ʕ i | s iː d o | i l m aː r k iː z" - }, - { - "voice_time_seconds": { - "start": 220.6, - "end": 223.6 - }, - "arabic": "مع إنّه كان هوّ، البسّ،", - "hebrew_transliteration": "מַע אִנּוֹ כַּאן הֻוֶّ, אִל-בֶּסّ,", - "hebrew": "אף על פי שהוא, החתול,", - "expected_phonemes": "m a ʕ | ʔ i n o | k aː n | h u w w e | i l b e s s" - }, - { - "voice_time_seconds": { - "start": 223.6, - "end": 226.4 - }, - "arabic": "إلّي مخبّي الأواعي بين النّباتات.", - "hebrew_transliteration": "אִלִّי מְחַבִּי לְ-אַוַאעִי בֵּין נַּבַּאתַאת.", - "hebrew": "הוא שהחביא את הבגדים בין הצמחים.", - "expected_phonemes": "ʔ i l l i | m ħ a b i | l ʔ a w aː ʕ i | b eː n | n a b aː t aː t" - }, - { - "voice_time_seconds": { - "start": 226.4, - "end": 228.0 - }, - "arabic": "الملك أمر خدمه", - "hebrew_transliteration": "אִל-מַלִכּ אַמַר חַ'דַמוֹ", - "hebrew": "המלך ציווה על משרתיו", - "expected_phonemes": "i l m a l i k | ʔ a m a r | x a d a m o" - }, - { - "voice_time_seconds": { - "start": 228.0, - "end": 230.4 - }, - "arabic": "يلبّسوا الماركيز أواعي فخمة", - "hebrew_transliteration": "יְלַבְּסוּ אִל-מַארְכִּיז אַוַאעִי פַחְ'מֶה", - "hebrew": "להלביש את המרקיז בגדים מפוארים", - "expected_phonemes": "j l a b s u | i l m aː r k iː z | ʔ a w aː ʕ i | f a x m e" - }, - { - "voice_time_seconds": { - "start": 230.4, - "end": 233.4 - }, - "arabic": "مناسبة لماركيز محترم.", - "hebrew_transliteration": "מְנַאסְבֶּה לַמַארְכִּיז מֻחתַרַם.", - "hebrew": "ההולמים מרקיז מכובד.", - "expected_phonemes": "m n aː s b e | l a m aː r k iː z | m u ħ t a r a m" - }, - { - "voice_time_seconds": { - "start": 233.4, - "end": 237.0 - }, - "arabic": "بعد ما لبس، بيّن كتير مرتّب ووسيم،", - "hebrew_transliteration": "בַּעֶד מַא לִבֶּס, בַּיַّן כְּתִיר מְרַתַّבּ וּוַסִים,", - "hebrew": "אחרי שהתלבש, נראה מסודר ונאה מאוד,", - "expected_phonemes": "b a ʕ e d | m a | l i b e s | b a j j a n | k t iː r | m r a t t a b | u w a s iː m" - }, - { - "voice_time_seconds": { - "start": 237.0, - "end": 240.2 - }, - "arabic": "لدرجة إنّه الملك إنبهر فيه كتير،", - "hebrew_transliteration": "לַדַרַגֶ'ה אִנּוֹ לְ-מַלִכּ אִנבַּהַר פִיה כְּתִיר,", - "hebrew": "עד כדי כך שהמלך התפעל ממנו מאוד,", - "expected_phonemes": "l a d a r a dʒ e | ʔ i n o | l m a l i k | ʔ i n b a h a r | f iː h | k t iː r" - }, - { - "voice_time_seconds": { - "start": 240.2, - "end": 244.2 - }, - "arabic": "وطلب منّه ييجي معه في العرباي الملكيّة.", - "hebrew_transliteration": "וּטַלַבּ מִנּוֹ יִיגִ'י מַעוֹ פִי לְ-עַרַבַּאי לֶ-מַלַכִּיֶّה.", - "hebrew": "וביקש ממנו לבוא איתו בכרכרה המלכותית.", - "expected_phonemes": "u tˤ a l a b | m i n o | j iː dʒ i | m a ʕ o | f i | l ʕ a r a b aː i | l e m a l a k i j j e" - }, - { - "voice_time_seconds": { - "start": 244.2, - "end": 247.2 - }, - "arabic": "الماركيز كان كتير وسيم في الأواعي،", - "hebrew_transliteration": "אִל-מַארְכִּיז כַּאן כְּתִיר וַסִים פִי לְ-אַוַאעִי,", - "hebrew": "המרקיז היה נאה מאוד בבגדים,", - "expected_phonemes": "i l m aː r k iː z | k aː n | k t iː r | w a s iː m | f i | l ʔ a w aː ʕ i" - }, - { - "voice_time_seconds": { - "start": 247.2, - "end": 251.2 - }, - "arabic": "حتّى إنّه كمان بنت الملك أعجبت فيه.", - "hebrew_transliteration": "חַתַّא אִנּוֹ כַּמַאן בִּנת לְ-מַלִכּ אֻעגְ'בַּת פִיה.", - "hebrew": "עד שגם בת המלך התאהבה בו.", - "expected_phonemes": "ħ a t t a | ʔ i n o | k a m aː n | b i n t | l m a l i k | ʔ u ʕ dʒ b a t | f iː h" - }, - { - "voice_time_seconds": { - "start": 251.2, - "end": 255.0 - }, - "arabic": "البسّ أبو الجّزمة ركض بسرعة قبل العرباي،", - "hebrew_transliteration": "אִל-בֶּסّ אַבּוּ גַּזמֶה רַכַד' בְּסֻרעַה קַבֶּל לְ-עַרַבַּאי,", - "hebrew": "החתול במגפיים רץ מהר לפני הכרכרה,", - "expected_phonemes": "i l b e s s | ʔ a b u | g a z m e | r a k a dˤ | b s u r ʕ a | ʔ a b e l | l ʕ a r a b aː i" - }, - { - "voice_time_seconds": { - "start": 255.0, - "end": 257.6 - }, - "arabic": "فخور بنفسه لأبعد الحدود،", - "hebrew_transliteration": "פַחוּר בְּנַפסוֹ לַאַבְּעַד אִל-חֻדוּד,", - "hebrew": "גאה בעצמו עד בלי גבול,", - "expected_phonemes": "f a ħ uː r | b n a f s o | l a ʔ a b ʕ a d | i l ħ u d uː d" - }, - { - "voice_time_seconds": { - "start": 257.6, - "end": 259.6 - }, - "arabic": "وصار يعطي أوامر للنّاس", - "hebrew_transliteration": "וּצַאר יַעטִי אַוַאמֶר לַ-נַּאס", - "hebrew": "והתחיל לתת פקודות לאנשים", - "expected_phonemes": "u sˤ aː r | j a ʕ tˤ i | ʔ a w aː m e r | l a n aː s" - }, - { - "voice_time_seconds": { - "start": 259.6, - "end": 262.4 - }, - "arabic": "إلّي شافها عم تشتغل على الطّريق.", - "hebrew_transliteration": "אִלִّי שַאפְהַא עַם-תִשתְעֶ'ל עַלַא טַּרִיק.", - "hebrew": "שראה עובדים לאורך הדרך.", - "expected_phonemes": "ʔ i l l i | ʃ aː f h a | ʕ a m t i ʃ t ɣ e l | ʕ a l a | tˤ a r iː ʔ" - }, - { - "voice_time_seconds": { - "start": 262.4, - "end": 265.4 - }, - "arabic": "لمّا الملك وعربايته مرقوا من هناك،", - "hebrew_transliteration": "לַמַّא לְ-מַלִכּ וּעַרַבַּאיתוֹ מַרַקוּ מִן הֻנַאכּ,", - "hebrew": "כשהמלך וכרכרתו עברו משם,", - "expected_phonemes": "l a m m a | l m a l i k | u ʕ a r a b aː j t o | m a r a ʔ u | m i n | h u n aː k" - }, - { - "voice_time_seconds": { - "start": 265.4, - "end": 269.0 - }, - "arabic": "شاف الملك عمّال عمتشتغل في المرج،", - "hebrew_transliteration": "שַאף אִל-מַלִכּ עֻמַّאל עַם-תִשתְעֶ'ל פִי לְ-מַרְג',", - "hebrew": "המלך ראה פועלים עובדים באחו,", - "expected_phonemes": "ʃ aː f | i l m a l i k | ʕ u m m aː l | ʕ a m t i ʃ t ɣ e l | f i | l m a r dʒ" - }, - { - "voice_time_seconds": { - "start": 269.0, - "end": 272.4 - }, - "arabic": "فسألهم: «لمين هدا الحقل؟»", - "hebrew_transliteration": "פַ-סַאַלְהֹם: לַמִין הַאדַא לְ-חַקֶל?", - "hebrew": "אז שאל אותם: של מי השדה הזה?", - "expected_phonemes": "f a s a ʔ a l h o m | l a m iː n | h aː d a | l ħ a ʔ e l" - }, - { - "voice_time_seconds": { - "start": 272.4, - "end": 274.4 - }, - "arabic": "ردّوا عليه وجاوبوه", - "hebrew_transliteration": "רַדّוּ עַלֵיה וּגַ'אוַבּוּה", - "hebrew": "הם השיבו לו וענו", - "expected_phonemes": "r a d d u | ʕ a l eː h | u dʒ aː w a b uː h" - }, - { - "voice_time_seconds": { - "start": 274.4, - "end": 277.6 - }, - "arabic": "متل ما أمرهم البسّ وقالوله:", - "hebrew_transliteration": "מִתֶל מַא אַמַרְהֹם אִל-בֶּסّ וּקַאלוּ-לוֹ:", - "hebrew": "כמו שציווה עליהם החתול ואמרו לו:", - "expected_phonemes": "m i t e l | m a | ʔ a m a r h o m | i l b e s s | u ʔ aː l uː l o" - }, - { - "voice_time_seconds": { - "start": 277.6, - "end": 281.4 - }, - "arabic": "«لسيدنا، الماركيز من كرابس.»", - "hebrew_transliteration": "לַסִידְנַא, אִל-מַארְכִּיז מִן כַּרַאבַּס.", - "hebrew": "של אדוננו, המרקיז מקרבס.", - "expected_phonemes": "l a s iː d n a | i l m aː r k iː z | m i n | k a r aː b a s" - }, - { - "voice_time_seconds": { - "start": 281.4, - "end": 283.6 - }, - "arabic": "الملك قال للماركيز:", - "hebrew_transliteration": "אִל-מַלִכּ קַאל לַל-מַארְכִּיז:", - "hebrew": "המלך אמר למרקיז:", - "expected_phonemes": "i l m a l i k | ʔ aː l | l a l m aː r k iː z" - }, - { - "voice_time_seconds": { - "start": 283.6, - "end": 286.4 - }, - "arabic": "«عندك شقفة أرض ممتازة.»", - "hebrew_transliteration": "עִנדַכּ שֻקפֶת אַרְד' מֻמתַאזֶה.", - "hebrew": "יש לך חלקת אדמה מצוינת.", - "expected_phonemes": "ʕ i n d a k | ʃ u ʔ f e t | ʔ a r dˤ | m u m t aː z e" - }, - { - "voice_time_seconds": { - "start": 286.4, - "end": 289.4 - }, - "arabic": "«إنت شديد الملاحظة سيدنا الملك،", - "hebrew_transliteration": "אִנתֶ שַדִיד לֶ-מֻלַאחַזֶה סִידְנַא אִל-מַלִכּ,", - "hebrew": "אתה חד הבחנה, אדוננו המלך,", - "expected_phonemes": "ʔ i n t e | ʃ a d iː d | l e m u l aː ħ a z e | s iː d n a | i l m a l i k" - }, - { - "voice_time_seconds": { - "start": 289.4, - "end": 294.0 - }, - "arabic": "هاي الأرض دايما خصبة وولا مرّة خذلتني.»", - "hebrew_transliteration": "הַאי לְ-אַרְד' דַאיְמַן חַ'סְבֶּה וּוַלַא מַרַّה חַ'דַלַתנִי.", - "hebrew": "האדמה הזאת תמיד פורייה ואף פעם לא אכזבה אותי.", - "expected_phonemes": "h aː i | l ʔ a r dˤ | d aː j m a n | x a s b e | u w a l a | m a r r a | x a d a l a t n i" - }, - { - "voice_time_seconds": { - "start": 294.0, - "end": 296.8 - }, - "arabic": "العرباي مرقت بكمان حقل،", - "hebrew_transliteration": "אִל-עַרַבַּאי מַרקַת בְּכַּמַאן חַקֶל,", - "hebrew": "הכרכרה עברה בעוד שדה,", - "expected_phonemes": "i l ʕ a r a b aː i | m a r ʔ a t | b k a m aː n | ħ a ʔ e l" - }, - { - "voice_time_seconds": { - "start": 296.8, - "end": 301.0 - }, - "arabic": "العمّال فيه كانوا عم بشتغلوا بهمّة ونشاط.", - "hebrew_transliteration": "אִל-עֻמַّאל פִיה כַּאנוּ עַם-בִּישתְעְ'לוּ בְּהִמֶّה וּנַשַאט.", - "hebrew": "הפועלים בו עבדו בחריצות ובמרץ.", - "expected_phonemes": "i l ʕ u m m aː l | f iː h | k aː n u | ʕ a m b iː ʃ t ɣ l u | b h i m m e | u n a ʃ aː tˤ" - }, - { - "voice_time_seconds": { - "start": 301.0, - "end": 304.6 - }, - "arabic": "لمّا الملك سألهم لمين هدا الحقل،", - "hebrew_transliteration": "לַמַّא לְ-מַלִכּ סַאַלְהֹם לַמִין הַאדַא לְ-חַקֶל,", - "hebrew": "כשהמלך שאל אותם של מי השדה הזה,", - "expected_phonemes": "l a m m a | l m a l i k | s a ʔ a l h o m | l a m iː n | h aː d a | l ħ a ʔ e l" - }, - { - "voice_time_seconds": { - "start": 304.6, - "end": 309.0 - }, - "arabic": "جاوبوه: «لسيدنا الماركيز من كرابس.»", - "hebrew_transliteration": "גַ'אוַבּוּה: לַסִידְנַא אִל-מַארְכִּיז מִן כַּרַאבַּס.", - "hebrew": "ענו לו: של אדוננו המרקיז מקרבס.", - "expected_phonemes": "dʒ aː w a b uː h | l a s iː d n a | i l m aː r k iː z | m i n | k a r aː b a s" - }, - { - "voice_time_seconds": { - "start": 309.0, - "end": 312.4 - }, - "arabic": "البسّ أبو الجّزمة ركض قبل العرباي", - "hebrew_transliteration": "אִל-בֶּסّ אַבּוּ גַּזמֶה רַכַד' קַבֶּל לְ-עַרַבַּאי", - "hebrew": "החתול במגפיים רץ לפני הכרכרה", - "expected_phonemes": "i l b e s s | ʔ a b u | g a z m e | r a k a dˤ | ʔ a b e l | l ʕ a r a b aː i" - }, - { - "voice_time_seconds": { - "start": 312.4, - "end": 316.8 - }, - "arabic": "ووصل لقلعة كبيرة، صاحبها عملاق ضخم.", - "hebrew_transliteration": "וּוִצֶל לַקַלעַה כְּבִּירֶה, צַאחֶבְּהַא עִמלַאק דַ'חְ'ם.", - "hebrew": "והגיע לטירה גדולה, שבעליה ענק עצום.", - "expected_phonemes": "u w i sˤ e l | l a ʔ a l ʕ a | k b iː r e | sˤ aː ħ e b h a | ʕ i m l aː ʔ | dˤ a x m" - }, - { - "voice_time_seconds": { - "start": 316.8, - "end": 319.4 - }, - "arabic": "هدا العملاق كان غني كتير،", - "hebrew_transliteration": "הַאדַא לְ-עִמלַאק כַּאן עַ'נִי כְּתִיר,", - "hebrew": "הענק הזה היה עשיר מאוד,", - "expected_phonemes": "h aː d a | l ʕ i m l aː ʔ | k aː n | ɣ a n i | k t iː r" - }, - { - "voice_time_seconds": { - "start": 319.4, - "end": 321.6 - }, - "arabic": "وكلّ الأراضي إلّي مرقت منها", - "hebrew_transliteration": "וּכֻּלّ אִל-אַרַאדִי אִלִّי מַרקַת מִנְהַא", - "hebrew": "וכל האדמות שעברה בהן", - "expected_phonemes": "u k u l l | i l ʔ a r aː d i | ʔ i l l i | m a r ʔ a t | m i n h a" - }, - { - "voice_time_seconds": { - "start": 321.6, - "end": 326.0 - }, - "arabic": "عرباية الملك هداك النّهار كانت ملكه.", - "hebrew_transliteration": "עַרַבַּאיֶת לְ-מַלִכּ הַדַאכּ נַּהַאר כַּאנַת מִלכּוֹ.", - "hebrew": "כרכרת המלך באותו יום היו שלו.", - "expected_phonemes": "ʕ a r a b aː j e t | l m a l i k | h a d aː k | n a h aː r | k aː n a t | m i l k o" - }, - { - "voice_time_seconds": { - "start": 326.0, - "end": 328.4 - }, - "arabic": "العملاق استقبل البسّ أبو الجّزمة", - "hebrew_transliteration": "אִל-עִמלַאק אִסתַקבַּל אִל-בֶּסّ אַבּוּ גַּזמֶה", - "hebrew": "הענק קיבל את החתול במגפיים", - "expected_phonemes": "i l ʕ i m l aː ʔ | ʔ i s t a ʔ b a l | i l b e s s | ʔ a b u | g a z m e" - }, - { - "voice_time_seconds": { - "start": 328.4, - "end": 330.0 - }, - "arabic": "بملقى حلو،", - "hebrew_transliteration": "בְּמַלקַא חִלוּ,", - "hebrew": "בקבלת פנים יפה,", - "expected_phonemes": "b m a l ʔ a | ħ i l u" - }, - { - "voice_time_seconds": { - "start": 330.0, - "end": 333.2 - }, - "arabic": "والمحادثة بينهم كانت هادية،", - "hebrew_transliteration": "וִל-מֻחַאדַתֶ'ה בֵּינְהֹם כַּאנַת הַאדְיֶה,", - "hebrew": "והשיחה ביניהם הייתה שקטה,", - "expected_phonemes": "w i l m u ħ aː d a t e | b eː n h o m | k aː n a t | h aː d j e" - }, - { - "voice_time_seconds": { - "start": 333.2, - "end": 336.8 - }, - "arabic": "متل ما بتحكي المخلوقات الأسطوريّة.", - "hebrew_transliteration": "מִתֶל מַא בְּתִחכִּי לְ-מַחְ'לוּקַאת אִל-אֻסטוּרִיֶّה.", - "hebrew": "כמו שמדברים יצורים אגדתיים.", - "expected_phonemes": "m i t e l | m a | b t i ħ k i | l m a x l uː ʔ aː t | i l ʔ u s tˤ uː r i j j e" - }, - { - "voice_time_seconds": { - "start": 336.8, - "end": 339.2 - }, - "arabic": "البسّ أبو الجّزمة قال:", - "hebrew_transliteration": "אִל-בֶּסّ אַבּוּ גַּזמֶה קַאל:", - "hebrew": "החתול במגפיים אמר:", - "expected_phonemes": "i l b e s s | ʔ a b u | g a z m e | ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 339.2, - "end": 341.4 - }, - "arabic": "«حكولي إنّه عندك القدرة", - "hebrew_transliteration": "חַכּוּ-לִי אִנּוֹ עִנדַכּ אִל-קֻדרַה", - "hebrew": "סיפרו לי שיש לך את היכולת", - "expected_phonemes": "ħ a k uː l i | ʔ i n o | ʕ i n d a k | i l ʔ u d r a" - }, - { - "voice_time_seconds": { - "start": 341.4, - "end": 346.4 - }, - "arabic": "تغيّر شكلك لأي حيوان بدّك... مزبوط؟»", - "hebrew_transliteration": "תְעַ'יֶّר שַכְּלַכּ לַאַיّ חַיַוַאן בַּדַّכּ... מַזבּוּט?", - "hebrew": "לשנות את צורתך לכל חיה שתרצה... נכון?", - "expected_phonemes": "t ɣ a j j e r | ʃ a k l a k | l a ʔ a j j | ħ a j a w aː n | b a d d a k | m a z b uː tˤ" - }, - { - "voice_time_seconds": { - "start": 346.4, - "end": 349.0 - }, - "arabic": "العملاق انبسط يحكي للبسّ أبو الجّزمة", - "hebrew_transliteration": "אִל-עִמלַאק אִנבַּסַט יִחכִּי לַל-בֶּסّ אַבּוּ גַּזמֶה", - "hebrew": "הענק שמח לספר לחתול במגפיים", - "expected_phonemes": "i l ʕ i m l aː ʔ | ʔ i n b a s a tˤ | j i ħ k i | l a l b e s s | ʔ a b u | g a z m e" - }, - { - "voice_time_seconds": { - "start": 349.0, - "end": 354.0 - }, - "arabic": "عن مقدرته الخارقة، إلّي كان فخور فيها كتير.", - "hebrew_transliteration": "עַן מַקדִרתוֹ לְ-חַ'ארְקַה, אִלִّי כַּאן פַחוּר פִיהַא כְּתִיר.", - "hebrew": "על יכולתו העל-טבעית, שבה התגאה מאוד.", - "expected_phonemes": "ʕ a n | m a ʔ d i r t o | l x aː r ʔ a | ʔ i l l i | k aː n | f a ħ uː r | f iː h a | k t iː r" - }, - { - "voice_time_seconds": { - "start": 354.0, - "end": 355.8 - }, - "arabic": "البسّ كمّل وسأل:", - "hebrew_transliteration": "אִל-בֶּסّ כַּמַّל וּסַאַל:", - "hebrew": "החתול המשיך ושאל:", - "expected_phonemes": "i l b e s s | k a m m a l | u s a ʔ a l" - }, - { - "voice_time_seconds": { - "start": 355.8, - "end": 361.0 - }, - "arabic": "«عملاق متلك بقدر يغيّر شكله لحيوان صغير؟", - "hebrew_transliteration": "עִמלַאק מִתלַכּ בִּיקדַר יְעַ'יֶّר שַכְּלוֹ לַחַיַוַאן זְעִ'יר?", - "hebrew": "ענק כמוך יכול לשנות את צורתו לחיה קטנה?", - "expected_phonemes": "ʕ i m l aː ʔ | m i t l a k | b iː ʔ d a r | j ɣ a j j e r | ʃ a k l o | l a ħ a j a w aː n | z ɣ iː r" - }, - { - "voice_time_seconds": { - "start": 361.0, - "end": 364.0 - }, - "arabic": "مثلا لبسّة؟»", - "hebrew_transliteration": "מַתַלַן לַבִּסֶّה?", - "hebrew": "למשל לחתולה?", - "expected_phonemes": "m a t a l a n | l a b i s s e" - }, - { - "voice_time_seconds": { - "start": 364.0, - "end": 367.8 - }, - "arabic": "العملاق عمل حاله شاعر بالإهانه وردّ:", - "hebrew_transliteration": "אִל-עִמלַאק עִמֶל חַאלוֹ שַאעֶר בִּל-אִהַאנֶה וּרַדّ:", - "hebrew": "הענק העמיד פנים שנעלב והשיב:", - "expected_phonemes": "i l ʕ i m l aː ʔ | ʕ i m e l | ħ aː l o | ʃ aː ʕ e r | b i l ʔ i h aː n e | u r a d d" - }, - { - "voice_time_seconds": { - "start": 367.8, - "end": 372.8 - }, - "arabic": "«طبعا، حتّى لحيوان أصغر، مثل الفار!»", - "hebrew_transliteration": "טַבְּעַן, חַתַּא לַחַיַוַאן אַזְעַ'ר, מִתֶל לְ-פַאר!", - "hebrew": "כמובן, אפילו לחיה קטנה יותר, כמו עכבר!", - "expected_phonemes": "tˤ a b ʕ a n | ħ a t a | l a ħ a j a w aː n | ʔ a z ɣ a r | m i t e l | l f aː r" - }, - { - "voice_time_seconds": { - "start": 372.8, - "end": 377.6 - }, - "arabic": "ومرّة واحدة تحوّل العملاق لفار صغير", - "hebrew_transliteration": "וּמַרַّה וַאחדֶה תְחַוַّל אִל-עִמלַאק לַפַאר זְעִ'יר", - "hebrew": "ובבת אחת הפך הענק לעכבר קטן", - "expected_phonemes": "u m a r r a | w aː ħ d e | t ħ a w w a l | i l ʕ i m l aː ʔ | l a f aː r | z ɣ iː r" - }, - { - "voice_time_seconds": { - "start": 377.6, - "end": 380.4 - }, - "arabic": "إلّي بلّش ينطنط في الغرفة.", - "hebrew_transliteration": "אִלִّי בַּלַّש יְנַטנֶט פִי לְ-עֻ'רְפֶה.", - "hebrew": "שהתחיל לקפץ בחדר.", - "expected_phonemes": "ʔ i l l i | b a l l a ʃ | j n a tˤ n e tˤ | f i | l ɣ u r f e" - }, - { - "voice_time_seconds": { - "start": 380.4, - "end": 383.4 - }, - "arabic": "ودغري نطّ البسّ على العملاق", - "hebrew_transliteration": "וּדֻעְ'רִי נַטّ אִל-בֶּסّ עַלַא לְ-עִמלַאק", - "hebrew": "ומיד קפץ החתול על הענק", - "expected_phonemes": "u d u ɣ r i | n a tˤ tˤ | i l b e s s | ʕ a l a | l ʕ i m l aː ʔ" - }, - { - "voice_time_seconds": { - "start": 383.4, - "end": 387.6 - }, - "arabic": "إلّي كان على هيئة فار وبلعه كلّه.", - "hebrew_transliteration": "אִלִّי כַּאן עַלַא הַיְאֶת פַאר וּבַּלַעוֹ כֻּלّוֹ.", - "hebrew": "שהיה בדמות עכבר ובלע אותו כולו.", - "expected_phonemes": "ʔ i l l i | k aː n | ʕ a l a | h a j ʔ e t | f aː r | u b a l a ʕ o | k u l l o" - }, - { - "voice_time_seconds": { - "start": 387.6, - "end": 389.4 - }, - "arabic": "خلال هدا الوقت", - "hebrew_transliteration": "חִ'לַאל הַאדַא לְ-וַקֶת", - "hebrew": "בינתיים", - "expected_phonemes": "x i l aː l | h aː d a | l w a ʔ e t" - }, - { - "voice_time_seconds": { - "start": 389.4, - "end": 393.2 - }, - "arabic": "وصلت عرباية الملك لقلعة العملاق.", - "hebrew_transliteration": "וִצלַת עַרַבַּאיֶת לְ-מַלִכּ לַקַלעֶת אִל-עִמלַאק.", - "hebrew": "כרכרת המלך הגיעה לטירת הענק.", - "expected_phonemes": "w i sˤ l a t | ʕ a r a b aː j e t | l m a l i k | l a ʔ a l ʕ e t | i l ʕ i m l aː ʔ" - }, - { - "voice_time_seconds": { - "start": 393.2, - "end": 397.6 - }, - "arabic": "الملك اندهش من كبر القلعة وجمالها", - "hebrew_transliteration": "אִל-מַלִכּ אִנדַהַש מִן כִּבַּר לְ-קַלעַה וּגַ'מַאלְהַא", - "hebrew": "המלך נדהם מגודל הטירה ומיופייה", - "expected_phonemes": "i l m a l i k | ʔ i n d a h a ʃ | m i n | k i b a r | l ʔ a l ʕ a | u dʒ a m aː l h a" - }, - { - "voice_time_seconds": { - "start": 397.6, - "end": 399.2 - }, - "arabic": "وطلب يزورها.", - "hebrew_transliteration": "וּטַלַבּ יְזוּרְהַא.", - "hebrew": "וביקש לבקר בה.", - "expected_phonemes": "u tˤ a l a b | j z uː r h a" - }, - { - "voice_time_seconds": { - "start": 399.2, - "end": 404.2 - }, - "arabic": "لهيك أمر خدمه يروحوا لعند البوّابة.", - "hebrew_transliteration": "לַהֵיכּ אַמַר חַ'דַמוֹ יְרוּחוּ לַעִנד לְ-בַּוַّאבֶּה.", - "hebrew": "לכן ציווה על משרתיו ללכת אל השער.", - "expected_phonemes": "l a h eː k | ʔ a m a r | x a d a m o | j r uː ħ u | l a ʕ i n d | l b a w w aː b e" - }, - { - "voice_time_seconds": { - "start": 404.2, - "end": 406.2 - }, - "arabic": "لمّا سمع البسّ أبو الجّزمة", - "hebrew_transliteration": "לַמַّא סִמֶע אִל-בֶּסّ אַבּוּ גַּזמֶה", - "hebrew": "כששמע החתול במגפיים", - "expected_phonemes": "l a m m a | s i m e ʕ | i l b e s s | ʔ a b u | g a z m e" - }, - { - "voice_time_seconds": { - "start": 406.2, - "end": 408.6 - }, - "arabic": "صوت عجلات العرباي،", - "hebrew_transliteration": "צוֹת עַגַלַאת אִל-עַרַבַּאי,", - "hebrew": "את קול גלגלי הכרכרה,", - "expected_phonemes": "sˤ oː t | ʕ a g a l aː t | i l ʕ a r a b aː i" - }, - { - "voice_time_seconds": { - "start": 408.6, - "end": 411.6 - }, - "arabic": "أسرع لجهتهم وقال:", - "hebrew_transliteration": "אַסרַע לַגִ'הִתְהֹם וּקַאל:", - "hebrew": "מיהר לעברם ואמר:", - "expected_phonemes": "ʔ a s r a ʕ | l a dʒ i h i t h o m | u ʔ aː l" - }, - { - "voice_time_seconds": { - "start": 411.6, - "end": 414.6 - }, - "arabic": "«هاد شرف كبير إلنا إذا الملك", - "hebrew_transliteration": "הַאד שַרַף כְּבִּיר אִלְנַא אִזַא לְ-מַלִכּ", - "hebrew": "זה כבוד גדול לנו אם המלך", - "expected_phonemes": "h aː d | ʃ a r a f | k b iː r | i l n a | ʔ i z a | l m a l i k" - }, - { - "voice_time_seconds": { - "start": 414.6, - "end": 417.4 - }, - "arabic": "بزور قلعة الماركيز من كرابس!»", - "hebrew_transliteration": "בִּיזוּר קַלעֶת אִל-מַארְכִּיז מִן כַּרַאבַּס!", - "hebrew": "יבקר בטירת המרקיז מקרבס!", - "expected_phonemes": "b iː z uː r | ʔ a l ʕ e t | i l m aː r k iː z | m i n | k a r aː b a s" - }, - { - "voice_time_seconds": { - "start": 417.4, - "end": 418.8 - }, - "arabic": "الملك سأل:", - "hebrew_transliteration": "אִל-מַלִכּ סַאַל:", - "hebrew": "המלך שאל:", - "expected_phonemes": "i l m a l i k | s a ʔ a l" - }, - { - "voice_time_seconds": { - "start": 418.8, - "end": 421.4 - }, - "arabic": "«كمان هاي القلعة الرّائعة إلك،", - "hebrew_transliteration": "כַּמַאן הַאי אִל-קַלעַה רַּאאִעַה אִלַכּ,", - "hebrew": "גם הטירה הנפלאה הזאת שלך,", - "expected_phonemes": "k a m aː n | h aː i | i l ʔ a l ʕ a | r aː ʔ i ʕ a | ʔ i l a k" - }, - { - "voice_time_seconds": { - "start": 421.4, - "end": 423.8 - }, - "arabic": "سيدي الماركيز من كرابس؟", - "hebrew_transliteration": "סִידִי לְ-מַארְכִּיז מִן כַּרַאבַּס?", - "hebrew": "אדוני המרקיז מקרבס?", - "expected_phonemes": "s iː d i | l m aː r k iː z | m i n | k a r aː b a s" - }, - { - "voice_time_seconds": { - "start": 423.8, - "end": 425.6 - }, - "arabic": "منقدر ندخل؟»", - "hebrew_transliteration": "מִנִקדַר נֻדחֹל?", - "hebrew": "אפשר להיכנס?", - "expected_phonemes": "m i n i ʔ d a r | n u d ħ o l" - }, - { - "voice_time_seconds": { - "start": 425.6, - "end": 429.8 - }, - "arabic": "الماركيز رافق الملك والأميرة للقلعة،", - "hebrew_transliteration": "אִל-מַארְכִּיז רַאפַק אִל-מַלִכּ וִל-אַמִירַה לַל-קַלעַה,", - "hebrew": "המרקיז ליווה את המלך ואת הנסיכה לטירה,", - "expected_phonemes": "i l m aː r k iː z | r aː f a ʔ | i l m a l i k | w i l ʔ a m iː r a | l a l ʔ a l ʕ a" - }, - { - "voice_time_seconds": { - "start": 429.8, - "end": 433.6 - }, - "arabic": "وهناك في قاعة المدخل الواسعة", - "hebrew_transliteration": "וּהֻנַאכּ פִי קַאעֶת אִל-מַדחַל אִל-וַאסְעַה", - "hebrew": "ושם, באולם הכניסה הרחב,", - "expected_phonemes": "u h u n aː k | f i | ʔ aː ʕ e t | i l m a d ħ a l | i l w aː s ʕ a" - }, - { - "voice_time_seconds": { - "start": 433.6, - "end": 436.4 - }, - "arabic": "لاقوا وليمة كبيرة جاهزة،", - "hebrew_transliteration": "לַאקוּ וַלִימֶה כְּבִּירֶה גַ'אהְזֶה,", - "hebrew": "מצאו משתה גדול מוכן,", - "expected_phonemes": "l aː ʔ u | w a l iː m e | k b iː r e | dʒ aː h z e" - }, - { - "voice_time_seconds": { - "start": 436.4, - "end": 440.8 - }, - "arabic": "عشان العملاق كان عم بستنّى صحابه ييجوا لعنده.", - "hebrew_transliteration": "עַשַאן אִל-עִמלַאק כַּאן עַם-בִּיסתַנַّא צְחַאבּוֹ יִיגוּ לַעִנדוֹ.", - "hebrew": "כי הענק חיכה שחבריו יבואו אליו.", - "expected_phonemes": "ʕ a ʃ aː n | i l ʕ i m l aː ʔ | k aː n | ʕ a m b iː s t a n n a | sˤ ħ aː b o | j iː g u | l a ʕ i n d o" - }, - { - "voice_time_seconds": { - "start": 440.8, - "end": 443.6 - }, - "arabic": "الملك كان مفتون من الأكل", - "hebrew_transliteration": "אִל-מַלִכּ כַּאן מַפתוּן מִן לְ-אַכֶּל", - "hebrew": "המלך היה מוקסם מהאוכל", - "expected_phonemes": "i l m a l i k | k aː n | m a f t uː n | m i n | l ʔ a k e l" - }, - { - "voice_time_seconds": { - "start": 443.6, - "end": 446.2 - }, - "arabic": "وكلّ إشي شافه في هاد لنهار،", - "hebrew_transliteration": "וּכֻּלّ אִשִי שַאפוֹ פִי הַאד לֶ-נְהַאר,", - "hebrew": "ומכל מה שראה באותו יום,", - "expected_phonemes": "u k u l l | ʔ i ʃ i | ʃ aː f o | f i | h aː d | l e n h aː r" - }, - { - "voice_time_seconds": { - "start": 446.2, - "end": 449.0 - }, - "arabic": "عشان هيك عرض على الماركيز", - "hebrew_transliteration": "עַשַאן הֵיכּ עַרַד' עַלַא לְ-מַארְכִּיז", - "hebrew": "ולכן הציע למרקיז", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | ʕ a r a dˤ | ʕ a l a | l m aː r k iː z" - }, - { - "voice_time_seconds": { - "start": 449.0, - "end": 451.6 - }, - "arabic": "يتجوّز بنته الأميرة.", - "hebrew_transliteration": "יִתגַ'וַّז בִּנתוֹ לְ-אַמִירַה.", - "hebrew": "להתחתן עם בתו הנסיכה.", - "expected_phonemes": "j i t dʒ a w w a z | b i n t o | l ʔ a m iː r a" - }, - { - "voice_time_seconds": { - "start": 451.6, - "end": 456.4 - }, - "arabic": "خلال فترة قصيرة تجوّزوا الأميرة والماركيز", - "hebrew_transliteration": "חִ'לַאל פַתרַה קַצִירֶה תְגַ'וַّזוּ אִל-אַמִירַה וִל-מַארְכִּיז", - "hebrew": "תוך זמן קצר התחתנו הנסיכה והמרקיז", - "expected_phonemes": "x i l aː l | f a t r a | ʔ a sˤ iː r e | t dʒ a w w a z u | i l ʔ a m iː r a | w i l m aː r k iː z" - }, - { - "voice_time_seconds": { - "start": 456.4, - "end": 459.8 - }, - "arabic": "وعاشوا سوا في ثبات ونبات.", - "hebrew_transliteration": "וּעַאשוּ סַוַא פִי תַבַּאת וּנַבַּאת.", - "hebrew": "וחיו יחד באושר ועושר.", - "expected_phonemes": "u ʕ aː ʃ u | s a w a | f i | t a b aː t | u n a b aː t" - }, - { - "voice_time_seconds": { - "start": 459.8, - "end": 463.4 - }, - "arabic": "البسّ أبو الجّزمة صار مدير القصر", - "hebrew_transliteration": "אִל-בֶּסّ אַבּוּ גַּזמֶה צַאר מֻדִיר אִל-קַצֶר", - "hebrew": "החתול במגפיים נעשה מנהל הארמון", - "expected_phonemes": "i l b e s s | ʔ a b u | g a z m e | sˤ aː r | m u d iː r | i l ʔ a sˤ e r" - }, - { - "voice_time_seconds": { - "start": 463.4, - "end": 467.2 - }, - "arabic": "وصار يلبس أحسن وأفخم الأواعي الموجودة،", - "hebrew_transliteration": "וּצַאר יִלבַּס אַחסַן וּאַפחַם לְ-אַוַאעִי לְ-מַוְגּ'וּדֶה,", - "hebrew": "והתחיל ללבוש את הבגדים הטובים והמפוארים ביותר,", - "expected_phonemes": "u sˤ aː r | j i l b a s | ʔ a ħ s a n | u ʔ a f ħ a m | l ʔ a w aː ʕ i | l m a w dʒ uː d e" - }, - { - "voice_time_seconds": { - "start": 467.2, - "end": 471.0 - }, - "arabic": "وعاش حياة رغيدة كلّها دلال", - "hebrew_transliteration": "וּעַאש חַיַאה רַעִ'ידֶה כֻּלְّהַא דַלַאל", - "hebrew": "וחי חיי רווחה מלאי פינוק", - "expected_phonemes": "u ʕ aː ʃ | ħ a j aː h | r a ɣ iː d e | k u l l h a | d a l aː l" - }, - { - "voice_time_seconds": { - "start": 471.0, - "end": 474.8 - }, - "arabic": "وبطّل بحياته يصيد فيران.", - "hebrew_transliteration": "וּבַּטַّל בְּחַיַאתוֹ יְצִיד פִירַאן.", - "hebrew": "ומעולם לא צד יותר עכברים.", - "expected_phonemes": "u b a tˤ tˤ a l | b ħ a j aː t o | j sˤ iː d | f iː r aː n" - } - ] - }, - { - "file_name": "נעליו של אבו אל-קאסם אל-טנבורי * כֻּנדַרָת אַבּוּ אל-קָאסֶם א-טָנבּוּרִי * كندرة أبو القاسم الطنبوري [DjeJk5SrSj8].webm", - "youtube_id": "DjeJk5SrSj8", - "title_hebrew": "נעליו של אבו אל-קאסם אל-טנבורי", - "segments": [ - { - "voice_time_seconds": { - "start": 7.4, - "end": 9.0 - }, - "arabic": "بحكوا عن واحد اسمه", - "hebrew_transliteration": "בִּיחכּוּ עַן וַאחַד אִסמוֹ", - "hebrew": "מספרים על מישהו ששמו", - "expected_phonemes": "b iː ħ k u | ʕ a n | w aː ħ a d | ʔ i s m o" - }, - { - "voice_time_seconds": { - "start": 9.0, - "end": 11.2 - }, - "arabic": "أبو القاسم الطّنبوري،", - "hebrew_transliteration": "אַבּוּ לְ-קַאסֶם אִ-טַּנבּוּרִי,", - "hebrew": "אבו אל-קאסם א-טנבורי,", - "expected_phonemes": "ʔ a b u | l ʔ aː s e m | ʔ i tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 11.2, - "end": 13.0 - }, - "arabic": "إلّي كان تاجر من بغداد", - "hebrew_transliteration": "אִלִّי כַּאן תַאגֶ'ר מִן בַּעְ'דַאד", - "hebrew": "שהיה סוחר מבגדאד", - "expected_phonemes": "ʔ i l l i | k aː n | t aː dʒ e r | m i n | b a ɣ d aː d" - }, - { - "voice_time_seconds": { - "start": 13.0, - "end": 17.0 - }, - "arabic": "في أيّام خلافة هارون الرّشيد.", - "hebrew_transliteration": "פִי אַיַّאם חִ'לַאפֶת הַארוּן אִ-רַּשִיד.", - "hebrew": "בימי ח'ליפותו של הארון א-רשיד.", - "expected_phonemes": "f i | ʔ a j j aː m | x i l aː f e t | h aː r uː n | ʔ i r a ʃ iː d" - }, - { - "voice_time_seconds": { - "start": 17.0, - "end": 18.8 - }, - "arabic": "كان الطّنبوري بخيل كتير", - "hebrew_transliteration": "כַּאן אִ-טַּנבּוּרִי בַּחִ'יל כְּתִיר", - "hebrew": "א-טנבורי היה קמצן גדול", - "expected_phonemes": "k aː n | ʔ i tˤ a n b uː r i | b a x iː l | k t iː r" - }, - { - "voice_time_seconds": { - "start": 18.8, - "end": 21.0 - }, - "arabic": "ومشهور في بخله.", - "hebrew_transliteration": "וּמַשהוּר פִי בֻּחְ'לוֹ.", - "hebrew": "ומפורסם בקמצנותו.", - "expected_phonemes": "u m a ʃ h uː r | f i | b u x l o" - }, - { - "voice_time_seconds": { - "start": 21.0, - "end": 25.0 - }, - "arabic": "وكان له كندرة لبسها فوق السّبع سنين.", - "hebrew_transliteration": "וּכַּאן-לוֹ כֻּנדַרַה לִבֶּסְהַא פוֹק אִ-סַּבֶּע סְנִין.", - "hebrew": "והיו לו נעליים שנעל יותר משבע שנים.", - "expected_phonemes": "u k aː n l o | k u n d a r a | l i b e s h a | f oː ʔ | ʔ i s a b e ʕ | s n iː n" - }, - { - "voice_time_seconds": { - "start": 25.0, - "end": 27.2 - }, - "arabic": "وكان دايما يضلّ يصلّح فيها", - "hebrew_transliteration": "וּכַּאן דַאיְמַן יְדַ'לّ יְצַלֶّח פִיהַא", - "hebrew": "והוא תמיד המשיך לתקן אותן", - "expected_phonemes": "u k aː n | d aː j m a n | j dˤ a l l | j sˤ a l l e ħ | f iː h a" - }, - { - "voice_time_seconds": { - "start": 27.2, - "end": 30.2 - }, - "arabic": "لحدّ ما بقي فيها ولا محلّ", - "hebrew_transliteration": "לַחַדّ מַא בְּקִי פִיהַא וְלַא מַחַלّ", - "hebrew": "עד שלא נשאר בהן מקום", - "expected_phonemes": "l a ħ a d d | m a | b ʔ i | f iː h a | u l aː | m a ħ a l l" - }, - { - "voice_time_seconds": { - "start": 30.2, - "end": 32.8 - }, - "arabic": "مش مصلّح ومش مرقّع", - "hebrew_transliteration": "מִש מְצַלַّח וּמִש מְרַקַّע", - "hebrew": "שלא תוקן ולא טולא", - "expected_phonemes": "m i ʃ | m sˤ a l l a ħ | u m i ʃ | m r a ʔ ʔ a ʕ" - }, - { - "voice_time_seconds": { - "start": 32.8, - "end": 35.6 - }, - "arabic": "وصارت تقيلة كتير.", - "hebrew_transliteration": "וּצַארַת תְקִילֶה כְּתִיר.", - "hebrew": "והן נעשו כבדות מאוד.", - "expected_phonemes": "u sˤ aː r a t | t ʔ iː l e | k t iː r" - }, - { - "voice_time_seconds": { - "start": 35.6, - "end": 37.6 - }, - "arabic": "سكّان بغداد كانوا كلّهم", - "hebrew_transliteration": "סֻכַּאן בַּעְ'דַאד כַּאנוּ כֻּלְّהֹם", - "hebrew": "תושבי בגדאד כולם", - "expected_phonemes": "s u k aː n | b a ɣ d aː d | k aː n u | k u l l h o m" - }, - { - "voice_time_seconds": { - "start": 37.6, - "end": 40.2 - }, - "arabic": "يحكوا عن الطّنبوري وبخله", - "hebrew_transliteration": "יִחכּוּ עַן טַּנבּוּרִי וּבֻּחְ'לוֹ", - "hebrew": "דיברו על א-טנבורי ועל קמצנותו", - "expected_phonemes": "j i ħ k u | ʕ a n | tˤ a n b uː r i | u b u x l o" - }, - { - "voice_time_seconds": { - "start": 40.2, - "end": 44.0 - }, - "arabic": "وبلّشوا يستعملوا المصطلح «كندرة الطّنبوري»", - "hebrew_transliteration": "וּבַּלַّשוּ יִסתַעמְלוּ לְ-מֻצטַלַח כֻּנדַרֶת טַּנבּוּרִי", - "hebrew": "והתחילו להשתמש בביטוי \"נעלי א-טנבורי\"", - "expected_phonemes": "u b a l l a ʃ u | j i s t a ʕ m l u | l m u sˤ tˤ a l a ħ | k u n d a r e t | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 44.0, - "end": 47.4 - }, - "arabic": "كمثال على إشي قديم، مهتري.", - "hebrew_transliteration": "כַּמִתַאל עַלַא אִשִי קַדִים, מִהתִרִי.", - "hebrew": "כדוגמה לדבר ישן ובלוי.", - "expected_phonemes": "k a m i t aː l | ʕ a l a | ʔ i ʃ i | ʔ a d iː m | m i h t i r i" - }, - { - "voice_time_seconds": { - "start": 48.0, - "end": 49.0 - }, - "arabic": "في يوم من الأيام", - "hebrew_transliteration": "פִי יוֹם מִן לְ-אַיַّאם", - "hebrew": "יום אחד", - "expected_phonemes": "f i | j oː m | m i n | l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 49.0, - "end": 51.8 - }, - "arabic": "راح أبو قاسم لسوق القزاز،", - "hebrew_transliteration": "רַאח אַבּוּ קַאסֶם לַסוּק אִל-קַזַאז,", - "hebrew": "אבו קאסם הלך לשוק הזכוכית,", - "expected_phonemes": "r aː ħ | ʔ a b u | ʔ aː s e m | l a s uː ʔ | i l ʔ a z aː z" - }, - { - "voice_time_seconds": { - "start": 51.8, - "end": 55.6 - }, - "arabic": "وهناك قابل تاجر من معارفه إلّي قالّه:", - "hebrew_transliteration": "וּהֻנַאכּ קַאבַּל תַאגֶ'ר מִן מַעַארְפוֹ אִלִّי קַאל-לוֹ:", - "hebrew": "ושם פגש סוחר ממכריו שאמר לו:", - "expected_phonemes": "u h u n aː k | ʔ aː b a l | t aː dʒ e r | m i n | m a ʕ aː r f o | ʔ i l l i | ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 55.6, - "end": 57.4 - }, - "arabic": "«تعال يا أبو قاسم،", - "hebrew_transliteration": "תַעַאל יַא אַבּוּ קַאסֶם,", - "hebrew": "בוא, אבו קאסם,", - "expected_phonemes": "t a ʕ aː l | j a | ʔ a b u | ʔ aː s e m" - }, - { - "voice_time_seconds": { - "start": 57.4, - "end": 59.2 - }, - "arabic": "شوف أواني القزاز الممتازة", - "hebrew_transliteration": "שוּף אַוַאנִי לְ-קַזַאז אִל-מֻמתַאזֶה", - "hebrew": "ראה את כלי הזכוכית המצוינים", - "expected_phonemes": "ʃ uː f | ʔ a w aː n i | l ʔ a z aː z | i l m u m t aː z e" - }, - { - "voice_time_seconds": { - "start": 59.2, - "end": 61.4 - }, - "arabic": "إلّي وصلت هلّق من سوريا.", - "hebrew_transliteration": "אִלִّי וִצלַת הַלַّק מִן סוּרְיַא.", - "hebrew": "שהגיעו עכשיו מסוריה.", - "expected_phonemes": "ʔ i l l i | w i sˤ l a t | h a l l a ʔ | m i n | s uː r j a" - }, - { - "voice_time_seconds": { - "start": 61.4, - "end": 64.2 - }, - "arabic": "صاحب الأواني مستعجل يرجع على بيته،", - "hebrew_transliteration": "צַאחֶבּ לְ-אַוַאנִי מִסתַעגֶ'ל יִרגַ'ע עַלַא בֵּיתוֹ,", - "hebrew": "בעל הכלים ממהר לחזור לביתו,", - "expected_phonemes": "sˤ aː ħ e b | l ʔ a w aː n i | m i s t a ʕ dʒ e l | j i r dʒ a ʕ | ʕ a l a | b eː t o" - }, - { - "voice_time_seconds": { - "start": 64.2, - "end": 68.0 - }, - "arabic": "ومستعدّ يبيعهم بتنزيل كبير.»", - "hebrew_transliteration": "וּמִסתַעֶדّ יְבִּיעְהֹם בְּתַנזִיל כְּבִּיר.", - "hebrew": "ומוכן למכור אותם בהנחה גדולה.", - "expected_phonemes": "u m i s t a ʕ e d d | j b iː ʕ h o m | b t a n z iː l | k b iː r" - }, - { - "voice_time_seconds": { - "start": 68.0, - "end": 72.0 - }, - "arabic": "اشترى الطّنبوري كلّ أواني القزاز بنصّ سعرهم،", - "hebrew_transliteration": "אִשתַרַא טַּנבּוּרִי כֻּלّ אַוַאנִי לְ-קַזַאז בְּנֻצّ סִעְרְהֹם,", - "hebrew": "א-טנבורי קנה את כל כלי הזכוכית בחצי מחירם,", - "expected_phonemes": "ʔ i ʃ t a r a | tˤ a n b uː r i | k u l l | ʔ a w aː n i | l ʔ a z aː z | b n u sˤ sˤ | s i ʕ r h o m" - }, - { - "voice_time_seconds": { - "start": 72.0, - "end": 75.6 - }, - "arabic": "دفع حقّهم وكمّل في طريقه لسوق العطّارين.", - "hebrew_transliteration": "דַפַע חַקְّהֹם וּכַּמַّל פִי טַרִיקוֹ לַסוּק לְ-עַטַّארִין.", - "hebrew": "שילם את מחירם והמשיך בדרכו לשוק הבשמים.", - "expected_phonemes": "d a f a ʕ | ħ a ʔ ʔ h o m | u k a m m a l | f i | tˤ a r iː ʔ o | l a s uː ʔ | l ʕ a tˤ tˤ aː r iː n" - }, - { - "voice_time_seconds": { - "start": 76.4, - "end": 80.2 - }, - "arabic": "شافه هناك كمان تاجر بعرفه وناداه:", - "hebrew_transliteration": "שַאפוֹ הֻנַאכּ כַּמַאן תַאגֶ'ר בְּעִרְפוֹ וּנַאדַאה:", - "hebrew": "שם ראה אותו עוד סוחר שהכיר אותו וקרא לו:", - "expected_phonemes": "ʃ aː f o | h u n aː k | k a m aː n | t aː dʒ e r | b ʕ i r f o | u n aː d aː h" - }, - { - "voice_time_seconds": { - "start": 80.2, - "end": 84.2 - }, - "arabic": "«تعال يا أبو قاسم، فيه فرصة استثنائيّة.", - "hebrew_transliteration": "תַעַאל יַא אַבּוּ קַאסֶם, פִיה פֻרצַה אִסתִתנַאאִיֶّה.", - "hebrew": "בוא, אבו קאסם, יש הזדמנות יוצאת דופן.", - "expected_phonemes": "t a ʕ aː l | j a | ʔ a b u | ʔ aː s e m | f iː h | f u r sˤ a | ʔ i s t i t n aː ʔ i j j e" - }, - { - "voice_time_seconds": { - "start": 84.2, - "end": 87.6 - }, - "arabic": "فيه تاجر إلّي وصل للمدينة قبل وقت قصير", - "hebrew_transliteration": "פִיה תַאגֶ'ר אִלִّי וִצֶל לַל-מַדִינֶה קַבֶּל וַקֶת קַצִיר", - "hebrew": "יש סוחר שהגיע לעיר לפני זמן קצר", - "expected_phonemes": "f iː h | t aː dʒ e r | ʔ i l l i | w i sˤ e l | l a l m a d iː n e | ʔ a b e l | w a ʔ e t | ʔ a sˤ iː r" - }, - { - "voice_time_seconds": { - "start": 87.6, - "end": 90.4 - }, - "arabic": "بسّ فجأة مرض وبدّه يرجع على بيته.", - "hebrew_transliteration": "בַּס פַגְ'אַה מִרֶד' וּבַּדּוֹ יִרגַ'ע עַלַא בֵּיתוֹ.", - "hebrew": "אבל פתאום חלה ורוצה לחזור לביתו.", - "expected_phonemes": "b a s | f a dʒ ʔ a | m i r e dˤ | u b a d o | j i r dʒ a ʕ | ʕ a l a | b eː t o" - }, - { - "voice_time_seconds": { - "start": 90.4, - "end": 92.0 - }, - "arabic": "معاه كميّة كبيرة", - "hebrew_transliteration": "מַעַאה כַּמִّיֶה כְּבִּירֶה", - "hebrew": "יש לו כמות גדולה", - "expected_phonemes": "m a ʕ aː h | k a m m i j e | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 92.0, - "end": 94.6 - }, - "arabic": "من ماء الورد عالية الجّودة،", - "hebrew_transliteration": "מִן מַאא אִל-וַרְד עַאלְיֶת גּוּדֶה,", - "hebrew": "של מי ורדים באיכות גבוהה,", - "expected_phonemes": "m i n | m aː a | i l w a r d | ʕ aː l j e t | g uː d e" - }, - { - "voice_time_seconds": { - "start": 94.6, - "end": 97.4 - }, - "arabic": "إلّي ما فشّ متلها في كلّ العالم", - "hebrew_transliteration": "אִלִّי מַא פִשّ מִתלְהַא פִי כֻּלّ לְ-עַאלַם", - "hebrew": "שאין כמותם בכל העולם", - "expected_phonemes": "ʔ i l l i | m a | f i ʃ ʃ | m i t l h a | f i | k u l l | l ʕ aː l a m" - }, - { - "voice_time_seconds": { - "start": 97.4, - "end": 99.8 - }, - "arabic": "وسعرها رخيص.»", - "hebrew_transliteration": "וּסִעְרְהַא רְחִ'יץ.", - "hebrew": "ומחירם זול.", - "expected_phonemes": "u s i ʕ r h a | r x iː sˤ" - }, - { - "voice_time_seconds": { - "start": 99.8, - "end": 102.8 - }, - "arabic": "انبسط الطّنبوري على الصّفقة النّاجحة،", - "hebrew_transliteration": "אִנבַּסַט טַּנבּוּרִי עַלַא צַּפקַה נַּאגְ'חַה,", - "hebrew": "א-טנבורי שמח על העסקה המוצלחת,", - "expected_phonemes": "ʔ i n b a s a tˤ | tˤ a n b uː r i | ʕ a l a | sˤ a f ʔ a | n aː dʒ ħ a" - }, - { - "voice_time_seconds": { - "start": 102.8, - "end": 105.8 - }, - "arabic": "استعجل واشترى ماء الورد،", - "hebrew_transliteration": "אִסתַעגַ'ל וּאִשתַרַא מַאא אִל-וַרְד,", - "hebrew": "מיהר וקנה את מי הוורדים,", - "expected_phonemes": "ʔ i s t a ʕ dʒ a l | u ʔ i ʃ t a r a | m aː a | i l w a r d" - }, - { - "voice_time_seconds": { - "start": 105.8, - "end": 108.2 - }, - "arabic": "صبّهم في أواني القزاز،", - "hebrew_transliteration": "צַבַּّהֹם פִי אַוַאנִי לְ-קַזַאז,", - "hebrew": "יצק אותם לכלי הזכוכית,", - "expected_phonemes": "sˤ a b b a h o m | f i | ʔ a w aː n i | l ʔ a z aː z" - }, - { - "voice_time_seconds": { - "start": 108.2, - "end": 110.6 - }, - "arabic": "ورجع مع لبضاعة على بيته.", - "hebrew_transliteration": "וּרִגֶ'ע מַע לֶ-בְּדַאעַה עַלַא בֵּיתוֹ.", - "hebrew": "וחזר עם הסחורה לביתו.", - "expected_phonemes": "u r i dʒ e ʕ | m a ʕ | l e b d aː ʕ a | ʕ a l a | b eː t o" - }, - { - "voice_time_seconds": { - "start": 111.4, - "end": 114.2 - }, - "arabic": "حطّ الطّنبوري لبضاعة في زاوية الغرفة", - "hebrew_transliteration": "חַטّ טַּנבּוּרִי לֶ-בְּדַאעַה פִי זַאוְיֶת לְ-עֻ'רְפֶה", - "hebrew": "א-טנבורי שם את הסחורה בפינת החדר", - "expected_phonemes": "ħ a tˤ tˤ | tˤ a n b uː r i | l e b d aː ʕ a | f i | z aː w j e t | l ɣ u r f e" - }, - { - "voice_time_seconds": { - "start": 114.2, - "end": 116.8 - }, - "arabic": "وصار يبتسم ويفكّر في المربح", - "hebrew_transliteration": "וּצַאר יִבּתִסֶם וִיפַכַّר פִי לְ-מַרבַּח", - "hebrew": "והתחיל לחייך ולחשוב על הרווח", - "expected_phonemes": "u sˤ aː r | j i b t i s e m | w iː f a k k a r | f i | l m a r b a ħ" - }, - { - "voice_time_seconds": { - "start": 116.8, - "end": 120.6 - }, - "arabic": "إلّي ممكن يعمله من بيعة لبضاعة.", - "hebrew_transliteration": "אִלִّי מֻמכֶּן יַעמְלוֹ מִן בֵּיעֶת לֶ-בְּדַאעַה.", - "hebrew": "שיוכל להרוויח ממכירת הסחורה.", - "expected_phonemes": "ʔ i l l i | m u m k e n | j a ʕ m l o | m i n | b eː ʕ e t | l e b d aː ʕ a" - }, - { - "voice_time_seconds": { - "start": 120.6, - "end": 123.2 - }, - "arabic": "في يوم من الأيّام، قرّر أبو القاسم", - "hebrew_transliteration": "פִי יוֹם מִן לְ-אַיַّאם, קַרַّר אַבּוּ לְ-קַאסֶם", - "hebrew": "יום אחד, אבו אל-קאסם החליט", - "expected_phonemes": "f i | j oː m | m i n | l ʔ a j j aː m | ʔ a r r a r | ʔ a b u | l ʔ aː s e m" - }, - { - "voice_time_seconds": { - "start": 123.2, - "end": 125.6 - }, - "arabic": "يفوت على الحمّام عشان يتحمّم،", - "hebrew_transliteration": "יְפוּת עַלַא לְ-חַמַّאם עַשַאן יִתחַמַّם,", - "hebrew": "להיכנס לבית המרחץ כדי להתרחץ,", - "expected_phonemes": "j f uː t | ʕ a l a | l ħ a m m aː m | ʕ a ʃ aː n | j i t ħ a m m a m" - }, - { - "voice_time_seconds": { - "start": 126.2, - "end": 130.2 - }, - "arabic": "وهناك شافه واحد من صحابه وقالّه:", - "hebrew_transliteration": "וּהֻנַאכּ שַאפוֹ וַאחַד מִן אַצְחַאבּוֹ וּקַאל-לוֹ:", - "hebrew": "ושם ראה אותו אחד מחבריו ואמר לו:", - "expected_phonemes": "u h u n aː k | ʃ aː f o | w aː ħ a d | m i n | ʔ a sˤ ħ aː b o | u ʔ aː l l o" - }, - { - "voice_time_seconds": { - "start": 130.2, - "end": 133.6 - }, - "arabic": "«أبو قاسم، أجا الوقت تبدّل كندرتك.", - "hebrew_transliteration": "אַבּוּ קַאסֶם, אַגַ'א לְ-וַקֶת תְבַּדֶّל כֻּנדַרתַכּ.", - "hebrew": "אבו קאסם, הגיע הזמן להחליף את הנעליים שלך.", - "expected_phonemes": "ʔ a b u | ʔ aː s e m | ʔ a dʒ a | l w a ʔ e t | t b a d d e l | k u n d a r t a k" - }, - { - "voice_time_seconds": { - "start": 133.6, - "end": 136.6 - }, - "arabic": "إنت زلمة محترم وغني، الحمد لله،", - "hebrew_transliteration": "אִנתֶ זַלַמֶה מֻחתַרַם וּעַ'נִי, אִל-חַמדֻ לִלַّאה,", - "hebrew": "אתה אדם מכובד ועשיר, תודה לאל,", - "expected_phonemes": "ʔ i n t e | z a l a m e | m u ħ t a r a m | u ɣ a n i | i l ħ a m d u | l i l l aː h" - }, - { - "voice_time_seconds": { - "start": 136.6, - "end": 140.2 - }, - "arabic": "ومش مناسب تمشي بهيك كندرة خربانة.»", - "hebrew_transliteration": "וּמִש מְנַאסֶבּ תִמשִי בְּהֵיכּ כֻּנדַרַה חַ'רבַּאנֶה.", - "hebrew": "ולא מתאים שתלך בנעליים הרוסות כאלה.", - "expected_phonemes": "u m i ʃ | m n aː s e b | t i m ʃ i | b h eː k | k u n d a r a | x a r b aː n e" - }, - { - "voice_time_seconds": { - "start": 140.2, - "end": 143.0 - }, - "arabic": "جاوبه الطّنبوري: «الحقّ معك.", - "hebrew_transliteration": "גַ'אוַבּוֹ טַּנבּוּרִי: אִל-חַקّ מַעַכּ.", - "hebrew": "א-טנבורי ענה לו: אתה צודק.", - "expected_phonemes": "dʒ aː w a b o | tˤ a n b uː r i | i l ħ a ʔ ʔ | m a ʕ a k" - }, - { - "voice_time_seconds": { - "start": 143.0, - "end": 147.4 - }, - "arabic": "رح أبدّلها بأوّل فرصة. إن شاءالله خير.»", - "hebrew_transliteration": "רַח-אַבַּדֶّלְהַא בְּאַוַّל פֻרצַה. אִן שַאאַללַّה חֵ'יר.", - "hebrew": "אחליף אותן בהזדמנות הראשונה. אם ירצה השם, לטובה.", - "expected_phonemes": "r a ħ ʔ a b a d d e l h a | b ʔ a w w a l | f u r sˤ a | ʔ i n | ʃ aː ʔ a l l l a | x eː r" - }, - { - "voice_time_seconds": { - "start": 147.4, - "end": 149.2 - }, - "arabic": "وفات على الحمّام يتحمّم.", - "hebrew_transliteration": "וּפַאת עַלַא לְ-חַמַّאם יִתחַמַّם.", - "hebrew": "ונכנס לבית המרחץ להתרחץ.", - "expected_phonemes": "u f aː t | ʕ a l a | l ħ a m m aː m | j i t ħ a m m a m" - }, - { - "voice_time_seconds": { - "start": 150.0, - "end": 151.8 - }, - "arabic": "لمّا لبس أواعيه وطلع،", - "hebrew_transliteration": "לַמַّא לִבֶּס אַוַאעִיה וּטִלֶע,", - "hebrew": "כשלבש את בגדיו ויצא,", - "expected_phonemes": "l a m m a | l i b e s | ʔ a w aː ʕ iː h | u tˤ i l e ʕ" - }, - { - "voice_time_seconds": { - "start": 151.8, - "end": 154.8 - }, - "arabic": "شاف حدّ كندرته كندرة جديدة.", - "hebrew_transliteration": "שַאף חַדّ כֻּנדַרתוֹ כֻּנדַרַה גְ'דִידֶה.", - "hebrew": "ראה ליד הנעליים שלו נעליים חדשות.", - "expected_phonemes": "ʃ aː f | ħ a d d | k u n d a r t o | k u n d a r a | dʒ d iː d e" - }, - { - "voice_time_seconds": { - "start": 154.8, - "end": 157.6 - }, - "arabic": "الطّنبوري فكّر إنّه صاحبه كان كريم", - "hebrew_transliteration": "אִ-טַּנבּוּרִי פַכַּר אִנּוֹ צַאחבּוֹ כַּאן כַּרִים", - "hebrew": "א-טנבורי חשב שחברו היה נדיב", - "expected_phonemes": "ʔ i tˤ a n b uː r i | f a k a r | ʔ i n o | sˤ aː ħ b o | k aː n | k a r iː m" - }, - { - "voice_time_seconds": { - "start": 157.6, - "end": 159.4 - }, - "arabic": "واشترله إيّاها.", - "hebrew_transliteration": "וּשתַרַא-לוֹ אִיַّאהַא.", - "hebrew": "וקנה לו אותן.", - "expected_phonemes": "u ʃ t a r aː l o | ʔ i j j aː h a" - }, - { - "voice_time_seconds": { - "start": 159.4, - "end": 163.8 - }, - "arabic": "لهيك، لبسها وروّح على بيته.", - "hebrew_transliteration": "לַהֵיכּ, לִבֶּסְהַא וּרַוַّח עַלַא בֵּיתוֹ.", - "hebrew": "לכן, נעל אותן והלך לביתו.", - "expected_phonemes": "l a h eː k | l i b e s h a | u r a w w a ħ | ʕ a l a | b eː t o" - }, - { - "voice_time_seconds": { - "start": 163.8, - "end": 166.8 - }, - "arabic": "هاي الكندرة لجديدة كانت لحاكم بغداد،", - "hebrew_transliteration": "הַאי לְ-כֻּנדַרַה לֶ-גְ'דִידֶה כַּאנַת לַחַאכֶּם בַּעְ'דַאד,", - "hebrew": "הנעליים החדשות האלה היו של מושל בגדאד,", - "expected_phonemes": "h aː i | l k u n d a r a | l e dʒ d iː d e | k aː n a t | l a ħ aː k e m | b a ɣ d aː d" - }, - { - "voice_time_seconds": { - "start": 166.8, - "end": 170.0 - }, - "arabic": "إلّي هوّ كمان أجا على الحمّام في هداك النّهار.", - "hebrew_transliteration": "אִלִّי הֻוֶّ כַּמַאן אַגַ'א עַלַא לְ-חַמַّאם פִי הַדַאכּ נַּהַאר.", - "hebrew": "שגם הוא בא לבית המרחץ באותו יום.", - "expected_phonemes": "ʔ i l l i | h u w w e | k a m aː n | ʔ a dʒ a | ʕ a l a | l ħ a m m aː m | f i | h a d aː k | n a h aː r" - }, - { - "voice_time_seconds": { - "start": 170.6, - "end": 174.2 - }, - "arabic": "بعد ما تغسّل وطلع أجا تيلبس كندرته،", - "hebrew_transliteration": "בַּעֶד מַא תְעַ'סַّל וּטִלֶע אַגַ'א תַ-יִלבַּס כֻּנדַרתוֹ,", - "hebrew": "אחרי שהתרחץ ויצא, בא לנעול את נעליו,", - "expected_phonemes": "b a ʕ e d | m a | t ɣ a s s a l | u tˤ i l e ʕ | ʔ a dʒ a | t a j i l b a s | k u n d a r t o" - }, - { - "voice_time_seconds": { - "start": 174.2, - "end": 175.6 - }, - "arabic": "ما لقاهاش.", - "hebrew_transliteration": "מַא לַקַאהַא-ש.", - "hebrew": "ולא מצא אותן.", - "expected_phonemes": "m a | l a ʔ aː h aː ʃ" - }, - { - "voice_time_seconds": { - "start": 175.6, - "end": 180.0 - }, - "arabic": "دوّر عليها في كلّ محلّ من دون فايدة.", - "hebrew_transliteration": "דַוַّר עַלֵיהַא פִי כֻּלّ מַחַלّ מִן דוּן פַאיְדֶה.", - "hebrew": "חיפש אותן בכל מקום ללא הועיל.", - "expected_phonemes": "d a w w a r | ʕ a l eː h a | f i | k u l l | m a ħ a l l | m i n | d uː n | f aː j d e" - }, - { - "voice_time_seconds": { - "start": 180.0, - "end": 181.8 - }, - "arabic": "قال الحاكم للنّاس:", - "hebrew_transliteration": "קַאל לְ-חַאכֶּם לַ-נַּאס:", - "hebrew": "המושל אמר לאנשים:", - "expected_phonemes": "ʔ aː l | l ħ aː k e m | l a n aː s" - }, - { - "voice_time_seconds": { - "start": 181.8, - "end": 184.6 - }, - "arabic": "«يا ناس، شو رأيكم،", - "hebrew_transliteration": "יַא נַאס, שוּ רַאיְכֹּם,", - "hebrew": "אנשים, מה דעתכם,", - "expected_phonemes": "j a | n aː s | ʃ u | r aː j k o m" - }, - { - "voice_time_seconds": { - "start": 184.6, - "end": 187.0 - }, - "arabic": "بلكي هاد إلّي أخد كندرتي", - "hebrew_transliteration": "בַּלְכִּי הַאד אִלִّי אַחַ'ד כֻּנדַרתִי", - "hebrew": "אולי מי שלקח את הנעליים שלי", - "expected_phonemes": "b a l k i | h aː d | ʔ i l l i | ʔ a x a d | k u n d a r t i" - }, - { - "voice_time_seconds": { - "start": 187.0, - "end": 189.6 - }, - "arabic": "خلّى محلّها إشي تاني؟»", - "hebrew_transliteration": "חַלַّא מַחַלְّהַא אִשִי תַאנִי?", - "hebrew": "השאיר במקומן משהו אחר?", - "expected_phonemes": "ħ a l l a | m a ħ a l l h a | ʔ i ʃ i | t aː n i" - }, - { - "voice_time_seconds": { - "start": 189.6, - "end": 192.2 - }, - "arabic": "النّاس فتّشت وما لاقتش إشي،", - "hebrew_transliteration": "אִ-נַּאס פַתַّשַת וּמַא לַאקַתֶ-ש אִשִי,", - "hebrew": "האנשים חיפשו ולא מצאו כלום,", - "expected_phonemes": "ʔ i n aː s | f a t t a ʃ a t | u m a | l aː ʔ a t e ʃ | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 192.2, - "end": 194.6 - }, - "arabic": "غير كندرة الطّنبوري،", - "hebrew_transliteration": "עֵ'יר כֻּנדַרֶת טַּנבּוּרִי,", - "hebrew": "מלבד הנעליים של א-טנבורי,", - "expected_phonemes": "ɣ eː r | k u n d a r e t | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 194.6, - "end": 196.6 - }, - "arabic": "وتعرّفوا عليها دغري.", - "hebrew_transliteration": "וּתְעַרַّפוּ עַלֵיהַא דֻעְ'רִי.", - "hebrew": "וזיהו אותן מיד.", - "expected_phonemes": "u t ʕ a r r a f u | ʕ a l eː h a | d u ɣ r i" - }, - { - "voice_time_seconds": { - "start": 196.6, - "end": 198.4 - }, - "arabic": "أصلًا مين في بغداد", - "hebrew_transliteration": "אַצלַן, מִין פִי בַּעְ'דַאד", - "hebrew": "בעצם, מי בבגדאד", - "expected_phonemes": "ʔ a sˤ l a n | m iː n | f i | b a ɣ d aː d" - }, - { - "voice_time_seconds": { - "start": 198.4, - "end": 201.6 - }, - "arabic": "ما كانش بتعرّف عليها وولا سمع عنّها؟", - "hebrew_transliteration": "מַא כַּאנֶש בִּתעַרַّף עַלֵיהַא וּוַלַא סִמֶע עַנְהַא?", - "hebrew": "לא היה מזהה אותן או לא שמע עליהן?", - "expected_phonemes": "m a | k aː n e ʃ | b i t ʕ a r r a f | ʕ a l eː h a | u w a l a | s i m e ʕ | ʕ a n h a" - }, - { - "voice_time_seconds": { - "start": 202.4, - "end": 205.4 - }, - "arabic": "بعت الحاكم حرّاسه يمسكوا الطّنبوري،", - "hebrew_transliteration": "בַּעַת לְ-חַאכֶּם חֻרַّאסוֹ יִמסְכּוּ טַּנבּוּרִי,", - "hebrew": "המושל שלח את שומריו לתפוס את א-טנבורי,", - "expected_phonemes": "b a ʕ a t | l ħ aː k e m | ħ u r r aː s o | j i m s k u | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 205.4, - "end": 208.2 - }, - "arabic": "يوخذوا منّه الكندرة، يضربوه،", - "hebrew_transliteration": "יוֹחְ'דוּ מִנּוֹ לְ-כֻּנדַרַה, יִדְ'רְבּוּה", - "hebrew": "לקחת ממנו את הנעליים, להכות אותו,", - "expected_phonemes": "j oː x d u | m i n o | l k u n d a r a | j i dˤ r b uː h" - }, - { - "voice_time_seconds": { - "start": 208.2, - "end": 211.6 - }, - "arabic": "ويحبسوه في السّجن لمدّة أسبوعين.", - "hebrew_transliteration": "וִיחבְּסוּה פִי סִّגֶ'ן לַמֻדֶّת אֻסבּוּעֵין.", - "hebrew": "ולכלוא אותו בכלא לשבועיים.", - "expected_phonemes": "w iː ħ b s uː h | f i | s s i dʒ e n | l a m u d d e t | ʔ u s b uː ʕ eː n" - }, - { - "voice_time_seconds": { - "start": 211.6, - "end": 214.2 - }, - "arabic": "بعد ما خلصت مدّة حبسه،", - "hebrew_transliteration": "בַּעֶד מַא חִ'לצַת מֻדֶּת חַבְּסוֹ,", - "hebrew": "אחרי שתמה תקופת מאסרו,", - "expected_phonemes": "b a ʕ e d | m a | x i l sˤ a t | m u d e t | ħ a b s o" - }, - { - "voice_time_seconds": { - "start": 214.2, - "end": 217.8 - }, - "arabic": "ألقى عليه الحاكم غرامة وطلّعه من السّجن.", - "hebrew_transliteration": "אַלקַא עַלֵיה לְ-חַאכֶּם עַ'רַאמֶה וּטַלַّעוֹ מִן סִّגֶ'ן.", - "hebrew": "המושל הטיל עליו קנס ושחרר אותו מהכלא.", - "expected_phonemes": "i l ʔ a | ʕ a l eː h | l ħ aː k e m | ɣ a r aː m e | u tˤ a l l a ʕ o | m i n | s s i dʒ e n" - }, - { - "voice_time_seconds": { - "start": 218.6, - "end": 221.8 - }, - "arabic": "أبو القاسم طلع من السّجن وأخد كندرته", - "hebrew_transliteration": "אַבּוּ לְ-קַאסֶם טִלֶע מִן סִّגֶ'ן וּאַחַ'ד כֻּנדַרתוֹ", - "hebrew": "אבו אל-קאסם יצא מהכלא ולקח את נעליו", - "expected_phonemes": "ʔ a b u | l ʔ aː s e m | tˤ i l e ʕ | m i n | s s i dʒ e n | u ʔ a x a d | k u n d a r t o" - }, - { - "voice_time_seconds": { - "start": 221.8, - "end": 223.8 - }, - "arabic": "وكان متعصبن كتير.", - "hebrew_transliteration": "וּכַּאן מִתעַצֶّבּ כְּתִיר.", - "hebrew": "והיה כועס מאוד.", - "expected_phonemes": "u k aː n | m i t ʕ a sˤ sˤ e b | k t iː r" - }, - { - "voice_time_seconds": { - "start": 223.8, - "end": 227.2 - }, - "arabic": "راح على نهر الدّجلة ورمها في الميّ،", - "hebrew_transliteration": "רַאח עַלַא נַהֶר דִּגְ'לֶה וּרַמַאהַא פִי לְ-מַיّ,", - "hebrew": "הלך לנהר החידקל וזרק אותן למים,", - "expected_phonemes": "r aː ħ | ʕ a l a | n a h e r | d i dʒ l e | u r a m aː h a | f i | l m a j j" - }, - { - "voice_time_seconds": { - "start": 227.2, - "end": 229.0 - }, - "arabic": "ورجع على بيته مبسوط", - "hebrew_transliteration": "וּרִגֶ'ע עַלַא בֵּיתוֹ מַבְּסוּט", - "hebrew": "וחזר לביתו מרוצה", - "expected_phonemes": "u r i dʒ e ʕ | ʕ a l a | b eː t o | m a b s uː tˤ" - }, - { - "voice_time_seconds": { - "start": 229.0, - "end": 231.8 - }, - "arabic": "عشان انتقم وخلص منها.", - "hebrew_transliteration": "עַשַאן אִנתַקַם וּחִ'לֶץ מִנְהַא.", - "hebrew": "כי נקם ונפטר מהן.", - "expected_phonemes": "ʕ a ʃ aː n | ʔ i n t a ʔ a m | u x i l e sˤ | m i n h a" - }, - { - "voice_time_seconds": { - "start": 232.4, - "end": 233.8 - }, - "arabic": "بعد وقت قصير،", - "hebrew_transliteration": "בַּעֶד וַקֶת קַצִיר,", - "hebrew": "אחרי זמן קצר,", - "expected_phonemes": "b a ʕ e d | w a ʔ e t | ʔ a sˤ iː r" - }, - { - "voice_time_seconds": { - "start": 233.8, - "end": 237.2 - }, - "arabic": "أجا صيّاد ورمى شبكته في نهر الدّجلة", - "hebrew_transliteration": "אַגַ'א צַיַّאד וּרַמַא שַבַּכְּתוֹ פִי נַהֶר דִּגְ'לֶה", - "hebrew": "בא דייג וזרק את רשתו לנהר החידקל", - "expected_phonemes": "ʔ a dʒ a | sˤ a j j aː d | u r a m a | ʃ a b a k t o | f i | n a h e r | d i dʒ l e" - }, - { - "voice_time_seconds": { - "start": 237.2, - "end": 239.2 - }, - "arabic": "عشان يصيد سمك.", - "hebrew_transliteration": "עַשַאן יְצִיד סַמַכּ.", - "hebrew": "כדי לדוג דגים.", - "expected_phonemes": "ʕ a ʃ aː n | j sˤ iː d | s a m a k" - }, - { - "voice_time_seconds": { - "start": 239.2, - "end": 241.2 - }, - "arabic": "لمّا طلّع الشّبكة من الميّ،", - "hebrew_transliteration": "לַמַّא טַלַّע אִ-שַּׁבַּכֶּה מִן לְ-מַיّ,", - "hebrew": "כשהוציא את הרשת מהמים,", - "expected_phonemes": "l a m m a | tˤ a l l a ʕ | ʔ i ʃ a b a k e | m i n | l m a j j" - }, - { - "voice_time_seconds": { - "start": 241.2, - "end": 244.0 - }, - "arabic": "شاف فيها كندرة تقيلة،", - "hebrew_transliteration": "שַאף פִיהַא כֻּנדַרַה תְקִילֶה,", - "hebrew": "ראה בה נעליים כבדות,", - "expected_phonemes": "ʃ aː f | f iː h a | k u n d a r a | t ʔ iː l e" - }, - { - "voice_time_seconds": { - "start": 244.0, - "end": 247.2 - }, - "arabic": "ودغري عرفها: كندرة الطّنبوري.", - "hebrew_transliteration": "וּדֻעְ'רִי עִרְפְהַא: כֻּנדַרֶת טַּנבּוּרִי.", - "hebrew": "ומיד זיהה אותן: הנעליים של א-טנבורי.", - "expected_phonemes": "u d u ɣ r i | ʕ i r f h a | k u n d a r e t | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 247.2, - "end": 249.0 - }, - "arabic": "الصّيّاد قال لنفسه:", - "hebrew_transliteration": "אִ-צַּיַّאד קַאל לַנַפסוֹ:", - "hebrew": "הדייג אמר לעצמו:", - "expected_phonemes": "ʔ i sˤ a j j aː d | ʔ aː l | l a n a f s o" - }, - { - "voice_time_seconds": { - "start": 249.0, - "end": 252.2 - }, - "arabic": "«يمكن الكندرة وقعت في النّهر بالغلط.»", - "hebrew_transliteration": "יֻמכֶּן לְ-כֻּנדַרַה וִקעַת פִי נַּהֶר בִּל-עַ'לַט.", - "hebrew": "אולי הנעליים נפלו לנהר בטעות.", - "expected_phonemes": "j u m k e n | l k u n d a r a | w i ʔ ʕ a t | f i | n a h e r | b i l ɣ a l a tˤ" - }, - { - "voice_time_seconds": { - "start": 252.2, - "end": 254.8 - }, - "arabic": "أخدها وراح على بيت الطّنبوري", - "hebrew_transliteration": "אַחַ'דְהַא וּרַאח עַלַא בֵּית טַּנבּוּרִי", - "hebrew": "לקח אותן והלך לבית א-טנבורי", - "expected_phonemes": "ʔ a x a d h a | u r aː ħ | ʕ a l a | b eː t | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 254.8, - "end": 257.2 - }, - "arabic": "عشان يرجّعله ايّاها.", - "hebrew_transliteration": "עַשַאן יְרַגֶّע-לוֹ יַּאהַא.", - "hebrew": "כדי להחזיר לו אותן.", - "expected_phonemes": "ʕ a ʃ aː n | j r a g g e ʕ l o | j aː h a" - }, - { - "voice_time_seconds": { - "start": 257.2, - "end": 259.8 - }, - "arabic": "لمّا وصل، دقّ على الباب", - "hebrew_transliteration": "לַמַّא וִצֶל, דַקّ עַלַא לְ-בַּאבּ", - "hebrew": "כשהגיע, דפק על הדלת", - "expected_phonemes": "l a m m a | w i sˤ e l | d a ʔ ʔ | ʕ a l a | l b aː b" - }, - { - "voice_time_seconds": { - "start": 259.8, - "end": 262.0 - }, - "arabic": "ونادى باسم الطّنبوري،", - "hebrew_transliteration": "וּנַאדַא בְּאִסֶם טַּנבּוּרִי,", - "hebrew": "וקרא בשמו של א-טנבורי,", - "expected_phonemes": "u n aː d a | b ʔ i s e m | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 262.0, - "end": 266.2 - }, - "arabic": "بسّ ولا حدا ردّ، عشان أبو قاسم", - "hebrew_transliteration": "בַּס וַלַא חַדַא רַדّ, עַשַאן אַבּוּ קַאסֶם", - "hebrew": "אבל אף אחד לא ענה, כי אבו קאסם", - "expected_phonemes": "b a s | w a l a | ħ a d a | r a d d | ʕ a ʃ aː n | ʔ a b u | ʔ aː s e m" - }, - { - "voice_time_seconds": { - "start": 266.2, - "end": 269.4 - }, - "arabic": "ما كان في البيت بهداك الوقت.", - "hebrew_transliteration": "מַא כַּאן פִי לְ-בֵּית בְּהַדַאכּ לְ-וַקֶת.", - "hebrew": "לא היה בבית באותו זמן.", - "expected_phonemes": "m a | k aː n | f i | l b eː t | b h a d aː k | l w a ʔ e t" - }, - { - "voice_time_seconds": { - "start": 269.4, - "end": 273.2 - }, - "arabic": "الصّيّاد كان مصرّ يرجّع للطّنبوري كندرته.", - "hebrew_transliteration": "אִ-צַּיַّאד כַּאן מְצִרّ יְרַגֶّע לַ-טַּנבּוּרִי כֻּנדַרתוֹ.", - "hebrew": "הדייג התעקש להחזיר לא-טנבורי את נעליו.", - "expected_phonemes": "ʔ i sˤ a j j aː d | k aː n | m sˤ i r r | j r a g g e ʕ | l a tˤ a n b uː r i | k u n d a r t o" - }, - { - "voice_time_seconds": { - "start": 273.2, - "end": 275.0 - }, - "arabic": "اتطلّع حوالين البيت", - "hebrew_transliteration": "אִתטַלַّע חַוַאלֵין לְ-בֵּית", - "hebrew": "הביט סביב הבית", - "expected_phonemes": "ʔ i t tˤ a l l a ʕ | ħ a w aː l eː n | l b eː t" - }, - { - "voice_time_seconds": { - "start": 275.0, - "end": 278.8 - }, - "arabic": "وشاف إنّه واحد من الشّبابيك مفتوح.", - "hebrew_transliteration": "וּשַאף אִנּוֹ וַאחַד מִן שַּׁבַּאבִּיכּ מַפתוּח.", - "hebrew": "וראה שאחד החלונות פתוח.", - "expected_phonemes": "u ʃ aː f | ʔ i n o | w aː ħ a d | m i n | ʃ a b aː b iː k | m a f t uː ħ" - }, - { - "voice_time_seconds": { - "start": 278.8, - "end": 282.8 - }, - "arabic": "أخد الصّيّاد الكندرة ورماها من الشّبّاك.", - "hebrew_transliteration": "אַחַ'ד צַּיַّאד לְ-כֻּנדַרַה וּרַמַאהַא מִן שֻּׁבַּّאכּ.", - "hebrew": "הדייג לקח את הנעליים וזרק אותן מהחלון.", - "expected_phonemes": "ʔ a x a d | sˤ a j j aː d | l k u n d a r a | u r a m aː h a | m i n | ʃ u b b aː k" - }, - { - "voice_time_seconds": { - "start": 282.8, - "end": 286.0 - }, - "arabic": "وكان مبسوط إنّه نجح يرجّعها لصحابها.", - "hebrew_transliteration": "וּכַּאן מַבְּסוּט אִנּוֹ נִגֶ'ח יְרַגֶّעְהַא לַצְחַאבְּהַא.", - "hebrew": "והיה מרוצה שהצליח להחזיר אותן לבעליהן.", - "expected_phonemes": "u k aː n | m a b s uː tˤ | ʔ i n o | n i dʒ e ħ | j r a g g e ʕ h a | l a sˤ ħ aː b h a" - }, - { - "voice_time_seconds": { - "start": 287.0, - "end": 289.2 - }, - "arabic": "الكندرة طارت على قلب البيت", - "hebrew_transliteration": "אִל-כֻּנדַרַה טַארַת עַלַא קַלבּ לְ-בֵּית", - "hebrew": "הנעליים עפו לתוך הבית", - "expected_phonemes": "i l k u n d a r a | tˤ aː r a t | ʕ a l a | ʔ a l b | l b eː t" - }, - { - "voice_time_seconds": { - "start": 289.2, - "end": 292.0 - }, - "arabic": "دغري على الرّفّ إلّي حطّ عليه الطّنبوري", - "hebrew_transliteration": "דֻעְ'רִי עַלַא רַّףّ אִלִّי חַטّ עַלֵיה טַּנבּוּרִי", - "hebrew": "ישר על המדף שעליו שם א-טנבורי", - "expected_phonemes": "d u ɣ r i | ʕ a l a | r r a f f | ʔ i l l i | ħ a tˤ tˤ | ʕ a l eː h | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 292.0, - "end": 294.2 - }, - "arabic": "القزاز والماء ورد.", - "hebrew_transliteration": "אִל-קַזַאז וִל-מַאא וַרְד.", - "hebrew": "את הזכוכית ואת מי הוורדים.", - "expected_phonemes": "i l ʔ a z aː z | w i l m aː a | w a r d" - }, - { - "voice_time_seconds": { - "start": 294.2, - "end": 298.4 - }, - "arabic": "الرّفّ انكسر ومعه كلّ لبضاعة الغالية.", - "hebrew_transliteration": "אִ-רַّףّ אִנכַּסַר וּמַעוֹ כֻּלّ לֶ-בְּדַאעַה לְ-עַאלְיֶה.", - "hebrew": "המדף נשבר ואיתו כל הסחורה היקרה.", - "expected_phonemes": "ʔ i r r a f f | ʔ i n k a s a r | u m a ʕ o | k u l l | l e b d aː ʕ a | l ʕ aː l j e" - }, - { - "voice_time_seconds": { - "start": 298.4, - "end": 302.0 - }, - "arabic": "لمّا رجع الطّنبوري على بيته وشاف إلّي صار،", - "hebrew_transliteration": "לַמַّא רִגֶ'ע טַּנבּוּרִי עַלַא בֵּיתוֹ וּשַאף אִלִّי צַאר,", - "hebrew": "כשא-טנבורי חזר לביתו וראה מה שקרה,", - "expected_phonemes": "l a m m a | r i dʒ e ʕ | tˤ a n b uː r i | ʕ a l a | b eː t o | u ʃ aː f | ʔ i l l i | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 302.0, - "end": 305.6 - }, - "arabic": "مسك راسه وبلّش ينتّف شعراته ويصيّح:", - "hebrew_transliteration": "מִסֶכּ רַאסוֹ וּבַּלַّש יְנַתֶּף שַעְרַאתוֹ וִיצַיֶּח:", - "hebrew": "תפס את ראשו והתחיל לתלוש את שערותיו ולצעוק:", - "expected_phonemes": "m i s e k | r aː s o | u b a l l a ʃ | j n a t e f | ʃ a ʕ r aː t o | w iː sˤ a j e ħ" - }, - { - "voice_time_seconds": { - "start": 305.6, - "end": 308.8 - }, - "arabic": "«خرّبتلي بيتي هاي الكندرة الملعونة!", - "hebrew_transliteration": "חַ'רַّבַּת-לִי בֵּיתִי הַאי לְ-כֻּנדַרַה לְ-מַלעוּנֶה!", - "hebrew": "הנעליים הארורות האלה הרסו לי את הבית!", - "expected_phonemes": "x a r r a b a t l i | b eː t i | h aː i | l k u n d a r a | l m a l ʕ uː n e" - }, - { - "voice_time_seconds": { - "start": 308.8, - "end": 310.4 - }, - "arabic": "خلّتني على الحديدة!", - "hebrew_transliteration": "חַלַّתנִי עַלַא לְ-חַדִידֶה!", - "hebrew": "השאירו אותי בלי כלום!", - "expected_phonemes": "ħ a l l a t n i | ʕ a l a | l ħ a d iː d e" - }, - { - "voice_time_seconds": { - "start": 310.4, - "end": 312.8 - }, - "arabic": "انخرب بيتي، يا ناس!", - "hebrew_transliteration": "אִנחַ'רַבּ בֵּיתִי, יַא נַאס!", - "hebrew": "ביתי נחרב, אנשים!", - "expected_phonemes": "ʔ i n x a r a b | b eː t i | j a | n aː s" - }, - { - "voice_time_seconds": { - "start": 312.8, - "end": 315.4 - }, - "arabic": "شو أعمل، لوين أروح؟»", - "hebrew_transliteration": "שוּ אַעמֶל, לַוֵין אַרוּח?", - "hebrew": "מה אעשה, לאן אלך?", - "expected_phonemes": "ʃ u | ʔ a ʕ m e l | l a w eː n | ʔ a r uː ħ" - }, - { - "voice_time_seconds": { - "start": 316.2, - "end": 320.6 - }, - "arabic": "بعدين قام أبو قاسم وبلّش يحفر حفرة،", - "hebrew_transliteration": "בַּעדֵין קַאם אַבּוּ קַאסֶם וּבַּלַّש יִחפֻר חֻפְרַה,", - "hebrew": "אחר כך קם אבו קאסם והתחיל לחפור בור,", - "expected_phonemes": "b a ʕ d eː n | ʔ aː m | ʔ a b u | ʔ aː s e m | u b a l l a ʃ | j i ħ f u r | ħ u f r a" - }, - { - "voice_time_seconds": { - "start": 320.6, - "end": 322.6 - }, - "arabic": "عشان يدفن فيها الكندرة", - "hebrew_transliteration": "עַשַאן יִדפֻן פִיהַא לְ-כֻּנדַרַה", - "hebrew": "כדי לקבור בו את הנעליים", - "expected_phonemes": "ʕ a ʃ aː n | j i d f u n | f iː h a | l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 322.6, - "end": 324.8 - }, - "arabic": "ويتخلّص منها للأبد.", - "hebrew_transliteration": "וִיתחַ'לַّץ מִנְהַא לַל-אַבַּד.", - "hebrew": "ולהיפטר מהן לתמיד.", - "expected_phonemes": "w iː t x a l l a sˤ | m i n h a | l a l ʔ a b a d" - }, - { - "voice_time_seconds": { - "start": 324.8, - "end": 327.6 - }, - "arabic": "سمعوا الجّيران صوت الحفر بنصّ اللّيل،", - "hebrew_transliteration": "סִמעוּ גִּירַאן צוֹת לְ-חַפֶר בְּנֻצّ לֵّיל,", - "hebrew": "השכנים שמעו את קול החפירה באמצע הלילה,", - "expected_phonemes": "s i m ʕ u | g iː r aː n | sˤ oː t | l ħ a f e r | b n u sˤ sˤ | l l eː l" - }, - { - "voice_time_seconds": { - "start": 327.6, - "end": 329.2 - }, - "arabic": "وكانوا متأكّدين إنّه فيه حرامي", - "hebrew_transliteration": "וּכַּאנוּ מִתאַכְּדִין אִנּוֹ פִיה חַרַאמִי", - "hebrew": "והיו בטוחים שיש גנב", - "expected_phonemes": "u k aː n u | m i t ʔ a k d iː n | ʔ i n o | f iː h | ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 329.2, - "end": 331.6 - }, - "arabic": "عم بحاول يسرق واحد من لبيوت.", - "hebrew_transliteration": "עַם-בִּחַאוֶל יִסרֻק וַאחַד מִן לֶ-בְּיוּת.", - "hebrew": "שמנסה לפרוץ לאחד הבתים.", - "expected_phonemes": "ʕ a m b i ħ aː w e l | j i s r u ʔ | w aː ħ a d | m i n | l e b j uː t" - }, - { - "voice_time_seconds": { - "start": 332.4, - "end": 336.0 - }, - "arabic": "أجوا حرّاس الحاكم، مسكوا الطّنبوري،", - "hebrew_transliteration": "אַגוּ חֻרַّאס לְ-חַאכֶּם, מַסְכּוּ טַּנבּוּרִי,", - "hebrew": "שומרי המושל באו, תפסו את א-טנבורי,", - "expected_phonemes": "ʔ a g u | ħ u r r aː s | l ħ aː k e m | m a s k u | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 336.0, - "end": 339.6 - }, - "arabic": "حبسوه وغرّموه كمان مرّة.", - "hebrew_transliteration": "חַבַּסוּה וּעַ'רַّמוּה כַּמַאן מַרַّה.", - "hebrew": "כלאו אותו וקנסו אותו שוב.", - "expected_phonemes": "ħ a b a s uː h | u ɣ a r r a m uː h | k a m aː n | m a r r a" - }, - { - "voice_time_seconds": { - "start": 339.6, - "end": 342.0 - }, - "arabic": "لمّا طلع أبو القاسم من السّجن،", - "hebrew_transliteration": "לַמַّא טִלֶע אַבּוּ לְ-קַאסֶם מִן סִّגֶ'ן,", - "hebrew": "כשאבו אל-קאסם יצא מהכלא,", - "expected_phonemes": "l a m m a | tˤ i l e ʕ | ʔ a b u | l ʔ aː s e m | m i n | s s i dʒ e n" - }, - { - "voice_time_seconds": { - "start": 342.0, - "end": 343.8 - }, - "arabic": "كان كتير متعصبن", - "hebrew_transliteration": "כַּאן כְּתִיר מִתעַצֶّבּ", - "hebrew": "הוא היה כועס מאוד", - "expected_phonemes": "k aː n | k t iː r | m i t ʕ a sˤ sˤ e b" - }, - { - "voice_time_seconds": { - "start": 343.8, - "end": 346.0 - }, - "arabic": "وما وقّفش يفكّر كيف يتخلّص من الكندرة", - "hebrew_transliteration": "וּמַא וַקַّפ-ש יפַכַّר כִּיף יִתחַ'לַّץ מִן לְ-כֻּנדַרַה", - "hebrew": "ולא הפסיק לחשוב איך להיפטר מהנעליים", - "expected_phonemes": "u m a | w a ʔ ʔ a f ʃ | j f a k k a r | k iː f | j i t x a l l a sˤ | m i n | l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 346.0, - "end": 348.2 - }, - "arabic": "مرّة واحدة وللأبد.", - "hebrew_transliteration": "מַרַّה וַאחדֶה וּלַל-אַבַּד.", - "hebrew": "אחת ולתמיד.", - "expected_phonemes": "m a r r a | w aː ħ d e | u l a l ʔ a b a d" - }, - { - "voice_time_seconds": { - "start": 349.0, - "end": 351.8 - }, - "arabic": "في النّهاي قرّر يرميها في المجاري،", - "hebrew_transliteration": "פִי נַּהַאי קַרַّר יִרמִיהַא פִי לְ-מַגַ'ארִי,", - "hebrew": "בסוף החליט לזרוק אותן לביוב,", - "expected_phonemes": "f i | n a h aː i | ʔ a r r a r | j i r m iː h a | f i | l m a dʒ aː r i" - }, - { - "voice_time_seconds": { - "start": 351.8, - "end": 353.4 - }, - "arabic": "وهيك عمل.", - "hebrew_transliteration": "וּהֵיכּ עִמֶל.", - "hebrew": "וכך עשה.", - "expected_phonemes": "u h eː k | ʕ i m e l" - }, - { - "voice_time_seconds": { - "start": 353.4, - "end": 356.2 - }, - "arabic": "بعد فترة قصيرة إنسدّت المجاري", - "hebrew_transliteration": "בַּעֶד פַתרַה קַצִירֶה אִנסַדַّת לְ-מַגַ'ארִי", - "hebrew": "אחרי זמן קצר הביוב נסתם", - "expected_phonemes": "b a ʕ e d | f a t r a | ʔ a sˤ iː r e | ʔ i n s a d d a t | l m a dʒ aː r i" - }, - { - "voice_time_seconds": { - "start": 356.2, - "end": 359.4 - }, - "arabic": "وبلّشت تطلع ريحة عاطلة منها.", - "hebrew_transliteration": "וּבַּלַّשַת תִטלַע רִיחַה עַאטְלֶה מִנְהַא.", - "hebrew": "והתחיל לעלות ממנו ריח רע.", - "expected_phonemes": "u b a l l a ʃ a t | t i tˤ l a ʕ | r iː ħ a | ʕ aː tˤ l e | m i n h a" - }, - { - "voice_time_seconds": { - "start": 359.4, - "end": 362.8 - }, - "arabic": "أجوا النّاس وبلّشوا يفتّشوا عن السّبب.", - "hebrew_transliteration": "אַגוּ נַּאס וּבַּלַّשוּ יפַתְّשוּ עַן סַּבַּבּ.", - "hebrew": "האנשים באו והתחילו לחפש את הסיבה.", - "expected_phonemes": "ʔ a g u | n aː s | u b a l l a ʃ u | j f a t t ʃ u | ʕ a n | s a b a b" - }, - { - "voice_time_seconds": { - "start": 363.4, - "end": 365.8 - }, - "arabic": "في الآخر لاقوا الكندرة", - "hebrew_transliteration": "פִי לְ-אַאחֶ'ר לַאקוּ לְ-כֻּנדַרַה", - "hebrew": "בסוף מצאו את הנעליים", - "expected_phonemes": "f i | l ʔ aː x e r | l aː ʔ u | l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 365.8, - "end": 368.0 - }, - "arabic": "بقلب ماسورة المجاري،", - "hebrew_transliteration": "בְּקַלבּ מַאסוּרֶת לְ-מַגַ'ארִי,", - "hebrew": "בתוך צינור הביוב,", - "expected_phonemes": "b ʔ a l b | m aː s uː r e t | l m a dʒ aː r i" - }, - { - "voice_time_seconds": { - "start": 368.0, - "end": 371.6 - }, - "arabic": "وعرفوها دغري إنّها كندرة الطّنبوري.", - "hebrew_transliteration": "וּעִרְפוּהַא דֻעְ'רִי אִנְّהַא כֻּנדַרֶת טַּנבּוּרִי.", - "hebrew": "וזיהו מיד שאלה הנעליים של א-טנבורי.", - "expected_phonemes": "u ʕ i r f uː h a | d u ɣ r i | ʔ i n n h a | k u n d a r e t | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 371.6, - "end": 373.4 - }, - "arabic": "راحوا وحكوا للحاكم.", - "hebrew_transliteration": "רַאחוּ וּחַכוּ לַל-חַאכֶּם.", - "hebrew": "הלכו וסיפרו למושל.", - "expected_phonemes": "r aː ħ u | u ħ a k u | l a l ħ aː k e m" - }, - { - "voice_time_seconds": { - "start": 373.4, - "end": 376.0 - }, - "arabic": "الحاكم أمر يجيبوه،", - "hebrew_transliteration": "אִל-חַאכֶּם אַמַר יְגִ'יבּוּה,", - "hebrew": "המושל ציווה להביא אותו,", - "expected_phonemes": "i l ħ aː k e m | ʔ a m a r | j dʒ iː b uː h" - }, - { - "voice_time_seconds": { - "start": 376.0, - "end": 380.2 - }, - "arabic": "سجنه وجبره يصلّح المجاري وينظّفها", - "hebrew_transliteration": "סַגַ'נוּה וּגַ'בַּרוּה יְצַלֶّח לְ-מַגַ'ארִי וִינַדֶّ'פְהַא", - "hebrew": "כלא אותו והכריח אותו לתקן את הביוב ולנקות אותו", - "expected_phonemes": "s a dʒ a n uː h | u dʒ a b a r uː h | j sˤ a l l e ħ | l m a dʒ aː r i | w iː n a dˤ dˤ e f h a" - }, - { - "voice_time_seconds": { - "start": 380.2, - "end": 383.4 - }, - "arabic": "ودفّعوه غرامة إضافيّة.", - "hebrew_transliteration": "וּדַפַّעוּה עַ'רַאמֶה אִדַ'אפִיֶّה.", - "hebrew": "והטילו עליו קנס נוסף.", - "expected_phonemes": "u d a f f a ʕ uː h | ɣ a r aː m e | ʔ i dˤ aː f i j j e" - }, - { - "voice_time_seconds": { - "start": 383.4, - "end": 385.0 - }, - "arabic": "مع نهاية فترة الحبس،", - "hebrew_transliteration": "מַע נִהַאיֶת פַתרֶת לְ-חַבֶּס,", - "hebrew": "עם סוף תקופת המאסר,", - "expected_phonemes": "m a ʕ | n i h aː j e t | f a t r e t | l ħ a b e s" - }, - { - "voice_time_seconds": { - "start": 385.0, - "end": 387.2 - }, - "arabic": "أخذ أبو قاسم كندرته", - "hebrew_transliteration": "אַחַ'ד אַבּוּ קַאסֶם כֻּנדַרתוֹ", - "hebrew": "אבו קאסם לקח את נעליו", - "expected_phonemes": "ʔ a x a d | ʔ a b u | ʔ aː s e m | k u n d a r t o" - }, - { - "voice_time_seconds": { - "start": 387.2, - "end": 388.8 - }, - "arabic": "وحطّها على سطح بيته", - "hebrew_transliteration": "וּחַטַّהַא עַלַא סַטֶח בֵּיתוֹ", - "hebrew": "ושם אותן על גג ביתו", - "expected_phonemes": "u ħ a tˤ tˤ a h a | ʕ a l a | s a tˤ e ħ | b eː t o" - }, - { - "voice_time_seconds": { - "start": 388.8, - "end": 391.8 - }, - "arabic": "عشان تنشف من الشّمس.", - "hebrew_transliteration": "עַשַאן תִנשַף מִן שַּׁמֶס.", - "hebrew": "כדי שיתייבשו בשמש.", - "expected_phonemes": "ʕ a ʃ aː n | t i n ʃ a f | m i n | ʃ a m e s" - }, - { - "voice_time_seconds": { - "start": 391.8, - "end": 395.6 - }, - "arabic": "شافها كلب وفكّرها بسّة ميتة.", - "hebrew_transliteration": "שַאפְהַא כַּלבּ וּפַכַּרְהַא בִּסֶّה מַיְתֶה.", - "hebrew": "כלב ראה אותן וחשב שהן חתולה מתה.", - "expected_phonemes": "ʃ aː f h a | k a l b | u f a k a r h a | b i s s e | m a j t e" - }, - { - "voice_time_seconds": { - "start": 395.6, - "end": 397.6 - }, - "arabic": "أخذها بتمّه وحملها", - "hebrew_transliteration": "אַחַ'דְהַא בְּתֻמּוֹ וּחִמְלְהַא", - "hebrew": "לקח אותן בפיו ונשא אותן", - "expected_phonemes": "ʔ a x a d h a | b t u m o | u ħ i m l h a" - }, - { - "voice_time_seconds": { - "start": 398.4, - "end": 402.4 - }, - "arabic": "وفجأة وقعت من بين سنانه على الشّارع،", - "hebrew_transliteration": "וּפַגְ'אַה וִקעַת מִן בֵּין סְנַאנוֹ עַלַא שַּׁארֶע,", - "hebrew": "ופתאום הן נפלו מבין שיניו לרחוב,", - "expected_phonemes": "u f a dʒ ʔ a | w i ʔ ʕ a t | m i n | b eː n | s n aː n o | ʕ a l a | ʃ aː r e ʕ" - }, - { - "voice_time_seconds": { - "start": 402.4, - "end": 406.6 - }, - "arabic": "رأسًا على راس زلمة كان مارق من هناك.", - "hebrew_transliteration": "רַאסַן עַלַא רַאס זַלַמֶה כַּאן מַארֶק מִן הֻנַאכּ.", - "hebrew": "ישר על ראשו של אדם שעבר שם.", - "expected_phonemes": "r aː s a n | ʕ a l a | r aː s | z a l a m e | k aː n | m aː r e ʔ | m i n | h u n aː k" - }, - { - "voice_time_seconds": { - "start": 406.6, - "end": 409.4 - }, - "arabic": "الزّلمة انجرح جرح غميق،", - "hebrew_transliteration": "אִ-זַּלַמֶה אִנגַ'רַח גֻ'רֶח עַ'מִיק,", - "hebrew": "האיש נפצע פצע עמוק,", - "expected_phonemes": "ʔ i z a l a m e | ʔ i n dʒ a r a ħ | dʒ u r e ħ | ɣ a m iː ʔ" - }, - { - "voice_time_seconds": { - "start": 409.4, - "end": 413.2 - }, - "arabic": "والنّاس تجمّعت حواليه وعرفوا دغري", - "hebrew_transliteration": "וִ-נַּאס תְגַ'מַّעַת חַוַאלֵיה וּעִרְפוּ דֻעְ'רִי", - "hebrew": "והאנשים התאספו סביבו וזיהו מיד", - "expected_phonemes": "w i n aː s | t dʒ a m m a ʕ a t | ħ a w aː l eː h | u ʕ i r f u | d u ɣ r i" - }, - { - "voice_time_seconds": { - "start": 413.2, - "end": 416.4 - }, - "arabic": "إنّه هاي كانت كندرة الطّنبوري", - "hebrew_transliteration": "אִנּוֹ הַאי כַּאנַת כֻּנדַרֶת טַּנבּוּרִי", - "hebrew": "שאלה היו הנעליים של א-טנבורי", - "expected_phonemes": "ʔ i n o | h aː i | k aː n a t | k u n d a r e t | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 416.4, - "end": 420.8 - }, - "arabic": "إلّي وقعت من السّطح وأدّت للجرح.", - "hebrew_transliteration": "אִלִّי וִקעַת מִן סַּטֶח וּאַדַّת לַל-גֻ'רֶח.", - "hebrew": "שנפלו מהגג וגרמו לפציעה.", - "expected_phonemes": "ʔ i l l i | w i ʔ ʕ a t | m i n | s a tˤ e ħ | u ʔ a d d a t | l a l dʒ u r e ħ" - }, - { - "voice_time_seconds": { - "start": 420.8, - "end": 424.8 - }, - "arabic": "راحت النّاس للحاكم وحكتله إلّي صار،", - "hebrew_transliteration": "רַאחַת נַּאס לַל-חַאכֶּם וּחַכַּת-לוֹ אִלִّי צַאר,", - "hebrew": "האנשים הלכו למושל וסיפרו לו מה שקרה,", - "expected_phonemes": "r aː ħ a t | n aː s | l a l ħ aː k e m | u ħ a k a t l o | ʔ i l l i | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 424.8, - "end": 430.2 - }, - "arabic": "وهوّ حمّل الطّنبوري بكلّ احتياجات الزّلمة", - "hebrew_transliteration": "וּהֻוֶّ חַמַّל טַּנבּוּרִי בְּכֻלّ אִחתִיַאגַ'את זַּלַמֶה", - "hebrew": "והוא הטיל על א-טנבורי את כל צרכי האיש", - "expected_phonemes": "u h u w w e | ħ a m m a l | tˤ a n b uː r i | b k u l l | ʔ i ħ t i j aː dʒ aː t | z a l a m e" - }, - { - "voice_time_seconds": { - "start": 430.2, - "end": 432.2 - }, - "arabic": "كلّ فترة مرضه،", - "hebrew_transliteration": "כֻּלّ פַתרֶת מַרַדוֹ,", - "hebrew": "כל תקופת מחלתו,", - "expected_phonemes": "k u l l | f a t r e t | m a r a d o" - }, - { - "voice_time_seconds": { - "start": 432.2, - "end": 435.6 - }, - "arabic": "لحدّ ما بقي معه مصاري", - "hebrew_transliteration": "לַחַדّ מַא בְּקִי מַעוֹ מַצַארִי", - "hebrew": "עד שלא נשאר לו כסף", - "expected_phonemes": "l a ħ a d d | m a | b ʔ i | m a ʕ o | m a sˤ aː r i" - }, - { - "voice_time_seconds": { - "start": 435.6, - "end": 438.8 - }, - "arabic": "وطفر على الآخر.", - "hebrew_transliteration": "וּטִפֶר עַלַא לְ-אַאחַ'ר.", - "hebrew": "והתרושש לגמרי.", - "expected_phonemes": "u tˤ i f e r | ʕ a l a | l ʔ aː x a r" - }, - { - "voice_time_seconds": { - "start": 438.8, - "end": 440.8 - }, - "arabic": "شو عمل الطّنبوري؟", - "hebrew_transliteration": "שוּ עִמֶל טַּנבּוּרִי?", - "hebrew": "מה עשה א-טנבורי?", - "expected_phonemes": "ʃ u | ʕ i m e l | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 440.8, - "end": 443.8 - }, - "arabic": "أخد الكندرة وراح فيها على المحكمة.", - "hebrew_transliteration": "אַחַ'ד אִל-כֻּנדַרַה וּרַאח פִיהַא עַלַא לְ-מַחכַּמֶה.", - "hebrew": "לקח את הנעליים והלך איתן לבית המשפט.", - "expected_phonemes": "ʔ a x a d | i l k u n d a r a | u r aː ħ | f iː h a | ʕ a l a | l m a ħ k a m e" - }, - { - "voice_time_seconds": { - "start": 445.0, - "end": 448.2 - }, - "arabic": "توجّه للقاضي بدموع وحكاله:", - "hebrew_transliteration": "תְוַגַّה לַל-קַאדִי בִּדמוּע וּחַכַא-לוֹ:", - "hebrew": "פנה אל השופט בדמעות ואמר לו:", - "expected_phonemes": "t w a g g a | l a l ʔ aː d i | b i d m uː ʕ | u ħ a k aː l o" - }, - { - "voice_time_seconds": { - "start": 448.2, - "end": 449.8 - }, - "arabic": "«سيدي القاضي،", - "hebrew_transliteration": "סִידִי לְ-קַאדִי,", - "hebrew": "אדוני השופט,", - "expected_phonemes": "s iː d i | l ʔ aː d i" - }, - { - "voice_time_seconds": { - "start": 449.8, - "end": 451.6 - }, - "arabic": "بدّي تكتبلي طلاق شرعي", - "hebrew_transliteration": "בַּדִّי תִכּתֶבּ-לִי טַלַאק שַרעִי", - "hebrew": "אני רוצה שתכתוב לי גט כדין", - "expected_phonemes": "b a d d i | t i k t e b l i | tˤ a l aː ʔ | ʃ a r ʕ i" - }, - { - "voice_time_seconds": { - "start": 451.6, - "end": 454.0 - }, - "arabic": "بيني وبين هاي الكندرة.", - "hebrew_transliteration": "בֵּינִי וּבֵּין הַאי לְ-כֻּנדַרַה.", - "hebrew": "ביני לבין הנעליים האלה.", - "expected_phonemes": "b eː n i | u b eː n | h aː i | l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 454.0, - "end": 455.4 - }, - "arabic": "واعمل معروف،", - "hebrew_transliteration": "וּאִעמֶל מַערוּף,", - "hebrew": "ועשה טובה,", - "expected_phonemes": "u ʔ i ʕ m e l | m a ʕ r uː f" - }, - { - "voice_time_seconds": { - "start": 455.4, - "end": 457.0 - }, - "arabic": "اكتب إنّها مش إلي", - "hebrew_transliteration": "אִכּתֶבּ אִנְّהַא מִש אִלִי", - "hebrew": "כתוב שהן לא שלי", - "expected_phonemes": "ʔ i k t e b | ʔ i n n h a | m i ʃ | ʔ i l i" - }, - { - "voice_time_seconds": { - "start": 457.0, - "end": 458.8 - }, - "arabic": "وإنّه أنا مش إلها.", - "hebrew_transliteration": "וּאִנּוֹ אַנַא מִש אִלְהַא.", - "hebrew": "ושאני לא שלהן.", - "expected_phonemes": "u ʔ i n o | ʔ a n a | m i ʃ | i l h a" - }, - { - "voice_time_seconds": { - "start": 458.8, - "end": 460.8 - }, - "arabic": "واكتب يا سيدي كمان،", - "hebrew_transliteration": "וּאִכּתֶבּ יַא סִידִי כַּמַאן,", - "hebrew": "וכתוב גם, אדוני,", - "expected_phonemes": "u ʔ i k t e b | j a | s iː d i | k a m aː n" - }, - { - "voice_time_seconds": { - "start": 460.8, - "end": 462.4 - }, - "arabic": "إنّه من اليوم ورايح", - "hebrew_transliteration": "אִנּוֹ מִן לְ-יוֹם וּרַאיֶח", - "hebrew": "שמהיום והלאה", - "expected_phonemes": "ʔ i n o | m i n | l j oː m | u r aː j e ħ" - }, - { - "voice_time_seconds": { - "start": 462.4, - "end": 465.4 - }, - "arabic": "أنا مش مسؤول عن هاي الكندرة", - "hebrew_transliteration": "אַנַא מִש מַסאוּל עַן הַאי לְ-כֻּנדַרַה", - "hebrew": "אני לא אחראי לנעליים האלה", - "expected_phonemes": "ʔ a n a | m i ʃ | m a s aː uː l | ʕ a n | h aː i | l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 465.4, - "end": 468.0 - }, - "arabic": "ولا عن أعمالها.»", - "hebrew_transliteration": "וַלַא עַן אַעמַאלְהַא.", - "hebrew": "ולא למעשיהן.", - "expected_phonemes": "w a l a | ʕ a n | ʔ a ʕ m aː l h a" - }, - { - "voice_time_seconds": { - "start": 468.0, - "end": 471.8 - }, - "arabic": "وحكى للقاضي كلّ قصّة الكندرة.", - "hebrew_transliteration": "וּחַכַא לַל-קַאדִי כֻּלّ קִצֶّת לְ-כֻּנדַרַה.", - "hebrew": "וסיפר לשופט את כל סיפור הנעליים.", - "expected_phonemes": "u ħ a k a | l a l ʔ aː d i | k u l l | ʔ i sˤ sˤ e t | l k u n d a r a" - }, - { - "voice_time_seconds": { - "start": 471.8, - "end": 474.6 - }, - "arabic": "ضحك القاضي وأعطى الطّنبوري", - "hebrew_transliteration": "דִ'חֶכּ לְ-קַאדִי וּאַעטַא טַּנבּוּרִי", - "hebrew": "השופט צחק ונתן לא-טנבורי", - "expected_phonemes": "dˤ i ħ e k | l ʔ aː d i | u ʔ a ʕ tˤ a | tˤ a n b uː r i" - }, - { - "voice_time_seconds": { - "start": 474.6, - "end": 480.4 - }, - "arabic": "أمر طلاق متل ما طلب.", - "hebrew_transliteration": "אַמֶר טַלַאק מִתֶל מַא טַלַבּ.", - "hebrew": "צו גירושין כמו שביקש.", - "expected_phonemes": "ʔ a m e r | tˤ a l aː ʔ | m i t e l | m a | tˤ a l a b" - } - ] - }, - { - "file_name": "עלי באבא וארבעים השודדים * עלי באבא ול-ארבעין חראמי * علي بابا والأربعين حرامي * סיפור בערבית מדוברת [mOf3UpJF3iY].mkv", - "youtube_id": "mOf3UpJF3iY", - "title_hebrew": "עלי באבא וארבעים השודדים", - "segments": [ - { - "voice_time_seconds": { - "start": 1.4, - "end": 6.0 - }, - "arabic": "علي بابا والأربعين حرامي", - "hebrew_transliteration": "עַלִי בַּאבַּא וִל-אַרבַּעִין חַרַאמִי", - "hebrew": "עלי באבא וארבעים השודדים", - "expected_phonemes": "ʕ a l i | b aː b a | w i l ʔ a r b a ʕ iː n | ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 6.8, - "end": 10.0 - }, - "arabic": "كان ياما كان في قديم الزّمان،", - "hebrew_transliteration": "כַּאן יַא מַא כַּאן פִי קַדִים אִ-זַّמַאן,", - "hebrew": "היה היה בימים עברו,", - "expected_phonemes": "k aː n | j a | m a | k aː n | f i | ʔ a d iː m | ʔ i z z a m aː n" - }, - { - "voice_time_seconds": { - "start": 10.0, - "end": 13.2 - }, - "arabic": "واحد اسمه علي بابا.", - "hebrew_transliteration": "וַאחַד אִסמוֹ עַלִי בַּאבַּא.", - "hebrew": "אחד ששמו עלי באבא.", - "expected_phonemes": "w aː ħ a d | ʔ i s m o | ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 13.2, - "end": 15.4 - }, - "arabic": "علي بابا كان حطّاب فقير", - "hebrew_transliteration": "עַלִי בַּאבַּא כַּאן חַטַّאבּ פַקִיר", - "hebrew": "עלי באבא היה חוטב עצים עני", - "expected_phonemes": "ʕ a l i | b aː b a | k aː n | ħ a tˤ tˤ aː b | f a ʔ iː r" - }, - { - "voice_time_seconds": { - "start": 15.4, - "end": 18.4 - }, - "arabic": "بشتغل في تحطيب الشّجر في الغابة", - "hebrew_transliteration": "בִּשתְעֶ'ל פִי תַחטִיבּ שַּׁגַ'ר פִי לְ-עַ'אבֶּה", - "hebrew": "שעבד בחטיבת עצים ביער", - "expected_phonemes": "b i ʃ t ɣ e l | f i | t a ħ tˤ iː b | ʃ a dʒ a r | f i | l ɣ aː b e" - }, - { - "voice_time_seconds": { - "start": 18.4, - "end": 21.0 - }, - "arabic": "وبروح ببيعه في سوق القرية", - "hebrew_transliteration": "וּבִּרוּח בִּבִּיעוֹ פִי סוּק לְ-קַרְיֶה", - "hebrew": "והולך למכור אותם בשוק הכפר", - "expected_phonemes": "u b i r uː ħ | b i b iː ʕ o | f i | s uː ʔ | l ʔ a r j e" - }, - { - "voice_time_seconds": { - "start": 21.0, - "end": 24.4 - }, - "arabic": "عشان يكسب لقمة عيشه.", - "hebrew_transliteration": "עַשַאן יִכּסַבּ לֻקמֶת עִישוֹ.", - "hebrew": "כדי להרוויח את לחמו.", - "expected_phonemes": "ʕ a ʃ aː n | j i k s a b | l u ʔ m e t | ʕ iː ʃ o" - }, - { - "voice_time_seconds": { - "start": 24.4, - "end": 25.6 - }, - "arabic": "في يوم من الأيّام", - "hebrew_transliteration": "פִי יוֹם מִן לְ-אַיַّאם", - "hebrew": "יום אחד", - "expected_phonemes": "f i | j oː m | m i n | l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 25.6, - "end": 27.8 - }, - "arabic": "لمّا كان علي بابا في الغابة", - "hebrew_transliteration": "לַמַّא כַּאן עַלִי בַּאבַּא פִי לְ-עַ'אבֶּה", - "hebrew": "כשעלי באבא היה ביער", - "expected_phonemes": "l a m m a | k aː n | ʕ a l i | b aː b a | f i | l ɣ aː b e" - }, - { - "voice_time_seconds": { - "start": 27.8, - "end": 29.4 - }, - "arabic": "عم بجمع حطب،", - "hebrew_transliteration": "עַם-בִּגְ'מַע חַטַבּ,", - "hebrew": "ואסף עצים,", - "expected_phonemes": "ʕ a m b i dʒ m a ʕ | ħ a tˤ a b" - }, - { - "voice_time_seconds": { - "start": 29.4, - "end": 32.2 - }, - "arabic": "شاف مجموعة زلام كبيرة", - "hebrew_transliteration": "שַאף מַגְ'מוּעַת זְלַאם כְּבִּירֶה", - "hebrew": "ראה קבוצה גדולה של אנשים", - "expected_phonemes": "ʃ aː f | m a dʒ m uː ʕ a t | z l aː m | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 32.2, - "end": 37.4 - }, - "arabic": "ومعها حمير محمّلة على ضهورها كياس كبيرة.", - "hebrew_transliteration": "וּמַעְהַא חַמִיר מְחַמַّלֶה עַלַא דְ'הוּרְהַא כְּיַאס כְּבִּירֶה.", - "hebrew": "ואיתם חמורים נושאים על גבם שקים גדולים.", - "expected_phonemes": "u m a ʕ h a | ħ a m iː r | m ħ a m m a l e | ʕ a l a | dˤ h uː r h a | k j aː s | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 37.4, - "end": 42.6 - }, - "arabic": "على راس هاي المجموعة كان فيه زلمة كبير وقوي.", - "hebrew_transliteration": "עַלַא רַאס הַאי לְ-מַגְ'מוּעַה כַּאן פִיה זַלַמֶה כְּבִּיר וּקַוִי.", - "hebrew": "בראש הקבוצה הזאת עמד איש גדול וחזק.", - "expected_phonemes": "ʕ a l a | r aː s | h aː i | l m a dʒ m uː ʕ a | k aː n | f iː h | z a l a m e | k b iː r | u ʔ a w i" - }, - { - "voice_time_seconds": { - "start": 42.6, - "end": 48.4 - }, - "arabic": "هالزّلمة كان يمشي بقلب الغابة وعارف طريقه منيح.", - "hebrew_transliteration": "הַ-זַּלַמֶה כַּאן יִמשִי בְּקַלבּ לְ-עַ'אבֶּה וּעַארֶף טַרִיקוֹ מְנִיח.", - "hebrew": "האיש הזה הלך בלב היער והכיר היטב את דרכו.", - "expected_phonemes": "h a z a l a m e | k aː n | j i m ʃ i | b ʔ a l b | l ɣ aː b e | u ʕ aː r e f | tˤ a r iː ʔ o | m n iː ħ" - }, - { - "voice_time_seconds": { - "start": 48.4, - "end": 53.0 - }, - "arabic": "علي بابا تخبّى ورا شجرة وقعد يتطلّع", - "hebrew_transliteration": "עַלִי בַּאבַּא תְחַ'בַּא וַרַא שַגַ'רַה וּקַעַד יִתטַלַّע", - "hebrew": "עלי באבא התחבא מאחורי עץ וישב להסתכל", - "expected_phonemes": "ʕ a l i | b aː b a | t x a b a | w a r a | ʃ a dʒ a r a | u ʔ a ʕ a d | j i t tˤ a l l a ʕ" - }, - { - "voice_time_seconds": { - "start": 53.0, - "end": 57.0 - }, - "arabic": "لوين هاد الزّلمة رايح هوّ وزلامه.", - "hebrew_transliteration": "לַוֵין הַאד זַּלַמֶה רַאיֶח הֻוֶّ וּזְלַאמוֹ.", - "hebrew": "לאן האיש הזה הולך הוא ואנשיו.", - "expected_phonemes": "l a w eː n | h aː d | z a l a m e | r aː j e ħ | h u w w e | u z l aː m o" - }, - { - "voice_time_seconds": { - "start": 57.0, - "end": 62.0 - }, - "arabic": "فجأة وقّف الزّلمة وفريقه قبال حجر كبير،", - "hebrew_transliteration": "פַגְ'אַה וַקַّף זַּלַמֶה וּפַרִיקוֹ קְבַּאל חַגַ'ר כְּבִּיר,", - "hebrew": "פתאום האיש וחבורתו עצרו מול סלע גדול,", - "expected_phonemes": "f a dʒ ʔ a | w a ʔ ʔ a f | z a l a m e | u f a r iː ʔ o | ʔ b aː l | ħ a dʒ a r | k b iː r" - }, - { - "voice_time_seconds": { - "start": 62.0, - "end": 64.8 - }, - "arabic": "والزّلمة قال بصوت عالي:", - "hebrew_transliteration": "וּ-זַּלַמֶה קַאל בְּצוֹת עַאלִי:", - "hebrew": "והאיש אמר בקול רם:", - "expected_phonemes": "u z a l a m e | ʔ aː l | b sˤ oː t | ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 64.8, - "end": 67.4 - }, - "arabic": "«افتح يا سمسم»", - "hebrew_transliteration": "אִפתַח יַא סִמסֶם", - "hebrew": "היפתח, סומסום", - "expected_phonemes": "ʔ i f t a ħ | j a | s i m s e m" - }, - { - "voice_time_seconds": { - "start": 67.4, - "end": 71.0 - }, - "arabic": "ومرّة واحدة تحرّك الحجر الكبير", - "hebrew_transliteration": "וּמַרַّה וַאחדֶה תְחַרַّכּ לְ-חַגַ'ר לֶ-כְּבִּיר", - "hebrew": "ובבת אחת זז הסלע הגדול", - "expected_phonemes": "u m a r r a | w aː ħ d e | t ħ a r r a k | l ħ a dʒ a r | l e k b iː r" - }, - { - "voice_time_seconds": { - "start": 71.0, - "end": 74.2 - }, - "arabic": "وبيّنت مغارة كبيرة.", - "hebrew_transliteration": "וּבַּיַّנַת מַעַ'ארַה כְּבִּירֶה.", - "hebrew": "ונגלתה מערה גדולה.", - "expected_phonemes": "u b a j j a n a t | m a ɣ aː r a | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 74.2, - "end": 76.4 - }, - "arabic": "بعد ما فتحت المغارة،", - "hebrew_transliteration": "בַּעֶד מַא פַתחַת לְ-מַעַ'ארַה,", - "hebrew": "אחרי שהמערה נפתחה,", - "expected_phonemes": "b a ʕ e d | m a | f a t ħ a t | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 76.4, - "end": 81.6 - }, - "arabic": "فاتوا عليها الزّلام مع حميرهم واحد ورا التّاني", - "hebrew_transliteration": "פַאתוּ עַלֵיהַא לֶ-זְלַאם מַע חַמִירְהֹם וַאחַד וַרַא תַּאנִי", - "hebrew": "האנשים נכנסו אליה עם חמוריהם אחד אחרי השני", - "expected_phonemes": "f aː t u | ʕ a l eː h a | l e z l aː m | m a ʕ | ħ a m iː r h o m | w aː ħ a d | w a r a | t aː n i" - }, - { - "voice_time_seconds": { - "start": 81.6, - "end": 83.8 - }, - "arabic": "واختفوا بقلبها.", - "hebrew_transliteration": "וּחְ'תַפוּ בְּקַלבְּהַא.", - "hebrew": "ונעלמו בתוכה.", - "expected_phonemes": "u x t a f u | b ʔ a l b h a" - }, - { - "voice_time_seconds": { - "start": 83.8, - "end": 85.8 - }, - "arabic": "بعد ما فاتوا كلّهم،", - "hebrew_transliteration": "בַּעֶד מַא פַאתוּ כֻּלְّהֹם,", - "hebrew": "אחרי שכולם נכנסו,", - "expected_phonemes": "b a ʕ e d | m a | f aː t u | k u l l h o m" - }, - { - "voice_time_seconds": { - "start": 85.8, - "end": 90.4 - }, - "arabic": "رجع الحجر يتحرّك وسكّر باب المغارة.", - "hebrew_transliteration": "רִגֶ'ע לְ-חַגַ'ר יִתחַרַّכּ וּסַכַّר בַּאבּ לְ-מַעַ'ארַה.", - "hebrew": "הסלע שוב זז וסגר את פתח המערה.", - "expected_phonemes": "r i dʒ e ʕ | l ħ a dʒ a r | j i t ħ a r r a k | u s a k k a r | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 90.4, - "end": 95.2 - }, - "arabic": "علي بابا صفن وما فهم شو إلّي عم بصير،", - "hebrew_transliteration": "עַלִי בַּאבַּא צֻפֶן וּמַא פִהֶם שוּ אִלִّי עַם-בִּצִיר,", - "hebrew": "עלי באבא בהה ולא הבין מה קורה,", - "expected_phonemes": "ʕ a l i | b aː b a | sˤ u f e n | u m a | f i h e m | ʃ u | ʔ i l l i | ʕ a m b i sˤ iː r" - }, - { - "voice_time_seconds": { - "start": 95.2, - "end": 97.8 - }, - "arabic": "عشان هيك ضلّ يستنّى", - "hebrew_transliteration": "עַשַאן הֵיכּ דַ'לّ יִסתַנַّא", - "hebrew": "לכן המשיך לחכות", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | dˤ a l l | j i s t a n n a" - }, - { - "voice_time_seconds": { - "start": 97.8, - "end": 101.6 - }, - "arabic": "لحدّ ما يرجعوا يطلعوا الزّلام من المغارة", - "hebrew_transliteration": "לַחַדّ מַא יִרגַ'עוּ יִטלַעוּ זְּלַאם מִן לְ-מַעַ'ארַה", - "hebrew": "עד שהאנשים יחזרו ויצאו מהמערה", - "expected_phonemes": "l a ħ a d d | m a | j i r dʒ a ʕ u | j i tˤ l a ʕ u | z l aː m | m i n | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 101.6, - "end": 104.6 - }, - "arabic": "ويفوت يتفقّد بنفسه.", - "hebrew_transliteration": "וִיפוּת יִתפַקַّד בְּנַפסוֹ.", - "hebrew": "וייכנס לבדוק בעצמו.", - "expected_phonemes": "w iː f uː t | j i t f a ʔ ʔ a d | b n a f s o" - }, - { - "voice_time_seconds": { - "start": 104.6, - "end": 106.2 - }, - "arabic": "بعد فترة", - "hebrew_transliteration": "בַּעֶד פַתרַה", - "hebrew": "אחרי זמן מה", - "expected_phonemes": "b a ʕ e d | f a t r a" - }, - { - "voice_time_seconds": { - "start": 106.2, - "end": 109.8 - }, - "arabic": "رجع يتحرّك الحجر وفتحت المغارة،", - "hebrew_transliteration": "רִגֶ'ע יִתחַרַّכּ לְ-חַגַ'ר וּפַתחַת לְ-מַעַ'ארַה,", - "hebrew": "הסלע שוב זז והמערה נפתחה,", - "expected_phonemes": "r i dʒ e ʕ | j i t ħ a r r a k | l ħ a dʒ a r | u f a t ħ a t | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 109.8, - "end": 113.4 - }, - "arabic": "طلعوا منها كلّ الزّلام مع الحمير.", - "hebrew_transliteration": "טִלעוּ מִנְהַא כֻּלّ זְּלַאם מַע לְ-חַמִיר.", - "hebrew": "כל האנשים יצאו ממנה עם החמורים.", - "expected_phonemes": "tˤ i l ʕ u | m i n h a | k u l l | z l aː m | m a ʕ | l ħ a m iː r" - }, - { - "voice_time_seconds": { - "start": 113.4, - "end": 117.0 - }, - "arabic": "بسّ كانوا هاي المرّة بدون كياس.", - "hebrew_transliteration": "בַּס כַּאנוּ הַאי לְ-מַרַّה בְּדוּן כְּיַאס.", - "hebrew": "אבל הפעם הם היו בלי שקים.", - "expected_phonemes": "b a s | k aː n u | h aː i | l m a r r a | b i d uː n | k j aː s" - }, - { - "voice_time_seconds": { - "start": 117.0, - "end": 121.6 - }, - "arabic": "بعد ما تأكّد رئيس الزّلام انّه كلّ زلامه طلعوا،", - "hebrew_transliteration": "בַּעֶד מַא תְאַכַּّד רַאִיס זְּלַאם אִנּוֹ כֻּלّ זְלַאמוֹ טִלעוּ,", - "hebrew": "אחרי שראש האנשים וידא שכל אנשיו יצאו,", - "expected_phonemes": "b a ʕ e d | m a | t ʔ a k k a d | r a ʔ iː s | z l aː m | ʔ i n o | k u l l | z l aː m o | tˤ i l ʕ u" - }, - { - "voice_time_seconds": { - "start": 121.6, - "end": 124.0 - }, - "arabic": "وقّف قبال باب المغارة", - "hebrew_transliteration": "וַקַّף קְבַּאל בַּאבּ לְ-מַעַ'ארַה", - "hebrew": "עמד מול פתח המערה", - "expected_phonemes": "w a ʔ ʔ a f | ʔ b aː l | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 124.0, - "end": 126.0 - }, - "arabic": "وقال بصوت عالي:", - "hebrew_transliteration": "וּקַאל בְּצוֹת עַאלִי:", - "hebrew": "ואמר בקול רם:", - "expected_phonemes": "u ʔ aː l | b sˤ oː t | ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 126.0, - "end": 128.6 - }, - "arabic": "«سكّر يا سمسم»", - "hebrew_transliteration": "סַכַّר יַא סִמסֶם", - "hebrew": "היסגר, סומסום", - "expected_phonemes": "s a k k a r | j a | s i m s e m" - }, - { - "voice_time_seconds": { - "start": 128.6, - "end": 133.2 - }, - "arabic": "وبعدها كلّهم مشيوا وراحوا.", - "hebrew_transliteration": "וּבַּעֶדהַא כֻּלְّהֹם מִשיוּ וּרַאחוּ.", - "hebrew": "ואחר כך כולם הלכו להם.", - "expected_phonemes": "u b a ʕ e d h a | k u l l h o m | m i ʃ j u | u r aː ħ u" - }, - { - "voice_time_seconds": { - "start": 133.2, - "end": 136.4 - }, - "arabic": "بعد ما تأكّد علي بابا انّهم راحوا،", - "hebrew_transliteration": "בַּעֶד מַא תְאַכַּّד עַלִי בַּאבַּא אִנְّהֹם רַאחוּ,", - "hebrew": "אחרי שעלי באבא וידא שהם הלכו,", - "expected_phonemes": "b a ʕ e d | m a | t ʔ a k k a d | ʕ a l i | b aː b a | ʔ i n n h o m | r aː ħ u" - }, - { - "voice_time_seconds": { - "start": 136.4, - "end": 138.2 - }, - "arabic": "طلع من ورا الشّجرة", - "hebrew_transliteration": "טִלֶע מִן וַרַא שַּׁגַ'רַה", - "hebrew": "יצא מאחורי העץ", - "expected_phonemes": "tˤ i l e ʕ | m i n | w a r a | ʃ a dʒ a r a" - }, - { - "voice_time_seconds": { - "start": 138.2, - "end": 142.4 - }, - "arabic": "وراح لعند المغارة وقال بصوت عالي:", - "hebrew_transliteration": "וּרַאח לַעִנד לְ-מַעַ'ארַה וּקַאל בְּצוֹת עַאלִי:", - "hebrew": "והלך אל המערה ואמר בקול רם:", - "expected_phonemes": "u r aː ħ | l a ʕ i n d | l m a ɣ aː r a | u ʔ aː l | b sˤ oː t | ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 142.4, - "end": 144.4 - }, - "arabic": "«افتح يا سمسم»،", - "hebrew_transliteration": "אִפתַח יַא סִמסֶם,", - "hebrew": "היפתח, סומסום,", - "expected_phonemes": "ʔ i f t a ħ | j a | s i m s e m" - }, - { - "voice_time_seconds": { - "start": 144.4, - "end": 148.4 - }, - "arabic": "وبالفعل الحجر تحرّك والمغارة فتحت.", - "hebrew_transliteration": "וּבִּל-פִעֶל אִל-חַגַ'ר תְחַרַّכּ וִל-מַעַ'ארַה פַתחַת.", - "hebrew": "ואכן הסלע זז והמערה נפתחה.", - "expected_phonemes": "u b i l f i ʕ e l | i l ħ a dʒ a r | t ħ a r r a k | w i l m a ɣ aː r a | f a t ħ a t" - }, - { - "voice_time_seconds": { - "start": 148.4, - "end": 151.4 - }, - "arabic": "فات علي بابا على قلب المغارة", - "hebrew_transliteration": "פַאת עַלִי בַּאבַּא עַלַא קַלבּ לְ-מַעַ'ארַה", - "hebrew": "עלי באבא נכנס לתוך המערה", - "expected_phonemes": "f aː t | ʕ a l i | b aː b a | ʕ a l a | ʔ a l b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 151.4, - "end": 155.2 - }, - "arabic": "وما قدر يصدّق عينيه على الّي شافه.", - "hebrew_transliteration": "וּמַא קִדֶר יְצַדֶּק עֵינֵיה עַלַא אִלִّי שַאפוֹ.", - "hebrew": "ולא האמין למראה עיניו.", - "expected_phonemes": "u m a | ʔ i d e r | j sˤ a d e ʔ | ʕ eː n eː h | ʕ a l a | ʔ i l l i | ʃ aː f o" - }, - { - "voice_time_seconds": { - "start": 155.2, - "end": 159.8 - }, - "arabic": "شاف هناك آلاف الكياس المعبّية بمصاري", - "hebrew_transliteration": "שַאף הֻנַאכּ אַלַאף לְ-כְּיַאס לֶ-מְעַבַּיֶּה בְּמַצַארִי", - "hebrew": "ראה שם אלפי שקים מלאים בכסף", - "expected_phonemes": "ʃ aː f | h u n aː k | ʔ a l aː f | l k j aː s | l e m ʕ a b a j e | b m a sˤ aː r i" - }, - { - "voice_time_seconds": { - "start": 159.8, - "end": 162.4 - }, - "arabic": "مجوهرات وذهب.", - "hebrew_transliteration": "מְגַ'וְהַרַאת וּדַ'הַבּ.", - "hebrew": "תכשיטים וזהב.", - "expected_phonemes": "m dʒ a w h a r aː t | u dˤ a h a b" - }, - { - "voice_time_seconds": { - "start": 162.4, - "end": 166.0 - }, - "arabic": "وهيك فهم انّه هاي الزّلام الّي شافها", - "hebrew_transliteration": "וּהֵיכּ פִהֶם אִנּוֹ הַאי זְּלַאם אִלִّי שַאפְהַא", - "hebrew": "וכך הבין שהאנשים האלה שראה", - "expected_phonemes": "u h eː k | f i h e m | ʔ i n o | h aː i | z l aː m | ʔ i l l i | ʃ aː f h a" - }, - { - "voice_time_seconds": { - "start": 166.0, - "end": 168.0 - }, - "arabic": "بتكون حراميّة بتسرق", - "hebrew_transliteration": "בְּתְכּוּן חַרַאמִיֶּה בְּתֻסרֹק", - "hebrew": "הם גנבים שגונבים", - "expected_phonemes": "b t k uː n | ħ a r aː m i j e | b t u s r o ʔ" - }, - { - "voice_time_seconds": { - "start": 168.0, - "end": 171.4 - }, - "arabic": "وبتخبّي مسروقاتها في هاي المغارة.", - "hebrew_transliteration": "וּבְּתְחַ'בִּי מַסרוּקַאתְהַא פִי הַאי לְ-מַעַ'ארַה.", - "hebrew": "ומחביאים את השלל שלהם במערה הזאת.", - "expected_phonemes": "u b t x a b i | m a s r uː ʔ aː t h a | f i | h aː i | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 171.4, - "end": 173.0 - }, - "arabic": "علي بابا انبسط كتير", - "hebrew_transliteration": "עַלִי בַּאבַּא אִנבַּסַט כְּתִיר", - "hebrew": "עלי באבא שמח מאוד", - "expected_phonemes": "ʕ a l i | b aː b a | ʔ i n b a s a tˤ | k t iː r" - }, - { - "voice_time_seconds": { - "start": 173.0, - "end": 176.4 - }, - "arabic": "وحسّ انّه كلّ مشاكله راح تنحلّ", - "hebrew_transliteration": "וּחַסّ אִנּוֹ כֻּלّ מַשַאכְּלוֹ רַאח תִנחַלّ", - "hebrew": "והרגיש שכל בעיותיו ייפתרו", - "expected_phonemes": "u ħ a s s | ʔ i n o | k u l l | m a ʃ aː k l o | r aː ħ | t i n ħ a l l" - }, - { - "voice_time_seconds": { - "start": 176.4, - "end": 177.8 - }, - "arabic": "لانّه لاقى مصاري", - "hebrew_transliteration": "לַאִנּוֹ לַאקַא מַצַארִי", - "hebrew": "כי מצא כסף", - "expected_phonemes": "l a ʔ i n o | l aː ʔ a | m a sˤ aː r i" - }, - { - "voice_time_seconds": { - "start": 177.8, - "end": 180.8 - }, - "arabic": "بقدر يعيش فيها ملك زمانه.", - "hebrew_transliteration": "בִּקדַר יְעִיש פִיהַא מַלֶכּ זַמַאנוֹ.", - "hebrew": "שבו יוכל לחיות כמלך.", - "expected_phonemes": "b i ʔ d a r | j ʕ iː ʃ | f iː h a | m a l e k | z a m aː n o" - }, - { - "voice_time_seconds": { - "start": 180.8, - "end": 184.2 - }, - "arabic": "أخد علي بابا كيس واحد معبّى دهب", - "hebrew_transliteration": "אַחַ'ד עַלִי בַּאבַּא כִּיס וַאחַד מְעַבַּא דַ'הַבּ", - "hebrew": "עלי באבא לקח שק אחד מלא זהב", - "expected_phonemes": "ʔ a x a d | ʕ a l i | b aː b a | k iː s | w aː ħ a d | m ʕ a b a | dˤ a h a b" - }, - { - "voice_time_seconds": { - "start": 184.2, - "end": 186.0 - }, - "arabic": "وطلع من المغارة،", - "hebrew_transliteration": "וּטִלֶע מִן לְ-מַעַ'ארַה,", - "hebrew": "ויצא מהמערה,", - "expected_phonemes": "u tˤ i l e ʕ | m i n | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 186.0, - "end": 188.0 - }, - "arabic": "تأكّد انّه باب المغارة سكّر", - "hebrew_transliteration": "תְאַכַּّד אִנּוֹ בַּאבּ לְ-מַעַ'ארַה סַכַּّר", - "hebrew": "וידא שפתח המערה נסגר", - "expected_phonemes": "t ʔ a k k a d | ʔ i n o | b aː b | l m a ɣ aː r a | s a k k a r" - }, - { - "voice_time_seconds": { - "start": 188.0, - "end": 190.2 - }, - "arabic": "بعد ما قال بصوت عالي:", - "hebrew_transliteration": "בַּעֶד מַא קַאל בְּצוֹת עַאלִי:", - "hebrew": "אחרי שאמר בקול רם:", - "expected_phonemes": "b a ʕ e d | m a | ʔ aː l | b sˤ oː t | ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 190.2, - "end": 192.0 - }, - "arabic": "«سكّر يا سمسم»", - "hebrew_transliteration": "סַכַّר יַא סִמסֶם", - "hebrew": "היסגר, סומסום", - "expected_phonemes": "s a k k a r | j a | s i m s e m" - }, - { - "voice_time_seconds": { - "start": 192.0, - "end": 195.4 - }, - "arabic": "وراح دغري على بيته لعند مرته", - "hebrew_transliteration": "וּרַאח דֻעְ'רִי עַלַא בֵּיתוֹ לַעִנד מַרְתוֹ", - "hebrew": "והלך ישר לביתו אל אשתו", - "expected_phonemes": "u r aː ħ | d u ɣ r i | ʕ a l a | b eː t o | l a ʕ i n d | m a r t o" - }, - { - "voice_time_seconds": { - "start": 195.4, - "end": 198.2 - }, - "arabic": "حكالها على الّي صار معه.", - "hebrew_transliteration": "חַכַּא-לְהַא עַלַא אִלִّי צַאר מַעוֹ.", - "hebrew": "סיפר לה על מה שקרה לו.", - "expected_phonemes": "ħ a k aː l h a | ʕ a l a | ʔ i l l i | sˤ aː r | m a ʕ o" - }, - { - "voice_time_seconds": { - "start": 198.2, - "end": 201.4 - }, - "arabic": "مرته لعلي بابا انبسطت وارتاحت،", - "hebrew_transliteration": "מַרְתוֹ לַעַלִי בַּאבַּא אִנבַּסטַת וּרְתַאחַת,", - "hebrew": "אשתו של עלי באבא שמחה ונרגעה,", - "expected_phonemes": "m a r t o | l a ʕ a l i | b aː b a | ʔ i n b a s tˤ a t | u r t aː ħ a t" - }, - { - "voice_time_seconds": { - "start": 201.4, - "end": 204.4 - }, - "arabic": "وقالتله انّه لازم يلاقوا طريقة", - "hebrew_transliteration": "וּקַאלַת-לוֹ אִנּוֹ לַאזֶם יְלַאקוּ טַרִיקַה", - "hebrew": "ואמרה לו שהם חייבים למצוא דרך", - "expected_phonemes": "u ʔ aː l a t l o | ʔ i n o | l aː z e m | j l aː ʔ u | tˤ a r iː ʔ a" - }, - { - "voice_time_seconds": { - "start": 204.4, - "end": 209.0 - }, - "arabic": "يجيبوا فيها كلّ الكنز الموجود في المغارة.", - "hebrew_transliteration": "יְגִ'יבּוּ פִיהַא כֻּלّ לְ-כַּנְז אִל-מַוְגּ'וּד פִי לְ-מַעַ'ארַה.", - "hebrew": "להביא את כל האוצר שנמצא במערה.", - "expected_phonemes": "j dʒ iː b u | f iː h a | k u l l | l k a n z | i l m a w dʒ uː d | f i | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 209.0, - "end": 211.4 - }, - "arabic": "علي بابا وافقها الرّأي،", - "hebrew_transliteration": "עַלִי בַּאבַּא וַאפַקְהַא אִ-רַּאִי,", - "hebrew": "עלי באבא הסכים איתה,", - "expected_phonemes": "ʕ a l i | b aː b a | w aː f a ʔ h a | ʔ i r a ʔ i" - }, - { - "voice_time_seconds": { - "start": 211.4, - "end": 215.4 - }, - "arabic": "بسّ طلب منها ما تخبّر حدا عن الموضوع", - "hebrew_transliteration": "בַּס טַלַבּ מִנְהַא מַא תְחַ'בֶּר חַדַא עַן לְ-מַוְדוּע", - "hebrew": "אבל ביקש ממנה לא לספר לאף אחד על העניין", - "expected_phonemes": "b a s | tˤ a l a b | m i n h a | m a | t x a b e r | ħ a d a | ʕ a n | l m a w d uː ʕ" - }, - { - "voice_time_seconds": { - "start": 215.4, - "end": 220.0 - }, - "arabic": "وتحديدًا أخوه الّي كان زلمة لئيم.", - "hebrew_transliteration": "וּתַחדִידַן אַחוּה אִלִّי כַּאן זַלַמֶה לַאִים.", - "hebrew": "ובמיוחד לאחיו שהיה אדם מרושע.", - "expected_phonemes": "u t a ħ d iː d a n | ʔ a ħ uː h | ʔ i l l i | k aː n | z a l a m e | l a ʔ iː m" - }, - { - "voice_time_seconds": { - "start": 220.0, - "end": 221.6 - }, - "arabic": "أخوه لعلي بابا", - "hebrew_transliteration": "אַחוּה לַעַלִי בַּאבַּא", - "hebrew": "אחיו של עלי באבא", - "expected_phonemes": "ʔ a ħ uː h | l a ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 221.6, - "end": 224.2 - }, - "arabic": "بالرّغم من انّه كان زلمة غني", - "hebrew_transliteration": "בִּ-רַّעְ'ם מִן אִנּוֹ כַּאן זַלַמֶה עַ'נִי", - "hebrew": "למרות שהיה אדם עשיר", - "expected_phonemes": "b i r r a ɣ m | m i n | ʔ i n o | k aː n | z a l a m e | ɣ a n i" - }, - { - "voice_time_seconds": { - "start": 224.2, - "end": 226.0 - }, - "arabic": "والله منعم عليه،", - "hebrew_transliteration": "וּאַללַה מִנעֶם עַלֵיה,", - "hebrew": "ואלוהים היטיב עמו,", - "expected_phonemes": "u i l l a | m i n ʕ e m | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 226.0, - "end": 229.4 - }, - "arabic": "كان واحد بخيل ما بحبّ أخوه", - "hebrew_transliteration": "כַּאן וַאחַד בַּחִ'יל מַא בִּחֶבّ אַחוּה", - "hebrew": "היה קמצן שלא אהב את אחיו", - "expected_phonemes": "k aː n | w aː ħ a d | b a x iː l | m a | b i ħ e b b | ʔ a ħ uː h" - }, - { - "voice_time_seconds": { - "start": 229.4, - "end": 234.2 - }, - "arabic": "وما كان يعطيه مصاري ولا حتّى يحاول يساعده.", - "hebrew_transliteration": "וּמַא כַּאן יַעטִיה מַצַארִי וְלַא חַתַّא יְחַאוֶל יְסַאעדוֹ.", - "hebrew": "ולא נתן לו כסף ואפילו לא ניסה לעזור לו.", - "expected_phonemes": "u m a | k aː n | j a ʕ tˤ iː h | m a sˤ aː r i | u l aː | ħ a t t a | j ħ aː w e l | j s aː ʕ d o" - }, - { - "voice_time_seconds": { - "start": 234.2, - "end": 240.2 - }, - "arabic": "هوّ كان مبسوط انّه أخوه علي بابا فقير ومسكين.", - "hebrew_transliteration": "הֻוֶّ כַּאן מַבְּסוּט אִנּוֹ אַחוּה עַלִי בַּאבַּא פַקִיר וּמַסכִּין.", - "hebrew": "הוא היה מרוצה שאחיו עלי באבא עני ומסכן.", - "expected_phonemes": "h u w w e | k aː n | m a b s uː tˤ | ʔ i n o | ʔ a ħ uː h | ʕ a l i | b aː b a | f a ʔ iː r | u m a s k iː n" - }, - { - "voice_time_seconds": { - "start": 240.2, - "end": 243.0 - }, - "arabic": "علي بابا بلّش يشتري بضاعة", - "hebrew_transliteration": "עַלִי בַּאבַּא בַּלַّש יִשתְרִי בְּדַאעַה", - "hebrew": "עלי באבא התחיל לקנות סחורה", - "expected_phonemes": "ʕ a l i | b aː b a | b a l l a ʃ | j i ʃ t r i | b d aː ʕ a" - }, - { - "voice_time_seconds": { - "start": 243.0, - "end": 245.8 - }, - "arabic": "في الدّهب الّي أخده من المغارة،", - "hebrew_transliteration": "פִי דַּהַבּ אִלִّי אַחַ'דוֹ מִן לְ-מַעַ'ארַה,", - "hebrew": "בזהב שלקח מהמערה,", - "expected_phonemes": "f i | d a h a b | ʔ i l l i | ʔ a x a d o | m i n | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 245.8, - "end": 249.2 - }, - "arabic": "عشان يفتح دكّان ويصير تاجر،", - "hebrew_transliteration": "עַשַאן יִפתַח דֻכַּאן וִיצִיר תַאגֶ'ר,", - "hebrew": "כדי לפתוח חנות ולהיות סוחר,", - "expected_phonemes": "ʕ a ʃ aː n | j i f t a ħ | d u k aː n | w iː sˤ iː r | t aː dʒ e r" - }, - { - "voice_time_seconds": { - "start": 249.2, - "end": 251.4 - }, - "arabic": "وهيك شوي شوي", - "hebrew_transliteration": "וּהֵיכּ שְוַי שְוַי", - "hebrew": "וכך לאט לאט", - "expected_phonemes": "u h eː k | ʃ w a j | ʃ w a j" - }, - { - "voice_time_seconds": { - "start": 251.4, - "end": 254.4 - }, - "arabic": "بلّش علي بابا يوخد من المغارة", - "hebrew_transliteration": "בַּלַّש עַלִי בַּאבַּא יוֹחֹ'ד מִן לְ-מַעַ'ארַה", - "hebrew": "עלי באבא התחיל לקחת מהמערה", - "expected_phonemes": "b a l l a ʃ | ʕ a l i | b aː b a | j oː x o d | m i n | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 254.4, - "end": 258.6 - }, - "arabic": "ويوسّع تجارته ويكبر.", - "hebrew_transliteration": "וִיוַסֶّע תִגַ'ארתוֹ וִיכַּבֶּר.", - "hebrew": "ולהרחיב את מסחרו ולגדול.", - "expected_phonemes": "w iː w a s s e ʕ | t i dʒ aː r t o | w iː k a b e r" - }, - { - "voice_time_seconds": { - "start": 258.6, - "end": 260.4 - }, - "arabic": "أخوه لعلي بابا", - "hebrew_transliteration": "אַחוּה לַעַלִי בַּאבַּא", - "hebrew": "אחיו של עלי באבא", - "expected_phonemes": "ʔ a ħ uː h | l a ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 260.4, - "end": 264.2 - }, - "arabic": "انتبه انّه علي بابا قاعد بصير تاجر كبير", - "hebrew_transliteration": "אִנתַבַּה אִנּוֹ עַלִי בַּאבַּא קַאעֶד בִּצִיר תַאגֶ'ר כְּבִּיר", - "hebrew": "שם לב שעלי באבא הופך לסוחר גדול", - "expected_phonemes": "ʔ i n t a b a | ʔ i n o | ʕ a l i | b aː b a | ʔ aː ʕ e d | b i sˤ iː r | t aː dʒ e r | k b iː r" - }, - { - "voice_time_seconds": { - "start": 264.2, - "end": 266.2 - }, - "arabic": "وصار معه مصاري،", - "hebrew_transliteration": "וּצַאר מַעוֹ מַצַארִי,", - "hebrew": "ושיש לו כסף,", - "expected_phonemes": "u sˤ aː r | m a ʕ o | m a sˤ aː r i" - }, - { - "voice_time_seconds": { - "start": 266.2, - "end": 267.8 - }, - "arabic": "عشان هيك", - "hebrew_transliteration": "עַשַאן הֵיכּ", - "hebrew": "לכן", - "expected_phonemes": "ʕ a ʃ aː n | h eː k" - }, - { - "voice_time_seconds": { - "start": 267.8, - "end": 272.6 - }, - "arabic": "قرّر يعرف شو هوّ السّرّ الّي مخبّيه علي بابا.", - "hebrew_transliteration": "קַרַّר יִערַף שוּ הֻוֶّ סִّר אִלִّי מְחַ'בִּיה עַלִי בַּאבַּא.", - "hebrew": "החליט לגלות מה הסוד שעלי באבא מסתיר.", - "expected_phonemes": "ʔ a r r a r | j i ʕ r a f | ʃ u | h u w w e | s s i r | ʔ i l l i | m x a b iː h | ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 272.6, - "end": 277.2 - }, - "arabic": "لهيك أجا يوم وقرّر أخوه لعلي بابا", - "hebrew_transliteration": "לַהֵיכּ אַגַ'א יוֹם וּקַרַّר אַחוּה לַעַלִי בַּאבַּא", - "hebrew": "לכן יום אחד אחיו של עלי באבא החליט", - "expected_phonemes": "l a h eː k | ʔ a dʒ a | j oː m | u ʔ a r r a r | ʔ a ħ uː h | l a ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 277.2, - "end": 281.2 - }, - "arabic": "يلحق علي بابا ويشوف شو سرّه.", - "hebrew_transliteration": "יִלחַק עַלִי בַּאבַּא וִישוּף שוּ סִרّוֹ.", - "hebrew": "לעקוב אחרי עלי באבא ולראות מה סודו.", - "expected_phonemes": "j i l ħ a ʔ | ʕ a l i | b aː b a | w iː ʃ uː f | ʃ u | s i r r o" - }, - { - "voice_time_seconds": { - "start": 281.2, - "end": 284.0 - }, - "arabic": "طلع علي بابا وراح على الغابة،", - "hebrew_transliteration": "טִלֶע עַלִי בַּאבַּא וּרַאח עַלַא לְ-עַ'אבֶּה,", - "hebrew": "עלי באבא יצא והלך ליער,", - "expected_phonemes": "tˤ i l e ʕ | ʕ a l i | b aː b a | u r aː ħ | ʕ a l a | l ɣ aː b e" - }, - { - "voice_time_seconds": { - "start": 284.0, - "end": 287.4 - }, - "arabic": "وأخوه وراه على السّكّيت.", - "hebrew_transliteration": "וּאַחוּה וַרַאה עַלַא סַּכִּית.", - "hebrew": "ואחיו אחריו בשקט.", - "expected_phonemes": "u ʔ a ħ uː h | w a r aː h | ʕ a l a | s a k iː t" - }, - { - "voice_time_seconds": { - "start": 287.4, - "end": 292.4 - }, - "arabic": "لحدّ ما شاف انّه علي بابا وقّف عند حجر كبير", - "hebrew_transliteration": "לַחַדّ מַא שַאף אִנּוֹ עַלִי בַּאבַּא וַקַّף עִנד חַגַ'ר כְּבִּיר", - "hebrew": "עד שראה שעלי באבא עצר ליד סלע גדול", - "expected_phonemes": "l a ħ a d d | m a | ʃ aː f | ʔ i n o | ʕ a l i | b aː b a | w a ʔ ʔ a f | ʕ i n d | ħ a dʒ a r | k b iː r" - }, - { - "voice_time_seconds": { - "start": 292.4, - "end": 296.4 - }, - "arabic": "وصار يحكي شويّة كلمات غريبة.", - "hebrew_transliteration": "וּצַאר יִחכִּי שְוַיֶּת כַּלִמַאת עַ'רִיבֶּה.", - "hebrew": "והתחיל לומר כמה מילים מוזרות.", - "expected_phonemes": "u sˤ aː r | j i ħ k i | ʃ w a j e t | k a l i m aː t | ɣ a r iː b e" - }, - { - "voice_time_seconds": { - "start": 296.4, - "end": 300.2 - }, - "arabic": "وساعتها شاف كيف فتح باب المغارة", - "hebrew_transliteration": "וּסַאעִתְהַא שַאף כִּיף פַתַח בַּאבּ לְ-מַעַ'ארַה", - "hebrew": "ואז ראה איך נפתח פתח המערה", - "expected_phonemes": "u s aː ʕ i t h a | ʃ aː f | k iː f | f a t a ħ | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 300.2, - "end": 303.2 - }, - "arabic": "وشاف كيف علي بابا فات عليها", - "hebrew_transliteration": "וּשַאף כִּיף עַלִי בַּאבַּא פַאת עַלֵיהַא", - "hebrew": "וראה איך עלי באבא נכנס אליה", - "expected_phonemes": "u ʃ aː f | k iː f | ʕ a l i | b aː b a | f aː t | ʕ a l eː h a" - }, - { - "voice_time_seconds": { - "start": 303.2, - "end": 305.6 - }, - "arabic": "وطلع محمّل كياس كبيرة،", - "hebrew_transliteration": "וּטִלֶע מְחַמֶّל כְּיַאס כְּבִּירֶה,", - "hebrew": "ויצא נושא שקים גדולים,", - "expected_phonemes": "u tˤ i l e ʕ | m ħ a m m e l | k j aː s | k b iː r e" - }, - { - "voice_time_seconds": { - "start": 305.6, - "end": 308.0 - }, - "arabic": "ورجع حكى كلمات غريبة", - "hebrew_transliteration": "וּרִגֶ'ע חַכַא כַּלִמַאת עַ'רִיבֶּה", - "hebrew": "ושוב אמר מילים מוזרות", - "expected_phonemes": "u r i dʒ e ʕ | ħ a k a | k a l i m aː t | ɣ a r iː b e" - }, - { - "voice_time_seconds": { - "start": 308.0, - "end": 310.4 - }, - "arabic": "وسكّر باب المغارة.", - "hebrew_transliteration": "וּסַכַּר בַּאבּ לְ-מַעַ'ארַה.", - "hebrew": "וסגר את פתח המערה.", - "expected_phonemes": "u s a k a r | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 310.4, - "end": 312.4 - }, - "arabic": "من كتر غيرته وفضوله", - "hebrew_transliteration": "מִן כֻּתְר עִ'ירתוֹ וּפְדוּלוֹ", - "hebrew": "מרוב קנאה וסקרנות", - "expected_phonemes": "m i n | k u t r | ɣ iː r t o | u f d uː l o" - }, - { - "voice_time_seconds": { - "start": 312.4, - "end": 315.2 - }, - "arabic": "قعد يستنّى تا علي بابا يروّح", - "hebrew_transliteration": "קַעַד יִסתַנַّא תַ-עַלִי בַּאבַּא יְרַוֶّח", - "hebrew": "ישב לחכות שעלי באבא ילך הביתה", - "expected_phonemes": "ʔ a ʕ a d | j i s t a n n a | t a ʕ a l i | b aː b a | j r a w w e ħ" - }, - { - "voice_time_seconds": { - "start": 315.2, - "end": 319.4 - }, - "arabic": "عشان يروح يفحص شو فيه بقلب المغارة.", - "hebrew_transliteration": "עַשַאן יְרוּח יִפחַץ שוּ פִיה בְּקַלבּ לְ-מַעַ'ארַה.", - "hebrew": "כדי ללכת לבדוק מה יש בתוך המערה.", - "expected_phonemes": "ʕ a ʃ aː n | j r uː ħ | j i f ħ a sˤ | ʃ u | f iː h | b ʔ a l b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 319.4, - "end": 321.2 - }, - "arabic": "وهيك صار،", - "hebrew_transliteration": "וּהֵיכּ צַאר,", - "hebrew": "וכך היה,", - "expected_phonemes": "u h eː k | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 321.2, - "end": 324.0 - }, - "arabic": "راح وقّف قبال باب المغارة", - "hebrew_transliteration": "רַאח וַקַّף קְבַּאל בַּאבּ לְ-מַעַ'ארַה", - "hebrew": "הלך ועמד מול פתח המערה", - "expected_phonemes": "r aː ħ | w a ʔ ʔ a f | ʔ b aː l | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 324.0, - "end": 326.6 - }, - "arabic": "وقال بصوت عالي:", - "hebrew_transliteration": "וּקַאל בְּצוֹת עַאלִי:", - "hebrew": "ואמר בקול רם:", - "expected_phonemes": "u ʔ aː l | b sˤ oː t | ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 326.6, - "end": 328.4 - }, - "arabic": "«افتح يا سمسم»...", - "hebrew_transliteration": "אִפתַח יַא סִמסֶם...", - "hebrew": "היפתח, סומסום...", - "expected_phonemes": "ʔ i f t a ħ | j a | s i m s e m" - }, - { - "voice_time_seconds": { - "start": 328.4, - "end": 333.0 - }, - "arabic": "وبالفعل متل السّحر فتح باب المغارة،", - "hebrew_transliteration": "וּבִּל-פִעֶל מִתֶל סִّחֶר פַתַח בַּאבּ לְ-מַעַ'ארַה,", - "hebrew": "ואכן, כמו בקסם, פתח המערה נפתח,", - "expected_phonemes": "u b i l f i ʕ e l | m i t e l | s s i ħ e r | f a t a ħ | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 333.0, - "end": 336.4 - }, - "arabic": "وهيك فات أخوه لعلي بابا", - "hebrew_transliteration": "וּהֵיכּ פַאת אַחוּה לַעַלִי בַּאבַּא", - "hebrew": "וכך נכנס אחיו של עלי באבא", - "expected_phonemes": "u h eː k | f aː t | ʔ a ħ uː h | l a ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 336.4, - "end": 337.8 - }, - "arabic": "على قلب المغارة", - "hebrew_transliteration": "עַלַא קַלבּ לְ-מַעַ'ארַה", - "hebrew": "לתוך המערה", - "expected_phonemes": "ʕ a l a | ʔ a l b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 337.8, - "end": 342.4 - }, - "arabic": "وشاف كلّ الكنوز والدّهب والمجوهرات.", - "hebrew_transliteration": "וּשַאף כֻּלّ לְ-כְּנוּז וִ-דַّהַבּ וִל-מְגַ'וְהַרַאת.", - "hebrew": "וראה את כל האוצרות, הזהב והתכשיטים.", - "expected_phonemes": "u ʃ aː f | k u l l | l k n uː z | w i d d a h a b | w i l m dʒ a w h a r aː t" - }, - { - "voice_time_seconds": { - "start": 342.4, - "end": 345.4 - }, - "arabic": "بلّش يحمّل ويعبّي من كلّ اشي", - "hebrew_transliteration": "בַּלַّש יְחַמֶّל וִיעַבִّי מִן כֻּלّ אִשִי", - "hebrew": "התחיל להעמיס ולמלא מכל דבר", - "expected_phonemes": "b a l l a ʃ | j ħ a m m e l | w iː ʕ a b b i | m i n | k u l l | ʔ i ʃ i" - }, - { - "voice_time_seconds": { - "start": 345.4, - "end": 348.2 - }, - "arabic": "لحدّ ما بطّل يقدر يحمل،", - "hebrew_transliteration": "לַחַדّ מַא בַּטַّל יִקדַר יִחמֶל,", - "hebrew": "עד שלא יכול היה לשאת עוד,", - "expected_phonemes": "l a ħ a d d | m a | b a tˤ tˤ a l | j i ʔ d a r | j i ħ m e l" - }, - { - "voice_time_seconds": { - "start": 348.2, - "end": 351.6 - }, - "arabic": "فقرّر انّه يروّح يجيب حصانه", - "hebrew_transliteration": "פַ-קַרַّר אִנּוֹ יְרַוֶّח יְגִ'יבּ חֻצַאנוֹ", - "hebrew": "אז החליט ללכת להביא את סוסו", - "expected_phonemes": "f a ʔ a r r a r | ʔ i n o | j r a w w e ħ | j dʒ iː b | ħ u sˤ aː n o" - }, - { - "voice_time_seconds": { - "start": 351.6, - "end": 354.0 - }, - "arabic": "عشان يحمّل عليه كمان.", - "hebrew_transliteration": "עַשַאן יְחַמֶّל עַלֵיה כַּמַאן.", - "hebrew": "כדי להעמיס גם עליו.", - "expected_phonemes": "ʕ a ʃ aː n | j ħ a m m e l | ʕ a l eː h | k a m aː n" - }, - { - "voice_time_seconds": { - "start": 354.0, - "end": 356.8 - }, - "arabic": "بسّ لمّا أجا عند باب المغارة", - "hebrew_transliteration": "בַּס לַמַّא אַגַ'א עִנד בַּאבּ לְ-מַעַ'ארַה", - "hebrew": "אבל כשהגיע לפתח המערה", - "expected_phonemes": "b a s | l a m m a | ʔ a dʒ a | ʕ i n d | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 356.8, - "end": 361.6 - }, - "arabic": "نسي كلمة السّرّ وما قدر يفتح الباب،", - "hebrew_transliteration": "נְסִי כִּלמֶת סִّר וּמַא קִדֶר יִפתַח לְ-בַּאבּ,", - "hebrew": "שכח את מילת הסוד ולא הצליח לפתוח את הדלת,", - "expected_phonemes": "n s i | k i l m e t | s s i r | u m a | ʔ i d e r | j i f t a ħ | l b aː b" - }, - { - "voice_time_seconds": { - "start": 361.6, - "end": 365.0 - }, - "arabic": "فصار يقول «افتح يا باب»", - "hebrew_transliteration": "פַ-צַאר יְקוּל: אִפתַח יַא בַּאבּ", - "hebrew": "אז התחיל לומר: היפתח, דלת", - "expected_phonemes": "f a sˤ aː r | j ʔ uː l | ʔ i f t a ħ | j a | b aː b" - }, - { - "voice_time_seconds": { - "start": 365.0, - "end": 367.0 - }, - "arabic": "و«افتحي يا مغارة»", - "hebrew_transliteration": "וּאִפתַחִי יַא מַעַ'ארַה", - "hebrew": "ו\"היפתחי, מערה\"", - "expected_phonemes": "u ʔ i f t a ħ i | j a | m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 367.0, - "end": 369.0 - }, - "arabic": "و«زيح يا حجر»", - "hebrew_transliteration": "וּזִיח יַא חַגַ'ר", - "hebrew": "ו\"זוז, סלע\"", - "expected_phonemes": "u z iː ħ | j a | ħ a dʒ a r" - }, - { - "voice_time_seconds": { - "start": 369.0, - "end": 370.6 - }, - "arabic": "وكمان وكمان", - "hebrew_transliteration": "וּכַּמַאן וּכַּמַאן", - "hebrew": "ועוד ועוד", - "expected_phonemes": "u k a m aː n | u k a m aː n" - }, - { - "voice_time_seconds": { - "start": 370.6, - "end": 372.6 - }, - "arabic": "بسّ ما فتحت المغارة،", - "hebrew_transliteration": "בַּס מַא פַתחַת לְ-מַעַ'ארַה,", - "hebrew": "אבל המערה לא נפתחה,", - "expected_phonemes": "b a s | m a | f a t ħ a t | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 372.6, - "end": 376.0 - }, - "arabic": "لانّه ما حكاش الكلمة المزبوطة.", - "hebrew_transliteration": "לַאִנּוֹ מַא חַכַּאש לְ-כִּלמֶה לְ-מַזבּוּטַה.", - "hebrew": "כי לא אמר את המילה הנכונה.", - "expected_phonemes": "l a ʔ i n o | m a | ħ a k aː ʃ | l k i l m e | l m a z b uː tˤ a" - }, - { - "voice_time_seconds": { - "start": 376.0, - "end": 378.6 - }, - "arabic": "أخوه لعلي بابا بلّش يخاف", - "hebrew_transliteration": "אַחוּה לַעַלִי בַּאבַּא בַּלַّש יְחַ'אף", - "hebrew": "אחיו של עלי באבא התחיל לפחד", - "expected_phonemes": "ʔ a ħ uː h | l a ʕ a l i | b aː b a | b a l l a ʃ | j x aː f" - }, - { - "voice_time_seconds": { - "start": 378.6, - "end": 380.6 - }, - "arabic": "وما عرفش شو يعمل،", - "hebrew_transliteration": "וּמַא עִרֶפ-ש שוּ יַעמַל,", - "hebrew": "ולא ידע מה לעשות,", - "expected_phonemes": "u m a | ʕ i r e f ʃ | ʃ u | j a ʕ m a l" - }, - { - "voice_time_seconds": { - "start": 380.6, - "end": 383.6 - }, - "arabic": "صار يصرّخ ويطلب المساعدة،", - "hebrew_transliteration": "צַאר יְצַרֶّח' וִיטלֹבּ לְ-מֻסַאעַדֶה,", - "hebrew": "התחיל לצעוק ולבקש עזרה,", - "expected_phonemes": "sˤ aː r | j sˤ a r r e x | w iː tˤ l o b | l m u s aː ʕ a d e" - }, - { - "voice_time_seconds": { - "start": 383.6, - "end": 386.0 - }, - "arabic": "بسّ ما حدا سمعه.", - "hebrew_transliteration": "בַּס מַא חַדַא סִמעוֹ.", - "hebrew": "אבל אף אחד לא שמע אותו.", - "expected_phonemes": "b a s | m a | ħ a d a | s i m ʕ o" - }, - { - "voice_time_seconds": { - "start": 386.0, - "end": 388.0 - }, - "arabic": "مرقت ساعات طويلة", - "hebrew_transliteration": "מַרקַת סַאעַאת טַוִילֶה", - "hebrew": "עברו שעות ארוכות", - "expected_phonemes": "m a r ʔ a t | s aː ʕ aː t | tˤ a w iː l e" - }, - { - "voice_time_seconds": { - "start": 388.0, - "end": 391.4 - }, - "arabic": "لحدّ ما سمع صوت عم بقرّب لعنده،", - "hebrew_transliteration": "לַחַדّ מַא סִמֶע צוֹת עַם-בִּקַרֶّבּ לַעִנדוֹ,", - "hebrew": "עד ששמע קול מתקרב אליו,", - "expected_phonemes": "l a ħ a d d | m a | s i m e ʕ | sˤ oː t | ʕ a m b i ʔ a r r e b | l a ʕ i n d o" - }, - { - "voice_time_seconds": { - "start": 391.4, - "end": 394.6 - }, - "arabic": "ومرّة واحدة فتح باب المغارة", - "hebrew_transliteration": "וּמַרַّה וַאחדֶה פַתַח בַּאבּ לְ-מַעַ'ארַה", - "hebrew": "ובבת אחת נפתח פתח המערה", - "expected_phonemes": "u m a r r a | w aː ħ d e | f a t a ħ | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 394.6, - "end": 397.6 - }, - "arabic": "وشاف قدّامه مجموعة زلام", - "hebrew_transliteration": "וּשַאף קֻדַّאמוֹ מַגְ'מוּעֶת זְלַאם", - "hebrew": "וראה לפניו קבוצת אנשים", - "expected_phonemes": "u ʃ aː f | ʔ u d d aː m o | m a dʒ m uː ʕ e t | z l aː m" - }, - { - "voice_time_seconds": { - "start": 397.6, - "end": 402.2 - }, - "arabic": "معها حمير كتيرة محمّلة كياس.", - "hebrew_transliteration": "מַעְהַא חַמִיר כְּתִירֶה מְחַמַّלֶה כְּיַאס.", - "hebrew": "ואיתם חמורים רבים עמוסי שקים.", - "expected_phonemes": "m a ʕ h a | ħ a m iː r | k t iː r e | m ħ a m m a l e | k j aː s" - }, - { - "voice_time_seconds": { - "start": 402.2, - "end": 404.8 - }, - "arabic": "أخوه لعلي بابا خاف كتير", - "hebrew_transliteration": "אַחוּה לַעַלִי בַּאבַּא חַ'אף כְּתִיר", - "hebrew": "אחיו של עלי באבא פחד מאוד", - "expected_phonemes": "ʔ a ħ uː h | l a ʕ a l i | b aː b a | x aː f | k t iː r" - }, - { - "voice_time_seconds": { - "start": 404.8, - "end": 409.8 - }, - "arabic": "لانّه فهم انّه هاي الزلام هيّ زلام خطيرة كتير", - "hebrew_transliteration": "לַאִנּוֹ פִהֶם אִנּוֹ הַאי לֶ-זְלַאם הִיֶّ זְלַאם חַ'טִירֶה כְּתִיר", - "hebrew": "כי הבין שהאנשים האלה הם אנשים מסוכנים מאוד", - "expected_phonemes": "l a ʔ i n o | f i h e m | ʔ i n o | h aː i | l e z l aː m | h i j j e | z l aː m | x a tˤ iː r e | k t iː r" - }, - { - "voice_time_seconds": { - "start": 409.8, - "end": 411.8 - }, - "arabic": "ومعها سلاح.", - "hebrew_transliteration": "וּמַעְהַא סְלַאח.", - "hebrew": "ויש להם נשק.", - "expected_phonemes": "u m a ʕ h a | s l aː ħ" - }, - { - "voice_time_seconds": { - "start": 411.8, - "end": 415.2 - }, - "arabic": "وبالفعل خوفه كان بمحلّه.", - "hebrew_transliteration": "וּבִּל-פִעֶל חוֹפוֹ כַּאן בְּמַחַלّוֹ.", - "hebrew": "ואכן פחדו היה מוצדק.", - "expected_phonemes": "u b i l f i ʕ e l | ħ oː f o | k aː n | b m a ħ a l l o" - }, - { - "voice_time_seconds": { - "start": 415.2, - "end": 417.4 - }, - "arabic": "بسّ شاف رئيس العصابة", - "hebrew_transliteration": "בַּס שַאף רַאִיס לְ-עִצַאבֶּה", - "hebrew": "כשראש הכנופיה ראה", - "expected_phonemes": "b a s | ʃ aː f | r a ʔ iː s | l ʕ i sˤ aː b e" - }, - { - "voice_time_seconds": { - "start": 417.4, - "end": 420.4 - }, - "arabic": "أخوه لعلي بابا في قلب المغارة", - "hebrew_transliteration": "אַחוּה לַעַלִי בַּאבַּא פִי קַלבּ לְ-מַעַ'ארַה", - "hebrew": "את אחיו של עלי באבא בתוך המערה", - "expected_phonemes": "ʔ a ħ uː h | l a ʕ a l i | b aː b a | f i | ʔ a l b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 420.4, - "end": 423.0 - }, - "arabic": "فكّر انّه هدا هوّ الزّلمة", - "hebrew_transliteration": "פַכַּר אִנּוֹ הַאדַא הֻוֶّ זַּלַמֶה", - "hebrew": "חשב שזה האיש", - "expected_phonemes": "f a k a r | ʔ i n o | h aː d a | h u w w e | z a l a m e" - }, - { - "voice_time_seconds": { - "start": 423.0, - "end": 425.2 - }, - "arabic": "الّي صار له فترة عم بسرقه،", - "hebrew_transliteration": "אִלִّי צַאר-לוֹ פַתרַה עַם-בִּסרֻקוֹ,", - "hebrew": "שכבר זמן מה גונב ממנו,", - "expected_phonemes": "ʔ i l l i | sˤ aː r l o | f a t r a | ʕ a m b i s r u ʔ o" - }, - { - "voice_time_seconds": { - "start": 425.2, - "end": 431.6 - }, - "arabic": "ومرّة واحدة وبدون تفكير طلّع سلاحه وقتله.", - "hebrew_transliteration": "וּמַרַّה וַאחדֶה וּבְּדוּן תַפכִּיר טַלַّע סְלַאחוֹ וּקַתַלוֹ.", - "hebrew": "ובבת אחת ובלי מחשבה שלף את נשקו והרג אותו.", - "expected_phonemes": "u m a r r a | w aː ħ d e | u b i d uː n | t a f k iː r | tˤ a l l a ʕ | s l aː ħ o | u ʔ a t a l o" - }, - { - "voice_time_seconds": { - "start": 431.6, - "end": 435.8 - }, - "arabic": "مرقت أيّام وأخوه لعلي بابا مختفي،", - "hebrew_transliteration": "מַרקַת אַיַّאם וּאַחוּה לַעַלִי בַּאבַּא מִחְ'תִפִי,", - "hebrew": "עברו ימים ואחיו של עלי באבא נעדר,", - "expected_phonemes": "m a r ʔ a t | ʔ a j j aː m | u ʔ a ħ uː h | l a ʕ a l i | b aː b a | m i x t i f i" - }, - { - "voice_time_seconds": { - "start": 435.8, - "end": 439.8 - }, - "arabic": "مرته للأخو راحت على بيت علي بابا تبكي", - "hebrew_transliteration": "מַרְתוֹ לַל-אַח' רַאחַת עַלַא בֵּית עַלִי בַּאבַּא תִבכִּי", - "hebrew": "אשת האח הלכה לבית עלי באבא בוכה", - "expected_phonemes": "m a r t o | l a l ʔ a x | r aː ħ a t | ʕ a l a | b eː t | ʕ a l i | b aː b a | t i b k i" - }, - { - "voice_time_seconds": { - "start": 439.8, - "end": 444.6 - }, - "arabic": "وتطلب المساعدة منّه يفتّشلها على جوزها.", - "hebrew_transliteration": "וּתֻטלֹבּ לְ-מֻסַאעַדֶה מִנּוֹ יְפַתֶّש-לְהַא עַלַא גוֹזְהַא.", - "hebrew": "וביקשה ממנו עזרה בחיפוש אחר בעלה.", - "expected_phonemes": "u t u tˤ l o b | l m u s aː ʕ a d e | m i n o | j f a t t e ʃ l h a | ʕ a l a | g oː z h a" - }, - { - "voice_time_seconds": { - "start": 444.6, - "end": 447.4 - }, - "arabic": "علي بابا كان انسان منيح", - "hebrew_transliteration": "עַלִי בַּאבַּא כַּאן אִנסַאן מְנִיח", - "hebrew": "עלי באבא היה אדם טוב", - "expected_phonemes": "ʕ a l i | b aː b a | k aː n | ʔ i n s aː n | m n iː ħ" - }, - { - "voice_time_seconds": { - "start": 447.4, - "end": 449.8 - }, - "arabic": "وعشان هيك خاف على أخوه", - "hebrew_transliteration": "וּעַשַאן הֵיכּ חַ'אף עַלַא אַחוּה", - "hebrew": "ולכן דאג לאחיו", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | x aː f | ʕ a l a | ʔ a ħ uː h" - }, - { - "voice_time_seconds": { - "start": 449.8, - "end": 452.2 - }, - "arabic": "وبلّش يفتّش عليه،", - "hebrew_transliteration": "וּבַּלַّש יְפַתֶّש עַלֵיה,", - "hebrew": "והתחיל לחפש אותו,", - "expected_phonemes": "u b a l l a ʃ | j f a t t e ʃ | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 452.2, - "end": 457.0 - }, - "arabic": "دوّر في كلّ محلّ بسّ ما لاقاهوش.", - "hebrew_transliteration": "דַוַّר פִי כֻּלّ מַחַלّ בַּס מַא לַאקַאהוּש.", - "hebrew": "חיפש בכל מקום אבל לא מצא אותו.", - "expected_phonemes": "d a w w a r | f i | k u l l | m a ħ a l l | b a s | m a | l aː ʔ aː h uː ʃ" - }, - { - "voice_time_seconds": { - "start": 457.0, - "end": 460.6 - }, - "arabic": "علي بابا يئس، وراح على المغارة", - "hebrew_transliteration": "עַלִי בַּאבַּא יִאֶס, וּרַאח עַלַא לְ-מַעַ'ארַה", - "hebrew": "עלי באבא התייאש, והלך למערה", - "expected_phonemes": "ʕ a l i | b aː b a | j i ʔ e s | u r aː ħ | ʕ a l a | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 460.6, - "end": 465.6 - }, - "arabic": "عشان يقعد لحاله ويفكّر شو بدّه يعمل.", - "hebrew_transliteration": "עַשַאן יֻקעֹד לַחַאלוֹ וִיפַכַּר שוּ בַּדּוֹ יַעמַל.", - "hebrew": "כדי לשבת לבדו ולחשוב מה לעשות.", - "expected_phonemes": "ʕ a ʃ aː n | j u ʔ ʕ u d | l a ħ aː l o | w iː f a k a r | ʃ u | b a d o | j a ʕ m a l" - }, - { - "voice_time_seconds": { - "start": 465.6, - "end": 472.2 - }, - "arabic": "هناك صَدمته كانت كبيرة لمّا لاقى أخوه مقتول.", - "hebrew_transliteration": "הֻנַאכּ צַדמִתוֹ כַּאנַת כְּבִּירֶה לַמַّא לַאקַא אַחוּה מַקתוּל.", - "hebrew": "שם הזדעזע מאוד כשמצא את אחיו הרוג.", - "expected_phonemes": "h u n aː k | sˤ a d m i t o | k aː n a t | k b iː r e | l a m m a | l aː ʔ a | ʔ a ħ uː h | m a ʔ t uː l" - }, - { - "voice_time_seconds": { - "start": 472.2, - "end": 474.4 - }, - "arabic": "علي بابا رجع على داره", - "hebrew_transliteration": "עַלִי בַּאבַּא רִגֶ'ע עַלַא דַארוֹ", - "hebrew": "עלי באבא חזר לביתו", - "expected_phonemes": "ʕ a l i | b aː b a | r i dʒ e ʕ | ʕ a l a | d aː r o" - }, - { - "voice_time_seconds": { - "start": 474.4, - "end": 477.6 - }, - "arabic": "وخبّر مرت الأخ انّه جوزها مات،", - "hebrew_transliteration": "וּחַ'בַּר מַרְת לְ-אַח' אִנּוֹ גוֹזְהַא מַאת,", - "hebrew": "והודיע לאשת אחיו שבעלה מת,", - "expected_phonemes": "u x a b a r | m a r t | l ʔ a x | ʔ i n o | g oː z h a | m aː t" - }, - { - "voice_time_seconds": { - "start": 477.6, - "end": 479.6 - }, - "arabic": "وطلب منها ومن ولادها", - "hebrew_transliteration": "וּטַלַבּ מִנְהַא וּמִן וְלַאדְהַא", - "hebrew": "וביקש ממנה ומילדיה", - "expected_phonemes": "u tˤ a l a b | m i n h a | u m i n | u l aː d h a" - }, - { - "voice_time_seconds": { - "start": 479.6, - "end": 482.4 - }, - "arabic": "ييجوا يعيشوا معاه ومع مرته", - "hebrew_transliteration": "יִיגוּ יְעִישוּ מַעַאה וּמַע מַרְתוֹ", - "hebrew": "לבוא לגור איתו ועם אשתו", - "expected_phonemes": "j iː g u | j ʕ iː ʃ u | m a ʕ aː h | u m a ʕ | m a r t o" - }, - { - "voice_time_seconds": { - "start": 482.4, - "end": 486.6 - }, - "arabic": "عشان يقدر يكون معاهم ويساعدهم.", - "hebrew_transliteration": "עַשַאן יִקדַר יְכּוּן מַעַאהֹם וִיסַאעֶדְהֹם.", - "hebrew": "כדי שיוכל להיות איתם ולעזור להם.", - "expected_phonemes": "ʕ a ʃ aː n | j i ʔ d a r | j k uː n | m a ʕ aː h o m | w iː s aː ʕ e d h o m" - }, - { - "voice_time_seconds": { - "start": 486.6, - "end": 488.2 - }, - "arabic": "مرقت الأيّام", - "hebrew_transliteration": "מַרקַת אִל-אַיַّאם", - "hebrew": "הימים עברו", - "expected_phonemes": "m a r ʔ a t | i l ʔ a j j aː m" - }, - { - "voice_time_seconds": { - "start": 488.2, - "end": 491.4 - }, - "arabic": "وعلي بابا كمّل يكبّر في تجارته", - "hebrew_transliteration": "וּעַלִי בַּאבַּא כַּמַّל יְכַבֶּר פִי תִגַ'ארתוֹ", - "hebrew": "ועלי באבא המשיך להרחיב את מסחרו", - "expected_phonemes": "u ʕ a l i | b aː b a | k a m m a l | j k a b e r | f i | t i dʒ aː r t o" - }, - { - "voice_time_seconds": { - "start": 491.4, - "end": 493.2 - }, - "arabic": "وصار غني كتير.", - "hebrew_transliteration": "וּצַאר עַ'נִי כְּתִיר.", - "hebrew": "ונעשה עשיר מאוד.", - "expected_phonemes": "u sˤ aː r | ɣ a n i | k t iː r" - }, - { - "voice_time_seconds": { - "start": 493.2, - "end": 496.4 - }, - "arabic": "وهيك كان دايما يروح على المغارة", - "hebrew_transliteration": "וּהֵיכּ כַּאן דַאיְמַן יְרוּח עַלַא לְ-מַעַ'ארַה", - "hebrew": "וכך היה הולך תמיד למערה", - "expected_phonemes": "u h eː k | k aː n | d aː j m a n | j r uː ħ | ʕ a l a | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 496.4, - "end": 500.0 - }, - "arabic": "وياخد من الدّهب الموجود هناك.", - "hebrew_transliteration": "וִיוֹחֹ'ד מִן דַּהַבּ לְ-מַוְגּ'וּד הֻנַאכּ.", - "hebrew": "ולוקח מהזהב שנמצא שם.", - "expected_phonemes": "w iː oː x o d | m i n | d a h a b | l m a w dʒ uː d | h u n aː k" - }, - { - "voice_time_seconds": { - "start": 500.0, - "end": 504.2 - }, - "arabic": "رئيس العصابة اكتشف انّه الكنوز بعدها عم تنقص", - "hebrew_transliteration": "רַאִיס לְ-עִצַאבֶּה אִכּתַשַף אִנּוֹ לְ-כְּנוּז בַּעֶדְהַא עַם-תִנקַץ", - "hebrew": "ראש הכנופיה גילה שהאוצרות עדיין הולכים ומתמעטים", - "expected_phonemes": "r a ʔ iː s | l ʕ i sˤ aː b e | ʔ i k t a ʃ a f | ʔ i n o | l k n uː z | b a ʕ e d h a | ʕ a m t i n ʔ a sˤ" - }, - { - "voice_time_seconds": { - "start": 504.2, - "end": 508.0 - }, - "arabic": "وعشان هيك أكيد فيه حدا عم بسرق منّه", - "hebrew_transliteration": "וּעַשַאן הֵיכּ אַכִּיד פִיה חַדַא עַם-בִּסרֻק מִנּוֹ", - "hebrew": "ולכן בטוח שיש מישהו שגונב ממנו", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | ʔ a k iː d | f iː h | ħ a d a | ʕ a m b i s r u ʔ | m i n o" - }, - { - "voice_time_seconds": { - "start": 508.0, - "end": 511.4 - }, - "arabic": "وهوّ مجبور يعرف مين.", - "hebrew_transliteration": "וּהֻוֶّ מַגְ'בּוּר יִערַף מִין.", - "hebrew": "והוא חייב לדעת מי.", - "expected_phonemes": "u h u w w e | m a dʒ b uː r | j i ʕ r a f | m iː n" - }, - { - "voice_time_seconds": { - "start": 511.4, - "end": 514.0 - }, - "arabic": "عشان هيك طلب من واحد من رجاله", - "hebrew_transliteration": "עַשַאן הֵיכּ טַלַבּ מִן וַאחַד מִן רְגַ'אלוֹ", - "hebrew": "לכן ביקש מאחד מאנשיו", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | tˤ a l a b | m i n | w aː ħ a d | m i n | r dʒ aː l o" - }, - { - "voice_time_seconds": { - "start": 514.0, - "end": 516.0 - }, - "arabic": "يربّط عند باب المغارة", - "hebrew_transliteration": "יְרַבֶּט עִנד בַּאבּ לְ-מַעַ'ארַה", - "hebrew": "לארוב ליד פתח המערה", - "expected_phonemes": "j r a b e tˤ | ʕ i n d | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 516.0, - "end": 519.6 - }, - "arabic": "لحدّ ما يعرف مين هوّ الحرامي.", - "hebrew_transliteration": "לַחַדّ מַא יִערַף מִין הֻוֶّ לְ-חַרַאמִי.", - "hebrew": "עד שיגלה מי הגנב.", - "expected_phonemes": "l a ħ a d d | m a | j i ʕ r a f | m iː n | h u w w e | l ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 519.6, - "end": 521.2 - }, - "arabic": "وهيك صار،", - "hebrew_transliteration": "וּהֵיכּ צַאר,", - "hebrew": "וכך היה,", - "expected_phonemes": "u h eː k | sˤ aː r" - }, - { - "voice_time_seconds": { - "start": 521.2, - "end": 525.0 - }, - "arabic": "واحد من الحراميّة قعد يستنّى متخبّي", - "hebrew_transliteration": "וַאחַד מִן לְ-חַרַאמִיֶּה קַעַד יִסתַנַّא מִתחַ'בִּי", - "hebrew": "אחד הגנבים ישב לחכות במחבוא", - "expected_phonemes": "w aː ħ a d | m i n | l ħ a r aː m i j e | ʔ a ʕ a d | j i s t a n n a | m i t x a b i" - }, - { - "voice_time_seconds": { - "start": 525.0, - "end": 529.8 - }, - "arabic": "لعند ما شاف علي بابا فايت طالع من المغارة", - "hebrew_transliteration": "לַעִנד מַא שַאף עַלִי בַּאבַּא פַאיֶת טַאלֶע מִן לְ-מַעַ'ארַה", - "hebrew": "עד שראה את עלי באבא יוצא מהמערה", - "expected_phonemes": "l a ʕ i n d | m a | ʃ aː f | ʕ a l i | b aː b a | f aː j e t | tˤ aː l e ʕ | m i n | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 529.8, - "end": 532.4 - }, - "arabic": "محمّل دهب وكنوز.", - "hebrew_transliteration": "מְחַמֶّל דַ'הַבּ וּכְּנוּז.", - "hebrew": "עמוס זהב ואוצרות.", - "expected_phonemes": "m ħ a m m e l | dˤ a h a b | u k n uː z" - }, - { - "voice_time_seconds": { - "start": 532.4, - "end": 537.6 - }, - "arabic": "هالزّلمة لحق علي بابا لحدّ ما وصل على بيته.", - "hebrew_transliteration": "הַ-זַּלַמֶה לִחֶק עַלִי בַּאבַּא לַחַדّ מַא וִצֶל עַלַא בֵּיתוֹ.", - "hebrew": "האיש הזה עקב אחרי עלי באבא עד שהגיע לביתו.", - "expected_phonemes": "h a z a l a m e | l i ħ e ʔ | ʕ a l i | b aː b a | l a ħ a d d | m a | w i sˤ e l | ʕ a l a | b eː t o" - }, - { - "voice_time_seconds": { - "start": 537.6, - "end": 542.2 - }, - "arabic": "وساعتها علّم الزّلمة باب بيته لعلي بابا", - "hebrew_transliteration": "וּסַאעִתְהַא עַלַّם זַּלַמֶה בַּאבּ בֵּיתוֹ לַעַלִי בַּאבַּא", - "hebrew": "ואז סימן האיש את דלת ביתו של עלי באבא", - "expected_phonemes": "u s aː ʕ i t h a | ʕ a l l a m | z a l a m e | b aː b | b eː t o | l a ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 542.2, - "end": 545.6 - }, - "arabic": "عشان يعرف يجيب لعنده باقي الحراميّة", - "hebrew_transliteration": "עַשַאן יִערַף יְגִ'יבּ לַעִנדוֹ בַּאקִי לְ-חַרַאמִיֶּה", - "hebrew": "כדי שיידע להביא אליו את שאר הגנבים", - "expected_phonemes": "ʕ a ʃ aː n | j i ʕ r a f | j dʒ iː b | l a ʕ i n d o | b aː ʔ i | l ħ a r aː m i j e" - }, - { - "voice_time_seconds": { - "start": 545.6, - "end": 550.2 - }, - "arabic": "ويقتلوه وياخدوا دهبهم.", - "hebrew_transliteration": "וִיקַתְלוּה וִיוֹחְ'דוּ דַּהַבְּהֹם.", - "hebrew": "ויהרגו אותו וייקחו את הזהב שלהם.", - "expected_phonemes": "w iː ʔ a t l uː h | w iː oː x d u | d a h a b h o m" - }, - { - "voice_time_seconds": { - "start": 550.2, - "end": 553.0 - }, - "arabic": "مرت علي بابا شافت العلامة", - "hebrew_transliteration": "מַרְת עַלִי בַּאבַּא שַאפַת לְ-עַלַאמֶה", - "hebrew": "אשתו של עלי באבא ראתה את הסימן", - "expected_phonemes": "m a r t | ʕ a l i | b aː b a | ʃ aː f a t | l ʕ a l aː m e" - }, - { - "voice_time_seconds": { - "start": 553.0, - "end": 556.4 - }, - "arabic": "الّي عملها الحرامي على باب بيتها،", - "hebrew_transliteration": "אִלִّי עִמלְהַא לְ-חַרַאמִי עַלַא בַּאבּ בֵּיתְהַא,", - "hebrew": "שהגנב עשה על דלת ביתה,", - "expected_phonemes": "ʔ i l l i | ʕ i m l h a | l ħ a r aː m i | ʕ a l a | b aː b | b eː t h a" - }, - { - "voice_time_seconds": { - "start": 556.4, - "end": 559.8 - }, - "arabic": "ففكّرت وطلعت بخطّة.", - "hebrew_transliteration": "פַ-פַכַּרַת וּטִלעַת בְּחֻ'טַّה.", - "hebrew": "אז חשבה והגתה תוכנית.", - "expected_phonemes": "f a f a k a r a t | u tˤ i l ʕ a t | b x u tˤ tˤ a" - }, - { - "voice_time_seconds": { - "start": 559.8, - "end": 563.4 - }, - "arabic": "راحت جابت دهان بنفس لون العلامة", - "hebrew_transliteration": "רַאחַת גַ'אבַּת דְהַאן בְּנַפֶס לוֹן לְ-עַלַאמֶה", - "hebrew": "הלכה והביאה צבע באותו צבע של הסימן", - "expected_phonemes": "r aː ħ a t | dʒ aː b a t | d h aː n | b n a f e s | l oː n | l ʕ a l aː m e" - }, - { - "voice_time_seconds": { - "start": 563.4, - "end": 566.4 - }, - "arabic": "وعلّمت كلّ بيوت الحارة.", - "hebrew_transliteration": "וּעַלַّמַת כֻּלّ בְּיוּת לְ-חַארַה.", - "hebrew": "וסימנה את כל בתי השכונה.", - "expected_phonemes": "u ʕ a l l a m a t | k u l l | b j uː t | l ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 566.4, - "end": 569.0 - }, - "arabic": "وهيك لمّا أجوا الزّلام", - "hebrew_transliteration": "וּהֵיכּ לַמַّא אַגוּ לֶ-זְלַאם", - "hebrew": "וכך כשהאנשים באו", - "expected_phonemes": "u h eː k | l a m m a | ʔ a g u | l e z l aː m" - }, - { - "voice_time_seconds": { - "start": 569.0, - "end": 572.2 - }, - "arabic": "ما عرفوا وين بيته لعلي بابا.", - "hebrew_transliteration": "מַא עִרְפוּ וֵין בֵּיתוֹ לַעַלִי בַּאבַּא.", - "hebrew": "הם לא ידעו איפה ביתו של עלי באבא.", - "expected_phonemes": "m a | ʕ i r f u | w eː n | b eː t o | l a ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 572.2, - "end": 573.8 - }, - "arabic": "فرجع الحرامي", - "hebrew_transliteration": "פַ-רִגֶ'ע לְ-חַרַאמִי", - "hebrew": "אז הגנב חזר", - "expected_phonemes": "f a r i dʒ e ʕ | l ħ a r aː m i" - }, - { - "voice_time_seconds": { - "start": 573.8, - "end": 577.0 - }, - "arabic": "ربط كمان مرّة عند باب المغارة", - "hebrew_transliteration": "רַבַּט כַּמַאן מַרַّה עִנד בַּאבּ לְ-מַעַ'ארַה", - "hebrew": "וארב שוב ליד פתח המערה", - "expected_phonemes": "r a b a tˤ | k a m aː n | m a r r a | ʕ i n d | b aː b | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 577.0, - "end": 579.6 - }, - "arabic": "واستنّى علي بابا تا يرجع،", - "hebrew_transliteration": "וּסתַנַّא עַלִי בַּאבַּא תַא יִרגַ'ע,", - "hebrew": "וחיכה שעלי באבא יחזור,", - "expected_phonemes": "u s t a n n a | ʕ a l i | b aː b a | t a | j i r dʒ a ʕ" - }, - { - "voice_time_seconds": { - "start": 579.6, - "end": 583.6 - }, - "arabic": "ولمّا شافه رجع لحقه على بيته.", - "hebrew_transliteration": "וּלַמַّא שַאפוֹ רִגֶ'ע לִחקוֹ עַלַא בֵּיתוֹ.", - "hebrew": "וכשראה אותו חוזר, עקב אחריו לביתו.", - "expected_phonemes": "u l a m m a | ʃ aː f o | r i dʒ e ʕ | l i ħ ʔ o | ʕ a l a | b eː t o" - }, - { - "voice_time_seconds": { - "start": 583.6, - "end": 586.8 - }, - "arabic": "بسّ هاي المرّة عرف يعلّم البيت", - "hebrew_transliteration": "בַּס הַאי לְ-מַרַّה עִרֶף יְעַלֶّם לְ-בֵּית", - "hebrew": "אבל הפעם ידע לסמן את הבית", - "expected_phonemes": "b a s | h aː i | l m a r r a | ʕ i r e f | j ʕ a l l e m | l b eː t" - }, - { - "voice_time_seconds": { - "start": 586.8, - "end": 588.6 - }, - "arabic": "من غير ما حدا ينتبه،", - "hebrew_transliteration": "מִן עֵ'יר מַא חַדַא יִנתַבֶּה,", - "hebrew": "בלי שאף אחד ישים לב,", - "expected_phonemes": "m i n | ɣ eː r | m a | ħ a d a | j i n t a b e" - }, - { - "voice_time_seconds": { - "start": 588.6, - "end": 591.4 - }, - "arabic": "ولا حتّى مرته لعلي بابا.", - "hebrew_transliteration": "וְלַא חַתַّא מַרְתוֹ לַעַלִי בַּאבַּא.", - "hebrew": "ואפילו לא אשתו של עלי באבא.", - "expected_phonemes": "u l aː | ħ a t t a | m a r t o | l a ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 591.4, - "end": 592.8 - }, - "arabic": "تاني نهار،", - "hebrew_transliteration": "תַאנִי נְהַאר,", - "hebrew": "למחרת,", - "expected_phonemes": "t aː n i | n h aː r" - }, - { - "voice_time_seconds": { - "start": 592.8, - "end": 595.8 - }, - "arabic": "أجا رئيس العصابة على بيت علي بابا", - "hebrew_transliteration": "אַגַ'א רַאִיס לְ-עִצַאבֶּה עַלַא בֵּית עַלִי בַּאבַּא", - "hebrew": "ראש הכנופיה בא לבית עלי באבא", - "expected_phonemes": "ʔ a dʒ a | r a ʔ iː s | l ʕ i sˤ aː b e | ʕ a l a | b eː t | ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 595.8, - "end": 598.2 - }, - "arabic": "عامل حاله تاجر كبير", - "hebrew_transliteration": "עַאמֶל חַאלוֹ תַאגֶ'ר כְּבִּיר", - "hebrew": "מעמיד פנים שהוא סוחר גדול", - "expected_phonemes": "ʕ aː m e l | ħ aː l o | t aː dʒ e r | k b iː r" - }, - { - "voice_time_seconds": { - "start": 598.2, - "end": 601.2 - }, - "arabic": "بدّه يعمل شغل تجارة مع علي بابا،", - "hebrew_transliteration": "בַּדּוֹ יַעמַל שֻעְ'ל תִגַ'ארַה מַע עַלִי בַּאבַּא,", - "hebrew": "שרוצה לעשות עסקים עם עלי באבא,", - "expected_phonemes": "b a d o | j a ʕ m a l | ʃ u ɣ l | t i dʒ aː r a | m a ʕ | ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 601.2, - "end": 605.6 - }, - "arabic": "فحكاله انّه معاه أربعين جرّت زيت زتون", - "hebrew_transliteration": "פַ-חַכַּא-לוֹ אִנּוֹ מַעַאה אַרבַּעִין גַ'רַّת זֵית זֵיתוּן", - "hebrew": "אז אמר לו שיש לו ארבעים כדים של שמן זית", - "expected_phonemes": "f a ħ a k aː l o | ʔ i n o | m a ʕ aː h | ʔ a r b a ʕ iː n | dʒ a r r a t | z eː t | z eː t uː n" - }, - { - "voice_time_seconds": { - "start": 605.6, - "end": 609.4 - }, - "arabic": "وبدّه يعرضهم للبيع على علي بابا.", - "hebrew_transliteration": "וּבַּדּוֹ יִערְדְ'הֹם לַל-בֵּיע עַלַא עַלִי בַּאבַּא.", - "hebrew": "ורוצה להציע אותם למכירה לעלי באבא.", - "expected_phonemes": "u b a d o | j i ʕ r dˤ h o m | l a l b eː ʕ | ʕ a l a | ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 609.4, - "end": 611.4 - }, - "arabic": "علي بابا انبسط كتير", - "hebrew_transliteration": "עַלִי בַּאבַּא אִנבַּסַט כְּתִיר", - "hebrew": "עלי באבא שמח מאוד", - "expected_phonemes": "ʕ a l i | b aː b a | ʔ i n b a s a tˤ | k t iː r" - }, - { - "voice_time_seconds": { - "start": 611.4, - "end": 616.0 - }, - "arabic": "لانّه هاي فرصة تجارة ممكن تجبله مربح كبير.", - "hebrew_transliteration": "לַאִנּוֹ הַאי פֻרצַת תִגַ'ארַה מֻמכֶּן תְגִ'בּ-לוֹ מַרבַּח כְּבִּיר.", - "hebrew": "כי זו הזדמנות עסקית שעשויה להביא לו רווח גדול.", - "expected_phonemes": "l a ʔ i n o | h aː i | f u r sˤ a t | t i dʒ aː r a | m u m k e n | t dʒ i b l o | m a r b a ħ | k b iː r" - }, - { - "voice_time_seconds": { - "start": 616.0, - "end": 619.4 - }, - "arabic": "عشان هيك عزم الزّلمة على داره", - "hebrew_transliteration": "עַשַאן הֵיכּ עַזַם זַּלַמֶה עַלַא דַארוֹ", - "hebrew": "לכן הזמין את האיש לביתו", - "expected_phonemes": "ʕ a ʃ aː n | h eː k | ʕ a z a m | z a l a m e | ʕ a l a | d aː r o" - }, - { - "voice_time_seconds": { - "start": 619.4, - "end": 623.6 - }, - "arabic": "ودخّل الجرار على واحدة من غرف الدّار.", - "hebrew_transliteration": "וּדַחַّל לֶ-גְ'רַאר עַלַא וַאחדֶה מִן עֻ'רַף דַּאר.", - "hebrew": "והכניס את הכדים לאחד מחדרי הבית.", - "expected_phonemes": "u d a ħ ħ a l | l e dʒ r aː r | ʕ a l a | w aː ħ d e | m i n | ɣ u r a f | d aː r" - }, - { - "voice_time_seconds": { - "start": 623.6, - "end": 627.4 - }, - "arabic": "لمّا كان علي بابا قاعد مع رئيس العصابة", - "hebrew_transliteration": "לַמַّא כַּאן עַלִי בַּאבַּא קַאעֶד מַע רַאִיס לְ-עִצַאבֶּה", - "hebrew": "כשעלי באבא ישב עם ראש הכנופיה", - "expected_phonemes": "l a m m a | k aː n | ʕ a l i | b aː b a | ʔ aː ʕ e d | m a ʕ | r a ʔ iː s | l ʕ i sˤ aː b e" - }, - { - "voice_time_seconds": { - "start": 627.4, - "end": 631.0 - }, - "arabic": "قامت مرت علي بابا تتفقّد الوضع،", - "hebrew_transliteration": "קַאמַת מַרְת עַלִי בַּאבַּא תִתפַקַّד לְ-וַדְע,", - "hebrew": "אשתו של עלי באבא קמה לבדוק את המצב,", - "expected_phonemes": "ʔ aː m a t | m a r t | ʕ a l i | b aː b a | t i t f a ʔ ʔ a d | l w a d ʕ" - }, - { - "voice_time_seconds": { - "start": 631.0, - "end": 634.0 - }, - "arabic": "فراحت على الغرفة عند جرار الزّيت", - "hebrew_transliteration": "פַ-רַאחַת עַלַא לְ-עֻ'רְפֶה עִנד גְ'רַאר זֵּית", - "hebrew": "אז הלכה לחדר שבו כדי השמן", - "expected_phonemes": "f a r aː ħ a t | ʕ a l a | l ɣ u r f e | ʕ i n d | dʒ r aː r | z eː t" - }, - { - "voice_time_seconds": { - "start": 634.0, - "end": 638.2 - }, - "arabic": "وهناك سمعت صوت زلام من قلب الجرار", - "hebrew_transliteration": "וּהֻנַאכּ סִמעַת צוֹת זְלַאם מִן קַלבּ לֶ-גְ'רַאר", - "hebrew": "ושם שמעה קולות של אנשים מתוך הכדים", - "expected_phonemes": "u h u n aː k | s i m ʕ a t | sˤ oː t | z l aː m | m i n | ʔ a l b | l e dʒ r aː r" - }, - { - "voice_time_seconds": { - "start": 638.2, - "end": 640.0 - }, - "arabic": "بحكوا بصوت واطي", - "hebrew_transliteration": "בִּחכּוּ בְּצוֹת וַאטִי", - "hebrew": "מדברים בקול שקט", - "expected_phonemes": "b i ħ k u | b sˤ oː t | w aː tˤ i" - }, - { - "voice_time_seconds": { - "start": 640.0, - "end": 643.4 - }, - "arabic": "وبيقولوا انّه كمان شوي راح يطلعوا", - "hebrew_transliteration": "וּבִּיקוּלוּ אִנּוֹ כַּמַאן שְוַי רַאח יִטלַעוּ", - "hebrew": "ואומרים שעוד מעט הם ייצאו", - "expected_phonemes": "u b iː ʔ uː l u | ʔ i n o | k a m aː n | ʃ w a j | r aː ħ | j i tˤ l a ʕ u" - }, - { - "voice_time_seconds": { - "start": 643.4, - "end": 648.0 - }, - "arabic": "ويقتلوا أهل الدّار وياخدوا الدّهب والكنوز.", - "hebrew_transliteration": "וִיקַתְלוּ אַהֶל דַּאר וִיוֹחְ'דוּ דַּהַבּ וִל-כְּנוּז.", - "hebrew": "ויהרגו את בני הבית וייקחו את הזהב והאוצרות.", - "expected_phonemes": "w iː ʔ a t l u | ʔ a h e l | d aː r | w iː oː x d u | d a h a b | w i l k n uː z" - }, - { - "voice_time_seconds": { - "start": 648.0, - "end": 651.2 - }, - "arabic": "مرت علي بابا كانت مرة ذكيّة،", - "hebrew_transliteration": "מַרְת עַלִי בַּאבַּא כַּאנַת מַרַה זַכִּיֶّה,", - "hebrew": "אשתו של עלי באבא הייתה אישה נבונה,", - "expected_phonemes": "m a r t | ʕ a l i | b aː b a | k aː n a t | m a r a | z a k i j j e" - }, - { - "voice_time_seconds": { - "start": 651.2, - "end": 655.8 - }, - "arabic": "وعشان هيك لمّا عرفت انّه كلّ هاي الخطّة الشّرّيرة", - "hebrew_transliteration": "וּעַשַאן הֵיכּ לַמַّא עִרְפַת אִנּוֹ כֻּלّ הַאי אִל-חֻ'טַّה אִ-שִّרִّירֶה", - "hebrew": "ולכן כשהבינה שאת כל התוכנית המרושעת הזאת", - "expected_phonemes": "u ʕ a ʃ aː n | h eː k | l a m m a | ʕ i r f a t | ʔ i n o | k u l l | h aː i | i l x u tˤ tˤ a | ʔ i ʃ ʃ i r r iː r e" - }, - { - "voice_time_seconds": { - "start": 655.8, - "end": 658.4 - }, - "arabic": "عاملينها عصابة الحراميّة،", - "hebrew_transliteration": "עַאמְלִינְהַא עִצַאבֶּת לְ-חַרַאמִיֶּה,", - "hebrew": "רוקמת כנופיית הגנבים,", - "expected_phonemes": "ʕ aː m l iː n h a | ʕ i sˤ aː b e t | l ħ a r aː m i j e" - }, - { - "voice_time_seconds": { - "start": 658.4, - "end": 661.4 - }, - "arabic": "راحت على المطبخ سخّنت زيت", - "hebrew_transliteration": "רַאחַת עַלַא לְ-מַטבַּח' סַחַّנַת זֵית", - "hebrew": "הלכה למטבח, חיממה שמן", - "expected_phonemes": "r aː ħ a t | ʕ a l a | l m a tˤ b a x | s a ħ ħ a n a t | z eː t" - }, - { - "voice_time_seconds": { - "start": 661.4, - "end": 665.4 - }, - "arabic": "ورجعت دارته كلّه في قلب الجرار.", - "hebrew_transliteration": "וּרַגְ'עַת דַארַתוֹ כֻּלّוֹ פִי קַלבּ לֶ-גְ'רַאר.", - "hebrew": "וחזרה ושפכה את כולו לתוך הכדים.", - "expected_phonemes": "u r a dʒ ʕ a t | d aː r a t o | k u l l o | f i | ʔ a l b | l e dʒ r aː r" - }, - { - "voice_time_seconds": { - "start": 665.4, - "end": 668.8 - }, - "arabic": "زلام العصابة ما قدروا يحكوا", - "hebrew_transliteration": "זְלַאם לְ-עִצַאבֶּה מַא קִדְרוּ יִחכּוּ", - "hebrew": "אנשי הכנופיה לא הצליחו לדבר", - "expected_phonemes": "z l aː m | l ʕ i sˤ aː b e | m a | ʔ i d r u | j i ħ k u" - }, - { - "voice_time_seconds": { - "start": 668.8, - "end": 671.6 - }, - "arabic": "ولا كلمة وبقيوا ساكتين", - "hebrew_transliteration": "וְלַא כִּלמֶה וּבְּקְיוּ סַאכְּתִין", - "hebrew": "אף מילה ונשארו שותקים", - "expected_phonemes": "u l aː | k i l m e | u b ʔ iː u | s aː k t iː n" - }, - { - "voice_time_seconds": { - "start": 671.6, - "end": 675.2 - }, - "arabic": "وهمّ عم بنحرقوا لحدّ الموت.", - "hebrew_transliteration": "וּהֻמّ עַם-בִּנחַרקוּ לַחַדّ לְ-מוֹת.", - "hebrew": "בזמן שנשרפו עד מוות.", - "expected_phonemes": "u h u m m | ʕ a m b i n ħ a r ʔ u | l a ħ a d d | l m oː t" - }, - { - "voice_time_seconds": { - "start": 675.2, - "end": 680.2 - }, - "arabic": "في اللّيل قام رئيس العصابة وراح عند زلامه.", - "hebrew_transliteration": "פִי לֵّיל קַאם רַאִיס לְ-עִצַאבֶּה וּרַאח עִנד זְלַאמוֹ.", - "hebrew": "בלילה קם ראש הכנופיה והלך אל אנשיו.", - "expected_phonemes": "f i | l l eː l | ʔ aː m | r a ʔ iː s | l ʕ i sˤ aː b e | u r aː ħ | ʕ i n d | z l aː m o" - }, - { - "voice_time_seconds": { - "start": 680.2, - "end": 683.2 - }, - "arabic": "دقّ على الجرار عشان يطلعوا،", - "hebrew_transliteration": "דַקّ עַלַא לֶ-גְ'רַאר עַשַאן יִטלַעוּ,", - "hebrew": "דפק על הכדים כדי שייצאו,", - "expected_phonemes": "d a ʔ ʔ | ʕ a l a | l e dʒ r aː r | ʕ a ʃ aː n | j i tˤ l a ʕ u" - }, - { - "voice_time_seconds": { - "start": 683.2, - "end": 685.6 - }, - "arabic": "بسّ ما حدا ردّ عليه.", - "hebrew_transliteration": "בַּס מַא חַדַא רַדّ עַלֵיה.", - "hebrew": "אבל אף אחד לא ענה לו.", - "expected_phonemes": "b a s | m a | ħ a d a | r a d d | ʕ a l eː h" - }, - { - "voice_time_seconds": { - "start": 685.6, - "end": 689.0 - }, - "arabic": "فتح الجرار ويا ريته ما فتحها.", - "hebrew_transliteration": "פַתַח לֶ-גְ'רַאר וּיַא רֵיתוֹ מַא פַתַחְהַא.", - "hebrew": "פתח את הכדים, והלוואי שלא היה פותח אותם.", - "expected_phonemes": "f a t a ħ | l e dʒ r aː r | u j a | r eː t o | m a | f a t a ħ h a" - }, - { - "voice_time_seconds": { - "start": 689.0, - "end": 691.8 - }, - "arabic": "شاف كلّ زلامه ميتين.", - "hebrew_transliteration": "שַאף כֻּלّ זְלַאמוֹ מֵיְתִין.", - "hebrew": "ראה את כל אנשיו מתים.", - "expected_phonemes": "ʃ aː f | k u l l | z l aː m o | m eː t iː n" - }, - { - "voice_time_seconds": { - "start": 691.8, - "end": 695.0 - }, - "arabic": "بهديك اللّحظة أجا علي بابا", - "hebrew_transliteration": "בְּהַדִיכּ לַּחזַה אַגַ'א עַלִי בַּאבַּא", - "hebrew": "באותו רגע בא עלי באבא", - "expected_phonemes": "b h a d iː k | l a ħ ðˤ a | ʔ a dʒ a | ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 695.0, - "end": 698.0 - }, - "arabic": "بعد ما عرف كلّ الحكاية من مرته،", - "hebrew_transliteration": "בַּעֶד מַא עִרֶף כֻּלّ לְ-חְכַּאיֶה מִן מַרְתוֹ,", - "hebrew": "אחרי שידע את כל הסיפור מאשתו,", - "expected_phonemes": "b a ʕ e d | m a | ʕ i r e f | k u l l | l ħ k aː j e | m i n | m a r t o" - }, - { - "voice_time_seconds": { - "start": 698.0, - "end": 700.2 - }, - "arabic": "وقتل رئيس العصابة.", - "hebrew_transliteration": "וּקַתַל רַאִיס לְ-עִצַאבֶּה.", - "hebrew": "והרג את ראש הכנופיה.", - "expected_phonemes": "u ʔ a t a l | r a ʔ iː s | l ʕ i sˤ aː b e" - }, - { - "voice_time_seconds": { - "start": 700.2, - "end": 703.6 - }, - "arabic": "وهيك تخلّص منّه ومن شرّه", - "hebrew_transliteration": "וּהֵיכּ תְחַ'לַّץ מִנּוֹ וּמִן שַרّוֹ", - "hebrew": "וכך נפטר ממנו ומרשעותו", - "expected_phonemes": "u h eː k | t x a l l a sˤ | m i n o | u m i n | ʃ a r r o" - }, - { - "voice_time_seconds": { - "start": 703.6, - "end": 707.0 - }, - "arabic": "وصار كلّ دهب وكنوز المغارة", - "hebrew_transliteration": "וּצַאר כֻּלّ דַ'הַבּ וּכְּנוּז לְ-מַעַ'ארַה", - "hebrew": "וכל הזהב והאוצרות של המערה עברו", - "expected_phonemes": "u sˤ aː r | k u l l | dˤ a h a b | u k n uː z | l m a ɣ aː r a" - }, - { - "voice_time_seconds": { - "start": 707.0, - "end": 709.6 - }, - "arabic": "تحت ايد علي بابا.", - "hebrew_transliteration": "תַחת אִיד עַלִי בַּאבַּא.", - "hebrew": "לידי עלי באבא.", - "expected_phonemes": "t a ħ t | ʔ iː d | ʕ a l i | b aː b a" - }, - { - "voice_time_seconds": { - "start": 709.6, - "end": 714.2 - }, - "arabic": "وتوتا توتا خلصت الحدّوتة.", - "hebrew_transliteration": "וּתוּתַא תוּתַא חִ'לצַת אִל-חַדּוּתֶה.", - "hebrew": "ותותה תותה, נגמרה המעשייה.", - "expected_phonemes": "u t uː t a | t uː t a | x i l sˤ a t | i l ħ a d uː t e" - } - ] - }, - { - "file_name": "כוחה של הכנסת אורחים * קֻוֶתֶ א-דִיָאפֶה * قوّة الضّيافة * סיפור בערבית מדוברת [9g1B1s3NL1A].webm", - "youtube_id": "9g1B1s3NL1A", - "title_hebrew": "כוחה של הכנסת אורחים", - "segments": [ - { - "voice_time_seconds": { - "start": 0.4, - "end": 5.6 - }, - "arabic": "قوّة الضّيافة", - "hebrew_transliteration": "קֻוֶּת אִ-דִّיַאפֶה", - "hebrew": "כוחה של הכנסת אורחים", - "expected_phonemes": "ʔ u w e t | ʔ i d d i j aː f e" - }, - { - "voice_time_seconds": { - "start": 6.4, - "end": 9.8 - }, - "arabic": "مرّة كان فيه خنفسة صغيرة.", - "hebrew_transliteration": "מַרַّה כַּאן פִיה חֻ'נפֻסֶה זְעִ'ירֶה.", - "hebrew": "פעם הייתה חיפושית קטנה.", - "expected_phonemes": "m a r r a | k aː n | f iː h | x u n f u s e | z ɣ iː r e" - }, - { - "voice_time_seconds": { - "start": 9.8, - "end": 13.6 - }, - "arabic": "كلّ يوم كانت خنفسة تلفلف في الحقول،", - "hebrew_transliteration": "כֻּלّ יוֹם כַּאנַת חֻ'נפֻסֶה תְלַפלֶף פִי לְ-חֻקוּל,", - "hebrew": "כל יום החיפושית הסתובבה בשדות,", - "expected_phonemes": "k u l l | j oː m | k aː n a t | x u n f u s e | t l a f l e f | f i | l ħ u ʔ uː l" - }, - { - "voice_time_seconds": { - "start": 13.6, - "end": 18.2 - }, - "arabic": "تركض من هون لهناك، ومن هناك لهون،", - "hebrew_transliteration": "תֻרכֹּד' מִן הוֹן לַהֻנַאכּ, וּמִן הֻנַאכּ לַהוֹן,", - "hebrew": "רצה מכאן לשם, ומשם לכאן,", - "expected_phonemes": "t u r k o dˤ | m i n | h oː n | l a h u n aː k | u m i n | h u n aː k | l a h oː n" - }, - { - "voice_time_seconds": { - "start": 18.2, - "end": 21.6 - }, - "arabic": "بتلمّ أكل وبتجيبه على بيتها.", - "hebrew_transliteration": "בִּתלֻםّ אַכֶּל וּבִּתגִ'יבּוֹ עַלַא בֵּיתְהַא.", - "hebrew": "אוספת אוכל ומביאה אותו לביתה.", - "expected_phonemes": "b i t l u m m | ʔ a k e l | u b i t dʒ iː b o | ʕ a l a | b eː t h a" - }, - { - "voice_time_seconds": { - "start": 21.6, - "end": 25.2 - }, - "arabic": "هيك كانت تعمل طول فصل الصّيف.", - "hebrew_transliteration": "הֵיכּ כַּאנַת תַעמַל טוּל פַצֶל אִ-צֵּיף.", - "hebrew": "כך עשתה כל הקיץ.", - "expected_phonemes": "h eː k | k aː n a t | t a ʕ m a l | tˤ uː l | f a sˤ e l | ʔ i sˤ eː f" - }, - { - "voice_time_seconds": { - "start": 25.2, - "end": 27.4 - }, - "arabic": "بواحد من هالصّبحيّات،", - "hebrew_transliteration": "בְּוַאחַד מִן הַ-צֻּבְחִיַّאת,", - "hebrew": "באחד הבקרים,", - "expected_phonemes": "b w aː ħ a d | m i n | h a sˤ u b ħ i j j aː t" - }, - { - "voice_time_seconds": { - "start": 27.4, - "end": 30.2 - }, - "arabic": "سمعت صوت غريب على شبّاكها.", - "hebrew_transliteration": "סִמעַת צוֹת עַ'רִיבּ עַלַא שֻבַּّאכְּהַא.", - "hebrew": "שמעה קול מוזר על החלון שלה.", - "expected_phonemes": "s i m ʕ a t | sˤ oː t | ɣ a r iː b | ʕ a l a | ʃ u b b aː k h a" - }, - { - "voice_time_seconds": { - "start": 30.2, - "end": 33.6 - }, - "arabic": "تيپ توپ، تيپ توپ...", - "hebrew_transliteration": "טִיפ טוּפ, טִיפ טוּפ...", - "hebrew": "טיפ טופ, טיפ טופ...", - "expected_phonemes": "tˤ iː f | tˤ uː f | tˤ iː f | tˤ uː f" - }, - { - "voice_time_seconds": { - "start": 33.6, - "end": 36.0 - }, - "arabic": "فتحت خنفسة الشّبّاك،", - "hebrew_transliteration": "פַתחַת חֻ'נפֻסֶה שֻּׁבַّאכּ,", - "hebrew": "החיפושית פתחה את החלון,", - "expected_phonemes": "f a t ħ a t | x u n f u s e | ʃ u b b aː k" - }, - { - "voice_time_seconds": { - "start": 36.0, - "end": 38.2 - }, - "arabic": "زاحت البرداي – وهوپ!", - "hebrew_transliteration": "זַאחַת לְ-בֻּרדַאי – וּהוֹפּ!", - "hebrew": "הזיזה את הווילון – והופ!", - "expected_phonemes": "z aː ħ a t | l b u r d aː i | u h oː f" - }, - { - "voice_time_seconds": { - "start": 38.2, - "end": 41.2 - }, - "arabic": "شو هاد؟ شو شافت؟", - "hebrew_transliteration": "שוּ הַאד? שוּ שַאפַת?", - "hebrew": "מה זה? מה ראתה?", - "expected_phonemes": "ʃ u | h aː d | ʃ u | ʃ aː f a t" - }, - { - "voice_time_seconds": { - "start": 41.2, - "end": 45.2 - }, - "arabic": "ميّ عم تنزل من السّما – شتا!", - "hebrew_transliteration": "מַיّ עַם-תִנזַל מִן סַּמַא – שִתַא!", - "hebrew": "מים יורדים מהשמיים – גשם!", - "expected_phonemes": "m a j j | ʕ a m t i n z a l | m i n | s a m a | ʃ i t a" - }, - { - "voice_time_seconds": { - "start": 45.2, - "end": 49.4 - }, - "arabic": "«طيّب»، قالت الخنفسة، «أجا فصل الشّتا.»", - "hebrew_transliteration": "טַיֶّבּ, קַאלַת לְ-חֻ'נפֻסֶה, אַגַ'א פַצֶל שִּׁתַא.", - "hebrew": "טוב, אמרה החיפושית, הגיע החורף.", - "expected_phonemes": "tˤ a j j e b | ʔ aː l a t | l x u n f u s e | ʔ a dʒ a | f a sˤ e l | ʃ i t a" - }, - { - "voice_time_seconds": { - "start": 49.4, - "end": 53.0 - }, - "arabic": "طلّعت الجرزاي من الخزانة،", - "hebrew_transliteration": "טַלַّעַת לְ-גַ'רזַאי מִן לְ-חַ'זַאנֶה,", - "hebrew": "הוציאה את הסוודר מהארון,", - "expected_phonemes": "tˤ a l l a ʕ a t | l dʒ a r z aː i | m i n | l x a z aː n e" - }, - { - "voice_time_seconds": { - "start": 53.0, - "end": 57.6 - }, - "arabic": "ضوّت الفرن وقعدت تتدفّى قبال النّار.", - "hebrew_transliteration": "דַוַّת לְ-פֻרֶן וּקַעדַת תִתדַפַّא קְבַּאל נַּאר.", - "hebrew": "הדליקה את התנור וישבה להתחמם מול האש.", - "expected_phonemes": "d a w w a t | l f u r e n | u ʔ a ʕ d a t | t i t d a f f a | ʔ b aː l | n aː r" - }, - { - "voice_time_seconds": { - "start": 57.6, - "end": 59.4 - }, - "arabic": "خنفسة كانت قاعدة،", - "hebrew_transliteration": "חֻ'נפֻסֶה כַּאנַת קַאעְדֶה,", - "hebrew": "החיפושית ישבה,", - "expected_phonemes": "x u n f u s e | k aː n a t | ʔ aː ʕ d e" - }, - { - "voice_time_seconds": { - "start": 59.4, - "end": 64.0 - }, - "arabic": "وصارت تفكّر، وفجأة، شو هاد؟", - "hebrew_transliteration": "וּצַארַת תְפַכַּר, וּפַגְ'אַה, שוּ הַאד?", - "hebrew": "והתחילה לחשוב, ופתאום, מה זה?", - "expected_phonemes": "u sˤ aː r a t | t f a k a r | u f a dʒ ʔ a | ʃ u | h aː d" - }, - { - "voice_time_seconds": { - "start": 64.0, - "end": 68.2 - }, - "arabic": "تهيّأ إلها إنها سمعت حدا بدقّ على الباب.", - "hebrew_transliteration": "תְהַיַّא אִלְהַא אִנְّהַא סִמעַת חַדַא בִּידֻקّ עַלַא לְ-בַּאבּ.", - "hebrew": "נדמה לה ששמעה מישהו דופק בדלת.", - "expected_phonemes": "t h a j j a | i l h a | ʔ i n n h a | s i m ʕ a t | ħ a d a | b iː d u ʔ ʔ | ʕ a l a | l b aː b" - }, - { - "voice_time_seconds": { - "start": 68.2, - "end": 72.4 - }, - "arabic": "نسطت منيح، وعنجد فيه حدا بدقّ.", - "hebrew_transliteration": "נַסטַת מְנִיח, וּעַנגַ'ד פִיה חַדַא בִּידֻקّ.", - "hebrew": "הקשיבה היטב, ובאמת מישהו דופק.", - "expected_phonemes": "n a s tˤ a t | m n iː ħ | u ʕ a n dʒ a d | f iː h | ħ a d a | b iː d u ʔ ʔ" - }, - { - "voice_time_seconds": { - "start": 72.4, - "end": 75.0 - }, - "arabic": "«مين؟» سألت خنفسة.", - "hebrew_transliteration": "מִין? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מי? שאלה החיפושית.", - "expected_phonemes": "m iː n | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 75.0, - "end": 77.0 - }, - "arabic": "«هاي أنا، فارة!»", - "hebrew_transliteration": "הַאי אַנַא, פַארַה!", - "hebrew": "זו אני, עכברה!", - "expected_phonemes": "h aː i | ʔ a n a | f aː r a" - }, - { - "voice_time_seconds": { - "start": 77.0, - "end": 79.0 - }, - "arabic": "«شو بدّك يا فارة؟»", - "hebrew_transliteration": "שוּ בַּדֶّכּ יַא פַארַה?", - "hebrew": "מה את רוצה, עכברה?", - "expected_phonemes": "ʃ u | b a d d e k | j a | f aː r a" - }, - { - "voice_time_seconds": { - "start": 79.0, - "end": 81.6 - }, - "arabic": "فارة قالت: «أنا بردانة!", - "hebrew_transliteration": "פַארַה קַאלַת: אַנַא בַּרדַאנֶה!", - "hebrew": "העכברה אמרה: קר לי!", - "expected_phonemes": "f aː r a | ʔ aː l a t | ʔ a n a | b a r d aː n e" - }, - { - "voice_time_seconds": { - "start": 81.6, - "end": 85.2 - }, - "arabic": "الدّنيا عم بتشتّي، كلّ فروتي مبلولة.", - "hebrew_transliteration": "אִ-דֻּנְיַא עַם-בִּתשַתִّי, כֻּלّ פַרוִתִי מַבְּלוּלֶה.", - "hebrew": "יורד גשם, כל הפרווה שלי רטובה.", - "expected_phonemes": "ʔ i d u n j a | ʕ a m b i t ʃ a t t i | k u l l | f a r w i t i | m a b l uː l e" - }, - { - "voice_time_seconds": { - "start": 85.2, - "end": 88.8 - }, - "arabic": "إعملي معروف، فوّتيني لبيتك.»", - "hebrew_transliteration": "אִעמַלִי מַערוּף, פַוְّתִינִי לַבֵּיתֶכּ.", - "hebrew": "עשי טובה, הכניסי אותי לביתך.", - "expected_phonemes": "ʔ i ʕ m a l i | m a ʕ r uː f | f a w w t iː n i | l a b eː t e k" - }, - { - "voice_time_seconds": { - "start": 88.8, - "end": 91.0 - }, - "arabic": "خنفسة جاوبت وقالت:", - "hebrew_transliteration": "חֻ'נפֻסֶה גַ'אוַבַּת וּקַאלַת:", - "hebrew": "החיפושית ענתה ואמרה:", - "expected_phonemes": "x u n f u s e | dʒ aː w a b a t | u ʔ aː l a t" - }, - { - "voice_time_seconds": { - "start": 91.0, - "end": 94.6 - }, - "arabic": "«فارة حبيبتي، كان بفوّتك بكلّ سرور،", - "hebrew_transliteration": "פַארַה חַבִּיבְּתִי, כַּאן בַּפַוְּתֶכּ בְּכֻלّ סֻרוּר,", - "hebrew": "עכברה יקירתי, הייתי מכניסה אותך בשמחה רבה,", - "expected_phonemes": "f aː r a | ħ a b iː b t i | k aː n | b a f a w t e k | b k u l l | s u r uː r" - }, - { - "voice_time_seconds": { - "start": 94.6, - "end": 96.2 - }, - "arabic": "بسّ بنفعش.", - "hebrew_transliteration": "בַּס בִּנפַעֶש.", - "hebrew": "אבל אי אפשר.", - "expected_phonemes": "b a s | b i n f a ʕ e ʃ" - }, - { - "voice_time_seconds": { - "start": 96.2, - "end": 98.4 - }, - "arabic": "كيف ممكن فارة كبيرة متلك", - "hebrew_transliteration": "כִּיף מֻמכֶּן פַארַה כְּבִּירֶה מִתלֶכּ", - "hebrew": "איך יכולה עכברה גדולה כמוך", - "expected_phonemes": "k iː f | m u m k e n | f aː r a | k b iː r e | m i t l e k" - }, - { - "voice_time_seconds": { - "start": 98.4, - "end": 100.8 - }, - "arabic": "تفوت على عشّ الخنفسة؟»", - "hebrew_transliteration": "תְפוּת עַלַא עֻשّ לְ-חֻ'נפֻסֶה?", - "hebrew": "להיכנס לקן של החיפושית?", - "expected_phonemes": "t f uː t | ʕ a l a | ʕ u ʃ ʃ | l x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 100.8, - "end": 104.6 - }, - "arabic": "«خلّيني أجرّب،» قالت فارة، «بلكي قدرت.»", - "hebrew_transliteration": "חַ'לִّינִי אַגַ'רֶّבּ, קַאלַת פַארַה, בַּלְכִּי קִדְרֶת.", - "hebrew": "תני לי לנסות, אמרה העכברה, אולי אצליח.", - "expected_phonemes": "x a l l iː n i | ʔ a dʒ a r r e b | ʔ aː l a t | f aː r a | b a l k i | ʔ i d r e t" - }, - { - "voice_time_seconds": { - "start": 104.6, - "end": 107.2 - }, - "arabic": "«طيّب، جرّبي.»", - "hebrew_transliteration": "טַיֶّבּ, גַ'רְّבִּי.", - "hebrew": "טוב, נסי.", - "expected_phonemes": "tˤ a j j e b | dʒ a r r b i" - }, - { - "voice_time_seconds": { - "start": 107.2, - "end": 108.8 - }, - "arabic": "وفارة حاولت:", - "hebrew_transliteration": "וּפַארַה חַאוְלַת:", - "hebrew": "והעכברה ניסתה:", - "expected_phonemes": "u f aː r a | ħ aː w l a t" - }, - { - "voice_time_seconds": { - "start": 108.8, - "end": 110.8 - }, - "arabic": "فوّتت إجر واحدة،", - "hebrew_transliteration": "פַוַّתַת אִגֶ'ר וַאחדֶה,", - "hebrew": "הכניסה רגל אחת,", - "expected_phonemes": "f a w w a t a t | ʔ i dʒ e r | w aː ħ d e" - }, - { - "voice_time_seconds": { - "start": 110.8, - "end": 113.0 - }, - "arabic": "فوّتت الإجر الثّانية،", - "hebrew_transliteration": "פַוַّתַת לְ-אִגֶ'ר תַּאנְיֶה,", - "hebrew": "הכניסה את הרגל השנייה,", - "expected_phonemes": "f a w w a t a t | l ʔ i dʒ e r | t aː n j e" - }, - { - "voice_time_seconds": { - "start": 113.0, - "end": 117.0 - }, - "arabic": "طوت ذنبها، قلّصت شواربها، وهوپ!", - "hebrew_transliteration": "טַוַّת דַנַבְּהַא, קַלַّצַת שַוַארְבְּהַא, וּהוֹפּ!", - "hebrew": "קיפלה את זנבה, כיווצה את שפמה, והופ!", - "expected_phonemes": "tˤ a w w a t | d a n a b h a | ʔ a l l a sˤ a t | ʃ a w aː r b h a | u h oː f" - }, - { - "voice_time_seconds": { - "start": 117.0, - "end": 121.6 - }, - "arabic": "وهيك بقلب العشّ قاعدات: خنفسة وفارة.", - "hebrew_transliteration": "וּהֵיכּ בְּקַלבּ לְ-עֻשّ קַאעְדַאת: חֻ'נפֻסֶה וּפַארַה.", - "hebrew": "וכך בתוך הקן יושבות: חיפושית ועכברה.", - "expected_phonemes": "u h eː k | b ʔ a l b | l ʕ u ʃ ʃ | ʔ aː ʕ d aː t | x u n f u s e | u f aː r a" - }, - { - "voice_time_seconds": { - "start": 121.6, - "end": 123.6 - }, - "arabic": "خنفسة وفارة قاعدات،", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה קַאעְדַאת,", - "hebrew": "החיפושית והעכברה יושבות,", - "expected_phonemes": "x u n f u s e | u f aː r a | ʔ aː ʕ d aː t" - }, - { - "voice_time_seconds": { - "start": 123.6, - "end": 127.2 - }, - "arabic": "بشربوا شاي وبحكوا عن هاد وهداك", - "hebrew_transliteration": "בִּישרַבּוּ שַאי וּבִּיחכּוּ עַן הַאד וּהַדַאכּ", - "hebrew": "שותות תה ומדברות על זה ועל זה", - "expected_phonemes": "b iː ʃ r a b u | ʃ aː j | u b iː ħ k u | ʕ a n | h aː d | u h a d aː k" - }, - { - "voice_time_seconds": { - "start": 127.2, - "end": 129.6 - }, - "arabic": "وبرّا الدّنيا عم بتشتّي:", - "hebrew_transliteration": "וּבַּרַّא אִ-דֻּנְיַא עַם-בִּתשַתִّי:", - "hebrew": "ובחוץ יורד גשם:", - "expected_phonemes": "u b a r r a | ʔ i d u n j a | ʕ a m b i t ʃ a t t i" - }, - { - "voice_time_seconds": { - "start": 129.6, - "end": 135.8 - }, - "arabic": "تيپ توپ، تيپ توپ، تيپ توپ!", - "hebrew_transliteration": "טִיפ טוּפ, טִיפ טוּפ, טִיפ טוּפ!", - "hebrew": "טיפ טופ, טיפ טופ, טיפ טופ!", - "expected_phonemes": "tˤ iː f | tˤ uː f | tˤ iː f | tˤ uː f | tˤ iː f | tˤ uː f" - }, - { - "voice_time_seconds": { - "start": 135.8, - "end": 140.0 - }, - "arabic": "الشّتا عم بنزل وبقوى أكتر وأكتر", - "hebrew_transliteration": "אִ-שִּׁתַא עַם-בִּינזַל וּבִּיקוַא אַכְּתַר וּאַכְּתַר", - "hebrew": "הגשם יורד ומתחזק יותר ויותר", - "expected_phonemes": "ʔ i ʃ i t a | ʕ a m b iː n z a l | u b iː ʔ w a | ʔ a k t a r | u ʔ a k t a r" - }, - { - "voice_time_seconds": { - "start": 140.0, - "end": 141.8 - }, - "arabic": "والهوا عم بهبّ:", - "hebrew_transliteration": "וִל-הַוַא עַם-בִּיהֶבّ:", - "hebrew": "והרוח נושבת:", - "expected_phonemes": "w i l h a w a | ʕ a m b iː h e b b" - }, - { - "voice_time_seconds": { - "start": 141.8, - "end": 147.2 - }, - "arabic": "تيپ توپ، تيپ توپ، تيپي تيپي تيپي توپ!", - "hebrew_transliteration": "טִיפ טוּפ, טִיפ טוּפ, טִיפִּי טִיפִּי טִיפִּי טוּפ!", - "hebrew": "טיפ טופ, טיפ טופ, טיפי טיפי טיפי טופ!", - "expected_phonemes": "tˤ iː f | tˤ uː f | tˤ iː f | tˤ uː f | tˤ iː f i | tˤ iː f i | tˤ iː f i | tˤ uː f" - }, - { - "voice_time_seconds": { - "start": 147.2, - "end": 149.4 - }, - "arabic": "وفجأة تهيّأ إلهم", - "hebrew_transliteration": "וּפַגְ'אַה תְהַיַّא אִלְהֹם", - "hebrew": "ופתאום נדמה להן", - "expected_phonemes": "u f a dʒ ʔ a | t h a j j a | i l h o m" - }, - { - "voice_time_seconds": { - "start": 149.4, - "end": 152.4 - }, - "arabic": "إنهم سمعوا صوت دقّ على الباب.", - "hebrew_transliteration": "אִנְّהֹם סִמעוּ צוֹת דַקّ עַלַא לְ-בַּאבּ.", - "hebrew": "ששמעו קול דפיקה בדלת.", - "expected_phonemes": "ʔ i n n h o m | s i m ʕ u | sˤ oː t | d a ʔ ʔ | ʕ a l a | l b aː b" - }, - { - "voice_time_seconds": { - "start": 152.4, - "end": 155.0 - }, - "arabic": "«مين؟» سألت خنفسة.", - "hebrew_transliteration": "מִין? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מי? שאלה החיפושית.", - "expected_phonemes": "m iː n | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 155.0, - "end": 157.6 - }, - "arabic": "«مين؟» سألت فارة.", - "hebrew_transliteration": "מִין? סַאַלַת פַארַה.", - "hebrew": "מי? שאלה העכברה.", - "expected_phonemes": "m iː n | s a ʔ a l a t | f aː r a" - }, - { - "voice_time_seconds": { - "start": 157.6, - "end": 160.6 - }, - "arabic": "«هاي أنا، جاجة أم نقّارة!»", - "hebrew_transliteration": "הַאי אַנַא, גַ'אגֶ'ת אֻםّ נַקַّארַה!", - "hebrew": "זו אני, התרנגולת אם נקארה!", - "expected_phonemes": "h aː i | ʔ a n a | dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 160.6, - "end": 163.4 - }, - "arabic": "«شو بدّك يا جاجة أم نقّارة؟»", - "hebrew_transliteration": "שוּ בַּדֶّכּ יַא גַ'אגֶ'ת אֻםّ נַקַّארַה?", - "hebrew": "מה את רוצה, תרנגולת אם נקארה?", - "expected_phonemes": "ʃ u | b a d d e k | j a | dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 163.4, - "end": 165.4 - }, - "arabic": "جاجة أم نقّارة قالت:", - "hebrew_transliteration": "גַ'אגֶ'ת אֻםّ נַקַّארַה קַאלַת:", - "hebrew": "התרנגולת אם נקארה אמרה:", - "expected_phonemes": "dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a | ʔ aː l a t" - }, - { - "voice_time_seconds": { - "start": 165.4, - "end": 166.8 - }, - "arabic": "«أنا بردانة!", - "hebrew_transliteration": "אַנַא בַּרדַאנֶה!", - "hebrew": "קר לי!", - "expected_phonemes": "ʔ a n a | b a r d aː n e" - }, - { - "voice_time_seconds": { - "start": 166.8, - "end": 170.6 - }, - "arabic": "الدّنيا عم بتشتّي، كلّ ريشاتي مبلولات.", - "hebrew_transliteration": "אִ-דֻּנְיַא עַם-בִּתשַתִّי, כֻּלّ רִישַאתִי מַבְּלוּלַאת.", - "hebrew": "יורד גשם, כל הנוצות שלי רטובות.", - "expected_phonemes": "ʔ i d u n j a | ʕ a m b i t ʃ a t t i | k u l l | r iː ʃ aː t i | m a b l uː l aː t" - }, - { - "voice_time_seconds": { - "start": 170.6, - "end": 172.8 - }, - "arabic": "بلكي بقدر أفوت؟»", - "hebrew_transliteration": "בַּלְכִּי בַּקדַר אַפוּת?", - "hebrew": "אולי אוכל להיכנס?", - "expected_phonemes": "b a l k i | b a ʔ d a r | ʔ a f uː t" - }, - { - "voice_time_seconds": { - "start": 172.8, - "end": 174.8 - }, - "arabic": "خنفسة جاوبت وقالت:", - "hebrew_transliteration": "חֻ'נפֻסֶה גַ'אוַבַּת וּקַאלַת:", - "hebrew": "החיפושית ענתה ואמרה:", - "expected_phonemes": "x u n f u s e | dʒ aː w a b a t | u ʔ aː l a t" - }, - { - "voice_time_seconds": { - "start": 174.8, - "end": 177.8 - }, - "arabic": "«أنا كتير متأسّفة، سيّدة أم نقّارة،", - "hebrew_transliteration": "אַנַא כְּתִיר מִתאַסְّפֶה, סַיֶّדֶה אֻםّ נַקַّארַה,", - "hebrew": "אני מצטערת מאוד, גברת אם נקארה,", - "expected_phonemes": "ʔ a n a | k t iː r | m i t ʔ a s s f e | s a j j e d e | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 177.8, - "end": 180.0 - }, - "arabic": "كان بفوّتك بكلّ سرور،", - "hebrew_transliteration": "כַּאן בַּפַוְּתֶכּ בְּכֻלّ סֻרוּר,", - "hebrew": "הייתי מכניסה אותך בשמחה רבה,", - "expected_phonemes": "k aː n | b a f a w t e k | b k u l l | s u r uː r" - }, - { - "voice_time_seconds": { - "start": 180.0, - "end": 182.4 - }, - "arabic": "بسّ فش عندي محلّ.", - "hebrew_transliteration": "בַּס פִש עִנדִי מַחַלّ.", - "hebrew": "אבל אין לי מקום.", - "expected_phonemes": "b a s | f i ʃ | ʕ i n d i | m a ħ a l l" - }, - { - "voice_time_seconds": { - "start": 182.4, - "end": 186.4 - }, - "arabic": "جاجة بعشّ خنفسة، هاد مش معقول!»", - "hebrew_transliteration": "גַ'אגֶ'ה בְּעֻשّ חֻ'נפֻסֶה, הַאד מִש מַעקוּל!", - "hebrew": "תרנגולת בקן של חיפושית, זה לא הגיוני!", - "expected_phonemes": "dʒ aː dʒ e | b ʕ u ʃ ʃ | x u n f u s e | h aː d | m i ʃ | m a ʕ ʔ uː l" - }, - { - "voice_time_seconds": { - "start": 186.4, - "end": 189.0 - }, - "arabic": "«فش إشي بوقّف قدّام الإرادة،»", - "hebrew_transliteration": "פִש אִשִי בִּוַקֶّף קֻדַّאם לְ-אִרַאדֶה,", - "hebrew": "שום דבר לא עומד בפני הרצון,", - "expected_phonemes": "f i ʃ | ʔ i ʃ i | b i w a ʔ ʔ e f | ʔ u d d aː m | l ʔ i r aː d e" - }, - { - "voice_time_seconds": { - "start": 189.0, - "end": 190.8 - }, - "arabic": "قالت جاجة أم نقّارة،", - "hebrew_transliteration": "קַאלַת גַ'אגֶ'ת אֻםّ נַקַّארַה,", - "hebrew": "אמרה התרנגולת אם נקארה,", - "expected_phonemes": "ʔ aː l a t | dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 190.8, - "end": 192.8 - }, - "arabic": "«خلّوني أجرّب.»", - "hebrew_transliteration": "חַלּוּנִי אַגַ'רֶّבּ.", - "hebrew": "תנו לי לנסות.", - "expected_phonemes": "ħ a l uː n i | ʔ a dʒ a r r e b" - }, - { - "voice_time_seconds": { - "start": 192.8, - "end": 194.8 - }, - "arabic": "«طيّب، جرّبي.»", - "hebrew_transliteration": "טַיֶّבּ, גַ'רְّבִּי.", - "hebrew": "טוב, נסי.", - "expected_phonemes": "tˤ a j j e b | dʒ a r r b i" - }, - { - "voice_time_seconds": { - "start": 194.8, - "end": 198.2 - }, - "arabic": "وجاجة أم نقّارة بتجرّب وبتحاول:", - "hebrew_transliteration": "וּגַ'אגֶ'ת אֻםّ נַקַّארַה בִּתגַ'רֶّבּ וּבִּתחַאוֶל:", - "hebrew": "והתרנגולת אם נקארה מנסה ומשתדלת:", - "expected_phonemes": "u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a | b i t dʒ a r r e b | u b i t ħ aː w e l" - }, - { - "voice_time_seconds": { - "start": 198.2, - "end": 201.6 - }, - "arabic": "بتطوي عرفها، بتكتّ تلت ريشات،", - "hebrew_transliteration": "בְּתִטוִי עֻרְפְהַא, בְּתְכֻּתّ תַלַת רִישַאת,", - "hebrew": "מקפלת את כרבולתה, משירה שלוש נוצות,", - "expected_phonemes": "b t i tˤ w i | ʕ u r f h a | b t k u t t | t a l a t | r iː ʃ aː t" - }, - { - "voice_time_seconds": { - "start": 201.6, - "end": 204.6 - }, - "arabic": "بتدحش هون، بتضغط هناك،", - "hebrew_transliteration": "בְּתִדחַש הוֹן, בְּתִדְ'עַ'ט הֻנַאכּ,", - "hebrew": "נדחקת כאן, לוחצת שם,", - "expected_phonemes": "b t i d ħ a ʃ | h oː n | b t i dˤ ɣ a tˤ | h u n aː k" - }, - { - "voice_time_seconds": { - "start": 204.6, - "end": 209.2 - }, - "arabic": "بسّ النّهاي سعيدة – كلّ إشي منيح.", - "hebrew_transliteration": "בַּס נִּהַאי סַעִידֶה – כֻּלّ אִשִי מְנִיח.", - "hebrew": "אבל הסוף טוב – הכול בסדר.", - "expected_phonemes": "b a s | n i h aː i | s a ʕ iː d e | k u l l | ʔ i ʃ i | m n iː ħ" - }, - { - "voice_time_seconds": { - "start": 209.2, - "end": 212.2 - }, - "arabic": "جاجة أم نقّارة قدرت تفوت.", - "hebrew_transliteration": "גַ'אגֶ'ת אֻםّ נַקַّארַה קִדְרַת תְפוּת.", - "hebrew": "התרנגולת אם נקארה הצליחה להיכנס.", - "expected_phonemes": "dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a | ʔ i d r a t | t f uː t" - }, - { - "voice_time_seconds": { - "start": 212.2, - "end": 215.0 - }, - "arabic": "وبقلب العشّ قاعدين:", - "hebrew_transliteration": "וּבְּקַלבּ לְ-עֻשّ קַאעְדִין:", - "hebrew": "ובתוך הקן יושבות:", - "expected_phonemes": "u b ʔ a l b | l ʕ u ʃ ʃ | ʔ aː ʕ d iː n" - }, - { - "voice_time_seconds": { - "start": 215.0, - "end": 219.2 - }, - "arabic": "خنفسة وفارة وجاجة أم نقّارة!", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה וּגַ'אגֶ'ת אֻםّ נַקַّארַה!", - "hebrew": "חיפושית ועכברה ותרנגולת אם נקארה!", - "expected_phonemes": "x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 219.2, - "end": 221.6 - }, - "arabic": "كمان مرّة! كلّنا سوا:", - "hebrew_transliteration": "כַּמַאן מַרַّה! כֻּלְّנַא סַוַא:", - "hebrew": "עוד פעם! כולנו יחד:", - "expected_phonemes": "k a m aː n | m a r r a | k u l l n a | s a w a" - }, - { - "voice_time_seconds": { - "start": 221.6, - "end": 225.2 - }, - "arabic": "خنفسة وفارة وجاجة أم نقّارة!", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה וּגַ'אגֶ'ת אֻםّ נַקַّארַה!", - "hebrew": "חיפושית ועכברה ותרנגולת אם נקארה!", - "expected_phonemes": "x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 225.2, - "end": 228.8 - }, - "arabic": "قاعدات خنفسة وفارة وجاجة أم نقّارة،", - "hebrew_transliteration": "קַאעְדַאת חֻ'נפֻסֶה וּפַארַה וּגַ'אגֶ'ת אֻםّ נַקַّארַה,", - "hebrew": "יושבות החיפושית והעכברה והתרנגולת אם נקארה,", - "expected_phonemes": "ʔ aː ʕ d aː t | x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 228.8, - "end": 231.6 - }, - "arabic": "قاعدات وبتدفّوا جنب الفرن.", - "hebrew_transliteration": "קַאעְדַאת וּבִּתדַפַّוּ גַ'נבּ לְ-פֻרֶן.", - "hebrew": "יושבות ומתחממות ליד התנור.", - "expected_phonemes": "ʔ aː ʕ d aː t | u b i t d a f f a w | dʒ a n b | l f u r e n" - }, - { - "voice_time_seconds": { - "start": 231.6, - "end": 233.0 - }, - "arabic": "فارة بتطبخ،", - "hebrew_transliteration": "פַארַה בְּתֻטבֻּחְ',", - "hebrew": "העכברה מבשלת,", - "expected_phonemes": "f aː r a | b t u tˤ b u x" - }, - { - "voice_time_seconds": { - "start": 233.0, - "end": 236.2 - }, - "arabic": "جاجة أم نقّارة بتقدّم الأكل للطّاولة،", - "hebrew_transliteration": "גַ'אגֶ'ת אֻםّ נַקַّארַה בִּתקַדֶّם לְ-אַכֶּל לַ-טַּאוְלֶה,", - "hebrew": "התרנגולת אם נקארה מגישה את האוכל לשולחן,", - "expected_phonemes": "dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a | b i t ʔ a d d e m | l ʔ a k e l | l a tˤ aː w l e" - }, - { - "voice_time_seconds": { - "start": 236.2, - "end": 239.4 - }, - "arabic": "وكلّهن قاعدات يوكلوا مبسوطات.", - "hebrew_transliteration": "וּכֻּלְّהֶן קַאעְדַאת יוֹכְּלוּ מַבְּסוּטַאת.", - "hebrew": "וכולן יושבות ואוכלות בשמחה.", - "expected_phonemes": "u k u l l h e n | ʔ aː ʕ d aː t | j oː k l u | m a b s uː tˤ aː t" - }, - { - "voice_time_seconds": { - "start": 239.4, - "end": 242.0 - }, - "arabic": "وبرّا الدّنيا عم بتشتّي:", - "hebrew_transliteration": "וּבַּרַّא אִ-דֻּנְיַא עַם-בִּתשַתִّי:", - "hebrew": "ובחוץ יורד גשם:", - "expected_phonemes": "u b a r r a | ʔ i d u n j a | ʕ a m b i t ʃ a t t i" - }, - { - "voice_time_seconds": { - "start": 242.0, - "end": 247.6 - }, - "arabic": "تيپ توپ، تيپ توپ، تيپي تيپي توپ!", - "hebrew_transliteration": "טִיפ טוּפ, טִיפ טוּפ, טִיפִּי טִיפִּי טוּפ!", - "hebrew": "טיפ טופ, טיפ טופ, טיפי טיפי טופ!", - "expected_phonemes": "tˤ iː f | tˤ uː f | tˤ iː f | tˤ uː f | tˤ iː f i | tˤ iː f i | tˤ uː f" - }, - { - "voice_time_seconds": { - "start": 247.6, - "end": 250.8 - }, - "arabic": "وفجأة سمعوا صوت: بوم!", - "hebrew_transliteration": "וּפַגְ'אַה סִמעוּ צוֹת: בּוּם!", - "hebrew": "ופתאום שמעו קול: בום!", - "expected_phonemes": "u f a dʒ ʔ a | s i m ʕ u | sˤ oː t | b uː m" - }, - { - "voice_time_seconds": { - "start": 250.8, - "end": 253.6 - }, - "arabic": "وكمان مرّة: بوم! بوم!", - "hebrew_transliteration": "וּכַּמַאן מַרַّה: בּוּם! בּוּם!", - "hebrew": "ועוד פעם: בום! בום!", - "expected_phonemes": "u k a m aː n | m a r r a | b uː m | b uː m" - }, - { - "voice_time_seconds": { - "start": 253.6, - "end": 255.6 - }, - "arabic": "«شو هاد؟» سألت خنفسة.", - "hebrew_transliteration": "שוּ הַאד? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מה זה? שאלה החיפושית.", - "expected_phonemes": "ʃ u | h aː d | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 255.6, - "end": 257.8 - }, - "arabic": "«مين هناك؟» سألت فارة.", - "hebrew_transliteration": "מִין הֻנַאכּ? סַאַלַת פַארַה.", - "hebrew": "מי שם? שאלה העכברה.", - "expected_phonemes": "m iː n | h u n aː k | s a ʔ a l a t | f aː r a" - }, - { - "voice_time_seconds": { - "start": 257.8, - "end": 261.2 - }, - "arabic": "«شو هاي الأصوات؟» قالت جاجة أم نقّارة.", - "hebrew_transliteration": "שוּ הַאי לְ-אַצוַאת? קַאלַת גַ'אגֶ'ת אֻםّ נַקַّארַה.", - "hebrew": "מה הקולות האלה? אמרה התרנגולת אם נקארה.", - "expected_phonemes": "ʃ u | h aː i | l ʔ a sˤ w aː t | ʔ aː l a t | dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 261.2, - "end": 264.0 - }, - "arabic": "هدوء. فش جواب.", - "hebrew_transliteration": "הֻדוּא. פִש גַ'וַאבּ.", - "hebrew": "שקט. אין תשובה.", - "expected_phonemes": "h u d uː a | f i ʃ | dʒ a w aː b" - }, - { - "voice_time_seconds": { - "start": 264.0, - "end": 266.2 - }, - "arabic": "وفجأة كمان مرّة:", - "hebrew_transliteration": "וּפַגְ'אַה כַּמַאן מַרַّה:", - "hebrew": "ופתאום עוד פעם:", - "expected_phonemes": "u f a dʒ ʔ a | k a m aː n | m a r r a" - }, - { - "voice_time_seconds": { - "start": 266.2, - "end": 269.6 - }, - "arabic": "بوم! بو-بو-بو-بوم! بوم!", - "hebrew_transliteration": "בּוּם! בּוּ-בּוּ-בּוּ-בּוּם! בּוּם!", - "hebrew": "בום! בו-בו-בו-בום! בום!", - "expected_phonemes": "b uː m | b uː b uː b uː b uː m | b uː m" - }, - { - "voice_time_seconds": { - "start": 269.6, - "end": 272.8 - }, - "arabic": "خنفسة راحت لعند الشّبّاك،", - "hebrew_transliteration": "חֻ'נפֻסֶה רַאחַת לַעִנד שֻּׁבַּّאכּ,", - "hebrew": "החיפושית הלכה אל החלון,", - "expected_phonemes": "x u n f u s e | r aː ħ a t | l a ʕ i n d | ʃ u b b aː k" - }, - { - "voice_time_seconds": { - "start": 272.8, - "end": 276.0 - }, - "arabic": "زاحت البرداي، وشو شافت؟", - "hebrew_transliteration": "זַאחַת לְ-בֻּרדַאי, וּשוּ שַאפַת?", - "hebrew": "הזיזה את הווילון, ומה ראתה?", - "expected_phonemes": "z aː ħ a t | l b u r d aː i | u ʃ u | ʃ aː f a t" - }, - { - "voice_time_seconds": { - "start": 276.0, - "end": 279.6 - }, - "arabic": "برد عم بنزل، وعم بضرب على الباب،", - "hebrew_transliteration": "בַּרַד עַם-בִּינזַל, וּעַם-בִּידְ'רֻבּ עַלַא לְ-בַּאבּ,", - "hebrew": "ברד יורד, ומכה על הדלת,", - "expected_phonemes": "b a r a d | ʕ a m b iː n z a l | u ʕ a m b iː dˤ r u b | ʕ a l a | l b aː b" - }, - { - "voice_time_seconds": { - "start": 279.6, - "end": 284.4 - }, - "arabic": "وبدوب على لتريس، بسّ جوّا دفا وعفا.", - "hebrew_transliteration": "וּבִּדוּבּ עַלַא לְ-תְרִיס, בַּס גֻ'וַّא דַפַא וּעַפַא.", - "hebrew": "ונמס על המרפסת, אבל בפנים חם ונעים.", - "expected_phonemes": "u b i d uː b | ʕ a l a | l t r iː s | b a s | dʒ u w w a | d a f a | u ʕ a f a" - }, - { - "voice_time_seconds": { - "start": 284.4, - "end": 287.0 - }, - "arabic": "وفجأة، مع صوت البرد،", - "hebrew_transliteration": "וּפַגְ'אַה, מַע צוֹת לְ-בַּרַד,", - "hebrew": "ופתאום, עם קול הברד,", - "expected_phonemes": "u f a dʒ ʔ a | m a ʕ | sˤ oː t | l b a r a d" - }, - { - "voice_time_seconds": { - "start": 287.0, - "end": 289.8 - }, - "arabic": "بسمعوا صوت دقّ على الباب.", - "hebrew_transliteration": "בִּסמְעוּ צוֹת דַקّ עַלַא לְ-בַּאבּ.", - "hebrew": "שומעות קול דפיקה בדלת.", - "expected_phonemes": "b i s m ʕ u | sˤ oː t | d a ʔ ʔ | ʕ a l a | l b aː b" - }, - { - "voice_time_seconds": { - "start": 289.8, - "end": 293.2 - }, - "arabic": "«بتهيّألي حدا عم بدقّ!» قالت فارة.", - "hebrew_transliteration": "בִּתהַיַّאלִי חַדַא עַם-בִּדֻקّ! קַאלַת פַארַה.", - "hebrew": "נדמה לי שמישהו דופק! אמרה העכברה.", - "expected_phonemes": "b i t h a j j aː l i | ħ a d a | ʕ a m b i d u ʔ ʔ | ʔ aː l a t | f aː r a" - }, - { - "voice_time_seconds": { - "start": 293.2, - "end": 296.0 - }, - "arabic": "«مين هناك؟» سألت خنفسة.", - "hebrew_transliteration": "מִין הֻנַאכּ? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מי שם? שאלה החיפושית.", - "expected_phonemes": "m iː n | h u n aː k | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 296.0, - "end": 299.2 - }, - "arabic": "«مين هاد؟» قالت جاجة أم نقّارة.", - "hebrew_transliteration": "מִין הַאד? קַאלַת גַ'אגֶ'ת אֻםّ נַקַّארַה.", - "hebrew": "מי זה? אמרה התרנגולת אם נקארה.", - "expected_phonemes": "m iː n | h aː d | ʔ aː l a t | dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 299.2, - "end": 302.6 - }, - "arabic": "«هاد أنا، ديك أبو سحّارة!»", - "hebrew_transliteration": "הַאד אַנַא, דִיכּ אַבּוּ סַחַّארַה!", - "hebrew": "זה אני, התרנגול אבו סחארה!", - "expected_phonemes": "h aː d | ʔ a n a | d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 302.6, - "end": 306.6 - }, - "arabic": "«شو بدّك؟ ديك أبو سحّارة؟» سألت خنفسة.", - "hebrew_transliteration": "שוּ בַּדַّכּ, דִיכּ אַבּוּ סַחַّארַה? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מה אתה רוצה, תרנגול אבו סחארה? שאלה החיפושית.", - "expected_phonemes": "ʃ u | b a d d a k | d iː k | ʔ a b u | s a ħ ħ aː r a | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 306.6, - "end": 309.6 - }, - "arabic": "«سمعت إنّه مرتي موجودة هون.", - "hebrew_transliteration": "סִמִעֶת אִנּוֹ מַרְתִי מַוְגּ'וּדֶה הוֹן.", - "hebrew": "שמעתי שאשתי נמצאת כאן.", - "expected_phonemes": "s i m i ʕ e t | ʔ i n o | m a r t i | m a w dʒ uː d e | h oː n" - }, - { - "voice_time_seconds": { - "start": 309.6, - "end": 311.4 - }, - "arabic": "بدّي أنضمّلها.", - "hebrew_transliteration": "בַּדִّי אַנדַ'מַّ-לְהַא.", - "hebrew": "אני רוצה להצטרף אליה.", - "expected_phonemes": "b a d d i | ʔ a n dˤ a m m a l h a" - }, - { - "voice_time_seconds": { - "start": 311.4, - "end": 314.8 - }, - "arabic": "أنا بردان، البرد عم بضرب على جسمي.", - "hebrew_transliteration": "אַנַא בַּרדַאן, אִל-בַּרַד עַם-בִּידְ'רֻבּ עַלַא גִ'סמִי.", - "hebrew": "קר לי, הברד מכה על גופי.", - "expected_phonemes": "ʔ a n a | b a r d aː n | i l b a r a d | ʕ a m b iː dˤ r u b | ʕ a l a | dʒ i s m i" - }, - { - "voice_time_seconds": { - "start": 314.8, - "end": 317.6 - }, - "arabic": "إعملوا معروف، خلّوني أفوت!»", - "hebrew_transliteration": "אִעמַלוּ מַערוּף, חַלּוּנִי אַפוּת!", - "hebrew": "עשו טובה, תנו לי להיכנס!", - "expected_phonemes": "ʔ i ʕ m a l u | m a ʕ r uː f | ħ a l uː n i | ʔ a f uː t" - }, - { - "voice_time_seconds": { - "start": 317.6, - "end": 320.8 - }, - "arabic": "«بسّ يا ديك أبو سحّارة،» قالت خنفسة،", - "hebrew_transliteration": "בַּס יַא דִיכּ אַבּוּ סַחַّארַה, קַאלַת חֻ'נפֻסֶה,", - "hebrew": "אבל, תרנגול אבו סחארה, אמרה החיפושית,", - "expected_phonemes": "b a s | j a | d iː k | ʔ a b u | s a ħ ħ aː r a | ʔ aː l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 320.8, - "end": 325.4 - }, - "arabic": "«كيف رح يقدر ديك، هالقدّ رائع وكبير،", - "hebrew_transliteration": "כִּיף רַח-יִקדַר דִיכּ, הַל-קַדّ רַאאֶע וּכְּבִּיר,", - "hebrew": "איך יוכל תרנגול, נהדר וגדול כל כך,", - "expected_phonemes": "k iː f | r a ħ j i ʔ d a r | d iː k | h a l ʔ a d d | r aː ʔ e ʕ | u k b iː r" - }, - { - "voice_time_seconds": { - "start": 325.4, - "end": 327.6 - }, - "arabic": "يفوت على عشّ الخنفسة؟", - "hebrew_transliteration": "יְפוּת עַלַא עֻשّ לְ-חֻ'נפֻסֶה?", - "hebrew": "להיכנס לקן של החיפושית?", - "expected_phonemes": "j f uː t | ʕ a l a | ʕ u ʃ ʃ | l x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 327.6, - "end": 330.2 - }, - "arabic": "عم تطلب إشي مستحيل!»", - "hebrew_transliteration": "עַם-תֻטלֹבּ אִשִי מֻסתַחִיל!", - "hebrew": "אתה מבקש דבר בלתי אפשרי!", - "expected_phonemes": "ʕ a m t u tˤ l o b | ʔ i ʃ i | m u s t a ħ iː l" - }, - { - "voice_time_seconds": { - "start": 330.2, - "end": 332.8 - }, - "arabic": "«بحياتك،» قال ديك أبو سحّارة،", - "hebrew_transliteration": "בְּחַיַאתֶכּ, קַאל דִיכּ אַבּוּ סַחַّארַה,", - "hebrew": "בחייך, אמר התרנגול אבו סחארה,", - "expected_phonemes": "b ħ a j aː t e k | ʔ aː l | d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 332.8, - "end": 336.0 - }, - "arabic": "«إسمحيلي أفوت. برّا كتير برد.", - "hebrew_transliteration": "אִסמַחִילִי אַפוּת. בַּרַّא כְּתִיר בַּרְד.", - "hebrew": "הרשי לי להיכנס. בחוץ קר מאוד.", - "expected_phonemes": "ʔ i s m a ħ iː l i | ʔ a f uː t | b a r r a | k t iː r | b a r d" - }, - { - "voice_time_seconds": { - "start": 336.0, - "end": 337.8 - }, - "arabic": "إذا مش رح ألاقي ملجأ،", - "hebrew_transliteration": "אִזַא מִש רַח-אַלַאקִי מַלגַ'א,", - "hebrew": "אם לא אמצא מחסה,", - "expected_phonemes": "ʔ i z a | m i ʃ | r a ħ ʔ a l aː ʔ i | m a l dʒ a" - }, - { - "voice_time_seconds": { - "start": 337.8, - "end": 339.6 - }, - "arabic": "رح أجمّد هون للموت.", - "hebrew_transliteration": "רַח-אַגַ'מֶّד הוֹן לַל-מוֹת.", - "hebrew": "אקפא כאן למוות.", - "expected_phonemes": "r a ħ ʔ a dʒ a m m e d | h oː n | l a l m oː t" - }, - { - "voice_time_seconds": { - "start": 339.6, - "end": 341.6 - }, - "arabic": "خلّوني أحاول.»", - "hebrew_transliteration": "חַלּוּנִי אַחַאוֶל.", - "hebrew": "תנו לי לנסות.", - "expected_phonemes": "ħ a l uː n i | ʔ a ħ aː w e l" - }, - { - "voice_time_seconds": { - "start": 341.6, - "end": 343.8 - }, - "arabic": "«طيّب،» قالت خنفسة،", - "hebrew_transliteration": "טַיֶّבּ, קַאלַת חֻ'נפֻסֶה,", - "hebrew": "טוב, אמרה החיפושית,", - "expected_phonemes": "tˤ a j j e b | ʔ aː l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 343.8, - "end": 346.2 - }, - "arabic": "«إذا نجحت – تفضّل.»", - "hebrew_transliteration": "אִזַא נִגְ'חֶת – תְפַדַّל.", - "hebrew": "אם תצליח – בבקשה.", - "expected_phonemes": "ʔ i z a | n i dʒ ħ e t | t f a dˤ dˤ a l" - }, - { - "voice_time_seconds": { - "start": 346.2, - "end": 348.8 - }, - "arabic": "وديك أبو سحّارة بحاول:", - "hebrew_transliteration": "וּדִיכּ אַבּוּ סַחַّארַה בִּחַאוֶל:", - "hebrew": "והתרנגול אבו סחארה מנסה:", - "expected_phonemes": "u d iː k | ʔ a b u | s a ħ ħ aː r a | b i ħ aː w e l" - }, - { - "voice_time_seconds": { - "start": 348.8, - "end": 352.4 - }, - "arabic": "فوّت إجر واحدة، فوّت الإجر التّانية،", - "hebrew_transliteration": "פַוַّת אִגֶ'ר וַאחדֶה, פַוַّת לְ-אִגֶ'ר תַּאנְיֶה,", - "hebrew": "הכניס רגל אחת, הכניס את הרגל השנייה,", - "expected_phonemes": "f a w w a t | ʔ i dʒ e r | w aː ħ d e | f a w w a t | l ʔ i dʒ e r | t aː n j e" - }, - { - "voice_time_seconds": { - "start": 352.4, - "end": 355.2 - }, - "arabic": "طوى عرفه، لفّ ذنبه،", - "hebrew_transliteration": "טַוַא עֻרְפוֹ, לַףّ דַנַבּוֹ,", - "hebrew": "קיפל את כרבולתו, גלגל את זנבו,", - "expected_phonemes": "tˤ a w a | ʕ u r f o | l a f f | d a n a b o" - }, - { - "voice_time_seconds": { - "start": 355.2, - "end": 357.8 - }, - "arabic": "كتّ تلت ريشات وإنحشر", - "hebrew_transliteration": "כַּתّ תַלַת רִישַאת וּאִנחַשַר", - "hebrew": "השיר שלוש נוצות ונדחק", - "expected_phonemes": "k a t t | t a l a t | r iː ʃ aː t | u ʔ i n ħ a ʃ a r" - }, - { - "voice_time_seconds": { - "start": 357.8, - "end": 361.4 - }, - "arabic": "و... هوپ! بقلب العشّ قاعدين –", - "hebrew_transliteration": "וּ... הוֹפּ! בְּקַלבּ לְ-עֻשّ קַאעְדִין –", - "hebrew": "ו... הופ! בתוך הקן יושבים –", - "expected_phonemes": "w | h oː f | b ʔ a l b | l ʕ u ʃ ʃ | ʔ aː ʕ d iː n" - }, - { - "voice_time_seconds": { - "start": 361.4, - "end": 364.6 - }, - "arabic": "الكلّ سوا معي:", - "hebrew_transliteration": "אִל-כֻּלّ סַוַא מַעִי:", - "hebrew": "כולם יחד איתי:", - "expected_phonemes": "i l k u l l | s a w a | m a ʕ i" - }, - { - "voice_time_seconds": { - "start": 364.6, - "end": 368.0 - }, - "arabic": "خنفسة وفارة وجاجة أم نقّارة", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה וּגַ'אגֶ'ת אֻםّ נַקַّארַה", - "hebrew": "חיפושית ועכברה ותרנגולת אם נקארה", - "expected_phonemes": "x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 368.0, - "end": 370.2 - }, - "arabic": "وديك أبو سحّارة!", - "hebrew_transliteration": "וּדִיכּ אַבּוּ סַחַّארַה!", - "hebrew": "ותרנגול אבו סחארה!", - "expected_phonemes": "u d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 370.2, - "end": 373.2 - }, - "arabic": "كمان مرّة! غنّوا كلّكو معي:", - "hebrew_transliteration": "כַּמַאן מַרַّה! עַ'נּוּ כֻּלְّכּוּ מַעִי:", - "hebrew": "עוד פעם! שירו כולכם איתי:", - "expected_phonemes": "k a m aː n | m a r r a | ɣ a n u | k u l l k u | m a ʕ i" - }, - { - "voice_time_seconds": { - "start": 373.2, - "end": 376.6 - }, - "arabic": "خنفسة وفارة وجاجة أم نقّارة", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה וּגַ'אגֶ'ת אֻםّ נַקַّארַה", - "hebrew": "חיפושית ועכברה ותרנגולת אם נקארה", - "expected_phonemes": "x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 376.6, - "end": 379.0 - }, - "arabic": "وديك أبو سحّارة!", - "hebrew_transliteration": "וּדִיכּ אַבּוּ סַחַّארַה!", - "hebrew": "ותרנגול אבו סחארה!", - "expected_phonemes": "u d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 379.0, - "end": 381.0 - }, - "arabic": "قاعدين خنفسة وفارة،", - "hebrew_transliteration": "קַאעְדִין חֻ'נפֻסֶה וּפַארַה,", - "hebrew": "יושבים החיפושית והעכברה,", - "expected_phonemes": "ʔ aː ʕ d iː n | x u n f u s e | u f aː r a" - }, - { - "voice_time_seconds": { - "start": 381.0, - "end": 384.2 - }, - "arabic": "وجاجة أم نقّارة وديك أبو سحّارة،", - "hebrew_transliteration": "וּגַ'אגֶ'ת אֻםّ נַקַّארַה וּדִיכּ אַבּוּ סַחַّארַה,", - "hebrew": "והתרנגולת אם נקארה והתרנגול אבו סחארה,", - "expected_phonemes": "u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a | u d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 384.2, - "end": 387.4 - }, - "arabic": "بقلب العشّ، قاعدين وبتدفّوا.", - "hebrew_transliteration": "בְּקַלבּ לְ-עֻשّ, קַאעְדִין וּבִּתדַפּוּ.", - "hebrew": "בתוך הקן, יושבים ומתחממים.", - "expected_phonemes": "b ʔ a l b | l ʕ u ʃ ʃ | ʔ aː ʕ d iː n | u b i t d a f u" - }, - { - "voice_time_seconds": { - "start": 387.4, - "end": 389.0 - }, - "arabic": "فارة بتقرا كتاب،", - "hebrew_transliteration": "פַארַה בְּתִקרַא כְּתַאבּ,", - "hebrew": "העכברה קוראת ספר,", - "expected_phonemes": "f aː r a | b t i ʔ r a | k t aː b" - }, - { - "voice_time_seconds": { - "start": 389.0, - "end": 391.6 - }, - "arabic": "خنفسة بتحضر تلفزيون،", - "hebrew_transliteration": "חֻ'נפֻסֶה בְּתִחְדַ'ר תִלִפִזיוֹן,", - "hebrew": "החיפושית צופה בטלוויזיה,", - "expected_phonemes": "x u n f u s e | b t i ħ dˤ a r | t i l i f i z j oː n" - }, - { - "voice_time_seconds": { - "start": 391.6, - "end": 394.2 - }, - "arabic": "ديك أبو سحّارة ملزّق بمرته", - "hebrew_transliteration": "דִיכּ אַבּוּ סַחַّארַה מְלַזֶّק בְּמַרְתוֹ", - "hebrew": "התרנגול אבו סחארה צמוד לאשתו", - "expected_phonemes": "d iː k | ʔ a b u | s a ħ ħ aː r a | m l a z z e ʔ | b m a r t o" - }, - { - "voice_time_seconds": { - "start": 394.2, - "end": 396.8 - }, - "arabic": "والكلّ فرحان ومبسوط.", - "hebrew_transliteration": "וִל-כֻּלّ פַרחַאן וּמַבְּסוּט.", - "hebrew": "וכולם שמחים ומרוצים.", - "expected_phonemes": "w i l k u l l | f a r ħ aː n | u m a b s uː tˤ" - }, - { - "voice_time_seconds": { - "start": 396.8, - "end": 399.4 - }, - "arabic": "وبرّا عم بنزل برد:", - "hebrew_transliteration": "וּבַּרַّא עַם-בִּינזַל בַּרַד:", - "hebrew": "ובחוץ יורד ברד:", - "expected_phonemes": "u b a r r a | ʕ a m b iː n z a l | b a r a d" - }, - { - "voice_time_seconds": { - "start": 399.4, - "end": 402.4 - }, - "arabic": "بوم بوم، بيلي بيلي بوم،", - "hebrew_transliteration": "בּוּם בּוּם, בִּילִי בִּילִי בּוּם,", - "hebrew": "בום בום, בילי בילי בום,", - "expected_phonemes": "b uː m | b uː m | b iː l i | b iː l i | b uː m" - }, - { - "voice_time_seconds": { - "start": 402.4, - "end": 405.6 - }, - "arabic": "بوم طراخ، طيخ طاخ!", - "hebrew_transliteration": "בּוּם טְרַאחַ', טִיחַ' טַאחַ'!", - "hebrew": "בום טראח', טיח' טאח'!", - "expected_phonemes": "b uː m | tˤ r aː x a | tˤ iː x a | tˤ aː x a" - }, - { - "voice_time_seconds": { - "start": 405.6, - "end": 408.4 - }, - "arabic": "البرد عم بنزل وعم بقوى،", - "hebrew_transliteration": "אִל-בַּרַד עַם-בִּינזַל וּעַם-בִּיקוַא,", - "hebrew": "הברד יורד ומתחזק,", - "expected_phonemes": "i l b a r a d | ʕ a m b iː n z a l | u ʕ a m b iː ʔ w a" - }, - { - "voice_time_seconds": { - "start": 408.4, - "end": 412.2 - }, - "arabic": "وبالقوّة صار الواحد قادر يحكي من الضّجّة،", - "hebrew_transliteration": "וּבִּל-קֻוֶּה צַאר לְ-וַאחַד קַאדֶר יִחכִּי מִן אִ-דַّ'גֶّה,", - "hebrew": "ובקושי אפשר לדבר מרוב הרעש,", - "expected_phonemes": "u b i l ʔ u w e | sˤ aː r | l w aː ħ a d | ʔ aː d e r | j i ħ k i | m i n | ʔ i dˤ dˤ a g g e" - }, - { - "voice_time_seconds": { - "start": 412.2, - "end": 414.6 - }, - "arabic": "وفجأة – بقلب كلّ الضّجّة", - "hebrew_transliteration": "וּפַגְ'אַה – בְּקַלבּ כֻּלّ אִ-דַّ'גֶّה", - "hebrew": "ופתאום – בתוך כל הרעש", - "expected_phonemes": "u f a dʒ ʔ a | b ʔ a l b | k u l l | ʔ i dˤ dˤ a g g e" - }, - { - "voice_time_seconds": { - "start": 414.6, - "end": 419.8 - }, - "arabic": "تهيّألهم عم بسمعوا صوت غريب برّا.", - "hebrew_transliteration": "תְהַיַّא-לְהֹם עַם-בִּסמְעוּ צוֹת עַ'רִיבּ בַּרַّא.", - "hebrew": "נדמה להם ששומעים קול מוזר בחוץ.", - "expected_phonemes": "t h a j j aː l h o m | ʕ a m b i s m ʕ u | sˤ oː t | ɣ a r iː b | b a r r a" - }, - { - "voice_time_seconds": { - "start": 419.8, - "end": 422.4 - }, - "arabic": "«مين هناك؟» سألت خنفسة.", - "hebrew_transliteration": "מִין הֻנַאכּ? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מי שם? שאלה החיפושית.", - "expected_phonemes": "m iː n | h u n aː k | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 422.4, - "end": 425.2 - }, - "arabic": "«شو ممكن يكون؟» قالت فارة.", - "hebrew_transliteration": "שוּ מֻמכֶּן יְכּוּן? קַאלַת פַארַה.", - "hebrew": "מה זה יכול להיות? אמרה העכברה.", - "expected_phonemes": "ʃ u | m u m k e n | j k uː n | ʔ aː l a t | f aː r a" - }, - { - "voice_time_seconds": { - "start": 425.2, - "end": 429.0 - }, - "arabic": "«بلكي لتريس زاح؟» قالت جاجة أم نقّارة.", - "hebrew_transliteration": "בַּלְכִּי לְ-תְרִיס זַאח? קַאלַת גַ'אגֶ'ת אֻםّ נַקַّארַה.", - "hebrew": "אולי המרפסת זזה? אמרה התרנגולת אם נקארה.", - "expected_phonemes": "b a l k i | l t r iː s | z aː ħ | ʔ aː l a t | dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 429.0, - "end": 432.6 - }, - "arabic": "«هاد أكيد الهوا!» قال ديك أبو سحّارة.", - "hebrew_transliteration": "הַאד אַכִּיד לְ-הַוַא! קַאל דִיכּ אַבּוּ סַחַّארַה.", - "hebrew": "זו בטח הרוח! אמר התרנגול אבו סחארה.", - "expected_phonemes": "h aː d | ʔ a k iː d | l h a w a | ʔ aː l | d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 432.6, - "end": 436.2 - }, - "arabic": "الكلّ رجع لشغله، بسّ بعد لحظة", - "hebrew_transliteration": "אִל-כֻּלّ רִגֶ'ע לַשֻעְ'לוֹ, בַּס בַּעֶד לַחזַה", - "hebrew": "כולם חזרו לעיסוקיהם, אבל אחרי רגע", - "expected_phonemes": "i l k u l l | r i dʒ e ʕ | l a ʃ u ɣ l o | b a s | b a ʕ e d | l a ħ ðˤ a" - }, - { - "voice_time_seconds": { - "start": 436.2, - "end": 440.8 - }, - "arabic": "رجعوا سمعوا صوت غريب: مه، مهه...", - "hebrew_transliteration": "רִגְעוּ סִמעוּ צוֹת עַ'רִיבּ: מֶה, מֶהֶה...", - "hebrew": "שוב שמעו קול מוזר: מה, מהה...", - "expected_phonemes": "r i g ʕ u | s i m ʕ u | sˤ oː t | ɣ a r iː b | m e | m e h e" - }, - { - "voice_time_seconds": { - "start": 440.8, - "end": 443.4 - }, - "arabic": "«مين هاد؟» قالت خنفسة.", - "hebrew_transliteration": "מִין הַאד? קַאלַת חֻ'נפֻסֶה.", - "hebrew": "מי זה? אמרה החיפושית.", - "expected_phonemes": "m iː n | h aː d | ʔ aː l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 443.4, - "end": 445.6 - }, - "arabic": "«مين هناك؟» صرّخت فارة.", - "hebrew_transliteration": "מִין הֻנַאכּ? צַרַّחַת פַארַה.", - "hebrew": "מי שם? צעקה העכברה.", - "expected_phonemes": "m iː n | h u n aː k | sˤ a r r a ħ a t | f aː r a" - }, - { - "voice_time_seconds": { - "start": 445.6, - "end": 449.4 - }, - "arabic": "«حدا دقّ؟» سأل ديك أبو سحّارة.", - "hebrew_transliteration": "חַדַא דַקّ? סַאַל דִיכּ אַבּוּ סַחַّארַה.", - "hebrew": "מישהו דפק? שאל התרנגול אבו סחארה.", - "expected_phonemes": "ħ a d a | d a ʔ ʔ | s a ʔ a l | d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 449.4, - "end": 453.6 - }, - "arabic": "«بلكي نفتح ونشوف؟» إقترحت جاجة أم نقّارة.", - "hebrew_transliteration": "בַּלְכִּי נִפתַח וּנְשוּף? אִקתַרְחַת גַ'אגֶ'ת אֻםّ נַקַّארַה.", - "hebrew": "אולי נפתח ונראה? הציעה התרנגולת אם נקארה.", - "expected_phonemes": "b a l k i | n i f t a ħ | u n ʃ uː f | ʔ i ʔ t a r ħ a t | dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 453.6, - "end": 455.4 - }, - "arabic": "فتحوا الباب شويّ،", - "hebrew_transliteration": "פַתַחוּ לְ-בַּאבּ שְוַיّ,", - "hebrew": "פתחו את הדלת קצת,", - "expected_phonemes": "f a t a ħ u | l b aː b | ʃ w a j j" - }, - { - "voice_time_seconds": { - "start": 455.4, - "end": 459.4 - }, - "arabic": "وشافوا راس مليان شعر واقف برّا.", - "hebrew_transliteration": "וּשַאפוּ רַאס מַליַאן שַעַר וַאקֶף בַּרַّא.", - "hebrew": "וראו ראש מלא שיער עומד בחוץ.", - "expected_phonemes": "u ʃ aː f u | r aː s | m a l j aː n | ʃ a ʕ a r | w aː ʔ e f | b a r r a" - }, - { - "voice_time_seconds": { - "start": 459.4, - "end": 462.0 - }, - "arabic": "«مين إنت؟» سألت خنفسة.", - "hebrew_transliteration": "מִין אִנתֶ? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מי אתה? שאלה החיפושית.", - "expected_phonemes": "m iː n | ʔ i n t e | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 462.0, - "end": 464.6 - }, - "arabic": "«أنا خروف أبو صوف.»", - "hebrew_transliteration": "אַנַא חַ'רוּף אַבּוּ צוּף.", - "hebrew": "אני הכבש אבו צוף.", - "expected_phonemes": "ʔ a n a | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 464.6, - "end": 467.0 - }, - "arabic": "«شو بدّك، خروف أبو صوف؟»", - "hebrew_transliteration": "שוּ בַּדַّכּ, חַ'רוּף אַבּוּ צוּף?", - "hebrew": "מה אתה רוצה, כבש אבו צוף?", - "expected_phonemes": "ʃ u | b a d d a k | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 467.0, - "end": 470.0 - }, - "arabic": "«أنا بردان، عم برجّ من البرد.", - "hebrew_transliteration": "אַנַא בַּרדַאן, עַם-בַּרֻגّ מִן לְ-בַּרְד.", - "hebrew": "קר לי, אני רועד מקור.", - "expected_phonemes": "ʔ a n a | b a r d aː n | ʕ a m b a r u g g | m i n | l b a r d" - }, - { - "voice_time_seconds": { - "start": 470.0, - "end": 473.4 - }, - "arabic": "البرد عم بجلدني. خلّوني أفوت!»", - "hebrew_transliteration": "אִל-בַּרַד עַם-בִּיגַ'לְדְנִי. חַלּוּנִי אַפוּת!", - "hebrew": "הברד מצליף בי. תנו לי להיכנס!", - "expected_phonemes": "i l b a r a d | ʕ a m b iː dʒ a l d n i | ħ a l uː n i | ʔ a f uː t" - }, - { - "voice_time_seconds": { - "start": 473.4, - "end": 475.8 - }, - "arabic": "«بسّ يا صاحبي، خروف أبو صوف،»", - "hebrew_transliteration": "בַּס יַא צַאחְבִּי, חַ'רוּף אַבּוּ צוּף,", - "hebrew": "אבל, ידידי, כבש אבו צוף,", - "expected_phonemes": "b a s | j a | sˤ aː ħ b i | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 475.8, - "end": 479.0 - }, - "arabic": "قالت خنفسة، «هاد مستحيل.", - "hebrew_transliteration": "קַאלַת חֻ'נפֻסֶה, הַאד מֻסתַחִיל.", - "hebrew": "אמרה החיפושית, זה בלתי אפשרי.", - "expected_phonemes": "ʔ aː l a t | x u n f u s e | h aː d | m u s t a ħ iː l" - }, - { - "voice_time_seconds": { - "start": 479.0, - "end": 482.0 - }, - "arabic": "كيف حتّى بتفكّر بهيك فكرة:", - "hebrew_transliteration": "כִּיף חַתַّא בְּתְפַכַּר בְּהֵיכּ פִכּרַה:", - "hebrew": "איך אתה בכלל חושב על רעיון כזה:", - "expected_phonemes": "k iː f | ħ a t t a | b t f a k a r | b h eː k | f i k r a" - }, - { - "voice_time_seconds": { - "start": 482.0, - "end": 486.0 - }, - "arabic": "خروف بقلب عشّ خنفسة؟!»", - "hebrew_transliteration": "חַ'רוּף בְּקַלבּ עֻשّ חֻ'נפֻסֶה?!", - "hebrew": "כבש בתוך קן של חיפושית?!", - "expected_phonemes": "x a r uː f | b ʔ a l b | ʕ u ʃ ʃ | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 486.0, - "end": 489.6 - }, - "arabic": "«إذا الله أراد، كلّ شي ممكن يتحقّق.", - "hebrew_transliteration": "אִזַא אַללַّה אַרַאד, כֻּלّ שִי מֻמכֶּן יִתחַקַّק.", - "hebrew": "אם אלוהים ירצה, הכול יכול להתגשם.", - "expected_phonemes": "ʔ i z a | ʔ a l l a | ʔ a r aː d | k u l l | ʃ i | m u m k e n | j i t ħ a ʔ ʔ a ʔ" - }, - { - "voice_time_seconds": { - "start": 489.6, - "end": 492.6 - }, - "arabic": "إعملي معروف، خلّيني أفوت!»", - "hebrew_transliteration": "אִעמַלִי מַערוּף, חַלִّינִי אַפוּת!", - "hebrew": "עשי טובה, תני לי להיכנס!", - "expected_phonemes": "ʔ i ʕ m a l i | m a ʕ r uː f | ħ a l l iː n i | ʔ a f uː t" - }, - { - "voice_time_seconds": { - "start": 492.6, - "end": 496.0 - }, - "arabic": "«هاد مستحيل،» قالت خنفسة،", - "hebrew_transliteration": "הַאד מֻסתַחִיל, קַאלַת חֻ'נפֻסֶה,", - "hebrew": "זה בלתי אפשרי, אמרה החיפושית,", - "expected_phonemes": "h aː d | m u s t a ħ iː l | ʔ aː l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 496.0, - "end": 497.8 - }, - "arabic": "«إذا بدّك، حاول،", - "hebrew_transliteration": "אִזַא בַּדַّכּ, חַאוֶל,", - "hebrew": "אם אתה רוצה, נסה,", - "expected_phonemes": "ʔ i z a | b a d d a k | ħ aː w e l" - }, - { - "voice_time_seconds": { - "start": 497.8, - "end": 501.4 - }, - "arabic": "بسّ أنا متأكّدة إنّك مش رح تنجح.»", - "hebrew_transliteration": "בַּס אַנַא מִתאַכְּדֶה אִנַّכּ מִש רַח-תִנגַ'ח.", - "hebrew": "אבל אני בטוחה שלא תצליח.", - "expected_phonemes": "b a s | ʔ a n a | m i t ʔ a k d e | ʔ i n n a k | m i ʃ | r a ħ t i n dʒ a ħ" - }, - { - "voice_time_seconds": { - "start": 501.4, - "end": 506.4 - }, - "arabic": "«طيّب.» خروف أبو صوف فوّت طرف إجر،", - "hebrew_transliteration": "טַיֶّבּ. חַ'רוּף אַבּוּ צוּף פַוַّת טַרַף אִגֶ'ר,", - "hebrew": "טוב. הכבש אבו צוף הכניס קצה רגל,", - "expected_phonemes": "tˤ a j j e b | x a r uː f | ʔ a b u | sˤ uː f | f a w w a t | tˤ a r a f | ʔ i dʒ e r" - }, - { - "voice_time_seconds": { - "start": 506.4, - "end": 511.4 - }, - "arabic": "فوّت طرف دان، طوى ذنبه، إندحش وإنضغط", - "hebrew_transliteration": "פַוַّת טַרַף דַאן, טַוַא דַנַבּוֹ, אִנדַחַש וּאִנדַ'עַ'ט", - "hebrew": "הכניס קצה אוזן, קיפל את זנבו, נדחק ונלחץ", - "expected_phonemes": "f a w w a t | tˤ a r a f | d aː n | tˤ a w a | d a n a b o | ʔ i n d a ħ a ʃ | u ʔ i n dˤ a ɣ a tˤ" - }, - { - "voice_time_seconds": { - "start": 511.4, - "end": 516.2 - }, - "arabic": "– مش عم بنجح بولا أي شكل من الأشكال.", - "hebrew_transliteration": "– מִש עַם-בִּינגַ'ח בּוּלַא אַיّ שַכֶּל מִן לְ-אַשכַּאל.", - "hebrew": "– לא מצליח בשום צורה.", - "expected_phonemes": "m i ʃ | ʕ a m b iː n dʒ a ħ | b uː l a | ʔ a j j | ʃ a k e l | m i n | l ʔ a ʃ k aː l" - }, - { - "voice_time_seconds": { - "start": 516.2, - "end": 520.0 - }, - "arabic": "«إسمع منّي،» قالّه ديك أبو سحّارة،", - "hebrew_transliteration": "אִסמַע מִנִّי, קַאל-לוֹ דִיכּ אַבּוּ סַחַّארַה,", - "hebrew": "שמע ממני, אמר לו התרנגול אבו סחארה,", - "expected_phonemes": "ʔ i s m a ʕ | m i n n i | ʔ aː l l o | d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 520.0, - "end": 525.0 - }, - "arabic": "«بلكي ترجع أكمّ متر لورا عشان تجمّع قوّة؟»", - "hebrew_transliteration": "בַּלְכִּי תִרגַ'ע אַכַּם מִתֶר לַוַרַא עַשַאן תְגַ'מֶّע קֻוֶّה?", - "hebrew": "אולי תחזור כמה מטרים אחורה כדי לאסוף כוח?", - "expected_phonemes": "b a l k i | t i r dʒ a ʕ | ʔ a k a m | m i t e r | l a w a r a | ʕ a ʃ aː n | t dʒ a m m e ʕ | ʔ u w w e" - }, - { - "voice_time_seconds": { - "start": 525.0, - "end": 528.2 - }, - "arabic": "«هاي فكرة!» قال خروف أبو صوف.", - "hebrew_transliteration": "הַאי פִכּרַה! קַאל חַ'רוּף אַבּוּ צוּף.", - "hebrew": "זה רעיון! אמר הכבש אבו צוף.", - "expected_phonemes": "h aː i | f i k r a | ʔ aː l | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 528.2, - "end": 533.6 - }, - "arabic": "خطا أكمّ خطوة لورا، ركض، ركض، ركض.", - "hebrew_transliteration": "חַ'טַא אַכַּם חֻ'טוֶה לַוַרַא, רַכַּד', רַכַּד', רַכַּד'.", - "hebrew": "צעד כמה צעדים אחורה, רץ, רץ, רץ.", - "expected_phonemes": "x a tˤ a | ʔ a k a m | x u tˤ w e | l a w a r a | r a k a dˤ | r a k a dˤ | r a k a dˤ" - }, - { - "voice_time_seconds": { - "start": 533.6, - "end": 536.6 - }, - "arabic": "بوم! طراخ! كلّ العشّ رجّ،", - "hebrew_transliteration": "בּוּם! טְרַאחַ'! כֻּלّ לְ-עֻשّ רַגّ,", - "hebrew": "בום! טראח'! כל הקן רעד,", - "expected_phonemes": "b uː m | tˤ r aː x a | k u l l | l ʕ u ʃ ʃ | r a g g" - }, - { - "voice_time_seconds": { - "start": 536.6, - "end": 540.4 - }, - "arabic": "وهوپا! – بقلب العشّ قاعدين –", - "hebrew_transliteration": "וּהוֹפַּה! – בְּקַלבּ לְ-עֻשّ קַאעְדִין –", - "hebrew": "והופה! – בתוך הקן יושבים –", - "expected_phonemes": "u h oː f a | b ʔ a l b | l ʕ u ʃ ʃ | ʔ aː ʕ d iː n" - }, - { - "voice_time_seconds": { - "start": 540.4, - "end": 542.8 - }, - "arabic": "الكلّ يغنّي معي:", - "hebrew_transliteration": "אִל-כֻּלّ יְעַ'נִّי מַעִי:", - "hebrew": "כולם שרים איתי:", - "expected_phonemes": "i l k u l l | j ɣ a n n i | m a ʕ i" - }, - { - "voice_time_seconds": { - "start": 542.8, - "end": 546.2 - }, - "arabic": "خنفسة وفارة وجاجة أم نقّارة،", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה וּגַ'אגֶ'ת אֻםّ נַקַّארַה,", - "hebrew": "חיפושית ועכברה ותרנגולת אם נקארה,", - "expected_phonemes": "x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 546.2, - "end": 549.8 - }, - "arabic": "وديك أبو سحّارة، خروف أبو صوف!", - "hebrew_transliteration": "וּדִיכּ אַבּוּ סַחַّארַה, חַ'רוּף אַבּוּ צוּף!", - "hebrew": "ותרנגול אבו סחארה, כבש אבו צוף!", - "expected_phonemes": "u d iː k | ʔ a b u | s a ħ ħ aː r a | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 549.8, - "end": 552.0 - }, - "arabic": "كمان مرّة، أعلى!", - "hebrew_transliteration": "כַּמַאן מַרַّה, אַעלַא!", - "hebrew": "עוד פעם, חזק יותר!", - "expected_phonemes": "k a m aː n | m a r r a | ʔ a ʕ l a" - }, - { - "voice_time_seconds": { - "start": 552.0, - "end": 555.4 - }, - "arabic": "خنفسة وفارة وجاجة أم نقّارة", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה וּגַ'אגֶ'ת אֻםّ נַקַّארַה", - "hebrew": "חיפושית ועכברה ותרנגולת אם נקארה", - "expected_phonemes": "x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 555.4, - "end": 559.4 - }, - "arabic": "وديك أبو سحّارة، خروف أبو صوف!", - "hebrew_transliteration": "וּדִיכּ אַבּוּ סַחַّארַה, חַ'רוּף אַבּוּ צוּף!", - "hebrew": "ותרנגול אבו סחארה, כבש אבו צוף!", - "expected_phonemes": "u d iː k | ʔ a b u | s a ħ ħ aː r a | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 559.4, - "end": 561.4 - }, - "arabic": "قاعدين خنفسة وفارة", - "hebrew_transliteration": "קַאעְדִין חֻ'נפֻסֶה וּפַארַה", - "hebrew": "יושבים החיפושית והעכברה", - "expected_phonemes": "ʔ aː ʕ d iː n | x u n f u s e | u f aː r a" - }, - { - "voice_time_seconds": { - "start": 561.4, - "end": 565.2 - }, - "arabic": "وجاجة أم نقّارة وديك أبو سحّارة", - "hebrew_transliteration": "וּגַ'אגֶ'ת אֻםّ נַקַّארַה וּדִיכּ אַבּוּ סַחַّארַה", - "hebrew": "והתרנגולת אם נקארה והתרנגול אבו סחארה", - "expected_phonemes": "u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a | u d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 565.2, - "end": 567.2 - }, - "arabic": "مع خروف أبو صوف،", - "hebrew_transliteration": "מַע חַ'רוּף אַבּוּ צוּף,", - "hebrew": "עם הכבש אבו צוף,", - "expected_phonemes": "m a ʕ | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 567.2, - "end": 570.2 - }, - "arabic": "قاعدين بحكوا وبشربوا شاي،", - "hebrew_transliteration": "קַאעְדִין בִּחכּוּ וּבִּשרַבּוּ שַאי,", - "hebrew": "יושבים, מדברים ושותים תה,", - "expected_phonemes": "ʔ aː ʕ d iː n | b i ħ k u | u b i ʃ r a b u | ʃ aː j" - }, - { - "voice_time_seconds": { - "start": 570.2, - "end": 572.6 - }, - "arabic": "والبرد عم بنزل برّا:", - "hebrew_transliteration": "וִל-בַּרַד עַם-בִּינזַל בַּרַّא:", - "hebrew": "והברד יורד בחוץ:", - "expected_phonemes": "w i l b a r a d | ʕ a m b iː n z a l | b a r r a" - }, - { - "voice_time_seconds": { - "start": 572.6, - "end": 576.8 - }, - "arabic": "بوم طراخ، طيخ طاخ!", - "hebrew_transliteration": "בּוּם טְרַאחַ', טִיחַ' טַאחַ'!", - "hebrew": "בום טראח', טיח' טאח'!", - "expected_phonemes": "b uː m | tˤ r aː x a | tˤ iː x a | tˤ aː x a" - }, - { - "voice_time_seconds": { - "start": 576.8, - "end": 581.8 - }, - "arabic": "بنزل من دون توقّف. وفجأة – هدوء!", - "hebrew_transliteration": "בִּינזַל מִן דוּן תוַקֻّף. וּפַגְ'אַה – הֻדוּא!", - "hebrew": "יורד בלי הפסקה. ופתאום – שקט!", - "expected_phonemes": "b iː n z a l | m i n | d uː n | t w a ʔ ʔ u f | u f a dʒ ʔ a | h u d uː a" - }, - { - "voice_time_seconds": { - "start": 581.8, - "end": 585.4 - }, - "arabic": "البرد وقّف، الهوا بطّل يهبّ", - "hebrew_transliteration": "אִל-בַּרַד וַקַّף, אִל-הַוַא בַּטַّל יְהֶבّ", - "hebrew": "הברד פסק, הרוח חדלה לנשוב", - "expected_phonemes": "i l b a r a d | w a ʔ ʔ a f | i l h a w a | b a tˤ tˤ a l | j h e b b" - }, - { - "voice_time_seconds": { - "start": 585.4, - "end": 589.6 - }, - "arabic": "وبقلب الهدوء سمعوا فجأة صوت غريب:", - "hebrew_transliteration": "וּבְּקַלבּ לְ-הֻדוּא סִמעוּ פַגְ'אַה צוֹת עַ'רִיבּ:", - "hebrew": "ובתוך השקט שמעו פתאום קול מוזר:", - "expected_phonemes": "u b ʔ a l b | l h u d uː a | s i m ʕ u | f a dʒ ʔ a | sˤ oː t | ɣ a r iː b" - }, - { - "voice_time_seconds": { - "start": 594.2, - "end": 596.8 - }, - "arabic": "«شو هاد؟» سألت خنفسة.", - "hebrew_transliteration": "שוּ הַאד? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מה זה? שאלה החיפושית.", - "expected_phonemes": "ʃ u | h aː d | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 596.8, - "end": 599.4 - }, - "arabic": "«شو صار؟» قال خروف أبو صوف.", - "hebrew_transliteration": "שוּ צַאר? קַאל חַ'רוּף אַבּוּ צוּף.", - "hebrew": "מה קרה? אמר הכבש אבו צוף.", - "expected_phonemes": "ʃ u | sˤ aː r | ʔ aː l | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 599.4, - "end": 603.4 - }, - "arabic": "«أمّا صوت غريب!» قالت جاجة أم نقّارة.", - "hebrew_transliteration": "אַמַّא צוֹת עַ'רִיבּ! קַאלַת גַ'אגֶ'ת אֻםّ נַקַّארַה.", - "hebrew": "איזה קול מוזר! אמרה התרנגולת אם נקארה.", - "expected_phonemes": "ʔ a m m a | sˤ oː t | ɣ a r iː b | ʔ aː l a t | dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 603.4, - "end": 608.2 - }, - "arabic": "راحوا لعند الشّبّاك، زاحوا البرداي، وشو شافوا؟", - "hebrew_transliteration": "רַאחוּ לַעִנד שֻּׁבַّאכּ, זַאחוּ לְ-בֻּרדַאי, וּשוּ שַאפוּ?", - "hebrew": "הלכו אל החלון, הזיזו את הווילון, ומה ראו?", - "expected_phonemes": "r aː ħ u | l a ʕ i n d | ʃ u b b aː k | z aː ħ u | l b u r d aː i | u ʃ u | ʃ aː f u" - }, - { - "voice_time_seconds": { - "start": 608.2, - "end": 611.8 - }, - "arabic": "كلّ إشي أبيض! عم تتلج:", - "hebrew_transliteration": "כֻּלّ אִשִי אַבְּיַד'! עַם-תִתלֶג':", - "hebrew": "הכול לבן! יורד שלג:", - "expected_phonemes": "k u l l | ʔ i ʃ i | ʔ a b j a dˤ | ʕ a m t i t l e dʒ" - }, - { - "voice_time_seconds": { - "start": 617.8, - "end": 620.8 - }, - "arabic": "كمان تلج وكمان تلج.", - "hebrew_transliteration": "כַּמַאן תַלֶג' וּכַּמַאן תַלֶג'.", - "hebrew": "עוד שלג ועוד שלג.", - "expected_phonemes": "k a m aː n | t a l e dʒ | u k a m aː n | t a l e dʒ" - }, - { - "voice_time_seconds": { - "start": 620.8, - "end": 623.6 - }, - "arabic": "كلّ إشي تغطّا بغطا أبيض –", - "hebrew_transliteration": "כֻּלّ אִשִי תְעַ'טַّא בְּעַ'טַא אַבְּיַד' –", - "hebrew": "הכול התכסה בכיסוי לבן –", - "expected_phonemes": "k u l l | ʔ i ʃ i | t ɣ a tˤ tˤ a | b ɣ a tˤ a | ʔ a b j a dˤ" - }, - { - "voice_time_seconds": { - "start": 623.6, - "end": 628.2 - }, - "arabic": "الشّجر، لحجار، لصخور.", - "hebrew_transliteration": "אִ-שַּׁגַ'ר, לֶ-חְגַ'אר, לֶ-צְחוּר.", - "hebrew": "העצים, האבנים, הסלעים.", - "expected_phonemes": "ʔ i ʃ a dʒ a r | l e ħ dʒ aː r | l e sˤ ħ uː r" - }, - { - "voice_time_seconds": { - "start": 628.2, - "end": 631.6 - }, - "arabic": "وفجأة مع همس التّلج", - "hebrew_transliteration": "וּפַגְ'אַה מַע הַמֶס אִ-תַּלֶג'", - "hebrew": "ופתאום עם לחישת השלג", - "expected_phonemes": "u f a dʒ ʔ a | m a ʕ | h a m e s | ʔ i t a l e dʒ" - }, - { - "voice_time_seconds": { - "start": 631.6, - "end": 634.0 - }, - "arabic": "سمعوا حدا عم بمشي برّا:", - "hebrew_transliteration": "סִמעוּ חַדַא עַם-בִּמשִי בַּרַّא:", - "hebrew": "שמעו מישהו הולך בחוץ:", - "expected_phonemes": "s i m ʕ u | ħ a d a | ʕ a m b i m ʃ i | b a r r a" - }, - { - "voice_time_seconds": { - "start": 636.6, - "end": 638.6 - }, - "arabic": "بدعس بقلب التّلج", - "hebrew_transliteration": "בִּדעַס בְּקַלבּ אִ-תַּלֶג'", - "hebrew": "דורך בתוך השלג", - "expected_phonemes": "b i d ʕ a s | b ʔ a l b | ʔ i t a l e dʒ" - }, - { - "voice_time_seconds": { - "start": 638.6, - "end": 641.6 - }, - "arabic": "ودغري سمعوا صوت دقّ على الباب.", - "hebrew_transliteration": "וּדֻעְ'רִי סִמעוּ צוֹת דַקّ עַלַא לְ-בַּאבּ.", - "hebrew": "ומיד שמעו קול דפיקה בדלת.", - "expected_phonemes": "u d u ɣ r i | s i m ʕ u | sˤ oː t | d a ʔ ʔ | ʕ a l a | l b aː b" - }, - { - "voice_time_seconds": { - "start": 641.6, - "end": 644.8 - }, - "arabic": "«مين هناك؟» سألت خنفسة.", - "hebrew_transliteration": "מִין הֻנַאכּ? סַאַלַת חֻ'נפֻסֶה.", - "hebrew": "מי שם? שאלה החיפושית.", - "expected_phonemes": "m iː n | h u n aː k | s a ʔ a l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 644.8, - "end": 647.2 - }, - "arabic": "«مين هاد؟» نادت فارة.", - "hebrew_transliteration": "מִין הַאד? נַאדַת פַארַה.", - "hebrew": "מי זה? קראה העכברה.", - "expected_phonemes": "m iː n | h aː d | n aː d a t | f aː r a" - }, - { - "voice_time_seconds": { - "start": 647.2, - "end": 650.2 - }, - "arabic": "«هاد أنا، جمل أبو طويل!»", - "hebrew_transliteration": "הַאד אַנַא, גַ'מַל אַבּוּ טַוִיל!", - "hebrew": "זה אני, הגמל אבו טוויל!", - "expected_phonemes": "h aː d | ʔ a n a | dʒ a m a l | ʔ a b u | tˤ a w iː l" - }, - { - "voice_time_seconds": { - "start": 650.2, - "end": 652.6 - }, - "arabic": "«شو بدّك، يا جمل أبو طويل؟»", - "hebrew_transliteration": "שוּ בַּדַّכּ, יַא גַ'מַל אַבּוּ טַוִיל?", - "hebrew": "מה אתה רוצה, גמל אבו טוויל?", - "expected_phonemes": "ʃ u | b a d d a k | j a | dʒ a m a l | ʔ a b u | tˤ a w iː l" - }, - { - "voice_time_seconds": { - "start": 652.6, - "end": 656.4 - }, - "arabic": "«أنا بردان، أنا مش متعوّد على هيك طقس.", - "hebrew_transliteration": "אַנַא בַּרדַאן, אַנַא מִש מִתעַוֶّד עַלַא הֵיכּ טַקֶס.", - "hebrew": "קר לי, אני לא רגיל למזג אוויר כזה.", - "expected_phonemes": "ʔ a n a | b a r d aː n | ʔ a n a | m i ʃ | m i t ʕ a w w e d | ʕ a l a | h eː k | tˤ a ʔ e s" - }, - { - "voice_time_seconds": { - "start": 656.4, - "end": 660.0 - }, - "arabic": "الدّنيا تلج. وأنا مفرّز من البرد.", - "hebrew_transliteration": "אִ-דֻּנְיַא תַלֶג'. וּאַנַא מְפַרֶّז מִן לְ-בַּרְד.", - "hebrew": "יורד שלג. ואני קופא מקור.", - "expected_phonemes": "ʔ i d u n j a | t a l e dʒ | u ʔ a n a | m f a r r e z | m i n | l b a r d" - }, - { - "voice_time_seconds": { - "start": 660.0, - "end": 661.4 - }, - "arabic": "خلّوني أفوت!»", - "hebrew_transliteration": "חַלּוּנִי אַפוּת!", - "hebrew": "תנו לי להיכנס!", - "expected_phonemes": "ħ a l uː n i | ʔ a f uː t" - }, - { - "voice_time_seconds": { - "start": 661.4, - "end": 664.0 - }, - "arabic": "«إنجنّيت؟» قالت خنفسة،", - "hebrew_transliteration": "אִנגַ'נֵّית? קַאלַת חֻ'נפֻסֶה,", - "hebrew": "השתגעת? אמרה החיפושית,", - "expected_phonemes": "ʔ i n dʒ a n n eː t | ʔ aː l a t | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 664.0, - "end": 667.4 - }, - "arabic": "«إنت بتصدّق يعني إنّه جمل كبير متلك", - "hebrew_transliteration": "אִנתֶ בִּתצַדֶّק יַעְנִי אִנּוֹ גַ'מַל כְּבִּיר מִתלַכּ", - "hebrew": "אתה באמת מאמין שגמל גדול כמוך", - "expected_phonemes": "ʔ i n t e | b i t sˤ a d d e ʔ | j a ʕ n i | ʔ i n o | dʒ a m a l | k b iː r | m i t l a k" - }, - { - "voice_time_seconds": { - "start": 667.4, - "end": 670.0 - }, - "arabic": "يقدر يفوت على عشّ خنفسة؟!»", - "hebrew_transliteration": "יִקדַר יְפוּת עַלַא עֻשّ חֻ'נפֻסֶה?!", - "hebrew": "יוכל להיכנס לקן של חיפושית?!", - "expected_phonemes": "j i ʔ d a r | j f uː t | ʕ a l a | ʕ u ʃ ʃ | x u n f u s e" - }, - { - "voice_time_seconds": { - "start": 670.0, - "end": 672.8 - }, - "arabic": "«فشّ عندي حلّ،» قال جمل أبو طويل،", - "hebrew_transliteration": "פִש עִנדִי חַלّ, קַאל גַ'מַל אַבּוּ טַוִיל,", - "hebrew": "אין לי ברירה, אמר הגמל אבו טוויל,", - "expected_phonemes": "f i ʃ | ʕ i n d i | ħ a l l | ʔ aː l | dʒ a m a l | ʔ a b u | tˤ a w iː l" - }, - { - "voice_time_seconds": { - "start": 672.8, - "end": 674.8 - }, - "arabic": "«إذا مش رح تخلّيني أفوت،", - "hebrew_transliteration": "אִזַא מִש רַח-תְחַ'לִّינִי אַפוּת,", - "hebrew": "אם לא תתני לי להיכנס,", - "expected_phonemes": "ʔ i z a | m i ʃ | r a ħ t x a l l iː n i | ʔ a f uː t" - }, - { - "voice_time_seconds": { - "start": 674.8, - "end": 676.4 - }, - "arabic": "رح أجمّد من البرد.»", - "hebrew_transliteration": "רַח-אַגַ'מֶّד מִן לְ-בַּרְד.", - "hebrew": "אקפא מקור.", - "expected_phonemes": "r a ħ ʔ a dʒ a m m e d | m i n | l b a r d" - }, - { - "voice_time_seconds": { - "start": 676.4, - "end": 678.2 - }, - "arabic": "«طيّب، حاول.", - "hebrew_transliteration": "טַיֶّבּ, חַאוֶל.", - "hebrew": "טוב, נסה.", - "expected_phonemes": "tˤ a j j e b | ħ aː w e l" - }, - { - "voice_time_seconds": { - "start": 678.2, - "end": 681.4 - }, - "arabic": "بسّ إعرف إنّه فشّ عندك فرصة!»", - "hebrew_transliteration": "בַּס אִערֶף אִנּוֹ פִש עִנדַכּ פֻרצַה!", - "hebrew": "אבל דע שאין לך סיכוי!", - "expected_phonemes": "b a s | ʔ i ʕ r e f | ʔ i n o | f i ʃ | ʕ i n d a k | f u r sˤ a" - }, - { - "voice_time_seconds": { - "start": 681.4, - "end": 683.8 - }, - "arabic": "جمل أبو طويل بحاول:", - "hebrew_transliteration": "גַ'מַל אַבּוּ טַוִיל בִּחַאוֶל:", - "hebrew": "הגמל אבו טוויל מנסה:", - "expected_phonemes": "dʒ a m a l | ʔ a b u | tˤ a w iː l | b i ħ aː w e l" - }, - { - "voice_time_seconds": { - "start": 683.8, - "end": 686.8 - }, - "arabic": "فوّت إجر، فوّت الإجر التّانية.", - "hebrew_transliteration": "פַוַّת אִגֶ'ר, פַוַّת לְ-אִגֶ'ר תַּאנְיֶה.", - "hebrew": "הכניס רגל, הכניס את הרגל השנייה.", - "expected_phonemes": "f a w w a t | ʔ i dʒ e r | f a w w a t | l ʔ i dʒ e r | t aː n j e" - }, - { - "voice_time_seconds": { - "start": 686.8, - "end": 689.4 - }, - "arabic": "ولا إشي، مش عم تزبط.", - "hebrew_transliteration": "וַלַא אִשִי, מִש עַם-תִזבַּט.", - "hebrew": "כלום, זה לא מסתדר.", - "expected_phonemes": "w a l a | ʔ i ʃ i | m i ʃ | ʕ a m t i z b a tˤ" - }, - { - "voice_time_seconds": { - "start": 689.4, - "end": 691.6 - }, - "arabic": "«هيك مش راح تنجح بحياتك،»", - "hebrew_transliteration": "הֵיכּ מִש רַאח תִנגַ'ח בְּחַיַאתַכּ,", - "hebrew": "כך לא תצליח לעולם,", - "expected_phonemes": "h eː k | m i ʃ | r aː ħ | t i n dʒ a ħ | b ħ a j aː t a k" - }, - { - "voice_time_seconds": { - "start": 691.6, - "end": 693.8 - }, - "arabic": "قال خروف أبو صوف،", - "hebrew_transliteration": "קַאל חַ'רוּף אַבּוּ צוּף,", - "hebrew": "אמר הכבש אבו צוף,", - "expected_phonemes": "ʔ aː l | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 693.8, - "end": 698.4 - }, - "arabic": "«بلكي ترجع أكمّ متر لورا عشان تجمّع قوّة؟»", - "hebrew_transliteration": "בַּלְכִּי תִרגַ'ע אַכַּם מִתֶר לַוַרַא עַשַאן תְגַ'מֶّע קֻוֶّה?", - "hebrew": "אולי תחזור כמה מטרים אחורה כדי לאסוף כוח?", - "expected_phonemes": "b a l k i | t i r dʒ a ʕ | ʔ a k a m | m i t e r | l a w a r a | ʕ a ʃ aː n | t dʒ a m m e ʕ | ʔ u w w e" - }, - { - "voice_time_seconds": { - "start": 698.4, - "end": 703.2 - }, - "arabic": "«طيّب.» جمل أبو طويل رجع أكمّ خطوة لورا،", - "hebrew_transliteration": "טַיֶّבּ. גַ'מַל אַבּוּ טַוִיל רִגֶ'ע אַכַּם חֻ'טוֶה לַוַרַא,", - "hebrew": "טוב. הגמל אבו טוויל חזר כמה צעדים אחורה,", - "expected_phonemes": "tˤ a j j e b | dʒ a m a l | ʔ a b u | tˤ a w iː l | r i dʒ e ʕ | ʔ a k a m | x u tˤ w e | l a w a r a" - }, - { - "voice_time_seconds": { - "start": 703.2, - "end": 706.4 - }, - "arabic": "ركض، ركض، ركض، بقلب التّلج،", - "hebrew_transliteration": "רַכַּד', רַכַּד', רַכַּד', בְּקַלבּ אִ-תַּלֶג',", - "hebrew": "רץ, רץ, רץ, בתוך השלג,", - "expected_phonemes": "r a k a dˤ | r a k a dˤ | r a k a dˤ | b ʔ a l b | ʔ i t a l e dʒ" - }, - { - "voice_time_seconds": { - "start": 706.4, - "end": 709.8 - }, - "arabic": "زاد سرعته و... بوم!", - "hebrew_transliteration": "זַאד סֻרעתוֹ וּ... בּוּם!", - "hebrew": "הגביר את מהירותו ו... בום!", - "expected_phonemes": "z aː d | s u r ʕ t o | w | b uː m" - }, - { - "voice_time_seconds": { - "start": 709.8, - "end": 711.4 - }, - "arabic": "كلّ العشّ إهتزّ،", - "hebrew_transliteration": "כֻּלّ לְ-עֻשّ אִהתַזّ,", - "hebrew": "כל הקן רעד,", - "expected_phonemes": "k u l l | l ʕ u ʃ ʃ | ʔ i h t a z z" - }, - { - "voice_time_seconds": { - "start": 711.4, - "end": 715.8 - }, - "arabic": "بسّ جمل أبو طويل بقي برّا.", - "hebrew_transliteration": "בַּס גַ'מַל אַבּוּ טַוִיל בְּקִי בַּרַّא.", - "hebrew": "אבל הגמל אבו טוויל נשאר בחוץ.", - "expected_phonemes": "b a s | dʒ a m a l | ʔ a b u | tˤ a w iː l | b ʔ i | b a r r a" - }, - { - "voice_time_seconds": { - "start": 715.8, - "end": 717.8 - }, - "arabic": "ما قدرش يفوت.", - "hebrew_transliteration": "מַא קִדְרֶש יְפוּת.", - "hebrew": "לא הצליח להיכנס.", - "expected_phonemes": "m a | ʔ i d r e ʃ | j f uː t" - }, - { - "voice_time_seconds": { - "start": 717.8, - "end": 719.8 - }, - "arabic": "«ما أخدتش كفاي قوّة!»", - "hebrew_transliteration": "מַא אַחַ'דתֶש כְּפַאי קֻוֶّה!", - "hebrew": "לא לקחת מספיק תנופה!", - "expected_phonemes": "m a | ʔ a x a d t e ʃ | k f aː i | ʔ u w w e" - }, - { - "voice_time_seconds": { - "start": 719.8, - "end": 721.6 - }, - "arabic": "صيّح ديك أبو سحّارة،", - "hebrew_transliteration": "צַיַّח דִיכּ אַבּוּ סַחַّארַה,", - "hebrew": "צעק התרנגול אבו סחארה,", - "expected_phonemes": "sˤ a j j a ħ | d iː k | ʔ a b u | s a ħ ħ aː r a" - }, - { - "voice_time_seconds": { - "start": 721.6, - "end": 724.2 - }, - "arabic": "«حاول كمان مرّة!»", - "hebrew_transliteration": "חַאוֶל כַּמַאן מַרַّה!", - "hebrew": "נסה עוד פעם!", - "expected_phonemes": "ħ aː w e l | k a m aː n | m a r r a" - }, - { - "voice_time_seconds": { - "start": 724.2, - "end": 727.0 - }, - "arabic": "جمل أبو طويل ما يأسش.", - "hebrew_transliteration": "גַ'מַל אַבּוּ טַוִיל מַא יִאֶס.", - "hebrew": "הגמל אבו טוויל לא התייאש.", - "expected_phonemes": "dʒ a m a l | ʔ a b u | tˤ a w iː l | m a | j i ʔ e s" - }, - { - "voice_time_seconds": { - "start": 727.0, - "end": 732.2 - }, - "arabic": "بعّد عن العشّ تقريبا ميّة خطوة وبلّش يركض،", - "hebrew_transliteration": "בַּעַّד עַן לְ-עֻשّ תַקרִיבַּן מִית חֻ'טוֶה וּבַּלַّש יִרכֹּד',", - "hebrew": "התרחק מהקן כמאה צעדים והתחיל לרוץ,", - "expected_phonemes": "b a ʕ ʕ a d | ʕ a n | l ʕ u ʃ ʃ | t a ʔ r iː b a n | m iː t | x u tˤ w e | u b a l l a ʃ | j i r k o dˤ" - }, - { - "voice_time_seconds": { - "start": 732.2, - "end": 735.4 - }, - "arabic": "وهوّ بركض، وبركض، وبركض، وبركض،", - "hebrew_transliteration": "וּהֻוֶّ בִּרכֹּד', וּבִּרכֹּד', וּבִּרכֹּד', וּבִּרכֹּד',", - "hebrew": "והוא רץ, ורץ, ורץ, ורץ,", - "expected_phonemes": "u h u w w e | b i r k o dˤ | u b i r k o dˤ | u b i r k o dˤ | u b i r k o dˤ" - }, - { - "voice_time_seconds": { - "start": 735.4, - "end": 738.4 - }, - "arabic": "إكتسب سرعة وطراخ!", - "hebrew_transliteration": "אִכּתַסַבּ סֻרעַה וּטְרַאחַ'!", - "hebrew": "צבר מהירות וטראח'!", - "expected_phonemes": "ʔ i k t a s a b | s u r ʕ a | u tˤ r aː x a" - }, - { - "voice_time_seconds": { - "start": 738.4, - "end": 740.6 - }, - "arabic": "دغري لقلب العشّ.", - "hebrew_transliteration": "דֻעְ'רִי לַקַלבּ לְ-עֻשّ.", - "hebrew": "ישר לתוך הקן.", - "expected_phonemes": "d u ɣ r i | l a ʔ a l b | l ʕ u ʃ ʃ" - }, - { - "voice_time_seconds": { - "start": 740.6, - "end": 742.0 - }, - "arabic": "العشّ رجّ.", - "hebrew_transliteration": "אִל-עֻשّ רַגّ.", - "hebrew": "הקן רעד.", - "expected_phonemes": "i l ʕ u ʃ ʃ | r a g g" - }, - { - "voice_time_seconds": { - "start": 742.0, - "end": 744.6 - }, - "arabic": "لطراشة وقعت من الحيطان،", - "hebrew_transliteration": "לְ-טְרַאשֶה וִקעַת מִן לְ-חִיטַאן,", - "hebrew": "הטיח נפל מהקירות,", - "expected_phonemes": "l tˤ r aː ʃ e | w i ʔ ʕ a t | m i n | l ħ iː tˤ aː n" - }, - { - "voice_time_seconds": { - "start": 744.6, - "end": 747.6 - }, - "arabic": "الأدوات في الخزانة طلّعوا صوت،", - "hebrew_transliteration": "אִל-אַדַוַאת פִי לְ-חַ'זַאנֶה טַלַّעוּ צוֹת,", - "hebrew": "הכלים בארון השמיעו קול,", - "expected_phonemes": "i l ʔ a d a w aː t | f i | l x a z aː n e | tˤ a l l a ʕ u | sˤ oː t" - }, - { - "voice_time_seconds": { - "start": 747.6, - "end": 752.2 - }, - "arabic": "بسّ جمل أبو طويل جوّا!", - "hebrew_transliteration": "בַּס גַ'מַל אַבּוּ טַוִיל גֻ'וַّא!", - "hebrew": "אבל הגמל אבו טוויל בפנים!", - "expected_phonemes": "b a s | dʒ a m a l | ʔ a b u | tˤ a w iː l | dʒ u w w a" - }, - { - "voice_time_seconds": { - "start": 752.2, - "end": 756.8 - }, - "arabic": "وبقلب العشّ قاعدين كلّ أصحاب الشّلّة –", - "hebrew_transliteration": "וּבְּקַלבּ לְ-עֻשّ קַאעְדִין כֻּלّ אַצְחַאבּ שִּׁלֶّה –", - "hebrew": "ובתוך הקן יושבים כל חברי החבורה –", - "expected_phonemes": "u b ʔ a l b | l ʕ u ʃ ʃ | ʔ aː ʕ d iː n | k u l l | ʔ a sˤ ħ aː b | ʃ i l l e" - }, - { - "voice_time_seconds": { - "start": 756.8, - "end": 759.8 - }, - "arabic": "كلّنا سوا بصوت عالي:", - "hebrew_transliteration": "כֻּלְّנַא סַוַא בְּצוֹת עַאלִי:", - "hebrew": "כולנו יחד בקול רם:", - "expected_phonemes": "k u l l n a | s a w a | b sˤ oː t | ʕ aː l i" - }, - { - "voice_time_seconds": { - "start": 759.8, - "end": 763.2 - }, - "arabic": "خنفسة وفارة، وجاجة أم نقّارة،", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה, וּגַ'אגֶ'ת אֻםّ נַקַّארַה,", - "hebrew": "חיפושית ועכברה, ותרנגולת אם נקארה,", - "expected_phonemes": "x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 763.2, - "end": 767.0 - }, - "arabic": "وديك أبو سحّارة، خروف أبو صوف،", - "hebrew_transliteration": "וּדִיכּ אַבּוּ סַחַّארַה, חַ'רוּף אַבּוּ צוּף,", - "hebrew": "ותרנגול אבו סחארה, כבש אבו צוף,", - "expected_phonemes": "u d iː k | ʔ a b u | s a ħ ħ aː r a | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 767.0, - "end": 769.4 - }, - "arabic": "وجمل أبو طويل!", - "hebrew_transliteration": "וּגַ'מַל אַבּוּ טַוִיל!", - "hebrew": "וגמל אבו טוויל!", - "expected_phonemes": "u dʒ a m a l | ʔ a b u | tˤ a w iː l" - }, - { - "voice_time_seconds": { - "start": 769.4, - "end": 772.4 - }, - "arabic": "هاي قوّة إدخال الضّيوف!", - "hebrew_transliteration": "הַאי קֻוֶّת אִדחַ'אל אִ-דְּיוּף!", - "hebrew": "זה כוחה של הכנסת אורחים!", - "expected_phonemes": "h aː i | ʔ u w w e t | ʔ i d x aː l | ʔ i d j uː f" - }, - { - "voice_time_seconds": { - "start": 772.4, - "end": 776.4 - }, - "arabic": "وعشان ما ننسى – غنّوا معي كمان مرّة:", - "hebrew_transliteration": "וּעַשַאן מַא נִנסַא – עַ'נּוּ מַעִי כַּמַאן מַרַّה:", - "hebrew": "וכדי שלא נשכח – שירו איתי עוד פעם:", - "expected_phonemes": "u ʕ a ʃ aː n | m a | n i n s a | ɣ a n u | m a ʕ i | k a m aː n | m a r r a" - }, - { - "voice_time_seconds": { - "start": 776.4, - "end": 780.2 - }, - "arabic": "خنفسة وفارة، وجاجة أم نقّارة،", - "hebrew_transliteration": "חֻ'נפֻסֶה וּפַארַה, וּגַ'אגֶ'ת אֻםّ נַקַّארַה,", - "hebrew": "חיפושית ועכברה, ותרנגולת אם נקארה,", - "expected_phonemes": "x u n f u s e | u f aː r a | u dʒ aː dʒ e t | ʔ u m m | n a ʔ ʔ aː r a" - }, - { - "voice_time_seconds": { - "start": 780.2, - "end": 783.8 - }, - "arabic": "وديك أبو سحّارة، خروف أبو صوف،", - "hebrew_transliteration": "וּדִיכּ אַבּוּ סַחַّארַה, חַ'רוּף אַבּוּ צוּף,", - "hebrew": "ותרנגול אבו סחארה, כבש אבו צוף,", - "expected_phonemes": "u d iː k | ʔ a b u | s a ħ ħ aː r a | x a r uː f | ʔ a b u | sˤ uː f" - }, - { - "voice_time_seconds": { - "start": 783.8, - "end": 785.8 - }, - "arabic": "وجمل أبو طويل!", - "hebrew_transliteration": "וּגַ'מַל אַבּוּ טַוִיל!", - "hebrew": "וגמל אבו טוויל!", - "expected_phonemes": "u dʒ a m a l | ʔ a b u | tˤ a w iː l" - } - ] - } -] diff --git a/docs/future-forms-draft.md b/docs/future-forms-draft.md deleted file mode 100644 index f3ab06fd1910e4754e600da80c5d866d5f851049..0000000000000000000000000000000000000000 --- a/docs/future-forms-draft.md +++ /dev/null @@ -1,293 +0,0 @@ -# טיוטת העתיד של פעלי שלב א' — לבדיקה - -הטבלה מציגה את צורות העתיד של כל פועל משלב א' שחסרה לו לפחות צורה אחת במאגר (LEX). ✓ = כבר במאגר ונבדקה. ✎ = טיוטה שנבנתה לפי הכלל, וצריך לבדוק אותה. אחרי הבדיקה, הצורות הנכונות עוברות ל־LEX (`app/workbook.py`) ונמחקות מ־`app/future_draft.py`. פועל שכל שמונה הצורות שלו נמצאות ב־LEX מגיע לתלמידים במשחק "בונים פועל עם איתן". עד אז רק מורים רואים אותו, עם התווית טיוטה. - -**כבר מוכנים לתלמידים (11):** שאל (يسأل), ישב (يقعد), בא (يجي), דיבר (يحكي), ביקר (يزور), ראה (يشوف), שתה (يشرب), יצא ועלה (يطلع), שמע (يسمع), אכל צהריים (يتغدّى), שיחק (يلعب) - -מה לבדוק בכל ✎: הכתיב בערבית, התעתיק (בעיקר התנועה של התחילית: יִ / יֻ / יְ) וההגייה. השמטת התנועה ב־ـي/ـו (למשל תֹאכְּלִי) נבנתה רק במקומות שבהם היא כבר מופיעה במאגר. - -## פתח · فتح · يفتح (שיעור 1) — 6 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | افتح | אַפְתַח | `ʔ a f t a ħ` | -| ✎ | אתה | تفتح | תִפְתַח | `t i f t a ħ` | -| ✎ | את | تفتحي | תִפְתַחִי | `t i f t a ħ i` | -| ✓ | הוא | يفتح | יִפְתַח | `j i f t a ħ` | -| ✎ | היא | تفتح | תִפְתַח | `t i f t a ħ` | -| ✎ | אנחנו | نفتح | נִפְתַח | `n i f t a ħ` | -| ✎ | אתם | تفتحو | תִפְתַחוּ | `t i f t a ħ u` | -| ✎ | הם | يفتحو | יִפְתַחוּ | `j i f t a ħ u` | - -## אכל · اكل · ياكل (שיעור 1) — 1 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | آكل | אַאכֹּל | `ʔ aː k u l / ʔ aː k i l` | -| ✓ | אתה | تاكل | תֹאכֹּל | `t oː k u l / t oː k i l / t aː k u l` | -| ✓ | את | تاكلي | תֹאכְּלִי | `t oː k l i / t aː k l i / t oː k i l i` | -| ✓ | הוא | ياكل | יֹאכֹּל | `j oː k u l / j oː k i l / j aː k u l` | -| ✓ | היא | تاكل | תֹאכֹּל | `t oː k u l / t oː k i l / t aː k u l` | -| ✓ | אנחנו | ناكل | נֹאכֹּל | `n oː k u l / n oː k i l / n aː k u l` | -| ✓ | אתם | تاكلو | תֹאכְּלוּ | `t oː k l u / t aː k l u / t oː k u l u` | -| ✎ | הם | ياكلو | יֹאכְּלוּ | `j oː k u l u / j oː k i l u / j aː k u l u / j oː k l u / j aː k l u` | - -## נכנס · دخل · يدخل (שיעור 3) — 3 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | ادخل | אַדְחֹ'ל | `ʔ a d x u l` | -| ✓ | אתה | تدخل | תֻדְחֹ'ל | `t u d x u l` | -| ✎ | את | تدخلي | תֻדְחֹ'לִי | `t u d x u l i` | -| ✓ | הוא | يدخل | יֻדְחֹ'ל | `j u d x u l` | -| ✓ | היא | تدخل | תֻדְחֹ'ל | `t u d x u l` | -| ✓ | אנחנו | ندخل | נֻדְחֹ'ל | `n u d x u l` | -| ✎ | אתם | تدخلو | תֻדְחֹ'לוּ | `t u d x u l u` | -| ✎ | הם | يدخلو | יֻדְחֹ'לוּ | `j u d x u l u` | - -## שחה · سبح · يسبح (שיעור 3) — 4 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✎ | אני | اسبح | אַסְבַּח | `ʔ a s b a ħ` | -| ✓ | אתה | تسبح | תִסְבַּח | `t i s b a ħ` | -| ✎ | את | تسبحي | תִסְבַּחִי | `t i s b a ħ i` | -| ✓ | הוא | يسبح | יִסְבַּח | `j i s b a ħ` | -| ✓ | היא | تسبح | תִסְבַּח | `t i s b a ħ` | -| ✓ | אנחנו | نسبح | נִסְבַּח | `n i s b a ħ` | -| ✎ | אתם | تسبحو | תִסְבַּחוּ | `t i s b a ħ u` | -| ✎ | הם | يسبحو | יִסְבַּחוּ | `j i s b a ħ u` | - -## בישל · طبخ · يطبخ (שיעור 3) — 2 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اطبخ | אַטְבֹּח' | `ʔ a tˤ b u x` | -| ✓ | אתה | تطبخ | תֻטְבֹּח' | `t u tˤ b u x` | -| ✓ | את | تطبخي | תֻטְבֹּחִ'י | `t u tˤ b u x i` | -| ✓ | הוא | يطبخ | יֻטְבֹּח' | `j u tˤ b u x` | -| ✓ | היא | تطبخ | תֻטְבֹּח' | `t u tˤ b u x` | -| ✓ | אנחנו | نطبخ | נֻטְבֹּח' | `n u tˤ b u x` | -| ✎ | אתם | تطبخو | תֻטְבֹּח'וּ | `t u tˤ b u x u` | -| ✎ | הם | يطبخو | יֻטְבֹּח'וּ | `j u tˤ b u x u` | - -## עבר דירה · نقل · ينقل (שיעור 3) — 4 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✎ | אני | انقل | אַנְקֹל | `ʔ a n ʔ u l / ʔ a n q u l` | -| ✓ | אתה | تنقل | תֻנְקֹל | `t u n ʔ u l / t u n q u l` | -| ✎ | את | تنقلي | תֻנְקֹלִי | `t u n ʔ u l i / t u n q u l i` | -| ✓ | הוא | ينقل | יֻנְקֹל | `j u n ʔ u l / j u n q u l` | -| ✓ | היא | تنقل | תֻנְקֹל | `t u n ʔ u l / t u n q u l` | -| ✎ | אנחנו | ننقل | נֻנְקֹל | `n u n ʔ u l / n u n q u l` | -| ✎ | אתם | تنقلو | תֻנְקֹלוּ | `t u n ʔ u l u / t u n q u l u` | -| ✓ | הם | ينقلو | יֻנְקֹלוּ | `j u n ʔ u l u / j u n q u l u` | - -## נתן · اعطى · يعطي (שיעור 4) — 3 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اعطي | אַעַטִי | `ʔ a ʕ tˤ i / ʔ a ʕ a tˤ i` | -| ✓ | אתה | تعطي | תֵעַטִי | `t a ʕ tˤ i / t i ʕ tˤ i` | -| ✓ | את | تعطي | תֵעַטִי | `t a ʕ tˤ i / t i ʕ tˤ i` | -| ✓ | הוא | يعطي | יֵעַטִי | `j a ʕ tˤ i / j i ʕ tˤ i` | -| ✓ | היא | تعطي | תֵעַטִי | `t a ʕ tˤ i / t i ʕ tˤ i` | -| ✎ | אנחנו | نعطي | נֵעַטִי | `n a ʕ tˤ i / n i ʕ tˤ i` | -| ✎ | אתם | تعطو | תֵעַטוּ | `t a ʕ tˤ u / t i ʕ tˤ u` | -| ✎ | הם | يعطو | יֵעַטוּ | `j a ʕ tˤ u / j i ʕ tˤ u` | - -## קנה · اشترى · يشتري (שיעור 4) — 2 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اشتري | אַשְתְרִי | `ʔ a ʃ t r i / ʔ a ʃ t i r i` | -| ✓ | אתה | تشتري | תִשְתְרִי | `t i ʃ t r i / t i ʃ t i r i` | -| ✓ | את | تشتري | תִשְתְרִי | `t i ʃ t r i / t i ʃ t i r i` | -| ✓ | הוא | يشتري | יִשְתְרִי | `j i ʃ t r i / j i ʃ t i r i` | -| ✓ | היא | تشتري | תִשְתְרִי | `t i ʃ t r i / t i ʃ t i r i` | -| ✓ | אנחנו | نشتري | נִשְתְרִי | `n i ʃ t r i / n i ʃ t i r i` | -| ✎ | אתם | تشترو | תִשְתְרוּ | `t i ʃ t r u / t i ʃ t i r u` | -| ✎ | הם | يشترو | יִשְתְרוּ | `j i ʃ t r u / j i ʃ t i r u` | - -## הלך · راح · يروح (שיעור 5) — 1 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اروح | אַרוּח | `ʔ a r uː ħ` | -| ✓ | אתה | تروح | תְרוּח | `t r uː ħ` | -| ✓ | את | تروحي | תְרוּחִי | `t r uː ħ i` | -| ✓ | הוא | يروح | יִרוּח | `j r uː ħ / i r uː ħ / j i r uː ħ` | -| ✓ | היא | تروح | תְרוּח | `t r uː ħ` | -| ✓ | אנחנו | نروح | נְרוּח | `n r uː ħ` | -| ✎ | אתם | تروحو | תְרוּחוּ | `t r uː ħ u / t i r uː ħ u` | -| ✓ | הם | يروحو | יִרוּחוּ | `j r uː ħ u / i r uː ħ u / j i r uː ħ u` | - -## אמר · قال · يقول (שיעור 5) — 4 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اقول | אַקוּל | `ʔ a ʔ uː l / ʔ a q uː l` | -| ✓ | אתה | تقول | תְקוּל | `t ʔ uː l / t q uː l` | -| ✎ | את | تقولي | תְקוּלִי | `t ʔ uː l i / t q uː l i` | -| ✓ | הוא | يقول | יִקוּל | `j ʔ uː l / i ʔ uː l / j q uː l / j i ʔ uː l` | -| ✓ | היא | تقول | תְקוּל | `t ʔ uː l / t q uː l` | -| ✎ | אנחנו | نقول | נְקוּל | `n ʔ uː l / n q uː l` | -| ✎ | אתם | تقولو | תְקוּלוּ | `t ʔ uː l u / t q uː l u` | -| ✎ | הם | يقولو | יִקוּלוּ | `j ʔ uː l u / i ʔ uː l u / j q uː l u / j i ʔ uː l u` | - -## היה · كان · يكون (שיעור 5) — 2 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اكون | אַכּוּן | `ʔ a k uː n` | -| ✓ | אתה | تكون | תְכּוּן | `t k uː n` | -| ✓ | את | تكوني | תְכּוּנִי | `t k uː n i` | -| ✓ | הוא | يكون | יִכּוּן | `j k uː n / i k uː n / j i k uː n` | -| ✓ | היא | تكون | תְכּוּן | `t k uː n` | -| ✓ | אנחנו | نكون | נְכּוּן | `n k uː n` | -| ✎ | אתם | تكونو | תְכּוּנוּ | `t k uː n u` | -| ✎ | הם | يكونو | יִכּוּנוּ | `j k uː n u / i k uː n u / j i k uː n u` | - -## נישק · باس · يبوس (שיעור 6) — 4 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | ابوس | אַבּוּס | `ʔ a b uː s` | -| ✓ | אתה | تبوس | תְבּוּס | `t b uː s` | -| ✎ | את | تبوسي | תְבּוּסִי | `t b uː s i` | -| ✓ | הוא | يبوس | יִבּוּס | `j b uː s / i b uː s / j i b uː s` | -| ✓ | היא | تبوس | תְבּוּס | `t b uː s` | -| ✎ | אנחנו | نبوس | נְבּוּס | `n b uː s` | -| ✎ | אתם | تبوسو | תְבּוּסוּ | `t b uː s u` | -| ✎ | הם | يبوسو | יִבּוּסוּ | `j b uː s u / i b uː s u / j i b uː s u` | - -## נהג · ساق · يسوق (שיעור 6) — 3 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اسوق | אַסוּק | `ʔ a s uː ʔ / ʔ a s uː q` | -| ✓ | אתה | تسوق | תְסוּק | `t s uː ʔ / t s uː q` | -| ✓ | את | تسوقي | תְסוּקִי | `t s uː ʔ i / t s uː q i` | -| ✓ | הוא | يسوق | יִסוּק | `j s uː ʔ / j s uː q / i s uː ʔ / j i s uː ʔ` | -| ✓ | היא | تسوق | תְסוּק | `t s uː ʔ / t s uː q` | -| ✎ | אנחנו | نسوق | נְסוּק | `n s uː ʔ / n s uː q` | -| ✎ | אתם | تسوقو | תְסוּקוּ | `t s uː ʔ u / t s uː q u` | -| ✎ | הם | يسوقو | יִסוּקוּ | `j s uː ʔ u / j s uː q u / i s uː ʔ u / j i s uː ʔ u` | - -## נכנס ועבר · فات · يفوت (שיעור 6) — 4 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | افوت | אַפוּת | `ʔ a f uː t` | -| ✓ | אתה | تفوت | תְפוּת | `t f uː t` | -| ✎ | את | تفوتي | תְפוּתִי | `t f uː t i` | -| ✓ | הוא | يفوت | יִפוּת | `j f uː t / i f uː t / j i f uː t` | -| ✓ | היא | تفوت | תְפוּת | `t f uː t` | -| ✎ | אנחנו | نفوت | נְפוּת | `n f uː t` | -| ✎ | אתם | تفوتو | תְפוּתוּ | `t f uː t u` | -| ✎ | הם | يفوتو | יִפוּתוּ | `j f uː t u / i f uː t u / j i f uː t u` | - -## לקח · اخد · ياخد (שיעור 6) — 2 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | آخد | אַאחֹ'ד | `ʔ aː x u d / ʔ aː x i d` | -| ✓ | אתה | تاخد | תֹאחֵ'ד | `t oː x i d / t aː x u d` | -| ✓ | את | تاخدي | תֹאחְ'דִי | `t oː x d i / t aː x d i / t oː x u d i` | -| ✓ | הוא | ياخد | יֹאחֵ'ד | `j oː x i d / j aː x u d / j oː x u d` | -| ✓ | היא | تاخد | תֹאחֵ'ד | `t oː x i d / t aː x u d` | -| ✎ | אנחנו | ناخد | נֹאחֵ'ד | `n oː x i d / n aː x u d` | -| ✎ | אתם | تاخدو | תֹאחְ'דוּ | `t oː x i d u / t aː x u d u / t oː x d u / t aː x d u / t oː x u d u` | -| ✓ | הם | ياخدو | יֹאחְ'דוּ | `j oː x d u / j aː x d u / j oː x u d u` | - -## הבין · فهم · يفهم (שיעור 8) — 2 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | افهم | אַפְהַם | `ʔ a f h a m` | -| ✓ | אתה | تفهم | תִפְהַם | `t i f h a m` | -| ✓ | את | تفهمي | תִפְהַמִי | `t i f h a m i` | -| ✓ | הוא | يفهم | יִפְהַם | `j i f h a m` | -| ✓ | היא | تفهم | תִפְהַם | `t i f h a m` | -| ✎ | אנחנו | نفهم | נִפְהַם | `n i f h a m` | -| ✎ | אתם | تفهمو | תִפְהַמוּ | `t i f h a m u` | -| ✓ | הם | يفهمو | יִפְהַמוּ | `j i f h a m u` | - -## חזר · رجع · يرجع (שיעור 8) — 1 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | ارجع | אַרְגַ'ע | `ʔ a r dʒ a ʕ` | -| ✓ | אתה | ترجع | תִרְגַ'ע | `t i r dʒ a ʕ` | -| ✓ | את | ترجعي | תִרְגַ'עִי | `t i r dʒ a ʕ i` | -| ✓ | הוא | يرجع | יִרְגַ'ע | `j i r dʒ a ʕ` | -| ✓ | היא | ترجع | תִרְגַ'ע | `t i r dʒ a ʕ` | -| ✎ | אנחנו | نرجع | נִרְגַ'ע | `n i r dʒ a ʕ` | -| ✓ | אתם | ترجعو | תִרְגַ'עוּ | `t i r dʒ a ʕ u` | -| ✓ | הם | يرجعو | יִרְגַ'עוּ | `j i r dʒ a ʕ u` | - -## הודה · شكر · يشكر (שיעור 9) — 6 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✎ | אני | اشكر | אַשְכֹּר | `ʔ a ʃ k u r` | -| ✎ | אתה | تشكر | תֻשְכֹּר | `t u ʃ k u r` | -| ✎ | את | تشكري | תֻשְכֹּרִי | `t u ʃ k u r i` | -| ✓ | הוא | يشكر | יֻשְכֹּר | `j u ʃ k u r` | -| ✎ | היא | تشكر | תֻשְכֹּר | `t u ʃ k u r` | -| ✓ | אנחנו | نشكر | נֻשְכֹּר | `n u ʃ k u r` | -| ✎ | אתם | تشكرو | תֻשְכֹּרוּ | `t u ʃ k u r u` | -| ✎ | הם | يشكرو | יֻשְכֹּרוּ | `j u ʃ k u r u` | - -## ירד · نزل · ينزل (שיעור 10) — 4 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✎ | אני | انزل | אַנְזֵל | `ʔ a n z i l / ʔ a n z a l` | -| ✓ | אתה | تنزل | תִנְזֵל | `t i n z i l / t i n z a l` | -| ✎ | את | تنزلي | תִנְזֵלִי | `t i n z i l i / t i n z a l i` | -| ✓ | הוא | ينزل | יִנְזֵל | `j i n z i l / j i n z a l` | -| ✓ | היא | تنزل | תִנְזֵל | `t i n z i l / t i n z a l` | -| ✎ | אנחנו | ننزل | נִנְזֵל | `n i n z i l / n i n z a l` | -| ✓ | אתם | تنزلو | תִנְזְלוּ | `t i n z l u / t i n z i l u / t i n z a l u` | -| ✎ | הם | ينزلو | יִנְזְלוּ | `j i n z i l u / j i n z a l u / j i n z l u` | - -## הגיע · وصل · يوصل (שיעור 10) — 1 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اوصل | אַוְצַל | `ʔ a w sˤ a l / ʔ oː sˤ a l` | -| ✓ | אתה | توصل | תוֹצַל | `t oː sˤ a l` | -| ✓ | את | توصلي | תוֹצַלִי | `t oː sˤ a l i` | -| ✓ | הוא | يوصل | יוֹצַל | `j oː sˤ a l` | -| ✓ | היא | توصل | תוֹצַל | `t oː sˤ a l` | -| ✓ | אנחנו | نوصل | נוֹצַל | `n oː sˤ a l` | -| ✎ | אתם | توصلو | תוֹצַלוּ | `t oː sˤ a l u` | -| ✓ | הם | يوصلو | יוֹצַלוּ | `j oː sˤ a l u` | - -## צחק וחייך · ضحك · يضحك (שיעור 10) — 1 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✓ | אני | اضحك | אַדְֿחַכּ | `ʔ a dˤ ħ a k` | -| ✓ | אתה | تضحك | תִדְֿחַכּ | `t i dˤ ħ a k` | -| ✓ | את | تضحكي | תִדְֿחַכִּי | `t i dˤ ħ a k i` | -| ✓ | הוא | يضحك | יִדְֿחַכּ | `j i dˤ ħ a k` | -| ✓ | היא | تضحك | תִדְֿחַכּ | `t i dˤ ħ a k` | -| ✓ | אנחנו | نضحك | נִדְֿחַכּ | `n i dˤ ħ a k` | -| ✓ | אתם | تضحكو | תִדְֿחַכּוּ | `t i dˤ ħ a k u` | -| ✎ | הם | يضحكو | יִדְֿחַכּוּ | `j i dˤ ħ a k u` | - -## לבש · لبس · يلبس (שיעור 10) — 4 לבדיקה - -| | גוף | ערבית | תעתיק | הגייה | -|---|---|---|---|---| -| ✎ | אני | البس | אַלְבַּס | `ʔ a l b a s` | -| ✓ | אתה | تلبس | תִלְבַּס | `t i l b a s` | -| ✓ | את | تلبسي | תִלְבַּסִי | `t i l b a s i` | -| ✓ | הוא | يلبس | יִלְבַּס | `j i l b a s` | -| ✓ | היא | تلبس | תִלְבַּס | `t i l b a s` | -| ✎ | אנחנו | نلبس | נִלְבַּס | `n i l b a s` | -| ✎ | אתם | تلبسو | תִלְבַּסוּ | `t i l b a s u` | -| ✎ | הם | يلبسو | יִלְבַּסוּ | `j i l b a s u` | diff --git a/docs/gop-pooling.md b/docs/gop-pooling.md deleted file mode 100644 index c236f05716acb93242b5ba78e1d0d1e83cc2bb2b..0000000000000000000000000000000000000000 --- a/docs/gop-pooling.md +++ /dev/null @@ -1,161 +0,0 @@ -# GOP frame pooling - -How `score_tokens` ([app/align.py](../app/align.py)) turns a token window's frames into one target score and one -competitor score, and why short vowels need something other than the max. Benchmark: -`python scripts/gop_pooling_bench.py`. Tests: [tests/test_gop_pooling.py](../tests/test_gop_pooling.py). - -## Notation - -- Window of token *i*: `W = [a, b)`, from the end of token *i−1* to the start of token *i+1*. `n = |W|`. -- `p_t(k)` is the posterior of column *k* at frame *t*, and `ℓ_t(k) = log p_t(k)`. *y* is the target. -- *C* is the set of competitors: every column except blank, the target, its two neighbours, and their accepted realisations. -- A pool `P(k)` maps the window to one posterior-like value per column. The score is: - -``` -GOP = log P(y) − max(log P(y), max_{c∈C} log P(c)) ≤ 0 -p̂ = max_t p_t(y) (always the max, whatever P is) -score = 100 · exp(λ · GOP) · sqrt(min(1, p̂ / κ)) λ = 0.5, κ = 0.30 -``` - -The change is the split between the two terms. **`ratio_pool` changes only `P`, the relative evidence. The -presence term `p̂` and the deletion test stay max-pooled.** The reason is below (result 1). - -## The pools - -| `ratio_pool` | `P(k)` | Limits | -|---|---|---| -| `max` (old behaviour) | `max_t p_t(k)` | — | -| `mean` | `(1/n) Σ_t p_t(k)` | — | -| `topk` | `(1/K) Σ_{t ∈ top-K of k} p_t(k)` (each column uses its own best K frames) | K=1 → max, K=n → mean | -| `lse` | `(1/n Σ_t p_t(k)^{1/τ})^τ`, which is `τ·(LSE_t(ℓ_t(k)/τ) − log n)` | τ→0 → max, τ=1 → mean | -| `energy` | `Σ_t w_t p_t(k)`, with `w_t ∝ ẽ_t^γ` | γ=0 → mean | -| `gated` | mean over `{t : ẽ_t ≥ θ}` | θ=0 → mean | - -`ẽ_t` is the frame's RMS, max-filtered over ±`energy_spread` frames and divided by the loudest frame in the -window. The token's own aligned span always gets `w = 1`: if the gate dropped the frame where the token was -emitted, the target would fall to ≈0 (3.7/100 in the benchmark without this guard). - -All pools work in the **probability** domain. The literal alternative, a weighted mean of frame log-ratios -`Σ w_t (ℓ_t(y) − ℓ_t(c))`, breaks on CTC output. On blank frames both posteriors are ~1e-4, so their log-ratio -is noise of order ±5. A single frame where the target is at its floor adds −9 to the sum. Mixing in the -probability domain caps what any one frame can contribute at `w_t`. - -## Results - -**1. A symmetric pool cannot tell two single-frame spikes apart.** If the target and the competitor each occupy -one frame, with heights `h_y` and `h_c`, every pool above gives `P(y)/P(c) = h_y/h_c`: the dilution factor -(`1/n`, `1/K`, `n^{−τ}`) is the same for both. A pool only helps when the two differ in **support** (how many -frames they cover) or in **location** (loud or quiet frames). A correct CTC vowel is itself a 1–2 frame spike, -so its *presence* has to be read with the max. That is why `p̂` is never pooled. - -**2. The mass ratio does not depend on window length; the presence term does.** For `mean`: -`P(y)/P(c) = Σ p_t(y) / Σ p_t(c)`, so the `1/n` cancels. Unweighted averaging hurts only when the averaged -value also feeds the confidence. A 0.92 one-frame emission in a 10-frame window gives `P = 0.092`, so -`sqrt(0.092/0.30) = 0.55`: score 55, which is the "naive" column in the benchmark. - -**3. Mass pooling accumulates haze.** A faint competitor at ε per frame adds up to `n·ε`, while the target stays one -peak. When a hesitation stretches the window, `mean` loses (a 0.05 schwa haze: 100 at 20 frames, 83 at 30, 63 -at 50). `gated` and `energy` drop the silent frames. `topk` caps the competitor at its K best frames. - -**4. Spike sensitivity.** For a correct /a/ with frames .2/.4/.35 and a one-frame /i/ spike of height *h* inside the window: - -| h | max | mean / energy / gated | top2 | top3 | lse τ=.5 | lse τ=.25 | -|---|---|---|---|---|---|---| -| .5 | 89 | 100 | 100 | 100 | 100 | 95 | -| .9 | 67 | 100 | 91 | 100 | 79 | 71 | -| .98 | 64 | 98 | 88 | 98 | 76 | 68 | - -LSE moves smoothly between max and mean. With r = 1/τ = 2 it still prefers one 0.9 frame to three moderate -ones, so it absorbs a tall spike only partly. - -**5. Substitutions still fail.** Every pool scores the sustained /i/-for-/a/ case at about 20, and the one-frame -substitution at about 6. The one-frame substitution is also a single spike, but there the target is *absent* -(`p̂` ≈ .03), so both the ratio and the presence term are low. A pool only rescues a target that is clearly present. - -**6. Separation.** Margin between the worst correct vowel and the best substitution in the synthetic set: max 46, -lse 59, top2 70, mean / energy / gated / top3 80. These are synthetic numbers. Use them to rank the pools, not to set thresholds. - -## Class-based windows - -`compute_token_gop` reads each token from a window chosen by its class (`token_window`): - -- **Consonants:** the aligned span ± `GopConfig.consonant_window` (default 4 frames, about 80 ms), clipped at the - neighbouring token boundaries. This prevents a long pause from contributing an unrelated consonant peak. - -- **Consonants: wide**, `[end(i−1), start(i+1))`. When the alignment forces a path through a consonant that was - substituted, the sound actually said peaks in the blanks around the forced frame - (`consonant: competitor in blanks`: wide 15.8, competitor q; read narrow, q is lost). -- **Vowels: `GOP_VOWEL_WINDOW`** (`GopConfig.vowel_window`). `wide` (the default), or the aligned span ± pad - (`narrow` = ±1), clamped to the wide window. Only the **competitor ratio** uses the narrow window. Presence - (`peak`) and `deleted` are always read from the wide window, so a vowel aligned to a single frame is still found - deleted, and a strong competitor anywhere in the wide window still blocks a deletion (it is a substitution, not a - deletion). -- **Glides**: for a vowel next to j or w, the vowels that glide is made of (j: i iː e eː; w: u uː o oː; - `GLIDE_KIN` in scorer.py) do not count as competitors **on the frames between the vowel's span and the glide**, - for the ratio and for the deletion test. Inside the vowel's own span they still count, so /ji/ said for /ja/ fails. - This is always on and does not depend on the window setting. - -CTC Viterbi gives every token at least one frame, so a span of 0 frames cannot happen; a deleted vowel gets one. - -### Why the vowel window defaults to wide - -From `python scripts/gop_window_bench.py` (spans from `ctc_viterbi`, no audio): - -| case | want | wide max | pad 1 max | wide mean | -|---|---|---|---|---| -| spike in transition | pass | 74.5 | 100 | 100 | -| displaced substitution (/a/ .35, /i/ .85 held 2 frames later) | fail | 64.2 | **100** | 46.1 | -| margin (worst pass − best fail) | | 10.3 | **0.0** | 53.9 | - -The narrow window drops the transition spike by not looking at those frames, so it also stops seeing a real -vowel held outside span ± pad. Each pad goes blind beyond its reach: pad 0 at 1 frame away, pad 1 at 2, pad 2 at 3. -A pooled ratio over the wide window drops the same spike by its support (one frame against several), and still -sees the held substitution. A pooled ratio over a narrow window inherits the blind spot (pad1 mean: 100). -So the default is wide plus a pooled ratio. Use `narrow` when the lab shows vowel false flags whose competitor -sits in the transitions and pooling does not clear them. - -The displaced case needs the target to be weakly present elsewhere. In a plain substitution the aligner usually -puts the target on the substituted frames, where every pad sees the competitor (`sustained substitution`: 21 -everywhere). - -## Failure modes - -- **Voiceless neighbours.** RMS measures loudness, not voicing. A vowel next to /s ʃ f/ shares its window - with sibilant frication, which can be as loud as the vowel. `energy` then puts weight on the fricative frames, - and a competitor spike there counts in full. A vowel before /t k q ʔ/ ends in a quiet devoiced tail; the gate - drops it, which is usually the intent. A voicing feature (low-band energy, or autocorrelation periodicity) would - be a better weight than RMS. -- **Pharyngeals and emphatics.** Next to ʕ ħ sˤ tˤ, /a/ backs to [ɑ], which is a *sustained* competitor across the - whole nucleus. That is coarticulation, but it looks like a real substitution to every pool. Pooling cannot fix it; - the near-vowel floor and the exemptions in `rules.py` handle it. -- **Fast speech.** Windows of 1–3 frames: `n == 1` returns the max; with small *n*, `topk` and `mean` agree, and - the energy nucleus may be a single frame that the ±1 spread blends with the consonants. All pools converge to - roughly the max, which is fine: there are no frames for a spike to hide in. -- **The spread cuts both ways.** `energy_spread` forgives a CTC emission one frame off the nucleus, but it also - lifts a competitor spike one frame off the nucleus over the gate. The span guard already protects the target, - so `energy_spread=0` rejects spikes better; keep 1 until real data shows CTC peaks landing outside the alignment. -- **Short-vowel deletion** (colloquial elision of unstressed /i/, /u/). `deleted` still reads the max peaks, so it - does not change. The ratio of a deleted vowel is target floor vs. whatever the consonants leak into the window. The gate keeps the - consonant frames because they are loud, so a deleted vowel should score about the same as before (not covered - by the synthetic set yet). -- **A real one-frame vowel that is in between** (target 0.4, competitor 0.9, both one frame) gives the same - ratio under every pool (result 1). It is irreducible: the model is unsure which vowel it heard. -- **Quiet consonants** (ʔ h ʕ). Energy pools would discard exactly the frames these live in, so - `pool_vowels_only` (default on) keeps the max for every consonant. -- **No audio.** `energy` and `gated` fall back to `mean` when `score_logp` gets no `energy` (for example a rescore - from cached posteriors), and inherit its haze problem. Use `topk` there, or pass the energy along. - -## Rollout - -The pool comes from the env var `GOP_RATIO_POOL` (empty: `gated`; see `.env.example`). `max` reproduces the old -scores exactly (`test_max_pool_is_the_previous_scorer`). The lab shows the pool beside the version, since the version -hashes only the code. -Switching changes the GOP distribution for vowels, which moves toward 0, so everything tuned on it has to be refreshed: - -1. the margins in `rules` (`Scorer._margin`) and the native-reference baselines (`_baseline`): re-record the references; -2. the calibration floors (`set_calibrations`): rerun `scripts/rescore.py` on the labelled set; -3. the teacher trace still reports `p_target` and `p_competitor` as maxima, so with a pooled ratio a competitor - can show a higher peak than the target while the GOP is 0. - -Suggested setting: `ratio_pool="gated"` (θ = 0.3, about −10 dB), `pool_vowels_only=True`, with `topk` K=3 as the -setting when there is no audio. diff --git a/docs/grading-pipeline.md b/docs/grading-pipeline.md deleted file mode 100644 index 3fa5e260b806761436a514de15eab0e3d5e030ca..0000000000000000000000000000000000000000 --- a/docs/grading-pipeline.md +++ /dev/null @@ -1,519 +0,0 @@ -# How grading works: from a recording to an exercise grade - -This document traces one recording through the scorer, from the audio bytes and the reference text to the numbers the student and the teacher see. It is written for a reader who knows CTC, forced alignment and GOP, and wants the exact formulas, thresholds and order of operations this codebase uses. - -The pipeline has no ASR in it. The model never transcribes freely and then diffs against the text. Instead, the reference text is turned into phoneme sequences, the audio is force-aligned to each sequence, and each phoneme is scored by how strongly the model's posteriors support it inside its own time window. - -| Stage | Where | -|---|---| -| Audio decode, silence trim | [app/scorer.py](../app/scorer.py) `decode_audio` | -| Phoneme posteriors (wav2vec2 CTC) | [app/scorer.py](../app/scorer.py) `HFEmitter` | -| Vocab pooling, many-ways letters | [app/scorer.py](../app/scorer.py) `Vocab`, [app/phones.py](../app/phones.py), [app/archi.py](../app/archi.py) | -| CTC Viterbi, GOP, greedy decode, edit distance | [app/align.py](../app/align.py) | -| Linguistic rules, exemptions, margins | [app/rules.py](../app/rules.py), [static/assets/pronunciation-rules.md](../static/assets/pronunciation-rules.md) | -| Native baseline from TTS | [app/reference.py](../app/reference.py) | -| Result assembly, word and sentence score | [app/scorer.py](../app/scorer.py) `Scorer.score_logp` | -| HTTP endpoint | [app/main.py](../app/main.py) `POST /api/score` | -| Exercise-level grades | [static/index.html](../static/index.html), [app/speech_evaluation.py](../app/speech_evaluation.py) | - ---- - -## 1. The whole pipeline at a glance - -```mermaid -flowchart TD - A["Browser recording
webm/opus, ogg, mp4, wav"] --> B["decode_audio
ffmpeg → 16 kHz mono f32
cap 32 s, energy trim"] - R["Phrase from the bank
Hebrew prompt + accepted variants
(Arabic, IPA)"] --> V - B --> E["wav2vec2-xlsr-53-espeak-cv-ft
log-softmax → logp (T × V)"] - E --> M["Vocab.merge
pool spelling variants of one sound"] - V["Expand variants
archify: Q, K, TH, DH, ZH, J, E
+ diphthong forms"] --> T["Tokenise each variant
collapse geminates / repeats"] - M --> X["Vocab.extend
add pooled class columns"] - T --> X - X --> F["CTC Viterbi per variant
keep max total log-prob"] - F --> G["GOP per token
in its window"] - G --> H["Exemptions
glottal drop · article vowel · near vowel"] - H --> I["Vowel-insertion cap
rule 3.10"] - I --> J["Native baseline
divide by TTS voice's score"] - J --> K["Teacher calibration floors"] - K --> L["Flags, tips, issues
margins + rules"] - L --> W["Word score
0.5·min + 0.5·mean"] - W --> S["Sentence score
0.5·phone mean + 0.5·word mean"] - F --> U["Greedy decode vs best variant
weighted edit distance ratio"] - U -->|"over 0.5, no veto"| C["unrelated: cap at 30, no tips"] - C --> S - S --> O["JSON result 0–100"] - O --> P["Exercise grades
band, best score, topic, quiz, verb game"] -``` - -## 2. Request sequence - -```mermaid -sequenceDiagram - autonumber - participant UI as Browser (index.html) - participant API as FastAPI /api/score - participant FF as ffmpeg - participant Ref as References (TTS baseline) - participant TTS as Sawtak TTS - participant Sc as Scorer - participant M as wav2vec2 CTC - participant DB as Supabase attempts - - UI->>API: multipart {phrase_id, audio} - API->>FF: decode to 16 kHz mono float32 - FF-->>API: wav (silence-trimmed) - alt wav shorter than 0.3 s - API-->>UI: {error: too_short} - end - API->>Ref: get(phrase_id, phrase), wait ≤ 8 s - opt first time this phrase is scored - Ref->>TTS: synthesise variant 0 (first voice) - TTS-->>Ref: native audio - Ref->>Sc: reference(wav, phrase) - Sc->>M: emit - M-->>Sc: logp - Sc-->>Ref: per-sound native scores, or None if untrustworthy - end - Ref-->>API: list of References (possibly empty) - API->>Sc: score(wav, phrase, refs, diagnostics=teacher) - Sc->>M: emit(wav) - M-->>Sc: logp (T × V) - Sc->>Sc: merge → variants → Viterbi → GOP → rules → baseline → floors → aggregate - Sc-->>API: result JSON - API-->>UI: result - API--)DB: background: {cat, phrase, score, unrelated, flagged issues} - UI->>UI: band label, letter colours, keep best score per sentence -``` - -The reference text never arrives from the client: the client sends only `phrase_id`, and the server looks up the phrase (bank phrase, teacher text line, or a verb-game form held server-side). That matters for the verb game, where the expected answer must not be exposed to the browser. - ---- - -## 3. Inputs - -### 3.1 Audio - -`decode_audio` pipes the upload through ffmpeg (`-ac 1 -ar 16000 -f f32le -t 32`). It then trims leading and trailing silence with a 20 ms RMS envelope (320 samples). A sample counts as voiced if its envelope is above `max(0.01, 0.05 · peak)`. 200 ms of padding is kept on each side. The trim keeps CTC from being fed seconds of blank, which would otherwise widen the first and last tokens' GOP windows. - -Anything shorter than 0.3 s after trimming is rejected before inference. - -### 3.2 Reference text: a phrase and its accepted variants - -A phrase in [app/phrases.py](../app/phrases.py) (and the generated workbook, grammar and teacher texts) carries a list of **accepted variants**, each `(arabic, ipa)`: - -```python -dict(cat="meet", he="אני מירושלים", variants=[ - ("انا من القدس", "ʔ a n a | m i n | i l ʔ u d s"), - ("انا من القدس", "ʔ a n a | m i n | i l q u d s"), -]) -``` - -IPA phonemes are space-separated, words are separated by `|`, and the Arabic and IPA must have the same word count. A variant is a genuinely different correct sentence (different lexical choice, a dropped word, a different vowel pattern). Dialectal realisations of single letters are handled by the next step instead. - -### 3.3 Variant expansion: many-ways letters ("archiphonemes") - -Six Arabic letters have more than one correct Palestinian realisation. [app/archi.py](../app/archi.py) `archify()` aligns each word's Arabic letters to its IPA consonants (a small edit-distance DP with cheap skips for weak letters like ا و ي ة ل). It then rewrites the phoneme that came from such a letter as `SYMBOL:written`: - -| Symbol | Letter | Accepted realisations (rules §5) | -|---|---|---| -| `Q` | ق | ʔ (urban), k (rural), q (also ok); g only if `optional_realisations_enabled` | -| `K` | ك | k, tʃ (rural) | -| `TH` | ث | t, θ; s only in learned words where the phrase writes s | -| `DH` | ذ | d, ð; z only in learned words where the phrase writes z | -| `ZH` | ظ | dˤ, zˤ, ðˤ | -| `J` | ج | dʒ, ʒ | -| `E` | final ة | a, e, i (positional) | -| `O` | pronoun ending | o, u (positional) | - -So `ʔ a h w e` for قهوة becomes `Q:ʔ a h w E:e`. The written phoneme is kept for display, tips and calibration keys. The class membership is what gets scored. - -Two more expansions: - -- **Diphthongs** (§6.3 #8). `diphthong_variants()` adds a copy of the variant with `eː → a j` and `oː → a w` where the Arabic writes ي / و. A diphthong is two tokens instead of one, so it cannot be a class. It becomes a separate variant that competes in the Viterbi step. -- **Deduplication.** Variants that differ only in how a many-ways letter is said collapse into one after archifying. - -A teacher can switch off any of these (the `dialect_*`, `final_vowel` and `diphthong` toggles). The scorer then requires the phoneme as written. - ---- - -## 4. Acoustic model and vocabulary handling - -### 4.1 Emissions - -`HFEmitter` runs `facebook/wav2vec2-xlsr-53-espeak-cv-ft` (or a fine-tuned checkpoint via `MODEL_ID`) and returns `log_softmax(logits)`, shape `(T, V)`. That is ~50 frames/s (20 ms hop), V ≈ 392 espeak IPA symbols plus `` as the CTC blank. - -Nothing downstream depends on torch. `Scorer` takes any `Emitter` callable, which is how [tests/test_scoring.py](../tests/test_scoring.py) drives the full scorer with synthetic posteriors. - -### 4.2 Pooling spelling variants (`Vocab.merge`) - -The multilingual vocab spells one sound several ways (`t / t̪ / tʰ / tː`, `r / ɾ`, `a / æ / ɐ`, `x / χ`). Left alone, a correctly said `t` would lose its GOP to its own allophone `t̪`. [app/phones.py](../app/phones.py) `base()` maps each symbol to a canonical sound by stripping length, aspiration, dentality, nasality and so on, and folding a fixed equivalence table. Emphatics stay distinct. - -`merge()` replaces each group's columns with one representative column holding the group's `logsumexp`. The other columns are set to −1e9. - -> A consequence: vowel length is invisible to the scorer, because `aː` and `a` share a base. Length errors can surface only through the margins and rules on the written phoneme, not through separate posteriors. - -### 4.3 Class columns for many-ways letters (`Vocab.add_class`, `Vocab.extend`) - -For a token like `Q:ʔ`, the scorer looks up each accepted realisation's model id (falling back via `FALLBACKS`, e.g. `ðˤ → zˤ → z`, when the vocab lacks a symbol). If more than one distinct id remains, it allocates a **virtual id** past the real vocab. `extend()` appends one column per virtual id: the `logsumexp` of its members' posteriors. - -This implements rules §6.1. If P(ʔ)=0.35 and P(k)=0.40 in a frame, the `Q` column holds 0.75, and neither member is treated as a competitor of the other. - -### 4.4 Tokenisation - -`tokenize()` turns the archified IPA into a list of `TargetToken(phoneme, id, word, sound)`. Adjacent tokens with the same (canonical) id collapse into one, as in CTC. That means geminates (`m m`) become one token, and so do two neighbouring phonemes that map to the same sound. Gemination is therefore not scored (§6.3 #10). - ---- - -## 5. Choosing the variant: CTC forced alignment - -For every expanded variant, [app/align.py](../app/align.py) `ctc_viterbi()` runs the standard CTC Viterbi over the extended label sequence `blank, y1, blank, y2, …, yN, blank` (S = 2N+1 states). It allows stay, advance by one, and the skip `s−2 → s` when the label differs from `s−2`. The path must end on the last label or the trailing blank. - -The result is `total_logp` (the best path's log-probability) and `spans[i] = [start, end)`, the frames where token *i* is emitted. - -```mermaid -stateDiagram-v2 - direction LR - [*] --> b0 - [*] --> y1 - b0 --> b0 - b0 --> y1 - y1 --> y1 - y1 --> b1 - y1 --> y2: skip if y2 ≠ y1 - b1 --> b1 - b1 --> y2 - y2 --> y2 - y2 --> b2 - b2 --> b2 - y2 --> [*] - b2 --> [*] -``` - -The variant with the highest `total_logp` wins (not length-normalised; all variants are scored against the same T frames). If T < N for every variant, the result is `error: too_short`. - -> Example: قديش said with *k* aligns better to `Q a d d eː ʃ` than to an alternative wording. Because `Q` pools ʔ/k/q, the *k* realisation costs nothing. - ---- - -## 6. Per-token scoring - -Each token goes through a fixed sequence of stages. The teacher's diagnostic view (`trace.tokens[i].stages`) reports the score after each one. - -```mermaid -flowchart LR - G["1 · GOP
score_tokens"] --> X["2 · Exemption
max(score, floor)"] - X --> I["3 · Insertion cap
min(score, 60)"] - I --> B["4 · Native baseline
100·s / b"] - B --> C["5 · Calibration floor
max(score, floor)"] - C --> F{"6 · Flag?
score under 50
AND beyond margin
AND consonant or flagged vowel"} - F -->|yes| T["tip (by rule)"] - F -->|no| N["maybe an 'issue' if under 90"] -``` - -### 6.1 Stage 1: GOP (`score_tokens`) - -**Window.** Token *i*'s window runs from the end of token *i−1* to the start of token *i+1* (clamped to include its own span). It therefore contains the blank frames around the emission, which is where a substituted phoneme typically peaks when the path was forced through the target. - - -**Target peak.** `peak = max_t logp[t, target]` inside the window. For a class token this is the pooled column. `realised` names the member with the highest peak (used for dialect detection and display). - -**Competitor.** Over the real columns only (no virtual columns), take each column's max inside the window. Then mask: -- the blank, -- the labels of tokens *i−1*, *i*, *i+1* and all their class members. - -Neighbours are masked because coarticulation into the adjacent phoneme is not an error (§6.3 #11). The argmax of what remains is the competitor, and its max is `comp_peak`. If the competitor is outside the set of sounds the system can name (Arabic, Hebrew interference sounds, rule labels), it is reported as `other` instead of by name. Example: a click or a retroflex from the multilingual vocab. - -**Score.** - -$$ -\text{gop} = \log p_{\text{target}} - \max(\log p_{\text{target}},\ \log p_{\text{comp}}) \;\le 0 -$$ - -$$ -\text{confidence} = \min\!\left(1,\ \frac{p_{\text{target}}}{0.30}\right) -$$ - -$$ -\text{score} = 100 \cdot e^{\,\lambda\cdot \text{gop}} \cdot \sqrt{\text{confidence}}, \qquad \lambda = 0.5\ (\texttt{leniency}) -$$ - -- With λ = 0.5 the ratio is square-rooted: a competitor twice as likely as the target gives 71, not 50. The zero-shot model is noisy on accented speech, so a linear ratio would punish the model's own uncertainty. -- The confidence term penalises a target that wins but only weakly. Below a peak posterior of 0.30, the score shrinks with √(p/0.3). -- A score of 100 requires the target to beat every non-neighbour competitor and reach p ≥ 0.30. - -**Deletion.** `deleted = p_target < 0.05 AND p_comp < 0.20`: nothing clearly there, target or otherwise. A deleted token is reported as heard `_`. - -**Worked numbers.** ħ said as a Hebrew ח (χ), with p(ħ) = 0.04 and p(χ) = 0.85 in the window: - -| quantity | value | -|---|---| -| gop | ln(0.04 / 0.85) = −3.056 | -| e^(0.5·gop) | 0.217 | -| confidence | 0.04 / 0.30 = 0.133, √ = 0.365 | -| score | 100 · 0.217 · 0.365 ≈ **7.9** | -| deleted? | no (p_comp 0.85 ≥ 0.20) | - -### 6.2 Stage 2: exemptions (rules §6.3) - -These are cases the model hears as a mistake that are ordinary native speech. The first rule that applies wins ([app/rules.py](../app/rules.py) `EXEMPTIONS`): - -| Exemption | Condition | Result | -|---|---|---| -| `glottal_dropped` | token deleted and its sound is ʔ or `Q` | 100 | -| `article_vowel_elided` | deleted vowel of ال right after a word ending in a vowel | 100 | -| `near_vowel` | vowel heard as same-group neighbour (i/e, u/o, a/ə/e) or other length | ≥ 90 (`near_vowel_floor`) | -| `unmarked_short_vowel` | short vowel deliberately not judged by policy; excludes word-internal post-consonantal u (rule 30) | ≥ 90 (`near_vowel_floor`) | -| `final_b_devoicing` | word-final b heard as p before a voiceless onset in the next word (rule 28) | 100 | -| `word_final_vowel_linking` | deleted word-final short vowel before a vowel-initial next word, including ʔ+V (rule 29) | ≥ 90 (`near_vowel_floor`) | - -A glottal stop replaced by another consonant is not exempt (only a deletion is), unless the class accepts it (ق → k). - -### 6.3 Stage 3: vowel insertion inside a cluster (rule 3.10) - -Hebrew speakers often break a word-internal consonant cluster with an epenthetic vowel (*katiir* for *ktiir*). For each pair of adjacent consonants in the same word (not the sentence's first cluster, and not when either is w or j), the scorer looks at the greedy (argmax) frames between the two consonants' spans. If at least `min_frames` = 2 frames have a vowel as argmax with p ≥ `min_peak` = 0.5, the second consonant's score is capped at `score_cap` = 60, and a severity-1 tip is attached. - -### 6.4 Stage 4: native baseline - -The phoneme model mishears some sounds even from natives (ʕ in على heard as dropped, l as r). A learner should not lose points for the model's weaknesses. - -[app/reference.py](../app/reference.py) synthesises the phrase's first variant with a Palestinian TTS voice and scores it with the same `Scorer`. It keeps the per-sound scores, unless the fit is `unrelated` or the mean phoneme score is under `ref_min_mean` = 60, in which case the voice is not trusted. One voice is generated per phrase. Further voices are used only if already cached from playback. The score request waits up to 8 s for a reference, then proceeds without one. - -Learner tokens are matched to reference tokens with `difflib.SequenceMatcher` on `(word, sound)` pairs. Sounds compare by symbol, so a reference that said ق as q is a valid baseline for a learner who says ʔ. Per learner token, `b` is the minimum score over all references that share that sound. Then: - -$$ -s' = \begin{cases} s & b \text{ is None or } b \ge 100 \\ \min\!\left(100,\ 100\cdot s / \max(b, 1)\right) & \text{otherwise} \end{cases} -$$ - -So a sound the TTS voice scores 40 on and the learner scores 40 on becomes 100. The baseline only ever raises scores. A word whose sounds were raised this way, and now all score ≥ 90, also shows the expected transliteration as "heard": the visible difference is one a native produces too. - -### 6.5 Stage 5: teacher calibration floors - -Confirmed tutor flags can become calibration rules `(expected, heard, phrase?) → floor`. A rule is looked up by sound symbol first, then by written phoneme, phrase-specific before global. It raises a matching token to `floor` and never lowers it. Applied floors are listed in `result.calibrated`. - -The base multilingual model also has one conservative built-in calibration: when expected `ʕ` is classified as deleted (`_`), its score is raised to 60. This prevents a known false negative from producing a definite “ع was swallowed” warning, while retaining a meaningful penalty. An explicit teacher calibration for `ʕ → _` overrides this floor. - -### 6.6 Stage 6: is it a mistake? Flags, margins, rules - -Final per-token scores now exist. The decision to **flag** a token (show "heard as X" and a tip) needs all three of: - -1. `score < flag_below` (50), or the sound's own threshold in `flag_below_sound`. That is set per model (`FLAG_BELOW_BY_MODEL`): for the base model ح uses 20, because it scores a correct ح in the 20s–40s (measured with `scripts/minerva_benchmark.py`). -2. The competitor beats the target by at least the pair's **margin** (rules §6.2): - - substitution: `−gop ≥ margin(target, heard)` - - deletion: `p_target ≤ deleted_peak − margin(target, "_")` in log space -3. The token is a consonant, or a short vowel a rule explicitly flags. Currently that is only **u after a consonant in the same word** (rule 3.11). Other short vowels are never flagged (§6.3 #7). - -Margins are natural-log ratios from the rules document: - -| Target | Heard | Margin | Meaning | -|---|---|---|---| -| sˤ tˤ dˤ zˤ ðˤ | s t d z ð | 0.7 | plain must be ≈ 2× likelier (emphasis is weak in the base model) | -| h | `_` | 0.7 | h is short and quiet; natives reduce it too | -| ħ | h | 0.4 | ≈ 1.5× | -| long vowels | short | 1.1 | declared, but see the note below | -| everything else | | 0.0 | the competitor merely has to win | - -> Margins on long-vowel targets are skipped at parse time (`"ː" not in t`), because `base()` drops length and the margin would otherwise land on vowel-quality pairs it was not meant for. - -The **rule** for a substitution is looked up as `(target, heard)` in the `checks` of [static/assets/pronunciation-rules.md](../static/assets/pronunciation-rules.md). Example: - -```yaml -id: pharyngeal_h -checks: - - {target: ħ, heard: [χ, x], severity: 3, feedback: "…"} - - {target: ħ, heard: [h, ɦ], severity: 2, feedback: "…"} - - {target: ħ, heard: ["_"], severity: 2, feedback: "…"} -``` - -Severity 3 means the error changes meaning (ħ/χ, ʕ/ʔ); 1 is accent. The rule chooses the Hebrew feedback text and the tip ordering. **It does not change the number.** The score is pure acoustics plus the stages above. - ---- - -## 7. From tokens to words, letters and the sentence - -```mermaid -flowchart BT - P1["phoneme scores
(vowels weight 0.5)"] --> WS["word score
0.5 · min + 0.5 · mean"] - P1 --> PM["phone mean
weighted"] - WS --> WM["word mean"] - PM --> SS["sentence = 0.5 · phone mean + 0.5 · word mean"] - WM --> SS - SS --> UC{"unrelated?"} - UC -->|yes| CAP["min(score, 30)"] - UC -->|no| R["round → 0–100"] - CAP --> R - P1 --> LT["letter colour
= weakest of its sounds"] -``` - -**Word.** `word = 0.5 · min(phonemes) + 0.5 · mean(phonemes)` (`word_min_weight`). One bad sound drags its word down hard, but not to zero. - -**Sentence.** - -$$ -\text{phone\_mean} = \frac{\sum_i w_i s_i}{\sum_i w_i},\quad w_i = \begin{cases}0.5 & \text{vowel}\\ 1 & \text{consonant}\end{cases} -\qquad -\text{overall} = 0.5\cdot\text{phone\_mean} + 0.5\cdot\overline{\text{word}} -$$ - -The word term exists so that a single wrong consonant moves a sentence from "excellent" to "almost". A plain phoneme mean would dilute it across all the correct sounds. - -**Letters.** For display, the Hebrew-letter transliteration of each word is mapped back to the tokens that carry each letter (`_carriers`, `word_letters`). Each letter gets the minimum score among its tokens. A letter under `LETTER_GOOD` = 75 also carries `why` (what the sound was heard as) and `tip`. The page colours letters ≥ 75 / ≥ 50 / below. - -### 7.1 Worked example - -A hypothetical phrase `w eː n | i l ħ a m m aː m` ("where is the bathroom"), with ħ said as χ as in §6.1. Geminate `m m` is one token. - -| word | tokens and scores | mean | min | word score | -|---|---|---|---|---| -| ween | w 100 · eː 95 · n 100 | 98.3 | 95 | 96.7 | -| ilħammaam | i 100 · l 100 · **ħ 7.9** · a 96 · m 100 · aː 92 · m 100 | 85.1 | 7.9 | **46.5** | - -- phone_mean = (5·100 + 7.9 + 0.5·(95+100+96+92)) / (6 + 4·0.5) = 699.4 / 8 = **87.4** -- word_mean = (96.7 + 46.5) / 2 = **71.6** -- overall = 0.5·87.4 + 0.5·71.6 = **79.5 → 80**, which the page labels "כמעט" (almost). - -With phoneme averaging alone, the same recording would score 87 and be called excellent. - -ħ is flagged: 7.9 < 50, the margin for (ħ, χ) is 0, so −gop = 3.06 ≥ 0, and ħ is a consonant. The rule `pharyngeal_h` gives a severity-3 tip. - ---- - -## 8. "Different sentence?" detection - -Forced alignment always produces a path, even for the wrong sentence. To catch this, the scorer greedy-decodes the posteriors (argmax per frame, collapse repeats, drop blanks; real columns only). It then computes a weighted Levenshtein distance to the chosen variant's token ids (`Scorer._distance`), with class members counting as equal. The weights forgive only what the exemptions already forgive: - -| Edit | Cost | -|---|---| -| a vowel heard as the competitor its exemption forgave (i→e near_vowel, i→ə unmarked_short_vowel) | 0 | -| another vowel for a vowel | 0.25 | -| a consonant heard as its exempted competitor (b→p, t→d); not for `glottal_dropped`, which forgives a drop only | 0 | -| a dropped ʔ (`glottal_dropped`) or article vowel (`article_vowel_elided`) | 0 | -| a dropped exempted short vowel | 0.25 | -| h θ f s x χ before the first sound or after the last (breath, noise) | 0.25 | -| anything else | 1 | - -The distance is divided by $W$, the sum of the deletion costs: the weight of the sounds that can be wrong at all. $W$ is at least $N/2$ and at least 1, so a short word does not turn unrelated over one stray sound. - -$$ -\text{unrelated} = \frac{\text{weighted\_distance}}{W} > 0.5 \;\wedge\; \neg\,\text{veto} -$$ - -**Veto.** It is never called unrelated when the forced alignment clearly found the sentence: at least 2 consonants, and 60% of them, peak at p ≥ 0.5, and the mean GOP score before exemptions is ≥ 60. - -The trace reports `distance` (weighted), `raw_distance` (every edit 1), `weight`, `ratio` and `veto`. Before the weights, plain edit distance over $N$ above 0.65 capped correct attempts: ابننا heard as `e b ə n a θ` was 4/6 and scored 30 instead of 97. Weighted, it is 0.25/3. - -If unrelated, the sentence score is capped at 30, no tips or issues are produced, letters get no reasons, and the page shows "משפט אחר?". It also covers hesitations and restarts (§6.3 #14). The greedy decode is returned as `heard_ipa`. The Hebrew-letter rendering of what was heard (`heard_tr`) is built by cutting the greedy sequence at the aligned word starts. - ---- - -## 9. Feedback selection - -```mermaid -flowchart TD - A["Flagged tokens"] --> B["tip_for(sound, heard, rule)
dedupe by tip key"] - B --> C["sort: severity desc, then score asc"] - C --> D["keep max_tips = 3"] - E["All tokens under 100 without a tip"] --> F["keep those under 90 AND beyond margin"] - F -->|none and no tips| G["the 3 weakest"] - F --> H["issues, up to 8, weakest first"] - G --> H -``` - -- **Tips** are the flagged substitutions with a rule, most severe first (meaning-changing before accent), then lowest score. At most 3 (4 with the `tips_by_severity` toggle off). -- **Issues** explain any score short of 100. Every imprecise sound not already covered by a tip, so the student never sees 94 with no reason. -- **Dialect.** If the accepted realisations used are all urban-only or all rural-only, the result carries `dialect`. If they disagree, it carries `mixed_dialect`. Either way this is informational and never affects the score (§6.3 #12). - ---- - -## 10. The response - -```jsonc -{ - "score": 80, // sentence, 0–100 - "variant": {"ar": "...", "tr": "...", "ipa": "..."}, // the chosen accepted variant - "heard_ipa": "w e n i l χ a m a m", // greedy decode - "heard_tr": "...", // the same in Hebrew letters - "unrelated": false, - "words": [{ - "ar": "...", "tr": "...", "heard_tr": "...", "score": 46.5, - "letters": [{"t": "חַ", "score": 7.9, "why": "...", "tip": "..."}], - "phonemes": [{"ipa": "ħ", "word": 1, "score": 7.9, "flagged": true, "heard": "χ"}] - }], - "tips": [{"key": "...", "title": "...", "text": "...", "severity": 3, "word": "...", "score": 8}], - "issues": [{"sound": "...", "word_tr": "...", "score": 92, "missing": false, "heard": "..."}], - "references": 1, // native baselines used - "dialect": "urban", // or "mixed_dialect": true - "calibrated": [ ... ], // only if a teacher floor applied - "phoneme_heatmap": { ... }, // teachers only: max-pooled posteriors, ≤ 180 columns - "trace": { ... } // teachers only: every stage, threshold and intermediate value -} -``` - -`trace` is the place to start when debugging a surprising score. Its full schema is [docs/trace-schema.ts](trace-schema.ts), and the trace dashboard ([static/trace.html](../static/trace.html), "לוח אבחון" in the lab and the phoneme dialog) draws it: the frames around a sound with its window, its score stage by stage, the candidate readings and the unrelated gauge, and the sentence with its aggregation. It holds the candidate variants with their path log-probs, the unrelated ratio, and per token: window, p_target, competitor, gop, confidence factor, each stage's score, margin needed vs. got, rule, and whether the token became a tip or an issue. - ---- - -## 11. From a recording's score to exercise grades - -The scorer grades one recording. Everything above that level is aggregation, mostly on the client. - -```mermaid -flowchart TD - R["Recording score 0–100"] --> BAND["Verdict on the card
≥ 75 מצוין · ≥ 60 כמעט · else נסו שוב
unrelated → משפט אחר?"] - R --> BEST["Sentence best = max over attempts
(device + attempts table)"] - BEST --> TOPIC["Topic: done when every sentence has a best
★ gold when all bests ≥ 75"] - TOPIC --> LESSON["Lesson / homework progress"] - BEST --> PANEL["Teacher panel: student grade = mean of bests
class 'hard sentences' = class mean of bests under 70"] - BEST --> DAILY["Daily session review pool: bests under 75"] - R --> QUIZ["Story quiz: mean of line scores said
+ top 3 mistakes by frequency"] - R --> VERB["Verb game L4–5: ≥ 75 correct
unrelated / too short → uncertain, not counted"] - VERB --> GATE["Level gates: share correct ≥ 85–90 %"] -``` - -| Exercise | Grade | Where | -|---|---|---| -| Single sentence | The recording's `score`; band 75 / 60 | `index.html` (`EXCELLENT = 75`) | -| Sentence progress | Best score over all attempts. Any non-empty recording, including an unrelated one, marks the sentence as practised | `keepScore` | -| Topic | Practised count / total; gold if every best ≥ 75 | `tally()` | -| Story quiz ("בוחן") | Arithmetic mean of the lines' scores. One recording per line, no retries | `quizEnd()` | -| Verb game, levels 4–5 | `correct` if score ≥ 75. `unrelated` or too short → `outcome: "uncertain"`, `counts_as_attempt: false`, never a grammar error | [app/speech_evaluation.py](../app/speech_evaluation.py) | -| Verb game, unlocking | Correct ratio ≥ `VERB_THRESH` (0.85–0.90) over a minimum number of attempts | `index.html` | -| Arabic → Hebrew translation | Not scored by this pipeline: browser `he-IL` ASR, share of expected words heard in order, 0.8 = correct. Not stored | `index.html` (`HE`) | -| Teacher panel | Student grade = mean of their best scores; class difficulty per sentence = mean of students' bests | `index.html`, `/api/class` | - -Verb game levels 1–3 (choice and matching) are graded server-side by exact match against a stored answer, with no audio involved. - ---- - -## 12. Configuration reference - -The values live in code, not in the rules document. The `scoring:` block of `pronunciation-rules.md` is read for **`margins`, `margin_default` and `max_tips` only**. Its other keys (`flag_below`, `bands`, `sentence_score`, `severity_weight`, …) are documentation. Its `sentence_score` string (`mean(word_min)`) also describes an older word formula; the code uses `0.5·min + 0.5·mean`. - -| Knob | Default | Effect | -|---|---|---| -| `GopConfig.leniency` | 0.5 | exponent on the target/competitor ratio | -| `GopConfig.confident_peak` | 0.30 | target posterior above which confidence stops lowering the score | -| `GopConfig.deleted_peak` / `deleted_comp` | 0.05 / 0.20 | deletion thresholds | -| `GopConfig.consonant_window` | 4 frames | maximum context on each side of a consonant span; never crosses neighbours | -| `ScorerConfig.flag_below` | 50 | max score for a flag and tip | -| `ScorerConfig.flag_below_sound` | `{ħ: 20}` for the base model | per-sound replacement of `flag_below`, from `FLAG_BELOW_BY_MODEL[MODEL_ID]` | -| `ScorerConfig.issue_below` | 90 | max score to be listed as an issue | -| `ScorerConfig.near_vowel_floor` | 90 | floor for neighbouring vowel qualities | -| `ScorerConfig.word_min_weight` | 0.5 | weight of the weakest phoneme in a word | -| `ScorerConfig.unrelated_ratio` | 0.5 | weighted edit-distance ratio for "different sentence" (§8) | -| `ScorerConfig.unrelated_veto_share` · `_peak` · `_mean` | 0.6 · 0.5 · 60 | the alignment veto of §8 | -| `ScorerConfig.ref_min_mean` | 60 | minimum mean for a TTS reference to be trusted | -| vowel weight | 0.5 | in the sentence phone mean (hard-coded) | -| unrelated cap | 30 | hard-coded | -| `LETTER_GOOD` | 75 | letter colour / reason threshold | -| insertion `min_peak`, `min_frames`, `score_cap` | 0.5, 2, 60 | rule 3.10, from the rules document | - -Teachers can switch off each rule-derived behaviour at runtime (the **ניטור** tab, `rules.TOGGLES`). Switching one off restores the pre-rules behaviour for that aspect from the next recording on. - ---- - -## 13. Known limitations - -- **Zero-shot model.** The base model was not trained on Palestinian Arabic or Hebrew-accented speech. The native baseline and margins compensate partially; fine-tuning ([train/finetune.py](../train/finetune.py)) on teacher and labelled learner recordings is the intended fix. -- **Duration is not modelled.** CTC posteriors are peaky and collapse repeats, so vowel length and gemination are effectively unscored. -- **Fallback symbols.** If the vocab lacks a phoneme (e.g. `ðˤ`), it is scored as its stand-in. `/api/health` → `phoneme_fallbacks` lists these, and the contrast they lose cannot be detected. -- **GOP is max-pooled, not frame-summed.** Using window peaks makes the score robust to alignment jitter, but a brief spurious competitor spike can still win the window. diff --git a/docs/mathly-supabase.sql b/docs/mathly-supabase.sql index b3327e8f6a08d48c1ae4a97a6edac1e7114ba0d2..7d89574ffb1648f99fbffd2d50de173572affd08 100644 --- a/docs/mathly-supabase.sql +++ b/docs/mathly-supabase.sql @@ -1,347 +1,54 @@ --- The whole database of a new Supabase project for the app, in one go: paste it into the SQL Editor and run it. --- Gathered from the README's sections (each is explained there); made for mathly, the math Space, whose classes --- are math classes by default (the last lines). -create table public.attempts ( - id bigint generated always as identity primary key, - user_id uuid not null default auth.uid() references auth.users (id) on delete cascade, - cat text not null, -- the topic id, e.g. l1-verbs - phrase text not null, -- the Hebrew prompt - score int not null, -- 0-100 - unrelated boolean not null default false, -- "different sentence?" - issues jsonb not null default '[]'::jsonb, -- compact [{word, expected, heard, score}], never audio - model text not null default '', - version text not null default '', - created_at timestamptz not null default now() -); -create index attempts_user_time on public.attempts (user_id, created_at desc); -create index attempts_created_at_idx on public.attempts (created_at desc); - -alter table public.attempts enable row level security; -grant select, insert on public.attempts to authenticated; -create policy "students add their own attempts" on public.attempts - for insert to authenticated with check (user_id = auth.uid()); -create policy "students read their own attempts" on public.attempts - for select to authenticated using (user_id = auth.uid()); - --- Anonymous aggregate buckets for the teacher's Monitoring graph. Only the backend service role may call it. -create or replace function public.activity_buckets(p_hours integer default 24, p_bucket_minutes integer default 15) -returns table (bucket timestamptz, attempts bigint, active_students bigint) -language sql stable security invoker set search_path = '' as $$ - select date_bin(make_interval(mins => greatest(1, least(p_bucket_minutes, 60))), a.created_at, - timestamptz '2000-01-01 00:00:00+00'), - count(*)::bigint, count(distinct a.user_id)::bigint - from public.attempts a - where a.created_at >= now() - make_interval(hours => greatest(1, least(p_hours, 168))) - group by 1 order by 1 -$$; -revoke execute on function public.activity_buckets(integer, integer) from public, anon, authenticated; -grant execute on function public.activity_buckets(integer, integer) to service_role; - --- Totals for the full selected window. This is separate from the buckets so a student who practised in several --- buckets is counted only once. -drop function if exists public.activity_summary(integer); -create or replace function public.activity_summary(p_hours integer default 24) -returns table (attempts bigint, active_students bigint, returning_students bigint, active_days bigint) -language sql stable security invoker set search_path = '' as $$ - with windowed as ( - select a.user_id, (a.created_at at time zone 'Asia/Jerusalem')::date as practice_day - from public.attempts a - where a.created_at >= now() - make_interval(hours => greatest(1, least(p_hours, 168))) - ), per_user as ( - select user_id, count(distinct practice_day) as days from windowed group by user_id - ) - select (select count(*) from windowed)::bigint, - (select count(*) from per_user)::bigint, - (select count(*) from per_user where days >= 2)::bigint, - (select count(distinct practice_day) from windowed)::bigint -$$; -revoke execute on function public.activity_summary(integer) from public, anon, authenticated; -grant execute on function public.activity_summary(integer) to service_role; - --- Subject-scoped monitoring aggregates. These keep busy classes accurate without downloading individual attempts. -create or replace function public.activity_buckets_scoped( - p_user_ids uuid[], p_since timestamptz, p_bucket_minutes integer default 15 -) -returns table (bucket timestamptz, attempts bigint, active_students bigint) -language sql stable security invoker set search_path = '' as $$ - select date_bin(make_interval(mins => greatest(1, least(p_bucket_minutes, 60))), a.created_at, - timestamptz '2000-01-01 00:00:00+00'), - count(*)::bigint, count(distinct a.user_id)::bigint - from public.attempts a - where a.created_at >= p_since and a.user_id = any(p_user_ids) - group by 1 order by 1 -$$; -revoke execute on function public.activity_buckets_scoped(uuid[], timestamptz, integer) from public, anon, authenticated; -grant execute on function public.activity_buckets_scoped(uuid[], timestamptz, integer) to service_role; - -create or replace function public.activity_summary_scoped(p_user_ids uuid[], p_since timestamptz) -returns table (attempts bigint, active_students bigint, returning_students bigint, active_days bigint) -language sql stable security invoker set search_path = '' as $$ - with windowed as ( - select a.user_id, (a.created_at at time zone 'Asia/Jerusalem')::date as practice_day - from public.attempts a - where a.created_at >= p_since and a.user_id = any(p_user_ids) - ), per_user as ( - select user_id, count(distinct practice_day) as days from windowed group by user_id - ) - select (select count(*) from windowed)::bigint, - (select count(*) from per_user)::bigint, - (select count(*) from per_user where days >= 2)::bigint, - (select count(distinct practice_day) from windowed)::bigint -$$; -revoke execute on function public.activity_summary_scoped(uuid[], timestamptz) from public, anon, authenticated; -grant execute on function public.activity_summary_scoped(uuid[], timestamptz) to service_role; - --- Recent system activity shown only in the management panel. The backend writes with the service-role key; --- no browser role gets direct table access. -create table public.system_events ( - id bigint generated always as identity primary key, - kind text not null check (kind in ('exercise','game','permission','content')), - actor_id uuid references auth.users(id) on delete set null, - actor_email text not null default '', - label text not null default '', - details jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now() -); -create index system_events_created_at_idx on public.system_events (created_at desc); -alter table public.system_events enable row level security; - -create table public.app_secrets ( - name text primary key check (name in ('SAWTAK_API_KEY','ANTHROPIC_API_KEY','TUTOR_MODEL')), - ciphertext text not null, - last4 text not null default '', - updated_by text not null default '', - updated_at timestamptz not null default now() -); -alter table public.app_secrets enable row level security; -revoke all on public.app_secrets from anon, authenticated; - -alter table public.app_secrets drop constraint if exists app_secrets_name_check; -alter table public.app_secrets add constraint app_secrets_name_check - check (name in ('SAWTAK_API_KEY','ANTHROPIC_API_KEY','TUTOR_MODEL')); - -alter table public.attempts - add column if not exists issues jsonb not null default '[]'::jsonb, - add column if not exists model text not null default '', - add column if not exists version text not null default ''; - -create table public.user_access ( +-- Destructive one-time migration from the former multi-product application. +-- Back up the project before running this file. +drop table if exists public.attempts cascade; +drop table if exists public.writings cascade; +drop table if exists public.tutors cascade; +drop table if exists public.flags cascade; +drop table if exists public.lab_samples cascade; +drop table if exists public.calibrations cascade; +drop table if exists public.scoring_rules cascade; +drop table if exists public.model_settings cascade; +drop table if exists public.videos cascade; +drop table if exists public.texts cascade; +drop table if exists public.dailies cascade; +drop table if exists public.library cascade; +drop table if exists public.learner_class_state cascade; +drop table if exists public.tutor_chats cascade; + +-- Mathly owns only math-class rosters and role assignments. +create table if not exists public.classes ( + id bigint generated always as identity primary key, + name text not null unique, + emails text[] not null default '{}', + nicknames jsonb not null default '{}', + lessons text[] not null default '{}', + teachers text[] not null default '{}', + created_at timestamptz not null default now() +); + +do $$ begin + if exists (select 1 from information_schema.columns + where table_schema = 'public' and table_name = 'classes' and column_name = 'subject') then + delete from public.classes where subject is distinct from 'מתמטיקה'; + end if; +end $$; +alter table public.classes drop column if exists subject, + drop column if exists homework, + drop column if exists due, + drop column if exists homeworks, + drop column if exists dailies; + +create table if not exists public.user_access ( user_id uuid primary key references auth.users(id) on delete cascade, - email text not null, + email text not null default '', roles text[] not null default array['student']::text[], - entitlements text[] not null default '{}'::text[], - subject text not null default '', -- developer-set subject for admin-created classes - updated_at timestamptz not null default now(), - updated_by uuid references auth.users(id), - constraint valid_roles check (roles <@ array['student','teacher','flagger','admin','developer']::text[]), - constraint valid_entitlements check (entitlements <@ array['practice','games','tutor','path','library']::text[]) -); -create unique index user_access_email_idx on public.user_access (lower(email)); -alter table public.user_access enable row level security; -create table public.access_audit ( - id bigint generated always as identity primary key, - target_user_id uuid not null, - changed_by uuid not null, - old_access jsonb not null, - new_access jsonb not null, - created_at timestamptz not null default now() -); -alter table public.access_audit enable row level security; - -alter table public.user_access add column if not exists subject text not null default ''; -alter table public.user_access drop constraint if exists valid_entitlements; -update public.user_access -set entitlements = array_append(entitlements, 'practice') -where 'path' = any(entitlements) and not ('practice' = any(entitlements)); -alter table public.user_access add constraint valid_entitlements check ( - entitlements <@ array['practice','games','tutor','path','library']::text[] -); - -create table public.tutors ( - email text primary key, - granted_by text not null default '', -- the teacher who gave the rank - created_at timestamptz not null default now() -); -alter table public.tutors enable row level security; -- no policy: only the secret key reads and writes it - -create table public.flags ( - id bigint generated always as identity primary key, - email text not null, -- the tutor - cat text not null, -- the topic id - phrase text not null, - he text not null, -- the Hebrew prompt - ar text not null, -- the accepted answer the scorer matched - ipa text not null, -- its phonemes: what the tutor says was said - heard_ipa text not null, -- the model's free decode - score int not null, - unrelated boolean not null default false, - note text not null default '', - result jsonb not null, -- the whole score response - model text not null, - version text not null default '', - audio text, -- object name in the private flags Storage bucket - status text not null default 'pending', -- pending, confirmed, rejected, or fixed - category text, - reviewed_by text, - reviewed_at timestamptz, - review_note text not null default '', - false_issues jsonb not null default '[]'::jsonb, -- [{word, expected, heard}] - created_at timestamptz not null default now() -); -alter table public.flags enable row level security; -- no policy: only the secret key reads and writes it - -insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types) -values ( - 'flags', - 'flags', - false, - 2000000, - array['audio/webm','audio/ogg','audio/mp4','audio/mpeg','audio/wav','audio/x-wav'] -) -on conflict (id) do update set - public = excluded.public, - file_size_limit = excluded.file_size_limit, - allowed_mime_types = excluded.allowed_mime_types; - -alter table public.flags - add column if not exists status text not null default 'pending', - add column if not exists category text, - add column if not exists reviewed_by text, - add column if not exists reviewed_at timestamptz, - add column if not exists review_note text not null default '', - add column if not exists false_issues jsonb not null default '[]'::jsonb; - -create table public.lab_samples ( - id bigint generated always as identity primary key, email text not null, - item text not null, ar text not null, ipa text not null, speaker text not null default '', - label text not null, -- 'correct' or 'mistake' - mistake jsonb, -- {"index", "expected"}: the sound said wrong on purpose - device text not null default '', score int not null, heard_ipa text not null, - flagged jsonb not null default '[]'::jsonb, -- the sounds the scorer marked down, "expected>heard" - result jsonb not null, model text not null, version text not null default '', - audio text, -- the recording's name in the storage bucket lab - created_at timestamptz not null default now()); -alter table public.lab_samples enable row level security; -insert into storage.buckets (id, name) values ('lab', 'lab') on conflict (id) do nothing; - -create table public.calibrations ( - id bigint generated always as identity primary key, - expected text not null, - heard text not null, - floor numeric not null default 70, - scope text not null default 'global', -- global or phrase - phrase text not null default '', - evidence jsonb not null default '{}', -- report/reporter counts and supporting phrase keys - model text not null default '', - status text not null default 'draft', -- draft, applied, or rejected - proposed_by text not null, - reviewed_by text, - reviewed_at timestamptz, - created_at timestamptz not null default now() -); -alter table public.calibrations enable row level security; - -create table public.scoring_rules ( - id text primary key, -- a rule of rules.TOGGLES, e.g. dialect_Q, margins, rule_pharyngeal_h - active boolean not null default true, - updated_by text not null default '', updated_at timestamptz not null default now() ); -alter table public.scoring_rules enable row level security; - -create table public.writings ( - user_id uuid not null default auth.uid() references auth.users (id) on delete cascade, - topic text not null, -- the topic id, e.g. l1-hw - lines jsonb not null, -- the sentences as written, "" for one not written yet - updated_at timestamptz not null default now(), - primary key (user_id, topic) -); - -alter table public.writings enable row level security; -grant select, insert, update on public.writings to authenticated; -create policy "students read their own writings" on public.writings - for select to authenticated using (user_id = auth.uid()); -create policy "students add their own writings" on public.writings - for insert to authenticated with check (user_id = auth.uid()); -create policy "students change their own writings" on public.writings - for update to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid()); - -create table public.videos ( - id bigint generated always as identity primary key, - lesson text not null, -- the lesson id, e.g. 2 or 2.1 - title text not null, -- the topic's name on the path - url text not null, -- https://vimeo.com/[/] or https://www.youtube.com/watch?v= - pos int, -- how many of the lesson's own topics stand before it; null = after them all - ord int not null default 0, -- its turn among the videos of one place - created_at timestamptz not null default now() -); -alter table public.videos enable row level security; - -alter table public.videos add column if not exists pos int, add column if not exists ord int not null default 0; - -create table public.dailies ( - id bigint generated always as identity primary key, - level text not null, -- a level id, e.g. 1 - title text not null, - url text not null, -- https://vimeo.com/[/] or a YouTube link - created_at timestamptz not null default now() -); -alter table public.dailies enable row level security; -create table public.library ( - id bigint generated always as identity primary key, - level text not null, -- a level id, e.g. 1 - title text not null, - url text not null, - ord int, -- its turn in the list; none until the teacher moves a video - created_at timestamptz not null default now() -); -alter table public.library enable row level security; - -alter table public.library add column if not exists ord int; -notify pgrst, 'reload schema'; - -create table public.texts ( - id bigint generated always as identity primary key, - lesson text not null, -- the lesson id, e.g. 2 or 2.1 - title text not null, -- the topic's name on the path - voices text[] not null default '{}', -- a dialogue's two voices by name (odd lines, even lines); empty = a story - lines jsonb not null, -- [[Hebrew, Arabic in Hebrew letters], ...] - pos int, -- as in videos - ord int not null default 0, - created_at timestamptz not null default now() -); -alter table public.texts enable row level security; +alter table public.user_access drop column if exists subject, + drop column if exists entitlements; -create table public.classes ( - id bigint generated always as identity primary key, - name text not null unique, -- e.g. אוקטובר 2026 - emails text[] not null default '{}', -- the students' Google emails, lower-case - nicknames jsonb not null default '{}', -- {email: nickname}, for the students who have one - lessons text[] not null default '{}', -- the lesson ids the class studies; empty = every lesson - subject text not null default 'ערבית', -- existing classes; new ones copy the creating admin - teachers text[] not null default '{}', -- class-scoped teachers; each address is also in emails - homework text not null default '', -- the topic id the class practises now, e.g. l2-talk; '' = none - due date, -- the day the homework is due, if it has one - homeworks jsonb not null default '[]', -- every homework of the class: [{"topic": ..., "due": ...}] - dailies jsonb not null default '[]', -- the days of its daily videos: [{"video": id, "date": ...}] - created_at timestamptz not null default now() -); alter table public.classes enable row level security; - -alter table public.classes add column if not exists nicknames jsonb not null default '{}', - add column if not exists lessons text[] not null default '{}', - add column if not exists homework text not null default '', - add column if not exists due date, - add column if not exists homeworks jsonb not null default '[]', - add column if not exists teachers text[] not null default '{}', - add column if not exists dailies jsonb not null default '[]', - add column if not exists subject text not null default 'ערבית'; -update public.classes set subject = 'ערבית' where subject = ''; - --- the tutor's conversations (app/tutor.py) -create table public.tutor_chats (id uuid primary key, user_id uuid, email text not null default '', scenario text not null, gender text not null default '', messages jsonb not null default '[]'::jsonb, turns int not null default 0, finished boolean not null default false, model text not null default '', input_tokens int not null default 0, output_tokens int not null default 0, created_at timestamptz not null default now(), updated_at timestamptz not null default now()); alter table public.tutor_chats enable row level security; - --- mathly: a class is a math class unless it says otherwise -alter table public.classes alter column subject set default 'מתמטיקה'; -notify pgrst, 'reload schema'; +alter table public.user_access enable row level security; +revoke all on public.classes from anon, authenticated; +revoke all on public.user_access from anon, authenticated; diff --git a/docs/scoped-activity-migration.sql b/docs/scoped-activity-migration.sql deleted file mode 100644 index 6fbd3242b007ba666575968bd7b77b120bd09fac..0000000000000000000000000000000000000000 --- a/docs/scoped-activity-migration.sql +++ /dev/null @@ -1,35 +0,0 @@ --- Accurate, subject-scoped monitoring aggregates. Safe to run more than once. -create or replace function public.activity_buckets_scoped( - p_user_ids uuid[], p_since timestamptz, p_bucket_minutes integer default 15 -) -returns table (bucket timestamptz, attempts bigint, active_students bigint) -language sql stable security invoker set search_path = '' as $$ - select date_bin(make_interval(mins => greatest(1, least(p_bucket_minutes, 60))), a.created_at, - timestamptz '2000-01-01 00:00:00+00'), - count(*)::bigint, count(distinct a.user_id)::bigint - from public.attempts a - where a.created_at >= p_since and a.user_id = any(p_user_ids) - group by 1 order by 1 -$$; -revoke execute on function public.activity_buckets_scoped(uuid[], timestamptz, integer) - from public, anon, authenticated; -grant execute on function public.activity_buckets_scoped(uuid[], timestamptz, integer) to service_role; - -create or replace function public.activity_summary_scoped(p_user_ids uuid[], p_since timestamptz) -returns table (attempts bigint, active_students bigint, returning_students bigint, active_days bigint) -language sql stable security invoker set search_path = '' as $$ - with windowed as ( - select a.user_id, (a.created_at at time zone 'Asia/Jerusalem')::date as practice_day - from public.attempts a - where a.created_at >= p_since and a.user_id = any(p_user_ids) - ), per_user as ( - select user_id, count(distinct practice_day) as days from windowed group by user_id - ) - select (select count(*) from windowed)::bigint, - (select count(*) from per_user)::bigint, - (select count(*) from per_user where days >= 2)::bigint, - (select count(distinct practice_day) from windowed)::bigint -$$; -revoke execute on function public.activity_summary_scoped(uuid[], timestamptz) - from public, anon, authenticated; -grant execute on function public.activity_summary_scoped(uuid[], timestamptz) to service_role; diff --git a/docs/scoring-dialects-plan.md b/docs/scoring-dialects-plan.md deleted file mode 100644 index c187869d06a58e37f4579d941b1f65d17ea25c04..0000000000000000000000000000000000000000 --- a/docs/scoring-dialects-plan.md +++ /dev/null @@ -1,172 +0,0 @@ -# Plan: dialect acceptance and Hebrew-speaker rules in the scorer - -Status: implemented 2026-10-15, except where "Implementation notes" at the end says otherwise. Source rules: [pronunciation-rules.md](../static/assets/pronunciation-rules.md). - -Two layers on top of today's scoring path: - -- **Layer A, dialect acceptance:** decides what counts as *correct*. Runs inside GOP: every correct realisation of a letter is pooled into one class, and members of the class never count as competitors. -- **Layer B, Hebrew-speaker rules:** decides how strictly to judge *real errors*. Runs after GOP: per-pair margins, severity, feedback, exemptions. - -The native baseline and the teacher calibration floors stay where they are, but are keyed on the abstract symbol instead of the sound actually said. - -Final order of the pipeline: - -1. `Vocab.merge` pools spelling variants (exists) -2. Layer A: class columns → alignment → GOP against the class -3. Layer B exemptions (6.3) -4. Native baseline normalisation (exists, keyed on the abstract symbol) -5. Layer B margins, severity, feedback -6. Teacher DB floors (exists, last, manual override) - -## Phase 0: Rule loader (`app/rules.py`, new) - -- Pull the YAML blocks out of `pronunciation-rules.md` (fenced ` ```yaml `, split on `---`). Load them into: - - `archiphonemes`, `dialects_enabled`, `optional_realisations_enabled` - - `scoring` (margins, floors) - - `checks` (target, heard, severity, feedback), collected from every rule block - - `heard_labels` -- Pass every symbol through `base()` when loading, so `ʁ` is stored as `ɣ`, `χ` as `x`, and so on. Warn about rules that become **undetectable**: - - target and heard end up the same sound after `base()`; - - a heard symbol is in the same accepted set as its target; - - a symbol is missing from the model vocab. -- Add `pyyaml` to `requirements.txt`. It's already installed through `huggingface_hub`, but it should be listed explicitly. -- Consider moving the rules file to `docs/` as the doc itself suggests. The student sections are served from `static/assets/`, so either keep it there or copy it at build time. -- **Tests:** every block parses; every `heard:` symbol has a label in section 8; the undetectable-rule check fires on a deliberately bad rule. - -## Phase 1: Layer A core, with `Q` only - -### 1a. Converting the phrases at load time, not in the source files - -`app/workbook.py` has ~930 ق and `app/grammar.py` has more, so rewriting the IPA by hand isn't realistic. - -- Add `archify(ar, ipa)` in a new `app/archi.py`. For each word whose Arabic contains ق, replace the matching `ʔ`/`q`/`k` with `Q`. - - Match by consonant order. `word_letters` in `app/translit.py` already lines up ق with its phoneme, so reuse that logic. -- **Ambiguous words** (both ء and ق, or ك next to ق) need an explicit marker written in the phrase. Add a test that fails on any ق word it can't resolve. -- After conversion, merge duplicate variants. Today's separate `q` and `k` variants (e.g. `i l q u d s`) become the same as the `ʔ` one. -- Run it inside `PHRASES` / `workbook_phrases()` so every consumer sees `Q`, including references and tokenisation at startup in `load_scorer`. - -### 1b. `Vocab` classes (`app/scorer.py`, `Vocab`) - -- `Vocab(token_to_id, classes=rules.archiphonemes)` creates one **virtual id** per abstract symbol, numbered after the real vocabulary (V, V+1, …). It also keeps `members[vid]`, the real representative ids of the accepted realisations. -- `merge()` returns `logp` with extra columns: column `vid` = `logaddexp` over its members. -- `resolve("Q")` returns `[vid]`. `i2t[vid] = "Q"`. -- Anything that ranges over "real" symbols has to ignore virtual columns: `greedy_decode`, `_heard_hebrew`, the heatmap's "heard" track, and `edit_distance` for the unrelated check. Easiest is a `vocab.real(logp)` view of `logp[:, :V]`. - -### 1c. GOP (`app/align.py`, `score_tokens`) - -- `score_tokens(..., accept: list[set[int]] | None)`. For a class token, mask all its members from the competitor search, and always mask every virtual column. -- Add `TokenScore.realised`: the member with the highest peak inside the window (`None` for plain tokens). -- The alignment itself doesn't change. It already runs on the virtual column. - -### 1d. Results and display - -- Each phoneme entry gets `realised`. `heard` comes from the competitor as before, so an accepted realisation never shows up as "heard". -- `_heard_hebrew`: when the frame is inside a class token's span and the greedy symbol is one of its members, write the **realised** letter (כּ, א or ק) uncoloured. -- `to_hebrew` / `word_letters`: `Q` is written ק in the answer (today's ق rule already does this). -- `app/tips.py`: add `Q` to `NO_DELETION_TIP` (6.3 #2). Remove the `ʔ>k` qaf tip. - -### 1e. Baseline and calibrations - -- References need no change. `Reference.sounds` now holds `(w, "Q")`, so the baseline is the same whatever realisation the TTS voice used. The cache is in memory and rebuilds on restart. -- Calibration lookup in `score_logp`: try `(archi, heard)` first, then fall back to `(realised target, heard)` so existing DB rows still apply. Add a teacher-panel note or a one-off SQL to re-key `ʔ` rows that came from ق phrases. - -### 1f. Tests (`tests/test_scoring.py`) - -- Rewrite `test_k_for_urban_qaf_picks_closest_variant_and_tips`: `k` for `Q` scores ≥ 90, has no tip, and `realised == "k"`. -- New tests: - - `q` for `Q` is accepted; - - `Q` dropped between vowels is not flagged; - - `ʔ` from ء said as `k` **is** flagged (e.g. اهلا → kahla); - - a reference voiced with `q` doesn't penalise a learner who says `ʔ`; - - `archify` on a sample of workbook phrases, plus the ambiguity test. - -## Phase 2: The other five abstract symbols - -- Extend `archify`: ك→`K`, ث→`TH`, ذ→`DH`, ظ→`ZH`, ج→`J`. -- **Per-word override syntax** for learned words, e.g. `TH{t,s}` (ثقافة) or a plain `t` to lock it. Parse it in `tokenize`, which creates a one-off class for that position. -- **Vocab gaps:** if `θ`, `ð`, `ðˤ` or `tʃ` aren't in the model, the class quietly shrinks. Log it, and expose it in `/api/health` under `rules` (section 7 of the rules doc). -- Add `tʃ` (צ'), `θ` (ת'), `ð` (ד״) to `CONSONANTS` in `app/translit.py` so rural realisations can be written in the heard line. -- Add `θ`, `ð`, `tʃ`, `ðˤ`, `dʒ` to the test vocab `SYMS`. Tests for each class, plus one mixed-dialect sentence that isn't penalised. - -## Phase 3: Positional classes and diphthongs - -- **`E`** for a final ة/ه vowel → {a, e, i}. Derived automatically from the Arabic ending (`word_letters` already detects it). -- **`O`** for the vowel of a pronoun ending → {o, u}. This one needs an explicit marker in the phrase, because it can't be told reliably from the Arabic. -- **`EY` / `OW`**: expand into extra **variants** (`eː` | `a j`, `oː` | `a w`) inside `archify`, before variants are merged. `_fit` already picks the best variant. These can't be a per-token class, because one token stands against two. -- `near_vowels` stays for other positions until the vowel-length decision is made (see "Open decisions"). -- Sounds that natives also drop (ʔ at the start or between vowels, il- after a vowel) are **not** Layer A: an optional token would need a separate alignment with and without it. They stay as Layer B exemptions. - -## Phase 4: Layer B, the rule engine - -### 4a. Competitor set - -- Limit competitors to the Arabic sounds plus the Hebrew-interference sounds (every `heard:` symbol in `checks`). Report any other winner as `competitor=None, other=True` ("not clear"). -- **Watch out:** this lowers `comp_peak` in the deletion test (`deleted_comp`). Re-check the deletion tests. - -### 4b. Flagging by margin - -- In `score_logp`, flag a sound when `score < flag_below` **and** the competitor is outside the target's accepted set **and** `−gop ≥ margin(target, heard)`. `gop` is already `log P(target) − log P(competitor)`. -- The issues list keeps its threshold (`issue_below`) but also respects the margin. - -### 4c. Feedback - -- `tip_for` looks up `checks[(target, heard)]` first, then falls back to `TIPS`. Keep the key format `expected>heard` so the front end doesn't change. -- Sort tips by severity, then by score. `max_tips` comes from the rules file. - -### 4d. Exemptions (6.3) - -- Move `NO_DELETION_TIP`, `elided_article` and `near_vowels` into a predicate registry in `rules.py`: `(name, applies(toks, i, ar_words, sc) -> bool, action)`. -- Add the doc's cases 1–3, 4 (il- not assimilated) and 12 (`mixed_dialect` as info only). - -### 4e. Letters that can only be heard, never expected - -- A `HEARD_ONLY` table in `translit.py`: `p`→פּ, `v`→ב/ו, `ts`→צ. Include it in `HEBREW_SOUNDS` so `_heard_hebrew` stops dropping these sounds. Today a student who says *pinjaan* sees the first letter missing instead of פּ. -- `sound_name` / `letter_reason` labels come from `heard_labels`. -- **Tests:** `f→p` (פִנְג'אן), `w→v`, `b→v`, `sˤ→ts`, `r→ʁ`. Emphatic just under the margin is not flagged, just over it is. - -## Phase 5: Teacher-facing parts - -- `dialects_enabled` / `optional_realisations_enabled` as teacher settings, stored wherever teacher settings live today. Pass them into `Scorer.score` per request, so different classes can differ. -- The taught dialect only changes hints and info, never what's accepted. -- Teacher panel: show `realised` and `mixed_dialect` in the diagnostics view. Show abstract symbols by their Hebrew letter, never as "Q". -- `/api/health`: a `rules` field listing each rule and whether it can be detected. - -## Phase 6: Rollout check - -- If recordings are stored, add a `scripts/rescore.py` that compares old and new scores per phrase. Review the biggest changes before switching over, especially ק phrases and emphatics. -- Ship Phase 1 on its own: it fixes ק/א and tests the class mechanism end to end. - -## Open decisions (outside this plan) - -1. **Vowel length:** the doc's `vowel_length` checks (severity 1, margin 1.1) vs. today's `near_vowel_floor`, which forgives length completely. -2. **Sentence formula:** the doc's `mean(word_min)` and severity weights vs. today's 0.5/0.5 blend. Keep it behind a config flag until decided. -3. **Bedouin `g` for `Q`:** on or off by default. -4. **Extra sounds:** inserted sounds aren't detected, since forced alignment only scores expected sounds. - -## Main risks - -- The virtual columns change the `logp` width. Anything that indexes `logp` by a real id is fine, but anything that iterates over all columns (heatmap, greedy) must use the real-only view. -- `archify` mismatches in the workbook. That's why the ambiguity test fails the build instead of guessing. -- Old calibration rows keyed on `ʔ` until they're re-keyed. That's why the lookup falls back to the old key. - -## Implementation notes - -Code: `app/rules.py` (loader, detectability, exemptions), `app/archi.py` (symbols, letter alignment, diphthong variants), `app/phones.py` (`base()`, moved out of the scorer), `app/scorer.py` / `app/align.py` (classes, GOP, margins, tips), `scripts/rescore.py`. Tests: `tests/test_rules.py` and the "dialects and Hebrew-speaker rules" section of `tests/test_scoring.py`. - -Where it differs from the plan above: - -- **`archify` runs inside the scorer, not on `PHRASES`.** `Scorer._variants` converts each variant when scoring (cached). Display, TTS, transliteration and the verb game keep the plain IPA. Symbols replace phonemes one for one, so positions in the IPA still line up. -- **Token format** is `SYMBOL:written` (`Q:ʔ`), or `SYMBOL{a,b}` for a hand-written set. A phrase that writes a symbol itself is left alone. -- **Ambiguity:** across the bank (~11.6k variants) no ق is unresolved. 5 × ث merged with ت into one geminate (بعثت) and 1 × ج (جوز) stay plain phonemes; only an unresolved ق fails the test. -- **Learned-only realisations** (`TH` as s, `DH` as z) are accepted where the phrase writes them, with no override syntax needed. -- **Missing model symbols:** a class member the model lacks uses the same `FALLBACKS` stand-in as a plain phoneme. With the base model, `ZH` is `{dˤ, z}`, so "ظ heard as z" shows as `accepted` (undetectable) in `/api/health` → `rules`. -- **Competitor naming (4a)** keeps the GOP against the strongest symbol of any kind, so scores and deletions don't change. Only the *name* is withheld (`other`) when that symbol isn't Arabic or a Hebrew-interference sound. -- **Tips:** `max_tips` is now 3 (from the rules file), sorted by severity then score. Every tip carries `severity`. -- **Results** gain `phonemes[].sound` (`Q`), `phonemes[].realised` (`k`), and `dialect: urban|rural` or `mixed_dialect: true`. None of them affect the score. -- **Teacher view:** the expected/heard phone rows compare an accepted realisation with what was said, and show the dialect. - -Not done: - -- **Per-teacher dialect settings (Phase 5).** The accepted set comes from the rules file for everyone (`dialects_enabled`, `optional_realisations_enabled`). Since the taught dialect was only meant to change hints, nothing in scoring depends on it yet. -- **Re-keying old calibration rows.** The lookup falls back to the written phoneme, so old rows keep working. Rows proposed from the page are still keyed on `phonemes[].ipa`. -- **The open decisions** above (vowel length, sentence formula, Bedouin g, insertions) are unchanged. Vowel-length rules show as undetectable in `/api/health`. diff --git a/docs/trace-schema.ts b/docs/trace-schema.ts deleted file mode 100644 index f789890723b0520d8c2289315dbb380a8e4e1fce..0000000000000000000000000000000000000000 --- a/docs/trace-schema.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * The scorer's result for one recording, with the teacher's diagnostic trace: the data of the trace dashboard - * (static/trace.html). Produced by Scorer.score(..., diagnostics=True) in app/scorer.py; POST /api/score returns it - * to a teacher, and POST /api/lab/score to the lab. A mock from the real pipeline: scripts/mock_trace.py writes - * static/assets/trace-mock.json. - * - * Units: scores are 0..100; probabilities 0..1 (posteriors of the wav2vec2 CTC model); `logp` and `gop` are natural - * logs; frames are the model's (~20 ms each, `TraceFrames.ms`), [start, end) half-open. - * IPA symbols are the phrase bank's. `sound` is the archi.py symbol of a letter with several correct realisations - * (Q for ق: ʔ / k / q; K, TH, DH, ZH, J, E, O), and equals `ipa` for any other sound. - * This file is documentation: nothing compiles it. - */ - -export type Ipa = string; -/** "_" is "nothing heard" (a deletion). */ -export type Heard = Ipa | "_"; - -/** The whole result of one recording. */ -export interface ScoreResult { - score: number; // the sentence's score (TraceSentence.final) - variant: { ar: string; tr: string; ipa: string }; // the accepted reading the audio fit best - heard_ipa: string; // greedy decode: what the model heard, free of the expected text - heard_tr: string; // the same in Hebrew letters, cut at the aligned word starts - unrelated: boolean; // a different sentence (TraceUnrelated): score capped, no tips - words: Word[]; - tips: Tip[]; // the most crucial mistakes, severity first (at most config.max_tips) - issues: Issue[]; // the rest of the imprecise sounds - references: number; // native TTS baselines used - dialect?: "urban" | "rural"; // what the accepted realisations point to (never part of the score) - mixed_dialect?: boolean; - calibrated?: { expected: Ipa; heard: Heard; from: number; to: number }[]; // teacher floors that applied - phoneme_heatmap?: Heatmap; - trace?: Trace; // teachers only - phrase?: { he: string; ar: string }; // the mock only: what was said -} - -export interface Word { - ar: string; - tr: string; // Hebrew transliteration of the expected word - heard_tr: string; - score: number; // word_min_weight · min + (1 − word_min_weight) · mean of its phonemes - letters: { t: string; score: number; why?: string; tip?: string }[]; // the transliteration letter by letter - phonemes: Phoneme[]; -} - -export interface Phoneme { - ipa: Ipa; - word: number; - score: number; - flagged: boolean; // under flag_below and beyond the pair's margin: a mistake - heard: Heard | null; // set when flagged - sound?: string; // archi symbol, when not ipa - realised?: Ipa; // a class sound: the realisation the model heard (ʔ for Q) - inserted?: Ipa; // a vowel heard before this consonant (rule 3.10) -} - -export interface Tip { title: string; text: string; key: string; severity: 1 | 2 | 3; word: string; word_tr: string; score: number } -export interface Issue { sound: string; word_tr: string; score: number; missing: boolean; heard: string | null } - -export interface Heatmap { - duration_ms: number | null; - frames: number; - frame_bins: number; // ≤ 180: each bin is the max over its frames - rows: { phoneme: string; expected: boolean; values: number[] }[]; // values 0..100, one per bin - tracks: { expected: Span[]; heard: Span[] }; -} -export interface Span { phoneme: Ipa; start: number; end: number } - -// ---------------------------------------------------------------- the trace - -export interface Trace { - config: TraceConfig; - /** Stage 0, Viterbi: every accepted reading (and its diphthong forms) the audio was aligned to. */ - variants: TraceVariant[]; - frames: TraceFrames; - aipa: string; // the chosen reading with archi symbols - unrelated: TraceUnrelated; - /** The native baselines, one per TTS voice; each sound is measured against the lowest of them (Stage "baseline"). */ - references: { voice: string; mean: number; sounds: number }[]; - /** Stages 1–5 per sound, in order: gop → exemption → insertion → baseline → calibration → final. */ - tokens: TraceToken[]; - words: TraceWord[]; - sentence: TraceSentence; - tips: { key: string; severity: 1 | 2 | 3; score: number }[]; -} - -export interface TraceConfig { - leniency: number; // gop_factor = exp(gop · leniency) - confident_peak: number; // confidence = min(1, p_target / confident_peak) - consonant_window: number | null; // max context frames on either side of a consonant span - deleted_peak: number; // deleted: p_target < deleted_peak and p_competitor < deleted_comp - deleted_comp: number; - flag_below: number; // a sound under this (and beyond its margin) is flagged - flag_below_sound: Record; // per sound, in place of flag_below (set per model) - issue_below: number; // a sound under this is listed as imprecise - near_vowel_floor: number; - word_min_weight: number; - vowel_weight: number; // a vowel's weight in the sentence's phone mean - unrelated_ratio: number; - unrelated_cap: number; - letter_good: number; - max_tips: number; - max_issues: number; - ref_min_mean: number; - ref_noise_floor: number; // a reference under this on a sound: the model cannot measure it, the score is given back - margin_default: number; - ayin_deleted_floor: number; - margins: { target: Ipa; heard: Heard; margin: number }[]; // the pairs that need a clearer win (rules 6.2) - exemptions: ("glottal_dropped" | "article_vowel_elided" | "near_vowel" | "unmarked_short_vowel" | "final_b_devoicing" | "word_final_vowel_linking")[]; - insertion: { score_cap: number; min_peak: number; min_frames: number } | null; - disabled: string[]; // rule switches the teacher turned off -} - -export interface TraceVariant { - ar: string; - ipa: string; - logp: number | null; // the CTC path's log-prob (null: the audio is too short for this reading) - chosen: boolean; // the highest logp wins - tokens: string[]; // the path's tokens (archi symbols), in order -} - -export interface TraceFrames { - count: number; // T - ms: number; // per frame - energy: number[] | null; // RMS per frame, 0..1 of the loudest (null when scored without the audio) -} - -export interface TraceUnrelated { - heard: string; // greedy decode - distance: number; // edit distance from the chosen reading (class members count as equal) - tokens: number; - ratio: number; // distance / tokens - threshold: number; // over this: a different sentence - unrelated: boolean; -} - -export type StageName = "gop" | "exemption" | "insertion" | "baseline" | "calibration"; -export type Stage = - | { stage: "gop"; score: number } - | { stage: "exemption"; score: number; applied: TraceConfig["exemptions"][number] | null } - | { stage: "insertion"; score: number; applied: Ipa | null } // the vowel heard inside the cluster - | { stage: "baseline"; score: number; ref: number | null; refs: (number | null)[]; applied: boolean; - heard?: (string | null)[]; refused?: "protected" | "kept" | null } - // heard: per voice, what took the sound's place in it ("_" nothing, null kept or no such sound); - // refused: why no voice counted although some have the sound. "protected": the learner's substitution is a pair the - // rules protect (ħ heard as x); "kept": every voice that has the sound kept it, while the learner's was replaced. - // ref: the lowest native score on this sound (what it is measured against); refs: each voice's, in - // Trace.references order (null: that voice's reading has no such sound) - | { stage: "calibration"; score: number; floor: number | null; applied: boolean }; - -export interface TraceToken { - i: number; - word: number; - ipa: Ipa; - sound: string; - class: Ipa[] | null; // a class sound's accepted realisations - realised: Ipa | null; - span: [number, number]; // frames the CTC path emits it in - window: [number, number]; // frames it was read from: the end of the sound before → the start of the one after - curves: TraceCurves; - p_target: number; // peak posterior of the sound in its window - competitor: Ipa | null; // strongest other sound, neighbours and blank excluded (null: none, or unnamed) - competitor_other: boolean; // the strongest is a sound outside Arabic and Hebrew - p_competitor: number; // its peak posterior in the window - competitor_frames: number; // frames where it clears deleted_comp; one frame may be an onset transient - gop: number; // ln(p_target / max(p_target, p_competitor)) ≤ 0 - confidence: number; - gop_factor: number; - confidence_factor: number; // score = 100 · gop_factor · confidence_factor - deleted: boolean; - stages: Stage[]; - final: number; - margin: { needed: number; got: number; beyond: boolean }; // the competitor's win against the pair's margin - vowel: boolean; - weight: number; - flagged: boolean; - rule: { id: string; title: string; severity: 1 | 2 | 3 } | null; // pronunciation-rules.md - tip: boolean; - issue: boolean; -} - -/** Posteriors frame by frame around a sound: frames curves.start .. start + length, its window ± 4. */ -export interface TraceCurves { - start: number; - target: number[]; - competitor: number[] | null; - prev: number[] | null; // the sound before (masked: never a competitor) - next: number[] | null; // the sound after (masked too) - blank: number[]; -} - -export interface TraceWord { tr: string; scores: number[]; min: number; mean: number; score: number } - -export interface TraceSentence { - phone_mean: number; // weighted: vowels count vowel_weight - word_mean: number; - phone_weight: number; - word_weight: number; - before_cap: number; // phone_weight · phone_mean + word_weight · word_mean - final: number; - capped: boolean; // unrelated: capped at unrelated_cap -} diff --git a/docs/zero-shot-phone-models.md b/docs/zero-shot-phone-models.md deleted file mode 100644 index 282cd38155a59cd50b108f02806b2ca720de8cf9..0000000000000000000000000000000000000000 --- a/docs/zero-shot-phone-models.md +++ /dev/null @@ -1,111 +0,0 @@ -# Zero-shot phone models on Palestinian Arabic - -Can a newer phone recognizer replace `facebook/wav2vec2-xlsr-53-espeak-cv-ft` as the base model? Every model was run -as-is (no training on our data) on the held-out Minerva lines and compared with the current base and the current -fine-tune (`checkpoints/ihki-ft-minerva`). Measured 2026-10-25 with -[scripts/zero_shot_phone_models.py](../scripts/zero_shot_phone_models.py). Raw results, including every line's -transcript, are saved locally in `data/minerva/bench/zero-shot-{dev,test}.json`. - -## Verdict - -- **Nothing replaces the fine-tune as-is.** It has the lowest phoneme error rate (23% on test) and the best long-vowel - recall, and it already fits the app. -- **ZIPA isn't worth pursuing.** It's no better than the current base overall, and it never hears ħ, ʕ or the - emphatics. -- **POWSM-CTC with the Arabic prompt is the strongest starting point for fine-tuning.** Untrained on our data, it hears - ħ (87%) and ʕ (85%) about as well as the fine-tune. It's also the only model that hears the emphatics: tˤ 66%, - sˤ 72%, dˤ 54%. The current vocab has no symbol for tˤ or sˤ, and the fine-tune gets dˤ only 4% of the time. - Its weak points: - - It never writes a long vowel: `aː` came out as `a` 235 times out of 268, though its vocab has `aː`. - - It runs at 25 frames/s, half our 50. - - It needs a new loader, because it runs on ESPnet rather than Hugging Face `transformers`. - -**Next step:** fine-tune POWSM-CTC with the Arabic prompt on `data/minerva` instead of w2v-BERT 2.0. Training on -labels that mark length should teach it the long vowels it currently never writes. Check what 25 frames/s does to the -vowel-length check before moving it into the app. - -## Results: test split (255 lines) - -| Model | PER | Consonant PER | ħ | ʕ | tˤ | sˤ | dˤ | ʔ | Long vowels | Vowel length | Frames/s | s/clip | Fits app? | -|---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| xlsr-53-espeak (current base) | 38% | 29% | 9% | 2% | no symbol | no symbol | 0% | 3% | 45% | 85% | 50 | 0.05 | yes | -| **ihki-ft-minerva (current fine-tune)** | **23%** | **15%** | **94%** | 69% | no symbol | no symbol | 4% | **72%** | **74%** | **91%** | 50 | 0.05 | yes | -| ZIPA large CR-CTC | 38% | 29% | 0% | 0% | 0% | 0% | 0% | 5% | 39% | 86% | 49 | 0.32 | new loader (ONNX) | -| POWSM-CTC, no language | 43% | 32% | 6% | 1% | 0% | 0% | 0% | 0% | 0% | 81% | 25 | 0.29 | new loader (ESPnet) | -| **POWSM-CTC, Arabic prompt** | 37% | 22% | 87% | **85%** | **66%** | **72%** | **54%** | 62% | 0% | 79% | 25 | 0.29 | new loader (ESPnet) | - -The dev split (192 lines) gives the same ranking: fine-tune 26%, POWSM with the Arabic prompt 40%, ZIPA 39%, -base 41%, POWSM without a language 46%. The fine-tune picked its best epoch on dev, so its dev number is slightly -optimistic. That's why test is the headline. - -What each column means: - -- **PER**: phoneme error rate, i.e. edits needed to turn the model's output into the label, divided by the label's - length. -- **Consonant PER**: the same with every vowel removed from both. These are the sounds the scorer grades hardest. -- **ħ … ʔ, Long vowels**: how often a labelled sound was heard as itself. "No symbol" means the model's vocab can't - write it. Long vowels covers `aː iː uː eː oː` together. -- **Vowel length**: of the labelled vowels the model heard as some vowel, how often it got long vs short right. -- **Frames/s**: CTC output rate. The long-vowel check measures vowel length in frames, so fewer frames means coarser - timing. -- **s/clip**: wall time on the laptop (RTX 1000 Ada, 6 GB). ZIPA runs on CPU (see Setup). - -What each sound was heard as instead (test): - -| Model | ħ (127) | ʕ (209) | tˤ (62) | aː (268) | -|---|---|---|---|---| -| Current base | h 79, nothing 15, x 8 | nothing 133, aː 25 | t 46 | a 70, nothing 32 | -| Fine-tune | x 5, ʃ 2 | nothing 31, ʔ 6 | t 58 (no symbol) | a 49 | -| ZIPA | h 89, nothing 19 | nothing 128, aː 22 | t 47 | a 72, nothing 28 | -| POWSM, no language | h 88, nothing 10 | nothing 135, a 22 | t 47 | a 127, e 68 | -| POWSM, Arabic prompt | x 6, a 5 | nothing 14, a 8 | t 14 | a 235 | - -With the Arabic prompt POWSM also leans toward Standard Arabic: it adds case endings and some MSA forms. For example, -it wrote *bsēder* as *sadari…* and *madrase* as *madrasi(ja)*. Fine-tuning on Palestinian labels should fix that, -but it's worth watching. - -## Method - -- **Models:** - - [anyspeech/zipa-large-crctc-500k](https://huggingface.co/anyspeech/zipa-large-crctc-500k): the official ONNX - export. - - [espnet/powsm_ctc](https://huggingface.co/espnet/powsm_ctc): run with the phone-recognition task token ``, - either with no language (``) or with ``, and padded to 20 s as its model card says. -- **Decoding:** greedy CTC for every model, on audio trimmed with the app's own `decode_audio`. -- **Mapping onto our labels** (`canon()` in the script): - - Length marks join their vowel, and long consonants count as geminates. - - Details the labels never write are dropped (dental `t̪`, aspiration). - - Symbols with no contrast in Palestinian are merged: χ→x, ʁ→ɣ, ɾ→r, ɡ→g. - - Vowel qualities fold to `a e i o u` (ɛ→e, ɪ→i, ə→e, ʊ→u). - - Geminates collapse to one sound on both sides, as the scorer's `tokenize()` does. -- **Fairness caveat:** the current base and the fine-tune have no symbols for tˤ, sˤ or ðˤ. Training mapped them to - t, s and z. Their PER counts those as errors, which costs the fine-tune a few points. The per-sound columns show it - directly. - -## Setup notes - -ZIPA and POWSM need packages that conflict with the app's environment. They install into a git-ignored -`.zero-shot-deps/` folder (`scripts/setup_zero_shot_deps.sh`, pinned in `requirements-zero-shot.txt`), and only this -script uses it. - -POWSM-CTC doesn't run on the newest ESPnet release for Python 3.10 (202511) as published. The script patches that -release in memory, with two fixes: - -1. The encoder passes the language/task prompt to its subsampling layer, which doesn't accept it in that release. The - patch copies ESPnet master's logic. -2. The checkpoint names that layer's weights differently from what the release expects, so the release silently leaves - the layer randomly initialised. Before this fix the model output junk. The script copies those weights in by hand. - -ESPnet also imports `torchaudio`, which has no build for torch 2.14. It's only used for multi-microphone beamforming -code that POWSM never runs, so the setup script installs a placeholder that fails loudly if anything calls it. - -ZIPA runs on CPU, because onnxruntime's GPU build needs CUDA 12 and this machine has CUDA 13. At about 0.3 s per clip -that's fine for evaluation. - -Rerun: - -```bash -scripts/setup_zero_shot_deps.sh # once -python scripts/zero_shot_phone_models.py --split test # all models; or --models powsm-ara,ft -python scripts/zero_shot_phone_models.py --split test --report # just the table -``` diff --git a/ihki.todo b/ihki.todo deleted file mode 100644 index c14453ba0886a5ca903ac8bbf87f5ed93391e250..0000000000000000000000000000000000000000 --- a/ihki.todo +++ /dev/null @@ -1,10 +0,0 @@ -- community -- llm dialoge -- practice better (e.g. least score) -- do i lock the lessons ? -- revise homeworks -- yalla hapoel -- pronuncation -- teacher presentation -- add tutors ? -- penalize on showing answer diff --git a/logo-intro-fastapi/Dockerfile b/logo-intro-fastapi/Dockerfile deleted file mode 100644 index bec0fb9d7e7a6c1aea6006d7c65522bbde432f65..0000000000000000000000000000000000000000 --- a/logo-intro-fastapi/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.11-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -EXPOSE 7860 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/logo-intro-fastapi/README.md b/logo-intro-fastapi/README.md deleted file mode 100644 index f86b4dfe54c03fd3c21ddb0c488d29f6e8609b38..0000000000000000000000000000000000000000 --- a/logo-intro-fastapi/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# SVG logo introduction — FastAPI / HTML - -The ornament is traced from the supplied raster image into SVG centerline paths, animated with stroke-dashoffset. This is real SVG line drawing, not a mask over the image. Raster tracing retains slight pixel irregularities; an original vector logo would improve precision further. Original calligraphy and Hebrew lettering are embedded unchanged as a cropped PNG. - -Timing: ornament 0–2 seconds; lettering 1.55–2.05 seconds; fade 2.65–3.00 seconds. No animation libraries, external requests, or Python processing at runtime. Reduced-motion users skip the splash. The intro does not wait for model loading or block the main interface. - -## Preview -Open index.html locally, or run: - - pip install -r requirements.txt - uvicorn app:app --host 0.0.0.0 --port 7860 - -## Existing FastAPI project -Copy static/logo-intro.svg into your static directory. Copy the #logo-splash CSS, reduced-motion rule, splash div and cleanup script from index.html into your existing page. Keep your existing main interface. Mount /static only if it is not mounted already. Adjust the image URL if the application uses a URL prefix. No changes to your model or backend are required. - -## Hugging Face Docker Space -Use your existing Docker setup and serve FastAPI on 0.0.0.0:7860. The example below can run this standalone demo. The animation starts after the HTML loads; it does not cover a Space cold start before the server responds. diff --git a/logo-intro-fastapi/app.py b/logo-intro-fastapi/app.py deleted file mode 100644 index 781079e5481edd6e9df6629e8a0b3d1ab95f91c6..0000000000000000000000000000000000000000 --- a/logo-intro-fastapi/app.py +++ /dev/null @@ -1,12 +0,0 @@ -from pathlib import Path -from fastapi import FastAPI -from fastapi.responses import FileResponse -from fastapi.staticfiles import StaticFiles - -ROOT = Path(__file__).resolve().parent -app = FastAPI() -app.mount("/static", StaticFiles(directory=ROOT / "static"), name="static") - -@app.get("/") -def index(): - return FileResponse(ROOT / "index.html") diff --git a/logo-intro-fastapi/index.html b/logo-intro-fastapi/index.html deleted file mode 100644 index 3bf7eb700d3c626f4a5311541b01d0a083234630..0000000000000000000000000000000000000000 --- a/logo-intro-fastapi/index.html +++ /dev/null @@ -1,6 +0,0 @@ -פתיחת האפליקציה

הממשק הראשי של האפליקציה

החלף אזור זה בממשק הקיים שלך.

- - \ No newline at end of file diff --git a/logo-intro-fastapi/requirements.txt b/logo-intro-fastapi/requirements.txt deleted file mode 100644 index 97dc7cd8c1fd2f07d8ec79a1117664c5ebaf2842..0000000000000000000000000000000000000000 --- a/logo-intro-fastapi/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi -uvicorn diff --git a/logo-intro-fastapi/static/logo-intro.svg b/logo-intro-fastapi/static/logo-intro.svg deleted file mode 100644 index d4c5d7946068e057ea08bcd2689c4c82104cb1f7..0000000000000000000000000000000000000000 --- a/logo-intro-fastapi/static/logo-intro.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/openspec/changes/add-grok-class-student-management/design.md b/openspec/changes/add-grok-class-student-management/design.md deleted file mode 100644 index 69f7f63b31b64680726406c2ab0bb31e2afc63f4..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-class-student-management/design.md +++ /dev/null @@ -1,75 +0,0 @@ -# Design - -## Context - -See `proposal.md` for motivation and the delta specs for behavior. The deployed OAuth-protected MCP server currently exposes four digital-library tools. Browser class management is implemented inside the FastAPI application and persists whole class rows through `SupabaseAuth.save_class`; subject administrators see only their subject through the existing scoped-class resolver. - -Class rows contain much more than the requested MCP scope: student emails and nicknames sit beside teachers, lessons, homework, scheduled videos, and subject. Any conversational mutation must therefore preserve fields it is not allowed to manage, reject stale prepared changes, and never expose the full management payload. - -## Goals / Non-Goals - -**Goals:** - -- Add a compact MCP surface for visible class names and student rosters. -- Apply the same normalized names, emails, nicknames, uniqueness rules, subject scope, and persistence behavior as browser class management. -- Require explicit confirmation for every class mutation and bind it to the preparing user and captured class state. -- Keep browser endpoints and response shapes unchanged. - -**Non-Goals:** - -- Generalizing MCP into an administrative API. -- Deleting classes or user accounts, creating Auth users, or editing roles, entitlements, teachers, lessons, homework, daily videos, progress, grades, attempts, or recordings. -- Changing the `classes` table schema, browser UI, OAuth setup, or Grok connector URL. - -## Decisions - -### Expose three class tools with an operation allowlist - -Add `list_classes`, `prepare_class_change`, and `publish_class_change` alongside the four library tools. `prepare_class_change` accepts only `create_class`, `rename_class`, `add_student`, or `remove_student`, with fields validated for that operation. A single preparation tool keeps discovery small while its enum and structured result make the authority explicit. - -Separate immediate mutation tools were rejected because each would duplicate confirmation and stale-state behavior. Reusing browser API endpoints internally was rejected because HTTP route dependencies do not provide a stable domain boundary and their broad class-save payload could accidentally widen MCP authority. - -### Extract a shared class-roster service - -Introduce a class-domain module used by both MCP and the existing browser class-save path for normalization, visible-class lookup, duplicate-name checks, email/nickname validation, and whole-row persistence. Narrow service operations construct the new row from a freshly loaded stored class and replace only the permitted fields before calling the existing persistence method. - -The service receives the subject lesson/unit catalog from the application. New subject-admin classes inherit the actor's assigned subject; developers must supply a supported subject. Creation selects the first eligible lesson or unit because the existing schema requires at least one, while subsequent lesson management remains browser-only. - -### Carry verified actor context into MCP tools - -The Supabase token verifier already reloads application access for every MCP HTTP request. Extend its verified access metadata with only the actor id, email, roles, subject, and permissions needed for authorization. MCP class tools read that server-verified context rather than accepting identity or subject from tool arguments. - -At both prepare and publish, the service recomputes visible classes using the current actor. Developers see every class; subject admins see only their assigned subject. A class teacher without `management.access` continues to be rejected by the MCP authentication boundary. - -### Bind approvals to actor and a class-state fingerprint - -Use a dedicated bounded ten-minute approval store for class changes. Each approval contains the operation, normalized proposed values, preparing user id, expiry, and for existing classes a fingerprint of the fields that must be preserved. Publishing consumes the approval atomically, verifies the current OAuth subject matches, reloads authorization and class state, and refuses if scope or fingerprint changed. - -For creation, publication rechecks name uniqueness and subject authorization. Consuming before persistence preserves the existing fail-safe replay behavior: a transient storage failure requires preparation again. - -### Minimize list output and preserve teacher membership - -`list_classes` maps rows to `{id, name, subject, students}`, where each student has only `email` and optional `nickname`. It does not return teachers, lesson ids, homework, scheduled videos, timestamps, user ids, roles, or activity. - -Student removal is refused when the target email appears in the stored class's teacher list. This prevents a roster-only operation from implicitly modifying or invalidating teacher assignment. Adding an email never creates a Supabase Auth account and does not inspect whether one already exists. - -### Reuse throttling and record actor-aware audits - -Class preparation and publication share the existing MCP mutation limiter. Successful and refused authenticated actions use the existing best-effort content/system event mechanism with the verified actor and safe metadata: operation, class id/name, target email, and outcome. Tokens, full rosters, and nicknames are not logged. - -## Risks / Trade-offs - -- **A conversational mistake could remove the wrong student** → show exact class, email, and operation in a short-lived confirmation and require a second call. -- **Whole-row persistence could overwrite concurrent browser edits** → fingerprint preserved fields and refuse publication whenever the row changed after preparation. -- **Email addresses are personal data** → return them only to currently authorized managers for classes in scope and omit every other learner attribute. -- **A new class needs a lesson even though MCP cannot manage lessons** → choose the subject's first eligible item deterministically and direct later curriculum editing to the browser UI. -- **An administrator's subject can change after consent or preparation** → reload access on every HTTP request and re-evaluate scope at publication. -- **More MCP tools increase prompt-injection impact** → retain an explicit allowlist, approvals, rate limits, no deletion, and no arbitrary class payload. - -## Migration Plan - -1. Add shared class-roster helpers and refactor the browser class path to use them without changing its API behavior. -2. Add the three scoped MCP tools, actor-bound approvals, auditing, and focused tests. -3. Run MCP, class-management, privacy, and full regression tests; manually confirm the existing Hebrew management UI still works by touch and keyboard. -4. Deploy to the existing Hugging Face Space. No Supabase schema, OAuth, secret, or Grok connector reconfiguration is required; Grok discovers the additional tools automatically. -5. Roll back by reverting the deployment; existing classes and the browser management interface remain compatible because storage is unchanged. diff --git a/openspec/changes/add-grok-class-student-management/proposal.md b/openspec/changes/add-grok-class-student-management/proposal.md deleted file mode 100644 index 7e351eebbc3bea3f2bed50036dc08b6ac5d49382..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-class-student-management/proposal.md +++ /dev/null @@ -1,33 +0,0 @@ -# Proposal - -## Why - -The teacher who already manages the digital library through Grok also needs a simple conversational way to maintain class names and student membership. Adding a deliberately small class-roster toolset avoids requiring the teacher to navigate the full management screen while preserving the application's existing authorization and subject boundaries. - -## What Changes - -- Add MCP tools to list the classes visible to the authenticated manager, including enrolled student email addresses and nicknames. -- Add a tool to create a class with a name, subject-appropriate initial lesson, and optional initial student roster. -- Add tools to rename a visible class and add or remove one student email and optional nickname at a time. -- Require an explicit, short-lived confirmation before create, rename, add-student, or remove-student mutations; listing remains read-only. -- Reuse the existing class validation, persistence, subject scoping, mutation throttling, and audit paths rather than granting direct Supabase access. -- Explicitly exclude class deletion, account creation/deletion, role or entitlement changes, teacher assignment, lessons, homework, daily-video scheduling, grades, attempts, recordings, and other learner data. - -## Capabilities - -### New Capabilities - -- `external-class-management`: Narrow conversational management of class names and student rosters through authenticated MCP tools. - -### Modified Capabilities - -- `teacher-content`: Extend class configuration to the restricted external class-roster workflow while preserving browser behavior. -- `access-and-privacy`: Apply current management permission and administrative subject scope to every external class operation and limit returned student data to roster metadata. - -## Impact - -- Affected code: MCP tool registration, shared class-domain validation/persistence helpers, management API reuse, safe audit events, and focused tests. -- APIs: the existing OAuth-protected `/mcp/` endpoint advertises additional class-roster tools; existing browser APIs remain compatible. -- Persistence: existing rows in the Supabase `classes` table are updated; no schema migration or new student account is created. -- Privacy and authorization: only student email addresses and optional class nicknames already present in visible class rosters are returned; no progress, grades, recordings, roles, or cross-subject classes are exposed. -- Paid APIs: no xAI API calls are added to ihki; Grok connector availability and billing remain external. diff --git a/openspec/changes/add-grok-class-student-management/specs/access-and-privacy/spec.md b/openspec/changes/add-grok-class-student-management/specs/access-and-privacy/spec.md deleted file mode 100644 index f0284b88dcfadcbce93cbfbbc69a3e33c5f12379..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-class-student-management/specs/access-and-privacy/spec.md +++ /dev/null @@ -1,27 +0,0 @@ -# Spec Delta - -## MODIFIED Requirements - -### Requirement: Administrative scope -Developers SHALL have full cross-subject access, subject admins SHALL manage only their assigned subject, and class teachers SHALL manage only their assigned classes; external class operations SHALL resolve and enforce that current scope again at preparation and publication time. - -#### Scenario: Subject admin targets another subject -- **WHEN** a subject admin attempts to change a class or user belonging to another subject -- **THEN** the service refuses the change - -#### Scenario: External approval outlives scope -- **WHEN** a manager's permission or subject scope changes after preparing an external class mutation -- **THEN** publishing that mutation is refused - -## ADDED Requirements - -### Requirement: External roster data minimization -External class tools SHALL return only the identifiers, class names, subjects, student email addresses, and class-local nicknames required for the permitted roster operations. - -#### Scenario: Integration lists student membership -- **WHEN** an authorized integration lists a visible class roster -- **THEN** the response omits account identifiers, roles, entitlements, activity, progress, grades, attempts, recordings, and authentication metadata - -#### Scenario: OAuth user loses management permission -- **WHEN** an OAuth user no longer has `management.access` -- **THEN** the next class discovery or mutation request is refused diff --git a/openspec/changes/add-grok-class-student-management/specs/external-class-management/spec.md b/openspec/changes/add-grok-class-student-management/specs/external-class-management/spec.md deleted file mode 100644 index 2781ff6bb0e817f0454d55876d2788c0ef0df247..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-class-student-management/specs/external-class-management/spec.md +++ /dev/null @@ -1,91 +0,0 @@ -# Spec Delta - -## Purpose - -Define a narrowly scoped conversational workflow for maintaining visible class names and student rosters without exposing broader learner or administrative data. - -## ADDED Requirements - -### Requirement: Scoped class roster listing -The MCP integration SHALL list only classes within the authenticated manager's current administrative scope and SHALL return only each class identifier, name, subject, student email addresses, and optional nicknames. - -#### Scenario: Subject administrator lists classes -- **WHEN** a subject administrator asks the integration to list classes -- **THEN** only classes assigned to that administrator's subject are returned -- **AND** no grades, attempts, recordings, roles, entitlements, homework, or scheduled content are returned - -#### Scenario: Developer lists classes -- **WHEN** a developer asks the integration to list classes -- **THEN** classes from every subject are returned with roster metadata only - -### Requirement: Prepared class mutations -The integration SHALL require a short-lived, single-use approval that displays the exact proposed effect before creating or renaming a class or adding or removing a student. - -#### Scenario: Manager confirms a prepared roster change -- **WHEN** the manager confirms an unexpired approval containing the class, student email, nickname, and operation -- **THEN** the integration applies that change once -- **AND** reusing the approval is refused - -#### Scenario: Manager does not confirm -- **WHEN** a mutation is prepared but not confirmed before its expiry -- **THEN** no class or roster data is changed - -### Requirement: Restricted class creation -The integration SHALL create a class only within the caller's administrative scope, with a validated unique name, the appropriate subject, the subject's first eligible lesson or unit, and an optional validated student roster. - -#### Scenario: Subject administrator creates a class -- **WHEN** a subject administrator confirms a valid new class proposal -- **THEN** the class is created in that administrator's assigned subject with its initial eligible lesson or unit - -#### Scenario: Developer creates a class without a valid subject -- **WHEN** a developer proposes a class without one of the application's supported subjects -- **THEN** the proposal is refused and no class is created - -#### Scenario: Class name already exists in scope -- **WHEN** the proposed name matches an existing visible class after case and whitespace normalization -- **THEN** the proposal is refused and no class is created - -### Requirement: Restricted class rename -The integration SHALL rename a visible class without changing its subject, lessons, teachers, homework, scheduled videos, or roster. - -#### Scenario: Manager renames a visible class -- **WHEN** the manager confirms a valid unique name for a class within scope -- **THEN** only that class's name changes - -#### Scenario: Manager targets a class outside scope -- **WHEN** the manager prepares or publishes a rename for a class outside current administrative scope -- **THEN** the operation is refused without revealing the class - -### Requirement: Restricted student membership changes -The integration SHALL add or remove one normalized student email at a time in a visible class while preserving every unrelated class field and SHALL treat the optional nickname as class-local metadata. - -#### Scenario: Manager adds a student -- **WHEN** the manager confirms a valid student email and optional nickname for a visible class -- **THEN** the email is added once to that class and the nickname is stored only for that membership -- **AND** no account, role, entitlement, or teacher assignment is created - -#### Scenario: Manager removes a student -- **WHEN** the manager confirms removal of an enrolled student who is not assigned as a class teacher -- **THEN** the email and its class-local nickname are removed from that class only - -#### Scenario: Manager removes a class teacher through the student tool -- **WHEN** the target email is assigned as a teacher of that class -- **THEN** removal is refused and the teacher assignment remains unchanged - -### Requirement: Class management failure safety -The integration SHALL reject invalid input, stale state, unavailable persistence, expired approvals, and rate-limited mutations without claiming success or partially changing a class. - -#### Scenario: Supabase class storage is unavailable -- **WHEN** a class tool requires storage while Supabase is unavailable or the classes table is missing -- **THEN** the tool reports that class storage is unavailable and performs no mutation - -#### Scenario: Class changes after preparation -- **WHEN** the target class or roster no longer matches the state captured by an approval -- **THEN** publication is refused and the manager must prepare the change again - -### Requirement: No broader learner administration -The class-roster integration SHALL expose no operation for deleting classes or accounts, changing roles or entitlements, assigning teachers, or reading learner activity, grades, attempts, recordings, homework, or scheduled content. - -#### Scenario: Client discovers class tools -- **WHEN** an authenticated MCP client lists available tools -- **THEN** every class tool is limited to listing, preparing, and confirming the class-name and student-membership operations defined here diff --git a/openspec/changes/add-grok-class-student-management/specs/teacher-content/spec.md b/openspec/changes/add-grok-class-student-management/specs/teacher-content/spec.md deleted file mode 100644 index a09fabc66d970fabf797a4908e76a51411180d03..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-class-student-management/specs/teacher-content/spec.md +++ /dev/null @@ -1,16 +0,0 @@ -# Spec Delta - -## MODIFIED Requirements - -### Requirement: Class configuration -Authorized staff SHALL be able to manage a class's students, nicknames, teachers, lessons, subject, homeworks, and scheduled daily videos through the management interface, while an authorized external integration SHALL be limited to class creation, class renaming, and individual student membership changes that preserve all other class settings. - -#### Scenario: Class configuration changes -- **WHEN** an authorized staff member saves valid class settings -- **THEN** the settings are available to class members on their next page load -- **AND** duplicate homework topics and invalid subjects are rejected - -#### Scenario: External integration changes a roster -- **WHEN** an authorized external integration confirms a valid student addition or removal -- **THEN** the class roster changes under the same email and nickname validation rules as the management interface -- **AND** teachers, lessons, subject, homeworks, and scheduled daily videos remain unchanged diff --git a/openspec/changes/add-grok-class-student-management/tasks.md b/openspec/changes/add-grok-class-student-management/tasks.md deleted file mode 100644 index af76536aac5431a01e3b8b48e3e23cf938cac278..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-class-student-management/tasks.md +++ /dev/null @@ -1,25 +0,0 @@ -# Tasks - -## 1. Shared scoped class service - -- [x] 1.1 Extract reusable class-name, roster, visible-scope, and safe whole-row persistence helpers from the browser management path; verify focused API tests preserve existing create/edit validation, subject scoping, and response behavior. -- [x] 1.2 Implement narrow create, rename, add-student, and remove-student service operations that preserve teachers, lessons, homework, daily videos, subject, and unrelated roster entries; verify unit tests cover normalization, duplicates, teacher-removal refusal, unknown/out-of-scope classes, missing storage, and unchanged protected fields. - -## 2. OAuth actor context and approvals - -- [x] 2.1 Carry the verified OAuth actor id, email, roles, subject, and permissions into MCP request context without accepting actor data from tool arguments or logging tokens; verify tests cover administrators, developers, students, revoked permission, and changed subject scope. -- [x] 2.2 Add a bounded ten-minute, actor-bound, single-use class-change approval store with preserved-state fingerprints; verify deterministic tests cover expiry, replay, wrong actor, concurrent row changes, rate limits, and persistence failure. - -## 3. Restricted MCP class tools - -- [x] 3.1 Add `list_classes`, `prepare_class_change`, and `publish_class_change` with an explicit operation enum and minimized structured results; verify MCP discovery exposes exactly the four library tools plus these three tools and no deletion, role, teacher, lesson, homework, activity, grade, attempt, recording, SQL, or general-user operation. -- [x] 3.2 Verify end-to-end tool calls for subject-scoped and developer listing, class creation, rename, add student, and remove student; assert outputs contain only permitted roster metadata, mutations require confirmation, protected class fields remain unchanged, and safe actor-aware audit events contain no token, full roster, or nickname. - -## 4. Documentation and integration verification - -- [x] 4.1 Document the additional Grok prompts, confirmation flow, privacy boundary, and browser-only operations in the README and teacher guide; verify examples match the runtime tool schemas and Hebrew/RTL text renders without layout changes. -- [ ] 4.2 Run focused MCP, OAuth, class-management, and privacy tests followed by the full suite; manually verify the existing Hebrew management screen can still create and edit a class with touch and keyboard, then verify the deployed connector discovers and exercises all three new tools without OAuth reconfiguration. - -## Workflow follow-up - -- Archive the change after production verification and sync its class-management and privacy deltas into the main specifications. diff --git a/openspec/changes/add-grok-library-mcp/design.md b/openspec/changes/add-grok-library-mcp/design.md deleted file mode 100644 index c9fa37969787fac9941f9df134db3c06521e64ec..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-library-mcp/design.md +++ /dev/null @@ -1,95 +0,0 @@ -# Design - -## Context - -See `proposal.md` for motivation. The production service is one FastAPI process on a Docker Hugging Face Space. Library writes already pass through `SupabaseAuth` using a server-only secret, while browser management routes authorize Supabase user sessions. Grok needs a standard remote MCP endpoint and must not receive either credential. - -The MCP protocol and remote-client behavior evolve independently from the application, so the implementation should use the official Python SDK rather than a handwritten JSON-RPC subset. The endpoint is public by necessity and therefore needs strict authentication, bounded request/session behavior, input validation, mutation throttling, and tests at the HTTP boundary. - -## Goals / Non-Goals - -**Goals:** - -- Serve standards-compatible Streamable HTTP MCP from the existing FastAPI deployment. -- Let Grok authenticate through standards-compatible OAuth discovery and the teacher's existing Supabase account. -- Recheck the authenticated user's current application permission on every MCP request. -- Reuse the exact normalization, validation, storage, ordering, and audit paths used by existing library operations. -- Require an explicit second tool call before a prepared video becomes visible to learners. -- Keep failure behavior deterministic when authentication, configuration, or Supabase is unavailable. - -**Non-Goals:** - -- Maintaining MCP conversation state or storing Grok conversations. -- Giving the integration a service-role credential, Hugging Face token, shared bearer secret, or xAI API key. -- Changing the management UI, or calling xAI from the application; the only new browser UI is OAuth consent. -- Durable approvals across process restarts or replicas; invalidating pending approvals is the safer failure mode. - -## Decisions - -### Use the official MCP Python SDK with stateless Streamable HTTP - -Add the current stable `mcp` 2.x dependency, pinned to a release containing the bounded-session security fixes, and mount its ASGI Streamable HTTP application under `/mcp`. Configure stateless operation, JSON responses where supported, a small request-body limit, and no server-initiated streams. The ihki lifespan owns the MCP application's lifespan. - -This is preferred to handwritten JSON-RPC because protocol negotiation, discovery, schemas, and client compatibility are easy to get subtly wrong. The legacy SSE transport is not used because Streamable HTTP supersedes it. Stateless mode fits Hugging Face restarts and avoids retaining untrusted client initialization payloads. - -### Use Supabase as the OAuth 2.1 authorization server - -Enable Supabase Auth's OAuth Server, dynamic client registration, and asymmetric JWT signing. Configure its issuer as `https://.supabase.co/auth/v1` and its authorization UI as the Space's `/oauth/consent` route. Grok discovers the authorization server from the MCP resource's RFC 9728 protected-resource metadata, dynamically registers its exact redirect URI, and uses authorization code with PKCE. Supabase owns authorization codes, access and refresh tokens, rotation, expiration, revocation, and client registration. - -This is preferred to implementing an authorization server in FastAPI: it reuses the existing Google/email identities and avoids custom storage of security-critical OAuth grants. The static `GROK_LIBRARY_TOKEN` path is removed because Grok.com's URL-only connector UI cannot configure it. Query-string credentials and an unauthenticated MCP endpoint are rejected because URLs leak into logs and public access would allow anyone to mutate the library. - -### Publish protected-resource metadata and verify user tokens - -Configure the MCP SDK's resource-server authentication with the Space's public MCP resource URL, Supabase's issuer URL, and the minimal identity scopes supported by Supabase. An SDK token verifier validates each presented token through the configured Supabase Auth service and returns only verified identity metadata. Challenges point to `/.well-known/oauth-protected-resource/mcp/`; no access token is included in errors or logs. - -After token verification, ihki resolves the user through the same centralized role and permission logic as browser sessions and requires current `management.access` for discovery and every tool call. This prevents an OAuth grant from outliving a teacher-role revocation. OAuth tokens remain invalid on unrelated `/api/*` routes unless they independently satisfy those routes' existing user authorization; the MCP layer never exposes service-role access. - -### Add an RTL OAuth consent route - -Serve `/oauth/consent?authorization_id=...` as a small Hebrew, RTL page using the existing Supabase browser client. It preserves the authorization ID through the current Google or email-link sign-in flow, requests the authorization details from Supabase, and shows the requesting client's name, redirect URI, requested identity scopes, and a fixed explanation that access is limited to listing, preparing, publishing, and renaming digital-library videos. - -Before enabling approval, the page sends the current Supabase access token to a narrow server check that uses the centralized permission resolver and returns only eligibility. Approval and denial use Supabase's OAuth consent APIs, then redirect only to the URL Supabase returns. A student, signed-out user, malformed/expired authorization ID, or user who lost management permission cannot approve. The page supports touch and keyboard use and does not expose administrative secrets. - -### Put content operations behind shared service functions - -Extract or introduce small library-domain helpers used by both the existing `/api/library` routes and the MCP tools. They normalize titles and provider URLs, access `app.state.auth`, check duplicate normalized URLs, perform writes, refresh cached rows, and write `system_events` using the synthetic actor `grok-library`. - -This avoids two validation implementations drifting. MCP tools return structured, human-readable results but never expose learner data or internal exceptions. Existing browser endpoints and response shapes remain compatible. - -### Use process-local, expiring, single-use approvals - -`prepare_library_video` validates and normalizes input, checks duplicates, then stores `{approval_id, title, url, expires_at}` in a lock-protected bounded in-memory store. Approval identifiers use cryptographically random values, expire after ten minutes, and are consumed atomically before the write attempt. At most a small fixed number of pending approvals are retained; expired entries are removed during access. - -`publish_library_video` accepts only the opaque approval identifier. It rechecks duplicates immediately before insertion to handle concurrent browser or connector changes. Consuming before insertion favors preventing replay; a transient storage failure requires preparing again and cannot produce an ambiguous retry. - -An alternative was immediate publication with conversational confirmation instructions. It was rejected because model behavior is not an authorization boundary. Durable approvals in Supabase were rejected for the first version because they require a migration and make stale proposals survive deployments. - -### Apply narrow mutation limits and safe audit data - -The authenticated token may make bounded read requests, while prepare, publish, and rename calls share a conservative per-process sliding-window mutation limit. Rate-limit state is lock-protected and bounded. Authentication failures are logged only by category and client network metadata already available to infrastructure, never by token value. - -Successful and refused authenticated mutations call the existing best-effort `system_events` audit path with action, target identifier/title, and outcome. If the optional audit table is missing, application logging remains available and the content operation is not blocked, matching existing audit behavior. - -### Keep content storage unchanged - -Published and renamed videos remain in the existing Supabase `library` table. No application table stores OAuth codes or tokens. Deployment documentation replaces `GROK_LIBRARY_TOKEN` instructions with Supabase OAuth Server, dynamic registration, asymmetric signing, Site URL, authorization path, and redirect-domain configuration. The integration does not consume paid xAI API credits because Grok initiates MCP calls. - -## Risks / Trade-offs - -- **A stolen OAuth access or refresh token can act as its user** → rely on short access-token lifetime, refresh rotation and revocation, never persist/log tokens, and recheck current management permission on every request. -- **Dynamic client registration permits unknown clients to register** → always require interactive user consent, display client and redirect details, monitor/revoke registered clients in Supabase, and keep MCP tool authority narrow. -- **A malicious redirect or consent request could phish the teacher** → let Supabase validate exact registered redirect URIs and authorization IDs; the consent page redirects only to Supabase-returned URLs. -- **An LLM may call a tool after reading malicious content** → require a separate opaque approval and publication call; expose no destructive or permission-management tool. -- **Per-process approvals disappear on restart or differ across replicas** → report invalid approval and ask the teacher to prepare again; document single-replica expectations for this first version. -- **MCP SDK changes could affect compatibility or resource use** → pin a secure compatible major/minor range and cover initialization, discovery, and calls through an MCP client in tests. -- **Duplicate checks can race** → recheck immediately before insertion; a database uniqueness constraint is unavailable without changing existing data and schema. -- **Best-effort audit storage may be absent** → emit safe application logs as a fallback and keep the optional `system_events` behavior visible in deployment documentation. - -## Migration Plan - -1. Implement the consent route, Supabase token verifier, protected-resource metadata, centralized permission check, documentation, and focused OAuth tests while retaining the current connector tools. -2. In Supabase, migrate to an asymmetric signing key if necessary, enable OAuth 2.1 Server and dynamic client registration, set the Space as Site URL, and set `/oauth/consent` as Authorization Path. -3. Deploy and verify signed-out, student, teacher approval, denial, expired request, revoked permission, refresh, and MCP discovery flows. -4. Remove `GROK_LIBRARY_TOKEN` from Hugging Face and local configuration; the old static bearer path must no longer authorize MCP. -5. Recreate the Grok Business custom connector using only the final `https://-.hf.space/mcp/` URL, complete browser consent, then test all four tools and audit entries. -6. Roll back by disabling Supabase OAuth Server or dynamic registration and reverting the deployment; existing library content and browser management remain unchanged. diff --git a/openspec/changes/add-grok-library-mcp/proposal.md b/openspec/changes/add-grok-library-mcp/proposal.md deleted file mode 100644 index ae9ab7f5ba4f5059f1da200aa17302bd225c3fd8..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-library-mcp/proposal.md +++ /dev/null @@ -1,36 +0,0 @@ -# Proposal - -## Why - -The teacher who curates the digital library prefers managing content through Grok instead of the application's management UI. A narrowly scoped MCP integration lets that teacher list and publish library videos conversationally without exposing Supabase, Hugging Face, or general administrative credentials. - -## What Changes - -- Add an authenticated MCP endpoint that Grok can discover over public HTTPS. -- Expose tools to list library videos, prepare a proposed YouTube or Vimeo video, explicitly publish a prepared proposal, and rename an existing library video. -- Authenticate Grok with Supabase OAuth 2.1 discovery, dynamic client registration, authorization-code flow with PKCE, and an explicit Hebrew consent screen using the teacher's existing account. -- Authorize every MCP request from the current Supabase user and current `management.access` permission; OAuth identity alone grants no content access. -- Validate video titles and URLs with the existing library rules, reject duplicates, make publication approvals short-lived and single-use, and record integration actions without logging credentials. -- Document the Supabase OAuth Server configuration needed for discovery, consent, token issuance, refresh, revocation, and degraded behavior when OAuth is unavailable. - -Out of scope are deleting videos, managing teachers or users, editing classes, arbitrary database access, automatic web research, and calling the paid xAI API from ihki. - -## Capabilities - -### New Capabilities - -- `external-content-management`: Restricted conversational integrations that prepare and perform approved teacher-content operations. - -### Modified Capabilities - -- `access-and-privacy`: Extend secret isolation and authorization requirements to external integration credentials and scoped MCP operations. -- `teacher-content`: Allow authorized external integrations to list, prepare, publish, and rename digital-library videos under the same validation rules as the management UI. - -## Impact - -- Affected code: FastAPI application routing, a new MCP protocol module, library persistence helpers, configuration documentation, and server tests. -- APIs: a public MCP-over-HTTP protected resource plus a Hebrew `/oauth/consent` browser route; existing content and browser-authentication endpoints remain unchanged. -- Persistence: library rows continue to use the existing Supabase `library` table; pending approvals are process-local and expire, so a restart safely invalidates them. -- Privacy and authorization: no learner data is exposed; the connector receives revocable user-scoped Supabase OAuth tokens, while Supabase and Hugging Face administrative secrets never leave the server and tokens never enter logs. -- Deployment: operators enable Supabase OAuth 2.1, dynamic client registration, an asymmetric signing key, and the Space's `/oauth/consent` authorization path. No application data migration is required. -- Paid APIs: the application does not call xAI and therefore adds no xAI API-credit usage; Grok Business connector availability and billing remain external to ihki. diff --git a/openspec/changes/add-grok-library-mcp/specs/access-and-privacy/spec.md b/openspec/changes/add-grok-library-mcp/specs/access-and-privacy/spec.md deleted file mode 100644 index 505ed5d6032740e3e6d0f4b7cee4b701b0be44e3..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-library-mcp/specs/access-and-privacy/spec.md +++ /dev/null @@ -1,22 +0,0 @@ -# Spec Delta - -## ADDED Requirements - -### Requirement: External integration isolation -The application SHALL authenticate each external integration independently and constrain it to an explicit allowlist of operations that cannot inherit browser, developer, service-role, or hosting privileges. - -#### Scenario: Library integration calls another management API -- **WHEN** a valid library-integration credential is presented to a class, user, teacher, or developer-management endpoint -- **THEN** the application refuses the request - -#### Scenario: OAuth token is handled -- **WHEN** the application verifies, logs, reports, or returns the result of integration authentication -- **THEN** the access or refresh token is neither persisted in application data nor included in logs or responses - -#### Scenario: Authenticated user lacks current management permission -- **WHEN** a valid OAuth token belongs to a user who does not currently have `management.access` -- **THEN** the application refuses MCP discovery and tool operations - -#### Scenario: Permission is revoked after consent -- **WHEN** a previously authorized user loses `management.access` while Grok still holds an unexpired or refreshed token -- **THEN** the next MCP request is refused diff --git a/openspec/changes/add-grok-library-mcp/specs/external-content-management/spec.md b/openspec/changes/add-grok-library-mcp/specs/external-content-management/spec.md deleted file mode 100644 index 365f995572f995f583de18fee87acdbabab18e5b..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-library-mcp/specs/external-content-management/spec.md +++ /dev/null @@ -1,77 +0,0 @@ -# Spec Delta - -## Purpose - -Define how an external conversational assistant can perform a small, explicitly authorized set of teacher-content operations without gaining broader application or database access. - -## ADDED Requirements - -### Requirement: Standard MCP discovery -The service SHALL expose a public HTTPS MCP endpoint whose advertised tools and JSON schemas can be discovered by a compatible remote client. - -#### Scenario: Grok initializes the connector -- **WHEN** an authenticated MCP client initializes a session and requests the tool list -- **THEN** the service returns the supported protocol response and only the configured library-management tools - -### Requirement: OAuth discovery and authentication -The MCP endpoint SHALL advertise standards-compatible protected-resource metadata and require Supabase OAuth 2.1 access tokens obtained through authorization code with PKCE and explicit user consent. - -#### Scenario: Grok discovers authentication -- **WHEN** Grok calls the MCP endpoint without an access token -- **THEN** the service refuses the request with protected-resource metadata that leads Grok to the configured Supabase authorization server - -#### Scenario: Teacher authorizes Grok -- **WHEN** an eligible signed-in teacher reviews the client identity and requested library access and approves it -- **THEN** Supabase returns Grok to its registered redirect URI with a short-lived authorization code usable only with the matching PKCE verifier - -#### Scenario: Teacher denies consent -- **WHEN** the teacher denies the authorization request -- **THEN** no authorization code or MCP access is granted - -#### Scenario: OAuth is not configured -- **WHEN** Supabase OAuth 2.1, dynamic registration, signing keys, or the authorization path is unavailable -- **THEN** connection fails safely and no library operation is performed - -### Requirement: Consent is restricted to authorized staff -The authorization page SHALL allow approval only for a currently authenticated user with `management.access` and SHALL clearly identify the requesting client and the library-only access being requested. - -#### Scenario: Student reaches the consent page -- **WHEN** a signed-in user without `management.access` opens a valid authorization request -- **THEN** approval is unavailable and the page explains that management permission is required - -#### Scenario: Signed-out user reaches the consent page -- **WHEN** a signed-out user opens a valid authorization request -- **THEN** the page preserves the authorization request through Google or email-link sign-in and returns to consent afterward - -### Requirement: Narrow tool authority -The connector SHALL expose only listing, preparing, publishing, and renaming digital-library videos and SHALL provide no tool for deletion, user or teacher management, classes, arbitrary URLs, SQL, or general database access. - -#### Scenario: Client discovers available operations -- **WHEN** an authenticated client lists MCP tools -- **THEN** every returned tool is confined to the digital-library operations named by this requirement - -### Requirement: Explicit publication approval -Publishing SHALL require a valid, short-lived, single-use approval created by preparing the same normalized title and video URL. - -#### Scenario: Teacher prepares and publishes a video -- **WHEN** the client prepares a valid non-duplicate video and then publishes its unexpired approval -- **THEN** the video is added once to the digital library and that approval cannot be reused - -#### Scenario: Approval is invalid or expired -- **WHEN** the client attempts publication with an unknown, expired, or previously used approval -- **THEN** the service refuses publication without adding a video - -### Requirement: Safe and observable integration actions -The service SHALL validate MCP inputs with the same content rules as the management API, reject duplicate video URLs, rate-limit mutations, and log action metadata without credentials. - -#### Scenario: Invalid or duplicate video is prepared -- **WHEN** the client prepares an invalid provider URL, invalid title, or URL already present in the library -- **THEN** the service returns an actionable error and creates no publishable approval - -#### Scenario: Integration action is recorded -- **WHEN** an authenticated MCP mutation succeeds or is refused after authentication -- **THEN** the server records the operation and outcome without recording the bearer credential - -#### Scenario: Persistence is unavailable -- **WHEN** the optional Supabase content store is unavailable during a tool operation -- **THEN** the tool reports that library storage is unavailable and does not claim success diff --git a/openspec/changes/add-grok-library-mcp/specs/teacher-content/spec.md b/openspec/changes/add-grok-library-mcp/specs/teacher-content/spec.md deleted file mode 100644 index 80e7032d9f1cbb37d309ae4dfe1d913df975bef2..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-library-mcp/specs/teacher-content/spec.md +++ /dev/null @@ -1,18 +0,0 @@ -# Spec Delta - -## ADDED Requirements - -### Requirement: External digital-library management -An authorized external integration SHALL be able to list, prepare, publish, and rename digital-library videos under the same title, provider-link, ordering, and visibility rules as authorized staff using the management interface. - -#### Scenario: Integration publishes a prepared video -- **WHEN** an authorized integration publishes a valid prepared YouTube or Vimeo video -- **THEN** the video is appended to the digital library and becomes available to learners with library access - -#### Scenario: Integration renames a library video -- **WHEN** an authorized integration supplies an existing video identifier and a valid title -- **THEN** subsequent library reads show the new title without changing the video URL or position - -#### Scenario: Integration lists the library -- **WHEN** an authorized integration requests the digital library -- **THEN** it receives the identifiers, titles, provider URLs, and current order needed for the permitted tools, but no learner data diff --git a/openspec/changes/add-grok-library-mcp/tasks.md b/openspec/changes/add-grok-library-mcp/tasks.md deleted file mode 100644 index 710e855e1f3ea636fadb781cd98355ac8df9cf39..0000000000000000000000000000000000000000 --- a/openspec/changes/add-grok-library-mcp/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -# Tasks - -## 1. MCP foundation and authentication - -- [x] 1.1 Add a secure compatible official MCP SDK version to `requirements.txt`, install it in the development environment, and verify the imported version satisfies the pinned security floor. -- [x] 1.2 Add the stateless Streamable HTTP MCP application and its FastAPI lifespan/mount integration, then verify an in-process MCP client can initialize and discover tools at `/mcp`. -- [x] 1.3 Add the initial bearer-token prototype using `GROK_LIBRARY_TOKEN`, constant-time comparison, disabled-without-configuration behavior, and bounded mutation rate state; verify focused HTTP tests cover missing, wrong, correct, and unconfigured credentials without secret disclosure. - -## 2. Restricted library tools - -- [x] 2.1 Refactor library title/URL validation and storage operations into shared helpers without changing existing `/api/library` behavior, and verify the existing library API tests still pass. -- [x] 2.2 Implement `list_library_videos` and `rename_library_video` tools with structured results, scope isolation, and safe audit events; verify tool tests cover success, invalid identifiers/titles, unavailable Supabase, and absence of learner data. -- [x] 2.3 Implement the bounded ten-minute approval store plus `prepare_library_video` and `publish_library_video`, including normalization, duplicate rejection, atomic single use, expiry, replay refusal, and pre-write duplicate recheck; verify each case with deterministic focused tests. -- [x] 2.4 Verify MCP discovery advertises exactly the four approved library tools and that no MCP credential authorizes existing class, user, teacher, deletion, or developer APIs. - -## 3. Deployment and integration verification - -- [x] 3.1 Document `GROK_LIBRARY_TOKEN` in `.env.example` and the README with secure generation, Hugging Face Space secret setup, rotation/removal, Grok connector URL, and the fact that no xAI API key or credits are required; verify no real token value appears in tracked files. -- [x] 3.2 Run the focused MCP and library tests, then the full automated test suite, and resolve regressions attributable to this change. -- [x] 3.3 Start the application locally with a disposable token and test MCP discovery plus list/prepare/publish/rename through a real Streamable HTTP client; confirm existing browser management still opens and manages the same library without UI regressions. - -## 4. Supabase OAuth replacement - -- [x] 4.1 Centralize browser and MCP role/permission resolution, add a Supabase OAuth token verifier, and require current `management.access` on every MCP discovery and tool request; verify focused tests cover teacher success, student refusal, malformed/expired token, unavailable Supabase, and permission revocation after consent. -- [x] 4.2 Replace the static bearer middleware with MCP protected-resource metadata and OAuth challenges pointing to the configured Supabase issuer; verify protocol tests cover unauthenticated discovery, issuer metadata, valid OAuth access, and complete removal of `GROK_LIBRARY_TOKEN` authorization. -- [x] 4.3 Add the Hebrew RTL `/oauth/consent` page and eligibility endpoint using the existing Supabase browser session, with client/scope details and approve/deny actions; verify browser tests cover signed-out return, keyboard/touch controls, teacher approval, student refusal, denial, and malformed or expired authorization IDs. -- [x] 4.4 Replace `.env.example` and README bearer-token instructions with Supabase OAuth Server setup, dynamic client registration, asymmetric signing, Site URL, authorization path, consent/revocation steps, and troubleshooting; verify tracked files contain no real tokens and the documented URLs match runtime metadata. - -## 5. OAuth integration verification - -- [ ] 5.1 Run focused MCP/OAuth/library tests and the full automated suite, then exercise discovery, browser consent, token exchange/refresh, all four tools, role revocation, and the unchanged management UI against a configured test deployment; resolve regressions attributable to this change and record any external Supabase dashboard steps still required. - -## Workflow follow-up - -- Enable Supabase OAuth 2.1 Server and dynamic client registration, configure asymmetric signing, and set the production Site URL and `/oauth/consent` authorization path. -- Remove the obsolete `GROK_LIBRARY_TOKEN` from Hugging Face after the OAuth deployment is verified. -- Recreate and authorize the Grok Business Custom MCP connector against the final `/mcp/` URL. -- Archive the change after production verification. diff --git a/openspec/changes/add-mcp-exercise-difficulty/design.md b/openspec/changes/add-mcp-exercise-difficulty/design.md deleted file mode 100644 index 2e5885f9e701c3285e522ff79f5f0e23e720833a..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-exercise-difficulty/design.md +++ /dev/null @@ -1,80 +0,0 @@ -# Design - -## Context - -See `proposal.md` for motivation. Attempt rows already store a learner id, category, phrase label, numeric score, unrelated flag, and timestamp; routine audio is not stored. The deployed monitoring service already validates 90-day date ranges and resolves one class or the caller's complete visible class scope from verified OAuth actor data. - -The existing `all_attempts` helper reads a global newest-first sample and is unsuitable for this report because it can silently omit older in-window rows and downloads attempts outside the selected scope. The browser has no new view in this change, and Supabase remains the only persistence source for the report. - -## Goals / Non-Goals - -**Goals:** - -- Query only authorized learner ids and the requested time window. -- Produce deterministic, testable difficulty metrics with explicit statistical semantics. -- Enforce privacy thresholds before serializing any exercise. -- Refuse truncated samples instead of presenting partial rankings as complete. - -**Non-Goals:** - -- Infer abandonment, motivation, mastery, causality, or a learner's need for intervention. -- Rank learners or return individual histories, phoneme traces, answers, or audio. -- Change scores, calibrations, lessons, assignments, or recommendations automatically. - -## Decisions - -### 1. Add a bounded scoped attempt reader - -`SupabaseAuth` will gain a read-only helper that requests only `id,user_id,cat,phrase,score,unrelated,created_at` for a supplied non-empty set of authorized user ids and UTC date bounds. It will paginate in stable `(created_at,id)` order up to a fixed safety cap plus one row. The extra row detects truncation; the report then refuses and asks for a narrower scope. - -Reusing `all_attempts` was rejected because its global limit can bias results toward recent high-volume learners and requires filtering after unrelated data has already been downloaded. A new database RPC was also considered, but the sequential per-learner metrics are straightforward to compute server-side and no migration is needed for the bounded first version. - -### 2. Reuse monitoring range and class-scope resolution - -The shared monitoring module will expose its validated range and scope-to-user-id path to the difficulty aggregator. The MCP handler accepts `from_date`, `to_date`, optional `class_id`, and `limit` defaulting to 10. Tool arguments never accept roles, subject, learner ids, privacy thresholds, or a success threshold. - -An unknown and an unauthorized class produce the same error. Developers retain cross-subject scope; other managers remain constrained by the current subject/class data fetched on every call. - -### 3. Group by stable stored exercise identity - -Rows are grouped by the stored `(cat, phrase)` pair. The output returns an opaque deterministic hash id plus the category and exercise label; it does not expose attempt ids. This includes built-in and teacher-authored spoken exercises without importing browser content construction into the monitoring layer. - -The stored phrase label is preferred over attempting to map to the current content bank because content can be renamed or removed after attempts were recorded. Empty or malformed category/phrase rows are ignored. - -### 4. Define metrics from ordered attempts - -Score 75 is success because it is the application's existing gold-bead threshold. For each exercise: - -- `success_rate` is successful attempts divided by all attempts, expressed as a rounded percentage. -- `student_count` is the distinct learner count. -- `average_attempts_to_first_success` averages the 1-based first-success position only across learners who reached success; it is null if none did. -- `never_succeeded_students` counts learners with no score of at least 75 in the window; it is not called abandonment. -- `average_score_change` averages each repeat learner's last score minus first score and is null if nobody has two attempts. - -Rows are ordered by timestamp and id before per-learner calculations. Unrelated attempts remain attempts and normally score as failures, matching existing progress accounting. - -### 5. Apply privacy suppression before ranking and output - -An exercise qualifies only with at least three distinct learners and five attempts. Suppressed exercises contribute no label, count, or suppression breakdown to the response. The response states only the fixed policy and the number of returned qualifying exercises. - -The rank key is success rate ascending, then attempts-to-success descending with null treated as hardest, then attempt count descending, then `(cat, phrase)` for stability. The caller's limit is applied only after suppression and sorting. - -### 6. Keep the MCP tool read-only and audit-free - -`get_exercise_difficulty` calls only the scoped read path and returns minimized structured output. It does not use mutation rate limits or approval stores and does not write audit events. Safe server logs may contain actor id, class id, date range, result count, and outcome category, but never rows, phrases, scores, learner ids, or exception text. - -## Risks / Trade-offs - -- **[Risk] A 90-day cross-system report may exceed the row safety cap** → Refuse partial analysis and direct the caller to narrow class or dates. -- **[Risk] Attempt-level success rate can overweight highly active learners** → Also return distinct learner and never-succeeded counts, document the metric, and avoid claiming mastery. -- **[Risk] First-to-last change can reflect regression to the mean or changing conditions** → Label it as descriptive score change, not learning caused by practice. -- **[Risk] Overlapping aggregate queries can enable differencing attacks** → Apply the same fixed threshold independently to every query and never disclose suppressed exercise identities or counts. -- **[Trade-off] Server-side aggregation reads bounded attempt metadata rather than using a database RPC** → Avoids a migration now; a future RPC can replace the internal reader without changing the MCP contract. - -## Migration Plan - -1. Deploy the bounded read helper, pure aggregation tests, and the tenth MCP tool together. -2. Verify tool discovery and privacy behavior locally, then deploy without changing OAuth or the connector URL. -3. Exercise a qualifying and a suppressed production query and confirm no management audit event is written. -4. Roll back the application commit if needed; no data or schema rollback is required. - diff --git a/openspec/changes/add-mcp-exercise-difficulty/proposal.md b/openspec/changes/add-mcp-exercise-difficulty/proposal.md deleted file mode 100644 index 92ab4dc813af03c047f586b933a124ba0e81866e..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-exercise-difficulty/proposal.md +++ /dev/null @@ -1,33 +0,0 @@ -# Proposal - -## Why - -Aggregate activity counts show whether learners practise but cannot tell a teacher which exercises cause difficulty. A scoped difficulty report will turn existing attempt metadata into actionable, privacy-preserving teaching priorities without exposing individual learners or recordings. - -## What Changes - -- Add a read-only `get_exercise_difficulty` MCP tool for one authorized class or all classes in the caller's current scope. -- Rank up to 10 difficult spoken exercises using attempt success at the existing gold threshold, distinct learner count, successful learners' attempts to first success, and first-to-last score improvement. -- Bound analysis to a caller-supplied non-future date range of at most 90 days and a fixed maximum attempt sample. -- Suppress every exercise with fewer than three distinct learners or five attempts; return no learner identity, per-learner value, answer, recording, raw attempt, or exact event time. -- Distinguish “did not reach success in this window” from abandonment; do not infer that a learner quit an exercise. -- Reuse verified OAuth class/subject scope and keep the tool read-only, without prepare/publish or audit writes. -- Explicitly exclude rankings of learners, individual intervention lists, raw pronunciation diagnostics, and automatic teaching or calibration changes. - -## Capabilities - -### New Capabilities - -- `exercise-difficulty-monitoring`: Scoped, statistically honest aggregate exercise-difficulty reporting with privacy thresholds and bounded analysis. - -### Modified Capabilities - -- `teacher-content`: Extend privacy-preserving teacher monitoring with aggregate exercise difficulty inside the caller's authorized scope. -- `access-and-privacy`: Require suppression of small cohorts and prohibit learner-level reconstruction from difficulty reports. - -## Impact - -- Extends the shared monitoring service, Supabase attempt-reading helper, and restricted MCP server. -- Uses existing `attempts` metadata (`user_id`, category, phrase, score, unrelated flag, timestamp); no audio is read and no database migration or paid API call is expected. -- Adds focused aggregation, authorization, privacy-threshold, bounded-query, MCP discovery, and degraded-storage tests. -- Updates README and the Hebrew teacher guide; no browser UI, OAuth registration, connector URL, or deployment setting changes. diff --git a/openspec/changes/add-mcp-exercise-difficulty/specs/access-and-privacy/spec.md b/openspec/changes/add-mcp-exercise-difficulty/specs/access-and-privacy/spec.md deleted file mode 100644 index e9dfe0a5dabe19ff8d91e7a81d1ff5d9dde017be..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-exercise-difficulty/specs/access-and-privacy/spec.md +++ /dev/null @@ -1,21 +0,0 @@ -# Spec Delta - -## ADDED Requirements - -### Requirement: Aggregate cohort protection -Reports derived from learner attempts SHALL enforce fixed minimum cohort and event counts before returning an exercise-level aggregate and SHALL return no learner-level values. - -#### Scenario: Small cohort requests are repeated with overlapping scopes -- **WHEN** a caller requests difficulty reports for overlapping authorized classes or date ranges -- **THEN** every individual result independently enforces the minimum cohort and attempt thresholds -- **AND** suppressed exercises and their counts are not identified - -#### Scenario: Aggregate includes one learner's sequence -- **WHEN** an internal calculation uses ordered attempts to compute first success or improvement -- **THEN** the remote response contains only the final qualifying aggregate -- **AND** no learner identifier, sequence, score, or timestamp is returned - -#### Scenario: Caller lacks management permission -- **WHEN** a caller without current management permission requests exercise difficulty -- **THEN** the service refuses the request before reading attempt data - diff --git a/openspec/changes/add-mcp-exercise-difficulty/specs/exercise-difficulty-monitoring/spec.md b/openspec/changes/add-mcp-exercise-difficulty/specs/exercise-difficulty-monitoring/spec.md deleted file mode 100644 index 6facdfa2376fd38267b34695068a3487653fbc1e..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-exercise-difficulty/specs/exercise-difficulty-monitoring/spec.md +++ /dev/null @@ -1,89 +0,0 @@ -# Spec Delta - -## Purpose - -Give authorized teachers a statistically honest, privacy-preserving ranking of difficult exercises from existing attempt metadata so they can prioritize instruction without inspecting individual learners. - -## ADDED Requirements - -### Requirement: Scoped difficulty ranking -The service SHALL rank exercise difficulty only from attempts by learners in the caller's current authorized class and subject scope. - -#### Scenario: Teacher requests one authorized class -- **WHEN** a teacher requests exercise difficulty for an authorized class -- **THEN** only attempts by that class's learners contribute to the report - -#### Scenario: Manager requests all visible classes -- **WHEN** an authorized manager omits the class identifier -- **THEN** attempts are combined only across classes currently visible to that manager - -#### Scenario: Class is unknown or unauthorized -- **WHEN** the caller requests an unknown or out-of-scope class -- **THEN** the service returns the same refusal without confirming whether the class exists - -### Requirement: Bounded analysis window -The service SHALL accept an inclusive, non-future date range of at most 90 days and a result limit from 1 through 20. - -#### Scenario: Valid report request -- **WHEN** a caller requests 30 days and a limit of 10 -- **THEN** the service returns at most ten qualifying exercises - -#### Scenario: Invalid request bounds -- **WHEN** dates are malformed, reversed, future, or span more than 90 days, or the limit is outside its bounds -- **THEN** the service refuses the request before reading attempts - -#### Scenario: Attempt safety cap is reached -- **WHEN** the bounded attempt query indicates that more rows exist than the analysis safety cap -- **THEN** the service refuses to rank a partial sample -- **AND** it asks the caller to narrow the date or class scope - -### Requirement: Difficulty metrics -For each qualifying exercise, the service SHALL report attempt success rate at score 75, distinct learner count, average attempts to first success among learners who succeeded, never-succeeded learner count, and average first-to-last score change among learners with repeated attempts. - -#### Scenario: Learner succeeds after retries -- **GIVEN** one learner scores below 75 twice and then reaches at least 75 -- **WHEN** the exercise is aggregated -- **THEN** that learner contributes three attempts to first success -- **AND** later attempts do not increase that metric - -#### Scenario: Learner never succeeds -- **WHEN** a learner has attempts but no score of at least 75 in the selected window -- **THEN** the learner contributes to the never-succeeded count -- **AND** the service does not label the learner as having abandoned the exercise - -#### Scenario: Improvement cannot be measured -- **WHEN** no learner has at least two attempts for an exercise -- **THEN** the exercise's improvement value is null rather than fabricated - -### Requirement: Deterministic hardest-first order -The service SHALL order qualifying exercises by lower attempt success rate, then higher average attempts to first success, then higher attempt count, using stable exercise identifiers to break remaining ties. - -#### Scenario: More than the requested limit qualify -- **WHEN** more exercises pass the privacy threshold than the requested limit -- **THEN** only the first exercises in deterministic hardest-first order are returned - -### Requirement: Small-cohort suppression -The service SHALL omit an exercise unless at least three distinct learners and at least five attempts contribute within the selected scope and window. - -#### Scenario: Exercise has many attempts from one learner -- **WHEN** an exercise has five or more attempts but fewer than three learners -- **THEN** the exercise is omitted entirely - -#### Scenario: No exercise passes the threshold -- **WHEN** every exercise has fewer than three learners or five attempts -- **THEN** the service returns an empty ranking and explains the fixed suppression threshold - -### Requirement: Difficulty report data minimization -The report SHALL contain only aggregate exercise labels and metrics and SHALL NOT contain learner identifiers, per-learner values, raw attempts, answers, recordings, exact event timestamps, or pronunciation traces. - -#### Scenario: Report succeeds -- **WHEN** qualifying exercises are returned -- **THEN** no output field permits reconstructing a named learner's attempts or scores - -### Requirement: Read-only degradation -The difficulty tool SHALL perform no mutation or paid-model call and SHALL report unavailability without fabricated results when attempt persistence cannot be queried. - -#### Scenario: Attempt storage is unavailable -- **WHEN** the bounded attempt query fails -- **THEN** the service returns a safe unavailable response with no partial ranking or infrastructure error text - diff --git a/openspec/changes/add-mcp-exercise-difficulty/specs/teacher-content/spec.md b/openspec/changes/add-mcp-exercise-difficulty/specs/teacher-content/spec.md deleted file mode 100644 index fed4f1e809a1e3d9354effe9b43eace3cc48256c..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-exercise-difficulty/specs/teacher-content/spec.md +++ /dev/null @@ -1,18 +0,0 @@ -# Spec Delta - -## MODIFIED Requirements - -### Requirement: Privacy-preserving monitoring -Teacher activity views and conversational monitoring tools SHALL expose only learners and aggregate data within the caller's scope, pronunciation-pattern reports SHALL not return learner identities, and exercise-difficulty reports SHALL suppress small cohorts and learner-level values. - -#### Scenario: Repeated pronunciation issue becomes a candidate -- **GIVEN** the same judgment occurred in at least five attempts by at least three learners and at least 30 percent of learners who attempted the phrase -- **WHEN** a teacher opens pronunciation patterns -- **THEN** the pattern appears as an investigation candidate -- **AND** it is not automatically applied as a calibration - -#### Scenario: Teacher requests difficult exercises -- **WHEN** a teacher requests an aggregate exercise-difficulty ranking -- **THEN** only exercises from the teacher's current scope that meet the privacy threshold are returned -- **AND** no learner identity or per-learner metric is returned - diff --git a/openspec/changes/add-mcp-exercise-difficulty/tasks.md b/openspec/changes/add-mcp-exercise-difficulty/tasks.md deleted file mode 100644 index 25d02f2a140bda40ba349d7db39c402557825810..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-exercise-difficulty/tasks.md +++ /dev/null @@ -1,26 +0,0 @@ -# Tasks - -## 1. Bounded scoped attempt access - -- [x] 1.1 Add a read-only attempt query for authorized learner ids and inclusive UTC bounds that selects only required metadata, paginates in stable order, detects the fixed safety-cap overflow, and verify tests cover encoded scope, boundaries, paging, empty scope, storage failure, and truncation without fetching unrelated learners. - -## 2. Difficulty aggregation - -- [x] 2.1 Extend the monitoring service to reuse date and visible-class scope resolution, map only visible roster emails to learner ids, and verify tests reject invalid dates, limits, and unknown/out-of-scope classes before reading attempts. -- [x] 2.2 Implement pure per-exercise aggregation at the score-75 threshold with success rate, distinct learners, attempts to first success, never-succeeded count, and first-to-last change; verify tests cover retries, later attempts, no success, one-attempt learners, unrelated attempts, malformed rows, and stable rounding. -- [x] 2.3 Apply the three-learner/five-attempt privacy threshold and deterministic hardest-first limit; verify tests cover suppression, overlapping scopes, ties, empty results, cap overflow, and absence of learner ids, per-learner scores, timestamps, answers, recordings, and pronunciation traces. - -## 3. Restricted MCP exposure - -- [x] 3.1 Add the read-only `get_exercise_difficulty` tool using only verified OAuth actor context, with `from_date`, `to_date`, optional `class_id`, and bounded `limit`; verify end-to-end MCP calls are scoped, audit-free, approval-free, and return safe unavailable errors. -- [x] 3.2 Update the exact MCP allowlist to ten tools and verify no learner-ranking, raw-attempt, abandonment, answer, recording, calibration, SQL, log, environment, or remediation tool is discoverable. -- [x] 3.3 Document metric definitions, privacy thresholds, limitations, and Hebrew prompts in README and the teacher guide; verify HTML parses and documented arguments match the runtime tool schema. - -## 4. Integration verification - -- [ ] 4.1 Run focused attempt-query, monitoring, MCP, OAuth, and privacy tests followed by the full suite and strict OpenSpec validation; after deployment, verify the existing connector discovers the new tool without OAuth reconfiguration and returns a qualifying or correctly suppressed read-only report. - -## Workflow follow-up - -- Archive this change and the preceding monitoring change after their production verification requirements are satisfied. - diff --git a/openspec/changes/add-mcp-learning-analytics/design.md b/openspec/changes/add-mcp-learning-analytics/design.md deleted file mode 100644 index 02b7ea887ee15b5525b772628714ad6260f7d72a..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-learning-analytics/design.md +++ /dev/null @@ -1,81 +0,0 @@ -# Design - -## Context - -See `proposal.md` for motivation. The MCP server already authenticates each request through Supabase OAuth, reconstructs the current ihki actor, and delegates activity and difficulty calculations to `app/monitoring.py`. `attempts` stores scoped learner, category, phrase, score, unrelated, timestamp, and optionally phoneme issues/model/version. Existing bounded reads cap analytics at 50,000 attempts; existing pronunciation monitoring reads diagnostics globally and therefore cannot be reused directly by MCP. - -The browser APIs and state are unaffected. Supabase remains optional, and no schema or deployment configuration changes are authorized. - -## Goals / Non-Goals - -**Goals:** - -- Share one parsing, scoping, privacy, and metric vocabulary across five analytics tools. -- Preserve the current `get_exercise_difficulty` arguments and fields while adding requested fields. -- Make every response directly usable by chart or table clients. -- Keep authorization filtering ahead of aggregation and prevent partial-sample calculations. - -**Non-Goals:** - -- Inferring sessions, abandonment, unrecorded exposure, or non-spoken answer correctness. -- Adding learner drill-down, browser UI, stored aggregates, migrations, or paid inference. -- Treating pedagogical recommendations as measured facts. - -## Decisions - -### Use one scoped attempt reader with optional diagnostics - -Extend the existing bounded `scoped_attempts` read with an `include_diagnostics` option. The default select remains metadata-only; pronunciation requests add `issues`, `model`, and `version`. A missing optional diagnostic column is represented as diagnostics unavailable rather than retrying an unscoped query. - -This keeps authorization in the shared service and avoids duplicating pagination. Reusing `all_attempt_issues` was rejected because it is global, lacks date filters, and cannot enforce MCP class scope. - -### Normalize rows once, then aggregate in pure functions - -`app/monitoring.py` will validate rows into an internal attempt record with UTC timestamp, Jerusalem local date, stable exercise ID, category, score, unrelated flag, and sanitized issues. Invalid fields are ignored only for the metric that requires them. Aggregators remain pure and receive already scoped rows, which makes denominators and tests deterministic. - -The stable ID remains the existing SHA-256 prefix of category plus stored phrase, preserving current identifiers. Display name falls back to the stored phrase. Exercise type is a deterministic category mapping with the original category returned separately; unknown categories map to `other`, never to a guessed teaching activity. Topic uses existing category/lesson metadata when available and otherwise is null. - -### Apply a common privacy envelope - -Every output group must contain at least three distinct learners and `max(5, min_attempts)` scored attempts. `min_attempts` defaults to 10 for the expanded difficulty tool and cannot weaken the fixed floor. Progress buckets that fail the threshold expose zero counts and null rates, not small exact counts. Class comparison retains requested class labels but marks suppressed rows `insufficient_data`. - -Top-level responses contain `status`, scope, inclusive dates, definitions, thresholds, and sample coverage. `ok` remains the MCP transport/tool outcome wrapper; `status` distinguishes `complete`, `insufficient_data`, and `unavailable`. - -### Define success and retry inside the requested window - -A success is `score >= 75` and `unrelated == false`. Attempts with invalid scores are excluded from score and success denominators. First attempt means first observed persisted attempt for a learner/exercise inside the selected window; the response states this explicitly. This avoids an unbounded historical lookup while preventing claims of lifetime-first behavior. - -Average attempts until success includes only learners who succeed; never-success counts are reported separately. Improvement is the mean per-learner last-valid-score minus first-valid-score for learners with two or more attempts. Trend is the difference between the first and last qualifying time buckets; when fewer than two qualifying buckets exist it is null. - -### Derive each report from the same evidence - -- Difficulty groups by stable exercise and ranks low success rate first, then retry burden, sample size, and stable ID. -- Pronunciation uses only stored flagged `issues`. A phoneme's denominator is evaluated attempts containing that expected phoneme when evidence permits; otherwise the metric is null rather than using all speech attempts. Example words are emitted only when `issue.word` maps to a token in a resolvable exercise text. -- Progress zero-fills calendar buckets for day/week/month, but suppresses score metrics and small counts per bucket. -- Class comparison runs the same aggregate independently per authorized class and reports attempts per participant and exercised-item exposure. Overlapping enrollment is flagged in metadata. -- Bottlenecks reuse exercise aggregates, optionally summarize by topic, and compute severity from fixed metric bands. Suggestions are deterministic templates keyed by the measured basis. - -### Keep MCP wrappers thin and backward compatible - -`app/grok_mcp.py` will declare four new structured tools and extend the existing difficulty signature. Wrappers obtain the current actor/store, call monitoring services, log only actor ID, requested scope/range, safe status, and result count, and convert safe domain errors to `{ok: false, error}`. They perform no audit-table write. - -Existing difficulty fields (`exercise`, `category`, `attempt_count`, `student_count`, and current retry/improvement fields) remain as aliases beside the requested canonical fields. This avoids breaking current Grok prompts and tests. - -## Risks / Trade-offs - -- [Historical attempts may lack diagnostics] → Return diagnostic coverage and `insufficient_data`; never infer pronunciation difficulty. -- [Stored phrase may be a key rather than display text] → Resolve known catalog entries and retain a stable stored fallback. -- [One learner may belong to multiple classes] → Calculate each class independently and disclose overlap instead of claiming independent cohorts. -- [Client-selected windows change which attempt is “first”] → Name it first observed in range in response definitions. -- [Privacy suppression can produce sparse charts] → Return explicit suppressed/insufficient status and null metrics without small counts. -- [Large broad queries can exceed the safety cap] → Refuse partial analysis and ask for a narrower date or class scope. -- [Heuristic interventions may be over-trusted] → Separate `observed_metrics` from typed recommendations and list each recommendation's measured basis. - -## Migration Plan - -1. Add the scoped optional-diagnostics read and pure analytics helpers. -2. Extend/register MCP tools and run focused unit and connector tests. -3. Run the full automated suite and validate OpenSpec artifacts. -4. Update documentation and examples. - -There is no database migration or automatic deployment. Rollback is a code revert; stored data is unchanged. diff --git a/openspec/changes/add-mcp-learning-analytics/proposal.md b/openspec/changes/add-mcp-learning-analytics/proposal.md deleted file mode 100644 index de82b30d4b52ac44501ea10a7f188ddb3258fc77..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-learning-analytics/proposal.md +++ /dev/null @@ -1,34 +0,0 @@ -# Proposal - -## Why - -Teachers can currently inspect aggregate activity and a basic exercise-difficulty ranking, but cannot ask the MCP connector for pronunciation patterns, progress trends, normalized class comparisons, or persistent learning bottlenecks. Adding these reports lets a non-technical teacher make evidence-based instructional decisions while preserving the existing class scope and small-cohort privacy boundaries. - -## What Changes - -- Expand `get_exercise_difficulty` with exercise-type filtering, caller-raised sample thresholds, stable display metadata, average score, per-student attempt metrics, and explicit ranks while preserving its existing fields. -- Add read-only MCP tools for pronunciation difficulties, learning progress, class comparisons, and learning bottlenecks. -- Define consistent success, retry, time-window, missing-data, sample-size, and trend semantics across all analytics responses. -- Resolve exercise metadata from existing course content where possible and return null or stored fallbacks rather than fabricating labels. -- Enforce current OAuth management authorization, class/subject scope, bounded queries, aggregate-only output, and small-cohort suppression for every report. -- Add focused tests, structured response examples, and Hebrew example questions. -- Explicitly exclude learner identities, raw attempts, recordings, abandonment/session claims, database migrations, paid-model calls, and production deployment. - -## Capabilities - -### New Capabilities - -- `mcp-learning-analytics`: Privacy-preserving MCP reports for exercise difficulty, pronunciation patterns, learning progress, class comparison, and learning bottlenecks. - -### Modified Capabilities - -- `access-and-privacy`: Apply existing administrative scope and sensitive-data boundaries to all learning analytics reports. -- `teacher-content`: Extend privacy-preserving teacher monitoring with aggregated learning and pronunciation analytics. - -## Impact - -- Affects `app/monitoring.py`, `app/grok_mcp.py`, bounded Supabase reads in `app/auth.py`, and their focused tests. -- Extends the existing MCP tool schemas without removing or renaming existing tools or response fields. -- Reads existing `attempts` metadata and optional diagnostic columns; it does not change the Supabase schema or store new data. -- Adds no dependency, browser UI, deployment configuration, paid API call, or routine audio access. -- When Supabase or optional pronunciation diagnostics are unavailable, reports return a safe unavailable or `insufficient_data` result instead of inferred data. diff --git a/openspec/changes/add-mcp-learning-analytics/specs/access-and-privacy/spec.md b/openspec/changes/add-mcp-learning-analytics/specs/access-and-privacy/spec.md deleted file mode 100644 index 05bc75663d0f9521de00539c9b3facdc6e3b736d..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-learning-analytics/specs/access-and-privacy/spec.md +++ /dev/null @@ -1,28 +0,0 @@ -# Spec Delta - -## MODIFIED Requirements - -### Requirement: Administrative scope -Developers SHALL have full cross-subject access, subject admins SHALL manage and analyze only their assigned subject, and class teachers SHALL manage and analyze only their assigned classes. Every MCP analytics request SHALL re-evaluate the caller's current management permission and scope before reading attempts. - -#### Scenario: Subject admin targets another subject -- **WHEN** a subject admin attempts to change a class or user belonging to another subject -- **THEN** the service refuses the change - -#### Scenario: Manager requests unauthorized analytics -- **WHEN** a manager requests analytics for a class outside the manager's current scope -- **THEN** the service refuses the request without confirming whether the class or its data exists - -#### Scenario: Permission is revoked after OAuth connection -- **WHEN** a previously connected MCP caller no longer has management permission -- **THEN** the next analytics request is refused before attempts are read - -## ADDED Requirements - -### Requirement: Aggregate analytics privacy -Learning analytics SHALL suppress small cohorts and SHALL NOT expose learner identifiers, per-learner values, raw attempts, private recordings, precise event timestamps, credentials, internal queries, or diagnostic traces that permit reconstruction of a learner's activity. - -#### Scenario: Cross-class report succeeds -- **WHEN** an authorized caller compares qualifying classes -- **THEN** the response contains class-level aggregates only -- **AND** it contains no student names, email addresses, account identifiers, or per-student metrics diff --git a/openspec/changes/add-mcp-learning-analytics/specs/mcp-learning-analytics/spec.md b/openspec/changes/add-mcp-learning-analytics/specs/mcp-learning-analytics/spec.md deleted file mode 100644 index 75cc8e3bf58b8e51981a5a19f2b42d60311f6c54..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-learning-analytics/specs/mcp-learning-analytics/spec.md +++ /dev/null @@ -1,127 +0,0 @@ -# Spec Delta - -## Purpose - -Provide authorized MCP clients with consistent, bounded, privacy-preserving aggregate evidence about exercise difficulty, pronunciation, progress, class differences, and learning bottlenecks. - -## ADDED Requirements - -### Requirement: Common analytics definitions -Analytics SHALL count each persisted scored attempt once, define success as a score of at least 75 on a non-unrelated result, group retries by learner and stable exercise, use Asia/Jerusalem calendar boundaries, and return null for metrics that cannot be calculated. - -#### Scenario: Related passing attempt -- **WHEN** a persisted attempt has score 75 or higher and is not marked unrelated -- **THEN** it contributes one attempt and one success to its exercise - -#### Scenario: Unrelated or missing result -- **WHEN** an attempt is marked unrelated or lacks a valid numeric score -- **THEN** it does not count as a success -- **AND** a missing score is excluded from score-dependent denominators - -#### Scenario: First and repeated attempts -- **WHEN** one learner has several attempts for one exercise in the requested window -- **THEN** the earliest persisted attempt is the first observed attempt in that window -- **AND** each later attempt is a retry - -### Requirement: Bounded and privacy-thresholded reports -Every analytics tool SHALL accept only valid non-future date ranges of at most 90 days, query only the caller's authorized learners, suppress groups with fewer than three learners or the effective attempt threshold, and refuse truncated samples. - -#### Scenario: Caller raises the attempt threshold -- **WHEN** `min_attempts` is greater than the fixed privacy floor -- **THEN** the caller's value becomes the effective threshold - -#### Scenario: Caller lowers the attempt threshold -- **WHEN** `min_attempts` is below the fixed privacy floor -- **THEN** the fixed privacy floor remains effective - -#### Scenario: No group qualifies -- **WHEN** no aggregate passes all privacy and sample thresholds -- **THEN** the tool returns `status` equal to `insufficient_data`, the effective thresholds, and no learner-level data - -#### Scenario: Safety cap is exceeded -- **WHEN** more attempts match than the bounded query can read completely -- **THEN** the tool refuses to calculate metrics from the partial sample - -### Requirement: Expanded exercise difficulty report -The difficulty tool SHALL support optional exercise-type filtering and return stable exercise metadata, attempts, learners, successes, success rate, attempts per learner, average score, and deterministic difficulty rank while preserving existing response fields. - -#### Scenario: Difficulty ranking is requested -- **WHEN** qualifying exercises exist in the selected scope and window -- **THEN** they are ranked primarily by lower success rate and then by repeated-attempt evidence and stable identifiers -- **AND** each result includes its sample sizes - -#### Scenario: Exercise type is selected -- **WHEN** the caller supplies a recognized exercise type -- **THEN** only exercises mapped to that type contribute to the ranking - -#### Scenario: Metadata cannot be resolved -- **WHEN** a stored exercise no longer has matching catalog metadata -- **THEN** the report uses the stable stored fallback and returns null for metadata it cannot establish - -### Requirement: Pronunciation difficulty report -The pronunciation tool SHALL aggregate actual stored diagnostic issues by expected Arabic phoneme, with examples, evaluated attempts, error rate, affected learners, average pronunciation score, and a time trend, without relying on assumed linguistic difficulty. - -#### Scenario: Diagnostic issues are available -- **WHEN** enough authorized learners have evaluated attempts and a phoneme has measured flagged issues -- **THEN** the phoneme appears with aggregate observed metrics and privacy-safe example words - -#### Scenario: Diagnostics are unavailable -- **WHEN** diagnostic columns are absent or no qualifying diagnostic sample exists -- **THEN** the tool returns `insufficient_data` without inferring likely Hebrew-speaker errors - -#### Scenario: Word linkage is uncertain -- **WHEN** a diagnostic word index cannot be mapped reliably to the stored exercise text -- **THEN** no example word is fabricated for that issue - -### Requirement: Learning progress series -The progress tool SHALL return day, week, or month buckets containing attempts, learners, success rate, average score, first-observed-attempt success rate, retry success rate, and an aggregate improvement trend. - -#### Scenario: Empty period bucket -- **WHEN** a requested calendar bucket contains no qualifying attempts -- **THEN** its counts are zero and score-derived rates are null - -#### Scenario: Exercise filter is supplied -- **WHEN** a stable exercise identifier is supplied -- **THEN** only that exercise contributes and an unknown or out-of-scope identifier is refused without exposing its existence - -### Requirement: Comparable class difficulty report -The class comparison tool SHALL compare only authorized classes using the same definitions and thresholds, and SHALL normalize metrics by participating learners and observed exercise exposure. - -#### Scenario: Selected classes are authorized -- **WHEN** the caller supplies multiple visible class identifiers -- **THEN** each qualifying class receives comparable per-learner and per-exposure metrics using the same date window - -#### Scenario: A class has insufficient data -- **WHEN** one selected class does not meet privacy thresholds -- **THEN** that class is represented only by an `insufficient_data` status and no small-cohort metrics - -#### Scenario: Learner belongs to overlapping classes -- **WHEN** the same learner belongs to more than one selected class -- **THEN** the response discloses the overlap limitation and does not present class rows as mutually exclusive populations - -### Requirement: Learning bottleneck report -The bottleneck tool SHALL identify qualifying exercises and topics using repeated failures, attempts until success, and observed improvement, and SHALL keep measured evidence separate from heuristic pedagogical suggestions. - -#### Scenario: Persistent failure is measured -- **WHEN** enough learners repeatedly fail an exercise or topic and do not improve -- **THEN** the result includes affected learners, repeated-failure rate, attempts until success, improvement rate, and deterministic severity - -#### Scenario: Intervention is suggested -- **WHEN** measured patterns trigger a pedagogical suggestion -- **THEN** the suggestion is labeled as a recommendation -- **AND** its measured basis is listed separately from the observed metrics - -#### Scenario: No bottleneck qualifies -- **WHEN** no exercise or topic meets the evidence threshold -- **THEN** the tool returns `insufficient_data` rather than asserting that no learning difficulty exists - -### Requirement: Read-only structured MCP behavior -All analytics tools SHALL return stable structured JSON, ISO 8601 dates, numeric metrics, sample sizes, scope metadata, and safe status values without mutation, paid-model calls, raw attempts, identities, recordings, or exact event timestamps. - -#### Scenario: Analytics request succeeds -- **WHEN** an authorized MCP client calls any analytics tool -- **THEN** the response is suitable for tables or charts and contains only aggregate fields - -#### Scenario: Persistence is unavailable -- **WHEN** Supabase is not configured or the required data cannot be read -- **THEN** the tool returns a safe unavailable result without infrastructure details or fabricated metrics diff --git a/openspec/changes/add-mcp-learning-analytics/specs/teacher-content/spec.md b/openspec/changes/add-mcp-learning-analytics/specs/teacher-content/spec.md deleted file mode 100644 index c1d0efd08ce4a10034788f6fb9b8dc5859542cef..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-learning-analytics/specs/teacher-content/spec.md +++ /dev/null @@ -1,17 +0,0 @@ -# Spec Delta - -## MODIFIED Requirements - -### Requirement: Privacy-preserving monitoring -Teacher activity views and conversational monitoring tools SHALL expose only learners and aggregate data within the caller's scope. Pronunciation and learning analytics SHALL not return learner identities, SHALL suppress small cohorts, and SHALL distinguish measured observations from recommendations. - -#### Scenario: Repeated pronunciation issue becomes a candidate -- **GIVEN** the same judgment occurred in at least five attempts by at least three learners and at least 30 percent of learners who attempted the phrase -- **WHEN** a teacher opens pronunciation patterns -- **THEN** the pattern appears as an investigation candidate -- **AND** it is not automatically applied as a calibration - -#### Scenario: Teacher asks an assistant about learning difficulties -- **WHEN** an authorized teacher requests exercise, pronunciation, progress, comparison, or bottleneck analytics through MCP -- **THEN** the service returns only privacy-thresholded aggregates from the teacher's current scope -- **AND** any suggested intervention is explicitly identified as a recommendation diff --git a/openspec/changes/add-mcp-learning-analytics/tasks.md b/openspec/changes/add-mcp-learning-analytics/tasks.md deleted file mode 100644 index 3dfb8e6ce8dbfcd5e94ecb3ec1cef1d40b54d664..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-learning-analytics/tasks.md +++ /dev/null @@ -1,35 +0,0 @@ -# Tasks - -## 1. Scoped analytics data - -- [x] 1.1 Extend the bounded Supabase attempt reader with optional diagnostic fields and a safe missing-diagnostics signal; verify focused auth tests cover scope, dates, pagination, truncation, and absent columns. -- [x] 1.2 Add shared attempt normalization, stable exercise metadata, success/retry definitions, privacy thresholds, and day/week/month bucketing in `app/monitoring.py`; verify unit tests cover malformed rows, unrelated attempts, missing scores, Jerusalem boundaries, and deterministic IDs. - -## 2. Difficulty and pronunciation analytics - -- [x] 2.1 Expand the exercise-difficulty service with `exercise_type`, `min_attempts`, canonical metrics, ranks, `status`, and backward-compatible aliases; verify focused tests cover filtering, calculations, ordering, suppression, and existing callers. -- [x] 2.2 Implement the pronunciation-difficulty service from scoped stored issues, including diagnostic coverage, affected learners, safe word examples, average score, and trend; verify tests cover missing diagnostics, uncertain word links, model data, privacy suppression, and actual-error-only output. -- [x] 2.3 Register the expanded difficulty schema and `get_pronunciation_difficulties` MCP tool as read-only wrappers; verify connector tests assert tool discovery, input validation, scoped aggregate output, and no audit or mutation calls. - -## 3. Progress and class comparison - -- [x] 3.1 Implement learning-progress series with optional exercise filtering, zero-filled calendar buckets, first-observed and retry success rates, average score, and improvement trend; verify unit tests cover day/week/month intervals, empty buckets, retries, null metrics, and unauthorized identifiers. -- [x] 3.2 Implement normalized class comparison for selected or all visible classes, including per-participant and exposure metrics, overlap disclosure, and per-class insufficient-data states; verify tests cover authorization boundaries, unequal participation, overlapping rosters, small classes, and deterministic ordering. -- [x] 3.3 Register `get_learning_progress` and `compare_class_difficulties` MCP tools; verify connector tests cover discovery, structured schemas, safe errors, read-only behavior, and absence of learner identities. - -## 4. Bottleneck analytics - -- [x] 4.1 Implement exercise/topic bottleneck aggregation, repeated-failure and improvement metrics, deterministic severity, and typed intervention templates with measured bases; verify tests distinguish observations from recommendations and cover never-success, eventual-success, flat, improving, and insufficient samples. -- [x] 4.2 Register `get_learning_bottlenecks` as a read-only MCP tool; verify connector tests cover scoped results, safe empty/unavailable states, structured recommendations, and no mutation or paid-model calls. - -## 5. Documentation and integration verification - -- [x] 5.1 Document all five analytics tool schemas, metric definitions, privacy behavior, limitations, example JSON responses, and Hebrew example questions; verify examples match the registered MCP schemas and explicitly state that sessions, abandonment, and untracked answers are unavailable. -- [ ] 5.2 Run focused analytics/auth/MCP tests, then the full test suite and `openspec validate add-mcp-learning-analytics --strict`; fix regressions and record that no database migration or production deployment was performed. - -Verification record (2026-10-26): focused analytics/auth/MCP tests pass (40 passed), Python compilation, `git diff --check`, and strict OpenSpec validation pass. The full suite was attempted twice but blocked in existing unrelated tests (`test_existing_library_api_is_compatible_and_mcp_token_has_no_api_authority` ASGI lifespan teardown, then `test_a_key_is_kept_encrypted_and_never_shown_back`); all tests reached before each block passed. No database migration or production deployment was performed. - -## Workflow follow-up - -- Obtain explicit approval before any database migration or production deployment. -- Archive the change only after implementation review and production verification requirements are satisfied. diff --git a/openspec/changes/add-mcp-monitoring-tools/design.md b/openspec/changes/add-mcp-monitoring-tools/design.md deleted file mode 100644 index 94635e061392ed3d5e78311032bdea7dd371768f..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-monitoring-tools/design.md +++ /dev/null @@ -1,93 +0,0 @@ -# Design - -## Context - -See `proposal.md` for motivation. The FastAPI management API already calculates privacy-preserving attempt activity through `SupabaseAuth.scoped_activity`, and `class_management.visible_classes` already resolves the verified actor's current class/subject scope. Persisted `attempts.created_at` is a database-generated `timestamptz` representing when a scored practice attempt was stored. The MCP server receives its actor only from a verified OAuth token. - -`LiveStore` also has a reliable but deliberately narrow presence signal: while an explicit live-class session exists in this process, a joined participant is online when its monotonic heartbeat age is at most `PRESENCE_TTL` (20 seconds). That state is not durable, disappears when the session ends or the process restarts, and does not cover ordinary application use. No durable general-purpose page-view, login-presence, or application-last-active event exists. The existing `/api/health` response is broader than the intended MCP contract and contains model-specific diagnostic structures, so it must not be forwarded wholesale. - -No browser state participates in MCP authorization. Supabase remains optional for ordinary practice, but MCP management and activity monitoring require the configured management store. There is no UI change, so RTL, touch, and keyboard behavior are unaffected. - -## Goals / Non-Goals - -**Goals:** - -- Share one scoped aggregation path between class-aware monitoring requests and tests. -- Produce deterministic daily and weekly series in `Asia/Jerusalem`, including zero buckets. -- Report recent practice activity and its latest qualifying timestamp without presenting recent activity as current presence. -- Expose current presence only from an active live-class heartbeat source and state its definition and freshness. -- Distinguish no activity, no data, unavailable data, and privacy suppression in a stable schema. -- Return a deliberately small health schema whose values cannot contain exception text. -- Keep monitoring calls strictly read-only and independent of mutation approvals. - -**Non-Goals:** - -- Diagnosing infrastructure from raw logs, probing third-party paid APIs, or repairing failed components. -- Returning individual learner activity, rankings, scores, answers, recordings, or login history. -- Adding a graph-rendering UI, database tables, Supabase RPCs, credentials, or environment settings. -- Adding application-wide presence collection, browser telemetry, durable heartbeat storage, or retention-policy changes. -- Inferring general application use, login state, or inactivity from the absence of a practice attempt. - -## Decisions - -### 1. Add a pure monitoring service between MCP and persistence - -A small `app/monitoring.py` module will validate ISO dates, resolve the actor's visible classes, map roster emails to Supabase user IDs, call the existing scoped aggregate helper, and normalize its output. MCP tool handlers will remain schema/auth adapters. - -This avoids calling the internal HTTP API from the same process and prevents authorization logic from diverging between tests and runtime. Directly duplicating `/api/activity` inside `grok_mcp.py` was rejected because that route has legacy parameter shapes and browser-specific compatibility branches. - -### 2. Query bounded hourly aggregates and roll them up locally - -The service will request 60-minute aggregate buckets from the start of the selected local date and combine them into local calendar days or ISO-calendar weeks. It will create missing buckets with zero counts and count `active_students` per source bucket only as an activity indicator; it will not sum that field into a false distinct-person total across a day or week. The series will expose attempts and a bounded active-student indicator supported by the existing aggregate source, with summary values supplied only when their semantics remain valid. - -The alternative—new daily and weekly database RPCs—would produce more exact distinct counts per output bucket but would require a migration. This change intentionally reuses the existing privacy-preserving RPC/fallback and will name fields according to their actual semantics. If exact daily distinct counts cannot be derived from the current aggregate response, implementation must omit that value rather than fabricate it. - -### 3. Build current activity from two explicitly different sources - -`get_current_activity` will accept an optional authorized `class_id` and a `window_minutes` enum of 5 or 15. For persisted practice it will make one bounded scoped attempt read from `checked_at - window_minutes` through `checked_at`, deduplicate by user ID before aggregation, and calculate qualifying distinct learners, attempts, the latest attempt timestamp, and elapsed seconds. The timestamp is returned as UTC ISO 8601 plus an `Asia/Jerusalem` rendering; `checked_at` and the source's update semantics are returned separately. - -The same response may include `live_presence` only for an active explicit live-class session in the selected scope. It will count participants whose heartbeat age is within the existing 20-second TTL at the query instant and report `checked_at`, the TTL definition, source=`live_session_heartbeat`, and ephemeral limitations. It will not expose participant rows or use heartbeat state as the persisted last-activity timestamp. If multiple visible classes are selected, live participants are deduplicated by internal user ID before the identifier is discarded. - -No new tracking will be added. A durable general application activity timestamp is therefore returned as `unavailable` with a safe reason code, while `last_practice_activity` is populated only from stored attempts. Alternatives considered were durable heartbeats or page/activity events; both were rejected for this change because they require a new collection purpose, retention rules, schema and privacy review. - -### 4. Apply privacy thresholds and explicit data states before serialization - -The recent-practice metrics use the existing analytics floor of three distinct learners and five attempts. Live presence is shown only when the authorized roster has at least three learners and the exact non-zero online count is either zero or at least three; counts of one or two are `suppressed`. Suppressed responses do not expose an exact timestamp or count from the small sample. - -The response distinguishes: `no_activity` when the source was queried successfully and no qualifying event exists in the window; `no_data` when the authorized scope has no stored observations from which a last event can be established; `unavailable` when a required source cannot be queried or does not exist; and `suppressed` when data exists but privacy thresholds prevent display. These are not interchangeable with numeric zero. Combined-class learner sets are unions, never sums, so enrollment overlap cannot double-count a learner. - -### 5. Resolve class scope before mapping learner identifiers - -When `class_id` is present, it must match one of `visible_classes`; an unknown and an unauthorized identifier return the same error. When omitted, all visible classes are combined. Roster emails are mapped to IDs using `auth.users()`, and only the ID set is passed to `scoped_activity`. No email or ID is included in the MCP result. - -Developers retain their existing cross-subject visibility. Subject administrators and class teachers remain constrained by current access data on every invocation. Tool arguments cannot supply roles or subject scope. - -### 6. Build health from bounded internal checks, not `/api/health` - -`get_system_health` will construct a whitelist response: overall status, UTC timestamp, and coarse statuses for application, authentication, database, model, and text-to-speech. Status values are limited to `healthy`, `degraded`, `unavailable`, and `not_configured`. Optional unconfigured components do not fail the overall service. - -Database reachability will use a minimal bounded read through the existing auth store; exceptions are logged server-side by type only and mapped to `unavailable`. The tool must not call external model or TTS providers, avoiding cost and side effects. Developers may additionally receive existing build/version identifiers, never diagnostic rule maps, paths, URLs, configuration values, or exception strings. - -### 7. Do not audit read-only monitoring calls - -The tools will not write management audit events because the spec requires no application mutation as a consequence of a call. Server logs may record the tool name, outcome category, actor ID, class ID, and requested date range, but not bearer tokens, roster data, returned series, or exception text. - -## Risks / Trade-offs - -- **[Risk] Existing hourly aggregates may not provide exact distinct learners for a larger displayed bucket** → Omit or explicitly label any non-additive metric; never sum hourly distinct counts and present them as daily distinct learners. -- **[Risk] A recent attempt can be mistaken for current presence** → Keep `recent_practice` and `live_presence` in separate response objects and state that the former does not imply a current connection. -- **[Risk] Live presence disappears on restart and covers only explicit live sessions** → Label the source ephemeral, return its check time and 20-second TTL, and return `unavailable` outside a qualifying live session. -- **[Risk] Latest exact timestamps can expose a small cohort's behavior** → Apply the learner and attempt floors before returning timestamps or elapsed time; use `suppressed` without small values otherwise. -- **[Risk] Learners enrolled in multiple visible classes can inflate combined counts** → Build one union of scoped user IDs and aggregate each stored event once. -- **[Risk] Mapping all visible roster emails through the user directory may be slower for developers** → Enforce the 90-day limit, reuse one user-directory read per call, and pass only the final bounded ID set to the aggregate helper. -- **[Risk] A health endpoint can become an infrastructure oracle** → Require OAuth management access and serialize only enum statuses from a fixed whitelist. -- **[Risk] Database degradation can make both activity and health checks slow** → Use existing bounded request timeouts and map failures to a stable unavailable response. -- **[Trade-off] No new RPC means local calendar roll-up is less precise for distinct-student counts** → Prefer truthful, limited metrics now; add a dedicated RPC in a future migration if teachers need exact per-day distinct counts. - -## Migration Plan - -1. Deploy the service and three additional MCP tool schemas without changing the connector URL or OAuth registration. -2. Verify local discovery, scoped activity fixtures, recent-window boundaries, privacy assertions, cross-class deduplication, degraded storage, ephemeral live presence, and role-aware health output. -3. After deployment, refresh an existing connector session if its client caches tool discovery; no reconnection or Supabase configuration change should normally be required. -4. Roll back by reverting the application commit; no stored data or schema requires rollback. - diff --git a/openspec/changes/add-mcp-monitoring-tools/proposal.md b/openspec/changes/add-mcp-monitoring-tools/proposal.md deleted file mode 100644 index 057e5708824b52a19b14b56a82239cf275d4d338..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-monitoring-tools/proposal.md +++ /dev/null @@ -1,36 +0,0 @@ -# Proposal - -## Why - -Teachers using the conversational connector can manage content and rosters, but they cannot ask the same assistant for the scoped activity graph already available in the management UI, recent aggregate activity and its latest available timestamp, or a safe operational status summary. Read-only monitoring tools will let a non-technical teacher understand learner activity and whether the service is available without granting access to learner identities, raw logs, or infrastructure secrets. - -## What Changes - -- Add a read-only `get_activity_timeseries` MCP tool that returns aggregate attempt and active-student buckets for one authorized class or the caller's authorized scope. -- Add a read-only `get_current_activity` MCP tool that reports privacy-thresholded recent practice activity for a configurable 5- or 15-minute window, the latest qualifying practice timestamp and elapsed time, and current live-session presence only when the selected scope has a reliable active live-class session. -- Distinguish live presence from recent persisted activity: define live presence as a heartbeat within the existing live-session TTL, and never describe a learner with a recent attempt as necessarily connected now. -- Return explicit `no_activity`, `no_data`, `unavailable`, and `suppressed` states, disclose source freshness, query time, UTC ISO 8601 timestamps, and `Asia/Jerusalem` display values, and deduplicate learners across combined class rosters. -- Bound activity requests to supported daily/weekly intervals and at most 90 days, and return no learner identities, scores, answers, recordings, or exact login events. -- Add a read-only `get_system_health` MCP tool with a small public component-status vocabulary and role-aware detail. -- Reuse verified OAuth actor permissions and current class/subject scope for every request; reject unavailable persistence safely. -- Keep all monitoring tools outside the mutation approval flow and record only safe operational audit metadata. -- Reuse existing persisted attempts and ephemeral live-session heartbeats without adding general application-presence tracking, changing retention, or claiming an application-wide last-active time that is not stored. -- Explicitly exclude raw logs, stack traces, database queries, environment variables, service endpoints, credentials, per-student timelines, and notification or remediation actions. - -## Capabilities - -### New Capabilities - -- `mcp-monitoring`: Privacy-preserving, read-only activity time series and operational health exposed through authenticated MCP clients. - -### Modified Capabilities - -- `teacher-content`: Extend privacy-preserving monitoring to conversational aggregate activity queries within the caller's class and subject scope. -- `access-and-privacy`: Define the sensitive operational data that MCP health responses must not disclose. - -## Impact - -- Affects `app/grok_mcp.py`, `app/monitoring.py`, bounded attempt reads in `app/auth.py`, and aggregate access to the existing in-memory live-session store. -- Adds focused MCP, authorization, aggregation, recency, privacy, deduplication, degraded-storage, and health tests; no database migration or new persistence is expected. -- Extends README and the Hebrew teacher guide with safe example prompts. -- Does not add paid API calls, browser UI changes, write operations, new credentials, deployment configuration, tracking events, or retention-policy changes. diff --git a/openspec/changes/add-mcp-monitoring-tools/specs/access-and-privacy/spec.md b/openspec/changes/add-mcp-monitoring-tools/specs/access-and-privacy/spec.md deleted file mode 100644 index 19b928a0b0a0084be877d48aedbd0f4b647564b5..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-monitoring-tools/specs/access-and-privacy/spec.md +++ /dev/null @@ -1,38 +0,0 @@ -# Spec Delta - -## ADDED Requirements - -### Requirement: Operational diagnostic isolation -Remote monitoring responses SHALL NOT disclose raw logs, stack traces, database queries, internal service addresses, environment variables, credentials, tokens, or secret presence values. - -#### Scenario: Component check fails -- **WHEN** a component health check raises an internal error -- **THEN** the remote response maps the result to a coarse status -- **AND** the raw error text and internal target are retained only in server-side logging - -#### Scenario: Developer requests health -- **WHEN** a caller with the developer role requests system health through MCP -- **THEN** any additional diagnostic fields remain non-sensitive -- **AND** developer status does not permit secrets or raw logs to appear in the response - -#### Scenario: Unauthenticated caller requests monitoring -- **WHEN** a caller without a valid OAuth access token requests any monitoring tool -- **THEN** the MCP transport rejects the request before monitoring data is evaluated - -### Requirement: Aggregate recency privacy -Remote monitoring SHALL NOT expose participant identities, per-learner presence, individual activity times, or timestamps derived from a sample below the existing privacy threshold. An aggregate latest timestamp MAY be returned only after the qualifying scoped sample passes its threshold. - -#### Scenario: Small recent sample exists -- **WHEN** recent scoped activity exists for fewer than the required learners or attempts -- **THEN** the response reports `suppressed` -- **AND** it omits the exact count, latest timestamp, elapsed time, and learner identifiers - -#### Scenario: Combined classes overlap -- **WHEN** an authorized manager requests all visible classes and one learner belongs to more than one class -- **THEN** that learner contributes at most once to distinct-user and live-presence counts - -#### Scenario: Aggregate timestamp qualifies -- **WHEN** the scoped sample passes the applicable privacy threshold -- **THEN** the response MAY return the latest aggregate event timestamp and safe event type -- **AND** it does not return the learner or event payload associated with that timestamp - diff --git a/openspec/changes/add-mcp-monitoring-tools/specs/mcp-monitoring/spec.md b/openspec/changes/add-mcp-monitoring-tools/specs/mcp-monitoring/spec.md deleted file mode 100644 index a3a004c7ab4f8c83c24ff4bf90a8dc0b2d1c3661..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-monitoring-tools/specs/mcp-monitoring/spec.md +++ /dev/null @@ -1,166 +0,0 @@ -# Spec Delta - -## Purpose - -Provide authorized MCP clients with bounded, privacy-preserving learning activity trends and a safe operational status summary without exposing learner-level or infrastructure-sensitive data. - -## ADDED Requirements - -### Requirement: Scoped activity time series -The MCP service SHALL return aggregate attempt and active-student buckets only for learners in the caller's current authorized class and subject scope. - -#### Scenario: Teacher requests one authorized class -- **GIVEN** a teacher is authorized for the requested class -- **WHEN** the teacher requests its activity time series -- **THEN** the service returns ordered aggregate buckets for that class -- **AND** no learner identity, score, answer, recording, or exact login event is returned - -#### Scenario: Subject administrator requests all visible activity -- **WHEN** a subject administrator omits the class identifier -- **THEN** the service aggregates only classes in that administrator's current subject scope - -#### Scenario: Caller requests an out-of-scope class -- **WHEN** a caller requests a class outside the caller's current scope -- **THEN** the service refuses the request without confirming whether the class exists - -### Requirement: Bounded activity query -The activity tool SHALL accept daily or weekly buckets for an inclusive date range of at most 90 days and SHALL reject invalid, future-only, reversed, or oversized ranges. - -#### Scenario: Valid daily range -- **WHEN** an authorized caller requests a valid range of 30 days with daily buckets -- **THEN** the service returns normalized dates and zero-filled buckets for days without activity - -#### Scenario: Oversized range -- **WHEN** a caller requests more than 90 days -- **THEN** the service refuses the request before querying activity storage - -#### Scenario: Weekly aggregation -- **WHEN** a caller selects weekly buckets -- **THEN** the service returns buckets aligned to stable calendar-week boundaries in the application timezone - -### Requirement: Safe activity degradation -The activity tool SHALL return an explicit unavailable response when activity persistence cannot be queried and SHALL not substitute fabricated or stale activity values. - -#### Scenario: Supabase is not configured -- **WHEN** an authorized MCP request is handled without configured activity persistence -- **THEN** the service reports that activity monitoring is unavailable -- **AND** it returns no time-series values - -#### Scenario: Aggregate RPC is absent -- **WHEN** the optimized aggregate query is unavailable but the existing bounded compatibility query succeeds -- **THEN** the service returns the correctly scoped aggregate result - -### Requirement: Current activity scope and windows -The MCP service SHALL provide a read-only current-activity report for one authorized class or the caller's combined authorized scope, using a 5- or 15-minute recent-practice window and deduplicating learners across overlapping class rosters. - -#### Scenario: Teacher selects an authorized class -- **WHEN** a teacher requests current activity for an authorized class and a supported window -- **THEN** only that class's scoped data contributes to the response - -#### Scenario: Manager selects all visible classes -- **WHEN** an authorized manager omits the class identifier -- **THEN** each learner and persisted event is counted once even when the learner belongs to multiple visible classes - -#### Scenario: Unsupported window is requested -- **WHEN** the caller supplies a window other than 5 or 15 minutes -- **THEN** the service refuses the request before reading activity data - -### Requirement: Recent practice is not presence -The current-activity report SHALL count distinct learners and attempts from persisted practice events in the selected window, and SHALL state that a recent event does not prove the learner is connected at query time. - -#### Scenario: Persisted attempts exist in the window -- **WHEN** qualifying scoped attempts satisfy the privacy thresholds -- **THEN** the response returns aggregate distinct learners and attempt count for the named window -- **AND** it does not label those learners online or currently connected - -#### Scenario: No attempt exists in the window -- **WHEN** the source is available but no scoped persisted attempt falls in the selected window -- **THEN** recent practice has status `no_activity` -- **AND** the response does not use a suppressed or unavailable value as a numeric zero - -### Requirement: Explicit live-session presence -The service SHALL report users active now only from an active explicit live-class heartbeat source, and SHALL disclose the heartbeat TTL, check time, ephemeral scope, and source freshness. - -#### Scenario: Reliable live session is active -- **WHEN** the selected authorized scope has an active live-class session -- **THEN** a participant is active now only when its last heartbeat is within the existing 20-second TTL at check time -- **AND** the response contains no participant identity - -#### Scenario: No live-session presence source applies -- **WHEN** there is no active live-class session in the selected scope -- **THEN** live presence is `unavailable` -- **AND** recent persisted practice is not substituted as presence - -#### Scenario: Service restarts -- **WHEN** the process-local live-session state is lost on restart -- **THEN** the service does not reconstruct or claim historical presence - -### Requirement: Last available activity time -The service SHALL return the latest qualifying persisted practice event as UTC ISO 8601, an `Asia/Jerusalem` display value, elapsed time at a stated check instant, and a safe event type, while reporting general application activity as unavailable when no such event is stored. - -#### Scenario: Latest practice event qualifies -- **WHEN** scoped attempt data satisfies privacy thresholds -- **THEN** the response returns `last_practice_activity` with event type `practice_attempt`, its aggregate latest timestamp, elapsed seconds, timezone, and check time - -#### Scenario: General application activity is not stored -- **WHEN** the application has no durable general activity event for the selected scope -- **THEN** `last_application_activity` is `unavailable` -- **AND** absence of a practice attempt is not described as absence of application use - -### Requirement: Current activity privacy and data states -Current-activity metrics and timestamps SHALL use the existing minimum of three distinct learners and five attempts where attempt-derived data is displayed, SHALL suppress live counts of one or two, and SHALL distinguish `no_activity`, `no_data`, `unavailable`, and `suppressed`. - -#### Scenario: Recent practice sample is too small -- **WHEN** persisted data exists but has fewer than three learners or five attempts in the selected window -- **THEN** attempt-derived counts and timestamps are `suppressed` -- **AND** no small exact value is returned - -#### Scenario: Live count is too small -- **WHEN** reliable live-session presence finds one or two active learners -- **THEN** the active-user count is `suppressed` - -#### Scenario: Source has no observations -- **WHEN** a source can be queried but has no stored observation from which an activity time can be established -- **THEN** the applicable last-activity result is `no_data` - -#### Scenario: Source query fails -- **WHEN** the required persistence or live source cannot be queried -- **THEN** the affected result is `unavailable` without internal error details - -### Requirement: Current activity freshness -The current-activity response SHALL identify its source, query time, supported update semantics, and any known collection or aggregation delay, and SHALL not call persisted attempt data real time. - -#### Scenario: Attempt data is returned -- **WHEN** recent practice or last-practice data is available -- **THEN** the response states that the source is persisted attempts and reports the query check time -- **AND** it describes the data as current as of the database read rather than real time - -#### Scenario: Live presence is returned -- **WHEN** live-session presence is available -- **THEN** the response reports the heartbeat TTL and check time as its freshness boundary - -### Requirement: Safe system health -The MCP service SHALL return a read-only health summary using a bounded status vocabulary and SHALL expose only details appropriate to the caller's verified management role. - -#### Scenario: Teacher checks health -- **WHEN** an authorized teacher requests system health -- **THEN** the response reports an overall status, check timestamp, and coarse application, authentication, database, model, and text-to-speech component statuses -- **AND** technical failure details are omitted - -#### Scenario: Developer checks health -- **WHEN** a verified developer requests system health -- **THEN** the response MAY include non-sensitive build identifiers and component availability flags -- **AND** it still excludes raw errors and secret configuration - -#### Scenario: Optional component is absent -- **WHEN** an optional component such as text-to-speech is not configured -- **THEN** that component is reported as `not_configured` -- **AND** the whole service is not reported as failed solely for that reason - -### Requirement: Monitoring tools are read-only -Monitoring MCP calls SHALL NOT mutate class, learner, content, audit, configuration, or infrastructure state and SHALL NOT require a prepare/publish approval. - -#### Scenario: Monitoring call completes -- **WHEN** any monitoring tool succeeds or fails -- **THEN** no application record is created, updated, or deleted as a consequence of the call - diff --git a/openspec/changes/add-mcp-monitoring-tools/specs/teacher-content/spec.md b/openspec/changes/add-mcp-monitoring-tools/specs/teacher-content/spec.md deleted file mode 100644 index 8f01b72fed16f8dd148d4b2f50f550da4c955f5d..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-monitoring-tools/specs/teacher-content/spec.md +++ /dev/null @@ -1,28 +0,0 @@ -# Spec Delta - -## MODIFIED Requirements - -### Requirement: Privacy-preserving monitoring -Teacher activity views and conversational monitoring tools SHALL expose only aggregate data within the caller's scope, SHALL keep current live-session presence distinct from recent persisted practice, and pronunciation-pattern reports SHALL not return learner identities. - -#### Scenario: Repeated pronunciation issue becomes a candidate -- **GIVEN** the same judgment occurred in at least five attempts by at least three learners and at least 30 percent of learners who attempted the phrase -- **WHEN** a teacher opens pronunciation patterns -- **THEN** the pattern appears as an investigation candidate -- **AND** it is not automatically applied as a calibration - -#### Scenario: Teacher asks for an activity graph -- **WHEN** a teacher requests aggregate activity through an authenticated conversational connector -- **THEN** the returned series contains only activity belonging to the teacher's current class scope -- **AND** it contains no learner identities, recordings, answers, or individual scores - -#### Scenario: Teacher asks who is active now -- **WHEN** a teacher requests current activity through an authenticated conversational connector -- **THEN** the response uses live-session presence only when a qualifying explicit live session exists -- **AND** otherwise reports privacy-thresholded recent practice without claiming those learners remain connected - -#### Scenario: Teacher asks for the last activity time -- **WHEN** a teacher requests the latest activity in an authorized scope -- **THEN** the response distinguishes the latest stored practice attempt from unavailable general application activity -- **AND** it does not infer that the application is unused from missing practice data - diff --git a/openspec/changes/add-mcp-monitoring-tools/tasks.md b/openspec/changes/add-mcp-monitoring-tools/tasks.md deleted file mode 100644 index 548b75a389474f9ef8bd48d3887d029bcd81db86..0000000000000000000000000000000000000000 --- a/openspec/changes/add-mcp-monitoring-tools/tasks.md +++ /dev/null @@ -1,31 +0,0 @@ -# Tasks - -## 1. Scoped activity service - -- [x] 1.1 Add a pure monitoring service that validates inclusive ISO date ranges and `day`/`week` intervals, enforces the 90-day bound, resolves one class or all visible classes from the verified actor, and verify unit tests cover invalid, reversed, future-only, oversized, unknown, and out-of-scope requests. -- [x] 1.2 Aggregate the existing scoped activity source into ordered Asia/Jerusalem daily or weekly buckets with explicit zero buckets and no fabricated distinct-student counts; verify tests cover timezone boundaries, empty periods, class combination, identity omission, and unavailable persistence. - -## 2. Read-only MCP tools - -- [x] 2.1 Add `get_activity_timeseries` to the MCP server using only OAuth actor context and the shared service; verify MCP schema/discovery and end-to-end calls expose aggregates only and perform no prepare/publish or audit write. -- [x] 2.2 Add `get_system_health` with whitelisted enum component statuses, a bounded read-only database check, and developer-only build metadata; verify tests cover teacher/developer output, optional components, database failure, and absence of errors, secrets, URLs, paths, tokens, and configuration values. -- [x] 2.3 Update the exact MCP tool allowlist test to include the two monitoring tools and verify no raw-log, SQL, environment, per-student timeline, remediation, or general infrastructure tool is discoverable. - -## 3. Current activity and recency - -- [x] 3.1 Implement a bounded `get_current_activity` monitoring service over scoped persisted attempts with 5/15-minute windows, aggregate last-practice timestamps, UTC and Asia/Jerusalem representations, elapsed time, explicit data states, existing privacy floors, and cross-class learner deduplication; verify unit tests cover window edges, authorization, overlapping rosters, thresholds, no activity, no data, unavailable persistence, and suppressed timestamps. -- [x] 3.2 Add an identity-free adapter for the existing process-local live-session heartbeat state without adding tracking or persistence, return presence only for authorized active live sessions with the 20-second TTL and check time, and verify tests cover online/offline boundaries, counts of zero/one/two/three, multiple classes, restart-equivalent missing state, and absence of participant fields. -- [x] 3.3 Register `get_current_activity` as a read-only MCP tool, keep recent practice distinct from current presence, return general application activity as unavailable, and document metric definitions, source freshness, status meanings, limitations, Hebrew prompts, and an example JSON response; verify connector tests cover schema/discovery, safe errors, read-only behavior, no identities, no audit writes, and documentation/schema agreement. - -## 4. Documentation and integration - -- [x] 4.1 Document Hebrew example prompts, date/interval limits, class scope, health status meanings, privacy exclusions, and read-only behavior in README and the teacher guide; verify HTML parses and the documented tool arguments match runtime schemas. -- [ ] 4.2 Run focused monitoring, MCP, OAuth, activity, live-session, and privacy tests followed by the full suite and strict OpenSpec validation; after deployment, verify the existing connector discovers all three monitoring tools without OAuth reconfiguration and that read-only queries do not alter management activity. - -Verification record (2026-10-27): all 48 focused monitoring, activity, and live-session tests pass; the focused MCP discovery/current-activity tests pass; Python compilation, `git diff --check`, HTML parsing, and strict OpenSpec validation pass. The full suite was attempted with a 120-second bound and stalled in the pre-existing `test_existing_library_api_is_compatible_and_mcp_token_has_no_api_authority` ASGI lifespan teardown after 121 passing tests. Production deployment and connector verification have not been performed. - -## Workflow follow-up - -- Obtain explicit approval before any database migration, new activity collection, retention-policy change, or production deployment. -- Archive the change after production connector verification and review requirements are satisfied. - diff --git a/openspec/changes/archive/2026-10-24-add-focused-learner-home/design.md b/openspec/changes/archive/2026-10-24-add-focused-learner-home/design.md deleted file mode 100644 index 85437c5f73c8e799b891381ff2e2e5df8e8d8c19..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-focused-learner-home/design.md +++ /dev/null @@ -1,217 +0,0 @@ -# Design - -## Context - -See `proposal.md` for motivation. The browser currently chooses one active class, renders its entire path, marks the first unfinished topic as current, and scrolls that topic into view. Class selection is remembered locally by class name, while phrase scores are restored from the server for signed-in learners. Classes expose a list of lesson IDs but `/api/me` does not expose their IDs or any per-student, per-class navigation state. - -Homework and ten-item daily practice already exist. The focused home should compose those capabilities rather than create a second progress system. Supabase is optional, so loss of server persistence must not make practice unavailable. - -Daily-video push reminders also already exist (`app/push.py`). On entry, the browser shows a "turn on reminders?" prompt (`askPushSoon`) once any open dialog closes, re-asking after seven days when declined. A reminder tap opens `/?view=daily`, which the entry code turns into the daily videos screen. Whether a daily video was watched is kept only in that device's browser storage (`ihki.dailyWatched`). Math classes have no daily videos. - -## Visual overview - -### System flow - -- **Browser** (active class, focused home, local fallback) calls `GET /api/me`. The server answers with each class and the learner's saved lesson. -- **FastAPI server** checks class membership and validates the lesson. It is the only part that reads or writes Supabase, using the server-only key. -- **Supabase** keeps the `learner_class_state` table: one row per email and `class_id`. -- **Browser storage** keeps the choice when the server is unavailable, under a key made of the subject, the signed-in email and the class. - -### Entry-state flow - -1. **Not signed in, or not allowed to use the path:** the existing entry behavior, unchanged. -2. **Opened from a daily-video reminder:** the daily videos screen opens first. When the student goes back to the home, continue from step 3. -3. **In more than one class:** the student chooses the active class. -4. **The class doesn't resolve to exactly one level:** show the message asking to fix the class configuration, and stop here. -5. **No valid saved current lesson:** lesson onboarding. -6. **Focused learner home.** From here the student can launch a recommended action, change the current lesson, or open the complete path at the current lesson. - -### Mobile wireframe: lesson onboarding - -```text -+----------------------------------+ -| אהלא וסהלא | -| | -| באיזה שיעור הכיתה עכשיו? | -| כיתת מתחילים · שלב א | -| | -| ( ) שיעור 1 | -| ( ) שיעור 2 | -| ( ) שיעור 3 | -| ( ) שיעור 4 | -| (*) שיעור 5 | -| | -| +----------------------------+ | -| | ממשיכים | | -| +----------------------------+ | -+----------------------------------+ -``` - -### Mobile wireframe: focused home - -```text -+----------------------------------+ -| אהלא וסהלא | -| | -| כיתת מתחילים · שיעור 5 שינוי | -| | -| +----------------------------+ | -| | שיעורי בית | | -| | 3 משפטים · עד מחר | | -| | התחילו --> | | -| +----------------------------+ | -| | -| +----------------------------+ | -| | הסרטון של היום | | -| | סרטון יומי חדש | | -| | צפו --> | | -| +----------------------------+ | -| | -| +----------------------------+ | -| | ממשיכים בשיעור 5 | | -| | משפטים: עברית -> ערבית | | -| | המשיכו --> | | -| +----------------------------+ | -| | -| צפייה במסלול המלא | -+----------------------------------+ -``` - -### Semantic HTML mock (non-functional) - -This mock communicates hierarchy and accessible structure. It is documentation, not implementation-ready markup, and spec viewers must display it as inert source rather than execute it. - -```html -
-
-

כיתת מתחילים · שיעור 5

-

מה עושים עכשיו?

- -
- -
    -
  1. - -
  2. -
  3. - -
  4. -
  5. - -
  6. -
- - -
-``` - -## Goals / Non-Goals - -**Goals:** - -- Establish a stable current lesson for each student/class pair before presenting recommendations. -- Make a small recommendation set the normal student entry point while retaining the full path. -- Synchronize the selection across devices without allowing a student to read or modify another student's state. -- Reuse existing progress, homework, topic-opening, and daily-practice behavior. -- Work in the existing single-page, mobile-first RTL interface with touch and keyboard. - -**Non-Goals:** - -- Letting a teacher set or override every student's current lesson. -- Automatically advancing the class lesson when one student completes it. -- Adding notification categories, schedules, or permission prompts. -- Changing push delivery, the reminder hour, admin broadcasts, the service worker, or the `push_subs` table. -- Replacing the existing path, homework model, completion rules, scoring, or paid tutor behavior. - -## Decisions - -### Store one current lesson per authenticated student and class - -Add a server-managed `learner_class_state` table with `email`, `class_id`, `current_lesson`, and `updated_at`, unique on `(email, class_id)`. Keep row-level security enabled and revoke direct client roles, following the existing server-secret persistence pattern. The API derives the normalized email from the authenticated session; it never accepts an email from the request body. - -Using the email matches current class membership and avoids introducing a second identity mapping in this change. A separate table is preferred to embedding selections in `classes`, because the state belongs to one learner and must not change the class for everyone. It is preferred to browser-only storage because signed-in progress is already expected to follow learners across devices. - -### Return state with class bootstrap data and update it through a narrow endpoint - -Include each class's stable numeric `id` and saved `current_lesson` in `/api/me`. Add `PUT /api/me/classes/{class_id}/current-lesson` with `{ "lesson": "" }`. - -The update endpoint must verify that the caller is a student member of the class, the class resolves to exactly one course level, and the lesson belongs to both that class and level. It upserts only the caller's row. Returning the state in `/api/me` avoids an extra startup request; a dedicated mutation keeps class administration separate from learner-owned state. - -### Treat ambiguous class levels as a configuration error - -The existing class model stores lesson IDs rather than a level field. Determine the class level from those lessons. If there are no resolvable lessons or they span multiple levels, onboarding shows a corrective message instead of guessing or exposing unrelated lessons. Existing classes are not rewritten automatically. - -Math classes are left out: their `lessons` column holds math units, and they use their own path (`/math`), so they are never asked for a current lesson and keep that path as their entry view. A class's own teachers (listed in its `teachers`) are also not asked, since they keep the path for that class. - -Adding a new `level` column to `classes` was considered, but it would duplicate information already encoded by the class lesson set and require a broader class migration. Enforcing a single-level invariant throughout class administration can be a follow-up if legacy data reveals ambiguity. - -### Use account-scoped local fallback state - -When the state endpoint is unavailable, keep the selected lesson in browser storage under a versioned key containing the subject, normalized signed-in email, and class ID. Server state wins when it is available. A locally saved selection may be retried on a later authenticated load if it is still valid for the class. - -Do not use the current unscoped `ihki.class` pattern for lesson state: another account on the same device must not inherit it. Open mode has no authenticated student/class pair and continues to use the current path behavior rather than showing this onboarding. - -### Build recommendations with a deterministic priority - -Render no more than three distinct actions: the first three that apply, in this order: - -1. the nearest-due incomplete homework, when one exists; -2. today's daily video for the active class, when one is dated today and has not been opened on this device; -3. the first unfinished activity in the selected current lesson; -4. a short review of up to ten weak, previously practiced items, or another valid short activity from the current lesson. - -The daily-video action reuses the existing daily-video list and watched state (`dailyVideos()`, `ihki.dailyWatched`) and opens the same player as the daily videos screen. Placing it second matches what the morning reminder promises the learner, while homework keeps priority because it has a due date. - -Omit an action when its target cannot be opened. Do not silently fill the home with arbitrary later lessons. If the current lesson is complete, offer review plus a clear action to change the selected lesson. Existing topic and homework entry functions remain the execution paths, preventing recommendation state from becoming a second completion ledger. - -The focused home's short activity is capped at ten items so its displayed workload is truthful. It uses the existing session execution path, while the separate daily-practice shortcut is no longer shown on the complete path. - -### Make the full path secondary and position by lesson - -The focused home becomes the default student view after class selection and onboarding. A visible full-path action calls the existing path renderer with a lesson anchor. Position the selected lesson heading or its first unfinished topic in the viewport, even if an earlier lesson remains unfinished. Preserve navigation to all lessons assigned to the class. - -Keep the complete “הערבית שכבר שלכם” progress panel on the focused home. Keep the daily-video and homework actions there as well, without duplicating them above the full path. Do not show the daily-practice shortcut on either the focused home or the full path. Move “חבר מביא חבר” from the focused learner's bottom navigation to a secondary home action beside full-path access; retain its existing screen and keep its navigation tab for modes that have no focused home. - -### Keep onboarding separate from notification consent - -Do not request browser notification permission during first-use onboarding, and do not show the existing reminder prompt at all on the visit where onboarding happens. The current prompt waits only for open dialogs to close, so without this rule it would appear immediately after the lesson question. Skip `askPushSoon` for that visit; from the next entry on, its existing once-and-again-after-seven-days rule applies unchanged. Reminder types and preferences need their own product and delivery rules; combining them with the required lesson question would add pressure and complexity to the learner's first task. - -### Let a reminder's link take priority over the home - -When the page opens with `?view=daily` (a reminder tap), open the daily videos screen directly, as today, even if the student has no saved current lesson. Onboarding and the focused home run when the student goes back to the home. The reminder promises a video, so putting a required question in front of it would break that promise. Admin broadcast notifications open `/` and follow the normal entry flow. - -### Accessibility and interaction - -Use native buttons or radios with Hebrew labels, an explicit confirmation action, visible focus, and a dialog or screen title announced to assistive technology. On opening, focus the prompt heading or selected option; after confirmation, focus the focused-home heading without unexpected scrolling. All controls must remain usable in RTL layout, by touch, and without a mouse. Respect reduced-motion settings when changing views or positioning the path. - -## Risks / Trade-offs - -- **[A student may choose the wrong lesson]** -> Keep a visible change-lesson action and never delete progress when the selection changes. -- **[Legacy classes may span multiple levels]** -> Detect ambiguity, show a corrective state, and avoid guessing; do not block unrelated class and teacher functions. -- **[Server persistence may fail after a local selection]** -> Keep account-scoped local state, continue learning, and retry only after validating it against current class data. -- **[Recommendations could duplicate the same homework/topic]** -> Deduplicate by target and apply the fixed priority order before rendering. -- **[A focused home can hide the learner's broader progress]** -> Keep the full path one explicit action away and preserve all existing progress visualization there. -- **[Daily-video watched state is per device]** -> A video opened on a phone may still be recommended on a laptop. Accept this for now: the cost is one extra card, and moving watched state to the server is outside this change. -- **[Prompts could stack on first entry]** -> Class choice, lesson onboarding, and the reminder prompt are all dialogs; skip the reminder prompt on the onboarding visit so a new student sees at most the two required questions. -- **[Email changes would orphan preference rows]** -> Accept this limitation because membership currently uses email; a future stable-user-ID migration should migrate this table with other learner-owned records. - -## Migration Plan - -1. Add the `learner_class_state` table, constraints, index/uniqueness, RLS, and revoked direct-client access. -2. Deploy server reads and the authenticated update endpoint while leaving the existing path as the entry view. -3. Deploy browser onboarding, local fallback, focused recommendations, and lesson-positioned path access. -4. Verify existing classes resolve to one level and document remediation for ambiguous legacy classes. -5. Roll back the browser entry-point change first if necessary; the additive endpoint and table can remain without affecting the existing path. Remove them only after confirming no deployed client uses them. - diff --git a/openspec/changes/archive/2026-10-24-add-focused-learner-home/proposal.md b/openspec/changes/archive/2026-10-24-add-focused-learner-home/proposal.md deleted file mode 100644 index e7b0ad631eaf58b6da7a1f0d708045558a9403fe..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-focused-learner-home/proposal.md +++ /dev/null @@ -1,66 +0,0 @@ -# Proposal - -## Why - -The full learning path asks learners to orient themselves among many lessons and topics before they can act, so a learner whose class is currently on lesson 5 can feel lost or repeatedly scroll past earlier work. The application should establish the learner's current class lesson once and use it to present a small, clear set of useful next actions. - -### Experience at a glance - -```text -BEFORE AFTER -+----------------------------+ +----------------------------+ -| Full course path | | What should I do now? | -| Lesson 1 | | | -| topic | | 1. Homework due tomorrow | -| topic | | 2. Today's daily video | -| Lesson 2 | | 3. Continue lesson 5 | -| topic | | | -| ... | | [Open the complete path] | -| Lesson 5 <-- scroll here | +----------------------------+ -+----------------------------+ - -Learner must find the work. The app presents the work. -``` - -**How a learner moves through it** - -1. **First entry into a class:** the learner chooses the class's current lesson. -2. **Focused learner home:** shows up to three actions, in this order: - - Homework - - Today's daily video - - Continue lesson - - Short review - - Each action returns to the home when it ends. -3. **Complete path:** one tap away from the home, opened at the selected lesson. - -A morning reminder tap skips the home and opens today's daily video directly. - -## What Changes - -- Add first-use onboarding for each student and class that asks which lesson the class is currently studying, with choices restricted to that class's single level. -- Persist the selected current lesson per student and class, restore it across devices when server persistence is available, and retain it locally when it is not. -- Add a focused learner home that presents at most three recommended actions, in priority order: unfinished homework, today's daily video when it has not been watched, continuing the selected lesson, and a short review or practice activity. -- Coordinate with the existing daily-video push reminders: the reminder-permission prompt is not shown on the visit where lesson onboarding happens, and tapping a reminder still opens the daily videos directly. -- Keep the complete learning path available as a secondary view and open it around the selected current lesson rather than requiring manual scrolling from the beginning. -- Allow the learner to change the current lesson without deleting existing progress. -- Preserve RTL, touch, keyboard, and no-mouse use throughout onboarding and the focused home. -- Keep notification preferences and new reminder types out of scope; they will be planned as a separate change. Push delivery, the reminder hour, admin broadcasts, and the `push_subs` table are unchanged. Teacher-controlled current-lesson assignment, changes to scoring, and paid AI behavior are also out of scope. - -## Capabilities - -### New Capabilities - -- `learner-home`: Current-lesson onboarding, per-class resume context, focused recommendations, and access to the full path. - -### Modified Capabilities - -- `progress-and-practice`: Use the learner's selected current lesson, rather than the first unfinished lesson across the path, when choosing new material for daily practice. - -## Impact - -- Browser UI and state in `static/index.html`, including sign-in/class selection, initial navigation, path positioning, daily-practice selection, the entry reminder prompt, and handling of the reminder's `?view=daily` link. `app/push.py` and the service worker are not changed. -- FastAPI endpoints in `app/main.py` and Supabase access in `app/auth.py` for reading and updating a student's per-class current lesson. -- A Supabase migration for learner/class onboarding state; only the authenticated learner may change or read their own selection, while existing class membership remains authoritative. -- Local browser storage as a degraded fallback when Supabase is absent or temporarily unavailable; local state must be scoped so one signed-in learner cannot inherit another learner's selection. -- Existing progress and homework data are read but not replaced or deleted. No new paid API usage or third-party dependency is introduced. diff --git a/openspec/changes/archive/2026-10-24-add-focused-learner-home/specs/learner-home/spec.md b/openspec/changes/archive/2026-10-24-add-focused-learner-home/specs/learner-home/spec.md deleted file mode 100644 index e8e0ed7b44123674c7f90a9fa5c64d34295f532d..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-focused-learner-home/specs/learner-home/spec.md +++ /dev/null @@ -1,160 +0,0 @@ -# Spec Delta - -## Purpose - -Define how a learner establishes their current class lesson and receives a small, accessible set of relevant next actions instead of navigating the complete course path first. - -**Behavior map (non-normative):** - -- A student entering a class goes straight to the focused home when a valid lesson is saved; otherwise they first choose a lesson from the class's level. -- From the focused home: homework, today's daily video, continue, short review, refer a friend, the full path at the current lesson, or change the current lesson. -- A morning reminder tap opens the daily videos screen. Going back to the home runs the same saved-lesson check. - -## ADDED Requirements - -### Requirement: Per-class current-lesson onboarding -The application SHALL require an authenticated student to select the current lesson the first time they enter a class for which they have no saved selection, and SHALL restrict the choices to lessons in that class's single level. - -#### Scenario: Student enters a class for the first time -- **GIVEN** the student has no saved current lesson for the selected class -- **WHEN** the student enters the learning experience -- **THEN** the application asks which lesson the class is currently studying -- **AND** offers only lessons belonging to that class's level -- **AND** waits for a selection before showing the focused learner home - -#### Scenario: Student has multiple classes -- **GIVEN** the student belongs to more than one class -- **WHEN** the student selects a class with no saved current lesson -- **THEN** the application asks for the current lesson of that class -- **AND** keeps that selection independent of selections for the student's other classes - -#### Scenario: Class configuration does not identify one level -- **GIVEN** the selected class contains lessons from zero or multiple levels -- **WHEN** current-lesson onboarding would begin -- **THEN** the application explains that the class configuration must be corrected -- **AND** does not offer lessons from an ambiguous level - -### Requirement: Current-lesson persistence -The application SHALL associate the selected current lesson with the authenticated student and class, restore it across devices when server persistence is available, and allow the student to change it without altering learning progress. - -#### Scenario: Student returns on another device -- **GIVEN** the student previously selected lesson 5 for the class -- **AND** server persistence is available -- **WHEN** the student enters that class on another device -- **THEN** lesson 5 is restored as the current lesson -- **AND** onboarding is not shown again - -#### Scenario: Student changes the current lesson -- **GIVEN** the student's current lesson is lesson 5 -- **WHEN** the student changes it to lesson 6 -- **THEN** lesson 6 becomes the source of focused recommendations -- **AND** progress already recorded for lesson 5 remains unchanged - -#### Scenario: Server persistence is unavailable -- **GIVEN** the server cannot read or save the current-lesson selection -- **WHEN** the student selects a lesson -- **THEN** the application retains the selection on that device when browser storage is available -- **AND** keeps the learning experience usable -- **AND** does not expose one student's selection to another student using the device - -### Requirement: Focused learner home -The application SHALL make a focused home the student's primary post-onboarding view and SHALL show no more than three recommended actions derived from the selected class, current lesson, homework, today's daily videos, and progress. When more than three actions apply, the application SHALL keep the first three in this order: nearest-due incomplete homework, today's unwatched daily video, unfinished work in the current lesson, and a short review. - -#### Scenario: Incomplete homework exists -- **GIVEN** the selected class has incomplete homework -- **WHEN** the focused home is shown -- **THEN** an action for the nearest-due incomplete homework is included -- **AND** its due or overdue state is visible - -#### Scenario: Current lesson has unfinished work -- **GIVEN** the current lesson contains unfinished activities -- **WHEN** the focused home is shown -- **THEN** an action continues unfinished work in that lesson -- **AND** the learner does not need to scroll through earlier lessons - -#### Scenario: Today's daily video has not been watched -- **GIVEN** the selected class has a daily video dated today -- **AND** the video has not been opened on this device -- **WHEN** the focused home is shown -- **THEN** an action that opens today's daily video is included -- **AND** it is placed after any homework action and before the continue-lesson action - -#### Scenario: No daily video to recommend -- **GIVEN** the selected class has no daily video dated today, or today's video has already been opened on this device -- **WHEN** the focused home is shown -- **THEN** no daily-video action is shown -- **AND** the remaining actions keep their priority order - -#### Scenario: Current lesson is complete -- **GIVEN** the learner has completed the selected current lesson -- **WHEN** the focused home is shown -- **THEN** the application offers a short review or allows the learner to select a different current lesson -- **AND** does not silently advance the class's current lesson - -### Requirement: Short recommended activity -When the focused home shows a short activity, it SHALL be bounded to no more than ten items and SHALL show its workload before the learner starts it. The short activity MAY be omitted when three higher-priority actions apply. - -#### Scenario: Three higher-priority actions apply -- **GIVEN** incomplete homework, an unwatched daily video for today, and unfinished work in the current lesson all exist -- **WHEN** the focused home is shown -- **THEN** those three actions are shown -- **AND** the short activity is not shown - -#### Scenario: Learner starts a short activity -- **WHEN** the learner activates the short recommendation -- **THEN** the session contains no more than ten items -- **AND** completion is clearly announced when the session ends - -#### Scenario: Recommended material is unavailable -- **GIVEN** the current lesson has insufficient available material for the recommendation -- **WHEN** the focused home is built -- **THEN** the application offers another valid action or omits that recommendation -- **AND** never shows an action that cannot be started - -### Requirement: Full-path access and positioning -The focused home SHALL provide access to the complete learning path, and the path SHALL open at the selected current lesson while preserving access to earlier and later class lessons. - -#### Scenario: Learner opens the complete path -- **GIVEN** the student's current lesson is lesson 5 -- **WHEN** the learner chooses the complete path -- **THEN** the path positions lesson 5 in view -- **AND** earlier and later lessons assigned to the class remain accessible -- **AND** the path does not show daily-practice or homework shortcut buttons above it - -### Requirement: Referral access from home -The focused home SHALL provide access to the existing refer-a-friend screen without counting that access as one of the three recommended learning actions, and SHALL remove the duplicate referral tab from the focused learner's bottom navigation. - -#### Scenario: Learner opens refer a friend -- **WHEN** the learner activates “חבר מביא חבר” on the focused home -- **THEN** the existing referral screen opens -- **AND** the referral action is not duplicated in the bottom navigation - -### Requirement: Reminder coordination -Current-lesson onboarding and the focused home SHALL work with the existing daily-video push reminders without stacking prompts or blocking a reminder's destination. - -#### Scenario: Onboarding visit shows no reminder prompt -- **GIVEN** the student completes current-lesson onboarding during this visit -- **WHEN** the focused home appears -- **THEN** the prompt to turn on daily-video reminders is not shown during this visit -- **AND** on a later entry the existing reminder-prompt rules apply - -#### Scenario: Reminder opened before a lesson is chosen -- **GIVEN** the student has no saved current lesson for the active class -- **WHEN** the student opens the application from a daily-video reminder -- **THEN** the daily videos screen opens directly without onboarding first -- **AND** onboarding is shown when the student goes to the home - -#### Scenario: Reminder opened with a saved lesson -- **GIVEN** the student has a saved current lesson for the active class -- **WHEN** the student opens the application from a daily-video reminder -- **THEN** the daily videos screen opens directly -- **AND** returning from it shows the focused home - -### Requirement: Accessible focused navigation -Current-lesson onboarding and focused-home actions SHALL support RTL layout, touch, keyboard-only operation, visible focus, and meaningful accessible names. - -#### Scenario: Keyboard-only onboarding -- **WHEN** a learner completes onboarding without a pointing device -- **THEN** every lesson option and confirmation action is reachable and operable by keyboard -- **AND** focus moves predictably into the focused home after selection - diff --git a/openspec/changes/archive/2026-10-24-add-focused-learner-home/specs/progress-and-practice/spec.md b/openspec/changes/archive/2026-10-24-add-focused-learner-home/specs/progress-and-practice/spec.md deleted file mode 100644 index d33eeb9aad110655df23d5b4a485c7d5a63818cf..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-focused-learner-home/specs/progress-and-practice/spec.md +++ /dev/null @@ -1,33 +0,0 @@ -# Spec Delta - -## MODIFIED Requirements - -### Requirement: Daily practice session -The browser SHALL build a ten-sentence daily session from weak practiced sentences and new course-ordered sentences, preferring homework and the selected class's current lesson before other unfinished lessons. - -**Selection priority (non-normative):** - -New material comes from the first source that applies: - -1. **Homework material**, when there is incomplete homework. -2. **Current-lesson material**, when a current lesson is saved and still has unpracticed sentences. -3. **The first unfinished lesson**, otherwise. - -The existing weak/new balance and ten-sentence session size still apply. - -#### Scenario: One pool has too few sentences -- **WHEN** either the weak or new sentence pool cannot supply its half -- **THEN** the other pool fills the remaining places -- **AND** story lines are excluded - -#### Scenario: Current lesson is selected -- **GIVEN** the learner selected a current lesson for the active class -- **AND** no incomplete homework takes priority -- **WHEN** the browser chooses new sentences for daily practice -- **THEN** it chooses available unpracticed sentences from the selected current lesson before other lessons - -#### Scenario: No current lesson is available -- **GIVEN** the learner has no usable current-lesson selection -- **WHEN** daily practice is available without completing onboarding -- **THEN** the browser falls back to the first unfinished lesson - diff --git a/openspec/changes/archive/2026-10-24-add-focused-learner-home/tasks.md b/openspec/changes/archive/2026-10-24-add-focused-learner-home/tasks.md deleted file mode 100644 index 2f6abdeb7b39a812879c008292cda7178cb7a4c7..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-focused-learner-home/tasks.md +++ /dev/null @@ -1,43 +0,0 @@ -# Tasks - -## 1. Learner/class state persistence - -- [x] 1.1 Add the documented Supabase migration for `learner_class_state`, including the `(email, class_id)` uniqueness rule, timestamps, RLS, and revoked client roles; verify the SQL applies cleanly to a test Supabase project. -- [x] 1.2 Add authenticated persistence methods for reading and upserting only a learner's own per-class current lesson; verify focused unit tests cover normalization, missing-table behavior, and unavailable Supabase. - -## 2. Current-lesson API contract - -- [x] 2.1 Include stable class IDs and the caller's saved `current_lesson` in `/api/me`; verify API tests cover no selection, one selection, and independent selections for multiple classes. -- [x] 2.2 Add `PUT /api/me/classes/{class_id}/current-lesson` and validate authentication, class membership, a single resolvable class level, and lesson membership; verify API tests cover valid update plus unauthenticated, foreign-class, ambiguous-level, and invalid-lesson rejection. -- [x] 2.3 Document the new table, migration, endpoint behavior, and degraded persistence behavior in `README.md`; verify the documented SQL and request shape agree with the implementation. - -## 3. Current-lesson onboarding - -- [x] 3.1 Add the RTL current-lesson prompt after active-class selection, restricted to the class's one level, and block the focused home until a choice is made; verify browser-facing tests cover first entry, returning entry, multiple classes, and ambiguous class configuration. -- [x] 3.2 Persist the selection through the authenticated endpoint with a versioned, subject/account/class-scoped local fallback; verify tests demonstrate that switching accounts or classes cannot reuse another selection and that server state wins when restored. -- [x] 3.3 Add a visible change-current-lesson action that preserves all recorded progress; verify changing lessons updates recommendations while existing best scores and completion state remain intact. -- [x] 3.4 Manually complete onboarding in Hebrew RTL on narrow mobile and desktop widths using touch and keyboard only; verify visible focus, accessible names, predictable focus movement, and reduced-motion behavior. - -## 4. Focused learner home - -- [x] 4.1 Add the focused home as the default authenticated student entry view, reuse existing topic and homework launch paths, and move refer-a-friend access there from the focused learner's bottom navigation; verify automated UI logic tests show no more than three distinct recommended actions and preserve referral access. -- [x] 4.2 Implement deterministic recommendation priority for nearest-due incomplete homework, today's unwatched daily video, unfinished work in the selected lesson, and a review or short activity, keeping the first three that apply; verify tests cover incomplete homework, no homework, an unwatched/watched/absent daily video, a math class without daily videos, all three higher-priority actions dropping the short activity, completed current lesson, insufficient material, and duplicate-target removal. -- [x] 4.3 Implement a short recommended session capped at ten items with its workload shown before start and a clear completion state; verify tests cover item limits and empty/insufficient pools. -- [x] 4.4 Manually exercise the focused home in Hebrew RTL on a phone-sized viewport with complete and incomplete lessons; verify homework status, the daily-video card opening today's video, primary action, short-task size, touch targets, keyboard order, and screen-reader labels. - -## 5. Path and daily-practice integration - -- [x] 5.1 Add explicit full-path navigation that positions the selected current lesson or its first unfinished topic in view while retaining every assigned class lesson; verify tests cover lesson 5 with unfinished earlier lessons and manual reduced-motion navigation. -- [x] 5.2 Update daily-practice new-item selection to prefer homework and then the selected current lesson before other unfinished lessons; verify focused tests retain the existing ten-item balance and fallback behavior. -- [x] 5.3 Preserve the existing path-first behavior for teachers and open mode, and skip the daily-video reminder prompt (`askPushSoon`) on the visit where lesson onboarding happens; verify browser tests cover both entry modes, confirm no reminder prompt or permission request follows onboarding on that visit, and confirm the prompt's existing rules apply on the next entry. -- [x] 5.4 Keep a reminder tap's `?view=daily` link opening the daily videos screen before onboarding or the focused home; verify browser tests cover a student without a saved lesson (onboarding shown on returning home) and with one (focused home shown on returning), and that an admin broadcast's `/` link follows the normal entry flow. - -## 6. Integration verification - -- [x] 6.1 Run the focused server and browser test selections, then the full test suite, and verify no regressions in class selection, homework, progress restoration, daily practice, or push notifications. - -## Workflow follow-up - -- Archive the change after implementation review and deployment migration checks are complete. -- Plan notification categories and reminder preferences as a separate OpenSpec change. - diff --git a/openspec/changes/archive/2026-10-24-add-requirements-browser/design.md b/openspec/changes/archive/2026-10-24-add-requirements-browser/design.md deleted file mode 100644 index 2f67b104938c9ad4ba6b85511a5a7ecc8541ad3f..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-requirements-browser/design.md +++ /dev/null @@ -1,75 +0,0 @@ -# Design - -## Context - -See `proposal.md` for motivation. OpenSpec currently exposes repository state through terminal commands and Markdown files. This repository already requires Python and the OpenSpec CLI, but it has no JavaScript build system and its production FastAPI process loads large scoring dependencies that a requirements viewer does not need. - -The viewer handles developer planning material, so its network and rendering boundaries matter even though it runs locally. It must not become a production application route or execute HTML embedded in repository artifacts. - -## Goals / Non-Goals - -**Goals:** - -- Start quickly with the repository's existing Python and OpenSpec installations. -- Treat OpenSpec CLI output and resolved artifact paths as the authoritative project inventory. -- Provide a responsive, keyboard-accessible overview and detailed inspection page. -- Keep all reads inside the resolved OpenSpec root and all presentation read-only. -- Make parser and HTTP behavior testable without starting the production application. - -**Non-Goals:** - -- Editing or approving requirements from the browser. -- Serving the interface beyond loopback or adding production FastAPI endpoints. -- Persisting UI preferences or requirement content. -- Rendering arbitrary Markdown HTML or supporting every Markdown extension. -- Replacing OpenSpec validation, lifecycle commands, or AI workflows. - -## Decisions - -### Use a standalone standard-library Python server - -Add a developer script that uses Python's HTTP server primitives and binds to `127.0.0.1` by default. It will serve one static client and a small read-only JSON API. A `--port` option allows recovery from port conflicts, while host selection is not exposed as a normal option. - -This keeps startup independent of the model-heavy production application and adds no package to `requirements.txt`. Reusing FastAPI was rejected because it would couple local documentation tooling to production dependencies and routing. A Node development server was rejected because the repository has no JavaScript package or build setup. - -### Use OpenSpec CLI output for discovery - -At startup, invoke `openspec context --json` using a fixed argument vector and no shell. Use the returned root as the only readable project boundary. Inventory refreshes use JSON-capable OpenSpec commands for specifications, changes, status, and structured specification content. - -This follows store and root resolution rules already implemented by OpenSpec instead of recreating them. If a command fails or returns invalid JSON, the server returns a typed error that the client can display. The implementation will never interpolate request data into a shell command. - -### Read planning artifacts only from CLI-resolved paths - -For active changes, use `openspec status --change --json` to obtain artifact paths. Before reading a file, resolve it and verify that it remains inside the OpenSpec root and is one of the paths reported by the CLI. Requests do not accept arbitrary filesystem paths. - -This permits proposal, design, task, and delta-spec inspection without creating a general local-file server. Symlink and traversal checks protect the boundary even on localhost. - -### Send structured JSON and render with DOM text nodes - -The server converts durable specs and planning Markdown into a small document model: headings, paragraphs, lists, requirements, scenarios, and task states. The browser creates elements with `textContent`; artifact-provided HTML is never inserted with `innerHTML`. - -This provides enough visual hierarchy for requirements without adding a Markdown dependency. Full-fidelity Markdown rendering was rejected because it increases sanitization complexity and is not needed for inspection. - -### Keep browser state ephemeral - -Selection, search terms, expanded sections, loading state, and errors live only in page memory. The refresh action retrieves a complete new snapshot and preserves the current selection when it still exists. No local storage, cookies, service worker, or server-side session is used. - -### Keep deployment surfaces unchanged - -The implementation adds no application API endpoint, Supabase table, environment variable, Docker step, or Hugging Face Space configuration. Documentation clearly labels the tool as local development tooling. - -## Risks / Trade-offs - -- **OpenSpec JSON output changes between versions** -> Validate required fields, isolate command adapters, and show the failing command and concise diagnostic without crashing the server. -- **Repeated CLI calls make refresh slow** -> Build one snapshot per refresh request and keep the current repository small; introduce caching only if measured latency becomes disruptive. -- **A malformed artifact prevents complete structure extraction** -> Return a file-specific warning and preserve raw inert text plus all unaffected documents. -- **Localhost is mistaken for authorization** -> Bind only to loopback, expose no host override in normal usage, and never return files outside CLI-resolved OpenSpec paths. -- **Minimal Markdown support omits advanced formatting** -> Preserve content as readable text and prioritize headings, lists, requirements, scenarios, and tasks over visual fidelity. -- **The OpenSpec executable is unavailable** -> Fail startup with an installation/PATH message rather than silently scanning directories with different semantics. - -## Migration Plan - -1. Add the standalone server, static client, parser/adapter tests, and developer documentation. -2. Verify the viewer against the repository's current six specifications and against an active change. -3. Run focused tests, the full test suite, and a manual keyboard/narrow-window browser check. -4. Roll back by removing the local tooling files and documentation; production behavior and stored data are unaffected. diff --git a/openspec/changes/archive/2026-10-24-add-requirements-browser/proposal.md b/openspec/changes/archive/2026-10-24-add-requirements-browser/proposal.md deleted file mode 100644 index 5b97b120c265efa5daab527da9ddd50bae7f7def..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-requirements-browser/proposal.md +++ /dev/null @@ -1,31 +0,0 @@ -# Proposal - -## Why - -The project's OpenSpec requirements are valid Markdown but are cumbersome to inspect across multiple files. A local browser interface will help maintainers understand capabilities, requirements, scenarios, and active changes without exposing planning material through the deployed application. - -## What Changes - -- Add a local, read-only requirements browser that binds only to the loopback interface. -- Present durable specifications and active changes with navigation, counts, search, and expandable requirement scenarios. -- Reflect files from the repository on refresh and show actionable errors for missing or malformed OpenSpec content. -- Document a single command for starting the viewer and the local URL it opens. -- Keep the viewer completely separate from production application routes and deployment. -- Exclude editing, approval workflows, remote access, authentication, persistence, Supabase, and paid APIs. - -## Capabilities - -### New Capabilities - -- `requirements-browser`: Local, read-only browser inspection of OpenSpec specifications and changes. - -### Modified Capabilities - -None. - -## Impact - -- Adds local developer tooling and its focused tests under the repository's scripts/tooling area. -- Reads `openspec/config.yaml`, `openspec/specs/`, and `openspec/changes/` without modifying them. -- Adds developer documentation for starting and using the viewer. -- Does not add production FastAPI endpoints, browser state, Supabase schema, deployment configuration, external services, or runtime dependencies for the learner application. diff --git a/openspec/changes/archive/2026-10-24-add-requirements-browser/specs/requirements-browser/spec.md b/openspec/changes/archive/2026-10-24-add-requirements-browser/specs/requirements-browser/spec.md deleted file mode 100644 index e18a43ce7ef99ad5624f0b1b51dc535fa703af09..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-requirements-browser/specs/requirements-browser/spec.md +++ /dev/null @@ -1,91 +0,0 @@ -# Spec Delta - -## Purpose - -Provide maintainers with a private, local browser interface for understanding OpenSpec capabilities, requirements, scenarios, and work in progress. - -## ADDED Requirements - -### Requirement: Local-only startup -The requirements browser SHALL start from the project root with one documented command and SHALL listen only on a loopback address by default. - -#### Scenario: Maintainer starts the browser -- **GIVEN** the repository contains a valid OpenSpec root -- **WHEN** the maintainer runs the documented startup command -- **THEN** the tool reports a localhost URL -- **AND** the interface is reachable from a browser on the same machine -- **AND** it is not exposed on external network interfaces - -#### Scenario: Default port is unavailable -- **WHEN** the configured port cannot be bound -- **THEN** the tool exits without starting a partial server -- **AND** reports an actionable message that explains how to choose another port - -### Requirement: Requirements inventory -The interface SHALL summarize durable specifications and active changes from the repository's resolved OpenSpec root. - -#### Scenario: Project contains specifications -- **WHEN** the requirements page loads -- **THEN** it lists every durable capability with its requirement count -- **AND** shows the total specification and requirement counts - -#### Scenario: Project contains active changes -- **WHEN** one or more OpenSpec changes are in progress -- **THEN** the interface lists each change and its available planning artifacts -- **AND** distinguishes current changes from durable specifications - -#### Scenario: Project has no active changes -- **WHEN** no change is in progress -- **THEN** the interface clearly reports that there are no active changes -- **AND** continues to show durable specifications - -### Requirement: Structured requirement inspection -The interface SHALL present specification and change content as structured, readable HTML without executing markup or scripts contained in an artifact. - -#### Scenario: Maintainer opens a capability -- **WHEN** the maintainer selects a capability -- **THEN** its purpose, requirements, and scenarios are displayed in document order -- **AND** requirement and scenario headings remain visually distinguishable - -#### Scenario: Artifact contains unsafe markup -- **WHEN** a Markdown artifact contains embedded HTML or script-like text -- **THEN** the interface renders that content as inert text or safe markup -- **AND** does not execute artifact-provided code - -### Requirement: Navigation and search -The interface SHALL let maintainers find content across capability names, requirement names, scenario names, and artifact text without requiring a mouse. - -#### Scenario: Search matches requirement content -- **WHEN** the maintainer enters a search term -- **THEN** the interface limits or highlights results that contain the term -- **AND** reports clearly when there are no matches - -#### Scenario: Keyboard-only inspection -- **WHEN** the maintainer navigates with a keyboard -- **THEN** all specification selection, change selection, search, expansion, and refresh controls are reachable and visibly focused - -#### Scenario: Narrow browser window -- **WHEN** the interface is viewed in a narrow window -- **THEN** navigation and content remain usable without horizontal page scrolling - -### Requirement: Read-only refresh -The requirements browser SHALL never modify OpenSpec artifacts and SHALL allow the maintainer to reload the latest files without restarting the production application. - -#### Scenario: Artifact changes on disk -- **GIVEN** the requirements browser is running -- **WHEN** an OpenSpec artifact changes and the maintainer refreshes the view -- **THEN** the interface displays the latest saved content -- **AND** no OpenSpec file is written by the viewer - -### Requirement: Actionable failure reporting -The tool SHALL distinguish missing OpenSpec setup, unreadable files, and malformed artifacts while keeping readable content available when possible. - -#### Scenario: Command runs outside an OpenSpec project -- **WHEN** the maintainer starts the tool where no OpenSpec root can be resolved -- **THEN** startup fails with an explanation that OpenSpec must be initialized or the command must run inside a configured project - -#### Scenario: One artifact cannot be parsed -- **GIVEN** other OpenSpec artifacts remain readable -- **WHEN** one artifact is malformed or unreadable -- **THEN** the interface identifies the affected file and problem -- **AND** continues to display unaffected specifications and changes diff --git a/openspec/changes/archive/2026-10-24-add-requirements-browser/tasks.md b/openspec/changes/archive/2026-10-24-add-requirements-browser/tasks.md deleted file mode 100644 index 9109426ba8f2007bc9a3e09139412985526dd0de..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-add-requirements-browser/tasks.md +++ /dev/null @@ -1,23 +0,0 @@ -# Tasks - -## 1. Local data service - -- [x] 1.1 Add a standalone Python requirements-browser module that resolves the OpenSpec root and invokes fixed OpenSpec CLI JSON commands without a shell; add focused tests proving valid discovery and actionable missing-CLI/root failures pass. -- [x] 1.2 Implement snapshot assembly for durable specs and active-change artifact paths, including containment checks, structured Markdown extraction, and partial file warnings; add focused tests for counts, requirements/scenarios/tasks, traversal or symlink refusal, malformed JSON, and one unreadable artifact while other content survives. -- [x] 1.3 Add the loopback-only HTTP entry point, static asset response, read-only snapshot endpoint, `--port` handling, and concise bind errors; add HTTP tests verifying successful responses, unsupported methods are refused, no arbitrary file path is accepted, and a busy port produces the documented failure. - -## 2. Browser interface and usage - -- [x] 2.1 Add the dependency-free HTML/CSS/JavaScript interface for summary counts, separate specification/change navigation, structured details, search/no-results feedback, refresh, loading, and partial-error states; verify browser rendering uses DOM text APIs rather than inserting artifact HTML. -- [x] 2.2 Add responsive layout, semantic controls, visible focus states, and keyboard-operable selection/expansion; manually verify current specs and this active change in a desktop and narrow viewport using keyboard-only navigation, including inert rendering of a script-like fixture. -- [x] 2.3 Document the single startup command, localhost URL, alternate-port option, read-only/local-only boundary, and troubleshooting in the developer documentation; verify every documented command runs as written from the project root. - -## 3. Integration verification - -- [x] 3.1 Run the focused requirements-browser tests and `openspec validate add-requirements-browser --strict --no-interactive`, resolving every failure. -- [x] 3.2 Run the full `pytest -q` suite and confirm the standalone viewer adds no production route, package dependency, Docker change, Supabase migration, or external request. - -## Workflow follow-up - -- Archive the change after implementation and review are complete. -- Verify the archived `requirements-browser` capability is present in the durable specifications. diff --git a/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/design.md b/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/design.md deleted file mode 100644 index 0f5b338725dac5df0a9314322e263d3e24a714f1..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/design.md +++ /dev/null @@ -1,101 +0,0 @@ -# Design - -## Context - -See `proposal.md` for motivation. The Atlas server currently converts Markdown into a small safe block model, but it emits only `code-boundary` markers for fence lines and parses the lines inside as ordinary paragraphs. The client always renders artifact values with `textContent`, which is the security property this change must preserve. - -The Atlas has no JavaScript build system or Markdown dependency. It binds to loopback, reads only CLI-resolved artifact paths, and must remain useful with malformed planning text rather than becoming a full Markdown engine. - -## Goals / Non-Goals - -**Goals:** - -- Preserve fenced content exactly enough for ASCII diagrams, wireframes, SQL, and HTML source examples. -- Keep all artifact content inert and maintain the current Content Security Policy. -- Contain wide content within its own scroll area on narrow screens. -- Keep parsing and rendering dependency-free and directly testable. - -**Non-Goals:** - -- Execute or preview artifact-provided HTML, SVG, scripts, or interactive controls. -- Add syntax highlighting, Mermaid, a general Markdown renderer, or third-party packages. -- Change durable-spec rendering returned structurally by the OpenSpec CLI. -- Add editing, file downloads, persistence, production routes, or external network access. - -## Decisions - -### Parse fences as a stateful block - -Extend the line parser with an `in_code` state. An opening fence records its optional language label; every subsequent raw line, including blank and indented lines, is collected without paragraph normalization until a closing fence. Emit one block: - -```text -{ - "type": "code", - "language": "text", - "text": "+------+\n| flow |\n+------+" -} -``` - -An unclosed fence consumes through end-of-file and still emits an inert code block, preserving information instead of mixing it into prose. Supporting backtick fences is sufficient for the repository's artifacts; nested fences and complete CommonMark delimiter rules are outside scope. - -This stateful parser is preferred to reconstructing code in the browser because the server already owns safe Markdown structure and search snapshots. A general Markdown library was rejected because it adds dependency and sanitization complexity. - -### Render with explicit DOM construction and `textContent` - -For a `code` block, the client constructs a wrapper, optional language label, `
`, and ``. It assigns the artifact content only through `textContent`.
-
-```text
-Artifact Markdown
-       |
-       v
-+-------------------+
-| Python parser     |
-| type: code        |
-| language + text   |
-+---------+---------+
-          | JSON
-          v
-+-------------------+
-| Browser renderer  |
-| createElement     |
-| textContent only  |
-+---------+---------+
-          |
-          v
-  
 inert source
-```
-
-Do not use `innerHTML`, iframe `srcdoc`, data URLs, or an HTML sanitizer. Those alternatives would either execute content or add a new security boundary for no requirement benefit. The existing CSP remains unchanged.
-
-### Make each block its own overflow and direction boundary
-
-Style preformatted blocks with a monospace font, preserved whitespace, maximum width, and `overflow: auto`. Give the `
` a visible `:focus-visible` outline and a programmatic accessible label that uses the language when present.
-
-Use LTR layout and left alignment for the preformatted canvas, with Unicode plaintext handling so source order and ASCII geometry remain stable when surrounded by RTL prose. The artifact prose keeps its existing direction. Do not wrap diagram lines because wrapping destroys geometry; narrow screens scroll within the block.
-
-### Keep search at the snapshot level
-
-The existing search serializes artifact block values. Storing the complete code content in the block's `text` field therefore makes it searchable without a second index. When any block matches, render the full artifact block sequence rather than slicing matched lines out of a diagram.
-
-The refresh endpoint already rebuilds the snapshot from disk, so no new cache or invalidation mechanism is needed.
-
-### Keep changes local to Atlas tooling
-
-No application API endpoint, Supabase schema, environment variable, Docker configuration, service worker, or Hugging Face deployment changes. The implementation touches only the local parser, static Atlas client/styles, tests, and local-tool documentation.
-
-## Risks / Trade-offs
-
-- **[Large blocks increase snapshot size]** -> Preserve the current local-only model and avoid duplicate rendered representations; add limits only if measured repository artifacts require them.
-- **[Mixed Hebrew and ASCII lines can display differently across fonts]** -> Use a monospace stack, LTR preformatted canvas, and Unicode plaintext handling; document that diagrams should use ASCII geometry.
-- **[An unclosed fence absorbs later prose]** -> Preserve it as inert code through EOF, matching the author's apparent intent and preventing accidental active rendering.
-- **[Search finds a change but not a highlighted line]** -> Keep the entire diagram intact; line-level highlighting is out of scope because it can split visual structure.
-- **[Future syntax highlighting encourages unsafe HTML insertion]** -> Retain tests that forbid `innerHTML` and assert script-like source remains text.
-
-## Migration Plan
-
-1. Add parser tests for labeled, empty, multiline, HTML-like, and unclosed fenced blocks.
-2. Change the parser block model and client renderer together so no intermediate deployment expects the old boundary blocks.
-3. Add responsive/focus styles and browser-source safety tests.
-4. Start the Atlas at `http://localhost:8765/` and verify the learner-home diagrams and HTML mock are readable on desktop and narrow widths.
-5. Roll back the parser, renderer, and styles together if necessary; OpenSpec artifacts remain unchanged and readable as Markdown.
-
diff --git a/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/proposal.md b/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/proposal.md
deleted file mode 100644
index 05588fe0724c47371a16fe3d3cba7d3034e88435..0000000000000000000000000000000000000000
--- a/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/proposal.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Proposal
-
-## Why
-
-The OpenSpec Atlas currently collapses fenced diagrams and mock HTML into ordinary text, so planning artifacts lose the visual structure that makes workflows and wireframes understandable. Maintainers need those blocks to remain readable at `http://localhost:8765/` without allowing artifact-provided HTML or scripts to execute.
-
-## What Changes
-
-- Parse fenced Markdown code blocks as complete blocks, preserving their language label, whitespace, line breaks, and content.
-- Render fenced blocks as responsive, preformatted `
` content using safe DOM text APIs.
-- Display HTML mocks as inert source code rather than live embedded interfaces.
-- Style diagrams and code for horizontal overflow, keyboard readability, narrow screens, and both LTR and RTL artifact content.
-- Preserve existing headings, lists, task states, searching, and read-only refresh behavior.
-- Keep live HTML previews, Markdown script execution, syntax-highlighting dependencies, editing, and production application routes out of scope.
-
-## Capabilities
-
-### New Capabilities
-
-- `atlas-artifact-rendering`: Safe, structure-preserving presentation of fenced diagrams and source examples in OpenSpec planning artifacts.
-
-### Modified Capabilities
-
-None.
-
-## Impact
-
-- Markdown block parsing in `scripts/requirements_browser.py`.
-- Safe DOM rendering in `scripts/requirements_browser.js` and responsive presentation in `scripts/requirements_browser.css`.
-- Parser, snapshot, browser-source, and HTTP tests in `tests/test_requirements_browser.py`.
-- Local developer documentation for the Atlas rendering behavior.
-- No production FastAPI, Supabase, deployment, persistence, paid API, or learner-data impact. Artifact contents remain local, read-only, and inert.
diff --git a/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/specs/atlas-artifact-rendering/spec.md b/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/specs/atlas-artifact-rendering/spec.md
deleted file mode 100644
index 4bd49b2af29f4cb9ca191d4bbef9bbd959a7f93c..0000000000000000000000000000000000000000
--- a/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/specs/atlas-artifact-rendering/spec.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# Spec Delta
-
-## Purpose
-
-Provide safe, structure-preserving display of diagrams and source examples embedded in OpenSpec planning artifacts within the local Atlas.
-
-## ADDED Requirements
-
-### Requirement: Fenced block preservation
-The Atlas SHALL present a fenced Markdown code block as one preformatted block, preserving its content, line breaks, indentation, and optional language label.
-
-#### Scenario: Artifact contains an ASCII diagram
-- **GIVEN** a planning artifact contains a fenced `text` block with an aligned diagram
-- **WHEN** the maintainer opens that artifact in the Atlas
-- **THEN** the complete diagram appears in one preformatted block
-- **AND** its line breaks and spacing are preserved
-
-#### Scenario: Artifact contains a labeled source example
-- **GIVEN** a planning artifact contains a fenced block with a language label
-- **WHEN** the block is displayed
-- **THEN** the source content remains intact
-- **AND** the language label is available as contextual information
-
-#### Scenario: Artifact contains an empty fenced block
-- **WHEN** an empty fenced block is parsed
-- **THEN** the Atlas displays an empty preformatted block without dropping adjacent content
-
-### Requirement: Inert artifact source
-The Atlas SHALL render every fenced block as inert text and SHALL NOT execute, interpret, or insert artifact-provided HTML, SVG, JavaScript, or other active content into the page.
-
-#### Scenario: HTML mock is displayed
-- **GIVEN** a fenced `html` block contains buttons, forms, or script-like text
-- **WHEN** the maintainer views the artifact
-- **THEN** the HTML is visible as source code
-- **AND** no element or script from that source becomes active page content
-
-#### Scenario: Closing markup attempts to escape the block
-- **GIVEN** a fenced block contains markup intended to alter the surrounding Atlas page
-- **WHEN** the block is rendered
-- **THEN** the markup remains text inside the code block
-- **AND** the surrounding Atlas DOM is unchanged
-
-### Requirement: Responsive and accessible code inspection
-Preformatted blocks SHALL remain readable and operable in narrow windows, with overflow contained within the block and content available to keyboard and assistive-technology users.
-
-#### Scenario: Diagram is wider than the content area
-- **WHEN** a preformatted diagram exceeds the available width
-- **THEN** horizontal overflow is confined to that diagram
-- **AND** the Atlas page itself does not require horizontal scrolling
-
-#### Scenario: Keyboard-only inspection
-- **WHEN** a keyboard user reaches an overflowing preformatted block
-- **THEN** the block can receive focus and be scrolled without a pointing device
-- **AND** visible focus identifies the active block
-
-#### Scenario: Artifact mixes RTL prose and LTR diagram text
-- **GIVEN** an artifact contains RTL prose around a fenced ASCII or source block
-- **WHEN** it is displayed
-- **THEN** prose retains the document's reading direction
-- **AND** diagram alignment and source order remain stable
-
-### Requirement: Code content participates in Atlas discovery
-The Atlas SHALL include fenced-block content in artifact search and SHALL show updated block content after a read-only refresh.
-
-#### Scenario: Search matches text inside a diagram
-- **WHEN** a maintainer searches for text that occurs only inside a fenced block
-- **THEN** the containing change remains discoverable
-- **AND** the matching artifact can be opened with the complete block intact
-
-#### Scenario: Diagram changes on disk
-- **GIVEN** a fenced block has changed in an artifact
-- **WHEN** the maintainer refreshes the Atlas
-- **THEN** the latest block content is displayed
-- **AND** no artifact file is modified by the Atlas
-
diff --git a/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/tasks.md b/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/tasks.md
deleted file mode 100644
index da3ca4cd6193c11a0c88859b057507609db6c460..0000000000000000000000000000000000000000
--- a/openspec/changes/archive/2026-10-24-render-atlas-code-blocks/tasks.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Tasks
-
-## 1. Fenced-block parsing
-
-- [x] 1.1 Replace code-boundary markers with one structured code block that preserves language, whitespace, blank lines, and content; verify parser tests cover labeled, unlabeled, empty, multiline, and unclosed fences.
-- [x] 1.2 Preserve script-like and HTML-like source as inert snapshot text; verify snapshot tests contain the exact unsafe-looking content without producing executable markup.
-
-## 2. Safe Atlas rendering
-
-- [x] 2.1 Render structured code blocks with DOM-created wrappers and `
` elements populated only through `textContent`; verify browser-source tests continue to forbid `innerHTML`, `srcdoc`, and equivalent active insertion paths.
-- [x] 2.2 Expose the optional language as context and make overflowing blocks keyboard-focusable with a meaningful accessible label; verify DOM behavior manually with keyboard and a screen reader or accessibility tree.
-- [x] 2.3 Add responsive preformatted-block styling with contained horizontal overflow, visible focus, monospace LTR geometry, and stable mixed-direction text; verify at desktop and phone widths that the page does not gain horizontal scrolling.
-
-## 3. Discovery and documentation
-
-- [x] 3.1 Keep fenced content searchable and refreshable through the existing snapshot flow; verify tests find text that appears only inside a code block and observe changed content after refresh.
-- [x] 3.2 Document that the Atlas displays fenced diagrams and HTML mocks as inert source and never runs them; verify the documented startup URL and refresh steps work as written.
-
-## 4. Integration verification
-
-- [x] 4.1 Run `pytest -q tests/test_requirements_browser.py` and the full test suite, then verify `add-focused-learner-home` diagrams and HTML mock render correctly at `http://localhost:8765/`.
-
-## Workflow follow-up
-
-- Apply this change before relying on rich diagram display in the Atlas.
-- Archive after the parser, security, accessibility, and manual browser checks pass.
-
diff --git a/openspec/changes/archive/2026-10-24-render-atlas-diagrams/design.md b/openspec/changes/archive/2026-10-24-render-atlas-diagrams/design.md
deleted file mode 100644
index 09a61c1f4ce0654142a60d6fc066d0d1d9338352..0000000000000000000000000000000000000000
--- a/openspec/changes/archive/2026-10-24-render-atlas-diagrams/design.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# Design
-
-## Context
-
-See `proposal.md` for motivation. The Atlas now parses fenced blocks into `{type, language, text}` records and renders them with safe DOM construction. It has no JavaScript build system, package manifest, or runtime network dependency. Its Content Security Policy currently permits scripts and images only from the same origin.
-
-Mermaid's browser API accepts diagram text and returns SVG source. Mermaid documents `securityLevel: "strict"` as encoding HTML labels and disabling click functionality; secure configuration keys prevent artifact directives from overriding protected settings. Because SVG is still generated from repository-controlled text, this design adds a second boundary and never inserts the returned SVG string into the Atlas DOM.
-
-## Goals / Non-Goals
-
-**Goals:**
-
-- Turn fenced `mermaid` source into clear diagrams without a build step or runtime internet access.
-- Preserve the exact source and provide a reliable per-diagram fallback.
-- Keep artifact-derived markup outside the live Atlas DOM.
-- Isolate rendering failures and stale asynchronous results.
-- Preserve keyboard, narrow-screen, RTL, read-only, and loopback-only behavior.
-
-**Non-Goals:**
-
-- Mermaid links, click callbacks, tooltips that require scripting, HTML labels, or other diagram interactivity.
-- Rendering unmarked text blocks heuristically as diagrams.
-- Editing diagrams, server-side SVG generation, syntax highlighting, or production application integration.
-- Supporting arbitrary HTML/SVG previews or fetching fonts, icons, scripts, or images from external origins.
-
-## Decisions
-
-### Vendor a pinned standalone Mermaid browser bundle
-
-Store a pinned official standalone Mermaid distribution and its license under `scripts/vendor/`, serve both through explicit Atlas asset routes, and load the library before `requirements_browser.js`. Record the upstream version, source URL, and checksum in local documentation.
-
-This preserves the repository's no-build startup and offline behavior. A CDN import was rejected because the Atlas promises no external requests and would fail offline. Adding npm and a bundler was rejected because it would expand a small standard-library tool's build and maintenance surface.
-
-### Initialize Mermaid once with locked-down site configuration
-
-Initialize once with `startOnLoad: false`, `securityLevel: "strict"`, `htmlLabels: false`, `suppressErrorRendering: true`, bounded `maxTextSize` and `maxEdges`, a neutral local font stack, and secure configuration keys that artifact frontmatter or directives cannot override. Render only blocks whose normalized language is exactly `mermaid`; do not scan the document automatically and do not call returned event-binding functions.
-
-Strict mode is necessary but not the only trust boundary. The Atlas treats the result as untrusted generated media and applies the image boundary below.
-
-### Present generated SVG through a sanitized blob image
-
-Parse Mermaid's returned SVG with `DOMParser` as SVG, reject parser errors, and sanitize it before presentation. Remove scripts, `foreignObject`, links, event-handler attributes, external URL-bearing attributes, and any URL reference that is not a local fragment. Preserve the SVG drawing, text, styles, markers, accessibility title/description, and view box needed for a static diagram.
-
-Serialize the sanitized SVG to a same-page Blob URL and assign that URL to an `` created by the Atlas. The generated SVG is therefore decoded as an image rather than adopted into the live DOM. Extend the Atlas CSP only with `img-src 'self' blob:`; keep `object-src` absent through `default-src 'self'` and do not permit data, remote, inline-script, or iframe sources.
-
-Direct `innerHTML` insertion was rejected because it would reverse the Atlas's strongest artifact-rendering invariant. A sandbox iframe was rejected because it complicates sizing, keyboard behavior, CSP, and source fallback. A home-grown diagram layout engine was rejected because layout quality and accessibility would become a large unrelated maintenance burden.
-
-### Use an asynchronous diagram component with stale-render protection
-
-For each Mermaid block, create a component containing a status region, an image region, and a native `
` source disclosure. Generate collision-free render IDs and render asynchronously. Associate every page render with a monotonically increasing generation token; before applying a result, confirm the component is still connected and its token is current. - -Track Blob URLs owned by the current page render and revoke them before rerendering, refresh, or replacement to avoid leaks. One diagram failure changes only its own component to the error/source state. - -```text -fenced mermaid source - | - v - Mermaid strict render - | - v - parse + sanitize SVG - | - v - Blob URL --> Diagram: ... - | - +--> native
with exact inert source -``` - -### Derive accessible presentation from artifact context - -Use the closest preceding artifact heading as the diagram's accessible name when available, falling back to an ordinal such as “Diagram 2”. Put that name in the image `alt` and component label. Announce rendering errors through a polite status region. The native source disclosure is keyboard operable, and the existing preformatted source block retains its focus and scrolling behavior. - -The image uses `max-width: 100%` and intrinsic height. Its container owns any residual overflow so the document never becomes wider than the viewport. Surrounding artifact direction is unchanged; the diagram image itself is direction-neutral. - -### Convert only explanatory flow diagrams - -Change the learner-home proposal, specs, and design flow diagrams from `text` fences to `mermaid` flowcharts where boxes and relationships benefit from visual layout. Keep mobile wireframes as preformatted text and the semantic HTML mock as inert `html` source because they communicate exact spatial/source structure better than a graph. - -### Test dependency integrity and security behavior - -Tests verify the vendored asset and attribution exist, asset routes use correct content types, the HTML loads no remote script, and the CSP allows only the required local/blob image path. Browser tests cover a valid flowchart, invalid source fallback, source disclosure, absence of live artifact SVG/HTML in the Atlas DOM, no external requests, accessibility names, mobile containment, refresh, stale-render protection, and Blob URL cleanup. - -## Risks / Trade-offs - -- **[Vendored Mermaid increases repository size]** -> Pin one minified standalone distribution, include its license and checksum, and update deliberately rather than at runtime. -- **[Mermaid strict mode has a future regression]** -> Keep the separate sanitizer and Blob-image boundary plus adversarial browser tests. -- **[Sanitization removes a legitimate diagram feature]** -> Prefer static safety; fall back to visible source rather than weakening the allowlist. -- **[Large diagrams are slow]** -> Bound text and edge counts, render only visible artifact blocks, and isolate failures. -- **[Blob URLs leak across repeated refreshes]** -> Track ownership and revoke URLs whenever artifact DOM is replaced. -- **[Async rendering races with navigation]** -> Require current generation and connected target checks before applying results. -- **[Mermaid updates change output structure]** -> Pin the version and verify the full valid/adversarial browser fixture set before upgrading. - -## Migration Plan - -1. Add the pinned Mermaid bundle, license/attribution, checksum, and explicit local asset route. -2. Add strict initialization, SVG sanitization, Blob-image presentation, source fallback, and lifecycle cleanup. -3. Add responsive/accessibility styles and automated unit, HTTP, security, and headless-browser coverage. -4. Convert selected learner-home flow diagrams to fenced Mermaid source while retaining text wireframes and HTML source. -5. Restart the local Atlas, verify diagrams at desktop and phone widths, and verify operation with external networking disabled. -6. Roll back by reverting Mermaid blocks to `text` and removing the renderer/assets together; ordinary fenced-source rendering remains available. - diff --git a/openspec/changes/archive/2026-10-24-render-atlas-diagrams/proposal.md b/openspec/changes/archive/2026-10-24-render-atlas-diagrams/proposal.md deleted file mode 100644 index 793f700d1a00a6cf39f32d9861c41b2a6ce551d1..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-render-atlas-diagrams/proposal.md +++ /dev/null @@ -1,34 +0,0 @@ -# Proposal - -## Why - -Preformatted ASCII diagrams preserve planning structure but still require maintainers to mentally interpret boxes and arrows. The OpenSpec Atlas should turn explicitly marked diagram source into clear visual diagrams while retaining a safe source fallback. - -## What Changes - -- Render fenced `mermaid` blocks as visual diagrams in active-change artifacts. -- Bundle a pinned Mermaid browser module with the local Atlas so rendering makes no CDN or external network request. -- Configure diagram rendering for untrusted repository text: strict security, no HTML labels, no click actions, bounded text and edge counts, and no automatic page scanning. -- Present the generated diagram as a non-interactive image rather than inserting artifact-derived SVG markup into the Atlas DOM. -- Keep the original Mermaid source available through a disclosure control and show it automatically with an actionable error when rendering fails. -- Make rendered diagrams responsive, keyboard reachable, accessible by name, and compatible with narrow screens and RTL surrounding prose. -- Convert the illustrative learner-home flow diagrams to Mermaid while retaining HTML mocks and other fenced blocks as inert source. -- Keep diagram editing, interactive Mermaid links/callbacks, live HTML previews, server-side rendering, production routes, and external asset loading out of scope. - -## Capabilities - -### New Capabilities - -- `atlas-diagram-rendering`: Safe local rendering, fallback, and accessible presentation of explicitly marked visual diagrams in OpenSpec artifacts. - -### Modified Capabilities - -None. - -## Impact - -- The local Atlas server and static assets in `scripts/requirements_browser.py`, `scripts/requirements_browser.html`, `scripts/requirements_browser.js`, and `scripts/requirements_browser.css`. -- A pinned, locally stored Mermaid distribution and its license/attribution; no runtime package manager or external request is introduced. -- Atlas parser, asset, security, rendering, accessibility, and browser tests in `tests/test_requirements_browser.py`. -- Mermaid source in the `add-focused-learner-home` planning artifacts. -- No learner-facing production code, FastAPI application route, Supabase data, private learner information, paid API, or deployment configuration is affected. diff --git a/openspec/changes/archive/2026-10-24-render-atlas-diagrams/specs/atlas-diagram-rendering/spec.md b/openspec/changes/archive/2026-10-24-render-atlas-diagrams/specs/atlas-diagram-rendering/spec.md deleted file mode 100644 index caf9ed449347d75f977dc479f8107ee764fb3b7b..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-render-atlas-diagrams/specs/atlas-diagram-rendering/spec.md +++ /dev/null @@ -1,90 +0,0 @@ -# Spec Delta - -## Purpose - -Define safe, accessible visual rendering of explicitly marked diagrams in OpenSpec planning artifacts while preserving their source and the Atlas's local read-only boundary. - -## ADDED Requirements - -### Requirement: Explicit diagram rendering -The Atlas SHALL render a valid fenced `mermaid` block as a visual diagram and SHALL continue to render every other fenced language as preformatted source. - -#### Scenario: Maintainer opens a Mermaid flowchart -- **GIVEN** an active-change artifact contains a valid fenced `mermaid` block -- **WHEN** the maintainer opens that artifact -- **THEN** the Atlas displays a visual diagram in the block's document position -- **AND** surrounding headings, prose, lists, and code blocks retain their order - -#### Scenario: Artifact contains an HTML example -- **GIVEN** an artifact contains a fenced `html` block -- **WHEN** the artifact is displayed -- **THEN** the block remains inert preformatted source -- **AND** is not treated as a visual diagram - -### Requirement: Local and non-interactive rendering -The Atlas SHALL render diagrams without external network requests and SHALL prevent diagram source from creating active HTML, navigation, callbacks, or executable content. - -#### Scenario: Atlas renders while offline -- **GIVEN** the local Atlas files and OpenSpec artifacts are available -- **AND** external network access is unavailable -- **WHEN** a diagram is opened -- **THEN** the visual diagram renders using only locally served assets - -#### Scenario: Diagram source contains an interaction directive -- **GIVEN** diagram source attempts to define a link, click action, HTML label, or script-like content -- **WHEN** the Atlas processes the diagram -- **THEN** no navigation or callback becomes active -- **AND** no artifact-provided element is inserted as live HTML in the Atlas document - -### Requirement: Source access and graceful failure -Every visual diagram SHALL provide access to its original source, and a diagram that cannot be rendered SHALL show an actionable error with its source without hiding other artifact content. - -#### Scenario: Maintainer inspects diagram source -- **WHEN** the maintainer activates the source disclosure for a rendered diagram -- **THEN** the exact original diagram source is shown as inert preformatted text -- **AND** the visual diagram remains available - -#### Scenario: Diagram syntax is invalid -- **GIVEN** a fenced `mermaid` block contains invalid syntax -- **WHEN** rendering is attempted -- **THEN** the Atlas identifies that diagram as unable to render -- **AND** shows the original source -- **AND** continues rendering all other readable artifact blocks - -#### Scenario: Diagram renderer is unavailable -- **GIVEN** the local diagram library fails to load -- **WHEN** an artifact containing diagrams is displayed -- **THEN** each diagram falls back to its inert source with an actionable message -- **AND** the rest of the Atlas remains usable - -### Requirement: Responsive and accessible diagrams -Rendered diagrams SHALL have a meaningful accessible name, support keyboard access to their controls, and fit narrow screens without causing horizontal page scrolling. - -#### Scenario: Diagram is wider than a phone viewport -- **WHEN** the diagram's natural width exceeds its content area -- **THEN** the diagram scales within the available width or scrolls inside its own bounded region -- **AND** the Atlas page does not require horizontal scrolling - -#### Scenario: Keyboard-only inspection -- **WHEN** a maintainer navigates without a pointing device -- **THEN** the diagram and its source disclosure are reachable with visible focus -- **AND** the diagram's accessible name identifies it as a diagram - -#### Scenario: Diagram appears within RTL prose -- **GIVEN** an artifact contains RTL prose around a diagram -- **WHEN** the artifact is displayed -- **THEN** the prose retains its reading direction -- **AND** diagram node and edge layout remains visually stable - -### Requirement: Refresh-safe rendering -The Atlas SHALL render the latest diagram source after refresh and SHALL prevent an obsolete asynchronous render from replacing a newer artifact selection. - -#### Scenario: Diagram changes on disk -- **GIVEN** diagram source changes after the Atlas page loads -- **WHEN** the maintainer selects Refresh files -- **THEN** the newly rendered diagram reflects the latest saved source - -#### Scenario: Maintainer changes artifacts during rendering -- **WHEN** the maintainer selects another artifact before a diagram render completes -- **THEN** the previous render does not insert output into the newly selected artifact - diff --git a/openspec/changes/archive/2026-10-24-render-atlas-diagrams/tasks.md b/openspec/changes/archive/2026-10-24-render-atlas-diagrams/tasks.md deleted file mode 100644 index 175535c9aa03fea98a81ee30287a3e1eef027dce..0000000000000000000000000000000000000000 --- a/openspec/changes/archive/2026-10-24-render-atlas-diagrams/tasks.md +++ /dev/null @@ -1,31 +0,0 @@ -# Tasks - -## 1. Local Mermaid dependency - -- [x] 1.1 Add a pinned official standalone Mermaid browser bundle plus license, upstream URL, version, and checksum under `scripts/vendor/`; verify the recorded checksum matches the committed asset. -- [x] 1.2 Serve the vendored files through explicit Atlas routes and load Mermaid locally before the Atlas client; verify HTTP tests assert content types, local URLs, and absence of CDN or other external script references. -- [x] 1.3 Update the Atlas CSP for same-origin scripts and Blob-backed images only, and document offline diagram behavior and the dependency update procedure; verify CSP and documentation tests reject remote, data, inline-script, object, and iframe allowances. - -## 2. Secure diagram rendering - -- [x] 2.1 Initialize Mermaid once with automatic scanning disabled, strict security, HTML labels and interactions disabled, protected configuration keys, and bounded input/edge limits; verify source tests assert every locked-down setting. -- [x] 2.2 Implement SVG parsing and sanitization that rejects parser errors and removes active elements, event attributes, links, external resources, and non-fragment URL references; verify adversarial unit tests cover scripts, `foreignObject`, callbacks, external URLs, and malformed SVG. -- [x] 2.3 Render sanitized SVG only through tracked Blob-backed `` elements, never live artifact SVG/HTML; verify browser tests find a visual diagram while confirming no artifact-derived SVG, HTML control, callback, or navigation target exists in the Atlas DOM. -- [x] 2.4 Add per-diagram source disclosure and isolated error fallback for invalid source or unavailable Mermaid; verify browser tests preserve exact source, announce the error, and keep later artifact blocks usable. -- [x] 2.5 Add generation checks and Blob URL cleanup across artifact navigation and refresh; verify asynchronous tests prevent stale insertion and confirm obsolete URLs are revoked. - -## 3. Responsive and accessible presentation - -- [x] 3.1 Add diagram, loading, error, and disclosure styles that preserve surrounding RTL, visible keyboard focus, narrow-screen containment, and readable light-theme output; verify automated layout checks and manual desktop/mobile inspection show no page-level horizontal overflow. -- [x] 3.2 Derive a meaningful diagram name from nearby headings and expose it through component labeling and image alternative text; verify accessibility-tree inspection and keyboard-only navigation cover the diagram and source disclosure. - -## 4. Illustrative artifacts and integration - -- [x] 4.1 Convert relationship-focused diagrams in `add-focused-learner-home` from ASCII `text` blocks to valid Mermaid flowcharts while retaining exact-layout wireframes and HTML examples as inert source; verify strict OpenSpec validation and Atlas rendering for every converted block. -- [x] 4.2 Run the focused Atlas tests and full repository suite, then start the Atlas with external networking unavailable and verify valid diagrams, invalid fallback, search, refresh, source access, and non-diagram code rendering at `http://localhost:8765/`. - -## Workflow follow-up - -- Review the vendored dependency license and security tests before archiving. -- Archive only after restarting the normal port-8765 Atlas and confirming the rendered learner-home diagrams. - diff --git a/openspec/changes/document-path-shadda-rendering/design.md b/openspec/changes/document-path-shadda-rendering/design.md deleted file mode 100644 index 93914a3a2ca1ba4627c2d6394e00729105b78ecd..0000000000000000000000000000000000000000 --- a/openspec/changes/document-path-shadda-rendering/design.md +++ /dev/null @@ -1,48 +0,0 @@ -# Design - -## Context - -The browser already centralizes Hebrew-transliteration rendering in a helper that replaces a Hebrew letter plus U+0651 with a text-preserving wrapper and draws the shadda through CSS. Practice views use that helper, while complete-path labels historically inserted names as plain text. See `proposal.md` for motivation and `specs/learner-home/spec.md` for observable behavior. - -The affected surface is client-only. There are no affected API endpoints, browser-storage keys, Supabase tables, authorization rules, paid services, dependencies, or deployment settings. - -## Goals / Non-Goals - -**Goals:** - -- Reuse one rendering rule for shadda-bearing Hebrew transliteration in practice and complete-path labels. -- Preserve RTL layout, button semantics, touch behavior, keyboard behavior, and accessible names. -- Keep unsupported CSS behavior non-blocking: text and navigation must remain available. - -**Non-Goals:** - -- Changing transliteration source data or normalizing lesson names. -- Altering path layout, progress calculations, lesson selection, or navigation. -- Adding a new font or browser dependency. - -## Decisions - -### Reuse the existing transliteration renderer - -Pass visible level, lesson, and topic name elements through the existing shadda-aware text renderer. This keeps the workaround in one place and retains text nodes instead of introducing HTML-bearing content data. - -Alternative considered: rely on the native U+0651 glyph. This remains inconsistent on Android and recreates the defect. A path-specific renderer was also rejected because it would duplicate the established rule. - -### Keep original strings in semantic text - -Continue deriving titles and accessible descriptions from the unmodified source name. The visual wrapper is presentational and must not change button behavior or spoken text. - -Alternative considered: replace U+0651 in the source string with a visual character or image. That would weaken copying, accessibility, and future text processing. - -### Degrade to readable text - -The CSS enhancement may be absent when masking is unsupported, but the wrapped Hebrew letter and the rest of the label remain visible. No capability detection, API fallback, or stored state is needed. - -## Risks / Trade-offs - -- [Risk] Font metrics can shift the visual mark slightly across devices. → Keep the shared relative positioning and include an Android/mobile visual check. -- [Risk] A future path label insertion could bypass the renderer. → Add a focused regression test covering level, lesson, and topic labels. - -## Migration Plan - -No data migration is required. Deploy the client rendering change normally; rollback consists of restoring plain-text path-label construction, with no persistent-state impact. diff --git a/openspec/changes/document-path-shadda-rendering/proposal.md b/openspec/changes/document-path-shadda-rendering/proposal.md deleted file mode 100644 index b881301ad5f1cf836551a4370650dc6e473c88ff..0000000000000000000000000000000000000000 --- a/openspec/changes/document-path-shadda-rendering/proposal.md +++ /dev/null @@ -1,28 +0,0 @@ -# Proposal - -## Why - -Hebrew transliterations can contain the Arabic shadda combining mark, which is not rendered reliably over Hebrew letters by some browsers, particularly on Android. The complete learning path must present those names with the same legible, consistent shadda treatment already used in practice content. - -## What Changes - -- Require level, lesson, and topic names in the complete path to render shadda-bearing Hebrew letters with the application's supported visual shadda treatment. -- Preserve the original text for accessible names and descriptive browser text. -- Keep path positioning, progress, navigation, and course content unchanged. -- Explicitly exclude changes to pronunciation scoring, transliteration data, persistence, authorization, and lesson ordering. - -## Capabilities - -### New Capabilities - -None. - -### Modified Capabilities - -- `learner-home`: Extend complete-path behavior so shadda-bearing Hebrew transliteration remains legible in visible path labels. - -## Impact - -- Affects the browser rendering of complete-path headings and labels in `static/index.html`. -- Requires a focused rendering regression test and a manual mobile/Android browser check. -- No API, Supabase schema, persistence, authorization, privacy, paid-API, dependency, or deployment-configuration impact. diff --git a/openspec/changes/document-path-shadda-rendering/specs/learner-home/spec.md b/openspec/changes/document-path-shadda-rendering/specs/learner-home/spec.md deleted file mode 100644 index 5238a83937ae587a21d58f0bd7854f8113ee5682..0000000000000000000000000000000000000000 --- a/openspec/changes/document-path-shadda-rendering/specs/learner-home/spec.md +++ /dev/null @@ -1,19 +0,0 @@ -# Spec Delta - -## ADDED Requirements - -### Requirement: Legible shadda in complete-path labels -The application SHALL render the shadda clearly over its associated Hebrew letter in visible level, lesson, and topic names on the complete learning path, while preserving the label's original text for assistive technology and descriptive browser text. - -#### Scenario: Path label contains a shadda-bearing Hebrew letter -- **GIVEN** a visible level, lesson, or topic name contains a Hebrew letter followed by the shadda combining mark -- **WHEN** the complete learning path is rendered -- **THEN** the shadda is visibly positioned over its associated Hebrew letter -- **AND** the label remains readable in the RTL path layout -- **AND** its accessible or descriptive text retains the original name - -#### Scenario: Browser lacks custom-mask support -- **GIVEN** the browser cannot draw the enhanced visual shadda treatment -- **WHEN** the complete learning path is rendered -- **THEN** the level, lesson, or topic name remains present and operable -- **AND** path navigation, progress, and ordering remain unchanged diff --git a/openspec/changes/document-path-shadda-rendering/tasks.md b/openspec/changes/document-path-shadda-rendering/tasks.md deleted file mode 100644 index cc30deb031549cc5cce03746160094bb98bf86e3..0000000000000000000000000000000000000000 --- a/openspec/changes/document-path-shadda-rendering/tasks.md +++ /dev/null @@ -1,11 +0,0 @@ -# Tasks - -## 1. Complete-path shadda rendering - -- [x] 1.1 Route visible level, lesson, and topic names through the shared shadda-aware transliteration renderer; verify the main inline script passes `node --check` and `git diff --check` -- [x] 1.2 Add a focused browser regression test using a shadda-bearing path name; verify the rendered label contains the visual shadda wrapper while its title or accessible name retains the original text -- [x] 1.3 Manually check the complete path in an Android or Android-equivalent mobile browser; verify the shadda is positioned above the associated Hebrew letter and path buttons remain usable by touch and keyboard - -## Workflow follow-up - -- Archive the change after the remaining verification tasks pass so the delta is merged into the main `learner-home` specification. diff --git a/openspec/changes/fix-baseline-forgiveness/design.md b/openspec/changes/fix-baseline-forgiveness/design.md deleted file mode 100644 index f6cee709ab36017fc625635a010c686cc5920b49..0000000000000000000000000000000000000000 --- a/openspec/changes/fix-baseline-forgiveness/design.md +++ /dev/null @@ -1,78 +0,0 @@ -# Design - -## Context - -See proposal.md (Why). The baseline today: - -- `Scorer.reference_logp` turns a native TTS recording into a `Reference`: per sound, `(word, sound)` and its score. A reference is dropped entirely when it is unrelated, or its mean score is under `ref_min_mean` (60). Trust is decided per voice, not per sound. -- `Scorer._baseline` takes, per learner token, the **lowest** score any reference got on the same sound (sounds matched with `SequenceMatcher`). -- In `score_logp` the learner's score becomes `100 * score / max(ref, 1)`, capped at 100. A reference near 0 makes any learner realization 100. The substitute in the learner's recording plays no part. -- Flagging (a tip and a letter reason) requires `score < flag_below[sound]` and `_beyond_margin`. `_beyond_margin` passes whenever the pair's margin is 0, which is the default, so a competitor that never won a frame can still produce a tip. The trace shows `competitor_frames` and `p_competitor`, but flagging ignores them. - -Measured facts that shape the approach: - -- **Real native speech (lab, 2026-10-19):** ح is heard as خ in only 2% of native tokens, and a real خ in place of ح is caught 64% of the time. ħ→x is reliable model evidence. -- **ع:** the model has a documented false negative (the code's `ayin_deleted_floor`, and `test_native_reference_forgives_what_the_model_mishears`). The native voice's ع heard as ء is a model limitation the baseline should keep forgiving. - -## Goals / Non-Goals - -**Goals:** -- Compensation is tied to a matching failure in the reference, per sound. -- Pairs proven reliable on natives can't be erased by TTS references. -- No substitution tip without frame-level or confident evidence. -- The trace explains each decision. - -**Non-Goals:** -- Replacing the TTS references with human recordings. That's a later data task, noted in Open Questions. -- Fixed per-sound trust thresholds or forgiveness caps. These were considered and rejected (below). -- Changing exemptions, calibration floors or the unrelated-sentence logic. - -## Decisions - -1. **Record what took each sound's place in the reference.** - - What: `Reference` gains `heard: list[str | None]`, per sound, computed exactly as the learner's `heard_ph` is ("_" when deleted, the competitor's symbol when it is beyond its margin, None when the sound was kept). Default `[]`, so older references in memory are treated as "unknown". Built in `reference_logp` from the fit's token scores. - - Why: compensation must compare failures, and the same function defining "heard" for both sides keeps them comparable. - - Alternative: re-decoding the reference at scoring time. Rejected as wasted work; references are cached once per phrase and voice. - -2. **Compensation only when the reference lost the sound too.** - - What: `_baseline` becomes per token. For each reference that has the sound, its score counts only if one of these holds: - - (a) the learner's sound was not a substitution (kept, or competitor not beyond margin with evidence): today's behavior for a weak but correct sound - - (b) the reference lost the sound too (its `heard` is not None: a substitute or deleted), however it lost it - - (c) the reference's `heard` is unknown (an old cached reference) - The minimum is then taken over the counting references. A reference that kept the sound while the learner substituted is refused ("kept"). - - Why: a reference that kept the sound is no evidence the model can't hear it. Requiring the *same* failure, as first planned, refused compensation on correct speech: TTS takes heard as ه against references heard as خ dropped from 100 to 86, and ħ→h is the common native false positive (about 13% in the lab). The reported ħ→x case is covered by the protected pairs (decision 3), not by matching failures. - - Alternatives: - - A per-sound trust threshold (ignore a reference under about 40 on a sound): it would cancel the deliberate ع compensation, and no data picks the number. - - Using the maximum over references instead of the minimum: it doesn't help when every voice fails, as in the reported trace. - -3. **Protected pairs in the rules.** - - What: the rules' `scoring` block gains `baseline_protected: [{target: ħ, heard: x}]`. When the learner's `(sound, heard_ph)` is a protected pair, no reference counts for that token. - - Why: the TTS voices themselves produce a ح that the model hears as خ. That's a property of the voices, not the model, so (b) would wrongly forgive it. The lab result justifies ħ→x specifically. - - Adding a pair later follows the existing "human review before calibration" practice: lab natives as negatives, swapped-phoneme natives as positives. - - Alternative: a hard-coded set in `scorer.py`. Rejected, because the rules file already owns per-pair scoring policy (margins). - -4. **Evidence requirement for flagging.** - - What: a substitution flag also requires `comp_frames >= 1` or `exp(comp_peak) >= confident_peak` (0.30). Deletions ("_") keep their own deletion evidence (`deleted_peak` / `deleted_comp`). - - Implemented as a check next to `_beyond_margin`, so it gates both the tip and the letter's `why`. The phoneme's score is unchanged. - - Why: the false "ء heard as ب" had p 0.06 against 0.08 and no frame won. Scores stay honest, but the learner isn't told something the model barely suggested. - -5. **Trace.** - - What: the baseline stage records per reference `heard` and, when compensation is refused, `refused: "protected" | "kept" | null`. The dashboard (`static/trace.html`) shows it as text, with no layout change. - - Why: to keep the stage-by-stage explanation the team already relies on. - -The affected code is listed in proposal.md (Impact). The API shape, browser state and Supabase schema are unchanged. Deployment needs only the code and the updated rules file. The browser renders scores and reasons it already understands. - -## Risks / Trade-offs - -- **[More false tips where the TTS voice kept a sound the model misses in learners]** → only a reference that kept the sound is refused; any failure in it still forgives. We'll measure native false flags on the stored lab recordings before and after (tasks), and hold the change if ħ false flags rise beyond the 2% baseline. -- **[Learners' scores drop on ح]** → intended. Best scores already saved stay, and only new attempts change. -- **[Old cached references lack `heard`]** → treated as unknown: today's behavior until they're rebuilt (in memory, rebuilt on restart). -- **[Evidence gate hides some true substitutions with brief, low-confidence competitors]** → the sound's score still shows it, and the gate only removes the tip and the reason. - -## Migration Plan - -Deploy with the rules file. There's no data migration. Rollback means reverting the commit. Without `baseline_protected` in the rules file, protected pairs are simply empty. - -## Open Questions - -- Should human native recordings replace the TTS voices as references for pharyngeals? It's a data task that doesn't change this design. diff --git a/openspec/changes/fix-baseline-forgiveness/proposal.md b/openspec/changes/fix-baseline-forgiveness/proposal.md deleted file mode 100644 index d1cc704298072084ea9554929ea05ec6cc1aabfc..0000000000000000000000000000000000000000 --- a/openspec/changes/fix-baseline-forgiveness/proposal.md +++ /dev/null @@ -1,33 +0,0 @@ -# Proposal - -## Why - -The native-reference baseline erases real learner errors on the sounds the app exists to teach. In a reported attempt at "אנחנו עזרנו" (احنا ساعدنا), the model heard ح as خ (p 0.39 against 0.11) and ع as ء, yet both scored 100 and the sentence 88 ("מצוין"). The only tip shown was a doubtful glottal stop the learner reported as wrong. The baseline divides a learner's sound score by the lowest score either TTS reference voice got on that sound, and both voices scored ح at about 1/100 in this phrase. So any learner realisation becomes 100, whatever took its place. The same happens across most lessons: a ح said as خ scores 80–100. That contradicts the 2026-10-19 lab finding that the model hears ح→خ correctly in real native speech (2% false flags, 64% detection of a real خ). It also undercuts the product's central promise to Hebrew speakers: the app hears which consonant went wrong. - -## What Changes - -- **Only when the reference lost the sound too.** A native reference compensates a learner's substitution only when that reference also lost the sound (whatever took its place, or nothing). A reference that kept the sound no longer cancels the learner's substitution. To support this, the reference records, per sound, what took its place. -- **Protected pairs.** For substitution pairs the model is shown to tell apart in native speech, the baseline never raises the score: initially ħ→x, from the lab evaluation. The list lives in the scoring rules, and pairs are added only after the same kind of lab evaluation. -- **Evidence for a tip.** A substitution tip, and the "heard as" reason a letter gives, require the substitute to win at least one frame in the sound's window, or to reach the confident peak (0.30). With weaker evidence the sound keeps its score but gets no substitution tip. This addresses the reported false "ء heard as ب". -- **Regression measurement.** The baseline change is measured on stored native recordings (false flags on ح) and on a set of ح→خ substitutions, before and after. -- **BREAKING (scores).** Learners who say خ for ح get lower scores and a tip where they previously got about 100. That's the intended correction. Saved best scores are not rewritten. - -Out of scope: changing the acoustic model, adding new teacher calibration floors, changing the ع deletion floor, rewriting stored attempts or calibrations, and the browser layout of results. - -## Capabilities - -### New Capabilities - -None. - -### Modified Capabilities - -- `pronunciation-practice`: "Native reference compensation" narrows from forgiving any sound the reference scored low, to forgiving only the same failure, and never for protected pairs. "Pronunciation scoring" gains an evidence requirement before a substitution tip is given. - -## Impact - -- **Code:** `app/scorer.py` (`Reference`, `reference_logp`, `_baseline`, the baseline step of `score_logp`, flagging, and the trace's baseline stage), `app/reference.py` (built references carry the new per-sound field), and `static/assets/pronunciation-rules.md` (the protected pairs). -- **Tests:** `tests/test_scoring.py` gains baseline, protected-pair and tip-evidence cases. The existing ع compensation test keeps passing. -- **Data and API:** the `/api/score` response shape is unchanged; only scores, tips and letter reasons change. The trace dashboard's baseline stage shows why compensation applied or was refused. There's no Supabase schema change, and stored attempts are not migrated. -- **Privacy, authorization, paid APIs:** no change. References are still built from the existing TTS voices, which are cached, so no extra TTS calls are needed. -- **Degraded behaviour:** without TTS (no references), scoring is unchanged except for the evidence requirement on tips. diff --git a/openspec/changes/fix-baseline-forgiveness/specs/pronunciation-practice/spec.md b/openspec/changes/fix-baseline-forgiveness/specs/pronunciation-practice/spec.md deleted file mode 100644 index 71d0acff4b1c9e827ba555aeddab5a04f41b2835..0000000000000000000000000000000000000000 --- a/openspec/changes/fix-baseline-forgiveness/specs/pronunciation-practice/spec.md +++ /dev/null @@ -1,52 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Native reference compensation -When a native TTS reference is available, the service SHALL compare learner sounds with the same model's scores for that reference so model errors in native speech do not unfairly reduce the learner score. A reference SHALL compensate a learner's substitution only when the reference lost that sound too, and never for a protected substitution pair. - -#### Scenario: Model also mishears the reference -- **GIVEN** the target sound is weak or misrecognized in the native reference -- **AND** the learner's realization of it is weak or misrecognized too -- **WHEN** the learner's result is assembled -- **THEN** the learner is not penalized solely for that model limitation -- **AND** no substitution tip is shown for it - -#### Scenario: Reference lost the sound differently -- **GIVEN** the model heard ح in the native reference as خ -- **AND** heard ح in the learner's recording as ه, a pair that is not protected -- **WHEN** the learner's result is assembled -- **THEN** the reference compensates the learner's score for ح, as it shows the model loses ح in this phrase - -#### Scenario: Reference kept the sound -- **GIVEN** the model recognized the target sound in the native reference -- **AND** heard another sound in its place in the learner's recording -- **WHEN** the learner's result is assembled -- **THEN** the learner's score for that sound is not raised by that reference - -#### Scenario: Protected substitution pair -- **GIVEN** a substitution pair listed as protected in the scoring rules, such as ح heard as خ -- **AND** every native reference lost ح the same way -- **WHEN** a learner's ح is heard as خ -- **THEN** no reference raises the learner's score for that sound -- **AND** the substitution tip is shown when the score is below the tip threshold - -#### Scenario: No native reference -- **GIVEN** no native reference can be built for the phrase (TTS unavailable or the phrase not recognized in it) -- **WHEN** the learner's result is assembled -- **THEN** scoring proceeds without compensation, as before - -## ADDED Requirements - -### Requirement: Evidence before a substitution tip -The service SHALL give a substitution tip, and a letter's "heard as" reason, only when the competing sound has acoustic evidence: it wins at least one frame in the target's window, or reaches the confident posterior. Without that evidence, the sound keeps its score and gets no substitution tip. - -#### Scenario: Competitor never wins a frame -- **GIVEN** the target ء at the start of احنا has a posterior of 0.06 -- **AND** its strongest competitor ب has 0.08 and wins no frame in the window -- **WHEN** the result is assembled -- **THEN** no tip saying ء was heard as ب is shown -- **AND** the letter shows no "heard as" reason - -#### Scenario: Competitor wins frames -- **GIVEN** the model heard خ in place of ح, winning at least one frame in its window -- **WHEN** the result is assembled with a score below the tip threshold -- **THEN** the substitution tip and the letter's "heard as" reason are shown diff --git a/openspec/changes/fix-baseline-forgiveness/tasks.md b/openspec/changes/fix-baseline-forgiveness/tasks.md deleted file mode 100644 index 0a4b47dfd460b4ed428c7ff5b02538c32c50a1cd..0000000000000000000000000000000000000000 --- a/openspec/changes/fix-baseline-forgiveness/tasks.md +++ /dev/null @@ -1,42 +0,0 @@ -# Tasks - -## 1. Baseline measurement (before) - -- [x] 1.1 Run `python scripts/minerva_benchmark.py --refs --json /tmp/before-refs.json` on the current code and keep the output. Verify: the JSON exists and reports per-sound false flags and the ħ→x detection rate. -- [x] 1.2 Score the ح→خ demo takes (`scripts/demo_video/takes/m892.wav` and its correct take `c892.wav`) through `/api/score` and record the scores. Verify: noted in the change's notes (the expected "before": m892 ≈ 71, c892 = 100). - -## 2. References record what took each sound's place - -- [x] 2.1 Add `heard: list[str | None]` (default empty) to `Reference`, and fill it in `reference_logp` with the same rule the learner side uses for `heard_ph` ("_" deleted, competitor beyond margin, None kept), factored into one helper used by both. Verify: a new test in `tests/test_scoring.py` builds a reference from emissions where ʕ is heard as ʔ, and asserts its `heard` has "ʔ" at that sound and None elsewhere. -- [x] 2.2 Make sure `app/reference.py` keeps building references unchanged apart from the new field. Verify: `pytest tests/test_scoring.py -k reference` passes. - -## 3. Compensation only for the same failure, and protected pairs - -- [x] 3.1 Add `baseline_protected` to the scoring block of `static/assets/pronunciation-rules.md` with `{target: ħ, heard: x}`, and read it in `app/rules.py` (missing means empty). Verify: a rules test asserts the pair is loaded, and an empty rules file gives an empty set. -- [x] 3.2 Rework `_baseline` and its use in `score_logp`: per token, a reference counts only when the learner's sound was not a substitution, or the reference lost the sound too (any substitute, or deleted), or the reference's `heard` is unknown; no reference counts for a protected pair. Verify with new tests: - - the existing `test_native_reference_forgives_what_the_model_mishears` still passes (ع→ء) - - a reference heard ħ as x and the learner ħ as h (not protected) → forgiven - - a reference heard ħ as h and the learner dropped ħ → forgiven (the reference lost it too) - - a reference kept ħ and the learner said x → not forgiven - - both the reference and the learner heard ħ as x → not forgiven (protected), tip shown - - a learner's weak but correct sound is still compensated -- [x] 3.3 Record in the trace's baseline stage each reference's `heard` and `refused` ("protected" / "kept" / null), and show it in `static/trace.html` as text. Verify: a test on `diagnostics=True` output for the protected case asserts `refused == "protected"`. Manual browser check: open `/trace` with a mock trace and see the refusal reason on the baseline stage. - -## 4. Evidence before a substitution tip - -- [x] 4.1 Gate substitution flags (and with them the tip and the letter's `why`) on `comp_frames >= 1` or `exp(comp_peak) >= confident_peak`; deletions keep their existing evidence rule. Verify with new tests: - - a token whose competitor has p 0.08 and no frame won, against a target at 0.06 → no tip, no `why`, same score - - a competitor that wins frames → tip shown -- [x] 4.2 Update the "Native reference compensation" and tip-evidence wording in `static/assets/pronunciation-rules.md` (its prose section on the baseline) to match the new behavior. Verify: the text names the same-failure rule, the protected pairs and the evidence rule. - -## 5. Integration and regression - -- [x] 5.1 Run the focused tests, then the full suite: `pytest tests/test_scoring.py tests/test_rules.py` and then `pytest`. Verify: all pass, apart from failures that exist on main without this change (list any). -- [x] 5.2 Generate the TTS references for the benchmark's test lines into the local cache (both baseline voices, not uploaded to the bucket; a paid TTS call per uncached line and voice, approved), then run `python scripts/minerva_benchmark.py --refs` on the code before and after this change and compare them with `--compare`. Verify: ħ→x detection with references rises; the ħ native false-flag rate stays at or below about 2%, plus or minus noise; no other sound's false flags rise by more than 2 points (report any that do). -- [x] 5.3 Score the demo takes again through `/api/score`, plus the reported "אנחנו עזרנו" case if its audio is available. Verify: the ح→خ take scores lower with a "ħ>x" tip; the correct take stays at 95 or more; the doubtful initial ء gives no tip. -- [x] 5.4 Manual browser check at phone width: a speaking exercise with a ح→خ attempt shows the red ح and its tip, and a correct attempt shows no false letter reason. - -## Workflow follow-up - -- After approval, archive the change and sync the "Native reference compensation" requirement into `openspec/specs/pronunciation-practice/spec.md`. -- Re-record the promo video's practice scene, because the demo takes' scores will change. diff --git a/openspec/config.yaml b/openspec/config.yaml deleted file mode 100644 index 4ec181356bc37eb14a4c5b77c894d87435f3a77c..0000000000000000000000000000000000000000 --- a/openspec/config.yaml +++ /dev/null @@ -1,34 +0,0 @@ -schema: spec-driven - -context: | - ihki is a mobile-first Hebrew/RTL learning application for spoken Palestinian - Arabic and 9th-grade mathematics. The production service is a FastAPI app - deployed as a Docker-based Hugging Face Space. Supabase authentication and - persistence are optional; when they are not configured, practice remains open. - Student-facing copy is primarily Hebrew, Arabic answers may use Arabic script - or Hebrew transliteration, and a practice day uses Asia/Jerusalem time. - Never expose Supabase secret keys, third-party API keys, private recordings, or - one student's identifiable data to another student. - -rules: - proposal: - - State the learner or teacher outcome and what is explicitly out of scope - - Identify privacy, authorization, persistence, and paid-API impact - specs: - - Use observable behavior and concrete Given/When/Then scenarios - - Cover degraded behavior when optional Supabase, TTS, model, or browser capabilities are unavailable - design: - - Identify affected API endpoints, browser state, Supabase schema, and deployment configuration - - Preserve RTL, touch, keyboard, and no-mouse interaction where relevant - tasks: - - Include focused automated tests for server-side behavior - - Include a manual browser check for user-interface changes - -operations: - apply: - guidance: - - Preserve unrelated working-tree changes - - Run focused tests before the full test suite - archive: - guidance: - - Confirm the implementation and migrations are reflected in the main specs diff --git a/openspec/specs/.gitkeep b/openspec/specs/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/openspec/specs/access-and-privacy/spec.md b/openspec/specs/access-and-privacy/spec.md deleted file mode 100644 index a6c5af441f0d29cc24e31d52477d8e65d9dfb43a..0000000000000000000000000000000000000000 --- a/openspec/specs/access-and-privacy/spec.md +++ /dev/null @@ -1,47 +0,0 @@ -# Access and Privacy - -## Purpose - -Define sign-in, roles, feature entitlements, subject scope, and sensitive-data boundaries. - -## Requirements - -### Requirement: Optional authentication -The service SHALL require a valid Supabase session for practice when Supabase is configured and SHALL allow open practice when it is not configured. - -#### Scenario: Protected request has no session -- **GIVEN** Supabase authentication is configured -- **WHEN** an unauthenticated client calls a protected practice endpoint -- **THEN** the service refuses the request - -### Requirement: Role and entitlement authorization -The service SHALL authorize every protected server operation from the caller's roles, entitlements, class membership, and subject scope rather than trusting browser state. - -#### Scenario: Student attempts a management operation -- **WHEN** a student without management permission calls a management endpoint -- **THEN** the service refuses the operation - -#### Scenario: Tutor entitlement is absent -- **WHEN** a learner without the explicit tutor entitlement requests the paid tutor -- **THEN** the service refuses the request even if other student features are enabled - -### Requirement: Administrative scope -Developers SHALL have full cross-subject access, subject admins SHALL manage only their assigned subject, and class teachers SHALL manage only their assigned classes. - -#### Scenario: Subject admin targets another subject -- **WHEN** a subject admin attempts to change a class or user belonging to another subject -- **THEN** the service refuses the change - -### Requirement: Secret isolation -The application SHALL keep service-role credentials and third-party API keys on the server and SHALL never return their plaintext values to the browser. - -#### Scenario: Developer inspects configured keys -- **WHEN** an authorized developer opens key management -- **THEN** the browser receives presence and masked metadata only - -### Requirement: Private recordings -The application SHALL store flagged and lab recordings in private storage and SHALL release them only through authorized server endpoints. - -#### Scenario: Unauthorized user requests flag audio -- **WHEN** a caller without review permission requests a stored flag recording -- **THEN** the service refuses the request diff --git a/openspec/specs/learner-home/spec.md b/openspec/specs/learner-home/spec.md deleted file mode 100644 index 29527edef3629b4c058819feef758f452e61a892..0000000000000000000000000000000000000000 --- a/openspec/specs/learner-home/spec.md +++ /dev/null @@ -1,159 +0,0 @@ -# learner-home Specification - -## Purpose - -Define how a learner establishes their current class lesson and receives a small, accessible set of relevant next actions instead of navigating the complete course path first. - -## Requirements - -### Requirement: Per-class current-lesson onboarding -The application SHALL require an authenticated student to select the current lesson the first time they enter a class for which they have no saved selection, and SHALL restrict the choices to lessons in that class's single level. - -#### Scenario: Student enters a class for the first time -- **GIVEN** the student has no saved current lesson for the selected class -- **WHEN** the student enters the learning experience -- **THEN** the application asks which lesson the class is currently studying -- **AND** offers only lessons belonging to that class's level -- **AND** waits for a selection before showing the focused learner home - -#### Scenario: Student has multiple classes -- **GIVEN** the student belongs to more than one class -- **WHEN** the student selects a class with no saved current lesson -- **THEN** the application asks for the current lesson of that class -- **AND** keeps that selection independent of selections for the student's other classes - -#### Scenario: Class configuration does not identify one level -- **GIVEN** the selected class contains lessons from zero or multiple levels -- **WHEN** current-lesson onboarding would begin -- **THEN** the application explains that the class configuration must be corrected -- **AND** does not offer lessons from an ambiguous level - -### Requirement: Current-lesson persistence -The application SHALL associate the selected current lesson with the authenticated student and class, restore it across devices when server persistence is available, and allow the student to change it without altering learning progress. - -#### Scenario: Student returns on another device -- **GIVEN** the student previously selected lesson 5 for the class -- **AND** server persistence is available -- **WHEN** the student enters that class on another device -- **THEN** lesson 5 is restored as the current lesson -- **AND** onboarding is not shown again - -#### Scenario: Student changes the current lesson -- **GIVEN** the student's current lesson is lesson 5 -- **WHEN** the student changes it to lesson 6 -- **THEN** lesson 6 becomes the source of focused recommendations -- **AND** progress already recorded for lesson 5 remains unchanged - -#### Scenario: Server persistence is unavailable -- **GIVEN** the server cannot read or save the current-lesson selection -- **WHEN** the student selects a lesson -- **THEN** the application retains the selection on that device when browser storage is available -- **AND** keeps the learning experience usable -- **AND** does not expose one student's selection to another student using the device - -### Requirement: Focused learner home -The application SHALL make a focused home the student's primary post-onboarding view and SHALL show no more than three recommended actions derived from the selected class, current lesson, homework, today's daily videos, and progress. When more than three actions apply, the application SHALL keep the first three in this order: nearest-due incomplete homework, today's unwatched daily video, unfinished work in the current lesson, and a short review. - -#### Scenario: Incomplete homework exists -- **GIVEN** the selected class has incomplete homework -- **WHEN** the focused home is shown -- **THEN** an action for the nearest-due incomplete homework is included -- **AND** its due or overdue state is visible - -#### Scenario: Current lesson has unfinished work -- **GIVEN** the current lesson contains unfinished activities -- **WHEN** the focused home is shown -- **THEN** an action continues unfinished work in that lesson -- **AND** the learner does not need to scroll through earlier lessons - -#### Scenario: Today's daily video has not been watched -- **GIVEN** the selected class has a daily video dated today -- **AND** the video has not been opened on this device -- **WHEN** the focused home is shown -- **THEN** an action that opens today's daily video is included -- **AND** it is placed after any homework action and before the continue-lesson action - -#### Scenario: No daily video to recommend -- **GIVEN** the selected class has no daily video dated today, or today's video has already been opened on this device -- **WHEN** the focused home is shown -- **THEN** no daily-video action is shown -- **AND** the remaining actions keep their priority order - -#### Scenario: Current lesson is complete -- **GIVEN** the learner has completed the selected current lesson -- **WHEN** the focused home is shown -- **THEN** the application offers a short review or allows the learner to select a different current lesson -- **AND** does not silently advance the class's current lesson - -### Requirement: Short recommended activity -When the focused home shows a short activity, it SHALL be bounded to no more than ten items and SHALL show its workload before the learner starts it. The short activity MAY be omitted when three higher-priority actions apply. - -#### Scenario: Three higher-priority actions apply -- **GIVEN** incomplete homework, an unwatched daily video for today, and unfinished work in the current lesson all exist -- **WHEN** the focused home is shown -- **THEN** those three actions are shown -- **AND** the short activity is not shown - -#### Scenario: Learner starts a short activity -- **WHEN** the learner activates the short recommendation -- **THEN** the session contains no more than ten items -- **AND** completion is clearly announced when the session ends - -#### Scenario: Recommended material is unavailable -- **GIVEN** the current lesson has insufficient available material for the recommendation -- **WHEN** the focused home is built -- **THEN** the application offers another valid action or omits that recommendation -- **AND** never shows an action that cannot be started - -### Requirement: Full-path access and positioning -The focused home SHALL provide access to the complete learning path, and the path SHALL open at the selected current lesson while preserving access to earlier and later class lessons. - -#### Scenario: Learner opens the complete path -- **GIVEN** the student's current lesson is lesson 5 -- **WHEN** the learner chooses the complete path -- **THEN** the path positions lesson 5 in view -- **AND** earlier and later lessons assigned to the class remain accessible -- **AND** the path does not show daily-practice or homework shortcut buttons above it - -### Requirement: Referral access from home -The focused home SHALL provide access to the existing refer-a-friend screen without counting that access as one of the three recommended learning actions, and SHALL remove the duplicate referral tab from the focused learner's bottom navigation. - -#### Scenario: Learner opens refer a friend -- **WHEN** the learner activates “חבר מביא חבר” on the focused home -- **THEN** the existing referral screen opens -- **AND** the referral action is not duplicated in the bottom navigation - -### Requirement: Reminder coordination -Current-lesson onboarding and the focused home SHALL work with the existing daily-video push reminders without stacking prompts or blocking a reminder's destination. - -#### Scenario: Onboarding visit shows no reminder prompt -- **GIVEN** the student completes current-lesson onboarding during this visit -- **WHEN** the focused home appears -- **THEN** the prompt to turn on daily-video reminders is not shown during this visit -- **AND** on a later visit the prompt follows the good-moment rule below - -#### Scenario: Reminder offered after a good moment -- **GIVEN** the learner did not decline the reminder in the last seven days and has not turned it on -- **WHEN** the learner finishes a topic or lesson, gets a first gold bead, or reaches the daily goal, and then returns to the home -- **THEN** the prompt to turn on daily-video reminders is shown -- **AND** it is not shown on entering the application or in the middle of practice - -#### Scenario: Reminder opened before a lesson is chosen -- **GIVEN** the student has no saved current lesson for the active class -- **WHEN** the student opens the application from a daily-video reminder -- **THEN** the daily videos screen opens directly without onboarding first -- **AND** onboarding is shown when the student goes to the home - -#### Scenario: Reminder opened with a saved lesson -- **GIVEN** the student has a saved current lesson for the active class -- **WHEN** the student opens the application from a daily-video reminder -- **THEN** the daily videos screen opens directly -- **AND** returning from it shows the focused home - -### Requirement: Accessible focused navigation -Current-lesson onboarding and focused-home actions SHALL support RTL layout, touch, keyboard-only operation, visible focus, and meaningful accessible names. - -#### Scenario: Keyboard-only onboarding -- **WHEN** a learner completes onboarding without a pointing device -- **THEN** every lesson option and confirmation action is reachable and operable by keyboard -- **AND** focus moves predictably into the focused home after selection diff --git a/openspec/specs/learning-activities/spec.md b/openspec/specs/learning-activities/spec.md deleted file mode 100644 index cc98b726688c860bb40d3a9fe58e064fe741d27c..0000000000000000000000000000000000000000 --- a/openspec/specs/learning-activities/spec.md +++ /dev/null @@ -1,59 +0,0 @@ -# Learning Activities - -## Purpose - -Define conjugation games, stories, live lessons, the AI tutor, and the mathematics path. - -## Requirements - -### Requirement: Conjugation game integrity -The service SHALL generate conjugation questions from complete course tables, keep correct answers server-side, and allow each issued question to be graded at most once. - -#### Scenario: Reusing a graded question -- **WHEN** a client submits another answer for an already graded question -- **THEN** the service refuses to grade it again - -#### Scenario: Spoken answer is uncertain -- **WHEN** speech recognition is unrelated or below confidence -- **THEN** the outcome is uncertain -- **AND** it does not count as a grammatical attempt - -### Requirement: Story quiz -The browser SHALL score each story line once, return skipped or unheard lines after the last line, and summarize the average and three most frequent mistakes. - -#### Scenario: Learner ends early -- **WHEN** the learner ends a quiz before all lines are scored -- **THEN** the browser summarizes only the lines completed so far - -### Requirement: Ephemeral live lesson -The service SHALL permit one active live session per class, give each learner one scored attempt per question, and keep session state only in application memory. - -#### Scenario: Empty live answer -- **WHEN** a learner submits empty or undecodable audio -- **THEN** no scored attempt is consumed -- **AND** the learner may retry - -#### Scenario: Session expires -- **WHEN** the teacher ends the session, the server restarts, or the teacher heartbeat is absent for two hours -- **THEN** attendance, answers, and live results are erased - -### Requirement: Paid AI tutor limits -The service SHALL require the tutor entitlement, enforce configured daily and conversation limits, and never expose the Anthropic API key to the browser. - -#### Scenario: Daily limit is reached -- **WHEN** a learner starts or continues beyond the daily allowance -- **THEN** the service refuses the paid model request with a limit result - -### Requirement: Mathematics skill graph -The mathematics path SHALL unlock a skill only after all prerequisite skills are complete and SHALL reject invalid graphs with cycles, unknown or redundant dependencies, invalid answer counts, or remediation edges that point forward. - -#### Scenario: Learner opens a locked skill -- **WHEN** the learner selects a skill with incomplete prerequisites -- **THEN** the browser identifies the prerequisite skills to complete first - -### Requirement: Photographed mathematics problem privacy -The application SHALL send an authorized learner's photo to the configured model for structured solution help, SHALL not retain the photo, and SHALL disclose the external processing in the privacy page. - -#### Scenario: Model cannot read a photo -- **WHEN** the model cannot extract a usable problem -- **THEN** the request does not consume the learner's daily photo allowance diff --git a/openspec/specs/progress-and-practice/spec.md b/openspec/specs/progress-and-practice/spec.md deleted file mode 100644 index 859951ca0f1e5596e5ba8d965aa3cd79bf992ead..0000000000000000000000000000000000000000 --- a/openspec/specs/progress-and-practice/spec.md +++ /dev/null @@ -1,98 +0,0 @@ -# Progress and Practice - -## Purpose - -Define durable learner progress, daily practice, homework, and completion feedback. - -## Requirements - -### Requirement: Attempt persistence -When Supabase is configured, the service SHALL store attempt metadata under the authenticated learner and SHALL NOT store routine practice audio. - -#### Scenario: Authenticated learner completes a score -- **WHEN** a learner receives a scored pronunciation result -- **THEN** the service stores the phrase, topic, score, diagnostic issues, model, and version for that learner -- **AND** does not store the recording bytes - -#### Scenario: Supabase is not configured -- **WHEN** a visitor practices in open mode -- **THEN** scoring remains available -- **AND** durable cross-device progress is unavailable - -### Requirement: Progress summary -The service SHALL summarize each learner's best score and attempts per phrase, practice days, weekly activity, and current and longest streaks. - -#### Scenario: Learner returns on another device -- **GIVEN** the learner signs in to an account with stored attempts -- **WHEN** progress is requested -- **THEN** the learner's best scores and practice history are restored - -#### Scenario: One missed day keeps the streak -- **GIVEN** the learner practised on consecutive days, then missed exactly one day -- **WHEN** the learner practises again the next day, and no other missed day was forgiven in the previous seven days -- **THEN** the streak continues as if the missed day was practised (a streak freeze, "יום חופש") -- **AND** a second missed day within the same seven days ends the streak - -### Requirement: Collection and completion -The browser SHALL represent practiced phrases as beads, make a bead gold at a best score of at least 75, and award a lesson masbaha only after every listed spoken phrase in that lesson is practiced. - -#### Scenario: Story topic reaches gold -- **GIVEN** a story contains multiple lines -- **WHEN** every line has a best score of at least 75 -- **THEN** the story is represented by one gold bead - -#### Scenario: A finished lesson opens a bead colour -- **GIVEN** the learner has earned N lesson masbahas -- **WHEN** the learner opens the rewards collection -- **THEN** the first N+1 bead colours can be chosen, and the rest are shown locked - -### Requirement: Daily goal and momentum -The browser SHALL show a daily goal of five different sentences said, celebrate reaching it, celebrate the first gold bead ever on a full screen, play a sound and vibrate (where supported) on each gold, and call out runs of gold sentences in a row. The focused home SHALL show the class's sentences this week against a shared goal, as totals only, with no other student's numbers or names. - -#### Scenario: Daily goal reached -- **GIVEN** the learner has said four different sentences today -- **WHEN** the learner says a fifth different sentence -- **THEN** the daily goal is shown as reached - -#### Scenario: Class week on the home -- **GIVEN** the learner's class has students who practised this week -- **WHEN** the focused home appears -- **THEN** it shows the class's total sentences against its goal and the learner's own count -- **AND** no other student's count or name is shown - -### Requirement: Daily practice session -The browser SHALL build a ten-sentence daily session from weak practiced sentences and new course-ordered sentences, preferring homework and the selected class's current lesson before other unfinished lessons. - -**Selection priority (non-normative):** - -New material comes from the first source that applies: - -1. **Homework material**, when there is incomplete homework. -2. **Current-lesson material**, when a current lesson is saved and still has unpracticed sentences. -3. **The first unfinished lesson**, otherwise. - -The existing weak/new balance and ten-sentence session size still apply. - -#### Scenario: One pool has too few sentences -- **WHEN** either the weak or new sentence pool cannot supply its half -- **THEN** the other pool fills the remaining places -- **AND** story lines are excluded - -#### Scenario: Current lesson is selected -- **GIVEN** the learner selected a current lesson for the active class -- **AND** no incomplete homework takes priority -- **WHEN** the browser chooses new sentences for daily practice -- **THEN** it chooses available unpracticed sentences from the selected current lesson before other lessons - -#### Scenario: No current lesson is available -- **GIVEN** the learner has no usable current-lesson selection -- **WHEN** daily practice is available without completing onboarding -- **THEN** the browser falls back to the first unfinished lesson - -### Requirement: Homework visibility and completion -The service SHALL support multiple unique homework topics per class, each with an optional due date, and SHALL consider a practice homework complete only after every sentence in its topic has been practiced. - -#### Scenario: Homework due date passes -- **WHEN** the learner opens the path after the due date -- **THEN** the homework remains visible with overdue status -- **AND** it does not change the path's normal next-topic focus diff --git a/openspec/specs/pronunciation-practice/spec.md b/openspec/specs/pronunciation-practice/spec.md deleted file mode 100644 index 3d52819e50eb207167ba159c919d052f81dc7ff9..0000000000000000000000000000000000000000 --- a/openspec/specs/pronunciation-practice/spec.md +++ /dev/null @@ -1,51 +0,0 @@ -# Pronunciation Practice - -## Purpose - -Define how learners receive prompts, record spoken Arabic, and receive phoneme-level feedback. - -## Requirements - -### Requirement: Available phrase bank -The service SHALL return only the phrases and lesson content available to the current learner, including teacher-added texts in the learner's classes. - -#### Scenario: Learner requests practice content -- **GIVEN** the learner is authorized for practice -- **WHEN** the learner requests the phrase bank -- **THEN** the response contains the built-in phrases and any eligible class text lines -- **AND** content outside the learner's lessons is excluded - -### Requirement: Pronunciation scoring -The service SHALL compare a recording with every accepted pronunciation variant and return a 0–100 result with word- and phoneme-level evidence. - -#### Scenario: Recording matches an accepted variant -- **WHEN** a learner submits decodable audio for an available phrase -- **THEN** the service selects the best-fitting accepted variant -- **AND** returns sentence, word, and phoneme scores -- **AND** identifies substitutions and available Hebrew articulation tips - -#### Scenario: Recording is unrelated -- **WHEN** the free phoneme decode is too far from every accepted variant -- **THEN** the result identifies the recording as an unrelated sentence -- **AND** does not present individual pronunciation tips as reliable findings - -#### Scenario: Audio cannot be scored -- **WHEN** uploaded audio is empty, oversized, unsupported, or undecodable -- **THEN** the service rejects the attempt without recording a scored result - -### Requirement: Native reference compensation -When a native TTS reference is available, the service SHALL compare learner sounds with the same model's scores for that reference so model errors in native speech do not unfairly reduce the learner score. - -#### Scenario: Model also mishears the reference -- **GIVEN** the target sound is weak or misrecognized in the native reference -- **WHEN** the learner's result is assembled -- **THEN** the learner is not penalized solely for that model limitation -- **AND** no substitution tip is shown for it - -### Requirement: Accessible feedback -The browser SHALL expose low-scoring letter feedback without requiring a mouse. - -#### Scenario: Learner taps a marked letter -- **GIVEN** a letter has a score below 75 and a reason -- **WHEN** the learner taps the dotted-underlined letter -- **THEN** the browser shows the reason and tip near that letter diff --git a/openspec/specs/quality-and-calibration/spec.md b/openspec/specs/quality-and-calibration/spec.md deleted file mode 100644 index e6606d5ebdc070689b7704c797365de2a2520299..0000000000000000000000000000000000000000 --- a/openspec/specs/quality-and-calibration/spec.md +++ /dev/null @@ -1,36 +0,0 @@ -# Quality and Calibration - -## Purpose - -Define how scoring errors are reported, reviewed, benchmarked, and calibrated safely. - -## Requirements - -### Requirement: Explicit scoring-error report -Only an authorized teacher or tutor SHALL be able to flag a scored attempt, and the report SHALL preserve the recording, score trace, model version, optional note, and private reporter identity for review. - -#### Scenario: Authorized tutor flags an attempt -- **WHEN** the tutor states that a correctly spoken recording was scored incorrectly -- **THEN** the service stores the audio privately and creates a pending structured report - -### Requirement: Human review before calibration -An authorized reviewer SHALL be able to confirm or reject a report, classify it, and identify false sound judgments; only confirmed evidence SHALL support calibration. - -#### Scenario: Candidate is not yet approved -- **WHEN** repeated attempt data suggests a scoring issue -- **THEN** it remains an investigation candidate until a reviewer explicitly creates and applies a calibration - -### Requirement: Diagnostic lab -The lab SHALL support labeled human and TTS samples, preserve benchmark recordings privately, and report false rejection and planted-error detection per sound. - -#### Scenario: TTS benchmark runs -- **WHEN** an authorized user runs the automated TTS test -- **THEN** each item is scored without the native reference baseline -- **AND** the TTS speaker is reported separately from human speakers - -### Requirement: Regression protection -Before a scoring model or rule change is shipped, its benchmark SHALL be compared with stored labeled samples so false rejection improves without silently reducing mistake detection. - -#### Scenario: Current scorer is benchmarked -- **WHEN** stored samples are rescored with current model and rules -- **THEN** the output reports old and new per-sound results and aggregate detection metrics diff --git a/openspec/specs/teacher-content/spec.md b/openspec/specs/teacher-content/spec.md deleted file mode 100644 index a2faecb13ac6c06f39f9d36d392254218e7a6094..0000000000000000000000000000000000000000 --- a/openspec/specs/teacher-content/spec.md +++ /dev/null @@ -1,43 +0,0 @@ -# Teacher Content and Classes - -## Purpose - -Define class membership, course content, teacher-authored texts and videos, and monitoring. - -## Requirements - -### Requirement: Class configuration -Authorized staff SHALL be able to manage a class's students, nicknames, teachers, lessons, subject, homeworks, and scheduled daily videos. - -#### Scenario: Class configuration changes -- **WHEN** an authorized staff member saves valid class settings -- **THEN** the settings are available to class members on their next page load -- **AND** duplicate homework topics and invalid subjects are rejected - -### Requirement: Teacher-authored texts -Authorized staff SHALL be able to add a story or dialogue from paired Hebrew and Arabic-transliteration lines to an eligible lesson. - -#### Scenario: Text line counts differ -- **WHEN** the Hebrew and Arabic-transliteration inputs contain different non-empty line counts -- **THEN** the service rejects the text and reports both counts - -#### Scenario: Unknown transliterated word is accepted -- **WHEN** a word is not in the course lexicon but can be read by spelling rules -- **THEN** the text is created -- **AND** the response identifies the rule-read word for teacher review - -### Requirement: Ordered lesson content -Authorized staff SHALL be able to place and reorder videos and teacher-authored texts among a lesson's built-in topics. - -#### Scenario: Learner opens the path -- **WHEN** lesson content is requested -- **THEN** built-in topics, videos, and texts appear in the teacher-defined order - -### Requirement: Privacy-preserving monitoring -Teacher activity views SHALL expose only learners and aggregate data within the caller's scope, and pronunciation-pattern reports SHALL not return learner identities. - -#### Scenario: Repeated pronunciation issue becomes a candidate -- **GIVEN** the same judgment occurred in at least five attempts by at least three learners and at least 30 percent of learners who attempted the phrase -- **WHEN** a teacher opens pronunciation patterns -- **THEN** the pattern appears as an investigation candidate -- **AND** it is not automatically applied as a calibration diff --git a/requirements-dev.txt b/requirements-dev.txt index b6c0e57c7140fbc63e4742c34a087c0c0b4eb538..ffd6277354c0051552ec5361086af9b954c839df 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ fastapi>=0.110 python-multipart>=0.0.9 httpx>=0.27 -numpy>=1.26 pytest>=8 diff --git a/requirements-train.txt b/requirements-train.txt deleted file mode 100644 index 50487c173494ad90ccbf8a574e21650c29b0bdb3..0000000000000000000000000000000000000000 --- a/requirements-train.txt +++ /dev/null @@ -1,4 +0,0 @@ --r requirements.txt -datasets>=2.19 -accelerate>=0.30 -soundfile>=0.12 diff --git a/requirements-zero-shot.txt b/requirements-zero-shot.txt deleted file mode 100644 index 84ee11b77cf6c20a37eda9f95d667078d6b9eade..0000000000000000000000000000000000000000 --- a/requirements-zero-shot.txt +++ /dev/null @@ -1,77 +0,0 @@ -# Extra packages for scripts/zero_shot_phone_models.py (ZIPA and POWSM-CTC), on top of requirements.txt. -# Installed with --no-deps into .zero-shot-deps/ by scripts/setup_zero_shot_deps.sh, never into the app venv: -# ESPnet's own pins (old librosa, typeguard, ...) conflict with the app's torch/numpy, so this is the exact set -# that was checked to work with torch 2.14 / numpy 2.2 / Python 3.10. - -# ZIPA (ONNX). The CPU build: onnxruntime-gpu 1.23 needs CUDA 12 and the app runs CUDA 13. -onnxruntime==1.23.2 -lhotse==1.33.0 -coloredlogs==15.0.1 -humanfriendly==10.0 -flatbuffers==25.12.19 -protobuf==7.36.2 -audioread==3.1.0 -cytoolz==1.2.0 -toolz==1.2.0 -intervaltree==3.2.1 -sortedcontainers==2.4.0 -soundfile==0.14.0 - -# exporting POWSM to ONNX (scripts/export_powsm_onnx.py) -onnx==1.17.0 - -# POWSM-CTC (ESPnet). 202511 is the newest release for Python 3.10; the script patches its two POWSM-CTC bugs. -espnet==202511 -espnet-model-zoo==0.1.7 -espnet-tts-frontend==0.0.4 -espnet-g2p-en==2.1.0.post1 -antlr4-python3-runtime==4.9.3 -ci_sdr==0.0.2 -cloudpickle==3.1.2 -ConfigArgParse==1.8.0 -ctc_segmentation==1.7.5 -Cython==3.3.0 -decorator==5.3.1 -defusedxml==0.7.1 -Distance==0.1.3 -editdistance==0.8.1 -einops==0.8.2 -fast_bss_eval==0.1.3 -h5py==3.16.0 -hydra-core==1.3.7 -importlib-metadata==4.13.0 -inflect==7.0.0 -jaconv==0.5.0 -jamo==0.4.1 -joblib==1.6.0 -kaldiio==2.18.1 -lazy-loader==0.6 -librosa==0.11.0 -llvmlite==0.50.0 -msgpack==1.2.3 -nltk==3.10.4 -numba==0.68.0 -omegaconf==2.3.1 -opt_einsum==3.4.0 -pandas==2.3.3 -platformdirs==4.12.4 -pooch==1.9.0 -pypinyin==0.44.0 -python-dateutil==3.0.0 -pytorch-wpe==0.0.1 -pytz==2026.5 -pyworld==0.3.5 -resampy==0.4.3 -scikit-learn==1.7.2 -scipy==1.15.3 -sentencepiece==0.1.97 -six==1.17.0 -soxr==1.1.0 -standard-aifc==3.13.0 -standard-sunau==3.13.0 -threadpoolctl==3.7.0 -torch-complex==0.4.4 -typeguard==2.13.3 -tzdata==2026.5 -Unidecode==1.4.0 -zipp==4.1.2 diff --git a/requirements.txt b/requirements.txt index 50643969fd2c7c0c235c10c7e87a08ace334db06..859ab1a420c6528cc1a8d19b410cdfe06dbcb868 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,7 @@ fastapi>=0.110 uvicorn[standard]>=0.29 -mcp>=2.2,<3 # Grok connector: official MCP SDK; 2.2+ bounds Streamable HTTP sessions -anyio>=4.10,<4.11 # newer releases hang the Python 3.10 worker-thread bridge used by FastAPI/MCP +anyio>=4.10,<4.11 # Python 3.10: newer releases can hang Starlette's worker-thread bridge python-multipart>=0.0.9 -numpy>=1.26 -pyyaml>=6.0 # the scorer reads its rules from the yaml blocks of static/assets/pronunciation-rules.md -transformers>=4.41 -huggingface_hub>=1.16 # Storage Bucket API (batch_bucket_files, list_bucket_tree) for the TTS cache -torch>=2.2 # Docker installs the CPU wheel first; declaring it here also makes local installs complete python-dotenv>=1.0 -anthropic>=1.0 # the AI tutor game בחכי ערבי (app/tutor.py) -cryptography>=42 # the API keys set in the panel are kept encrypted (app/keystore.py) -pywebpush>=2.0 # the daily video's morning reminder (app/push.py) -edge-tts>=7.0 -gTTS>=2.5 -tzdata>=2024.1 # time zones for zoneinfo: the slim image may not carry the system ones -onnxruntime>=1.20 # the exported POWSM phoneme model (app/powsm.py PowsmOnnxEmitter, MODEL_ID=powsm-onnx:) +anthropic>=1.0 # photographed-problem hints and solutions +tzdata>=2024.1 diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100644 index 7efe1a02273761f40b4b53e396e771e2c9c7b534..0000000000000000000000000000000000000000 --- a/scripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Developer and maintenance scripts that are also exercised by tests.""" diff --git a/scripts/bench.py b/scripts/bench.py deleted file mode 100644 index 8fc31a78046e9813e6e76adc3c44687e796f310f..0000000000000000000000000000000000000000 --- a/scripts/bench.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -"""Benchmark the phoneme recognition on the lab's labelled recordings, with the current scorer, against their stored scores. - -Usage: python scripts/bench.py # every lab recording - python scripts/bench.py --speaker "מרב" # one speaker's - python scripts/bench.py --flags # also: are the confirmed flags' false issues still flagged? - python scripts/bench.py --no-baseline # without the native TTS baseline (the acoustic model alone) - python scripts/bench.py --json out.json # every recording's old and new result - -Reads SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY (as the server does), MODEL_ID and, for the -baseline, SAWTAK_API_KEY. The lab (מעבדה, see app/lab.py) labels each recording said right or with one planted -mistake, so per sound: frr = said right but flagged / said right; detection = planted mistake flagged / planted. -Run it before shipping a scoring or model change: frr should go down without detection going down with it. -"stored" is the result kept when the recording was made, by the code of that day: to measure a change, save a run -of the code before it (--json) and compare. -""" -from __future__ import annotations - -import argparse -import json -import os -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from dotenv import load_dotenv # noqa: E402 - -from app import lab # noqa: E402 -from app.auth import AuthUnavailable, load_auth # noqa: E402 -from app.reference import References # noqa: E402 -from app.scorer import decode_audio, load_scorer # noqa: E402 -from app.tts import BASELINE_VOICES, load_tts # noqa: E402 - - -def pct(x: float | None) -> str: - return " – " if x is None else f"{100 * x:4.0f}%" - - -def change(old: float | None, new: float | None) -> str: - return f"{pct(old)} -> {pct(new)}" if old != new else f"{pct(new)} " - - -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - ap.add_argument("--speaker", default="", help="only this speaker's recordings") - ap.add_argument("--flags", action="store_true", help="also rescore the confirmed flags") - ap.add_argument("--no-baseline", action="store_true", help="score without the native TTS baseline") - ap.add_argument("--json", type=Path, help="write every recording's old and new result here") - args = ap.parse_args() - - load_dotenv() - auth = load_auth() - if auth is None or not auth.secret: - print("needs SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY", file=sys.stderr) - return 2 - try: - samples = [s for s in auth.lab_samples(full=True) if s.get("audio") - and (not args.speaker or (s.get("speaker") or "") == args.speaker)] - except AuthUnavailable as e: - print(f"the lab's recordings are not available ({e}); is the table lab_samples created? see the README", - file=sys.stderr) - return 2 - if not samples and not args.flags: - print("no lab recordings yet: record some in the lab (מעבדה), or run with --flags", file=sys.stderr) - return 1 - - model = os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft") - scorer = load_scorer(model) - tts = None if args.no_baseline else load_tts() - refs = References(scorer, tts, list(BASELINE_VOICES.values())) if tts else None - - def score(audio: bytes, phrase: dict, key) -> dict: - return scorer.score(decode_audio(audio), phrase, refs.get(key, phrase) if refs else []) - new: dict[int, dict] = {} - for k, s in enumerate(samples, 1): - phrase = lab.sample_phrase(s) - if phrase is None: # a legacy row without enough information to reconstruct its phrase - continue - try: - found = auth.lab_audio(s["id"]) - if found: - new[s["id"]] = score(found[0], phrase, phrase["key"]) - except (AuthUnavailable, ValueError) as e: - print(f"sample {s['id']}: skipped ({e})", file=sys.stderr) - print(f"\r{k}/{len(samples)} rescored", end="", file=sys.stderr) - print(file=sys.stderr) - scored = [s for s in samples if s["id"] in new] - reports = auth.list_flags("confirmed") - before, after = lab.benchmark(scored, reports=reports), lab.benchmark(scored, new, reports) - - t0, t1 = before["totals"], after["totals"] - print(f"model {model}{' (no baseline)' if args.no_baseline else ''} · {t1['recordings']} recordings " - f"({t1['correct']} said right, {t1['mistake']} with a planted mistake, {t1['unrelated']} unrelated)") - print(f"false rejections {change(t0['frr'], t1['frr'])} detection {change(t0['detection'], t1['detection'])} " - f"said right scoring >= {lab.GOOD_SCORE}: {change(t0['good_share'], t1['good_share'])}") - if t1["unplaced"]: - print(f"{t1['unplaced']} planted mistakes could not be placed on a sound of the reading the scorer chose") - if not scored: - print("(no lab recordings yet)") - print(f"\n{'sound':<6} {'right':>5} {'false rejections (stored -> now)':<32} {'planted':>7} " - f"{'detection':<18} {'mean':>5} {'reports':>7} heard instead") - old = {r["sound"]: r for r in before["sounds"]} - for r in after["sounds"]: - o = old.get(r["sound"], {}) - heard = " ".join(f"{h}×{n}" for h, n in list(r["confusions"].items())[:4]) - print(f"{r['sound']:<6} {r['right']:>5} {r['false_flags']:>3} {change(o.get('frr'), r['frr']):<28} " - f"{r['planted']:>7} {change(o.get('detection'), r['detection']):<18} " - f"{'' if r['mean'] is None else r['mean']:>5} {r['reports']:>7} {heard}") - - rows = [] - if args.flags: - fixed = still = 0 - print("\nconfirmed flags: the sounds a teacher marked as wrongly flagged, with the current scorer") - for f in reports: - if not f.get("audio") or not f.get("false_issues"): - continue - try: - found = auth.flag_audio(f["id"]) - result = score(found[0], lab.flag_phrase(f), ("flag", f.get("phrase"))) if found else None - except (AuthUnavailable, ValueError) as e: - print(f"flag {f['id']}: skipped ({e})", file=sys.stderr) - continue - if not result or result.get("error"): - continue - marks = [(i, lab.still_flagged(result, i)) for i in f["false_issues"]] - still += sum(s for _, s in marks); fixed += sum(not s for _, s in marks) - print(f"{f['id']:>6} {f.get('score')!s:>3} -> {result['score']:>3} " - + " ".join(f"{i['expected']}>{i.get('heard') or '_'}:{'still' if s else 'fixed'}" for i, s in marks) - + f" {f.get('he') or f.get('phrase')}") - rows.append({"flag": f["id"], "old": f.get("result"), "new": result}) - print(f"{fixed} fixed, {still} still flagged") - - if args.json: - rows += [{"sample": s["id"], "item": s["item"], "speaker": s.get("speaker"), "label": s["label"], - "mistake": s.get("mistake"), "old": s.get("result"), "new": new[s["id"]]} for s in scored] - args.json.write_text(json.dumps({"before": before, "after": after, "rows": rows}, ensure_ascii=False, indent=1), - encoding="utf-8") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/compare_verbs.py b/scripts/compare_verbs.py deleted file mode 100644 index b432c3d7c4ca5cdd40ef0c47512c0954298dd8ee..0000000000000000000000000000000000000000 --- a/scripts/compare_verbs.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python3 -"""Check the phonemes of static/assets/arabic_verbs.json against what the scorer uses. - -Usage: python scripts/compare_verbs.py # summary + every difference - python scripts/compare_verbs.py --all # also list the forms that match - python scripts/compare_verbs.py --no-vocab # skip the model-vocab check (no tokenizer download) - -For every past / future form (and its accepted_phonemes) it reports: - bank the same Arabic word in the phrase bank (phrases.py, workbook LEX, grammar VERBS) and whether its IPA - agrees: "same" (identical), "same sounds" (identical once archify() symbols and base() are applied, - i.e. the scorer would build the same target), or "differs" - archi how archify() rewrites the form (ق ك ث ذ ظ ج as Q K TH DH ZH J) and the letters it could not place - vocab phonemes the model has no symbol for (mapped to a fallback, or not expressible at all) -""" -from __future__ import annotations - -import argparse -import json -import os -import re -import sys -from collections import Counter -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(ROOT)) - -from app.archi import archify, split_token, unresolved # noqa: E402 -from app.grammar import VERBS # noqa: E402 -from app.phones import base # noqa: E402 -from app.phrases import PHRASES # noqa: E402 -from app.workbook import LEX # noqa: E402 - -VERBS_JSON = ROOT / "static/assets/arabic_verbs.json" -_MARKS = re.compile(r"[ً-ْٰـ]") # harakat, shadda, sukun, dagger alif, tatweel - - -def loose(ar: str) -> str: - """Arabic without diacritics and with the hamza seats of alif folded, for a lookup that tolerates spelling.""" - return _MARKS.sub("", ar).translate(str.maketrans("أإآ", "ااا")).replace("ى", "ي") - - -def bank_words() -> dict[str, set[str]]: - """Arabic word -> every IPA the bank gives it (single words of each variant, LEX, grammar VERBS).""" - out: dict[str, set[str]] = {} - for p in PHRASES: - for ar, ipa in p["variants"]: - ars, ipas = ar.split(), [w.strip() for w in ipa.split("|")] - if len(ars) == len(ipas): - for a, i in zip(ars, ipas): - out.setdefault(a, set()).add(i) - for key, (_tr, ipas) in LEX.items(): - out.setdefault(key.partition("#")[0], set()).update(ipas) - for forms in VERBS.values(): - for _he, _tr, ar, ipas in forms: - out.setdefault(ar, set()).update(ipas) - return out - - -def sounds(ar: str, ipa: str) -> tuple[str, ...]: - """The target as the scorer sees it: archify() symbols kept, other phonemes reduced to their base() sound.""" - out = [] - for tok in archify(ar, ipa).split(): - if tok == "|": - continue - sym, _members, ph = split_token(tok) - out.append(sym if sym else base(ph)) - return tuple(out) - - -def load_vocab(): - from huggingface_hub import hf_hub_download - - from app.scorer import Vocab - model_id = os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft") - path = Path(model_id) / "vocab.json" if Path(model_id).is_dir() else hf_hub_download(model_id, "vocab.json") - return Vocab(json.loads(Path(path).read_text(encoding="utf-8"))) - - -def vocab_issues(vocab, ar: str, ipa: str) -> list[str]: - issues = [] - for tok in archify(ar, ipa).split(): - before = dict(vocab.downgraded) - try: - vocab.token(tok) - except KeyError as e: - issues.append(f"{tok}: {e}") - continue - issues += [f"{k}->{v}" for k, v in vocab.downgraded.items() if before.get(k) != v] - return issues - - -def main() -> None: - ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - ap.add_argument("--all", action="store_true", help="also print forms that match the bank") - ap.add_argument("--no-vocab", action="store_true", help="skip the model-vocab check") - args = ap.parse_args() - - data = json.loads(VERBS_JSON.read_text(encoding="utf-8")) - bank = bank_words() - bank_loose: dict[str, set[str]] = {} - for a, ipas in bank.items(): - bank_loose.setdefault(loose(a), set()).update(ipas) - vocab = None if args.no_vocab else load_vocab() - - tally: Counter[str] = Counter() - symbols: Counter[str] = Counter() - for section, entry in data.items(): - for verb in entry["verbs"]: - for tense in ("past", "future"): - form = verb[tense] - ar = form["arabic"] - for ipa in [form["phonemes"], *form.get("accepted_phonemes", [])]: - lines = [] - for tok in ipa.split(): - if split_token(tok)[0]: - symbols[tok] += 1 - known = bank.get(ar) or bank_loose.get(loose(ar)) - if not known: - status = "not in bank" - elif ipa in known: - status = "same" - elif sounds(ar, ipa) in {sounds(ar, k) for k in known}: - status = "same sounds" - else: - status = "differs" - how = "" if ar in bank else " (matched without diacritics: may be another word)" - lines.append(f" bank: {' / '.join(sorted(known))}{how}") - tally[status] += 1 - arch = archify(ar, ipa) - if arch != ipa: - lines.append(f" archi: {arch}") - if missing := unresolved(ar, ipa): - tally["unresolved"] += 1 - lines.append(f" unresolved letters: {' '.join(l for _, l in missing)}") - if vocab is not None and (bad := vocab_issues(vocab, ar, ipa)): - tally["vocab"] += 1 - lines.append(f" vocab: {', '.join(bad)}") - if status in ("differs",) or missing or args.all or (vocab is not None and bad): - print(f"[{section}] {verb['hebrew']} {tense} {ar} {ipa} -> {status}") - print("\n".join(lines)) if lines else None - - print("\n== summary ==") - for k in ("same", "same sounds", "differs", "not in bank", "unresolved", "vocab"): - print(f"{k:12} {tally[k]}") - if symbols: - print("symbols written in the JSON itself (archify() leaves such a word as it is):", - ", ".join(f"{s}×{n}" for s, n in symbols.most_common())) - - -if __name__ == "__main__": - main() diff --git a/scripts/concat_oga.py b/scripts/concat_oga.py deleted file mode 100755 index 7c73841207b03471fd7a6d6c889ccde0cb3c1631..0000000000000000000000000000000000000000 --- a/scripts/concat_oga.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -"""Concatenate the .oga files of a folder into one long .oga file (needs ffmpeg). - -Usage: python scripts/concat_oga.py # data/amir-oga/*.oga -> data/amir-oga.oga - python scripts/concat_oga.py data/other -o out.oga - python scripts/concat_oga.py --gap 0.5 # half a second of silence between files - -Files are joined in natural filename order (2.oga before 10.oga). The audio is re-encoded to -mono Opus so files with different sample rates or channel counts still join cleanly; ---copy joins without re-encoding (faster, lossless) when all files share the same format. -""" -from __future__ import annotations - -import argparse -import re -import subprocess -import sys -import tempfile -from pathlib import Path - - -def natural_key(path: Path): - return [int(p) if p.isdigit() else p.lower() for p in re.split(r"(\d+)", path.name)] - - -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - ap.add_argument("folder", nargs="?", default="data/amir-oga", type=Path) - ap.add_argument("-o", "--output", type=Path, help="default: .oga next to the folder") - ap.add_argument("--gap", type=float, default=0.0, help="seconds of silence between files (re-encode only)") - ap.add_argument("--bitrate", default="32k", help="Opus bitrate when re-encoding (default 32k)") - ap.add_argument("--copy", action="store_true", help="join without re-encoding") - args = ap.parse_args() - - out = args.output or args.folder.with_suffix(".oga") - files = sorted((f for f in args.folder.glob("*.oga") if f.resolve() != out.resolve()), key=natural_key) - if not files: - print(f"no .oga files in {args.folder}", file=sys.stderr) - return 1 - if args.copy and args.gap: - print("--gap needs re-encoding; drop --copy", file=sys.stderr) - return 1 - - # ffmpeg's concat demuxer reads the file list from a text file; quotes in names must be escaped - with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False, encoding="utf-8") as lst: - for f in files: - lst.write("file '" + str(f.resolve()).replace("'", "'\\''") + "'\n") - cmd = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-f", "concat", "-safe", "0", "-i", lst.name] - if args.copy: - cmd += ["-c", "copy"] - elif args.gap: - # the gap is added by padding every input, so it needs one input per file instead of the list - cmd = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y"] - for f in files: - cmd += ["-i", str(f)] - pads = "".join(f"[{i}:a]aresample=48000,aformat=channel_layouts=mono,apad=pad_dur={args.gap}[a{i}];" - for i in range(len(files))) - joined = "".join(f"[a{i}]" for i in range(len(files))) - cmd += ["-filter_complex", f"{pads}{joined}concat=n={len(files)}:v=0:a=1[out]", "-map", "[out]", - "-c:a", "libopus", "-b:a", args.bitrate] - else: - cmd += ["-ac", "1", "-c:a", "libopus", "-b:a", args.bitrate] - cmd += ["-f", "ogg", str(out)] - - try: - subprocess.run(cmd, check=True) - except FileNotFoundError: - print("ffmpeg not found (sudo apt install ffmpeg)", file=sys.stderr) - return 1 - except subprocess.CalledProcessError as e: - print(f"ffmpeg failed with exit code {e.returncode}", file=sys.stderr) - return 1 - finally: - Path(lst.name).unlink(missing_ok=True) - print(f"{len(files)} files -> {out}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/demo_video/.gitignore b/scripts/demo_video/.gitignore deleted file mode 100644 index 9c79407bc77a187cd7ec5760c7fa0887f0cccaa8..0000000000000000000000000000000000000000 --- a/scripts/demo_video/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -takes/ -voice/ -*.mp4 diff --git a/scripts/demo_video/README.md b/scripts/demo_video/README.md deleted file mode 100644 index 568659c4ffe0564441ada43dcc4d809011698113..0000000000000000000000000000000000000000 --- a/scripts/demo_video/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Students' feature video - -An 83-second vertical (1080×1920) silent walkthrough for Reels/TikTok/WhatsApp status, with Hebrew captions: -the opening logo → collecting beads → practicing "פתח, יפתח" (a ח mistake, its tip, the retry, a gold bead) → -a story → "הערבית שכבר שלכם" → daily practice → the digital library → a round of "הפועל זכרון" → the logo. - -```bash -# 1. the app, with sign-in off so nothing reaches the real database -SUPABASE_URL= SUPABASE_PUBLISHABLE_KEY= SUPABASE_SECRET_KEY= .venv/bin/uvicorn app.main:app --port 7861 -# 2. once: the browser, and the student's two takes (Sawtak TTS) -.venv/bin/pip install playwright && .venv/bin/python -m playwright install chromium -.venv/bin/python scripts/demo_video/make_takes.py scripts/demo_video/takes -# 3. record (about 2 minutes) -D=scripts/demo_video; .venv/bin/python $D/record.py --takes $D/takes --out $D/ihki-students.mp4 -``` - -- **Captions:** `script.py` holds each scene's caption; each stays up long enough to read. -- **Sound (off by default):** `--sound` adds the app's own sounds; `--voice ` a Hebrew voiceover from - make_voice.py (edge-tts >= 7.3); `--music 0.22` music.py's track, or `--music-file song.mp3` your own. -- **Look:** captions and tap ripples are drawn by `overlay.js`. -- The digital library shows the real list, read (read-only) from Supabase with the keys in .env; the local app - has none with sign-in off. -- The scores in the video are the model's real scores of the takes. The mistake take has to score under 75, or the - app says "מצוין" and shows no letters to tap; if a scoring change moves it, pick another take in make_takes.py. -- The app listens by itself after "next"; the recorder plays the take into the fake mic either way. - -## The short promo (88 s, a 3D iPhone) - -The opening logo, with "חדש! האפליקציה של אהלא וסהלא" rising under it; the booklet (its cover face on, then lying -open, pages flipping to lesson 4's new verbs, the camera going into the line "דיבר · חכא · יחכי", whose three words then lift off the page, float over the home and -land in the practice: דיבר on the prompt when the practice opens, חכא and יחכי on the answer once the app reveals it); the focused home -("מה עושים עכשיו?", its two actions numbered); "דיבר (עבר, עתיד)" said with a Hebrew speaker's mistake: chat bubbles -show what the student said (the ח' marked), a card compares ח' (חיכוך בחך) with ح (מהלוע), the app says it right, the -second try comes back at 100; collecting beads; driving mode (the phone in a car's mount, the conversation in bubbles, -a misbaha whose next bead turns gold); a grid of the other ways to practice (lesson 4's new words, pairs, story, and -"הפועל זכרון"), each coming forward for a short example; the summary, "החוברת מלמדת את השפה. האפליקציה עוזרת לשלוף, -לדבר, לתקן ולזכור", beside "הערבית שכבר שלכם"; the animated logo. Silent. The app's screens play 15% slower than -recorded (`SLOW` in promo.html). - -The focused home appears only to a signed-in student of a class; with sign-in off, promo.py draws it with the app's -own component (`LearnerHome.renderHome`) for class א1 on lesson 4, with the two actions such a student would get. -If the app's GPU scoring fails ("CUDA error"), run the app with `CUDA_VISIBLE_DEVICES=` (CPU). - -The booklet's images are made once (cover from a photo, pages from the PDF), into `assets/`: - -```bash -python scripts/demo_video/make_book.py "שלב א אוקטובר 2026.pdf" scripts/demo_video/assets # needs Pillow, pdftoppm -``` - -```bash -D=scripts/demo_video; .venv/bin/python $D/promo.py --takes $D/takes --out $D/ihki-promo.mp4 --keep -# design changes in promo.html (headlines, camera, closing card) re-render without recording again: -.venv/bin/python $D/promo.py --again --out $D/ihki-promo.mp4 -``` - -`promo.py` records the practice screen at an iPhone's size (393x852), then renders `promo.html` frame by frame. diff --git a/scripts/demo_video/assets/cover.jpg b/scripts/demo_video/assets/cover.jpg deleted file mode 100644 index ebb9b4a65301402ecba5e5d02aa3588e7979ace6..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/cover.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:17f4db462c6dbcd8334271defca715a65f32cc943eb3e571edc6da49f667c963 -size 255731 diff --git a/scripts/demo_video/assets/p53.jpg b/scripts/demo_video/assets/p53.jpg deleted file mode 100644 index 1ec508e2ee2a70f92f9ef06fffb36d93cddcc6ad..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p53.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aaf5568549a4cb2531454b623510c5c0faa14197f305422ee24b228bb76391d3 -size 119161 diff --git a/scripts/demo_video/assets/p54.jpg b/scripts/demo_video/assets/p54.jpg deleted file mode 100644 index 6cde592188fb47207adff734c4d172790358bd6f..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p54.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0505242e9e5248d52b96f2cd43635975f076a4d599d672679a41f32b27443831 -size 116116 diff --git a/scripts/demo_video/assets/p55.jpg b/scripts/demo_video/assets/p55.jpg deleted file mode 100644 index b7b9ffaec8a20e27fd547b2f372cc36d209c08d7..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p55.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a796b1e96c8d43b239ae6855af3e8945a39de05074df5f9fc3564939e421ab7 -size 123745 diff --git a/scripts/demo_video/assets/p56.jpg b/scripts/demo_video/assets/p56.jpg deleted file mode 100644 index 8b75faef830e29623425f95480819a5fddd87e4d..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p56.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2269b95dcfc347b3ad14b28bc5a3be0222bf67cfc884606813db8b84b6b54533 -size 102873 diff --git a/scripts/demo_video/assets/p57.jpg b/scripts/demo_video/assets/p57.jpg deleted file mode 100644 index 7e8a8aa57a0db0130a51da890ebc59c8d3b88953..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p57.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:687209285e19ca2d9c237f9bf2a4edb77b23296493c5ed3cea9cece93e5b1497 -size 103980 diff --git a/scripts/demo_video/assets/p58.jpg b/scripts/demo_video/assets/p58.jpg deleted file mode 100644 index 59d945300d360c8c6c7b9a82fb29f8b1909ab7ed..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p58.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ef14f26928008f2e6311645ae0fcc9fa7ba26918f6b916220113e97a15ec2740 -size 79042 diff --git a/scripts/demo_video/assets/p59.jpg b/scripts/demo_video/assets/p59.jpg deleted file mode 100644 index 8d400b28b89c5cdd1589807798bbf548b406c6cf..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p59.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff93b59ca4480cf11c6d95b42992af6b10a9ae123443a39631ea09e239827614 -size 68695 diff --git a/scripts/demo_video/assets/p60.jpg b/scripts/demo_video/assets/p60.jpg deleted file mode 100644 index 11a103ffaa2aa38d5a672c57f7eb5bf6395977c3..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p60.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dbbdb4b8b9ffb294297723ef791d0cc73ae3fd59c22497305cdd7ab5cdb1b021 -size 128698 diff --git a/scripts/demo_video/assets/p61.jpg b/scripts/demo_video/assets/p61.jpg deleted file mode 100644 index c465ff68fab25c354fae455c86da8c2bf9ec2a3a..0000000000000000000000000000000000000000 --- a/scripts/demo_video/assets/p61.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1597b418dff83f0f07f17c1005fbfca3768a9739813948641f15512ba84e169 -size 116516 diff --git a/scripts/demo_video/install.html b/scripts/demo_video/install.html deleted file mode 100644 index c6fa161665e3821aa1bf6bac9eae27c60f93bb19..0000000000000000000000000000000000000000 --- a/scripts/demo_video/install.html +++ /dev/null @@ -1,396 +0,0 @@ - - - - -ihki install - - - - -
-
-

מתקינים פעם אחת,
ונכנסים בלחיצה מהמסך הראשי

-
-
- -
-
-
-
-
-
-
-
- - - - - - - - - - - diff --git a/scripts/demo_video/install.py b/scripts/demo_video/install.py deleted file mode 100644 index 6203683609293ef928abf09720abfe3c0ebef483..0000000000000000000000000000000000000000 --- a/scripts/demo_video/install.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Two short videos on installing the app on the phone: an iPhone (Safari) and an Android phone (Chrome). - - python scripts/demo_video/install.py --out-dir scripts/demo_video - -The promo's look (install.html: the 3D phone, the headlines, the logo): the browser's menus are drawn, in Hebrew, -around a real screenshot of the app's first screen, taken here from --url at an iPhone's size. ---frames 3,8.5,12 saves those moments as PNGs only (to check the design without rendering the whole video). -""" -import argparse, asyncio, shutil, subprocess, tempfile -from pathlib import Path -from playwright.async_api import async_playwright - -HERE = Path(__file__).parent -STATIC = HERE.parents[1] / "static" -OW, OH, FPS = 1080, 1920, 30 - - -async def shoot(url, out): - """The app's first screen, at an iPhone 15's CSS size (393x852), twice the pixels.""" - async with async_playwright() as p: - browser = await p.chromium.launch() - ctx = await browser.new_context(viewport={"width": 393, "height": 852}, device_scale_factor=2, is_mobile=True, - has_touch=True, locale="he-IL", color_scheme="light") - pg = await ctx.new_page() - await pg.goto(url, wait_until="networkidle"); await pg.wait_for_timeout(2500) - await pg.screenshot(path=str(out)) - await browser.close() - - -async def render(os_, work, out, frames=None): - files = {"install.html": HERE / "install.html", "app.png": work / "app.png", "logo-intro.svg": STATIC / "logo-intro.svg", - "icon.png": STATIC / "icons" / "icon-512.png", "icon-maskable.png": STATIC / "icons" / "icon-maskable-512.png"} - shots = work / f"out-{os_}"; shutil.rmtree(shots, ignore_errors=True); shots.mkdir() - async with async_playwright() as p: - browser = await p.chromium.launch() - pg = await browser.new_page(viewport={"width": OW, "height": OH}) - - async def serve(route): - await route.fulfill(path=str(files[route.request.url.split("install.local/", 1)[1].split("?")[0]])) - await pg.route("http://install.local/**", serve) - await pg.goto(f"http://install.local/install.html?os={os_}") - await pg.evaluate("document.fonts.ready") - await pg.evaluate("async()=>await setup()") - if frames: - for t in frames: - await pg.evaluate("t=>seek(t)", t) - await pg.screenshot(path=str(out.with_name(f"{out.stem}-{t:05.2f}.png"))) - await browser.close() - return - n = int(await pg.evaluate("duration()") * FPS) - for i in range(n): - await pg.evaluate("t=>seek(t)", i / FPS) - await pg.screenshot(path=str(shots / f"{i:05d}.jpg"), type="jpeg", quality=92) - if i % 150 == 0: - print(f" {os_} {i}/{n}") - await browser.close() - subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(FPS), "-i", str(shots / "%05d.jpg"), - "-c:v", "libx264", "-preset", "slow", "-crf", "18", "-pix_fmt", "yuv420p", - "-movflags", "+faststart", str(out)], check=True) - - -async def main(a): - work = Path(tempfile.mkdtemp(prefix="install_")) - try: - await shoot(a.url, work / "app.png") - frames = [float(x) for x in a.frames.split(",")] if a.frames else None - for os_ in a.os.split(","): - out = Path(a.out_dir) / f"ihki-install-{'iphone' if os_ == 'ios' else 'android'}.mp4" - await render(os_, work, out, frames) - print(out) - finally: - shutil.rmtree(work) - - -if __name__ == "__main__": - ap = argparse.ArgumentParser() - ap.add_argument("--url", default="https://wzender-ihki.hf.space/") - ap.add_argument("--os", default="ios,android") - ap.add_argument("--out-dir", default=str(HERE)) - ap.add_argument("--frames", help="only these moments (seconds), as PNGs") - asyncio.run(main(ap.parse_args())) diff --git a/scripts/demo_video/make_book.py b/scripts/demo_video/make_book.py deleted file mode 100644 index 28a3d2dc2d0ae3631d7fb53e11542cf438cc6584..0000000000000000000000000000000000000000 --- a/scripts/demo_video/make_book.py +++ /dev/null @@ -1,46 +0,0 @@ -"""The booklet for the promo's opening: its cover, from a photo, and the pages that flip, from the booklet's PDF. - - python scripts/demo_video/make_book.py - -The cover is straightened from the photo's four corners (COVER_CORNERS, set by hand for the photo of -"שלב ב" on a wooden table), and its stage line is painted over with the cover's own white, so it stands for -either booklet. The pages are LEAVES' page numbers, then the spread the flip stops on (SPREAD: right, left): -lesson 4's new verbs, with "דיבר · חכא · יחכי", the verb the app scene then practices. -Needs Pillow and pdftoppm (poppler-utils). -""" -import subprocess, sys -from pathlib import Path -import numpy as np -from PIL import Image - -COVER_CORNERS = {"tl": (128, 346), "tr": (934, 350), "br": (980, 1544), "bl": (72, 1544)} -COVER_SIZE = (1000, 1377) # the booklet's proportions (654 x 901 pt) -STAGE_LINE = (330, 866, 620, 920) # where "שלב ב" is on the straightened cover (x0, y0, x1, y1) -LEAVES = [(53, 54), (55, 56), (57, 58), (59, 60)] # (front: on the left before the flip, back: on the right after) -LEFT_BASE = 61 # the left page left when the flip is done - - -def cover(photo: Path, out: Path): - im = Image.open(photo).convert("RGB") - c = COVER_CORNERS - cov = im.transform(COVER_SIZE, Image.QUAD, (*c["tl"], *c["bl"], *c["br"], *c["tr"]), Image.BICUBIC) - a = np.asarray(cov).copy() - x0, y0, x1, y1 = STAGE_LINE - above = a[y0 - 10:y0 - 3, x0:x1].mean(axis=0) # each column's white, from just above the line - a[y0:y1, x0:x1] = above[None, :, :].astype(np.uint8) - Image.fromarray(a).save(out / "cover.jpg", quality=90) - - -def pages(pdf: Path, out: Path): - for n in sorted({p for leaf in LEAVES for p in leaf} | {LEFT_BASE}): - stem = out / f"p{n}" - subprocess.run(["pdftoppm", "-r", "90", "-f", str(n), "-l", str(n), "-singlefile", "-jpeg", - "-jpegopt", "quality=85", str(pdf), str(stem)], check=True) - - -if __name__ == "__main__": - photo, pdf, out = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3]) - out.mkdir(parents=True, exist_ok=True) - cover(photo, out) - pages(pdf, out) - print(sorted(p.name for p in out.iterdir())) diff --git a/scripts/demo_video/make_takes.py b/scripts/demo_video/make_takes.py deleted file mode 100644 index d7f81911cb3eed98f5538b8bf15b55156490f448..0000000000000000000000000000000000000000 --- a/scripts/demo_video/make_takes.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Makes the "student" takes for the demo video with the Sawtak TTS voice (SAWTAK_API_KEY in .env). - - python scripts/demo_video/make_takes.py - -m876.wav: حكى، يحكي said with خ instead of ح (a Hebrew speaker's "dirty" ח'), in Raef's voice: 67 on the POWSM model -(exports/powsm-ft-shortpad), with "ח (ح) נשמעה כמו ח' (خ)" on the ח of يحكي. c876.wav: said right, also Raef: 100. -Phrase 876 is "דיבר (עבר, עתיד)" (topic "פעלים", lesson 4). The takes depend on the model: re-check them after a change. -The mistake has to score under 75: from 75 up the app says "מצוין" and shows no letters to tap. -TTS_BUCKET is cleared, so these takes don't go into the app's TTS cache bucket. -Each take gets 1 s of silence before (the recorder is listening) and 2.5 s after (so it stops by itself). -""" -import subprocess, sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) -from dotenv import load_dotenv -load_dotenv(Path(__file__).resolve().parents[2] / ".env") -import os -os.environ["TTS_BUCKET"] = "" -from app.tts import load_tts, LINE_VOICES - -TAKES = {"m876": ("خَكَى، يِخْكِي", ""), "c876": ("حَكَى، يِحْكِي", "")} - -out = Path(sys.argv[1]); out.mkdir(parents=True, exist_ok=True) -tts = load_tts() -if tts is None: - sys.exit("no TTS: set SAWTAK_API_KEY") -for name, (text, voice) in TAKES.items(): - data, _ = tts.wav(text, voice) - raw = out / f"{name}_raw.wav"; raw.write_bytes(data) - subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(raw), "-af", "adelay=1000,apad=pad_dur=2.5", - "-ar", "48000", "-ac", "1", str(out / f"{name}.wav")], check=True) - raw.unlink() - print(out / f"{name}.wav") diff --git a/scripts/demo_video/make_voice.py b/scripts/demo_video/make_voice.py deleted file mode 100644 index 6c3dfb88fa536a190c4276f76f071f0239adb920..0000000000000000000000000000000000000000 --- a/scripts/demo_video/make_voice.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Makes the Hebrew voiceover: one WAV per line of script.py, read by Microsoft's Hila voice (edge-tts, no key). - - python scripts/demo_video/make_voice.py [--voice he-IL-HilaNeural] [--rate +0%] - -Needs edge-tts 7.3 or newer (older ones get 403 from the service); it can live in any venv, since this script -only imports it. Writes .wav and durations.json ({key: seconds}), which record.py waits on. -""" -import argparse, asyncio, json, subprocess, sys -from pathlib import Path - -import edge_tts - -sys.path.insert(0, str(Path(__file__).parent)) -from script import LINES - - -async def main(out: Path, voice: str, rate: str): - out.mkdir(parents=True, exist_ok=True) - durations = {} - for key, (*_, text) in LINES.items(): - mp3 = out / f"{key}.mp3" - await edge_tts.Communicate(text, voice, rate=rate).save(str(mp3)) - wav = out / f"{key}.wav" - # trim the voice's leading/trailing silence so the timing is the speech itself - subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(mp3), "-af", - "silenceremove=start_periods=1:start_threshold=-45dB,areverse," - "silenceremove=start_periods=1:start_threshold=-45dB,areverse", - "-ar", "48000", "-ac", "1", str(wav)], check=True) - mp3.unlink() - dur = float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", - str(wav)], capture_output=True, text=True, check=True).stdout) - durations[key] = round(dur, 3) - print(f"{key:8s} {dur:4.1f}s {text}") - (out / "durations.json").write_text(json.dumps(durations, indent=1)) - - -if __name__ == "__main__": - ap = argparse.ArgumentParser() - ap.add_argument("out", type=Path) - ap.add_argument("--voice", default="he-IL-HilaNeural") - ap.add_argument("--rate", default="+0%") - a = ap.parse_args() - asyncio.run(main(a.out, a.voice, a.rate)) diff --git a/scripts/demo_video/music.py b/scripts/demo_video/music.py deleted file mode 100644 index 0b15630255695cca2785cd58375ee51d08723902..0000000000000000000000000000000000000000 --- a/scripts/demo_video/music.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Uplifting "background music for a presentation", synthesized here so it has no license to worry about: -piano chord pulses over a warm pad, a soft four-on-the-floor kick with hats, then claps and a bright plucked -arpeggio; C–G–Am–F at 110 BPM, stereo, with a light reverb. - - python scripts/demo_video/music.py -""" -import sys, wave -import numpy as np - -SR, BPM = 44100, 110 -BEAT = 60 / BPM -BAR = 4 * BEAT -rng = np.random.default_rng(11) -hz = lambda midi: 440 * 2 ** ((midi - 69) / 12) - -CHORDS = [[60, 64, 67], [59, 62, 67], [57, 60, 64], [57, 60, 65]] # C G/B Am F (right hand) -ROOTS = [36, 43, 45, 41] # C2 G2 A2 F2 -ARP = [[72, 76, 79, 84], [71, 74, 79, 83], [72, 76, 81, 84], [72, 77, 81, 84]] -# when each part plays, by bar: the beat builds up, and the last two bars thin out to an ending -INTRO, BUILD = 2, 4 - - -def env(n, attack, decay): - t = np.arange(n) / SR - return np.minimum(1, t / attack) * np.exp(-t / decay) - - -def piano(f, dur=1.8): - n = int(dur * SR); t = np.arange(n) / SR - tone = sum(a * np.sin(2 * np.pi * f * k * (1 + 0.0004 * k * k) * t) * np.exp(-t * (1.2 + 0.9 * k)) - for k, a in enumerate([1, 0.45, 0.22, 0.12, 0.06, 0.03], start=1)) - return tone * env(n, 0.004, 1.4) - - -def pad(freqs, dur): - n = int(dur * SR); t = np.arange(n) / SR - left = sum(np.sin(2 * np.pi * f * (1 - 0.003) * t) + 0.3 * np.sin(4 * np.pi * f * t) for f in freqs) - right = sum(np.sin(2 * np.pi * f * (1 + 0.003) * t) + 0.3 * np.sin(4 * np.pi * f * t) for f in freqs) - shape = np.minimum(1, t / 0.5) * np.minimum(1, (dur - t) / 0.4).clip(0) - return np.stack([left, right], 1) * shape[:, None] - - -def pluck(f, dur=0.35): - n = int(dur * SR); t = np.arange(n) / SR - return (np.sin(2 * np.pi * f * t) + 0.3 * np.sign(np.sin(2 * np.pi * f * t)) * np.exp(-t * 30)) * env(n, 0.002, 0.09) - - -def kick(): - n = int(0.3 * SR); t = np.arange(n) / SR - freq = 45 + 85 * np.exp(-t * 28) - return np.sin(2 * np.pi * np.cumsum(freq) / SR) * env(n, 0.001, 0.12) - - -def noise_hit(dur, decay, hp=True): - n = int(dur * SR) - x = rng.uniform(-1, 1, n + 1) - x = np.diff(x) if hp else x[:n] - return x * env(n, 0.001, decay) - - -def clap(): - hit = np.zeros(int(0.25 * SR)) - for off in (0, 0.011, 0.022): # a clap is a few hits close together - h = noise_hit(0.2, 0.05, hp=False); i = int(off * SR) - hit[i:i + len(h)] += h[: len(hit) - i] - return np.convolve(hit, [1, 0, -1], "same") # thins the low end - - -def bass(f, dur): - n = int(dur * SR); t = np.arange(n) / SR - return (np.sin(2 * np.pi * f * t) + 0.25 * np.sin(4 * np.pi * f * t)) * np.minimum(1, t / 0.01) \ - * np.minimum(1, (dur - t) / 0.05).clip(0) - - -def place(track, sig, at, gain=1.0, pan=0.0): - i = int(at * SR) - if i >= len(track): - return - if sig.ndim == 1: - sig = np.stack([sig * (1 - max(pan, 0)), sig * (1 + min(pan, 0))], 1) - j = min(len(track), i + len(sig)) - track[i:j] += gain * sig[: j - i] - - -def reverb(x, seconds=1.6, wet=0.18): - n = int(seconds * SR) - out = np.empty_like(x) - size = 1 << int(np.ceil(np.log2(len(x) + n))) - for ch in range(2): - ir = rng.standard_normal(n) * np.exp(-np.arange(n) / SR * 4.0) - ir /= np.sqrt(np.sum(ir ** 2)) - out[:, ch] = np.fft.irfft(np.fft.rfft(x[:, ch], size) * np.fft.rfft(ir, size), size)[: len(x)] - return x + wet * out - - -def render(seconds): - n = int(seconds * SR) + 2 * SR - keys, pads, drums, low, arps = (np.zeros((n, 2)) for _ in range(5)) - bars = int(np.ceil(seconds / BAR)) - duck = np.ones(n) # the bass and pad dip after each kick - notes = {m: piano(hz(m)) for c in CHORDS for m in c} | {m: piano(hz(m)) for m in ROOTS} - plucks = {m: pluck(hz(m)) for a in ARP for m in a} - k, cl = kick(), clap() - for bar in range(bars): - s, ci = bar * BAR, bar % 4 - ending = bar >= bars - 2 - # piano: root on 1 and 3, chord pulses on the eighths - place(keys, notes[ROOTS[ci]], s, 0.35); place(keys, notes[ROOTS[ci]], s + 2 * BEAT, 0.25) - for e in range(8): - for v, m in enumerate(CHORDS[ci]): - place(keys, notes[m], s + e * BEAT / 2 + v * 0.004, 0.11 if e % 2 else 0.15, pan=(v - 1) * 0.3) - place(pads, pad([hz(m - 12) for m in CHORDS[ci]], BAR + 0.3), s, 0.05) - if bar >= INTRO and not ending: - for b in range(4): - place(drums, k, s + b * BEAT, 0.55) - i = int((s + b * BEAT) * SR); j = min(n, i + int(0.22 * SR)) - duck[i:j] = np.minimum(duck[i:j], 0.45 + 0.55 * np.linspace(0, 1, j - i)) - place(drums, noise_hit(0.06, 0.018), s + (b + 0.5) * BEAT, 0.09, pan=0.3) - place(low, bass(hz(ROOTS[ci]), BAR - 0.05), s, 0.32) - if bar >= BUILD and not ending: - place(drums, cl, s + BEAT, 0.22); place(drums, cl, s + 3 * BEAT, 0.22) - for x in range(16): - m = ARP[ci][[0, 1, 2, 3, 2, 1, 2, 3][x % 8]] - place(arps, plucks[m], s + x * BEAT / 4, 0.07, pan=0.35 if x % 2 else -0.35) - mix = keys + pads * duck[:, None] + low * duck[:, None] + arps + drums - mix = reverb(mix)[: int(seconds * SR)] - fade_in, fade_out = int(0.3 * SR), int(3 * SR) - mix[:fade_in] *= np.linspace(0, 1, fade_in)[:, None] - mix[-fade_out:] *= np.linspace(1, 0, fade_out)[:, None] ** 1.5 - return mix / np.max(np.abs(mix)) * 0.85 - - -if __name__ == "__main__": - out, secs = sys.argv[1], float(sys.argv[2]) - pcm = (render(secs) * 32767).astype(np.int16) - with wave.open(out, "wb") as w: - w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR); w.writeframes(pcm.tobytes()) - print(out, f"{secs:.1f}s") diff --git a/scripts/demo_video/overlay.js b/scripts/demo_video/overlay.js deleted file mode 100644 index 46e4740b9d0fdedad1ecf857d19bf990957c4ec3..0000000000000000000000000000000000000000 --- a/scripts/demo_video/overlay.js +++ /dev/null @@ -1,61 +0,0 @@ -// Injected into every page of the demo recording. Two things the real app doesn't have: -// 1. a fake microphone: getUserMedia streams whatever __demo.play(take) plays (a WAV served by the recorder); -// 2. an overlay for captions and tap ripples, drawn by the page so Hebrew renders with the app's own fonts. -(() => { - // a fake microphone: each request gets its own audio line (as a real mic would), and record.py "speaks" - // with __demo.play(take) after tapping the mic; the take plays into every line the page holds - const realGUM = navigator.mediaDevices && navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); - const lines = []; - navigator.mediaDevices.getUserMedia = async (c) => { - if (!c || !c.audio) return realGUM(c); - const ctx = new AudioContext({ sampleRate: 48000 }), dest = ctx.createMediaStreamDestination(); - lines.push({ ctx, dest }); - return dest.stream; - }; - const playTake = async (take) => { - const data = await (await fetch(`/__demo/${take}.wav`)).arrayBuffer(); - for (const { ctx, dest } of lines.filter(l => l.dest.stream.getAudioTracks().some(t => t.readyState === "live"))) { - await ctx.resume(); - const src = ctx.createBufferSource(); src.buffer = await ctx.decodeAudioData(data.slice(0)); - src.connect(dest); src.start(); - } - }; - - const css = ` - #demoCap{position:fixed;left:14px;right:14px;z-index:100000;pointer-events:none;direction:rtl;text-align:center; - font:700 25px/1.3 "Secular One","Assistant",system-ui,sans-serif;color:#fff;background:rgba(28,36,29,.92); - border-radius:20px;padding:14px 18px;box-shadow:0 8px 24px rgba(0,0,0,.25); - opacity:0;transform:translateY(12px) scale(.97);transition:opacity .35s,transform .35s} - #demoCap.on{opacity:1;transform:none} - #demoCap small{display:block;font:600 17px/1.35 "Assistant",system-ui,sans-serif;color:#E3EAD6;margin-top:4px} - #demoCap.top{top:18px}#demoCap.bottom{bottom:22px}#demoCap.mid{top:40%} - .demoTap{position:fixed;z-index:100001;width:46px;height:46px;margin:-23px 0 0 -23px;border-radius:50%;pointer-events:none; - background:rgba(163,48,63,.35);border:3px solid rgba(163,48,63,.85);animation:demoTap .6s ease-out forwards} - @keyframes demoTap{from{transform:scale(.4);opacity:1}to{transform:scale(1.5);opacity:0}} - #demoCard{position:fixed;inset:0;z-index:100002;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px; - background:#FAFBF6;color:#1F2156;direction:rtl;text-align:center;padding:24px;opacity:0;transition:opacity .5s;pointer-events:none} - #demoCard.on{opacity:1} - #demoCard img{width:82%} - #demoCard h2{margin:0;font:400 38px/1.2 "Secular One","Assistant",system-ui,sans-serif} - #demoCard p{margin:0;font:600 21px/1.4 "Assistant",system-ui,sans-serif;color:#5E685C}`; - const boot = () => { - if (document.getElementById("demoCap")) return; - const st = document.createElement("style"); st.textContent = css; document.head.appendChild(st); - const cap = document.createElement("div"); cap.id = "demoCap"; document.body.appendChild(cap); - const card = document.createElement("div"); card.id = "demoCard"; document.body.appendChild(card); - }; - window.__demo = { - play: playTake, - caption(text, sub = "", pos = "bottom") { - boot(); const c = document.getElementById("demoCap"); - if (!text) { c.classList.remove("on"); return; } - const show = () => { c.className = pos; c.innerHTML = text + (sub ? `${sub}` : ""); requestAnimationFrame(() => c.classList.add("on")); }; - if (c.classList.contains("on")) { c.classList.remove("on"); setTimeout(show, 300); } else show(); - }, - tap(x, y) { boot(); const d = document.createElement("div"); d.className = "demoTap"; d.style.left = x + "px"; d.style.top = y + "px"; - document.body.appendChild(d); setTimeout(() => d.remove(), 700); }, - card(html) { boot(); const c = document.getElementById("demoCard"); if (html == null) { c.classList.remove("on"); return; } - c.innerHTML = html; c.classList.add("on"); }, - }; - if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot(); -})(); diff --git a/scripts/demo_video/promo.html b/scripts/demo_video/promo.html deleted file mode 100644 index 4711a9cbb7e541957efd958f49ec4f3fc5ce3e66..0000000000000000000000000000000000000000 --- a/scripts/demo_video/promo.html +++ /dev/null @@ -1,475 +0,0 @@ - - - - -ihki promo - - - - - -
- -
-
חדש!

האפליקציה של
אהלא וסהלא

מתרגלים ערבית מדוברת,
בכל מקום ובכל זמן

-
-
-
-
- - - - - - - - -
- - - - - - -
-
-
-
-

בנויה בדיוק לפי החוברת 📖

כל שיעור בחוברת הוא נושא באפליקציה,
עם אותם פעלים ואותם משפטים

-

החוברת מלמדת את השפה

האפליקציה עוזרת לשלוף, לדבר, לתקן ולזכור

-
🧠 לשלוףמילים ותרגול יומי
🎙️ לדברבקול, גם בנסיעה
✍️ לתקןעיצור אחרי עיצור
📿 לזכורחרוזים ומשחקים
-

יודעים מה לתרגל עכשיו

1 ממשיכים בשיעור של הכיתה
2 או תרגול קצר, כמה דקות

-
1
-
2
-

אומרים בקול

רואים פועל בעברית,
ואומרים אותו בערבית מדוברת

-

האפליקציה מקשיבה
לכל עיצור

ומסמנת באדום את מה
שלא נשמע נכון

-

ח׳ במקום ח?

-
خח׳ · חיכוך בחךמה שנשמע
← -
حח · מהלועמה שצריך
-

שומעים איך אומרים נכון 🔊

ומנסים שוב

-

מנסים שוב — 100!

ח נקייה, בדיוק כמו שצריך

-

כל משפט — חרוז 📿

מ־75 ומעלה הוא מזהיב ★
ואוספים אותם לאורך כל הדרך

-

מצב נסיעה 🚗

מתרגלים בלי ידיים: האפליקציה מקריאה,
מקשיבה ואומרת את הציון בקול

-

ועוד דרכים לתרגל

-

📖 מילים חדשות

נזכרים, ואז חושפים את המילה

-

🧩 זוגות מילים

מחברים כל מילה לתעתיק שלה

-

💬 סיפור

שומעים את כל הסיפור,
ואומרים שורה אחרי שורה

-

📿 הפועל זכרון

הופכים חרוזים ומוצאים זוגות
של פעלים בעברית ובערבית

- -
-
-
-
-
-
-
-
-
-
-
דיבר
חַכַּא
יִחְכִּי
-
-
📖 מילים חדשות
-
🧩 זוגות מילים
-
💬 סיפור
-
📿 הפועל זכרון
-
- - - - - -
🎙️ אומרים● ● ●
-
🎙️ מה שהאפליקציה שמעהחַ'כַּא, יִחְ'כִּי
-
🔊 האפליקציהחַכַּא, יִחְכִּי
-
🎙️ ניסיון שניחַכַּא, יִחְכִּי ✓
-
🔊 דיבר — עבר ועתיד
-
🎙️ חַכַּא, יִחְכִּי
-
🔊 מצוין! · 100 ✓
- - -
-

הערבית של דוברי עברית
נשמעת אחרת

-
-
حח
عע
خח׳
-
قק
صצ
طט
-
-

אהלא וסהלא שומעת בדיוק את העיצורים האלה,
ומלמדת להגות אותם נכון

-
- - - - - diff --git a/scripts/demo_video/promo.py b/scripts/demo_video/promo.py deleted file mode 100644 index 4121c13301adf9baff2485649e24d51b9c6e5f09..0000000000000000000000000000000000000000 --- a/scripts/demo_video/promo.py +++ /dev/null @@ -1,334 +0,0 @@ -"""The short promo: the app's core in ~30 seconds, inside a 3D iPhone. The student says a verb, the app hears -which consonant went wrong (the ح a Hebrew speaker swallows), marks it and says how to fix it; then the retry. - - python scripts/demo_video/promo.py --takes scripts/demo_video/takes --out scripts/demo_video/ihki-promo.mp4 - -Two passes: -1. the practice screen is recorded at an iPhone's size (393x852), as frames with their times, and the moments - that matter (the score, the red letter, the tip) are noted with where they are on the screen; -2. promo.html places those frames on a CSS 3D iPhone, with the headlines around it and a camera that moves in on - the red letter, and is rendered frame by frame (so the timing is exact, whatever the machine's speed). - -The app should run with sign-in off (empty SUPABASE_* variables); make_takes.py makes the takes. -""" -import argparse, asyncio, base64, json, shutil, subprocess, tempfile, time -from pathlib import Path -from playwright.async_api import async_playwright - -HERE = Path(__file__).parent -SW, SH, SDPR = 393, 852, 2 # the phone's screen: an iPhone 15's CSS size -OW, OH, FPS = 1080, 1920, 30 # the promo - - -HOME = """(()=>{ - const box=$("vids"); box.replaceChildren(); document.body.dataset.view="home"; - const nav=$("nav"); nav.querySelector('[data-view="home"]').hidden=false; nav.querySelector('[data-view="refer"]').hidden=true; - nav.querySelectorAll("button").forEach(b=>{ if(b.dataset.view==="home") b.setAttribute("aria-current","page"); else b.removeAttribute("aria-current"); }); - drawRewardShelf(); - LH.renderHome(box,{className:"א1",lessonName:"שיעור 4",done:false, - actions:[{kind:"continue",title:"ממשיכים בשיעור 4",detail:"פעלים",go:"המשיכו",minutes:LH.minutesFor(5),run(){}}, - {kind:"short",title:"תרגול קצר",detail:"10 משפטים חדשים מהשיעור",minutes:LH.minutesFor(10),run(){}}], - onChange(){},onPath(){},onRefer(){}}); - scrollTo(0,0); -})()""" - - -async def record_screen(url, takes, work): - """Pass 1: the practice screen's frames, [{t, file}], and the noted moments {name: {t, box?}}.""" - frames, marks = [], {} - async with async_playwright() as p: - browser = await p.chromium.launch(args=["--autoplay-policy=no-user-gesture-required"]) - ctx = await browser.new_context(viewport={"width": SW, "height": SH}, device_scale_factor=SDPR, is_mobile=True, - has_touch=True, locale="he-IL", color_scheme="light", permissions=["microphone"]) - await ctx.add_init_script(path=str(HERE / "overlay.js")) - await ctx.route("**/__demo/*.wav", lambda r: r.fulfill( - path=str(Path(takes) / r.request.url.split("/")[-1]), content_type="audio/wav")) - pg = await ctx.new_page() - cdp = await ctx.new_cdp_session(pg) - wait = pg.wait_for_timeout - t0, paused = [None], {"on": False, "at": 0.0, "skipped": 0.0} # the skipped stretches leave the clock - - async def frame(f): - await cdp.send("Page.screencastFrameAck", {"sessionId": f["sessionId"]}) - if paused["on"]: - return - ts = f["metadata"]["timestamp"] - if t0[0] is None: - t0[0] = ts - name = f"s{len(frames):05d}.jpg" - (work / name).write_bytes(base64.b64decode(f["data"])) - frames.append({"t": round(max(ts - t0[0] - paused["skipped"], frames[-1]["t"] if frames else 0), 4), - "file": name}) - - def pause(): - paused.update(on=True, at=time.time()) - - async def resume(): - paused["skipped"] += time.time() - paused["at"]; paused["on"] = False - # a still screen sends no frames: an invisible change makes Chrome send the one now on screen - await pg.evaluate("""(()=>{ let k=document.getElementById('demoKick'); if(!k){ k=document.createElement('div'); k.id='demoKick'; - k.style.cssText='position:fixed;left:0;top:0;width:2px;height:2px;pointer-events:none;z-index:999999'; document.body.append(k); } - k.style.background=k.style.background==='rgba(0, 0, 0, 0.01)'?'rgba(0, 0, 0, 0.02)':'rgba(0, 0, 0, 0.01)'; })()""") - await asyncio.sleep(0.12) - - async def mark(name, sel=None): - m = {"t": round(time.time() - t0[0] - paused["skipped"], 3)} - if sel: - b = await pg.locator(sel).first.bounding_box() - m["box"] = [b["x"] / SW, b["y"] / SH, b["width"] / SW, b["height"] / SH] - marks[name] = m - - async def tap(sel, pause=300): - loc = pg.locator(sel).first - b = await loc.bounding_box() - await pg.evaluate("a=>__demo.tap(...a)", [b["x"] + b["width"] / 2, b["y"] + b["height"] / 2]) - await wait(180); await loc.click(); await wait(pause) - - recording = lambda: pg.evaluate("typeof rec!=='undefined'&&rec&&rec.state==='recording'") - - async def say(take, fresh=False): - """fresh: drop a recording the app started by itself (it would hold silence before the take).""" - await pg.evaluate("document.getElementById('result').hidden=true") - if fresh and await recording(): - await pg.evaluate("recCancel=true; rec.stop()"); await wait(400) - for _ in range(80): # the app may be counting down ("מקליט בעוד 3…") to record by itself - if await recording() or not (await pg.inner_text("#status")).startswith("מקליט בעוד"): - break - await wait(50) - if await recording(): - b = await pg.locator("#mic").bounding_box() # the app is already listening by itself - await pg.evaluate("a=>__demo.tap(...a)", [b["x"] + b["width"] / 2, b["y"] + b["height"] / 2]) - else: - await tap("#mic", 0) - for _ in range(40): - if await recording(): - break - await wait(50) - await wait(150) - print(f" {take}: recording={await recording()} status={await pg.inner_text('#status')!r}") - await pg.evaluate("t=>__demo.play(t)", take) - await mark("said_" + take) # the student starts speaking (the bubble shows it) - try: - await pg.wait_for_selector("#result:not([hidden])", timeout=30000) - except Exception: - await pg.screenshot(path=str(work / "stuck.png")) - raise SystemExit(f"no score for {take}: status {await pg.inner_text('#status')!r}, see {work / 'stuck.png'}") - - # off camera: in, and the focused home a signed-in student of class א1 on lesson 4 would get (with sign-in - # off the app shows the path instead, so the app's own home component is drawn here with that content) - await pg.goto(url, wait_until="networkidle"); await wait(1500) - await pg.evaluate("ME_PERMISSIONS=new Set(['practice.use','exercises.use','path.use','games.use'])") - await pg.evaluate("store.set('ihki.firstGold',true)") # the 100 stays on screen (no first-gold window over it) - await pg.click("#practiceBtn"); await wait(800) - if await pg.locator("#guideTipClose").is_visible(): - await pg.click("#guideTipClose") - await pg.evaluate(HOME); await wait(700) - - cdp.on("Page.screencastFrame", lambda f: asyncio.ensure_future(frame(f))) - await cdp.send("Page.startScreencast", {"format": "jpeg", "quality": 90, - "maxWidth": SW * SDPR, "maxHeight": SH * SDPR}) - while t0[0] is None: # the first frame sets the clock - await asyncio.sleep(0.05) - await mark("home", ".focus-action.hero") - await mark("short", ".focus-action:not(.hero)") - await wait(2600) - await mark("homeTap", ".focus-action.hero") - await tap(".focus-action.hero", 200) - - # off camera: the hero card's topic, "פעלים" of lesson 4, on "דיבר (עבר, עתיד)" - pause() - await pg.evaluate("pickCat('l4-verbs')"); await wait(800) - for _ in range(40): - if (await pg.inner_text("#prompt")).strip().startswith("דיבר"): - break - await pg.click("#nextBtn"); await wait(250) - await pg.evaluate("scrollTo(0,0)"); await wait(200) - await resume() - await mark("start") - await mark("promptBox", "#prompt") # where the booklet's words land: the verb, and the answer's card - await mark("hintBox", "#hint") - await wait(1500) - await mark("mic", "#mic") - await say("m876") - await mark("score", "#result") - # the answer is now revealed, word by word: where חכא and יחכי drop in from the top - await mark("ans1", "#wordReveals .analysis-word:nth-child(1) .expected") - await mark("ans2", "#wordReveals .analysis-word:nth-child(2) .expected") - bad = '#wordReveals .bad.why[title^="ח (ح) נשמעה כמו ח\'"]' - await mark("letter", bad) - print(" score:", await pg.inner_text("#score")) - await wait(2600) - await mark("tap", bad) - await tap(bad, 0) - await wait(400) - await mark("tip", "#whyTip") - await wait(3600) - await pg.mouse.click(5, 5) # closes the tip - await wait(400) - await mark("listen", "#speakBtn") # the right way, said by the app - await tap("#speakBtn", 2200) - await mark("retry", "#mic") - await say("c876", fresh=True) - await pg.evaluate("cancelAutoNext()") # keeps the score on screen - await mark("great", "#result") - print(" retry:", await pg.inner_text("#score")) - await wait(5200) # the 100, then the beads headline over the gold bead - - # driving mode on the same verb, fresh: the app reads it out (most of that wait is skipped), says "תורכם" - # and listens, then says the verdict - pause() - await pg.evaluate("store.set('ihki.driveSeen',true)") # past the first-time explanation - # still on "דיבר" (re-opening the topic would start a round without it: it was just passed) - await pg.evaluate("cancelAutoNext(); show(cur,true); scrollTo(0,0)") # fresh: no old score, no countdown to the next verb - await wait(400) - await pg.click("#driveBtn"); await wait(150) - await resume() - await mark("drive") - await wait(1800) - pause() - status = lambda: pg.inner_text("#status") - for _ in range(300): - if (await status()).startswith("תורכם"): - break - await wait(50) - for _ in range(200): # after its beep the app starts recording: only then the answer - if await pg.evaluate("typeof rec!=='undefined'&&rec&&rec.state==='recording'"): - break - await wait(50) - await resume() - await mark("turn") - await wait(250) - await pg.evaluate("t=>__demo.play(t)", "c876") - for _ in range(400): - if (await status()).startswith("ציון"): - break - await wait(50) - await mark("verdict") - print(" driving:", await status(), "·", await pg.inner_text("#prompt")) - await wait(1500) - await mark("driveEnd") - pause() - await pg.evaluate("stopDrive()") - - # the grid of other ways to practice, lesson 4: four short examples, each with a still half second at its - # start and end (the camera moves in and out on those) - async def path(): - await pg.locator("#nav button", has_text="מסלול").first.click(); await wait(500) - - pause() # new words: a card, revealed, "ידעתי" - await pg.locator("button", has_text="חזרה למסלול").first.click(); await wait(600) - sat = pg.locator("button.sat", has_text="מילים חדשות").nth(3) - await sat.scroll_into_view_if_needed(); await sat.click(); await wait(1200) - await resume(); await mark("w0") - await wait(2000) # the whole grid first, then the camera moves in - await tap("#learnShow", 1000) - await tap("#learnKnow", 300) - await wait(1600); await mark("w1") - pause(); await pg.click("#learnClose"); await wait(500) - - sat = pg.locator("button.sat", has_text="התאימו זוגות").nth(3) # pairs: two matched - await sat.scroll_into_view_if_needed(); await sat.click(); await wait(1300) - await resume(); await mark("p0") - await wait(350) - ids = await pg.evaluate("[...document.querySelectorAll('.mtile[data-side=he]')].map(t=>t.dataset.id)") - for i in ids[:2]: - await tap(f'.mtile[data-side="he"][data-id="{i}"]', 180) - await tap(f'.mtile[data-side="tr"][data-id="{i}"]', 500) - await wait(1600); await mark("p1") - pause(); await pg.click("#matchClose"); await wait(500) - - sat = pg.locator("button.sat", has_text="סיפור: שכני החדש").first # a story, read aloud line by line - await sat.scroll_into_view_if_needed(); await sat.click(); await wait(1100) - await pg.evaluate("scrollTo(0,0)"); await wait(300) - await resume(); await mark("s0") - await wait(400) - await tap("#hearBtn", 0) - await wait(3500); await mark("s1") - pause(); await pg.click("#hearBtn"); await wait(300) - await pg.locator("button", has_text="חזרה למסלול").first.click(); await wait(600) - - await pg.locator("#nav button", has_text="משחקים").first.click(); await wait(600) # "הפועל זכרון": two pairs - await pg.locator("#vids button", has_text="הפועל זכרון").first.click(); await wait(1200) - await resume(); await mark("m0") - await wait(350) - for he, ar in [("פתח", "פַתַח"), ("אכל", "אַכַּל")]: - await tap(f'.mg-bead.he:has-text("{he}")', 200) - await tap(f'.mg-bead.ar:has-text("{ar}")', 480) - await wait(2050); await mark("m1") # the camera moves back out to the grid - - # the summary: "the Arabic that's already yours", what this session collected - pause() - await pg.locator(".mg-close").first.click(); await wait(500) - await pg.evaluate(HOME); await wait(600) # the home again, its card now counting this session's beads - await pg.click("#rewardShelf"); await wait(900) - await resume() - await mark("shelf") - await wait(4600) - await mark("end") - await cdp.send("Page.stopScreencast") - await asyncio.sleep(0.3) - await browser.close() - return frames, marks - - -async def render(frames, marks, work, out): - """Pass 2: promo.html, frame by frame.""" - out_frames = work / "out"; out_frames.mkdir() - async with async_playwright() as p: - browser = await p.chromium.launch() - pg = await browser.new_page(viewport={"width": OW, "height": OH}) - - async def serve(route): - path = route.request.url.split("promo.local/", 1)[1].split("?")[0] - f = HERE / path if path == "promo.html" or path.startswith("assets/") else work / path - if path == "logo-intro.svg": - f = HERE.parents[1] / "static" / "logo-intro.svg" - await route.fulfill(path=str(f)) - await pg.route("http://promo.local/**", serve) - await pg.goto("http://promo.local/promo.html") - await pg.evaluate("async d=>await setup(d)", {"frames": frames, "marks": marks}) - await pg.evaluate("document.fonts.ready") - total = await pg.evaluate("duration()") - n = int(total * FPS) - for i in range(n): - await pg.evaluate("t=>seek(t)", i / FPS) - await pg.screenshot(path=str(out_frames / f"{i:05d}.jpg"), type="jpeg", quality=92) - if i % 150 == 0: - print(f" {i}/{n}") - await browser.close() - subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(FPS), "-i", str(out_frames / "%05d.jpg"), - "-c:v", "libx264", "-preset", "slow", "-crf", "18", "-pix_fmt", "yuv420p", - "-movflags", "+faststart", str(out)], check=True) - - -async def main(a): - if a.again: # pass 2 only, on the frames of a kept run - work = Path(a.again) - frames, marks = json.loads((work / "frames.json").read_text()), json.loads((work / "marks.json").read_text()) - shutil.rmtree(work / "out", ignore_errors=True) - a.keep = True - else: - work = Path(tempfile.mkdtemp(prefix="promo_")) - try: - if not a.again: - frames, marks = await record_screen(a.url, a.takes, work) - print(f"pass 1: {len(frames)} frames, {marks['end']['t']:.1f}s") - (work / "marks.json").write_text(json.dumps(marks, indent=1)) - (work / "frames.json").write_text(json.dumps(frames)) - await render(frames, marks, work, a.out) - print(a.out) - finally: - if a.keep: - print("work kept in", work) - else: - shutil.rmtree(work) - - -if __name__ == "__main__": - ap = argparse.ArgumentParser() - ap.add_argument("--url", default="http://localhost:7861/") - ap.add_argument("--takes") - ap.add_argument("--out", default="ihki-promo.mp4") - ap.add_argument("--keep", action="store_true", help="keep the frames (to work on promo.html)") - ap.add_argument("--again", metavar="WORK_DIR", help="re-render pass 2 from a kept run's frames") - asyncio.run(main(ap.parse_args())) diff --git a/scripts/demo_video/record.py b/scripts/demo_video/record.py deleted file mode 100644 index 0bc234583d71e832b7d4527f8fe097ef2cc2238b..0000000000000000000000000000000000000000 --- a/scripts/demo_video/record.py +++ /dev/null @@ -1,371 +0,0 @@ -"""Records the students' feature video: a phone-sized (1080x1920), silent walkthrough of the app with Hebrew captions. - - python scripts/demo_video/record.py --takes --out demo.mp4 [--sound] [--voice ] [--music 0.22 | --music-file f] - ---sound (optional): the app's own sounds (the student's takes, the native answer, the word cards, the story). - -The app should run with sign-in off (empty SUPABASE_* variables) so no demo progress reaches the real database. ---takes: m449.wav / c449.wav from make_takes.py. The "student" is the TTS voice: m449 says فتح، يفتح with ه -for ح (the ח swallowed into a soft ה), c449 says it right. The scores in the video are the real model's. ---voice (optional): a voiceover from make_voice.py (one WAV per line of script.py, and durations.json); each scene -then waits for its line. Without it, each caption stays up long enough to read. ---music / --music-file (optional): music.py's loop at that level, or your own track, ducked under speech. - -Frames come from Chrome's screencast (sharper than Playwright's built-in video), then ffmpeg encodes them and -mixes the sound. -""" -import argparse, asyncio, base64, json, shutil, subprocess, sys, tempfile, time -from pathlib import Path -from playwright.async_api import async_playwright - -HERE = Path(__file__).parent -sys.path.insert(0, str(HERE)) -from script import LINES - -W, H, DPR, FPS = 432, 768, 2.5, 30 # 9:16 viewport -> 1080x1920 frames -LOGO_SECONDS = 7.4 # static/logo-intro.svg's animation, to the dots' last bounce - - -def real_library(): - """The real library's list (titles and players), read from Supabase with the keys in .env, so the video shows - what students see; the local app, with sign-in off, has none. None when it can't be read.""" - try: - sys.path.insert(0, str(HERE.parents[1])) - from dotenv import load_dotenv - load_dotenv(HERE.parents[1] / ".env") - from app.auth import load_auth - from app.main import video_embed - rows = load_auth().level_videos("library") - return [{"id": v["id"], "title": v["title"], "embed": video_embed(v["url"])} for v in rows] - except Exception as e: - print("library: not read:", e) - return None - - -def hold(text, sub): - """How long a caption stays up without a voice: enough to read it.""" - return max(1.8, 0.5 + 0.045 * len(text + sub)) if text else 1.0 - - -class Recorder: - def __init__(self, cdp, frames_dir): - self.cdp, self.dir = cdp, frames_dir - self.frames = [] # (path, chrome timestamp, cut_before) - self.cues = [] # (wall time, wav path, gain): sounds to lay on the video - self.paused, self.cut = False, False - self.paused_at = {} # frame index after a pause -> when the pause began (wall clock) - - async def start(self): - self.cdp.on("Page.screencastFrame", lambda f: asyncio.ensure_future(self._frame(f))) - await self.cdp.send("Page.startScreencast", {"format": "jpeg", "quality": 92, - "maxWidth": int(W * DPR), "maxHeight": int(H * DPR)}) - - async def stop(self): - await self.cdp.send("Page.stopScreencast") - await asyncio.sleep(0.3) - - async def _frame(self, f): - await self.cdp.send("Page.screencastFrameAck", {"sessionId": f["sessionId"]}) - if self.paused: - return - p = self.dir / f"{len(self.frames):06d}.jpg" - p.write_bytes(base64.b64decode(f["data"])) - if self.cut: - self.paused_at[len(self.frames)] = self.pause_began - self.frames.append((p, f["metadata"]["timestamp"], self.cut)) - self.cut = False - - def pause(self): - self.paused, self.pause_began = True, time.time() - - def resume(self): - self.paused, self.cut = False, True - - def cue(self, wav, gain=1.0): - self.cues.append((time.time(), Path(wav), gain)) - - def video_time(self, t): - """A wall-clock moment on the video's timeline: from the first frame, minus the paused stretches.""" - v = t - self.frames[0][1] - for i, (_, ts, cut) in enumerate(self.frames): - began = self.paused_at.get(i, ts) - if cut and i and t > began: # a sound just after the resume lands at the cut, not in the gap - v -= max(0.0, min(t, ts) - began) - return max(0.0, v) - - def encode(self, out): - # Chrome sends a frame only when the screen changes; each frame lasts until the next one. - # Across a pause the gap is dropped, so the skipped part doesn't show as a freeze. - lines = ["ffconcat version 1.0"] - for i, (p, ts, _) in enumerate(self.frames): - nxt = self.frames[i + 1] if i + 1 < len(self.frames) else None - # before a pause, the frame lasts until the pause began (a still screen sends no new frames) - end = None if nxt is None else (self.paused_at[i + 1] if nxt[2] else nxt[1]) - dur = 1.5 if end is None else max(end - ts, 0.001) - lines += [f"file '{p}'", f"duration {dur:.4f}"] - lines.append(f"file '{self.frames[-1][0]}'") - lst = self.dir / "list.txt" - lst.write_text("\n".join(lines)) - subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", str(lst), - "-vf", f"fps={FPS},scale={int(W * DPR)}:{int(H * DPR)}:flags=lanczos,format=yuv420p", - "-c:v", "libx264", "-preset", "slow", "-crf", "18", str(out)], check=True) - - def mix(self, silent, out, music_gain, music_file=None): - length = float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", - "csv=p=0", str(silent)], capture_output=True, text=True).stdout) - music = self.dir / "music.wav" - if not music_gain and not music_file: - music = None - elif music_file: # your own track: looped to the video's length, faded out at the end - subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-stream_loop", "-1", "-i", str(music_file), - "-t", f"{length:.2f}", "-af", f"afade=t=out:st={max(0, length - 3):.2f}:d=3", - "-ar", "44100", "-ac", "2", str(music)], check=True) - else: - subprocess.run([sys.executable, str(HERE / "music.py"), str(music), f"{length:.2f}"], check=True, - stdout=subprocess.DEVNULL) - args, chains = ["-i", str(silent), "-i", str(music or silent)], [] - for k, (t, wav, gain) in enumerate(self.cues): - ms = int(self.video_time(t) * 1000) - print(f" {ms / 1000:5.1f}s on the video: {wav.name}") - args += ["-i", str(wav)] - chains.append(f"[{k + 2}:a]aformat=sample_rates=48000:channel_layouts=mono," - f"adelay={ms}:all=1,volume={gain}[c{k}]") - n = len(self.cues) - graph = ";".join((chains + [ - "".join(f"[c{k}]" for k in range(n)) + f"amix=inputs={n}:normalize=0:duration=longest," - f"apad=whole_dur={length:.2f},aformat=channel_layouts=stereo,asplit=2[voice][key]", - f"[1:a]aformat=sample_rates=48000:channel_layouts=stereo,volume={music_gain or 1}[bed]", - "[bed][key]sidechaincompress=threshold=0.015:ratio=6:attack=30:release=450[ducked]", - "[voice][ducked]amix=inputs=2:normalize=0:duration=first,loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000[a]"] - if music else chains + [ - "".join(f"[c{k}]" for k in range(n)) + f"amix=inputs={n}:normalize=0:duration=longest," - f"apad=whole_dur={length:.2f},aformat=channel_layouts=stereo,loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000[a]"])) - subprocess.run(["ffmpeg", "-y", "-loglevel", "error", *args, "-filter_complex", graph, - "-map", "0:v", "-map", "[a]", "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", - "-ac", "2", "-shortest", "-movflags", "+faststart", str(out)], check=True) - - -async def run(url, takes, voice, out, music_gain, music_file, sound, keep_frames): - frames_dir = Path(tempfile.mkdtemp(prefix="demo_frames_")) - durations = json.loads((Path(voice) / "durations.json").read_text()) if voice else {} - async with async_playwright() as p: - browser = await p.chromium.launch(args=["--autoplay-policy=no-user-gesture-required"]) - ctx = await browser.new_context(viewport={"width": W, "height": H}, device_scale_factor=DPR, is_mobile=True, - has_touch=True, locale="he-IL", color_scheme="light", permissions=["microphone"]) - await ctx.add_init_script(path=str(HERE / "overlay.js")) - await ctx.route("**/__demo/*.wav", lambda r: r.fulfill( - path=str(Path(takes) / r.request.url.split("/")[-1]), content_type="audio/wav")) - pg = await ctx.new_page() - rec = Recorder(await ctx.new_cdp_session(pg), frames_dir) - wait = pg.wait_for_timeout - t0, speech_end = time.monotonic(), [0.0] - - def app_sound(key, url): - """The page started playing one of its sounds (the native answer, a word, a story line): laid on - the video at this moment. Its data comes right after (some are blob: URLs, readable only in the page).""" - rec.cues.append((time.time(), frames_dir / f"sound{key}.audio", 0.9)) - print(f"{time.monotonic() - t0:5.1f}s ♪ {url.split('/', 3)[-1][:40]}") - - def app_sound_data(key, data_url): - (frames_dir / f"sound{key}.audio").write_bytes(base64.b64decode(data_url.split(",", 1)[1])) - - await pg.expose_function("__demoSound", app_sound) - await pg.expose_function("__demoSoundData", app_sound_data) - await pg.add_init_script("""(()=>{ const play=HTMLMediaElement.prototype.play; let n=0; - HTMLMediaElement.prototype.play=function(){ - const src=this.currentSrc||this.src; - if(src&&!src.includes("__demo")){ const key=n++; window.__demoSound(key, src); - fetch(src).then(r=>r.blob()).then(b=>new Promise(ok=>{ const f=new FileReader(); f.onload=()=>ok(f.result); f.readAsDataURL(b); })) - .then(d=>window.__demoSoundData(key, d)); } - return play.apply(this, arguments); }; })()""") - - async def line(key): - """Shows the scene's caption and starts its voiceover line, if there is a voice.""" - text, sub, pos, _ = LINES[key] - print(f"{time.monotonic() - t0:5.1f}s {key}: {text}") - if text: - await pg.evaluate("a=>__demo.caption(...a)", [text, sub, pos]) - if voice: - rec.cue(Path(voice) / f"{key}.wav") - speech_end[0] = time.monotonic() + durations[key] - else: # time to read the caption - speech_end[0] = time.monotonic() + hold(text, sub) - - async def spoken(pad=0.3): - """Waits for the voiceover line to end (without a voice: for the caption to be read).""" - left = speech_end[0] + pad - time.monotonic() - if left > 0: - await asyncio.sleep(left) - - async def tap(loc, pause=350): - loc = pg.locator(loc).first if isinstance(loc, str) else loc - await loc.scroll_into_view_if_needed() - box = await loc.bounding_box() - await pg.evaluate("a=>__demo.tap(...a)", [box["x"] + box["width"] / 2, box["y"] + box["height"] / 2]) - await wait(180) - await loc.click() - await wait(pause) - - async def point(sel): - """A tap ripple without the tap: draws the eye to a button.""" - box = await pg.locator(sel).first.bounding_box() - await pg.evaluate("a=>__demo.tap(...a)", [box["x"] + box["width"] / 2, box["y"] + box["height"] / 2]) - - async def say(take): - """Taps the mic with this take as the student's voice, and waits for the score.""" - await pg.evaluate("document.getElementById('result').hidden=true") - # the app may already be listening by itself (it does after "next" and after a retry prompt) - if await pg.evaluate("typeof rec!=='undefined'&&rec&&rec.state==='recording'"): - await point("#mic"); await wait(150) - else: - await tap("#mic", 150) - await pg.evaluate("t=>__demo.play(t)", take) - rec.cue(Path(takes) / f"{take}.wav") - try: - await pg.wait_for_selector("#result:not([hidden])", timeout=30000) - except Exception: - await pg.screenshot(path=str(frames_dir / "stuck.png")) - sys.exit(f"no score for {take}; status: {await pg.inner_text('#status')!r}; see {frames_dir / 'stuck.png'}") - - PERMS = "ME_PERMISSIONS=new Set(['practice.use','exercises.use','path.use','games.use','tutor.use','library.view'])" - library = real_library() # the page loads it only when signed in, so it is handed over below - - await pg.goto(url, wait_until="networkidle") - await rec.start() - - # 1 · the opening logo - await wait(1200) - await line("intro"); await spoken(0.5) - await pg.evaluate(PERMS) # a student's permissions (the page loads them only on sign-in) - await tap("#practiceBtn", 700) - if await pg.locator("#guideTipClose").is_visible(): - await pg.click("#guideTipClose") - - # 2 · beads: what they are - await line("beads") - await wait(600); await point("#rewardShelf"); await wait(1200); await point("#rewardShelf") - await spoken() - - # 3 · practicing a topic: a sentence, the mistake and its tip, the retry, a gold bead - rec.pause() - await pg.evaluate("__demo.caption('')") - await pg.locator("button.sat", has_text="פעלים").first.click() - await wait(800) - for _ in range(40): - if (await pg.inner_text("#prompt")).strip().startswith("פתח"): - break - await pg.click("#nextBtn"); await wait(300) - await pg.evaluate("scrollTo(0,0)"); await wait(500) - rec.resume() - await line("say"); await spoken(0.2) - await say("m449") - print(" score:", await pg.inner_text("#score"), "|", (await pg.inner_text("#wordReveals"))[:80].replace("\n", " ")) - await wait(400) - await line("colors"); await spoken(0.2) - await line("tip") - await wait(1200) - await tap(pg.locator("#wordReveals .bad.why", has_text="ח").first, 0) - await spoken() - await wait(2400) # time to read the tip - await pg.mouse.click(5, 5) # closes the letter's bubble - await line("listen"); await spoken(0.1) - await tap("#speakBtn", 1900) # the native answer - await line("again"); await spoken(0.1) - await say("c449") - await pg.evaluate("cancelAutoNext()") # keeps the 100 and its gold bead on screen - await wait(300) - await line("gold"); await spoken(0.6) - - # 4 · a story: the whole text, read aloud line by line - rec.pause() - await pg.evaluate("__demo.caption('')") - await pg.locator("button", has_text="חזרה למסלול").first.click(); await wait(600) - await pg.locator("button.sat", has_text="סיפור: חברתי רובה").first.click(); await wait(900) - await pg.evaluate("scrollTo(0,0)"); await wait(400) - rec.resume() - await line("story") - await wait(700) - await tap("#hearBtn", 0) - await spoken(); await wait(1500) - await tap("#hearBtn", 300) # pauses the reading - - # 5 · "the Arabic that's already yours": streak, weekly goal, the beads collected - rec.pause() - await pg.evaluate("__demo.caption('')") - await pg.locator("button", has_text="חזרה למסלול").first.click(); await wait(600) - await pg.evaluate("scrollTo({top:0})"); await wait(300) - rec.resume() - await line("shelf") - await tap("#rewardShelf", 0) - await spoken(); await wait(1500) - await tap(pg.locator("dialog[open] button", has_text="סגירה").first, 500) - - # 6 · daily practice: a round of ten sentences - await line("daily") - await tap(pg.locator("#today button", has_text="תרגול יומי").first, 0) - await spoken(); await wait(800) - - # 7 · the digital library (its own door on the opening screen) - rec.pause() - await pg.evaluate("__demo.caption('')") - await pg.goto(url, wait_until="networkidle"); await wait(1500) - await pg.evaluate(PERMS) - if library: - await pg.evaluate("l=>{LIBRARY=l}", library) - await pg.click("#libraryBtn"); await wait(2500) - rec.resume() - await line("library") - await spoken(); await wait(1500) - - # 8 · games: a round of the memory game (on lesson 1's four verbs) - await line("memory") - await pg.evaluate(PERMS) - await tap(pg.locator("#nav button", has_text="משחקים").first, 500) - await tap(pg.locator("#vids button", has_text="הפועל זכרון").first, 900) - pairs = [("פתח", "פַתַח"), ("אכל", "אַכַּל"), ("שאל", "סַאַל"), ("ישב", "קַעַד")] - await tap(pg.locator(".mg-bead.he", has_text=pairs[0][0]).first, 350) # a wrong pair first - await tap(pg.locator(".mg-bead.ar", has_text=pairs[1][1]).first, 1100) - for he, ar in pairs: - await tap(pg.locator(".mg-bead.he", has_text=he).first, 300) - await tap(pg.locator(".mg-bead.ar", has_text=ar).first, 650) - await spoken(); await wait(1500) - - # 9 · closing card - await pg.evaluate("__demo.caption('')") - await pg.evaluate("""__demo.card(` -

יאללה, מתחילים!

פותחים את האפליקציה
ומתרגלים כמה דקות ביום

`)""") - await wait(700) - await line("outro"); await spoken() - await wait(2400) - - await rec.stop() - await wait(500) # the last sounds' data - rec.cues = [c for c in rec.cues if c[1].exists()] - await browser.close() - - silent = frames_dir / "silent.mp4" - rec.encode(silent) - if sound or voice or music_gain or music_file: - rec.mix(silent, out, music_gain, music_file) - else: # the default: a silent video - subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(silent), "-c", "copy", - "-movflags", "+faststart", str(out)], check=True) - print(f"{out}: {len(rec.frames)} frames, {len(rec.cues)} sounds") - if keep_frames: - print("frames kept in", frames_dir) - else: - shutil.rmtree(frames_dir) - - -if __name__ == "__main__": - ap = argparse.ArgumentParser() - ap.add_argument("--url", default="http://localhost:7861/") - ap.add_argument("--takes", required=True) - ap.add_argument("--sound", action="store_true", help="the app's own sounds (default: a silent video)") - ap.add_argument("--voice", help="a voiceover from make_voice.py (default: captions only)") - ap.add_argument("--out", default="demo.mp4") - ap.add_argument("--music", type=float, default=0, help="music.py's loop at this level, e.g. 0.22 (default: none)") - ap.add_argument("--music-file", help="your own music instead of music.py's (looped and faded to fit)") - ap.add_argument("--keep-frames", action="store_true") - a = ap.parse_args() - asyncio.run(run(a.url, a.takes, a.voice, a.out, a.music, a.music_file, a.sound, a.keep_frames)) diff --git a/scripts/demo_video/script.py b/scripts/demo_video/script.py deleted file mode 100644 index 1f0be4a901b5280411013de1987dad00c2e3152c..0000000000000000000000000000000000000000 --- a/scripts/demo_video/script.py +++ /dev/null @@ -1,48 +0,0 @@ -"""The video's words: per scene, the caption on screen (title, subtitle, where) and what the Hebrew voice says. - -The voice text (used only with record.py --voice) may carry niqqud where the TTS voice would misread a word (the app's name, mainly); -captions stay without it. Without a voice, each caption stays up long enough to read (record.py, `hold`). -""" - -LINES = { - "intro": ("לומדים לדבר ערבית מדוברת", "ככה מתרגלים באהלא וסהלא 👇", "bottom", - "רוצים לדבר ערבית מדוברת? בואו נראה איך מתרגלים באַהְלָא וְסַהְלָא!"), - "beads": ("אוספים חרוזים 📿", "כל משפט שאומרים בקול הוא חרוז · ציון 75 ומעלה הופך אותו לזהב ★", "bottom", - "כל משפט שאומרים בקול הוא חרוז. ציון שבעים וחמש ומעלה, והוא הופך לזהב."), - "gold": ("עוד חרוז זהב ★", "כל משפט שאמרתם נאסף כחרוז", "bottom", - "עוד חרוז זהב!"), - "shelf": ("הערבית שכבר שלכם", "רצף ימים, יעד שבועי וכל החרוזים שאספתם", "bottom", - "וכאן רואים את כל הערבית שכבר שלכם: רצף, יעד שבועי, וכל החרוזים."), - "library": ("ספרייה דיגיטלית", "סרטונים מהקורס, לצפייה מתי שרוצים", "bottom", - "בספרייה הדיגיטלית מחכים סרטונים מהקורס."), - "path": ("המסלול שלכם", "שיעור אחרי שיעור · כל נושא פתוח", "bottom", - "זה המסלול שלכם. שיעור אחרי שיעור, וכל נושא פתוח."), - "words": ("מילים חדשות", "נזכרים, ואז חושפים את המילה", "bottom", - "מתחילים במילים חדשות: נזכרים, ואז חושפים."), - "say": ("רואים משפט בעברית", "ואומרים אותו בקול בערבית", "bottom", - "רואים משפט בעברית, ואומרים אותו בקול, בערבית."), - "colors": ("כל אות נצבעת לפי מה שנשמע", "ירוק: נכון · אדום: כדאי לתקן", "bottom", - "כל אות נצבעת לפי מה שנשמע. ירוק, נכון. אדום, כדאי לתקן."), - "tip": ("מקישים על האות האדומה", "ומקבלים טיפ: מה נשמע ואיך לתקן", "top", - "מקישים על האות האדומה, ומקבלים טיפ איך לתקן."), - "listen": ("שומעים איך אומרים נכון", "ומנסים שוב", "top", - "שומעים איך אומרים נכון,"), - "again": ("שומעים איך אומרים נכון", "ומנסים שוב", "top", - "ומנסים שוב."), - "great": ("מצוין! 🎉", "כל משפט שאמרתם נאסף כחרוז", "bottom", - "מצוין! כל משפט שאמרתם נאסף כחרוז."), - "story": ("סיפורים ושיחות", "שומעים את כל הסיפור, ואומרים שורה אחרי שורה", "bottom", - "יש גם סיפורים ושיחות: שומעים את כל הסיפור, ואומרים שורה אחרי שורה."), - "games": ("משחקים", "זיכרון פעלים, שיחה עם דמויות ועוד", "bottom", - "ובלשונית המשחקים: זיכרון פעלים, שיחה עם דמויות, ועוד."), - "memory": ("הפועל זכרון", "הופכים חרוזים ומוצאים זוגות: עברית וערבית", "bottom", - "הופכים חרוזים, ומוצאים זוגות של עברית וערבית."), - "streak": ("רצף 🔥 · יעד שבועי · חרוזי זהב ★", "רואים כמה ערבית כבר שלכם", "bottom", - "יש רצף ימים, יעד שבועי, וחרוזי זהב."), - "daily": ("תרגול יומי", "10 משפטים ביום: חצי לשיפור, חצי חדשים", "bottom", - "ובכל יום מחכה לכם תרגול יומי קצר."), - "verbs": ("טבלת הפעלים", "כל הצורות שלמדתם, לפי שיעור", "bottom", - "וכל הפעלים שלמדתם, בטבלה אחת."), - "outro": ("", "", "bottom", - "יאללה, מתחילים!"), -} diff --git a/scripts/export_powsm_onnx.py b/scripts/export_powsm_onnx.py deleted file mode 100644 index a6be4b0f6d408c45adaf8c896d01dd96f04d6934..0000000000000000000000000000000000000000 --- a/scripts/export_powsm_onnx.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 -"""Export a POWSM-CTC fine-tune to ONNX, so the Space can run it with onnxruntime alone (no ESPnet). - -Usage: python scripts/export_powsm_onnx.py checkpoints/powsm-ft-shortpad/powsm_ft.pt exports/powsm-ft-shortpad - python scripts/export_powsm_onnx.py --check data/minerva/dev.csv # compare with ESPnet - -Needs the export-time packages (scripts/setup_zero_shot_deps.sh). Writes to : - model.onnx encoder + CTC head for one recording: feats (1, T, 80) and their length -> log-posteriors - (1, T', V) over POWSM's phone tokens only. The prompt is baked in: , , no previous - text, so its two positions are already dropped from T'. - model.int8.onnx the same with int8 weights (dynamic quantisation): ~4x smaller, faster on a CPU - frontend.npz what turns 16 kHz audio into feats, copied from the model: STFT window, mel matrix, - global mean and std (app/powsm.py PowsmOnnxEmitter applies them with torch.stft) - powsm_onnx.json vocab (phone -> column), blank, padding, frame rate and where it came from -Upload to a Hub model repo and serve it with MODEL_ID=powsm-onnx: (or MODEL_ID= locally). -""" -from __future__ import annotations - -import argparse -import csv -import json -import sys -import time -from pathlib import Path - -import numpy as np - -ROOT = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(ROOT)) - -from app.powsm import PROMPT, load_powsm, pad_seconds, use_extra_deps # noqa: E402 - - -def main() -> None: - ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) - ap.add_argument("checkpoint", help="a train/finetune_powsm.py checkpoint (powsm_ft.pt)") - ap.add_argument("out", type=Path) - ap.add_argument("--lang", default="") - ap.add_argument("--check", type=Path, help="a CSV whose first clips are scored both ways and compared") - ap.add_argument("--check-n", type=int, default=20) - ap.add_argument("--no-int8", action="store_true", help="skip the int8 copy") - args = ap.parse_args() - use_extra_deps() - import torch - - s2t = load_powsm(args.lang, args.checkpoint, device="cpu") - model = s2t.s2t_model.eval() - conv = s2t.converter - lang_id, task_id = conv.token2id[args.lang], conv.token2id[""] - - # the phone columns, named as the app's scorer spells them (no slashes, no tie bars) - cols, names = [model.blank_id], {"": 0} - for i, t in enumerate(model.token_list): - if len(t) > 2 and t[0] == "/" and t[-1] == "/": - name = t[1:-1].replace("͡", "") - if name not in names: - names[name] = len(cols) - cols.append(i) - - class EncoderCTC(torch.nn.Module): - """POWSM's encoder and CTC head with the prompt fixed: (feats, lengths) -> log-posteriors over phones.""" - - def __init__(self): - super().__init__() - self.encoder, self.ctc = model.encoder, model.ctc - with torch.no_grad(): - prefix = torch.tensor([[lang_id, task_id]]) - prev = torch.tensor([[model.na]]) - memory, mem_lens, _ = model.prompt_encoder(model.pos_enc(model.embed(prev)), torch.tensor([1])) - self.register_buffer("prefix", model.embed_proj(model.embed(prefix))) - self.register_buffer("memory", model.prompt_proj(memory)) - self.register_buffer("memory_mask", torch.ones(1, 1, memory.shape[1], dtype=torch.bool)) - self.register_buffer("cols", torch.tensor(cols)) - - def forward(self, feats, feats_lens): - enc, _, _ = self.encoder(feats, feats_lens, ctc=self.ctc, prefix_embeds=self.prefix, - memory=self.memory, memory_mask=self.memory_mask) - if isinstance(enc, tuple): - enc = enc[0] - logits = self.ctc.ctc_lo(enc[:, PROMPT:]) - return torch.log_softmax(logits.index_select(-1, self.cols), dim=-1) - - def features(wav: np.ndarray): - x = torch.from_numpy(wav.astype(np.float32))[None] - feats, lens = model.frontend(x, torch.tensor([x.shape[1]])) - return model.normalize(feats, lens) - - net = EncoderCTC().eval() - args.out.mkdir(parents=True, exist_ok=True) - sample = (np.random.default_rng(0).standard_normal(16000 * 3) * 0.05).astype(np.float32) - feats, lens = features(sample) - onnx_path = args.out / "model.onnx" - print("exporting", onnx_path, flush=True) - with torch.no_grad(): - torch.onnx.export(net, (feats, lens), str(onnx_path), input_names=["feats", "feats_lens"], - output_names=["logp"], dynamic_axes={"feats": {1: "T"}, "logp": {1: "frames"}}, - opset_version=17, dynamo=False) - if not args.no_int8: - from onnxruntime.quantization import QuantType, quantize_dynamic - print("quantising to int8", flush=True) - # only the matrix products: onnxruntime's CPU kernels have no int8 convolution (ConvInteger) - quantize_dynamic(str(onnx_path), str(args.out / "model.int8.onnx"), weight_type=QuantType.QInt8, - op_types_to_quantize=["MatMul", "Gemm"]) - - fe = model.frontend - stft = fe.stft - window = torch.hann_window(stft.win_length).numpy() - np.savez(args.out / "frontend.npz", window=window, melmat=fe.logmel.melmat.numpy(), - mean=model.normalize.mean.numpy(), std=model.normalize.std.numpy(), - n_fft=stft.n_fft, win_length=stft.win_length, hop_length=stft.hop_length) - assert stft.center and stft.window == "hann" and fe.logmel.log_base is None - meta = {"kind": "powsm-ctc-onnx", "lang": args.lang, "blank": 0, "vocab": names, - "pad_seconds": pad_seconds(args.checkpoint), "frames_per_second": 25, "prompt_positions": PROMPT, - "source": {"base": "espnet/powsm_ctc", "checkpoint": str(args.checkpoint)}, - "files": {"fp32": "model.onnx", "int8": None if args.no_int8 else "model.int8.onnx"}} - (args.out / "powsm_onnx.json").write_text(json.dumps(meta, ensure_ascii=False, indent=1), encoding="utf-8") - print("wrote", sorted(p.name for p in args.out.iterdir()), flush=True) - - if args.check: - check(args, net, features) - - -def check(args, net, features) -> None: - """ESPnet's own model vs the app's ONNX emitter (fp32 and int8) on real clips: same frames, same argmax?""" - import torch - - from app.powsm import PowsmOnnxEmitter - from app.scorer import decode_audio - rows = list(csv.DictReader(open(args.check, encoding="utf-8")))[: args.check_n] - clips = [decode_audio((args.check.parent / r["audio"]).read_bytes()) for r in rows] - for kind in ("fp32", "int8"): - if kind == "int8" and args.no_int8: - continue - em = PowsmOnnxEmitter(str(args.out), precision=kind) - diffs, agree, total, t = [], 0, 0, 0.0 - for wav in clips: - x = np.pad(wav, (0, max(0, int(em.pad * 16000) - len(wav)))).astype(np.float32) - with torch.no_grad(): - f, ln = features(x) - ref = net(f, ln)[0].numpy() - keep = max(1, round(ref.shape[0] * len(wav) / len(x))) - ref = ref[:keep] - t0 = time.perf_counter() - got = em(wav)[::2] # the emitter repeats each frame for the scorer's 50 frames/s - t += time.perf_counter() - t0 - n = min(len(ref), len(got)) - diffs.append(float(np.abs(np.exp(ref[:n]) - np.exp(got[:n])).max())) - agree += int((ref[:n].argmax(-1) == got[:n].argmax(-1)).sum()) - total += n - print(f"{kind}: frames whose best phone matches ESPnet {100 * agree / total:.2f}% · " - f"largest posterior difference {max(diffs):.4f} · {t / len(clips):.2f} s/clip", flush=True) - - -if __name__ == "__main__": - main() diff --git a/scripts/gop_pooling_bench.py b/scripts/gop_pooling_bench.py deleted file mode 100644 index 0c34bd2cdbdf71b10b54e066717e03e5ae508cb5..0000000000000000000000000000000000000000 --- a/scripts/gop_pooling_bench.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Compare the GOP frame pools (GopConfig.ratio_pool) on the synthetic CTC scenarios of tests/test_gop_pooling.py. - - python scripts/gop_pooling_bench.py - -Prints the /a/ score per scenario and pool, the margin between the worst correct vowel and the best -substitution (higher separates better), then two sweeps: a spurious spike's height, and a faint -competitor haze over a growing silent window. -""" -from __future__ import annotations - -import sys -from dataclasses import replace -from pathlib import Path - -import numpy as np - -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) - -from app.align import Alignment, GopConfig, score_tokens # noqa: E402 -from tests.test_gop_pooling import A, BLANK, CONFIGS, I, K, SCENARIOS, SCHWA, T_, TOKENS, V, emissions, \ - naive_mean_score, score_a # noqa: E402 - -CONFIGS = {**CONFIGS, "lse.25": GopConfig(ratio_pool="lse", pool_tau=0.25), - "gated0": GopConfig(ratio_pool="gated", energy_spread=0)} - - -def table() -> None: - names = list(CONFIGS) - print(f"{'scenario':24}{'want':>6}" + "".join(f"{n:>8}" for n in names) + f"{'naive':>8}") - for sc in SCENARIOS: - row = [score_a(sc, cfg).score for cfg in CONFIGS.values()] - print(f"{sc.name:24}{'pass' if sc.good else 'fail':>6}" + "".join(f"{x:8.1f}" for x in row) - + f"{naive_mean_score(sc):8.1f}") - margin = [] - for cfg in CONFIGS.values(): - good = [score_a(s, cfg).score for s in SCENARIOS if s.good] - bad = [score_a(s, cfg).score for s in SCENARIOS if not s.good] - margin.append(min(good) - max(bad)) - print(f"{'margin (min ok - max bad)':30}" + "".join(f"{x:8.1f}" for x in margin)) - - -def spike_sweep() -> None: - print("\nspurious /i/ spike in the nucleus vs a correct /a/ of .2/.4/.35") - print(f"{'spike':>6}" + "".join(f"{n:>8}" for n in CONFIGS)) - for h in (.3, .5, .7, .9, .98): - sc = replace(SCENARIOS[1], logp=emissions({4: {A: .2}, 5: {A: .4}, 6: {A: .35}, 8: {I: h}})) - print(f"{h:6.2f}" + "".join(f"{score_a(sc, cfg).score:8.1f}" for cfg in CONFIGS.values())) - - -def haze_sweep() -> None: - print("\none-frame /a/ at .9, a .05 schwa haze, a silent pause growing the window") - print(f"{'frames':>6}" + "".join(f"{n:>8}" for n in CONFIGS)) - for n in (10, 15, 20, 30, 50): - p = np.full((n, V), 1e-4) - p[:, SCHWA] = .05 - p[1, K], p[5, A], p[n - 2, T_] = .9, .9, .9 - p[:, BLANK] = 0 - p[:, BLANK] = 1 - p.sum(1) - energy = np.r_[[.3, .6, .5], np.ones(4), np.full(n - 9, .02), [.5, .1]] - al = Alignment(0.0, [(1, 2), (5, 6), (n - 2, n - 1)]) - row = [score_tokens(np.log(p), al, TOKENS, BLANK, cfg, energy=energy)[1].score for cfg in CONFIGS.values()] - print(f"{n:6d}" + "".join(f"{x:8.1f}" for x in row)) - - -if __name__ == "__main__": - table() - spike_sweep() - haze_sweep() diff --git a/scripts/gop_window_bench.py b/scripts/gop_window_bench.py deleted file mode 100644 index 83a8510d3e111160e195517d187fc84caacd46c2..0000000000000000000000000000000000000000 --- a/scripts/gop_window_bench.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Compare GOP windows (GopConfig.vowel_window) and pools on the synthetic cases of tests/test_gop_window.py. - - python scripts/gop_window_bench.py - -The spans come from ctc_viterbi. The cases have no audio, so "gated" falls back to the plain mean here -(scripts/gop_pooling_bench.py measures the energy pools). Prints the score of the token under test per case and -setting, and the margin between the worst token said right and the best one said wrong (higher separates better); -then how far a substituted vowel can sit from the aligned span before each pad stops seeing it. -""" -from __future__ import annotations - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) - -from app.align import GopConfig # noqa: E402 -from tests.test_gop_window import A, CASES, I, K, T_, Case, score # noqa: E402 - -SETTINGS = { - "wide max": GopConfig(ratio_pool="max", vowel_window=None), - "pad0 max": GopConfig(ratio_pool="max", vowel_window=0), - "pad1 max": GopConfig(ratio_pool="max", vowel_window=1), - "pad2 max": GopConfig(ratio_pool="max", vowel_window=2), - "wide mean": GopConfig(ratio_pool="mean", vowel_window=None), - "pad1 mean": GopConfig(ratio_pool="mean", vowel_window=1), -} - - -def table() -> None: - print(f"{'case':34}{'want':>6}" + "".join(f"{n:>11}" for n in SETTINGS)) - for c in CASES: - print(f"{c.name:34}{'pass' if c.good else 'fail':>6}" - + "".join(f"{score(c, cfg).score:11.1f}" for cfg in SETTINGS.values())) - margins = [] - for cfg in SETTINGS.values(): - good = [score(c, cfg).score for c in CASES if c.good] - bad = [score(c, cfg).score for c in CASES if not c.good] - margins.append(min(good) - max(bad)) - print(f"{'margin (min ok - max bad)':40}" + "".join(f"{m:11.1f}" for m in margins)) - - -def distance_sweep() -> None: - print("\n/a/ at .35 on frame 4, /i/ held two frames at .85, d frames after the /a/") - print(f"{'d':>4}" + "".join(f"{n:>11}" for n in SETTINGS)) - for d in range(1, 7): - c = Case("", [K, A, T_], {1: {K: .9}, 4: {A: .35}, 4 + d: {I: .85}, 5 + d: {I: .85}, 13: {T_: .9}}, 1, False, n=15) - print(f"{d:4d}" + "".join(f"{score(c, cfg).score:11.1f}" for cfg in SETTINGS.values())) - - -if __name__ == "__main__": - table() - distance_sweep() diff --git a/scripts/import_minerva.py b/scripts/import_minerva.py deleted file mode 100644 index fc700c89fe1806a7cbcbcecf38fe26ea2ebc3c82..0000000000000000000000000000000000000000 --- a/scripts/import_minerva.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Import the Minerva videos into the lab as native, correctly spoken samples. - -The videos have no subtitle stream: their captions are burned in. ``data/minerva_metadata.json`` holds the reviewed -caption of every line (Arabic, pointed-Hebrew transliteration, translation, expected IPA) with its spoken time span, -so the importer only cuts, scores and uploads. The lab page and every rescore read the same file. - -Usage: - python scripts/import_minerva.py --dry-run - python scripts/import_minerva.py --email teacher@example.com [--video ...] - -Existing ``minerva::`` rows are skipped, making an interrupted import safe to resume. -""" -from __future__ import annotations - -import argparse -import os -import subprocess -import sys -import uuid -from datetime import date -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from dotenv import load_dotenv # noqa: E402 - -from app import lab # noqa: E402 -from app.auth import AuthUnavailable, load_auth # noqa: E402 -from app.main import MODEL_ID, build_version # noqa: E402 -from app.reference import References # noqa: E402 -from app.scorer import decode_audio, load_scorer # noqa: E402 -from app.tts import BASELINE_VOICES, load_tts # noqa: E402 - -SPEAKER = "מינרווה" - - -def source_file(folder: Path, youtube_id: str) -> Path: - found = sorted(folder.glob(f"*{youtube_id}*")) - if len(found) != 1: - raise ValueError(f"expected one video containing {youtube_id}, found {len(found)}") - return found[0] - - -def wav_segment(video: Path, start: float, end: float) -> bytes: - command = ["ffmpeg", "-v", "error", "-ss", str(start), "-to", str(end), "-i", str(video), - "-vn", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", "-f", "wav", "-"] - done = subprocess.run(command, stdout=subprocess.PIPE, check=False) - if done.returncode or not done.stdout: - raise RuntimeError(f"ffmpeg could not extract {video.name} at {start:.1f}-{end:.1f}") - return done.stdout - - -def phrases(only: set[str] | None = None) -> list[tuple[dict, dict, dict]]: - """(video, segment, phrase) for every reviewed line, optionally limited to some YouTube ids.""" - out = [] - for video in lab.minerva_metadata(): - if only and video["id"] not in only: - continue - for segment in video["segments"]: - out.append((video, segment, {"cat": "lab", "key": segment["item"], "he": segment["he"], - "variants": [(segment["ar"], segment["ipa"])]})) - return out - - -def default_email() -> str: - raw = os.environ.get("MINERVA_IMPORT_EMAIL") or os.environ.get("TEACHER_EMAILS", "") - return raw.replace(",", " ").split()[0].lower() if raw.split() else "" - - -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - ap.add_argument("--videos-dir", type=Path, default=Path("/home/eyalshw/Videos/minerva")) - ap.add_argument("--email", default="", help="teacher email recorded as the importer (or MINERVA_IMPORT_EMAIL)") - ap.add_argument("--dry-run", action="store_true", help="validate videos, segments and transcripts; do not score/upload") - ap.add_argument("--video", action="append", default=[], help="import only this YouTube id (repeatable)") - ap.add_argument("--verify", action="store_true", help="verify the remote rows and audio names; do not score/upload") - ap.add_argument("--no-baseline", action="store_true", help="score without the native TTS baseline") - args = ap.parse_args() - load_dotenv() - - entries = phrases(set(args.video) or None) - if not entries: - print("no Minerva lines selected", file=sys.stderr) - return 2 - files = {v["id"]: source_file(args.videos_dir, v["id"]) for v, _, _ in entries} - total = len(entries) - print(f"validated {total} lines in {len(files)} videos") - if args.dry_run: - return 0 - - email = (args.email or default_email()).strip().lower() - if not email: - print("give --email or set MINERVA_IMPORT_EMAIL/TEACHER_EMAILS", file=sys.stderr) - return 2 - auth = load_auth() - if auth is None or not auth.secret: - print("needs SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY", file=sys.stderr) - return 2 - try: - remote = [s for s in auth.lab_samples() if (s.get("speaker") or "") == SPEAKER] - existing = {s["item"] for s in remote} - except AuthUnavailable as e: - print(f"lab is unavailable: {e}", file=sys.stderr) - return 2 - if args.verify: - expected = {phrase["key"] for _, _, phrase in entries} - audio = sum(bool(s.get("audio")) for s in remote if s.get("item") in expected) - missing = sorted(expected - existing) - extra = sorted(existing - expected) if not args.video else [] - print(f"remote Minerva: {len(expected & existing)}/{total} rows, {audio}/{total} audio files named") - if missing: - print(f"missing: {' '.join(missing)}") - if extra: - print(f"other Minerva rows: {' '.join(extra)}") - return 0 if not missing and audio == total else 1 - - model = os.environ.get("MODEL_ID", MODEL_ID) - scorer = load_scorer(model) - tts = None if args.no_baseline else load_tts() - refs = References(scorer, tts, list(BASELINE_VOICES.values())) if tts else None - saved = skipped = 0 - for n, (video, segment, phrase) in enumerate(entries, 1): - item, start, end = phrase["key"], segment["start"], segment["end"] - if item in existing: - skipped += 1 - print(f"[{n}/{total}] skip {item}") - continue - data = wav_segment(files[video["id"]], start, end) - wav = decode_audio(data) - baseline = refs.get(item, phrase) if refs else [] - result = scorer.score(wav, phrase, baseline, diagnostics=True) - if result.get("error"): - raise RuntimeError(f"{item}: scoring failed: {result['error']}") - row = {"email": email, "item": item, "ar": result["variant"]["ar"], "ipa": result["variant"]["ipa"], - "speaker": SPEAKER, "label": "correct", "mistake": None, - "device": f"minerva:{video['id']}:{start:.1f}-{end:.1f}", "score": result["score"], - "heard_ipa": result["heard_ipa"], "flagged": lab.flagged_sounds(result), "result": result, - "model": model, "version": build_version()} - name = f"{date.today():%Y-%m}/minerva-{video['id']}-{segment['line']:02d}-{uuid.uuid4().hex}.wav" - if not auth.add_lab_sample(row, data, "audio/wav", name, attempts=4): - raise RuntimeError(f"{item}: row saved but audio upload failed; stop before creating more incomplete rows") - saved += 1 - print(f"[{n}/{total}] saved {item}: {result['score']}") - print(f"Minerva import complete: {saved} saved, {skipped} already present") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/minerva_benchmark.py b/scripts/minerva_benchmark.py deleted file mode 100644 index 73aa1f1d5970b07b36135d56495cec762480488e..0000000000000000000000000000000000000000 --- a/scripts/minerva_benchmark.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 -"""Benchmark a phoneme model on held-out Minerva lines: false flags on native speech, and planted-mistake detection. - -Usage: python scripts/minerva_benchmark.py --json base.json # MODEL_ID (or the default) on test.csv - python scripts/minerva_benchmark.py --model checkpoints/ihki-ft --json ft.json - python scripts/minerva_benchmark.py --refs --json base-refs.json # with the cached TTS baselines - python scripts/minerva_benchmark.py --compare base.json ft.json # side by side, no scoring - python scripts/minerva_benchmark.py --compare ft.json --threshold ħ=65 --threshold ʕ=40 - python scripts/minerva_benchmark.py --sweep dev-ft.json # false flags vs detection per threshold - -Every line is a native speaker saying it right, so - false flags = sounds the scorer marks down although they were said right (per sound, and what it heard instead); - detection = a learner's typical substitution, planted by scoring the line as if another sound was asked: - a clip with خ (x) scored as if ح (ħ) was written is a learner saying a Hebrew ח for ح. -The pairs are SWAPS below: (sound said, sound asked, its letter said, its letter asked). Run on dev.csv while choosing -thresholds or checkpoints and on test.csv for the numbers you report. - -Every sound is scored once with the flag threshold open (flag_below=100) and its score saved, so --compare/--sweep can -apply any per-sound threshold afterwards: flagged at t = flagged at 100 and score < t (flag_below is used nowhere else). - -Without --refs the numbers describe the acoustic model itself. --refs adds the TTS baseline the server uses, but only -from the local cache (no provider call): lines whose model answer was never generated are scored without it. A planted -sound never matches the baseline (the voice said the written sound), so with --refs detection is somewhat optimistic. -""" -from __future__ import annotations - -import argparse -import collections -import csv -import json -import os -import sys -from dataclasses import replace -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from app.scorer import SAMPLE_RATE, decode_audio, frame_energy, load_scorer # noqa: E402 - -ROOT = Path(__file__).resolve().parent.parent -DEFAULT_THRESHOLD = 50.0 # ScorerConfig.flag_below -SWAPS = [ # (said, asked, letter said, letter asked): what a Hebrew speaker says instead of the Arabic sound - ("x", "ħ", "خ", "ح"), # Hebrew ח for ح - ("h", "ħ", "ه", "ح"), # ה for ح - ("ʔ", "ʕ", "ء", "ع"), # א for ع - ("ħ", "ʕ", "ح", "ع"), # ח for ع (voicing) - ("t", "tˤ", "ت", "ط"), # plain for emphatic - ("s", "sˤ", "س", "ص"), - ("d", "dˤ", "د", "ض"), - ("k", "q", "ك", "ق"), # Palestinian has no q in the bank; kept for completeness -] - - -def swapped(phrase: dict, said: str, asked: str, l_said: str, l_asked: str) -> dict | None: - """The phrase with every `said` sound written as `asked` (None if it has none).""" - variants = [] - for ar, ipa in phrase["variants"]: - toks = ipa.split(" ") - if said not in toks: - return None - variants.append((ar.replace(l_said, l_asked), " ".join(asked if t == said else t for t in toks))) - return {**phrase, "key": f"{phrase['key']}:{said}>{asked}", "variants": variants} - - -def phones(result: dict) -> list[dict]: - return [p for w in result.get("words", []) for p in w.get("phonemes", [])] - - -def cached_refs(scorer, tts): - """phrase -> [Reference] from the TTS cache only; never calls the provider.""" - from app.arabic_diacritics import tts_arabic - from app.tts import RAEF_VOICE_ID - memo = {} - - def get(phrase: dict) -> list: - text = tts_arabic(*phrase["variants"][0]) - if text not in memo: - memo[text] = [] - if tts is not None and tts.cached(text, RAEF_VOICE_ID): - ref = scorer.reference(decode_audio(tts.wav(text, RAEF_VOICE_ID)[0]), phrase) - memo[text] = [ref] if ref is not None else [] - return memo[text] - return get - - -def run(model: str, csv_path: Path, limit: int, use_refs: bool) -> dict: - scorer = load_scorer(model) - production = {"*": scorer.cfg.flag_below, **scorer.cfg.flag_below_sound} # what the server would apply - scorer.cfg = replace(scorer.cfg, flag_below=100.0, flag_below_sound={}) # open: thresholds are applied afterwards - refs_for = lambda phrase: [] - if use_refs: - from dotenv import load_dotenv - from app.tts import load_tts - load_dotenv(ROOT / ".env") - refs_for = cached_refs(scorer, load_tts()) - rows = list(csv.DictReader(open(csv_path, encoding="utf-8")))[: limit or None] - tokens, scores, unrelated, with_refs = [], [], 0, 0 - for n, row in enumerate(rows, 1): - wav = decode_audio((csv_path.parent / row["audio"]).read_bytes()) - logp = scorer.emit(wav) - energy, duration = frame_energy(wav, len(logp)), len(wav) / SAMPLE_RATE - phrase = {"cat": "lab", "key": row["item"], "he": row["item"], "variants": [(row["ar"], row["ipa"])]} - refs = refs_for(phrase) - with_refs += bool(refs) - res = scorer.score_logp(logp, phrase, refs, False, duration, energy) - if res.get("error") or res.get("unrelated"): - unrelated += 1 - continue - scores.append(res["score"]) - for p in phones(res): - tokens.append({"kind": "native", "sound": p.get("sound") or p["ipa"], "score": round(p["score"], 1), - "flag": bool(p.get("flagged")), "heard": p.get("heard") or "_"}) - for said, asked, l_said, l_asked in SWAPS: - planted = swapped(phrase, said, asked, l_said, l_asked) - if planted is None: - continue - r = scorer.score_logp(logp, planted, refs, False, duration, energy) - if r.get("error") or r.get("unrelated"): - continue - for p in phones(r): - if asked in (p["ipa"], p.get("sound")): - tokens.append({"kind": f"{said}>{asked}", "sound": asked, "score": round(p["score"], 1), - "flag": bool(p.get("flagged")), "heard": p.get("heard") or "_"}) - if n % 50 == 0: - print(f"{n}/{len(rows)}", file=sys.stderr) - return {"model": model, "csv": str(csv_path), "refs": use_refs, "thresholds": production, "lines": len(rows), "with_refs": with_refs, - "unrelated": unrelated, "mean_score": round(sum(scores) / len(scores), 2) if scores else None, - "tokens": tokens} - - -def flagged(tok: dict, thresholds: dict[str, float]) -> bool: - return tok["flag"] and tok["score"] < thresholds.get(tok["sound"], thresholds.get("*", DEFAULT_THRESHOLD)) - - -def thresholds_of(run_: dict, override: dict[str, float]) -> dict[str, float]: - """The run's production thresholds (the model's FLAG_BELOW_BY_MODEL), with --threshold on top.""" - return {**run_.get("thresholds", {}), **override} - - -def summary(run_: dict, thresholds: dict[str, float]) -> dict: - """False flags per sound and detection per pair, at these thresholds.""" - sounds = collections.defaultdict(lambda: {"right": 0, "false_flags": 0, "heard": collections.Counter()}) - pairs = {f"{s}>{a}": {"planted": 0, "caught": 0, "caught_as_said": 0} for s, a, *_ in SWAPS} - for t in run_["tokens"]: - hit = flagged(t, thresholds) - if t["kind"] == "native": - s = sounds[t["sound"]] - s["right"] += 1 - if hit: - s["false_flags"] += 1 - s["heard"][t["heard"]] += 1 - else: - p = pairs[t["kind"]] - p["planted"] += 1 - p["caught"] += hit - p["caught_as_said"] += hit and t["heard"] == t["kind"].split(">")[0] - rate = lambda a, b: round(a / b, 4) if b else None - right = sum(s["right"] for s in sounds.values()) - false = sum(s["false_flags"] for s in sounds.values()) - return { - "frr": rate(false, right), - "sounds": {k: {"right": v["right"], "false_flags": v["false_flags"], "frr": rate(v["false_flags"], v["right"]), - "heard": dict(v["heard"].most_common(5))} - for k, v in sorted(sounds.items(), key=lambda kv: -(kv[1]["false_flags"] / max(1, kv[1]["right"])))}, - "pairs": {k: {**v, "detection": rate(v["caught"], v["planted"]), "as_said": rate(v["caught_as_said"], v["planted"])} - for k, v in pairs.items()}, - } - - -def pct(x) -> str: - return " - " if x is None else f"{100 * x:5.1f}%" - - -def label(r: dict) -> str: - return (Path(r["model"]).name + (" +refs" if r.get("refs") else ""))[-22:] - - -def report(runs: list[dict], thresholds: dict[str, float], min_right: int = 30) -> None: - sums = [summary(r, thresholds_of(r, thresholds)) for r in runs] - print(f"\n{'':<14}" + " ".join(f"{label(r):>22}" for r in runs)) - print(f"{'lines':<14}" + " ".join(f"{r['lines'] - r['unrelated']:>22}" for r in runs)) - print(f"{'with refs':<14}" + " ".join(f"{r.get('with_refs', 0):>22}" for r in runs)) - print(f"{'mean score':<14}" + " ".join(f"{r['mean_score']:>22}" for r in runs)) - print(f"{'false flags':<14}" + " ".join(f"{pct(s['frr']):>22}" for s in sums)) - print(f"{'thresholds':<14}" + " ".join( - f"{', '.join(f'{k}={v:g}' for k, v in thresholds_of(r, thresholds).items()) or '*=50':>22}" for r in runs)) - print("\nfalse flags per sound (native speech said right; lower is better)") - for k in [k for k, v in sums[0]["sounds"].items() if v["right"] >= min_right]: - cells = [] - for s in sums: - v = s["sounds"].get(k) - cells.append(f"{pct(v['frr'])} {json.dumps(v['heard'], ensure_ascii=False)[:14]:<14}" if v else " " * 21) - print(f" {k:<5} n={sums[0]['sounds'][k]['right']:<5}" + " ".join(f"{c:>22}" for c in cells)) - print("\ndetection of planted mistakes (said>asked; higher is better; in brackets: heard as the sound said)") - for k in sums[0]["pairs"]: - cells = [f"{pct(s['pairs'][k]['detection'])} ({pct(s['pairs'][k]['as_said']).strip()})" for s in sums] - print(f" {k:<6} n={sums[0]['pairs'][k]['planted']:<5}" + " ".join(f"{c:>22}" for c in cells)) - - -def sweep(run_: dict, steps=range(20, 100, 5)) -> None: - """Per asked sound: false flags on native speech vs detection of each planted pair, for thresholds 20..95.""" - print(f"\n{label(run_)} ({run_['csv']})") - for asked in dict.fromkeys(a for _, a, *_ in SWAPS): - pairs = [f"{s}>{a}" for s, a, *_ in SWAPS if a == asked] - native = [t for t in run_["tokens"] if t["kind"] == "native" and t["sound"] == asked] - planted = {p: [t for t in run_["tokens"] if t["kind"] == p] for p in pairs} - if not native or not any(planted.values()): - continue - print(f"\n {asked}: n={len(native)} native; " + ", ".join(f"{p} n={len(v)}" for p, v in planted.items())) - print(f" {'thr':>4} {'false':>7} " + " ".join(f"{p:>7}" for p in pairs)) - for th in steps: - th_ = {asked: th} - ff = sum(flagged(t, th_) for t in native) / len(native) - det = [sum(flagged(t, th_) for t in v) / len(v) if v else None for v in planted.values()] - now = run_.get("thresholds", {}) - mark = " <- now" if th == now.get(asked, now.get("*", DEFAULT_THRESHOLD)) else "" - print(f" {th:>4} {pct(ff)} " + " ".join(f"{pct(d)}" for d in det) + mark) - - -def parse_thresholds(items: list[str]) -> dict[str, float]: - out = {} - for item in items: - sound, _, value = item.partition("=") - out[sound] = float(value) - return out - - -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - ap.add_argument("--model", default=os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft")) - ap.add_argument("--csv", type=Path, default=ROOT / "data" / "minerva" / "test.csv") - ap.add_argument("--limit", type=int, default=0, help="at most this many lines (0: all)") - ap.add_argument("--refs", action="store_true", help="use the cached TTS baselines, as the server does") - ap.add_argument("--json", type=Path, help="write the run (every sound's score) here") - ap.add_argument("--compare", type=Path, nargs="+", help="print saved runs side by side instead of scoring") - ap.add_argument("--sweep", type=Path, nargs="+", help="print threshold sweeps of saved runs") - ap.add_argument("--threshold", action="append", default=[], metavar="SOUND=T", help="per-sound flag threshold") - args = ap.parse_args() - thresholds = parse_thresholds(args.threshold) - load = lambda p: json.loads(p.read_text(encoding="utf-8")) - if args.sweep: - for p in args.sweep: - sweep(load(p)) - return 0 - if args.compare: - report([load(p) for p in args.compare], thresholds) - return 0 - result = run(args.model, args.csv, args.limit, args.refs) - if args.json: - args.json.write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8") - report([result], thresholds) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/minerva_dataset.py b/scripts/minerva_dataset.py deleted file mode 100644 index 0a6bdf5ddd6b1fa262efd40957ac72c08225caf8..0000000000000000000000000000000000000000 --- a/scripts/minerva_dataset.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -"""Cut every reviewed Minerva line from the local videos into a training set, split by video. - -Usage: python scripts/minerva_dataset.py # -> data/minerva/{clips/,train.csv,dev.csv,test.csv} - python scripts/minerva_dataset.py --test 5 --dev 4 --seed 1 - -Nothing is uploaded: the clips stay on this machine (data/minerva/ is git-ignored). The split is by video, never by -line, so a speaker or a scene heard in training is never in dev/test. The CSVs have the columns train/finetune.py -reads (`audio,ipa`) plus `item,video,ar` for the benchmark (scripts/minerva_benchmark.py). Clips already cut are -kept, so a rerun only cuts what is missing. -""" -from __future__ import annotations - -import argparse -import csv -import random -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from app import lab # noqa: E402 -from scripts.import_minerva import source_file, wav_segment # noqa: E402 - -ROOT = Path(__file__).resolve().parent.parent - - -def split(videos: list[dict], n_test: int, n_dev: int, seed: int) -> dict[str, str]: - """{video id: "train" | "dev" | "test"}, reproducible for a seed.""" - ids = sorted(v["id"] for v in videos) - random.Random(seed).shuffle(ids) - return {vid: "test" if k < n_test else "dev" if k < n_test + n_dev else "train" for k, vid in enumerate(ids)} - - -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - ap.add_argument("--videos-dir", type=Path, default=Path("/home/eyalshw/Videos/minerva")) - ap.add_argument("--out", type=Path, default=ROOT / "data" / "minerva") - ap.add_argument("--test", type=int, default=5, help="videos held out for the benchmark") - ap.add_argument("--dev", type=int, default=4, help="videos held out for choosing checkpoints and thresholds") - ap.add_argument("--seed", type=int, default=0) - args = ap.parse_args() - - videos = list(lab.minerva_metadata()) - parts = split(videos, args.test, args.dev, args.seed) - clips = args.out / "clips" - clips.mkdir(parents=True, exist_ok=True) - rows: dict[str, list[dict]] = {"train": [], "dev": [], "test": []} - cut = 0 - for video in videos: - source = source_file(args.videos_dir, video["id"]) - for segment in video["segments"]: - path = clips / f"{video['id']}-{segment['line']:02d}.wav" - if not path.exists(): - path.write_bytes(wav_segment(source, segment["start"], segment["end"])) - cut += 1 - rows[parts[video["id"]]].append({"audio": str(path.relative_to(args.out)), "ipa": segment["ipa"], - "item": segment["item"], "video": video["id"], "ar": segment["ar"]}) - print(f"{parts[video['id']]:<5} {video['id']} {len(video['segments']):>3} lines {video['title']}") - for part, items in rows.items(): - with open(args.out / f"{part}.csv", "w", encoding="utf-8", newline="") as f: - w = csv.DictWriter(f, fieldnames=["audio", "ipa", "item", "video", "ar"]) - w.writeheader() - w.writerows(items) - seconds = sum(s["end"] - s["start"] for v in videos for s in v["segments"]) - print(f"cut {cut} new clips; " + ", ".join(f"{p} {len(r)}" for p, r in rows.items()) - + f" lines; {seconds / 60:.0f} min in all -> {args.out}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/mock_trace.py b/scripts/mock_trace.py deleted file mode 100644 index dfe11cf746909b7454a851d68f5f6ffbc675013f..0000000000000000000000000000000000000000 --- a/scripts/mock_trace.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -"""Writes a mock scoring trace for the trace dashboard (static/trace.html) and its schema (docs/trace-schema.ts). - -Usage: python scripts/mock_trace.py # -> static/assets/trace-mock.json - python scripts/mock_trace.py --out other.json - -Not a hand-written JSON: the real Scorer scores synthetic posteriors and energy, so every field is what the pipeline -produces. The recording is a learner saying وين سيدك؟ سيدي قعد في بيته (a bank sentence with several accepted -readings: سيدي / جدّي, بيت / بيت with the diphthong), with what the dashboard should show planted in it: - - ع of قعد said as a glottal stop (ء): flagged, with a severity-3 tip, and raised by a teacher's calibration floor - - a spike of h in a near-silent frame two frames after the short a of سيدك, inside the a's window and the k's - (the window runs from the end of the sound before to the start of the one after) - - k at the end of سيدك weak, as the native TTS voices' is too: the native baseline raises it. Two voices: the - male one (ראאף) weaker on that k (55) than the female one (פדוא, 70), so the score is measured against the lower; the female - voice also has a weak ع (80), which the calibration floor then still decides -""" -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path - -import numpy as np - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from app.phrases import PHRASES # noqa: E402 -from app.scorer import Reference, Scorer, Vocab # noqa: E402 - -SYMS = ["", "w", "eː", "n", "s", "iː", "d", "a", "k", "i", "ʔ", "ʕ", "f", "b", "t", "o", "dʒ", "j", "q", "g", - "h", "χ", "e", "u", "tʃ", "r", "l", "m", "ħ"] -VOCAB = {s: i for i, s in enumerate(SYMS)} -PHRASE = next(p for p in PHRASES if p["he"].startswith("איפה סבא שלך")) - -# (symbol, posterior, frames, energy) — what the learner said, frame by frame; "-" is the CTC blank -GAP, PAUSE = ("-", 0.95, 2, 0.15), ("-", 0.97, 6, 0.03) -SAID = [ - ("-", 0.97, 4, 0.02), - ("w", 0.85, 2, 0.55), GAP, ("eː", 0.9, 4, 1.0), GAP, ("n", 0.8, 2, 0.5), PAUSE, - ("s", 0.9, 3, 0.45), GAP, ("iː", 0.88, 4, 0.95), GAP, ("d", 0.8, 2, 0.5), GAP, - ("a", 0.45, 1, 0.85), ("-", 0.9, 1, 0.2), ("h", 0.96, 1, 0.04), ("-", 0.9, 1, 0.1), # the noise spike - ("k", 0.3, 2, 0.4), ("-", 0.8, 1, 0.1), PAUSE, # a weak k - ("s", 0.9, 3, 0.45), GAP, ("iː", 0.9, 4, 0.95), GAP, ("d", 0.85, 2, 0.5), GAP, ("i", 0.8, 2, 0.8), PAUSE, - ("ʔ", 0.82, 2, 0.4), GAP, ("a", 0.85, 2, 0.9), GAP, ("ʔ", 0.8, 2, 0.35), GAP, # ع said as ء - ("a", 0.85, 2, 0.9), GAP, ("d", 0.85, 2, 0.5), PAUSE, - ("f", 0.85, 3, 0.35), GAP, ("i", 0.85, 2, 0.8), PAUSE, - ("b", 0.85, 2, 0.5), GAP, ("eː", 0.9, 4, 1.0), GAP, ("t", 0.85, 2, 0.45), GAP, ("o", 0.85, 3, 0.85), - ("-", 0.97, 4, 0.02), -] - - -def emissions() -> tuple[np.ndarray, np.ndarray]: - rows, energy = [], [] - for sym, p, frames, e in SAID: - for _ in range(frames): - r = np.full(len(SYMS), (1 - p) / (len(SYMS) - 1)) - r[VOCAB["" if sym == "-" else sym]] = p - rows.append(r) - energy.append(e) - return np.log(np.array(rows)), np.array(energy) - - -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - ap.add_argument("--out", type=Path, default=Path(__file__).resolve().parent.parent / "static/assets/trace-mock.json") - args = ap.parse_args() - - logp, energy = emissions() - scorer = Scorer(lambda wav: logp, Vocab(VOCAB)) - scorer.set_calibrations([{"expected": "ʕ", "heard": "ʔ", "floor": 45, "scope": "global"}]) - # the native voices: everything right but their own weak k at the end of سيدك (and the female voice's ع) - toks = scorer._variants(PHRASE)[0][3] - weak = {"ראאף": {(1, "K"): 55.0}, "פדוא": {(1, "K"): 70.0, (3, "ʕ"): 80.0}} - refs = [Reference([(t.word, t.sound) for t in toks], [w.get((t.word, t.sound), 100.0) for t in toks], voice) - for voice, w in weak.items()] - result = scorer.score_logp(logp, PHRASE, refs, diagnostics=True, duration_s=len(logp) * 0.02, energy=energy) - result["phrase"] = {"he": PHRASE["he"], "ar": PHRASE["variants"][0][0]} - args.out.write_text(json.dumps(result, ensure_ascii=False, indent=1), encoding="utf-8") - print(f"{args.out}: score {result['score']}, {len(result['trace']['tokens'])} sounds, " - f"{len(result['trace']['variants'])} candidate readings, {result['trace']['frames']['count']} frames") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/rescore.py b/scripts/rescore.py deleted file mode 100644 index ddd7f4062f1f263af3bb8c0084ecabae27a064cb..0000000000000000000000000000000000000000 --- a/scripts/rescore.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -"""Rescore the flagged recordings with the current scorer and list the biggest changes against the stored score. - -Usage: python scripts/rescore.py # every flag that kept its recording - python scripts/rescore.py --status pending,fixed # only these review states - python scripts/rescore.py --limit 50 --top 20 - python scripts/rescore.py --json out.json # every row, with the old and new sound-level results - -Reads SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY (as the server does) and MODEL_ID. -A flag stores the phrase it was said for, its old result and the recording, so the comparison needs no -other data. Run it before shipping a scoring change: the rows that moved most are the ones to listen to. -""" -from __future__ import annotations - -import argparse -import json -import os -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from dotenv import load_dotenv # noqa: E402 - -from app.auth import load_auth # noqa: E402 -from app.lab import flag_phrase as phrase_for, flagged_sounds as sounds # noqa: E402 -from app.scorer import decode_audio, load_scorer # noqa: E402 - - -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - ap.add_argument("--status", default="pending,confirmed,rejected,fixed") - ap.add_argument("--limit", type=int, default=0, help="at most this many flags (0: all)") - ap.add_argument("--top", type=int, default=30, help="rows to print, biggest change first") - ap.add_argument("--json", type=Path, help="write every row here") - args = ap.parse_args() - - load_dotenv() - auth = load_auth() - if auth is None or not auth.secret: - print("needs SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY", file=sys.stderr) - return 2 - scorer = load_scorer(os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft")) - flags = [f for s in args.status.split(",") for f in auth.list_flags(s.strip()) if f.get("audio")] - if args.limit: - flags = flags[:args.limit] - - rows = [] - for f in flags: - audio = auth.flag_audio(f["id"]) - if audio is None: - continue - try: - new = scorer.score(decode_audio(audio[0]), phrase_for(f)) - except (ValueError, KeyError) as e: - print(f"flag {f['id']}: skipped ({e})", file=sys.stderr) - continue - if new.get("error"): - continue - old = f.get("result") or {} - rows.append({"id": f["id"], "status": f.get("status"), "phrase": f.get("he") or f.get("phrase"), - "old": f.get("score"), "new": new["score"], "delta": new["score"] - (f.get("score") or 0), - "old_flagged": sounds(old), "new_flagged": sounds(new), - "dialect": "mixed" if new.get("mixed_dialect") else new.get("dialect", ""), - "old_result": old, "new_result": new}) - - rows.sort(key=lambda r: -abs(r["delta"])) - print(f"{len(rows)} flags rescored; mean change {sum(r['delta'] for r in rows) / max(1, len(rows)):+.1f}") - for r in rows[:args.top]: - print(f"{r['id']:>6} {r['status'] or '':<9} {r['old']!s:>4} -> {r['new']:>3} ({r['delta']:+4}) " - f"{r['dialect']:<6} {r['phrase']}") - if r["old_flagged"] != r["new_flagged"]: - print(f" flagged: {' '.join(r['old_flagged']) or '-'} -> {' '.join(r['new_flagged']) or '-'}") - if args.json: - args.json.write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/setup_zero_shot_deps.sh b/scripts/setup_zero_shot_deps.sh deleted file mode 100755 index 2a58b8bdf478986a6b067a5a33edef7e38098c17..0000000000000000000000000000000000000000 --- a/scripts/setup_zero_shot_deps.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -# Install the extra packages for scripts/zero_shot_phone_models.py into .zero-shot-deps/ (git-ignored), -# outside the app venv. The script adds that folder to its own import path; nothing else sees it. -set -euo pipefail -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -PY="${PYTHON:-$ROOT/.venv/bin/python}" -DEST="$ROOT/.zero-shot-deps" - -rm -rf "$DEST" -"$PY" -m pip install --quiet --no-deps --target "$DEST/pkgs" -r "$ROOT/requirements-zero-shot.txt" - -# ESPnet imports torchaudio for multi-microphone beamforming, which POWSM never runs, and there is no -# torchaudio build for torch 2.14. A placeholder lets the import succeed and fails loudly if anything uses it. -mkdir -p "$DEST/stubs/torchaudio" -cat > "$DEST/stubs/torchaudio/__init__.py" <<'EOF' -"""Placeholder: ESPnet imports torchaudio for beamforming code that POWSM never runs.""" - - -def __getattr__(name): - raise ImportError(f"torchaudio placeholder: '{name}' is not available (no torchaudio build for this torch)") -EOF -echo "installed into $DEST" diff --git a/scripts/zero_shot_phone_models.py b/scripts/zero_shot_phone_models.py deleted file mode 100644 index b2c5145fa58ef7ff72f9df85c449954fe56598ac..0000000000000000000000000000000000000000 --- a/scripts/zero_shot_phone_models.py +++ /dev/null @@ -1,388 +0,0 @@ -#!/usr/bin/env python3 -"""How well do other phone recognizers hear Palestinian Arabic as-is? Zero-shot, on held-out Minerva lines. - -Usage: python scripts/zero_shot_phone_models.py # every model, dev.csv - python scripts/zero_shot_phone_models.py --models zipa,ft --split test - python scripts/zero_shot_phone_models.py --report # print the table from the saved JSON - -ZIPA and POWSM need extra packages: run scripts/setup_zero_shot_deps.sh once (it installs into .zero-shot-deps/, -outside the app venv). Results go to data/minerva/bench/zero-shot-.json, merged with earlier runs, so models -can be run one at a time. - -Each model's greedy CTC output is mapped onto the labels' phoneme set before scoring (canon()): length marks join -their vowel, details the labels never write (dental t̪, aspiration) are dropped, symbols with no contrast in -Palestinian are merged (χ→x, ʁ→ɣ, ɾ→r), and vowel qualities fold to a e i o u (ɛ→e, ɪ→i, ə→e). Geminates collapse -to one sound in both, as the scorer's tokenize() does. Measured: - PER phoneme error rate (edit distance / reference length) - consonant PER the same with every vowel removed from both sides: the sounds the scorer grades hardest - per sound how often each labelled sound was heard as itself, and what was heard instead - vowel length of the labelled vowels the model heard as some vowel, how often it got long/short right - frames/s CTC frame rate: the long-vowel check measures vowel length in frames - s/clip wall time per clip on this machine -The fine-tuned checkpoint picked its best epoch on dev.csv, so its dev numbers are slightly optimistic; use ---split test for reported numbers. -""" -from __future__ import annotations - -import argparse -import collections -import csv -import json -import sys -import time -import unicodedata -from pathlib import Path - -import numpy as np - -ROOT = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(ROOT)) - -from app.align import greedy_decode # noqa: E402 -from app.scorer import SAMPLE_RATE, decode_audio # noqa: E402 - -DATA = ROOT / "data" / "minerva" -# the sounds the table shows (the JSON has every sound) -KEY_SOUNDS = ["ħ", "ʕ", "x", "ɣ", "h", "ʔ", "tˤ", "sˤ", "dˤ"] -VOWELS = set("aeiou") - -# ---------- mapping a model's IPA onto the labels' phoneme set ---------- - -DROP = set("̪̺̻̥̬̃̚ʰʲʷ̩̯̆̈ʼ̟̠̝̞̹̜˞ˑ̤̰") # details the labels never write -SAME = { # no contrast in Palestinian (or a spelling of the same symbol) - "ɡ": "g", "ʤ": "dʒ", "ʧ": "tʃ", "χ": "x", "ʁ": "ɣ", "ɾ": "r", "ɹ": "r", "ʀ": "r", "ɫ": "l", - "ʜ": "ħ", "ʢ": "ʕ", "ɦ": "h", -} -FOLD = { # vowel quality -> the five the labels use - "ɑ": "a", "æ": "a", "ɐ": "a", "ʌ": "a", "ä": "a", "ɒ": "a", - "ɛ": "e", "ə": "e", "œ": "e", "ɘ": "e", "ɜ": "e", - "ɪ": "i", "ɨ": "i", "y": "i", "ʏ": "i", - "ɔ": "o", "ɵ": "o", "ø": "o", - "ʊ": "u", "ɯ": "u", "ʉ": "u", -} - - -def is_modifier(ch: str) -> bool: - return unicodedata.category(ch) in ("Mn", "Lm", "Sk") and ch not in "ʔʕ" - - -def canon(phone: str) -> str: - """One phone (as a model writes it) -> the labels' notation, '' if nothing is left.""" - p = "".join(ch for ch in phone.replace("͡", "").replace("‿", "") if ch not in DROP) - p = p.replace("ˠ", "ˤ").replace("̴", "ˤ") # velarized ~ pharyngealized: both are how emphatics get written - long = "ː" in p - p = p.replace("ː", "") - p = SAME.get(p, p) - p = "".join(FOLD.get(ch, ch) for ch in p) if p and all(FOLD.get(ch, ch) in VOWELS for ch in p) else p - if len(p) > 1 and all(ch in VOWELS for ch in p): # a diphthong or a doubled vowel - p = p[0] - if not p: - return "" - return p + "ː" if long and p in VOWELS else p # a long consonant is a geminate: collapsed below anyway - - -AFFRICATES = {("d", "ʒ"): "dʒ", ("t", "ʃ"): "tʃ"} - - -def to_phones(tokens: list[str]) -> list[str]: - """Model tokens (whole phones, or single characters as ZIPA writes them) -> canonical phones, - geminates collapsed.""" - phones: list[str] = [] - for tok in tokens: - tok = tok.strip() - if not tok or tok in ("|", "▁") or (tok.startswith("<") and tok.endswith(">")): - continue - if phones and all(is_modifier(ch) for ch in tok): # ː or ˤ written as its own token - phones[-1] += tok - elif phones and phones[-1].endswith("͡"): # tie bar: t͡ ʃ - phones[-1] += tok - else: - phones.append(tok) - out: list[str] = [] - for p in phones: - c = canon(p) - if not c: - continue - if out and (out[-1], c) in AFFRICATES: - out[-1] = AFFRICATES[(out[-1], c)] - elif not out or out[-1] != c: - out.append(c) - return out - - -def reference(ipa: str) -> list[str]: - return to_phones([t for t in ipa.split() if t != "|"]) - - -def align(ref: list[str], hyp: list[str]) -> tuple[int, list[tuple[str | None, str | None]]]: - """Levenshtein distance and the aligned pairs (ref, hyp); None marks an insertion or a deletion.""" - n, m = len(ref), len(hyp) - d = np.zeros((n + 1, m + 1), dtype=np.int32) - d[:, 0] = np.arange(n + 1) - d[0, :] = np.arange(m + 1) - for i in range(1, n + 1): - for j in range(1, m + 1): - d[i, j] = min(d[i - 1, j - 1] + (ref[i - 1] != hyp[j - 1]), d[i - 1, j] + 1, d[i, j - 1] + 1) - pairs: list[tuple[str | None, str | None]] = [] - i, j = n, m - while i or j: - if i and j and d[i, j] == d[i - 1, j - 1] + (ref[i - 1] != hyp[j - 1]): - pairs.append((ref[i - 1], hyp[j - 1])) - i, j = i - 1, j - 1 - elif i and d[i, j] == d[i - 1, j] + 1: - pairs.append((ref[i - 1], None)) - i -= 1 - else: - pairs.append((None, hyp[j - 1])) - j -= 1 - return int(d[n, m]), pairs[::-1] - - -def consonants(phones: list[str]) -> list[str]: - return [p for p in phones if p.rstrip("ː") not in VOWELS] - - -# ---------- models ---------- - -def use_extra_deps() -> None: - from app.powsm import use_extra_deps as use - try: - use() - except RuntimeError as e: - sys.exit(str(e)) - - -class HFModel: - """The app's own emitter (wav2vec2 phoneme CTC).""" - - def __init__(self, model_id: str): - from app.scorer import HFEmitter - self.em = HFEmitter(model_id) - self.i2t = {i: t for t, i in self.em.token_to_id.items()} - self.blank = self.em.token_to_id[""] - self.inventory = {canon(t) for t in self.em.token_to_id if not t.startswith("<")} - - def __call__(self, wav: np.ndarray) -> tuple[list[str], int]: - logp = self.em(wav) - return [self.i2t[i] for i in greedy_decode(logp, self.blank)], logp.shape[0] - - def close(self) -> None: - del self.em - - -class Zipa: - """ZIPA (Zipformer, CR-CTC) from its official ONNX export, on CPU: onnxruntime's GPU build needs CUDA 12.""" - - def __init__(self, repo: str = "anyspeech/zipa-large-crctc-500k"): - use_extra_deps() - import onnxruntime as ort - from huggingface_hub import hf_hub_download - from lhotse.features.kaldi.extractors import Fbank, FbankConfig - self.sess = ort.InferenceSession(hf_hub_download(repo, "model.onnx"), providers=["CPUExecutionProvider"]) - self.fbank = Fbank(FbankConfig(num_filters=80, dither=0.0, snip_edges=False)) # as ZIPA's inference/utils.py - self.tokens: dict[int, str] = {} - for line in open(hf_hub_download(repo, "tokens.txt"), encoding="utf-8"): - tok, idx = line.rstrip("\n").rsplit(" ", 1) - self.tokens[int(idx)] = tok - self.inventory = None # it writes phones letter by letter, so any combination is possible - - def __call__(self, wav: np.ndarray) -> tuple[list[str], int]: - import torch - feats = self.fbank.extract_batch([torch.from_numpy(wav.copy())], sampling_rate=SAMPLE_RATE)[0] - feats = feats.unsqueeze(0).numpy() - logp, lens = self.sess.run(None, {"x": feats, "x_lens": np.array([feats.shape[1]], dtype=np.int64)}) - logp = logp[0][: int(lens[0])] - return [self.tokens[i] for i in greedy_decode(logp, 0)], logp.shape[0] - - def close(self) -> None: - del self.sess - - -class Powsm: - """POWSM-CTC (ESPnet), greedy CTC with the phone-recognition task token and a language token.""" - - SECONDS = 20 # trained on 20 s inputs: the model card says pad to 20 s (None or 0: no padding) - - def __init__(self, lang: str = "", checkpoint: str | None = None, pad: float | None = None): - use_extra_deps() - self.pad = self.SECONDS if pad is None else pad - from app.powsm import load_powsm - self.s2t = load_powsm(lang, checkpoint) - self.enc_frames = 0 - self.s2t.s2t_model.encoder.register_forward_hook(self._count) - toks = self.s2t.s2t_model.token_list - self.inventory = {canon(t[1:-1]) for t in toks if len(t) > 2 and t[0] == "/" and t[-1] == "/"} - - def _count(self, module, inputs, output): - enc = output[0][0] if isinstance(output[0], tuple) else output[0] - self.enc_frames = enc.shape[1] - 2 # minus the language and task positions - - def __call__(self, wav: np.ndarray) -> tuple[list[str], int]: - n = int((self.pad or 0) * SAMPLE_RATE) - x = np.pad(wav, (0, max(0, n - len(wav)))).astype(np.float32) # a longer recording runs as it is - tokens = self.s2t(x)[0][1] - phones = [t[1:-1] for t in tokens if len(t) > 2 and t[0] == "/" and t[-1] == "/"] - return phones, round(self.enc_frames * len(wav) / len(x)) # frames that cover the speech - - def close(self) -> None: - del self.s2t - - -MODELS = { # name -> (label, how it would plug into the app, loader) - "base": ("xlsr-53-espeak (current base)", "yes", lambda: HFModel("facebook/wav2vec2-xlsr-53-espeak-cv-ft")), - "ft": ("ihki-ft-minerva (current fine-tune)", "yes", - lambda: HFModel(str(ROOT / "checkpoints" / "ihki-ft-minerva"))), - "zipa": ("ZIPA large CR-CTC", "new loader (ONNX)", lambda: Zipa()), - "powsm": ("POWSM-CTC, no language", "new loader (ESPnet)", lambda: Powsm("", None, POWSM_PAD)), - "powsm-ara": ("POWSM-CTC, Arabic prompt", "new loader (ESPnet)", lambda: Powsm("", None, POWSM_PAD)), - "powsm-ft": ("POWSM-CTC fine-tuned (Arabic prompt)", "new loader (ESPnet)", - lambda: Powsm("", str(ROOT / "checkpoints" / "powsm-ft-minerva" / "powsm_ft.pt"), POWSM_PAD)), - "powsm-ft-short": ("POWSM-CTC fine-tuned on short padding", "new loader (ESPnet)", - lambda: Powsm("", str(ROOT / "checkpoints" / "powsm-ft-shortpad" / "powsm_ft.pt"), - POWSM_PAD)), -} -POWSM_PAD: float | None = None # --powsm-pad: seconds the POWSM models pad to (None: their 20 s) - - -# ---------- running and reporting ---------- - -def evaluate(name: str, rows: list[dict], clips: list[np.ndarray]) -> dict: - label, fits, load = MODELS[name] - print(f"[{name}] loading {label}", file=sys.stderr) - model = load() - model(clips[0]) # warm-up, not timed - errs = c_errs = total = c_total = 0 - frames = seconds = elapsed = 0.0 - n = collections.Counter() - heard: dict[str, collections.Counter] = collections.defaultdict(collections.Counter) - length_ok = length_n = 0 - lines = [] - for k, (row, wav) in enumerate(zip(rows, clips)): - t = time.perf_counter() - tokens, n_frames = model(wav) - elapsed += time.perf_counter() - t - frames += n_frames - seconds += len(wav) / SAMPLE_RATE - ref, hyp = reference(row["ipa"]), to_phones(tokens) - e, pairs = align(ref, hyp) - errs, total = errs + e, total + len(ref) - ce, _ = align(consonants(ref), consonants(hyp)) - c_errs, c_total = c_errs + ce, c_total + len(consonants(ref)) - for r, h in pairs: - if r is None: - continue - n[r] += 1 - heard[r][h if h is not None else "∅"] += 1 - if h is not None and r.rstrip("ː") in VOWELS and h.rstrip("ː") in VOWELS: - length_n += 1 - length_ok += r.endswith("ː") == h.endswith("ː") - lines.append({"item": row["item"], "ref": " ".join(ref), "hyp": " ".join(hyp), "errors": e}) - if (k + 1) % 50 == 0: - print(f"[{name}] {k + 1}/{len(rows)} PER so far {errs / total:.3f}", file=sys.stderr) - inventory = getattr(model, "inventory", None) - model.close() - try: - import torch - torch.cuda.empty_cache() - except ImportError: - pass - sounds = {} - for s in sorted(n, key=lambda s: -n[s]): - sounds[s] = {"n": n[s], "recall": heard[s][s] / n[s], - "heard_as": dict(heard[s].most_common(6)), - "in_vocab": None if inventory is None else s in inventory} - return {"label": label, "fits_app": fits, "clips": len(rows), "per": errs / total, - "consonant_per": c_errs / c_total, "vowel_length_accuracy": length_ok / max(1, length_n), - "frames_per_second": frames / seconds, "seconds_per_clip": elapsed / len(rows), - "sounds": sounds, "lines": lines} - - -def ordered(results: dict) -> list[str]: - """Saved runs in MODELS order, runs with another padding (powsm-ft@8s) after their model.""" - out = [] - for m in MODELS: - out += [k for k in results if k == m or k.startswith(m + "@")] - return out + [k for k in results if k not in out] - - -def pct(x: float | None) -> str: - return "—" if x is None else f"{100 * x:.0f}%" - - -def table(results: dict) -> str: - head = ["Model", "PER", "Consonant PER", *KEY_SOUNDS, "Long vowels", "Vowel length", "Frames/s", "s/clip", - "Fits app?"] - out = ["| " + " | ".join(head) + " |", "|" + "---|" * len(head)] - for name in ordered(results): - r = results[name] - cells = [r["label"], pct(r["per"]), pct(r["consonant_per"])] - for s in KEY_SOUNDS: - info = r["sounds"].get(s) - if info is None: - cells.append("—") - elif info["in_vocab"] is False: - cells.append("no symbol") - else: - cells.append(pct(info["recall"])) - longs = [v for s, v in r["sounds"].items() if s.endswith("ː")] - hit = sum(v["recall"] * v["n"] for v in longs) - cells += [pct(hit / max(1, sum(v["n"] for v in longs))), pct(r["vowel_length_accuracy"]), - f"{r['frames_per_second']:.0f}", f"{r['seconds_per_clip']:.2f}", r["fits_app"]] - out.append("| " + " | ".join(cells) + " |") - return "\n".join(out) - - -def confusions(results: dict, sounds: list[str]) -> str: - out = [] - for name in ordered(results): - r = results[name] - parts = [] - for s in sounds: - info = r["sounds"].get(s) - if not info: - continue - wrong = [f"{h} {c}" for h, c in info["heard_as"].items() if h != s][:3] - parts.append(f"{s} ({info['n']}): " + (", ".join(wrong) or "—")) - out.append(f"- **{r['label']}**: " + "; ".join(parts)) - return "\n".join(out) - - -def main() -> None: - ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) - local = {"powsm-ft": "powsm-ft-minerva", "powsm-ft-short": "powsm-ft-shortpad"} - default = [m for m in MODELS if m not in local or (ROOT / "checkpoints" / local[m] / "powsm_ft.pt").exists()] - ap.add_argument("--models", default=",".join(default), help=f"comma-separated, from: {', '.join(MODELS)}") - ap.add_argument("--split", default="dev", choices=["dev", "test"]) - ap.add_argument("--limit", type=int, help="only the first N clips (a quick check)") - ap.add_argument("--report", action="store_true", help="print the table from the saved JSON, run nothing") - ap.add_argument("--powsm-pad", type=float, help="seconds the POWSM models pad to (default 20; 0: none). " - "A run with it is saved under the model's name plus @") - args = ap.parse_args() - global POWSM_PAD - POWSM_PAD = args.powsm_pad - - out_path = DATA / "bench" / f"zero-shot-{args.split}.json" - results = json.loads(out_path.read_text(encoding="utf-8")) if out_path.exists() else {} - if not args.report: - names = [m.strip() for m in args.models.split(",") if m.strip()] - unknown = [m for m in names if m not in MODELS] - if unknown: - sys.exit(f"unknown model(s): {', '.join(unknown)}") - with open(DATA / f"{args.split}.csv", encoding="utf-8") as f: - rows = list(csv.DictReader(f))[: args.limit] - clips = [decode_audio((DATA / r["audio"]).read_bytes()) for r in rows] - for name in names: - key = f"{name}@{args.powsm_pad:g}s" if args.powsm_pad is not None and name.startswith("powsm") else name - results[key] = evaluate(name, rows, clips) - results[key]["label"] += f" (pad {args.powsm_pad:g} s)" if key != name else "" - if not args.limit: # a partial run doesn't overwrite the saved numbers - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(results, ensure_ascii=False, indent=1), encoding="utf-8") - print(table(results)) - print() - print("What each key sound was heard as instead (count):") - print(confusions(results, ["ħ", "ʕ", "tˤ", "sˤ", "aː", "iː"])) - - -if __name__ == "__main__": - main() diff --git a/static/assets/arabic-calligraphy-path.webp b/static/assets/arabic-calligraphy-path.webp deleted file mode 100644 index 56bfb529f983cb82a47f61d8f78738525f3eac52..0000000000000000000000000000000000000000 --- a/static/assets/arabic-calligraphy-path.webp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5071d6eaa0b5152f1f427978713a2e0e875bbb4413cee5dcb8e725eb8b2bdf36 -size 189896 diff --git a/static/assets/arabic_verbs.json b/static/assets/arabic_verbs.json deleted file mode 100644 index c4bab97f1bcf9a7551278ddbd35a7400acfba634..0000000000000000000000000000000000000000 --- a/static/assets/arabic_verbs.json +++ /dev/null @@ -1,451 +0,0 @@ -{ - "alif": { - "source": { - "title": "פעלים בערבית מדוברת המתחילים באליף", - "url": "https://www.youtube.com/watch?v=c95B23kxOws", - "dialect": "Palestinian spoken Arabic", - "notes": "One entry per unique verb, in video order. The video repeats أبطل and أخذ. Phonemes use the scorer alphabet and are space-separated." - }, - "verbs": [ - {"hebrew": "בא", "timestamp": "01:03", "past": {"arabic": "اجا", "phonemes": "ʔ a dʒ a"}, "future": {"arabic": "يجي", "phonemes": "j i dʒ i"}}, - {"hebrew": "לקח", "timestamp": "01:12", "past": {"arabic": "اخد", "phonemes": "ʔ a x a d"}, "future": {"arabic": "ياخد", "phonemes": "j oː x i d", "accepted_phonemes": ["j aː x u d", "j oː x u d"]}}, - {"hebrew": "חייך", "timestamp": "01:21", "past": {"arabic": "ابتسم", "phonemes": "ʔ i b t a s a m"}, "future": {"arabic": "يبتسم", "phonemes": "j i b t a s i m"}}, - {"hebrew": "דחה", "timestamp": "01:29", "past": {"arabic": "أجّل", "phonemes": "ʔ a dʒ dʒ a l"}, "future": {"arabic": "يأجّل", "phonemes": "j ʔ a dʒ dʒ i l"}}, - {"hebrew": "השכיר", "timestamp": "01:39", "past": {"arabic": "أجّر", "phonemes": "ʔ a dʒ dʒ a r"}, "future": {"arabic": "يأجّر", "phonemes": "j ʔ a dʒ dʒ i r"}}, - {"hebrew": "הדהים", "timestamp": "01:55", "past": {"arabic": "أدهش", "phonemes": "ʔ a d h a ʃ"}, "future": {"arabic": "يدهش", "phonemes": "j i d h a ʃ"}}, - {"hebrew": "ירד", "timestamp": "02:03", "past": {"arabic": "انخفض", "phonemes": "ʔ i n x a f a dˤ"}, "future": {"arabic": "ينخفض", "phonemes": "j i n x a f i dˤ"}}, - {"hebrew": "הדגיש", "timestamp": "02:13", "past": {"arabic": "أبرز", "phonemes": "ʔ a b r a z"}, "future": {"arabic": "يبرز", "phonemes": "j i b r u z"}}, - {"hebrew": "נטרל, פירק", "timestamp": "02:21", "past": {"arabic": "أبطل", "phonemes": "ʔ a b tˤ a l"}, "future": {"arabic": "يبطل", "phonemes": "j i b tˤ a l"}}, - {"hebrew": "אירח לחברה, ישב עם אורח כדי שלא יישאר לבד", "timestamp": "02:30", "past": {"arabic": "آنس", "phonemes": "ʔ aː n a s"}, "future": {"arabic": "يآنس", "phonemes": "j ʔ aː n i s"}}, - {"hebrew": "התעניין, החשיב", "timestamp": "02:46", "past": {"arabic": "اهتمّ", "phonemes": "ʔ i h t a m m"}, "future": {"arabic": "يهتمّ", "phonemes": "j i h t a m m"}}, - {"hebrew": "היה חלוק בדעתו", "timestamp": "02:52", "past": {"arabic": "اختلف", "phonemes": "ʔ i x t a l a f"}, "future": {"arabic": "يختلف", "phonemes": "j i x t a l i f"}}, - {"hebrew": "בחר", "timestamp": "03:19", "past": {"arabic": "اختار", "phonemes": "ʔ i x t aː r"}, "future": {"arabic": "يختار", "phonemes": "j i x t aː r"}}, - {"hebrew": "המציא", "timestamp": "03:28", "past": {"arabic": "اخترع", "phonemes": "ʔ i x t a r a ʕ"}, "future": {"arabic": "يخترع", "phonemes": "j i x t a r i ʕ"}}, - {"hebrew": "גילה", "timestamp": "03:39", "past": {"arabic": "اكتشف", "phonemes": "ʔ i k t a ʃ a f"}, "future": {"arabic": "يكتشف", "phonemes": "j i k t a ʃ i f"}}, - {"hebrew": "התחזה", "timestamp": "03:51", "past": {"arabic": "انتحل", "phonemes": "ʔ i n t a ħ a l"}, "future": {"arabic": "ينتحل", "phonemes": "j i n t a ħ i l"}}, - {"hebrew": "נפצע", "timestamp": "04:00", "past": {"arabic": "انجرح", "phonemes": "ʔ i n dʒ a r a ħ"}, "future": {"arabic": "ينجرح", "phonemes": "j i n dʒ a r i ħ"}}, - {"hebrew": "נדרס", "timestamp": "04:09", "past": {"arabic": "اندهس", "phonemes": "ʔ i n d a h a s"}, "future": {"arabic": "يندهس", "phonemes": "j i n d a h i s"}}, - {"hebrew": "התהפך", "timestamp": "04:27", "past": {"arabic": "انقلب", "phonemes": "ʔ i n Q a l a b"}, "future": {"arabic": "ينقلب", "phonemes": "j i n Q a l i b"}}, - {"hebrew": "עבר ממקום למקום", "timestamp": "04:35", "past": {"arabic": "انتقل", "phonemes": "ʔ i n t a Q a l"}, "future": {"arabic": "ينتقل", "phonemes": "j i n t a Q i l"}}, - {"hebrew": "הבשיל", "timestamp": "04:45", "past": {"arabic": "استوى", "phonemes": "ʔ i s t a w a"}, "future": {"arabic": "يستوي", "phonemes": "j i s t a w i"}}, - {"hebrew": "ייבא", "timestamp": "04:54", "past": {"arabic": "استورد", "phonemes": "ʔ i s t a w r a d"}, "future": {"arabic": "يستورد", "phonemes": "j i s t a w r i d"}}, - {"hebrew": "חשב", "timestamp": "05:04", "past": {"arabic": "افتكر", "phonemes": "ʔ i f t a k a r"}, "future": {"arabic": "يفتكر", "phonemes": "j i f t a k i r"}}, - {"hebrew": "עבד", "timestamp": "05:14", "past": {"arabic": "اشتغل", "phonemes": "ʔ i ʃ t a ɣ a l", "accepted_phonemes": ["ʔ a ʃ t ɣ i l", "ʔ a ʃ t i ɣ i l"]}, "future": {"arabic": "يشتغل", "phonemes": "j i ʃ t a ɣ i l"}}, - {"hebrew": "ניצת, נדלק", "timestamp": "05:23", "past": {"arabic": "اشتعل", "phonemes": "ʔ i ʃ t a ʕ a l"}, "future": {"arabic": "يشتعل", "phonemes": "j i ʃ t a ʕ i l"}}, - {"hebrew": "השתתף", "timestamp": "05:33", "past": {"arabic": "اشترك", "phonemes": "ʔ i ʃ t a r a k"}, "future": {"arabic": "يشترك", "phonemes": "j i ʃ t a r i k"}}, - {"hebrew": "אימץ פתרון או קיבל החלטה", "timestamp": "05:42", "past": {"arabic": "اتّخذ", "phonemes": "ʔ i t t a x a d"}, "future": {"arabic": "يتّخذ", "phonemes": "j i t t a x i d"}}, - {"hebrew": "נשען על חפץ", "timestamp": "05:54", "past": {"arabic": "اتّكأ", "phonemes": "ʔ i t t a k a ʔ"}, "future": {"arabic": "يتّكئ", "phonemes": "j i t t a k i ʔ"}}, - {"hebrew": "הסכים", "timestamp": "06:07", "past": {"arabic": "اتّفق", "phonemes": "ʔ i t t a f a Q"}, "future": {"arabic": "يتّفق", "phonemes": "j i t t a f i Q"}}, - {"hebrew": "התקשר", "timestamp": "06:15", "past": {"arabic": "اتّصل", "phonemes": "ʔ i t t a sˤ a l"}, "future": {"arabic": "يتّصل", "phonemes": "j i t t a sˤ i l"}}, - {"hebrew": "התעייף", "timestamp": "06:24", "past": {"arabic": "اتعب", "phonemes": "ʔ i t ʕ a b"}, "future": {"arabic": "يتعب", "phonemes": "j i t ʕ a b"}}, - {"hebrew": "עלה", "timestamp": "06:34", "past": {"arabic": "ارتفع", "phonemes": "ʔ i r t a f a ʕ"}, "future": {"arabic": "يرتفع", "phonemes": "j i r t a f i ʕ"}}, - {"hebrew": "שיתף", "timestamp": "06:43", "past": {"arabic": "أشرك", "phonemes": "ʔ a ʃ r a k"}, "future": {"arabic": "يشرك", "phonemes": "j i ʃ r i k"}}, - {"hebrew": "אלתר", "timestamp": "06:51", "past": {"arabic": "ارتجل", "phonemes": "ʔ i r t a dʒ a l"}, "future": {"arabic": "يرتجل", "phonemes": "j i r t a dʒ i l"}}, - {"hebrew": "הרחיק", "timestamp": "07:08", "past": {"arabic": "أبعد", "phonemes": "ʔ a b ʕ a d"}, "future": {"arabic": "يبعد", "phonemes": "j i b ʕ i d"}}, - {"hebrew": "הזניח", "timestamp": "07:19", "past": {"arabic": "أهمل", "phonemes": "ʔ a h m a l"}, "future": {"arabic": "يهمل", "phonemes": "j i h m i l"}}, - {"hebrew": "איחר", "timestamp": "07:53", "past": {"arabic": "اتأخّر", "phonemes": "ʔ i t ʔ a x x a r"}, "future": {"arabic": "يتأخّر", "phonemes": "j i t ʔ a x x a r"}}, - {"hebrew": "תמך ב־, אישר", "timestamp": "08:02", "past": {"arabic": "أكّد", "phonemes": "ʔ a k k a d"}, "future": {"arabic": "يأكّد", "phonemes": "j ʔ a k k i d"}}, - {"hebrew": "אכל", "timestamp": "08:08", "past": {"arabic": "اكل", "phonemes": "ʔ a k a l"}, "future": {"arabic": "ياكل", "phonemes": "j oː k u l", "accepted_phonemes": ["j oː k i l", "j aː k u l"]}}, - {"hebrew": "ציווה", "timestamp": "08:36", "past": {"arabic": "أمر", "phonemes": "ʔ a m a r"}, "future": {"arabic": "يأمر", "phonemes": "j ʔ a m u r"}}, - {"hebrew": "נתן שהות", "timestamp": "08:40", "past": {"arabic": "أمهل", "phonemes": "ʔ a m h a l"}, "future": {"arabic": "يمهل", "phonemes": "j i m h i l"}}, - {"hebrew": "סיים", "timestamp": "08:54", "past": {"arabic": "أنهى", "phonemes": "ʔ a n h a"}, "future": {"arabic": "ينهي", "phonemes": "j i n h i"}}, - {"hebrew": "ייסד", "timestamp": "09:01", "past": {"arabic": "أسّس", "phonemes": "ʔ a s s a s"}, "future": {"arabic": "يأسّس", "phonemes": "j ʔ a s s i s"}}, - {"hebrew": "שיכר", "timestamp": "09:10", "past": {"arabic": "أسكر", "phonemes": "ʔ a s k a r"}, "future": {"arabic": "يسكر", "phonemes": "j i s k i r"}}, - {"hebrew": "מיהר", "timestamp": "09:24", "past": {"arabic": "أسرع", "phonemes": "ʔ a s r a ʕ"}, "future": {"arabic": "يسرع", "phonemes": "j i s r a ʕ"}}, - {"hebrew": "נתן", "timestamp": "09:33", "past": {"arabic": "أعطى", "phonemes": "ʔ a ʕ tˤ a"}, "future": {"arabic": "يعطي", "phonemes": "j i ʕ tˤ i"}}, - {"hebrew": "הדביק במחלה", "timestamp": "09:40", "past": {"arabic": "أعدى", "phonemes": "ʔ a ʕ d a"}, "future": {"arabic": "يعدي", "phonemes": "j i ʕ d i"}}, - {"hebrew": "הכריז", "timestamp": "09:48", "past": {"arabic": "أعلن", "phonemes": "ʔ a ʕ l a n"}, "future": {"arabic": "يعلن", "phonemes": "j i ʕ l i n"}}, - {"hebrew": "פטר", "timestamp": "09:56", "past": {"arabic": "أعفى", "phonemes": "ʔ a ʕ f a"}, "future": {"arabic": "يعفي", "phonemes": "j i ʕ f i"}}, - {"hebrew": "עיוור, סינוור", "timestamp": "10:05", "past": {"arabic": "أعمى", "phonemes": "ʔ a ʕ m a"}, "future": {"arabic": "يعمي", "phonemes": "j i ʕ m i"}}, - {"hebrew": "השביע את רצונו של", "timestamp": "10:12", "past": {"arabic": "أرضى", "phonemes": "ʔ a r dˤ a"}, "future": {"arabic": "يرضي", "phonemes": "j i r dˤ i"}}, - {"hebrew": "סימן, אותת", "timestamp": "10:23", "past": {"arabic": "أشّر", "phonemes": "ʔ a ʃ ʃ a r"}, "future": {"arabic": "يأشّر", "phonemes": "j ʔ a ʃ ʃ i r"}}, - {"hebrew": "השפיע", "timestamp": "10:32", "past": {"arabic": "أثّر", "phonemes": "ʔ a TH TH a r"}, "future": {"arabic": "يأثّر", "phonemes": "j ʔ a TH TH i r"}}, - {"hebrew": "השלים בין, תיקן", "timestamp": "10:41", "past": {"arabic": "أصلح", "phonemes": "ʔ a sˤ l a ħ"}, "future": {"arabic": "يصلح", "phonemes": "j i sˤ l i ħ"}} - ] - }, - "ba": { - "source": { - "title": "פעלים שימושיים בערבית מדוברת — אות בא", - "url": "https://www.youtube.com/watch?v=w19b7mJ-j4U", - "dialect": "Palestinian spoken Arabic", - "notes": "One entry per verb, in video order. Phonemes use the scorer alphabet and are space-separated." - }, - "verbs": [ - {"hebrew": "התחיל", "timestamp": "01:39", "past": {"arabic": "بدأ", "phonemes": "b a d a ʔ"}, "future": {"arabic": "يبدأ", "phonemes": "j i b d a ʔ"}}, - {"hebrew": "בכה", "timestamp": "01:48", "past": {"arabic": "بكى", "phonemes": "b a k a"}, "future": {"arabic": "يبكي", "phonemes": "j i b k i"}}, - {"hebrew": "נשאר", "timestamp": "01:57", "past": {"arabic": "بقي", "phonemes": "b a Q i"}, "future": {"arabic": "يبقى", "phonemes": "j i b Q a"}}, - {"hebrew": "מכר", "timestamp": "02:06", "past": {"arabic": "باع", "phonemes": "b aː ʕ"}, "future": {"arabic": "يبيع", "phonemes": "j i b iː ʕ"}}, - {"hebrew": "אירח מישהו כל הלילה", "timestamp": "02:17", "past": {"arabic": "بات", "phonemes": "b aː t"}, "future": {"arabic": "يبيت", "phonemes": "j i b iː t"}}, - {"hebrew": "נזף, דיבר בצורה קשה", "timestamp": "02:26", "past": {"arabic": "بهدل", "phonemes": "b a h d a l"}, "future": {"arabic": "يبهدل", "phonemes": "j i b a h d i l"}}, - {"hebrew": "החליף, שינה", "timestamp": "02:36", "past": {"arabic": "بدّل", "phonemes": "b a d d a l"}, "future": {"arabic": "يبدّل", "phonemes": "j i b a d d i l"}}, - {"hebrew": "חפר באדמה", "timestamp": "02:46", "past": {"arabic": "بحش", "phonemes": "b a ħ a ʃ"}, "future": {"arabic": "يبحش", "phonemes": "j i b ħ a ʃ"}}, - {"hebrew": "סיווג", "timestamp": "02:56", "past": {"arabic": "بوّب", "phonemes": "b a w w a b"}, "future": {"arabic": "يبوّب", "phonemes": "j i b a w w i b"}}, - {"hebrew": "הפסיק, חזר בו", "timestamp": "03:06", "past": {"arabic": "بطّل", "phonemes": "b a tˤ tˤ a l"}, "future": {"arabic": "يبطّل", "phonemes": "j i b a tˤ tˤ i l"}}, - {"hebrew": "הופיע, נמצא", "timestamp": "03:15", "past": {"arabic": "بيّن", "phonemes": "b a j j a n"}, "future": {"arabic": "يبيّن", "phonemes": "j i b a j j i n"}}, - {"hebrew": "הודיע", "timestamp": "03:25", "past": {"arabic": "بلّغ", "phonemes": "b a l l a ɣ"}, "future": {"arabic": "يبلّغ", "phonemes": "j i b a l l i ɣ"}}, - {"hebrew": "בלע", "timestamp": "03:38", "past": {"arabic": "بلع", "phonemes": "b a l a ʕ"}, "future": {"arabic": "يبلع", "phonemes": "j i b l a ʕ"}}, - {"hebrew": "התחיל", "timestamp": "03:44", "past": {"arabic": "بلّش", "phonemes": "b a l l a ʃ"}, "future": {"arabic": "يبلّش", "phonemes": "j i b a l l i ʃ"}}, - {"hebrew": "בכה בקול, יילל", "timestamp": "04:05", "past": {"arabic": "بعق", "phonemes": "b a ʕ a Q"}, "future": {"arabic": "يبعق", "phonemes": "j i b ʕ a Q"}}, - {"hebrew": "פיזר וגם בזבז", "timestamp": "04:14", "past": {"arabic": "بعتر", "phonemes": "b a ʕ t a r"}, "future": {"arabic": "يبعتر", "phonemes": "j i b a ʕ t i r"}}, - {"hebrew": "בנה", "timestamp": "04:23", "past": {"arabic": "بنى", "phonemes": "b a n a"}, "future": {"arabic": "يبني", "phonemes": "j i b n i"}}, - {"hebrew": "הצדיק", "timestamp": "04:30", "past": {"arabic": "برّر", "phonemes": "b a r r a r"}, "future": {"arabic": "يبرّر", "phonemes": "j i b a r r i r"}}, - {"hebrew": "כיער", "timestamp": "04:37", "past": {"arabic": "بشّع", "phonemes": "b a ʃ ʃ a ʕ"}, "future": {"arabic": "يبشّع", "phonemes": "j i b a ʃ ʃ i ʕ"}}, - {"hebrew": "תכנת", "timestamp": "04:47", "past": {"arabic": "برمج", "phonemes": "b a r m a dʒ"}, "future": {"arabic": "يبرمج", "phonemes": "j i b a r m i dʒ"}}, - {"hebrew": "שיחד", "timestamp": "05:11", "past": {"arabic": "برطل", "phonemes": "b a r tˤ a l"}, "future": {"arabic": "يبرطل", "phonemes": "j i b a r tˤ i l"}} - ] - }, - "jim": { - "source": {"title": "פעלים שימושיים בערבית מדוברת — אות ג'ים", "url": "https://www.youtube.com/watch?v=A90bTU8w3BY", "dialect": "Palestinian spoken Arabic"}, - "verbs": [ - {"hebrew":"הביא","past":{"arabic":"جاب","phonemes":"J aː b"},"future":{"arabic":"يجيب","phonemes":"j i J iː b"}}, - {"hebrew":"ענה","past":{"arabic":"جاوب","phonemes":"J aː w a b"},"future":{"arabic":"يجاوب","phonemes":"j i J aː w i b"}}, - {"hebrew":"גרר, סחב","past":{"arabic":"جرجر","phonemes":"J a r J a r"},"future":{"arabic":"يجرجر","phonemes":"j i J a r J i r"}}, - {"hebrew":"ניסה","past":{"arabic":"جرّب","phonemes":"J a r r a b"},"future":{"arabic":"يجرّب","phonemes":"j i J a r r i b"}}, - {"hebrew":"הקפיא","past":{"arabic":"جمّد","phonemes":"J a m m a d"},"future":{"arabic":"يجمّد","phonemes":"j i J a m m i d"}}, - {"hebrew":"ייבש","past":{"arabic":"جفّف","phonemes":"J a f f a f"},"future":{"arabic":"يجفّف","phonemes":"j i J a f f i f"}}, - {"hebrew":"חידש","past":{"arabic":"جدّد","phonemes":"J a d d a d"},"future":{"arabic":"يجدّد","phonemes":"j i J a d d i d"}}, - {"hebrew":"רץ, קרה, זרם","past":{"arabic":"جرى","phonemes":"J a r a"},"future":{"arabic":"يجري","phonemes":"j i J r i"}}, - {"hebrew":"גמל, העניש","past":{"arabic":"جازى","phonemes":"J aː z a"},"future":{"arabic":"يجازي","phonemes":"j i J aː z i"}}, - {"hebrew":"גרר, סחב","past":{"arabic":"جرّ","phonemes":"J a r r"},"future":{"arabic":"يجرّ","phonemes":"j i J u r r"}} - ] - }, - "dal": { - "source": {"title":"פעלים שימושיים בערבית מדוברת — אות דאל","url":"https://www.youtube.com/watch?v=Wu2oeHyKujo","dialect":"Palestinian spoken Arabic"}, - "verbs": [ - {"hebrew":"אבד","past":{"arabic":"ضاع","phonemes":"dˤ aː ʕ"},"future":{"arabic":"يضيع","phonemes":"j i dˤ iː ʕ"}}, - {"hebrew":"סידר, הסדיר","past":{"arabic":"دبّر","phonemes":"d a b b a r"},"future":{"arabic":"يدبّر","phonemes":"j i d a b b i r"}}, - {"hebrew":"צבע","past":{"arabic":"دهن","phonemes":"d a h a n"},"future":{"arabic":"يدهن","phonemes":"j i d h a n"}}, - {"hebrew":"דרס","past":{"arabic":"دهس","phonemes":"d a h a s"},"future":{"arabic":"يدهس","phonemes":"j i d h a s"}}, - {"hebrew":"חיפש","past":{"arabic":"دوّر على","phonemes":"d a w w a r ʕ a l a"},"future":{"arabic":"يدوّر على","phonemes":"j i d a w w i r ʕ a l a"}}, - {"hebrew":"עישן","past":{"arabic":"دخّن","phonemes":"d a x x a n"},"future":{"arabic":"يدخّن","phonemes":"j i d a x x i n"}}, - {"hebrew":"הצביע על, הראה","past":{"arabic":"دلّ على","phonemes":"d a l l ʕ a l a"},"future":{"arabic":"يدلّ على","phonemes":"j i d u l l ʕ a l a"}}, - {"hebrew":"שילם, דחף","past":{"arabic":"دفع","phonemes":"d a f a ʕ"},"future":{"arabic":"يدفع","phonemes":"j i d f a ʕ"}}, - {"hebrew":"דפק","past":{"arabic":"دقّ","phonemes":"d a Q Q"},"future":{"arabic":"يدقّ","phonemes":"j i d u Q Q"}}, - {"hebrew":"עזב, נטש","past":{"arabic":"دشّر","phonemes":"d a ʃ ʃ a r"},"future":{"arabic":"يدشّر","phonemes":"j i d a ʃ ʃ i r"}}, - {"hebrew":"סובב, הפנה","past":{"arabic":"دار","phonemes":"d aː r"},"future":{"arabic":"يدير","phonemes":"j i d iː r"}} - ] - }, - "ha": { - "source": {"title":"פעלים שימושיים בערבית מדוברת — אות ה","url":"https://www.youtube.com/watch?v=le--lfAYjgE","dialect":"Palestinian spoken Arabic"}, - "verbs": [ - {"hebrew":"ברח","past":{"arabic":"هرب","phonemes":"h a r a b"},"future":{"arabic":"يهرب","phonemes":"j i h r a b"}}, - {"hebrew":"היגר","past":{"arabic":"هاجر","phonemes":"h aː J a r"},"future":{"arabic":"يهاجر","phonemes":"j i h aː J i r"}}, - {"hebrew":"הגלה, גרם להגר","past":{"arabic":"هجّر","phonemes":"h a J J a r"},"future":{"arabic":"يهجّر","phonemes":"j i h a J J i r"}}, - {"hebrew":"נפל, ירד","past":{"arabic":"هبط","phonemes":"h a b a tˤ"},"future":{"arabic":"يهبط","phonemes":"j i h b a tˤ"}}, - {"hebrew":"הוריד, החליש","past":{"arabic":"هبّط","phonemes":"h a b b a tˤ"},"future":{"arabic":"يهبّط","phonemes":"j i h a b b i tˤ"}}, - {"hebrew":"התכוון, שאף","past":{"arabic":"هدف","phonemes":"h a d a f"},"future":{"arabic":"يهدف","phonemes":"j i h d a f"}}, - {"hebrew":"בירך","past":{"arabic":"هنّى","phonemes":"h a n n a"},"future":{"arabic":"يهنّي","phonemes":"j i h a n n i"}}, - {"hebrew":"הריע, קרא","past":{"arabic":"هتف","phonemes":"h a t a f"},"future":{"arabic":"يهتف","phonemes":"j i h t i f"}} - ] - }, - "waw": { - "source": {"title":"פעלים שימושיים בערבית מדוברת — אות ואו","url":"https://www.youtube.com/watch?v=r5nOxemdyMI","dialect":"Palestinian spoken Arabic"}, - "verbs": [ - {"hebrew":"מצא","past":{"arabic":"وجد","phonemes":"w a J a d"},"future":{"arabic":"يجد","phonemes":"j i J i d"}}, - {"hebrew":"הביא למקום","past":{"arabic":"ودّى","phonemes":"w a d d a"},"future":{"arabic":"يودّي","phonemes":"j i w a d d i"}}, - {"hebrew":"נפרד","past":{"arabic":"ودّع","phonemes":"w a d d a ʕ"},"future":{"arabic":"يودّع","phonemes":"j i w a d d i ʕ"}}, - {"hebrew":"מינה, הסמיך","past":{"arabic":"وكّل","phonemes":"w a k k a l"},"future":{"arabic":"يوكّل","phonemes":"j i w a k k i l"}}, - {"hebrew":"עצר, עמד","past":{"arabic":"وقف","phonemes":"w a Q a f"},"future":{"arabic":"يوقف","phonemes":"j i w Q a f"}}, - {"hebrew":"ייבא","past":{"arabic":"ورّد","phonemes":"w a r r a d"},"future":{"arabic":"يورّد","phonemes":"j i w a r r i d"}}, - {"hebrew":"הנמיך","past":{"arabic":"وطّى","phonemes":"w a tˤ tˤ a"},"future":{"arabic":"يوطّي","phonemes":"j i w a tˤ tˤ i"}}, - {"hebrew":"חיבר, הוביל, הביא","past":{"arabic":"وصّل","phonemes":"w a sˤ sˤ a l"},"future":{"arabic":"يوصّل","phonemes":"j i w a sˤ sˤ i l"}} - ] - }, - "zay": { - "source": {"title":"פעלים שימושיים בערבית מדוברת — אות זאי","url":"https://www.youtube.com/watch?v=Qgal8RB1N00","dialect":"Palestinian spoken Arabic"}, - "verbs": [ - {"hebrew":"כעס, התרגז","past":{"arabic":"زعل","phonemes":"z a ʕ a l"},"future":{"arabic":"يزعل","phonemes":"j i z ʕ a l"}}, - {"hebrew":"זז, הסיר","past":{"arabic":"زاح","phonemes":"z aː ħ"},"future":{"arabic":"يزيح","phonemes":"j i z iː ħ"}}, - {"hebrew":"סיפק, צייד","past":{"arabic":"زوّد","phonemes":"z a w w a d"},"future":{"arabic":"يزوّد","phonemes":"j i z a w w i d"}}, - {"hebrew":"צפר","past":{"arabic":"زمّر","phonemes":"z a m m a r"},"future":{"arabic":"يزمّر","phonemes":"j i z a m m i r"}}, - {"hebrew":"הוסיף, גדל","past":{"arabic":"زاد","phonemes":"z aː d"},"future":{"arabic":"يزيد","phonemes":"j i z iː d"}}, - {"hebrew":"ביקר","past":{"arabic":"زار","phonemes":"z aː r"},"future":{"arabic":"يزور","phonemes":"j i z uː r"}}, - {"hebrew":"הזכיר, ציטט","past":{"arabic":"ذكر","phonemes":"DH a k a r"},"future":{"arabic":"يذكر","phonemes":"j i DH k u r"}}, - {"hebrew":"הזכיר למישהו","past":{"arabic":"ذكّر","phonemes":"DH a k k a r"},"future":{"arabic":"يذكّر","phonemes":"j i DH a k k i r"}}, - {"hebrew":"דחף","past":{"arabic":"زقّ","phonemes":"z a Q Q"},"future":{"arabic":"يزقّ","phonemes":"j i z u Q Q"}}, - {"hebrew":"זייף","past":{"arabic":"زيّف","phonemes":"z a j j a f"},"future":{"arabic":"يزيّف","phonemes":"j i z a j j i f"}}, - {"hebrew":"שחט","past":{"arabic":"ذبح","phonemes":"DH a b a ħ"},"future":{"arabic":"يذبح","phonemes":"j i DH b a ħ"}}, - {"hebrew":"נמאס לו","past":{"arabic":"زهق","phonemes":"z a h a Q"},"future":{"arabic":"يزهق","phonemes":"j i z h a Q"}}, - {"hebrew":"החליק, גרם להחליק","past":{"arabic":"زحلق","phonemes":"z a ħ l a Q"},"future":{"arabic":"يزحلق","phonemes":"j i z a ħ l i Q"}} - ] - }, - "kha": { - "source": {"title":"פעלים שימושיים בערבית מדוברת — אות ח'א","url":"https://www.youtube.com/watch?v=RJnw8Bjtvlg","dialect":"Palestinian spoken Arabic"}, - "verbs": [ - {"hebrew":"בגד","past":{"arabic":"خان","phonemes":"x aː n"},"future":{"arabic":"يخون","phonemes":"j i x uː n"}}, - {"hebrew":"נגמר, הסתיים","past":{"arabic":"خلص","phonemes":"x a l a sˤ"},"future":{"arabic":"يخلص","phonemes":"j i x l a sˤ"}}, - {"hebrew":"הפסיד","past":{"arabic":"خسر","phonemes":"x a s a r"},"future":{"arabic":"يخسر","phonemes":"j i x s a r"}}, - {"hebrew":"התקלקל","past":{"arabic":"خرب","phonemes":"x a r a b"},"future":{"arabic":"يخرب","phonemes":"j i x r a b"}}, - {"hebrew":"פחד","past":{"arabic":"خاف","phonemes":"x aː f"},"future":{"arabic":"يخاف","phonemes":"j i x aː f"}}, - {"hebrew":"הודיע, סיפר","past":{"arabic":"خبّر","phonemes":"x a b b a r"},"future":{"arabic":"يخبّر","phonemes":"j i x a b b i r"}}, - {"hebrew":"אחסן","past":{"arabic":"خزّن","phonemes":"x a z z a n"},"future":{"arabic":"يخزّن","phonemes":"j i x a z z i n"}}, - {"hebrew":"התבייש","past":{"arabic":"خجل","phonemes":"x a J a l"},"future":{"arabic":"يخجل","phonemes":"j i x J a l"}}, - {"hebrew":"השאיר, נטש","past":{"arabic":"خلّى","phonemes":"x a l l a"},"future":{"arabic":"يخلّي","phonemes":"j i x a l l i"}}, - {"hebrew":"השאיר אחריו, הוליד","past":{"arabic":"خلّف","phonemes":"x a l l a f"},"future":{"arabic":"يخلّف","phonemes":"j i x a l l i f"}}, - {"hebrew":"יצר, ברא","past":{"arabic":"خلق","phonemes":"x a l a Q"},"future":{"arabic":"يخلق","phonemes":"j i x l a Q"}}, - {"hebrew":"הפחית","past":{"arabic":"خفّف","phonemes":"x a f f a f"},"future":{"arabic":"يخفّف","phonemes":"j i x a f f i f"}}, - {"hebrew":"התערבב, התחבר","past":{"arabic":"خالط","phonemes":"x aː l a tˤ"},"future":{"arabic":"يخالط","phonemes":"j i x aː l i tˤ"}}, - {"hebrew":"אכזב","past":{"arabic":"خيّب","phonemes":"x a j j a b"},"future":{"arabic":"يخيّب","phonemes":"j i x a j j i b"}}, - {"hebrew":"חתם","past":{"arabic":"ختم","phonemes":"x a t a m"},"future":{"arabic":"يختم","phonemes":"j i x t i m"}}, - {"hebrew":"קלקל","past":{"arabic":"خرّب","phonemes":"x a r r a b"},"future":{"arabic":"يخرّب","phonemes":"j i x a r r i b"}}, - {"hebrew":"יצא","past":{"arabic":"خرج","phonemes":"x a r a J"},"future":{"arabic":"يخرج","phonemes":"j i x r a J"}}, - {"hebrew":"נשא נאום","past":{"arabic":"خطب","phonemes":"x a tˤ a b"},"future":{"arabic":"يخطب","phonemes":"j i x tˤ a b"}} - ] - }, - "ta_emphatic": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות טא","url":"https://www.youtube.com/watch?v=qYeEWydByZU","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"נרפא, היה טוב","past":{"arabic":"طاب","phonemes":"tˤ aː b"},"future":{"arabic":"يطيب","phonemes":"j i tˤ iː b"}}, - {"hebrew":"הדפיס","past":{"arabic":"طبع","phonemes":"tˤ a b a ʕ"},"future":{"arabic":"يطبع","phonemes":"j i tˤ b a ʕ"}}, - {"hebrew":"האריך, התעכב","past":{"arabic":"طوّل","phonemes":"tˤ a w w a l"},"future":{"arabic":"يطوّل","phonemes":"j i tˤ a w w i l"}}, - {"hebrew":"דרש, ביקש","past":{"arabic":"طلب","phonemes":"tˤ a l a b"},"future":{"arabic":"يطلب","phonemes":"j i tˤ l a b"}}, - {"hebrew":"האכיל","past":{"arabic":"طعّم","phonemes":"tˤ a ʕ ʕ a m"},"future":{"arabic":"يطعّم","phonemes":"j i tˤ a ʕ ʕ i m"}}, - {"hebrew":"היה מסוגל לסבול","past":{"arabic":"طاق","phonemes":"tˤ aː Q"},"future":{"arabic":"يطيق","phonemes":"j i tˤ iː Q"}}, - {"hebrew":"הרגיע","past":{"arabic":"طمّن","phonemes":"tˤ a m m a n"},"future":{"arabic":"يطمّن","phonemes":"j i tˤ a m m i n"}}, - {"hebrew":"פיצח","past":{"arabic":"طقش","phonemes":"tˤ a Q a ʃ"},"future":{"arabic":"يطقش","phonemes":"j i tˤ Q a ʃ"}}, - {"hebrew":"התעלם","past":{"arabic":"طنّش","phonemes":"tˤ a n n a ʃ"},"future":{"arabic":"يطنّش","phonemes":"j i tˤ a n n i ʃ"}}, - {"hebrew":"עף","past":{"arabic":"طار","phonemes":"tˤ aː r"},"future":{"arabic":"يطير","phonemes":"j i tˤ iː r"}}, - {"hebrew":"רדף","past":{"arabic":"طارد","phonemes":"tˤ aː r a d"},"future":{"arabic":"يطارد","phonemes":"j i tˤ aː r i d"}}, - {"hebrew":"כיבה","past":{"arabic":"طفّى","phonemes":"tˤ a f f a"},"future":{"arabic":"يطفّي","phonemes":"j i tˤ a f f i"}}, - {"hebrew":"יישם","past":{"arabic":"طبّق","phonemes":"tˤ a b b a Q"},"future":{"arabic":"يطبّق","phonemes":"j i tˤ a b b i Q"}}, - {"hebrew":"הציץ, ביקר","past":{"arabic":"طلّ","phonemes":"tˤ a l l"},"future":{"arabic":"يطلّ","phonemes":"j i tˤ u l l"}}, - {"hebrew":"שאף","past":{"arabic":"طمح","phonemes":"tˤ a m a ħ"},"future":{"arabic":"يطمح","phonemes":"j i tˤ m a ħ"}}, - {"hebrew":"התיז","past":{"arabic":"طرّش","phonemes":"tˤ a r r a ʃ"},"future":{"arabic":"يطرّش","phonemes":"j i tˤ a r r i ʃ"}} - ] - }, - "kaf": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות כאף","url":"https://www.youtube.com/watch?v=wSoGsCXTyig","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"יצר, הרכיב","past":{"arabic":"كوّن","phonemes":"k a w w a n"},"future":{"arabic":"يكوّن","phonemes":"j i k a w w i n"}}, - {"hebrew":"המשיך, השלים","past":{"arabic":"كمّل","phonemes":"k a m m a l"},"future":{"arabic":"يكمّل","phonemes":"j i k a m m i l"}}, - {"hebrew":"טאטא","past":{"arabic":"كنس","phonemes":"k a n a s"},"future":{"arabic":"يكنس","phonemes":"j i k n u s"}}, - {"hebrew":"שפך","past":{"arabic":"كبّ","phonemes":"k a b b"},"future":{"arabic":"يكبّ","phonemes":"j i k u b b"}}, - {"hebrew":"חרק שיניים","past":{"arabic":"كدّ","phonemes":"k a d d"},"future":{"arabic":"يكدّ","phonemes":"j i k u d d"}}, - {"hebrew":"עלה במחיר","past":{"arabic":"كلّف","phonemes":"k a l l a f"},"future":{"arabic":"يكلّف","phonemes":"j i k a l l i f"}}, - {"hebrew":"גדל","past":{"arabic":"كبر","phonemes":"k a b a r"},"future":{"arabic":"يكبر","phonemes":"j i k b a r"}}, - {"hebrew":"קימט את המצח","past":{"arabic":"كشّر","phonemes":"k a ʃ ʃ a r"},"future":{"arabic":"يكشّر","phonemes":"j i k a ʃ ʃ i r"}}, - {"hebrew":"שבר","past":{"arabic":"كسر","phonemes":"k a s a r"},"future":{"arabic":"يكسر","phonemes":"j i k s a r"}}, - {"hebrew":"כתב","past":{"arabic":"كتب","phonemes":"k a t a b"},"future":{"arabic":"يكتب","phonemes":"j i k t u b"}}, - {"hebrew":"צבר","past":{"arabic":"كدّس","phonemes":"k a d d a s"},"future":{"arabic":"يكدّس","phonemes":"j i k a d d i s"}}, - {"hebrew":"עשה בלגן","past":{"arabic":"كركب","phonemes":"k a r k a b"},"future":{"arabic":"يكركب","phonemes":"j i k a r k i b"}}, - {"hebrew":"חזר על","past":{"arabic":"كرّر","phonemes":"k a r r a r"},"future":{"arabic":"يكرّر","phonemes":"j i k a r r i r"}}, - {"hebrew":"גירש","past":{"arabic":"كشّ","phonemes":"k a ʃ ʃ"},"future":{"arabic":"يكشّ","phonemes":"j i k u ʃ ʃ"}} - ] - }, - "lam": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות לאם","url":"https://www.youtube.com/watch?v=-Jd2w7EEs90","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"צופף","past":{"arabic":"لزّ","phonemes":"l a z z"},"future":{"arabic":"يلزّ","phonemes":"j i l u z z"}}, - {"hebrew":"עקץ, הכיש, צרב","past":{"arabic":"لسع","phonemes":"l a s a ʕ"},"future":{"arabic":"يلسع","phonemes":"j i l s a ʕ"}}, - {"hebrew":"הפציר","past":{"arabic":"لحّ","phonemes":"l a ħ ħ"},"future":{"arabic":"يلحّ","phonemes":"j i l i ħ ħ"}}, - {"hebrew":"אסף","past":{"arabic":"لمّ","phonemes":"l a m m"},"future":{"arabic":"يلمّ","phonemes":"j i l u m m"}}, - {"hebrew":"האשים, גינה","past":{"arabic":"لام","phonemes":"l aː m"},"future":{"arabic":"يلوم","phonemes":"j i l uː m"}}, - {"hebrew":"לימד בשינון","past":{"arabic":"لقّن","phonemes":"l a Q Q a n"},"future":{"arabic":"يلقّن","phonemes":"j i l a Q Q i n"}}, - {"hebrew":"מילא בקשה","past":{"arabic":"لبّى","phonemes":"l a b b a"},"future":{"arabic":"يلبّي","phonemes":"j i l a b b i"}}, - {"hebrew":"לבש","past":{"arabic":"لبس","phonemes":"l a b a s"},"future":{"arabic":"يلبس","phonemes":"j i l b a s"}}, - {"hebrew":"הלביש","past":{"arabic":"لبّس","phonemes":"l a b b a s"},"future":{"arabic":"يلبّس","phonemes":"j i l a b b i s"}}, - {"hebrew":"טייח, העלים","past":{"arabic":"لفلف","phonemes":"l a f l a f"},"future":{"arabic":"يلفلف","phonemes":"j i l a f l i f"}}, - {"hebrew":"סטר","past":{"arabic":"لطش","phonemes":"l a tˤ a ʃ"},"future":{"arabic":"يلطش","phonemes":"j i l tˤ a ʃ"}}, - {"hebrew":"רמז","past":{"arabic":"لمّح","phonemes":"l a m m a ħ"},"future":{"arabic":"يلمّح","phonemes":"j i l a m m i ħ"}} - ] - }, - "mim": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות מים","url":"https://www.youtube.com/watch?v=yKUcU45Ngpg","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"דחה ועיכב","past":{"arabic":"ماطل","phonemes":"m aː tˤ a l"},"future":{"arabic":"يماطل","phonemes":"j i m aː tˤ i l"}}, - {"hebrew":"עסק ב־, הפעיל","past":{"arabic":"مارس","phonemes":"m aː r a s"},"future":{"arabic":"يمارس","phonemes":"j i m aː r i s"}}, - {"hebrew":"מת","past":{"arabic":"مات","phonemes":"m aː t"},"future":{"arabic":"يموت","phonemes":"j i m uː t"}}, - {"hebrew":"מימן","past":{"arabic":"موّل","phonemes":"m a w w a l"},"future":{"arabic":"يموّل","phonemes":"j i m a w w i l"}}, - {"hebrew":"קרע לגזרים","past":{"arabic":"مزّع","phonemes":"m a z z a ʕ"},"future":{"arabic":"يمزّع","phonemes":"j i m a z z i ʕ"}}, - {"hebrew":"הבחין בין","past":{"arabic":"ميّز","phonemes":"m a j j a z"},"future":{"arabic":"يميّز","phonemes":"j i m a j j i z"}}, - {"hebrew":"מילא","past":{"arabic":"ملا","phonemes":"m a l a"},"future":{"arabic":"يملي","phonemes":"j i m l i"}}, - {"hebrew":"שיבח","past":{"arabic":"مدح","phonemes":"m a d a ħ"},"future":{"arabic":"يمدح","phonemes":"j i m d a ħ"}}, - {"hebrew":"סיפק","past":{"arabic":"موّن","phonemes":"m a w w a n"},"future":{"arabic":"يموّن","phonemes":"j i m a w w i n"}}, - {"hebrew":"שטף, היה בעלים","past":{"arabic":"ملك","phonemes":"m a l a k"},"future":{"arabic":"يملك","phonemes":"j i m l u k"}}, - {"hebrew":"ליטף, החליק","past":{"arabic":"ملّس","phonemes":"m a l l a s"},"future":{"arabic":"يملّس","phonemes":"j i m a l l i s"}}, - {"hebrew":"העניק","past":{"arabic":"منح","phonemes":"m a n a ħ"},"future":{"arabic":"يمنح","phonemes":"j i m n a ħ"}}, - {"hebrew":"מנע, אסר","past":{"arabic":"منع","phonemes":"m a n a ʕ"},"future":{"arabic":"يمنع","phonemes":"j i m n a ʕ"}}, - {"hebrew":"מצץ","past":{"arabic":"مصّ","phonemes":"m a sˤ sˤ"},"future":{"arabic":"يمصّ","phonemes":"j i m u sˤ sˤ"}}, - {"hebrew":"העמיד פנים","past":{"arabic":"مثّل","phonemes":"m a TH TH a l"},"future":{"arabic":"يمثّل","phonemes":"j i m a TH TH i l"}}, - {"hebrew":"ערבב","past":{"arabic":"مزج","phonemes":"m a z a J"},"future":{"arabic":"يمزج","phonemes":"j i m z a J"}}, - {"hebrew":"התנדנד","past":{"arabic":"مرجح","phonemes":"m a r J a ħ"},"future":{"arabic":"يمرجح","phonemes":"j i m a r J i ħ"}}, - {"hebrew":"הוליך, הפעיל","past":{"arabic":"مشّى","phonemes":"m a ʃ ʃ a"},"future":{"arabic":"يمشّي","phonemes":"j i m a ʃ ʃ i"}}, - {"hebrew":"עבר","past":{"arabic":"مرق","phonemes":"m a r a Q"},"future":{"arabic":"يمرق","phonemes":"j i m r u Q"}} - ] - }, - "nun": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות נון","url":"https://www.youtube.com/watch?v=KdipB6ik22Y","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"ישן, שכב","past":{"arabic":"نام","phonemes":"n aː m"},"future":{"arabic":"ينام","phonemes":"j i n aː m"}}, - {"hebrew":"הרדים","past":{"arabic":"نوّم","phonemes":"n a w w a m"},"future":{"arabic":"ينوّم","phonemes":"j i n a w w i m"}}, - {"hebrew":"שכח","past":{"arabic":"نسي","phonemes":"n a s i"},"future":{"arabic":"ينسى","phonemes":"j i n s a"}}, - {"hebrew":"השכיח","past":{"arabic":"نسّى","phonemes":"n a s s a"},"future":{"arabic":"ينسّي","phonemes":"j i n a s s i"}}, - {"hebrew":"התנמנם","past":{"arabic":"نعس","phonemes":"n i ʕ i s"},"future":{"arabic":"ينعس","phonemes":"j i n ʕ a s"}}, - {"hebrew":"נשף, נשם","past":{"arabic":"نفش","phonemes":"n a f a ʃ"},"future":{"arabic":"ينفش","phonemes":"j i n f u ʃ"}}, - {"hebrew":"ניתר","past":{"arabic":"نطّ","phonemes":"n a tˤ tˤ"},"future":{"arabic":"ينطّ","phonemes":"j i n u tˤ tˤ"}}, - {"hebrew":"העביר, העתיק","past":{"arabic":"نقل","phonemes":"n a Q a l"},"future":{"arabic":"ينقل","phonemes":"j i n Q u l"}}, - {"hebrew":"התחרה","past":{"arabic":"نافس","phonemes":"n aː f a s"},"future":{"arabic":"ينافس","phonemes":"j i n aː f i s"}}, - {"hebrew":"ניקה","past":{"arabic":"نضّف","phonemes":"n a dˤ dˤ a f"},"future":{"arabic":"ينضّف","phonemes":"j i n a dˤ dˤ i f"}}, - {"hebrew":"בחר","past":{"arabic":"نقّى","phonemes":"n a Q Q a"},"future":{"arabic":"ينقّي","phonemes":"j i n a Q Q i"}}, - {"hebrew":"סגר","past":{"arabic":"نتّ","phonemes":"n a t t"},"future":{"arabic":"ينتّ","phonemes":"j i n u t t"}} - ] - }, - "sin": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות סין","url":"https://www.youtube.com/watch?v=DlM_AscrScY","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"החביא, שמר","past":{"arabic":"ستر","phonemes":"s a t a r"},"future":{"arabic":"يستر","phonemes":"j i s t u r"}}, - {"hebrew":"שמע","past":{"arabic":"سمع","phonemes":"s a m i ʕ"},"future":{"arabic":"يسمع","phonemes":"j i s m a ʕ"}}, - {"hebrew":"עזר","past":{"arabic":"ساعد","phonemes":"s aː ʕ a d"},"future":{"arabic":"يساعد","phonemes":"j i s aː ʕ i d"}}, - {"hebrew":"כישף","past":{"arabic":"سحر","phonemes":"s a ħ a r"},"future":{"arabic":"يسحر","phonemes":"j i s ħ a r"}}, - {"hebrew":"שאל","past":{"arabic":"سأل","phonemes":"s a ʔ a l"},"future":{"arabic":"يسأل","phonemes":"j i s ʔ a l"}}, - {"hebrew":"גר, שכן","past":{"arabic":"سكن","phonemes":"s a k a n"},"future":{"arabic":"يسكن","phonemes":"j i s k u n"}}, - {"hebrew":"השתלט","past":{"arabic":"سيطر","phonemes":"s a j tˤ a r"},"future":{"arabic":"يسيطر","phonemes":"j i s a j tˤ i r"}}, - {"hebrew":"נסע","past":{"arabic":"سافر","phonemes":"s aː f a r"},"future":{"arabic":"يسافر","phonemes":"j i s aː f i r"}}, - {"hebrew":"הרגיז","past":{"arabic":"سفّن","phonemes":"s a f f a n"},"future":{"arabic":"يسفّن","phonemes":"j i s a f f i n"}}, - {"hebrew":"סלח","past":{"arabic":"سامح","phonemes":"s aː m a ħ"},"future":{"arabic":"يسامح","phonemes":"j i s aː m i ħ"}}, - {"hebrew":"גנב","past":{"arabic":"سرق","phonemes":"s a r a Q"},"future":{"arabic":"يسرق","phonemes":"j i s r u Q"}}, - {"hebrew":"שתק","past":{"arabic":"سكت","phonemes":"s a k a t"},"future":{"arabic":"يسكت","phonemes":"j i s k u t"}}, - {"hebrew":"צחק","past":{"arabic":"سخف","phonemes":"s a x a f"},"future":{"arabic":"يسخف","phonemes":"j i s x a f"}}, - {"hebrew":"קילל","past":{"arabic":"سبّ","phonemes":"s a b b"},"future":{"arabic":"يسبّ","phonemes":"j i s u b b"}}, - {"hebrew":"עזב","past":{"arabic":"ساب","phonemes":"s aː b"},"future":{"arabic":"يسيب","phonemes":"j i s iː b"}}, - {"hebrew":"שיבח, עזר לשחות","past":{"arabic":"سبّح","phonemes":"s a b b a ħ"},"future":{"arabic":"يسبّح","phonemes":"j i s a b b i ħ"}}, - {"hebrew":"הקל","past":{"arabic":"سهّل","phonemes":"s a h h a l"},"future":{"arabic":"يسهّل","phonemes":"j i s a h h i l"}}, - {"hebrew":"סידר, עשה","past":{"arabic":"سوّى","phonemes":"s a w w a"},"future":{"arabic":"يسوّي","phonemes":"j i s a w w i"}} - ] - }, - "ayn": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות עין","url":"https://www.youtube.com/watch?v=BZU4jitxcuU","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"גילה עירנות","past":{"arabic":"وعى","phonemes":"w a ʕ a"},"future":{"arabic":"يوعى","phonemes":"j i w ʕ a"}}, - {"hebrew":"לימד","past":{"arabic":"علّم","phonemes":"ʕ a l l a m"},"future":{"arabic":"يعلّم","phonemes":"j i ʕ a l l i m"}}, - {"hebrew":"כעס","past":{"arabic":"عكّر","phonemes":"ʕ a k k a r"},"future":{"arabic":"يعكّر","phonemes":"j i ʕ a k k i r"}}, - {"hebrew":"הזיע","past":{"arabic":"عرق","phonemes":"ʕ a r i Q"},"future":{"arabic":"يعرق","phonemes":"j i ʕ r a Q"}}, - {"hebrew":"איפס, כיוון","past":{"arabic":"عيّر","phonemes":"ʕ a j j a r"},"future":{"arabic":"يعيّر","phonemes":"j i ʕ a j j i r"}}, - {"hebrew":"הכיר, ידע","past":{"arabic":"عرف","phonemes":"ʕ i r i f"},"future":{"arabic":"يعرف","phonemes":"j i ʕ r a f"}}, - {"hebrew":"בירך לכבוד חג","past":{"arabic":"عايد","phonemes":"ʕ aː j a d"},"future":{"arabic":"يعايد","phonemes":"j i ʕ aː j i d"}}, - {"hebrew":"ספר","past":{"arabic":"عدّ","phonemes":"ʕ a d d"},"future":{"arabic":"يعدّ","phonemes":"j i ʕ u d d"}}, - {"hebrew":"חזר על","past":{"arabic":"عاد","phonemes":"ʕ aː d"},"future":{"arabic":"يعيد","phonemes":"j i ʕ iː d"}}, - {"hebrew":"שם מכשול","past":{"arabic":"عرقل","phonemes":"ʕ a r Q a l"},"future":{"arabic":"يعرقل","phonemes":"j i ʕ a r Q i l"}}, - {"hebrew":"חי","past":{"arabic":"عاش","phonemes":"ʕ aː ʃ"},"future":{"arabic":"يعيش","phonemes":"j i ʕ iː ʃ"}}, - {"hebrew":"עבר, נכנס","past":{"arabic":"عبر","phonemes":"ʕ a b a r"},"future":{"arabic":"يعبر","phonemes":"j i ʕ b u r"}}, - {"hebrew":"התעקש","past":{"arabic":"عاند","phonemes":"ʕ aː n a d"},"future":{"arabic":"يعاند","phonemes":"j i ʕ aː n i d"}}, - {"hebrew":"נתן פיצוי","past":{"arabic":"عوّض","phonemes":"ʕ a w w a dˤ"},"future":{"arabic":"يعوّض","phonemes":"j i ʕ a w w i dˤ"}}, - {"hebrew":"אהב, חיבב","past":{"arabic":"عزّ","phonemes":"ʕ a z z"},"future":{"arabic":"يعزّ","phonemes":"j i ʕ i z z"}}, - {"hebrew":"הדיח, מיהר","past":{"arabic":"عجّل","phonemes":"ʕ a J J a l"},"future":{"arabic":"يعجّل","phonemes":"j i ʕ a J J i l"}}, - {"hebrew":"בנה","past":{"arabic":"عمّر","phonemes":"ʕ a m m a r"},"future":{"arabic":"يعمّر","phonemes":"j i ʕ a m m i r"}} - ] - }, - "ghayn": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות ע'ין","url":"https://www.youtube.com/watch?v=kqtBFuKjx8o","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"עזב, נעלם","past":{"arabic":"غاب","phonemes":"ɣ aː b"},"future":{"arabic":"يغيب","phonemes":"j i ɣ iː b"}}, - {"hebrew":"שקע","past":{"arabic":"غرق","phonemes":"ɣ i r i Q"},"future":{"arabic":"يغرق","phonemes":"j i ɣ r a Q"}}, - {"hebrew":"שטף","past":{"arabic":"غسل","phonemes":"ɣ a s a l"},"future":{"arabic":"يغسل","phonemes":"j i ɣ s i l"}}, - {"hebrew":"כיסה","past":{"arabic":"غطّى","phonemes":"ɣ a tˤ tˤ a"},"future":{"arabic":"يغطّي","phonemes":"j i ɣ a tˤ tˤ i"}}, - {"hebrew":"שינה","past":{"arabic":"غيّر","phonemes":"ɣ a j j a r"},"future":{"arabic":"يغيّر","phonemes":"j i ɣ a j j i r"}}, - {"hebrew":"רתח","past":{"arabic":"غلى","phonemes":"ɣ a l a"},"future":{"arabic":"يغلي","phonemes":"j i ɣ l i"}}, - {"hebrew":"ניצח","past":{"arabic":"غلب","phonemes":"ɣ a l a b"},"future":{"arabic":"يغلب","phonemes":"j i ɣ l a b"}}, - {"hebrew":"סלח","past":{"arabic":"غفر","phonemes":"ɣ a f a r"},"future":{"arabic":"يغفر","phonemes":"j i ɣ f i r"}} - ] - }, - "fa": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות פא","url":"https://www.youtube.com/watch?v=pCn0vhBVR5Q","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"נכשל","past":{"arabic":"فشل","phonemes":"f a ʃ a l"},"future":{"arabic":"يفشل","phonemes":"j i f ʃ a l"}}, - {"hebrew":"הבין","past":{"arabic":"فهم","phonemes":"f i h i m"},"future":{"arabic":"يفهم","phonemes":"j i f h a m"}}, - {"hebrew":"פתח","past":{"arabic":"فتح","phonemes":"f a t a ħ"},"future":{"arabic":"يفتح","phonemes":"j i f t a ħ"}}, - {"hebrew":"נכנס, עבר","past":{"arabic":"فات","phonemes":"f aː t"},"future":{"arabic":"يفوت","phonemes":"j i f uː t"}}, - {"hebrew":"הפריד","past":{"arabic":"فرّق","phonemes":"f a r r a Q"},"future":{"arabic":"يفرّق","phonemes":"j i f a r r i Q"}}, - {"hebrew":"ברח","past":{"arabic":"فلّ","phonemes":"f a l l"},"future":{"arabic":"يفلّ","phonemes":"j i f u l l"}}, - {"hebrew":"חשב, הניח","past":{"arabic":"فكّر","phonemes":"f a k k a r"},"future":{"arabic":"يفكّر","phonemes":"j i f a k k i r"}}, - {"hebrew":"שחרר, התיר","past":{"arabic":"فكّ","phonemes":"f a k k"},"future":{"arabic":"يفكّ","phonemes":"j i f u k k"}}, - {"hebrew":"התעורר","past":{"arabic":"فاق","phonemes":"f aː Q"},"future":{"arabic":"يفيق","phonemes":"j i f iː Q"}}, - {"hebrew":"הפסיק, ניתק","past":{"arabic":"فصل","phonemes":"f a sˤ a l"},"future":{"arabic":"يفصل","phonemes":"j i f sˤ a l"}} - ] - }, - "dad": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות דאד","url":"https://www.youtube.com/watch?v=ULZkU-MoU6w","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"היכה","past":{"arabic":"ضرب","phonemes":"dˤ a r a b"},"future":{"arabic":"يضرب","phonemes":"j i dˤ r u b"}}, - {"hebrew":"צחק","past":{"arabic":"ضحك","phonemes":"dˤ i ħ i k"},"future":{"arabic":"يضحك","phonemes":"j i dˤ ħ a k"}}, - {"hebrew":"נשאר","past":{"arabic":"ضلّ","phonemes":"dˤ a l l"},"future":{"arabic":"يضلّ","phonemes":"j i dˤ a l l"}}, - {"hebrew":"הלך לאיבוד","past":{"arabic":"ضاع","phonemes":"dˤ aː ʕ"},"future":{"arabic":"يضيع","phonemes":"j i dˤ iː ʕ"}}, - {"hebrew":"הבטיח","past":{"arabic":"ضمن","phonemes":"dˤ a m a n"},"future":{"arabic":"يضمن","phonemes":"j i dˤ m a n"}}, - {"hebrew":"לחץ","past":{"arabic":"ضغط","phonemes":"dˤ a ɣ a tˤ"},"future":{"arabic":"يضغط","phonemes":"j i dˤ ɣ a tˤ"}} - ] - }, - "sad": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות צאד","url":"https://www.youtube.com/watch?v=jCpab4z3dXY","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"קרה","past":{"arabic":"صار","phonemes":"sˤ aː r"},"future":{"arabic":"يصير","phonemes":"j i sˤ iː r"}}, - {"hebrew":"צעק","past":{"arabic":"صرخ","phonemes":"sˤ a r a x"},"future":{"arabic":"يصرخ","phonemes":"j i sˤ r a x"}}, - {"hebrew":"תיקן","past":{"arabic":"صلّح","phonemes":"sˤ a l l a ħ"},"future":{"arabic":"يصلّح","phonemes":"j i sˤ a l l i ħ"}}, - {"hebrew":"צם","past":{"arabic":"صام","phonemes":"sˤ aː m"},"future":{"arabic":"يصوم","phonemes":"j i sˤ uː m"}}, - {"hebrew":"צילם","past":{"arabic":"صوّر","phonemes":"sˤ a w w a r"},"future":{"arabic":"يصوّر","phonemes":"j i sˤ a w w i r"}}, - {"hebrew":"סגר","past":{"arabic":"سكّر","phonemes":"s a k k a r"},"future":{"arabic":"يسكّر","phonemes":"j i s a k k i r"}}, - {"hebrew":"התעורר","past":{"arabic":"صحي","phonemes":"sˤ i ħ i"},"future":{"arabic":"يصحى","phonemes":"j i sˤ ħ a"}} - ] - }, - "qaf": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות קאף","url":"https://www.youtube.com/watch?v=zx4scJYgYAM","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"אמר","past":{"arabic":"قال","phonemes":"Q aː l"},"future":{"arabic":"يقول","phonemes":"j i Q uː l"}}, - {"hebrew":"קרא","past":{"arabic":"قرأ","phonemes":"Q a r a ʔ"},"future":{"arabic":"يقرأ","phonemes":"j i Q r a ʔ"}}, - {"hebrew":"ישב","past":{"arabic":"قعد","phonemes":"Q a ʕ a d"},"future":{"arabic":"يقعد","phonemes":"j i Q ʕ a d"}}, - {"hebrew":"קם","past":{"arabic":"قام","phonemes":"Q aː m"},"future":{"arabic":"يقوم","phonemes":"j i Q uː m"}}, - {"hebrew":"החליט","past":{"arabic":"قرّر","phonemes":"Q a r r a r"},"future":{"arabic":"يقرّر","phonemes":"j i Q a r r i r"}}, - {"hebrew":"חתך","past":{"arabic":"قطع","phonemes":"Q a tˤ a ʕ"},"future":{"arabic":"يقطع","phonemes":"j i Q tˤ a ʕ"}}, - {"hebrew":"סגר","past":{"arabic":"قفل","phonemes":"Q a f a l"},"future":{"arabic":"يقفل","phonemes":"j i Q f u l"}}, - {"hebrew":"הסכים","past":{"arabic":"قبل","phonemes":"Q i b i l"},"future":{"arabic":"يقبل","phonemes":"j i Q b a l"}}, - {"hebrew":"מדד","past":{"arabic":"قاس","phonemes":"Q aː s"},"future":{"arabic":"يقيس","phonemes":"j i Q iː s"}}, - {"hebrew":"פגש","past":{"arabic":"قابل","phonemes":"Q aː b a l"},"future":{"arabic":"يقابل","phonemes":"j i Q aː b i l"}} - ] - }, - "ra": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות רא","url":"https://www.youtube.com/watch?v=fMIJT4HowVg","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"הלך","past":{"arabic":"راح","phonemes":"r aː ħ"},"future":{"arabic":"يروح","phonemes":"j i r uː ħ"}}, - {"hebrew":"ראה","past":{"arabic":"رأى","phonemes":"r a ʔ a"},"future":{"arabic":"يرى","phonemes":"j i r a"}}, - {"hebrew":"חזר","past":{"arabic":"رجع","phonemes":"r a J a ʕ"},"future":{"arabic":"يرجع","phonemes":"j i r J a ʕ"}}, - {"hebrew":"ענה","past":{"arabic":"ردّ","phonemes":"r a d d"},"future":{"arabic":"يردّ","phonemes":"j i r u d d"}}, - {"hebrew":"רכב","past":{"arabic":"ركب","phonemes":"r i k i b"},"future":{"arabic":"يركب","phonemes":"j i r k a b"}}, - {"hebrew":"רץ","past":{"arabic":"ركض","phonemes":"r a k a dˤ"},"future":{"arabic":"يركض","phonemes":"j i r k u dˤ"}}, - {"hebrew":"סידר","past":{"arabic":"رتّب","phonemes":"r a t t a b"},"future":{"arabic":"يرتّب","phonemes":"j i r a t t i b"}}, - {"hebrew":"זרק","past":{"arabic":"رمى","phonemes":"r a m a"},"future":{"arabic":"يرمي","phonemes":"j i r m i"}}, - {"hebrew":"ניצח","past":{"arabic":"ربح","phonemes":"r i b i ħ"},"future":{"arabic":"يربح","phonemes":"j i r b a ħ"}}, - {"hebrew":"גידל","past":{"arabic":"ربّى","phonemes":"r a b b a"},"future":{"arabic":"يربّي","phonemes":"j i r a b b i"}} - ] - }, - "shin": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות שין","url":"https://www.youtube.com/watch?v=yrL4wM1-jH8","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"ראה","past":{"arabic":"شاف","phonemes":"ʃ aː f"},"future":{"arabic":"يشوف","phonemes":"j i ʃ uː f"}}, - {"hebrew":"שתה","past":{"arabic":"شرب","phonemes":"ʃ i r i b"},"future":{"arabic":"يشرب","phonemes":"j i ʃ r a b"}}, - {"hebrew":"עבד","past":{"arabic":"اشتغل","phonemes":"ʔ i ʃ t a ɣ a l"},"future":{"arabic":"يشتغل","phonemes":"j i ʃ t a ɣ i l"}}, - {"hebrew":"קנה","past":{"arabic":"شرى","phonemes":"ʃ a r a"},"future":{"arabic":"يشري","phonemes":"j i ʃ r i"}}, - {"hebrew":"הסביר","past":{"arabic":"شرح","phonemes":"ʃ a r a ħ"},"future":{"arabic":"يشرح","phonemes":"j i ʃ r a ħ"}}, - {"hebrew":"הדליק","past":{"arabic":"شغّل","phonemes":"ʃ a ɣ ɣ a l"},"future":{"arabic":"يشغّل","phonemes":"j i ʃ a ɣ ɣ i l"}}, - {"hebrew":"סחב","past":{"arabic":"شال","phonemes":"ʃ aː l"},"future":{"arabic":"يشيل","phonemes":"j i ʃ iː l"}}, - {"hebrew":"השתתף","past":{"arabic":"شارك","phonemes":"ʃ aː r a k"},"future":{"arabic":"يشارك","phonemes":"j i ʃ aː r i k"}}, - {"hebrew":"קילל","past":{"arabic":"شتم","phonemes":"ʃ a t a m"},"future":{"arabic":"يشتم","phonemes":"j i ʃ t u m"}}, - {"hebrew":"משך","past":{"arabic":"شدّ","phonemes":"ʃ a d d"},"future":{"arabic":"يشدّ","phonemes":"j i ʃ u d d"}} - ] - }, - "ta": { - "source":{"title":"פעלים שימושיים בערבית מדוברת — אות תא, חלק 1","url":"https://www.youtube.com/watch?v=z41lWU0o54M","dialect":"Palestinian spoken Arabic"}, - "verbs":[ - {"hebrew":"עזב","past":{"arabic":"ترك","phonemes":"t a r a k"},"future":{"arabic":"يترك","phonemes":"j i t r u k"}}, - {"hebrew":"עייף","past":{"arabic":"تعب","phonemes":"t i ʕ i b"},"future":{"arabic":"يتعب","phonemes":"j i t ʕ a b"}}, - {"hebrew":"למד","past":{"arabic":"تعلّم","phonemes":"t a ʕ a l l a m"},"future":{"arabic":"يتعلّم","phonemes":"j i t a ʕ a l l a m"}}, - {"hebrew":"דיבר","past":{"arabic":"تكلّم","phonemes":"t k a l l a m"},"future":{"arabic":"يتكلّم","phonemes":"j i t k a l l a m"}}, - {"hebrew":"התחתן","past":{"arabic":"تجوّز","phonemes":"t J a w w a z"},"future":{"arabic":"يتجوّز","phonemes":"j i t J a w w a z"}}, - {"hebrew":"איחר","past":{"arabic":"تأخّر","phonemes":"t ʔ a x x a r"},"future":{"arabic":"يتأخّر","phonemes":"j i t ʔ a x x a r"}}, - {"hebrew":"התקשר","past":{"arabic":"اتّصل","phonemes":"ʔ i t t a sˤ a l"},"future":{"arabic":"يتّصل","phonemes":"j i t t a sˤ i l"}}, - {"hebrew":"השתנה","past":{"arabic":"تغيّر","phonemes":"t ɣ a j j a r"},"future":{"arabic":"يتغيّر","phonemes":"j i t ɣ a j j a r"}}, - {"hebrew":"הסכים","past":{"arabic":"اتّفق","phonemes":"ʔ i t t a f a Q"},"future":{"arabic":"يتّفق","phonemes":"j i t t a f i Q"}}, - {"hebrew":"התקדם","past":{"arabic":"تقدّم","phonemes":"t Q a d d a m"},"future":{"arabic":"يتقدّم","phonemes":"j i t Q a d d a m"}}, - {"hebrew":"תרגם","past":{"arabic":"ترجم","phonemes":"t a r J a m"},"future":{"arabic":"يترجم","phonemes":"j i t a r J i m"}}, - {"hebrew":"נזכר","past":{"arabic":"تذكّر","phonemes":"t DH a k k a r"},"future":{"arabic":"يتذكّر","phonemes":"j i t DH a k k a r"}} - ] - } -} diff --git a/static/assets/pronunciation-rules.md b/static/assets/pronunciation-rules.md deleted file mode 100644 index e01bbd8c79285e201fb599c991870655d432ca88..0000000000000000000000000000000000000000 --- a/static/assets/pronunciation-rules.md +++ /dev/null @@ -1,476 +0,0 @@ -# כללי הגייה לדוברי עברית: ערבית פלסטינית מדוברת - -גרסה: 2026-10 · מיקום מוצע ב־repo: `docs/pronunciation-rules.md` - -המסמך משמש שני קהלים: - -| קהל | חלקים | איך משתמשים | -|---|---|---| -| תלמידים | חלקים 1–4 (מסומנים `audience: student`) | מוצגים באפליקציה כמדריך. בלי כתב ערבי, רק תעתיק עברי. | -| ה־scorer | חלקים 5–8 (מסומנים `audience: scorer`) | הבלוקים ` ```yaml ` נקראים בקוד ומגדירים מימושים מקובלים, שגיאות, חומרה, ספים ומקרים מיוחדים. | - -עיקרון: **עירוני וכפרי תקינים שניהם.** ה־scorer לא מתקן הגייה שתקינה באחד הניבים, גם אם התלמיד מערבב ניבים באותו משפט. - ---- - - -## 1. איך קוראים את התעתיק - -| בתעתיק | איך הוגים | דוגמה | -|---|---|---| -| ח | ח לועית: מכווצים את הגרון ונושפים, בלי גירוד | חַמַּאם (שירותים) | -| ח' | ח עברית רגילה | ח'ֵיר (טוב) | -| ע | ע לועית עם קול | סַאעַה (שעה) | -| ע' | ר עברית רגילה (גרונית) | בַּשְתְע'ִל (אני עובד) | -| ר | ר מתגלגלת בקצה הלשון | כְּתִיר (הרבה) | -| צ | ס מודגשת, **לא** ts | צַבַּאח (בוקר) | -| ט | ת מודגשת | מַחַטַּה (תחנה) | -| ד' | ד מודגשת | בַּעְד' (אחד את השני) | -| ט' | ז מודגשת | ט'ֻהְר (צהריים) | -| ק | לפי הניב: עצירה בגרון (עיר) או כּ (כפר). ראו חלק 2 | אַהְוֶה / כַּהְוֶה (קפה) | -| ג' | כמו ב'ג'ינס', או ז' | פִנְג'אן (כוס קפה) | -| צ' | כמו ב'צ'יפס'. רק בניב הכפרי, במקום כּ | צ'ַלְבּ (כלב, בכפר) | -| ת' | th כמו ב־think. רק בניב הכפרי | ת'לַאת'ֶה (שלוש, בכפר) | -| ד״ | th כמו ב־this. רק בניב הכפרי | ד״ַהַבּ (זהב, בכפר) | -| ו | w, אף פעם לא v | וֵין (איפה) | -| ב | b, אף פעם לא v, גם בסוף מילה | שוֹבּ (חם) | -| פ | f, אף פעם לא p | פַאהֵם (מבין) | -| ה | נשיפה רכה, תמיד נשמעת | אַהְוֶה (קפה) | -| א | עצירה קצרה בגרון | אַהְלַא (שלום) | -| אַא, ִי, וּ | תנועה ארוכה: מחזיקים בערך כפול | כְּתִיר = ktiir | -| ְ (שווא) | בלי תנועה: שני העיצורים צמודים. בתחילת משפט קשה להגות שווא, ולכן מותר שיישמע כמו e קצרה (צירה) | כְּתִיר = ktiir, ובתחילת משפט גם ketiir | -| ֻ (קובוץ) | u קצרה אבל ברורה ומלאה, אף פעם לא נבלעת לשווא | מֻחַאמִי = muħaami, לא mħaami | -| ּ (דגש) | עיצור כפול: מחזיקים אותו | חַמַּאם = ħam-maam | - ---- - - -## 2. שני ניבים, שניהם נכונים - -בערבית הפלסטינית יש שני ניבים עיקריים: **עירוני** (ירושלים, יפו, חיפה, שכם, רמאללה) ו**כפרי** (פַלַּאחִי, בכפרי הגדה והגליל). ההבדל העיקרי ביניהם הוא בחמש אותיות. האפליקציה מקבלת את שתי ההגיות, והמורה מחליט איזו מהן ללמד. - -| אות | עירוני | כפרי | דוגמה: עיר / כפר | -|---|---|---|---| -| ק | עצירה בגרון (א) | כּ | אַהְוֶה / כַּהְוֶה (קפה) | -| כּ | כּ | בכפרים רבים צ' | כַּלְבּ / צ'ַלְבּ (כלב) | -| ת' | ת (ובמילים ספרותיות ס) | ת' (th כמו think) | תְלַאתֶה / ת'לַאת'ֶה (שלוש) | -| ד״ | ד (ובמילים ספרותיות ז) | ד״ (th כמו this) | דַהַבּ / ד״ַהַבּ (זהב) | -| ט' | ד' או ט' מודגשת | ט' מודגשת (th מודגשת) | ד'ֻהְר / ט'ֻהְר (צהריים) | - -**דוגמה שמראה למה זה חשוב:** בכפר, קַלְבּ (לב) נהגה כַּלְבּ, וכַּלְבּ (כלב) נהגה צ'ַלְבּ. בעיר, אותן שתי מילים נהגות אַלְבּ (לב) וכַּלְבּ (כלב). כלומר "כַּלְבּ" פירושו לב בכפר וכלב בעיר. האפליקציה בודקת לפי המשפט שביקשו מכם לומר, ולכן שתי ההגיות יתקבלו במקומן. - -**המלצה:** בחרו ניב אחד והיצמדו אליו. האפליקציה לא תוריד ציון על ערבוב, אבל דובר ילידי לא מערבב. - ---- - - -## 3. כללים שהאפליקציה בודקת - -הכללים מסודרים לפי כמה הטעות עולה לכם: **משנה משמעות** קודם, אחריו **מבטא עברי חזק**, ובסוף **דיוק**. - -### 3.1 ח לועית · משנה משמעות -- **למה זה קשה:** בעברית המודרנית ח ו־כ נהגות אותו דבר, בחיכוך של גב הלשון בחך הרך. בערבית זה צליל אחר (ח'), ושתי המילים בזוג שלמטה שונות לגמרי. -- **מה עושים:** גב הלשון לא נוגע בחך. מכווצים את שרירי הלוע, נמוך בגרון, ונושפים. הצליל יבש ונקי, בלי גירוד. אם מרגישים רטט או חיכוך מאחור בפה, יצאה ח' עברית. -- **זוג מינימלי:** חַאל (מצב) ≠ ח'ַאל (דוד מצד האם) -- **דוגמאות:** חַמַּאם (שירותים), מְנִיח (טוב), תִחְכִּי (לדבר) - -```yaml -id: pharyngeal_h -title: ח לועית -arabic: ح -checks: - - {target: ħ, heard: [χ, x], severity: 3, feedback: "נהגתה כמו ח עברית (חיכוך בחך). ח ערבית נוצרת בלוע: כווצו את הגרון ונשפו, בלי גירוד."} - - {target: ħ, heard: [h, ɦ], severity: 2, feedback: "נהגתה רכה מדי, כמו ה. צריך יותר כיווץ בלוע: לחישה חזקה מגרון צר."} - - {target: ħ, heard: ["_"], severity: 2, feedback: "לא נשמעה. ח היא עיצור מלא, גם באמצע מילה."} -``` - -### 3.2 ע לועית · משנה משמעות -- **למה זה קשה:** בעברית הישראלית ע נהגית כמו א, או נבלעת לגמרי. -- **מה עושים:** אותו כיווץ לוע של ח, אבל עם קול, כלומר מיתרי הקול רוטטים. אומרים 'אַ' כשהגרון מכווץ, בלי לסגור אותו לגמרי כמו ב־א. -- **זוג מינימלי:** עַלַם (דגל) ≠ אַלַם (כאב) -- **דוגמאות:** סַאעַה (שעה), בְּתִעְמַל (אתה עושה), אִלְעַרַבִּי (הערבי) - -```yaml -id: ayin -title: ע לועית -arabic: ع -checks: - - {target: ʕ, heard: [ʔ], severity: 3, feedback: "נהגתה כמו א (סגירה של הגרון). ע לא סוגרת את הגרון, היא מכווצת אותו, עם קול."} - - {target: ʕ, heard: ["_", a, aː], severity: 3, feedback: "נבלעה. ע נהגית תמיד, גם באמצע מילה."} -``` - -### 3.3 עיצורים מודגשים: צ, ט, ד', ט' · מבטא חזק -- **למה זה קשה:** בעברית אין עיצורים מודגשים: ט עברית זהה ל־ת. ו־צ עברית היא ts, צליל שלא קיים בערבית בכלל. בתעתיק, צ מסמנת ס מודגשת. -- **מה עושים:** אומרים ס, ת, ד או ז רגילים, ובאותו רגע מושכים את גב הלשון לאחור ומורידים מעט את הלסת. הסימן שהצלחתם: התנועה שאחרי העיצור נשמעת כהה ועמוקה (אָ במקום אַ). -- **זוגות מינימליים:** צֵיף (קיץ) ≠ סֵיף (חרב) · טִין (בוץ) ≠ תִין (תאנים) -- **דוגמאות:** צַבַּאח (בוקר), מַחַטַּה (תחנה), בַּעְד' (אחד את השני) - -```yaml -id: emphatics -title: עיצורים מודגשים -arabic: ص ط ض ظ -checks: - - {target: sˤ, heard: [s], severity: 2, feedback: "נהגתה כ־ס רגילה. משכו את גב הלשון לאחור: התנועה אחריה צריכה להישמע כהה."} - - {target: sˤ, heard: [ts, t͡s], severity: 2, feedback: "נהגתה כמו צ עברית (ts). בתעתיק צ היא ס מודגשת, בלי t בהתחלה."} - - {target: tˤ, heard: [t], severity: 2, feedback: "נהגתה כ־ת רגילה. ט ערבית מודגשת: גב הלשון לאחור, תנועה כהה."} - - {target: dˤ, heard: [d], severity: 2, feedback: "נהגתה כ־ד רגילה. ד' מודגשת: גב הלשון לאחור, תנועה כהה."} - - {target: ZH, heard: [z, ð, d], severity: 2, feedback: "נהגתה בלי הדגשה. ט' מודגשת: גב הלשון לאחור, תנועה כהה."} -``` - -### 3.4 ר מתגלגלת · מבטא חזק -- **למה זה קשה:** ר עברית נהגית בגרון, ובאוזן ערבית היא נשמעת כמו ע', שהיא עיצור אחר. -- **מה עושים:** ר ערבית נוצרת בקצה הלשון: נקישה אחת או גלגול מאחורי השיניים העליונות. -- **זוג מינימלי:** רֵים (צבייה) ≠ ע'ֵים (עננים) -- **דוגמאות:** כְּתִיר (הרבה), בֻּכְּרַה (מחר) - -```yaml -id: r_tap -title: ר מתגלגלת -arabic: ر -checks: - - {target: r, heard: [ʁ, ɣ, χ, x], severity: 2, feedback: "נהגתה גרונית, כמו ר עברית. ר ערבית מתגלגלת בקצה הלשון מאחורי השיניים."} -``` - -### 3.5 ע' היא ה־ר העברית שלכם · מבטא -- **למה זה קשה:** זה בעצם העיצור הקל ביותר לדוברי עברית, כי ע' היא כמעט בדיוק ה־ר העברית. הטעות הנפוצה היא לתקן יותר מדי: להגות אותה כ־ר מתגלגלת או כ־ג. -- **מה עושים:** אומרים ר עברית רגילה, מהגרון. -- **דוגמאות:** בַּשְתְע'ִל (אני עובד), ע'ַאלִי (יקר) - -```yaml -id: ghain -title: ע' היא ה־ר העברית שלכם -arabic: غ -checks: - - {target: ɣ, heard: [x, χ], severity: 1, feedback: "נהגתה בלי קול, כמו ח עברית. ע' קולית: אמרו ר עברית."} - - {target: ɣ, heard: [r, ɾ], severity: 2, feedback: "נהגתה כ־ר מתגלגלת. ע' היא ה־ר הגרונית של העברית."} - - {target: ɣ, heard: [g, ɡ], severity: 2, feedback: "נהגתה כ־ג. ע' היא חיכוך רציף, כמו ר עברית."} -``` - -### 3.6 ה נהגית תמיד · מבטא -- **למה זה קשה:** בעברית מדוברת ה נבלעת ברוב המקרים. -- **מה עושים:** נשיפה רכה וקצרה, גם באמצע מילה ולפני עיצור, ובלי שום חיכוך. -- **דוגמאות:** אַהְוֶה (קפה), פַאהֵם (מבין) - -```yaml -id: h -title: ה נהגית תמיד -arabic: ه -checks: - - {target: h, heard: ["_"], severity: 2, feedback: "נבלעה. ה נהגית תמיד, גם לפני עיצור."} - - {target: h, heard: [ħ, χ, x], severity: 1, feedback: "נהגתה חזקה מדי. ה היא נשיפה רכה בלבד."} -``` - -### 3.7 ו = w, ב = b, פ = f · מבטא -- **למה זה קשה:** התעתיק העברי עצמו מטעה. ו נקראת v, ב רפה נקראת v, ופ דגושה נקראת p. בערבית אין v ואין p. -- **מה עושים:** - - ו: מעגלים את השפתיים, w. וֵין נהגית wein ולא vein. - - ב: סוגרים את השפתיים, גם בסוף מילה. שוֹבּ ולא שוֹב. - - פ: תמיד f, גם בתחילת מילה. פִנְג'אן ולא פִּנְג'אן. -- **דוגמאות:** וֵין (איפה), אִלְחְסַאבּ (החשבון), פִנְג'אן (כוס קפה) - -```yaml -id: w -title: ו היא w -arabic: و -checks: - - {target: w, heard: [v, ʋ], severity: 2, feedback: "נהגתה v. בערבית אין v: עגלו את השפתיים, w."} ---- -id: b -title: ב היא תמיד b -arabic: ب -checks: - - {target: b, heard: [v, β], severity: 2, feedback: "נהגתה v. בערבית ב היא תמיד b, גם בסוף מילה."} ---- -id: f -title: פ היא תמיד f -arabic: ف -checks: - - {target: f, heard: [p], severity: 2, feedback: "נהגתה p. בערבית אין p: פ היא תמיד f."} -``` - -### 3.8 תנועה ארוכה · דיוק -- **למה זה קשה:** בעברית אין הבדל בין תנועה קצרה לארוכה. בערבית יש, והוא יכול לשנות משמעות. -- **מה עושים:** כשבתעתיק מופיעה א, י או ו אחרי התנועה, היא ארוכה ומחזיקים אותה בערך כפול. -- **זוג מינימלי:** עַלַם (דגל) ≠ עַאלַם (עולם) -- **דוגמאות:** כְּתִיר = ktiir, שוּף = shuuf (תראה) - -הכלל משנה משמעות, אבל מקבל חומרה 1 כי מודל ההגאים מבחין באורך תנועה פחות טוב. ראו חלק 7. - -המודל לא מבחין בין a ל־aː, ולכן ה־scorer מודד זמן: כמה פריימים רועשים יש בין העיצור שלפני התנועה לעיצור שאחריה, ביחס לאותו מרווח סביב התנועות הקצרות של אותו דובר. aː, iː ו־uː בלבד, ורק בין שני עיצורים. תנועה ארוכה שקצרה בבירור מקבלת לכל היותר 60 וטיפ. הפטור של 6.3 #7 (תנועות קרובות) עדיין חל על שאר הבלבולים באורך. - -```yaml -id: vowel_length -title: תנועה קצרה ותנועה ארוכה -checks: - - {target: aː, heard: [a], severity: 1, feedback: "התנועה הארוכה נהגתה קצרה. החזיקו אותה בערך כפול."} - - {target: iː, heard: [i], severity: 1, feedback: "התנועה הארוכה נהגתה קצרה. החזיקו אותה בערך כפול."} - - {target: uː, heard: [u], severity: 1, feedback: "התנועה הארוכה נהגתה קצרה. החזיקו אותה בערך כפול."} - - {target: eː, heard: [e, i], severity: 1, feedback: "התנועה הארוכה נהגתה קצרה. החזיקו אותה בערך כפול."} - - {target: oː, heard: [o, u], severity: 1, feedback: "התנועה הארוכה נהגתה קצרה. החזיקו אותה בערך כפול."} -duration: # the model pools a and aː, so the scorer times the vowel instead (scorer.Scorer._shortened) - severity: 1 - # a long vowel whose span is under this share of the speaker's short-vowel span is said short. Set on the native - # minerva train clips so that about 10% of native long vowels fall under it (dev: 10% aː, 9% iː, 13% uː); - # about half of short vowels do. eː and oː are not timed: natives often say them no longer than e and o. - min_ratio: {aː: 1.0, iː: 0.8, uː: 1.0} - prior_frames: 4 # a short vowel's span in loud frames (native median), for sentences with few of their own - prior_frames_plain: 6 # the same counting every frame, when there is no audio energy - prior_weight: 2 # worth this many of the sentence's own short vowels - score_cap: 60 # the long vowel scores at most this - feedback: "התנועה הארוכה נהגתה קצרה. החזיקו אותה בערך כפול." -``` - -### 3.9 ג' ולא g · דיוק -- **מה עושים:** הוגים ג' כמו ב'ג'ינס', או ז' כמו ב'ז'קט'. שתיהן תקינות. ג רגילה (g) היא מבטא מצרי. -- **דוגמאות:** פִנְג'אן (כוס קפה), גַ'בַל או זַ'בַל (הר) - -```yaml -id: jim -title: ג' ולא g -arabic: ج -checks: - - {target: J, heard: [g, ɡ], severity: 1, feedback: "נהגתה g. בערבית פלסטינית ג' היא dʒ או ז'."} -``` - -### 3.10 שני עיצורים צמודים · דיוק -- **למה זה קשה:** בעברית מדוברת מוסיפים תנועה בין שני עיצורים שבאים ברצף (שווא נע), וכך גם בערבית: katiir במקום ktiir. -- **מה עושים:** מצמידים את שני העיצורים, בלי תנועה ביניהם. בתחילת משפט קשה להגות שווא, ולכן שם מותר שיישמע כמו e קצרה (צירה). -- **דוגמאות:** כְּתִיר = ktiir (לא katiir; בתחילת משפט גם ketiir), מְנִיח = mniiħ, בְּנְשוּף = bnʃuuf - -ה־scorer בודק תנועה שהמודל שומע בבירור בין שני עיצורים של אותה מילה. הוא לא בודק את הצרור שפותח את המשפט (6.3 #16), ולא צירוף עם w או j, שהמעבר אליהם נשמע כמו תנועה גם אצל דוברים ילידיים. - -```yaml -id: cluster -title: שני עיצורים צמודים -insertion: - severity: 1 - min_peak: 0.5 # a vowel the model is at least this sure of ... - min_frames: 2 # ... in at least this many frames (40 ms) between two consonants of one word - score_cap: 60 # the second consonant scores at most this - feedback: "נוספה תנועה בין שני העיצורים. הצמידו אותם, בלי תנועה ביניהם: ktiir ולא katiir. רק בתחילת משפט מותר e קצרה." -``` - -### 3.11 קובוץ ברור · דיוק -- **למה זה קשה:** את השווא מותר להחליש, ודוברי עברית מחלישים גם תנועות קצרות אחרות. אבל u (קובוץ) היא תנועה מובחנת מאוד: מֻחַאמִי (עורך דין) היא muħaami, ולא mħaami. -- **מה עושים:** הוגים את ה־u מלאה וברורה גם כשהיא קצרה, בלי להטעים אותה ובלי לבלוע אותה לשווא. -- **דוגמאות:** מֻחַאמִי = muħaami, בֻּכְּרַה = bukra, מֻשְכִּלֶה = muʃkile - -זו התנועה הקצרה היחידה שה־scorer מסמן (ראו 6.3 #7), ורק אחרי עיצור באותה מילה, לא ו' החיבור. - -```yaml -id: kubutz -title: קובוץ ברור -flag_vowels: [u] # short u after a consonant is flagged, unlike the other short vowels (6.3 #7) -checks: - - {target: u, heard: ["_"], severity: 1, feedback: "ה־u נבלעה. הגו אותה מלאה וברורה גם כשהיא קצרה: muħaami ולא mħaami."} - - {target: u, heard: [e, i, ə], severity: 1, feedback: "ה־u נשמעה כמו e או שווא. u היא תנועה מובחנת: עגלו את השפתיים, muħaami."} -``` - ---- - - -## 4. כללים לתרגול עצמי - -את הכללים האלה האפליקציה עוד לא בודקת. תרגלו אותם מול ההקלטות של המורה. - -| כלל | מה עושים | דוגמה | -|---|---|---| -| **הטעם לא בסוף המילה** | בעברית הטעם בדרך כלל בסוף המילה, ודוברי עברית נוטים להטעים את ההברה האחרונה גם בערבית, בלי צורך. בערבית מטעימים את ההברה הסגורה או הארוכה האחרונה שאינה בסוף המילה. | מֻ־חַא־מִי (עורך דין) = mu-**ĦAA**-mi, לא muħaa-**MI** · מֻשְ־כִּ־לֶה = **MUSH**-kile, מַדְ־רַ־סֶה = **MAD**-rase, בְּתִעְ־מַל = b**tiʕ**-mal | -| **ה סופית היא תנועה** | ה בסוף מילה היא לא אות שקטה. ברוב המילים נהגית e, ואחרי ח, ע, ר ועיצור מודגש נהגית a. בכפר שומעים לפעמים i. | אַהְוֶה, מֻשְכִּלֶה, אבל סַאעַה, מַחַטַּה | -| **עיצור כפול** | מחזיקים את העיצור לפני שממשיכים. ההבדל משנה משמעות. | חַמַּאם (שירותים) ≠ חַמַאם (יונים) | -| **אִל־ מתחברת** | לפני ת, ד, ס, צ, ש, ז, ר, נ, ט ו־ד' ה־ל נעלמת והעיצור מוכפל. | אִסְּסַאעַה (השעה), לא אִלְסַאעַה | - ---- - - -## 5. סימונים: הגאים מופשטים - -ב־`phrases.py` כל אות שנהגית אחרת בעיר ובכפר נכתבת **כסימן מופשט** ולא כהגה ממומש. ה־scorer פורש כל סימן לכל המימושים התקינים שלו. כך נפתרות הכפילויות בשני הכיוונים: -- **אות אחת, כמה הגאים:** ق יכולה להיות ʔ, k או q. -- **הגה אחד, כמה אותיות:** ʔ יכול לבוא מ־ق או מ־ء, k יכול לבוא מ־ك או מ־ق כפרית, t יכול לבוא מ־ت או מ־ث עירונית. - -```yaml -# archiphonemes: symbol in phrases.py -> realisations that are correct Palestinian speech -archiphonemes: - Q: {arabic: ق, urban: [ʔ], rural: [k], also_ok: [q], optional: [g]} # q: Druze / MSA. g: Bedouin, off by default - K: {arabic: ك, urban: [k], rural: [k, tʃ], also_ok: []} - TH: {arabic: ث, urban: [t, s], rural: [θ], also_ok: []} # s in learned words (ثقافة) - DH: {arabic: ذ, urban: [d, z], rural: [ð], also_ok: []} # z in learned words - ZH: {arabic: ظ, urban: [dˤ, zˤ], rural: [ðˤ], also_ok: []} - J: {arabic: ج, urban: [dʒ, ʒ], rural: [dʒ], also_ok: [ʒ]} -# plain phonemes are NOT expanded: ʔ written as ʔ (hamza) never accepts k; k written as k (from ك in urban-only words) uses K -dialects_enabled: [urban, rural] -optional_realisations_enabled: false # true -> Bedouin g for Q -``` - -**דוגמאות לכתיבה ב־`phrases.py`:** - -| עברית | כתיבה | יתקבל | -|---|---|---| -| קפה | `Q a h w e` | ʔahwe, kahwe, qahwe | -| כמה | `Q a d d eː ʃ` | ʔaddeeʃ, kaddeeʃ | -| כלב | `K a l b` | kalb, tʃalb | -| לב | `Q a l b` | ʔalb, kalb, qalb | -| הרבה | `K t iː r` (במקור ث) | ktiir, tʃtiir | -| שלוש | `TH l aː TH e` | tlaate, θlaaθe, וגם צירוף מעורב | -| שלום | `ʔ a h l a` (ء, לא ق) | ʔahla. ההגייה kahla **לא** מתקבלת | - -**בניית המטרות:** לכל וריאנט במשפט ה־scorer בונה שתי מחרוזות מטרה, עירונית וכפרית. כל סימן מופשט ממומש במימוש הראשון שלו באותו ניב. כל מחרוזת עוברת יישור כפוי (forced alignment), ונבחרת זו שסבירותה גבוהה יותר. ערבוב ניבים בתוך משפט מטופל בשלב ההערכה, לפי סעיף 6.1. - ---- - - -## 6. היוריסטיקות להערכת הגה - -### 6.1 מחלקות שקילות -לכל הגה מטרה שנבנה מסימן מופשט, ההסתברות היא **סכום ההסתברויות של כל המימושים התקינים** בחלון שלו, ולא רק של המימוש שנבחר ביישור. - -לדוגמה, ב־`Q`: אם P(ʔ)=0.35 ו־P(k)=0.40, ה־GOP מחושב מול 0.75. התלמיד אמר מימוש תקין, גם אם ביישור נבחרה המחרוזת העירונית. הגה אחר מהמחלקה לא נחשב "מתחרה". - -### 6.2 סף לשגיאה -הגה מסומן כשגוי רק אם מתקיימים שלושה תנאים: -1. הציון שלו נמוך מ־`flag_below`. -2. המתחרה החזק ביותר בחלון **לא** במחלקת השקילות של המטרה. -3. היחס בין המתחרה למטרה עובר את מרווח הביטחון של הזוג (`margin`). - -לזוגות שהמודל מבלבל גם אצל דוברים ילידיים יש מרווח גדול יותר, כדי לא להעניש תלמיד על חולשה של המודל. - -**ראיה להחלפה.** המתחרה נקרא בשמו (טיפ, ו"נשמעה כמו" באות) רק כשיש עליו ראיה בהקלטה: הוא גובר על ההגה הנכון לפחות בפריים אחד, או מגיע להסתברות 0.3. מתחרה שרק קצת סביר יותר ממטרה חלשה, ולא גובר באף פריים, אינו ממצא: הציון נשאר, אבל אין טיפ, והאות אומרת רק שההגה "לא נשמעה בבירור". - -```yaml -scoring: - flag_below: 50 # phoneme score 0..100 - unrelated_ratio: 0.5 # weighted edit distance of greedy decode vs target (grading-pipeline §8) -> "different sentence?" - accept_alt_min_post: 0.20 # an accepted realisation counts if its peak posterior >= this - accepted_score_floor: 90 # score given to a phoneme realised as an accepted alternative - deletion: {target_peak_max: 0.05, competitor_peak_max: 0.20} - gop_leniency: 0.35 # a competitor twice as likely still scores about 78 - vowel_weight: 0.5 - word_min_weight: 0.35 # weakest phoneme contributes 35%; the other 65% is the word's phoneme mean - severity_weight: {1: 1.0, 2: 1.5, 3: 2.0} - sentence_score: "0.5 * weighted_phoneme_mean + 0.5 * mean(word_score)" - bands: {excellent: 75, almost: 60} # below 60 = try again - max_tips: 3 - # default margin: flag when log P(competitor) - log P(target) >= log(1.0), i.e. competitor at least as likely - margin_default: 0.0 - margins: # stricter pairs (natural log ratio): competitor must exceed target by e^margin - - {target: [sˤ, tˤ, dˤ, zˤ, ðˤ], heard: [s, t, d, z, ð], margin: 0.7} # ~2x: emphasis is weak in the base model - - {target: [h], heard: ["_"], margin: 0.7} # h is short and quiet, natives reduce it too - - {target: [aː, iː, uː, eː, oː], heard: [a, i, u, e, o], margin: 1.1} # ~3x: length is unreliable from CTC posteriors - - {target: [ħ], heard: [h], margin: 0.4} - # pairs the model tells apart in native speech (lab, 2026-10-19: natives' ح heard as خ 2%, a real خ caught 64%): - # a native reference never forgives them, since a TTS voice can say them the learner's way (see 6.4) - baseline_protected: - - {target: ħ, heard: x} -``` - -### 6.3 מקרים מיוחדים: לא מסמנים שגיאה - -| # | מקרה | למה | כלל ל־scorer | -|---|---|---|---| -| 1 | ʔ (ء) נשמטת בתחילת משפט או אחרי תנועה | גם דוברים ילידיים משמיטים: `أنا` / `انا`, ʔana → ana | מחיקה של ʔ מ־ء: לא מסמנים | -| 2 | ʔ מ־`Q` נשמטת בין תנועות | עצירה קצרה מאוד בדיבור מהיר | מחיקה: לא מסמנים. החלפה ב־k: תקינה (כפרי) | -| 3 | i ב־il־ נשמטת אחרי תנועה | חיבור מילים: fi l-beet | מחיקת i ראשונה במילה שמתחילה ב־il: לא מסמנים | -| 4 | אִל־ בלי התבוללות (il-saaʕa במקום is-saaʕa) | שגיאה קלה, וה־CTC לא מבחין בה באמינות | לא מסמנים. הכלל מופיע במדריך בלבד | -| 5 | e מול a ב־ة סופית, וגם i בכפר | שונות לגיטימית | בהגה אחרון במילה שמקורה ב־ة: {a, e, i} שקולים | -| 6 | o מול u בכינוי נסתר (בֵּיתוֹ/בֵּיתוּ) | הבדל בין ניבים | בהגה אחרון של כינוי: {o, u} שקולים | -| 7 | שונות בתנועות קצרות (miʃ/muʃ, i/e, u/o) | שונות בין דוברים | תנועות קצרות לא מסומנות, רק אורך דרך כלל 3.8. חריג: u אחרי עיצור שנשמטה או נשמעה כ־e, i או שווא (3.11). u כ־o היא עדיין תנועה קרובה | -| 8 | eː/aj ו־oː/aw (בֵּית/בַּיְת) | הגייה כפרית או ותיקה | {eː, aj} ו־{oː, aw} שקולים | -| 9 | תנועה כהה ליד עיצור מודגש | זו בדיוק ההגייה הנכונה | אין כלל שמסמן איכות תנועה | -| 10 | עיצור כפול (חַמַּאם) | ה־CTC מכווץ כפילות לטוקן אחד, ואין מדידת משך אמינה | לא נבדק. הכלל מופיע במדריך בלבד | -| 11 | מתחרה שהוא ההגה השכן | מעבר בין הגאים (קו־ארטיקולציה) | המתחרה לא נלקח מבין תוויות ההגאים השכנים | -| 12 | ערבוב ניבים במשפט (אַהְוֶה + צ'ְתִיר) | שני המימושים תקינים | לא מורידים ציון. אפשר להציג למורה כ־`info: mixed_dialect` | -| 13 | g במקום `Q` | מבטא בדואי ודרומי | מתקבל רק אם `optional_realisations_enabled: true`. אחרת סומן כשגיאה כללית, חומרה 1 | -| 14 | חזרה, היסוס או התחלה מחדש | הפענוח רחוק מכל וריאנט | `unrelated=true`, ציון עד 30, בלי טיפים | -| 15 | הקלטה קצרה מדי או שקטה | אין חלון לכל הגה | `error: too_short` | -| 16 | שווא בתחילת משפט נשמע כ־e (ketiir במקום ktiir) | קשה להגות צרור עיצורים בפתיחת המשפט | לא מסמנים: הצרור הראשון של המשפט פטור מכלל 3.10. באמצע משפט תנועה כזו מסומנת. בבדיקת "משפט אחר" היא נספרת כהבדל אחד | -| 28 | b בסוף מילה מזוהה כ־p לפני עיצור אטום במילה הבאה | אובדן קוליות חלקי בגבול מילים גורם למודל הרב־לשוני לשמוע p גם אצל דובר ילידי: `ħaliib tˤaare` | רק `b→p` בסוף מילה ולפני עיצור אטום מקבל 100. באמצע מילה, בסוף משפט או לפני עיצור קולי אין פטור | -| 29 | תנועה קצרה בסוף מילה נראית מחוקה לפני תנועה בתחילת המילה הבאה | בדיבור מחובר המעבר שייך אקוסטית לשתי המילים, אבל CTC עשוי לשייך אותו רק לאחת: `ʕala ʔiidak` | תנועה קצרה מחוקה בסוף מילה לפני V או ʔ+V מקבלת לפחות 90. באמצע מילה או לפני עיצור אין פטור | -| 30 | תנועה קצרה אינה מסומנת אך ציון אקוסטי נמוך שלה מוריד את המילה | הצגה וחישוב חייבים ליישם אותה מדיניות; אין היגיון ב„לא סומן” עם ציון 1 | תנועה קצרה שאינה נבדקת מקבלת לפחות 90. `u` אחרי עיצור באותה מילה נשארת נבדקת לפי 3.11 | -| 31 | העיצור הראשון במשפט חלש, ומתחרה מופיע לפניו בפריים אחד בלבד | פוצץ בתחילת מבע (סגר שקט ופיצוץ קצר) נשמע חלש למודל, ונשימה או רעש מיקרופון בפריים בודד גוברים עליו בשם שרירותי: `طاولتنا`, tˤ שנשמעה כ־j | עיצור ראשון שנשמע (לא מחוק, שיא ≥ 5%) ומתחרה בפריים אחד בלבד: לפחות 90, לא מסומן. השמטה, או מתחרה בשני פריימים ומעלה, נבדקים כרגיל. ʔ מטופלת בכללים 1–2 | -| 32 | t נשמעה כ־d, או d כ־t | בעברית יש אותה הבחנה (ת / ד), ולומדים כמעט לא מחליפים ביניהן. המודל הרב־לשוני כן: t אחרי n (`انتي`) או בין תנועות (`فتح`) מקבלת קוליות חלקית בדיבור טבעי ונשמעת לו d | `t→d` ו־`d→t` מקבלות 100. עיצורים מודגשים (tˤ, dˤ) לא בכלל הזה ונבדקים לפי 3.3 | - -במאגר התרגול `انا` מיוצגת במפורש בשני וריאנטים תקינים, `ʔ a n a` ו־`a n a`. כך השמטה טבעית נבחרת כווריאנט מלא במקום ליישר בכוח את תחילת ההקלטה מול ʔ ולפרש רעש נשימה כ־`h`. - -### 6.4 קו בסיס ילידי -אותו מודל מקשיב גם לקולות ילידיים (TTS) שאומרים את המשפט. הגה שהמודל לא מזהה גם אצלם הוא חולשה של המודל, לא של התלמיד, ולכן ציון ההגה של התלמיד נמדד מול הציון שהקול קיבל עליו. - -- **רק כשגם הקול איבד את ההגה.** קול מוחל על החלפה של התלמיד רק כשגם אצלו המודל לא זיהה את ההגה, בכל דרך שהיא (ע שנשמעת כמו ء, או ח שנשמעת כמו ה). כשהקול שמר על ההגה, ההחלפה של התלמיד נשארת. -- **זוגות מוגנים** (`baseline_protected` בבלוק שב־6.2) לא נמחלים אף פעם. המודל מבחין בהם אצל דוברים ילידיים (בבדיקת המעבדה, ח נשמעה כמו ח' רק אצל 2% מהם, וח' אמיתית נתפסה ב־64%), ואילו קול TTS עלול להגות אותם בדיוק כמו התלמיד. מוסיפים זוג רק אחרי בדיקה מול הקלטות ילידיות. -- **הגה חלש בלי החלפה** (התלמיד שמר עליו, רק חלש) נמדד מול הקול כמו קודם. - ---- - - -## 7. מה ה־scorer יכול לזהות - -הזיהוי תלוי באוצר ההגאים של המודל. ב־`/api/health` השדה `rules` מחזיר לכל כלל אם הוא ניתן לזיהוי. אם הגה מטרה לא קיים במודל וממופה להגה אחר, הכלל לא יסומן, כי לא תהיה לו שגיאה אפשרית. לדוגמה, אם sˤ ממופה ל־s, כלל ההדגשה לא יופעל. - -| כלל | אמינות צפויה עם מודל הבסיס | הערה | -|---|---|---| -| ħ מול χ | גבוהה | שני ההגאים קיימים באוצר, והם רחוקים אקוסטית | -| ʕ מול ʔ | בינונית־גבוהה | | -| r מול ʁ | גבוהה | | -| w מול v, b מול v, f מול p | גבוהה | | -| עיצורים מודגשים | נמוכה־בינונית | לכן `margin` גבוה | -| ה נבלעת | בינונית | | -| אורך תנועה | נמוכה | חומרה 1 ו־`margin` גבוה | -| טעם, כפילות, התבוללות | לא ניתן | מופיעים במדריך בלבד | -| תנועה שנוספה בין עיצורים (katiir) | בינונית | נבדקת רק כשהמודל בטוח בתנועה (0.5) לפחות 40 ms. ליד ח, ע ו־ה המעבר עלול להישמע כתנועה גם אצל דובר ילידי | -| קובוץ שנבלע או נהגה כ־e (mħaami) | בינונית | תנועות קצרות הן החלק החלש של המודל, ולכן חומרה 1 | - ---- - - -## 8. תוויות של הגאים שנשמעו - -אלה התוויות בעברית שמוצגות לתלמיד במקום IPA. כל סמל שמופיע ב־`heard:` בכללים חייב להופיע כאן. - -```yaml -heard_labels: - "_": לא נשמע - χ: ח עברית - x: ח עברית - ʁ: ר עברית - ɣ: ר עברית - h: ה - ɦ: ה - ħ: ח - ʔ: א - k: כּ - q: ק - g: g - ɡ: g - v: v - ʋ: v - β: v - p: p - s: ס רגילה - t: ת רגילה - d: ד רגילה - z: ז רגילה - ð: th קולית - ts: צ עברית (ts) - t͡s: צ עברית (ts) - r: ר מתגלגלת - ɾ: ר מתגלגלת - a: תנועה קצרה - aː: תנועה - i: תנועה קצרה - u: תנועה קצרה - e: תנועה קצרה - o: תנועה קצרה - ə: שווא -``` - ---- - -## שינויים לעומת הקוד הנוכחי - -הסעיפים האלה מתארים מה צריך לשנות ב־`app/rules.py`, ב־`app/scorer.py` וב־`app/phrases.py` כדי שיתאימו למסמך: - -1. **כלל ק עירונית (`qaf`) מבוטל כשגיאה.** בגלל ש־kahwe תקינה בכפר, ق הופכת למחלקת שקילות `Q`. -2. **סימנים מופשטים חדשים:** `K`, `TH`, `DH`, `ZH` ו־`J` מצטרפים ל־`Q`. הפרופיל הבודד מוחלף ב־`dialects_enabled: [urban, rural]`. -3. **ה־GOP מחושב מול סכום ההסתברויות של מחלקת השקילות** (סעיף 6.1), במקום ה"החלפה בדיעבד" הנוכחית, שבה ציון ההגה פשוט מוגבה ל־90. -4. **מרווחים לפי זוג** (סעיף 6.2) ו**מקרים מיוחדים 1–8** הם חדשים. -5. **תעתיק:** צ' (tʃ), ת' (θ) ו־ד״ (ð) נוספים. ד' נשארת ד מודגשת (ض). diff --git a/static/assets/trace-mock.json b/static/assets/trace-mock.json deleted file mode 100644 index 20083d9202747bc675f7273170b0d6835cc48cb8..0000000000000000000000000000000000000000 --- a/static/assets/trace-mock.json +++ /dev/null @@ -1,7515 +0,0 @@ -{ - "score": 96, - "variant": { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "tr": "וֵין סִידַכּ סִידִי קַעַד פִי בֵּיתוֹ", - "ipa": "w eː n | s iː d a k | s iː d i | ʔ a ʕ a d | f i | b eː t o" - }, - "heard_ipa": "w e n s i d a h k s i d i ʔ a ʔ a d f i b e t o", - "heard_tr": "וֵין סִידַכּ סִידִי קַקַד פִי בֵּיתוֹ", - "unrelated": false, - "words": [ - { - "ar": "وين", - "tr": "וֵין", - "heard_tr": "וֵין", - "score": 100.0, - "letters": [ - { - "t": "וֵי", - "score": 100.0 - }, - { - "t": "ן", - "score": 100.0 - } - ], - "phonemes": [ - { - "ipa": "w", - "word": 0, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "eː", - "word": 0, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "n", - "word": 0, - "score": 100.0, - "flagged": false, - "heard": null - } - ] - }, - { - "ar": "سيدك؟", - "tr": "סִידַכּ", - "heard_tr": "סִידַכּ", - "score": 100.0, - "letters": [ - { - "t": "סִי", - "score": 100.0 - }, - { - "t": "דַ", - "score": 100.0 - }, - { - "t": "כּ", - "score": 100.0 - } - ], - "phonemes": [ - { - "ipa": "s", - "word": 1, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "iː", - "word": 1, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "d", - "word": 1, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "a", - "word": 1, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "k", - "word": 1, - "score": 100.0, - "flagged": false, - "heard": null, - "sound": "K", - "realised": "k" - } - ] - }, - { - "ar": "سيدي", - "tr": "סִידִי", - "heard_tr": "סִידִי", - "score": 100.0, - "letters": [ - { - "t": "סִי", - "score": 100.0 - }, - { - "t": "דִי", - "score": 100.0 - } - ], - "phonemes": [ - { - "ipa": "s", - "word": 2, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "iː", - "word": 2, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "d", - "word": 2, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "i", - "word": 2, - "score": 100.0, - "flagged": false, - "heard": null - } - ] - }, - { - "ar": "قعد", - "tr": "קַעַד", - "heard_tr": "קַקַד", - "score": 73.6, - "letters": [ - { - "t": "קַ", - "score": 100.0 - }, - { - "t": "עַ", - "score": 45.0, - "why": "ע (ع) נשמעה כמו עצירה סותמית (ء / ق)", - "tip": "נהגתה כמו א (סגירה של הגרון). ע לא סוגרת את הגרון, היא מכווצת אותו, עם קול." - }, - { - "t": "ד", - "score": 100.0 - } - ], - "phonemes": [ - { - "ipa": "ʔ", - "word": 3, - "score": 100.0, - "flagged": false, - "heard": null, - "sound": "Q", - "realised": "ʔ" - }, - { - "ipa": "a", - "word": 3, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "ʕ", - "word": 3, - "score": 45.0, - "flagged": true, - "heard": "ʔ" - }, - { - "ipa": "a", - "word": 3, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "d", - "word": 3, - "score": 100.0, - "flagged": false, - "heard": null - } - ] - }, - { - "ar": "في", - "tr": "פִי", - "heard_tr": "פִי", - "score": 100.0, - "letters": [ - { - "t": "פִי", - "score": 100.0 - } - ], - "phonemes": [ - { - "ipa": "f", - "word": 4, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "i", - "word": 4, - "score": 100.0, - "flagged": false, - "heard": null - } - ] - }, - { - "ar": "بيته.", - "tr": "בֵּיתוֹ", - "heard_tr": "בֵּיתוֹ", - "score": 100.0, - "letters": [ - { - "t": "בֵּי", - "score": 100.0 - }, - { - "t": "תוֹ", - "score": 100.0 - } - ], - "phonemes": [ - { - "ipa": "b", - "word": 5, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "eː", - "word": 5, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "t", - "word": 5, - "score": 100.0, - "flagged": false, - "heard": null - }, - { - "ipa": "o", - "word": 5, - "score": 100.0, - "flagged": false, - "heard": null - } - ] - } - ], - "tips": [ - { - "title": "ע (ع)", - "text": "נהגתה כמו א (סגירה של הגרון). ע לא סוגרת את הגרון, היא מכווצת אותו, עם קול.", - "key": "ʕ>ʔ", - "severity": 3, - "word": "قعد", - "word_tr": "קַעַד", - "score": 45 - } - ], - "issues": [], - "references": 2, - "dialect": "urban", - "calibrated": [ - { - "expected": "ʕ", - "heard": "ʔ", - "from": 4, - "to": 45 - } - ], - "phoneme_heatmap": { - "duration_ms": 2620, - "frames": 131, - "frame_bins": 131, - "rows": [ - { - "phoneme": "w", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 85, - 85, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "eː", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 90, - 90, - 90, - 90, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 4, - 1, - 0, - 1, - 5, - 5, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 90, - 90, - 90, - 90, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "n", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 80, - 80, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "s", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 90, - 90, - 90, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 90, - 90, - 90, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "i", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 88, - 88, - 88, - 88, - 0, - 0, - 1, - 1, - 0, - 0, - 4, - 1, - 0, - 1, - 5, - 5, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 90, - 90, - 90, - 90, - 0, - 0, - 1, - 1, - 0, - 0, - 81, - 81, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 86, - 86, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "d", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 80, - 80, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 85, - 85, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 85, - 85, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "a", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 45, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 85, - 85, - 0, - 0, - 1, - 1, - 0, - 0, - 85, - 85, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "k/tʃ", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 4, - 1, - 0, - 1, - 32, - 32, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "k/ʔ/q", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 2, - 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 2, - 2, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 2, - 2, - 1, - 1, - 6, - 1, - 0, - 1, - 35, - 35, - 2, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 2, - 2, - 1, - 1, - 2, - 2, - 0, - 0, - 0, - 0, - 0, - 0, - 83, - 83, - 1, - 1, - 2, - 2, - 1, - 1, - 81, - 81, - 1, - 1, - 2, - 2, - 1, - 1, - 2, - 2, - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 2, - 2, - 1, - 1, - 2, - 2, - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 2, - 2, - 1, - 1, - 2, - 2, - 2, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "ʕ", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "f", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 85, - 85, - 85, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "b", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 85, - 85, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "t", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 85, - 85, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "o", - "expected": true, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 85, - 85, - 85, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "h", - "expected": false, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 96, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "ʔ", - "expected": false, - "values": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 82, - 82, - 0, - 0, - 1, - 1, - 0, - 0, - 80, - 80, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - { - "phoneme": "blank", - "expected": false, - "values": [ - 97, - 97, - 97, - 97, - 1, - 1, - 95, - 95, - 0, - 0, - 0, - 0, - 95, - 95, - 1, - 1, - 97, - 97, - 97, - 97, - 97, - 97, - 0, - 0, - 0, - 95, - 95, - 0, - 0, - 0, - 0, - 95, - 95, - 1, - 1, - 95, - 95, - 2, - 90, - 0, - 90, - 2, - 2, - 80, - 97, - 97, - 97, - 97, - 97, - 97, - 0, - 0, - 0, - 95, - 95, - 0, - 0, - 0, - 0, - 95, - 95, - 1, - 1, - 95, - 95, - 1, - 1, - 97, - 97, - 97, - 97, - 97, - 97, - 1, - 1, - 95, - 95, - 1, - 1, - 95, - 95, - 1, - 1, - 95, - 95, - 1, - 1, - 95, - 95, - 1, - 1, - 97, - 97, - 97, - 97, - 97, - 97, - 1, - 1, - 1, - 95, - 95, - 1, - 1, - 97, - 97, - 97, - 97, - 97, - 97, - 1, - 1, - 95, - 95, - 0, - 0, - 0, - 0, - 95, - 95, - 1, - 1, - 95, - 95, - 1, - 1, - 1, - 97, - 97, - 97, - 97 - ] - } - ], - "tracks": { - "expected": [ - { - "phoneme": "w", - "start": 4, - "end": 6 - }, - { - "phoneme": "eː", - "start": 8, - "end": 12 - }, - { - "phoneme": "n", - "start": 14, - "end": 16 - }, - { - "phoneme": "s", - "start": 22, - "end": 25 - }, - { - "phoneme": "iː", - "start": 27, - "end": 31 - }, - { - "phoneme": "d", - "start": 33, - "end": 35 - }, - { - "phoneme": "a", - "start": 37, - "end": 38 - }, - { - "phoneme": "k", - "start": 41, - "end": 43 - }, - { - "phoneme": "s", - "start": 50, - "end": 53 - }, - { - "phoneme": "iː", - "start": 55, - "end": 59 - }, - { - "phoneme": "d", - "start": 61, - "end": 63 - }, - { - "phoneme": "i", - "start": 65, - "end": 67 - }, - { - "phoneme": "ʔ", - "start": 73, - "end": 75 - }, - { - "phoneme": "a", - "start": 77, - "end": 79 - }, - { - "phoneme": "ʕ", - "start": 81, - "end": 82 - }, - { - "phoneme": "a", - "start": 85, - "end": 87 - }, - { - "phoneme": "d", - "start": 89, - "end": 91 - }, - { - "phoneme": "f", - "start": 97, - "end": 100 - }, - { - "phoneme": "i", - "start": 102, - "end": 104 - }, - { - "phoneme": "b", - "start": 110, - "end": 112 - }, - { - "phoneme": "eː", - "start": 114, - "end": 118 - }, - { - "phoneme": "t", - "start": 120, - "end": 122 - }, - { - "phoneme": "o", - "start": 124, - "end": 127 - } - ], - "heard": [ - { - "phoneme": "w", - "start": 4, - "end": 6 - }, - { - "phoneme": "e", - "start": 8, - "end": 12 - }, - { - "phoneme": "n", - "start": 14, - "end": 16 - }, - { - "phoneme": "s", - "start": 22, - "end": 25 - }, - { - "phoneme": "i", - "start": 27, - "end": 31 - }, - { - "phoneme": "d", - "start": 33, - "end": 35 - }, - { - "phoneme": "a", - "start": 37, - "end": 38 - }, - { - "phoneme": "h", - "start": 39, - "end": 40 - }, - { - "phoneme": "k", - "start": 41, - "end": 43 - }, - { - "phoneme": "s", - "start": 50, - "end": 53 - }, - { - "phoneme": "i", - "start": 55, - "end": 59 - }, - { - "phoneme": "d", - "start": 61, - "end": 63 - }, - { - "phoneme": "i", - "start": 65, - "end": 67 - }, - { - "phoneme": "ʔ", - "start": 73, - "end": 75 - }, - { - "phoneme": "a", - "start": 77, - "end": 79 - }, - { - "phoneme": "ʔ", - "start": 81, - "end": 83 - }, - { - "phoneme": "a", - "start": 85, - "end": 87 - }, - { - "phoneme": "d", - "start": 89, - "end": 91 - }, - { - "phoneme": "f", - "start": 97, - "end": 100 - }, - { - "phoneme": "i", - "start": 102, - "end": 104 - }, - { - "phoneme": "b", - "start": 110, - "end": 112 - }, - { - "phoneme": "e", - "start": 114, - "end": 118 - }, - { - "phoneme": "t", - "start": 120, - "end": 122 - }, - { - "phoneme": "o", - "start": 124, - "end": 127 - } - ] - } - }, - "trace": { - "config": { - "leniency": 0.35, - "confident_peak": 0.3, - "consonant_window": 4, - "deleted_peak": 0.05, - "deleted_comp": 0.2, - "flag_below": 50.0, - "flag_below_sound": {}, - "issue_below": 90.0, - "near_vowel_floor": 90.0, - "word_min_weight": 0.35, - "vowel_weight": 0.5, - "unrelated_ratio": 0.65, - "unrelated_cap": 30.0, - "letter_good": 75, - "max_tips": 3, - "max_issues": 8, - "ref_min_mean": 60.0, - "margin_default": 0.0, - "ayin_deleted_floor": 60.0, - "onset_transient_floor": 90.0, - "margins": [ - { - "target": "dˤ", - "heard": "d", - "margin": 0.7 - }, - { - "target": "dˤ", - "heard": "s", - "margin": 0.7 - }, - { - "target": "dˤ", - "heard": "t", - "margin": 0.7 - }, - { - "target": "dˤ", - "heard": "z", - "margin": 0.7 - }, - { - "target": "dˤ", - "heard": "ð", - "margin": 0.7 - }, - { - "target": "h", - "heard": "_", - "margin": 0.7 - }, - { - "target": "sˤ", - "heard": "d", - "margin": 0.7 - }, - { - "target": "sˤ", - "heard": "s", - "margin": 0.7 - }, - { - "target": "sˤ", - "heard": "t", - "margin": 0.7 - }, - { - "target": "sˤ", - "heard": "z", - "margin": 0.7 - }, - { - "target": "sˤ", - "heard": "ð", - "margin": 0.7 - }, - { - "target": "tˤ", - "heard": "d", - "margin": 0.7 - }, - { - "target": "tˤ", - "heard": "s", - "margin": 0.7 - }, - { - "target": "tˤ", - "heard": "t", - "margin": 0.7 - }, - { - "target": "tˤ", - "heard": "z", - "margin": 0.7 - }, - { - "target": "tˤ", - "heard": "ð", - "margin": 0.7 - }, - { - "target": "zˤ", - "heard": "d", - "margin": 0.7 - }, - { - "target": "zˤ", - "heard": "s", - "margin": 0.7 - }, - { - "target": "zˤ", - "heard": "t", - "margin": 0.7 - }, - { - "target": "zˤ", - "heard": "z", - "margin": 0.7 - }, - { - "target": "zˤ", - "heard": "ð", - "margin": 0.7 - }, - { - "target": "ðˤ", - "heard": "d", - "margin": 0.7 - }, - { - "target": "ðˤ", - "heard": "s", - "margin": 0.7 - }, - { - "target": "ðˤ", - "heard": "t", - "margin": 0.7 - }, - { - "target": "ðˤ", - "heard": "z", - "margin": 0.7 - }, - { - "target": "ðˤ", - "heard": "ð", - "margin": 0.7 - }, - { - "target": "ħ", - "heard": "h", - "margin": 0.4 - } - ], - "exemptions": [ - "glottal_dropped", - "onset_transient", - "article_vowel_elided", - "near_vowel", - "final_b_devoicing", - "word_final_vowel_linking", - "unmarked_short_vowel" - ], - "insertion": { - "score_cap": 60, - "min_peak": 0.5, - "min_frames": 2 - }, - "disabled": [] - }, - "variants": [ - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w eː n | s iː d a k | s iː d i | ʔ a ʕ a d | f i | b eː t o", - "logp": -30.3, - "chosen": true, - "tokens": [ - "w", - "eː", - "n", - "s", - "iː", - "d", - "a", - "K", - "s", - "iː", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "eː", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w a j n | s iː d a k | s iː d i | ʔ a ʕ a d | f i | b a j t o", - "logp": -74.57, - "chosen": false, - "tokens": [ - "w", - "a", - "j", - "n", - "s", - "iː", - "d", - "a", - "K", - "s", - "iː", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "a", - "j", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w eː n | s iː d a k | s iː d i | q a ʕ a d | f i | b eː t o", - "logp": -30.3, - "chosen": false, - "tokens": [ - "w", - "eː", - "n", - "s", - "iː", - "d", - "a", - "K", - "s", - "iː", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "eː", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w a j n | s iː d a k | s iː d i | q a ʕ a d | f i | b a j t o", - "logp": -74.57, - "chosen": false, - "tokens": [ - "w", - "a", - "j", - "n", - "s", - "iː", - "d", - "a", - "K", - "s", - "iː", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "a", - "j", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w eː n | s iː d a k | dʒ i d d i | ʔ a ʕ a d | f i | b eː t o", - "logp": -46.89, - "chosen": false, - "tokens": [ - "w", - "eː", - "n", - "s", - "iː", - "d", - "a", - "K", - "dʒ", - "i", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "eː", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w a j n | s iː d a k | dʒ i d d i | ʔ a ʕ a d | f i | b a j t o", - "logp": -91.16, - "chosen": false, - "tokens": [ - "w", - "a", - "j", - "n", - "s", - "iː", - "d", - "a", - "K", - "dʒ", - "i", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "a", - "j", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w eː n | s iː d a k | dʒ i d d i | q a ʕ a d | f i | b eː t o", - "logp": -46.89, - "chosen": false, - "tokens": [ - "w", - "eː", - "n", - "s", - "iː", - "d", - "a", - "K", - "dʒ", - "i", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "eː", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w a j n | s iː d a k | dʒ i d d i | q a ʕ a d | f i | b a j t o", - "logp": -91.16, - "chosen": false, - "tokens": [ - "w", - "a", - "j", - "n", - "s", - "iː", - "d", - "a", - "K", - "dʒ", - "i", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "a", - "j", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w eː n | dʒ i d d a k | s iː d i | ʔ a ʕ a d | f i | b eː t o", - "logp": -46.89, - "chosen": false, - "tokens": [ - "w", - "eː", - "n", - "dʒ", - "i", - "d", - "a", - "K", - "s", - "iː", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "eː", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w a j n | dʒ i d d a k | s iː d i | ʔ a ʕ a d | f i | b a j t o", - "logp": -91.16, - "chosen": false, - "tokens": [ - "w", - "a", - "j", - "n", - "dʒ", - "i", - "d", - "a", - "K", - "s", - "iː", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "a", - "j", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w eː n | dʒ i d d a k | s iː d i | q a ʕ a d | f i | b eː t o", - "logp": -46.89, - "chosen": false, - "tokens": [ - "w", - "eː", - "n", - "dʒ", - "i", - "d", - "a", - "K", - "s", - "iː", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "eː", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w a j n | dʒ i d d a k | s iː d i | q a ʕ a d | f i | b a j t o", - "logp": -91.16, - "chosen": false, - "tokens": [ - "w", - "a", - "j", - "n", - "dʒ", - "i", - "d", - "a", - "K", - "s", - "iː", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "a", - "j", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w eː n | dʒ i d d a k | dʒ i d d i | ʔ a ʕ a d | f i | b eː t o", - "logp": -63.48, - "chosen": false, - "tokens": [ - "w", - "eː", - "n", - "dʒ", - "i", - "d", - "a", - "K", - "dʒ", - "i", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "eː", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w a j n | dʒ i d d a k | dʒ i d d i | ʔ a ʕ a d | f i | b a j t o", - "logp": -107.75, - "chosen": false, - "tokens": [ - "w", - "a", - "j", - "n", - "dʒ", - "i", - "d", - "a", - "K", - "dʒ", - "i", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "a", - "j", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w eː n | dʒ i d d a k | dʒ i d d i | q a ʕ a d | f i | b eː t o", - "logp": -63.48, - "chosen": false, - "tokens": [ - "w", - "eː", - "n", - "dʒ", - "i", - "d", - "a", - "K", - "dʒ", - "i", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "eː", - "t", - "o" - ] - }, - { - "ar": "وين سيدك؟ سيدي قعد في بيته.", - "ipa": "w a j n | dʒ i d d a k | dʒ i d d i | q a ʕ a d | f i | b a j t o", - "logp": -107.75, - "chosen": false, - "tokens": [ - "w", - "a", - "j", - "n", - "dʒ", - "i", - "d", - "a", - "K", - "dʒ", - "i", - "d", - "i", - "Q", - "a", - "ʕ", - "a", - "d", - "f", - "i", - "b", - "a", - "j", - "t", - "o" - ] - } - ], - "frames": { - "count": 131, - "ms": 20.0, - "energy": [ - 0.02, - 0.02, - 0.02, - 0.02, - 0.55, - 0.55, - 0.15, - 0.15, - 1.0, - 1.0, - 1.0, - 1.0, - 0.15, - 0.15, - 0.5, - 0.5, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.45, - 0.45, - 0.45, - 0.15, - 0.15, - 0.95, - 0.95, - 0.95, - 0.95, - 0.15, - 0.15, - 0.5, - 0.5, - 0.15, - 0.15, - 0.85, - 0.2, - 0.04, - 0.1, - 0.4, - 0.4, - 0.1, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.45, - 0.45, - 0.45, - 0.15, - 0.15, - 0.95, - 0.95, - 0.95, - 0.95, - 0.15, - 0.15, - 0.5, - 0.5, - 0.15, - 0.15, - 0.8, - 0.8, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.4, - 0.4, - 0.15, - 0.15, - 0.9, - 0.9, - 0.15, - 0.15, - 0.35, - 0.35, - 0.15, - 0.15, - 0.9, - 0.9, - 0.15, - 0.15, - 0.5, - 0.5, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.35, - 0.35, - 0.35, - 0.15, - 0.15, - 0.8, - 0.8, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.03, - 0.5, - 0.5, - 0.15, - 0.15, - 1.0, - 1.0, - 1.0, - 1.0, - 0.15, - 0.15, - 0.45, - 0.45, - 0.15, - 0.15, - 0.85, - 0.85, - 0.85, - 0.02, - 0.02, - 0.02, - 0.02 - ] - }, - "aipa": "w eː n | s iː d a K:k | s iː d i | Q:ʔ a ʕ a d | f i | b eː t o", - "unrelated": { - "heard": "w e n s i d a h k s i d i ʔ a ʔ a d f i b e t o", - "distance": 2, - "tokens": 23, - "ratio": 0.087, - "threshold": 0.65, - "unrelated": false - }, - "references": [ - { - "voice": "ראאף", - "mean": 98.0, - "sounds": 23 - }, - { - "voice": "פדוא", - "mean": 97.8, - "sounds": 23 - } - ], - "tokens": [ - { - "i": 0, - "word": 0, - "ipa": "w", - "sound": "w", - "class": null, - "realised": null, - "span": [ - 4, - 6 - ], - "window": [ - 0, - 8 - ], - "curves": { - "start": 0, - "target": [ - 0.001, - 0.001, - 0.001, - 0.001, - 0.85, - 0.85, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004 - ], - "competitor": [ - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004, - 0.007, - 0.007, - 0.007, - 0.007 - ], - "prev": null, - "next": [ - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004, - 0.904, - 0.904, - 0.904, - 0.904 - ], - "blank": [ - 0.97, - 0.97, - 0.97, - 0.97, - 0.005, - 0.005, - 0.95, - 0.95, - 0.004, - 0.004, - 0.004, - 0.004 - ] - }, - "p_target": 0.8496, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0107, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 1, - "word": 0, - "ipa": "eː", - "sound": "eː", - "class": null, - "realised": null, - "span": [ - 8, - 12 - ], - "window": [ - 6, - 14 - ], - "curves": { - "start": 2, - "target": [ - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004, - 0.904, - 0.904, - 0.904, - 0.904, - 0.004, - 0.004, - 0.014, - 0.014, - 0.002, - 0.002 - ], - "competitor": [ - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004, - 0.007, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.014, - 0.014, - 0.002, - 0.002 - ], - "prev": [ - 0.001, - 0.001, - 0.85, - 0.85, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.007, - 0.007, - 0.001, - 0.001 - ], - "next": [ - 0.001, - 0.001, - 0.005, - 0.005, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.8, - 0.8, - 0.001, - 0.001 - ], - "blank": [ - 0.97, - 0.97, - 0.005, - 0.005, - 0.95, - 0.95, - 0.004, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.007, - 0.007, - 0.97, - 0.97 - ] - }, - "p_target": 0.9039, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0071, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "near_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": { - "id": "vowel_length", - "title": "תנועה קצרה ותנועה ארוכה", - "severity": 1 - }, - "tip": false, - "issue": false - }, - { - "i": 2, - "word": 0, - "ipa": "n", - "sound": "n", - "class": null, - "realised": null, - "span": [ - 14, - 16 - ], - "window": [ - 12, - 20 - ], - "curves": { - "start": 8, - "target": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.8, - 0.8, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.004, - 0.004 - ], - "competitor": [ - 0.007, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.014, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.007, - 0.007 - ], - "prev": [ - 0.904, - 0.904, - 0.904, - 0.904, - 0.004, - 0.004, - 0.014, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.007, - 0.007 - ], - "next": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.007, - 0.007, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.9, - 0.9 - ], - "blank": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.007, - 0.007, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.004, - 0.004 - ] - }, - "p_target": 0.8001, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0143, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 3, - "word": 1, - "ipa": "s", - "sound": "s", - "class": null, - "realised": null, - "span": [ - 22, - 25 - ], - "window": [ - 18, - 27 - ], - "curves": { - "start": 14, - "target": [ - 0.007, - 0.007, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.9, - 0.9, - 0.9, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004 - ], - "competitor": [ - 0.014, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.009, - 0.009, - 0.009, - 0.009 - ], - "prev": [ - 0.8, - 0.8, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004 - ], - "next": [ - 0.014, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.884, - 0.884, - 0.884, - 0.884 - ], - "blank": [ - 0.007, - 0.007, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.004, - 0.004, - 0.004, - 0.004 - ] - }, - "p_target": 0.9003, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0071, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 4, - "word": 1, - "ipa": "iː", - "sound": "iː", - "class": null, - "realised": null, - "span": [ - 27, - 31 - ], - "window": [ - 25, - 33 - ], - "curves": { - "start": 21, - "target": [ - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.884, - 0.884, - 0.884, - 0.884, - 0.004, - 0.004, - 0.014, - 0.014, - 0.004, - 0.004 - ], - "competitor": [ - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.009, - 0.009, - 0.009, - 0.009, - 0.004, - 0.004, - 0.014, - 0.014, - 0.004, - 0.004 - ], - "prev": [ - 0.001, - 0.9, - 0.9, - 0.9, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002 - ], - "next": [ - 0.001, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.8, - 0.8, - 0.002, - 0.002 - ], - "blank": [ - 0.97, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.004, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.007, - 0.007, - 0.95, - 0.95 - ] - }, - "p_target": 0.8843, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0086, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "near_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 5, - "word": 1, - "ipa": "d", - "sound": "d", - "class": null, - "realised": null, - "span": [ - 33, - 35 - ], - "window": [ - 31, - 37 - ], - "curves": { - "start": 27, - "target": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.8, - 0.8, - 0.002, - 0.002, - 0.02, - 0.004, - 0.001, - 0.004 - ], - "competitor": [ - 0.009, - 0.009, - 0.009, - 0.009, - 0.004, - 0.004, - 0.014, - 0.014, - 0.004, - 0.004, - 0.039, - 0.007, - 0.003, - 0.007 - ], - "prev": [ - 0.884, - 0.884, - 0.884, - 0.884, - 0.004, - 0.004, - 0.014, - 0.014, - 0.004, - 0.004, - 0.039, - 0.007, - 0.003, - 0.007 - ], - "next": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002, - 0.45, - 0.004, - 0.001, - 0.004 - ], - "blank": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.007, - 0.007, - 0.95, - 0.95, - 0.02, - 0.9, - 0.001, - 0.9 - ] - }, - "p_target": 0.8001, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0143, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 6, - "word": 1, - "ipa": "a", - "sound": "a", - "class": null, - "realised": null, - "span": [ - 37, - 38 - ], - "window": [ - 35, - 41 - ], - "curves": { - "start": 31, - "target": [ - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002, - 0.45, - 0.004, - 0.001, - 0.004, - 0.025, - 0.025, - 0.007, - 0.001 - ], - "competitor": [ - 0.004, - 0.004, - 0.014, - 0.014, - 0.004, - 0.004, - 0.039, - 0.007, - 0.003, - 0.007, - 0.05, - 0.05, - 0.014, - 0.002 - ], - "prev": [ - 0.002, - 0.002, - 0.8, - 0.8, - 0.002, - 0.002, - 0.02, - 0.004, - 0.001, - 0.004, - 0.025, - 0.025, - 0.007, - 0.001 - ], - "next": [ - 0.004, - 0.004, - 0.014, - 0.014, - 0.004, - 0.004, - 0.039, - 0.007, - 0.003, - 0.007, - 0.325, - 0.325, - 0.014, - 0.002 - ], - "blank": [ - 0.95, - 0.95, - 0.007, - 0.007, - 0.95, - 0.95, - 0.02, - 0.9, - 0.001, - 0.9, - 0.025, - 0.025, - 0.8, - 0.97 - ] - }, - "p_target": 0.4498, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0393, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "unmarked_short_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 7, - "word": 1, - "ipa": "k", - "sound": "K", - "class": [ - "k", - "tʃ" - ], - "realised": "k", - "span": [ - 41, - 43 - ], - "window": [ - 38, - 47 - ], - "curves": { - "start": 34, - "target": [ - 0.014, - 0.004, - 0.004, - 0.039, - 0.007, - 0.003, - 0.007, - 0.325, - 0.325, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.007 - ], - "competitor": [ - 0.007, - 0.002, - 0.002, - 0.02, - 0.004, - 0.96, - 0.004, - 0.025, - 0.025, - 0.007, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.004 - ], - "prev": [ - 0.007, - 0.002, - 0.002, - 0.45, - 0.004, - 0.001, - 0.004, - 0.025, - 0.025, - 0.007, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.004 - ], - "next": [ - 0.007, - 0.002, - 0.002, - 0.02, - 0.004, - 0.001, - 0.004, - 0.025, - 0.025, - 0.007, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.9 - ], - "blank": [ - 0.007, - 0.95, - 0.95, - 0.02, - 0.9, - 0.001, - 0.9, - 0.025, - 0.025, - 0.8, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.004 - ] - }, - "p_target": 0.325, - "competitor": "h", - "competitor_other": false, - "p_competitor": 0.9598, - "competitor_frames": 1, - "gop": -1.083, - "confidence": 1.0, - "gop_factor": 0.685, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 68.4 - }, - { - "stage": "exemption", - "score": 68.4, - "applied": null - }, - { - "stage": "insertion", - "score": 68.4, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 55.0, - "refs": [ - 55.0, - 70.0 - ], - "applied": true - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": 1.083, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 8, - "word": 2, - "ipa": "s", - "sound": "s", - "class": null, - "realised": null, - "span": [ - 50, - 53 - ], - "window": [ - 46, - 55 - ], - "curves": { - "start": 42, - "target": [ - 0.025, - 0.007, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.9, - 0.9, - 0.9, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004 - ], - "competitor": [ - 0.05, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.007, - 0.007, - 0.007, - 0.007 - ], - "prev": [ - 0.325, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.007, - 0.007, - 0.007, - 0.007 - ], - "next": [ - 0.05, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.904, - 0.904, - 0.904, - 0.904 - ], - "blank": [ - 0.025, - 0.8, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.004, - 0.004, - 0.004, - 0.004 - ] - }, - "p_target": 0.9003, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0071, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 9, - "word": 2, - "ipa": "iː", - "sound": "iː", - "class": null, - "realised": null, - "span": [ - 55, - 59 - ], - "window": [ - 53, - 61 - ], - "curves": { - "start": 49, - "target": [ - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.904, - 0.904, - 0.904, - 0.904, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004 - ], - "competitor": [ - 0.002, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.007, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004 - ], - "prev": [ - 0.001, - 0.9, - 0.9, - 0.9, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002 - ], - "next": [ - 0.001, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002 - ], - "blank": [ - 0.97, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.004, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95 - ] - }, - "p_target": 0.9039, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0071, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "near_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 10, - "word": 2, - "ipa": "d", - "sound": "d", - "class": null, - "realised": null, - "span": [ - 61, - 63 - ], - "window": [ - 59, - 65 - ], - "curves": { - "start": 55, - "target": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.007, - 0.007, - 0.001, - 0.001 - ], - "competitor": [ - 0.007, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.014, - 0.014, - 0.002, - 0.002 - ], - "prev": [ - 0.904, - 0.904, - 0.904, - 0.904, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.807, - 0.807, - 0.002, - 0.002 - ], - "next": [ - 0.904, - 0.904, - 0.904, - 0.904, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.807, - 0.807, - 0.002, - 0.002 - ], - "blank": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95, - 0.007, - 0.007, - 0.97, - 0.97 - ] - }, - "p_target": 0.8496, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0107, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 11, - "word": 2, - "ipa": "i", - "sound": "i", - "class": null, - "realised": null, - "span": [ - 65, - 67 - ], - "window": [ - 63, - 73 - ], - "curves": { - "start": 59, - "target": [ - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.807, - 0.807, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.013, - 0.013, - 0.004, - 0.004 - ], - "competitor": [ - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.014, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.013, - 0.013, - 0.004, - 0.004 - ], - "prev": [ - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.007, - 0.007, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.006, - 0.006, - 0.002, - 0.002 - ], - "next": [ - 0.005, - 0.005, - 0.016, - 0.016, - 0.005, - 0.005, - 0.021, - 0.021, - 0.003, - 0.003, - 0.003, - 0.003, - 0.003, - 0.003, - 0.833, - 0.833, - 0.005, - 0.005 - ], - "blank": [ - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95, - 0.007, - 0.007, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.006, - 0.006, - 0.95, - 0.95 - ] - }, - "p_target": 0.8073, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0143, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "near_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 12, - "word": 3, - "ipa": "ʔ", - "sound": "Q", - "class": [ - "k", - "q", - "ʔ" - ], - "realised": "ʔ", - "span": [ - 73, - 75 - ], - "window": [ - 69, - 77 - ], - "curves": { - "start": 65, - "target": [ - 0.021, - 0.021, - 0.003, - 0.003, - 0.003, - 0.003, - 0.003, - 0.003, - 0.833, - 0.833, - 0.005, - 0.005, - 0.016, - 0.016, - 0.005, - 0.005 - ], - "competitor": [ - 0.014, - 0.014, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.013, - 0.013, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004 - ], - "prev": [ - 0.807, - 0.807, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.013, - 0.013, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004 - ], - "next": [ - 0.007, - 0.007, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.006, - 0.006, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002 - ], - "blank": [ - 0.007, - 0.007, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.006, - 0.006, - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95 - ] - }, - "p_target": 0.8328, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0129, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 13, - "word": 3, - "ipa": "a", - "sound": "a", - "class": null, - "realised": null, - "span": [ - 77, - 79 - ], - "window": [ - 75, - 81 - ], - "curves": { - "start": 71, - "target": [ - 0.001, - 0.001, - 0.006, - 0.006, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002 - ], - "competitor": [ - 0.002, - 0.002, - 0.013, - 0.013, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.014, - 0.014, - 0.004, - 0.004 - ], - "prev": [ - 0.003, - 0.003, - 0.833, - 0.833, - 0.005, - 0.005, - 0.016, - 0.016, - 0.005, - 0.005, - 0.814, - 0.814, - 0.005, - 0.005 - ], - "next": [ - 0.001, - 0.001, - 0.006, - 0.006, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002 - ], - "blank": [ - 0.97, - 0.97, - 0.006, - 0.006, - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95, - 0.007, - 0.007, - 0.95, - 0.95 - ] - }, - "p_target": 0.8496, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0107, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "unmarked_short_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 14, - "word": 3, - "ipa": "ʕ", - "sound": "ʕ", - "class": null, - "realised": null, - "span": [ - 81, - 82 - ], - "window": [ - 79, - 85 - ], - "curves": { - "start": 75, - "target": [ - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002 - ], - "competitor": [ - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.8, - 0.8, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002 - ], - "prev": [ - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002 - ], - "next": [ - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002 - ], - "blank": [ - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95, - 0.007, - 0.007, - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95 - ] - }, - "p_target": 0.0071, - "competitor": "ʔ", - "competitor_other": false, - "p_competitor": 0.8001, - "competitor_frames": 2, - "gop": -4.718, - "confidence": 0.024, - "gop_factor": 0.192, - "confidence_factor": 0.154, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 3.0 - }, - { - "stage": "exemption", - "score": 3.0, - "applied": null - }, - { - "stage": "insertion", - "score": 3.0, - "applied": null - }, - { - "stage": "baseline", - "score": 3.8, - "ref": 80.0, - "refs": [ - 100.0, - 80.0 - ], - "applied": true - }, - { - "stage": "calibration", - "score": 45.0, - "floor": 45.0, - "applied": true - } - ], - "final": 45.0, - "margin": { - "needed": 0.0, - "got": 4.718, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": true, - "rule": { - "id": "ayin", - "title": "ע לועית", - "severity": 3 - }, - "tip": true, - "issue": false - }, - { - "i": 15, - "word": 3, - "ipa": "a", - "sound": "a", - "class": null, - "realised": null, - "span": [ - 85, - 87 - ], - "window": [ - 82, - 89 - ], - "curves": { - "start": 78, - "target": [ - 0.85, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001 - ], - "competitor": [ - 0.005, - 0.002, - 0.002, - 0.8, - 0.8, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001 - ], - "prev": [ - 0.005, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001 - ], - "next": [ - 0.005, - 0.002, - 0.002, - 0.007, - 0.007, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.85, - 0.85, - 0.001, - 0.001 - ], - "blank": [ - 0.005, - 0.95, - 0.95, - 0.007, - 0.007, - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95, - 0.005, - 0.005, - 0.97, - 0.97 - ] - }, - "p_target": 0.8496, - "competitor": "ʔ", - "competitor_other": false, - "p_competitor": 0.8001, - "competitor_frames": 1, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "unmarked_short_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 16, - "word": 3, - "ipa": "d", - "sound": "d", - "class": null, - "realised": null, - "span": [ - 89, - 91 - ], - "window": [ - 87, - 95 - ], - "curves": { - "start": 83, - "target": [ - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.85, - 0.85, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.005, - 0.005 - ], - "competitor": [ - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.011, - 0.011, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011 - ], - "prev": [ - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.005, - 0.005 - ], - "next": [ - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.85, - 0.85 - ], - "blank": [ - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95, - 0.005, - 0.005, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.005, - 0.005 - ] - }, - "p_target": 0.8496, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0107, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 17, - "word": 4, - "ipa": "f", - "sound": "f", - "class": null, - "realised": null, - "span": [ - 97, - 100 - ], - "window": [ - 93, - 102 - ], - "curves": { - "start": 89, - "target": [ - 0.005, - 0.005, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.85, - 0.85, - 0.85, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001 - ], - "competitor": [ - 0.011, - 0.011, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011, - 0.011, - 0.004, - 0.004, - 0.011, - 0.011, - 0.002, - 0.002 - ], - "prev": [ - 0.85, - 0.85, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.005, - 0.005, - 0.005, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001 - ], - "next": [ - 0.011, - 0.011, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011, - 0.011, - 0.004, - 0.004, - 0.855, - 0.855, - 0.002, - 0.002 - ], - "blank": [ - 0.005, - 0.005, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.005, - 0.005, - 0.005, - 0.95, - 0.95, - 0.005, - 0.005, - 0.97, - 0.97 - ] - }, - "p_target": 0.8496, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0107, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 18, - "word": 4, - "ipa": "i", - "sound": "i", - "class": null, - "realised": null, - "span": [ - 102, - 104 - ], - "window": [ - 100, - 110 - ], - "curves": { - "start": 96, - "target": [ - 0.002, - 0.011, - 0.011, - 0.011, - 0.004, - 0.004, - 0.855, - 0.855, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004 - ], - "competitor": [ - 0.002, - 0.011, - 0.011, - 0.011, - 0.004, - 0.004, - 0.011, - 0.011, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004 - ], - "prev": [ - 0.001, - 0.85, - 0.85, - 0.85, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.005, - 0.005, - 0.002, - 0.002 - ], - "next": [ - 0.001, - 0.005, - 0.005, - 0.005, - 0.002, - 0.002, - 0.005, - 0.005, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.85, - 0.85, - 0.002, - 0.002 - ], - "blank": [ - 0.97, - 0.005, - 0.005, - 0.005, - 0.95, - 0.95, - 0.005, - 0.005, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.005, - 0.005, - 0.95, - 0.95 - ] - }, - "p_target": 0.8556, - "competitor": "e", - "competitor_other": false, - "p_competitor": 0.0107, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "near_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 19, - "word": 5, - "ipa": "b", - "sound": "b", - "class": null, - "realised": null, - "span": [ - 110, - 112 - ], - "window": [ - 106, - 114 - ], - "curves": { - "start": 102, - "target": [ - 0.005, - 0.005, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.85, - 0.85, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004 - ], - "competitor": [ - 0.005, - 0.005, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.001, - 0.005, - 0.005, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004 - ], - "prev": [ - 0.855, - 0.855, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004, - 0.007, - 0.007, - 0.007, - 0.007 - ], - "next": [ - 0.011, - 0.011, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004, - 0.904, - 0.904, - 0.904, - 0.904 - ], - "blank": [ - 0.005, - 0.005, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.97, - 0.005, - 0.005, - 0.95, - 0.95, - 0.004, - 0.004, - 0.004, - 0.004 - ] - }, - "p_target": 0.8496, - "competitor": "w", - "competitor_other": false, - "p_competitor": 0.0054, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 20, - "word": 5, - "ipa": "eː", - "sound": "eː", - "class": null, - "realised": null, - "span": [ - 114, - 118 - ], - "window": [ - 112, - 120 - ], - "curves": { - "start": 108, - "target": [ - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004, - 0.904, - 0.904, - 0.904, - 0.904, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004 - ], - "competitor": [ - 0.002, - 0.002, - 0.011, - 0.011, - 0.004, - 0.004, - 0.007, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004 - ], - "prev": [ - 0.001, - 0.001, - 0.85, - 0.85, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002 - ], - "next": [ - 0.001, - 0.001, - 0.005, - 0.005, - 0.002, - 0.002, - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002 - ], - "blank": [ - 0.97, - 0.97, - 0.005, - 0.005, - 0.95, - 0.95, - 0.004, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95 - ] - }, - "p_target": 0.9039, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0071, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "near_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": { - "id": "vowel_length", - "title": "תנועה קצרה ותנועה ארוכה", - "severity": 1 - }, - "tip": false, - "issue": false - }, - { - "i": 21, - "word": 5, - "ipa": "t", - "sound": "t", - "class": null, - "realised": null, - "span": [ - 120, - 122 - ], - "window": [ - 118, - 124 - ], - "curves": { - "start": 114, - "target": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.005, - 0.005, - 0.005, - 0.001 - ], - "competitor": [ - 0.007, - 0.007, - 0.007, - 0.007, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.011, - 0.011, - 0.011, - 0.002 - ], - "prev": [ - 0.904, - 0.904, - 0.904, - 0.904, - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.011, - 0.011, - 0.011, - 0.002 - ], - "next": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.85, - 0.85, - 0.85, - 0.001 - ], - "blank": [ - 0.004, - 0.004, - 0.004, - 0.004, - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95, - 0.005, - 0.005, - 0.005, - 0.97 - ] - }, - "p_target": 0.8496, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0107, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": null - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": false, - "weight": 1.0, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - }, - { - "i": 22, - "word": 5, - "ipa": "o", - "sound": "o", - "class": null, - "realised": null, - "span": [ - 124, - 127 - ], - "window": [ - 122, - 131 - ], - "curves": { - "start": 118, - "target": [ - 0.002, - 0.002, - 0.005, - 0.005, - 0.002, - 0.002, - 0.85, - 0.85, - 0.85, - 0.001, - 0.001, - 0.001, - 0.001 - ], - "competitor": [ - 0.004, - 0.004, - 0.011, - 0.011, - 0.004, - 0.004, - 0.011, - 0.011, - 0.011, - 0.002, - 0.002, - 0.002, - 0.002 - ], - "prev": [ - 0.002, - 0.002, - 0.85, - 0.85, - 0.002, - 0.002, - 0.005, - 0.005, - 0.005, - 0.001, - 0.001, - 0.001, - 0.001 - ], - "next": null, - "blank": [ - 0.95, - 0.95, - 0.005, - 0.005, - 0.95, - 0.95, - 0.005, - 0.005, - 0.005, - 0.97, - 0.97, - 0.97, - 0.97 - ] - }, - "p_target": 0.8496, - "competitor": "i", - "competitor_other": false, - "p_competitor": 0.0107, - "competitor_frames": 0, - "gop": 0.0, - "confidence": 1.0, - "gop_factor": 1.0, - "confidence_factor": 1.0, - "deleted": false, - "stages": [ - { - "stage": "gop", - "score": 100.0 - }, - { - "stage": "exemption", - "score": 100.0, - "applied": "unmarked_short_vowel" - }, - { - "stage": "insertion", - "score": 100.0, - "applied": null - }, - { - "stage": "baseline", - "score": 100.0, - "ref": 100.0, - "refs": [ - 100.0, - 100.0 - ], - "applied": false - }, - { - "stage": "calibration", - "score": 100.0, - "floor": null, - "applied": false - } - ], - "final": 100.0, - "margin": { - "needed": 0.0, - "got": -0.0, - "beyond": true - }, - "vowel": true, - "weight": 0.5, - "flagged": false, - "rule": null, - "tip": false, - "issue": false - } - ], - "words": [ - { - "tr": "וֵין", - "scores": [ - 100.0, - 100.0, - 100.0 - ], - "min": 100.0, - "mean": 100.0, - "score": 100.0 - }, - { - "tr": "סִידַכּ", - "scores": [ - 100.0, - 100.0, - 100.0, - 100.0, - 100.0 - ], - "min": 100.0, - "mean": 100.0, - "score": 100.0 - }, - { - "tr": "סִידִי", - "scores": [ - 100.0, - 100.0, - 100.0, - 100.0 - ], - "min": 100.0, - "mean": 100.0, - "score": 100.0 - }, - { - "tr": "קַעַד", - "scores": [ - 100.0, - 100.0, - 45.0, - 100.0, - 100.0 - ], - "min": 45.0, - "mean": 89.0, - "score": 73.6 - }, - { - "tr": "פִי", - "scores": [ - 100.0, - 100.0 - ], - "min": 100.0, - "mean": 100.0, - "score": 100.0 - }, - { - "tr": "בֵּיתוֹ", - "scores": [ - 100.0, - 100.0, - 100.0, - 100.0 - ], - "min": 100.0, - "mean": 100.0, - "score": 100.0 - } - ], - "sentence": { - "phone_mean": 96.9, - "word_mean": 95.6, - "phone_weight": 0.5, - "word_weight": 0.5, - "before_cap": 96.3, - "final": 96, - "capped": false - }, - "tips": [ - { - "key": "ʕ>ʔ", - "severity": 3, - "score": 45 - } - ] - }, - "phrase": { - "he": "איפה סבא שלך? סבא שלי ישב בביתו. (לגבר)", - "ar": "وين سيدك؟ سيدي قعد في بيته." - } -} \ No newline at end of file diff --git a/static/classroom-demo.html b/static/classroom-demo.html deleted file mode 100644 index a9f0694d31fcde87bddb84cb490b60b913687063..0000000000000000000000000000000000000000 --- a/static/classroom-demo.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - احكي · שיעור חי — הדגמת תפעול - - - -
-
احكي · ihkiהדגמת שיעור חי · מסך המדריך
המפגש פתוח
-

שיעור 6 · קבוצת יום רביעי

המדריך מכין שאלה, שולח אותה לתלמידים ורואה את התשובות בזמן אמת.

✓המפגש פתוח לתלמידי הקבוצה5 מתוך 8 הצטרפו אוטומטית
-
- -
-
-
1מכינים שאלה2שולחים לכיתה3רואים תשובות
-

מה אומרים עכשיו?

אפשר לבחור מהשיעור או לומר משפט בעברית.

-
- - -
-
-
-
התלמידים יראו

איפה היית אתמול?

وين كنت امبارح؟
-
-
-
◌עוד לא נשלחה שאלה
התשובות והציונים יופיעו כאן.
-
-
-
-
- - - diff --git a/static/guide.html b/static/guide.html deleted file mode 100644 index 3bbcd623835bdba58c78e1ff80fdce0edbe458b4..0000000000000000000000000000000000000000 --- a/static/guide.html +++ /dev/null @@ -1,489 +0,0 @@ - - - - - - -איך מתרגלים · אהלא וסהלא - - - - - - - -
- → חזרה לאפליקציה -
- אהלא וסהלא · מדריך קצר -

איך מתרגלים במסלול

-

המסלול הוא הדרך שלכם בין השיעורים. לומדים מילים, מתרגלים משפטים בקול, מקבלים משוב על ההגייה ואוספים את המשפטים שכבר הפכו לשלכם. כאן תמצאו את המסלול, הפרסים והמשוב, כל סוגי התרגול והכלים הנוספים באפליקציה.

-
- - - - -
-
-

מסך המסלול

-

זה המסך שנפתח אחרי הכניסה. הוא נפתח תמיד על הנושא שכדאי לתרגל עכשיו.

-
-
-
-
1אהלא וסהלאכיתה ב׳ ▾? מדריך
-
2 -
סרטון יומייום 3 · ברכות
-
תרגול יומי10 משפטים
-
שיעורי ביתעוד יומיים
-
-
3🔥 6הערבית שכבר שלכםרצף של 6 ימים · שיא 113/4 השבוע
18 ★
- -
שלב א׳8 מתוך 10 שיעורים מוכנים
-
4
שיעור 3
תורגלו 22 מתוך 40
-
- -
★1
פעליםתורגלו 8 מתוך 8
-
✓2
משפטים א׳עב׳ ← ער׳תורגלו 10 מתוך 10
-
✓▶︎
בשוק✓ נצפה
- 5 -
התחילו
3
משפטים א׳ער׳ ← עב׳תורגלו 4 מתוך 10
-
שיעורי ביתעוד יומיים
💬
סיפור השוקתורגלו 0 מתוך 12
- 6 -
✎︎
כתיבת משפטיםנכתבו 0 מתוך 8
-
-
-
    -
  1. 1
    הכיתה והמדריךלחיצה על שם הכיתה מראה את חברי הכיתה, או מחליפה כיתה אם אתם ביותר מאחת. הכפתור ? מחזיר למדריך הזה בכל רגע מהמסלול.
  2. -
  3. 2
    שלושה קיצורים להיוםסרטון יומי שהמורה קבע להיום (מודגש עד שצופים). תרגול יומי: סבב של 10 משפטים, חצי לשיפור וחצי חדשים מהשיעור הנוכחי. שיעורי בית: פותח ישר את הנושא הקרוב להגשה.
  4. -
  5. 3
    ההתקדמות והרצף שלכם🔥 הוא מספר ימי התרגול הרצופים ומופיע במרכז הכרטיס. לידו מופיעים יעד ארבעת הימים השבועי ומספר משפטי הזהב ★. לחיצה פותחת את כל אוסף החרוזים, לפי שיעור ונושא.
  6. -
  7. 4
    כותרת השיעורהפס מראה כמה מהשיעור תורגל. לחיצה עליה פותחת את הנושא הבא שעוד לא נגמר. בסוף כל שיעור יש גביע 🏆 שמזהיב כשהשיעור הושלם.
  8. -
  9. 5
    הנושא של עכשיומסומן בהילה ובבועה „התחילו” או „המשך”. אם גללתם רחוק, הכפתור „חזרה לנושא הנוכחי” מחזיר אליו.
  10. -
  11. 6
    כל הנושאים פתוחיםאפשר לפתוח כל נושא בכל שיעור, גם בלי לסיים את מה שלפניו.
  12. -
-
-
- - -
-
-

מה אומר כל עיגול

-

הטבעת סביב העיגול מתמלאת לפי כמה משפטים כבר אמרתם בנושא. החוט בין העיגולים נצבע ירוק אחרי נושא שנגמר.

-
-
-
4
עוד לא התחלתםהמספר הוא מקום הנושא בשיעור
-
3
בתהליךהטבעת מראה כמה כבר תורגל
-
✓2
הושלםכל המשפטים נאמרו לפחות פעם אחת
-
★1
זהבכל המשפטים קיבלו 75 ומעלה
-
עוד 3 ימים
5
שיעורי ביתהבועה סופרת את הימים עד ההגשה
-
▶︎
סרטון✓ מופיע אחרי שפתחתם וצפיתם
-
-
- - -
-
-

הערבית שכבר שלכם

-

זה המקום לראות לא רק מה סיימתם, אלא כמה ערבית כבר צברתם לאורך הדרך. פותחים אותו בלחיצה על הכרטיס שמעל המסלול.

-
-
-
-
🔥 6רצף · שיא 11
42משפטים שלכם
18חרוזי זהב
3ימים השבוע
-
שלב א׳ · שיעור 3
-
משפטים: עברית ← ערבית א׳
- -
פעלים בעבר
- -
-
-

חרוז לכל משפט

הקלטה ראשונה עם דיבור אוספת את המשפט. לחיצה על חרוז באוסף מחזירה ישר למשפט שלו.

-

זהב ב־75

כשהציון הטוב ביותר במשפט מגיע ל־75, החרוז הופך לזהב. האפליקציה שומרת את הציון הגבוה ביותר, לא את האחרון.

-

יעד שבועי 4/4

הטבעת סופרת ימים שונים שבהם תרגלתם השבוע, מיום שני עד יום ראשון. די בהקלטה אחת עם דיבור כדי שהיום ייספר.

-

רצף 🔥

הרצף סופר ימי תרגול רצופים. הרצף של אתמול נשאר מוצג במשך היום כדי שתספיקו להמשיך אותו; אם היום עובר בלי תרגול, הוא מתאפס. השיא האישי נשמר.

-

רגעים שחוגגים

חרוז חדש, מעבר לזהב, שיא חדש, המשך רצף והשלמת נושא מקבלים הודעת חגיגה קצרה אחרי הציון.

-
-
-
- - -
-
-

סוגי הנושאים

-

הסמל בעיגול והתווית מתחת לשם אומרים איזה סוג תרגול מחכה בפנים.

-
-
-
-
📖

מילים חדשות

סמל ספר, בתחילת כל שיעור
-
רואים מילה בעברית←נזכרים וחושפים←ידעתי / עוד לא
-

כרטיס לכל מילה חדשה בשיעור. נסו להיזכר איך אומרים אותה, ואז הקישו לגילוי: מופיעים התעתיק והכתב הערבי, והמילה מושמעת (כפתור לאט משמיע אותה שוב לאט). מסמנים ידעתי או עוד לא, ומילה שסימנתם „עוד לא” חוזרת בסוף הסבב. אין כאן ציון ואין מיקרופון.

-

נספר כ: „נלמדו 12 מתוך 19”. הנושא הושלם כשכל מילה סומנה „ידעתי” פעם אחת. ההתקדמות נשמרת במכשיר הזה.

-
-
-
🧩

התאימו זוגות

סמל פאזל, אחרי המילים החדשות
-
עברית בטור אחד←תעתיק בטור השני←מחברים זוגות
-

משחק על המילים החדשות של השיעור, בסבבים של עד חמש מילים. מקישים על מילה בעברית ועל התעתיק שלה: זוג נכון נצבע בירוק ומושמע, וזוג שגוי רועד באדום ונספר כטעות. בסוף רואים כמה טעויות היו וכמה זמן לקח, ואפשר לשחק שוב.

-

נספר כ: „הותאמו 15 מתוך 19”. משחק שלם בלי אף טעות מזהיב את הנושא ★. ההתקדמות נשמרת במכשיר הזה.

-
-
-
1

עברית ← ערבית

תווית ירוקה עב׳ ← ער׳
-
רואים עברית←אומרים בערבית←ציון הגייה
-

הסוג הנפוץ. מופיע משפט בעברית, ואתם אומרים אותו בערבית מדוברת. מי שלא בטוח לוחץ „הצג תשובה”. בנושאים האלה יש גם תרגול שקט ללא ציון: מרכיבים את המשפט מאוסף מילים, בלי מיקרופון.

-

למשל: משפטים א׳ וב׳, פעלים, הטיות בעבר, המספרים, הזוגי, עבר ועתיד

-
-
-
2

ערבית ← עברית

תווית צהובה ער׳ ← עב׳
-
רואים תעתיק←קוראים בקול←מתרגמים
-

מופיע משפט בערבית בתעתיק עברי. יש שני מיקרופונים: ע לקריאה בערבית, עם ציון על ההגייה, וא לתרגום בעברית, עם ציון על התרגום שלא נשמר. „הצג תרגום” מראה את התרגום הנכון.

-

למשל: משפטים: ערבית ← עברית, שאלות על סיפור המוסך, קפטן איוב

-
-
-
💬

שיחה, סיפור ושיר

סמל בועת דיבור
-
כל השורות על המסך←אומרים בתורן
-

אומרים את השורות אחת אחרי השנייה. השמעת השיחה מקריאה הכול מההתחלה (עם אפשרות „ערבית בלבד”), ובוחן מבקש לומר את כל השיחה ברצף ונותן ציון לכל שורה ובסוף ציון כולל. בשיחה יש שני דוברים, בסיפור קול אחד.

-

למשל: שיחת היכרות, סיפור השוק, סיפור הפלאפל, שיר הצבעים

-
-
-
✎︎

כתיבת משפטים

סמל עיפרון, בסוף כל שיעור
-
מטלה מהחוברת←כותבים משפטים←המורה קורא
-

שיעורי הבית של החוברת: כותבים משפטים משלכם לפי ההנחיה, שורה לכל משפט. הטקסט נשמר אוטומטית, המורה קורא אותו בפאנל, ואין ציון.

-

נספר כ: „נכתבו 3 מתוך 5”

-
-
-
▶︎

סרטון

עיגול מקווקו
-
צופים←✓ נצפה
-

סרטון שהמורה הוסיף לשיעור, במקום שלו בדרך. אחרי צפייה מופיע עליו ✓. סרטון לא נועל את הנושאים שאחריו.

-
-
-
- - -
-
-

מסך התרגול

-

נפתח כשבוחרים נושא. כאן אומרים את המשפטים ורואים מה המערכת שמעה.

-
-
-
-
1→ חזרה למסלול🚗 מצב נסיעה☰ רשימה⚙
-
-
3 / 10משפטים א׳
-
2 -
מה שלומך?
-
איך אומרים את זה בערבית מדוברת?
-
-
3 -
-
-
לחצו ודברו. ההקלטה נעצרת לבד כשמסיימים
- תרגול שקט ללא ציון -
4 -
78כמעט
-
כִּיףחָאלַכּ
-
-
5🔊 השמע▶ הקלטההצג תשובההקודםהבא
-
-
-
    -
  1. 1
    שורת הכליםחזרה למסלול, מצב נסיעה שמקריא ומקשיב בלי לגעת במסך, רשימת המשפטים עם הציון הכי טוב בכל משפט, ו־⚙ להגדרות ההשמעה. בשיחה מופיעים כאן גם השמעת השיחה ובוחן.
  2. -
  3. 2
    המשפטמה שצריך לומר, ומעליו המיקום בנושא.
  4. -
  5. 3
    המיקרופוןלוחצים פעם אחת ומדברים, וההקלטה נעצרת לבד. בלי מיקרופון אפשר לבחור תרגול שקט ולהרכיב את המשפט ממילים.
  6. -
  7. 4
    הציוןכל מילה נצבעת לפי מה שנשמע: ירוק נאמר נכון, צהוב כמעט. מתחת מופיעים טיפים לשיפור.
  8. -
  9. 5
    הכפתורים למטההשמע את התשובה הנכונה, הקלטה כדי לשמוע את עצמכם, הצג תשובה, ומעבר בין משפטים. בחלק מהנושאים יש גם שלילה והטיית פעלים: אותו משפט בשלילה או בגוף אחר.
  10. -
-
-
-

סולם הציון

-
נסו שוב
כמעט
מצוין
-
06075–100
-
-
- - -
-
-

עוד דרכים לתרגל

-

לא חייבים לבחור נושא ולתרגל אותו מההתחלה עד הסוף. אלה המסלולים הקצרים והמשחקיים שמופיעים לפי ההרשאות והשיעורים של הכיתה.

-
-
-
10

תרגול יומי

הכפתור מעל המסלול

סבב קצר של עד עשרה משפטים. הוא משלב משפטים חלשים שכדאי לשפר עם משפטים חדשים משיעורי הבית או מהשיעור הראשון שעוד לא הושלם. בסוף רואים כמה נאמרו וכמה ציונים השתפרו.

הסבב מסומן כהושלם להיום: אחרי שאמרתם לפחות חצי ממנו.

-
🚗

מצב נסיעה

במסך התרגול

האפליקציה מקריאה את ההנחיה, מקשיבה לתשובה, אומרת את ההערכה בקול וממשיכה. מתאים להליכה או נסיעה שבהן לא רוצים לגעת במסך. מסיימים בכפתור „סיום נסיעה”.

-
פעל

לשונית פעלים

טבלת הפעלים של הקורס

מחפשים פועל, עוברים בין השיעורים ורואים את הצורות שכבר נלמדו לפי בניין וגזרה. לחיצה על צורה בערבית משמיעה אותה. הסברים קצרים עוזרים לזהות את התבנית.

-
📿

משחקי חזרה

בלשונית משחקים, כשזמינים

משחק הזיכרון הופך חרוזים ומחבר בין פעלים בעברית ובערבית. משחקים נוספים יכולים להיפתח לפי החומר וההרשאה של הכיתה.

-
💬

יאללה נדבר

שיחה עם דמויות מהשיעורים, כשזמינה

בוחרים תרחיש ומנהלים שיחה קצרה בערבית עם דמות מהקורס. השיחה משתמשת במילים ובדקדוק שכבר נלמדו, ומתקנת ניסוח או דקדוק בעברית כשצריך.

-
▶︎

סרטונים וספרייה

לפי התוכן שהמורה פרסם

סרטון יומי מופיע מעל המסלול ביום שנקבע. סרטוני שיעור נמצאים בתוך הדרך, והספרייה הדיגיטלית נפתחת ממסך הכניסה למי שקיבל גישה.

-
-
- - -
-
-

למורים: ניהול ספרייה וכיתות דרך Grok

-

מחברים פעם אחת את המחבר „הספרייה הדיגיטלית” לחשבון עם הרשאת ניהול. כל שינוי מוצג לבדיקה ורק אישור מפורש מבצע אותו.

-
-
-

צפייה

כתבו: „הצג את הכיתות והתלמידים שאני רשאי/ת לנהל” או „הצג את הסרטונים בספרייה הדיגיטלית”. ברשימת כיתה מוחזרים רק שם הכיתה, המקצוע, כתובות התלמידים והכינויים המקומיים.

-

הכנה לפני שינוי

כתבו למשל: „הכן הוספת התלמיד noam@example.com לכיתה 12. אל תבצע עדיין”. בדקו את מספר הכיתה, כתובת המייל והכינוי ש־Grok מציג.

-

אישור

רק אחרי שהפרטים נכונים כתבו: „אני מאשר/ת לבצע את השינוי שהוכן”. האישור תקף לעשר דקות ולשימוש אחד בלבד.

-

גרף פעילות

כתבו: „הצג גרף פעילות יומי של כיתה 12 בין 2026-10-01 ל־2026-10-26”. אפשר לבחור יום או שבוע וטווח של עד 90 יום שאינו בעתיד. בלי מספר כיתה יוצגו יחד רק הכיתות שמותר לכם לראות.

-

פעילות עדכנית

כתבו: „הצג פעילות עדכנית בכיתה 12 בחמש הדקות האחרונות, והפרד בין נוכחות חיה לבין תרגול שנשמר”. נוכחות חיה זמינה רק במפגש כיתה פעיל ומבוססת על heartbeat מ־20 השניות האחרונות. תרגול בחלון אינו אומר שהתלמיד עדיין מחובר. ספירות קטנות וחותמות זמן מוסתרות לפי ספי הפרטיות.

-

מועד הפעילות האחרון

אפשר לבקש את זמן ניסיון התרגול האחרון ב־UTC ובשעון ישראל. האפליקציה אינה שומרת אירוע כללי של „פעילות אחרונה באפליקציה”, ולכן היעדר ניסיון אינו הוכחה שלא השתמשו בה.

-

מצב המערכת

כתבו: „בדוק את מצב המערכת והסבר בעברית פשוטה”. המצבים הם תקין, חלקי, לא זמין או לא מוגדר. רכיב אופציונלי שלא הוגדר, למשל הקראה, אינו הופך לבדו את כל המערכת לתקולה.

-

תרגילים קשים

כתבו: „הצג את 10 התרגילים הקשים ביותר בכל הכיתות שאני מורשה/ית לראות בין 2026-10-01 ל־2026-10-26”. הצלחה היא ציון 75 ומעלה. הדוח מציג שיעור הצלחה, מספר תלמידים, ניסיונות ממוצעים עד הצלחה ושינוי בין ניסיון ראשון לאחרון.

-

סף פרטיות

תרגיל נכלל בדירוג רק אם לפחות שלושה תלמידים ביצעו יחד לפחות חמישה ניסיונות. שמות של תרגילים שלא עברו את הסף אינם נחשפים. „לא הגיעו להצלחה בטווח” אינו אומר שהתלמידים נטשו, והשיפור המתואר אינו מוכיח שהתרגול לבדו גרם ללמידה.

-

פרטיות בניטור

כלי הניטור הם לקריאה בלבד ואינם דורשים אישור. הם מחזירים ספירות מצטברות בלבד — בלי שמות או כתובות תלמידים, ציונים, תשובות, הקלטות, זמני כניסה מדויקים, לוגים, כתובות פנימיות או סודות.

-

מה נשאר במסך הניהול

מחיקה, הרשאות ותפקידים, שיוך מורים, שיעורים, שיעורי בית, סרטונים יומיים, ציונים והקלטות אינם זמינים ל־Grok ומנוהלים במסך הניהול בלבד.

-
-
- - - -
-
-

הגדרות השמעה ⚙

-

במסך התרגול יש כפתור ⚙. הוא פותח שתי הגדרות שנשמרות במכשיר לפעם הבאה.

-
-
-
-
→ חזרה למסלול☰ רשימה⚙
-
-
הגדרות השמעהסגירה
-
1מהירותx1
-
2הקראת המשפט בעברית
-
-
-
    -
  1. 1
    מהירותx0.8 לאט יותר, כדי לשמוע כל הברה. x1 רגיל. x1.5 מהיר, לאימון הבנת שמיעה.
  2. -
  3. 2
    הקראת המשפט בעבריתכשמסומן, כל משפט חדש מוקרא בקול בעברית לפני שעונים, כך שאפשר לתרגל בלי לקרוא מהמסך. בנושאי ערבית ← עברית אין הקראה, כי העברית היא התשובה.
  4. -
-
-
- - -
-

כמה טיפים

-
-

מתחילים שיעור חדש?

עברו קודם על המילים החדשות ושחקו בהתאמת הזוגות. כך המשפטים של השיעור יהיו מוכרים כשתגיעו אליהם.

-

לא יודעים מה לתרגל?

לחצו „תרגול יומי”. הסבב בוחר לבד משפטים לשיפור ומשפטים חדשים.

-

רוצים זהב?

חזרו לנושא שהושלם ושפרו כל משפט ל־75 ומעלה. נשמר תמיד הציון הכי טוב.

-

בדרך לעבודה?

מצב נסיעה מקריא, מקשיב ונותן משוב בקול, משפט אחרי משפט.

-

ציון נמוך מסיבה לא ברורה?

דברו קרוב למיקרופון, בשקט סביב, ולחצו „השמע” כדי לשמוע שוב את הנוסח הנכון.

-
-
-
- - - - diff --git a/static/index.html b/static/index.html index 960e682d59ee166467c7553ac5127785d17274bb..48b1ff913cf4b8115b83662ca7ca14d15d02c73e 100644 --- a/static/index.html +++ b/static/index.html @@ -1,8605 +1,29 @@ - + - - - - - - - - - - -__TITLE__ - - - + + + + מתמטיקה בכיף + + + - - - - - - -
__SPLASH_ALT__
- -
-
-
אהלא וסהלא
- -
- ? -
בחרו נושא במסלול ותרגלו אותו בקול
- - - -
- - - -
- - -
- - -
- -
- - - - - - - - -
- - -
- - - - - -
-
- - - -

טוען…

-

איך אומרים את זה בערבית מדוברת?

- - -
- - - -
לחצו ודברו. ההקלטה נעצרת לבד כשמסיימים
- - - קוראים בערביתציון על ההגייה - מתרגמים לעבריתציון על התרגום -
- - - - -
- לחצו על הכרטיסייה כדי לגלות את התשובה -
-
- - -
- - - - -
+ +
+ מתמטיקה בכיף +

טוענים את מסלול המתמטיקה…

+
- -
- -
-

ניהול

-
- -
-
-
-
-
- - - - -
- -
-
-

מעבדה

-

רשימה קבועה של הברות ומילים קצרות, שכל אחת בודקת הגה אחד. כל דובר מקריא אותה פעם אחת. אומרים נכון, או בוחרים מראש את ההגה שבו תיעשה טעות מכוונת. כל הקלטה נשמרת עם התווית שלה, ומהן נמדד זיהוי הפונמות: כמה פעמים הגה שנאמר נכון סומן כטעות, וכמה טעויות מכוונות לא נתפסו.

-
-
-
-
-

בדיקה מהירה לדוברי עברית

-

עשר מילים שבודקות ח ו־ע גרוניות, ח׳, ע׳, עיצורים נחציים והבדל בין תנועה קצרה לאליף ארוכה. הקליטו כל מילה פעם אחת; אותה הקלטה נבדקת ב־Facebook, ב־wav2vec2 המאומן וב־POWSM short-pad (ONNX). ההקלטות אינן נשמרות.

-
בחרו שאלה והקליטו
-
-
-
-
לחצו, אמרו את המילה, ולחצו שוב לסיום
-
-
-
-
-
-
-
-

כל שורה היא פריט לומר. השורה המסומנת היא הבאה בתור: לוחצים 🎤, אומרים את הפריט נכון ככל האפשר, וההקלטה מושמעת לבד. אחרי ההשמעה: ✓ שולח אותה ועובר לשורה הבאה, ✗ מוחק אותה ומקליטים שוב. מה שנשלח נשמר כ"נאמר נכון" של הדובר, והוא הבסיס לבדיקת זיהוי הפונמות.

-
מי מקליט:
- -
מקשים: רווח הקלטה · Enter אישור · Backspace דחייה
-
-
-
- מבחן אוטומטי בקול TTS -

קול ה-TTS אומר כל פריט ברשימה, וההקלטה נשמרת כאילו הקליט ואישר אותה דובר בשם "TTS" ושם הקול, בלי אישור ידני. הציון נמדד בלי קו הבסיס הילידי, שבנוי מאותו קול ומודד כל הגה מול עצמו. זה נותן דובר "ילידי" נוסף לבדיקה, אבל זה דיבור מסונתז: השוו אותו בתוצאות להקלטות של אנשים, ולא במקומן.

-
-
    -
    -
    -
    -
    - מי מקליט: - -
    -
    - -
    -
    -
    -

    הדיווחים שאושרו בטאב ניטור, כל אחד עם ההגאים שהמורה סימנה שסומנו בטעות. ההרצה מחדש מנתחת כל הקלטה עם המודל והכללים של עכשיו, ובודקת אם אותם הגאים עדיין מסומנים. דיווח שבו לא סומן הגה מסוים לא נספר.

    -
    -
    -
    -
    -
    -
    -

    בחרו סרטון כדי לנתח מחדש את כל קטעי הדיבור שלו. הטבלה משווה בין התוצאה שנשמרה בייבוא לבין ה־pipeline והגדרת ה־GOP הנוכחיים.

    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - -
    -

    -
    -

    איך בודקים שינוי במימוש: הריצו מחדש ושמרו כנקודת השוואה, שנו את המימוש, והריצו מחדש. ירוק: השתפר, אדום: הורע. שיפור אמיתי מוריד את "סומנו בטעות" בלי להוריד את "זיהוי". "סומן בטעות": הגה שנאמר נכון וסומן כטעות. "זיהוי": טעות מכוונת שסומנה. "דיווחים": ההגה סומן בטעות בדיווח מאושר מתרגול אמיתי. אותה בדיקה משורת הפקודה: python scripts/bench.py

    -
    -
    - -
    -
    -
    - -

    - -
      -
      - -

      -
      - -
      השיעוריםהתלמידים של הכיתה רואים במסלול רק אותם. שיעור מקווקו עוד לא מוכן: הוא ייפתח לכיתה כשיהיה.
      -
      שיעורי ביתנושא לכל שיעורי בית, ותאריך הגשה אם רוצים. אפשר כמה, למשל הנושא האחרון של כל שיעור. התלמידים רואים כל אחד מסומן במסלול, עם הימים שנשארו; בפאנל רואים מי סיים. -
      -
      סרטונים יומייםהסרטונים של הרמות שהכיתה לומדת, מהספרייה שתחת „תוכן”. קבעו תאריך לסרטון, ומאותו יום התלמידים רואים אותו בכפתור „סרטון יומי” שמעל המסלול, והוא מסומן עד שצופים בו. סרטון בלי תאריך לא מוצג לכיתה.
      - - -
      -
      -
      - -

      כיתה חדשה

      -
      - -
      - -

      השם מופיע אצלכם בפאנל ואצל התלמידים בראש המסלול.

      -
      -
      -
      באיזו רמה הכיתה לומדת?התלמידים יראו במסלול את השיעורים של הרמה הזו. שיעור שעוד לא מוכן ייפתח להם לבד כשיהיה.
      -
      -
      -
      מי בכיתה?סמנו את התלמידים. ליד כל תלמיד שסימנתם אפשר לכתוב כינוי: כך יקראו לו בכיתה, במקום השם שלו ב-Google.
      - - -
      -
      - - -
      באילו ימים צופים בסרטונים היומיים?קבעו תאריך לכל סרטון של הרמה, ומאותו יום התלמידים רואים אותו בכפתור „סרטון יומי” שמעל המסלול. לא חובה: סרטון בלי תאריך לא מוצג לכיתה, ואפשר לקבוע אחר כך.
      -
      -
      -
      -

      -
      -
      -
      -
      - -

      -
      -
      - -

      -

      -
        - -
        - -

        -
        - - -
        -

        איך אומרים בערבית?

        -

        - - -
        -
        - - -
        - -

        -
        - -

        הקישו על מילה בעברית ועל התעתיק שלה

        -
        - -
        - -

        -
        -

        - -
        - -

        באיזו כיתה מתרגלים עכשיו?אפשר להחליף אחר כך, בלחיצה על שם הכיתה

        -
        - - -
        - - -

        שליחת התראהמגיעה לטלפון או למחשב של מי שהפעיל תזכורות

        -
        - -
        מי הפעיל התראות
          - - -

          -
          -
          -
          - - -

          כל הכבוד! רוצים תזכורת כל בוקר?

          -

          -
          -
          - -

          -

          -
          -
          - - -

          -
          -

          -
          -
          -
          - - -

          -

          -
          -
          - -

          הערבית שכבר שלכם

          -

          כל תרגיל שהשלמתם מעניק חרוז, וציון 75 ומעלה הופך אותו לזהב. השלמתם שיעור שלם? הרווחתם מסבחה, וגם צבע חרוזים חדש! פספסתם יום? 🧊 יום חופש אחד בשבוע שומר על הרצף. סיפור או שיחה שלמים הם חרוז אחד, שמזהיב כשכל שורותיו בזהב.

          -
          -
          -
          -
          -
          - -

          מצב נסיעה

          -

          תרגול בלי לגעת במסך: האפליקציה מקריאה כל משפט, מצפצפת כשתורכם לדבר, ואומרת בקול אם הצלחתם. כשלא הצלחתם שומעים את התשובה ומנסים פעם אחת נוספת. בסוף הנושא שואלת אם לעבור לנושא הבא. אם לא עונים פעמיים ברצף, המצב נעצר לבד. לסיום לוחצים על "סיום נסיעה".

          -
          -
          - -

          איך להקשיב לשיחה?

          -

          בחרו מה לשמוע במצב נסיעה:

          -
          - - -
          -
          - -

          איך משחקים בפועל זמני?

          -

          בכל סיבוב מופיע פועל בעברית בתוך חרוז. אמרו אותו בערבית מדוברת — בעבר ואז בעתיד — לפני שטבעת הזמן נעלמת.

          -

          אחרי 3 שניות יופיע רמז של בניין וגזרה, ואחרי 6 שניות תופיע גם צורת העבר. תשובה נכונה לפני הרמזים מזכה בחרוז ירוק.

          -

          בסבב יש 10 פעלים. הניקוד הוא מספר התשובות הנכונות, והשיא נשמר במכשיר הזה.

          -
          -
          - -

          הגדרות השמעה

          -
          - - -
          - מה עושה כל הגדרה? -
          - -
          -

          פועל יוצא

          -
          מוכנים?רצף 0 🔥
          -
          -
          - - -
          - -
          -
          - -
          -
          -
          - -

          ניתוח פונמותהרצף הצפוי מול הפענוח החופשי של המודל

          -

          זהו פירוש המודל להקלטה, ולא תעתיק פונטי מוסמך. תנועות קרובות ואורך תנועה מסומנים כהתאמה מקורבת.

          -
          -
          התאמההתאמה מקורבתהחלפה או השמטהתוספת
          - -

          מיקום הפונמות בזמן

          -

          הסתברות פונמות לאורך ההקלטה

          0%100%· ★ פונמה צפויה
          -
          - -

          🚩 הדיווח נשמרזה מה שהמורה יראה על ההקלטה הזו

          -
          -
          - - -

          - - - -
          - -
          -

          סיבת ההחרגה

          - - -
          -
          -
          - - - - - - - - - + + + diff --git a/static/learner_home.js b/static/learner_home.js deleted file mode 100644 index ed60cfa83161918421c8418317d5d4c53ac604ed..0000000000000000000000000000000000000000 --- a/static/learner_home.js +++ /dev/null @@ -1,146 +0,0 @@ -/* The focused learner home (openspec/changes/add-focused-learner-home): which lesson a student's class is on now, - and the few actions the home offers in its place of the whole path. Pure helpers, and the two small screens (the - lesson question and the home). index.html gives them the course, the class and the student's progress. */ -(function(root){ -"use strict"; -const KEY="ihki.lesson.v1", MAX_ACTIONS=3, SHORT=10; - -/* Where the device keeps a class's lesson for one account: another account, subject or class never reads it. */ -function storageKey(subject,email,classId){ - return `${KEY}:${subject||"arabic"}:${String(email||"").trim().toLowerCase()}:${classId}`; -} - -/* The lesson a class is on now. `server`: the one /api/me gave (null when none); `local`: what the device kept, - {lesson, synced}, or null; `lessons`: the class's lesson ids. The server's wins; a device's choice the server does not - have (saved while it was unreachable) is used and sent again. A lesson the class no longer has is none. */ -function resolveLesson(server,local,lessons){ - const ok=id=>typeof id==="string"&&id!==""&&lessons.includes(id); - if(ok(server)) return {lesson:server,retry:false}; - if(local&&ok(local.lesson)) return {lesson:local.lesson,retry:true}; - return {lesson:null,retry:false}; -} - -/* Whether a class's current lesson can be asked: "ok"; "math" (its own path, not asked); "teacher" (they teach it: - the path stays); "ambiguous" (its lessons are not of one level, so the class must be corrected); "none". */ -function classState(cls){ - if(!cls) return "none"; - if(cls.subject==="math") return "math"; - if(cls.teacher) return "teacher"; - return cls.level?"ok":"ambiguous"; -} - -/* The lessons daily practice takes its new sentences from, in turn: the homework's, the current one, the first - unfinished, then every other in the course's order; each once. */ -function lessonOrder(homework,current,unfinished,all){ - const out=[]; - for(const l of [homework,current,unfinished,...all]) if(l&&!out.includes(l)) out.push(l); - return out; -} - -/* The home's actions: of the candidates, in their order, the first MAX_ACTIONS that can start, each target once. */ -function pickActions(candidates){ - const seen=new Set(), out=[]; - for(const a of candidates){ - if(!a||!a.ready||seen.has(a.target)) continue; - seen.add(a.target); out.push(a); - if(out.length===MAX_ACTIONS) break; - } - return out; -} - -/* The short activity, at most SHORT sentences: the weakest ones the student said and can say better (under - `excellent`), or else the first unsaid ones of the current lesson. null when there are none. */ -function shortPlan(practised,lesson,best,excellent){ - const weak=practised.filter(p=>best[p.id]!=null&&best[p.id]best[a.id]-best[b.id]); - if(weak.length) return {kind:"review",ids:weak.slice(0,SHORT).map(p=>p.id)}; - const fresh=lesson.filter(p=>best[p.id]==null); - return fresh.length?{kind:"new",ids:fresh.slice(0,SHORT).map(p=>p.id)}:null; -} - -/* About how long `n` items take (a sentence, a word, a pair): some 25 seconds each, at least a minute. */ -const minutesFor=n=>Math.max(1,Math.round(n*25/60)); - -function el(tag,cls,text){ - const e=document.createElement(tag); - if(cls) e.className=cls; - if(text!=null) e.textContent=text; - return e; -} - -/* The home. `o`: {className, lessonName, actions: [{kind, title, detail, go, state, run}], done: the current lesson is - finished, onChange, onPath, onRefer}. Its heading takes the focus when `focus` is set. */ -function renderHome(box,o,focus){ - const page=el("section","focus"), head=el("header"), where=el("p","focus-where"), title=el("h2","","מה עושים עכשיו?"); - page.setAttribute("aria-labelledby","focusTitle"); title.id="focusTitle"; title.tabIndex=-1; - const change=el("button","focus-change","שינוי"); change.type="button"; - change.setAttribute("aria-label",`שינוי השיעור הנוכחי (${o.lessonName})`); change.onclick=o.onChange; - where.append(el("span","",`כיתת ${o.className} · ${o.lessonName}`),change); - head.append(where,title); page.append(head); - if(o.done) page.append(el("p","focus-note",`סיימתם את ${o.lessonName}. אפשר לחזור על משפטים, או לבחור את השיעור שהכיתה לומדת עכשיו.`)); - const list=el("ol","focus-actions"); list.setAttribute("aria-label","הפעילויות המומלצות"); - o.actions.forEach((a,k)=>{ // the first is the one to do now: big and filled, with about how long it takes - const li=el("li"), b=el("button","focus-action"+(k===0?" hero":"")+(a.state?" "+a.state:"")); b.type="button"; - const time=a.minutes?`כ-${a.minutes} דק׳`:""; - b.append(el("strong","",a.title)); if(a.detail) b.append(el("span","focus-detail",a.detail)); - if(time) b.append(el("span","focus-time","⏱ "+time)); - b.append(el("span","focus-go",(a.go||"התחילו")+(k===0?" ←":""))); - b.setAttribute("aria-label",[a.title,a.detail,time,a.go||"התחילו"].filter(Boolean).join(", ")); - b.onclick=a.run; li.append(b); list.append(li); - }); - if(o.actions.length) page.append(list); - else page.append(el("p","focus-note","אין כרגע פעילות מומלצת. אפשר להמשיך מהמסלול המלא.")); - const more=el("div","focus-more"), refer=el("button","focus-refer","חבר מביא חבר"), path=el("button","focus-path","צפייה במסלול המלא"); - refer.type=path.type="button"; refer.onclick=o.onRefer; path.onclick=o.onPath; - more.append(path,refer); page.append(more); // the full path first (on the right, in RTL) - box.append(page); - if(focus) title.focus({preventScroll:true}); - return page; -} - -/* The question "which lesson is the class on now?" in `dlg`. `o`: {className, levelName, lessons: [{id, name}], - selected, required: Escape does not close it, error: a message in place of the lessons, onConfirm(id) (may be async; - a thrown error is shown and the question stays), after(id): once it has closed, onPath}. */ -function openLessonDialog(dlg,o){ - dlg.replaceChildren(); - dlg.setAttribute("aria-labelledby","lessonTitle"); dlg.setAttribute("aria-describedby","lessonWhere"); - const title=el("h2","","באיזה שיעור הכיתה עכשיו?"); title.id="lessonTitle"; title.tabIndex=-1; - const where=el("p","lesson-where",[`כיתת ${o.className}`,o.levelName].filter(Boolean).join(" · ")); where.id="lessonWhere"; - const status=el("p","lesson-status"); status.setAttribute("role","status"); status.setAttribute("aria-live","polite"); - dlg.append(title,where); - dlg.oncancel=e=>{ if(o.required&&!o.error) e.preventDefault(); }; - if(o.error){ - dlg.append(el("p","lesson-error",o.error)); - const go=el("button","btn primary","למסלול המלא"); go.type="button"; - go.onclick=()=>{ dlg.close(); if(o.onPath) o.onPath(); }; - const row=el("div","row"); row.append(go); dlg.append(row); - dlg.showModal(); go.focus(); return; - } - const form=el("form","lesson-form"), set=el("fieldset"), legend=el("legend","sr-only","השיעור הנוכחי"); - form.method="dialog"; set.append(legend); - let first=null; - for(const l of o.lessons){ - const label=el("label","lesson-option"), radio=el("input"); - radio.type="radio"; radio.name="lesson"; radio.value=l.id; radio.checked=l.id===o.selected; - label.append(radio,el("span","",l.name)); set.append(label); - if(!first||radio.checked) first=radio; - } - const ok=el("button","btn primary","ממשיכים"); ok.type="submit"; - const row=el("div","row"); row.append(ok); - if(!o.required){ const cancel=el("button","btn","ביטול"); cancel.type="button"; cancel.onclick=()=>dlg.close(); row.append(cancel); } - form.append(set,status,row); dlg.append(form); - form.onsubmit=async e=>{ - e.preventDefault(); - const picked=form.querySelector("input[name=lesson]:checked"); - if(!picked){ status.textContent="בחרו שיעור כדי להמשיך."; (first||ok).focus(); return; } - ok.disabled=true; - try{ await o.onConfirm(picked.value); } - catch(err){ status.textContent=err&&err.message?err.message:"לא הצלחנו לשמור. נסו שוב."; ok.disabled=false; return; } - ok.disabled=false; dlg.close(); - if(o.after) o.after(picked.value); // after closing, which gives the focus back to where it was - }; - dlg.showModal(); (first||ok).focus(); -} - -root.LearnerHome=Object.freeze({KEY,MAX_ACTIONS,SHORT,minutesFor,storageKey,resolveLesson,classState,lessonOrder,pickActions, - shortPlan,renderHome,openLessonDialog}); -})(typeof globalThis!=="undefined"?globalThis:this); diff --git a/static/logo-intro-alt.svg b/static/logo-intro-alt.svg deleted file mode 100644 index 1a19a5014a2b8485fee1daf1f2068468e7f692dc..0000000000000000000000000000000000000000 --- a/static/logo-intro-alt.svg +++ /dev/null @@ -1,81 +0,0 @@ - - אהלא וסהלא - לוגו בית הספר ללימוד השפה הערבית - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/logo-intro.svg b/static/logo-intro.svg deleted file mode 100644 index 0e33a886304e41992e5abd2077dbce5478e33dc7..0000000000000000000000000000000000000000 --- a/static/logo-intro.svg +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/manifest.webmanifest b/static/manifest.webmanifest index ee396f8071dec727a4817f9e8d9d806f413bbfa3..c9643d795eb35c164fa51230fd174353cfc38010 100644 --- a/static/manifest.webmanifest +++ b/static/manifest.webmanifest @@ -1,34 +1,17 @@ { "id": "/", - "name": "אהלא וסהלא", - "short_name": "אהלא וסהלא", - "description": "תרגול ערבית מדוברת", + "name": "מתמטיקה בכיף", + "short_name": "מתמטיקה בכיף", + "description": "תרגול מתמטיקה לכיתה ט׳", "lang": "he", "dir": "rtl", "start_url": "/", "scope": "/", "display": "standalone", - "orientation": "portrait-primary", - "background_color": "#EEF0E8", - "theme_color": "#4B6A2C", + "background_color": "#000000", + "theme_color": "#000000", "icons": [ - { - "src": "/static/icons/icon-192.png?v=2", - "sizes": "192x192", - "type": "image/png", - "purpose": "any" - }, - { - "src": "/static/icons/icon-512.png?v=2", - "sizes": "512x512", - "type": "image/png", - "purpose": "any" - }, - { - "src": "/static/icons/icon-maskable-512.png?v=2", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } + {"src": "/static/icons/icon-192.png", "sizes": "192x192", "type": "image/png"}, + {"src": "/static/icons/icon-512.png", "sizes": "512x512", "type": "image/png"} ] } diff --git a/static/math.css b/static/math.css index aa8bda81687815a142dd4d9bc9bfa63f27581d21..9f577d1e59387bcfc162a75892da603de31be6a6 100644 --- a/static/math.css +++ b/static/math.css @@ -1,7 +1,4 @@ -/* The math path (static/math.js). Layout: one narrow column like the Arabic path: progress, "go on here", then each - unit's skills as a winding trail of circles; a skill opens in a bottom sheet. It sits over the Arabic page, which - stays loaded underneath. The math class is black in either system theme, as its opening screen is: the path sets - the app's colour tokens itself, with a blue accent and a colour per unit. */ +/* Mathly's narrow practice path: progress, the next skill, and each unit's winding trail. */ @import url("https://fonts.googleapis.com/css2?family=STIX+Two+Text:wght@400;600&display=swap"); #mathView{ color-scheme:dark; diff --git a/static/math.js b/static/math.js index 30e36512ee6fc9f66af6af893445f7d7c5ad5893..f28e12a7fdee89b528996fd3bfeeae6a1e36af6f 100644 --- a/static/math.js +++ b/static/math.js @@ -1,21 +1,25 @@ -/* The math path, on /math only: after the opening screen it takes the whole screen in place of the Arabic path. +/* The Mathly path. The skill graph comes from /api/math/skills (app/math_skills.py). A skill is open once every skill it needs is done, and the first open one is where the student goes on. A student may still open a locked skill: without a placement test, one who already knows the foundations should not have to answer them first. - For now each skill has one sample question, and answering it right marks the skill done. Progress is kept on - this device, per account (ihki.math.v1:), until the server keeps attempts. */ + Each skill has one sample question, and answering it correctly marks the skill done. Progress is kept on + this device, per account (mathly.progress.v1:). */ (()=>{ -if(document.documentElement.dataset.subject!=="math") return; const css=document.createElement("link"); css.rel="stylesheet"; css.href=document.currentScript.src.replace("math.js","math.css"); document.head.append(css); +const CFG=window.MATHLY||{}, AUTH_URL=CFG.supabaseUrl||""; +let ME_EMAIL="", MY_CLASSES=[], MY_CLASS=null, client=null; +const can=permission=>CFG.permissions.includes("*")||CFG.permissions.includes(permission); +const api=(url,options={})=>fetch(url,{...options,headers:{...(options.headers||{}),...(CFG.token?{Authorization:`Bearer ${CFG.token}`}:{})}}); + const esc=s=>String(s??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); const fmt=s=>esc(s).replace(/\$([^$]+)\$/g,'$1'); // $…$ is math: set left to right const plain=s=>String(s).replace(/\$/g,""); // the model's text: as fmt, and a signed number it left outside $…$ is set left to right too, or its minus lands after it const fmtLoose=s=>String(s??"").split("$").map((part,i)=>i%2?`${esc(part)}` :esc(part).replace(/(^|[\s(־])([−-]\d+(?:[.,]\d+)?)/g,'$1$2')).join(""); -const key=()=>"ihki.math.v1:"+(ME_EMAIL||"guest"); +const key=()=>"mathly.progress.v1:"+(ME_EMAIL||"guest"); const load=()=>{ try{ return JSON.parse(localStorage.getItem(key())||"{}"); }catch{ return {}; } }; const save=()=>{ try{ localStorage.setItem(key(),JSON.stringify(P)); }catch{} }; @@ -67,7 +71,7 @@ function draw(){ const UNITS=G.units.filter(u=>SHOWN.has(u.id)), mine=G.skills.filter(s=>SHOWN.has(s.unit)); const total=mine.length, doneN=mine.filter(s=>P.done[s.id]).length, nxt=next(); const cls=typeof MY_CLASS!=="undefined"&&MY_CLASS?` · כיתת ${esc(MY_CLASS.name)}`:""; - const manage=AUTH_URL&&can("management.access"), pct=Math.round(100*doneN/total); + const manage=false, pct=Math.round(100*doneN/total); const units=UNITS.map(u=>{ const ids=ORDER.filter(id=>BY[id].unit===u.id), d=ids.filter(id=>P.done[id]).length; return `
          ${esc(u.name)}${d} מתוך ${ids.length}
          `; @@ -147,8 +151,7 @@ function onClick(e){ case "work": PH.works.add(+t.dataset.i); return drawPhoto(); case "all": PH.all=true; return drawPhoto(); case "close": return closeSheet(); - case "manage": hide(); return showManage(); - case "out": return document.getElementById("logoutBtn").click(); + case "out": return client?.auth.signOut().then(()=>location.reload()); } } @@ -228,38 +231,25 @@ function plot(g){ ${ticks}${dots}`; } -// the start button (and a teacher's "תרגול") opens the math path once the opening screen is gone -const arabicEnter=enter; -const enterMath=()=>{ arabicEnter(); if(!AUTH_URL||can("path.use")) show(); }; -document.getElementById("startBtn").onclick=document.getElementById("practiceBtn").onclick=enterMath; - -// the manage panel: "לתרגול" goes to the math path, and "תוכן" lists the math path (units and their skills) in -// place of the Arabic levels. The graph is a file in the repo (app/math_algebra9.json), so here it is only shown. -const toPractice=document.getElementById("mPractice"), arabicPractice=toPractice.onclick; -const practice=async id=>{ arabicPractice(); await show(); if(id&&G) open(id); }; -toPractice.onclick=()=>practice(); -const contentTab=document.querySelector('#mModes [data-mode="content"] small'); if(contentTab) contentTab.textContent="יחידות ומיומנויות"; -async function drawContent(){ - const box=document.getElementById("mContent"); - if(!G){ - box.innerHTML='

          טוענים את המסלול…

          '; - try{ const r=await fetch("/api/math/skills"); if(!r.ok) throw new Error(`השרת ענה ${r.status}`); index(await r.json()); } - catch(e){ box.innerHTML=`

          לא הצלחנו לטעון את המסלול: ${esc(e.message)}. לחצו „רענון” ונסו שוב.

          `; return; } +async function boot(){ + const state=document.getElementById("bootState"), login=document.getElementById("loginBtn"); + if(AUTH_URL){ + if(!window.supabase){ state.className="error"; state.textContent="לא ניתן לטעון את שירות הכניסה."; return; } + client=window.supabase.createClient(AUTH_URL,CFG.supabaseKey); + const {data}=await client.auth.getSession(), session=data.session; + if(!session){ + state.textContent="יש להיכנס כדי לפתוח את מסלול המתמטיקה."; login.hidden=false; + login.onclick=()=>client.auth.signInWithOAuth({provider:"google",options:{redirectTo:location.origin+location.pathname}}); + return; + } + CFG.token=session.access_token; + const response=await api("/api/me"); + if(!response.ok){ state.className="error"; state.textContent=response.status===403?"החשבון אינו משויך לכיתת מתמטיקה.":"לא ניתן להשלים את הכניסה."; return; } + const me=await response.json(); ME_EMAIL=me.email||""; MY_CLASSES=me.classes||[]; MY_CLASS=MY_CLASSES.length===1?MY_CLASSES[0]:null; + CFG.permissions=me.permissions||[]; } - const wide=(()=>{ try{ return JSON.parse(localStorage.getItem("ihki.math.copen")||"{}"); }catch{ return {}; } })(); - box.innerHTML=`

          המסלול של כיתה ט׳, 5 יח״ל: ${G.skills.length} מיומנויות ב־${G.units.length} יחידות, לפי סדר המסלול. לכל מיומנות שאלה לדוגמה, וכל תשובה שגויה בה היא טעות מוכרת עם הסבר. ב„תרגול” רואים את השאלה כמו שהתלמידים רואים אותה.

          `+ - G.units.map(u=>{ - const mine=ORDER.filter(id=>BY[id].unit===u.id).map(id=>BY[id]); - const rows=mine.map((s,k)=>{ - const needs=s.prereqs.map(p=>plain(BY[p].short)).join(", "), wrong=s.sample.choices.filter(c=>!c.correct).length; - const meta=[needs?`דורשת: ${esc(needs)}`:"בלי דרישות קודמות",s.month?`בפריסה: ${esc(s.month)}`:"",s.core?"ליבה 5 יח״ל":"",`${wrong} טעויות מוכרות`].filter(Boolean).join(" · "); - return `
        • ${k+1}
          ${fmt(s.name)}${meta}
        • `; - }).join(""); - return `
          ${esc(u.name)}${mine.length} מיומנויות${u.when?` · ${esc(u.when)}`:""} -

          ${esc(u.name)}${esc(u.source||"")}

            ${rows}
          `; - }).join(""); - box.querySelectorAll("details.csec").forEach(d=>d.ontoggle=()=>{ wide[d.dataset.unit]=d.open?1:0; try{ localStorage.setItem("ihki.math.copen",JSON.stringify(wide)); }catch{} }); - box.querySelectorAll("[data-mxpractice]").forEach(b=>b.onclick=()=>practice(b.dataset.mxpractice)); + document.getElementById("boot").hidden=true; + show(); } -window.drawContent=drawContent; +boot().catch(error=>{ const state=document.getElementById("bootState"); state.className="error"; state.textContent=`לא הצלחנו לפתוח את המסלול: ${error.message}`; }); })(); diff --git a/static/oauth-consent.html b/static/oauth-consent.html deleted file mode 100644 index be99d217f2f1e1df10d4032198923bf508a357e8..0000000000000000000000000000000000000000 --- a/static/oauth-consent.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - אישור חיבור לספרייה הדיגיטלית - - -
          -

          אישור חיבור לספרייה הדיגיטלית

          -

          בודקים את בקשת החיבור…

          - - -
          - - diff --git a/static/privacy.html b/static/privacy.html deleted file mode 100644 index 2e3fb3d2435fae300ce30994974ca9e6910c990c..0000000000000000000000000000000000000000 --- a/static/privacy.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - -מדיניות פרטיות · אהלא וסהלא - - - -
          -

          מדיניות פרטיות

          -

          אהלא וסהלא (ihki) · עודכן ב-10 באוקטובר 2026

          - -

          כניסה

          -

          הכניסה לאפליקציה נעשית באמצעות חשבון Google. בעת הכניסה אנחנו מקבלים מ-Google את השם, כתובת הדוא"ל ותמונת הפרופיל שלכם, ושומרים אותם בשירות Supabase שמנהל עבורנו את החשבונות. הסיסמה שלכם ב-Google אינה מגיעה אלינו. אפשר להיכנס גם בלי חשבון Google, בקישור שנשלח לכתובת הדוא"ל שלכם: אז נשמרת אצלנו הכתובת בלבד.

          - -

          הקלטות

          -

          כשאתם מקליטים משפט, ההקלטה נשלחת לשרת כדי לנתח את ההגייה ולהחזיר ציון. הקלטות רגילות של תלמידים אינן נשמרות בשרת לאחר הניתוח.

          -

          החריג היחיד הוא חשבון שהמורה הגדיר כמדווח תקלות, או חשבון מורה: רק כאשר המשתמש לוחץ במפורש על „אמרתי נכון, המערכת טעתה”, ההקלטה של אותו דיווח נשמרת במאגר פרטי כדי שהמורה יוכל לבדוק את התקלה. תלמידים אחרים אינם יכולים לשלוח או לשמור הקלטות כדיווח.

          -

          כשאתם בוחרים לומר תרגום בעברית (המיקרופון "מתרגמים לעברית"), הדיבור מזוהה על ידי הדפדפן שלכם ואינו נשלח לשרת שלנו. חלק מהדפדפנים, ובהם Chrome, שולחים את הקול לשירות זיהוי הדיבור של יצרן הדפדפן (למשל Google). אנחנו לא שומרים את מה שנאמר.

          - -

          התקדמות בלימוד

          -

          לאחר כל תרגול נשמרים ב-Supabase, תחת החשבון שלכם, המשפט, הציון, המועד ותקציר טכני של הצלילים שהמערכת סימנה. התקציר אינו כולל קול או הקלטה. גם המשפטים שאתם כותבים בשיעורי הבית נשמרים שם. המורה יכול לראות את הנתונים האלה כדי לעקוב אחר ההתקדמות ולזהות תרגילים בעייתיים.

          - -

          מה נשמר במכשיר שלכם

          -

          הציונים הטובים ביותר וההעדפות שלכם (שיעור, קול) נשמרים בדפדפן שלכם בלבד, יחד עם פרטי ההתחברות.

          - -

          שיתוף עם צדדים שלישיים

          -

          איננו מוכרים או משתפים את הפרטים שלכם. השירותים שבהם האפליקציה משתמשת: Google (כניסה), Supabase (ניהול חשבונות), Hugging Face (אירוח) ו-Vimeo (סרטוני השיעורים: הנגן שלו נטען רק כשפותחים סרטון).

          - -

          מחיקה ויצירת קשר

          -

          למחיקת החשבון והפרטים שלכם, או לכל שאלה, כתבו אל wzwzchatgpt@gmail.com.

          - -

          Summary in English

          -

          Sign-in uses your Google account: we receive your name, email address and profile picture, stored with Supabase, which manages accounts for us. You can also sign in with a link sent to your email address, in which case only the address is stored. Recordings are sent to the server only to score pronunciation and are not kept afterwards. A Hebrew translation you choose to say is recognised by your browser, which may send the audio to its maker's speech service (Google, in Chrome); it is not sent to our server or kept. For each recording we store the phrase practised, the score and the time under your account, and your teacher can see them to follow your progress. Best scores and preferences stay in your browser. We do not sell or share your data. To delete your account or ask a question, write to wzwzchatgpt@gmail.com.

          -
          - - diff --git a/static/service-worker.js b/static/service-worker.js deleted file mode 100644 index 6f4cbbc819648d4551ef6f24a00a689e474ea818..0000000000000000000000000000000000000000 --- a/static/service-worker.js +++ /dev/null @@ -1,58 +0,0 @@ -const CACHE = "ihki-shell-v22"; -const SHELL = [ - "/static/manifest.webmanifest", - "/static/icons/icon-192.png?v=2", - "/static/icons/icon-512.png?v=2", - "/static/icons/icon-maskable-512.png?v=2", - "/static/logo-intro.svg", - "/static/logo-math.svg", - "/static/assets/arabic-calligraphy-path.webp" -]; - -self.addEventListener("install", event => { - event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(SHELL))); - self.skipWaiting(); -}); - -self.addEventListener("activate", event => { - event.waitUntil( - caches.keys() - .then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))) - .then(() => self.clients.claim()) - ); -}); - -self.addEventListener("fetch", event => { - if (event.request.method !== "GET") return; - const url = new URL(event.request.url); - if (url.origin !== self.location.origin || !url.pathname.startsWith("/static/")) return; - event.respondWith( - caches.match(event.request).then(cached => cached || fetch(event.request).then(response => { - if (response.ok) { - const copy = response.clone(); // cloned now: the page reads the body before the cache opens - caches.open(CACHE).then(cache => cache.put(event.request, copy)); - } - return response; - })) - ); -}); - -// the daily video's morning reminder (app/push.py) -self.addEventListener("push", event => { - let msg = {}; - try { msg = event.data ? event.data.json() : {}; } catch { msg = { body: event.data && event.data.text() }; } - event.waitUntil(self.registration.showNotification(msg.title || "אהלא וסהלא", { - body: msg.body || "", tag: msg.tag || "ihki", lang: "he", dir: "rtl", - icon: "/static/icons/icon-192.png?v=2", badge: "/static/icons/icon-192.png?v=2", - data: { url: msg.url || "/" } - })); -}); - -self.addEventListener("notificationclick", event => { - event.notification.close(); - const url = new URL((event.notification.data && event.notification.data.url) || "/", self.location.origin).href; - event.waitUntil(self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(wins => { - const open = wins.find(w => new URL(w.url).origin === self.location.origin); - return open ? open.navigate(url).then(w => (w || open).focus()) : self.clients.openWindow(url); - })); -}); diff --git a/static/trace.html b/static/trace.html deleted file mode 100644 index 368fefc56399762d108f298266c68301fba16ced..0000000000000000000000000000000000000000 --- a/static/trace.html +++ /dev/null @@ -1,732 +0,0 @@ - - - - - -לוח אבחון ציון - - - - -
          -
          -

          לוח אבחון ציון

          - - - - - -
          - -
          -

          המשפט, מילה אחר מילה

          ▶ לחצו על מילה כדי לשמוע אותה · מעבר עם העכבר על הגה מציג אותו בכל הלוחות; לחיצה מקבעת
          -
          -
          -
          75 ומעלה50–74מתחת ל־50! סומן כטעות
          -
          עץ הצבירה: הגאים ← מילים ← משפט
          -
          - -
          -
          -

          פריימים וחלון

          -
          -
          -
          -
          -
          -
          -
          -

          הציון שלב אחר שלב

          -
          -
          -
          -
          - -
          -

          קריאות מועמדות (Viterbi) ומדד "משפט אחר"

          -
          -
          -
          -
          -
          - -
          -
          טבלת הגאים (כל המספרים)
          -
          - -
          -

          מידע להתייעצות על ה־pipeline

          אבחון מלא להעתקה לצ׳אט, ללא זהות המשתמש וללא קובץ קול
          - -
          -
          -
          - -
          -

          סיבת ההחרגה

          - - -
          -
          -
          - - - - diff --git a/tests/conftest.py b/tests/conftest.py index d0671f5901d14f8bc5dabcbd60c5c84e6d9245d2..0d934b1451a4e8a25fee079e0d15bdcbe1f50196 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ -"""Tests run without sign-in whatever the local .env says: set before app.main loads it.""" +"""Tests run without sign-in regardless of the developer's local environment.""" import os os.environ["SUPABASE_URL"] = "" -os.environ["HEBREW_NIQQUD"] = "0" # no calls to Dicta's Nakdan; test_nakdan.py fakes it +os.environ["SUPABASE_PUBLISHABLE_KEY"] = "" diff --git a/tests/test_activity.py b/tests/test_activity.py deleted file mode 100644 index 32887f2835e0aefb4a9ab2c672d095d630a65748..0000000000000000000000000000000000000000 --- a/tests/test_activity.py +++ /dev/null @@ -1,115 +0,0 @@ -from datetime import datetime, timezone -from urllib.parse import parse_qs, urlsplit - -import pytest - -from app.auth import AuthUnavailable, SupabaseAuth - - -def test_scoped_activity_uses_database_aggregates(): - auth = SupabaseAuth("https://example.supabase.co", "public", secret="secret") - calls = [] - - def admin(path, method="GET", body=None, prefer="return=representation", timeout=10): - calls.append((path, body)) - if path.endswith("activity_buckets_scoped"): - return [{"bucket": "2026-10-22T10:00:00+00:00", "attempts": 4, "active_students": 2}] - return [{"attempts": 4, "active_students": 2, "returning_students": 1, "active_days": 2}] - - auth._admin = admin - data = auth.scoped_activity({"u2", "u1"}, datetime(2026, 10, 15, tzinfo=timezone.utc), 60) - assert data["summary"]["returning_students"] == 1 - assert calls[0][1]["p_user_ids"] == ["u1", "u2"] - assert calls[0][1]["p_bucket_minutes"] == 60 - - -def test_scoped_activity_fallback_is_not_limited_to_the_newest_day(): - auth = SupabaseAuth("https://example.supabase.co", "public", secret="secret") - - def admin(path, method="GET", body=None, prefer="return=representation", timeout=10): - if "/rpc/" in path: - raise AuthUnavailable("supabase answered 404: function missing") - return [ - {"user_id": "u1", "created_at": "2026-10-22T08:00:00.12345+00:00"}, - {"user_id": "u1", "created_at": "2026-10-21T08:00:00+00:00"}, - {"user_id": "u2", "created_at": "2026-10-22T09:00:00+00:00"}, - ] - - auth._admin = admin - data = auth.scoped_activity({"u1", "u2"}, datetime(2026, 10, 15, tzinfo=timezone.utc), 60) - assert data["summary"] == {"attempts": 3, "active_students": 2, "returning_students": 1, "active_days": 2} - assert sum(bucket["attempts"] for bucket in data["buckets"]) == 3 - - -def test_database_health_uses_one_bounded_read(): - auth = SupabaseAuth("https://example.supabase.co", "public", secret="secret") - calls = [] - auth._admin = lambda path, *args, **kwargs: calls.append(path) or [] - assert auth.database_health() is True - assert calls == ["/rest/v1/classes?select=id&limit=1"] - - -def test_scoped_attempts_is_bounded_encoded_and_stably_paged(monkeypatch): - monkeypatch.setattr("app.auth.PAGE", 2) - auth = SupabaseAuth("https://example.supabase.co", "public", secret="secret") - calls = [] - rows = [ - {"id": 1, "user_id": "u1", "cat": "c", "phrase": "p", "score": 10, - "unrelated": False, "created_at": "2026-10-01T00:00:00+00:00"}, - {"id": 2, "user_id": "u2", "cat": "c", "phrase": "p", "score": 20, - "unrelated": False, "created_at": "2026-10-01T01:00:00+00:00"}, - {"id": 3, "user_id": "u1", "cat": "c", "phrase": "p", "score": 30, - "unrelated": False, "created_at": "2026-10-01T02:00:00+00:00"}, - {"id": 4, "user_id": "u2", "cat": "c", "phrase": "p", "score": 40, - "unrelated": False, "created_at": "2026-10-01T03:00:00+00:00"}, - ] - - def admin(path, *args, **kwargs): - calls.append(path) - query = parse_qs(urlsplit(path).query) - offset, limit = int(query["offset"][0]), int(query["limit"][0]) - assert query["select"] == ["id,user_id,cat,phrase,score,unrelated,created_at"] - assert query["user_id"] == ["in.(u1,u2)"] - assert query["created_at"] == ["gte.2026-10-01T00:00:00+00:00", "lt.2026-10-02T00:00:00+00:00"] - assert query["order"] == ["created_at.asc,id.asc"] - return rows[offset:offset + limit] - - auth._admin = admin - got, truncated = auth.scoped_attempts( - {"u2", "u1"}, datetime(2026, 10, 1, tzinfo=timezone.utc), - datetime(2026, 10, 2, tzinfo=timezone.utc), limit=3) - assert [row["id"] for row in got] == [1, 2, 3] and truncated is True - assert len(calls) == 2 and "%2B00%3A00" in calls[0] - - -def test_scoped_attempts_empty_scope_does_not_query_and_storage_errors_propagate(): - auth = SupabaseAuth("https://example.supabase.co", "public", secret="secret") - auth._admin = lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("must not query")) - start, end = datetime(2026, 10, 1, tzinfo=timezone.utc), datetime(2026, 10, 2, tzinfo=timezone.utc) - assert auth.scoped_attempts(set(), start, end) == ([], False) - auth._admin = lambda *args, **kwargs: (_ for _ in ()).throw(AuthUnavailable("offline")) - with pytest.raises(AuthUnavailable): - auth.scoped_attempts({"u1"}, start, end) - - -def test_scoped_attempts_diagnostics_are_optional_and_missing_columns_are_safe(): - auth = SupabaseAuth("https://example.supabase.co", "public", secret="secret") - start, end = datetime(2026, 10, 1, tzinfo=timezone.utc), datetime(2026, 10, 2, tzinfo=timezone.utc) - seen = [] - - def admin(path, *args, **kwargs): - seen.append(parse_qs(urlsplit(path).query)["select"][0]) - return [{"id": 1, "user_id": "u1", "cat": "c", "phrase": "p", "score": 50, - "unrelated": False, "created_at": start.isoformat(), "issues": [], - "model": "m", "version": "v"}] - - auth._admin = admin - rows, truncated, available = auth.scoped_attempts( - {"u1"}, start, end, include_diagnostics=True) - assert available is True and truncated is False and rows[0]["model"] == "m" - assert seen == ["id,user_id,cat,phrase,score,unrelated,created_at,issues,model,version"] - - auth._admin = lambda *args, **kwargs: (_ for _ in ()).throw( - AuthUnavailable("supabase answered 400: column attempts.issues does not exist")) - assert auth.scoped_attempts({"u1"}, start, end, include_diagnostics=True) == ([], False, False) - assert auth.scoped_attempts(set(), start, end, include_diagnostics=True) == ([], False, True) diff --git a/tests/test_arabic_diacritics.py b/tests/test_arabic_diacritics.py deleted file mode 100644 index 1093fe7a846ff98a204cf8ef15313db1b9c82d60..0000000000000000000000000000000000000000 --- a/tests/test_arabic_diacritics.py +++ /dev/null @@ -1,184 +0,0 @@ -from app.arabic_diacritics import point_arabic - - -def test_points_short_vowels_sukun_and_long_vowel(): - assert point_arabic("مرحبا", "m a r ħ a b a") == "مَرْحَبَا" - - -def test_points_geminate_and_keeps_punctuation(): - assert point_arabic("يلّا!", "j a l l a") == "يَلَّا!" - - -def test_points_each_word_of_a_phrase(): - assert point_arabic("شو اسمك", "ʃ u | ʔ i s m a k") == "شُو اِسْمَكْ" - - -def test_public_phrase_has_a_separate_pointed_answer(): - from app.phrases import public_phrases - - answer = next(phrase for phrase in public_phrases() if phrase["he"] == "אני") - assert answer["ar"] == "انا" - assert answer["ar_pointed"] == "اَنَا" - - -def test_sawtak_keeps_pronunciation_enhancement_on_for_pointed_input(tmp_path): - from app.tts import SawtakTTS - - request = {} - - class Response: - headers = {"X-Sample-Rate": "24000"} - - def __enter__(self): - return self - - def __exit__(self, *_args): - pass - - sent = False - - def read(self, _n=-1): # the audio once, then the end of the stream - if self.sent: - return b"" - self.sent = True - return b"\0" * 5000 - - tts = SawtakTTS("key", cache_dir=tmp_path) - - def fake_request(path, body=None): - request.update(path=path, body=body) - return Response() - - tts._request = fake_request - tts.wav("اَكَلْ، يُاكُلْ", "voice") - assert request["path"] == "/audio/speech" - assert request["body"]["input"] == "اَكَلْ، يُاكُلْ" - assert request["body"]["enhance_pronunciation"] is True - assert "temperature" not in request["body"] - - -def test_raef_recording_is_cached_and_cache_can_be_cleared(tmp_path): - from app.tts import RAEF_VOICE_ID, SawtakTTS - - generated, requests = 0, [] - - class Response: - headers = {"X-Sample-Rate": "24000"} - - def __enter__(self): - return self - - def __exit__(self, *_args): - pass - - sent = False - - def read(self, _n=-1): # the audio once, then the end of the stream - if self.sent: - return b"" - self.sent = True - return b"\0" * 5000 - - tts = SawtakTTS("key", cache_dir=tmp_path) - - def fake_request(_path, _body=None): - nonlocal generated - generated += 1 - requests.append(_body) - return Response() - - tts._request = fake_request - first, voice = tts.wav("مَرْحَبَا", "ignored-voice") - second, second_voice = tts.wav("مَرْحَبَا") - assert first == second - assert voice == second_voice == RAEF_VOICE_ID - assert generated == 1 - assert next(tmp_path.rglob("*.wav")).parent.name == RAEF_VOICE_ID - assert all("temperature" not in body and body["enhance_pronunciation"] is True for body in requests) - - (tmp_path / "keep.txt").write_text("not audio") - assert tts.clear_cache() == 1 - assert (tmp_path / "keep.txt").exists() - assert not list(tmp_path.rglob("*.wav")) - - -def test_flat_sawtak_cache_is_migrated_into_voice_folder(tmp_path): - from app.tts import RAEF_VOICE_ID, SawtakTTS - - tts = SawtakTTS("key", cache_dir=tmp_path) - nested = tts._path("مَرْحَبَا", RAEF_VOICE_ID) - legacy = tmp_path / nested.name - legacy.write_bytes(b"old recording") - tts._generate = lambda *_args: (_ for _ in ()).throw(AssertionError("must use cache")) - - data, _ = tts.wav("مَرْحَبَا") - - assert data == b"old recording" and nested.exists() and not legacy.exists() - - -def test_sawtak_voice_id_configures_sentence_playback(monkeypatch): - from app.tts import RAEF_VOICE_ID, VOICES, load_tts - - monkeypatch.setenv("SAWTAK_API_KEY", "key") - monkeypatch.setenv("SAWTAK_VOICE_ID", "a-different-voice") - tts = load_tts() - assert VOICES == {"רַאאִף": RAEF_VOICE_ID} - assert tts.voice_id == "a-different-voice" - tts._wav = lambda _text, voice, **_kwargs: (b"audio", voice) - assert tts.wav("مَرْحَبَا") == (b"audio", "a-different-voice") - - - - -def test_a_cut_off_audio_download_is_a_retryable_tts_error(tmp_path, monkeypatch): - import http.client - - import pytest - - from app.tts import SawtakTTS, TTSUnavailable - - class Broken: - headers = {"X-Sample-Rate": "24000"} - def __enter__(self): return self - def __exit__(self, *a): return False - def read(self, _n=-1): raise http.client.IncompleteRead(b"x" * 6640) - - tts = SawtakTTS("key", cache_dir=tmp_path) - monkeypatch.setattr(tts, "_request", lambda path, body=None: Broken()) - with pytest.raises(TTSUnavailable) as e: - tts.wav("مرحبا") - assert e.value.transient - assert TTSUnavailable("x", 429).transient and TTSUnavailable("x", 502).transient - assert not TTSUnavailable("x", 400).transient and not TTSUnavailable("no audio").transient - - -def test_an_endless_audio_stream_is_abandoned_and_retryable(tmp_path, monkeypatch): - import pytest - - import app.tts as tts_module - from app.tts import SawtakTTS, TTSUnavailable - - class Endless: # Sawtak's failure mode: minutes of silence, still streaming - headers = {"X-Sample-Rate": "24000"} - def __enter__(self): return self - def __exit__(self, *a): return False - def read(self, n=-1): return b"\0" * 65536 - - monkeypatch.setattr(tts_module, "MAX_AUDIO_SECONDS", 5) - tts = SawtakTTS("key", cache_dir=tmp_path) - monkeypatch.setattr(tts, "_request", lambda path, body=None: Endless()) - with pytest.raises(TTSUnavailable) as e: - tts.wav("مرحبا") - assert e.value.transient and not list(tmp_path.rglob("*.wav")) - - -def test_the_voice_gets_huwwe_and_hiyye_respelled_but_the_page_does_not(): - from app.arabic_diacritics import point_arabic, tts_arabic - - ar, ipa = "هوّ اكل وهيّ اكلت", "h u w w e | ʔ a k a l | u h i j j e | ʔ a k l a t" - shown, said = point_arabic(ar, ipa), tts_arabic(ar, ipa) - assert said.split()[0] == "هوّة" and shown.split()[0] != "هوّة" - assert said.split()[1:] == shown.split()[1:2] + ["وُهِيِّي"] + shown.split()[3:] # "and she" too - assert tts_arabic("هوّ؟", "h u w w e") == "هوّة؟" - assert tts_arabic("هيّ", "h i j j e") == "هِيِّي" - assert tts_arabic("هوّ", "h u") == point_arabic("هوّ", "h u") # another reading: untouched diff --git a/tests/test_audio_storage.py b/tests/test_audio_storage.py deleted file mode 100644 index 8dc05085d94c7acf2e497565071f390efd2970b7..0000000000000000000000000000000000000000 --- a/tests/test_audio_storage.py +++ /dev/null @@ -1,100 +0,0 @@ -from datetime import datetime, timedelta, timezone - -from app.auth import AuthUnavailable, HFAudioStore, SupabaseAuth - - -class MemoryAudio: - def __init__(self): - self.files = {} - self.fail = set() - - def put(self, bucket, name, data): - if name in self.fail: - raise OSError("bucket unavailable") - self.files[(bucket, name)] = data - - def get(self, bucket, name): - return self.files[(bucket, name)] - - def delete(self, bucket, names): - for name in names: - self.files.pop((bucket, name), None) - - -class AudioAuth(SupabaseAuth): - def __init__(self, mirror): - super().__init__("https://p.supabase.co", "key", secret="secret", audio_store=mirror) - self.supabase = {} - self.rows = {"flags": [], "lab_samples": []} - self.deleted = [] - - def _put_object(self, bucket, name, data, mime): - self.supabase[(bucket, name)] = (data, mime) - return True - - def _get_object(self, bucket, name): - try: - return self.supabase[(bucket, name)] - except KeyError: - raise AuthUnavailable("missing") - - def _delete_objects(self, bucket, names): - self.deleted += [(bucket, name) for name in names] - for name in names: - self.supabase.pop((bucket, name), None) - return True - - def _admin(self, path, method="GET", body=None, prefer="return=representation", timeout=10): - table = "flags" if "/flags?" in path else "lab_samples" - return self.rows[table] - - -def test_new_audio_is_written_only_to_hf(): - mirror, auth = MemoryAudio(), AudioAuth(MemoryAudio()) - auth.audio_store = mirror - assert auth._put_audio("flags", "2026-10/a.webm", b"voice", "audio/webm") - assert mirror.files[("flags", "2026-10/a.webm")] == b"voice" - assert ("flags", "2026-10/a.webm") not in auth.supabase - - -def test_audio_prefers_hf_and_falls_back_to_supabase(): - mirror, auth = MemoryAudio(), AudioAuth(MemoryAudio()) - auth.audio_store = mirror - mirror.files[("flags", "old.webm")] = b"hf-old" - auth.supabase[("flags", "old.webm")] = (b"supa-old", "audio/webm") - old = (datetime.now(timezone.utc) - timedelta(days=8)).isoformat() - assert auth._get_audio("flags", "old.webm", old)[0] == b"hf-old" - auth.supabase[("flags", "legacy.webm")] = (b"legacy", "audio/webm") - assert auth._get_audio("flags", "legacy.webm", datetime.now(timezone.utc).isoformat())[0] == b"legacy" - - -def test_migration_deletes_only_successfully_mirrored_supabase_audio(): - mirror, auth = MemoryAudio(), AudioAuth(MemoryAudio()) - auth.audio_store = mirror - auth.audio_migrate_before = datetime.now(timezone.utc).isoformat() - old = (datetime.now(timezone.utc) - timedelta(days=8)).isoformat() - auth.rows["flags"] = [{"audio": "yes.webm", "created_at": old}, - {"audio": "no.webm", "created_at": old}] - auth.supabase[("flags", "yes.webm")] = (b"yes", "audio/webm") - auth.supabase[("flags", "no.webm")] = (b"no", "audio/webm") - mirror.fail.add("no.webm") - auth._migrate_legacy_audio() - assert mirror.files[("flags", "yes.webm")] == b"yes" - assert ("flags", "yes.webm") not in auth.supabase - assert auth.supabase[("flags", "no.webm")][0] == b"no" - - -def test_completed_migration_is_not_scanned_again(): - mirror, auth = MemoryAudio(), AudioAuth(MemoryAudio()) - auth.audio_store = mirror - auth.audio_migrate_before = "2026-10-17T00:00:00+03:00" - auth._migrate_legacy_audio() - auth.rows["flags"] = [{"audio": "late.webm", "created_at": "2026-10-01T00:00:00Z"}] - auth.supabase[("flags", "late.webm")] = (b"late", "audio/webm") - auth._migrate_legacy_audio() - assert ("flags", "late.webm") in auth.supabase - - -def test_hf_path_keeps_audio_namespaces_separate(): - store = HFAudioStore("owner/private", "/audio/") - assert store.path("lab", "2026-10/a.wav") == "audio/lab/2026-10/a.wav" diff --git a/tests/test_class_management.py b/tests/test_class_management.py deleted file mode 100644 index 6d8864573eaa08f7081fbe9d33c3a39fb5a73c16..0000000000000000000000000000000000000000 --- a/tests/test_class_management.py +++ /dev/null @@ -1,114 +0,0 @@ -from copy import deepcopy - -import pytest - -from app.auth import AuthUnavailable -from app.class_management import (ClassManagementError, ClassStorageUnavailable, apply, parse_roster, - prepare, public_classes) - - -INITIAL = {"ערבית": "1", "מתמטיקה": "linear-functions"} -ADMIN = {"id": "admin-1", "email": "admin@example.com", "roles": ["admin"], - "subject": "ערבית", "permissions": ["management.access"]} -DEVELOPER = {"id": "dev-1", "email": "dev@example.com", "roles": ["developer"], - "subject": "", "permissions": ["*"]} - - -class ClassAuth: - def __init__(self): - self.rows = [{"id": 1, "name": "כיתה א", "subject": "ערבית", - "emails": ["student@example.com", "teacher@example.com"], - "nicknames": {"student@example.com": "סמי"}, "teachers": ["teacher@example.com"], - "lessons": ["1", "2"], "homeworks": [{"topic": "l1-hw", "due": None}], - "dailies": [{"video": 9, "date": "2026-10-27"}], "created_at": "keep"}, - {"id": 2, "name": "ט1", "subject": "מתמטיקה", "emails": ["math@example.com"], - "nicknames": {}, "teachers": [], "lessons": ["linear-functions"], - "homeworks": [], "dailies": []}] - - def access_for(self, user_id, fresh=False): - return None - - def classes(self, fresh=False): - return deepcopy(self.rows) - - def save_class(self, class_id, name, emails, nicknames, lessons, homeworks=(), dailies=None, - subject="", teachers=None): - if class_id is None: - self.rows.insert(0, {"id": max(row["id"] for row in self.rows) + 1, "name": name, - "emails": emails, "nicknames": nicknames, "lessons": lessons, - "homeworks": list(homeworks), "dailies": list(dailies or []), - "subject": subject, "teachers": list(teachers or [])}) - return - row = next(row for row in self.rows if row["id"] == class_id) - row.update(name=name, emails=emails, nicknames=nicknames, lessons=lessons, - homeworks=list(homeworks), dailies=list(dailies or []), subject=subject) - if teachers is not None: - row["teachers"] = teachers - - -def test_browser_roster_parser_retains_existing_format(): - assert parse_roster("B@x.com בני\na@x.com\nc@x.com, d@x.com") == ( - ["a@x.com", "b@x.com", "c@x.com", "d@x.com"], {"b@x.com": "בני"}) - with pytest.raises(ValueError): - parse_roster("not an email") - - -def test_listing_is_subject_scoped_and_minimized(): - auth = ClassAuth() - assert public_classes(auth, ADMIN) == [{"id": 1, "name": "כיתה א", "subject": "ערבית", - "students": [{"email": "student@example.com", "nickname": "סמי"}, {"email": "teacher@example.com"}]}] - listed = public_classes(auth, DEVELOPER) - assert [row["id"] for row in listed] == [1, 2] - assert all(set(row) == {"id", "name", "subject", "students"} for row in listed) - assert all(set(student) <= {"email", "nickname"} for row in listed for student in row["students"]) - - -def test_create_and_rename_are_scoped_and_unique(): - auth = ClassAuth() - plan = prepare(auth, ADMIN, "create_class", name=" חדשה ", subject="מתמטיקה", - students=[{"email": "New@Example.com", "nickname": " נועה "}], initial_lessons=INITIAL) - assert plan["subject"] == "ערבית" and plan["lesson"] == "1" - made = apply(auth, ADMIN, plan, initial_lessons=INITIAL) - assert made["class_name"] == "חדשה" and auth.rows[0]["emails"] == ["new@example.com"] - with pytest.raises(ClassManagementError): - prepare(auth, ADMIN, "create_class", name="חדשה", initial_lessons=INITIAL) - with pytest.raises(ClassManagementError): - prepare(auth, ADMIN, "rename_class", class_id=2, name="אחרת", initial_lessons=INITIAL) - with pytest.raises(ClassManagementError): - prepare(auth, DEVELOPER, "create_class", name="בלי מקצוע", initial_lessons=INITIAL) - - -def test_roster_mutations_preserve_every_other_class_field(): - auth = ClassAuth() - original = deepcopy(auth.rows[0]) - add = prepare(auth, ADMIN, "add_student", class_id=1, email="NEW@example.com", nickname=" חדש ", - initial_lessons=INITIAL) - apply(auth, ADMIN, add, initial_lessons=INITIAL) - changed = auth.rows[0] - assert changed["emails"] == ["new@example.com", "student@example.com", "teacher@example.com"] - assert changed["nicknames"]["new@example.com"] == "חדש" - for key in ("name", "subject", "teachers", "lessons", "homeworks", "dailies"): - assert changed[key] == original[key] - - remove = prepare(auth, ADMIN, "remove_student", class_id=1, email="student@example.com", - initial_lessons=INITIAL) - apply(auth, ADMIN, remove, initial_lessons=INITIAL) - assert "student@example.com" not in auth.rows[0]["emails"] - assert "student@example.com" not in auth.rows[0]["nicknames"] - with pytest.raises(ClassManagementError): - prepare(auth, ADMIN, "remove_student", class_id=1, email="teacher@example.com", initial_lessons=INITIAL) - - -def test_stale_plan_and_unavailable_storage_fail_safely(): - auth = ClassAuth() - plan = prepare(auth, ADMIN, "rename_class", class_id=1, name="חדש", initial_lessons=INITIAL) - auth.rows[0]["lessons"].append("3") - with pytest.raises(ClassManagementError, match="השתנתה"): - apply(auth, ADMIN, plan, initial_lessons=INITIAL) - - class Offline(ClassAuth): - def classes(self, fresh=False): - raise AuthUnavailable("offline") - - with pytest.raises(ClassStorageUnavailable): - public_classes(Offline(), ADMIN) diff --git a/tests/test_gop_pooling.py b/tests/test_gop_pooling.py deleted file mode 100644 index bcedee990fc76abc68cc81ca1b3eb2ac329ca6cc..0000000000000000000000000000000000000000 --- a/tests/test_gop_pooling.py +++ /dev/null @@ -1,240 +0,0 @@ -"""GOP frame pooling (align.score_tokens with GopConfig.ratio_pool) on synthetic CTC emissions. - -The phrase is /k a t/; the vowel /a/ is the token under test. Every frame is a distribution: the named -sounds get their probability, a 1e-4 floor goes to the rest, blank takes what is left (CTC is peaky). -""" -from __future__ import annotations - -from dataclasses import dataclass, replace - -import numpy as np -import pytest - -from app.align import Alignment, GopConfig, POOLS, energy_weights, pool_frames, score_tokens, spread_energy - -BLANK, K, A, I, T_, SCHWA = 0, 1, 2, 3, 4, 5 -V = 6 -TOKENS = [K, A, T_] -N_FRAMES = 14 -SPANS = [(1, 2), (5, 7), (12, 13)] # k at 1, a around 5-6, t at 12: the window of /a/ is frames 2..11 -LOUD = np.array([.2, .6, .5, 1, 1, 1, 1, 1, .9, .7, .15, .1, .4, .1]) # vowel nucleus 3..9, quiet release 10..11 - - -def emissions(frames: dict[int, dict[int, float]]) -> np.ndarray: - out = np.full((N_FRAMES, V), 1e-4) - out[1, K], out[12, T_] = .9, .9 - for t, probs in frames.items(): - for tok, p in probs.items(): - out[t, tok] = p - for t in range(N_FRAMES): - out[t, BLANK] = 0.0 - out[t, BLANK] = max(1e-4, 1.0 - out[t].sum()) - return np.log(out / out.sum(1, keepdims=True)) - - -@dataclass -class Scenario: - name: str - logp: np.ndarray - good: bool # a correct /a/ (should pass) or a substitution (should fail) - energy: np.ndarray = LOUD - spans: tuple = tuple(SPANS) - - -SCENARIOS = [ - # correct /a/ over two frames, a one-frame /i/ spike in the quiet release before /t/ - Scenario("spike in release", emissions({4: {A: .2}, 5: {A: .4}, 6: {A: .35}, 10: {I: .9}}), True), - # the same spike inside the loud nucleus: energy cannot tell it apart, the pooling has to - Scenario("spike in nucleus", emissions({4: {A: .2}, 5: {A: .4}, 6: {A: .35}, 8: {I: .9}}), True), - # a correct /a/ emitted on one frame, blanks everywhere else: nothing must dilute it - Scenario("peaky emission", emissions({5: {A: .92}}), True), - # the same with a weak schwa haze across the whole window - Scenario("peaky + haze", emissions({**{t: {SCHWA: .04} for t in range(2, 12)}, 5: {A: .92, SCHWA: .04}}), True), - # CTC put the emission a frame before the loud nucleus (alignment jitter) - Scenario("emission off nucleus", emissions({3: {A: .8}, 9: {I: .5}}), True, - energy=np.array([.2, .6, .5, .2, 1, 1, 1, 1, 1, .9, .15, .1, .4, .1]), spans=((1, 2), (3, 4), (12, 13))), - # /i/ said instead of /a/, held over three frames - Scenario("sustained substitution", emissions({4: {I: .6, A: .08}, 5: {I: .75, A: .1}, 6: {I: .6, A: .05}}), False), - # a short /i/ said instead of /a/: also a one-frame spike, but /a/ is absent - Scenario("one-frame substitution", emissions({5: {I: .85, A: .03}}), False), -] - - -def score_a(sc: Scenario, cfg: GopConfig, energy: bool = True): - al = Alignment(0.0, list(sc.spans)) - return score_tokens(sc.logp, al, TOKENS, BLANK, cfg, energy=sc.energy if energy else None)[1] - - -CONFIGS = { - "max": GopConfig(ratio_pool="max"), - "mean": GopConfig(ratio_pool="mean"), - "top2": GopConfig(ratio_pool="topk", pool_k=2), - "top3": GopConfig(ratio_pool="topk", pool_k=3), - "lse": GopConfig(ratio_pool="lse", pool_tau=0.5), - "energy": GopConfig(ratio_pool="energy"), - "gated": GopConfig(ratio_pool="gated"), -} - - -def naive_mean_score(sc: Scenario, cfg: GopConfig = GopConfig()) -> float: - """The diluting variant this design avoids: the mean posterior over the window used for the ratio AND the confidence.""" - a, b = sc.spans[0][1], sc.spans[2][0] - p = np.exp(sc.logp[a:b]).mean(0) - comp = max(p[I], p[SCHWA]) - return 100 * np.exp(cfg.leniency * (np.log(p[A]) - np.log(max(p[A], comp)))) * np.sqrt(min(1, p[A] / cfg.confident_peak)) - - -# ---- the pooling operator ---- - -def test_pool_limits(): - seg = np.log(np.array([[.9, .1], [.05, .1], [.05, .8]])) - assert np.allclose(pool_frames(seg, "max"), seg.max(0)) - assert np.allclose(pool_frames(seg, "mean"), np.log(np.exp(seg).mean(0))) - assert np.allclose(pool_frames(seg, "topk", k=1), seg.max(0)) - assert np.allclose(pool_frames(seg, "topk", k=3), pool_frames(seg, "mean")) - assert np.allclose(pool_frames(seg, "lse", tau=1.0), pool_frames(seg, "mean")) - assert np.allclose(pool_frames(seg, "lse", tau=0.02), seg.max(0), atol=0.03) - # a 0/1 weight is the mean over the kept frames - assert np.allclose(pool_frames(seg, "mean", weight=np.array([1, 0, 1])), np.log(np.exp(seg[[0, 2]]).mean(0))) - - -def test_mass_ratio_is_window_length_invariant(): - """Padding the window with blank frames dilutes each pooled posterior but not their ratio - (up to the floor each padded frame adds to both masses).""" - seg = np.log(np.array([[.6, .3, .1], [.1, .2, .7]])) - pad = np.log(np.tile([[1e-4, 1e-4, 1 - 2e-4]], (20, 1))) - r = lambda s: pool_frames(s, "mean")[0] - pool_frames(s, "mean")[1] - assert r(np.vstack([seg, pad])) == pytest.approx(r(seg), abs=5e-3) - - -def test_spread_energy(): - assert spread_energy(np.array([0, 0, 1., 0, 0]), 1).tolist() == [0, 1, 1, 1, 0] - - -# ---- the scenarios ---- - -def test_max_pool_is_the_previous_scorer(): - """ratio_pool='max' is bit-for-bit the original formula.""" - for sc in SCENARIOS: - s = score_a(sc, CONFIGS["max"]) - a, b = s.window - comp_peak = sc.logp[a:b, [I, SCHWA]].max() - gop = s.peak - max(s.peak, comp_peak) - want = 100 * np.exp(0.5 * gop) * np.sqrt(min(1, np.exp(s.peak) / 0.30)) - assert s.score == pytest.approx(want, abs=0.06), sc.name - - -@pytest.mark.parametrize("pool", ["top2", "mean", "energy", "gated"]) -def test_spike_in_release_passes(pool): - sc = SCENARIOS[0] - assert score_a(sc, CONFIGS["max"]).score < 70 # the flaw - assert score_a(sc, CONFIGS[pool]).score >= 80 - - -def test_lse_only_partly_absorbs_a_tall_spike(): - """A power mean still favours one 0.9 frame over a 0.4/0.35/0.2 target; colder is closer to the max.""" - sc = SCENARIOS[0] - hot, warm, cold = (score_a(sc, GopConfig(ratio_pool="lse", pool_tau=t)).score for t in (1.0, 0.5, 0.25)) - assert score_a(sc, CONFIGS["max"]).score < cold < warm < hot - - -@pytest.mark.parametrize("pool", ["mean", "energy", "gated"]) -def test_spike_in_nucleus_passes(pool): - """Energy cannot hide a spike on a loud frame; the mass ratio (the /a/ has more frames) still does.""" - assert score_a(SCENARIOS[1], CONFIGS[pool]).score >= 80 - - -def test_energy_weights_the_quiet_spike_down(): - """Compared before the gop clips at 0: the pooled log ratio /a/ over /i/.""" - sc = SCENARIOS[0] - a, b = score_a(sc, CONFIGS["max"]).window - seg = sc.logp[a:b] - margin = lambda w: float(np.subtract(*pool_frames(seg[:, [A, I]], "mean", weight=w))) - e0, e1 = sc.energy[a:b], spread_energy(sc.energy, 1)[a:b] - assert margin(energy_weights(e1, "energy")) > margin(None) - # the spread that forgives a jittered target emission also lifts a spike next to the nucleus over the gate - assert margin(energy_weights(e0, "gated")) > margin(energy_weights(e1, "energy")) > margin(energy_weights(e1, "gated")) - - -def test_long_pause_haze_needs_the_gate(): - """A faint competitor over a long silent window adds up in a plain mean; the voice gate drops the silence.""" - n = 30 - p = np.full((n, V), 1e-4) - p[:, SCHWA] = .05 - p[1, K], p[5, A], p[n - 2, T_] = .9, .9, .9 - p[:, BLANK] = 0 - p[:, BLANK] = 1 - p.sum(1) - energy = np.r_[[.3, .6, .5], np.ones(4), np.full(n - 9, .02), [.5, .1]] - al = Alignment(0.0, [(1, 2), (5, 6), (n - 2, n - 1)]) - s = lambda cfg: score_tokens(np.log(p), al, TOKENS, BLANK, cfg, energy=energy, - vowels=[False, True, False])[1].score - assert s(CONFIGS["mean"]) < 85 - assert s(CONFIGS["gated"]) >= 95 and s(CONFIGS["top2"]) >= 95 - - -@pytest.mark.parametrize("pool", list(CONFIGS)) -@pytest.mark.parametrize("name", ["peaky emission", "peaky + haze"]) -def test_peaky_emission_is_not_diluted(pool, name): - sc = next(s for s in SCENARIOS if s.name == name) - assert score_a(sc, CONFIGS[pool]).score >= 95 - assert naive_mean_score(sc) < 70 # the flaw: averaging the confidence too - - -@pytest.mark.parametrize("pool", list(CONFIGS)) -def test_emission_off_the_nucleus_still_passes(pool): - sc = next(s for s in SCENARIOS if s.name == "emission off nucleus") - assert score_a(sc, CONFIGS[pool]).score >= 85 - - -@pytest.mark.parametrize("pool", ["energy", "gated"]) -def test_the_aligned_emission_is_never_gated_out(pool): - """Without the spread the emission frame is quiet, yet the alignment's own span keeps its full weight.""" - sc = next(s for s in SCENARIOS if s.name == "emission off nucleus") - assert score_a(sc, replace(CONFIGS[pool], energy_spread=0, energy_gate=0.9)).score >= 85 - - -@pytest.mark.parametrize("pool", list(CONFIGS)) -@pytest.mark.parametrize("name", ["sustained substitution", "one-frame substitution"]) -def test_substitution_fails(pool, name): - sc = next(s for s in SCENARIOS if s.name == name) - s = score_a(sc, CONFIGS[pool]) - assert s.score < 40 - assert s.competitor == I - - -@pytest.mark.parametrize("pool", list(CONFIGS)) -def test_energy_pools_without_audio_fall_back_to_mean(pool): - sc = SCENARIOS[0] - s = score_a(sc, CONFIGS[pool], energy=False) - want = CONFIGS["mean"] if pool in ("energy", "gated") else CONFIGS[pool] - assert s.score == score_a(sc, want, energy=False).score - - -def test_unpooled_tokens_keep_the_max(): - sc = SCENARIOS[0] - al = Alignment(0.0, list(sc.spans)) - on = score_tokens(sc.logp, al, TOKENS, BLANK, CONFIGS["mean"], pooled=[False, True, False])[1] - off = score_tokens(sc.logp, al, TOKENS, BLANK, CONFIGS["mean"], pooled=[True, False, True])[1] - assert on.score > off.score == score_a(sc, CONFIGS["max"]).score - - -def test_peak_and_deletion_do_not_depend_on_the_pool(): - for sc in SCENARIOS: - ref = score_a(sc, CONFIGS["max"]) - for pool in POOLS[1:]: - s = score_a(sc, GopConfig(ratio_pool=pool)) - assert (s.peak, s.deleted, s.window) == (ref.peak, ref.deleted, ref.window) - - -# ---- GOP_RATIO_POOL ---- - -def test_env_picks_the_pool(monkeypatch): - monkeypatch.delenv("GOP_RATIO_POOL", raising=False) - assert GopConfig().ratio_pool == "gated" - monkeypatch.setenv("GOP_RATIO_POOL", "") - assert GopConfig().ratio_pool == "gated" - monkeypatch.setenv("GOP_RATIO_POOL", " Max ") - assert GopConfig().ratio_pool == "max" - monkeypatch.setenv("GOP_RATIO_POOL", "maxx") - assert GopConfig().ratio_pool == "gated" # a typo is logged, not a crash - assert GopConfig(ratio_pool="topk").ratio_pool == "topk" # an explicit config wins over the env diff --git a/tests/test_gop_window.py b/tests/test_gop_window.py deleted file mode 100644 index d421fbf8557255f4c45333cc3cbc664275bafe73..0000000000000000000000000000000000000000 --- a/tests/test_gop_window.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Class-based GOP windows (align.compute_token_gop): a consonant reads its competitors from the wide window, a vowel -from its aligned span +- GopConfig.vowel_window frames. The spans come from ctc_viterbi, as in the scorer. - -Every frame is a distribution: the named sounds get their probability, a 1e-4 floor the rest, blank what is left. -""" -from __future__ import annotations - -from dataclasses import dataclass, field, replace - -import numpy as np -import pytest - -from app.align import GopConfig, compute_token_gop, ctc_viterbi, score_tokens, token_window - -BLANK, K, A, I, T_, Q, J = 0, 1, 2, 3, 4, 5, 6 -V = 7 -NAMES = {BLANK: "_", K: "k", A: "a", I: "i", T_: "t", Q: "q", J: "j"} - - -def emissions(n: int, frames: dict[int, dict[int, float]]) -> np.ndarray: - out = np.full((n, V), 1e-4) - for t, probs in frames.items(): - for tok, p in probs.items(): - out[t, tok] = p - out[:, BLANK] = 0.0 - out[:, BLANK] = np.maximum(1e-4, 1.0 - out.sum(1)) - return np.log(out / out.sum(1, keepdims=True)) - - -@dataclass -class Case: - name: str - tokens: list[int] - frames: dict[int, dict[int, float]] - target: int # the index of the token under test - good: bool # said right (should pass) or not (should fail) - n: int = 14 - edges: list = field(default_factory=list) - - @property - def logp(self): - return emissions(self.n, self.frames) - - @property - def vowels(self): - return [t in (A, I) for t in self.tokens] - - -CASES = [ - # /a/ over frames 4-6, a one-frame /i/ spike in the transition into /t/ - Case("vowel: spike in transition", [K, A, T_], - {1: {K: .9}, 4: {A: .3}, 5: {A: .5}, 6: {A: .4}, 9: {I: .9}, 12: {T_: .9}}, 1, True), - # /i/ said for /a/, held across the nucleus - Case("vowel: sustained substitution", [K, A, T_], - {1: {K: .9}, 4: {I: .6, A: .08}, 5: {I: .75, A: .1}, 6: {I: .6, A: .05}, 12: {T_: .9}}, 1, False), - # /a/ present but weak, /i/ held two frames later, outside the narrow window: what the narrow window gives up - Case("vowel: displaced substitution", [K, A, T_], - {1: {K: .9}, 4: {A: .35}, 7: {I: .8}, 8: {I: .85}, 12: {T_: .9}}, 1, False), - # /k/ barely there, the /q/ said instead peaking in the blanks before it - Case("consonant: competitor in blanks", [A, K, A], - {2: {A: .9}, 4: {Q: .85}, 5: {Q: .5}, 7: {K: .08}, 11: {A: .9}}, 1, False), - # /a/ deleted: the alignment gives it one frame it barely has - Case("vowel: deleted", [K, A, T_], {1: {K: .9}, 6: {A: .02}, 12: {T_: .9}}, 1, False), - # /ja/: the glide's /i/ colour lingers into the frame before the vowel - Case("vowel: glide transition", [J, A, T_], - {1: {J: .9}, 3: {I: .8}, 4: {I: .8}, 5: {A: .45}, 6: {A: .4}, 12: {T_: .9}}, 1, True, - edges=[None, ({I}, set()), None]), - # /ji/ said for /ja/: the vowel's own frames are /i/, so the glide does not excuse it - Case("vowel: substitution beside glide", [J, A, T_], - {1: {J: .9}, 5: {I: .7, A: .05}, 6: {I: .7, A: .05}, 7: {I: .6}, 12: {T_: .9}}, 1, False, - edges=[None, ({I}, set()), None]), -] - -WIDE, NARROW = GopConfig(ratio_pool="max", vowel_window=None), GopConfig(ratio_pool="max", vowel_window=1) - - -def case(name: str) -> Case: - return next(c for c in CASES if c.name == name) - - -def score(c: Case, cfg: GopConfig, edges: bool = True): - al = ctc_viterbi(c.logp, c.tokens, BLANK) - return score_tokens(c.logp, al, c.tokens, BLANK, cfg, vowels=c.vowels, - edges=c.edges if edges and c.edges else None)[c.target] - - -def test_windows(): - spans = [(1, 2), (5, 6), (12, 13)] - assert token_window(spans, 1, 14) == (2, 12) - assert token_window(spans, 1, 14, 1) == (4, 7) - assert token_window(spans, 1, 14, 0) == (5, 6) - assert token_window(spans, 1, 14, 9) == (2, 12) # never past the neighbours - assert token_window(spans, 0, 14, 1) == (0, 3) - - -def test_consonant_window_does_not_cross_a_long_pause(): - logp = np.log(np.full((100, 4), 0.01)) - logp[:, BLANK] = np.log(0.95) - logp[44, 1] = np.log(0.9) # the aligned consonant - logp[70, 3] = np.log(0.99) # unrelated sound deep inside the following pause - spans = [(44, 45), (80, 81)] - score = compute_token_gop(logp, spans, 0, [1, 2], BLANK, GopConfig(consonant_window=4)) - assert score.window == (40, 49) - assert score.score == 100 and np.exp(score.comp_peak) < 0.02 # not the 99% peak at frame 70 - - -def test_wide_vowel_window_is_the_previous_scorer(): - """vowel_window None reads a vowel exactly as score_tokens did before it had classes.""" - for c in CASES: - al = ctc_viterbi(c.logp, c.tokens, BLANK) - old = score_tokens(c.logp, al, c.tokens, BLANK, replace(WIDE, consonant_window=None)) - new = score_tokens(c.logp, al, c.tokens, BLANK, WIDE, vowels=c.vowels) - assert new[c.target] == old[c.target], c.name - - -def test_narrow_window_drops_a_transition_spike(): - c = case("vowel: spike in transition") - assert score(c, WIDE).score < 80 and score(c, WIDE).competitor == I - assert score(c, NARROW).score >= 95 - - -def test_sustained_vowel_substitution_still_fails(): - s = score(case("vowel: sustained substitution"), NARROW) - assert s.score < 30 and s.competitor == I - - -def test_narrow_window_gives_up_a_displaced_substitution(): - """The cost: a target that is present, with the real vowel held outside span +- pad, now passes.""" - c = case("vowel: displaced substitution") - assert score(c, WIDE).score < 70 - assert score(c, NARROW).score >= 95 - assert score(c, GopConfig(ratio_pool="max", vowel_window=3)).score < 70 # a wider pad catches it again - - -def test_wide_pooled_ratio_gets_both(): - """Why wide is the default: a pooled ratio passes the one-frame spike and still fails the held substitution.""" - cfg = GopConfig(ratio_pool="mean", vowel_window=None) - assert score(case("vowel: spike in transition"), cfg).score >= 95 - assert score(case("vowel: displaced substitution"), cfg).score < 50 - - -def test_consonant_keeps_the_wide_window(): - c = case("consonant: competitor in blanks") - for cfg in (WIDE, NARROW): - s = score(c, cfg) - assert s.score < 50 and s.competitor == Q - # read as a vowel (narrow), the /q/ in the blanks is lost - al = ctc_viterbi(c.logp, c.tokens, BLANK) - narrow = compute_token_gop(c.logp, al.spans, 1, c.tokens, BLANK, NARROW, vowel=True) - assert narrow.competitor != Q - - -@pytest.mark.parametrize("pad", [None, 0, 1, 2]) -def test_deletion_is_read_from_the_wide_window(pad): - c = case("vowel: deleted") - al = ctc_viterbi(c.logp, c.tokens, BLANK) - assert al.spans[1][1] - al.spans[1][0] == 1 # one frame - s = score(c, GopConfig(ratio_pool="max", vowel_window=pad)) - assert s.deleted and s.score < 30 - - -def test_a_competitor_outside_the_narrow_window_still_blocks_deletion(): - """Not deleted but substituted: the /i/ is strong in the wide window, if out of the narrow one.""" - c = Case("x", [K, A, T_], {1: {K: .9}, 6: {A: .02}, 9: {I: .9}, 12: {T_: .9}}, 1, False) - assert not score(c, NARROW).deleted - - -def test_glide_colour_is_not_a_competitor(): - c = case("vowel: glide transition") - assert score(c, NARROW, edges=False).score < 85 - assert score(c, NARROW).score >= 95 - assert score(c, WIDE).score >= 95 # the mask works in the wide window too - - -def test_glide_does_not_excuse_a_substitution(): - s = score(case("vowel: substitution beside glide"), NARROW) - assert s.score < 30 and s.competitor == I - - -def test_glide_colour_does_not_block_deletion(): - c = Case("x", [J, A, T_], {1: {J: .9}, 3: {I: .3}, 6: {A: .02}, 12: {T_: .9}}, 1, False, - edges=[None, ({I}, set()), None]) - assert score(c, NARROW, edges=False).deleted is False - assert score(c, NARROW).deleted - - -def test_env_picks_the_vowel_window(monkeypatch): - for raw, want in (("", None), ("wide", None), (" Wide ", None), ("narrow", 1), ("0", 0), ("3", 3), ("tight", None)): - monkeypatch.setenv("GOP_VOWEL_WINDOW", raw) - assert GopConfig().vowel_window == want, raw - monkeypatch.delenv("GOP_VOWEL_WINDOW") - assert GopConfig().vowel_window is None - - -def test_scorer_marks_glides_beside_vowels(): - from types import SimpleNamespace - - from app.scorer import Scorer, TargetToken - - ids = {"j": 1, "a": 2, "w": 3, "i": 4, "iː": 5, "u": 6} # no e, eː, uː, o, oː in this model - fake = SimpleNamespace(vocab=SimpleNamespace(sound_id=ids.get)) - toks = [TargetToken(p, ids[p], 0) for p in ("j", "a", "w", "a")] - assert Scorer._glide_edges(fake, toks) == [None, ({4, 5}, {6}), None, ({6}, set())] diff --git a/tests/test_grok_mcp.py b/tests/test_grok_mcp.py deleted file mode 100644 index 17aef28dadeeb846fe71a53cdde32e557e8abc2a..0000000000000000000000000000000000000000 --- a/tests/test_grok_mcp.py +++ /dev/null @@ -1,609 +0,0 @@ -"""The Grok connector has a deliberately tiny, independently authenticated scope.""" -import asyncio -from datetime import datetime, timedelta, timezone -from types import SimpleNamespace - -import httpx -import pytest -from mcp.server.mcpserver.exceptions import ToolError - -from app.auth import AuthUnavailable -from app.grok_mcp import (ApprovalStore, ClassApprovalStore, MutationLimiter, - SupabaseTokenVerifier, build) -from app.main import create_app, video_link -from app.live import LiveStore -from tests.test_scoring import emissions, scorer_for - - -class Clock: - def __init__(self): - self.now = 100.0 - - def __call__(self): - return self.now - - -class LibraryAuth: - url = "https://example.supabase.co" - key = "publishable" - teachers = frozenset({"teacher@example.com"}) - tutors = frozenset() - - def __init__(self): - self.videos = [] - self.class_rows = [] - self.events = [] - - def level_videos(self, table, fresh=False): - assert table == "library" - return list(self.videos) - - def add_level_video(self, table, level, title, url): - self.videos.append({"id": len(self.videos) + 1, "level": level, "title": title, "url": url}) - - def rename_level_video(self, table, video_id, title): - next(video for video in self.videos if video["id"] == video_id)["title"] = title - - def log_event(self, kind, actor, label, details): - self.events.append((kind, actor, label, details)) - - def user(self, token): - if token == "teacher-session": - return {"id": "teacher-1", "email": "teacher@example.com"} - if token == "student-session": - return {"id": "student-1", "email": "student@example.com"} - return None - - def classes(self, fresh=False): - return [dict(row) for row in self.class_rows] - - def users(self): - return [{"id": "student-1", "email": "student@example.com", "name": "Student"}] - - def scoped_activity(self, ids, since, bucket_minutes): - return {"buckets": [], "summary": {"attempts": 0, "active_students": 0, - "returning_students": 0, "active_days": 0}} - - def scoped_attempts(self, ids, since, until, *, include_diagnostics=False): - return ([], False, True) if include_diagnostics else ([], False) - - def save_class(self, class_id, name, emails, nicknames, lessons, homeworks=(), dailies=None, - subject="", teachers=None): - if class_id is None: - self.class_rows.append({"id": len(self.class_rows) + 1, "name": name, "emails": emails, - "nicknames": nicknames, "lessons": lessons, "homeworks": list(homeworks), - "dailies": list(dailies or []), "subject": subject, "teachers": list(teachers or [])}) - return - row = next(row for row in self.class_rows if row["id"] == class_id) - row.update(name=name, emails=emails, nicknames=nicknames, lessons=lessons, - homeworks=list(homeworks), dailies=list(dailies or []), subject=subject) - if teachers is not None: - row["teachers"] = teachers - - def ranked(self, fresh=False): - return [] - - def access_for(self, user_id, fresh=False): - return None - - -class UnavailableAuth(LibraryAuth): - def level_videos(self, table, fresh=False): - raise AuthUnavailable("offline") - - -def connector(auth=None, clock=None, actor=None, live=None): - clock = clock or Clock() - host = SimpleNamespace(state=SimpleNamespace(auth=auth, video_link=video_link, scorer=object(), tts=None, - mcp_build_info={"version": "test", "revision": "abc"}, - initial_class_lessons={"ערבית": "1", "מתמטיקה": "linear-functions"}, - live=live or LiveStore())) - server, protected, _ = build(host, approvals=ApprovalStore(clock), limiter=MutationLimiter(clock), - class_approvals=ClassApprovalStore(clock), actor_provider=(lambda: actor) if actor else None) - return server, protected - - -def call(server, name, arguments=None): - answer = asyncio.run(server.call_tool(name, arguments or {})) - return answer.structured_content - - -def test_narrow_tools_are_discoverable(): - server, _ = connector(LibraryAuth()) - tools = asyncio.run(server.list_tools()) - assert [tool.name for tool in tools] == [ - "list_library_videos", "prepare_library_video", "publish_library_video", "rename_library_video", - "list_classes", "prepare_class_change", "publish_class_change", - "get_activity_timeseries", "get_current_activity", "get_system_health", "get_exercise_difficulty", - "get_pronunciation_difficulties", "get_learning_progress", "compare_class_difficulties", - "get_learning_bottlenecks"] - assert all(tool.output_schema for tool in tools) - difficulty = next(tool for tool in tools if tool.name == "get_exercise_difficulty") - assert set(difficulty.input_schema["properties"]) == { - "from_date", "to_date", "class_id", "exercise_type", "limit", "min_attempts"} - assert set(difficulty.input_schema["required"]) == {"from_date", "to_date"} - assert difficulty.input_schema["properties"]["limit"]["default"] == 20 - current = next(tool for tool in tools if tool.name == "get_current_activity") - assert set(current.input_schema["properties"]) == {"window_minutes", "class_id"} - assert current.input_schema["properties"]["window_minutes"]["enum"] == [5, 15] - assert difficulty.input_schema["properties"]["min_attempts"]["default"] == 10 - assert not any(word in tool.name for tool in tools - for word in ("delete", "teacher", "user", "role", "lesson", "homework", "grade", "recording", - "sql", "log", "environment", "timeline", "remediate", "restart", "shell", - "learner", "raw_attempt", "abandonment", "answer", "calibration")) - - -def test_list_and_rename_return_only_library_metadata(): - auth = LibraryAuth() - auth.videos = [{"id": 7, "level": "", "title": "ישן", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "student_email": "must-not-leak@example.com"}] - server, _ = connector(auth) - listed = call(server, "list_library_videos") - assert listed == {"ok": True, "videos": [{"id": 7, "title": "ישן", - "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "position": 1}]} - assert call(server, "rename_library_video", {"video_id": 7, "title": " שם חדש "}) == { - "ok": True, "video_id": 7, "title": "שם חדש"} - assert auth.videos[0]["url"].endswith("dQw4w9WgXcQ") - assert call(server, "rename_library_video", {"video_id": 99, "title": "x"})["ok"] is False - assert call(server, "rename_library_video", {"video_id": 7, "title": ""})["ok"] is False - assert auth.events and all("token" not in str(event).lower() for event in auth.events) - - -def test_prepare_publish_is_expiring_single_use_and_rechecks_duplicates(): - clock, auth = Clock(), LibraryAuth() - server, _ = connector(auth, clock) - prepared = call(server, "prepare_library_video", { - "title": " ברכות בערבית ", "url": "https://youtu.be/dQw4w9WgXcQ?t=10"}) - assert prepared["ok"] and not prepared["published"] - assert prepared["title"] == "ברכות בערבית" - assert prepared["url"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" - published = call(server, "publish_library_video", {"approval_id": prepared["approval_id"]}) - assert published["ok"] and published["published"] and len(auth.videos) == 1 - assert call(server, "publish_library_video", {"approval_id": prepared["approval_id"]})["ok"] is False - assert call(server, "prepare_library_video", { - "title": "כפול", "url": "https://youtu.be/dQw4w9WgXcQ"})["ok"] is False - - other = call(server, "prepare_library_video", {"title": "אחר", "url": "https://vimeo.com/1233611297/d46ac373c8"}) - auth.videos.append({"id": 2, "level": "", "title": "נוסף במקביל", "url": other["url"]}) - assert call(server, "publish_library_video", {"approval_id": other["approval_id"]})["ok"] is False - - expiring = call(server, "prepare_library_video", {"title": "פג", "url": "https://youtu.be/abc12345678"}) - clock.now += 601 - assert call(server, "publish_library_video", {"approval_id": expiring["approval_id"]})["ok"] is False - - -def test_invalid_and_unavailable_operations_are_safe(): - server, _ = connector(LibraryAuth()) - assert call(server, "prepare_library_video", {"title": "x", "url": "https://example.com/x"})["ok"] is False - assert call(server, "prepare_library_video", {"title": "", "url": "https://youtu.be/dQw4w9WgXcQ"})["ok"] is False - unavailable, _ = connector(UnavailableAuth()) - assert call(unavailable, "list_library_videos")["ok"] is False - assert call(unavailable, "prepare_library_video", { - "title": "x", "url": "https://youtu.be/dQw4w9WgXcQ"})["ok"] is False - - -def test_oauth_verifier_requires_current_management_permission(): - class ChangingAuth(LibraryAuth): - teachers = frozenset() - - def __init__(self): - super().__init__() - self.roles = ["admin"] - self.subject = "ערבית" - self.fresh = [] - - def access_for(self, user_id, fresh=False): - self.fresh.append(fresh) - return {"roles": self.roles, "entitlements": [], "subject": self.subject} - - auth = ChangingAuth() - host = SimpleNamespace(state=SimpleNamespace(auth=auth)) - verifier = SupabaseTokenVerifier(host) - accepted = asyncio.run(verifier.verify_token("teacher-session")) - assert accepted and accepted.subject == "teacher-1" and accepted.token == "teacher-session" - assert accepted.claims["ihki_actor"]["id"] == "teacher-1" - assert accepted.claims["ihki_actor"]["email"] == "teacher@example.com" - assert accepted.claims["ihki_actor"]["roles"] == ["admin"] - assert accepted.claims["ihki_actor"]["subject"] == "ערבית" - assert "management.access" in accepted.claims["ihki_actor"]["permissions"] - assert "teacher-session" not in str(accepted.claims) - auth.roles = ["student"] - assert asyncio.run(verifier.verify_token("teacher-session")) is None - assert asyncio.run(verifier.verify_token("malformed")) is None - assert auth.fresh == [True, True] - - class BrokenAuth(ChangingAuth): - def user(self, token): - raise AuthUnavailable("offline") - - assert asyncio.run(SupabaseTokenVerifier( - SimpleNamespace(state=SimpleNamespace(auth=BrokenAuth()))).verify_token("expired-or-offline")) is None - - -def test_class_approvals_are_expiring_single_use_and_actor_bound(): - clock, approvals = Clock(), ClassApprovalStore(Clock()) - approvals = ClassApprovalStore(clock) - approval_id, _ = approvals.prepare("manager-1", {"operation": "rename_class"}) - assert approvals.consume(approval_id, "manager-2") is None - assert approvals.consume(approval_id, "manager-1") is not None - assert approvals.consume(approval_id, "manager-1") is None - expiring, _ = approvals.prepare("manager-1", {"operation": "rename_class"}) - clock.now += 601 - assert approvals.consume(expiring, "manager-1") is None - - -def test_class_tools_manage_only_scoped_rosters_with_confirmation(): - actor = {"id": "admin-1", "email": "admin@example.com", "roles": ["admin"], - "subject": "ערבית", "permissions": ["management.access"]} - auth = LibraryAuth() - auth.class_rows = [{"id": 1, "name": "א", "subject": "ערבית", "emails": ["old@example.com"], - "nicknames": {"old@example.com": "ישן"}, "teachers": [], "lessons": ["1", "2"], - "homeworks": [{"topic": "l1-hw", "due": None}], "dailies": [{"video": 2, "date": "2026-10-27"}]}, - {"id": 2, "name": "מתמטיקה", "subject": "מתמטיקה", "emails": ["math@example.com"], - "nicknames": {}, "teachers": [], "lessons": ["linear-functions"], "homeworks": [], "dailies": []}] - server, _ = connector(auth, actor=actor) - listed = call(server, "list_classes") - assert listed == {"ok": True, "classes": [{"id": 1, "name": "א", "subject": "ערבית", - "students": [{"email": "old@example.com", "nickname": "ישן"}]}]} - - prepared = call(server, "prepare_class_change", {"operation": "add_student", "class_id": 1, - "email": "NEW@example.com", "nickname": "חדש"}) - assert prepared["ok"] and not prepared["published"] and len(auth.class_rows[0]["emails"]) == 1 - published = call(server, "publish_class_change", {"approval_id": prepared["approval_id"]}) - assert published["ok"] and published["published"] and "new@example.com" in auth.class_rows[0]["emails"] - assert auth.class_rows[0]["lessons"] == ["1", "2"] and auth.class_rows[0]["homeworks"] - assert call(server, "publish_class_change", {"approval_id": prepared["approval_id"]})["ok"] is False - - renamed = call(server, "prepare_class_change", {"operation": "rename_class", "class_id": 1, "name": "חדשה"}) - assert call(server, "publish_class_change", {"approval_id": renamed["approval_id"]})["class_name"] == "חדשה" - removed = call(server, "prepare_class_change", {"operation": "remove_student", "class_id": 1, - "email": "old@example.com"}) - assert call(server, "publish_class_change", {"approval_id": removed["approval_id"]})["ok"] - created = call(server, "prepare_class_change", {"operation": "create_class", "name": "חדשים", - "students": [{"email": "first@example.com", "nickname": "ראשון"}]}) - assert call(server, "publish_class_change", {"approval_id": created["approval_id"]})["ok"] - assert len(auth.class_rows) == 3 - assert all("token" not in str(event).lower() and "nickname" not in str(event).lower() for event in auth.events) - - -def test_failed_class_publish_consumes_approval_and_does_not_claim_success(): - class FailingAuth(LibraryAuth): - def save_class(self, *args, **kwargs): - raise AuthUnavailable("offline") - - actor = {"id": "admin-1", "email": "admin@example.com", "roles": ["admin"], - "subject": "ערבית", "permissions": ["management.access"]} - auth = FailingAuth() - auth.class_rows = [{"id": 1, "name": "א", "subject": "ערבית", "emails": [], "nicknames": {}, - "teachers": [], "lessons": ["1"], "homeworks": [], "dailies": []}] - server, _ = connector(auth, actor=actor) - prepared = call(server, "prepare_class_change", {"operation": "rename_class", "class_id": 1, "name": "ב"}) - failed = call(server, "publish_class_change", {"approval_id": prepared["approval_id"]}) - replay = call(server, "publish_class_change", {"approval_id": prepared["approval_id"]}) - assert failed["ok"] is False and replay["ok"] is False and auth.class_rows[0]["name"] == "א" - - -def test_monitoring_tools_are_scoped_read_only_and_privacy_preserving(): - actor = {"id": "admin-1", "email": "admin@example.com", "roles": ["admin"], - "subject": "ערבית", "permissions": ["management.access"]} - - class MonitoringAuth(LibraryAuth): - def __init__(self): - super().__init__() - self.class_rows = [ - {"id": 1, "name": "ערבית", "subject": "ערבית", "emails": ["student@example.com"]}, - {"id": 2, "name": "מתמטיקה", "subject": "מתמטיקה", "emails": ["math@example.com"]}, - ] - self.activity_ids = None - - def users(self): - return [{"id": "student-1", "email": "student@example.com"}, - {"id": "math-1", "email": "math@example.com"}] - - def scoped_activity(self, ids, since, bucket_minutes): - self.activity_ids = ids - return {"buckets": [{"bucket": "2026-10-25T09:00:00+00:00", "attempts": 4, - "active_students": 1}], - "summary": {"attempts": 4, "active_students": 1, - "returning_students": 0, "active_days": 1}} - - auth = MonitoringAuth() - server, _ = connector(auth, actor=actor) - before = list(auth.events) - activity = call(server, "get_activity_timeseries", { - "from_date": "2026-10-25", "to_date": "2026-10-26", "interval": "day", "class_id": 1}) - health = call(server, "get_system_health") - assert activity["ok"] and auth.activity_ids == {"student-1"} - assert activity["series"][0]["attempts"] == 4 - assert not any(word in str(activity).lower() for word in ("student@example", "student-1", "score", "recording")) - assert health["ok"] and health["status"] == "healthy" and "build" not in health - assert auth.events == before - assert call(server, "get_activity_timeseries", { - "from_date": "2026-10-25", "to_date": "2026-10-26", "class_id": 2})["ok"] is False - - -def test_system_health_developer_metadata_is_whitelisted(): - actor = {"id": "dev-1", "email": "dev@example.com", "roles": ["developer"], - "subject": "", "permissions": ["management.access"]} - server, _ = connector(LibraryAuth(), actor=actor) - health = call(server, "get_system_health") - assert health["build"] == {"version": "test", "revision": "abc"} - assert not any(word in str(health).lower() for word in ("token", "secret", "trace", "http", "path")) - - -def test_current_activity_tool_is_scoped_private_read_only_and_audit_free(): - actor = {"id": "admin-1", "email": "admin@example.com", "roles": ["admin"], - "subject": "ערבית", "permissions": ["management.access"]} - - class CurrentAuth(LibraryAuth): - def __init__(self): - super().__init__() - self.class_rows = [{"id": 1, "name": "א", "subject": "ערבית", - "emails": ["one@example.com", "two@example.com", "three@example.com"]}, - {"id": 2, "name": "מתמטיקה", "subject": "מתמטיקה", - "emails": ["math@example.com"]}] - - def users(self): - return [{"id": f"u{i}", "email": f"{name}@example.com"} - for i, name in enumerate(("one", "two", "three"), 1)] - - def scoped_attempts(self, ids, since, until, *, include_diagnostics=False): - if include_diagnostics: - return [], False, True - latest = datetime.now(timezone.utc) - timedelta(seconds=10) - return ([{"id": i, "user_id": f"u{(i % 3) + 1}", "created_at": latest.isoformat()} - for i in range(5)], False) - - auth = CurrentAuth() - live = SimpleNamespace(presence_summary=lambda class_ids: { - "active_session": True, "active_users": 3, "heartbeat_ttl_seconds": 20, "ephemeral": True}) - server, _ = connector(auth, actor=actor, live=live) - before = list(auth.events) - current = call(server, "get_current_activity", {"window_minutes": 5, "class_id": 1}) - assert current["ok"] and current["recent_practice"]["status"] == "complete" - assert current["recent_practice"]["students_with_activity"] == 3 - assert current["live_presence"]["active_users"] == 3 - assert current["last_application_activity"]["status"] == "unavailable" - assert current["recent_practice"]["presence_claim"] is False - assert auth.events == before - assert not any(word in str(current).lower() for word in - ("one@example", "u1", "participant", "recording", "raw_attempt")) - with pytest.raises(ToolError, match="Input should be 5 or 15"): - call(server, "get_current_activity", {"window_minutes": 10}) - assert call(server, "get_current_activity", {"class_id": 2})["ok"] is False - - -def test_exercise_difficulty_tool_is_scoped_private_read_only_and_audit_free(): - actor = {"id": "admin-1", "email": "admin@example.com", "roles": ["admin"], - "subject": "ערבית", "permissions": ["management.access"]} - - class DifficultyAuth(LibraryAuth): - def __init__(self): - super().__init__() - self.class_rows = [{"id": 1, "name": "א", "subject": "ערבית", - "emails": ["one@example.com", "two@example.com", "three@example.com"]}] - self.seen = None - - def users(self): - return [{"id": "u1", "email": "one@example.com"}, {"id": "u2", "email": "two@example.com"}, - {"id": "u3", "email": "three@example.com"}, {"id": "outside", "email": "x@example.com"}] - - def scoped_attempts(self, ids, since, until, *, include_diagnostics=False): - self.seen = ids - rows = [ - {"id": 1, "user_id": "u1", "cat": "c", "phrase": "קשה", "score": 20, - "unrelated": False, "created_at": "2026-10-25T01:00:00+00:00"}, - {"id": 2, "user_id": "u1", "cat": "c", "phrase": "קשה", "score": 80, - "unrelated": False, "created_at": "2026-10-25T02:00:00+00:00"}, - {"id": 3, "user_id": "u2", "cat": "c", "phrase": "קשה", "score": 40, - "unrelated": False, "created_at": "2026-10-25T03:00:00+00:00"}, - {"id": 4, "user_id": "u2", "cat": "c", "phrase": "קשה", "score": 50, - "unrelated": False, "created_at": "2026-10-25T04:00:00+00:00"}, - {"id": 5, "user_id": "u3", "cat": "c", "phrase": "קשה", "score": 75, - "unrelated": False, "created_at": "2026-10-25T05:00:00+00:00"}, - ] - return (rows, False, True) if include_diagnostics else (rows, False) - - auth = DifficultyAuth() - server, _ = connector(auth, actor=actor) - before = list(auth.events) - result = call(server, "get_exercise_difficulty", { - "from_date": "2026-10-25", "to_date": "2026-10-26", "class_id": 1, - "limit": 10, "min_attempts": 5}) - assert result["ok"] and result["returned"] == 1 and auth.seen == {"u1", "u2", "u3"} - assert result["exercises"][0]["exercise"] == "קשה" - assert not any(word in str(result).lower() for word in - ("u1", "example.com", "created_at", "recording", "answer", "abandon")) - assert auth.events == before - - -def test_pronunciation_tool_is_scoped_aggregate_read_only_and_handles_missing_diagnostics(): - actor = {"id": "admin-1", "email": "admin@example.com", "roles": ["admin"], - "subject": "ערבית", "permissions": ["management.access"]} - - class PronunciationAuth(LibraryAuth): - def __init__(self): - super().__init__() - self.class_rows = [{"id": 1, "name": "א", "subject": "ערבית", - "emails": ["student@example.com"]}] - - def scoped_attempts(self, ids, since, until, *, include_diagnostics=False): - assert ids == {"student-1"} and include_diagnostics is True - return [], False, False - - auth = PronunciationAuth(); server, _ = connector(auth, actor=actor) - before = list(auth.events) - result = call(server, "get_pronunciation_difficulties", { - "from_date": "2026-10-25", "to_date": "2026-10-26", "class_id": 1}) - assert result["ok"] and result["status"] == "insufficient_data" - assert result["diagnostics_available"] is False and result["phonemes"] == [] - assert auth.events == before - - -def test_progress_comparison_and_bottleneck_tools_are_structured_scoped_and_read_only(): - actor = {"id": "admin-1", "email": "admin@example.com", "roles": ["admin"], - "subject": "ערבית", "permissions": ["management.access"]} - auth = LibraryAuth() - auth.class_rows = [{"id": 1, "name": "א", "subject": "ערבית", - "emails": ["student@example.com"]}] - server, _ = connector(auth, actor=actor) - before = list(auth.events) - common = {"from_date": "2026-10-25", "to_date": "2026-10-26"} - progress = call(server, "get_learning_progress", {**common, "class_id": 1, "interval": "month"}) - comparison = call(server, "compare_class_difficulties", {**common, "class_ids": [1]}) - bottlenecks = call(server, "get_learning_bottlenecks", {**common, "class_id": 1}) - assert progress["ok"] and progress["status"] == "insufficient_data" - assert comparison["ok"] and comparison["classes"][0]["status"] == "insufficient_data" - assert bottlenecks["ok"] and bottlenecks["bottlenecks"] == [] - combined = str((progress, comparison, bottlenecks)).lower() - assert not any(word in combined for word in ("student@example", "student-1", "recording", "raw_attempt")) - assert auth.events == before - assert call(server, "compare_class_difficulties", {**common, "class_ids": [999]})["ok"] is False - - -def test_streamable_http_initializes_and_discovers_tools(monkeypatch): - monkeypatch.setenv("MCP_PUBLIC_URL", "http://test/mcp/") - monkeypatch.setenv("SUPABASE_URL", "") - monkeypatch.setenv("SUPABASE_PUBLISHABLE_KEY", "") - monkeypatch.setenv("SUPABASE_SECRET_KEY", "") - monkeypatch.setenv("SAWTAK_API_KEY", "") - monkeypatch.setenv("ANTHROPIC_API_KEY", "") - app = create_app(scorer_for(emissions(["w"])), auth=LibraryAuth()) - initialize = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { - "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "test", "version": "1"}}} - - async def request(): - async with app.router.lifespan_context(app): - async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: - accept = {"Accept": "application/json, text/event-stream"} - assert (await client.post("/mcp/", json=initialize, headers=accept)).status_code == 401 - headers = {**accept, "Authorization": "Bearer teacher-session"} - initialized = await client.post("/mcp/", json=initialize, headers=headers) - discovered = await client.post("/mcp/", json={ - "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}, headers=headers) - return initialized, discovered - - initialized, discovered = asyncio.run(request()) - assert initialized.status_code == 200 - assert initialized.json()["result"]["serverInfo"]["name"] == "ihki-digital-library" - assert [tool["name"] for tool in discovered.json()["result"]["tools"]] == [ - "list_library_videos", "prepare_library_video", "publish_library_video", "rename_library_video", - "list_classes", "prepare_class_change", "publish_class_change", - "get_activity_timeseries", "get_current_activity", "get_system_health", "get_exercise_difficulty", - "get_pronunciation_difficulties", "get_learning_progress", "compare_class_difficulties", - "get_learning_bottlenecks"] - - -def test_existing_library_api_is_compatible_and_mcp_token_has_no_api_authority(monkeypatch): - monkeypatch.setenv("MCP_PUBLIC_URL", "http://test/mcp/") - monkeypatch.setenv("SAWTAK_API_KEY", "") - monkeypatch.setenv("ANTHROPIC_API_KEY", "") - auth = LibraryAuth() - app = create_app(scorer_for(emissions(["w"])), auth=auth) - - async def request(): - async with app.router.lifespan_context(app): - async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: - mcp = {"Authorization": "Bearer mcp-only-token"} - teacher = {"Authorization": "Bearer teacher-session"} - assert (await client.get("/api/library", headers=mcp)).status_code == 401 - first = await client.post("/api/library", headers=teacher, json={ - "title": " ראשון ", "url": "https://youtu.be/dQw4w9WgXcQ?t=3"}) - second = await client.post("/api/library", headers=teacher, json={ - "title": "שני", "url": "https://youtu.be/dQw4w9WgXcQ"}) - renamed = await client.put("/api/library/1", headers=teacher, json={"title": " חדש "}) - listed = await client.get("/api/library", headers=teacher) - return first, second, renamed, listed - - first, second, renamed, listed = asyncio.run(request()) - assert first.status_code == second.status_code == renamed.status_code == 200 - assert [(row["id"], row["title"], row["url"]) for row in listed.json()["library"]] == [ - (1, "חדש", "https://www.youtube.com/watch?v=dQw4w9WgXcQ"), - (2, "שני", "https://www.youtube.com/watch?v=dQw4w9WgXcQ")] - - -def test_streamable_http_manages_library_end_to_end_and_browser_still_opens(monkeypatch): - monkeypatch.setenv("MCP_PUBLIC_URL", "http://test/mcp/") - monkeypatch.setenv("SAWTAK_API_KEY", "") - monkeypatch.setenv("ANTHROPIC_API_KEY", "") - auth = LibraryAuth() - app = create_app(scorer_for(emissions(["w"])), auth=auth) - - async def request(): - async with app.router.lifespan_context(app): - async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: - headers = {"Authorization": "Bearer teacher-session", - "Accept": "application/json, text/event-stream"} - next_id = 1 - - async def tool(name, arguments=None): - nonlocal next_id - response = await client.post("/mcp/", headers=headers, json={ - "jsonrpc": "2.0", "id": next_id, "method": "tools/call", - "params": {"name": name, "arguments": arguments or {}}}) - next_id += 1 - assert response.status_code == 200, response.text - return response.json()["result"]["structuredContent"] - - prepared = await tool("prepare_library_video", { - "title": " דרך גרוק ", "url": "https://youtu.be/dQw4w9WgXcQ?t=4"}) - published = await tool("publish_library_video", {"approval_id": prepared["approval_id"]}) - listed = await tool("list_library_videos") - renamed = await tool("rename_library_video", {"video_id": published["video_id"], "title": "שם חדש"}) - page = await client.get("/", headers={"Authorization": "Bearer teacher-session"}) - return prepared, published, listed, renamed, page - - prepared, published, listed, renamed, page = asyncio.run(request()) - assert prepared["ok"] and not prepared["published"] - assert published["ok"] and published["published"] - assert listed["videos"] == [{"id": 1, "title": "דרך גרוק", - "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "position": 1}] - assert renamed == {"ok": True, "video_id": 1, "title": "שם חדש"} - assert page.status_code == 200 and "ניהול" in page.text - - -def test_oauth_discovery_challenge_and_consent_routes(monkeypatch): - monkeypatch.setenv("MCP_PUBLIC_URL", "https://ihki.example/mcp/") - monkeypatch.setenv("SAWTAK_API_KEY", "") - monkeypatch.setenv("ANTHROPIC_API_KEY", "") - auth = LibraryAuth() - app = create_app(scorer_for(emissions(["w"])), auth=auth) - initialize = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { - "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "test", "version": "1"}}} - - async def request(): - async with app.router.lifespan_context(app): - async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: - denied = await client.post("/mcp/", json=initialize, headers={"Accept": "application/json, text/event-stream"}) - metadata = await client.get("/.well-known/oauth-protected-resource/mcp/") - consent = await client.get("/oauth/consent?authorization_id=request-1") - unsigned = await client.get("/api/oauth/eligibility", headers={"Authorization": "Bearer invalid"}) - student = await client.get("/api/oauth/eligibility", headers={"Authorization": "Bearer student-session"}) - teacher = await client.get("/api/oauth/eligibility", headers={"Authorization": "Bearer teacher-session"}) - return denied, metadata, consent, unsigned, student, teacher - - denied, metadata, consent, unsigned, student, teacher = asyncio.run(request()) - assert denied.status_code == 401 - assert 'resource_metadata="https://ihki.example/.well-known/oauth-protected-resource/mcp/"' in denied.headers["www-authenticate"] - assert metadata.json() == {"resource": "https://ihki.example/mcp/", - "authorization_servers": ["https://example.supabase.co/auth/v1"], - "scopes_supported": ["openid", "email"], "bearer_methods_supported": ["header"], - "resource_name": "ihki digital library"} - assert consent.status_code == 200 and 'dir="rtl"' in consent.text - assert "getAuthorizationDetails" in consent.text and "approveAuthorization" in consent.text and "denyAuthorization" in consent.text - assert "authorization_id" in consent.text and "redirectTo:location.href" in consent.text - assert unsigned.status_code == 401 - assert student.json() == {"eligible": False} - assert teacher.json() == {"eligible": True} - - -def test_mutations_are_bounded_but_listing_remains_available(): - auth, clock = LibraryAuth(), Clock() - server, _ = connector(auth, clock) - for number in range(20): - result = call(server, "prepare_library_video", { - "title": str(number), "url": f"https://youtu.be/{number:011d}"}) - assert result["ok"] - refused = call(server, "prepare_library_video", {"title": "later", "url": "https://youtu.be/abcdefghijk"}) - assert refused["ok"] is False and "דקה" in refused["error"] - assert call(server, "list_library_videos")["ok"] is True diff --git a/tests/test_keystore.py b/tests/test_keystore.py deleted file mode 100644 index 038643399900071c596a4b4b68ec8610e19fefdf..0000000000000000000000000000000000000000 --- a/tests/test_keystore.py +++ /dev/null @@ -1,185 +0,0 @@ -"""The API keys a developer replaces from the panel (app/keystore.py): kept encrypted, never shown back, put to work at once.""" -import pytest -from cryptography.fernet import Fernet -from fastapi.testclient import TestClient -from test_lab import FakeAuth -from test_scoring import emissions, scorer_for - -import app.main -from app.keystore import KeyStore -from app.main import create_app -from app.tts import SawtakTTS -from app.tutor import Tutor - -MASTER = Fernet.generate_key().decode() -NEW = "sk-ant-new-key-0123456789wxyz" - - -class KeyAuth(FakeAuth): - secret = "sb_secret_x" - - def __init__(self, table=True): - super().__init__() - self.rows, self.table, self.events = {}, table, [] - - def secret_rows(self): - return list(self.rows.values()) if self.table else None - - def save_secret(self, row): - self.rows[row["name"]] = {**row, "updated_at": "2026-10-18T10:00:00+00:00"} - - def delete_secret(self, name): - self.rows.pop(name, None) - - def log_event(self, kind, user, label="", details=None): - self.events.append((kind, label, details)) - - -class AdminAuth(KeyAuth): - developers = frozenset() # t@b.c is a teacher (the panel), not a developer - - -def bearer(token): - return {"Authorization": f"Bearer {token}"} - - -@pytest.fixture -def keys_env(monkeypatch): - monkeypatch.setenv("SECRETS_MASTER_KEY", MASTER) - monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-from-the-environment-1111") - monkeypatch.setattr(app.main, "check_key", lambda name, value, anthropic_key="": None) - - -def client(auth, tmp_path, tutor=None): - return TestClient(create_app(scorer_for(emissions(["ħ", "aː", "l"])), SawtakTTS("old-sawtak-key", cache_dir=tmp_path), - auth=auth, tutor=tutor or Tutor(object()))) - - -def test_a_key_is_kept_encrypted_and_never_shown_back(keys_env, tmp_path): - auth = KeyAuth() - with client(auth, tmp_path) as c: - before = c.get("/api/admin/keys", headers=bearer("teacher")).json() - assert {k["name"]: k["source"] for k in before["keys"]}["ANTHROPIC_API_KEY"] == "env" - assert "1111" in repr(before) and "from-the-environment" not in repr(before) # its last four only - res = c.put("/api/admin/keys/ANTHROPIC_API_KEY", json={"value": f" {NEW} "}, headers=bearer("teacher")) - assert res.status_code == 200, res.text - assert NEW not in res.text and res.headers["cache-control"] == "no-store" - row = next(k for k in res.json()["keys"] if k["name"] == "ANTHROPIC_API_KEY") - assert (row["source"], row["last4"], row["updated_by"]) == ("db", "wxyz", "T@b.c") - stored = auth.rows["ANTHROPIC_API_KEY"] - assert NEW not in stored["ciphertext"] - assert c.app.state.tutor.client.api_key == NEW # in use without a restart - assert auth.events and NEW not in repr(auth.events) - # a restart reads it back from the table - assert KeyStore(auth, MASTER).get("ANTHROPIC_API_KEY") == "sk-ant-from-the-environment-1111" # not loaded yet - keys = KeyStore(auth, MASTER) - keys.load() - assert keys.get("ANTHROPIC_API_KEY") == NEW - - -def test_the_sawtak_key_is_swapped_in_place(keys_env, tmp_path): - with client(KeyAuth(), tmp_path) as c: - tts = c.app.state.tts - assert c.put("/api/admin/keys/SAWTAK_API_KEY", json={"value": "sawtak-new-9876"}, - headers=bearer("teacher")).status_code == 200 - assert c.app.state.tts is tts and tts.api_key == "sawtak-new-9876" - - -def test_clearing_goes_back_to_the_environment_key(keys_env, tmp_path): - auth = KeyAuth() - with client(auth, tmp_path) as c: - c.put("/api/admin/keys/ANTHROPIC_API_KEY", json={"value": NEW}, headers=bearer("teacher")) - res = c.delete("/api/admin/keys/ANTHROPIC_API_KEY", headers=bearer("teacher")) - assert res.status_code == 200 and "ANTHROPIC_API_KEY" not in auth.rows - assert c.app.state.tutor.client.api_key == "sk-ant-from-the-environment-1111" - - -def test_only_developers_reach_the_keys(keys_env, tmp_path, monkeypatch): - with client(KeyAuth(), tmp_path) as c: # a student - assert c.get("/api/admin/keys", headers=bearer("student")).status_code == 403 - assert c.put("/api/admin/keys/ANTHROPIC_API_KEY", json={"value": NEW}, headers=bearer("student")).status_code == 403 - with client(AdminAuth(), tmp_path) as c: # the management panel is not enough - assert c.get("/api/admin/keys", headers=bearer("teacher")).status_code == 403 - monkeypatch.setattr(app.main, "load_auth", lambda: None) # an open deployment, without sign-in - with TestClient(create_app(scorer_for(emissions(["ħ"])), SawtakTTS("k", cache_dir=tmp_path), - tutor=Tutor(object()))) as c: - assert c.get("/api/admin/keys").status_code == 403 - - -def test_only_the_two_keys_are_settable(keys_env, tmp_path): - with client(KeyAuth(), tmp_path) as c: - for name in ("SUPABASE_SECRET_KEY", "SECRETS_MASTER_KEY"): - assert c.put(f"/api/admin/keys/{name}", json={"value": NEW}, headers=bearer("teacher")).status_code == 404 - assert c.put("/api/admin/keys/ANTHROPIC_API_KEY", json={"value": "two words"}, headers=bearer("teacher")).status_code == 400 - - -def test_a_refused_key_is_not_kept(keys_env, tmp_path, monkeypatch): - from app.keystore import KeyRefused - - def refuse(name, value, anthropic_key=""): - raise KeyRefused("Anthropic דחה את המפתח.") - monkeypatch.setattr(app.main, "check_key", refuse) - auth = KeyAuth() - with client(auth, tmp_path) as c: - res = c.put("/api/admin/keys/ANTHROPIC_API_KEY", json={"value": NEW}, headers=bearer("teacher")) - assert res.status_code == 400 and NEW not in res.text and not auth.rows - - -def test_without_the_master_key_or_the_table_nothing_is_saved(keys_env, tmp_path, monkeypatch): - with client(KeyAuth(table=False), tmp_path) as c: - data = c.get("/api/admin/keys", headers=bearer("teacher")).json() - assert not data["writable"] and "app_secrets" in data["setup"] - assert c.put("/api/admin/keys/ANTHROPIC_API_KEY", json={"value": NEW}, headers=bearer("teacher")).status_code == 503 - monkeypatch.delenv("SECRETS_MASTER_KEY") - with client(KeyAuth(), tmp_path) as c: - data = c.get("/api/admin/keys", headers=bearer("teacher")).json() - assert not data["writable"] and "SECRETS_MASTER_KEY" in data["why"] - - -def test_a_row_does_not_decrypt_under_another_name_or_master(tmp_path): - auth = KeyAuth() - keys = KeyStore(auth, MASTER) - keys.set("ANTHROPIC_API_KEY", NEW, "t@b.c") - auth.rows["SAWTAK_API_KEY"] = {**auth.rows["ANTHROPIC_API_KEY"], "name": "SAWTAK_API_KEY"} # copied across - again = KeyStore(auth, MASTER) - again.load() - assert again.get("ANTHROPIC_API_KEY") == NEW and again.get("SAWTAK_API_KEY") != NEW - other = KeyStore(auth, Fernet.generate_key().decode()) - other.load() - assert other.get("ANTHROPIC_API_KEY") != NEW - - -def test_the_tutor_model_is_shown_and_switched_at_once(keys_env, tmp_path, monkeypatch): - seen = [] - monkeypatch.setattr(app.main, "check_key", lambda name, value, anthropic_key="": seen.append((name, anthropic_key))) - monkeypatch.delenv("TUTOR_MODEL", raising=False) - auth = KeyAuth() - with client(auth, tmp_path, tutor=Tutor(object(), "claude-haiku-5-5")) as c: - row = next(k for k in c.get("/api/admin/keys", headers=bearer("teacher")).json()["keys"] if k["name"] == "TUTOR_MODEL") - assert row["source"] == "none" and row["value"] == "" and row["choices"][0]["id"] == "claude-haiku-5-5" - res = c.put("/api/admin/keys/TUTOR_MODEL", json={"value": "claude-sonnet-5-5"}, headers=bearer("teacher")) - assert res.status_code == 200, res.text - row = next(k for k in res.json()["keys"] if k["name"] == "TUTOR_MODEL") - assert (row["source"], row["value"], row["last4"]) == ("db", "claude-sonnet-5-5", "") # no secret: shown in full - assert c.app.state.tutor.model == "claude-sonnet-5-5" - assert seen == [("TUTOR_MODEL", "sk-ant-from-the-environment-1111")] # checked with the key in use - c.delete("/api/admin/keys/TUTOR_MODEL", headers=bearer("teacher")) - assert c.app.state.tutor.model == "claude-haiku-5-5" # back to the code's default - - -def test_a_model_id_must_look_like_one(): - from app.keystore import KeyRefused, check - for bad in ("gpt-5", "claude haiku", "claude-"): - with pytest.raises(KeyRefused): - check("TUTOR_MODEL", bad, "sk-ant-x") - - -def test_an_old_table_asks_for_its_migration(keys_env, tmp_path): - from app.auth import AuthUnavailable - - class OldTable(KeyAuth): - def save_secret(self, row): - raise AuthUnavailable('supabase answered 400: {"code":"23514","message":"violates check constraint"}') - with client(OldTable(), tmp_path) as c: - res = c.put("/api/admin/keys/TUTOR_MODEL", json={"value": "claude-sonnet-5-5"}, headers=bearer("teacher")) - assert res.status_code == 409 and "app_secrets_name_check" in res.json()["detail"] diff --git a/tests/test_lab.py b/tests/test_lab.py deleted file mode 100644 index eadcbb6d28ffcb45458029d25803f3c097e90aa1..0000000000000000000000000000000000000000 --- a/tests/test_lab.py +++ /dev/null @@ -1,470 +0,0 @@ -import io -import wave - -import numpy as np -from test_scoring import emissions, scorer_for - -from app.auth import AuthUnavailable -from app.lab import ITEMS, QUICK_QUIZ, benchmark, public_items, public_quick_quiz, still_flagged -from app.translit import to_hebrew - - -def tone_wav(): - t = np.arange(16000) / 16000 - pcm = (0.3 * np.sin(2 * np.pi * 220 * t) * 32767).astype("= 2 for words in pairs.values()) # a minimal pair is two words (or three) - assert all(it["sounds"] for it in public_items()) - - -def test_lab_scores_and_keeps_labelled_recordings(): - from fastapi.testclient import TestClient - - from app.main import create_app - - auth = FakeAuth() - teacher, student = {"Authorization": "Bearer teacher"}, {"Authorization": "Bearer student"} - audio = {"audio": ("a.wav", tone_wav(), "audio/wav")} - # حال said with a Hebrew ח: the model hears χ for ħ - with TestClient(create_app(scorer_for(emissions(["χ", "aː", "l"])), NoTTS(), auth=auth)) as c: - assert c.get("/api/lab", headers=student).status_code == 403 # teachers only - assert c.post("/api/lab/score", data={"item": "pair-ħaal"}, files=audio, headers=student).status_code == 403 - d = c.get("/api/lab", headers=teacher).json() - assert d["setup"] == "" and d["counts"] == {} and d["items"][0]["id"] == ITEMS[0]["id"] - - r = c.post("/api/lab/score", data={"item": "pair-ħaal", "mistake": 0, "speaker": "מרב"}, files=audio, - headers=teacher) # a speaker sent by the page is ignored: it is the signed-in teacher - assert r.status_code == 200, r.text - d = r.json() - assert d["saved"] and "trace" in d["result"] # the teacher's stage-by-stage view - row = auth.samples[-1] - assert row["label"] == "mistake" and row["mistake"] == {"index": 0, "expected": "ħ"} and row["speaker"] == "t@b.c" - assert row["flagged"] == ["ħ>χ"] and row["email"] == "t@b.c" - - # said right (the default), and not kept when saving is off - c.post("/api/lab/score", data={"item": "pair-ħaal"}, files=audio, headers=teacher) - assert auth.samples[-1]["label"] == "correct" and auth.samples[-1]["mistake"] is None - d = c.post("/api/lab/score", data={"item": "pair-ħaal", "save": "false"}, files=audio, headers=teacher).json() - assert not d["saved"] and len(auth.samples) == 2 - assert c.get("/api/lab", headers=teacher).json()["counts"] == {"pair-ħaal": {"correct": 1, "mistake": 1}} - assert c.get("/api/lab", headers=teacher).json()["mine"] == {"pair-ħaal": {"correct": 1, "mistake": 1}} - assert c.get("/api/lab", headers={"Authorization": "Bearer teacher2"}).json()["mine"] == {} - assert c.get("/api/lab?speaker=t@b.c", headers={"Authorization": "Bearer teacher2"}).json()["mine"] \ - == {"pair-ħaal": {"correct": 1, "mistake": 1}} - assert c.get("/api/lab?speaker=אחר", headers=teacher).json()["mine"] == {} - - assert c.post("/api/lab/score", data={"item": "nope"}, files=audio, headers=teacher).status_code == 404 - assert c.post("/api/lab/score", data={"item": "pair-ħaal", "mistake": 3}, files=audio, - headers=teacher).status_code == 400 # حال has three sounds - - # before the table is created: the list still shows and scores, with the SQL to run - auth.missing = True - d = c.get("/api/lab", headers=teacher).json() - assert "create table public.lab_samples" in d["setup"] - d = c.post("/api/lab/score", data={"item": "pair-ħaal"}, files=audio, headers=teacher).json() - assert not d["saved"] and "lab_samples" in d["error"] and d["result"]["score"] is not None - - -def phones(*ps): - return {"score": 90, "words": [{"phonemes": [{"ipa": i, "score": sc, "flagged": f, "heard": h} for i, sc, f, h in ps]}]} - - -def test_benchmark_counts_false_rejections_and_caught_mistakes(): - samples = [ - {"id": 1, "label": "correct", "result": phones(("ħ", 20, True, "χ"), ("aː", 100, False, None), ("l", 95, False, None))}, - {"id": 2, "label": "correct", "result": phones(("ħ", 90, False, None), ("aː", 100, False, None), ("l", 95, False, None))}, - # ħ said wrong on purpose and caught; and one not caught - {"id": 3, "label": "mistake", "mistake": {"index": 0, "expected": "ħ"}, - "result": phones(("ħ", 5, True, "h"), ("aː", 100, False, None), ("l", 95, False, None))}, - {"id": 4, "label": "mistake", "mistake": {"index": 0, "expected": "ħ"}, - "result": phones(("ħ", 70, False, None), ("aː", 100, False, None), ("l", 30, True, "r"))}, - {"id": 5, "label": "correct", "result": {**phones(("ħ", 0, True, "_")), "unrelated": True}}, # tells nothing - ] - b = benchmark(samples, reports=[{"false_issues": [{"word": 0, "expected": "ħ", "heard": "χ"}]}]) - by = {r["sound"]: r for r in b["sounds"]} - assert by["ħ"]["right"] == 2 and by["ħ"]["false_flags"] == 1 and by["ħ"]["frr"] == 0.5 - assert by["ħ"]["planted"] == 2 and by["ħ"]["caught"] == 1 and by["ħ"]["detection"] == 0.5 - assert by["ħ"]["confusions"] == {"χ": 1} and by["ħ"]["reports"] == 1 - assert by["l"]["right"] == 4 and by["l"]["false_flags"] == 1 # said right in a mistake recording too - assert b["sounds"][0]["sound"] == "ħ" # the worst first - t = b["totals"] - assert t["recordings"] == 5 and t["unrelated"] == 1 and t["planted"] == 2 and t["good_share"] == 1.0 - # a rescore replaces the stored result - fixed = benchmark(samples, {1: phones(("ħ", 95, False, None), ("aː", 100, False, None), ("l", 95, False, None))}) - assert {r["sound"]: r for r in fixed["sounds"]}["ħ"]["frr"] == 0.0 - - -def test_still_flagged(): - r = phones(("ħ", 1, True, "χ"), ("aː", 100, False, None)) - assert still_flagged(r, {"word": 0, "expected": "ħ"}) and not still_flagged(r, {"word": 0, "expected": "aː"}) - assert not still_flagged(r, {"word": 3, "expected": "ħ"}) - - -def test_lab_results_and_flag_rescore(): - from fastapi.testclient import TestClient - - from app.main import create_app - - auth, teacher = FakeAuth(), {"Authorization": "Bearer teacher"} - audio = {"audio": ("a.wav", tone_wav(), "audio/wav")} - with TestClient(create_app(scorer_for(emissions(["χ", "aː", "l"])), NoTTS(), auth=auth)) as c: - for token in ("teacher", "teacher2"): - c.post("/api/lab/score", data={"item": "pair-ħaal"}, files=audio, headers={"Authorization": f"Bearer {token}"}) - d = c.get("/api/lab/results", headers=teacher).json() - assert d["speakers"] == ["t@b.c", "u@b.c"] and d["totals"]["recordings"] == 2 - assert {r["sound"]: r for r in d["sounds"]}["ħ"]["false_flags"] == 2 and d["sounds"][0]["reports"] == 1 - assert c.get("/api/lab/results?speaker=t@b.c", headers=teacher).json()["totals"]["recordings"] == 1 - assert c.get("/api/lab/results", headers={"Authorization": "Bearer student"}).status_code == 403 - - d = c.post("/api/lab/flags/7/rescore", headers=teacher).json() - assert d["old"] == {"score": 40, "flagged": ["ħ>χ"]} and d["issues"][0]["still"] and "trace" in d["result"] - assert c.post("/api/lab/flags/8/rescore", headers=teacher).status_code == 404 - - -def test_minerva_lab_groups_imported_segments_by_video(): - from fastapi.testclient import TestClient - - from app.main import create_app - - auth, teacher = FakeAuth(), {"Authorization": "Bearer teacher"} - auth.samples = [ - {"id": 2, "item": "minerva:U8aBRd3-wlM:02", "speaker": "מינרווה", "device": "minerva:U8aBRd3-wlM:4.2-7.8", - "ar": "مرحبا", "ipa": "m a r ħ a b a", "score": 81, "heard_ipa": "m a r ħ a b a", "flagged": [], - "result": {}, "audio": "two.webm"}, - {"id": 1, "item": "minerva:U8aBRd3-wlM:01", "speaker": "מינרווה", "device": "minerva:U8aBRd3-wlM:1.0-4.0", - "ar": "أهلا", "ipa": "ʔ a h l a", "score": 70, "heard_ipa": "ʔ a h l a", "flagged": ["h>x"], - "result": {"unrelated": False, "words": [{"ar": "أهلا", "score": 70, "phonemes": []}]}, "audio": "one.webm"}, - {"id": 3, "item": "pair-ħaal", "speaker": "אחר", "device": "", "audio": "other.wav"}, - ] - with TestClient(create_app(scorer_for(emissions(["ħ", "aː", "l"])), NoTTS(), auth=auth)) as c: - d = c.get("/api/lab/minerva", headers=teacher).json() - assert len(d["videos"]) == 1 and d["videos"][0]["id"] == "U8aBRd3-wlM" - assert [s["line"] for s in d["videos"][0]["samples"]] == [1, 2] - # Sentence data comes from data/minerva_metadata.json, not the deliberately different stored row above. - first = d["videos"][0]["samples"][0] - assert first["start"] == 9.0 and first["end"] == 10.8 - assert first["ar"] == "فيه عندك حليب طارة؟" - assert first["ipa"] == "f iː h | ʕ i n d a k | ħ a l iː b | tˤ aː r e" - assert first["tr"] == "פִיה עִנְדַכּ חַלִיב טַארֶה?" and first["he"] == "יש לך חלב טרי?" - audio = c.get("/api/lab/samples/1/audio", headers=teacher) - assert audio.status_code == 200 and audio.headers["content-type"] == "audio/wav" - assert c.get("/api/lab/samples/99/audio", headers=teacher).status_code == 404 - excluded = c.post("/api/lab/samples/1/exclude-word", - json={"word": 0, "excluded": True, "reason": "חיתוך פגום"}, headers=teacher).json() - assert excluded["excluded_words"] == {"0": "חיתוך פגום"} - assert c.get("/api/lab/minerva", headers=teacher).json()["videos"][0]["samples"][0]["excluded_words"] == excluded["excluded_words"] - assert c.post("/api/lab/samples/1/exclude-word", json={"word": 2, "excluded": True}, headers=teacher).status_code == 400 - assert c.get("/api/lab/minerva", headers={"Authorization": "Bearer student"}).status_code == 403 - - -def test_lab_rescores_its_recordings_and_compares_with_the_stored_scores(): - from fastapi.testclient import TestClient - - from app.main import create_app - - auth, teacher = FakeAuth(), {"Authorization": "Bearer teacher"} - audio = {"audio": ("a.wav", tone_wav(), "audio/wav")} - with TestClient(create_app(scorer_for(emissions(["χ", "aː", "l"])), NoTTS(), auth=auth)) as c: - c.post("/api/lab/score", data={"item": "pair-ħaal"}, files=audio, headers=teacher) # ħ said as χ: flagged - d = c.get("/api/lab/results", headers=teacher).json() - assert d["samples"] == [1] and d["version"] - # the scorer changed since: now it hears ħ - c.app.state.scorer = scorer_for(emissions(["ħ", "aː", "l"])) - r = c.post("/api/lab/samples/1/rescore", headers=teacher).json() - assert r["id"] == 1 and not any(p["flagged"] for w in r["result"]["words"] for p in w["phonemes"]) - assert "trace" not in r["result"] - d = c.post("/api/lab/results", json={"results": {"1": r["result"]}}, headers=teacher).json() - assert d["pool"] == c.app.state.scorer.cfg.gop.ratio_pool - before, after = ({x["sound"]: x for x in d[k]["sounds"]}["ħ"] for k in ("stored", "now")) - assert before["frr"] == 1.0 and after["frr"] == 0.0 - assert c.post("/api/lab/samples/9/rescore", headers=teacher).status_code == 404 - assert c.post("/api/lab/results", json={"results": []}, headers=teacher).status_code == 400 - assert c.post("/api/lab/samples/1/rescore", headers={"Authorization": "Bearer student"}).status_code == 403 - - -class FakeTTS: - """Says anything as the test tone; counts what it was asked.""" - - def __init__(self): - self.said = [] - - def wav(self, text, voice=""): - self.said.append((text, voice)) - return tone_wav(), voice - - def cached(self, text, voice): - return True - - -def test_lab_automatic_test_with_tts(): - from fastapi.testclient import TestClient - - from app.main import LAB_TTS_VOICES, create_app - - auth, tts, teacher = FakeAuth(), FakeTTS(), {"Authorization": "Bearer teacher"} - with TestClient(create_app(scorer_for(emissions(["ħ", "aː", "l"])), tts, auth=auth)) as c: - assert c.get("/api/lab/tts", headers=teacher).json()["voices"] == list(LAB_TTS_VOICES) - tts.said.clear() # (nothing else may ask the voice: the TTS run is scored without the native baseline) - d = c.post("/api/lab/tts", data={"item": "pair-ħaal", "voice": "ראאף"}, headers=teacher).json() - assert d["saved"] and d["speaker"] == "TTS ראאף" and d["result"]["references"] == 0 - assert len(tts.said) == 1 and "ح" in tts.said[0][0] - row = auth.samples[-1] - assert row["speaker"] == "TTS ראאף" and row["label"] == "correct" and row["device"].startswith("tts:") - assert row["audio"].endswith(".wav") - assert c.get("/api/lab?speaker=TTS ראאף", headers=teacher).json()["mine"] == {"pair-ħaal": {"correct": 1, "mistake": 0}} - r = c.get("/api/lab/tts/pair-ħaal?voice=ראאף", headers=teacher) - assert r.status_code == 200 and r.headers["content-type"] == "audio/wav" - assert c.post("/api/lab/tts", data={"item": "pair-ħaal", "voice": "x"}, headers=teacher).status_code == 400 - assert c.post("/api/lab/tts", data={"item": "nope", "voice": "ראאף"}, headers=teacher).status_code == 404 - assert c.post("/api/lab/tts", data={"item": "pair-ħaal", "voice": "ראאף"}, - headers={"Authorization": "Bearer student"}).status_code == 403 - with TestClient(create_app(scorer_for(emissions(["ħ", "aː", "l"])), NoTTS(), auth=FakeAuth())) as c: - c.app.state.tts = None - assert c.get("/api/lab/tts", headers=teacher).json()["voices"] == [] - assert c.post("/api/lab/tts", data={"item": "pair-ħaal", "voice": "ראאף"}, headers=teacher).status_code == 503 - - -def test_lab_sweeps_every_gop_setting(): - from fastapi.testclient import TestClient - - from app import lab - from app.main import create_app - - assert len(lab.SWEEP) == 24 and len(lab.SWEEP_KEYS) == 24 - assert lab.quality({"frr": .1, "detection": .5}) == 70.0 and lab.quality({"frr": .1, "detection": None}) == 90.0 - assert lab.quality({"frr": None}) is None and lab.quality_partial({"frr": .1, "detection": None}) - - auth, teacher = FakeAuth(), {"Authorization": "Bearer teacher"} - audio = {"audio": ("a.wav", tone_wav(), "audio/wav")} - with TestClient(create_app(scorer_for(emissions(["χ", "aː", "l"])), NoTTS(), auth=auth)) as c: - c.post("/api/lab/score", data={"item": "pair-ħaal"}, files=audio, headers=teacher) # ħ heard as χ - c.post("/api/lab/score", data={"item": "pair-ħaal", "mistake": "0"}, files=audio, - headers=teacher) - before = c.app.state.scorer.cfg.gop - r = c.post("/api/lab/samples/1/sweep", headers=teacher).json() - assert r["id"] == 1 and set(r["results"]) == set(lab.SWEEP_KEYS) - assert c.app.state.scorer.cfg.gop == before # the server's own setting is untouched - r2 = c.post("/api/lab/samples/2/sweep", headers=teacher).json() - d = c.post("/api/lab/sweep", json={"results": {"1": r["results"], "2": r2["results"]}}, headers=teacher).json() - assert len(d["settings"]) == 24 and d["recordings"] == 2 - assert d["current"] == lab.sweep_key(before.ratio_pool, before.vowel_window) - s = d["settings"][0] - assert {"key", "pool", "window", "quality", "partial", "totals"} <= set(s) - qs = [x["quality"] for x in d["settings"]] - assert qs == sorted(qs, reverse=True) - assert c.post("/api/lab/samples/9/sweep", headers=teacher).status_code == 404 - assert c.post("/api/lab/sweep", json={"results": []}, headers=teacher).status_code == 400 - assert c.post("/api/lab/samples/1/sweep", headers={"Authorization": "Bearer student"}).status_code == 403 - - -def test_vowel_stats_score_vowels_not_flags(): - from app.lab import vowel_stats - - def sample(i, phones, mistake=None): - return {"id": i, "label": "mistake" if mistake else "correct", - "mistake": {"index": 0, "expected": mistake} if mistake else None, - "result": {"words": [{"phonemes": [{"ipa": p, "score": sc} for p, sc in phones]}]}} - - rows = [sample(1, [("k", 40), ("a", 95), ("i", 70)]), sample(2, [("a", 30), ("l", 100)], mistake="a"), - {**sample(3, [("a", 10)]), "result": {"unrelated": True, "words": [{"phonemes": [{"ipa": "a", "score": 10}]}]}}] - v = vowel_stats(rows) - assert v == {"right": 2, "mean": 82.5, "weak": 0.5, "planted": 1, "auc": 1.0} # k is no vowel, #3 is unrelated - assert vowel_stats(rows[:1])["auc"] is None - assert vowel_stats(rows, {2: {"words": [{"phonemes": [{"ipa": "a", "score": 70}]}]}})["auc"] == 0.75 - - -def test_the_sweep_scores_every_setting_whatever_the_env(monkeypatch): - """The sweep passes each setting to the scorer itself: the env only picks the server's own one.""" - from fastapi.testclient import TestClient - - import app.scorer as scorer_module - from app import lab - from app.main import create_app - - monkeypatch.setenv("GOP_RATIO_POOL", "max") - monkeypatch.setenv("GOP_VOWEL_WINDOW", "wide") - seen, real = [], scorer_module.score_tokens - monkeypatch.setattr(scorer_module, "score_tokens", - lambda logp, al, ids, blank, cfg, *a, **k: seen.append((cfg.ratio_pool, cfg.vowel_window)) - or real(logp, al, ids, blank, cfg, *a, **k)) - auth, teacher = FakeAuth(), {"Authorization": "Bearer teacher"} - with TestClient(create_app(scorer_for(emissions(["ħ", "aː", "l"])), NoTTS(), auth=auth)) as c: - c.post("/api/lab/score", data={"item": "pair-ħaal"}, files={"audio": ("a.wav", tone_wav(), "audio/wav")}, - headers=teacher) - assert c.app.state.scorer.cfg.gop.ratio_pool == "max" - seen.clear() - r = c.post("/api/lab/samples/1/sweep", headers=teacher).json() - assert sorted(seen, key=str) == sorted(lab.SWEEP, key=str) - d = c.post("/api/lab/sweep", json={"results": {"1": r["results"]}}, headers=teacher).json() - assert all({"right", "mean", "weak", "planted", "auc"} <= set(s["vowels"]) for s in d["settings"]) - base = next(s for s in d["settings"] if s["key"] == "max|wide") - assert base["changed"] == 0 and base["sounds"] == 3 - assert d["current"] == "max|wide" - - -def test_the_sweep_shows_which_settings_changed_a_score(): - """A long vowel with an /i/ spike inside its window: max|wide marks it down, the pooled settings do not.""" - from fastapi.testclient import TestClient - - from app.main import create_app - from test_scoring import SYMS, VOCAB - - lp = emissions(["ħ", "aː", "l"], gap=5) - p = np.exp(lp) - vowel = [t for t in range(len(p)) if lp[t].argmax() == VOCAB["aː"]] - for t in vowel: - p[t] = (1 - .45) / (len(SYMS) - 1); p[t, VOCAB["aː"]] = .45 - p[vowel[-1] + 3] = .1 / (len(SYMS) - 1); p[vowel[-1] + 3, VOCAB["i"]] = .9 - auth, teacher = FakeAuth(), {"Authorization": "Bearer teacher"} - with TestClient(create_app(scorer_for(np.log(p / p.sum(1, keepdims=True))), NoTTS(), auth=auth)) as c: - c.post("/api/lab/score", data={"item": "pair-ħaal"}, files={"audio": ("a.wav", tone_wav(), "audio/wav")}, - headers=teacher) - r = c.post("/api/lab/samples/1/sweep", headers=teacher).json() - d = {s["key"]: s for s in c.post("/api/lab/sweep", json={"results": {"1": r["results"]}}, headers=teacher).json()["settings"]} - assert d["max|wide"]["changed"] == 0 and d["max|wide"]["vowels"]["mean"] < 80 - for key in ("mean|wide", "gated|wide", "topk|wide", "max|1"): - assert d[key]["changed"] == 1 and d[key]["vowels"]["mean"] > 95, key - - -def test_lab_opens_the_trace_of_any_saved_recording(): - from fastapi.testclient import TestClient - - from app.main import create_app - - auth, teacher = FakeAuth(), {"Authorization": "Bearer teacher"} - audio = {"audio": ("a.wav", tone_wav(), "audio/wav")} - with TestClient(create_app(scorer_for(emissions(["χ", "aː", "l"])), NoTTS(), auth=auth)) as c: - c.post("/api/lab/score", data={"item": "pair-ħaal"}, files=audio, headers=teacher) - recs = c.get("/api/lab/results", headers=teacher).json()["recordings"] - assert [(r["id"], r["item"], r["speaker"], r["flagged"]) for r in recs] == [(1, "pair-ħaal", "t@b.c", ["ħ>χ"])] - assert recs[0]["unrelated"] is False - d = c.post("/api/lab/samples/1/rescore?trace=true", headers=teacher).json() - assert "trace" in d["result"] and d["result"]["trace"]["tokens"] and d["sample"]["item"] == "pair-ħaal" - g = c.app.state.scorer.cfg.gop - assert d["setting"] == f"{g.ratio_pool}|{'wide' if g.vowel_window is None else g.vowel_window}" - d = c.post("/api/lab/samples/1/rescore?trace=true&setting=max|1", headers=teacher).json() - assert d["setting"] == "max|1" and "trace" in d["result"] - assert "trace" not in c.post("/api/lab/samples/1/rescore", headers=teacher).json()["result"] - assert c.post("/api/lab/samples/1/rescore?trace=true&setting=nope", headers=teacher).status_code == 400 - - -def test_imported_lab_line_reconstructs_its_phrase_outside_the_fixed_list(): - from app.lab import sample_phrase - - reviewed = sample_phrase({"id": 6, "item": "minerva:U8aBRd3-wlM:01", "ar": "wrong", "ipa": "x"}) - assert reviewed["variants"] == [("فيه عندك حليب طارة؟", - "f iː h | ʕ i n d a k | ħ a l iː b | tˤ aː r e")] - - phrase = sample_phrase({"id": 7, "item": "minerva:video:01", "ar": "حال", "ipa": "ħ aː l", - "speaker": "מינרווה"}) - assert phrase == {"cat": "lab", "key": "minerva:video:01", "he": "מינרווה", - "variants": [("حال", "ħ aː l")]} - assert sample_phrase({"id": 8, "item": "removed-item"}) is None diff --git a/tests/test_learner_home.py b/tests/test_learner_home.py deleted file mode 100644 index 87e4d1bd4f187b2c03ff5e571adc9d74a2db3e02..0000000000000000000000000000000000000000 --- a/tests/test_learner_home.py +++ /dev/null @@ -1,245 +0,0 @@ -"""The learner's current lesson in each class (openspec/changes/add-focused-learner-home): storage and API.""" -import urllib.parse - -import pytest - -from app.auth import CREATE_LEARNER_CLASS_STATE, AuthUnavailable, SupabaseAuth - - -class StateAuth(SupabaseAuth): - """SupabaseAuth with PostgREST faked in memory: rows of learner_class_state keyed by (email, class_id).""" - - def __init__(self, secret="sb_secret_x", fail=None): - super().__init__("https://proj.supabase.co", "sb_publishable_x", secret=secret) - self.rows, self.fail, self.calls = {}, fail, [] - - def _admin(self, path, method="GET", body=None, prefer="return=representation", timeout=10): - self.calls.append((method, path, body, prefer)) - if self.fail: - raise AuthUnavailable(self.fail) - if method == "GET": - email = urllib.parse.parse_qs(urllib.parse.urlsplit(path).query)["email"][0].removeprefix("eq.") - return [{"class_id": k[1], "current_lesson": v["current_lesson"]} for k, v in self.rows.items() if k[0] == email] - assert method == "POST" and "on_conflict=email,class_id" in path and "merge-duplicates" in prefer - self.rows[(body["email"], body["class_id"])] = body - return None - - -def test_saves_and_reads_only_the_accounts_own_lessons(): - a = StateAuth() - a.set_current_lesson(" Dana@X.com ", "7", " 5 ") - a.set_current_lesson("dana@x.com", 8, "2.1") - a.set_current_lesson("other@x.com", 7, "3") - a.set_current_lesson("dana@x.com", 7, "6") # a new choice replaces the old one in the same class - assert a.current_lessons("DANA@x.com") == {7: "6", 8: "2.1"} - assert a.current_lessons("other@x.com") == {7: "3"} - saved = a.rows[("dana@x.com", 7)] - assert saved["current_lesson"] == "6" and saved["updated_at"] - - -def test_without_the_secret_key_nothing_is_read_and_a_save_is_refused(): - a = StateAuth(secret="") - assert a.current_lessons("dana@x.com") == {} - with pytest.raises(AuthUnavailable, match="SUPABASE_SECRET_KEY"): - SupabaseAuth("https://proj.supabase.co", "k").set_current_lesson("dana@x.com", 7, "5") - - -def test_a_missing_table_reads_as_none_and_a_save_says_how_to_make_it(): - a = StateAuth(fail="supabase answered 404: relation does not exist") - assert a.current_lessons("dana@x.com") == {} - with pytest.raises(AuthUnavailable) as e: - a.set_current_lesson("dana@x.com", 7, "5") - assert CREATE_LEARNER_CLASS_STATE in str(e.value) - - -def test_supabase_down_is_raised(): - a = StateAuth(fail="timed out") - with pytest.raises(AuthUnavailable): - a.current_lessons("dana@x.com") - with pytest.raises(AuthUnavailable, match="timed out"): - a.set_current_lesson("dana@x.com", 7, "5") - - -def test_migration_keeps_rows_private_and_unique(): - sql = CREATE_LEARNER_CLASS_STATE - assert "primary key (email, class_id)" in sql - assert "enable row level security" in sql and "revoke all on public.learner_class_state from anon, authenticated" in sql - assert "references public.classes (id) on delete cascade" in sql - - -# through the app -CLASSES = [ - {"id": 7, "name": "א1", "emails": ["dana@x.com", "t@x.com"], "teachers": ["t@x.com"], "lessons": ["3", "4", "5"]}, - {"id": 8, "name": "ב2", "emails": ["dana@x.com"], "lessons": ["2.1", "2.2"]}, - {"id": 9, "name": "מעורבת", "emails": ["dana@x.com"], "lessons": ["5", "2.1"]}, - {"id": 10, "name": "אחרת", "emails": ["other@x.com"], "lessons": ["1"]}, -] - - -class AppAuth(StateAuth): - def _admin(self, path, method="GET", body=None, prefer="return=representation", timeout=10): - if "learner_class_state" in path: - return super()._admin(path, method, body, prefer, timeout) - return [] # every other table is empty - - def user(self, token): - return {"id": f"id-{token}", "email": f"{token}@x.com"} - - def classes(self, fresh=False): - return [dict(c, nicknames={}, homeworks=[], dailies=[], homework="", due=None) for c in CLASSES] - - -@pytest.fixture -def client(tmp_path): - from fastapi.testclient import TestClient - from test_scoring import emissions, scorer_for - from app.main import create_app - from app.tts import SawtakTTS - auth = AppAuth() - with TestClient(create_app(scorer_for(emissions(["ħ"])), SawtakTTS("k", cache_dir=tmp_path), auth=auth)) as c: - c.fake = auth - yield c - - -def as_(name): - return {"Authorization": f"Bearer {name}"} - - -def classes_of(c, name): - return {k["id"]: k for k in c.get("/api/me", headers=as_(name)).json()["classes"]} - - -def test_me_has_class_ids_levels_and_no_lesson_before_a_choice(client): - mine = classes_of(client, "dana") - assert set(mine) == {7, 8, 9} - assert [(mine[i]["level"], mine[i]["current_lesson"]) for i in (7, 8, 9)] == [("1", None), ("2", None), (None, None)] - assert "teacher" not in mine[7] - assert classes_of(client, "t")[7]["teacher"] is True - - -def test_choices_are_kept_apart_for_each_class(client): - assert client.put("/api/me/classes/7/current-lesson", headers=as_("dana"), json={"lesson": "5"}).json() == \ - {"class_id": 7, "current_lesson": "5"} - mine = classes_of(client, "dana") - assert (mine[7]["current_lesson"], mine[8]["current_lesson"]) == ("5", None) - client.put("/api/me/classes/8/current-lesson", headers=as_("dana"), json={"lesson": "2.2"}) - client.put("/api/me/classes/7/current-lesson", headers=as_("dana"), json={"lesson": "4"}) - mine = classes_of(client, "dana") - assert (mine[7]["current_lesson"], mine[8]["current_lesson"]) == ("4", "2.2") - assert set(client.fake.rows) == {("dana@x.com", 7), ("dana@x.com", 8)} - - -def test_a_saved_lesson_the_class_no_longer_has_is_not_offered(client): - client.fake.rows[("dana@x.com", 7)] = {"current_lesson": "9"} - assert classes_of(client, "dana")[7]["current_lesson"] is None - - -def test_update_is_refused_outside_the_students_own_valid_choice(client): - put = lambda who, cls, lesson, **k: client.put(f"/api/me/classes/{cls}/current-lesson", json={"lesson": lesson}, - headers=as_(who) if who else {}, **k).status_code - assert put(None, 7, "5") == 401 # not signed in - assert put("dana", 10, "1") == 403 # someone else's class - assert put("t", 7, "5") == 403 # the class's teacher keeps the path - assert put("dana", 9, "5") == 409 # lessons of two levels: the class must be fixed first - assert put("dana", 7, "2.1") == 400 # a lesson of another class - assert put("dana", 7, "") == 400 - assert client.fake.rows == {} - - -def test_unavailable_storage_keeps_me_working_and_refuses_the_save(client): - client.fake.fail = "timed out" - assert classes_of(client, "dana")[7]["current_lesson"] is None - assert client.put("/api/me/classes/7/current-lesson", headers=as_("dana"), json={"lesson": "5"}).status_code == 503 - - -# the page's logic (static/learner_home.js), run in Node -import json -import shutil -import subprocess -from pathlib import Path - -NODE = shutil.which("node") -SCRIPT = Path(__file__).parent.parent / "static" / "learner_home.js" - - -def js(expr): - """The JSON value of `expr`, evaluated with LearnerHome loaded.""" - if not NODE: - pytest.skip("node is not installed") - code = f"{SCRIPT.read_text(encoding='utf-8')}\nconst LH=globalThis.LearnerHome;\nprocess.stdout.write(JSON.stringify({expr}));" - out = subprocess.run([NODE, "-e", code], capture_output=True, text=True, check=True, timeout=30) - return json.loads(out.stdout) - - -def test_device_keys_are_per_subject_account_and_class(): - keys = js('[LH.storageKey("arabic"," Dana@X.com ",7),LH.storageKey("arabic","other@x.com",7),' - 'LH.storageKey("arabic","dana@x.com",8),LH.storageKey("math","dana@x.com",7),LH.storageKey("","dana@x.com",7)]') - assert keys[0] == "ihki.lesson.v1:arabic:dana@x.com:7" == keys[4] - assert len(set(keys[:4])) == 4 - - -def test_the_servers_lesson_wins_and_a_devices_one_is_sent_again(): - r = js('[LH.resolveLesson("5",{lesson:"4",synced:false},["4","5"]),' - ' LH.resolveLesson(null,{lesson:"4",synced:false},["4","5"]),' - ' LH.resolveLesson(null,{lesson:"9",synced:true},["4","5"]),' - ' LH.resolveLesson("9",null,["4","5"]),' - ' LH.resolveLesson(null,null,["4","5"])]') - assert r == [{"lesson": "5", "retry": False}, {"lesson": "4", "retry": True}, {"lesson": None, "retry": False}, - {"lesson": None, "retry": False}, {"lesson": None, "retry": False}] - - -def test_who_is_asked_for_a_lesson(): - assert js('[LH.classState({level:"1"}),LH.classState({level:null}),LH.classState({level:"1",teacher:true}),' - 'LH.classState({subject:"math",level:null}),LH.classState(null)]') == \ - ["ok", "ambiguous", "teacher", "math", "none"] - - -def test_actions_keep_priority_drop_duplicates_and_what_cannot_start(): - picked = js('LH.pickActions([{target:"topic:a",ready:true,kind:"homework"},null,' - '{target:"video:1",ready:true,kind:"video"},{target:"topic:a",ready:true,kind:"continue"},' - '{target:"x",ready:false,kind:"broken"},{target:"short",ready:true,kind:"short"},' - '{target:"change",ready:true,kind:"change"}]).map(a=>a.kind)') - assert picked == ["homework", "video", "short"] # never more than three; the same topic once - all_three = js('LH.pickActions([{target:"h",ready:true,kind:"homework"},{target:"v",ready:true,kind:"video"},' - '{target:"c",ready:true,kind:"continue"},{target:"s",ready:true,kind:"short"}]).map(a=>a.kind)') - assert all_three == ["homework", "video", "continue"] # the short activity gives way - assert js('LH.pickActions([{target:"c",ready:true,kind:"continue"},{target:"s",ready:true,kind:"short"}])' - '.map(a=>a.kind)') == ["continue", "short"] # no homework, no video today - - -def test_short_activity_is_at_most_ten_weak_or_else_new_sentences(): - assert js("LH.SHORT") == 10 - weak = js('LH.shortPlan([1,2,3,4,5,6,7,8,9,10,11,12].map(id=>({id})),[],{1:40,2:90,3:10,4:60,5:20,6:50,7:30,8:15,9:35,10:25,11:45,12:55},75)') - assert weak == {"kind": "review", "ids": [3, 8, 5, 10, 7, 9, 1, 11, 6, 12]} - fresh = js('LH.shortPlan([{id:1}],[1,8,9,10,11,12,13,14,15,16,17,18].map(id=>({id})),{1:95},75)') - assert fresh == {"kind": "new", "ids": [8, 9, 10, 11, 12, 13, 14, 15, 16, 17]} - assert js('LH.shortPlan([{id:1}],[{id:1}],{1:95},75)') is None # nothing to offer: the action is left out - - -def test_daily_practice_takes_homework_then_current_lesson_then_first_unfinished(): - assert js('LH.lessonOrder("hw","5",null,["1","2","5","hw","6"])') == ["hw", "5", "1", "2", "6"] - assert js('LH.lessonOrder(null,"5","2",["1","2","5","6"])') == ["5", "2", "1", "6"] - assert js('LH.lessonOrder(null,null,"2",["1","2","5"])') == ["2", "1", "5"] # no current lesson: as before - - -def test_the_class_week_is_totals_only_without_teachers(client): - """GET /api/class/week: the class's sentences this week against its goal, and the student's own count; no names.""" - asked = [] - - def users(): - return [{"id": f"id-{n}", "email": f"{n}@x.com", "name": n} for n in ("dana", "rina", "t", "other")] - - def scoped_activity(ids, since, bucket_minutes=15): - asked.append(set(ids)) - said = {"id-dana": 4, "id-rina": 6, "id-t": 100} - return {"buckets": [], "summary": {"attempts": sum(said.get(i, 0) for i in ids), - "active_students": sum(1 for i in ids if said.get(i))}} - - client.fake.users, client.fake.scoped_activity = users, scoped_activity - client.fake.classes = lambda fresh=False: [dict(CLASSES[0], emails=["dana@x.com", "rina@x.com", "t@x.com"], - nicknames={}, homeworks=[], dailies=[], homework="", due=None)] - week = client.get("/api/class/week", headers=as_("dana")).json()["classes"] - from app.main import CLASS_WEEK_GOAL - assert week == [{"name": "א1", "students": 2, "said": 10, "active": 2, "mine": 4, "goal": 2 * CLASS_WEEK_GOAL}] - assert {"id-dana", "id-rina"} in asked and not any("id-t" in ids for ids in asked) # the teacher is not counted - assert client.get("/api/class/week").status_code == 401 diff --git a/tests/test_learner_home_browser.py b/tests/test_learner_home_browser.py deleted file mode 100644 index f324c53332ed2b5f17367ec22d72c738024dafee..0000000000000000000000000000000000000000 --- a/tests/test_learner_home_browser.py +++ /dev/null @@ -1,588 +0,0 @@ -"""The focused learner home in the real page (static/index.html), driven in headless Chrome against the real app. - -Only Supabase is faked: the server's tables live in memory (AppAuth), and the page's Supabase client is a stub that is -signed in as the account the test names. Each case runs a short script in the page and reads its results from the DOM.""" -import json -import re -import shutil -import socket -import subprocess -import threading -import time - -import pytest - -from test_learner_home import CLASSES, AppAuth - -CHROME = shutil.which("google-chrome") or shutil.which("chromium") or shutil.which("chromium-browser") -pytestmark = pytest.mark.skipif(not CHROME, reason="Chrome is not installed") - - -class PageAuth(AppAuth): - def __init__(self): - super().__init__() - self.teachers = frozenset({"boss@x.com"}) # a teacher of the whole service - # by default one class for the student (lessons 3-5 of level 1), so no class has to be chosen first - self.layout = [dict(CLASSES[0], emails=["dana@x.com", "rina@x.com", "t@x.com"]), CLASSES[3]] - self.daily_rows = [] # the table `dailies`: the daily videos' library - - def classes(self, fresh=False): - return [dict({"nicknames": {}, "homeworks": [], "dailies": [], "homework": "", "due": None}, **c) - for c in self.layout] - - def _admin(self, path, method="GET", body=None, prefer="return=representation", timeout=10): - if path.startswith("/rest/v1/dailies"): - return self.daily_rows - return super()._admin(path, method, body, prefer, timeout) - - def attempts(self, token): - return [] - - def writings(self, token): - return {} - - -def fake_supabase(email): - session = {"access_token": email.split("@")[0], "user": {"id": f"id-{email}", "email": email}} - return f"""""" - - -DRIVER = """""" - - -@pytest.fixture -def site(monkeypatch, tmp_path): - """The real app on a local port, with the test's page at /t// (and ?view=daily passed through).""" - yield from serve(PageAuth(), monkeypatch, tmp_path) - - -@pytest.fixture -def open_site(monkeypatch, tmp_path): - """The same, as the open service: no sign-in at all.""" - yield from serve(None, monkeypatch, tmp_path) - - -def serve(auth, monkeypatch, tmp_path): - import uvicorn - from fastapi import Request - from fastapi.responses import HTMLResponse - from test_scoring import emissions, scorer_for - from app.main import create_app - from app.tts import SawtakTTS - from app import push as pushes - - async def no_loop(*a, **k): - return None - monkeypatch.setattr(pushes, "loop", no_loop) # no reminders are sent while the page is driven - cases = {} - app = create_app(scorer_for(emissions(["ħ"])), SawtakTTS("k", cache_dir=tmp_path), auth=auth) - - @app.get("/t/{email}/{case}") - def harness(email: str, case: str, request: Request): - page = index_html() # the page as the app serves it - page = re.sub(r'', - fake_supabase(email) if auth else "", page) - return HTMLResponse(page.replace("", DRIVER % cases[case] + "")) - - def index_html(): - return next(r for r in app.routes if getattr(r, "path", "") == "/").endpoint().body.decode() - - with socket.socket() as s: - s.bind(("127.0.0.1", 0)); port = s.getsockname()[1] - server = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning")) - thread = threading.Thread(target=server.run, daemon=True); thread.start() - while not server.started: - time.sleep(0.05) - profile = tmp_path / "chrome" - - def run(email, script, query="", width=390, mobile=False): - case = f"c{len(cases)}"; cases[case] = script - url = f"http://127.0.0.1:{port}/t/{email}/{case}{query}" - args = [CHROME, "--headless=new", "--disable-gpu", "--no-sandbox", "--no-first-run", - f"--user-data-dir={profile}", f"--window-size={width},860", "--virtual-time-budget=20000"] - if mobile: - args += ["--touch-events=enabled", "--user-agent=Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36"] - dom = subprocess.run(args + ["--dump-dom", url], capture_output=True, text=True, timeout=90).stdout - found = re.search(r'
          (.*?)
          ', dom, re.S) - assert found, dom[-2000:] - result = json.loads(found.group(1).replace("<", "<").replace(">", ">").replace("&", "&").replace(""", '"')) - assert "error" not in result, (result["error"], result.get("state")) - return result - - run.auth = auth - yield run - server.should_exit = True; thread.join(5) - - -FIRST_VISIT = """ -await until(()=>open("lessonDlg")); -const dlg=document.getElementById("lessonDlg"); -out.lessons=[...dlg.querySelectorAll("input[name=lesson]")].map(r=>r.value); -out.labels=[...dlg.querySelectorAll(".lesson-option")].map(l=>l.textContent); -out.focusInDialog=dlg.contains(document.activeElement); -dlg.dispatchEvent(new Event("cancel",{cancelable:true})); // Escape: the question stays -out.openAfterEscape=open("lessonDlg"); -dlg.querySelector("button[type=submit]").click(); // nothing chosen yet -await sleep(50); out.unchosen=dlg.querySelector(".lesson-status").textContent; -dlg.querySelector('input[value="5"]').checked=true; -dlg.querySelector("form").requestSubmit(); -await until(()=>view()==="home"); -out.focused=document.activeElement&&document.activeElement.id; -out.actions=[...document.querySelectorAll("#vids .focus-action")].map(b=>b.getAttribute("aria-label")); -out.rewardVisible=getComputedStyle(document.getElementById("rewardShelf")).display!=="none"; -out.dailyPracticeOnHome=[...document.querySelectorAll("#vids button")].some(b=>b.textContent.includes("תרגול יומי")); -out.referOnHome=document.querySelector("#vids .focus-refer")?.textContent; -out.referInNav=!document.querySelector('#nav [data-view="refer"]').hidden; -out.homeTab=!document.querySelector('#nav [data-view="home"]').hidden; -out.where=document.querySelector("#vids .focus-where span").textContent; -out.noOverflow=document.documentElement.scrollWidth<=innerWidth; -out.local=localStorage.getItem("ihki.lesson.v1:arabic:dana@x.com:7"); -await sleep(3000); -out.pushPrompt=open("pushDlg"); -""" - - -def test_first_visit_asks_the_lesson_then_shows_the_home_without_a_reminder_prompt(site): - r = site("dana@x.com", FIRST_VISIT) - assert r["pushCan"] is True # the prompt could come up in this browser, so its absence means something - assert r["lessons"] == ["3", "4", "5"] and all("שיעור" in label for label in r["labels"]) - assert r["focusInDialog"] and r["openAfterEscape"] and r["unchosen"] == "בחרו שיעור כדי להמשיך." - assert r["focused"] == "focusTitle" and r["homeTab"] and r["noOverflow"] - assert r["rewardVisible"] is True and r["dailyPracticeOnHome"] is False - assert r["referOnHome"] == "חבר מביא חבר" and r["referInNav"] is False - assert "שיעור 5" in r["where"] and "א1" in r["where"] - assert 1 <= len(r["actions"]) <= 3 - assert json.loads(r["local"]) == {"lesson": "5", "synced": True} - assert site.auth.rows[("dana@x.com", 7)]["current_lesson"] == "5" - assert r["pushPrompt"] is False - - -RETURN_VISIT = """ -await sleep(1500); -out.asked=open("lessonDlg"); out.view=view(); -out.actions=[...document.querySelectorAll("#vids .focus-action strong")].map(s=>s.textContent); -document.querySelector("#vids .focus-path").click(); -await sleep(100); -out.pathView=view(); -out.rewardOnPath=getComputedStyle(document.getElementById("rewardShelf")).display!=="none"; -out.pathShortcuts=[...document.querySelectorAll("#today button")].map(b=>b.textContent.trim()); -const cur=document.querySelector("#path [aria-current]"), heads=[...document.querySelectorAll("#path .chapter")]; -const at=heads.findIndex(h=>h.dataset.lesson==="5"); -out.inLesson5=!!cur&&at>=0&&!!(heads[at].compareDocumentPosition(cur)&Node.DOCUMENT_POSITION_FOLLOWING) - &&(!heads[at+1]||!!(heads[at+1].compareDocumentPosition(cur)&Node.DOCUMENT_POSITION_PRECEDING)); -out.lessonsOnPath=heads.map(h=>h.dataset.lesson); -const r=cur&&cur.getBoundingClientRect(); out.inView=!!r&&r.top>=0&&r.bottom<=innerHeight; -await sleep(2000); -out.pushPrompt=open("pushDlg"); -""" - - -def test_a_later_visit_opens_the_home_and_the_path_at_the_current_lesson(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", RETURN_VISIT) - assert r["asked"] is False and r["view"] == "home" - assert any(a.startswith("ממשיכים ב") for a in r["actions"]) - assert r["pathView"] == "" and r["lessonsOnPath"] == ["3", "4", "5"] - assert r["rewardOnPath"] is False - assert r["pathShortcuts"] == [] # daily practice and homework are no longer path shortcuts - assert r["inLesson5"] and r["inView"] # lessons 3 and 4 are unfinished too, yet the path opens at lesson 5 - assert r["pushPrompt"] is False # not on entering: the prompt waits for a good moment - - -PATH_SHADDA = """ -await sleep(1500); -const lesson=LESSONS[0], level=ALL_LEVELS.find(v=>v.id===lesson.level), topic=CATS.find(c=>c.lesson===lesson.id); -level.name="רַמّה"; lesson.name="דַרّס"; topic.name="כֻּלّ"; -showView(""); drawPath(true); -const levelEl=document.querySelector("#path .level"); -const lessonEl=document.querySelector(`#path .chapter[data-lesson="${lesson.id}"]`); -const topicEl=[...document.querySelectorAll("#path .sat")].find(b=>b.getAttribute("aria-label")?.startsWith(topic.name+":")); -const check=(el,original)=>{ - const sh=el.querySelector(".sh"), pseudo=getComputedStyle(sh,"::before"); - return {wrappers:el.querySelectorAll(".sh").length, original, - visible:!!(el.offsetWidth&&el.offsetHeight), baseText:el.textContent.includes(original.replaceAll("ّ","")), - mark:{content:pseudo.content,top:pseudo.top,mask:pseudo.maskImage||pseudo.webkitMaskImage}}; -}; -out.level=check(levelEl,level.name); out.level.label=levelEl.getAttribute("aria-label"); -out.lesson=check(lessonEl,lesson.name); out.lesson.label=lessonEl.getAttribute("aria-label"); -out.topic=check(topicEl,topic.name); out.topic.label=topicEl.getAttribute("aria-label"); -out.controls=[lessonEl,topicEl].map(b=>({tag:b.tagName,disabled:b.disabled,tabIndex:b.tabIndex,action:typeof b.onclick==="function"})); -out.noOverflow=document.documentElement.scrollWidth<=innerWidth; -const fallback=document.createElement("style"); fallback.textContent=".sh::before{content:none!important}"; document.head.append(fallback); -out.fallback=[levelEl,lessonEl,topicEl].every((el,i)=>el.textContent.includes([level.name,lesson.name,topic.name][i].replaceAll("ّ",""))&&el.offsetWidth>0); -fallback.remove(); -""" - - -def test_complete_path_renders_shadda_with_original_accessible_text_on_mobile(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", PATH_SHADDA, mobile=True) - for name in ("level", "lesson", "topic"): - item = r[name] - assert item["wrappers"] == 1 and item["visible"] and item["baseText"] - assert item["mark"]["content"] != "none" and item["mark"]["mask"] != "none" - assert float(item["mark"]["top"].removesuffix("px")) < 0 - assert item["original"] in item["label"] - assert r["controls"] == [{"tag": "BUTTON", "disabled": False, "tabIndex": 0, "action": True}] * 2 - assert r["noOverflow"] and r["fallback"] is True - - -EXERCISE_HEADING_SHADDA = """ -await sleep(1500); -const headingTopic=CATS.find(c=>!c.story&&!c.bubble&&PHRASES.some(p=>p.cat===c.id)); -const lesson=LESSONS.find(l=>l.id===headingTopic.lesson), phrase=PHRASES.find(p=>p.cat===headingTopic.id); -headingTopic.name="כֻּלّ"; lesson.name="דַרّס"; -cat=headingTopic.id; home=false; document.body.classList.remove("home"); setRound(); show(phrase,true); -await until(()=>document.getElementById("catName").querySelector(".sh")); -const check=(id,original)=>{ - const el=document.getElementById(id), sh=el.querySelector(".sh"), pseudo=getComputedStyle(sh,"::before"); - return {wrappers:el.querySelectorAll(".sh").length, text:el.textContent, original, - visible:!!(el.offsetWidth&&el.offsetHeight), mark:{content:pseudo.content,mask:pseudo.maskImage||pseudo.webkitMaskImage}}; -}; -out.topic=check("catName",headingTopic.name); out.lesson=check("lessonName",lesson.name); -out.noOverflow=document.documentElement.scrollWidth<=innerWidth; -""" - - -def test_exercise_heading_renders_shadda_on_mobile(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", EXERCISE_HEADING_SHADDA, mobile=True) - for name in ("topic", "lesson"): - item = r[name] - assert item["wrappers"] == 1 and item["visible"] - assert item["original"].replace("ّ", "") in item["text"] - assert item["mark"]["content"] != "none" and item["mark"]["mask"] != "none" - assert r["noOverflow"] is True - - -GOOD_MOMENT = """ -await sleep(1500); -out.before=open("pushDlg"); -const p=PHRASES.find(p=>p.listed!==false&&CATS.some(c=>c.id===p.cat&&!c.story)); -pickCat(p.cat); await sleep(200); -keepScore(p.id,92); // the first gold ever: a celebration, and a good moment for the prompt -out.celebrated=open("celebrateDlg"); -document.getElementById("celebrateGo").click(); -out.duringPractice=open("pushDlg"); // not in the middle of practice -showPath(); await sleep(2500); -out.pushPrompt=open("pushDlg"); -out.title=document.getElementById("pushTitle").textContent; -""" - - -def test_the_reminder_is_offered_back_home_after_a_good_moment(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", GOOD_MOMENT) - assert r["before"] is False and r["celebrated"] is True and r["duringPractice"] is False - assert r["pushPrompt"] is True and "כל הכבוד" in r["title"] - - -REFER = """ -await sleep(1500); -out.navHidden=document.querySelector('#nav [data-view="refer"]').hidden; -document.querySelector("#vids .focus-refer").click(); -await until(()=>view()==="refer"); -out.view=view(); out.title=document.querySelector("#vids .refer h2").textContent; -out.seen=localStorage.getItem("ihki.referSeen.2027"); -""" - - -def test_refer_a_friend_moves_from_the_nav_to_the_focused_home(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", REFER) - assert r == {"pushCan": True, "navHidden": True, "view": "refer", "title": "חבר מביא חבר 🧡", "seen": "true"} - - -REMINDER = """ -await sleep(1500); -out.view=view(); out.asked=open("lessonDlg"); -document.querySelector("#vids .back").click(); -await sleep(300); -out.afterBack=view(); out.askedAfterBack=open("lessonDlg"); -""" - - -def test_a_reminder_opens_the_daily_videos_before_the_lesson_question(site): - r = site("dana@x.com", REMINDER, "?view=daily") - assert r["view"] == "daily" and r["asked"] is False - assert r["askedAfterBack"] is True - - -def test_a_reminder_with_a_saved_lesson_returns_to_the_home(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "4"} - r = site("dana@x.com", REMINDER, "?view=daily") - assert r["view"] == "daily" and r["asked"] is False - assert r["afterBack"] == "home" and r["askedAfterBack"] is False - - -PATH_FIRST = """ -await sleep(1500); -out.view=view(); out.asked=open("lessonDlg"); -out.homeTab=!document.querySelector('#nav [data-view="home"]').hidden; -""" - - -def test_teachers_keep_the_path(site): - for teacher in ("boss@x.com", "t@x.com"): # a teacher of the whole service, and a class's own teacher - r = site(teacher, PATH_FIRST) - assert r == {"pushCan": True, "view": "", "asked": False, "homeTab": False}, teacher - - -UNREACHABLE = """ -await until(()=>open("lessonDlg")); -const dlg=document.getElementById("lessonDlg"); -dlg.querySelector('input[value="4"]').checked=true; dlg.querySelector("form").requestSubmit(); -await until(()=>view()==="home"); -out.local=localStorage.getItem("ihki.lesson.v1:arabic:dana@x.com:7"); -""" - - -def test_a_choice_the_server_cannot_keep_stays_on_the_device_and_is_sent_later(site): - site.auth.fail = "timed out" - r = site("dana@x.com", UNREACHABLE) - assert json.loads(r["local"]) == {"lesson": "4", "synced": False} - assert site.auth.rows == {} - site.auth.fail = None - r = site("dana@x.com", PATH_FIRST) # the next visit: the device's choice, sent again - assert r["view"] == "home" and r["asked"] is False - assert site.auth.rows[("dana@x.com", 7)]["current_lesson"] == "4" - r = site("rina@x.com", PATH_FIRST) # another account on this device does not inherit it - assert r["asked"] is True - - -SEVERAL_CLASSES = """ -await until(()=>open("pickDlg")); -out.askedBeforeClass=open("lessonDlg"); -[...document.querySelectorAll("#picks button")].find(b=>b.textContent.startsWith("ב2")).click(); -await until(()=>open("lessonDlg")); -out.lessons=[...document.querySelectorAll("#lessonDlg input[name=lesson]")].map(r=>r.value); -document.querySelector('#lessonDlg input[value="2.2"]').checked=true; -document.querySelector("#lessonDlg form").requestSubmit(); -await until(()=>view()==="home"); -out.where=document.querySelector("#vids .focus-where span").textContent; -""" - - -def test_each_class_asks_for_its_own_lesson(site): - site.auth.layout = [dict(CLASSES[0], teachers=[]), CLASSES[1]] # dana studies in two classes, of levels 1 and 2 - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", SEVERAL_CLASSES) - assert r["askedBeforeClass"] is False and r["lessons"] == ["2.1", "2.2"] - assert "ב2" in r["where"] - assert site.auth.rows[("dana@x.com", 7)]["current_lesson"] == "5" # the other class keeps its own - assert site.auth.rows[("dana@x.com", 8)]["current_lesson"] == "2.2" - - -AMBIGUOUS = """ -await until(()=>open("lessonDlg")); -const dlg=document.getElementById("lessonDlg"); -out.radios=dlg.querySelectorAll("input[type=radio]").length; -out.message=dlg.querySelector(".lesson-error").textContent; -dlg.querySelector("button").click(); -await sleep(100); -out.view=view(); out.open=open("lessonDlg"); out.pathShown=document.querySelectorAll("#path .chapter").length>0; -""" - - -def test_a_class_of_two_levels_asks_for_a_correction_and_keeps_the_path(site): - site.auth.layout = [CLASSES[2]] # lessons 5 and 2.1 - r = site("dana@x.com", AMBIGUOUS) - assert r["radios"] == 0 and "לתקן את הגדרות הכיתה" in r["message"] - assert r["view"] == "" and r["open"] is False and r["pathShown"] - - -CHANGE = """ -await sleep(1500); -localStorage.setItem("ihki.best2",JSON.stringify({"1":80})); -const before=localStorage.getItem("ihki.best2"); -document.querySelector("#vids .focus-change").click(); -await until(()=>open("lessonDlg")); -out.selected=document.querySelector("#lessonDlg input[name=lesson]:checked").value; -out.canCancel=!!document.querySelector("#lessonDlg .row button[type=button]"); -document.querySelector('#lessonDlg input[value="3"]').checked=true; -document.querySelector("#lessonDlg form").requestSubmit(); -await until(()=>document.querySelector("#vids .focus-where span")?.textContent.includes("שיעור 3")); -out.best=localStorage.getItem("ihki.best2")===before; -out.continueTitle=[...document.querySelectorAll("#vids .focus-action strong")].map(s=>s.textContent); -""" - - -def test_changing_the_lesson_moves_the_home_and_keeps_progress(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", CHANGE) - assert r["selected"] == "5" and r["canCancel"] is True and r["best"] is True - assert "ממשיכים בשיעור 3" in r["continueTitle"] - assert site.auth.rows[("dana@x.com", 7)]["current_lesson"] == "3" - - -RESUME_STARTED_TOPIC = """ -await sleep(1500); -const lesson=LESSONS.find(l=>l.id===currentLesson()), topics=CATS.filter(c=>c.lesson===lesson.id&&!c.story); -const started=topics.slice(1).find(c=>PHRASES.some(p=>p.cat===c.id&&p.listed!==false)); -const phrase=started&&PHRASES.find(p=>p.cat===started.id&&p.listed!==false); -if(!started||!phrase) throw new Error("the current lesson needs two practice topics"); -cat=started.id; store.set("ihki.cat",cat); best[phrase.id]=60; store.set("ihki.best2",best); -drawNav(); -const action=[...document.querySelectorAll("#vids .focus-action")].find(b=>b.textContent.includes("ממשיכים ב")); -out.started=started.name; out.proposed=action.querySelector(".focus-detail").textContent; -""" - - -def test_home_resumes_the_partly_started_topic_instead_of_the_first_unfinished_one(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", RESUME_STARTED_TOPIC) - assert r["proposed"] == r["started"] - - -RESUME_OPEN_STORY = """ -await sleep(1500); -const current=LESSONS.find(l=>l.id===currentLesson()); -const started=CATS.find(c=>c.story&&c.lesson!==current.id&&PHRASES.some(p=>p.cat===c.id&&p.listed!==false)); -if(!started) throw new Error("the class needs a story in an earlier lesson"); -cat=started.id; store.set("ihki.cat",cat); // opened, but left before the first line received a score -drawNav(); -const action=[...document.querySelectorAll("#vids .focus-action")].find(b=>b.textContent.includes("ממשיכים ב")); -out.story=started.name; -out.lesson=LESSONS.find(l=>l.id===started.lesson).name; -out.proposed=action.querySelector(".focus-detail").textContent; -out.title=action.querySelector("strong").textContent; -""" - - -def test_home_resumes_an_open_story_before_its_first_score_even_from_an_earlier_lesson(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", RESUME_OPEN_STORY) - assert r["proposed"] == r["story"] - assert r["lesson"] in r["title"] - - -PRIORITY = """ -await sleep(1500); -const titles=()=>[...document.querySelectorAll("#vids .focus-action strong")].map(s=>s.textContent); -out.first=titles(); -out.homework=document.querySelector("#vids .focus-action").getAttribute("aria-label"); -document.querySelectorAll("#vids .focus-action")[1].click(); // today's video: watched now -await sleep(300); -document.querySelectorAll("dialog[open]").forEach(d=>d.close()); -showView("home"); await sleep(100); -out.afterWatching=titles(); -""" - - -def test_homework_then_todays_video_then_the_lesson(site): - from datetime import date, timedelta - today, tomorrow = date.today().isoformat(), (date.today() + timedelta(days=1)).isoformat() - site.auth.layout[0] = dict(site.auth.layout[0], homeworks=[{"topic": "l4-say", "due": tomorrow}], - dailies=[{"video": 1, "date": today}]) - site.auth.daily_rows = [{"id": 1, "level": "1", "title": "ברוכים הבאים", "url": "https://vimeo.com/76979871"}] - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", PRIORITY) - assert r["first"][:2] == ["שיעורי בית", "הסרטון של היום"] and r["first"][2].startswith("ממשיכים ב") - assert len(r["first"]) == 3 # the short activity gives way to the three before it - assert "מגישים מחר" in r["homework"] - assert "הסרטון של היום" not in r["afterWatching"] and len(r["afterWatching"]) == 3 - - -SHORT = """ -await sleep(1500); -const short=[...document.querySelectorAll("#vids .focus-action")].find(b=>b.textContent.includes("תרגול קצר")); -out.detail=short.querySelector(".focus-detail").textContent; -short.click(); -await sleep(400); -out.counter=document.getElementById("counter").textContent; -out.size=daily.length; -out.plan=dailyPlan().slice(0,10).map(id=>CATS.find(c=>c.id===PHRASES.find(p=>p.id===id).cat).lesson); -endDaily(); -await sleep(100); -out.doneTitle=document.getElementById("dailyTitle").textContent; out.more=!document.getElementById("dailyMore").hidden; -out.dailyCounted=localStorage.getItem("ihki.daily"); -out.backTo=document.getElementById("dailyClose").textContent; out.view=view(); -""" - - -def test_short_activity_shows_its_size_and_announces_its_end(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", SHORT) - n = int(r["detail"].split()[0]) - assert 1 <= n <= 10 and r["size"] == n and "תרגול קצר" in r["counter"] - assert r["doneTitle"] == "התרגול הקצר הסתיים" and r["more"] is False - assert r["dailyCounted"] is None # a short round is not the day's practice - assert r["backTo"] == "חזרה לדף הבית" and r["view"] == "home" - assert r["plan"] and set(r["plan"]) == {"5"} # daily practice: new sentences of the current lesson first - - -def test_the_open_service_keeps_the_path(open_site): - r = open_site("anyone@x.com", PATH_FIRST) - assert r["view"] == "" and r["asked"] is False and r["homeTab"] is False - - -LOG_OUT_TO_GOOGLE = """ -await sleep(200); -const calls=[]; -pushSignOut=async()=>calls.push("push"); -sb.auth.signOut=async()=>{ calls.push("sign-out"); return {}; }; -sb.auth.signInWithOAuth=async options=>{ calls.push("google"); out.oauth=options; return {}; }; -document.getElementById("classExit").click(); -await until(()=>calls.includes("google")); -out.calls=calls; -""" - - -def test_account_exit_signs_out_then_returns_to_google_authentication(site): - r = site("dana@x.com", LOG_OUT_TO_GOOGLE) - assert r["calls"] == ["push", "sign-out", "google"] - assert r["oauth"]["provider"] == "google" - assert r["oauth"]["options"]["queryParams"] == {"prompt": "select_account"} - assert r["oauth"]["options"]["redirectTo"].startswith("http") - - -FINISHED = """ -await sleep(1500); -for(const p of PHRASES) best[p.id]=60; // every sentence said, none excellent: there is something to improve -for(const c of CATS.filter(c=>c.lesson==="5")){ - if(c.learn) learned[c.id]=lWords(c).map(p=>p.id); - if(c.match) matched[c.id]={ids:lWords({id:c.match}).map(p=>p.id)}; - if(c.write) written[c.id]=Array.from({length:c.write.lines},()=>"כתבתי"); -} -showView("home"); await sleep(100); -out.titles=[...document.querySelectorAll("#vids .focus-action strong")].map(s=>s.textContent); -out.note=(document.querySelector("#vids .focus-note")||{}).textContent||""; -out.lesson=document.querySelector("#vids .focus-where span").textContent; -""" - - -def test_a_finished_lesson_offers_a_review_and_another_lesson_without_moving_on(site): - site.auth.rows[("dana@x.com", 7)] = {"current_lesson": "5"} - r = site("dana@x.com", FINISHED) - assert r["titles"] == ["תרגול קצר", "בחירת שיעור אחר"] - assert "סיימתם את שיעור 5" in r["note"] and "שיעור 5" in r["lesson"] # the class's lesson is not advanced - assert site.auth.rows[("dana@x.com", 7)]["current_lesson"] == "5" diff --git a/tests/test_live.py b/tests/test_live.py deleted file mode 100644 index c47db9342f94306ec169972b548b1b96c8bb5a95..0000000000000000000000000000000000000000 --- a/tests/test_live.py +++ /dev/null @@ -1,81 +0,0 @@ -import pytest - -from app.live import LiveConflict, LiveStore - - -COURSE = {"id": 7, "name": "כיתה א", "emails": ["student@example.com"], - "nicknames": {"student@example.com": "דנה"}, "teachers": ["teacher@example.com"]} -TEACHER = {"id": "teacher-id", "email": "teacher@example.com"} -STUDENT = {"id": "student-id", "email": "student@example.com"} -PHRASE = {"id": 12, "he": "איפה היית?", "ar": "وين كنت؟", "tr": "וֵין כֻּנְת?"} - - -def test_live_session_one_attempt_and_private_student_view(): - live = LiveStore() - session = live.start(COURSE, TEACHER) - assert len(session["code"]) == 6 and session["question"] is None - assert live.start(COURSE, TEACHER)["code"] == session["code"] # one active session per class - - live.join(session["code"], STUDENT, "דנה") - live.ask(session["code"], PHRASE) - assert live.begin_answer(session["code"], STUDENT["id"])["phrase_id"] == 12 - with pytest.raises(LiveConflict): - live.begin_answer(session["code"], STUDENT["id"]) - - answer = {"score": 81, "expected_tr": PHRASE["tr"], "heard_tr": "וֵין כּוּנְת?", "issues": []} - live.finish_answer(session["code"], STUDENT["id"], answer) - student = live.heartbeat(session["code"], STUDENT) - assert student["answered"] is True and student["answer"] == answer - assert "participants" not in student - - teacher = live.heartbeat(session["code"], TEACHER, teacher=True) - assert teacher["participants"][0]["name"] == "דנה" - assert teacher["participants"][0]["answer"] == answer - - -def test_failed_recording_can_be_retried_and_end_erases_session(): - live = LiveStore() - code = live.start(COURSE, TEACHER)["code"] - live.join(code, STUDENT) - live.ask(code, PHRASE) - live.begin_answer(code, STUDENT["id"]) - live.cancel_answer(code, STUDENT["id"]) - live.begin_answer(code, STUDENT["id"]) - live.end(code) - assert live.get(code) is None - - -def test_question_accepts_the_server_phrase_shape(): - live = LiveStore() - code = live.start(COURSE, TEACHER)["code"] - question = live.ask(code, {"id": 13, "he": "מה שלומך?", "tr": "כִּיף חַאלַכּ?", - "variants": [("كيف حالك؟", "kiːf ħaːlak")]})["question"] - assert question["ar"] == "كيف حالك؟" - - -def test_presence_summary_is_identity_free_ttl_bounded_and_deduplicated(monkeypatch): - clock = {"now": 100.0} - monkeypatch.setattr("app.live.time.monotonic", lambda: clock["now"]) - live = LiveStore() - first = live.start(COURSE, TEACHER)["code"] - second_course = {**COURSE, "id": 8, "name": "כיתה ב"} - second = live.start(second_course, TEACHER)["code"] - another = {"id": "another-id", "email": "another@example.com"} - live.join(first, STUDENT) - live.join(second, STUDENT) # Same learner in two classes must count once. - live.join(second, another) - - two = live.presence_summary({7, 8}, now=119.0) - assert two == {"active_session": True, "active_users": 2, - "heartbeat_ttl_seconds": 20, "ephemeral": True} - assert not any(key in str(two).lower() for key in ("student-id", "email", "name", "participant")) - assert live.presence_summary({7}, now=120.0)["active_users"] == 1 - assert live.presence_summary({7, 8}, now=121.0)["active_users"] == 0 - assert live.presence_summary({999}, now=121.0)["active_session"] is False - - -def test_new_store_has_no_reconstructed_presence(): - assert LiveStore().presence_summary({7}) == { - "active_session": False, "active_users": 0, - "heartbeat_ttl_seconds": 20, "ephemeral": True, - } diff --git a/tests/test_math_skills.py b/tests/test_math_skills.py index 6fb2fea1c416b45872a6dc0d02c652fb66feb986..9b1643de0aa2377ae7454b6f614d5d970c2bf65c 100644 --- a/tests/test_math_skills.py +++ b/tests/test_math_skills.py @@ -1,156 +1,50 @@ import copy +import asyncio +import httpx + +from app.main import create_app from app.math_skills import algebra9, problems def test_the_algebra_graph_is_whole(): graph = algebra9() assert problems(graph) == [] - assert len(graph["skills"]) == 56 and {u["id"] for u in graph["units"]} == {"f", "t", "n", "q", "p", "w"} - # every unit opens somewhere a student can start: foundations need nothing - assert all(s["prereqs"] == [] for s in graph["skills"] if s["id"] in ("f_neg", "f_frac")) + assert len(graph["skills"]) == 56 + assert {unit["id"] for unit in graph["units"]} == {"f", "t", "n", "q", "p", "w"} def test_a_broken_graph_is_named(): - graph = algebra9() - skill = lambda g, sid: next(s for s in g["skills"] if s["id"] == sid) - - g = copy.deepcopy(graph) - skill(g, "f_neg")["prereqs"] = ["f_ops"] # f_ops needs f_neg - assert problems(g)[0].startswith("cycle:") + graph = copy.deepcopy(algebra9()) + skill = lambda sid: next(item for item in graph["skills"] if item["id"] == sid) + skill("f_neg")["prereqs"] = ["f_ops"] + assert problems(graph)[0].startswith("cycle:") - g = copy.deepcopy(graph) - skill(g, "f_ops")["prereqs"].append("f_neg_typo") - assert problems(g) == ["f_ops: unknown prerequisite f_neg_typo"] - g = copy.deepcopy(graph) - skill(g, "f_like")["prereqs"].append("f_neg") # f_ops already brings it - assert problems(g) == ["f_like: f_neg is implied by f_ops"] - - g = copy.deepcopy(graph) - skill(g, "f_neg")["sample"]["choices"][1] = {"text": "$−13$", "correct": True} - assert problems(g) == ["f_neg: needs exactly one correct choice"] - - g = copy.deepcopy(graph) - g["mistakes"]["minus_minus"]["back"] = "t_ext" # f_ops's mistake may not send it forward - assert "f_ops: mistake minus_minus sends back to t_ext, which is not before it" in problems(g) +def test_math_is_the_only_page_and_api(): + async def check(): + async with httpx.AsyncClient(transport=httpx.ASGITransport(app=create_app()), base_url="http://test") as client: + assert "מתמטיקה בכיף" in (await client.get("/")).text + assert "מתמטיקה בכיף" in (await client.get("/math")).text + assert (await client.get("/api/math/skills")).json() == algebra9() + assert (await client.get("/api/health")).json() == {"ok": True, "app": "mathly"} + asyncio.run(check()) class FakeSolver: - def __init__(self, readable=True): - self.seen, self.readable = [], readable - def solve(self, image, media_type): - self.seen.append((image, media_type)) - return {"readable": self.readable, "problem": "$x² = 9$", "steps": [{"hint": "h", "work": "w"}], + return {"readable": True, "problem": "$x² = 9$", "steps": [{"hint": "h", "work": "w"}], "answer": "$x = ±3$", "skill": "q_eq_simple", "note": ""} -def photo_app(monkeypatch, auth=None): - from fastapi.testclient import TestClient - from app.main import create_app - from tests.test_scoring import emissions, scorer_for - - monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test") - c = TestClient(create_app(scorer_for(emissions(["w"])), auth=auth)) - c.__enter__() - c.app.state.photo, c.app.state.photo_key = FakeSolver(), "sk-test" - return c - - def test_a_photographed_problem_is_solved(monkeypatch): - c = photo_app(monkeypatch) - send = lambda data=b"jpeg", kind="image/jpeg": c.post("/api/math/photo", files={"photo": ("p.jpg", data, kind)}) - r = send() - assert r.status_code == 200 and r.json()["answer"] == "$x = ±3$" and r.json()["left"] is None # open service: no limit - assert c.app.state.photo.seen == [(b"jpeg", "image/jpeg")] - assert send(kind="application/pdf").status_code == 415 and send(data=b"").status_code == 400 - assert send(data=b"x" * 5_000_001).status_code == 413 and len(c.app.state.photo.seen) == 1 - monkeypatch.setenv("ANTHROPIC_API_KEY", "") # no key: the feature says it is off - monkeypatch.delenv("CLAUDE_API_KEY", raising=False) - assert send().status_code == 503 - c.__exit__(None, None, None) - - -def test_a_student_has_a_daily_number_of_photos(monkeypatch): - import app.main as main - - class Auth: - url, key, teachers, tutors, developers = "https://p.supabase.co", "k", frozenset(), frozenset(), frozenset() - def user(self, token): return {"id": "u1", "email": "s@b.c"} if token == "s" else None - def classes(self, fresh=False): return [{"id": 1, "name": "ט", "emails": ["s@b.c"], "nicknames": {}, "lessons": []}] - def ranked(self, fresh=False): return frozenset() - def __getattr__(self, name): return lambda *a, **k: [] - - monkeypatch.setattr(main, "DAILY_PHOTOS", 2) - c = photo_app(monkeypatch, Auth()) - send = lambda: c.post("/api/math/photo", files={"photo": ("p.jpg", b"jpeg", "image/jpeg")}, headers={"Authorization": "Bearer s"}) - assert c.post("/api/math/photo", files={"photo": ("p.jpg", b"jpeg", "image/jpeg")}).status_code == 401 - c.app.state.photo.readable = False # a photo it could not read does not count - assert send().json()["left"] == 2 - c.app.state.photo.readable = True - assert [send().json()["left"], send().json()["left"]] == [1, 0] and send().status_code == 429 - c.__exit__(None, None, None) - - -def test_the_path_reads_the_graph(): - from fastapi.testclient import TestClient - from app.main import create_app - from tests.test_scoring import emissions, scorer_for - - with TestClient(create_app(scorer_for(emissions(["w"])))) as c: - r = c.get("/api/math/skills") - assert r.status_code == 200 and r.json() == algebra9() - - -def test_a_math_class_studies_math_units(): - from fastapi.testclient import TestClient - from app.main import create_app, ALL_LESSONS - from tests.test_scoring import emissions, scorer_for - - class Auth: - url, key, teachers, tutors, developers = "https://p.supabase.co", "k", frozenset(), frozenset(), {"d@b.c"} - users_ = {t: {"id": i, "email": f"{t}@b.c", "name": t} for t, i in (("s", "u1"), ("m", "u2"), ("d", "u3"))} - def __init__(self): self.cls, self.saved = [], [] - def user(self, token): return self.users_.get(token) - def users(self): return list(self.users_.values()) - def classes(self, fresh=False): return self.cls - def access_for(self, user_id): return {"user_id": "u2", "roles": ["admin"], "subject": "מתמטיקה"} if user_id == "u2" else None - def access_rows(self): return [self.access_for("u2")] - def ranked(self, fresh=False): return frozenset() - def save_class(self, *args, **kw): self.saved.append(args) - def __getattr__(self, name): return lambda *a, **k: [] - - tok = lambda t: {"Authorization": f"Bearer {t}"} - with TestClient(create_app(scorer_for(emissions(["w"])), auth=Auth())) as c: - auth = c.app.state.auth - auth.cls = [{"id": 1, "name": "ט1", "emails": ["s@b.c"], "nicknames": {}, "lessons": ["q", "f"], "subject": "מתמטיקה"}, - {"id": 2, "name": "ערבית", "emails": [], "nicknames": {}, "lessons": [], "subject": "ערבית"}] - me = c.get("/api/me", headers=tok("s")).json() # the student's path shows the class's units, in the path's order - assert me["classes"][0]["lessons"] == ["f", "q"] and me["lessons"] == [] - listed = {k["name"]: k["lessons"] for k in c.get("/api/class", headers=tok("d")).json()["classes"]} - assert listed == {"ט1": ["f", "q"], "ערבית": ALL_LESSONS} - # the math admin opens a math class: units only, and no homework or daily videos even when sent - new = lambda body, who="m": c.post("/api/classes", json={"name": "ט2", "emails": "", **body}, headers=tok(who)) - assert new({"lessons": [ALL_LESSONS[0]]}).status_code == 400 - assert new({"lessons": ["t", "f"], "homeworks": [{"topic": "x", "due": ""}], "dailies": [{"video": 9, "date": "2026-01-01"}]}).status_code == 200 - assert auth.saved[-1][4:] == (["f", "t"], [], None, "מתמטיקה") - # a developer has no subject: the page says which; an edit keeps the class's own - assert new({"lessons": ["n"], "subject": "מתמטיקה"}, "d").status_code == 200 and auth.saved[-1][4:] == (["n"], [], None, "מתמטיקה") - assert new({"lessons": ["n"], "subject": "חשבון"}, "d").status_code == 400 # no subject: Arabic lessons - assert c.put("/api/classes/1", json={"name": "ט1", "emails": "s@b.c", "lessons": ["w"]}, headers=tok("d")).status_code == 200 - assert auth.saved[-1][4] == ["w"] - - -def test_a_space_of_its_own_opens_on_its_subject(monkeypatch): - from fastapi.testclient import TestClient - from app.main import create_app - from tests.test_scoring import emissions, scorer_for - - monkeypatch.setenv("HOME_SUBJECT", "math") # mathly: "/" is the math app, and Arabic still has its own path - with TestClient(create_app(scorer_for(emissions(["w"])))) as c: - assert 'data-subject="math" data-home="math"' in c.get("/").text - assert 'data-subject="arabic" data-home="math"' in c.get("/arabic").text - monkeypatch.delenv("HOME_SUBJECT") - with TestClient(create_app(scorer_for(emissions(["w"])))) as c: - assert 'data-subject="arabic" data-home="arabic"' in c.get("/").text + monkeypatch.setenv("ANTHROPIC_API_KEY", "test") + app = create_app() + app.state.photo, app.state.photo_key = FakeSolver(), "test" + async def check(): + async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: + response = await client.post("/api/math/photo", files={"photo": ("problem.jpg", b"jpeg", "image/jpeg")}) + assert response.status_code == 200 + assert response.json()["answer"] == "$x = ±3$" + assert response.json()["left"] is None + asyncio.run(check()) diff --git a/tests/test_minerva_import.py b/tests/test_minerva_import.py deleted file mode 100644 index 164c94b3e3a4205de8523673a5eac9c4383423cc..0000000000000000000000000000000000000000 --- a/tests/test_minerva_import.py +++ /dev/null @@ -1,17 +0,0 @@ -from scripts.import_minerva import phrases - - -def test_minerva_import_takes_every_reviewed_line_from_the_metadata(): - made = phrases() - keys = [p[2]["key"] for p in made] - assert len(keys) == len(set(keys)) - assert all(s["start"] < s["end"] and p["variants"] == [(s["ar"], s["ipa"])] for _, s, p in made) - pilot = phrases({"U8aBRd3-wlM", "OuAwxXUZBU4", "V2y6LqnoXTo"}) - assert len(pilot) == 70 and pilot[0][2]["key"] == "minerva:U8aBRd3-wlM:01" - - -def test_grocery_price_keeps_the_spoken_number_as_two_arabic_words(): - price = next(segment for video, segment, _ in phrases({"U8aBRd3-wlM"}) - if segment["line"] == 8) - assert price["ar"] == "خمسة وعشرين شيقل. تفضّل." - assert len(price["ar"].split()) == len(price["ipa"].split("|")) == 4 diff --git a/tests/test_models_tab.py b/tests/test_models_tab.py deleted file mode 100644 index 0be3b415f5c1afdfc07f9895375d0a8e9d0e7415..0000000000000000000000000000000000000000 --- a/tests/test_models_tab.py +++ /dev/null @@ -1,265 +0,0 @@ -"""The panel's "מודלים" tab (app/models.py, /api/admin/models): what each model measured, and the developer's switch.""" -import time - -from fastapi.testclient import TestClient -from test_keystore import AdminAuth, KeyAuth, bearer -from test_scoring import emissions, scorer_for - -import app.main -from app.main import create_app -from app.models import CATALOG - -BASE = "facebook/wav2vec2-xlsr-53-espeak-cv-ft" -FT = "checkpoints/ihki-ft-minerva" - - -def client(auth): - return TestClient(create_app(scorer_for(emissions(["ħ", "aː", "l"])), auth=auth)) - - -def wait_switch(c, headers, seconds=5.0): - for _ in range(int(seconds / 0.05)): - data = c.get("/api/admin/models", headers=headers).json() - if data["switch"]["state"] != "loading": - return data - time.sleep(0.05) - raise AssertionError("the switch never finished") - - -def test_the_catalog_shows_every_model_with_its_numbers_and_the_active_one(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - with client(KeyAuth()) as c: - data = c.get("/api/admin/models", headers=bearer("teacher")).json() - assert data["active"] == BASE and data["can_switch"] is True - ids = [m["id"] for m in data["models"]] - assert ids == [m["id"] for m in CATALOG] - active = [m for m in data["models"] if m["active"]] - assert [m["id"] for m in active] == [BASE] - assert all(0 <= m["metrics"]["per"] <= 1 for m in data["models"]) - - -def test_a_model_set_in_model_id_but_not_in_the_catalog_is_listed_first(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", "someone/else") - with client(KeyAuth()) as c: - data = c.get("/api/admin/models", headers=bearer("teacher")).json() - assert data["models"][0]["id"] == "someone/else" and data["models"][0]["active"] - assert data["models"][0]["metrics"] is None - - -def test_an_admin_sees_the_models_but_cannot_switch(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - with client(AdminAuth()) as c: - data = c.get("/api/admin/models", headers=bearer("teacher")).json() - assert data["can_switch"] is False - res = c.put("/api/admin/models/active", json={"model_id": FT}, headers=bearer("teacher")) - assert res.status_code == 403 - - -def test_switching_loads_the_new_model_and_swaps_it_in(monkeypatch, tmp_path): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - monkeypatch.setattr(app.main, "model_availability", lambda model_id: (True, "")) - loaded = [] - new = scorer_for(emissions(["ħ", "aː", "l"])) - monkeypatch.setattr(app.main, "load_scorer", lambda model_id: loaded.append(model_id) or new) - auth = KeyAuth() - with client(auth) as c: - res = c.put("/api/admin/models/active", json={"model_id": FT}, headers=bearer("teacher")) - assert res.status_code == 200 - data = wait_switch(c, bearer("teacher")) - assert loaded == [FT] - assert data["active"] == FT and data["switch"]["state"] == "idle" - assert c.app.state.scorer is new - assert c.get("/api/health").json()["model"] == FT - assert any(details == {"model": FT, "action": "switch"} for _, _, details in auth.events) - - -def test_a_failed_load_keeps_the_running_model_and_says_why(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - monkeypatch.setattr(app.main, "model_availability", lambda model_id: (True, "")) - - def broken(model_id): - raise OSError("no such model") - monkeypatch.setattr(app.main, "load_scorer", broken) - with client(KeyAuth()) as c: - before = c.app.state.scorer - c.put("/api/admin/models/active", json={"model_id": FT}, headers=bearer("teacher")) - data = wait_switch(c, bearer("teacher")) - assert data["active"] == BASE and data["switch"]["state"] == "error" - assert "no such model" in data["switch"]["error"] - assert c.app.state.scorer is before - - -def test_unknown_or_unavailable_models_are_refused(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - monkeypatch.setattr(app.main, "model_availability", lambda model_id: (False, "קובץ המודל לא נמצא בשרת")) - with client(KeyAuth()) as c: - assert c.put("/api/admin/models/active", json={"model_id": "evil/model"}, - headers=bearer("teacher")).status_code == 400 - res = c.put("/api/admin/models/active", json={"model_id": FT}, headers=bearer("teacher")) - assert res.status_code == 409 and "לא נמצא" in res.json()["detail"] - - -# ---- the scorer's settings per model ---- - -class SettingsAuth(KeyAuth): - def __init__(self, table=True): - super().__init__() - self.settings_rows, self.settings_table = {}, table - - def model_settings(self): - if not self.settings_table: - raise app.main.AuthUnavailable("Supabase answered 404: relation model_settings does not exist") - return list(self.settings_rows.values()) - - def set_model_settings(self, model, settings, by): - self.settings_rows[model] = {"model": model, "settings": settings, "updated_by": by, - "updated_at": "2026-10-25T12:00:00+00:00"} - - -def test_settings_show_each_default_and_the_base_models_built_in_ħ_threshold(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - with client(SettingsAuth()) as c: - data = c.get(f"/api/admin/model-settings?model={BASE}", headers=bearer("teacher")).json() - assert data["stored"] and data["can_edit"] and data["active"] - flag = next(f for f in data["fields"] if f["key"] == "flag_below") - assert flag["default"] == 50.0 and flag["value"] == 50.0 and not flag["changed"] - sounds = {s["sound"]: s for g in data["sounds"] for s in g["sounds"]} - assert sounds["ħ"]["default"] == 20.0 and sounds["ʕ"]["default"] is None - - -def test_saving_settings_for_the_active_model_applies_them_at_once(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - auth = SettingsAuth() - with client(auth) as c: - res = c.put("/api/admin/model-settings", headers=bearer("teacher"), json={ - "model": BASE, "settings": {"flag_below": 42, "gop.leniency": 0.5, "flag_below_sound": {"ħ": None, "ʕ": 35}}}) - assert res.status_code == 200, res.text - cfg = c.app.state.scorer.cfg - assert cfg.flag_below == 42 and cfg.gop.leniency == 0.5 - assert "ħ" not in cfg.flag_below_sound and cfg.flag_below_sound["ʕ"] == 35 - data = res.json() - assert next(f for f in data["fields"] if f["key"] == "flag_below")["changed"] - assert auth.settings_rows[BASE]["settings"]["flag_below"] == 42 - - -def test_settings_for_another_model_wait_until_it_is_switched_on(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - monkeypatch.setattr(app.main, "model_availability", lambda model_id: (True, "")) - monkeypatch.setattr(app.main, "load_scorer", lambda model_id: scorer_for(emissions(["ħ", "aː", "l"]))) - with client(SettingsAuth()) as c: - before = c.app.state.scorer.cfg.flag_below - c.put("/api/admin/model-settings", headers=bearer("teacher"), json={"model": FT, "settings": {"flag_below": 33}}) - assert c.app.state.scorer.cfg.flag_below == before - c.put("/api/admin/models/active", json={"model_id": FT}, headers=bearer("teacher")) - wait_switch(c, bearer("teacher")) - assert c.app.state.scorer.cfg.flag_below == 33 - - -def test_bad_settings_are_refused_with_the_reason(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - with client(SettingsAuth()) as c: - for bad in ({"flag_below": 140}, {"gop.ratio_pool": "median"}, {"nonsense": 1}, - {"flag_below_sound": {"ħ": -3}}, {"flag_below_sound": {"🙂": 30}}, {"max_issues": 2.5}): - res = c.put("/api/admin/model-settings", headers=bearer("teacher"), json={"model": BASE, "settings": bad}) - assert res.status_code == 400, bad - - -def test_without_the_table_settings_are_read_only_and_say_how_to_create_it(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - with client(SettingsAuth(table=False)) as c: - data = c.get("/api/admin/model-settings", headers=bearer("teacher")).json() - assert not data["stored"] and not data["can_edit"] and "create table public.model_settings" in data["create_sql"] - res = c.put("/api/admin/model-settings", headers=bearer("teacher"), json={"model": BASE, "settings": {}}) - assert res.status_code == 503 - - -def test_an_admin_reads_settings_but_cannot_save_them(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - - class Admin(SettingsAuth): - developers = frozenset() - with client(Admin()) as c: - assert c.get("/api/admin/model-settings", headers=bearer("teacher")).json()["can_edit"] is False - assert c.put("/api/admin/model-settings", headers=bearer("teacher"), - json={"model": BASE, "settings": {}}).status_code == 403 - - -def test_stored_settings_are_applied_when_the_server_starts(monkeypatch): - monkeypatch.setattr(app.main, "MODEL_ID", BASE) - auth = SettingsAuth() - auth.set_model_settings(BASE, {"issue_below": 80}, "dev@x") - with client(auth) as c: - assert c.app.state.scorer.cfg.issue_below == 80 - - -# ---- the practice page's model comparison (developers) ---- - -def a_second_of_audio() -> bytes: - import io - import wave - import numpy as np - t = np.arange(16000) / 16000 - pcm = (0.3 * np.sin(2 * np.pi * 220 * t) * 32767).astype(" checked - assert data["recent_practice"] == { - "source": "persisted_attempts", "window_minutes": 5, - "checked_at": "2026-10-27T10:10:00Z", "collection_delay_seconds": None, - "update_semantics": "current_as_of_database_read", "presence_claim": False, - "status": "complete", "students_with_activity": 4, "attempts": 5, - } - assert data["last_practice_activity"]["timestamp"] == "2026-10-27T10:10:00Z" - assert data["last_practice_activity"]["local_timestamp"] == "2026-10-27T12:10:00+02:00" - assert data["last_practice_activity"]["elapsed_seconds"] == 0 - assert data["last_application_activity"]["status"] == "unavailable" - assert not any(value in str(data) for value in ("u1", "u2", "example.com")) - - -def test_current_activity_distinguishes_no_activity_suppression_and_unavailable_data(): - auth = CurrentActivityAuth() - checked = datetime(2026, 10, 27, 10, 10, tzinfo=timezone.utc) - empty = current_activity(auth, ACTOR, window_minutes=15, class_id=1, checked_at=checked) - assert empty["recent_practice"]["status"] == "no_activity" - assert empty["last_practice_activity"]["status"] == "no_data" - assert "students_with_activity" not in empty["recent_practice"] - - auth.rows = [recent_attempt(i, "u1" if i < 4 else "u2", 5 + i) for i in range(1, 6)] - hidden = current_activity(auth, ACTOR, window_minutes=15, class_id=1, checked_at=checked) - assert hidden["recent_practice"]["status"] == "suppressed" - assert hidden["last_practice_activity"]["status"] == "suppressed" - assert "attempts" not in hidden["recent_practice"] and "timestamp" not in hidden["last_practice_activity"] - - auth.scoped_attempts = lambda *args: (_ for _ in ()).throw(AuthUnavailable("postgres://secret")) - with pytest.raises(MonitoringUnavailable, match="אינם זמינים") as failure: - current_activity(auth, ACTOR, window_minutes=5, checked_at=checked) - assert "postgres" not in str(failure.value) and "secret" not in str(failure.value) - - -def test_current_activity_rejects_bad_windows_scope_and_truncated_samples(): - auth = CurrentActivityAuth() - checked = datetime(2026, 10, 27, 10, 10, tzinfo=timezone.utc) - for window in (0, 10, 30, True, "5"): - with pytest.raises(MonitoringError): - current_activity(auth, ACTOR, window_minutes=window, checked_at=checked) - assert auth.attempt_args is None - for class_id in (3, 999): - with pytest.raises(MonitoringError, match="הכיתה אינה זמינה"): - current_activity(auth, ACTOR, class_id=class_id, checked_at=checked) - auth.truncated = True - with pytest.raises(MonitoringError, match="יותר מדי"): - current_activity(auth, ACTOR, checked_at=checked) - - -@pytest.mark.parametrize("active,expected_status,shown", [ - (0, "complete", 0), (1, "suppressed", None), (2, "suppressed", None), (3, "complete", 3), -]) -def test_current_activity_live_presence_applies_privacy_without_identities(active, expected_status, shown): - auth = CurrentActivityAuth() - checked = datetime(2026, 10, 27, 10, 10, tzinfo=timezone.utc) - live = SimpleNamespace(presence_summary=lambda class_ids: { - "active_session": True, "active_users": active, "heartbeat_ttl_seconds": 20, "ephemeral": True}) - data = current_activity(auth, ACTOR, checked_at=checked, live_store=live) - presence = data["live_presence"] - assert presence["status"] == expected_status and presence["heartbeat_ttl_seconds"] == 20 - assert presence.get("active_users") == shown - assert not any(key in str(presence).lower() for key in ("email", "name", "participant", "user_id")) - - -def test_current_activity_live_presence_is_unavailable_without_active_session_or_store(): - auth = CurrentActivityAuth() - checked = datetime(2026, 10, 27, 10, 10, tzinfo=timezone.utc) - no_store = current_activity(auth, ACTOR, checked_at=checked) - assert no_store["live_presence"]["status"] == "unavailable" - live = SimpleNamespace(presence_summary=lambda class_ids: {"active_session": False}) - no_session = current_activity(auth, ACTOR, checked_at=checked, live_store=live) - assert no_session["live_presence"]["status"] == "unavailable" - - -def test_health_is_whitelisted_role_aware_and_read_only(): - auth = ActivityAuth() - state = SimpleNamespace(auth=auth, scorer=object(), tts=None, - mcp_build_info={"version": "v1", "revision": "abc", "secret": "never"}) - teacher = system_health(SimpleNamespace(state=state), ACTOR) - assert teacher["status"] == "healthy" - assert teacher["components"]["text_to_speech"] == "not_configured" - assert "build" not in teacher - developer = system_health(SimpleNamespace(state=state), {**ACTOR, "roles": ["developer"]}) - assert developer["build"] == {"version": "v1", "revision": "abc"} - assert not any(word in str(developer).lower() for word in ("secret", "http", "token", "trace", "path")) - - -def test_health_maps_database_error_without_leaking_it(): - auth = ActivityAuth() - auth.classes = lambda fresh=False: (_ for _ in ()).throw(AuthUnavailable("postgres://secret")) - data = system_health(SimpleNamespace(state=SimpleNamespace(auth=auth, scorer=object(), tts=object())), ACTOR) - assert data["status"] == "degraded" and data["components"]["database"] == "unavailable" - assert "postgres" not in str(data).lower() and "secret" not in str(data).lower() - - -def attempt(row_id, user, exercise, score, hour, category="spoken", **extra): - return {"id": row_id, "user_id": user, "cat": category, "phrase": exercise, "score": score, - "unrelated": extra.pop("unrelated", False), - "created_at": f"2026-10-25T{hour:02d}:00:00+00:00", **extra} - - -def test_difficulty_metrics_retries_no_success_improvement_and_unrelated_attempts(): - rows = [ - attempt(1, "u1", "קשה", 40, 1), attempt(2, "u1", "קשה", 60, 2, unrelated=True), - attempt(3, "u1", "קשה", 80, 3), attempt(4, "u1", "קשה", 90, 4), - attempt(5, "u2", "קשה", 50, 1), attempt(6, "u2", "קשה", 55, 2), - attempt(7, "u3", "קשה", 75, 1), - ] - got = aggregate_exercise_difficulty(rows, limit=10) - assert len(got) == 1 - item = got[0] - assert item["attempt_count"] == 7 and item["student_count"] == 3 - assert item["success_rate"] == 42.9 - assert item["average_attempts_to_first_success"] == 2.0 # u1 at 3, u3 at 1; u2 excluded - assert item["never_succeeded_students"] == 1 - assert item["average_score_change"] == 27.5 # u1 +50, u2 +5; one-attempt u3 excluded - assert item["successful_attempts"] == 3 and item["difficulty_rank"] == 1 - assert item["average_attempts_per_student"] == 2.33 and item["average_score"] == 64.3 - assert not any(key in item for key in ("user_id", "created_at", "unrelated", "score", "recording", "answer")) - - -def test_difficulty_ignores_malformed_rows_suppresses_small_cohorts_and_is_stable(): - hard = [attempt(1, "u1", "Hard", 10, 1), attempt(2, "u1", "Hard", 20, 2), - attempt(3, "u2", "Hard", 30, 3), attempt(4, "u2", "Hard", 40, 4), - attempt(5, "u3", "Hard", 50, 5)] - easier = [attempt(10, "u1", "Easy", 80, 1), attempt(11, "u1", "Easy", 90, 2), - attempt(12, "u2", "Easy", 80, 3), attempt(13, "u2", "Easy", 90, 4), - attempt(14, "u3", "Easy", 80, 5)] - private = [attempt(20 + at, "only-one", "Private Name", 0, at) for at in range(1, 7)] - malformed = [{"id": 30, "user_id": "u4", "cat": "spoken", "phrase": "Broken", - "score": "nan", "created_at": "bad", "answer": "must-not-leak"}] - got = aggregate_exercise_difficulty([*easier, *private, *malformed, *hard], limit=1) - assert len(got) == 1 and got[0]["exercise"] == "Hard" and got[0]["success_rate"] == 0.0 - assert "Private Name" not in str(got) and "must-not-leak" not in str(got) - assert got == aggregate_exercise_difficulty([*reversed(hard), *reversed(easier)], limit=1) - - -class DifficultyAuth(ActivityAuth): - def __init__(self): - super().__init__() - self.attempt_rows = [] - self.attempt_args = None - self.truncated = False - - def scoped_attempts(self, ids, since, until): - self.attempt_args = ids, since, until - return list(self.attempt_rows), self.truncated - - -def test_exercise_difficulty_scopes_before_reading_and_returns_minimized_result(): - auth = DifficultyAuth() - auth.attempt_rows = [attempt(at, user, "תרגיל", score, at) for at, user, score in ( - (1, "u1", 40), (2, "u1", 80), (3, "u1", 90), (4, "u1", 90), (5, "u1", 90))] - # One learner is deliberately suppressed even with five attempts. - data = exercise_difficulty(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_id=1, today=date(2026, 10, 26)) - ids, since, until = auth.attempt_args - assert ids == {"u1"} and since.isoformat() == "2026-10-24T21:00:00+00:00" - assert until.isoformat() == "2026-10-26T22:00:00+00:00" - assert data["exercises"] == [] and data["returned"] == 0 - assert data["privacy_threshold"] == {"minimum_students": 3, "minimum_attempts": 5} - - -@pytest.mark.parametrize("kwargs", [ - {"from_date": "bad", "to_date": "2026-10-26"}, - {"from_date": "2026-10-25", "to_date": "2026-10-26", "limit": 0}, - {"from_date": "2026-10-25", "to_date": "2026-10-26", "limit": 21}, - {"from_date": "2026-10-25", "to_date": "2026-10-26", "class_id": 3}, - {"from_date": "2026-10-25", "to_date": "2026-10-26", "class_id": 999}, -]) -def test_exercise_difficulty_rejects_invalid_input_before_attempt_read(kwargs): - auth = DifficultyAuth() - with pytest.raises(MonitoringError): - exercise_difficulty(auth, ACTOR, today=date(2026, 10, 26), **kwargs) - assert auth.attempt_args is None - - -def test_exercise_difficulty_refuses_truncation_and_safe_storage_failure(): - auth = DifficultyAuth() - auth.truncated = True - with pytest.raises(MonitoringError, match="צמצמו"): - exercise_difficulty(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - today=date(2026, 10, 26)) - auth.truncated = False - auth.scoped_attempts = lambda *args: (_ for _ in ()).throw(AuthUnavailable("postgres://secret")) - with pytest.raises(MonitoringUnavailable, match="אינם זמינים") as failure: - exercise_difficulty(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - today=date(2026, 10, 26)) - assert "postgres" not in str(failure.value) and "secret" not in str(failure.value) - - -def test_expanded_difficulty_filters_thresholds_status_and_canonical_fields(): - auth = DifficultyAuth() - auth.class_rows[0]["emails"] = ["one@example.com", "two@example.com"] - auth.users = lambda: [{"id": "u1", "email": "one@example.com"}, - {"id": "u2", "email": "two@example.com"}, - {"id": "u3", "email": "three@example.com"}] - auth.class_rows[0]["emails"].append("three@example.com") - auth.attempt_rows = [attempt(i, f"u{(i % 3) + 1}", "x", 40, i, category="spoken") - for i in range(1, 7)] - data = exercise_difficulty(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - exercise_type="other", min_attempts=6, today=date(2026, 10, 26)) - assert data["status"] == "complete" and data["privacy_threshold"]["minimum_attempts"] == 6 - assert data["exercises"][0]["total_attempts"] == data["exercises"][0]["attempt_count"] == 6 - assert data["exercises"][0]["unique_students"] == data["exercises"][0]["student_count"] == 3 - assert data["exercises"][0]["difficulty_rank"] == 1 - empty = exercise_difficulty(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - exercise_type="game", today=date(2026, 10, 26)) - assert empty["status"] == "insufficient_data" and empty["exercises"] == [] - - -def test_normalization_is_stable_jerusalem_aware_and_ignores_bad_scores(): - rows = [attempt(1, "u1", "x", 80, 22), - {**attempt(2, "u2", "x", 50, 1), "score": None}, - {**attempt(3, "u3", "x", 50, 1), "created_at": "bad"}] - got = normalize_attempts(rows) - assert len(got) == 1 and got[0]["local_date"].isoformat() == "2026-10-26" - assert got[0]["exercise_id"] == normalize_attempts(rows)[0]["exercise_id"] - - -class DiagnosticAuth(DifficultyAuth): - diagnostics = True - - def scoped_attempts(self, ids, since, until, *, include_diagnostics=False): - self.attempt_args = ids, since, until, include_diagnostics - if include_diagnostics: - return list(self.attempt_rows), self.truncated, self.diagnostics - return list(self.attempt_rows), self.truncated - - -def test_pronunciation_uses_only_scoped_stored_issues_and_safe_examples(): - auth = DiagnosticAuth() - auth.class_rows[0]["emails"] = ["one@example.com", "two@example.com", "three@example.com"] - auth.users = lambda: [{"id": f"u{i}", "email": email} for i, email in enumerate( - ("one@example.com", "two@example.com", "three@example.com"), 1)] - auth.attempt_rows = [ - attempt(i, f"u{i}", "בית יפה", 60, i, issues=[ - {"word": 1, "expected": "ħ", "heard": "x", "score": 30}], model="m", version="v") - for i in range(1, 4) - ] + [ - attempt(4, "u1", "בית יפה", 70, 4, issues=[ - {"word": 99, "expected": "ħ", "heard": "x", "score": 40}], model="m", version="v"), - attempt(5, "u2", "בית יפה", 80, 5, issues=[ - {"word": 1, "expected": "ħ", "heard": "x", "score": 50}], model="m", version="v"), - ] - data = pronunciation_difficulties(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_id=1, today=date(2026, 10, 26)) - assert data["status"] == "complete" and data["phonemes"][0]["phoneme"] == "ħ" - assert data["phonemes"][0]["example_words"] == ["יפה"] - assert data["phonemes"][0]["pronunciation_error_rate"] is None - assert auth.attempt_args[0] == {"u1", "u2", "u3"} and auth.attempt_args[3] is True - assert not any(key in str(data) for key in ("u1", "example.com")) - - -def test_pronunciation_missing_diagnostics_is_insufficient_not_inferred(): - auth = DiagnosticAuth(); auth.diagnostics = False - data = pronunciation_difficulties(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - today=date(2026, 10, 26)) - assert data["status"] == "insufficient_data" and data["phonemes"] == [] - assert data["diagnostics_available"] is False - - -def analytics_auth(): - auth = DifficultyAuth() - auth.class_rows = [ - {"id": 1, "name": "א", "subject": "ערבית", - "emails": ["one@example.com", "two@example.com", "three@example.com"]}, - {"id": 2, "name": "ב", "subject": "ערבית", - "emails": ["three@example.com", "four@example.com", "five@example.com"]}, - ] - auth.users = lambda: [{"id": f"u{i}", "email": name} for i, name in enumerate(( - "one@example.com", "two@example.com", "three@example.com", - "four@example.com", "five@example.com"), 1)] - return auth - - -def test_learning_progress_buckets_retries_months_nulls_and_filter_refusal(): - auth = analytics_auth() - first = [attempt(i, f"u{((i - 1) % 3) + 1}", "x", 40 if i <= 3 else 80, i) - for i in range(1, 7)] - second = [{**attempt(10 + i, f"u{((i - 1) % 3) + 1}", "x", 80, i), - "created_at": f"2026-10-26T{i:02d}:00:00+00:00"} for i in range(1, 7)] - auth.attempt_rows = first + second - data = learning_progress(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_id=1, interval="day", today=date(2026, 10, 26)) - assert [row["attempts"] for row in data["series"]] == [6, 6] - assert data["series"][0]["first_attempt_success_rate"] == 0.0 - assert data["series"][1]["repeated_attempt_success_rate"] == 100.0 - assert data["improvement_trend"] == 20.0 - monthly = learning_progress(auth, ACTOR, from_date="2026-10-01", to_date="2026-10-26", - class_id=1, interval="month", today=date(2026, 10, 26)) - assert monthly["series"][0]["date"] == "2026-10-01" - with pytest.raises(MonitoringError, match="אינו זמין"): - learning_progress(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_id=1, exercise_id="000000000000", today=date(2026, 10, 26)) - - -def test_progress_suppresses_small_bucket_counts_and_metrics(): - auth = analytics_auth(); auth.attempt_rows = [attempt(1, "u1", "private", 80, 1)] - data = learning_progress(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_id=1, interval="day", today=date(2026, 10, 26)) - assert data["status"] == "insufficient_data" - assert data["series"][0]["attempts"] == data["series"][0]["unique_students"] == 0 - assert data["series"][0]["average_score"] is None - - -def test_class_comparison_normalizes_and_discloses_overlapping_enrollment(): - auth = analytics_auth() - auth.attempt_rows = ([attempt(i, f"u{((i - 1) % 3) + 1}", "x", 80, i) for i in range(1, 7)] + - [attempt(10 + i, f"u{((i - 1) % 3) + 3}", "x", 40, i) for i in range(1, 7)]) - - def scoped(ids, since, until): - return [row for row in auth.attempt_rows if row["user_id"] in ids], False - auth.scoped_attempts = scoped - data = compare_class_difficulties(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_ids=[1, 2], today=date(2026, 10, 26)) - assert data["overlapping_enrollment"] is True and len(data["classes"]) == 2 - assert data["classes"][0]["success_rate"] == 75.0 - assert data["classes"][1]["success_rate"] == 25.0 - with pytest.raises(MonitoringError, match="אינה זמינה"): - compare_class_difficulties(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_ids=[1, 999], today=date(2026, 10, 26)) - - -def test_bottlenecks_separate_observations_recommendations_and_rank_severity(): - auth = analytics_auth() - auth.attempt_rows = [ - attempt(i, f"u{((i - 1) % 3) + 1}", "קשה", 30 + i, i) for i in range(1, 10) - ] + [attempt(20 + i, f"u{((i - 1) % 3) + 1}", "משתפר", 30 if i <= 3 else 90, 10 + i) - for i in range(1, 7)] - data = learning_bottlenecks(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_id=1, min_attempts=5, today=date(2026, 10, 26)) - assert data["status"] == "complete" and data["bottlenecks"][0]["name"] == "קשה" - item = data["bottlenecks"][0] - assert "affected_students" in item["observed_metrics"] - assert item["suggested_intervention"]["type"] == "recommendation" - assert item["suggested_intervention"]["basis"] - assert not any(key in str(data) for key in ("u1", "example.com")) - - -def test_bottlenecks_do_not_claim_no_difficulty_for_small_sample(): - auth = analytics_auth(); auth.attempt_rows = [attempt(1, "u1", "x", 10, 1)] - data = learning_bottlenecks(auth, ACTOR, from_date="2026-10-25", to_date="2026-10-26", - class_id=1, today=date(2026, 10, 26)) - assert data["status"] == "insufficient_data" and data["bottlenecks"] == [] diff --git a/tests/test_nakdan.py b/tests/test_nakdan.py deleted file mode 100644 index 233513e3eca696fcb5182883edc3c7f850e24c0a..0000000000000000000000000000000000000000 --- a/tests/test_nakdan.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Niqqud before TTS: Nakdan's reply is faked, no network.""" -from app import nakdan - - -def reply(*words): - out = [] - for k, (word, options) in enumerate(words): - if k: - out.append({"word": " ", "sep": True, "options": []}) - out.append({"word": word, "sep": False, "options": options}) - return out - - -def setup(monkeypatch, tmp_path, answer): - monkeypatch.setenv("HEBREW_NIQQUD", "1") - monkeypatch.setenv("TTS_CACHE_DIR", str(tmp_path)) - monkeypatch.setattr(nakdan, "_cache", None) - calls = [] - - def fetch(text): - calls.append(text) - if isinstance(answer, Exception): - raise answer - return answer - - monkeypatch.setattr(nakdan, "_fetch", fetch) - return calls - - -def test_points_unpointed_words_and_keeps_hand_pointed_ones(monkeypatch, tmp_path): - calls = setup(monkeypatch, tmp_path, reply(("אני", ["אֲנִי"]), ("בבית", ["בְּ|בַיִת"]), ("אָבִיךָ", ["אָבִיךְ"]))) - assert nakdan.point("אני בבית אָבִיךָ") == ("אֲנִי בְּבַיִת אָבִיךָ", True) - assert nakdan.point("אני בבית אָבִיךָ") == ("אֲנִי בְּבַיִת אָבִיךָ", True) and len(calls) == 1 # cached - monkeypatch.setattr(nakdan, "_cache", None) - assert nakdan.point("אני בבית אָבִיךָ")[0] == "אֲנִי בְּבַיִת אָבִיךָ" and len(calls) == 1 # ... on disk - - -def test_said_to_a_man_or_a_woman(monkeypatch, tmp_path): - setup(monkeypatch, tmp_path, reply(("מה", ["מָה"]), ("שלומך", ["שְׁלוֹמֵךְ", "שְׁלוֹמְךָ"]))) - assert nakdan.point("מה שלומך", "m")[0] == "מָה שְׁלוֹמְךָ" - assert nakdan.point("מה שלומך", "f")[0] == "מָה שְׁלוֹמֵךְ" - assert nakdan.point("מה שלומך")[0] == "מָה שְׁלוֹמֵךְ" - - -def test_unreachable_or_odd_reply_reads_it_as_it_is(monkeypatch, tmp_path): - setup(monkeypatch, tmp_path, OSError("down")) - assert nakdan.point("לא שמעתי") == ("לא שמעתי", False) # not final: tried again next time - setup(monkeypatch, tmp_path, reply(("לא", ["לֹא"]), ("שמעתי", ["שָׁמְעוּ"]))) - assert nakdan.point("לא שמעתי") == ("לֹא שמעתי", True) # a different spelling is never taken - monkeypatch.setenv("HEBREW_NIQQUD", "0") - assert nakdan.point("שלום") == ("שלום", True) diff --git a/tests/test_powsm_onnx.py b/tests/test_powsm_onnx.py deleted file mode 100644 index 30379b0fbc0cc09a2ce8a23aceeddd3fa5bd2d95..0000000000000000000000000000000000000000 --- a/tests/test_powsm_onnx.py +++ /dev/null @@ -1,108 +0,0 @@ -"""The exported POWSM (app/powsm.py PowsmOnnxEmitter): what the Space runs with onnxruntime alone. -The real model is 1.6 GB, so onnxruntime's session is replaced by one that answers with known log-posteriors; -scripts/export_powsm_onnx.py --check compares the real export with ESPnet frame by frame.""" -import json - -import numpy as np -import pytest - -import app.powsm as powsm -from app.models import availability - - -def export(tmp_path, pad=0.0): - folder = tmp_path / "export" - folder.mkdir() - meta = {"kind": "powsm-ctc-onnx", "lang": "", "blank": 0, "vocab": {"": 0, "a": 1, "ħ": 2}, - "pad_seconds": pad, "frames_per_second": 25, "prompt_positions": 2, - "files": {"fp32": "model.onnx", "int8": "model.int8.onnx"}} - (folder / "powsm_onnx.json").write_text(json.dumps(meta), encoding="utf-8") - rng = np.random.default_rng(0) - np.savez(folder / "frontend.npz", window=np.hanning(400).astype(np.float32), - melmat=rng.random((257, 80)).astype(np.float32), mean=np.zeros(80, np.float32), - std=np.ones(80, np.float32), n_fft=512, win_length=400, hop_length=160) - for name in ("model.onnx", "model.int8.onnx"): - (folder / name).write_bytes(b"") - return folder - - -class FakeSession: - """Like the export: 4x fewer frames than feats, log-posteriors over the 3 vocab columns.""" - seen: list = [] - - def __init__(self, path, opts=None, providers=None): - self.path = path - FakeSession.seen.append(path) - - def run(self, outputs, inputs): - feats = inputs["feats"] - assert feats.shape[0] == 1 and feats.shape[2] == 80 and inputs["feats_lens"][0] == feats.shape[1] - frames = feats.shape[1] // 4 - logp = np.log(np.full((1, frames, 3), 1 / 3, dtype=np.float32)) - return [logp] - - -@pytest.fixture -def fake_ort(monkeypatch): - import onnxruntime - monkeypatch.setattr(onnxruntime, "InferenceSession", FakeSession) - monkeypatch.delenv("POWSM_PAD_SECONDS", raising=False) - monkeypatch.delenv("POWSM_ONNX_PRECISION", raising=False) - FakeSession.seen = [] - - -def test_an_unpadded_export_gives_the_scorer_50_frames_a_second(tmp_path, fake_ort): - em = powsm.PowsmOnnxEmitter(str(export(tmp_path, pad=0.0))) - assert em.pad == 0.0 and em.token_to_id["ħ"] == 2 and em.blank_token == "" - logp = em(np.zeros(16000 * 2, dtype=np.float32)) - stft_frames = 1 + 2 * 16000 // 160 # a centred STFT: one frame every 10 ms, plus one - assert logp.shape == ((stft_frames // 4) * 2, 3) # POWSM's 25 frames/s, each repeated once - assert np.allclose(np.exp(logp).sum(-1), 1) - - -def test_padding_keeps_only_the_frames_that_cover_the_speech(tmp_path, fake_ort): - em = powsm.PowsmOnnxEmitter(str(export(tmp_path, pad=8.0))) - one_second = em(np.zeros(16000, dtype=np.float32)) - padded_frames = (1 + 8 * 16000 // 160) // 4 - assert one_second.shape[0] == round(padded_frames / 8) * 2 - - -def test_pad_seconds_from_the_environment_wins(tmp_path, fake_ort, monkeypatch): - monkeypatch.setenv("POWSM_PAD_SECONDS", "4") - assert powsm.PowsmOnnxEmitter(str(export(tmp_path, pad=0.0))).pad == 4.0 - - -def test_fp32_is_the_default_and_int8_is_one_setting_away(tmp_path, fake_ort, monkeypatch): - folder = export(tmp_path) - assert powsm.PowsmOnnxEmitter(str(folder)).weights == "model.onnx" - monkeypatch.setenv("POWSM_ONNX_PRECISION", "int8") - assert powsm.PowsmOnnxEmitter(str(folder)).weights == "model.int8.onnx" - - -def test_model_ids_that_name_an_export(tmp_path, monkeypatch): - folder = export(tmp_path) - assert powsm.onnx_model_dir(str(folder)) == folder - assert powsm.onnx_model_dir("facebook/wav2vec2-xlsr-53-espeak-cv-ft") is None - assert powsm.onnx_model_dir("checkpoints/powsm-ft-shortpad/powsm_ft.pt") is None - fetched = [] - import huggingface_hub - monkeypatch.setattr(huggingface_hub, "snapshot_download", - lambda repo, allow_patterns=None: fetched.append((repo, allow_patterns)) or str(folder)) - assert powsm.onnx_model_dir("powsm-onnx:someone/powsm") == folder - repo, patterns = fetched[0] - assert repo == "someone/powsm" and "model.onnx" in patterns and "model.int8.onnx" not in patterns - - -def test_the_image_build_refuses_a_model_that_needs_espnet(monkeypatch): - from app import prefetch - monkeypatch.setenv("MODEL_ID", "checkpoints/powsm-ft-shortpad/powsm_ft.pt") - with pytest.raises(SystemExit) as e: - prefetch.main() - assert "export_powsm_onnx" in str(e.value) - - -def test_the_models_tab_offers_an_export_when_onnxruntime_is_there(tmp_path): - assert availability(f"{powsm.ONNX_PREFIX}someone/powsm") == (True, "") - assert availability(str(export(tmp_path))) == (True, "") - ok, why = availability("exports/not-there") - assert not ok and "ONNX" in why diff --git a/tests/test_push.py b/tests/test_push.py deleted file mode 100644 index 7173cf2abc4a4d919e72f77fbeb9e3e701135b16..0000000000000000000000000000000000000000 --- a/tests/test_push.py +++ /dev/null @@ -1,159 +0,0 @@ -import base64 -import urllib.parse - -import pytest - -from app import push as pushes -from app.auth import AuthUnavailable - - -class FakeAuth: - secret = "s" - developers = frozenset({"dev@x.com"}) - - def __init__(self, rows=None): - self.rows = list(rows or []) - self.calls = [] - - def _admin(self, path, method="GET", body=None, prefer=""): - self.calls.append((method, path, body)) - if method == "GET": - q = urllib.parse.parse_qs(urllib.parse.urlparse(path).query) - day = q.get("or", [""])[0].rpartition("lt.")[2].rstrip(")") - return [r for r in self.rows if not day or not r.get("last_daily") or r["last_daily"] < day] - if method == "PATCH": - ep = urllib.parse.parse_qs(urllib.parse.urlparse(path).query)["endpoint"][0][3:] - for r in self.rows: - if r["endpoint"] == ep: - r.update(body) - if method == "DELETE": - ep = urllib.parse.parse_qs(urllib.parse.urlparse(path).query)["endpoint"][0][3:] - self.rows = [r for r in self.rows if r["endpoint"] != ep] - - -def test_keys_round_trip(): - key = pushes.new_private_key() - pub = base64.urlsafe_b64decode(pushes.public_key(key) + "==") - assert len(pub) == 65 and pub[0] == 4 - - -def test_off_without_key_or_secret(): - assert not pushes.Push(FakeAuth(), "").on - a = FakeAuth(); a.secret = "" - assert not pushes.Push(a, pushes.new_private_key()).on - assert not pushes.Push(FakeAuth(), "not-a-key").on - assert pushes.Push(FakeAuth(), pushes.new_private_key()).on - - -def test_save_refuses_what_is_not_a_subscription(): - p = pushes.Push(FakeAuth(), pushes.new_private_key()) - with pytest.raises(ValueError): - p.save("a@x.com", {"endpoint": "http://x", "keys": {"p256dh": "k", "auth": "a"}}, False) - with pytest.raises(ValueError): - p.save("a@x.com", {"endpoint": "https://x"}, False) - p.save("A@x.com", {"endpoint": "https://push/1", "keys": {"p256dh": "k", "auth": "a"}}, True) - assert p.auth.calls[-1][2] == {"endpoint": "https://push/1", "email": "a@x.com", "p256dh": "k", "auth": "a", - "every_class": True} - - -def test_missing_table_says_how_to_make_it(): - class NoTable(FakeAuth): - def _admin(self, *a, **k): - raise AuthUnavailable("supabase answered 404: no table") - p = pushes.Push(NoTable(), pushes.new_private_key()) - with pytest.raises(AuthUnavailable, match="create table public.push_subs"): - p.save("a@x.com", {"endpoint": "https://push/1", "keys": {"p256dh": "k", "auth": "a"}}, False) - - -def test_remind_once_a_day_and_only_with_a_video(monkeypatch): - rows = [{"endpoint": "https://p/1", "email": "a@x.com", "p256dh": "k", "auth": "a", "last_daily": None}, - {"endpoint": "https://p/2", "email": "b@x.com", "p256dh": "k", "auth": "a", "last_daily": None}, - {"endpoint": "https://p/3", "email": "c@x.com", "p256dh": "k", "auth": "a", "last_daily": "2026-10-23"}] - p = pushes.Push(FakeAuth(rows), pushes.new_private_key()) - sent = [] - monkeypatch.setattr(p, "send", lambda row, msg: sent.append((row["email"], msg)) or True) - today_of = lambda email, every: [{"title": "שלום"}] if email != "b@x.com" else [] - assert p.remind(today_of, "2026-10-23") == 1 - assert sent == [("a@x.com", {"title": "🎬 הסרטון היומי מחכה לכם", "body": "שלום", "url": "/?view=daily", - "tag": "daily-video"})] - assert p.remind(today_of, "2026-10-23") == 0 # a@x.com was marked for today - assert p.remind(today_of, "2026-10-24") == 2 # a new day: a and c - - -def test_gone_subscription_is_deleted(monkeypatch): - from pywebpush import WebPushException - - class Gone: - status_code = 410 - rows = [{"endpoint": "https://p/1", "email": "a@x.com", "p256dh": "k", "auth": "a"}] - p = pushes.Push(FakeAuth(rows), pushes.new_private_key()) - - def boom(*a, **k): - raise WebPushException("gone", response=Gone()) - monkeypatch.setattr("pywebpush.webpush", boom) - assert p.send(rows[0], {"title": "t"}) is False - assert p.auth.rows == [] - - -def test_message_is_checked_and_tagged_apart(): - m = pushes.message(" אין שיעור מחר ", "נתראה ביום ראשון") - assert m["title"] == "אין שיעור מחר" and m["tag"].startswith("msg-") and m["url"] == "/" - for title, body in (("", "x"), ("x" * 61, ""), ("ok", "x" * 241)): - with pytest.raises(ValueError): - pushes.message(title, body) - - -# the admin's message, through the app -from test_lab import FakeAuth as LabAuth # noqa: E402 - - -class BroadcastAuth(LabAuth): - secret = "sb_secret_x" - - def __init__(self): - super().__init__() - self.events = [] - self.subs = [{"endpoint": f"https://p/{e}", "email": e, "p256dh": "k", "auth": "a"} - for e in ("s@b.c", "s2@b.c", "other@b.c", "t@b.c")] - - def classes(self, fresh=False): - return [{"id": 1, "name": "א1", "emails": ["s@b.c", "s2@b.c"], "teachers": ["t@b.c"], "lessons": []}, - {"id": 2, "name": "ב2", "emails": ["nobody@b.c"], "lessons": []}] - - def _admin(self, path, method="GET", body=None, prefer=""): - return self.subs if method == "GET" else None - - def log_event(self, kind, user, label="", details=None): - self.events.append((kind, label, details)) - - -def test_admin_sends_to_everyone_or_to_a_class(monkeypatch, tmp_path): - from fastapi.testclient import TestClient - from test_scoring import emissions, scorer_for - from app.main import create_app - from app.tts import SawtakTTS - from app.tutor import Tutor - - async def no_loop(*a, **k): - return None - monkeypatch.setenv("VAPID_PRIVATE_KEY", pushes.new_private_key()) - monkeypatch.setattr(pushes, "loop", no_loop) - sent = [] - monkeypatch.setattr(pushes.Push, "send", lambda self, row, msg: sent.append((row["email"], msg["title"])) or True) - auth = BroadcastAuth() - teacher, student = {"Authorization": "Bearer teacher"}, {"Authorization": "Bearer student"} - with TestClient(create_app(scorer_for(emissions(["ħ"])), SawtakTTS("k", cache_dir=tmp_path), auth=auth, - tutor=Tutor(object()))) as c: - aud = c.get("/api/push/audience", headers=teacher).json() - assert {k: aud[k] for k in ("all", "classes", "outside")} == {"all": 4, "classes": {"א1": 3, "ב2": 0}, "outside": 1} - assert [(p["email"], p["devices"], p["classes"]) for p in aud["people"]] == [ - ("other@b.c", 1, []), ("s2@b.c", 1, ["א1"]), ("s@b.c", 1, ["א1"]), ("t@b.c", 1, ["א1"])] - r = c.post("/api/push/broadcast", headers=teacher, json={"title": "שלום", "body": "", "class": "א1"}) - assert r.json() == {"devices": 3, "sent": 3} - assert sorted(e for e, _ in sent) == ["s2@b.c", "s@b.c", "t@b.c"] - sent.clear() - assert c.post("/api/push/broadcast", headers=teacher, json={"title": "לכולם"}).json() == {"devices": 4, "sent": 4} - assert c.post("/api/push/broadcast", headers=teacher, json={"title": "x", "class": "אין"}).status_code == 404 - assert c.post("/api/push/broadcast", headers=teacher, json={"title": ""}).status_code == 400 - assert c.post("/api/push/broadcast", headers=student, json={"title": "x"}).status_code == 403 - assert [e[0] for e in auth.events] == ["push", "push"] diff --git a/tests/test_rules.py b/tests/test_rules.py deleted file mode 100644 index af8ba16310149223c73fd89d9b086ff119d859a6..0000000000000000000000000000000000000000 --- a/tests/test_rules.py +++ /dev/null @@ -1,51 +0,0 @@ -"""The yaml blocks of pronunciation-rules.md, as the scorer reads them.""" -import re - -import yaml - -from app.rules import RULES_PATH, Check, check_detectable, load, parse, undetectable - - -def test_every_block_parses_into_rules(): - r = load() - assert {"Q", "K", "TH", "DH", "ZH", "J"} <= set(r.archiphonemes) - assert r.dialects == ["urban", "rural"] and len(r.checks) >= 30 - assert r.check("ħ", "χ").severity == 3 # written χ, stored by its base() name x - assert r.margin(["ZH", "ðˤ"], "z") == 0.7 # ZH has no margin of its own: the written phoneme's applies - - -def test_every_heard_symbol_has_a_hebrew_label(): - text = RULES_PATH.read_text(encoding="utf-8") - heard = {str(h) for block in re.findall(r"```yaml\n(.*?)```", text, re.S) - for doc in re.split(r"^---$", block, flags=re.M) if doc.strip() - for c in (yaml.safe_load(doc) or {}).get("checks", []) for h in c["heard"]} - assert heard - set(load().heard_labels) == set() - - -def test_correct_realisations_per_letter(): - r = load() - assert r.members("Q") == ["ʔ", "k", "q"] # Bedouin g is off by default - assert r.members("TH") == ["t", "θ"] and "s" in r.members("TH", written="s") # s only in learned words - assert r.dialects_of("Q", "k") == {"rural"} and r.dialects_of("K", "k") == {"urban", "rural"} - - -def test_undetectable_rules_are_reported(): - r = load() - everything = lambda p: True - assert undetectable(Check("x", "ħ", "ħ", 1, ""), r, everything) == "same_sound" - assert undetectable(Check("x", "Q", "k", 1, ""), r, everything) == "accepted" - assert undetectable(Check("x", "ħ", "ǀ", 1, ""), r, lambda p: p != "ǀ") == "heard_not_in_model" - report = check_detectable(r, everything) - assert report["pharyngeal_h"]["detectable"] - # base() drops ː, but a long vowel said short is measured by its duration (3.8) - assert report["vowel_length"]["detectable"] and not report["vowel_length"]["missing"] - bad = parse("```yaml\nid: bad\ntitle: bad\nchecks:\n - {target: aː, heard: [a], severity: 1, feedback: x}\n```\n") - assert not check_detectable(bad, everything)["bad"]["detectable"] - - -def test_baseline_protected_pairs_come_from_the_scoring_block(): - r = load() - assert r.protected("ħ", "x") and r.protected(["ħ"], "χ") # χ is stored by its base() name x - assert not r.protected("ħ", "h") and not r.protected("ħ", None) - bare = parse("```yaml\nscoring:\n margin_default: 0.0\n```\n") - assert bare.baseline_protected == set() and not bare.protected("ħ", "x") diff --git a/tests/test_scoring.py b/tests/test_scoring.py deleted file mode 100644 index de3e9d92ef5b7e2b84bafeb06015e368e09c1ec4..0000000000000000000000000000000000000000 --- a/tests/test_scoring.py +++ /dev/null @@ -1,2907 +0,0 @@ -"""Tests with synthetic CTC emissions: no model download needed.""" -import io -import re -import wave -from dataclasses import replace - -import numpy as np -import pytest - -from app.align import ctc_viterbi, greedy_decode -from app.phrases import PHRASES -from app.scorer import Scorer, Vocab, tokenize - -SYMS = ["", "w", "eː", "n", "i", "l", "ħ", "a", "m", "aː", "χ", "h", "k", "ʔ", "d", "ʃ", "s", "ʕ", "q", "e"] -VOCAB = {s: i for i, s in enumerate(SYMS)} - - -def emissions(phones, frames=3, gap=2, peak=0.9, long_by=2): - """Log-posteriors where each phone peaks for `frames` frames (a long vowel `long_by` times as many), separated - by blanks.""" - V = len(SYMS) - rows = [] - - def row(sym, p): - r = np.full(V, (1 - p) / (V - 1)) - r[VOCAB[sym]] = p - return r - - for _ in range(gap): - rows.append(row("", 0.95)) - for ph in phones: - for _ in range(frames * long_by if "ː" in ph else frames): # a long vowel is held longer (3.8) - rows.append(row(ph, peak)) - for _ in range(gap): - rows.append(row("", 0.95)) - return np.log(np.array(rows)) - - -def find(he): - return next(p for p in PHRASES if p["he"] == he) - - -def scorer_for(logp): - return Scorer(lambda wav: logp, Vocab(VOCAB)) - - -TOILET = find("איפה השירותים?") # وين الحمام w eː n | i l ħ a m m aː m -TIME = find("מה השעה?") # قديش الساعة - - -def test_viterbi_spans_cover_every_token(): - toks = [VOCAB[s] for s in ["w", "eː", "n"]] - logp = emissions(["w", "eː", "n"]) - al = ctc_viterbi(logp, toks, 0) - assert al is not None - assert all(0 <= a < b for a, b in al.spans) - assert [a for a, _ in al.spans] == sorted(a for a, _ in al.spans) - assert greedy_decode(logp, 0) == toks - - -def test_too_short_returns_none(): - logp = emissions(["w"], frames=1, gap=0) - assert ctc_viterbi(logp, [VOCAB["w"], VOCAB["eː"], VOCAB["n"]], 0) is None - - -def test_geminates_collapse(): - toks = tokenize("i l ħ a m m aː m", Vocab(VOCAB)) - assert [t.phoneme for t in toks] == ["i", "l", "ħ", "a", "m", "aː", "m"] - - -def test_correct_pronunciation_scores_high(): - logp = emissions(["w", "eː", "n", "i", "l", "ħ", "a", "m", "aː", "m"]) - r = scorer_for(logp).score(None, TOILET) - assert r["score"] >= 90 - assert r["tips"] == [] - assert not r["unrelated"] - - -def test_teacher_diagnostics_include_compact_phoneme_heatmap(): - logp = emissions(["w", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"]) - scorer = scorer_for(logp) - assert "phoneme_heatmap" not in scorer.score(None, TOILET) - heatmap = scorer.score_logp(logp, TOILET, diagnostics=True, duration_s=2.5)["phoneme_heatmap"] - assert heatmap["duration_ms"] == 2500 - assert heatmap["frame_bins"] == len(logp) - assert {"ħ", "χ", "blank"} <= {row["phoneme"] for row in heatmap["rows"]} - assert all(len(row["values"]) == heatmap["frame_bins"] for row in heatmap["rows"]) - assert [p["phoneme"] for p in heatmap["tracks"]["expected"]] == [ - "w", "eː", "n", "i", "l", "ħ", "a", "m", "aː", "m"] - assert [p["phoneme"] for p in heatmap["tracks"]["heard"]] == [ - "w", "e", "n", "i", "l", "χ", "a", "m", "a", "m"] - - -def test_hebrew_chet_flagged_on_hammam(): - # ħ said as the Hebrew uvular χ - logp = emissions(["w", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"]) - r = scorer_for(logp).score(None, TOILET) - word = r["words"][1] - h = next(p for p in word["phonemes"] if p["ipa"] == "ħ") - assert h["flagged"] and h["heard"] == "χ" - assert r["words"][0]["score"] > 90 # وين is fine - assert r["tips"][0]["key"] == "ħ>x" - assert r["tips"][0]["word"] == "الحمّام" - assert 50 <= r["score"] < 85 # one wrong consonant: "almost", not "excellent" - - -def test_applied_phrase_calibration_raises_only_the_matching_confusion(): - logp = emissions(["w", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"]) - scorer = scorer_for(logp) - key = TOILET.get("key", TOILET["he"]) - scorer.set_calibrations([{"expected": "ħ", "heard": "χ", "floor": 70, - "scope": "phrase", "phrase": key}]) - r = scorer.score(None, TOILET) - h = next(p for p in r["words"][1]["phonemes"] if p["ipa"] == "ħ") - assert h["score"] == 70 and not h["flagged"] - assert r["calibrated"] == [{"expected": "ħ", "heard": "χ", "from": 2, "to": 70}] - - -def test_deleted_ayin_gets_conservative_builtin_calibration(): - phrase = {"variants": [("مع", "m a ʕ")]} - scorer = scorer_for(emissions(["m", "a"])) - r = scorer.score(None, phrase, diagnostics=True) - ayin = next(p for p in r["words"][0]["phonemes"] if p["ipa"] == "ʕ") - trace = next(p for p in r["trace"]["tokens"] if p["ipa"] == "ʕ") - calibration = next(s for s in trace["stages"] if s["stage"] == "calibration") - assert ayin["score"] == 60 and not ayin["flagged"] - assert calibration == {"stage": "calibration", "score": 60, "floor": 60, "applied": True} - applied = r["calibrated"][0] - assert applied["expected"] == "ʕ" and applied["heard"] == "_" - assert applied["from"] < 50 and applied["to"] == 60 - scorer.set_calibrations([{"expected": "ʕ", "heard": "_", "floor": 75, "scope": "global"}]) - overridden = scorer.score(None, phrase) - assert next(p for p in overridden["words"][0]["phonemes"] if p["ipa"] == "ʕ")["score"] == 75 - - -def test_h_instead_of_pharyngeal(): - logp = emissions(["w", "eː", "n", "i", "l", "h", "a", "m", "aː", "m"]) - r = scorer_for(logp).score(None, TOILET) - assert r["tips"][0]["key"] == "ħ>h" - - -def test_k_for_qaf_is_rural_and_correct(): - # learner says "kaddeesh" for قديش: rural Palestinian, so ق is accepted as k; the answer stays the urban one - logp = emissions(["k", "a", "d", "eː", "ʃ", "i", "s", "aː", "ʕ", "a"]) - r = scorer_for(logp).score(None, TIME) - q = r["words"][0]["phonemes"][0] - assert r["variant"]["ipa"].startswith("ʔ") and q["sound"] == "Q" and q["realised"] == "k" - assert q["score"] >= 90 and not q["flagged"] and not r["tips"] and r["score"] >= 90 - assert r["words"][0]["heard_tr"].startswith("כּ") # written as said, not coloured as a mistake - - -# ---------- dialects and Hebrew-speaker rules (docs/scoring-dialects-plan.md) ---------- - -def custom_scorer(extra, phones, frames=3, gap=2, peak=0.9, long_by=2): - """A scorer over SYMS plus `extra`, hearing `phones` cleanly (a long vowel held `long_by` times as long).""" - syms = SYMS + [s for s in extra if s not in SYMS] - vocab = {s: i for i, s in enumerate(syms)} - - def row(sym, p): - r = np.full(len(syms), (1 - p) / (len(syms) - 1)) - r[vocab[sym]] = p - return r - - rows = [row("", 0.95)] * gap - for ph in phones: - rows += [row(ph, peak)] * (frames * long_by if "ː" in ph else frames) + [row("", 0.95)] * gap - return Scorer(lambda wav: np.log(np.array(rows)), Vocab(vocab)) - - -COFFEE = {"variants": [("قهوة", "ʔ a h w e")]} - - -def test_q_for_qaf_is_accepted(): - r = scorer_for(emissions(["q", "a", "d", "eː", "ʃ", "i", "s", "aː", "ʕ", "a"])).score(None, TIME) - assert r["words"][0]["phonemes"][0]["realised"] == "q" and not r["tips"] and r["score"] >= 90 - - -def test_dropped_qaf_between_vowels_is_not_an_error(): - r = scorer_for(emissions(["a", "h", "w", "e"])).score(None, COFFEE) - assert r["score"] >= 90 and not r["tips"] and not any(i["missing"] for i in r["issues"]) - - -def test_hamza_is_not_a_qaf(): - # اهلا is written with ء: kahla is not a dialect form, it is a mistake - phrase = {"variants": [("اهلا", "ʔ a h l a")]} - r = scorer_for(emissions(["k", "a", "h", "l", "a"])).score(None, phrase) - p = r["words"][0]["phonemes"][0] - assert "sound" not in p and p["flagged"] and p["heard"] == "k" - assert [t["key"] for t in r["tips"]] == ["ʔ>k"] - - -def test_reference_is_matched_by_letter_not_by_realisation(): - ref = scorer_for(emissions(["q", "a", "h", "w", "e"])).reference(None, COFFEE) # the voice says ق as q - assert ref.sounds[0] == (0, "Q") - r = scorer_for(emissions(["ʔ", "a", "h", "w", "e"])).score(None, COFFEE, [ref]) - assert r["score"] >= 95 and not r["tips"] and r["references"] == 1 - - -def test_mixed_dialect_is_noted_but_not_marked_down(): - phrase = {"variants": [("قديش قهوة", "ʔ a d d eː ʃ | ʔ a h w e")]} - mixed = scorer_for(emissions(["ʔ", "a", "d", "eː", "ʃ", "k", "a", "h", "w", "e"])).score(None, phrase) - assert mixed["mixed_dialect"] and mixed["score"] >= 90 and not mixed["tips"] - rural = scorer_for(emissions(["k", "a", "d", "eː", "ʃ", "k", "a", "h", "w", "e"])).score(None, phrase) - assert rural["dialect"] == "rural" and "mixed_dialect" not in rural - - -def test_rural_kaf_and_tha(): - phrase = {"variants": [("كثير", "k t iː r")]} - r = custom_scorer(["tʃ", "θ", "r", "iː", "t"], ["tʃ", "θ", "iː", "r"]).score(None, phrase) - assert [p.get("realised") for p in r["words"][0]["phonemes"][:2]] == ["tʃ", "θ"] - assert r["score"] >= 90 and not r["tips"] and r["dialect"] == "rural" - r = custom_scorer(["tʃ", "θ", "r", "iː", "t"], ["tʃ", "t", "iː", "r"]).score(None, phrase) - assert r["mixed_dialect"] and r["score"] >= 90 # rural ك with urban ث - - -def test_final_ta_marbuta_vowel_may_be_a_e_or_i(): - for vowel in ("a", "e", "i"): - r = scorer_for(emissions(["ʔ", "a", "h", "w", vowel])).score(None, COFFEE) - assert r["words"][0]["phonemes"][-1]["score"] >= 95, vowel - - -def test_diphthong_for_long_vowel(): - # بيت said as bayt (rural / older speech) for beet - phrase = {"variants": [("بيت", "b eː t")]} - r = custom_scorer(["b", "t", "j"], ["b", "a", "j", "t"]).score(None, phrase) - assert r["score"] >= 90 and not r["tips"] and r["variant"]["ipa"] == "b a j t" - - -def test_archify_places_each_letter(): - from app.archi import archify, unresolved - assert archify("قديش الساعة", "ʔ a d d eː ʃ | i s s aː ʕ a") == "Q:ʔ a d d eː ʃ | i s s aː ʕ E:a" - assert archify("انا من القدس", "ʔ a n a | m i n | i l q u d s") == "ʔ a n a | m i n | i l Q:q u d s" - assert archify("شكرا كثير", "ʃ u k r a n | k t iː r") == "ʃ u K:k r a n | K:k TH:t iː r" - assert archify("قهوة", "Q:k a h w e") == "Q:k a h w e" # written by hand: left alone - assert unresolved("بعثت", "b a ʕ a t t") == [("بعثت", "ث")] # ث and ت said as one geminate - - -def test_every_qaf_in_the_bank_is_placed(): - from app.archi import unresolved - bad = {(w, ipa) for p in PHRASES for ar, ipa in p["variants"] for w, letter in unresolved(ar, ipa) if letter == "ق"} - assert not bad, f"mark these ق by hand in the phrase (Q:ʔ): {sorted(bad)[:10]}" - - -def test_emphatic_heard_plain_is_a_mistake_with_the_rules_feedback(): - from app.rules import load - phrase = {"variants": [("ظهر", "ðˤ u h r")]} - r = custom_scorer(["ðˤ", "dˤ", "zˤ", "z", "u", "r"], ["z", "u", "h", "r"]).score(None, phrase) - assert [t["key"] for t in r["tips"]] == ["ZH>z"] - assert r["tips"][0]["text"] == load().check("ZH", "z").feedback and r["tips"][0]["severity"] == 2 - - -def margin_scorer(competitor_ratio): - """طين where tˤ is weak (p 0.12) and plain t is `competitor_ratio` times likelier in the same frames (from p 0.2 up: - enough evidence to be named, so the margin alone decides).""" - syms = SYMS + ["tˤ", "t"] - vocab = {s: i for i, s in enumerate(syms)} - V = len(syms) - - def row(probs): - r = np.full(V, 1e-4) - for sym, p in probs.items(): - r[vocab[sym]] = p - return r / r.sum() - - rows = [row({"": 0.95})] * 2 - rows += [row({"tˤ": 0.12, "t": 0.12 * competitor_ratio, "": 0.55})] * 3 + [row({"": 0.95})] * 2 - for ph in ("i", "n"): - rows += [row({ph: 0.9})] * 3 + [row({"": 0.95})] * 2 - return Scorer(lambda wav: np.log(np.array(rows)), Vocab(vocab)) - - -def test_emphatic_needs_a_clear_win_before_it_is_flagged(): - phrase = {"variants": [("طين", "tˤ i n")]} - under = margin_scorer(1.8).score(None, phrase, diagnostics=True) # plain t under twice as likely: the model's doubt - p = under["words"][0]["phonemes"][0] - assert p["score"] < 60 and not p["flagged"] and not under["tips"] - assert under["trace"]["tokens"][0]["margin"]["beyond"] is False # the margin itself holds it back - over = margin_scorer(2.5).score(None, phrase, diagnostics=True) - assert over["words"][0]["phonemes"][0]["flagged"] and [t["key"] for t in over["tips"]] == ["tˤ>t"] - assert over["trace"]["tokens"][0]["margin"]["beyond"] is True - - -@pytest.mark.parametrize("ar,ipa,extra,said,key,heard_letter", [ - ("فنجان", "f i n dʒ aː n", ["f", "p", "dʒ"], ["p", "i", "n", "dʒ", "aː", "n"], "f>p", "פּ"), - ("وين", "w eː n", ["v"], ["v", "eː", "n"], "w>v", "ב"), - ("بس", "b a s", ["b", "v"], ["v", "a", "s"], "b>v", "ב"), - ("صباح", "sˤ a b aː ħ", ["sˤ", "ts", "b"], ["ts", "a", "b", "aː", "ħ"], "sˤ>ts", "טס"), - ("مرة", "m a r r a", ["r", "ʁ"], ["m", "a", "ʁ", "a"], "r>ʁ", None), -]) -def test_hebrew_speaker_substitutions(ar, ipa, extra, said, key, heard_letter): - from app.rules import load - r = custom_scorer(extra, said).score(None, {"variants": [(ar, ipa)]}) - tip = next(t for t in r["tips"] if t["key"] == key) - expected, heard = key.split(">") - assert tip["text"] == load().check(expected, heard).feedback - if heard_letter: # a sound Arabic does not have is still written in the heard line - assert heard_letter in r["heard_tr"].replace("\u05b0", "").replace("\u05b7", "").replace("\u05b4", "") - - -def test_a_sound_outside_arabic_and_hebrew_is_not_named(): - # the multilingual model offers a click for ħ: reported as unclear, not as "heard as ǀ" - r = custom_scorer(["ǀ"], ["w", "eː", "n", "i", "l", "ǀ", "a", "m", "aː", "m"]).score(None, TOILET) - h = next(p for p in r["words"][1]["phonemes"] if p["ipa"] == "ħ") - assert h["flagged"] and h["heard"] is None - - -def test_meaning_changing_mistakes_lead_the_tips(): - # w said as v (severity 2) and ħ as Hebrew ח (severity 3): ħ leads even though v scored lower - r = custom_scorer(["v"], ["v", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"]).score(None, TOILET) - assert [t["key"] for t in r["tips"]][:2] == ["ħ>x", "w>v"] - - -def test_unrelated_sentence(): - logp = emissions(["ʃ", "a", "l", "a", "m"]) - r = scorer_for(logp).score(None, TOILET) - assert r["unrelated"] and r["score"] <= 30 - - -SON = {"variants": [("ابننا", "ʔ i b i n n a")]} # הבן שלנו -SON_SYMS = ["b", "ə", "θ", "u"] - - -def test_weighted_edit_distance_costs(): - from app.align import weighted_edit_distance - one = lambda *_: 1.0 - assert weighted_edit_distance(list("kitten"), list("sitting"), lambda x, j: float(x != "sitting"[j]), one, one) == 3 - # an insertion after the last target sound gets the edge cost - edge = lambda x, j: 0.25 if j in (0, 3) else 1.0 - assert weighted_edit_distance(list("abcz"), list("abc"), lambda x, j: float(x != "abc"[j]), one, edge) == 0.25 - - -def test_exempted_differences_do_not_make_a_word_unrelated(): - # the reported attempt: ʔ dropped, i heard as e and as ə, a breath (θ) at the end. All four are forgiven sounds, - # plain edit distance was 4/6 > 0.65 and capped a 97 at 30 - r = custom_scorer(SON_SYMS, ["e", "b", "ə", "n", "a", "θ"]).score(None, SON, diagnostics=True) - u = r["trace"]["unrelated"] - assert u["raw_distance"] == 4 and u["distance"] <= 1 - assert not r["unrelated"] and r["score"] >= 85 - - -def test_trailing_breath_costs_little(): - clean = custom_scorer(SON_SYMS, ["ʔ", "i", "b", "i", "n", "a"]).score(None, SON, diagnostics=True) - noisy = custom_scorer(SON_SYMS, ["ʔ", "i", "b", "i", "n", "a", "θ"]).score(None, SON, diagnostics=True) - assert noisy["trace"]["unrelated"]["distance"] == 0.25 - assert not noisy["unrelated"] and abs(noisy["score"] - clean["score"]) <= 5 - - -@pytest.mark.parametrize("phones", [["ʃ", "a", "l", "a", "m"], ["d", "a", "ʃ", "u", "m", "a"]]) -def test_another_word_with_forgivable_vowels_is_still_unrelated(phones): - # ابننا has three short vowels and a ʔ the scorer forgives: the gate must still look at its consonants - r = custom_scorer(SON_SYMS, phones).score(None, SON) - assert r["unrelated"] and r["score"] <= 30 - - -def test_confident_consonants_veto_the_unrelated_gate(): - # every vowel replaced by ʃ, the consonants clean. A strict gate calls it unrelated unless the alignment veto, - # which sees the consonants peaking where the sentence has them, overrules it - logp = emissions(["w", "ʃ", "n", "ʃ", "l", "ħ", "ʃ", "m", "ʃ", "m"]) - strict = scorer_for(logp) - strict.cfg = replace(strict.cfg, unrelated_ratio=0.1) - r = strict.score(None, TOILET, diagnostics=True) - assert r["trace"]["unrelated"]["veto"] and not r["unrelated"] - strict.cfg = replace(strict.cfg, unrelated_veto_share=1.01) - assert strict.score(None, TOILET)["unrelated"] - - -@pytest.mark.parametrize("p", PHRASES, ids=lambda p: p["he"]) -def test_phrase_bank_word_counts_match(p): - for ar, ipa in p["variants"]: - assert len(ar.split()) == len(ipa.split("|")), (ar, ipa) - - -def test_api_end_to_end_with_stub_model(): - from fastapi.testclient import TestClient - from app.main import create_app - - logp = emissions(["w", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"]) - app = create_app(scorer_for(logp)) - # 1 s of a 220 Hz tone as a WAV upload, so ffmpeg decoding is exercised - t = np.arange(16000) / 16000 - pcm = (0.3 * np.sin(2 * np.pi * 220 * t) * 32767).astype("x" - - -def test_anything_below_100_says_what_was_wrong(): - for phones in (["w", "eː", "n", "i", "l", "ħ", "a", "m", "aː", "m"], # clean - ["w", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"], # wrong consonant - ["w", "e", "n", "i", "l", "ħ", "a", "m", "a", "m"]): # short vowels - r = scorer_for(emissions(phones)).score(None, TOILET) - assert r["score"] == 100 or r["unrelated"] or r["tips"] or r["issues"], phones - for i in r["issues"]: - assert i["word_tr"] and i["sound"] and not i["sound"].isascii() - - -def test_dropped_glottal_stop_is_not_an_error(): - # اهلا said without the initial ʔ, as natives do - phrase = {"variants": [("اهلا", "ʔ a h l a")]} - r = scorer_for(emissions(["a", "h", "l", "a"])).score(None, phrase) - assert r["words"][0]["score"] >= 90 and r["score"] >= 90 - assert not r["tips"] and not any("עצירה" in i["sound"] for i in r["issues"]) - - -@pytest.mark.parametrize("competitor", ["f", "s", "χ"]) -def test_one_frame_onset_spike_does_not_become_a_glottal_substitution(competitor): - """A generic microphone/breath transient must not be reported as a consonant the learner produced.""" - syms = SYMS + ([competitor] if competitor not in SYMS else []) - vocab = {s: i for i, s in enumerate(syms)} - V = len(syms) - - def row(sym, peak): - p = np.full(V, (1 - peak) / (V - 1)) - p[vocab[sym]] = peak - return p - - # No glottal stop: one weak arbitrary-phone spike, then the clean vowel-initial word. - rows = [row("", .95), row(competitor, .23)] - for ph in ["a", "h", "l", "a"]: - rows += [row(ph, .9)] * 3 + [row("", .95)] * 2 - logp = np.log(np.array(rows)) - phrase = {"variants": [("اهلا", "ʔ a h l a")]} - r = Scorer(lambda wav: logp, Vocab(vocab)).score_logp(logp, phrase, diagnostics=True) - onset = r["trace"]["tokens"][0] - assert onset["competitor_frames"] == 1 - assert onset["stages"][1]["applied"] == "glottal_dropped" - assert r["words"][0]["score"] >= 90 and not r["tips"] - - -def test_sustained_initial_consonant_is_still_a_glottal_substitution(): - phrase = {"variants": [("اهلا", "ʔ a h l a")]} - r = scorer_for(emissions(["s", "a", "h", "l", "a"])).score(None, phrase, diagnostics=True) - onset = r["trace"]["tokens"][0] - assert onset["competitor_frames"] >= 2 - assert onset["stages"][1]["applied"] is None - assert r["words"][0]["phonemes"][0]["heard"] == "s" - assert r["tips"][0]["key"] == "ʔ>s" - - -TAWLITNA = {"variants": [("طاولتنا", "tˤ aː w l i t n a")]} - - -def onset_logp(lead, competitor_frames=1, target_peak=.10, competitor="j"): - """طاولتنا whose first consonant (after the `lead` phones) peaks weakly, behind `competitor_frames` frames of a - competitor at 23%: the shape of a breath or microphone onset in front of a word-initial stop.""" - syms = ["", "tˤ", "aː", "w", "l", "i", "t", "n", "a", "j", "o"] - vocab = {s: i for i, s in enumerate(syms)} - - def row(sym, peak): - p = np.full(len(syms), (1 - peak) / (len(syms) - 1)) - p[vocab[sym]] = peak - return p - - rows = [row("", .95)] - for ph in lead: - rows += [row(ph, .9)] * 3 + [row("", .95)] * 2 - rows += [row(competitor, .23)] * competitor_frames + [row("tˤ", target_peak), row("", .95)] - for ph in ["aː", "w", "l", "i", "t", "n", "a"]: - rows += [row(ph, .9)] * (6 if ph == "aː" else 3) + [row("", .95)] * 2 - logp = np.log(np.array(rows)) - return Scorer(lambda wav: logp, Vocab(vocab)), logp - - -def test_one_frame_spike_before_the_first_consonant_is_not_a_substitution(): - """Rule 31: tˤ said but weak at onset, outscored by one j frame (the reviewed طاولتنا recording).""" - scorer, logp = onset_logp([]) - r = scorer.score_logp(logp, TAWLITNA, diagnostics=True) - onset = r["trace"]["tokens"][0] - assert onset["competitor_frames"] == 1 and onset["gop"] < 0 - assert onset["stages"][1]["applied"] == "onset_transient" - assert onset["final"] >= 90 and not onset["flagged"] - assert not r["tips"] and all(i["heard"] is None for i in r["issues"]) # listed as the weakest, j not named - - -def test_two_frame_competitor_before_the_first_consonant_is_still_a_substitution(): - scorer, logp = onset_logp([], competitor_frames=2) - r = scorer.score_logp(logp, TAWLITNA, diagnostics=True) - onset = r["trace"]["tokens"][0] - assert onset["competitor_frames"] >= 2 - assert onset["stages"][1]["applied"] is None - assert onset["flagged"] and r["tips"][0]["key"] == "tˤ>j" - - -def test_one_frame_spike_inside_the_phrase_is_not_an_onset_transient(): - scorer, logp = onset_logp(["a"]) - phrase = {"variants": [("اطاولتنا", "a tˤ aː w l i t n a")]} - r = scorer.score_logp(logp, phrase, diagnostics=True) - tok = next(t for t in r["trace"]["tokens"] if t["ipa"] == "tˤ") - assert tok["i"] > 0 and tok["stages"][1]["applied"] != "onset_transient" - assert tok["flagged"] - - -def test_onset_transient_toggle_switches_the_exemption_off(): - scorer, logp = onset_logp([]) - scorer.disabled = {"onset_transient"} - r = scorer.score_logp(logp, TAWLITNA, diagnostics=True) - assert r["trace"]["tokens"][0]["stages"][1]["applied"] is None - assert r["trace"]["tokens"][0]["flagged"] - - -def test_ana_explicitly_accepts_the_natural_vowell_initial_form(): - from app.grammar import PRONOUNS - - ana = next(p for p in PRONOUNS if p[0] == "ana") - assert ("انا", "a n a") in ana[3] - phrase = {"variants": ana[3]} - r = scorer_for(emissions(["a", "n", "a"])).score(None, phrase) - assert r["variant"]["ipa"] == "a n a" and r["score"] >= 90 - assert not r["tips"] and not r["issues"] - - -def test_tts_endpoint_serves_phrase_audio_and_503_without_key(monkeypatch): - monkeypatch.delenv("SAWTAK_VOICE_ID", raising=False) # a developer's .env may set a playback voice - from fastapi.testclient import TestClient - - from app.main import create_app - - class FakeTTS: - from app.tts import RAEF_VOICE_ID - voice_id, voice_name = RAEF_VOICE_ID, "רַאאִף" - - def wav(self, text, voice=""): - from app.arabic_diacritics import point_arabic - assert text == point_arabic(*PHRASES[0]["variants"][0]) - assert voice == self.voice_id - return b"RIFFfake", self.voice_id - - with TestClient(create_app(scorer_for(emissions(["w"])), FakeTTS())) as c: - r = c.get("/api/tts/0") - assert r.status_code == 200 and r.headers["content-type"] == "audio/wav" and r.content == b"RIFFfake" - assert r.headers["x-tts-provider"] == "sawtakarabi.ai" and r.headers["x-tts-voice-id"] == FakeTTS.voice_id - - from app.tts import playback_voices - voices = c.get("/api/phrases").json()["voices"] - assert [v["name"] for v in voices] == list(playback_voices()) - assert voices == [{"id": FakeTTS.voice_id, "name": "רַאאִף"}] - assert c.get("/api/tts/9999").status_code == 404 - health = c.get("/api/health").json() - assert health["tts"] is True and health["rules"]["pharyngeal_h"]["detectable"] - - -def test_monitoring_can_warm_the_unique_tts_bank(monkeypatch): - from fastapi.testclient import TestClient - - import app.main - from app.main import create_app - from app.tts import RAEF_VOICE_ID - - phrases = [ - {"variants": [("مرحبا", "m a r ħ a b a")]}, - {"variants": [("مرحبا", "m a r ħ a b a")]}, # duplicate text and voice is generated once - {"variants": [("اهلا", "ʔ a h l a")], "voice": "חנאן"}, - ] - monkeypatch.setattr(app.main, "PHRASES", phrases) - monkeypatch.setattr(app.main, "load_auth", lambda: None) - - class FakeTTS: - def __init__(self): self.calls = [] - def cached(self, text, voice): return text.startswith("مَ") - def wav_for_voice(self, text, voice): self.calls.append((text, voice)); return b"RIFF", voice - def cache_stats(self): return {"files": len(self.calls), "bytes": 0, "hits": 0, "misses": 0, "hit_rate": None} - - tts = FakeTTS() - with TestClient(create_app(scorer_for(emissions(["w"])), tts, tutor=object())) as c: - started = c.post("/api/tts-cache/warm", json={"voice_id": RAEF_VOICE_ID}) - assert started.status_code == 200 and started.json()["total"] == 2 - for _ in range(100): - state = c.get("/api/tts-cache").json()["warm"] - if not state["running"]: break - import time; time.sleep(.01) - assert state.pop("started_at") > 0 - assert state == {"running": False, "voice_id": RAEF_VOICE_ID, "total": 2, "done": 2, "generated": 1, - "skipped": 1, "failed": 0, "current": "", "waiting": 0, "error": "", - "bucket": "", "uploaded": 0, "upload_failed": 0, "upload_error": "", "stopped": False, - "phase": "", "to_generate": 1} - assert [voice for _text, voice in tts.calls] == [RAEF_VOICE_ID] # the cached one is not read again - - -def test_warm_sweep_uploads_every_cached_wav_to_the_bucket(monkeypatch, tmp_path): - from fastapi.testclient import TestClient - - import app.main - import app.tts - from app.main import create_app - from app.tts import RAEF_VOICE_ID, BucketMirror, SawtakTTS - - phrases = [{"variants": [("مرحبا", "m a r ħ a b a")]}, {"variants": [("اهلا", "ʔ a h l a")]}] - monkeypatch.setattr(app.main, "PHRASES", phrases) - monkeypatch.setattr(app.main, "load_auth", lambda: None) - - tts = SawtakTTS("key", cache_dir=tmp_path) - monkeypatch.setattr(tts, "_generate", lambda text, voice, *_: b"RIFF" + text.encode()) - (tmp_path / "legacy.wav").write_bytes(b"old") # cached before the per-voice layout - other = tmp_path / "other-voice"; other.mkdir(); (other / "x.wav").write_bytes(b"xx") - - class FakeMirror(BucketMirror): - def __init__(self): - super().__init__("user/bucket", "ihki-tts") - self.uploads = [] - def remote_sizes(self): - return {"ihki-tts/other-voice/x.wav": 2} # already in the bucket: not sent again - def upload(self, files, source=""): - self.uploads += [(src if isinstance(src, bytes) else src.read_bytes(), dest) for src, dest in files] - - tts.mirror = mirror = FakeMirror() - with TestClient(create_app(scorer_for(emissions(["w"])), tts, tutor=object())) as c: - assert c.post("/api/tts-cache/warm", json={"voice_id": RAEF_VOICE_ID}).status_code == 200 - for _ in range(200): - state = c.get("/api/tts-cache").json()["warm"] - if not state["running"]: break - import time; time.sleep(.01) - dests = sorted(dest for _data, dest in mirror.uploads) - keys = sorted(f"ihki-tts/{tts.cache_key(app.main.point_arabic(*ph['variants'][0]), RAEF_VOICE_ID)}" for ph in phrases) - assert dests == sorted(keys + ["ihki-tts/legacy.wav"]) - assert all(dest.startswith(f"ihki-tts/{RAEF_VOICE_ID}/") for dest in keys) - assert state["bucket"] == "user/bucket" and state["uploaded"] == 3 and state["upload_failed"] == 0 - - -def test_spelling_variants_of_a_sound_are_not_errors(): - # the multilingual vocab spells one sound several ways; the model saying t̪ / ɾ / æ for t / r / a is not a mistake - syms = SYMS + ["t", "t̪", "tː", "r", "ɾ", "æ", "b"] - vocab = {s: i for i, s in enumerate(syms)} - V = len(syms) - - def row(sym, p): - r = np.full(V, (1 - p) / (V - 1)) - r[vocab[sym]] = p - return r - - rows = [row("", 0.95)] * 2 - for ph in ["m", "æ", "ɾ", "ħ", "æ", "b", "æ"]: # مرحبا as the model may spell it - rows += [row(ph, 0.9)] * 3 + [row("", 0.95)] * 2 - phrase = {"variants": [("مرحبا", "m a r ħ a b a")]} - r = Scorer(lambda wav: np.log(np.array(rows)), Vocab(vocab)).score(None, phrase) - assert r["score"] >= 95 and not r["unrelated"] and not r["tips"] - import unicodedata - assert unicodedata.normalize("NFC", r["heard_tr"]) == unicodedata.normalize("NFC", "מַרְחַבַּא") - assert r["words"][0]["heard_tr"] == r["heard_tr"] - - -def test_heard_is_reported_per_word_in_hebrew(): - # ħ said as χ: the second word shows the ח' the model heard - r = scorer_for(emissions(["w", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"])).score(None, TOILET) - from app.translit import to_hebrew - assert r["words"][0]["heard_tr"] == r["words"][0]["tr"] - assert [w["heard_tr"] for w in r["words"]] == to_hebrew("w eː n | i l x a m m aː m") - assert r["words"][1]["tr"] != r["words"][1]["heard_tr"] - - -def test_day2_grammar_reaches_every_pronoun_and_negation(): - from app.grammar import PRONOUNS, SENTENCES - # day 2 has no tab on the page, so the bank is read directly, in the page's shape - day2 = [{**p, "id": i, "ar": p["variants"][0][0]} for i, p in enumerate(PHRASES) if p["cat"] == "day2"] - listed = [p for p in day2 if p["listed"]] - assert len(listed) == len(SENTENCES) and not any(p["neg"] for p in listed) - for p in listed: # each taught sentence has its negation and the other seven subjects - same = [q for q in day2 if q["grp"] == p["grp"]] - assert len(same) == 2 * len(PRONOUNS) - neg = next(q for q in same if q["pron"] == p["pron"] and q["neg"]) - assert " לא " in neg["he"] and " ما " in neg["ar"] and " מַא " in neg["tr"] - she = next(p for p in day2 if p["he"] == "היא לא פתחה את הדלת ולא אכלה עם אחמד") - assert she["ar"] == "هيّ ما فتحت الباب وما اكلت مع احمد" - assert PHRASES[she["id"]]["variants"][0][1] == "h i j j e | m a | f a t ħ a t | i l b aː b | u m a | ʔ a k l a t | m a ʕ | ʔ a ħ m a d" - - -def test_hebrew_prompt_audio_uses_requested_engine_and_falls_back(tmp_path, monkeypatch): - from fastapi.testclient import TestClient - - from app import prompt_tts - from app.main import create_app - - said = [] - - def broken(text): - raise OSError("blocked") - - monkeypatch.setenv("TTS_CACHE_DIR", str(tmp_path)) - monkeypatch.setitem(prompt_tts._SYNTH, "edge", lambda text: said.append(("edge", text)) or b"EDGE") - monkeypatch.setitem(prompt_tts._SYNTH, "gtts", broken) - pid = PHRASES.index(find("מה שלומך? (לגבר)")) - with TestClient(create_app(scorer_for(emissions(["w"])))) as c: - r = c.get(f"/api/prompt-audio/{pid}?engine=edge") - assert r.status_code == 200 and r.content == b"EDGE" and r.headers["x-tts-engine"] == "edge" - assert said == [("edge", "מה שלומך?")] # the bracketed note is not read out - r = c.get(f"/api/prompt-audio/{pid}?engine=gtts") # gTTS down: served by the other engine - assert r.status_code == 200 and r.headers["x-tts-engine"] == "edge" and len(said) == 1 # ... from cache - assert c.get(f"/api/prompt-audio/{pid}?engine=other").status_code == 400 - r = c.get("/api/say", params={"text": "ציון 82. כמעט.", "engine": "edge"}) # driving mode's spoken assessment - assert r.status_code == 200 and said[-1] == ("edge", "ציון 82. כמעט.") and not list(tmp_path.rglob("*82*")) - assert len(list((tmp_path / "he").glob("*.mp3"))) == 1 # ... is not cached - assert c.get("/api/say", params={"text": "hello"}).status_code == 400 - monkeypatch.setitem(prompt_tts._SYNTH, "edge", broken) - assert c.get("/api/prompt-audio/0?engine=edge").status_code == 503 - - -def test_tips_come_most_crucial_first(): - # ħ badly wrong (χ), ʕ only swallowed into the vowel... both flagged: the worse sound leads - phrase = {"variants": [("ساعة الحمام", "s aː ʕ a | i l ħ a m aː m")]} - r = scorer_for(emissions(["s", "aː", "ʔ", "a", "i", "l", "χ", "a", "m", "aː", "m"])).score(None, phrase) - assert len(r["tips"]) == 2 - assert [t["score"] for t in r["tips"]] == sorted(t["score"] for t in r["tips"]) - assert all("idx" not in t for t in r["tips"]) - - - -def test_raised_a_is_a_near_vowel(): - # اكلتي said with e for a, as the voices do: not marked down - phrase = {"variants": [("اكلتي", "ʔ a k a l t i")]} - syms = SYMS + ["t"] - vocab = {s: i for i, s in enumerate(syms)} - rows = [np.full(len(syms), 0.05 / (len(syms) - 1)) for _ in range(2)] - for r in rows: - r[0] = 0.95 - for ph in ["ʔ", "e", "k", "e", "l", "t", "i"]: - for p, sym in [(0.9, ph)] * 3 + [(0.95, "")] * 2: - r = np.full(len(syms), (1 - p) / (len(syms) - 1)); r[vocab[sym]] = p - rows.append(r) - r = Scorer(lambda wav: np.log(np.array(rows)), Vocab(vocab)).score(None, phrase) - assert r["score"] >= 90 and not r["tips"] - - -def test_article_vowel_after_a_vowel_may_be_dropped(): - phrase = {"variants": [("على الحمام", "ʕ a l a | i l ħ a m m aː m")]} - r = scorer_for(emissions(["ʕ", "a", "l", "a", "l", "ħ", "a", "m", "aː", "m"])).score(None, phrase) - assert r["score"] >= 95 and not r["tips"] and not any(i["missing"] for i in r["issues"]) - # ... but not after a consonant: وين الحمام without the i is still noted - r = scorer_for(emissions(["w", "eː", "n", "l", "ħ", "a", "m", "aː", "m"])).score(None, TOILET) - assert any(i["missing"] for i in r["issues"]) - - -def test_rule_28_exempts_final_b_heard_as_p_only_before_a_voiceless_onset(): - phrase = {"variants": [("حليب طارة", "ħ a l iː b | tˤ aː r e")]} - extra = ["iː", "b", "p", "tˤ", "r"] - phones = ["ħ", "a", "l", "iː", "p", "tˤ", "aː", "r", "e"] - scorer = custom_scorer(extra, phones) - result = scorer.score(None, phrase, diagnostics=True) - b = next(t for t in result["trace"]["tokens"] if t["ipa"] == "b") - exemption = next(stage for stage in b["stages"] if stage["stage"] == "exemption") - assert b["competitor"] == "p" and exemption == { - "stage": "exemption", "score": 100.0, "applied": "final_b_devoicing" - } - assert result["words"][0]["phonemes"][-1]["score"] == 100 - - scorer.set_disabled({"final_b_devoicing"}) - without_rule = scorer.score(None, phrase) - assert without_rule["words"][0]["phonemes"][-1]["score"] < 50 - - # The same model confusion before a voiced onset is not covered by the narrow contextual rule. - voiced = custom_scorer(extra, ["ħ", "a", "l", "iː", "p", "d", "a", "r"]) - no_exemption = voiced.score(None, {"variants": [("حليب دار", "ħ a l iː b | d a r")]}) - assert no_exemption["words"][0]["phonemes"][-1]["score"] < 50 - - -def test_rule_32_exempts_t_heard_as_d_and_d_heard_as_t(): - phrase = {"variants": [("انتي فتحتي", "i n t i | f a t a ħ t i")]} - scorer = custom_scorer(["d", "f", "t", "ħ"], ["i", "n", "d", "i", "f", "a", "t", "a", "ħ", "t", "i"]) - result = scorer.score(None, phrase, diagnostics=True) - t = next(tok for tok in result["trace"]["tokens"] if tok["ipa"] == "t") - assert t["competitor"] == "d" - assert next(s for s in t["stages"] if s["stage"] == "exemption")["applied"] == "t_d_voicing" - assert result["words"][0]["phonemes"][2]["score"] == 100 and not result["tips"] - - scorer.set_disabled({"t_d_voicing"}) - assert scorer.score(None, phrase)["words"][0]["phonemes"][2]["score"] < 50 - - # d heard as t is covered too - dar = custom_scorer(["d", "t", "aː", "r"], ["t", "aː", "r"]).score(None, {"variants": [("دار", "d aː r")]}) - assert dar["words"][0]["phonemes"][0]["score"] == 100 - - # ... but not an emphatic: ط heard as plain d is still judged - emph = custom_scorer(["d", "tˤ", "aː", "r"], ["d", "aː", "r"]).score(None, {"variants": [("طار", "tˤ aː r")]}) - assert emph["words"][0]["phonemes"][0]["score"] < 50 - - -def test_rule_29_floors_a_deleted_final_short_vowel_before_a_vowel_initial_word(): - phrase = {"variants": [("على إيدك", "ʕ a l a | ʔ iː d e k")]} - scorer = custom_scorer(["iː"], ["ʕ", "a", "l", "ʔ", "iː", "d", "e", "k"]) - result = scorer.score(None, phrase, diagnostics=True) - final_a = [t for t in result["trace"]["tokens"] if t["word"] == 0][-1] - exemption = next(stage for stage in final_a["stages"] if stage["stage"] == "exemption") - assert final_a["ipa"] == "a" and final_a["deleted"] - assert exemption == {"stage": "exemption", "score": 90.0, "applied": "word_final_vowel_linking"} - - scorer.set_disabled({"word_final_vowel_linking"}) - without_rule = scorer.score(None, phrase, diagnostics=True) - final_a = [t for t in without_rule["trace"]["tokens"] if t["word"] == 0][-1] - exemption = next(stage for stage in final_a["stages"] if stage["stage"] == "exemption") - assert exemption == {"stage": "exemption", "score": 90.0, "applied": "unmarked_short_vowel"} - - # A deleted final vowel before a consonant is not a hiatus and remains penalised. - consonant = custom_scorer(["r"], ["ʕ", "a", "l", "d", "a", "r"]) - no_exemption = consonant.score(None, {"variants": [("على دار", "ʕ a l a | d a r")]}, diagnostics=True) - final_a = [t for t in no_exemption["trace"]["tokens"] if t["word"] == 0][-1] - assert next(s for s in final_a["stages"] if s["stage"] == "exemption")["applied"] != "word_final_vowel_linking" - - -def test_rule_30_floors_an_unmarked_short_vowel_but_not_checked_internal_u(): - # The conjunction's word-initial u is not a learner check; a native u mislabelled v cannot collapse the word. - scorer = custom_scorer(["u", "v"], ["a", "v", "ʕ"]) - result = scorer.score(None, {"variants": [("ا و", "a | u ʕ")]}, diagnostics=True) - u = next(t for t in result["trace"]["tokens"] if t["ipa"] == "u") - exemption = next(stage for stage in u["stages"] if stage["stage"] == "exemption") - assert u["competitor"] == "v" - assert exemption == {"stage": "exemption", "score": 90.0, "applied": "unmarked_short_vowel"} - assert result["words"][1]["phonemes"][0]["score"] == 90 - - # u after a consonant in the same word remains checked by the kubutz rule (3.11). - checked = custom_scorer(["u", "v"], ["k", "v", "l"]) - result = checked.score(None, {"variants": [("كل", "k u l")]}, diagnostics=True) - u = next(t for t in result["trace"]["tokens"] if t["ipa"] == "u") - exemption = next(stage for stage in u["stages"] if stage["stage"] == "exemption") - assert exemption["applied"] is None and exemption["score"] < 50 - - -ALA = {"variants": [("على الحمام", "ʕ a l a | i l ħ a m m aː m")]} -MISHEARD = ["ʔ", "a", "l", "a", "i", "l", "ħ", "a", "m", "aː", "m"] # the model's ʕ in على: a glottal stop - - -def test_native_reference_forgives_what_the_model_mishears(): - s = scorer_for(emissions(MISHEARD)) - plain = s.score(None, ALA) - assert [t["key"] for t in plain["tips"]] == ["ʕ>ʔ"] and plain["words"][0]["heard_tr"] != plain["words"][0]["tr"] - ref = s.reference(None, ALA) # the native voice is heard the same way - r = s.score(None, ALA, [ref]) - assert r["score"] == 100 and not r["tips"] and not r["issues"] and r["references"] == 1 - assert r["words"][0]["heard_tr"] == r["words"][0]["tr"] - - -def test_native_reference_keeps_real_errors(): - ref = scorer_for(emissions(MISHEARD)).reference(None, ALA) - # the learner also says χ for ħ, which the voice did not - r = scorer_for(emissions(["ʔ", "a", "l", "a", "i", "l", "χ", "a", "m", "aː", "m"])).score(None, ALA, [ref]) - assert [t["key"] for t in r["tips"]] == ["ħ>x"] and r["score"] < 85 - # a reference in which the model does not recognise the phrase is not a baseline - assert scorer_for(emissions(["ʃ", "e", "k", "e", "d"])).reference(None, ALA) is None - assert scorer_for(emissions(["ʕ", "χ", "l", "χ", "i", "l", "χ", "a", "χ", "aː", "χ"])).reference(None, ALA) is None - - -def h_as(sym): # على الحمام with its ħ said as `sym` - return ["ʕ", "a", "l", "a", "i", "l", sym, "a", "m", "aː", "m"] - - -def weakened(phones, at, p): - """emissions(phones) with phone `at` held at posterior p and nothing else standing out: kept, but weak.""" - logp = np.exp(emissions(phones)) - start = 2 + sum((3 * 2 if "ː" in ph else 3) + 2 for ph in phones[:at]) - for f in range(start, start + 3): - logp[f] = (1 - p) / (len(SYMS) - 1) - logp[f, VOCAB[phones[at]]] = p - return np.log(logp) - - -def test_reference_that_lost_the_sound_another_way_still_forgives(): - ref = scorer_for(emissions(h_as("χ"))).reference(None, ALA) # the voice's ħ heard as x - r = scorer_for(emissions(h_as("h"))).score(None, ALA, [ref]) # the learner's as h: not a protected pair - assert r["words"][1]["phonemes"][2]["score"] == 100 and not r["tips"] - - -def test_reference_that_kept_the_sound_does_not_forgive(): - ref = scorer_for(emissions(h_as("ħ"))).reference(None, ALA) # the voice says ħ, and it is heard - dropped = [ph for k, ph in enumerate(h_as("ħ")) if k != 6] # the learner leaves ħ out altogether - r = scorer_for(emissions(dropped)).score(None, ALA, [ref], diagnostics=True) - assert "ħ>_" in [t["key"] for t in r["tips"]] and r["words"][1]["phonemes"][2]["score"] < 50 - stage = next(s for s in r["trace"]["tokens"][6]["stages"] if s["stage"] == "baseline") - assert stage["refused"] in ("kept", None) and not stage["applied"] - - -def test_protected_pair_is_not_forgiven_even_when_the_voice_says_it_that_way(): - ref = scorer_for(emissions(h_as("χ"))).reference(None, ALA) # the TTS voice itself says خ for ح - assert ref is not None and ref.heard[6] in ("x", "χ") - r = scorer_for(emissions(h_as("χ"))).score(None, ALA, [ref], diagnostics=True) - assert "ħ>x" in [t["key"] for t in r["tips"]] and r["words"][1]["phonemes"][2]["score"] < 50 - stage = next(s for s in r["trace"]["tokens"][6]["stages"] if s["stage"] == "baseline") - assert stage["refused"] == "protected" and stage["heard"] == ["x"] - - -def test_weak_but_correct_sound_is_still_compensated(): - weak = weakened(h_as("ħ"), 6, 0.12) # ħ faint in the voice and the learner alike - ref = scorer_for(weak).reference(None, ALA) - assert ref.heard[6] is None and ref.scores[6] < 100 - plain = scorer_for(weak).score(None, ALA)["words"][1]["phonemes"][2]["score"] - r = scorer_for(weak).score(None, ALA, [ref]) - assert plain < 100 and r["words"][1]["phonemes"][2]["score"] == 100 and not r["tips"] - - -def onset_scorer(p_target, p_comp): - """احنا whose opening ʔ is faint (p_target) with b at p_comp in the same frames, under the blank.""" - syms = SYMS + ["b"] - vocab = {s: i for i, s in enumerate(syms)} - - def row(probs): - r = np.full(len(syms), 1e-4) - for sym, p in probs.items(): - r[vocab[sym]] = p - return r / r.sum() - - rows = [row({"": 0.95})] * 2 - rows += [row({"ʔ": p_target, "b": p_comp, "": 1 - p_target - p_comp})] * 3 + [row({"": 0.95})] * 2 - for ph in ("i", "ħ", "n", "a"): - rows += [row({ph: 0.9})] * 3 + [row({"": 0.95})] * 2 - return Scorer(lambda wav: np.log(np.array(rows)), Vocab(vocab)) - - -ONSET = {"variants": [("احنا", "ʔ i ħ n a")]} - - -def test_no_substitution_tip_without_evidence(): - r = onset_scorer(0.06, 0.08).score(None, ONSET, diagnostics=True) # b barely likelier, never winning a frame - tok = r["trace"]["tokens"][0] - assert tok["competitor_frames"] == 0 and tok["p_competitor"] < 0.3 - assert not r["tips"] and not r["words"][0]["phonemes"][0]["flagged"] - letter = r["words"][0]["letters"][0] - assert letter["score"] == tok["final"] < 75 and "כמו" not in letter.get("why", "") and "tip" not in letter - - -def test_substitution_tip_with_evidence(): - r = onset_scorer(0.05, 0.6).score(None, ONSET) # b wins the frames - assert r["words"][0]["phonemes"][0]["flagged"] and r["tips"] - assert "נשמעה כמו ב" in r["words"][0]["letters"][0]["why"] - - -def test_reference_records_what_took_each_sounds_place(): - ref = scorer_for(emissions(MISHEARD)).reference(None, ALA) # the voice's ʕ heard as a glottal stop - assert ref.heard[0] == "ʔ" - assert all(h is None for h in ref.heard[1:]) - - -def test_baseline_voices_are_raef_then_fadwa_by_name(): - from app.tts import BASELINE_VOICES, FADWA_VOICE_ID, RAEF_VOICE_ID, voice_name - assert list(BASELINE_VOICES.values()) == [RAEF_VOICE_ID, FADWA_VOICE_ID] # Raef first: the one generated - assert voice_name(RAEF_VOICE_ID) == "ראאף" and voice_name(FADWA_VOICE_ID) == "פדוא" and voice_name("x") == "x" - - -def test_trace_keeps_every_voices_score_and_measures_against_the_lowest(): - from app.scorer import Reference - s = scorer_for(emissions(MISHEARD)) - toks = s._variants(ALA)[0][3] - weak = lambda score_on: Reference([(t.word, t.sound) for t in toks], [score_on.get(t.sound, 100.0) for t in toks]) - a, b = replace(weak({"l": 40.0}), voice="A"), replace(weak({"l": 60.0, "ʕ": 70.0}), voice="B") - r = s.score_logp(s.emit(None), ALA, [a, b], diagnostics=True) - assert [x["voice"] for x in r["trace"]["references"]] == ["A", "B"] - for t in r["trace"]["tokens"]: - st = next(x for x in t["stages"] if x["stage"] == "baseline") - assert len(st["refs"]) == 2 and st["ref"] == min(x for x in st["refs"] if x is not None) - if t["sound"] == "ʕ": - assert st["refs"] == [100.0, 70.0] - - -def test_references_use_one_generated_voice_and_any_cached_one(): - from app.reference import References - from app.tts import TTSUnavailable - - calls = [] - - class FakeTTS: - down = False - - def cached(self, text, voice): - return voice == "played" - - def wav_for_voice(self, text, voice, source="", upload=False): # keeps the voice id (wav() would not) - calls.append(voice) - if self.down: - raise TTSUnavailable("down") - return b"wav", voice - - import app.reference as reference - tts, s = FakeTTS(), scorer_for(emissions(MISHEARD)) - orig, reference.decode_audio = reference.decode_audio, lambda data: None - try: - refs = References(s, tts, ["first", "never-played", "played"]) - assert len(refs.get(7, ALA)) == 2 and calls == ["first", "played"] - assert [r.voice for r in refs.get(7, ALA)] == ["first", "played"] # named for the trace dashboard - assert len(refs.get(7, ALA)) == 2 and len(calls) == 2 # kept in memory - assert refs.invalidate(7, "first") == 1 - assert len(refs.get(7, ALA)) == 2 and calls == ["first", "played", "first"] - tts.down = True - assert refs.get(8, ALA) == [] and refs.get(8, ALA) == [] and len(calls) == 5 # failures are not retried at once - finally: - reference.decode_audio = orig - - -WORD_TOPICS = lambda c: c.get("learn") or c.get("match") # a lesson's new words and their game (test_workbook_new_words) - - -def test_workbook_lesson_1(): - from app.phrases import CATEGORIES, GAMES, LESSONS, public_phrases - - assert [l["id"] for l in LESSONS if not l.get("soon")] == ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9"] - assert [c["id"] for c in CATEGORIES if c["lesson"] == "1"] == ["l1-new", "l1-pairs", "l1-pron","l1-verbs", "l1-past", "l1-say", "l1-read", "l1-hw"] - assert GAMES == [{"id": "game-yalla-verb", "name": "יאללה הפועל", "lesson": "1", "bubble": True, "hidden": True, - "unit": "פועל", "task": "אמרו את הפועל בערבית מדוברת: בעבר ואז בעתיד, לפני שהבועה נעלמת"}] - hw = next(c for c in CATEGORIES if c["id"] == "l1-hw")["write"] # the workbook's homework: a task and five sentences to write, with nothing to say - assert hw["lines"] == 5 and hw["task"].startswith("כתבו חמישה משפטים") and "מִין - מי" in hw["task"] - pub = public_phrases() - count = lambda cat: sum(p["cat"] == cat for p in pub) - assert [count(c) for c in ("l1-pron", "l1-verbs", "game-yalla-verb", "l1-past", "l1-say", "l1-read")] == [8, 4, 4, 32, 10, 22] - assert all(p["cat"] in {c["id"] for c in CATEGORIES + GAMES} for p in pub) - assert all(p["read"] == (p["cat"] == "l1-read") and "#" not in p["ar"] for p in pub if p["cat"].startswith("l1-")) - assert all("seq" not in p for p in pub if p["cat"] == "l1-past") # conjugations are drilled in random order - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l1-pron", "אנחנו"]["tr"] == "אִחְנַא" - assert by["l1-verbs", "ישב (עבר, עתיד)"]["tr"] == "קַעַד, יֻקְעֹד" - assert by["game-yalla-verb", "ישב (עבר, עתיד)"]["tr"] == "קַעַד, יֻקְעֹד" - assert by["l1-past", "היא שאלה"]["tr"] == "הִיֵّ סַאְלַת" - # the prompt voice gets pointed Hebrew (פתח is otherwise read "petach"); the page shows it plain - assert by["l1-verbs", "פתח (עבר, עתיד)"]["he_say"] == "פָּתַח" - assert by["l1-past", "את ישבת"]["he_say"] == "אַתְּ יָשַׁבְתְּ" and by["l1-pron", "הם / הן"]["he_say"] == "הֵם" - assert by["l1-past", "אתה פתחת"]["variants"] == [("انت فتحت", "ʔ i n t e | f a t a ħ t"), ("انت فتحت", "ʔ i n t a | f a t a ħ t")] - p = next(p for p in PHRASES if p["cat"] == "l1-read" and p["seq"] == 11) - assert p["tr"] == "אֵלְמְעַלֵّם קַעַד עַלַא אֵ(ל)טַّאוְלֵה וּסַאַל-נַא סֻאַאל." - ar, ipa = p["variants"][0] - assert ar.split()[-2] == "وسألنا" and ipa.split(" | ")[-2] == "u s a ʔ a l n a" - assert any(i.startswith("i l m ʕ a l l i m | q a ʕ a d") for _, i in p["variants"]) # rural ق accepted - - -def test_workbook_new_words(): - from app.phrases import CATEGORIES, LESSONS, public_phrases - - # every lesson opens with its new words, learned before anything is said, then a game on them; lesson 7, a review, - # brings too few new words for either - for l in (l["id"] for l in LESSONS if not l.get("soon")): - mine = [c for c in CATEGORIES if c["lesson"] == l] - if l == "7": - assert not any(WORD_TOPICS(c) for c in mine) - continue - words, pairs = mine[:2] - assert (words["id"], words["name"], words["learn"]) == (f"l{l}-new", "מילים חדשות", True) - assert (pairs["id"], pairs["name"], pairs["match"]) == (f"l{l}-pairs", "התאימו זוגות", f"l{l}-new") # it deals the words' cards - assert not any(WORD_TOPICS(c) for c in mine[2:]) - cards = [p for p in public_phrases() if p["cat"].endswith("-new")] - assert all(p["listed"] is False and not p["read"] for p in cards) # no tally, daily round or teacher figure counts them - assert not any(p["cat"].endswith("-pairs") for p in PHRASES) - count = lambda l: sum(p["cat"] == f"l{l}-new" for p in cards) - assert [count(l) for l in ("1", "2", "6", "2.8")] == [19, 20, 10, 10] and min(map(count, ["3", "4", "5", "8", "9", "10"])) >= 13 - # added at the bank's end: the ids of the phrases before them, which recorded attempts name, do not move - ids = [i for i, p in enumerate(PHRASES) if p["cat"].endswith("-new")] - assert ids == list(range(len(PHRASES) - len(ids), len(PHRASES))) - by = {(p["cat"], p["he"]): p for p in cards} - assert (by["l1-new", "דלת"]["ar"], by["l1-new", "דלת"]["tr"]) == ("باب", "בַּאבּ") and by["l1-new", "מתי"]["tr"] == "אֵימְתַא" - assert by["l5-new", "בבקשה"]["ar"] == "من فضلك" and by["l2.1-new", "מוקדם"]["ar"] == "بكّير" - - -def test_levels_plan_every_lesson(): - from app.phrases import CATEGORIES, LESSONS, LEVELS - from app.workbook import LESSONS as WRITTEN, LEVELS as PLAN, lesson_id - - # every lesson names its level, and each level lists all of its planned lessons, the unwritten ones as soon - assert [v["id"] for v in LEVELS] == ["1", "2"] and len(LESSONS) == 19 # level 2 ends at שיעור 18 - assert all(isinstance(key, tuple) and key[0] in PLAN for key in WRITTEN) - # level 2's workbook goes on counting from level 1's: its first lesson is shown as the tenth, and keeps the id "2.1" - assert [(l["level"], l["n"]) for l in LESSONS] == [("1", n) for n in range(1, 11)] + [("2", n) for n in range(10, 19)] - assert [l["name"] for l in LESSONS if l["level"] == "2"][:2] == ["שיעור 10", "שיעור 11"] and LESSONS[0]["name"] == "שיעור 1" - assert [l["id"] for l in LESSONS if l["level"] == "2"][:2] == ["2.1", "2.2"] # level 1 keeps "1", "2": attempts name them - assert {l["id"] for l in LESSONS if not l.get("soon")} == {lesson_id(*k) for k in WRITTEN} == {c["lesson"] for c in CATEGORIES} - assert lesson_id(2, 1) == "2.1" and lesson_id(1, 3) == "3" - - -def test_workbook_lesson_2(): - from app.phrases import CATEGORIES, public_phrases - - cats = ["l2-dual", "l2-family", "l2-poss", "l2-say", "l2-say2", "l2-read", "l2-talk", "l2-meet"] - assert [c["id"] for c in CATEGORIES if c["lesson"] == "2" and not WORD_TOPICS(c)] == cats + ["l2-hw"] # the lesson ends with its written homework - hw = next(c for c in CATEGORIES if c["id"] == "l2-hw")["write"] - assert hw["lines"] == 5 and "פתרו את כל השיעור השני" in hw["task"] and "קַעַד / יֻקְעֹד – ישב" in hw["task"] - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [17, 24, 48, 23, 25, 13, 27, 24] - assert all(p["read"] == (p["cat"] == "l2-read") and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2-")) - assert all("seq" not in p for p in pub if p["cat"] == "l2-poss") # drilled in random order, like the conjugations - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l2-dual", "שולחן, שני שולחנות"]["tr"] == "טַאוְלֵה, טַאוְלְתֵין" # singular and dual said together - grandpa = by["l2-family", "סבא (שתי מילים)"] - grandma = by["l2-family", "סבתא (שתי מילים)"] - assert grandpa["variants"] == [("سيد، جدّ", "s iː d | dʒ i d d"), ("سيد", "s iː d"), ("جدّ", "dʒ i d d")] - assert grandma["variants"] == [("ستّ، جدّة", "s i t t | dʒ i d d e"), ("ستّ", "s i t t"), ("جدّة", "dʒ i d d e")] - # כינויי הקניין: the stem changes before a consonant suffix, and שלך is pointed for the prompt voice - assert by["l2-poss", "השולחן שלי"]["tr"] == "טַאוְלְת-י" and by["l2-poss", "השולחן שלנו"]["tr"] == "טַאוְלֵת-נַא" - you = by["l2-poss", "הבת שלך (לאישה)"] - assert you["tr"] == "בִּנְתֵ-כּ" and you["he_say"] == "הבת שֶׁלָּךְ" and you["variants"] == [("بنتك", "b i n t i k")] - assert by["l2-poss", "הכלב שלכם"]["variants"] == [("كلبكم", "k a l b k o m"), ("كلبكم", "k a l b k u")] - assert ("جدّي", "s iː d i") in by["l2-poss", "הסבא שלי"]["variants"] # the other word for סבא is accepted - # דוד is pointed for the prompt voice, which otherwise reads it "David"; דודה is read correctly as it is - assert by["l2-family", "דוד (מצד האב)"]["he_say"] == "דּוֹד (מצד האב)" and "he_say" not in by["l2-family", "דודה (מצד האב)"] - assert by["l2-say", "איפה כלבכם? הוא אצל דודי (מצד אמא)."]["he_say"] == "איפה כלבכם? הוא אצל דּוֹדִי (מצד אמא)." - assert by["l2-talk", "ה' ישמור אותם!"]["he_say"] == "אלוהים ישמור אותם!" # the voice says אלוהים for ה' - p = next(p for p in PHRASES if p["cat"] == "l2-talk" and p["seq"] == 2) - assert p["tr"] == "וּעַלֵיכֹּם אֵ(ל)סַّלַאםֻ" and p["variants"][0][1] == "u ʕ a l eː k u m | i s s a l aː m u" - p = next(p for p in PHRASES if p["cat"] == "l2-say" and p["seq"] == 22) - assert p["tr"].endswith("אִמّ-י סַאְלַת-וֹ: לֵיש פַתַחְת-וֹ?") - # חברי is pointed for the prompt voice, which otherwise reads it "chaverei" (my friends) - assert by["l2-say", "אני ישבתי עם חברי לפני יומיים ואכלנו פיצה בסלון."]["he_say"].split()[3] == "חֲבֵרִי" - assert "וַחֲבֵרִי" in by["l2-say", "מי ישב בבית השיח'? אני וחברי ישבנו בבית השיח'."]["he_say"] - assert by["l2-dual", "שבוע, שבועיים"]["he_say"] == "שָׁבוּעַ, שבועיים" # not "shvua" - - -def test_workbook_lesson_3(): - from app.phrases import CATEGORIES, public_phrases - - cats = ["l3-verbs", "l3-past", "l3-read", "l3-say", "l3-say2", "l3-nums", "l3-numread", "l3-numsay", "l3-story"] - mine = [c for c in CATEGORIES if c["lesson"] == "3" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l3-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "פתרו את כל השיעור השלישי" in hw["task"] and "נַקַל / יֻנְקֹל – עבר (דירה, עבודה)" in hw["task"] - # the story is laid out like the conversation, but told in the student's own voice: it names none - assert mine[-2]["story"] and "voices" not in mine[-2] and CATEGORIES[[c["id"] for c in CATEGORIES].index("l2-talk")]["voices"] == ["אמיר", "ראאף"] - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [4, 32, 26, 12, 14, 37, 11, 11, 10] - assert all(p["read"] == (p["cat"] in ("l3-read", "l3-numread")) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l3-")) - assert all("seq" not in p for p in pub if p["cat"] == "l3-past") - by = {(p["cat"], p["he"]): p for p in PHRASES} - # a verb is said in the past and the future together; עבר alone would read as "past", so it names what is moved - assert by["l3-verbs", "בישל (עבר, עתיד)"]["tr"] == "טַבַּח', יֻטְבֹּח'" and by["l3-verbs", "בישל (עבר, עתיד)"]["he_say"] == "בִּישֵּׁל" - assert by["l3-verbs", "עבר דירה (עבר, עתיד)"]["variants"][:2] == [("نقل، ينقل", "n a ʔ a l | j u n ʔ u l"), ("نقل، ينقل", "n a ʔ a l | j u n q u l")] - assert by["l3-past", "היא שחתה"]["tr"] == "הִיֵّ סַבְּחַת" and by["l3-past", "אנחנו בישלנו"]["tr"] == "אִחְנַא טַבַּחְ'נַא" - # a number shows its digits; the prompt voice says the Hebrew word only - assert by["l3-nums", "שבע עשרה (17)"]["tr"] == "סַבְּעְתַעַש" and by["l3-nums", "שבע עשרה (17)"]["he_say"] == "שבע עשרה" - assert by["l3-nums", "עשרת אלפים (10,000)"]["tr"] == "עַשְרַת אַלַאף" and by["l3-nums", "שלוש מאות (300)"]["tr"] == "תַלַאת מִיֵّה" - p = next(p for p in PHRASES if p["cat"] == "l3-read" and p["seq"] == 9) - assert "הִיֵّ טַבְּחַ'ת-לוֹ מַקְלוּבֵּה טַיְّבֵּה." in p["tr"] - p = next(p for p in PHRASES if p["cat"] == "l3-numsay" and p["seq"] == 6) - assert p["tr"] == "עִנְד-נַא חְדַעְשַר עַםّ וּסַבְּעְתַעְשַר עַמֵّה." # 11-19 gain a ר before a noun, which stays singular - p = next(p for p in PHRASES if p["cat"] == "l3-story" and p["seq"] == 1) - assert p["tr"] == "צַאחִבְּת-י רוּבַּה נַקְלַת עַלַא בֵּית גְ'דִיד פִי חַארֵת-נַא." - - -def test_workbook_lesson_4(): - from app.phrases import CATEGORIES, public_phrases - - cats = ["l4-verbs", "l4-past", "l4-read", "l4-read2", "l4-say", "l4-say2", "l4-story", "l4-meet", "l4-meetread"] - mine = [c for c in CATEGORIES if c["lesson"] == "4" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l4-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "פתרו את כל השיעור הרביעי" in hw["task"] and "אִשְתַרַא / יִשְתְרִי – קנה" in hw["task"] - story = mine[cats.index("l4-story")] - assert story["story"] and "voices" not in story - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [4, 32, 12, 14, 12, 12, 20, 18, 6] - reads = ("l4-read", "l4-read2", "l4-meetread") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l4-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - # verbs that end in ا: the ا becomes ي before the endings of "I", "you" and "we" - assert by["l4-verbs", "בא (עבר, עתיד)"]["tr"] == "אַגַ'א, יִגִ'י" and by["l4-verbs", "נתן (עבר, עתיד)"]["tr"] == "אַעְטַא, יֵעַטִי" - assert by["l4-past", "אנחנו דיברנו"]["tr"] == "אִחְנַא חַכֵּינַא" and by["l4-past", "היא קנתה"]["tr"] == "הִיֵّ אִשְתַרַת" - assert by["l4-past", "הם באו"]["variants"][0] == ("همّ اجو", "h u m m e | ʔ a dʒ u") - p = next(p for p in PHRASES if p["cat"] == "l4-read" and p["seq"] == 4) - assert p["tr"].startswith("גַ'אר-י אַגַ'א וּאַעַטַא-נִי תִסְעַה וּתַמַאנִין שֵיכֵּל") - p = next(p for p in PHRASES if p["cat"] == "l4-story" and p["seq"] == 1) - assert p["tr"] == "קַבֵּל אֻסְבּוּע אַגַ'א עַלַא חַארֵת-נַא גַ'אר גְ'דִיד." - assert by["l4-story", "הוא אמר: \"ה' ישמור אותם!\""]["he_say"] == "הוא אמר: \"אלוהים ישמור אותם!\"" - assert by["l4-meet", "אני רוצה קפה עם סוכר, תודה!"]["tr"] == "בִּדّ-י קַהְוֵה מַע סֻכַּّר, שֻכְּרַן!" - - -def test_workbook_lesson_5(): - from app.phrases import CATEGORIES, public_phrases - - # the workbook's order: more of lesson 4's story, the negation, then the new verbs and the market story - cats = ["l5-meet", "l5-meetread", "l5-neg", "l5-neg2", "l5-verbs", "l5-past", "l5-read", "l5-story", "l5-aread", - "l5-say", "l5-say2", "l5-say3"] - mine = [c for c in CATEGORIES if c["lesson"] == "5" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l5-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "פתרו את כל השיעור החמישי" in hw["task"] and "זַאר / יִזוּר – ביקר" in hw["task"] - story = mine[cats.index("l5-story")] - assert story["story"] and "voices" not in story - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [13, 12, 13, 13, 4, 32, 23, 15, 12, 13, 11, 12] - reads = ("l5-meetread", "l5-neg", "l5-neg2", "l5-read", "l5-aread") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l5-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l5-verbs", "היה (עבר, עתיד)"]["tr"] == "כַּאן, יִכּוּן" and by["l5-past", "אנחנו היינו"]["tr"] == "אִחְנַא כֻּנַّא" - assert ("انا قلت", "ʔ a n a | q u l t") in by["l5-past", "אני אמרתי"]["variants"] # rural ق accepted - # the negation: مش before a noun or an adjective, ما before a verb, which may also end in ش - p = next(p for p in PHRASES if p["cat"] == "l5-neg" and p["seq"] == 9) - assert p["tr"].startswith("אַנַא מַא קַעַדְתֵש עִנְד גַ'אר-י") and p["he"].startswith("אני לא ישבתי") - p = next(p for p in PHRASES if p["cat"] == "l5-neg2" and p["seq"] == 8) - assert p["tr"].endswith("קֻלְתִ-לّוֹ: וַאללַّה מַא אִשְתַרֵינַא אִשִי!") # والله is one word, not "and" + الله - p = next(p for p in PHRASES if p["cat"] == "l5-story" and p["seq"] == 5) - assert p["tr"].startswith("לַמַّא וְצִלְנַא עַלַא אֵ(ל)סّוּק") and p["variants"][0][1].split(" | ")[1] == "w sˤ i l n a" - assert by["l5-story", "ה' ייתן לך בריאות!"]["he_say"] == "אלוהים ייתן לך בריאות!" - - -def test_workbook_lesson_6(): - from app.phrases import CATEGORIES, public_phrases - - cats = ["l6-fut", "l6-futsay", "l6-verbs", "l6-past", "l6-read", "l6-read2", "l6-story", "l6-meet", "l6-meetread"] - mine = [c for c in CATEGORIES if c["lesson"] == "6" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l6-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "פתרו את כל השיעור השישי" in hw["task"] and "פעם אחת בעבר ופעם אחת בעתיד" in hw["task"] - story = mine[cats.index("l6-story")] - assert story["story"] and story["voices"] == ["איוב", "נסרין"] - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [24, 13, 5, 40, 11, 13, 12, 12, 12] - reads = ("l6-read", "l6-read2", "l6-meetread") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l6-")) - assert all("seq" not in p for p in pub if p["cat"] in ("l6-fut", "l6-past")) # conjugations are drilled in random order - by = {(p["cat"], p["he"]): p for p in PHRASES} - # the future: the Hebrew letters אית"ן on the future's base form, and אַ for "I" whatever the base form's vowel - assert by["l6-fut", "אני אשב"]["tr"] == "אַנַא אַקְעֹד" and by["l6-fut", "אנחנו נשב"]["tr"] == "אִחְנַא נֻקְעֹד" - assert by["l6-fut", "את תבקרי"]["tr"] == "אִנְתִי תְזוּרִי" and by["l6-fut", "הם ישמעו"]["he_say"] == "הֵם יִשְׁמְעוּ" - # فات is "entered" like lesson 3's دخل, so its card says more; اخد is the homework's fifth verb - assert by["l6-verbs", "נכנס ועבר (עבר, עתיד)"]["tr"] == "פַאת, יִפוּת" and by["l6-verbs", "לקח (עבר, עתיד)"]["tr"] == "אַחַ'ד, יֹאחֵ'ד" - assert by["l6-past", "אתם נכנסתם"]["tr"] == "אִנְתוּ פֻתّוּ" and by["l6-past", "אני ראיתי"]["tr"] == "אַנַא שֻפֵת" - p = next(p for p in PHRASES if p["cat"] == "l6-futsay" and p["seq"] == 3) - assert p["tr"] == "אִחְנַא סַבַּחְנַא פִי אֵלְבַּחַר, בֻּכְּרַא נִסְבַּח פִי אֵלְבִּרְכֵּה." - # a dialogue: the speaker is written before a turn's first line but neither said nor scored; - # נסרין's turns are in Hanan's voice, איוב's in Raef's - lines = [p for p in PHRASES if p["cat"] == "l6-story"] - assert not any(":" in p["he"] or ":" in p["variants"][0][0] for p in lines) - assert [(p["seq"], p["speaker"]) for p in lines if "speaker" in p] == [(1, "איוב"), (2, "נסרין"), (3, "איוב"), (10, "נסרין")] - assert [p["seq"] for p in lines if p.get("voice") == "חנאן"] == [2, 10, 11, 12] - p = next(p for p in PHRASES if p["cat"] == "l6-story" and p["seq"] == 9) - assert p["tr"] == "לַוְ כַּאן עִנְד-י מַרַה זַיֵّ-כּ..." and p["variants"][0][0].endswith("زيّك...") - - -def test_workbook_lesson_7(): - from app.phrases import CATEGORIES, public_phrases - - # a review lesson: no new verbs and no story - cats = ["l7-fem", "l7-dual", "l7-nums", "l7-count", "l7-verbs", "l7-read", "l7-say", "l7-say2"] - mine = [c for c in CATEGORIES if c["lesson"] == "7" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l7-hw"] and not any(c.get("story") for c in mine) - assert mine[0]["task"] == "אמרו בערבית מדוברת: בזכר ואז בנקבה" and mine[1]["task"].endswith("ביחיד ואז בזוגי") - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "בשיעור השביעי" in hw["task"] and "פעם אחת בעבר ופעם אחת בעתיד" in hw["task"] - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [8, 13, 12, 15, 21, 23, 14, 13] - assert all(p["read"] == (p["cat"] == "l7-read") and "#" not in p["ar"] for p in pub if p["cat"].startswith("l7-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l7-fem", "רווק, רווקה"]["tr"] == "עַזַّאבִּי, עַזַّאבִּיֵّה" and by["l7-dual", "מיטה, שתי מיטות"]["tr"] == "תַחְ'ת, תַחְ'תֵין" - assert by["l7-fem", "רווק, רווקה"]["he_say"].startswith("רַוָּק") - assert by["l7-nums", "מאה שבעים וחמש (175)"]["tr"] == "מִיֵّה חַ'מְסֵה וּסַבְּעִין" # units before tens - # 3-10: the noun in the plural and the number without its ة; from 11: the noun in the singular, and 11-19 with a ר - assert by["l7-count", "שבעה ילדים"]["tr"] == "סַבְּע אַוְלַאד" and by["l7-count", "שתים עשרה בנות"]["tr"] == "תְנַעְשַר בִּנְת" - # every verb of lessons 1-6, each still said in the past and the future together - verbs = [p for p in PHRASES if p["cat"] == "l7-verbs"] - assert [p["tr"] for p in verbs][:2] == ["פַתַח, יִפְתַח", "אַכַּל, יֹאכֹּל"] and verbs[-1]["tr"] == "פַאת, יִפוּת" - assert all(" | " in p["variants"][0][1] and p["he"].endswith("(עבר, עתיד)") for p in verbs) - - -def test_workbook_lesson_8(): - from app.phrases import CATEGORIES, public_phrases - - cats = ["l8-howmany", "l8-verbs", "l8-past", "l8-read", "l8-read2", "l8-read3", "l8-colors", "l8-colread", "l8-story", - "l8-colsay", "l8-colsay2"] - mine = [c for c in CATEGORIES if c["lesson"] == "8" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l8-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "פתרו את כל השיעור השמיני" in hw["task"] and "פעלים וצבעים" in hw["task"] - song = mine[cats.index("l8-story")] - assert song["name"] == "שיר הצבעים" and song["story"] and "voices" not in song - assert mine[cats.index("l8-colors")]["task"].endswith("בזכר ואז בנקבה") - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [13, 5, 40, 12, 12, 13, 17, 13, 25, 10, 12] - reads = ("l8-read", "l8-read2", "l8-read3", "l8-colread") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l8-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - # גזרת פִעֵל: the first vowel is lost before the endings of "I", "you" and "we" - assert by["l8-verbs", "הבין (עבר, עתיד)"]["tr"] == "פִהֵם, יִפְהַם" and by["l8-verbs", "יצא ועלה (עבר, עתיד)"]["tr"] == "טִלֵע, יִטְלַע" - assert by["l8-past", "אני שתיתי"]["tr"] == "אַנַא שְרִבֵּת" and by["l8-past", "היא חזרה"]["tr"] == "הִיֵّ רִגְ'עַת" - # a colour is said in the masculine and the feminine together - assert by["l8-colors", "לבן, לבנה"]["tr"] == "אַבְּיַדֿ, בֵּידַֿא" and by["l8-colors", "לבן, לבנה"]["variants"][0][1] == "ʔ a b j a dˤ | b eː dˤ a" - assert by["l8-colors", "אפור, אפורה (מהמילה אפר)"]["tr"] == "רַמַאדִי, רַמַאדִיֵّה" - # how many: اكم and a singular noun for what is counted, قدّيش for what is not - p = next(p for p in PHRASES if p["cat"] == "l8-howmany" and p["seq"] == 9) - assert p["tr"].startswith("קַדֵّיש מַצַארִי בִּדّ-וֹ?") - p = next(p for p in PHRASES if p["cat"] == "l8-howmany" and p["seq"] == 2) - assert p["tr"].startswith("אַכַּם וַאחַד כַּאן פִי אֵ(ל)סַّיַّארַה?") - assert by["l8-story", "אני בשמיים נמצא"]["tr"] == "אַנַא פִי אֵ(ל)סַّמַאִי מַוְג'וּד" - - -def test_workbook_lesson_9(): - from app.phrases import CATEGORIES, public_phrases - - cats = ["l9-story", "l9-aread", "l9-asay", "l9-verbs", "l9-past", "l9-want", "l9-read", "l9-read2", "l9-say", "l9-mix", - "l9-mixread"] - mine = [c for c in CATEGORIES if c["lesson"] == "9" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l9-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "השיעור התשיעי" in hw["task"] and "אחד בעבר ואחד בעתיד" in hw["task"] - story = mine[cats.index("l9-story")] - assert story["name"] == "סיפור הקפה" and story["story"] and "voices" not in story - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [15, 13, 12, 2, 16, 24, 13, 13, 12, 6, 6] - reads = ("l9-aread", "l9-read", "l9-read2", "l9-mixread") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l9-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l9-verbs", "הודה (עבר, עתיד)"]["tr"] == "שַכַּר, יֻשְכֹּר" - # "to lunch" is one verb in Arabic: the Hebrew prompt names the meal, once - assert by["l9-verbs", "אכל צהריים (עבר, עתיד)"]["tr"] == "תְעַ'דַّא, יִתְעַ'דַّא" - assert by["l9-past", "אנחנו אכלנו צהריים"]["tr"] == "אִחְנַא תְעַ'דֵّינַא" - # wanting: بدّ with the person's suffix, then the verb in the future (Arabic has no infinitive) - assert by["l9-want", "אני רוצה לצאת"]["tr"] == "בִּדّ-י אַטְלַע" and by["l9-want", "את רוצה לצאת"]["tr"] == "בִּדֵّ-כּ תִטְלַעִי" - assert by["l9-want", "הם רוצים לאכול צהריים"]["tr"] == "בִּדّ-הֹם יִתְעַ'דּוּ" - assert by["l9-want", "אתה רוצה לשמוע"]["variants"][0][1] == "b i d d a k | t i s m a ʕ" - p = next(p for p in PHRASES if p["cat"] == "l9-story" and p["seq"] == 8) - assert p["tr"].startswith("שְרִבְּנַא קַהְוֵה סַאדַא") - - -def test_workbook_lesson_10(): - from app.phrases import CATEGORIES, public_phrases - - cats = ["l10-adj", "l10-compread", "l10-compread2", "l10-verbs", "l10-past", "l10-read", "l10-read2", "l10-say", "l10-say2", - "l10-dem", "l10-story", "l10-phrases"] - mine = [c for c in CATEGORIES if c["lesson"] == "10" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l10-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "השיעור העשירי" in hw["task"] and "אחד בעבר ואחד בעתיד" in hw["task"] - story = mine[cats.index("l10-story")] - assert story["name"] == "סיפור דודי סעיד" and story["story"] and "voices" not in story - assert mine[0]["unit"] == "תואר" and mine[0]["task"].endswith("ערך היתרון שלו") - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [33, 8, 8, 5, 40, 12, 9, 12, 9, 12, 25, 19] - reads = ("l10-compread", "l10-compread2", "l10-read", "l10-read2") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l10-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - # an adjective is said with its comparative (אַפְעַל) - assert by["l10-adj", "גדול, יותר גדול"]["tr"] == "כְּבִּיר, אַכְּבַּר" - assert by["l10-adj", "מתוק, יותר מתוק"]["tr"] == "חִלוּ, אַחְלַא" and by["l10-adj", "חשוב, יותר חשוב"]["tr"] == "מֻהִםّ, אַהַםّ" - assert by["l10-adj", "טעים, יותר טעים (לאוכל בלבד)"]["tr"] == "זַאכִּי, אַזְכַּא" - assert by["l10-adj", "נקי, יותר נקי"]["variants"][0][1] == "n dˤ iː f | ʔ a n dˤ a f" - # גזרת פִעֵל again - assert by["l10-verbs", "שיחק (עבר, עתיד)"]["tr"] == "לִעֵבּ, יִלְעַבּ" and by["l10-verbs", "הגיע (עבר, עתיד)"]["tr"] == "וִצֵל, יוֹצַל" - assert by["l10-past", "אני שיחקתי"]["tr"] == "אַנַא לְעִבֵּת" and by["l10-past", "היא צחקה"]["tr"] == "הִיֵّ דִֿחְכַּת" - assert by["l10-past", "אנחנו הגענו"]["tr"] == "אִחְנַא וְצִלְנַא" - assert by["l10-dem", "אלה"]["tr"] == "הַדוֹל" and by["l10-dem", "הפעם הזאת"]["tr"] == "הַאדִי אֵלְמַרַّה" - p = next(p for p in PHRASES if p["cat"] == "l10-story" and p["seq"] == 8) - assert p["tr"].startswith("הוּّ קַאלִّ-י: וַלַא יְהִמֵّ-כּ") - - -def test_workbook_level_2_lesson_1(): - from app.phrases import CATEGORIES, public_phrases - - # the second workbook (שלב ב') opens with the lesson it numbers the tenth: five verbs, and the future after لازم and رح - cats = ["l2.1-verbs", "l2.1-past", "l2.1-must", "l2.1-will", "l2.1-read", "l2.1-read2", "l2.1-say", "l2.1-say2", "l2.1-story"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.1" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.1-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "רַח ולַאזֵם" in hw["task"] and "רַבַּט / יֻרְבֹּט – קשר" in hw["task"] - story = mine[cats.index("l2.1-story")] - assert story["name"] == "סיפור המכולת" and story["story"] and "voices" not in story - assert mine[0]["unit"] == "פועל" and mine[0]["task"].endswith("בעבר ואז בעתיד") - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [5, 40, 24, 24, 12, 11, 12, 11, 29] - reads = ("l2.1-read", "l2.1-read2") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.1-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - # a verb is said in the past and the future together - assert by["l2.1-verbs", "רץ (עבר, עתיד)"]["tr"] == "רַכַּדֿ, יֻרְכֹּדֿ" and by["l2.1-verbs", "שלח (עבר, עתיד)"]["tr"] == "בַּעַת', יִבְּעַת'" - assert by["l2.1-verbs", "שלח (עבר, עתיד)"]["variants"] == [("بعث، يبعث", "b a ʕ a t | j i b ʕ a t")] - assert by["l2.1-past", "אני רצתי"]["tr"] == "אַנַא רַכַּדֵֿת" and by["l2.1-past", "היא שילמה"]["tr"] == "הִיֵّ דַפְעַת" - assert by["l2.1-past", "הם קשרו"]["variants"][0][1] == "h u m m e | r a b a tˤ u" - # لازم and رح stay as they are; the verb after them is in the future - assert by["l2.1-must", "את צריכה לראות"]["tr"] == "אִנְתִי לַאזֵם תְשוּפִי" and by["l2.1-must", "הם צריכים לקשור"]["tr"] == "הֹםّ לַאזֵם יֻרֻבְּטוּ" - assert by["l2.1-will", "אני הולך לרוץ"]["tr"] == "אַנַא רַח אַרְכֹּדֿ" and by["l2.1-will", "אנחנו הולכים לשתות"]["tr"] == "אִחְנַא רַח נִשְרַבּ" - p = next(p for p in PHRASES if p["cat"] == "l2.1-story" and p["seq"] == 10) - assert "אֻרְבֹּט (א)לִחְמַאר מַטְרַח מַא יֵקוּל-לַّכּ צַאחְבּ-וֹ" in p["tr"] and "קְשׁוֹר" in p["he_say"] - - -def test_workbook_level_2_lesson_2(): - from app.phrases import CATEGORIES, public_phrases - from app.workbook import _present_of - - # the workbook's eleventh lesson: the days of the week and the present, which is بـ on the future form - cats = ["l2.2-days", "l2.2-pres", "l2.2-read", "l2.2-read2", "l2.2-read3", "l2.2-say", "l2.2-say2", "l2.2-story", "l2.2-fread"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.2" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.2-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "ואז בהוו'ה" in hw["task"] - story = mine[cats.index("l2.2-story")] - assert story["name"] == "סיפור הפלאפל" and story["story"] and "voices" not in story - assert mine[0]["unit"] == "מילה" - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [10, 24, 8, 8, 8, 8, 8, 19, 11] - reads = ("l2.2-read", "l2.2-read2", "l2.2-read3", "l2.2-fread") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.2-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l2.2-days", "יום שלישי"]["tr"] == "יוֹם אֵ(ל)תַّלַאתַא" and by["l2.2-days", "חודש"]["tr"] == "שַהֵר" - # the prefix takes the vowel the form needs, and "we" may take مـ - assert by["l2.2-pres", "אני שואל"]["tr"] == "אַנַא בַּסְאַל" and by["l2.2-pres", "אתה שואל"]["tr"] == "אִנְתֵ בְּתִסְאַל" - assert by["l2.2-pres", "הוא רואה"]["tr"] == "הוּّ בִּישוּף" and by["l2.2-pres", "אנחנו רואים"]["tr"] == "אִחְנַא בִּנְשוּף" - assert by["l2.2-pres", "הם שותים"]["variants"][0][1] == "h u m m e | b i ʃ r a b u" - assert _present_of("منيجي") == ("מְנִיגִ'י", ["m n iː dʒ i"]) and _present_of("بيشوفني")[0] == "בִּישוּפ-נִי" - assert _present_of("باكل")[0] == "בַּאכֹּל" and _present_of("بيت") is None and _present_of("بدّي") is None - p = next(p for p in PHRASES if p["cat"] == "l2.2-fread" and p["seq"] == 5) - assert p["tr"].endswith("וּבְּנִשְרַבּ שַאיְ מַע בַּעַדֿ.") # "and" on a present form - - -def test_workbook_level_2_lesson_3(): - from app.phrases import CATEGORIES, public_phrases - - # the workbook's twelfth lesson: the clock, and בניין שני (פַעַّל) beside the פִעֵל verbs it is made from - cats = ["l2.3-hours", "l2.3-clock", "l2.3-talk", "l2.3-read", "l2.3-read2", "l2.3-say", "l2.3-say2", "l2.3-verbs", "l2.3-past", - "l2.3-vread", "l2.3-vread2", "l2.3-vsay", "l2.3-vsay2", "l2.3-story"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.3" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.3-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "שַרַّבּ – השקה" in hw["task"] and "עבר – עתיד – הווה" in hw["task"] - talk, song = mine[cats.index("l2.3-talk")], mine[cats.index("l2.3-story")] - assert talk["story"] and talk["voices"] and song["story"] and "voices" not in song # a talk has two speakers, a song none - assert mine[0]["unit"] == "שעה" and mine[cats.index("l2.3-verbs")]["unit"] == "פועל" - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [12, 15, 6, 7, 7, 9, 8, 9, 16, 12, 12, 12, 12, 14] - reads = ("l2.3-read", "l2.3-read2", "l2.3-vread", "l2.3-vread2") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.3-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - # an hour is feminine, and the minutes come after it: a quarter to ten is "ten less a quarter" - assert by["l2.3-hours", "השעה שתיים"]["tr"] == "אֵ(ל)סֵّיעַה תִנְתֵין" and by["l2.3-hours", "השעה אחת עשרה"]["tr"] == "אֵ(ל)סֵّיעַה חְדַעַש" - assert by["l2.3-clock", "השעה רבע לעשר"]["tr"] == "אֵ(ל)סֵّיעַה עַשַרַה אִלַّא רֻבֵּע" - assert by["l2.3-clock", "השעה אחת עשרה שלושים וחמש"]["variants"][0][1] == "i s s eː ʕ a | ħ d a ʕ a ʃ | u n u sˤ sˤ | u x a m s e" - # a verb is said in the past and the future together; لبس beside لبّس in the past - assert by["l2.3-verbs", "הוריד (עבר, עתיד)"]["tr"] == "נַזַّל, יְנַזֵّל" and by["l2.3-verbs", "השקה (עבר, עתיד)"]["tr"] == "שַרַّבּ, יְשַרֵّבּ" - assert by["l2.3-past", "היא לבשה"]["tr"] == "הִיֵّ לִבְּסַת" and by["l2.3-past", "היא הלבישה"]["tr"] == "הִיֵّ לַבַּّסַת" - assert by["l2.3-past", "אנחנו הלבשנו"]["variants"][0][1] == "ʔ i ħ n a | l a b b a s n a" - # the song asks (שַרֵّבּ), where the exercises tell (שַרַّבּ) - first = next(p for p in PHRASES if p["cat"] == "l2.3-story" and p["seq"] == 1) - assert first["tr"] == "שַרֵّבְּ-נִי קַהְוֵה פַתֵّח לִי רַאס-י!" - told = next(p for p in PHRASES if p["cat"] == "l2.3-vread2" and "קפה קר" in p["he"]) - assert "וּשַרַّבְּ-נִי קַהְוֵה בַּארְדֵה" in told["tr"] - - -def test_workbook_level_2_lesson_4(): - from app.phrases import CATEGORIES, public_phrases - - # the workbook's thirteenth lesson: five more of בניין שני, the three words for "that" (انّه, ما, الّي), and a phone call - cats = ["l2.4-verbs", "l2.4-past", "l2.4-vread", "l2.4-vread2", "l2.4-vsay", "l2.4-vsay2", "l2.4-rel", "l2.4-rread", "l2.4-rread2", - "l2.4-rsay", "l2.4-rsay2", "l2.4-words", "l2.4-talk", "l2.4-talk2"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.4" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.4-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "פַכַּّר / יְפַכֵּّר – חשב" in hw["task"] - assert all(mine[cats.index(c)]["story"] and mine[cats.index(c)]["voices"] for c in ("l2.4-talk", "l2.4-talk2")) # two speakers each - assert mine[0]["unit"] == "פועל" - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [5, 16, 13, 13, 13, 13, 14, 7, 7, 7, 7, 12, 14, 11] - reads = ("l2.4-vread", "l2.4-vread2", "l2.4-rread", "l2.4-rread2") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.4-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l2.4-verbs", "שר (עבר, עתיד)"]["tr"] == "עַ'נַّא, יְעַ'נִّי" and by["l2.4-verbs", "חשב (עבר, עתיד)"]["tr"] == "פַכַּّר, יְפַכֵּّר" - # غنّى ends in a vowel: its past takes ـيت, and "she" and "they" drop it - assert by["l2.4-past", "אני שרתי"]["tr"] == "אַנַא עַ'נֵّית" and by["l2.4-past", "היא שרה"]["tr"] == "הִיֵّ עַ'נַّת" - assert by["l2.4-past", "הם חשבו"]["variants"][0][1] == "h u m m e | f a k k a r u" - assert by["l2.4-rel", "הילד שהלך"]["tr"] == "אֵלְוַלַד אִלִّי רַאח" and by["l2.4-rel", "לפני שאצא מהבית שלו"]["tr"] == "קַבֵּל מַא אַטְלַע מִן בֵּית-וֹ" - # the present of a פַעַّל verb: בִּי on "he", בַּ on "I" - son = next(p for p in PHRASES if p["cat"] == "l2.4-rread" and "עברתי לבית חדש" in p["he"]) - assert "אִבְּנ-י בִּיפַכֵּّר יֻנְקֹל" in son["tr"] - call = next(p for p in PHRASES if p["cat"] == "l2.4-talk2" and p["seq"] == 7) - assert call["tr"].startswith("טַבַּעַן בַּחַ'בְּّרַ-כּ") and "אוֹתְךָ" in call["he_say"] - lunch = next(p for p in PHRASES if p["cat"] == "l2.4-rread" and "לבדך" in p["he"]) - assert "כֻּנֵת בַּתְעַ'דַّא מַעַ-כּ" in lunch["tr"] # "I", where بـ on تتغدّى would be "you" - - -def test_workbook_level_2_lesson_5(): - from app.phrases import CATEGORIES, public_phrases - - # the workbook's fourteenth lesson: גזרת ע"י, what صار does, and the garage story - cats = ["l2.5-verbs", "l2.5-past", "l2.5-pres", "l2.5-sar", "l2.5-vread", "l2.5-vread2", "l2.5-vsay", "l2.5-words", "l2.5-story", - "l2.5-ask", "l2.5-sread", "l2.5-ssay", "l2.5-adj"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.5" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.5-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "צַאר / יְצִיר – נהיה" in hw["task"] - story = mine[cats.index("l2.5-story")] - assert story["name"] == "סיפור המוסך" and story["story"] and "voices" not in story - assert mine[0]["unit"] == "פועל" - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [6, 16, 16, 10, 8, 7, 7, 12, 14, 10, 12, 12, 10] - reads = ("l2.5-vread", "l2.5-vread2", "l2.5-ask", "l2.5-sread") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.5-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - # the past's ا is the future's ي, and the past of "I" loses it: גַ'אבּ, יְגִ'יבּ, גִ'בֵּת - assert by["l2.5-verbs", "הביא (עבר, עתיד)"]["tr"] == "גַ'אבּ, יְגִ'יבּ" and by["l2.5-verbs", "חיכה (עבר, עתיד)"]["tr"] == "אִסְתַנַّא, יִסְתַנַّא" - assert by["l2.5-past", "אני הבאתי"]["tr"] == "אַנַא גִ'בֵּת" and by["l2.5-past", "היא נהיתה"]["tr"] == "הִיֵّ צַארַת" - assert by["l2.5-pres", "הוא מביא"]["tr"] == "הוּّ בִּיגִ'יבּ" and by["l2.5-pres", "אנחנו נהיים"]["variants"][0][1] == "ʔ i ħ n a | b i n sˤ iː r" - assert by["l2.5-sar", "אתה כבר חצי שעה פה?"]["tr"] == "צַאר-לַכּ נֻצّ סֵיעַה הוֹן?" - # استنّى is the past of "he"; the present of "I" is made from the future, which is written the same - line = next(p for p in PHRASES if p["cat"] == "l2.5-story" and p["seq"] == 7) - assert "צַאר-לִי נֻצّ סֵיעַה בַּסְתַנַّא-כּ הוֹן!" in line["tr"] and "לְךָ" in line["he_say"] - assert next(p for p in PHRASES if p["cat"] == "l2.5-story" and p["seq"] == 8)["tr"].startswith("טַיֵّבּ וֵינַ-כּ חַבִּיבּ-י?") - assert by["l2.5-adj", "גדול, גדולה, גדולים"]["tr"] == "כְּבִּיר, כְּבִּירֵה, כְּבַּאר" - - -def test_workbook_level_2_lesson_6(): - from app.phrases import CATEGORIES, public_phrases - - # the workbook's fifteenth lesson: בניין שלישי, the old jar's story, and "him" after a vowel - cats = ["l2.6-verbs", "l2.6-past", "l2.6-pres", "l2.6-vread", "l2.6-vread2", "l2.6-vread3", "l2.6-vsay", "l2.6-words", "l2.6-story", - "l2.6-ask", "l2.6-him"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.6" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.6-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "לַאקַא / יְלַאקִי – מצא" in hw["task"] - story = mine[cats.index("l2.6-story")] - assert story["name"] == "סיפור הכד העתיק" and story["story"] and "voices" not in story - assert mine[0]["unit"] == "פועל" - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [5, 16, 16, 8, 8, 7, 7, 14, 20, 10, 12] - reads = ("l2.6-vread", "l2.6-vread2", "l2.6-vread3", "l2.6-ask") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.6-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l2.6-verbs", "עזר (עבר, עתיד)"]["tr"] == "סַאעַד, יְסַאעֵד" and by["l2.6-verbs", "מצא, פגש (עבר, עתיד)"]["tr"] == "לַאקַא, יְלַאקִי" - assert by["l2.6-past", "אני מצאתי"]["tr"] == "אַנַא לַאקֵית" and by["l2.6-past", "היא עזרה"]["tr"] == "הִיֵّ סַאעַדַת" - assert by["l2.6-pres", "הוא עוזר"]["tr"] == "הוּّ בִּיסַאעֵד" and by["l2.6-pres", "אנחנו מוצאים"]["variants"][0][1] == "ʔ i ħ n a | b i n l aː ʔ i" - # after a vowel "him" is a ه that is hardly heard, and "her" is whole - assert by["l2.6-him", "אנחנו פתחנו אותו"]["tr"] == "אִחְנַא פַתַחְנַא-ה" and by["l2.6-him", "אנחנו פתחנו אותה"]["tr"] == "אִחְנַא פַתַחְנַא-הַא" - assert by["l2.6-him", "ה' ישמור אותו"]["variants"][0][1] == "ʔ a l l a | j x a l l iː h" - line = next(p for p in PHRASES if p["cat"] == "l2.6-story" and p["seq"] == 8) - assert "תַעַאל סַאעֵד-נִי וִחְיַאתַ-כּ" in line["tr"] and "בְּחַיֶּיךָ" in line["he_say"] # a request: סַאעֵד, not the past's סַאעַד - - -def test_workbook_level_2_lesson_7(): - from app.phrases import CATEGORIES, public_phrases - - # the workbook's sixteenth lesson: the imperative, more of פִעֵל, and Yusef's bus - cats = ["l2.7-imp", "l2.7-verbs", "l2.7-past", "l2.7-pres", "l2.7-ex", "l2.7-fill", "l2.7-fill2", "l2.7-isay", "l2.7-words", "l2.7-pre", - "l2.7-story", "l2.7-ask", "l2.7-sread", "l2.7-ssay"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.7" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.7-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "ואז בציווי" in hw["task"] and "וִקֵע / יוֹקַע – נפל" in hw["task"] - story = mine[cats.index("l2.7-story")] - assert story["name"] == "סיפור יוסף והאוטובוס" and story["story"] and "voices" not in story - assert mine[cats.index("l2.7-verbs")]["unit"] == "פועל" - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [19, 9, 16, 16, 5, 6, 6, 7, 14, 8, 17, 10, 9, 12] - reads = ("l2.7-fill", "l2.7-fill2", "l2.7-ask", "l2.7-sread") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.7-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - # the future without its first letter; two consonants at the start take a helping ا with the future's vowel - assert by["l2.7-imp", "חשוב!"]["tr"] == "פַכֵּّר!" and by["l2.7-imp", "שתה!"]["tr"] == "אִשְרַבּ!" and by["l2.7-imp", "שב!"]["tr"] == "אֻקְעֹד!" - assert by["l2.7-imp", "קח! קחי! קחו!"]["variants"][0][1] == "x u d | x u d i | x u d u" - assert by["l2.7-imp", "אל תשתה קולה ואל תשתה זירו"]["tr"] == "לַא תִשְרַבּ כּוֹלַה וּמַא תִשְרַבּ זִירוֹ" # "don't" is the future - assert by["l2.7-verbs", "שכח (עבר, עתיד)"]["tr"] == "נִסִי, יִנְסַא" and by["l2.7-verbs", "נפל (עבר, עתיד)"]["tr"] == "וִקֵע, יוֹקַע" - assert by["l2.7-past", "אני שכחתי"]["tr"] == "אַנַא נְסִית" and by["l2.7-past", "היא כעסה"]["tr"] == "הִיֵّ זִעְלַת" - assert by["l2.7-pres", "הוא רץ"]["tr"] == "הוּّ בְּיֻרְכֹּדֿ" and by["l2.7-pres", "הוא כועס"]["tr"] == "הוּّ בִּיזְעַל" - line = next(p for p in PHRASES if p["cat"] == "l2.7-story" and p["seq"] == 12) - assert line["tr"].startswith("יַא שַבַּאבּ, סַאעְדוּ-נִי, וַאללַّה וְקִעֵת") - go = next(p for p in PHRASES if p["cat"] == "l2.7-fill" and "לכי" in p["he"]) - assert "יַאללַّה רוּחִי וּבַּעַדֵין תַעַאלִי" in go["tr"] - - -def test_workbook_level_2_lesson_8(): - from app.phrases import CATEGORIES, public_phrases - - # the workbook's seventeenth lesson: גזרת הכפולים, عم for what is happening now, and the song شاطر شاطر - cats = ["l2.8-verbs", "l2.8-past", "l2.8-pres", "l2.8-vread", "l2.8-vread2", "l2.8-vsay", "l2.8-vsay2", "l2.8-am", "l2.8-amsay", - "l2.8-amsay2", "l2.8-words", "l2.8-story"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.8" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.8-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "דַֿלّ / יְדַֿלّ – נשאר" in hw["task"] and "ורק אז בציווי" in hw["task"] - song = mine[cats.index("l2.8-story")] - assert song["name"] == "השיר: שַאטֵר שַאטֵר" and song["story"] and "voices" not in song - assert mine[0]["unit"] == "פועל" - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [7, 16, 16, 10, 9, 10, 9, 9, 9, 8, 12, 10] - reads = ("l2.8-vread", "l2.8-vread2") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.8-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - assert by["l2.8-verbs", "שם, הניח (עבר, עתיד)"]["tr"] == "חַטّ, יְחֻטّ" and by["l2.8-verbs", "נשאר, המשיך (עבר, עתיד)"]["tr"] == "דַֿלّ, יְדַֿלّ" - # "I" and "you" take ـيت in the past, "she" and "they" only their ending - assert by["l2.8-past", "אני נשארתי"]["tr"] == "אַנַא דַֿלֵّית" and by["l2.8-past", "היא נשארה"]["tr"] == "הִיֵّ דַֿלַّת" - assert by["l2.8-past", "הם שמו"]["variants"][0][1] == "h u m m e | ħ a tˤ tˤ u" - assert by["l2.8-pres", "אתה נשאר"]["tr"] == "אִנְתֵ בִּתְדַֿלّ" and by["l2.8-pres", "אנחנו אוהבים"]["tr"] == "אִחְנַא בִּנְחִבّ" - assert by["l2.8-am", "אני מדבר איתך כעת"]["tr"] == "אַנַא עַםְ בַּחְכִּי מַעַ-כּ" and by["l2.8-am", "אני עובד כעת"]["tr"] == "אַנַא עַםְ בַּשְתְעֵ'ל" - # a request (חֻטّ) beside the past (חַטّ), and the song's "we": מִנְ before the future - order = next(p for p in PHRASES if p["cat"] == "l2.8-vread" and "צ'יפס" in p["he"]) - assert "חֻטّ-לִי כַּמַאן טְחִינֵה" in order["tr"] and "הוּّ חַטّ-לִי זַיّ מַא" in order["tr"] - assert next(p for p in PHRASES if p["cat"] == "l2.8-story" and p["seq"] == 6)["tr"] == "מִנְחִבّ-וֹ וּמִנְדַֿלّ מִנְקוּל-לّוֹ שַאטֵר שַאטֵר" - - -def test_workbook_level_2_lesson_9(): - from app.phrases import CATEGORIES, public_phrases - - # the workbook's eighteenth and last lesson: the active participle, a song from Egypt, and the father, the son and the donkey - cats = ["l2.9-part", "l2.9-part2", "l2.9-verbs", "l2.9-ex", "l2.9-pread", "l2.9-pread2", "l2.9-psay", "l2.9-psay2", "l2.9-psay3", - "l2.9-words", "l2.9-song", "l2.9-story"] - mine = [c for c in CATEGORIES if c["lesson"] == "2.9" and not WORD_TOPICS(c)] - assert [c["id"] for c in mine] == cats + ["l2.9-hw"] - hw = mine[-1]["write"] - assert hw["lines"] == 5 and "בבינוני הפועל" in hw["task"] and "עַאש / יְעִיש – חי" in hw["task"] - for c in ("l2.9-song", "l2.9-story"): - assert mine[cats.index(c)]["story"] and "voices" not in mine[cats.index(c)] - assert mine[cats.index("l2.9-verbs")]["unit"] == "פועל" - pub = public_phrases() - assert [sum(p["cat"] == c for p in pub) for c in cats] == [8, 16, 5, 17, 10, 10, 10, 9, 9, 14, 8, 14] - reads = ("l2.9-pread", "l2.9-pread2") - assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l2.9-")) - by = {(p["cat"], p["he"]): p for p in PHRASES} - # the participle's four forms, and where the middle letter is weak a י comes in (رايح, نايم) - assert by["l2.9-part", "גר, גרה, גרים, גרות"]["tr"] == "סַאכֵּן, סַאכְּנֵה, סַאכְּנִין, סַאכְּנַאת" - assert by["l2.9-part", "ישן, ישנה, ישנים, ישנות"]["tr"] == "נַאיֵם, נַאיְמֵה, נַאיְמִין, נַאיְמַאת" - assert by["l2.9-part2", "ישב – יושב"]["tr"] == "קַעַד, קַאעֵד" and by["l2.9-part2", "אהב, רצה – אוהב, רוצה"]["tr"] == "חַבّ, חַאבֵּבּ" - assert by["l2.9-verbs", "ישן (עבר, עתיד)"]["tr"] == "נַאם, יְנַאם" and by["l2.9-verbs", "חי (עבר, עתיד)"]["tr"] == "עַאש, יְעִיש" - # the habit is the present (בַּרוּח), this moment the participle (רַאיֵח) - assert by["l2.9-ex", "כל יום שני אני הולך לחבר שלי"]["tr"].endswith("אַנַא בַּרוּח לַעִנְד צַאחְבּ-י") - assert by["l2.9-ex", "אני הולך לחבר שלי עכשיו"]["tr"] == "אַנַא רַאיֵח לַעִנְד צַאחְבּ-י הַלַّק" - song = [p for p in PHRASES if p["cat"] == "l2.9-song"] - assert song[2]["tr"].endswith("עַלַא בֻּעְדַ-כּ") and song[3]["tr"].endswith("חַבִּיבּ בַּעְדַ-כּ") # your distance, and after you - assert "מַא בֵּינַא זַמַאן" in song[5]["tr"] - last = next(p for p in PHRASES if p["cat"] == "l2.9-story" and p["seq"] == 14) - assert last["tr"].startswith("אִנְתֵ עַאיֵש וִלַّא מַאיֵת") and "בְּךָ" in last["he_say"] - - -def test_sign_in_guards_the_practice_endpoints(): - from fastapi.testclient import TestClient - - from app.main import create_app - - VERB = next(p for p in PHRASES if p["cat"] == "l1-verbs") - - class FakeAuth: - url, key, teachers, tutors = "https://proj.supabase.co", "sb_publishable_x", frozenset(), frozenset() - - def __init__(self): - self.rows, self.cls, self.vids, self.txts, self.days, self.wrote = [], [], [], [], {"dailies": [], "library": []}, {} - self.rank = frozenset() - - def writings(self, token): - return {t: lines for (u, t), lines in self.wrote.items() if u == "u1"} - - def save_writing(self, token, user_id, topic, lines): - self.wrote[user_id, topic] = lines - - def all_writings(self): - return [{"user_id": u, "topic": t, "lines": lines} for (u, t), lines in self.wrote.items()] - - def classes(self, fresh=False): - return self.cls - - def ranked(self, fresh=False): - return self.rank - - def set_tutor(self, email, on, by): - if self.rank is None: - raise AuthUnavailable("supabase answered 404: no table") - self.rank = self.rank | {email} if on else self.rank - {email} - self.by = by - - def save_class(self, id, name, emails, nicknames, lessons, homeworks=(), dailies=None, subject=""): - row = {"name": name, "emails": emails, "nicknames": nicknames, "lessons": lessons, "homeworks": list(homeworks), - **({} if dailies is None else {"dailies": list(dailies)}), **({"subject": subject} if id is None and subject else {})} - if id is None: - self.cls.insert(0, {"id": len(self.cls) + 1, **row}) - elif c := next((c for c in self.cls if c["id"] == id), None): - c.update(row) - else: - raise KeyError(id) - - def delete_class(self, id): - self.cls = [c for c in self.cls if c["id"] != id] - - def videos(self, fresh=False): - return self.vids - - def level_videos(self, table, fresh=False): - return self.days[table] - - def add_level_video(self, table, level, title, url): - self.days[table].append({"id": len(self.days[table]) + 1, "level": level, "title": title, "url": url}) - - def delete_level_video(self, table, id): - self.days[table] = [v for v in self.days[table] if v["id"] != id] - - def rename_level_video(self, table, id, title): - next(v for v in self.days[table] if v["id"] == id)["title"] = title - - def order_level_videos(self, table, ids): - self.days[table].sort(key=lambda v: ids.index(v["id"])) - - def add_video(self, lesson, title, url, pos, ord): - self.vids.append({"id": len(self.vids) + 1, "lesson": lesson, "title": title, "url": url, "pos": pos, "ord": ord}) - - def place(self, places): - for table, id, pos, ord in places: - next(v for v in {"videos": self.vids, "texts": self.txts}[table] if v["id"] == id).update(pos=pos, ord=ord) - - def texts(self, fresh=False): - return self.txts - - def add_text(self, lesson, title, voices, lines, pos, ord): - self.txts.append({"id": len(self.txts) + 1, "lesson": lesson, "title": title, "voices": voices, "lines": lines, "pos": pos, "ord": ord}) - - def delete_text(self, id): - self.txts = [t for t in self.txts if t["id"] != id] - - def delete_video(self, id): - self.vids = [v for v in self.vids if v["id"] != id] - - def user(self, token): - return {"id": "u1", "email": "A@b.c"} if token == "good" else None - - def record(self, token, row): - self.rows.append((token, row)) - - def attempts(self, token): # two tries of one sentence on one Israeli day (across midnight UTC), one unlisted sentence - return [{"cat": VERB["cat"], "phrase": VERB["he"], "score": 62, "created_at": "2026-10-08T21:30:00.5+00:00"}, - {"cat": VERB["cat"], "phrase": VERB["he"], "score": 88, "created_at": "2026-10-09T10:00:00+00:00"}, - {"cat": TOILET["cat"], "phrase": TOILET["he"], "score": 90, "created_at": "2026-10-01T10:00:00.123456+00:00"}] - - def users(self): # the teacher, a student, and an account that is not on the list - return [{"id": "u1", "email": "A@b.c", "name": "Alef"}, {"id": "u2", "email": "s@b.c", "name": ""}, - {"id": "u3", "email": "out@b.c", "name": ""}] - - def all_attempts(self, limit=200_000): - return [{**r, "user_id": "u2"} for r in self.attempts("")[:2]][:limit] - - def activity(self, hours=24, bucket_minutes=15): - self.activity_args = hours, bucket_minutes - return [{"bucket": "2026-10-13T10:00:00+00:00", "attempts": 4, "active_students": 2}] - - def all_attempt_issues(self): - return [] - - with TestClient(create_app(scorer_for(emissions(["w"])), auth=FakeAuth())) as c: - page = c.get("/").text - assert 'AUTH_URL="https://proj.supabase.co"' in page and 'AUTH_KEY="sb_publishable_x"' in page - assert c.get("/api/phrases").status_code == 200 # the phrase list stays public - assert c.get("/api/prompt-audio/0").status_code == 401 - assert c.get("/api/tts/0", headers={"Authorization": "Bearer bad"}).status_code == 401 - assert c.get("/api/tts/0", headers={"Authorization": "Bearer good"}).status_code == 503 # in, but no TTS key - good = {"Authorization": "Bearer good"} - from app.phrases import CATEGORIES, LESSONS - ALL = [l["id"] for l in LESSONS] - NO_HW = {"homework": "", "due": None, "homeworks": [], "dailies": []} - T1, T2 = (next(k["id"] for k in CATEGORIES if k["lesson"] == l) for l in ALL[:2]) # a topic of each of the first two lessons - # written homework is kept for the student, a topic's lines in place of the ones before, and read back anywhere - assert c.get("/api/writing").status_code == 401 and c.get("/api/writing", headers=good).json() == {"writing": {}} - put = lambda topic, lines: c.put(f"/api/writing/{topic}", json={"lines": lines}, headers=good).status_code - assert put("l1-hw", ["אַנַא אַכַּלֵת ", ""]) == 200 and put("l1-hw", ["אַנַא אַכַּלֵת", "", "הִיֵّ פַתְחַת"]) == 200 - assert c.get("/api/writing", headers=good).json() == {"writing": {"l1-hw": ["אַנַא אַכַּלֵת", "", "הִיֵّ פַתְחַת"]}} - assert put(T1, ["x"]) == 404 and put("l1-hw", ["x"] * 6) == 400 and put("l1-hw", ["x" * 301]) == 400 and put("l1-hw", "x") == 400 - assert c.put("/api/writing/l1-hw", json={"lines": ["x"]}).status_code == 401 - # no class yet: any account is let in, to every lesson - assert c.get("/api/me", headers=good).json() == {"email": "A@b.c", "teacher": False, "classes": [], "lessons": ALL} - assert c.get("/api/class").status_code == 401 and c.get("/api/class", headers=good).status_code == 403 - assert c.post("/api/classes", json={"name": "x", "emails": "", "lessons": ALL}, headers=good).status_code == 403 # teachers only - assert c.get("/api/classmates", headers=good).json() == {"classes": []} - beginners = {"id": 1, "name": "Beginners", "emails": ["other@b.c"], "nicknames": {}, "lessons": ALL[:1]} - c.app.state.auth.cls = [beginners] # a class without this account - assert c.get("/api/me", headers=good).status_code == 403 and c.get("/api/tts/0", headers=good).status_code == 403 - beginners["emails"].append("a@b.c") # case does not matter; the student studies their class's lessons - assert c.get("/api/me", headers=good).json() == {"email": "A@b.c", "teacher": False, "lessons": ALL[:1], - "classes": [{"name": "Beginners", "lessons": ALL[:1], **NO_HW}]} - # the class's homework reaches its students: a topic of its lessons, and the day it is due - beginners.update(homework=T1, due="2026-10-15") - assert c.get("/api/me", headers=good).json()["classes"] == [{"name": "Beginners", "lessons": ALL[:1], "homework": T1, "due": "2026-10-15", - "homeworks": [{"topic": T1, "due": "2026-10-15"}], "dailies": []}] - beginners.update(homework=T2) # a topic of a lesson the class does not study is no homework - assert c.get("/api/me", headers=good).json()["classes"] == [{"name": "Beginners", "lessons": ALL[:1], **NO_HW}] - del beginners["homework"], beginners["due"] - # in a second class too: the lessons of both, in the course's order - c.app.state.auth.cls = [{"id": 2, "name": "Advanced", "emails": ["a@b.c"], "nicknames": {}, "lessons": ALL[1:]}, beginners] - me = c.get("/api/me", headers=good).json() # each class with its own lessons: the student chooses one to practise in - assert me["lessons"] == ALL and me["classes"] == [{"name": "Advanced", "lessons": ALL[1:], **NO_HW}, {"name": "Beginners", "lessons": ALL[:1], **NO_HW}] - c.app.state.auth.cls = [{**beginners, "lessons": []}] # a class saved before classes had lessons has every one - assert c.get("/api/me", headers=good).json()["lessons"] == ALL - # a teacher is let in without a class, studies every lesson, and opens classes in the panel - c.app.state.auth.cls, c.app.state.auth.teachers = [], frozenset({"a@b.c"}) - assert c.post("/api/classes", json={"name": "September 2026", "emails": "x@b.c", "lessons": ALL[:1]}, - headers=good).status_code == 200 - assert c.get("/api/me", headers=good).json() == {"email": "A@b.c", "teacher": True, "tutor": True, "classes": [], "lessons": ALL, "dailies": []} - activity = c.get("/api/activity?hours=12&bucket_minutes=30", headers=good) - assert activity.json() == {"hours": 12, "bucket_minutes": 30, "buckets": [ - {"bucket": "2026-10-13T10:00:00+00:00", "attempts": 4, "active_students": 2}]} - assert c.app.state.auth.activity_args == (12, 30) - assert c.get("/api/activity?hours=0", headers=good).status_code == 422 - # the content part: a teacher adds a video to a ready lesson by its Vimeo link, kept without the sharing parameters - READY = [l["id"] for l in LESSONS if not l.get("soon")] - SOON = next(l["id"] for l in LESSONS if l.get("soon")) - link = "https://vimeo.com/1233611297/d46ac373c8?share=copy&fl=sv&fe=ci" - assert c.get("/api/videos").status_code == 401 and c.get("/api/videos", headers=good).json() == {"videos": []} - for body, code in (({"lesson": READY[0], "title": " פתיחה לשיעור ", "url": link}, 200), - ({"lesson": READY[1], "title": "public", "url": "vimeo.com/76979871"}, 200), - ({"lesson": READY[0], "title": "", "url": link}, 400), ({"lesson": READY[0], "title": "x" * 81, "url": link}, 400), - ({"lesson": SOON, "title": "x", "url": link}, 400), ({"lesson": "no-such-lesson", "title": "x", "url": link}, 400), - ({"lesson": READY[0], "title": "x", "url": "https://youtube.com/watch?v=1233611297"}, 400), - ({"lesson": READY[0], "title": "x", "url": "https://www.youtube.com/playlist?list=PLdQw4w9WgXcQ"}, 400), - ({"lesson": READY[0], "title": "x", "url": "https://evil.example/watch?v=dQw4w9WgXcQ"}, 400), - ({"lesson": READY[0], "title": "x", "url": "https://evil.example/?u=https://vimeo.com/1233611297"}, 400), - ({"lesson": READY[0], "title": "x", "url": "https://vimeo.com/channels/staffpicks"}, 400)): - r = c.post("/api/videos", json=body, headers=good) - assert r.status_code == code, (body, r.text) - first, second = c.get("/api/videos", headers=good).json()["videos"] - N1 = sum(k["lesson"] == READY[0] for k in CATEGORIES) # a new video stands after its lesson's topics - assert first == {"id": 1, "lesson": READY[0], "title": "פתיחה לשיעור", "url": "https://vimeo.com/1233611297/d46ac373c8", - "embed": "https://player.vimeo.com/video/1233611297?h=d46ac373c8&dnt=1&vimeo_logo=0&title=0&byline=0&portrait=0", "pos": N1, "ord": 1} - assert (second["url"], second["embed"]) == ("https://vimeo.com/76979871", "https://player.vimeo.com/video/76979871?dnt=1&vimeo_logo=0&title=0&byline=0&portrait=0") - from app.main import vimeo_link, video_embed, video_link - assert vimeo_link("https://player.vimeo.com/video/1233611297?h=d46ac373c8") == first["url"] - # a YouTube link, in any of its forms, is kept as the watch link and played without cookies - for yt in ("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=5s", "https://youtube.com/watch?list=PL1&v=dQw4w9WgXcQ", "youtu.be/dQw4w9WgXcQ?si=abc", - "https://m.youtube.com/shorts/dQw4w9WgXcQ", "https://www.youtube.com/live/dQw4w9WgXcQ?feature=share", - "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?rel=0"): - assert video_link(yt) == "https://www.youtube.com/watch?v=dQw4w9WgXcQ", yt - assert video_embed(video_link("youtu.be/dQw4w9WgXcQ")) == "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?rel=0" - assert video_link("https://www.youtube.com/watch?v=dQw4w9WgXcQQ") is None - # the teacher moves a lesson's videos among its topics: another one is added, and the two are placed in a new order - assert c.post("/api/videos", json={"lesson": READY[0], "title": "b", "url": link}, headers=good).status_code == 200 - seen = lambda: [(v["id"], v["pos"]) for v in c.get("/api/videos", headers=good).json()["videos"] if v["lesson"] == READY[0]] - assert seen() == [(1, N1), (3, N1)] - order = lambda *o: c.put("/api/content/order", json={"lesson": READY[0], "order": [{"kind": "video", "id": i, "pos": p} for i, p in o]}, headers=good).status_code - assert order((3, 0), (1, 0)) == 200 and seen() == [(3, 0), (1, 0)] # both before the first topic, the new one first - assert order((3, 0), (1, 2)) == 200 and seen() == [(3, 0), (1, 2)] - assert order((3, 0)) == 409 and order((3, 0), (2, 0)) == 409 # every video of the lesson, and no other's - assert order((3, 0), (1, N1 + 1)) == 400 and order((3, -1), (1, 0)) == 400 and seen() == [(3, 0), (1, 2)] - assert c.put("/api/content/order", json={"lesson": SOON, "order": []}, headers=good).status_code == 400 - c.app.state.auth.vids[0].update(pos=None) # kept before videos had places: after the topics - assert seen() == [(3, 0), (1, N1)] - assert c.delete("/api/videos/3", headers=good).status_code == 200 - first.update(pos=N1, ord=1) - second = c.get("/api/videos", headers=good).json()["videos"][1] - # a text: its lines in Hebrew and in Arabic written in Hebrew letters, the same number of each; a dialogue names two voices - he, tr = ["שלום", "מה שלומך?", "שלום"], ["מַרְחַבַּא", "כִּיף חַאלַכּ?", "מַרְחַבַּא יַא זַלַמֶה"] - talk = {"lesson": READY[0], "title": " בשוק ", "he": he, "tr": tr, "voices": ["אמיר", "ראאף"]} - assert c.get("/api/texts").status_code == 401 and c.get("/api/texts", headers=good).json() == {"categories": [], "phrases": []} - for body, code in ((talk, 200), ({**talk, "title": "story", "voices": []}, 200), - ({**talk, "tr": tr[:2]}, 400), ({**talk, "he": [], "tr": []}, 400), ({**talk, "tr": ["x", "1", "-"]}, 400), # not Arabic in Hebrew letters - ({**talk, "voices": ["אמיר"]}, 400), ({**talk, "voices": ["אמיר", "nobody"]}, 400), - ({**talk, "lesson": SOON}, 400), ({**talk, "title": ""}, 400), ({**talk, "he": he * 80, "tr": tr * 80}, 400)): - r = c.post("/api/texts", json=body, headers=good) - assert r.status_code == code, (body, r.text) - d = c.get("/api/texts", headers=good).json() - assert d["categories"][0] == {"id": "t1", "name": "בשוק", "lesson": READY[0], "story": True, "text": 1, "voices": ["אמיר", "ראאף"], - "pos": N1, "ord": 2, "guessed": ["זַלַמֶה"]} # the course has the other words - assert "voices" not in d["categories"][1] and d["categories"][1]["ord"] == 3 - line1, line2, line3 = d["phrases"][:3] - assert line1 == {"id": 1_001_000, "cat": "t1", "he": "שלום", "tr": "מַרְחַבַּא", "ar": "مرحبا", - "ar_pointed": "مَرْحَبَا", "seq": 1} - assert (line2["id"], line2["ar"], line3["ar"]) == (1_001_001, "كيف حالك؟", "مرحبا يا زلمة") and len(d["phrases"]) == 6 - # a line is a phrase like the bank's: it has a prompt and an answer to play, and its recordings count (two lines - # with the same Hebrew stay apart) - assert c.get("/api/prompt-audio/1001999", headers=good).status_code == 404 and c.get("/api/tts/1001001", headers=good).status_code == 503 - from app.main import built, summary as sums, text_bank - bank = text_bank(c.app.state.auth.txts) - assert bank[1_001_002]["key"] == "שלום (2)" and bank[1_001_001]["variants"][0] == ("كيف حالك؟", "k iː f | ħ aː l a k") - got = sums([{"cat": "t1", "phrase": "שלום (2)", "score": 80, "created_at": "2026-10-09T10:00:00+00:00"}, - {"cat": "t1", "phrase": "שלום", "score": 60, "created_at": "2026-10-09T10:00:00+00:00"}, - {"cat": "t9", "phrase": "שלום", "score": 99, "created_at": "2026-10-09T10:00:00+00:00"}], bank) # a deleted text's - assert got["best"] == {1_001_002: 80, 1_001_000: 60} and got["recordings"] == 3 - for ar, ipa in (v for p in bank.values() for v in p["variants"]): - assert len(ar.split()) == len(ipa.split("|")) - # a text is placed among the topics like a video, and may be a class's homework - mixed = lambda *o: c.put("/api/content/order", json={"lesson": READY[0], "order": [{"kind": k, "id": i, "pos": p} for k, i, p in o]}, headers=good).status_code - assert mixed(("text", 2, 0), ("video", 1, 1), ("text", 1, 1)) == 200 and mixed(("text", 2, 0), ("text", 1, 1)) == 409 - assert [(k["id"], k["pos"], k["ord"]) for k in c.get("/api/texts", headers=good).json()["categories"]] == [("t2", 0, 0), ("t1", 1, 2)] - assert c.get("/api/videos", headers=good).json()["videos"][0]["ord"] == 1 - first.update(pos=1, ord=1) - assert c.post("/api/classes", json={"name": "T", "emails": "a@b.c", "lessons": READY[:1], "homework": "t1"}, headers=good).status_code == 200 - assert c.get("/api/me", headers=good).json()["classes"][-1]["homework"] == "t1" - assert c.delete("/api/texts/1", headers=good).status_code == 200 # with its text gone, the homework is none - assert c.get("/api/me", headers=good).json()["classes"][-1]["homework"] == "" - assert c.delete("/api/texts/2", headers=good).status_code == 200 - c.app.state.auth.cls = [k for k in c.app.state.auth.cls if k["name"] != "T"] - # a student gets the videos of their classes' lessons only, and adds or deletes none - kept, c.app.state.auth.teachers = c.app.state.auth.cls, frozenset() - c.app.state.auth.cls = [{"id": 7, "name": "V", "emails": ["a@b.c"], "nicknames": {}, "lessons": READY[1:2]}] - assert c.get("/api/videos", headers=good).json()["videos"] == [{k: v for k, v in second.items() if k != "url"}] # without the link - assert c.post("/api/videos", json={"lesson": READY[0], "title": "x", "url": link}, headers=good).status_code == 403 - assert c.delete("/api/videos/1", headers=good).status_code == 403 - assert c.put("/api/content/order", json={"lesson": READY[0], "order": []}, headers=good).status_code == 403 - assert c.post("/api/texts", json={}, headers=good).status_code == 403 and c.delete("/api/texts/1", headers=good).status_code == 403 - c.app.state.auth.cls, c.app.state.auth.teachers = kept, frozenset({"a@b.c"}) - assert c.delete("/api/videos/1", headers=good).status_code == 200 - assert c.get("/api/videos", headers=good).json()["videos"] == [second] - # the daily videos: a library by level; a class gives a video its day, and its students get it with that day - from app.phrases import LEVELS - L1, L2 = LEVELS[0]["id"], LEVELS[1]["id"] - daily = lambda **body: c.post("/api/dailies", json=body, headers=good).status_code - assert c.get("/api/dailies").status_code == 401 and c.get("/api/dailies", headers=good).json() == {"dailies": []} - assert daily(level=L1, title=" יום 1 ", url=link + "?share=copy") == 200 and daily(level=L2, title="other", url="https://youtu.be/dQw4w9WgXcQ") == 200 - assert daily(level="9", title="x", url=link) == 400 and daily(level=L1, title="", url=link) == 400 and daily(level=L1, title="x", url="https://example.com/1") == 400 - lib = c.get("/api/dailies", headers=good).json()["dailies"] - assert [(v["id"], v["level"], v["title"]) for v in lib] == [(1, L1, "יום 1"), (2, L2, "other")] and lib[0]["embed"].startswith("https://player.vimeo.com/video/") - D = {"name": "D", "emails": "a@b.c", "lessons": [l["id"] for l in LESSONS if l["level"] == L1][:1]} - save = lambda days: c.post("/api/classes", json={**D, "dailies": days}, headers=good).status_code - assert save([{"video": 9, "date": "2026-10-12"}]) == 400 and save([{"video": 1, "date": "12.10"}]) == 400 - assert save([{"video": 1, "date": "2026-10-12"}, {"video": 1, "date": "2026-10-13"}]) == 400 and save("x") == 400 - assert save([{"video": 1, "date": "2026-10-12"}, {"video": 2, "date": "2026-10-11"}, {"video": 9, "date": ""}]) == 200 # one without a day is dropped - d = c.app.state.auth.cls[0] - assert d["dailies"] == [{"video": 1, "date": "2026-10-12"}, {"video": 2, "date": "2026-10-11"}] - mine = lambda: next(k for k in c.get("/api/me", headers=good).json()["classes"] if k["name"] == "D")["dailies"] - assert mine() == [{"id": 1, "title": "יום 1", "embed": lib[0]["embed"], "date": "2026-10-12"}] # the other level's is not the class's - assert c.get("/api/me", headers=good).json()["dailies"] == mine() # a teacher gets every class's - # saved without the days they stay; saved with none they are gone - assert c.put(f"/api/classes/{d['id']}", json=D, headers=good).status_code == 200 and len(mine()) == 1 - assert c.put(f"/api/classes/{d['id']}", json={**D, "dailies": []}, headers=good).status_code == 200 and mine() == [] and d["dailies"] == [] - assert c.put(f"/api/classes/{d['id']}", json={**D, "dailies": [{"video": 1, "date": "2026-10-12"}]}, headers=good).status_code == 200 - assert c.delete("/api/dailies/1", headers=good).status_code == 200 and mine() == [] # gone from the library, gone from the class - # the library: videos for every student, whatever the level and the class - shelf = lambda **body: c.post("/api/library", json=body, headers=good).status_code - assert c.get("/api/library").status_code == 401 and c.get("/api/library", headers=good).json() == {"library": []} - assert shelf(title="two", url=link) == 200 and shelf(level=L1, title=" one ", url=link) == 200 and shelf(title="gone", url=link) == 200 - assert shelf(title="", url=link) == 400 and shelf(title="x", url="nope") == 400 - assert c.delete("/api/library/3", headers=good).status_code == 200 - shelves = c.get("/api/library", headers=good).json()["library"] - assert [(v["id"], v["title"], v["url"]) for v in shelves] == [(1, "two", link.split("?")[0]), (2, "one", link.split("?")[0])] - # a video of the library is renamed, and the library is put in the teacher's order - rename = lambda id, **body: c.put(f"/api/library/{id}", json=body, headers=good).status_code - order = lambda ids: c.put("/api/library/order", json={"order": ids}, headers=good).status_code - assert rename(1, title=" the first ") == 200 and rename(1, title="") == 400 and rename(1, title="x" * 200) == 400 and rename(9, title="x") == 404 - assert order([2]) == 409 and order([2, 2]) == 409 and order([2, 1, 9]) == 409 and order("x") == 400 and order(["2", "1"]) == 400 - assert order([2, 1]) == 200 - shelves = c.get("/api/library", headers=good).json()["library"] - assert [(v["id"], v["title"]) for v in shelves] == [(2, "one"), (1, "the first")] - c.app.state.auth.teachers = frozenset() - assert c.get("/api/library", headers=good).json()["library"] == [{k: v for k, v in s.items() if k != "url"} for s in shelves] # all of it, without the links - assert shelf(title="x", url=link) == 403 and c.delete("/api/library/1", headers=good).status_code == 403 - assert rename(1, title="x") == 403 and order([1, 2]) == 403 - assert c.get("/api/dailies", headers=good).status_code == 403 and daily(level=L1, title="x", url=link) == 403 and c.delete("/api/dailies/2", headers=good).status_code == 403 - c.app.state.auth.cls, c.app.state.auth.teachers = [k for k in c.app.state.auth.cls if k["name"] != "D"], frozenset({"a@b.c"}) - new_class = {"name": " October 2026 ", "emails": "S@b.c, new@b.c\nnew@b.c דנה \nx@b.c\n", # an optional nickname - "lessons": ALL[::-1], # x is in September's class too: a student may be in several - "homework": T2, "due": "2026-10-15"} - assert c.post("/api/classes", json=new_class, headers=good).status_code == 200 - assert c.app.state.auth.cls[0] == {"id": 2, "name": "October 2026", "emails": ["new@b.c", "s@b.c", "x@b.c"], - "nicknames": {"new@b.c": "דנה"}, "lessons": ALL, "homeworks": [{"topic": T2, "due": "2026-10-15"}]} - for body, code in (({"name": "", "emails": "", "lessons": ALL}, 400), ({"name": "y", "emails": "not-an-email", "lessons": ALL}, 400), - ({"name": "y", "emails": "z@b.c w@b.c Dana", "lessons": ALL}, 400), # whose nickname? - ({"name": "y", "emails": "", "lessons": []}, 400), ({"name": "y", "emails": ""}, 400), # no lesson - ({"name": "y", "emails": "", "lessons": ["no-such-lesson"]}, 400), - ({"name": "y", "emails": "", "lessons": ALL[:1], "homework": T2}, 400), # not of the class's lessons - ({"name": "y", "emails": "", "lessons": ALL, "homework": ALL[0]}, 400), # a lesson is not a topic - ({"name": "y", "emails": "", "lessons": ALL, "homework": T1, "due": "15/10"}, 400), - ({"name": "October 2026", "emails": "", "lessons": ALL}, 409), # the name is taken, - ({"name": " october 2026", "emails": "", "lessons": ALL}, 409)): # in any letter case and spacing - r = c.post("/api/classes", json=body, headers=good) - assert r.status_code == code, (body, r.text) - assert "October 2026" in r.json()["detail"] - # renaming a class to another's name is refused too; keeping its own name is not - assert c.put("/api/classes/1", json={"name": "OCTOBER 2026", "emails": "", "lessons": ALL}, headers=good).status_code == 409 - assert c.put("/api/classes/9", json={"name": "z", "emails": "", "lessons": ALL}, headers=good).status_code == 404 - assert c.put("/api/classes/1", json={"name": "September 2026", "emails": "x@b.c y@b.c", "lessons": ALL[:1]}, - headers=good).status_code == 200 - # the panel: the classes with their lessons; the classes' accounts, each with {class: nickname} - d = c.get("/api/class", headers=good).json() - assert [(k["name"], k["lessons"]) for k in d["classes"]] == [("October 2026", ALL), ("September 2026", ALL[:1])] - assert [(k["homework"], k["due"]) for k in d["classes"]] == [(T2, "2026-10-15"), ("", None)] - # a date without homework is dropped; saving without the two clears them - assert c.put("/api/classes/2", json={**new_class, "homework": ""}, headers=good).status_code == 200 - assert c.app.state.auth.cls[0]["homeworks"] == [] - # a class may have several homeworks, a topic each with its own day: they come back in the order of its lessons - several = {**new_class, "homework": "", "homeworks": [{"topic": T2, "due": ""}, {"topic": T1, "due": "2026-10-20"}, {"topic": "", "due": "2026-10-21"}]} - assert c.put("/api/classes/2", json=several, headers=good).status_code == 200 - k = c.get("/api/class", headers=good).json()["classes"][0] - assert k["homeworks"] == [{"topic": T1, "due": "2026-10-20"}, {"topic": T2, "due": None}] and (k["homework"], k["due"]) == (T1, "2026-10-20") - for bad in ([{"topic": T1}, {"topic": T1}], [{"topic": "no-such-topic"}], [{"topic": T1, "due": "soon"}], "x", ["x"]): - assert c.put("/api/classes/2", json={**several, "homeworks": bad}, headers=good).status_code == 400, bad - assert c.put("/api/classes/2", json={**new_class, "homework": ""}, headers=good).status_code == 200 - student, new, x, y = d["students"] # the teacher, in no class, is not a student - assert student["best"] == {str(PHRASES.index(VERB)): 88} and student["tries"] == {str(PHRASES.index(VERB)): 2} - assert (student["recordings"], student["days"], student["last"]) == (2, 1, "2026-10-09T10:00:00+00:00") - assert (new["email"], new["joined"], new["recordings"]) == ("new@b.c", False, 0) and student["joined"] - c.app.state.auth.wrote["u2", "l1-hw"] = ["הִיֵّ פַתְחַת"] # the panel reads what each student wrote - assert [k["written"] for k in c.get("/api/class", headers=good).json()["students"][:2]] == [{"l1-hw": ["הִיֵّ פַתְחַת"]}, {}] - assert (student["classes"], new["classes"]) == ({"October 2026": ""}, {"October 2026": "דנה"}) - assert x["classes"] == {"October 2026": "", "September 2026": ""} and y["classes"] == {"September 2026": ""} - # progress is the student's, not the class's: a class opened after they practised still shows it all, - # so a student moved to another class with the same lessons keeps their progress - c.app.state.auth.cls[0]["created_at"] = "2026-10-10T00:00:00+00:00" # opened after both tries - student = c.get("/api/class", headers=good).json()["students"][0] - assert "since" not in student and student["classes"] == {"October 2026": ""} - assert (student["best"], student["recordings"], student["last"]) == ( - {str(PHRASES.index(VERB)): 88}, 2, "2026-10-09T10:00:00+00:00") - # a recording with speech completes the exercise even when it did not match; it is also counted diagnostically - from app.main import summary - off = summary([{"cat": VERB["cat"], "phrase": VERB["he"], "score": 12, "unrelated": True, "created_at": "2026-10-09T11:00:00+00:00"}, - {"cat": VERB["cat"], "phrase": VERB["he"], "score": 70, "unrelated": False, "created_at": "2026-10-09T10:00:00+00:00"}]) - assert (off["best"], off["tries"], off["missed"]) == ({PHRASES.index(VERB): 70}, {PHRASES.index(VERB): 2}, {PHRASES.index(VERB): 1}) - assert (off["unmatched"], off["recordings"], off["last"]) == (1, 2, "2026-10-09T11:00:00+00:00") - assert c.delete("/api/classes/1", headers=good).status_code == 200 and len(c.app.state.auth.cls) == 1 - # a student sees each of their classes: by nickname, else the Google name; one with neither is only counted - c.app.state.auth.cls[0]["emails"].append("a@b.c") - assert c.get("/api/classmates", headers=good).json() == {"classes": [{"name": "October 2026", "unnamed": 2, "students": [ - {"name": "Alef", "me": True}, {"name": "דנה", "me": False}]}]} - c.app.state.auth.cls[0]["emails"].remove("a@b.c") - c.app.state.auth.cls = [] # no class: every account that signed in - assert [s["email"] for s in c.get("/api/class", headers=good).json()["students"]] == ["A@b.c", "s@b.c", "out@b.c"] - # a scored recording is recorded as the student's attempt - auth, pid = FakeAuth(), PHRASES.index(TOILET) - buf = io.BytesIO() - with wave.open(buf, "wb") as w: - w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000) - w.writeframes((0.3 * np.sin(2 * np.pi * 220 * np.arange(16000) / 16000) * 32767).astype(" 75 - # the coloured letter says why, with the advice for that mistake; a letter that was fine says nothing - het, mem = (next(l for l in r["words"][1]["letters"] if l["t"][0] == c) for c in "חמ") - assert het["why"] == "ח (ح) נשמעה כמו ח' (خ)" and het["tip"] == r["tips"][0]["text"] and "why" not in mem - - -def test_a_tutor_flags_a_recording_scored_wrong(monkeypatch): - from fastapi.testclient import TestClient - - monkeypatch.setenv("SAWTAK_API_KEY", "") # no native baseline from a local .env: the score is the stub's own - - from app.auth import AuthUnavailable - from app.main import MODEL_ID, create_app - - class FakeAuth: - url, key, teachers, tutors = "https://proj.supabase.co", "sb_publishable_x", frozenset(), frozenset({"t@b.c"}) - - def __init__(self): - self.cls, self.flags, self.table, self.rank, self.calibration_rows = [], [], True, frozenset(), [] - self.rule_rows = {} - - def user(self, token): - return {"t": {"id": "u1", "email": "T@b.c"}, "s": {"id": "u2", "email": "s@b.c"}}.get(token) - - def classes(self, fresh=False): - return self.cls - - def ranked(self, fresh=False): - return self.rank - - def set_tutor(self, email, on, by): - if self.rank is None: - raise AuthUnavailable("supabase answered 404: no table") - self.rank = self.rank | {email} if on else self.rank - {email} - self.by = by - - def level_videos(self, table, fresh=False): - return [] - - def texts(self, fresh=False): - return [] - - def add_flag(self, row, audio, mime, name): - if not self.table: - raise AuthUnavailable("supabase answered 404: no table") - self.flag_recordings = getattr(self, "flag_recordings", {}) - self.flag_recordings[len(self.flags) + 1] = (audio, mime) - self.flags.append({**row, "audio": name}) - return True - - def list_flags(self, status="pending"): - if status != "pending": - return [] - return [{"id": i + 1, **row, "status": status} for i, row in enumerate(self.flags)] - - def review_flag(self, flag_id, values): - self.review = (flag_id, values) - - def flag_audio(self, flag_id): - return getattr(self, "flag_recordings", {}).get(flag_id) - - def calibrations(self, status=None): - return [r for r in self.calibration_rows if not status or r["status"] == status] - - def add_calibration(self, row): - made = {"id": len(self.calibration_rows) + 1, **row} - self.calibration_rows.append(made) - return made - - def update_calibration(self, calibration_id, values): - next(r for r in self.calibration_rows if r["id"] == calibration_id).update(values) - - def delete_calibration(self, calibration_id): - before = len(self.calibration_rows) - self.calibration_rows = [r for r in self.calibration_rows if r["id"] != calibration_id] - return len(self.calibration_rows) < before - - def scoring_rules(self): - if self.rule_rows is None: - raise AuthUnavailable("supabase answered 404: no table") - return list(self.rule_rows.values()) - - def set_scoring_rule(self, rule_id, active, by): - if self.rule_rows is None: - raise AuthUnavailable("supabase answered 404: no table") - self.rule_rows[rule_id] = {"id": rule_id, "active": active, "updated_by": by} - - def all_attempt_issues(self): - return [{"user_id": f"u{i}", "phrase": TOILET["he"], "score": 60, - "issues": [{"expected": "ħ", "heard": "χ", "score": 20}], - "model": MODEL_ID, "version": "v"} for i in range(3)] - - t = np.arange(16000) / 16000 - buf = io.BytesIO() - with wave.open(buf, "wb") as w: - w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000) - w.writeframes((0.3 * np.sin(2 * np.pi * 220 * t) * 32767).astype("x" and row["result"]["trace"]["tokens"] - assert "phoneme_heatmap" not in row["result"] - assert send(c, "t", phrase_id=10**6).status_code == 404 - a.teachers = frozenset({"s@b.c"}) # a teacher flags too - assert c.get("/api/me", headers={"Authorization": "Bearer s"}).json()["tutor"] is True and send(c, "s").status_code == 200 - hs = {"Authorization": "Bearer s"} - listed = c.get("/api/flags", headers=hs) - assert listed.status_code == 200 and len(listed.json()["flags"]) == 2 - played = c.get("/api/flags/1/audio", headers=hs) - assert played.status_code == 200 and played.content == buf.getvalue() - assert c.get("/api/flags/999/audio", headers=hs).status_code == 404 - review = c.put("/api/flags/1", headers=hs, json={"status": "confirmed", "category": "recognition", - "false_issues": [{"word": 1, "expected": "ħ", "heard": "χ"}], "review_note": " תקין "}) - assert review.status_code == 200 - assert a.review[0] == 1 and a.review[1]["false_issues"] == [{"word": 1, "expected": "ħ", "heard": "χ"}] - assert a.review[1]["review_note"] == "תקין" and a.review[1]["reviewed_by"] == "s@b.c" - assert c.put("/api/flags/1", headers=hs, json={"status": "pending", "category": "other"}).status_code == 400 - patterns = c.get("/api/attempt-patterns", headers=hs).json()["patterns"] - assert patterns[0]["expected"] == "ħ" and patterns[0]["heard"] == "χ" - assert patterns[0]["students"] == 3 and patterns[0]["student_rate"] == 1 - proposal = c.post("/api/calibrations", headers=hs, json={"expected": "ħ", "heard": "χ", - "evidence": {"reports": 5, "reporters": 2, "phrases": [TOILET["he"]]}}) - assert proposal.status_code == 200 and proposal.json()["calibration"]["status"] == "draft" - applied = c.put("/api/calibrations/1", headers=hs, json={"status": "applied", "floor": 70, - "scope": "phrase", "phrase": TOILET["he"]}) - assert applied.status_code == 200 and applied.json()["active"] == 1 - assert c.app.state.scorer.calibrations[("ħ", "χ", TOILET["he"])] == 70 - deleted = c.delete("/api/calibrations/1", headers=hs) - assert deleted.status_code == 200 and deleted.json()["active"] == 0 - assert not c.app.state.scorer.calibrations and c.delete("/api/calibrations/1", headers=hs).status_code == 404 - dismissed = c.post("/api/calibrations", headers=hs, json={"expected": "r", "heard": "ɣ", - "evidence": {"dismissed": True, "source": "confirmed_flags"}}) - assert dismissed.status_code == 200 and dismissed.json()["calibration"]["status"] == "draft" - assert dismissed.json()["calibration"]["evidence"] == {"dismissed": True, "source": "confirmed_flags"} - # the rules of pronunciation-rules.md: all on, a teacher switches one off, and the scorer follows at once - listed = c.get("/api/scoring-rules", headers=hs).json() - assert listed["stored"] and all(r["active"] for r in listed["rules"]) and len(listed["rules"]) >= 20 - assert all(isinstance(r["detectable"], bool) and r["title"] and r["description"] for r in listed["rules"]) - assert c.put("/api/scoring-rules/dialect_Q", headers=hs, json={"active": False}).json()["disabled"] == ["dialect_Q"] - assert c.app.state.scorer.disabled == {"dialect_Q"} and a.rule_rows["dialect_Q"]["updated_by"] == "s@b.c" - assert not next(r for r in c.get("/api/scoring-rules", headers=hs).json()["rules"] if r["id"] == "dialect_Q")["active"] - assert c.put("/api/scoring-rules/nope", headers=hs, json={"active": False}).status_code == 400 - assert c.put("/api/scoring-rules/margins", headers=hs, json={"active": "no"}).status_code == 400 - assert c.put("/api/scoring-rules/margins", headers={"Authorization": "Bearer t"}, json={"active": False}).status_code == 403 - assert c.put("/api/scoring-rules/dialect_Q", headers=hs, json={"active": True}).json()["disabled"] == [] - a.rule_rows = None # no table yet: the list still shows, all on, with the SQL to create it - missing = c.get("/api/scoring-rules", headers=hs).json() - assert not missing["stored"] and "create table public.scoring_rules" in missing["create_sql"] - assert c.put("/api/scoring-rules/margins", headers=hs, json={"active": False}).status_code == 503 - a.teachers = frozenset() - a.table = False # before the table is created: the command to create it - r = send(c, "t") - assert r.status_code == 503 and "create table public.flags" in r.json()["detail"] - # a tutor is let in without a class, a student is not - a.cls = [{"id": 1, "name": "B", "emails": ["other@b.c"], "nicknames": {}, "lessons": []}] - assert c.get("/api/me", headers={"Authorization": "Bearer t"}).status_code == 200 - assert c.get("/api/me", headers={"Authorization": "Bearer s"}).status_code == 403 - # the rank the teacher gives a student in the panel: theirs in every class, and it lets nobody in by itself - tok = lambda t: {"Authorization": f"Bearer {t}"} - rank = lambda t, **body: c.put("/api/tutors", json=body, headers=tok(t)) - a.users, a.all_attempts, a.all_writings = lambda: [{"id": "u2", "email": "s@b.c", "name": ""}], lambda: [], lambda: [] - assert rank("s", email="s@b.c", tutor=True).status_code == 403 # teachers only (s is not in a class either) - a.cls[0]["emails"].append("s@b.c"); a.teachers = frozenset({"t@b.c"}) - no_report = lambda: setattr(a, "access_for", {"u2": {"roles": ["student"], "entitlements": ["practice"]}}.get) - no_report() # the rank matters once a manager removed the default - assert rank("s", email="s@b.c", tutor=True).status_code == 403 and "tutor" not in c.get("/api/me", headers=tok("s")).json() - del a.access_for - assert rank("t", email="nobody", tutor=True).status_code == 400 and rank("t", email="s@b.c", tutor="yes").status_code == 400 - assert rank("t", email=" S@b.c ", tutor=True).status_code == 200 and a.rank == {"s@b.c"} and a.by == "t@b.c" - no_report() - assert c.get("/api/me", headers=tok("s")).json()["tutor"] is True and send(c, "s").status_code == 503 # may flag (the table is still missing) - del a.access_for - panel = c.get("/api/class", headers=tok("t")).json() - assert [(s["email"], s["tutor"], s.get("tutor_fixed")) for s in panel["students"]] == [("s@b.c", True, None), ("other@b.c", False, None)] - assert rank("t", email="s@b.c", tutor=False).status_code == 200 - no_report(); assert "tutor" not in c.get("/api/me", headers=tok("s")).json(); del a.access_for - a.rank = frozenset({"out@b.c"}) # ranked, but in no class: not let in - known = a.user - a.user = lambda token: {"id": "u3", "email": "out@b.c"} if token == "x" else known(token) - assert c.get("/api/me", headers=tok("x")).status_code == 403 - a.rank = None # before the table is created: the panel says what to run, and so does giving the rank - assert "create table public.tutors" in c.get("/api/class", headers=tok("t")).json()["setup"][-1]["sql"] - r = rank("t", email="s@b.c", tutor=True) - assert r.status_code == 503 and "create table public.tutors" in r.json()["detail"] - - -def test_switched_off_rules_bring_back_the_old_behaviour(): - kaddeesh = emissions(["k", "a", "d", "eː", "ʃ", "i", "s", "aː", "ʕ", "a"]) - s = scorer_for(kaddeesh) - assert not s.score(None, TIME)["tips"] # rural ق accepted - s.set_disabled({"dialect_Q", "not-a-rule"}) - assert s.disabled == {"dialect_Q"} - assert [t["key"] for t in s.score(None, TIME)["tips"]] == ["ʔ>k"] # as before the document - # the emphatic margin: off, plain t winning by less than twice is a mistake again - phrase = {"variants": [("طين", "tˤ i n")]} - s = margin_scorer(1.8) - assert s.score(None, phrase, diagnostics=True)["trace"]["tokens"][0]["margin"]["beyond"] is False - s.set_disabled({"margins"}) - assert s.score(None, phrase, diagnostics=True)["trace"]["tokens"][0]["margin"]["beyond"] is True - # a section-3 rule off: the general tip and severity instead of the document's - s = custom_scorer(["p", "f", "dʒ"], ["p", "i", "n", "dʒ", "aː", "n"]) - fin = {"variants": [("فنجان", "f i n dʒ aː n")]} - assert s.score(None, fin)["tips"][0]["text"].startswith("נהגתה p") - s.set_disabled({"rule_f"}) - tip = s.score(None, fin)["tips"][0] - assert tip["key"] == "f>p" and not tip["text"].startswith("נהגתה p") and tip["severity"] == 2 - # the final vowel of ة: off, i for e is marked down like any other vowel - s = scorer_for(emissions(["ʔ", "a", "h", "w", "i"])) - assert s.score(None, COFFEE)["words"][0]["phonemes"][-1]["score"] >= 95 - s.set_disabled({"final_vowel", "near_vowel"}) - assert s.score(None, COFFEE)["words"][0]["phonemes"][-1]["score"] < 50 - - -THANKS = {"variants": [("شكرا كتير", "ʃ u k r a n | k t iː r")]} -CLUSTER_SYMS = ["r", "t", "iː", "u", "ʃ"] - - -def test_vowel_between_two_consonants_is_a_mistake_mid_sentence(): - clean = custom_scorer(CLUSTER_SYMS, ["ʃ", "u", "k", "r", "a", "n", "k", "t", "iː", "r"]).score(None, THANKS) - assert not clean["tips"] and clean["score"] >= 95 - s = custom_scorer(CLUSTER_SYMS, ["ʃ", "u", "k", "r", "a", "n", "k", "a", "t", "iː", "r"]) # katiir - r = s.score(None, THANKS) - t = next(p for p in r["words"][1]["phonemes"] if p["ipa"] == "t") - assert t["inserted"] == "a" and t["score"] == 60 and [x["key"] for x in r["tips"]] == ["cluster"] - assert r["tips"][0]["severity"] == 1 and "ktiir" in r["tips"][0]["text"] and r["score"] < clean["score"] - letter = next(l for l in r["words"][1]["letters"] if "ת" in l["t"]) - assert letter["why"].startswith("נוספה") and "ktiir" in letter["tip"] - s.set_disabled({"rule_cluster"}) # switched off: not checked, as before - assert not s.score(None, THANKS)["tips"] - - -def test_vowel_in_the_cluster_that_opens_the_sentence_is_fine(): - # ketiir at the start of a sentence (6.3 #16), and the way into a glide (shuway) - r = custom_scorer(["t", "iː", "r"], ["k", "e", "t", "iː", "r"]).score(None, {"variants": [("كتير", "k t iː r")]}) - assert not r["tips"] and r["score"] >= 95 - phrase = {"variants": [("شو شوي", "ʃ u | ʃ w a j j")]} - r = custom_scorer(["ʃ", "u", "j"], ["ʃ", "u", "ʃ", "u", "w", "a", "j"]).score(None, phrase) - assert not any(t["key"] == "cluster" for t in r["tips"]) - - -LAWYER = {"variants": [("محامي", "m u ħ aː m i")]} - - -@pytest.mark.parametrize("said,flagged", [ - (["m", "u", "ħ", "aː", "m", "i"], False), - (["m", "ħ", "aː", "m", "i"], True), # mħaami: the u swallowed - (["m", "e", "ħ", "aː", "m", "i"], True), # said as e - (["m", "o", "ħ", "aː", "m", "i"], False), # o is a near vowel -]) -def test_kubutz_is_a_distinct_vowel(said, flagged): - r = custom_scorer(["u", "o"], said).score(None, LAWYER) - u = r["words"][0]["phonemes"][1] - assert u["ipa"] == "u" and u["flagged"] == flagged, u - if flagged: - assert r["tips"][0]["text"].startswith("ה־u") and r["tips"][0]["severity"] == 1 - - -def test_only_short_u_after_a_consonant_is_flagged(): - s = custom_scorer(["u", "o"], ["m", "ħ", "aː", "m", "i"]) - s.set_disabled({"rule_kubutz"}) # switched off: like any other short vowel - assert not s.score(None, LAWYER)["words"][0]["phonemes"][1]["flagged"] - # the و of "and" (u m a) and a long uː (ʃ uː f) are not the kubutz rule's - r = custom_scorer(["u"], ["m", "a"]).score(None, {"variants": [("وما", "u m a")]}) - assert not r["words"][0]["phonemes"][0]["flagged"] - r = custom_scorer(["u", "uː", "f", "ʃ"], ["ʃ", "e", "f"]).score(None, {"variants": [("شوف", "ʃ uː f")]}) - assert not r["words"][0]["phonemes"][1]["flagged"] and not r["tips"] - - -def test_teacher_trace_follows_every_stage(): - # ħ said as χ, then forgiven by a teacher floor: each stage of the trace shows where the score came from - logp = emissions(["w", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"]) - scorer = scorer_for(logp) - assert "trace" not in scorer.score(None, TOILET) # students do not get it - scorer.set_calibrations([{"expected": "ħ", "heard": "χ", "floor": 60}]) - t = scorer.score_logp(logp, TOILET, diagnostics=True)["trace"] - assert t["config"]["flag_below"] == 50 and t["config"]["leniency"] == 0.35 and t["variants"][0]["chosen"] - assert t["config"]["word_min_weight"] == 0.35 - h = next(x for x in t["tokens"] if x["ipa"] == "ħ") - assert [s["stage"] for s in h["stages"]] == ["gop", "exemption", "insertion", "length", "baseline", "calibration"] - assert h["stages"][0]["score"] < 50 and h["stages"][5]["applied"] and h["final"] == 60 - assert h["competitor"] == "χ" and h["rule"]["severity"] == 3 and not h["flagged"] - assert t["sentence"]["final"] == round(0.5 * t["sentence"]["phone_mean"] + 0.5 * t["sentence"]["word_mean"]) - - -def test_trace_carries_frames_curves_and_paths(): - from app.scorer import frame_energy - - r = scorer_for(emissions(["w", "eː", "n", "i", "l", "ħ", "a", "m", "aː", "m"])).score(None, TOILET, diagnostics=True) - t = r["trace"] - assert t["frames"]["count"] > 0 and t["frames"]["energy"] is None # no audio: no energy - tok = t["tokens"][0] - assert len(tok["curves"]["target"]) == len(tok["curves"]["blank"]) and tok["curves"]["prev"] is None - assert all(v["tokens"] for v in t["variants"]) and sum(v["chosen"] for v in t["variants"]) == 1 - e = frame_energy(np.r_[np.zeros(320), np.ones(320)], 2) - assert np.allclose(e, [0, 1]) - - -def test_flag_threshold_per_sound(): - # ħ said as χ: flagged at the global 50, not once its own threshold is under the score; other sounds unchanged - logp = emissions(["w", "eː", "n", "i", "l", "χ", "a", "m", "aː", "m"]) - scorer = scorer_for(logp) - h = lambda r: next(p for w in r["words"] for p in w["phonemes"] if p["ipa"] == "ħ") - before = h(scorer.score(None, TOILET)) - assert before["flagged"] and before["heard"] == "χ" - scorer.cfg = replace(scorer.cfg, flag_below_sound={"ħ": before["score"]}) - r = scorer.score_logp(logp, TOILET, diagnostics=True) - assert not h(r)["flagged"] and h(r)["score"] == before["score"] - assert r["trace"]["config"]["flag_below_sound"] == {"ħ": before["score"]} - - -def test_flag_thresholds_belong_to_their_model(): - from app.scorer import scorer_config - assert scorer_config("facebook/wav2vec2-xlsr-53-espeak-cv-ft").flag_below_sound == {"ħ": 20.0} - assert scorer_config("checkpoints/ihki-ft").flag_below_sound == {} - assert scorer_config("checkpoints/ihki-ft").flag_below == 50 - - -def test_each_class_topic_opens_on_its_own_screen(): - from fastapi.testclient import TestClient - from app.main import create_app - - with TestClient(create_app(scorer_for(emissions(["w"])))) as c: - for path in ("/", "/arabic"): # "/" stays Arabic - page = c.get(path).text - assert 'data-subject="arabic"' in page and "/static/logo-intro.svg" in page and "אהלא וסהלא" in page - math = c.get("/math").text - assert 'data-subject="math"' in math and "/static/logo-math.svg" in math and "מתמטיקה בכיף" in math - assert "__SUBJECT__" not in math and "__START_LABEL__" not in math - assert c.get("/history").status_code == 404 - assert c.get("/static/logo-math.svg").status_code == 200 - # its installed app has its own name, colour and start page; Arabic keeps the static manifest - assert '' in math and 'href="/math/manifest.webmanifest"' in math - assert '"__' not in math and '' in c.get("/").text - assert re.search(r'