A powerful AI-driven video subtitle editing tool focused on intelligent subtitle generation, editing, and video clipping.
- 🎤 Intelligent Speech Recognition: High-precision speech-to-text based on Whisper model, supporting multiple languages
- ✂️ Visual Subtitle Editing: Intuitive subtitle segment selection and deletion interface
- 🎬 Real-time Video Preview: Video player synchronized with subtitles, supporting interval playback
- 📤 Multi-format Export: Support for SRT, JSON subtitle formats and video file export
- 🎨 Subtitle Style Customization: Custom subtitle fonts, colors, positions and other styles
- 🌐 Internationalization Support: Componentized internationalization design, supporting Chinese, English, and custom language packs (such as Japanese examples)
- ⚡ Modern Tech Stack: React 19 + TypeScript + Vite + Tailwind CSS
- 🧠 Local AI Processing: Using Hugging Face Transformers.js to run AI models locally in the browser
- 🎯 Web Workers: ASR processing runs in background threads without blocking the main interface
- 📱 Responsive Design: Modern interface adapted to different screen sizes
- 🎪 Component Architecture: Modular design, easy to maintain and extend
- Node.js 18+
- pnpm (recommended) or npm
- Clone the project
git clone https://github.com/x007xyz/flycut-caption.git
cd flycut-caption- Install dependencies
pnpm install- Start development server
pnpm dev- Open browser
http://localhost:5173
# Build project
pnpm build
# Preview build result
pnpm preview- Supported formats: MP4, WebM, AVI, MOV
- Supported audio: MP3, WAV, OGG
- Drag and drop files to upload area or click to select files
After uploading, enter the ASR configuration interface:
- Select recognition language (supports Chinese, English and other languages)
- Click start recognition, AI will automatically generate timestamped subtitles
- Recognition process runs in background without affecting interface operations
- Select segments: Choose subtitle segments to delete from the list
- Batch operations: Support select all, batch delete, undo delete operations
- Real-time preview: Click subtitle segments to jump to corresponding time points
- History records: Support undo/redo operations
- Preview mode: Automatically skip deleted segments to preview final result
- Keyboard shortcuts:
Space: Play/Pause←/→: Rewind/Fast forward 5 secondsShift + ←/→: Rewind/Fast forward 10 seconds↑/↓: Adjust volumeM: Mute/UnmuteF: Fullscreen
- Font settings: Font size, weight, color
- Position adjustment: Subtitle display position, alignment
- Background style: Background color, transparency, border
- Real-time preview: WYSIWYG style adjustment
- Subtitle export: SRT format (universal subtitle format), JSON format
- Video export:
- Keep only non-deleted segments
- Option to burn subtitles into video
- Support different quality settings
- Multiple format outputs
FlyCut Caption adopts componentized internationalization design, supporting flexible language pack management and real-time language switching. The component can automatically sync external language changes with internal UI components.
import { FlyCutCaption, zhCN, enUS } from '@flycut/caption-react'
// Use built-in Chinese language pack
<FlyCutCaption
config={{ language: 'zh' }}
locale={zhCN}
/>
// Use built-in English language pack
<FlyCutCaption
config={{ language: 'en' }}
locale={enUS}
/>import { FlyCutCaption, type FlyCutCaptionLocale } from '@flycut/caption-react'
// Create custom language pack (Japanese example)
const customJaJP: FlyCutCaptionLocale = {
common: {
loading: '読み込み中...',
error: 'エラー',
success: '成功',
confirm: '確認',
cancel: 'キャンセル',
ok: 'OK',
// ... more common translations
},
components: {
fileUpload: {
dragDropText: 'ビデオファイルをここにドラッグするか、クリックして選択',
selectFile: 'ファイルを選択',
supportedFormats: 'サポート形式:',
// ... more component translations
},
subtitleEditor: {
title: '字幕エディター',
addSubtitle: '字幕を追加',
deleteSelected: '選択項目を削除',
// ... more editor translations
},
// ... other component translations
},
messages: {
fileUpload: {
uploadSuccess: 'ファイルアップロード成功',
uploadFailed: 'ファイルアップロード失敗',
// ... more message translations
},
// ... other message translations
}
}
// Use custom language pack
<FlyCutCaption
config={{ language: 'ja' }}
locale={customJaJP}
/>The new componentized approach provides better language synchronization between external controls and internal components:
import { useState } from 'react'
import { FlyCutCaption, zhCN, enUS, type FlyCutCaptionLocale } from '@flycut/caption-react'
function App() {
const [currentLanguage, setCurrentLanguage] = useState('zh')
const [currentLocale, setCurrentLocale] = useState<FlyCutCaptionLocale | undefined>(undefined)
const handleLanguageChange = (language: string) => {
console.log('Language switched to:', language)
setCurrentLanguage(language)
// Set corresponding language pack based on language
switch (language) {
case 'zh':
case 'zh-CN':
setCurrentLocale(zhCN)
break
case 'en':
case 'en-US':
setCurrentLocale(enUS)
break
case 'ja':
case 'ja-JP':
setCurrentLocale(customJaJP) // Custom Japanese pack
break
default:
setCurrentLocale(undefined) // Use default language pack
}
}
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold text-center mb-8">
FlyCut Caption Internationalization Demo
</h1>
{/* External Language Controls */}
<div className="mb-8 text-center space-y-4">
<div>
<h2 className="text-xl font-semibold mb-4">Language Switcher</h2>
<div className="flex justify-center gap-4">
<button
className={`px-4 py-2 rounded ${currentLanguage === 'zh' ? 'bg-primary text-primary-foreground' : 'bg-secondary'}`}
onClick={() => handleLanguageChange('zh')}
>
中文 (Built-in)
</button>
<button
className={`px-4 py-2 rounded ${currentLanguage === 'en' ? 'bg-primary text-primary-foreground' : 'bg-secondary'}`}
onClick={() => handleLanguageChange('en')}
>
English (Built-in)
</button>
<button
className={`px-4 py-2 rounded ${currentLanguage === 'ja' ? 'bg-primary text-primary-foreground' : 'bg-secondary'}`}
onClick={() => handleLanguageChange('ja')}
>
日本語 (Custom)
</button>
</div>
</div>
<div className="bg-muted p-4 rounded-lg">
<p className="text-sm">
<strong>Current Language:</strong> {currentLanguage}
</p>
<p className="text-sm">
<strong>Language Pack Type:</strong> {currentLocale ? 'Custom Language Pack' : 'Built-in Language Pack'}
</p>
</div>
</div>
{/* FlyCut Caption Component */}
<div className="border rounded-lg p-4">
<h2 className="text-xl font-semibold mb-4">FlyCut Caption Component</h2>
<FlyCutCaption
config={{
theme: 'auto',
language: currentLanguage,
enableThemeToggle: true,
enableLanguageSelector: true // Internal language selector will sync with external changes
}}
locale={currentLocale}
onLanguageChange={handleLanguageChange} // Sync internal changes back to external state
onError={(error) => {
console.error('Component error:', error)
}}
onProgress={(stage, progress) => {
console.log(`Progress: ${stage} - ${progress}%`)
}}
/>
</div>
</div>
</div>
)
}| Language | Import | Description |
|---|---|---|
| Chinese (Simplified) | zhCN |
简体中文 |
| English (US) | enUS |
English (United States) |
| Default | defaultLocale |
Same as zhCN |
// Import language pack utilities
import { LocaleProvider, useLocale, useTranslation } from '@flycut/caption-react'
// Use LocaleProvider for nested components
<LocaleProvider language="zh" locale={zhCN}>
<YourComponent />
</LocaleProvider>
// Access language pack context
const { t, setLanguage, registerLocale } = useLocale()
// Register custom language pack
registerLocale('fr', frenchLocale)
// Programmatic language switching
setLanguage('fr')📚 Detailed internationalization guide: See INTERNATIONALIZATION.md for complete documentation on language packs, custom localization and advanced i18n features.
# Install package
npm install @flycut/caption-react
# TypeScript projects don't need additional type packages
# Type definitions are includedThe component requires CSS styles to work properly:
import '@flycut/caption-react/styles'
// or specific CSS file
import '@flycut/caption-react/dist/caption-react.css'import { FlyCutCaption } from '@flycut/caption-react'
import '@flycut/caption-react/styles'
function VideoEditor() {
return (
<div className="video-editor-container">
<FlyCutCaption />
</div>
)
}import { FlyCutCaption } from '@flycut/caption-react'
function VideoEditorWithEvents() {
const handleFileSelected = (file: File) => {
console.log('Selected file:', file.name, file.size)
}
const handleSubtitleGenerated = (subtitles: SubtitleChunk[]) => {
console.log('Generated subtitles:', subtitles.length)
// Save subtitles to backend
saveSubtitles(subtitles)
}
const handleVideoProcessed = (blob: Blob, filename: string) => {
// Handle processed video
const url = URL.createObjectURL(blob)
// Download or upload to server
downloadFile(url, filename)
}
const handleError = (error: Error) => {
// Handle errors gracefully
console.error('FlyCut Caption error:', error)
showErrorNotification(error.message)
}
return (
<FlyCutCaption
onFileSelected={handleFileSelected}
onSubtitleGenerated={handleSubtitleGenerated}
onVideoProcessed={handleVideoProcessed}
onError={handleError}
/>
)
}import { FlyCutCaption } from '@flycut/caption-react'
function ConfiguredEditor() {
const config = {
// Theme settings
theme: 'dark' as const,
// Language settings
language: 'zh-CN',
asrLanguage: 'zh',
// Feature toggles
enableDragDrop: true,
enableExport: true,
enableVideoProcessing: true,
enableThemeToggle: true,
enableLanguageSelector: true,
// File constraints
maxFileSize: 1000, // 1GB
supportedFormats: ['mp4', 'webm', 'mov']
}
return (
<FlyCutCaption config={config} />
)
}import { FlyCutCaption } from '@flycut/caption-react'
import './custom-styles.css'
function StyledEditor() {
return (
<FlyCutCaption
className="my-custom-editor"
style={{
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
/>
)
}/* custom-styles.css */
.my-custom-editor {
--flycut-primary: #10b981;
--flycut-border-radius: 12px;
}
.my-custom-editor .subtitle-item {
border-radius: var(--flycut-border-radius);
}| Property | Type | Default | Description |
|---|---|---|---|
className |
string |
undefined |
Custom CSS class name |
style |
CSSProperties |
undefined |
Custom inline styles |
config |
FlyCutCaptionConfig |
defaultConfig |
Component configuration |
locale |
FlyCutCaptionLocale |
undefined |
Custom language pack |
onReady |
() => void |
undefined |
Called when component is ready |
onFileSelected |
(file: File) => void |
undefined |
Called when a file is selected |
onSubtitleGenerated |
(subtitles: SubtitleChunk[]) => void |
undefined |
Called when subtitles are generated |
onSubtitleChanged |
(subtitles: SubtitleChunk[]) => void |
undefined |
Called when subtitles are changed |
onVideoProcessed |
(blob: Blob, filename: string) => void |
undefined |
Called when video processing is complete |
onExportComplete |
(blob: Blob, filename: string) => void |
undefined |
Called when export is complete |
onError |
(error: Error) => void |
undefined |
Called when an error occurs |
onProgress |
(stage: string, progress: number) => void |
undefined |
Called to report progress updates |
onLanguageChange |
(language: string) => void |
undefined |
Called when language changes |
| Property | Type | Default | Description |
|---|---|---|---|
theme |
'light' | 'dark' | 'auto' |
'auto' |
Theme mode |
language |
string |
'zh-CN' |
Interface language |
asrLanguage |
string |
'auto' |
ASR recognition language |
enableDragDrop |
boolean |
true |
Enable drag and drop file upload |
enableExport |
boolean |
true |
Enable export functionality |
enableVideoProcessing |
boolean |
true |
Enable video processing functionality |
enableThemeToggle |
boolean |
true |
Enable theme toggle button |
enableLanguageSelector |
boolean |
true |
Enable language selector |
maxFileSize |
number |
500 |
Maximum file size in MB |
supportedFormats |
string[] |
['mp4', 'webm', 'avi', 'mov', 'mp3', 'wav', 'ogg'] |
Supported file formats |
The component comes with built-in styles that you need to import:
import '@flycut/caption-react/styles'You can also customize the appearance by:
- CSS Custom Properties: Override CSS variables for colors and spacing
- Custom CSS Classes: Use the
classNameprop to apply custom styles - Theme Configuration: Use the
themeconfig option for light/dark modes
:root {
--flycut-primary: #3b82f6;
--flycut-background: #ffffff;
--flycut-foreground: #1f2937;
--flycut-muted: #f3f4f6;
--flycut-border: #e5e7eb;
}
.dark {
--flycut-background: #111827;
--flycut-foreground: #f9fafb;
--flycut-muted: #374151;
--flycut-border: #4b5563;
}- Frontend Framework: React 19 with Hooks
- Type Checking: TypeScript 5.8
- Build Tool: Vite 7.1
- Styling Solution: Tailwind CSS 4.1 + Shadcn/ui
- State Management: Zustand + React Context
- AI Model: Hugging Face Transformers.js
- Video Processing: WebAV
- Internationalization: react-i18next
src/
├── components/ # UI Components
│ ├── FileUpload/ # File upload component
│ ├── VideoPlayer/ # Video player
│ ├── SubtitleEditor/ # Subtitle editor
│ ├── ProcessingPanel/ # Processing panel
│ ├── ExportPanel/ # Export panel
│ └── ui/ # Basic UI components
├── hooks/ # Custom Hooks
├── services/ # Business service layer
│ ├── asrService.ts # ASR speech recognition service
│ └── UnifiedVideoProcessor.ts # Video processing service
├── stores/ # State management
│ ├── appStore.ts # Application global state
│ ├── historyStore.ts # Subtitle history records
│ └── themeStore.ts # Theme state
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── workers/ # Web Workers
│ └── asrWorker.ts # ASR processing worker thread
└── locales/ # Internationalization files
- Local speech recognition based on Whisper model
- Web Workers background processing without blocking main thread
- Support multiple languages and audio formats
- Generate precise word-level timestamps
- Visual subtitle segment management
- Support batch selection and operations
- Real-time video playback position synchronization
- History records and undo/redo functionality
- Local video processing based on WebAV
- Support interval clipping and merging
- Subtitle burn-in functionality
- Multiple output formats and quality options
# Start development server
pnpm dev
# Type checking
pnpm run typecheck
# Code linting
pnpm lint
# Build project
pnpm build
# Preview build
pnpm previewProject uses Shadcn/ui component library:
pnpm dlx shadcn@latest add <component-name>- TypeScript strict mode
- ESLint + React related rules
- Functional components + Hooks
- Componentized and modular design
The component supports various video processing features:
- Video: MP4, WebM, AVI, MOV
- Audio: MP3, WAV, OGG
- Quality: Low, Medium, High
- Format: MP4, WebM
- Subtitle Processing: Burn-in, Separate file
- Audio Preservation: Enabled by default
- Chrome 88+
- Firefox 78+
- Safari 14+
- Edge 88+
import React, { useState, useCallback } from 'react'
import { FlyCutCaption, zhCN, enUS, type FlyCutCaptionLocale } from '@flycut/caption-react'
import '@flycut/caption-react/styles'
function VideoEditorApp() {
const [language, setLanguage] = useState<'zh' | 'en'>('zh')
const [subtitles, setSubtitles] = useState([])
const [isProcessing, setIsProcessing] = useState(false)
const locale = language === 'zh' ? zhCN : enUS
const handleLanguageChange = useCallback((newLang: string) => {
setLanguage(newLang as 'zh' | 'en')
}, [])
const handleSubtitleGenerated = useCallback((newSubtitles) => {
setSubtitles(newSubtitles)
// Auto-save to local storage
localStorage.setItem('flycut-subtitles', JSON.stringify(newSubtitles))
}, [])
const handleProgress = useCallback((stage: string, progress: number) => {
setIsProcessing(progress < 100)
}, [])
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Video Editor</h1>
<div className="flex gap-2">
<button
onClick={() => handleLanguageChange('zh')}
className={language === 'zh' ? 'btn-primary' : 'btn-secondary'}
>
中文
</button>
<button
onClick={() => handleLanguageChange('en')}
className={language === 'en' ? 'btn-primary' : 'btn-secondary'}
>
English
</button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<FlyCutCaption
config={{
theme: 'auto',
language,
enableDragDrop: true,
enableExport: true,
maxFileSize: 1000
}}
locale={locale}
onLanguageChange={handleLanguageChange}
onSubtitleGenerated={handleSubtitleGenerated}
onProgress={handleProgress}
onError={(error) => {
console.error('Error:', error)
// Show user-friendly error message
alert('An error occurred during processing, please try again')
}}
/>
</div>
{isProcessing && (
<div className="mt-4 text-center">
<div className="inline-flex items-center px-4 py-2 bg-blue-100 rounded-lg">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
Processing, please wait...
</div>
</div>
)}
{subtitles.length > 0 && (
<div className="mt-8 bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold mb-4">Generated Subtitles ({subtitles.length} items)</h2>
<div className="text-sm text-gray-600">
Subtitles have been automatically saved to local storage
</div>
</div>
)}
</main>
</div>
)
}
export default VideoEditorApp// pages/editor.tsx
import dynamic from 'next/dynamic'
import { useState } from 'react'
// Dynamically import to avoid SSR issues
const FlyCutCaption = dynamic(
() => import('@flycut/caption-react').then(mod => mod.FlyCutCaption),
{ ssr: false }
)
export default function EditorPage() {
return (
<div style={{ height: '100vh' }}>
<FlyCutCaption
config={{
theme: 'auto',
language: 'zh'
}}
onVideoProcessed={(blob, filename) => {
// Handle video processing result
const url = URL.createObjectURL(blob)
window.open(url, '_blank')
}}
/>
</div>
)
}- Always import styles: The component requires CSS to work properly
- Handle errors gracefully: Implement proper error boundaries and user feedback
- Optimize for performance: Use dynamic imports for SSR applications
- Provide user feedback: Show loading states and progress indicators
- Responsive design: Ensure your container has appropriate height/width
- Accessibility: The component includes ARIA labels and keyboard navigation
- Memory management: Clean up blob URLs when components unmount
- Node.js 18+
- pnpm 8+
git clone https://github.com/x007xyz/flycut-caption.git
cd flycut-caption
pnpm install# Start development server
pnpm dev
# Build library
pnpm run build:lib
# Build demo
pnpm run build:demo
# Lint code
pnpm lint
# Run test app
cd test-app && pnpm devWe welcome contributions of all kinds!
- Fork this project
- Create feature branch (
git checkout -b feature/AmazingFeature) - Commit changes (
git commit -m 'Add some AmazingFeature') - Push to branch (
git push origin feature/AmazingFeature) - Create Pull Request
- 🐛 Bug fixes
- ✨ New feature development
- 📝 Documentation improvements
- 🎨 UI/UX optimizations
- ⚡ Performance optimizations
- 🌐 Internationalization translations
This project is licensed under the MIT License with additional terms:
- ✅ Allowed: Personal, educational, commercial use
- ✅ Allowed: Modification, distribution, creating derivative works
- ❌ Prohibited: Removing or modifying logos, watermarks, brand elements in the software interface
- ❌ Prohibited: Hiding or tampering with attribution statements
To remove brand elements, please contact FlyCut Team for explicit written permission.
See LICENSE file for details.
- Hugging Face - Excellent Transformers.js library
- OpenAI Whisper - Powerful speech recognition model
- Shadcn/ui - Elegant UI component library
- WebAV - Powerful web audio/video processing library
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 📖 Documentation: API Docs
If this project helps you, please give us a ⭐ Star!
Made with ❤️ by FlyCut Team