Building a Video Management Solution for a video creator + editor workflow. Videos are stored on Cloudflare R2 (not Frappe's file system); Frappe DocTypes hold metadata only. The React frontend is the primary interface — users don't use Frappe Desk. Authentication uses Frappe's default /login page.
- Asset Categories: Every asset has a
category— "Source", "Cut" (work-in-progress edit), "Review" (shared for feedback), or "Final" (approved deliverable). Only "Review" and "Final" assets are eligible for transcription. - Inbox: Assets uploaded without a project land in the Inbox — a global holding area visible to the uploader. From the Inbox, assets can be moved into any project. This supports ad-hoc uploads (quick screen recordings, rough cuts, etc.) that don't yet have a home.
- Asset Movement: Assets can be moved between projects or from the Inbox into a project at any time. Moving an asset updates its
projectlink — the R2 object stays in place (no file copy needed).
1. VMS Settings (Single DocType) — vms/video_management_solution/doctype/vms_settings/
- R2 Account ID, Access Key ID, Secret Access Key (Password field), Bucket Name, Public URL
- Upload settings: max file size (default 5GB), presigned URL expiry (default 3600s), allowed extensions
2. VMS Project — vms/video_management_solution/doctype/vms_project/
- Fields:
project_name,description(Text Editor),status(Open/In Progress/In Review/Completed/Archived),owner_user(Link→User),due_date,thumbnail_url - Naming:
naming_series:VMS-PROJ-.#####(global counter viatabSeries) - Permissions: Video Creator (CRUD own), Video Editor (read+write), System Manager (full)
3. VMS Asset (standalone, optionally linked to Project) — vms/video_management_solution/doctype/vms_asset/
- Fields:
project(Link→VMS Project, optional — null means the asset is in the Inbox),file_name,r2_key(unique),file_size,file_type(MIME),status(Uploading/Ready/Processing/Error),category(Select: Source/Cut/Review/Final — default "Source"),uploaded_by,uploaded_at,duration_seconds,thumbnail_r2_key - Naming:
naming_series:VMS-ASSET-.#####(global counter viatabSeries) - Permissions: Video Creator (CRUD), Video Editor (read only), System Manager (full)
- Inbox rule: Assets where
projectis null are "in the Inbox" — no separate DocType needed
- Create
vms/install.pywithafter_install()hook to create "Video Creator" and "Video Editor" roles withdesk_access=0 - Register in
hooks.py:after_install = "vms.install.after_install"
vms/r2.py— boto3 S3 client for Cloudflare R2, functions:get_r2_client(),generate_presigned_upload_url(),generate_presigned_view_url(),delete_r2_object()- Add
boto3topyproject.tomldependencies
vms/api.py— All whitelisted endpoints:get_upload_url(file_name, content_type, project=None, category="Source")→ returns{upload_url, r2_key, asset_name}. Validates extension, creates VMS Asset in "Uploading" status with given category. Ifprojectis omitted, the asset goes to the Inbox.confirm_upload(asset_name, file_size)→ marks asset as "Ready", sets file_size and uploaded_atget_view_url(asset_name)→ returns presigned GET URL for streamingmove_asset(asset_name, target_project)→ moves an asset to a different project (or from Inbox to a project). Updates theprojectfield. Validates the user has write access to both source and target projects.update_asset_category(asset_name, category)→ changes an asset's category (Source/Cut/Review/Final). Validates allowed transitions.
vms/permissions.py— Query conditions so Video Creators only see their own projects/assets, Video Editors see all- Register in
hooks.pyviapermission_query_conditions
- Add
boto3topyproject.toml - Create
vms/install.py(role creation) - Update
hooks.py(after_install, permission_query_conditions) - Create VMS Settings DocType files
- Create VMS Project DocType files
- Create VMS Asset DocType files
- Create
vms/r2.py - Create
vms/api.py - Create
vms/permissions.py - Run
bench --site <site> migrate
Install in frontend/:
- Tailwind CSS v4:
tailwindcss+@tailwindcss/vite(add plugin tovite.config.ts) - shadcn/ui deps:
class-variance-authority,clsx,tailwind-merge,@radix-ui/react-slot,lucide-react - React Router v7:
react-router - Toasts:
sonner - Create
src/lib/utils.ts(cn helper), updateindex.cssto@import "tailwindcss" - Add shadcn/ui components: button, input, card, badge, dialog, progress
- Delete
App.css,assets/react.svg
- No custom login page — use Frappe's default
/login. ProtectedRoute redirects unauthenticated users to/login(full page redirect, not a React route) main.tsx: BrowserRouter withbasename="/frontend", FrappeProvider, ToasterApp.tsx: Routes with ProtectedRoute guard (redirects Guest to Frappe/login)- Layout:
AppLayout.tsx(sidebar + header + Outlet),Sidebar.tsx(nav: Dashboard, Inbox, Projects),Header.tsx(user info + logout) src/types/index.ts: TypeScript interfaces for VMSProject, VMSAsset (includingcategoryfield), API responses- Stub pages: DashboardPage, InboxPage, ProjectsPage, ProjectDetailPage
- Install frontend deps (tailwind, shadcn, react-router, lucide-react, sonner)
- Update
vite.config.ts(Tailwind plugin) - Replace
index.css, createsrc/lib/utils.ts - Add shadcn/ui components to
src/components/ui/ - Create
src/types/index.ts - Create layout components (AppLayout, Sidebar, Header)
- Rewrite
main.tsxandApp.tsxwith routing (redirect to/loginfor auth) - Create stub pages (Dashboard, Inbox, Projects, ProjectDetail)
- Delete
App.css,assets/react.svg - Verify with
yarn dev
- Project counts by status, recent projects, recent uploads
- Inbox badge showing count of unassigned assets
- Uses
useFrappeGetDocList
- Lists all assets where
projectis null (uploaded by the current user) - Each asset card shows: filename, category badge, upload date, thumbnail/icon
- Quick upload: "Upload" button opens upload dialog with no project (assets land in Inbox)
- Move to project: Select one or more assets → "Move to Project" action → project picker dialog → calls
vms.api.move_assetfor each - Bulk select with checkboxes for batch move operations
- Card grid of projects with status badges
- "New Project" dialog using
useFrappeCreateDoc - Click navigates to project detail
- Project header with metadata and edit capability
- Asset list/grid with file info, status, and category badge (Source/Cut/Review/Final)
- Filter/group assets by category
- "Upload Assets" button → opens upload dialog (with category selector)
- Click asset → opens video player dialog with presigned view URL
- Asset context menu: change category, move to another project
- Upload is a dialog, not a page — can be triggered from any view (project detail, inbox, dashboard, etc.)
- Accepts an optional
projectprop — if omitted, assets go to the Inbox - Category selector: dropdown to pick Source/Cut/Review/Final (defaults to "Source")
- Upload flow:
- Drag-and-drop zone to select files
POST vms.api.get_upload_url→ get presigned PUT URL + create asset record (passes project + category)XMLHttpRequestPUT directly to R2 (with progress tracking viaxhr.upload.onprogress)POST vms.api.confirm_upload→ mark asset as Ready
- 2 concurrent uploads — pool-based concurrency (start next file as soon as one finishes, max 2 in flight at any time)
- Per-file progress bars, status indicators, error handling
- Reusable dialog triggered from Inbox or Project Detail
- Shows a searchable list of projects to move the asset(s) into
- Calls
vms.api.move_asset— only updates theprojectlink, no file copy - Toast confirmation on success
- Fetches presigned view URL via
vms.api.get_view_url - Native
<video>element with controls
- Build DashboardPage (with Inbox badge)
- Build ProjectsPage with create dialog
- Build ProjectDetailPage with asset listing + category badges
- Build
useUploadhook (with concurrency pool of 2) - Build UploadDialog component (with category selector + optional project)
- Build InboxPage (asset list, bulk select, quick upload)
- Build MoveAssetDialog (project picker, move API call)
- Build VideoPlayer component
- End-to-end test: upload to inbox → move to project → upload to project with category → play it back
Automatically transcribe videos categorized as "Review" or "Final" — not raw source footage. Transcriptions are useful for searchability, accessibility, and review workflows (e.g., reading through a cut's narration without watching the full video).
1. VMS Transcription (DocType) — vms/video_management_solution/doctype/vms_transcription/
- Fields:
asset(Link→VMS Asset, unique — one transcription per asset),status(Pending/Processing/Completed/Failed),transcript_text(Long Text — full plain-text transcript),transcript_segments(JSON — timestamped segments array[{start, end, text}]),language(default "en"),provider(e.g., "Cloudflare Workers AI", "OpenAI Whisper"),error_message,created_at,completed_at - Naming:
naming_series:VMS-TXN-.#####
2. Transcription trigger logic — vms/transcription.py
- When an asset's category is changed to "Review" or "Final" (or uploaded directly as one), check if a transcription already exists. If not, enqueue a background job.
enqueue_transcription(asset_name)— creates a VMS Transcription doc in "Pending" status, enqueuesrun_transcriptionviafrappe.enqueuerun_transcription(transcription_name)— downloads the video from R2 (presigned GET), sends audio to the transcription provider, parses the result intotranscript_text+transcript_segments, updates status- Provider options (configurable in VMS Settings):
- Cloudflare Workers AI (Whisper model via REST API — stays in Cloudflare ecosystem)
- OpenAI Whisper API (fallback)
3. VMS Settings additions
- Transcription provider (Select: Cloudflare Workers AI / OpenAI Whisper)
- Transcription API key (Password field)
- Auto-transcribe on upload (Check, default on) — when enabled, "Review" and "Final" assets are auto-transcribed on upload confirmation
4. API endpoints (added to vms/api.py)
get_transcription(asset_name)→ returns transcription text + segments if availablerequest_transcription(asset_name)→ manually trigger transcription for an asset (must be Review or Final category)
Transcription panel on the Video Player / Asset Detail view:
- If transcription exists and is "Completed": show full transcript text, clickable timestamped segments (click a segment → seek video to that timestamp)
- If "Processing": show a spinner with "Transcribing..."
- If "Failed": show error + "Retry" button
- If no transcription and category is Review/Final: show "Transcribe" button to manually trigger
- If category is Source/Cut: no transcription UI shown (not eligible)
Search (future enhancement placeholder): transcription text is stored as Long Text in Frappe, so useFrappeGetDocList with text filters can search across transcripts.
- Create VMS Transcription DocType
- Add transcription settings to VMS Settings DocType
- Create
vms/transcription.py(enqueue + worker logic) - Add transcription API endpoints to
vms/api.py - Hook into asset category change / upload confirmation to auto-trigger transcription
- Build Transcription panel component in frontend
- Integrate panel into Video Player / Asset Detail view
- End-to-end test: upload a "Review" video → transcription auto-starts → transcript appears with timestamps
Auto-generate thumbnails for video assets using server-side FFmpeg. Thumbnails are displayed in grid/list views for quick visual identification.
1. Thumbnail generation — vms/thumbnails.py
generate_thumbnail(asset_name)— downloads the first ~5MB of the video from R2 (presigned GET), runs FFmpeg to extract a single frame (e.g. at 1s or 10% into the video), uploads the thumbnail JPEG to R2 at{prefix}/thumbs/{asset_id}.jpg, updatesthumbnail_r2_keyon the VMS Asset doc- Uses
subprocessto call FFmpeg:ffmpeg -ss 1 -i input -vframes 1 -f image2 -q:v 3 thumb.jpg - Requires FFmpeg installed on the server
2. Background job integration
- After
confirm_uploadmarks an asset as "Ready", enqueuegenerate_thumbnailviafrappe.enqueue - Thumbnail generation is non-blocking — the asset is usable immediately, thumbnail appears once the job completes
3. API additions (in vms/api.py)
get_thumbnail_url(asset_name)→ returns presigned GET URL for the thumbnail (or null if not yet generated)- Alternatively, serve via Cloudflare Image Transformations for on-the-fly resizing if configured
4. VMS Settings additions
thumbnail_frame_offset(Int, default 1) — seconds into the video to capture the thumbnail frame
- Asset cards (grid and list views) show the thumbnail image when
thumbnail_r2_keyis populated - Placeholder icon shown while thumbnail is being generated or if generation failed
- Fetch thumbnail URLs alongside asset data (batch or per-asset)
- Create
vms/thumbnails.py(FFmpeg subprocess + R2 upload) - Hook into
confirm_uploadto enqueue thumbnail generation - Add
get_thumbnail_urlAPI endpoint - Update frontend asset cards to display thumbnails
- Test: upload video → thumbnail auto-generates → appears in grid/list views
- Video Transcoding Pipeline: Uploaded videos in non-web-friendly formats (ProRes, HEVC in
.mov, etc.) are served as-is, causing playback issues in Chrome/Firefox (audio plays but no video). Add server-side FFmpeg transcoding to convert uploads to H.264/AAC in MP4 container — the universally supported web format. Flow: afterconfirm_upload, enqueue a background job that downloads from R2, transcodes via FFmpeg (-c:v libx264 -c:a aac -movflags +faststart), uploads the transcoded MP4 back to R2, and updates the asset'sr2_key/file_type. Serve the transcoded version for playback while optionally retaining the original for download. Addtranscoding_statusfield to VMS Asset and a progress indicator in the frontend. - Folder support within projects: R2 is flat (no real folders — just key prefixes). Add a
folderfield to VMS Asset, update R2 key format to{project}/{folder}/{uuid}.{ext}, build a folder browser UI in ProjectDetailPage (create/rename/delete folders, drag assets between folders). No R2 API calls needed to "create" folders — just use the prefix. - Video Review: Timestamped comments (VMS Comment DocType), Frame.io-like review workflow with transcript-linked comments
- Version tracking: Multiple asset versions with comparison
- Email notifications: New uploads, review completions
- Transcript search: Full-text search across all transcriptions to find specific moments across projects
bench --site <site> migrate— DocTypes created without errorscd frontend && yarn dev— React app boots, redirects to Frappe/loginif not authenticated- Configure R2 credentials in VMS Settings (Desk)
- Login → upload a video with no project → appears in Inbox
- Move the Inbox asset to a project → asset disappears from Inbox, appears in project
- Upload a video directly to a project with category "Review" → asset shows category badge
- Change an asset's category from Source → Review → transcription auto-triggers (Phase 3)
- Play back a video via presigned URL → transcript panel shows timestamped text (Phase 3)