learnx-cli 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. learnx_cli-0.3.0.dist-info/METADATA +240 -0
  2. learnx_cli-0.3.0.dist-info/RECORD +131 -0
  3. learnx_cli-0.3.0.dist-info/WHEEL +4 -0
  4. learnx_cli-0.3.0.dist-info/entry_points.txt +2 -0
  5. tutor/.env copy.example +4 -0
  6. tutor/__init__.py +0 -0
  7. tutor/__main__.py +4 -0
  8. tutor/assets/__init__.py +5 -0
  9. tutor/assets/html/fonts/Inter-Bold.woff2 +0 -0
  10. tutor/assets/html/fonts/Inter-Regular.woff2 +0 -0
  11. tutor/assets/html/fonts/Inter-SemiBold.woff2 +0 -0
  12. tutor/assets/html/fonts/JetBrainsMono-Regular.woff2 +0 -0
  13. tutor/assets/html/highlight-java.min.js +2 -0
  14. tutor/assets/html/highlight-javascript.min.js +2 -0
  15. tutor/assets/html/highlight-python.min.js +2 -0
  16. tutor/assets/html/highlight.min.js +17 -0
  17. tutor/assets/html/mermaid.min.js +31 -0
  18. tutor/assets/html/slide_base.css +464 -0
  19. tutor/assets/html/theme-learnx-dark.css +12 -0
  20. tutor/audio/__init__.py +0 -0
  21. tutor/audio/audio_builder.py +143 -0
  22. tutor/audio/sanitizer.py +9 -0
  23. tutor/audio/tts_renderer.py +54 -0
  24. tutor/cli/__init__.py +0 -0
  25. tutor/cli/commands.py +391 -0
  26. tutor/cli/logo.py +21 -0
  27. tutor/cli/playback_commands.py +239 -0
  28. tutor/cli/shell.py +91 -0
  29. tutor/cli/shell_context.py +18 -0
  30. tutor/cli/theme.py +39 -0
  31. tutor/cli/video_commands.py +123 -0
  32. tutor/config.py +122 -0
  33. tutor/conftest.py +5 -0
  34. tutor/constants.py +82 -0
  35. tutor/exceptions.py +26 -0
  36. tutor/generation/__init__.py +0 -0
  37. tutor/generation/assembler.py +81 -0
  38. tutor/generation/curriculum.py +97 -0
  39. tutor/generation/dialogue.py +172 -0
  40. tutor/generation/narrator.py +122 -0
  41. tutor/generation/segment_parser.py +223 -0
  42. tutor/generation/segment_planner.py +200 -0
  43. tutor/generation/visual_planner.py +205 -0
  44. tutor/infra/__init__.py +0 -0
  45. tutor/infra/llm.py +152 -0
  46. tutor/ingestion/__init__.py +0 -0
  47. tutor/ingestion/chunker.py +171 -0
  48. tutor/ingestion/doc_analyzer.py +41 -0
  49. tutor/ingestion/parse_content.py +19 -0
  50. tutor/ingestion/summarizer.py +51 -0
  51. tutor/inspector.py +117 -0
  52. tutor/llm_config.toml +58 -0
  53. tutor/models.py +147 -0
  54. tutor/player/__init__.py +0 -0
  55. tutor/player/input_handler.py +45 -0
  56. tutor/player/player.py +308 -0
  57. tutor/player/player_display.py +117 -0
  58. tutor/prompts/curriculum.txt +67 -0
  59. tutor/prompts/dialogue.txt +62 -0
  60. tutor/prompts/narrate.txt +34 -0
  61. tutor/prompts/qa.txt +17 -0
  62. tutor/prompts/summarize.txt +9 -0
  63. tutor/prompts/visual.txt +60 -0
  64. tutor/prompts/visual_v3.txt +91 -0
  65. tutor/qa/__init__.py +0 -0
  66. tutor/qa/qa.py +105 -0
  67. tutor/requirements-dev.txt +2 -0
  68. tutor/requirements.txt +12 -0
  69. tutor/sample_docs/headingless_large.md +1 -0
  70. tutor/sample_docs/headingless_test.md +1 -0
  71. tutor/sample_docs/java-basics.md +78 -0
  72. tutor/tests/__init__.py +0 -0
  73. tutor/tests/audio/__init__.py +0 -0
  74. tutor/tests/audio/test_audio_builder.py +106 -0
  75. tutor/tests/audio/test_sanitizer.py +41 -0
  76. tutor/tests/cli/__init__.py +0 -0
  77. tutor/tests/cli/test_commands.py +67 -0
  78. tutor/tests/cli/test_video_commands.py +190 -0
  79. tutor/tests/e2e/README.md +61 -0
  80. tutor/tests/e2e/__init__.py +0 -0
  81. tutor/tests/e2e/conftest.py +117 -0
  82. tutor/tests/e2e/fixtures/README.md +17 -0
  83. tutor/tests/e2e/fixtures/sample.md +13 -0
  84. tutor/tests/e2e/test_audio_quality.py +40 -0
  85. tutor/tests/e2e/test_av_sync.py +56 -0
  86. tutor/tests/e2e/test_pipeline_smoke.py +37 -0
  87. tutor/tests/e2e/test_slide_render.py +72 -0
  88. tutor/tests/e2e/test_video_streams.py +104 -0
  89. tutor/tests/generation/__init__.py +0 -0
  90. tutor/tests/generation/conftest.py +134 -0
  91. tutor/tests/generation/test_assembler.py +64 -0
  92. tutor/tests/generation/test_curriculum.py +107 -0
  93. tutor/tests/generation/test_narrator.py +165 -0
  94. tutor/tests/generation/test_segment_edge_cases.py +280 -0
  95. tutor/tests/generation/test_segment_planner.py +324 -0
  96. tutor/tests/generation/test_visual_planner.py +319 -0
  97. tutor/tests/ingestion/__init__.py +0 -0
  98. tutor/tests/ingestion/test_chunker.py +94 -0
  99. tutor/tests/ingestion/test_doc_analyzer.py +51 -0
  100. tutor/tests/player/__init__.py +0 -0
  101. tutor/tests/player/test_player_states.py +88 -0
  102. tutor/tests/test_assets.py +39 -0
  103. tutor/tests/test_models_visual.py +180 -0
  104. tutor/tests/visual/__init__.py +0 -0
  105. tutor/tests/visual/test_beat_timer.py +321 -0
  106. tutor/tests/visual/test_pipeline_integration.py +178 -0
  107. tutor/tests/visual/test_slide_renderer.py +298 -0
  108. tutor/tests/visual/test_subtitle_writer.py +165 -0
  109. tutor/tests/visual/test_video_assembler.py +108 -0
  110. tutor/tests/visual/test_visual_pipeline.py +270 -0
  111. tutor/tutor.py +365 -0
  112. tutor/visual/__init__.py +213 -0
  113. tutor/visual/beat_timer.py +222 -0
  114. tutor/visual/slide_renderer.py +236 -0
  115. tutor/visual/subtitle_writer.py +187 -0
  116. tutor/visual/templates/_base.html.j2 +40 -0
  117. tutor/visual/templates/analogy.html.j2 +21 -0
  118. tutor/visual/templates/callout.html.j2 +10 -0
  119. tutor/visual/templates/code_example.html.j2 +12 -0
  120. tutor/visual/templates/comparison.html.j2 +28 -0
  121. tutor/visual/templates/decision_guide.html.j2 +37 -0
  122. tutor/visual/templates/definition.html.j2 +13 -0
  123. tutor/visual/templates/diagram.html.j2 +11 -0
  124. tutor/visual/templates/hook_question.html.j2 +17 -0
  125. tutor/visual/templates/key_insight.html.j2 +9 -0
  126. tutor/visual/templates/memory_hook.html.j2 +7 -0
  127. tutor/visual/templates/outro.html.j2 +16 -0
  128. tutor/visual/templates/question_prompt.html.j2 +13 -0
  129. tutor/visual/templates/step_sequence.html.j2 +14 -0
  130. tutor/visual/templates/title_card.html.j2 +12 -0
  131. tutor/visual/video_assembler.py +299 -0
