{clip.title}
{clip.reason}
{clip.subtitle_text}
)} {/* Action row — primary CTA on top, icon group below */}import {
ArrowLeft,
Captions,
Check,
Clock3,
Download,
Film,
FolderOpen,
Gauge,
Languages,
Layers,
Link as LinkIcon,
Loader2,
Maximize2,
Moon,
Move,
Music2,
PanelRightOpen,
Pause,
Play,
RefreshCcw,
Scissors,
SkipBack,
SkipForward,
SlidersHorizontal,
Sparkles,
Sun,
Trash2,
Type,
Upload,
Volume2,
Wand2,
Zap,
} from "lucide-react";
import React, { useEffect, useMemo, useRef, useState } from "react";
const API_BASE = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
const LANGUAGES = [
{ code: "en", label: "EN", name: "English" },
{ code: "th", label: "TH", name: "ไทย" },
{ code: "ja", label: "JP", name: "日本語" },
{ code: "zh", label: "ZH", name: "中文" },
{ code: "ko", label: "KO", name: "한국어" },
];
const NICHES = [
"education",
"gaming",
"podcast",
"commentary",
"cars",
"beauty",
"fitness",
"finance",
"tech",
"lifestyle",
"music",
"other",
];
const CLIP_STYLES = ["informative", "funny", "dramatic", "educational", "commentary"];
const LANGUAGE_OPTIONS = ["Thai", "English", "Japanese", "Chinese", "Korean", "Auto"];
const PLATFORM_OPTIONS = ["tiktok", "youtube_shorts", "instagram_reels"];
const FONT_OPTIONS = ["Inter", "Noto Sans Thai", "Poppins", "Montserrat", "Arial", "Impact"];
const CUE_DENSITIES = ["word", "short", "medium", "long"];
const CAPTION_ANIMATIONS = ["none", "highlight", "pop", "bounce"];
const defaultCaptionStyle = {
fontFamily: "Inter",
fontSize: 38,
fillColor: "#ffffff",
strokeColor: "#080b12",
strokeWidth: 4,
position: 18,
x: 50, // horizontal % of preview stage
y: 82, // vertical % of preview stage (default near bottom)
cueDensity: "short",
animation: "highlight",
};
const captionPresets = {
clean: {
fontFamily: "Inter",
fontSize: 34,
fillColor: "#ffffff",
strokeColor: "#111827",
strokeWidth: 3,
position: 18,
cueDensity: "medium",
animation: "none",
},
bold: {
fontFamily: "Impact",
fontSize: 46,
fillColor: "#fef08a",
strokeColor: "#020617",
strokeWidth: 5,
position: 20,
cueDensity: "short",
animation: "pop",
},
karaoke: {
fontFamily: "Poppins",
fontSize: 40,
fillColor: "#ffffff",
strokeColor: "#020617",
strokeWidth: 4,
position: 22,
cueDensity: "word",
animation: "highlight",
},
};
const defaultProfile = {
niche: "education",
niche_custom: "",
channel_description: "",
clip_style: "informative",
clip_length_seconds: 60,
clip_count: 5,
primary_language: "Thai",
target_platform: "tiktok",
};
const en = {
appSubtitle: "Turn long videos into short clips — powered by AMD ROCm",
idle: "Idle",
queued: "Queued",
running: "Processing",
completed: "Done",
failed: "Failed",
demoMode: "Demo mode",
productionMode: "Live mode",
theme: "Theme",
language: "Language",
startPipeline: "Generate clips",
channelProfile: "Channel profile",
channelProfileText: "Tell the AI about your channel so it picks the right moments.",
videoInput: "Video",
niche: "Channel niche",
nicheHelp: "Pick the closest category, or choose Other for something more specific.",
customNiche: "Your niche",
customNichePlaceholder: "e.g. Thai AI tutorials for beginners",
channelDescription: "Channel description",
channelDescriptionHelp: "Write how you'd describe your channel to a friend. The AI uses this to stay on-brand.",
channelDescriptionPlaceholder: "e.g. I explain AI tools in simple Thai with a bit of humor.",
clipStyle: "Clip style",
clipLength: "Clip length (seconds)",
clipCount: "Number of clips",
clipCountHelp: "How many clips the AI should find in this video.",
primaryLanguage: "Language",
platform: "Platform",
youtube: "YouTube",
upload: "Upload",
youtubeUrl: "YouTube URL",
youtubePlaceholder: "https://www.youtube.com/watch?v=...",
videoFile: "Video file",
pipeline: "Processing",
ready: "Ready",
progressNote: "Updates step by step — clip-by-clip progress shows during rendering.",
currentStep: "Current step",
timing: "Timing",
timing_input: "Download",
timing_transcription: "Transcription",
timing_highlight_detection: "Highlights",
timing_multimodal_analysis: "Visual check",
timing_clip_generation: "Rendering",
timing_total: "Total",
transcript: "Transcript",
transcriptEmpty: "Transcript will appear once the audio is processed.",
clips: "Clips",
noClips: "No clips yet",
noClipsText: "Hit Generate to let the AI find and cut the best moments.",
readyClips: "ready",
score: "Score",
reason: "Why",
duration: "Duration",
start: "Start",
end: "End",
subtitles: "Subtitles",
openEditor: "Edit clip",
approve: "Approve",
approved: "Approved",
regenerate: "Redo",
delete: "Delete",
download: "Download",
backToDashboard: "Back",
editor: "Clip editor",
editorText: "Trim timing, edit subtitles, and approve before downloading.",
preview: "Preview",
clipRange: "Clip range",
subtitleCues: "Subtitle cues",
subtitleCueHelp: "Shorter cues work better on TikTok, Reels, and Shorts.",
timelineTracks: "Timeline",
videoTrack: "Video",
subtitleTrack: "Subtitles",
audioTrack: "Audio",
toolSelect: "Select",
toolTrim: "Trim",
toolCaptions: "Captions",
toolStyle: "Style",
toolExport: "Export",
captionStyle: "Caption style",
captionPreset: "Preset",
presetClean: "Clean",
presetBold: "Bold",
presetKaraoke: "Karaoke",
font: "Font",
fontSize: "Size",
fillColor: "Fill color",
strokeColor: "Outline color",
strokeWidth: "Outline width",
captionPosition: "Position",
captionLength: "Line length",
animation: "Animation",
density_word: "Word by word",
density_short: "Short phrases",
density_medium: "Medium phrases",
density_long: "Full lines",
animation_none: "None",
animation_highlight: "Highlight",
animation_pop: "Pop",
animation_bounce: "Bounce",
editorTools: "Quick trim",
rangeStart: "Start",
rangeEnd: "End",
trimStartBack: "−0.5s",
trimStartForward: "+0.5s",
trimEndBack: "−0.5s",
trimEndForward: "+0.5s",
moveClipLeft: "−1s",
moveClipRight: "+1s",
setClipLength30: "30s",
setClipLength60: "60s",
setClipLength90: "90s",
inspector: "Details",
title: "Title",
status: "Status",
notApproved: "Not approved",
model: "Model",
source: "Source",
source_upload: "Upload",
source_youtube: "YouTube",
source_video: "Video",
settings: "Settings",
stepInput: "Download",
stepTranscription: "Transcribe",
stepHighlights: "Highlights",
stepVisual: "Visual AI",
stepRender: "Render",
stepFinal: "Done",
renderProgress: "Rendering {{current}} of {{total}}",
appCta: "Generate clips",
niche_education: "Education",
niche_gaming: "Gaming",
niche_podcast: "Podcast",
niche_commentary: "Commentary",
niche_cars: "Cars",
niche_beauty: "Beauty",
niche_fitness: "Fitness",
niche_finance: "Finance",
niche_tech: "Tech",
niche_lifestyle: "Lifestyle",
niche_music: "Music",
niche_other: "Other",
style_informative: "Informative",
style_funny: "Funny",
style_dramatic: "Dramatic",
style_educational: "Educational",
style_commentary: "Commentary",
platform_tiktok: "TikTok",
platform_youtube_shorts: "YouTube Shorts",
platform_instagram_reels: "Instagram Reels",
languageOption_Thai: "Thai",
languageOption_English: "English",
languageOption_Japanese: "Japanese",
languageOption_Chinese: "Chinese",
languageOption_Korean: "Korean",
languageOption_Auto: "Auto-detect",
// NLE editor
mediaBin: "Clips",
aiAssistant: "AI Assistant",
aiReason: "AI hasn't explained yet — try regenerating.",
aiReasonHead: "Why this moment",
aiVisualHead: "Visual analysis",
aiTighten: "Tighten to 30s",
aiEmphasize: "Extend to 60s",
aiRedoAll: "Regenerate clip",
aiDeleteClip: "Remove clip",
aiActionRedoSub: "Let AI pick a new moment",
aiActionTightenSub: "Best for Reels & Shorts",
aiActionEmphasizeSub: "Best for TikTok storytelling",
aiActionDeleteSub: "Drop from this batch",
dragToTrim: "Drag edges to trim · drag body to move",
dragCueToRetime: "Drag cue edges or body to retime",
dragToPosition: "Drag caption to reposition",
// Subtitle editor
addCue: "Add subtitle",
cuePlaceholder: "Type subtitle text...",
seekToCue: "Jump to this cue",
aiSubtitleHead: "AI subtitle helpers",
aiPolish: "Polish all",
aiTranslate: "Translate",
aiAutoTime: "Auto-time",
aiAutoTimeHelp: "Re-time using Whisper word-level timestamps",
// Clip edit
clipEdit: "Clip length",
clipLengthLabel: "Set length",
clipExtendLabel: "Extend",
clipSkipLabel: "Cut middle out",
clipSkipAdd: "Cut",
clipRebuildBtn: "Rebuild clip",
from: "from",
to: "to",
// GPU status
gpuActive: "GPU active",
gpuDemo: "demo",
gpuPending: "GPU pending",
};
const translations = {
en,
th: {
...en,
appSubtitle: "ตัดคลิปสั้นจากวิดีโอยาวอัตโนมัติ บน AMD ROCm",
idle: "พร้อมใช้",
queued: "รอคิว",
running: "กำลังประมวลผล",
completed: "เสร็จแล้ว",
failed: "เกิดข้อผิดพลาด",
demoMode: "โหมดเดโม",
productionMode: "โหมดจริง",
theme: "ธีม",
language: "ภาษา",
startPipeline: "สร้างคลิป",
channelProfile: "โปรไฟล์ช่อง",
channelProfileText: "บอก AI ว่าช่องคุณเป็นแนวไหน เพื่อให้เลือกไฮไลต์ได้ตรงกับสไตล์คุณ",
videoInput: "วิดีโอ",
niche: "แนวช่อง",
nicheHelp: "เลือกหมวดที่ใกล้เคียงที่สุด หากเฉพาะเจาะจงมากให้เลือก อื่น ๆ",
customNiche: "แนวช่องของคุณ",
customNichePlaceholder: "เช่น สอน AI ภาษาไทยสำหรับมือใหม่",
channelDescription: "อธิบายช่อง",
channelDescriptionHelp: "เขียนแบบเป็นกันเอง AI จะนำไปใช้คัดเลือกช่วงที่เหมาะกับสไตล์ช่อง",
channelDescriptionPlaceholder: "เช่น ช่องสอนใช้ AI แบบง่าย ๆ เข้าใจได้ทันที มีมุกนิดหน่อย",
clipStyle: "สไตล์คลิป",
clipLength: "ความยาวคลิป (วินาที)",
clipCount: "จำนวนคลิป",
clipCountHelp: "จำนวนคลิปที่ต้องการให้ AI หาให้จากวิดีโอนี้",
primaryLanguage: "ภาษา",
platform: "แพลตฟอร์ม",
youtube: "YouTube",
upload: "อัปโหลด",
youtubeUrl: "ลิงก์ YouTube",
youtubePlaceholder: "https://www.youtube.com/watch?v=...",
videoFile: "ไฟล์วิดีโอ",
pipeline: "การประมวลผล",
ready: "พร้อม",
progressNote: "อัปเดตทีละขั้นตอน — ระหว่างเรนเดอร์จะแสดงความคืบหน้าทีละคลิป",
currentStep: "ขั้นตอนปัจจุบัน",
timing: "เวลาที่ใช้",
timing_input: "ดาวน์โหลด",
timing_transcription: "ถอดเสียง",
timing_highlight_detection: "ไฮไลต์",
timing_multimodal_analysis: "วิเคราะห์ภาพ",
timing_clip_generation: "เรนเดอร์",
timing_total: "รวม",
transcript: "คำบรรยาย",
transcriptEmpty: "คำบรรยายจะแสดงเมื่อถอดเสียงเสร็จ",
clips: "คลิป",
noClips: "ยังไม่มีคลิป",
noClipsText: "กด สร้างคลิป เพื่อให้ AI ค้นหาและตัดช่วงที่ดีที่สุด",
readyClips: "คลิปพร้อมแล้ว",
score: "คะแนน",
reason: "เหตุผล",
duration: "ความยาว",
start: "เริ่ม",
end: "จบ",
subtitles: "ซับไตเติล",
openEditor: "แก้ไขคลิป",
approve: "อนุมัติ",
approved: "อนุมัติแล้ว",
regenerate: "ทำใหม่",
delete: "ลบ",
download: "ดาวน์โหลด",
backToDashboard: "กลับ",
editor: "ตัดต่อคลิป",
editorText: "ปรับช่วงเวลา แก้ซับไตเติล และอนุมัติก่อนดาวน์โหลด",
preview: "ตัวอย่าง",
clipRange: "ช่วงเวลาคลิป",
subtitleCues: "ซับไตเติล",
subtitleCueHelp: "ซับสั้น ๆ อ่านง่ายกว่าบน TikTok, Reels และ Shorts",
timelineTracks: "ไทม์ไลน์",
videoTrack: "วิดีโอ",
subtitleTrack: "ซับไตเติล",
audioTrack: "เสียง",
toolSelect: "เลือก",
toolTrim: "ตัด",
toolCaptions: "ซับ",
toolStyle: "สไตล์",
toolExport: "ส่งออก",
captionStyle: "สไตล์ซับไตเติล",
captionPreset: "พรีเซ็ต",
presetClean: "เรียบง่าย",
presetBold: "โดดเด่น",
presetKaraoke: "คาราโอเกะ",
font: "ฟอนต์",
fontSize: "ขนาดตัวอักษร",
fillColor: "สีตัวอักษร",
strokeColor: "สีขอบ",
strokeWidth: "ความหนาขอบ",
captionPosition: "ตำแหน่ง",
captionLength: "ความยาวต่อบรรทัด",
animation: "อนิเมชัน",
density_word: "ทีละคำ",
density_short: "วลีสั้น",
density_medium: "วลีกลาง",
density_long: "บรรทัดยาว",
animation_none: "ไม่มี",
animation_highlight: "ไฮไลต์",
animation_pop: "เด้งเข้า",
animation_bounce: "เด้งตามจังหวะ",
editorTools: "ปรับเวลาเร็ว",
rangeStart: "จุดเริ่มต้น",
rangeEnd: "จุดสิ้นสุด",
trimStartBack: "−0.5 วิ",
trimStartForward: "+0.5 วิ",
trimEndBack: "−0.5 วิ",
trimEndForward: "+0.5 วิ",
moveClipLeft: "−1 วิ",
moveClipRight: "+1 วิ",
setClipLength30: "30 วิ",
setClipLength60: "60 วิ",
setClipLength90: "90 วิ",
inspector: "รายละเอียด",
title: "ชื่อคลิป",
status: "สถานะ",
notApproved: "ยังไม่อนุมัติ",
model: "โมเดล AI",
source: "แหล่งที่มา",
source_upload: "อัปโหลด",
source_youtube: "YouTube",
source_video: "วิดีโอ",
settings: "ตั้งค่า",
stepInput: "ดาวน์โหลด",
stepTranscription: "ถอดเสียง",
stepHighlights: "ไฮไลต์",
stepVisual: "AI ภาพ",
stepRender: "เรนเดอร์",
stepFinal: "เสร็จ",
renderProgress: "กำลังเรนเดอร์คลิป {{current}} จาก {{total}}",
appCta: "สร้างคลิป",
niche_education: "การศึกษา",
niche_gaming: "เกม",
niche_podcast: "พอดแคสต์",
niche_commentary: "คอมเมนทารี",
niche_cars: "รถยนต์",
niche_beauty: "บิวตี้",
niche_fitness: "ฟิตเนส",
niche_finance: "การเงิน",
niche_tech: "เทคโนโลยี",
niche_lifestyle: "ไลฟ์สไตล์",
niche_music: "ดนตรี",
niche_other: "อื่น ๆ",
style_informative: "ให้ข้อมูล",
style_funny: "ตลก",
style_dramatic: "ดราม่า",
style_educational: "สอน",
style_commentary: "วิเคราะห์",
platform_tiktok: "TikTok",
platform_youtube_shorts: "YouTube Shorts",
platform_instagram_reels: "Instagram Reels",
languageOption_Thai: "ไทย",
languageOption_English: "อังกฤษ",
languageOption_Japanese: "ญี่ปุ่น",
languageOption_Chinese: "จีน",
languageOption_Korean: "เกาหลี",
languageOption_Auto: "ตรวจจับอัตโนมัติ",
// NLE editor
mediaBin: "คลิปทั้งหมด",
aiAssistant: "ผู้ช่วย AI",
aiReason: "AI ยังไม่ได้อธิบาย ลองสร้างใหม่ดูสิ",
aiReasonHead: "เหตุผลที่เลือกช่วงนี้",
aiVisualHead: "วิเคราะห์ภาพ",
aiTighten: "ตัดเหลือ 30 วิ",
aiEmphasize: "ขยายเป็น 60 วิ",
aiRedoAll: "สร้างคลิปนี้ใหม่",
aiDeleteClip: "ลบคลิปนี้",
aiActionRedoSub: "ให้ AI หาช่วงใหม่",
aiActionTightenSub: "เหมาะกับ Reels และ Shorts",
aiActionEmphasizeSub: "เหมาะกับ TikTok แบบเล่าเรื่อง",
aiActionDeleteSub: "เอาออกจากชุดนี้",
dragToTrim: "ลากขอบเพื่อ trim · ลากกลางเพื่อย้าย",
dragCueToRetime: "ลากขอบหรือกลางซับเพื่อปรับเวลา",
dragToPosition: "ลากข้อความเพื่อย้ายตำแหน่ง",
addCue: "เพิ่มซับ",
cuePlaceholder: "พิมพ์ข้อความซับ...",
seekToCue: "ข้ามไปที่ซับนี้",
aiSubtitleHead: "ผู้ช่วย AI สำหรับซับ",
aiPolish: "เกลาคำพูด",
aiTranslate: "แปล",
aiAutoTime: "ตั้งเวลาอัตโนมัติ",
aiAutoTimeHelp: "ปรับเวลาซับจาก Whisper รายคำ",
clipEdit: "ปรับความยาวคลิป",
clipLengthLabel: "ตั้งความยาว",
clipExtendLabel: "เพิ่มเวลา",
clipSkipLabel: "ตัดช่วงตรงกลางออก",
clipSkipAdd: "ตัดออก",
clipRebuildBtn: "สร้างคลิปใหม่",
from: "จาก",
to: "ถึง",
gpuActive: "GPU ทำงาน",
gpuDemo: "demo",
gpuPending: "รอ GPU",
},
ja: {
...en,
appSubtitle: "長尺動画を短編クリップに ― AMD ROCm 搭載",
idle: "待機中",
queued: "順番待ち",
running: "処理中",
completed: "完了",
failed: "失敗",
demoMode: "デモモード",
productionMode: "本番モード",
theme: "テーマ",
language: "言語",
startPipeline: "クリップを生成",
channelProfile: "チャンネルプロフィール",
channelProfileText: "AIが最適な瞬間を選べるよう、チャンネルの情報を入力してください。",
videoInput: "動画",
niche: "ジャンル",
nicheHelp: "近いカテゴリを選んでください。当てはまらない場合は「その他」を選択します。",
customNiche: "ジャンルを入力",
customNichePlaceholder: "例: 初心者向けタイ語AIチュートリアル",
channelDescription: "チャンネル説明",
channelDescriptionHelp: "友人に紹介するつもりで書いてください。AIがブランドの方向性を保つために使用します。",
channelDescriptionPlaceholder:
"例: タイ語でAIツールをわかりやすく、少しユーモアを交えて解説しています。",
clipStyle: "クリップのスタイル",
clipLength: "クリップの長さ(秒)",
clipCount: "クリップ数",
clipCountHelp: "この動画からAIに見つけてほしいクリップの数です。",
primaryLanguage: "言語",
platform: "配信プラットフォーム",
youtube: "YouTube",
upload: "アップロード",
youtubeUrl: "YouTube URL",
youtubePlaceholder: "https://www.youtube.com/watch?v=...",
videoFile: "動画ファイル",
pipeline: "処理状況",
ready: "準備完了",
progressNote: "工程ごとに更新され、レンダリング中はクリップ単位で進捗を表示します。",
currentStep: "現在の工程",
timing: "処理時間",
timing_input: "ダウンロード",
timing_transcription: "文字起こし",
timing_highlight_detection: "ハイライト検出",
timing_multimodal_analysis: "映像分析",
timing_clip_generation: "クリップ生成",
timing_total: "合計",
transcript: "文字起こし",
transcriptEmpty: "文字起こしが完了するとここに表示されます。",
clips: "クリップ",
noClips: "クリップがまだありません",
noClipsText: "処理を開始すると、プロフィールに合わせた候補クリップが生成されます。",
readyClips: "件のクリップ",
score: "スコア",
reason: "理由",
duration: "長さ",
start: "開始",
end: "終了",
openEditor: "エディターを開く",
editor: "クリップエディター",
editorText: "このクリップのタイミング、字幕、最終承認を調整します。",
preview: "プレビュー",
clipRange: "クリップ範囲",
subtitles: "字幕",
subtitleCues: "字幕ブロック",
subtitleCueHelp: "短い字幕ブロックのほうがTikTok、Reels、Shortsで読みやすくなります。",
timelineTracks: "タイムライン",
videoTrack: "映像",
subtitleTrack: "字幕",
audioTrack: "音声",
toolSelect: "選択",
toolTrim: "トリム",
toolCaptions: "字幕",
toolStyle: "スタイル",
toolExport: "書き出し",
captionStyle: "字幕スタイル",
captionPreset: "プリセット",
presetClean: "シンプル",
presetBold: "太字",
presetKaraoke: "カラオケ",
font: "フォント",
fontSize: "サイズ",
fillColor: "文字色",
strokeColor: "縁の色",
strokeWidth: "縁の太さ",
captionPosition: "字幕の位置",
captionLength: "字幕の長さ",
animation: "アニメーション",
density_word: "単語単位",
density_short: "短めの行",
density_medium: "標準",
density_long: "長めの行",
animation_none: "なし",
animation_highlight: "ハイライト",
animation_pop: "ポップ",
animation_bounce: "バウンス",
editorTools: "編集ツール",
rangeStart: "開始位置",
rangeEnd: "終了位置",
trimStartBack: "開始 −0.5秒",
trimStartForward: "開始 +0.5秒",
trimEndBack: "終了 −0.5秒",
trimEndForward: "終了 +0.5秒",
moveClipLeft: "クリップ −1秒",
moveClipRight: "クリップ +1秒",
setClipLength30: "30秒に調整",
setClipLength60: "60秒に調整",
setClipLength90: "90秒に調整",
inspector: "クリップ情報",
title: "タイトル",
status: "ステータス",
notApproved: "未承認",
model: "モデル",
source: "ソース",
source_upload: "アップロード",
source_youtube: "YouTube",
source_video: "動画",
settings: "設定",
stepInput: "入力",
stepTranscription: "文字起こし",
stepHighlights: "ハイライト",
stepVisual: "映像分析",
stepRender: "レンダリング",
stepFinal: "仕上げ",
renderProgress: "クリップ {{current}}/{{total}} をレンダリング中",
appCta: "クリップを生成",
approve: "承認",
approved: "承認済み",
regenerate: "再生成",
delete: "削除",
download: "ダウンロード",
backToDashboard: "ホームに戻る",
niche_education: "教育",
niche_gaming: "ゲーム",
niche_podcast: "ポッドキャスト",
niche_commentary: "解説",
niche_cars: "車",
niche_beauty: "美容",
niche_fitness: "フィットネス",
niche_finance: "ファイナンス",
niche_tech: "テクノロジー",
niche_lifestyle: "ライフスタイル",
niche_music: "音楽",
niche_other: "その他",
style_informative: "情報系",
style_funny: "コメディ",
style_dramatic: "ドラマチック",
style_educational: "教育系",
style_commentary: "解説",
platform_tiktok: "TikTok",
platform_youtube_shorts: "YouTube Shorts",
platform_instagram_reels: "Instagram Reels",
languageOption_Thai: "タイ語",
languageOption_English: "英語",
languageOption_Japanese: "日本語",
languageOption_Chinese: "中国語",
languageOption_Korean: "韓国語",
languageOption_Auto: "自動検出",
// NLE editor
mediaBin: "クリップ一覧",
aiAssistant: "AIアシスタント",
aiReason: "AIの説明はまだありません。再生成してみてください。",
aiReasonHead: "この場面を選んだ理由",
aiVisualHead: "映像分析",
aiTighten: "30秒に短縮",
aiEmphasize: "60秒に延長",
aiRedoAll: "このクリップを再生成",
aiDeleteClip: "クリップを削除",
aiActionRedoSub: "AIに別の場面を選ばせる",
aiActionTightenSub: "Reels・Shortsに最適",
aiActionEmphasizeSub: "TikTokのストーリーテリングに最適",
aiActionDeleteSub: "このバッチから外す",
dragToTrim: "端をドラッグでトリム · 中央をドラッグで移動",
dragCueToRetime: "字幕の端や本体をドラッグしてタイミング調整",
dragToPosition: "字幕をドラッグして移動",
addCue: "字幕を追加",
cuePlaceholder: "字幕テキストを入力...",
seekToCue: "この字幕にジャンプ",
aiSubtitleHead: "AI字幕アシスタント",
aiPolish: "字幕を整える",
aiTranslate: "翻訳",
aiAutoTime: "自動タイミング",
aiAutoTimeHelp: "Whisperの単語タイムスタンプで字幕を再調整",
clipEdit: "クリップ長さ",
clipLengthLabel: "長さを設定",
clipExtendLabel: "延長",
clipSkipLabel: "中央を切り取る",
clipSkipAdd: "切り取り",
clipRebuildBtn: "クリップを再生成",
from: "から",
to: "まで",
gpuActive: "GPU動作中",
gpuDemo: "デモ",
gpuPending: "GPU待機中",
},
zh: {
...en,
appSubtitle: "把长视频变成短视频 — 由 AMD ROCm 驱动",
idle: "待机",
queued: "排队中",
running: "处理中",
completed: "完成",
failed: "失败",
demoMode: "演示模式",
productionMode: "正式模式",
theme: "主题",
language: "语言",
startPipeline: "生成短片",
channelProfile: "频道资料",
channelProfileText: "把你的频道情况告诉 AI,它才会挑出最合适的精彩瞬间。",
videoInput: "视频",
niche: "频道类型",
nicheHelp: "选最接近的分类。如果不在列表中,请选择「其他」。",
customNiche: "自定义类型",
customNichePlaceholder: "例如:面向初学者的泰语 AI 教程",
channelDescription: "频道介绍",
channelDescriptionHelp: "像介绍给朋友一样写就好。AI 会用它判断内容是否符合频道风格。",
channelDescriptionPlaceholder: "例如:我用泰语讲解 AI 工具,例子简单,也带一点幽默。",
clipStyle: "短片风格",
clipLength: "短片时长(秒)",
clipCount: "短片数量",
clipCountHelp: "希望 AI 从这段视频中生成多少条候选短片。",
primaryLanguage: "语言",
platform: "发布平台",
youtube: "YouTube",
upload: "上传",
youtubeUrl: "YouTube 链接",
youtubePlaceholder: "https://www.youtube.com/watch?v=...",
videoFile: "视频文件",
pipeline: "处理进度",
ready: "准备就绪",
progressNote: "每个步骤都会更新进度,渲染阶段会显示每条短片的状态。",
currentStep: "当前步骤",
timing: "耗时",
timing_input: "下载",
timing_transcription: "语音转写",
timing_highlight_detection: "亮点识别",
timing_multimodal_analysis: "画面分析",
timing_clip_generation: "短片生成",
timing_total: "总计",
transcript: "字幕原文",
transcriptEmpty: "转写完成后会显示在这里。",
clips: "短片",
noClips: "还没有短片",
noClipsText: "启动流程后,AI 会根据频道资料生成候选短片。",
readyClips: "条短片",
score: "评分",
reason: "原因",
duration: "时长",
start: "起点",
end: "终点",
openEditor: "打开编辑器",
editor: "短片编辑器",
editorText: "调整这条短片的时间、字幕,并完成最终确认。",
preview: "预览",
clipRange: "片段范围",
subtitles: "字幕",
subtitleCues: "字幕段落",
subtitleCueHelp: "短字幕段落更适合 TikTok、Reels 和 Shorts。",
timelineTracks: "时间线",
videoTrack: "画面",
subtitleTrack: "字幕",
audioTrack: "音频",
toolSelect: "选择",
toolTrim: "裁剪",
toolCaptions: "字幕",
toolStyle: "样式",
toolExport: "导出",
captionStyle: "字幕样式",
captionPreset: "预设",
presetClean: "简洁",
presetBold: "粗体",
presetKaraoke: "卡拉 OK",
font: "字体",
fontSize: "字号",
fillColor: "填充色",
strokeColor: "描边色",
strokeWidth: "描边宽度",
captionPosition: "字幕位置",
captionLength: "字幕长度",
animation: "动效",
density_word: "逐词",
density_short: "短句",
density_medium: "中等长度",
density_long: "长句",
animation_none: "无",
animation_highlight: "高亮",
animation_pop: "弹出",
animation_bounce: "弹跳",
editorTools: "编辑工具",
rangeStart: "起点位置",
rangeEnd: "终点位置",
trimStartBack: "起点 −0.5 秒",
trimStartForward: "起点 +0.5 秒",
trimEndBack: "终点 −0.5 秒",
trimEndForward: "终点 +0.5 秒",
moveClipLeft: "整段 −1 秒",
moveClipRight: "整段 +1 秒",
setClipLength30: "调整为 30 秒",
setClipLength60: "调整为 60 秒",
setClipLength90: "调整为 90 秒",
inspector: "属性",
title: "标题",
status: "状态",
notApproved: "未确认",
model: "模型",
source: "来源",
source_upload: "上传",
source_youtube: "YouTube",
source_video: "视频",
settings: "设置",
stepInput: "导入",
stepTranscription: "语音转写",
stepHighlights: "亮点识别",
stepVisual: "画面分析",
stepRender: "渲染",
stepFinal: "收尾",
renderProgress: "正在渲染第 {{current}}/{{total}} 条短片",
appCta: "生成短片",
approve: "确认",
approved: "已确认",
regenerate: "重新生成",
delete: "删除",
download: "下载",
backToDashboard: "返回主界面",
niche_education: "教育",
niche_gaming: "游戏",
niche_podcast: "播客",
niche_commentary: "解说",
niche_cars: "汽车",
niche_beauty: "美妆",
niche_fitness: "健身",
niche_finance: "财经",
niche_tech: "科技",
niche_lifestyle: "生活",
niche_music: "音乐",
niche_other: "其他",
style_informative: "知识型",
style_funny: "幽默",
style_dramatic: "戏剧化",
style_educational: "教学型",
style_commentary: "评论型",
platform_tiktok: "TikTok",
platform_youtube_shorts: "YouTube Shorts",
platform_instagram_reels: "Instagram Reels",
languageOption_Thai: "泰语",
languageOption_English: "英语",
languageOption_Japanese: "日语",
languageOption_Chinese: "中文",
languageOption_Korean: "韩语",
languageOption_Auto: "自动检测",
// NLE editor
mediaBin: "片段列表",
aiAssistant: "AI 助手",
aiReason: "AI 还没解释,试试重新生成。",
aiReasonHead: "为什么选这一段",
aiVisualHead: "画面分析",
aiTighten: "压缩到 30 秒",
aiEmphasize: "延长到 60 秒",
aiRedoAll: "重新生成此片段",
aiDeleteClip: "删除此片段",
aiActionRedoSub: "让 AI 找新的精彩瞬间",
aiActionTightenSub: "适合 Reels 和 Shorts",
aiActionEmphasizeSub: "适合 TikTok 故事化内容",
aiActionDeleteSub: "从本批次移除",
dragToTrim: "拖动边缘修剪 · 拖动中央移动",
dragCueToRetime: "拖动字幕边缘或中央调整时间",
dragToPosition: "拖动字幕移动位置",
addCue: "添加字幕",
cuePlaceholder: "输入字幕文字...",
seekToCue: "跳到该字幕",
aiSubtitleHead: "AI 字幕助手",
aiPolish: "润色字幕",
aiTranslate: "翻译",
aiAutoTime: "自动对时",
aiAutoTimeHelp: "用 Whisper 单词时间戳重新对齐",
clipEdit: "片段长度",
clipLengthLabel: "设置长度",
clipExtendLabel: "延长",
clipSkipLabel: "切掉中段",
clipSkipAdd: "切掉",
clipRebuildBtn: "重建片段",
from: "从",
to: "到",
gpuActive: "GPU 活动",
gpuDemo: "演示",
gpuPending: "等待 GPU",
},
ko: {
...en,
appSubtitle: "긴 영상을 짧은 클립으로 ― AMD ROCm 기반",
idle: "대기 중",
queued: "대기열",
running: "처리 중",
completed: "완료",
failed: "실패",
demoMode: "데모 모드",
productionMode: "정식 모드",
theme: "테마",
language: "언어",
startPipeline: "클립 만들기",
channelProfile: "채널 프로필",
channelProfileText: "AI가 적절한 순간을 고를 수 있도록 채널 정보를 입력해 주세요.",
videoInput: "영상",
niche: "채널 분야",
nicheHelp: "가장 가까운 카테고리를 선택하세요. 해당 항목이 없다면 기타를 선택합니다.",
customNiche: "분야 직접 입력",
customNichePlaceholder: "예: 초보자를 위한 태국어 AI 튜토리얼",
channelDescription: "채널 소개",
channelDescriptionHelp: "친구에게 소개하듯 자연스럽게 작성하세요. AI가 채널 톤을 유지하는 데 사용합니다.",
channelDescriptionPlaceholder:
"예: AI 도구를 태국어로 쉽게 설명하고, 약간의 유머도 곁들입니다.",
clipStyle: "클립 스타일",
clipLength: "클립 길이(초)",
clipCount: "클립 개수",
clipCountHelp: "이 영상에서 AI가 찾아낼 클립 개수입니다.",
primaryLanguage: "언어",
platform: "플랫폼",
youtube: "YouTube",
upload: "업로드",
youtubeUrl: "YouTube URL",
youtubePlaceholder: "https://www.youtube.com/watch?v=...",
videoFile: "영상 파일",
pipeline: "처리 진행",
ready: "준비 완료",
progressNote: "단계별로 진행률이 갱신되며, 렌더링 중에는 클립별 상태가 표시됩니다.",
currentStep: "현재 단계",
timing: "소요 시간",
timing_input: "다운로드",
timing_transcription: "음성 인식",
timing_highlight_detection: "하이라이트 감지",
timing_multimodal_analysis: "영상 분석",
timing_clip_generation: "클립 생성",
timing_total: "총 시간",
transcript: "스크립트",
transcriptEmpty: "음성 인식이 끝나면 여기에 표시됩니다.",
clips: "클립",
noClips: "아직 클립이 없습니다",
noClipsText: "처리를 시작하면 채널 프로필에 맞춰 후보 클립이 생성됩니다.",
readyClips: "개의 클립",
score: "점수",
reason: "선정 이유",
duration: "길이",
start: "시작",
end: "끝",
openEditor: "에디터 열기",
editor: "클립 에디터",
editorText: "이 클립의 타이밍, 자막, 최종 확인 상태를 조정합니다.",
preview: "미리보기",
clipRange: "클립 범위",
subtitles: "자막",
subtitleCues: "자막 단락",
subtitleCueHelp: "짧은 자막 단락이 TikTok, Reels, Shorts에서 더 읽기 좋습니다.",
timelineTracks: "타임라인",
videoTrack: "영상",
subtitleTrack: "자막",
audioTrack: "오디오",
toolSelect: "선택",
toolTrim: "자르기",
toolCaptions: "자막",
toolStyle: "스타일",
toolExport: "내보내기",
captionStyle: "자막 스타일",
captionPreset: "프리셋",
presetClean: "심플",
presetBold: "굵은 글씨",
presetKaraoke: "노래방",
font: "글꼴",
fontSize: "글자 크기",
fillColor: "글자색",
strokeColor: "외곽선 색",
strokeWidth: "외곽선 두께",
captionPosition: "자막 위치",
captionLength: "자막 길이",
animation: "애니메이션",
density_word: "단어 단위",
density_short: "짧은 줄",
density_medium: "보통",
density_long: "긴 줄",
animation_none: "없음",
animation_highlight: "하이라이트",
animation_pop: "팝",
animation_bounce: "바운스",
editorTools: "편집 도구",
rangeStart: "시작 위치",
rangeEnd: "끝 위치",
trimStartBack: "시작 −0.5초",
trimStartForward: "시작 +0.5초",
trimEndBack: "끝 −0.5초",
trimEndForward: "끝 +0.5초",
moveClipLeft: "클립 −1초",
moveClipRight: "클립 +1초",
setClipLength30: "30초로 맞추기",
setClipLength60: "60초로 맞추기",
setClipLength90: "90초로 맞추기",
inspector: "상세 정보",
title: "제목",
status: "상태",
notApproved: "미확정",
model: "모델",
source: "소스",
source_upload: "업로드",
source_youtube: "YouTube",
source_video: "영상",
settings: "설정",
stepInput: "불러오기",
stepTranscription: "음성 인식",
stepHighlights: "하이라이트",
stepVisual: "영상 분석",
stepRender: "렌더링",
stepFinal: "마무리",
renderProgress: "클립 {{current}}/{{total}} 렌더링 중",
appCta: "클립 만들기",
approve: "확정",
approved: "확정됨",
regenerate: "다시 만들기",
delete: "삭제",
download: "다운로드",
backToDashboard: "홈으로",
niche_education: "교육",
niche_gaming: "게임",
niche_podcast: "팟캐스트",
niche_commentary: "해설",
niche_cars: "자동차",
niche_beauty: "뷰티",
niche_fitness: "피트니스",
niche_finance: "금융",
niche_tech: "테크",
niche_lifestyle: "라이프스타일",
niche_music: "음악",
niche_other: "기타",
style_informative: "정보형",
style_funny: "유머",
style_dramatic: "감동적",
style_educational: "교육형",
style_commentary: "해설형",
platform_tiktok: "TikTok",
platform_youtube_shorts: "YouTube Shorts",
platform_instagram_reels: "Instagram Reels",
languageOption_Thai: "태국어",
languageOption_English: "영어",
languageOption_Japanese: "일본어",
languageOption_Chinese: "중국어",
languageOption_Korean: "한국어",
languageOption_Auto: "자동 감지",
// NLE editor
mediaBin: "클립 목록",
aiAssistant: "AI 어시스턴트",
aiReason: "AI가 아직 설명하지 않았습니다. 다시 만들어 보세요.",
aiReasonHead: "이 장면을 고른 이유",
aiVisualHead: "영상 분석",
aiTighten: "30초로 줄이기",
aiEmphasize: "60초로 늘리기",
aiRedoAll: "이 클립 다시 만들기",
aiDeleteClip: "클립 삭제",
aiActionRedoSub: "AI가 다른 장면을 찾도록",
aiActionTightenSub: "Reels와 Shorts에 적합",
aiActionEmphasizeSub: "TikTok 스토리텔링에 적합",
aiActionDeleteSub: "이번 배치에서 제외",
dragToTrim: "끝을 드래그해 트림 · 가운데를 드래그해 이동",
dragCueToRetime: "자막 끝이나 중앙을 드래그해 타이밍 조정",
dragToPosition: "자막을 드래그해 이동",
addCue: "자막 추가",
cuePlaceholder: "자막 텍스트 입력...",
seekToCue: "이 자막으로 이동",
aiSubtitleHead: "AI 자막 도우미",
aiPolish: "자막 다듬기",
aiTranslate: "번역",
aiAutoTime: "자동 타이밍",
aiAutoTimeHelp: "Whisper 단어 타임스탬프로 재조정",
clipEdit: "클립 길이",
clipLengthLabel: "길이 설정",
clipExtendLabel: "연장",
clipSkipLabel: "중간 잘라내기",
clipSkipAdd: "잘라내기",
clipRebuildBtn: "클립 다시 만들기",
from: "부터",
to: "까지",
gpuActive: "GPU 활성",
gpuDemo: "데모",
gpuPending: "GPU 대기",
},
};
// ============================================================
// App root
// ============================================================
function App() {
const [profile, setProfile] = useState(defaultProfile);
const [sourceMode, setSourceMode] = useState("youtube");
const [youtubeUrl, setYoutubeUrl] = useState("");
const [file, setFile] = useState(null);
const [job, setJob] = useState(null);
const [health, setHealth] = useState(null);
const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
// Explicit view state — fixes the editor navigation bug
const [view, setView] = useState("dashboard"); // 'dashboard' | 'editor'
const [editorClipId, setEditorClipId] = useState(null);
const [captionStyles, setCaptionStyles] = useState(() => {
try {
return JSON.parse(localStorage.getItem("elevenclip.captionStyles") || "{}");
} catch {
return {};
}
});
const [language, setLanguage] = useState(
() => localStorage.getItem("elevenclip.language") || "en"
);
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem("elevenclip.theme");
if (saved) return saved;
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
});
const t = (key, params) => {
let value = translations[language]?.[key] ?? translations.en[key] ?? key;
if (params) {
Object.entries(params).forEach(([name, replacement]) => {
value = value.replaceAll(`{{${name}}}`, String(replacement));
});
}
return value;
};
const activeClips = useMemo(
() => (job?.clips || []).filter((clip) => !clip.deleted),
[job?.clips]
);
// Derive the editor clip from view state — no useEffect reset needed
const editorClip =
view === "editor" && editorClipId
? activeClips.find((clip) => clip.id === editorClipId) || null
: null;
const editorCaptionStyle = editorClip
? { ...defaultCaptionStyle, ...(captionStyles[editorClip.id] || {}) }
: defaultCaptionStyle;
// Navigation helpers
function openEditor(clip) {
setEditorClipId(clip.id);
setView("editor");
}
function closeEditor() {
setView("dashboard");
}
useEffect(() => {
document.documentElement.dataset.theme = theme;
localStorage.setItem("elevenclip.theme", theme);
}, [theme]);
useEffect(() => {
localStorage.setItem("elevenclip.language", language);
}, [language]);
useEffect(() => {
localStorage.setItem("elevenclip.captionStyles", JSON.stringify(captionStyles));
}, [captionStyles]);
useEffect(() => {
fetchJson("/health").then(setHealth).catch(() => setHealth(null));
}, []);
useEffect(() => {
if (!job || !["queued", "running"].includes(job.status)) return;
const timer = window.setInterval(async () => {
const next = await fetchJson(`/api/jobs/${job.id}`);
setJob(next);
}, 1200);
return () => window.clearInterval(timer);
}, [job]);
async function submitJob(event) {
event.preventDefault();
setError("");
setIsSubmitting(true);
setView("dashboard");
setEditorClipId(null);
try {
if (sourceMode === "youtube") {
const next = await fetchJson("/api/jobs/youtube", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ youtube_url: youtubeUrl, profile }),
});
setJob(next);
} else {
const data = new FormData();
data.append("profile_json", JSON.stringify(profile));
data.append("file", file);
const next = await fetchJson("/api/jobs/upload", { method: "POST", body: data });
setJob(next);
}
} catch (exc) {
setError(exc.message);
} finally {
setIsSubmitting(false);
}
}
async function patchClip(clipId, patch) {
setJob((current) => ({
...current,
clips: current.clips.map((clip) => (clip.id === clipId ? { ...clip, ...patch } : clip)),
}));
try {
const nextClip = await fetchJson(`/api/jobs/${job.id}/clips/${clipId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
setJob((current) => ({
...current,
clips: current.clips.map((clip) => (clip.id === clipId ? nextClip : clip)),
}));
} catch (exc) {
setError(exc.message);
}
}
async function regenerateClip(clip) {
const nextClip = await fetchJson(`/api/jobs/${job.id}/clips/${clip.id}/regenerate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clip_style: profile.clip_style,
clip_length_seconds: Number(profile.clip_length_seconds),
subtitle_text: clip.subtitle_text,
}),
});
setJob((current) => ({
...current,
clips: current.clips.map((item) => (item.id === clip.id ? nextClip : item)),
}));
}
// ─── AI subtitle actions ────────────────────────────────────
async function callAiSubtitle(endpoint, clip, body) {
try {
const nextClip = await fetchJson(
`/api/jobs/${job.id}/clips/${clip.id}/subtitle/${endpoint}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body || {}),
}
);
setJob((current) => ({
...current,
clips: current.clips.map((item) => (item.id === clip.id ? nextClip : item)),
}));
} catch (exc) {
setError(exc.message);
}
}
function polishSubtitles(clip) {
return callAiSubtitle("polish", clip, { style: profile.clip_style });
}
function translateSubtitles(clip, targetLanguage) {
return callAiSubtitle("translate", clip, { target_language: targetLanguage });
}
function autoTimeSubtitles(clip) {
return callAiSubtitle("auto-time", clip, {});
}
function setProfileValue(key) {
return (value) => setProfile((current) => ({ ...current, [key]: value }));
}
function updateCaptionStyle(clipId, patch) {
setCaptionStyles((current) => ({
...current,
[clipId]: { ...defaultCaptionStyle, ...(current[clipId] || {}), ...patch },
}));
}
return (
{t("appSubtitle")}
}{message}
{label}
{t("progressNote")}
{job?.timings && Object.keys(job.timings).length > 0 && ({t("transcriptEmpty")}
}{segment.text}
{clips.length} {t("readyClips")}
{t("noClipsText")}
{clip.reason}
{clip.subtitle_text}
)} {/* Action row — primary CTA on top, icon group below */}{formatTime(effStart)} – {formatTime(effEnd)} · {" "} {duration.toFixed(1)}s · {Math.round(clip.score)} {t("score")}
{t("transcriptEmpty")}
)} {rows.map((segment) => ({segment.text}