Tools and systems
This page describes Chris's practices for organizing filesystems. These are the result of over a decade of iteration, so there might be some wisdom baked in. But use whatever works for you.
Principles
I agree with the principles of Kieran Healy's Plain-text social science. For your own sake, to be a good collaborator, and to be a responsible steward of other people's private data, you should give some thought to how you work on and store information. You should be aware of and comply with UB's Data Risk Classification Policies.
Filesystems
I try to maintain a consistent layout for my files across systems:
- Laptop
- Google Drive. Good for collaboration. If you choose to use Drive for your writing, don't use your university email address. Your academic career will likely last longer than your time at UB.
- Box. You can't organize shared content (e.g. move it into a folder), so it turns into a mess. But it's a place to store large files. I use it for shared research corpora (e.g. videos) and nothing else, unless I don't have a choice.
Layout
At a top level, I think it makes sense to distinguish your doctoral work from your previous professional work and from your future as an academic, if that's your thing. Here's a partial example of Chris's laptop filesystem.
~/Documents
├── Academic
│ ├── Career
│ │ └── 2019 Job Search
│ ├── Consulting
│ ├── Identity
│ │ └── CV
│ ├── Network
│ ├── Research
│ │ ├── Grants
│ │ │ ├── 2022 NSF CS for All
│ │ │ └── 2022 NSF CAREER
│ │ ├── Projects
│ │ │ ├── 2022 NSF CS for All
│ │ │ ├── Making With Code
│ │ │ ├── Minecraft Utopia
│ │ │  └── Unfold Studio
│ │ └── Publications
│ │   ├── 2021 AERA identity as interface
│ │   ├── 2021 CS Teaching Book Chapter
│ │   ├── 2022 CSCL Minecraft
│ │   └── 2022 Encyclopedia CT
│ ├── Service
│ │ ├── Letters
│ │ ├── Reviewing
│ │ └── NSF
│ └── UB
│ ├── Admin
│ ├── Service
│ │ └── Advising
│ └── Teaching
│ └── LAI 615
└── PhD
├── Admin
├── Coursework
└── Research
Note the recursive structure. Academic work is organized into research, service, and teaching, so these are top-level categories. There is service which is UB-related, and service which is not. I don't keep research under UB.
I like to organize my research into grants, projects, and publications. Grants contain everything related to grant applications. Projects are ongoing engagements with a set of ideas, a corpus of data, and/or a particular site. Once a grant is funded, I create a corresponding project. Finally, a publication contains everything related to one particular publication.
File formats
Text
My preference is to store text in markdown, odt, or latex.
Video and transcripts
I record videos for presentations and course lectures. Video content can be huge, so I compress it,
and I host it separately from the main site, at video.computationalliteracies.net (an S3 bucket,
not a Zola content path). Lesson pages embed a video with the video shortcode:
<video controls width="100%" crossorigin="anonymous">
<source src="https://video.computationalliteracies.net/lai_676_2024_02.mp4" type="video/mp4">
<track kind="captions" src="https://video.computationalliteracies.net/lai_676_2024_02.vtt" srclang="en" label="English" default>
Sorry, your browser doesn't support the mp4 video format.
</video>
All of this -- compressing, uploading, transcribing, and validating -- is handled by the video
collection of invoke tasks in the computationalliteracies web site
repository (tasks/video.py). Run inv --list to see the full set; the important ones are below.
Naming. Two patterns, depending on what the video is for:
- A video tied to a specific iteration of a course (e.g. a recording of a particular lecture):
{course}_{year}_{index}[_{topic}].mp4, e.g.lai_676_2024_02_variables.mp4. - The canonical public video for an MWC module (lab, problem set, or project):
{module_slug}_{index}.mp4, e.g.lab_turtle_01.mp4, with a zero-padded two-digit index since a module can have several videos. Modules get remixed across courses and indexed differently there, so these aren't scoped to any one course -- that's the point of phase 2 of the current video project (see the archive note below).
Format.
- Container: MP4
- Video: AV1 (
libsvtav1), CRF ~34-38, capped at 1080p - Audio: AAC, 128kbps stereo
-movflags +faststartso the seek index is at the front of the file, not the end
ffmpeg -i "$1" -c:v libsvtav1 -crf 36 -preset 6 -c:a aac -b:a 128k -movflags +faststart "$2"
inv video.compress <path> runs this and also enforces the 1080p cap: it probes the source first
and, only if it's larger, adds a scale filter (force_original_aspect_ratio=decrease, so it only
ever scales down, never up, and force_divisible_by=2 for AV1/yuv420's even-dimension requirement).
A source already at or under 1080p re-encodes at its original resolution, untouched. This isn't
optional/manual -- a source above 1080p that skips this (e.g. an ffmpeg call copied from the snippet
above without the scale filter) is exactly the gap that let two videos into the 2025 pedprog archive
import at 2560x1664 before inv video.validate caught it after upload; the cap is now baked into
video.compress itself so it can't recur.
This replaced the original WebM/VP9/Opus standard: benchmarking against
a real archived video showed SVT-AV1 encodes 6x+ faster than the old libvpx-vp9 2-pass pipeline while
producing smaller files at matched quality (VMAF ~95-96 against the VP9 source, 13-25% smaller even
re-encoding an already-compressed file). More importantly, Safari has never supported the WebM
container -- videos in the old format silently fail to play on any Mac or iOS device, which matters
since MWC videos are watched by high schoolers and teachers on whatever device they have. MP4 doesn't
have that gap, and +faststart is a longer-established fix for seeking than WebM's -cues_to_front.
Audio is AAC rather than Opus for the same reason: Opus decode support in Safari/AVFoundation is a
comparatively recent addition and I don't trust it's universal across devices still in use, whereas AAC
has had zero compatibility gaps anywhere for well over a decade -- not worth trading one compatibility
risk for another in the same migration.
Transcripts and captions. Every video needs a companion WebVTT caption file, {name}.vtt, and a
plain-text transcript, {name}.txt, both uploaded to the same bucket prefix as the video. The .vtt
is what the video shortcode's <track> element points at for accessible, timestamped captions; the
.txt is what makes videos searchable and reusable -- raw material for planning shorter, modular
clips later. Both come from a single transcription pass on
GSEAI, UB GSE's AI server:
inv video.check-model-- lists GSEAI's available models and reports the best transcription model available (seePREFERRED_TRANSCRIBE_MODELSintasks/video.py). GSEAI's client API doesn't document a way to pull a model that isn't listed yet; if none of the preferred models are there, this task says so, and I need to ask a GSEAI admin to add one.inv video.transcribe <path>-- extracts the audio track (ffmpeg, locally, so only the audio is uploaded to GSEAI, not the whole video) and queues a transcription job requesting WebVTT output. Transcription runs asynchronously, since most recordings are far longer than an HTTP request should block for. The audio is extracted as low-bitrate (16k), voip-tuned Opus, not WAV -- GSEAI's upload endpoint has a request-size limit well under what even an hour of raw PCM needs, so this isn't optional for anything longer than a couple of minutes.inv video.transcribe-all --prefix <course_prefix>-- queues every video under a prefix that doesn't already have both a transcript and captions.inv video.check-jobs --out <dir>-- polls queued jobs and, for any that finished, writes{name}.vtt(the raw result) plus{name}.txt(the same cues with timestamps and cue markup stripped out -- seevtt_to_textintasks/video.py) and the raw job record,{name}.json.inv video.upload <path>-- uploads a video together with its.txtand.vtt, and refuses to upload a video missing either unless you pass--force.inv video.validate <path>(or--remote <filename>for a video already in the bucket) -- checks container/codec/resolution and transcript/caption presence without uploading anything.
Getting real timestamped captions out of GSEAI's job queue took an upstream fix: the queue didn't
expose a response-format option the way the synchronous transcription endpoint does, so queued jobs
used to return plain text only. gseai (the GSEAI client package)
1.5.0 added -f/--format to queue upload, and the matching GSEAI server change forwards it through
to the underlying Whisper call -- see plans/server_queue_response_format_plan.md in the gseai
repo for the design (including a LocalAI quirk it hit: the backend 500s on a literal
response_format="text", so both the client and server request "json" under the hood for that case
and extract the text field themselves). video.transcribe now always asks for vtt, and derives the
.txt locally from the same result instead of transcribing twice.
Note: the archive (65 videos, 27.6 hours, 18.3GB across every course, not just pedprog) predates this MP4/AV1 standard and is being re-encoded, unlike the original webm/vp9 revision of this page, which found nothing worth re-encoding. What changed: this isn't a settings tweak to an already- compliant archive, it's a container change (fixing the Safari/WebM gap above), and SVT-AV1 is fast enough that the whole archive costs a few hours of background compute, not the CPU-day a full VP9 re-encode would have. Filenames are unchanged in the migration (same basename,
.webm->.mp4) -- the two naming patterns above are for new recordings, not a renaming project for the archive. Re-encoding is happening pedprog-first, since those transcripts are the immediate prerequisite for planning phase 2's modular videos; the rest of the bucket follows once that's done and verified.
Qualitative coding
Chris uses qc as a primary tool for qualitative coding.
Here is the project's documentation.