Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions canvigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def print_help():
ch.print_global_help(task_groups)


def _run_assignment_task(task, course, canvas, canv_config, dry_run, auto_grade_flag):
def _run_assignment_task(task, course, canvas, canv_config, dry_run, auto_grade_flag, save_transcript=False):
"""Dispatch the media-recording-assignment tasks."""
import canvigator_assignment as ca
if task == 'create-media-recording-assignment':
Expand All @@ -59,7 +59,7 @@ def _run_assignment_task(task, course, canvas, canv_config, dry_run, auto_grade_
if task == 'analyze-media-recordings':
cassign.analyzeRecordings()
else:
cassign.getMediaRecordings(auto_grade=auto_grade_flag, dry_run=dry_run)
cassign.getMediaRecordings(auto_grade=auto_grade_flag, dry_run=dry_run, save_transcript=save_transcript)


# Course-level tasks that need only `course` + a few primitives. Kept in a
Expand Down Expand Up @@ -87,7 +87,7 @@ def _run_course_task(task, course, canv_config, n_days, cloud_questions):
cd.prepClassDigest(course, days=n_days, cloud_questions=cloud_questions)


def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=False):
def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=False, choose_model=False, save_transcript=False):
"""Dispatch a quiz-level task to the appropriate method."""
if task == 'get-quiz-questions':
quiz.getQuizQuestions(tag=tag)
Expand All @@ -96,7 +96,14 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=False):
quiz.getAllSubmissionsAndEvents()
quiz.generateFirstAttemptHistograms()
elif task == 'assess-replies':
quiz.assessFollowUpReplies(reply_window_days=reply_window_days)
chosen_model = None
if choose_model:
import canvigator_llm
chosen_model = canvigator_llm.pickLocalGemmaModel()
quiz.assessFollowUpReplies(
reply_window_days=reply_window_days, chosen_model=chosen_model,
save_transcript=save_transcript,
)
elif task == 'send-quiz-reminder':
quiz.sendQuizReminders(dry_run=dry_run, send_all=send_all)
elif task == 'send-follow-up-question':
Expand Down Expand Up @@ -133,6 +140,8 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=False):
'-n': '--days',
'-q': '--cloud-questions',
'-s': '--send-all',
'-M': '--choose-model',
'-S': '--save-transcript',
'-h': '--help',
}
args = [_SHORT_TO_LONG.get(a, a) for a in sys.argv[1:]]
Expand Down Expand Up @@ -173,6 +182,14 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=False):
if cloud_questions_flag:
args.remove('--cloud-questions')

choose_model_flag = '--choose-model' in args
if choose_model_flag:
args.remove('--choose-model')

save_transcript_flag = '--save-transcript' in args
if save_transcript_flag:
args.remove('--save-transcript')

crn = None
if '--crn' in args:
crn_idx = args.index('--crn')
Expand Down Expand Up @@ -358,7 +375,7 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=False):
course.sendAllQuizReminders(dry_run=dry_run, send_all=send_all)

elif task in ('create-media-recording-assignment', 'get-media-recordings', 'analyze-media-recordings'):
_run_assignment_task(task, course, canvas, canv_config, dry_run, auto_grade_flag)
_run_assignment_task(task, course, canvas, canv_config, dry_run, auto_grade_flag, save_transcript=save_transcript_flag)

elif task == 'generate-follow-up-questions':
# Driven by a pre-selected tagged-questions CSV, not a Canvas quiz
Expand Down Expand Up @@ -392,6 +409,9 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=False):
print(f"\nSelected quiz: {quiz_choice.title}")
skip = task in ('get-quiz-questions', 'assess-replies', 'send-follow-up-assessments')
quiz = cq.CanvigatorQuiz(canvas, course, quiz_choice, canv_config, verbose=False, skip_student_data=skip)
_run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=send_all)
_run_quiz_task(
task, quiz, dry_run, tag, reply_window_days,
send_all=send_all, choose_model=choose_model_flag, save_transcript=save_transcript_flag,
)

