Spaces:
Paused
Paused
File size: 6,406 Bytes
3d2098f 630a6c9 3d2098f 4021f36 630a6c9 3d2098f 630a6c9 3d2098f d2d6ea9 3d2098f 630a6c9 3d2098f 630a6c9 3d2098f d2d6ea9 3d2098f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | import { useState } from 'react'
import { api, runJob } from '../api'
import { useJob, ProgressBox, ErrorBox } from '../hooks'
const PRESETS = ['bold-center-karaoke', 'clean-lower-third', 'minimal', 'typewriter', 'punch']
const DUB_LANGS = [['es', 'Spanish'], ['hi', 'Hindi'], ['fr', 'French'], ['de', 'German'],
['pt', 'Portuguese'], ['ja', 'Japanese'], ['ar', 'Arabic'], ['it', 'Italian'],
['ko', 'Korean'], ['zh', 'Chinese'], ['ru', 'Russian'], ['id', 'Indonesian']]
export default function ReviewStep({ reelData, refreshReel, setStep, projectId, reelId }) {
const base = `/api/projects/${projectId}/reels/${reelId}`
const results = reelData?.renders?.results || []
const [picked, setPicked] = useState(0)
const [tweakPreset, setTweakPreset] = useState(null)
const [swapping, setSwapping] = useState(false)
const [error, setError] = useState('')
const rerenderJob = useJob()
const [dubLang, setDubLang] = useState('es')
const [dubBusy, setDubBusy] = useState(false)
const [dubMsg, setDubMsg] = useState('')
const [dubbedUrl, setDubbedUrl] = useState('')
if (!results.length) {
return (
<div>
<h2>Review</h2>
<p className="subtitle">Nothing rendered yet.</p>
<button className="btn secondary" onClick={() => setStep(6)}>← Go to Render</button>
</div>
)
}
const chosen = results[Math.min(picked, results.length - 1)]
// Tweak: re-render only the picked variant, optionally with a new caption preset.
const rerender = async () => {
const spec = {
name: chosen.variant,
caption_preset: tweakPreset || chosen.caption_preset,
clip_offset: results.indexOf(chosen),
}
try {
await rerenderJob.start(`${base}/render`, { variants: [spec] })
await refreshReel()
setPicked(0)
setTweakPreset(null)
} catch { /* shown */ }
}
const swapClip = async (lineIndex) => {
setSwapping(true)
try {
await api.post(`${base}/media/swap`, { line_index: lineIndex })
await refreshReel()
} catch (e) { setError(e.message) } finally { setSwapping(false) }
}
const dubIt = async () => {
setDubBusy(true); setError(''); setDubbedUrl(''); setDubMsg('Starting…')
try {
const r = await runJob(`${base}/dub`, { result_index: results.indexOf(chosen), target_lang: dubLang },
(p) => setDubMsg(p[p.length - 1] || 'Dubbing…'))
setDubbedUrl(r.url)
} catch (e) { setError(e.message) } finally { setDubBusy(false); setDubMsg('') }
}
return (
<div>
<h2>Review & export</h2>
<p className="subtitle">Preview the variants side by side, pick one, tweak and re-render, then download.</p>
<ErrorBox error={error || rerenderJob.error} />
{rerenderJob.running && <ProgressBox progress={rerenderJob.progress.length ? rerenderJob.progress : ['Re-rendering…']} />}
<div className="variant-grid">
{results.map((r, i) => (
<div key={r.variant + i} className={`variant-card card ${picked === i ? 'picked' : ''}`}>
<video src={r.url} controls playsInline />
<p style={{ margin: '8px 0 2px' }}><strong>{r.variant}</strong></p>
<p className="muted" style={{ margin: '0 0 8px' }}>{r.caption_preset} · {r.duration_sec.toFixed(1)}s</p>
<button className={`btn small ${picked === i ? '' : 'secondary'}`} onClick={() => setPicked(i)}>
{picked === i ? '✓ Picked' : 'Pick this one'}
</button>
</div>
))}
</div>
<div className="card">
<strong>Tweak “{chosen.variant}”</strong>
<div className="row" style={{ marginTop: 10 }}>
<label className="row" style={{ gap: 8 }}>
<span className="muted">Caption style:</span>
<select value={tweakPreset || chosen.caption_preset} onChange={(e) => setTweakPreset(e.target.value)}>
{PRESETS.map((p) => <option key={p} value={p}>{p}</option>)}
</select>
</label>
<button className="btn secondary" disabled={rerenderJob.running} onClick={rerender}>
↻ Re-render this variant
</button>
</div>
{reelData?.media && (
<>
<p className="muted" style={{ marginBottom: 6 }}>Swap a scene’s clip (then re-render):</p>
<div className="row">
{reelData.media.scenes.map((sm) => (
<button key={sm.line_index} className="btn small secondary"
disabled={swapping || sm.candidates.length < 2}
onClick={() => swapClip(sm.line_index)}>
Scene {sm.line_index + 1} ({sm.selected + 1}/{sm.candidates.length})
</button>
))}
</div>
</>
)}
</div>
<div className="card">
<strong>🌍 Dub to another language (ElevenLabs)</strong>
<p className="prompt-hint">Re-voice “{chosen.variant}” in another language, keeping the delivery. Uses credits; takes a few minutes.</p>
<div className="row">
<select value={dubLang} onChange={(e) => setDubLang(e.target.value)} disabled={dubBusy}>
{DUB_LANGS.map(([c, n]) => <option key={c} value={c}>{n}</option>)}
</select>
<button className="btn" disabled={dubBusy} onClick={dubIt}>{dubBusy ? '🌍 Dubbing…' : '🌍 Dub this'}</button>
{dubMsg && <span className="muted">{dubMsg}</span>}
</div>
{dubbedUrl && (
<div style={{ marginTop: 10 }}>
<video src={dubbedUrl} controls playsInline style={{ maxWidth: 250, borderRadius: 10, background: '#000' }} />
<div style={{ marginTop: 6 }}><a className="btn small" style={{ textDecoration: 'none' }} href={dubbedUrl} download>⬇ Download dubbed video</a></div>
</div>
)}
</div>
<div className="row">
<button className="btn secondary" onClick={() => setStep(6)}>← Render settings</button>
<div className="grow" />
<a className="btn" style={{ textDecoration: 'none' }} href={chosen.url} download>
⬇ Download {chosen.variant}
</a>
</div>
<p className="muted" style={{ marginTop: 14 }}>
Tip: trending audio must be added in the YouTube/Instagram app at posting time — platform-licensed sounds are never embedded here.
</p>
</div>
)
}
|