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.
- learnx_cli-0.3.0.dist-info/METADATA +240 -0
- learnx_cli-0.3.0.dist-info/RECORD +131 -0
- learnx_cli-0.3.0.dist-info/WHEEL +4 -0
- learnx_cli-0.3.0.dist-info/entry_points.txt +2 -0
- tutor/.env copy.example +4 -0
- tutor/__init__.py +0 -0
- tutor/__main__.py +4 -0
- tutor/assets/__init__.py +5 -0
- tutor/assets/html/fonts/Inter-Bold.woff2 +0 -0
- tutor/assets/html/fonts/Inter-Regular.woff2 +0 -0
- tutor/assets/html/fonts/Inter-SemiBold.woff2 +0 -0
- tutor/assets/html/fonts/JetBrainsMono-Regular.woff2 +0 -0
- tutor/assets/html/highlight-java.min.js +2 -0
- tutor/assets/html/highlight-javascript.min.js +2 -0
- tutor/assets/html/highlight-python.min.js +2 -0
- tutor/assets/html/highlight.min.js +17 -0
- tutor/assets/html/mermaid.min.js +31 -0
- tutor/assets/html/slide_base.css +464 -0
- tutor/assets/html/theme-learnx-dark.css +12 -0
- tutor/audio/__init__.py +0 -0
- tutor/audio/audio_builder.py +143 -0
- tutor/audio/sanitizer.py +9 -0
- tutor/audio/tts_renderer.py +54 -0
- tutor/cli/__init__.py +0 -0
- tutor/cli/commands.py +391 -0
- tutor/cli/logo.py +21 -0
- tutor/cli/playback_commands.py +239 -0
- tutor/cli/shell.py +91 -0
- tutor/cli/shell_context.py +18 -0
- tutor/cli/theme.py +39 -0
- tutor/cli/video_commands.py +123 -0
- tutor/config.py +122 -0
- tutor/conftest.py +5 -0
- tutor/constants.py +82 -0
- tutor/exceptions.py +26 -0
- tutor/generation/__init__.py +0 -0
- tutor/generation/assembler.py +81 -0
- tutor/generation/curriculum.py +97 -0
- tutor/generation/dialogue.py +172 -0
- tutor/generation/narrator.py +122 -0
- tutor/generation/segment_parser.py +223 -0
- tutor/generation/segment_planner.py +200 -0
- tutor/generation/visual_planner.py +205 -0
- tutor/infra/__init__.py +0 -0
- tutor/infra/llm.py +152 -0
- tutor/ingestion/__init__.py +0 -0
- tutor/ingestion/chunker.py +171 -0
- tutor/ingestion/doc_analyzer.py +41 -0
- tutor/ingestion/parse_content.py +19 -0
- tutor/ingestion/summarizer.py +51 -0
- tutor/inspector.py +117 -0
- tutor/llm_config.toml +58 -0
- tutor/models.py +147 -0
- tutor/player/__init__.py +0 -0
- tutor/player/input_handler.py +45 -0
- tutor/player/player.py +308 -0
- tutor/player/player_display.py +117 -0
- tutor/prompts/curriculum.txt +67 -0
- tutor/prompts/dialogue.txt +62 -0
- tutor/prompts/narrate.txt +34 -0
- tutor/prompts/qa.txt +17 -0
- tutor/prompts/summarize.txt +9 -0
- tutor/prompts/visual.txt +60 -0
- tutor/prompts/visual_v3.txt +91 -0
- tutor/qa/__init__.py +0 -0
- tutor/qa/qa.py +105 -0
- tutor/requirements-dev.txt +2 -0
- tutor/requirements.txt +12 -0
- tutor/sample_docs/headingless_large.md +1 -0
- tutor/sample_docs/headingless_test.md +1 -0
- tutor/sample_docs/java-basics.md +78 -0
- tutor/tests/__init__.py +0 -0
- tutor/tests/audio/__init__.py +0 -0
- tutor/tests/audio/test_audio_builder.py +106 -0
- tutor/tests/audio/test_sanitizer.py +41 -0
- tutor/tests/cli/__init__.py +0 -0
- tutor/tests/cli/test_commands.py +67 -0
- tutor/tests/cli/test_video_commands.py +190 -0
- tutor/tests/e2e/README.md +61 -0
- tutor/tests/e2e/__init__.py +0 -0
- tutor/tests/e2e/conftest.py +117 -0
- tutor/tests/e2e/fixtures/README.md +17 -0
- tutor/tests/e2e/fixtures/sample.md +13 -0
- tutor/tests/e2e/test_audio_quality.py +40 -0
- tutor/tests/e2e/test_av_sync.py +56 -0
- tutor/tests/e2e/test_pipeline_smoke.py +37 -0
- tutor/tests/e2e/test_slide_render.py +72 -0
- tutor/tests/e2e/test_video_streams.py +104 -0
- tutor/tests/generation/__init__.py +0 -0
- tutor/tests/generation/conftest.py +134 -0
- tutor/tests/generation/test_assembler.py +64 -0
- tutor/tests/generation/test_curriculum.py +107 -0
- tutor/tests/generation/test_narrator.py +165 -0
- tutor/tests/generation/test_segment_edge_cases.py +280 -0
- tutor/tests/generation/test_segment_planner.py +324 -0
- tutor/tests/generation/test_visual_planner.py +319 -0
- tutor/tests/ingestion/__init__.py +0 -0
- tutor/tests/ingestion/test_chunker.py +94 -0
- tutor/tests/ingestion/test_doc_analyzer.py +51 -0
- tutor/tests/player/__init__.py +0 -0
- tutor/tests/player/test_player_states.py +88 -0
- tutor/tests/test_assets.py +39 -0
- tutor/tests/test_models_visual.py +180 -0
- tutor/tests/visual/__init__.py +0 -0
- tutor/tests/visual/test_beat_timer.py +321 -0
- tutor/tests/visual/test_pipeline_integration.py +178 -0
- tutor/tests/visual/test_slide_renderer.py +298 -0
- tutor/tests/visual/test_subtitle_writer.py +165 -0
- tutor/tests/visual/test_video_assembler.py +108 -0
- tutor/tests/visual/test_visual_pipeline.py +270 -0
- tutor/tutor.py +365 -0
- tutor/visual/__init__.py +213 -0
- tutor/visual/beat_timer.py +222 -0
- tutor/visual/slide_renderer.py +236 -0
- tutor/visual/subtitle_writer.py +187 -0
- tutor/visual/templates/_base.html.j2 +40 -0
- tutor/visual/templates/analogy.html.j2 +21 -0
- tutor/visual/templates/callout.html.j2 +10 -0
- tutor/visual/templates/code_example.html.j2 +12 -0
- tutor/visual/templates/comparison.html.j2 +28 -0
- tutor/visual/templates/decision_guide.html.j2 +37 -0
- tutor/visual/templates/definition.html.j2 +13 -0
- tutor/visual/templates/diagram.html.j2 +11 -0
- tutor/visual/templates/hook_question.html.j2 +17 -0
- tutor/visual/templates/key_insight.html.j2 +9 -0
- tutor/visual/templates/memory_hook.html.j2 +7 -0
- tutor/visual/templates/outro.html.j2 +16 -0
- tutor/visual/templates/question_prompt.html.j2 +13 -0
- tutor/visual/templates/step_sequence.html.j2 +14 -0
- tutor/visual/templates/title_card.html.j2 +12 -0
- 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
|
+
)
|
tutor/requirements.txt
ADDED