Skip to content

Commit eb60026

Browse files
committed
note generator v0.4
1 parent 0409bb4 commit eb60026

4 files changed

Lines changed: 439 additions & 120 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ dist/
2727
build/
2828
*.spec.bak
2929

30+
# ── User config ───────────────────────────────────────────────────────────────
31+
config.json
32+
3033
# ── Misc ──────────────────────────────────────────────────────────────────────
3134
*.egg-info/
3235
.DS_Store

downloader.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,21 @@
3636
# ── Configuration ──────────────────────────────────────────────────────────────
3737

3838
PROJECT_DIR = Path(__file__).parent
39-
CANVAS_URL = "https://canvas.nus.edu.sg"
39+
MANIFEST_FILE = PROJECT_DIR / "manifest.json"
40+
41+
# ── User-configurable connection settings (set via GUI Settings page) ──────────
42+
_config_file = PROJECT_DIR / "config.json"
43+
_config: dict = json.load(open(_config_file)) if _config_file.exists() else {}
44+
45+
CANVAS_URL = _config.get("CANVAS_URL", "")
46+
PANOPTO_HOST = _config.get("PANOPTO_HOST", "")
47+
4048
_canvas_token_file = PROJECT_DIR / "canvas_token.txt"
41-
CANVAS_TOKEN = (
49+
CANVAS_TOKEN = (
4250
_canvas_token_file.read_text().strip()
4351
if _canvas_token_file.exists() else
4452
os.environ.get("CANVAS_TOKEN", "")
4553
)
46-
PANOPTO_HOST = "mediaweb.ap.panopto.com"
47-
MANIFEST_FILE = PROJECT_DIR / "manifest.json"
4854
SIZE_LIMIT = 1 * 1024 ** 3 # 1 GB
4955

5056
SKIP_COURSE_KEYWORDS = [

gui.py

Lines changed: 209 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,23 @@ def _read_constant(script_key: str, name: str) -> str:
9999

100100

101101
def _write_constant(script_key: str, name: str, new_display_val: str) -> bool:
102-
"""Write a constant back to its source file, preserving the original quote style."""
102+
"""Write a constant back to its source file with the correct Python literal type."""
103103
path = SCRIPTS[script_key]
104104
try:
105105
src = path.read_text(errors="ignore")
106106

107107
def _replacer(m: re.Match) -> str:
108-
old_raw = m.group(2).lstrip()
109-
# Re-quote if original was a string literal
110-
if old_raw.startswith(('"', "'")):
111-
return m.group(1) + f'"{new_display_val}"'
112-
return m.group(1) + new_display_val
108+
# None keyword → bare None
109+
if new_display_val == "None":
110+
return m.group(1) + "None"
111+
# Numeric → bare (int or float)
112+
try:
113+
float(new_display_val)
114+
return m.group(1) + new_display_val
115+
except ValueError:
116+
pass
117+
# String → always quoted
118+
return m.group(1) + f'"{new_display_val}"'
113119

114120
new_src, n = re.subn(
115121
rf'^({name}\s*=\s*)([^\n#]+)',
@@ -976,11 +982,70 @@ def _snack(msg: str, ok: bool = True) -> None:
976982
page.snack_bar.open = True
977983
page.update()
978984

985+
def _key_row(label: str, tf: ft.TextField, on_save) -> ft.Row:
986+
return ft.Row(controls=[
987+
ft.Text(label, size=12, color=ft.Colors.WHITE, width=140),
988+
tf,
989+
ft.FilledButton("Save", icon=ft.Icons.SAVE_OUTLINED,
990+
on_click=on_save, style=ft.ButtonStyle(
991+
bgcolor=C_PRIMARY, color=ft.Colors.BLACK)),
992+
], spacing=8)
993+
994+
# ── Connection settings (config.json) ────────────────────────────────────
995+
996+
config_file = PROJECT_DIR / "config.json"
997+
998+
def _load_config() -> dict:
999+
return json.load(open(config_file)) if config_file.exists() else {}
1000+
1001+
def _save_config(key: str, value: str) -> None:
1002+
cfg = _load_config()
1003+
cfg[key] = value
1004+
with open(config_file, "w") as f:
1005+
json.dump(cfg, f, indent=2)
1006+
1007+
_cfg = _load_config()
1008+
tf_canvas_url = ft.TextField(
1009+
value=_cfg.get("CANVAS_URL", ""),
1010+
hint_text="https://canvas.yourschool.edu", expand=True, dense=True,
1011+
bgcolor=C_OUTPUT_BG, border_color=C_PRIMARY, text_size=12,
1012+
)
1013+
tf_panopto = ft.TextField(
1014+
value=_cfg.get("PANOPTO_HOST", ""),
1015+
hint_text="mediaweb.ap.panopto.com", expand=True, dense=True,
1016+
bgcolor=C_OUTPUT_BG, border_color=C_PRIMARY, text_size=12,
1017+
)
1018+
1019+
def _save_canvas_url(_):
1020+
try:
1021+
_save_config("CANVAS_URL", tf_canvas_url.value.strip())
1022+
_snack("Canvas URL saved.")
1023+
except Exception as e:
1024+
_snack(f"Error: {e}", ok=False)
1025+
1026+
def _save_panopto(_):
1027+
try:
1028+
_save_config("PANOPTO_HOST", tf_panopto.value.strip())
1029+
_snack("Panopto host saved.")
1030+
except Exception as e:
1031+
_snack(f"Error: {e}", ok=False)
1032+
1033+
conn_card = _card(ft.Column(controls=[
1034+
ft.Text("Connection", size=13,
1035+
weight=ft.FontWeight.BOLD, color=ft.Colors.WHITE),
1036+
ft.Text("Saved to config.json in the project directory.",
1037+
size=11, color=ft.Colors.with_opacity(0.5, ft.Colors.WHITE)),
1038+
ft.Container(height=4),
1039+
_key_row("Canvas URL", tf_canvas_url, _save_canvas_url),
1040+
_key_row("Panopto Host", tf_panopto, _save_panopto),
1041+
], spacing=10))
1042+
9791043
# ── API Keys ──────────────────────────────────────────────────────────────
9801044

9811045
canvas_file = PROJECT_DIR / "canvas_token.txt"
9821046
openai_file = PROJECT_DIR / "openai_api.txt"
9831047
anthropic_file = PROJECT_DIR / "anthropic_key.txt"
1048+
gemini_file = PROJECT_DIR / "gemini_api.txt"
9841049

9851050
tf_canvas = ft.TextField(
9861051
value=canvas_file.read_text().strip() if canvas_file.exists() else "",
@@ -1000,6 +1065,12 @@ def _snack(msg: str, ok: bool = True) -> None:
10001065
hint_text="sk-ant-… (no default)", expand=True, dense=True,
10011066
bgcolor=C_OUTPUT_BG, border_color=C_PRIMARY, text_size=12,
10021067
)
1068+
tf_gemini = ft.TextField(
1069+
value=gemini_file.read_text().strip() if gemini_file.exists() else "",
1070+
password=True, can_reveal_password=True,
1071+
hint_text="AIza… (Google AI Studio key, no default)", expand=True, dense=True,
1072+
bgcolor=C_OUTPUT_BG, border_color=C_PRIMARY, text_size=12,
1073+
)
10031074

10041075
def _save_canvas(_):
10051076
try:
@@ -1022,14 +1093,12 @@ def _save_anthropic(_):
10221093
except Exception as e:
10231094
_snack(f"Error: {e}", ok=False)
10241095

1025-
def _key_row(label: str, tf: ft.TextField, on_save) -> ft.Row:
1026-
return ft.Row(controls=[
1027-
ft.Text(label, size=12, color=ft.Colors.WHITE, width=140),
1028-
tf,
1029-
ft.FilledButton("Save", icon=ft.Icons.SAVE_OUTLINED,
1030-
on_click=on_save, style=ft.ButtonStyle(
1031-
bgcolor=C_PRIMARY, color=ft.Colors.BLACK)),
1032-
], spacing=8)
1096+
def _save_gemini(_):
1097+
try:
1098+
gemini_file.write_text(tf_gemini.value.strip())
1099+
_snack("Gemini key saved to gemini_api.txt.")
1100+
except Exception as e:
1101+
_snack(f"Error: {e}", ok=False)
10331102

10341103
keys_card = _card(ft.Column(controls=[
10351104
ft.Text("API Keys & Credentials", size=13,
@@ -1040,6 +1109,7 @@ def _key_row(label: str, tf: ft.TextField, on_save) -> ft.Row:
10401109
_key_row("Canvas Token", tf_canvas, _save_canvas),
10411110
_key_row("OpenAI API Key", tf_openai, _save_openai),
10421111
_key_row("Anthropic API Key",tf_anthropic, _save_anthropic),
1112+
_key_row("Gemini API Key", tf_gemini, _save_gemini),
10431113
ft.Row(controls=[
10441114
ft.Text("Project dir", size=11,
10451115
color=ft.Colors.with_opacity(0.5, ft.Colors.WHITE)),
@@ -1056,55 +1126,143 @@ def _key_row(label: str, tf: ft.TextField, on_save) -> ft.Row:
10561126
"generate": ft.Colors.PURPLE_200,
10571127
}
10581128

1129+
# options: list of (display_label, stored_value) pairs, or None for free text
10591130
CONSTANTS = [
1060-
# script_key name description default
1061-
("transcribe", "WHISPER_MODEL_SIZE", "Whisper model variant", "large-v3"),
1062-
("transcribe", "WHISPER_LANGUAGE", "Language (None = auto)", "None"),
1063-
("align", "EMBED_MODEL", "Sentence-transformer", "all-mpnet-base-v2"),
1064-
("align", "CONTEXT_SEC", "Context window (s)", "30"),
1065-
("align", "OFF_SLIDE_THRESHOLD", "Off-slide cosine cutoff","0.28"),
1066-
("align", "PRIOR_SIGMA", "Temporal prior σ", "5"),
1067-
("generate", "NOTE_MODEL", "Note generation LLM", "gpt-5.1"),
1068-
("generate", "VERIFY_MODEL", "Verification LLM", "gpt-4.1-mini"),
1069-
("generate", "DETAIL_LEVEL", "Default detail level", "8"),
1070-
("generate", "CHAPTER_SIZE", "Slides per GPT call", "15"),
1071-
("generate", "QUALITY_TARGET", "Self-score target", "8.0"),
1131+
# script name desc default options
1132+
("transcribe", "WHISPER_MODEL_SIZE", "Whisper model variant", "large-v3", [
1133+
("tiny", "tiny"),
1134+
("base", "base"),
1135+
("small", "small"),
1136+
("medium", "medium"),
1137+
("large", "large"),
1138+
("large-v2", "large-v2"),
1139+
("large-v3", "large-v3"),
1140+
("large-v3-turbo", "large-v3-turbo"),
1141+
("distil-large-v3", "distil-large-v3"),
1142+
]),
1143+
("transcribe", "WHISPER_LANGUAGE", "Transcription language", "None", [
1144+
("Auto-detect", "None"),
1145+
("English", "en"),
1146+
("Chinese", "zh"),
1147+
("Japanese", "ja"),
1148+
("Korean", "ko"),
1149+
("French", "fr"),
1150+
("German", "de"),
1151+
("Spanish", "es"),
1152+
]),
1153+
("align", "EMBED_MODEL", "Sentence-transformer model", "all-mpnet-base-v2", [
1154+
("all-mpnet-base-v2 (best quality)", "all-mpnet-base-v2"),
1155+
("all-MiniLM-L12-v2 (balanced)", "all-MiniLM-L12-v2"),
1156+
("all-MiniLM-L6-v2 (fast)", "all-MiniLM-L6-v2"),
1157+
("paraphrase-multilingual-mpnet-base-v2", "paraphrase-multilingual-mpnet-base-v2"),
1158+
]),
1159+
("align", "CONTEXT_SEC", "Context window (s)", "30", None),
1160+
("align", "OFF_SLIDE_THRESHOLD", "Off-slide cosine cutoff", "0.28", None),
1161+
("align", "PRIOR_SIGMA", "Temporal prior σ", "5", None),
1162+
("generate", "NOTE_LANGUAGE", "Note language", "en", [
1163+
("English", "en"),
1164+
("Chinese (中文)", "zh"),
1165+
]),
1166+
("generate", "NOTE_MODEL", "Note generation LLM", "gpt-5.1", [
1167+
# OpenAI
1168+
("gpt-5.1", "gpt-5.1"),
1169+
("gpt-5.2", "gpt-5.2"),
1170+
("gpt-5.3", "gpt-5.3"),
1171+
("gpt-5.4", "gpt-5.4"),
1172+
("gpt-4.1", "gpt-4.1"),
1173+
("gpt-4.1-mini", "gpt-4.1-mini"),
1174+
("o3", "o3"),
1175+
("o1", "o1"),
1176+
# Gemini
1177+
("Gemini 2.5 Pro", "gemini-2.5-pro"),
1178+
("Gemini 2.5 Flash", "gemini-2.5-flash"),
1179+
("Gemini 2.0 Flash", "gemini-2.0-flash"),
1180+
("Gemini 1.5 Pro", "gemini-1.5-pro"),
1181+
# Anthropic
1182+
("Claude Opus 4.6", "claude-opus-4-6"),
1183+
("Claude Sonnet 4.6", "claude-sonnet-4-6"),
1184+
("Claude Sonnet 4.5", "claude-sonnet-4-5"),
1185+
("Claude Sonnet 3.5", "claude-3-5-sonnet-20241022"),
1186+
("Claude Haiku 4.5", "claude-haiku-4-5-20251001"),
1187+
]),
1188+
("generate", "VERIFY_MODEL", "Verification LLM", "gpt-4.1-mini", [
1189+
# OpenAI
1190+
("gpt-4.1-mini", "gpt-4.1-mini"),
1191+
("gpt-4.1", "gpt-4.1"),
1192+
("gpt-5.1", "gpt-5.1"),
1193+
# Gemini
1194+
("Gemini 2.5 Flash", "gemini-2.5-flash"),
1195+
("Gemini 2.0 Flash", "gemini-2.0-flash"),
1196+
# Anthropic
1197+
("Claude Haiku 4.5", "claude-haiku-4-5-20251001"),
1198+
("Claude Sonnet 3.5", "claude-3-5-sonnet-20241022"),
1199+
("Claude Sonnet 4.5", "claude-sonnet-4-5"),
1200+
]),
1201+
("generate", "DETAIL_LEVEL", "Default detail level", "8", None),
1202+
("generate", "CHAPTER_SIZE", "Slides per GPT call", "15", None),
1203+
("generate", "QUALITY_TARGET", "Self-score target", "8.0", None),
10721204
]
10731205

1074-
def _const_row(script: str, name: str, desc: str, default: str) -> ft.Row:
1075-
cur = _read_constant(script, name)
1076-
tf = ft.TextField(
1077-
value=cur, expand=True, dense=True,
1078-
bgcolor=C_OUTPUT_BG, border_color=_SCRIPT_COLORS.get(script, C_PRIMARY),
1079-
text_size=12, content_padding=ft.padding.symmetric(horizontal=8, vertical=6),
1080-
)
1206+
def _const_row(script: str, name: str, desc: str, default: str,
1207+
options: list[tuple[str, str]] | None = None) -> ft.Row:
1208+
cur = _read_constant(script, name)
1209+
accent = _SCRIPT_COLORS.get(script, C_PRIMARY)
1210+
1211+
if options:
1212+
ctrl = ft.Dropdown(
1213+
value=cur if any(v == cur for _, v in options) else default,
1214+
options=[ft.dropdown.Option(key=val, text=label) for label, val in options],
1215+
expand=True, dense=True,
1216+
bgcolor=C_OUTPUT_BG, border_color=accent, text_size=12,
1217+
content_padding=ft.padding.symmetric(horizontal=8, vertical=4),
1218+
)
1219+
1220+
def _save(_, c=ctrl, s=script, n=name):
1221+
if _write_constant(s, n, c.value or default):
1222+
_snack(f"{n} saved.")
1223+
else:
1224+
_snack(f"Failed to save {n}.", ok=False)
10811225

1082-
def _save(_):
1083-
if _write_constant(script, name, tf.value.strip()):
1084-
_snack(f"{name} saved.")
1085-
else:
1086-
_snack(f"Failed to save {name}.", ok=False)
1226+
def _reset(_, c=ctrl, s=script, n=name, d=default):
1227+
c.value = d
1228+
c.update()
1229+
if _write_constant(s, n, d):
1230+
_snack(f"{n} reset to default.")
1231+
else:
1232+
_snack(f"Failed to reset {n}.", ok=False)
1233+
1234+
else:
1235+
ctrl = ft.TextField(
1236+
value=cur, expand=True, dense=True,
1237+
bgcolor=C_OUTPUT_BG, border_color=accent,
1238+
text_size=12, content_padding=ft.padding.symmetric(horizontal=8, vertical=6),
1239+
)
10871240

1088-
def _reset(_):
1089-
tf.value = default
1090-
tf.update()
1091-
if _write_constant(script, name, default):
1092-
_snack(f"{name} reset to default ({default}).")
1093-
else:
1094-
_snack(f"Failed to reset {name}.", ok=False)
1241+
def _save(_, c=ctrl, s=script, n=name):
1242+
if _write_constant(s, n, c.value.strip()):
1243+
_snack(f"{n} saved.")
1244+
else:
1245+
_snack(f"Failed to save {n}.", ok=False)
1246+
1247+
def _reset(_, c=ctrl, s=script, n=name, d=default):
1248+
c.value = d
1249+
c.update()
1250+
if _write_constant(s, n, d):
1251+
_snack(f"{n} reset to default ({d}).")
1252+
else:
1253+
_snack(f"Failed to reset {n}.", ok=False)
10951254

10961255
return ft.Row(controls=[
10971256
ft.Container(
1098-
ft.Text(script, size=10, color=_SCRIPT_COLORS.get(script, C_PRIMARY),
1099-
weight=ft.FontWeight.BOLD),
1257+
ft.Text(script, size=10, color=accent, weight=ft.FontWeight.BOLD),
11001258
width=74, padding=ft.padding.symmetric(horizontal=4, vertical=2),
11011259
border_radius=4,
1102-
bgcolor=ft.Colors.with_opacity(0.12, _SCRIPT_COLORS.get(script, C_PRIMARY)),
1260+
bgcolor=ft.Colors.with_opacity(0.12, accent),
11031261
),
11041262
ft.Text(desc, size=12, color=ft.Colors.WHITE, width=190),
11051263
ft.Text(name, size=11, color=ft.Colors.with_opacity(0.5, ft.Colors.WHITE),
11061264
width=160, italic=True),
1107-
tf,
1265+
ctrl,
11081266
ft.FilledButton(
11091267
"Save", icon=ft.Icons.SAVE_OUTLINED, on_click=_save,
11101268
style=ft.ButtonStyle(bgcolor=C_PRIMARY, color=ft.Colors.BLACK),
@@ -1113,7 +1271,7 @@ def _reset(_):
11131271
"Default", on_click=_reset,
11141272
style=ft.ButtonStyle(side=ft.BorderSide(1, C_SECONDARY),
11151273
color=C_SECONDARY),
1116-
tooltip=f"Reset to: {default}",
1274+
tooltip=f"Default: {default}",
11171275
),
11181276
], spacing=8)
11191277

@@ -1132,6 +1290,7 @@ def _reset(_):
11321290
controls=[
11331291
ft.Column(controls=[
11341292
_section_title("Settings", ft.Icons.SETTINGS_OUTLINED),
1293+
conn_card,
11351294
keys_card,
11361295
const_card,
11371296
], spacing=12, scroll=ft.ScrollMode.AUTO, expand=True),

0 commit comments

Comments
 (0)