print("\n*** Done ***\n")
4 changes: 2 additions & 2 deletions canvigator_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ def _writeRecordingsCsv(self, rows):
df.to_csv(csv_path, index=False)
return csv_path, df

def getMediaRecordings(self, auto_grade=False, dry_run=False):
def getMediaRecordings(self, auto_grade=False, dry_run=False, save_transcript=False):
"""Fetch every submission, transcribe locally, optionally review and grade interactively.

For each submitter, ffmpeg pulls the media URL directly (handling DASH
Expand Down Expand Up @@ -410,7 +410,7 @@ def getMediaRecordings(self, auto_grade=False, dry_run=False):
rows.append(self._buildRecordingRow(sub, student_name, audio_rel, ""))
continue
print(" transcribing...", end="", flush=True)
transcript = transcribe_audio(str(local_path), client, audio_model)
transcript = transcribe_audio(str(local_path), client, audio_model, save_transcript=save_transcript)
chars = len(transcript)
print(f" done ({chars} chars)")

Expand Down
6 changes: 6 additions & 0 deletions canvigator_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,12 @@ def exportAnonymizedData(data_path):
n_total = len(csv_files)
print(f"Exported {n_total} file{'s' if n_total != 1 else ''} ({n_anonymized} anonymized) to {anon_dir.name}/")
logger.info(f"Exported {n_total} anonymized files to {zip_path}.zip")
print(
"\nNOTE: This export is NOT automatically privacy-safe. Before sharing, "
"manually verify the CSVs contain no student IDs or names, and no "
"open-ended text (transcripts, replies, feedback, conversation subjects), "
"audio, or images."
)


def _renderQuestionPreview(question_dict):
Expand Down
19 changes: 17 additions & 2 deletions canvigator_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@
"Requires a matching --dry-run run on the same task within the last "
"10 minutes (otherwise refuses). Mutually exclusive with --dry-run.",
),
'--choose-model': (
'-M', None,
"Interactively pick which local gemma4 model to use for grading; "
"defaults to OLLAMA_MODEL. Useful for trading some assessment quality "
"for throughput (e.g. swap gemma4:31b for gemma4:e4b).",
),
'--save-transcript': (
'-S', None,
"Save each audio transcription as a sibling .txt file alongside the "
"audio (debug aid for noisy or mistranscribed recordings — listen + "
"read side by side).",
),
}


Expand Down Expand Up @@ -571,10 +583,12 @@
"data/<course>/quiz<id>_followup_assessments.csv (persistent; "
"instructor edits 'feedback' before send-follow-up-assessments).",
],
'flags': ['--reply-window-days'],
'flags': ['--reply-window-days', '--choose-model', '--save-transcript'],
'examples': [
"python canvigator.py --crn 12345 assess-replies",
"python canvigator.py --crn 12345 -w 10 assess-replies",
"python canvigator.py --crn 12345 -M assess-replies",
"python canvigator.py --crn 12345 -S assess-replies",
],
'run_before': ['send-follow-up-question'],
'run_after': ['send-follow-up-assessments', 'prep-class-digest'],
Expand Down Expand Up @@ -675,11 +689,12 @@
"audio_path, transcript, transcribed_at, grade, graded_at).",
"Canvas: posted_grade on each submission (skipped under --dry-run).",
],
'flags': ['--auto-grade', '--dry-run'],
'flags': ['--auto-grade', '--dry-run', '--save-transcript'],
'examples': [
"python canvigator.py --crn 12345 get-media-recordings",
"python canvigator.py --crn 12345 --auto-grade --dry-run "
"get-media-recordings",
"python canvigator.py --crn 12345 -S get-media-recordings",
],
'run_before': ['create-media-recording-assignment'],
'run_after': ['analyze-media-recordings', 'prep-class-digest'],
Expand Down
Loading
Loading