Browser-native interval tagging workstation for long-form speech and video analysis. Researchers can ingest YouTube streams or offline video/audio files, capture millisecond-precise segments, annotate each interval with multi-language labels and remarks, and export structured CSV or JSON records without a build step or backend.
- Audience
- Researchers & coders who annotate long recordings for language studies.
- Facilitators who need deterministic exports for downstream statistics.
- Core Loop
- Load media (offline or YouTube) → default to audio-only mode to minimise cognitive load.
- Use wired controls (
#play-pause-btn,#video-progress,#volume-slider,Mark Start,Mark End) to scrub and capture intervals. - Review and edit tags in tabular form or via modals, then export/share.
- Key Differentiators
- Password-protected audio/video toggle (
ADMIN_PASSWORD = 'ks2.0') prevents accidental mode flips during multi-hour reviews. - Unified controller handles both HTML5 media and the YouTube IFrame API through shared helpers (
VideoTagger.playernamespace). - Timeline overlays and context menu disambiguate overlapping intervals with no additional libraries.
- Password-protected audio/video toggle (
- Fallback Strategy
- If YouTube API fails to instantiate, the UI remains in audio-first mode with loading cues, and local files still operate.
- When media sources disappear (
emptiedevent) the app tears down state, clears tags, and resets controls to safe defaults. - All exports fall back to sentinel values (
label: '9999',remarks: '9999') to keep downstream CSV ingestion robust.
| Layer | Purpose | Key Files |
|---|---|---|
| Shell | Static SPA scaffolding | index.html |
| Core Utilities | Namespace bootstrap, theming, global lifecycle | js/core/main.js, js/core/namespace.js, js/core/shortcut.js, js/core/shortcut_help.js |
| Media Subsystem | Player orchestration, shared state, diagnostics | js/player/state.js, js/player/dom.js, js/player/audioControls.js, js/player/controller.js, js/player/timeline.js, js/player/youtubeController.js, audioControls.js (legacy alias) |
| Tagging Subsystem | Interval capture, editing, summary, persistence | js/tagging/tag.js, js/tagging/tagsummary.js, js/tagging/export.js, js/tagging/save.js, js/tagging/load.js |
| Styling | Layout, theming, timeline, modals | css/*.css (notably modern.css, tagging.css, timeline.css, shortcut_help.css) |
window.VideoTagger: namespaced buckets (core,player,tagging).window.mediaMode:'audio'(default) or'video'.window.ytPlayer: YouTube IFrame instance ornull.window.plyrInstance: wrapper around the<video>element for HTML5 playback.window._timelineTags: authoritative array of interval records.window.currentVID,window.currentVideoSource: exported metadata.- Dirty tracking via
window.markDirty()/window.markSaved()ensures the browser warns before unload.
index.htmldeclares all DOM elements (hero loader, player surface, controls, modals).js/core/main.jsis the orchestrator—onDOMContentLoadedit boots the module namespace, applies persisted theme, then callsinitVideo(),initTags(),initTagSummary(),initExport(),initSaveLoad(), and keyboard helpers.js/player/controller.jswires media inputs, hooking HTML5<input type="file">and the YouTube URL button. It keeps the player UI coherent (showPlayer(),primeLocalVideo(),setupYouTubeLoading()), handles scrubbing/volume, and binds keyboard shortcuts to the global media API.js/player/audioControls.jsnormalises the playback API, mapping either YouTube or HTML5 calls to a consistent control surface (updateAudioControls('reason')).js/player/timeline.jsdraws the ruler ticks, lays out interval bars, renders context menus for overlaps, and shows transient dots when marking starts.js/tagging/tag.jsgoverns the tagging workflow, modelling modals, start/end gatekeeping, per-interval editing, and table rendering.- Export and save modules flatten
_timelineTagsinto CSV/JSON, baking in the fallback semantics.
- Purpose: Provide a binary choice—local upload (
#local-video-input) vs. streaming (#youtube-url+#load-youtube-btn). - Transition:
showApp()(declared inline inindex.html) hides the hero, flipsbody.player-active, and reveals the grid layout once metadata events (loadedmetadata,youtube:ready) fire. - Fallback: If YouTube iframe never arrives, the hero remains visible and the console logs diagnostics.
- Left Column
- Player Surface
- HTML5 wrapper (
#html5-wrapper > video#video) and YouTube mount (#youtube-container). - Placeholder text toggled by
applyMediaMode()(audio vs. video cues).
- HTML5 wrapper (
- Playback Control Bar
- Play/pause button (
#play-pause-btn) with Material icon, managed byattachAudioToggle(). - Scrubber (
#video-progress) bound toattachAudioProgressHandlers()for pointer/touch scrubbing and commit semantics. - Time readout (
#audio-status) fed byformatMediaTime(). - Volume slider (
#volume-slider) writing to whichever active API is available (setVolume()for YouTube, property for HTML5/Plyr).
- Play/pause button (
- Timeline Strip (
#timeline)- Drawn via
drawTimelineRuler(); intervals appended byupdateTimelineMarkers(). - Overlap interactions spawn
#timeline-context-menu, enabling explicit tag selection.
- Drawn via
- Jump-to-Time and Session Metadata sections supply manual seeking and the required VID field.
- Tag Capture block:
+ Add Tagsmodal button, language pills, remarks textarea,Mark Start/Mark Endgating.
- Player Surface
- Right Column
- Tag List Table (
#tag-list-table): Start/End cells double as seek triggers; other cells open modals. - Tag Summary Table (
#tag-summary-table): Live frequency view toggled between tags and languages (summary-mode-btn). - Session Actions: Export, Save, Load, and the audio/video mode toggle button.
- Tag List Table (
- Add Tags Modal (
#add-tag-modal): Pre-loads reusable tag labels, storing them inpendingTagsFromModal. - Manage Tags Modal (
#tag-label-modal): Edits interval-specific tag arrays. - Language Modal (
#language-modal): Toggles Cantonese/English/Mandarin flags via checkboxes. - Remarks Modal (
#remarks-modal): Captures rich context per interval. - Admin Modal (
#admin-modal):prompt()password flow is backed by this modal in markup for future enhancements. - Shortcut Help Modal (wired by
initShortcutHelp()): surfaces the key map fromrequirements.mdwith accessible toggles.
- All interactive elements have ARIA labels (
title,aria-label) and keyboard affordances. - Theme uses CSS custom properties (
css/modern.css) and persists vialocalStoragekeytheme-preference. - Buttons adopt large hit areas, consistent Material icons, and high-contrast states for dark/light parity.
- Local Files (
setupLocalVideoLoading()injs/player/controller.js)- File selection clears previous YouTube instances (
destroyYouTubePlayer()), revokes prior object URLs, and sets the<video>src. - Lifecycle listeners (
loadedmetadata,canplay) callinitializePlyrOnVideo()to hook controls and reveal the app viashowApp(). - Fallback: If load errors occur, the handler logs diagnostic info (network state, ready state) to the console.
- File selection clears previous YouTube instances (
- YouTube Streams (
setupYouTubeLoading())- URL is regex-parsed for a video ID; invalid inputs raise blocking alerts.
- All HTML5 sources are cleared, placeholder shown, and
createYouTubePlayer(videoId)is scheduled. global.pendingYouTubeLoadensures the player is created once the API is ready (onYouTubeIframeAPIReady).- On success,
defaultOnPlayerReady()enables tagging controls and re-applies audio mode semantics.
attachAudioToggle()listens to#play-pause-btnclicks and routes them to the active medium:- YouTube: uses
getPlayerState()andpauseVideo()/playVideo()with state introspection to keep icon text consistent. - Plyr/HTML5: toggles
.pausedorvideoElement.pausedand updates controls.
- YouTube: uses
- This function also attaches a listener array (
['play','pause','timeupdate', ...]) on the HTML5 element to keep the UI snapshot fresh. - Keyboard handler (
setupKeyboardShortcuts()) captures the space bar for global toggling when focus is outside inputs.
attachAudioProgressHandlers()manages scrubbing semantics:- Maintains
data-scrubbingstate to prevent conflicting updates while the user holds the slider. commitSeek(val, reason)normalises the target time, clamping it to whichever duration (YouTube,Plyr, orvideo) is authoritative, then callsseekTo()/currentTime =.- Supports pointer, touch, and keyboard events with discrete handlers for
pointerdown,touchstart, etc.
- Maintains
setupJumpToTime()accepts HH:MM:SS.mmm, MM:SS.mmm, or numeric seconds and dispatches to the active player.
- The volume slider updates the active media via
setVolume()(0–100 for YouTube, normalised to 0–1 for HTML5/Plyr). - On no source, the slider is disabled and reset to 100 to avoid false state impressions.
drawTimelineRuler()chooses tick intervals dynamically based on duration and container width (>60px spacing).updateTimelineMarkers(tagList)sorts tags, enforces a minimum interval width (0.5%), and renders accessible bars withdata-tag-idxattributes.- Click handling:
- Direct click on a bar seeks to the start time.
- Clicking empty timeline space calculates the approximate time, finds overlapping tags, and either seeks directly (single match) or spawns
#timeline-context-menuwith selection items.
- Start markers:
showStartDotOnTimeline(startTime)previews the start point between the start/end click sequence;removeStartDotFromTimeline()cleans up afterMark Endor cancellation.
applyMediaMode()(injs/player/audioControls.js) is the master switch:- Updates
body.audio-mode-active/body.video-mode-activeto drive theming. - Applies
opacity: 0.001andpointer-events: noneto the active video container when in audio mode, ensuring playback continues without visual distraction. - Reveals the custom audio control bar only in audio mode; in video mode the expectation is to use inline Plyr/YouTube controls.
- For YouTube specifically, the iframe remains in the DOM for audio mode but is visually de-emphasised, avoiding API teardown.
- Updates
- Access is gated by
ADMIN_PASSWORD.main.jsprompts viawindow.prompt()and toggleswindow.mediaModebefore re-applying the mode and refreshing the toggle button text.
player.logPlayerLayout(context)(fromjs/player/dom.js) dumps a thorough console snapshot: computed styles, bounding boxes, audio control metadata, YouTube/HTML5 diagnostics, and Plyr state. Use this when debugging layout regressions.updateAudioControls(trigger)logs whenever a meaningful delta (playback state, current time, duration) occurs, helping trace stale UI states during QA.
type TagRecord = {
start: number; // seconds, three decimal places
end: number; // seconds, three decimal places
label: string | string[]; // sentinel '9999' if none
languages: string[]; // subset of LANGUAGE_OPTIONS
remarks: string; // empty string allowed
};- Stored in
_timelineTags.tag.jsalways copies/sorts arrays when reading to avoid accidental mutation side-effects. - Derived helper
languagesToInitials()condenses array values toC:E:Mfor tabular display.
initTags()disables tagging controls until media is ready.pendingTagsFromModalcaches the selection from the Add Tags modal so repeated intervals can inherit the same labels without retyping.- Language pills default to Cantonese and English active (per historical workflow) but can be toggled off; state is encoded in the button class
.activerather than checkboxes for quick scanning.
- Mark Start (
startTagBtnclick orIkey)- Validates VID presence (
#vid-input), blocking progress with an alert if missing. - Captures current playback time via
getCurrentTime(), gating on either YouTube or HTML5 API. - Disables
Mark Start, enablesMark End, locks remarks/language inputs, and showstimeline-start-dot.
- Validates VID presence (
- Mark End (
endTagBtnclick orOkey)- Captures end time and ensures it is ≥ start time.
- Constructs the tag object, applying defaults: label(s) from
pendingTagsFromModalor'9999', languages from active pills, remarks text or empty string. - Pushes to
_timelineTags, re-enables inputs, clears remarks, resets pills, removes dot, and redraws timeline + summary. - Calls
window.markDirty()to trigger unload protection.
- Error Handling
- If end time < start time, the workflow aborts gracefully, re-enabling controls and alerting the user.
- Clicking cells in the tag table triggers modals that operate on
editingTagIndex(bound to the underlying array index). - Delete actions include a
confirm()prompt before splicing the array and refreshing UI artefacts. renderTagList()sorts tags on each render to keep chronological order independent of insertion sequence.
window.updateTagSummary()(intagsummary.js) recomputes counts by tag or by language. The toggle persists inside the DOM dataset so the view updates reactively after edits.- Timeline is refreshed alongside the summary to keep visual cues aligned.
- Guard clauses ensure tags exist and VID is provided.
- Each row contains both numeric seconds (
Start (s),End (s)) and formatted times (HH:MM:SS.mmm). - Languages are exported as binary columns (Cantonese/English/Mandarin) to simplify pivot tables.
- Remarks default to
'9999'when empty so spreadsheets flag missing annotations explicitly. - UTF-8 BOM is preprended to avoid Mojibake when opening in Excel.
- Save: Serialises the session object and forces download via blob. Marks the document as saved (
markSaved()). - Load: Reads a JSON file, normalises each tag (filters invalid languages, ensures numeric times), repopulates
_timelineTags, updates UI, and alerts on success. - Fallback: Malformed files trigger an alert with the thrown error message; no partial state is applied.
video-tagger:clear-session-request(CustomEvent) resets tagging state when switching sources (local vs. YouTube) to prevent interval leakage.goHomeBtnhandler inmain.jsusesresetMediaState('home')+clearTaggingSession('home')to fully revert the interface, including destroying YouTube/Plyr instances and revoking object URLs.
| Key | Action | Implementation |
|---|---|---|
Space |
Play/Pause | setupKeyboardShortcuts() monitors document-level keydown. |
I |
Mark Start | Bound to startTagBtn.click() through event listeners in tag.js. |
O |
Mark End | Same as above for endTagBtn. |
← / → |
Seek ±5s | Implemented in js/core/shortcut.js (not shown) to call the active media API. |
Alt + ← / → |
Seek ±1s | Fine-grained adjustments. |
Ctrl + ← / → |
Seek ±30s | Coarse adjustments. |
? |
Toggle shortcut help modal | Provided by initShortcutHelp(). |
Esc |
Close any modal | Bound via data attributes in modal setup. |
Focus is intentionally suppressed when the user is editing form inputs; shortcuts only fire if document.activeElement is not typing (tag.js guards this).
css/modern.cssdefines root variables for light/dark themes; dark mode is applied by togglingdata-theme="dark"on<html>.#theme-toggleand header toggle rely on Material icons to indicate the next mode (sun/moon idiom).- Buttons and pills use consistent border-radius and box-shadow to communicate affordance; the timeline uses elevated drop shadows to highlight active bars (
.timeline-intervalhover states). shortcut_help.cssandtagsummary.cssunify typography (Inter/Roboto) to match the hero screen for brand consistency.
- No Bundler: Everything runs as static assets. This simplifies deployment (copy to any static file server) and debugging (view-source friendly).
- Password-Protected Mode Switch: Chosen to avoid UI drift mid-session; the password
ks2.0aligns with the institution’s internal code. - Audio-First Design: The default mode hides video surfaces (
opacity: 0.001). This reduces bandwidth and emphasises linguistic analysis. Video mode can be enabled when visual cues are required. - Sentinel Values:
'9999'is used for empty tags/remarks to stay compatible with legacy scripts that expect mandatory values. - YouTube Resilience:
pendingYouTubeLoadtracks deferred player creation. If the API fails, diagnostics are logged and the UI remains responsive for local files. - Object URL Hygiene: Every local file load revokes the previous
URL.createObjectURLto avoid gradually leaking memory in Chrome during marathon sessions. - Dirty State Handling:
beforeunloadwarns users if_timelineTags, metadata, or VID changed but weren’t saved.
- Local Development
Open
# Python static server python -m http.server 8000 # Node alternative (requires `npm install -g serve`) serve .
http://localhost:8000in a Chromium-based browser (Chrome, Edge). Safari is partially supported but not officially validated. - Production Deployment
- Upload the contents of the repository to any static hosting provider (e.g., Netlify, GitHub Pages, on-prem HTTP server).
- Ensure external CDNs (Plyr JS/CSS, Google Fonts, YouTube IFrame API) are not blocked by network policies.
- Smoke Test Checklist
- Load a local MP4 → confirm hero hides, audio controls enable, timeline draws.
- Paste a YouTube URL → confirm iframe appears after authentication, audio mode still hides video until password toggle.
- Capture at least two tags with overlapping time slices → verify timeline context menu.
- Edit languages/remarks → re-open row to confirm persisted.
- Export CSV → inspect BOM and columns, ensure VID present.
- Save JSON, refresh page, load JSON → confirm state fully restored.
- Toggle video mode with password → ensure audio banner hides, video surface visible.
- Diagnostics
- Use browser devtools Console to inspect
[video.js]logs for playback issues. player.logPlayerLayout('context')can be called manually for DOM snapshots.
- Use browser devtools Console to inspect
Video-Tagger/
├─ index.html # SPA scaffold, inline showApp logic, modal markup
├─ audioControls.js # Legacy global shim pointing to player controls
├─ css/
│ ├─ main.css # Base layout & typography
│ ├─ modern.css # Theme tokens, timeline polish
│ ├─ tagging.css # Control column styles
│ ├─ video.css # Player sizing
│ ├─ tags.css # Tag list table elements
│ ├─ tagsummary.css # Summary table
│ ├─ shortcut_help.css # Shortcut modal styling
│ └─ modals.css # Shared modal design
├─ js/
│ ├─ core/
│ │ ├─ main.js # App bootstrap, theming, admin toggle, reset logic
│ │ ├─ namespace.js # Ensures consistent namespaces on window
│ │ ├─ shortcut.js # Keyboard navigation & seeking
│ │ ├─ shortcut_help.js # Renders help modal content
│ │ └─ utils.js # Misc helpers (if present)
│ ├─ player/
│ │ ├─ state.js # Shared constants (MEDIA_MODE) and state snapshot
│ │ ├─ dom.js # DOM query helpers & layout logging
│ │ ├─ audioControls.js # Format time, getActiveMediaApi, applyMediaMode
│ │ ├─ controller.js # Wire inputs, scrubbing, keyboard hooks, showPlayer
│ │ ├─ timeline.js # Ruler drawing, interval overlay, context menus
│ │ └─ youtubeController.js # YouTube API bootstrap & polling
│ └─ tagging/
│ ├─ tag.js # Capture loop, modals, table rendering
│ ├─ tagsummary.js # Summary table recalculation
│ ├─ export.js # CSV generator with BOM
│ ├─ save.js # JSON save/load flows
│ └─ load.js # (Optional legacy helper if referenced)
├─ test-player.html # Minimal playback harness for debugging
├─ requirements.md # Historic product brief & shortcut tables
└─ README.md # This document (PRD + technical reference)
- Drag-adjust interval boundaries directly on the timeline (requires pointer tracking and collision detection).
- Tag filtering/search in the right column for quick retrieval on large datasets.
- Multi-user collaboration with server-backed storage (would require replacing the static architecture).
- Additional export formats (Excel, XML) or direct integration with analytics pipelines.
- Plyr (
https://cdn.plyr.io/3.7.8/) for consistent HTML5 audio/video controls. Currently used passively; custom bar takes over in audio mode. - YouTube IFrame API (
https://www.youtube.com/iframe_api) for streaming playback with JS control. - Google Fonts (Roboto, Inter) and Material Symbols for cohesive typography and icons.
- Developed for The Education University of Hong Kong’s KeySteps@JC programme.
For historical decisions, UI mockups, and design rationales, cross-reference requirements.md.