Open Frame Annotator is an open-source tool for collaborative YouTube video annotation. Create a project from any YouTube video, invite your team to leave time-stamped comments, reply in threads, tag issues by category, and export everything to Premiere Pro markers or a printable PDF report.
Perfect for video reviews, editorial feedback, client approvals, and team QA workflows.
If it saves you time, β buy me a coffee β it keeps the project going.
| Feature | Details |
|---|---|
| π₯ YouTube Integration | Paste any YouTube URL β public or unlisted β to embed the video |
| β±οΈ Time-Stamped Comments | Annotate at any timestamp; author name is free-text (no accounts needed) |
| π¬ Threaded Replies | Reply directly to any comment; threads collapse cleanly in the sidebar |
| π·οΈ Custom Tags | Define up to 8 colour-coded tags per project (e.g. color, audio, pacing) |
| β Review Status | Mark each comment Accepted β, Rejected β, or leave it Pending β― |
| π Progress Indicator | Live "X / Y reviewed" counter updates as you work through comments |
| π― Timeline Clustering | Nearby markers merge into clusters; hover to expand individual items |
| β‘ Real-Time Collaboration | All connected viewers see new comments and status changes instantly via WebSocket |
| β¨οΈ Keyboard Shortcuts | Space, arrow keys, and A so you never have to leave the keyboard |
| π PDF Report | One-click printable report with thumbnail, stats, and full comment table |
| ποΈ Premiere Pro CSV Export | Download markers as CSV with frame-accurate timecodes; built-in import tutorial |
| π Edit Tokens | Delete-your-own-comment system without user accounts |
| π οΈ Admin Panel | Password-protected panel for viewing stats and deleting projects |
| π Audio Feedback | Subtle sounds on accept and delete actions |
| π Dark Theme | Responsive dark UI, print-optimised report |
| πΎ SQLite | Zero external database β everything lives in a single file |
- Docker + Docker Compose (recommended)
or Node.js 18+ for a bare-metal run
git clone https://github.com/YoYoZ/OFA.git
cd OFA
# Set your admin password (default is CHANGE_ME β change it)
echo "ADMIN_PASSWORD=your_secure_password" > .env
docker-compose up --buildnpm install runs automatically inside the Docker build step β you do not need to run it manually.
Access at: http://localhost:3000
git clone https://github.com/YoYoZ/OFA.git
cd OFA
npm install
echo "ADMIN_PASSWORD=your_secure_password" > .env
npm startAccess at: http://localhost:3000
- Open
http://localhost:3000 - Fill in:
- Project Title (required) β shown in the header and PDF report
- YouTube URL (required) β public or unlisted link
- Description (optional) β a brief note for reviewers
- Custom Tags (optional) β comma-separated list, max 8 (e.g.
color, audio, pacing, continuity)
- Click Create Project
- Copy the unique share link and send it to your team
- Open the project link
- Play the video and pause at the moment you want to comment on
- Enter your name and comment text
- Optionally select one or more tags
- Click "Add at [timestamp]"
Timeline marker colours:
- π΄ Red β pending comment
- π’ Green β accepted comment
- π‘ Yellow β cluster with mixed statuses
Click any marker to jump to that timestamp. Hover over a cluster to see individual entries.
Click Reply under any comment to open an inline reply form. Replies are threaded under the parent comment and carry the same timecode.
- Accept β click β to mark a comment as accepted (plays a bell tone)
- Reject β click β to mark as rejected
- Delete β click ποΈ to delete (you can only delete comments you created in the current browser session; see Edit Tokens)
The progress bar shows "X / Y reviewed" (accepted + rejected out of total root comments).
PDF Report
Click π Report in the top bar. The report page shows the video thumbnail, project metadata, stats summary, and a full comment table including replies. Use your browser's Print β Save as PDF to export.
Premiere Pro CSV
Click β¬ Export Markers, choose the frame rate matching your Premiere sequence, and click Download CSV. Follow the built-in step-by-step tutorial to import into Adobe Premiere Pro or DaVinci Resolve.
| Key | Action |
|---|---|
Space |
Play / Pause |
β / β |
Seek Β±5 seconds |
Shift+β / Shift+β |
Seek Β±10 seconds |
A |
Focus the comment text field |
Esc |
Close export modal / close open reply forms |
Shortcuts are disabled when focus is inside any text input.
- Go to
http://localhost:3000/admin - Enter the password from your
.envfile - View total project and comment counts, list all projects, and delete any project (cascades to all comments)
Session lasts 24 hours.
OFA/
βββ public/
β βββ index.html # Homepage β create new project
β βββ project.html # Annotation interface
β βββ report.html # Printable PDF report page
β βββ admin.html # Admin panel
β βββ project.js # All client-side logic
β βββ style.css # Styles
βββ server.js # Express + Socket.io server and API routes
βββ data/
β βββ annotations.db # SQLite database (auto-created on first run)
βββ .env # Environment variables (gitignored)
βββ .gitignore
βββ package.json
βββ docker-compose.yml
βββ README.md
All routes use JSON unless otherwise noted.
Create project
POST /api/projects
Content-Type: application/json
{
"youtube_url": "https://youtu.be/dQw4w9WgXcQ",
"title": "Brand Ad β Cut 3",
"description": "Final round of reviews before delivery",
"tags_config": "color, audio, pacing, continuity"
}
β 201
{
"project_id": "uuid",
"share_url": "http://localhost:3000/project/uuid"
}Get project + annotations
GET /api/projects/:id
β 200
{
"project": {
"id": "uuid",
"youtube_url": "...",
"title": "...",
"description": "...",
"tags_config": "[\"color\",\"audio\"]",
"created_at": "2025-10-29T12:00:00Z"
},
"annotations": [
{
"id": "uuid",
"project_id": "uuid",
"parent_id": null,
"author": "Alice",
"text": "Colour grade feels warm here",
"timecode": 42.5,
"status": 0,
"tags": "[\"color\"]",
"created_at": "..."
}
]
}status: 0 = pending, 1 = accepted, 2 = rejected
parent_id: null for root comments, parent annotation UUID for replies
Add annotation (or reply)
POST /api/projects/:id/annotations
Content-Type: application/json
{
"author": "Alice",
"text": "Colour grade feels warm here",
"timecode": 42.5,
"tags": ["color"],
"parent_id": null // omit or null for root; UUID for reply
}
β 201
{
"id": "uuid",
"edit_token": "uuid" // store this β required to delete the annotation later
}Set review status
PATCH /api/annotations/:id/status
Content-Type: application/json
{ "status": 1 } // 0 = pending, 1 = accepted, 2 = rejectedDelete annotation
DELETE /api/annotations/:id
Content-Type: application/json
{ "edit_token": "uuid" } // the token returned when the annotation was createdDeleting a root annotation automatically deletes all its replies. The server emits thread:deleted (root) or annotation:deleted (reply) via WebSocket to all connected clients.
Export markers (Premiere Pro CSV)
GET /api/projects/:id/export/premiere?fps=24
β 200 text/csv
Name,Description,In,Out,Duration,Comment
...fps accepts: 23.976, 24, 25, 29.97, 30, 48, 50, 59.94, 60
POST /api/admin/login { "password": "..." }
GET /api/admin/projects β { projects: [...], totalAnnotations: N }
DELETE /api/admin/projects/:id
POST /api/admin/logoutLogin is rate-limited to 10 attempts per 15 minutes per IP.
Socket.io (v4) handles live updates. Every client joins a room keyed to the project UUID on page load. The server broadcasts to the room on every write:
| Event | Payload | Trigger |
|---|---|---|
annotation:created |
full annotation object | new comment or reply added |
annotation:deleted |
{ id } |
a reply was deleted |
thread:deleted |
{ id } |
a root comment (and its replies) was deleted |
annotation:status |
{ id, status } |
review status changed |
Clients deduplicate annotation:created events to avoid double-rendering their own submissions.
Comments can be deleted without user accounts using a one-time edit token. When an annotation is created, the server generates a UUID token and returns it once in the API response. The browser stores it in localStorage under annotation_tokens. The delete button only appears for comments whose token is present in the current browser's storage. The token is required in the DELETE request body.
Legacy annotations (created before tokens were introduced) have a NULL token and can be deleted freely β this preserves backwards compatibility.
- Password compared with
crypto.timingSafeEqual(prevents timing attacks) express-sessionwithhttpOnlycookies, 24-hour TTL- Rate limiter: 10 login attempts per 15 minutes per IP, in-memory
- YouTube URL validated against a strict regex before storing
- All user text HTML-escaped before rendering (no
innerHTMLwith raw data) tags_configlimited to 8 tags, comma-parsed and serialised as JSON
- Set a strong
ADMIN_PASSWORDandSESSION_SECRETin.env - Run behind HTTPS (nginx + Let's Encrypt)
- Enable
cookie.secure: trueinserver.jswhen behind HTTPS - Consider restricting
/adminby IP in nginx
.env file
# Required β admin panel password (default: CHANGE_ME)
ADMIN_PASSWORD=your_secure_password
# Optional β session signing secret (auto-generated if absent)
SESSION_SECRET=a_long_random_string
# Optional β port (default: 3000)
PORT=3000ssh user@your-server
git clone https://github.com/YoYoZ/OFA.git
cd OFA
echo "ADMIN_PASSWORD=your_password" > .env
docker-compose up -d --buildSocket.io requires WebSocket upgrade headers. Make sure your nginx config includes them:
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}Without Upgrade / Connection headers, real-time collaboration will fall back to HTTP long-polling and may not work at all depending on your proxy settings.
npm install
npm install -g pm2
pm2 start server.js --name ofa
pm2 save
pm2 startup- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit your changes:
git commit -m 'Add my feature' - Push to the branch:
git push origin feature/my-feature - Open a Pull Request
Development setup:
git clone https://github.com/YoYoZ/OFA.git
cd OFA
npm install
npm run dev # starts with nodemonGNU Affero General Public License v3.0 β see LICENSE.
If you modify this software and run it as a network service, you must make your source code available under the same license.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: yoyoza5@gmail.com
- Express.js β web framework
- Socket.io β real-time WebSocket layer
- SQLite3 β embedded database
- YouTube IFrame API β video player
- express-session β admin session management
Made with β€οΈ by the Open Source Community
β If you like this project, please give it a star on GitHub!