@@ -0,0 +1,91 @@
1
+ You are generating slide content for an audio tutorial video.
2
+
3
+ Given the dialogue transcript for one teaching unit, group consecutive lines into
4
+ visual segments. Each segment covers 1–3 lines that share one idea.
5
+
6
+ For each segment output a JSON object with these exact fields:
7
+ lines_start — 0-based index of first line (inclusive)
8
+ lines_end — 0-based index of last line (inclusive)
9
+ visual_type — one of the 12 types listed below
10
+ title — slide header, ≤ 10 words
11
+ body — main text content (see per-type notes below); null if unused
12
+ code — code string if relevant, else null
13
+ language — highlight.js language id (e.g. "java", "python") if code present, else null
14
+ mermaid — Mermaid diagram string if visual_type is "diagram", else null
15
+ left — left column/panel label for analogy, comparison, decision_guide; else null
16
+ right — right column/panel label; else null
17
+ rows — list of [left_cell, right_cell] pairs for comparison/analogy/decision_guide; else null
18
+
19
+ Visual types and when to use them:
20
+
21
+ hook_question — ALEX's opening hook lines (always the first segment).
22
+ body: the hook question text. rows: up to 3 learning objectives
23
+ as [[objective, ""], ...] (single-column list).
24
+
25
+ definition — ALEX introduces or formally defines a named term or concept.
26
+ body: the definition text (1–2 sentences). code: short example
27
+ if the definition is illustrated by code.
28
+
29
+ analogy — ALEX uses "like", "think of", "imagine", "similar to", or
30
+ "it's as if". body: not used when rows is present.
31
+ left/right: the two things being compared (real-world vs code).
32
+ rows: [[real_world_description, code_description]].
33
+
34
+ comparison — ALEX contrasts two named concepts side by side.
35
+ left/right: the two concept names. rows: property rows
36
+ [[left_value, right_value], ...] (3–5 rows).
37
+
38
+ code_example — ALEX demonstrates or references a specific code pattern.
39
+ body: one-line description of what the code shows (optional).
40
+ code: the code string. language: highlight.js id.
41
+
42
+ diagram — ALEX describes a structural relationship (class hierarchy,
43
+ data flow, state machine, dependency graph).
44
+ mermaid: a valid Mermaid diagram string.
45
+ Use classDiagram for class/interface relationships,
46
+ flowchart TD for process flows.
47
+
48
+ question_prompt — MAYA or SAM lines only. Do NOT mix with ALEX lines.
49
+ body: "SPEAKER: question text" (e.g. "MAYA: But why not just…").
50
+
51
+ decision_guide — ALEX explains when to choose X over Y using if/when/unless
52
+ criteria. left: "Use X when". right: "Avoid X when".
53
+ rows: [[criterion_for, criterion_against], ...].
54
+
55
+ key_insight — ALEX states a rule, principle, or memorable law.
56
+ body: the complete rule statement (one sentence, max 20 words).
57
+ Reserve for concise, standalone rules. Use callout for longer
58
+ important statements.
59
+
60
+ memory_hook — ALEX's closing memory-hook lines (always the last segment).
61
+ body: the mnemonic or summary phrase ALEX uses.
62
+
63
+ step_sequence — ALEX explains a sequential multi-step process ("first…",
64
+ "step one…", "then you…", "next…", "finally…").
65
+ body: steps separated by \n, one step per line (3–6 steps).
66
+ Each line becomes a numbered circle in the slide.
67
+
68
+ callout — A single important statement that stands alone: a prerequisite,
69
+ a warning, a key quote, or a tip. Use when the content is too
70
+ long or complex for key_insight but is a single highlighted point.
71
+ title: the label ("NOTE", "WARNING", "TIP", "PREREQUISITE", "QUOTE").
72
+ body: the statement (1–3 sentences).
73
+
74
+ Assignment rules:
75
+ 1. Cover every line — no gaps; lines_end of segment N + 1 == lines_start of N+1.
76
+ 2. First segment: hook_question. Last segment: memory_hook.
77
+ 3. MAYA or SAM lines alone → question_prompt.
78
+ 4. ALEX using "like", "imagine", "think of", "similar to" → analogy.
79
+ 5. ALEX contrasting two named things → comparison or decision_guide.
80
+ 6. ALEX describing class/flow/dependency structure → diagram (Mermaid required).
81
+ 7. ALEX showing or referencing specific code → code_example.
82
+ 8. ALEX explaining "first… then… finally…" or numbered steps → step_sequence.
83
+ 9. ALEX stating a single short rule (≤ 20 words) → key_insight.
84
+ 10. ALEX making a single important point that's longer than a rule → callout.
85
+ 11. ALEX introducing a named concept with a formal definition → definition.
86
+ 12. Produce 8–15 segments; never fewer than 5.
87
+ 13. Diversity rule: definition must not exceed 35% of total segments.
88
+ If you would produce more, merge adjacent definitions or reclassify as
89
+ key_insight, callout, or code_example where appropriate.
90
+
91
+ Output: a JSON array only. No prose before or after. No markdown code fences.
tutor/qa/__init__.py ADDED
File without changes
tutor/qa/qa.py ADDED
@@ -0,0 +1,105 @@
1
+ import json
2
+ import logging
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ from tutor.exceptions import LLMError
7
+ from tutor.infra.llm import LLMFn
8
+ from tutor.models import Chunk, QAExchange, SessionLog, TeachingUnit
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+ QA_PROMPT_PATH = Path(__file__).parent.parent / "prompts" / "qa.txt"
13
+
14
+
15
+ def answer(
16
+ question: str,
17
+ current_unit: TeachingUnit,
18
+ all_chunks: list[Chunk],
19
+ session: SessionLog,
20
+ llm_fn: LLMFn,
21
+ position_seconds: int = 0,
22
+ ) -> str:
23
+ context = _build_context(current_unit, all_chunks, session)
24
+ log.debug("Q&A context length: %d chars", len(context))
25
+ prompt = _load_qa_prompt(current_unit.concept)
26
+ messages = [
27
+ {"role": "system", "content": prompt},
28
+ {"role": "user", "content": f"{context}\n\nStudent's question: {question}"},
29
+ ]
30
+
31
+ try:
32
+ raw = llm_fn(messages, call_type="qa")
33
+ except LLMError as e:
34
+ log.error("Q&A LLM call failed: %s", e)
35
+ return f"Sorry — could not generate an answer right now. ({e})"
36
+
37
+ answer_text = raw.strip()
38
+ _append_exchange(session, current_unit, question, answer_text, position_seconds)
39
+ return answer_text
40
+
41
+
42
+ def _build_context(
43
+ current_unit: TeachingUnit,
44
+ all_chunks: list[Chunk],
45
+ session: SessionLog,
46
+ ) -> str:
47
+ chunk_map = {c.chunk_id: c for c in all_chunks}
48
+
49
+ current_chunks = [chunk_map[s] for s in current_unit.source_sections if s in chunk_map]
50
+
51
+ recent = session.exchanges[-3:] if session.exchanges else []
52
+
53
+ parts: list[str] = []
54
+
55
+ if current_chunks:
56
+ parts.append("=== Source Content ===")
57
+ for chunk in current_chunks:
58
+ parts.append(f"[{chunk.breadcrumb}]\n{chunk.text}")
59
+
60
+ if recent:
61
+ parts.append("\n=== Prior Questions This Session ===")
62
+ for ex in recent:
63
+ parts.append(f"Q: {ex.question}\nA: {ex.answer}")
64
+
65
+ return "\n\n".join(parts) if parts else "No source content available."
66
+
67
+
68
+ def _append_exchange(
69
+ session: SessionLog,
70
+ unit: TeachingUnit,
71
+ question: str,
72
+ answer: str,
73
+ position_seconds: int,
74
+ ) -> None:
75
+ exchange = QAExchange(
76
+ id=len(session.exchanges) + 1,
77
+ unit_number=unit.unit,
78
+ unit_concept=unit.concept,
79
+ position_seconds=position_seconds,
80
+ question=question,
81
+ answer=answer,
82
+ source_sections=unit.source_sections,
83
+ timestamp=datetime.utcnow().isoformat(),
84
+ )
85
+ session.exchanges.append(exchange)
86
+ _save_session(session)
87
+
88
+
89
+ def _save_session(session: SessionLog) -> None:
90
+ from dataclasses import asdict
91
+
92
+ path = Path("tutorial.session.json")
93
+ with open(path, "w", encoding="utf-8") as f:
94
+ json.dump(asdict(session), f, indent=2, ensure_ascii=False)
95
+
96
+
97
+ def _load_qa_prompt(concept: str) -> str:
98
+ try:
99
+ template = QA_PROMPT_PATH.read_text(encoding="utf-8")
100
+ return template.replace("{concept}", concept)
101
+ except FileNotFoundError:
102
+ return (
103
+ f"Answer the student's question about: {concept}. "
104
+ "Be concise, cite sources, end with a follow-up question."
105
+ )
@@ -0,0 +1,2 @@
1
+ pytest>=7.0.0
2
+ pytest-asyncio>=0.21.0
tutor/requirements.txt ADDED
@@ -0,0 +1,12 @@
1
+ openai>=1.0.0
2
+ groq>=0.9.0
3
+ edge-tts>=6.1.9
4
+ pydub>=0.25.1
5
+ pygame>=2.5.0
6
+ tqdm>=4.66.0
7
+ readchar>=4.0.0
8
+ python-dotenv>=1.0.0
9
+ markdown-it-py>=3.0.0
10
+ audioop-lts>=0.2.1 # pydub compatibility for Python 3.13+
11
+ jinja2>=3.1
12
+ playwright>=1.44