reel-studio-2 / web /src /steps /RenderStep.jsx
xvepkj's picture
Add free Hugging Face Space features: translate, music, upscale, talking-head
ed9bf62
Raw
History Blame
5.78 kB
import { useEffect, useState } from 'react'
import { api } from '../api'
import { useJob, ProgressBox, ErrorBox } from '../hooks'
const PRESETS = ['bold-center-karaoke', 'clean-lower-third', 'minimal', 'typewriter', 'punch']
export default function RenderStep({ reelData, refreshReel, setStep, projectId, reelId, selectReel, refreshProject }) {
const job = useJob()
// Translate this reel into another language → a new sibling reel.
const tjob = useJob()
const [langs, setLangs] = useState([])
const [targetLang, setTargetLang] = useState('es')
useEffect(() => { api.get('/api/translate/languages').then((d) => setLangs(d.languages || [])).catch(() => {}) }, [])
const translate = async () => {
try {
const result = await tjob.start(`/api/projects/${projectId}/reels/${reelId}/translate`, { target_lang: targetLang })
if (refreshProject) await refreshProject()
if (selectReel && result?.reel) { selectReel(result.reel.id); setStep(4) } // → Media: pick a voice in that language
} catch { /* shown */ }
}
// Default to ONE variant that matches the editor (WYSIWYG); add more for A/B.
const editPreset = reelData?.overlays?.caption_preset || PRESETS[0]
const editCaptions = reelData?.overlays?.captions_on !== false
const [specs, setSpecs] = useState([
{ name: 'As edited', caption_preset: editPreset, captions: editCaptions, voice: true, clip_offset: 0 },
])
const lines = reelData?.voice?.lines?.length || 0
// segments + captions + final per variant — rough progress out of total steps
const totalSteps = specs.length * (lines + 2)
const pct = job.running ? Math.min(95, Math.round((job.progress.length / Math.max(totalSteps, 1)) * 100)) : 0
const start = async () => {
try {
await job.start(`/api/projects/${projectId}/reels/${reelId}/render`, { variants: specs })
await refreshReel()
setStep(7)
} catch { /* shown */ }
}
return (
<div>
<h2>Render</h2>
<p className="subtitle">
Renders {specs.length} draft variants — different clip picks (from the alternates) and caption styles — to compare side by side.
</p>
{specs.map((s, i) => {
const upd = (patch) => setSpecs(specs.map((x, j) => (j === i ? { ...x, ...patch } : x)))
const captionsOn = s.captions !== false
const voiceOn = s.voice !== false
return (
<div key={i} className="card">
<div className="row">
<strong style={{ width: 90 }}>{s.name}</strong>
<label className="row" style={{ gap: 6 }}>
<input type="checkbox" checked={captionsOn} onChange={(e) => upd({ captions: e.target.checked })} />
<span className="muted">Captions</span>
</label>
<select value={s.caption_preset} disabled={!captionsOn}
onChange={(e) => upd({ caption_preset: e.target.value })}>
{PRESETS.map((p) => <option key={p} value={p}>{p}</option>)}
</select>
<label className="row" style={{ gap: 6 }}>
<input type="checkbox" checked={voiceOn} onChange={(e) => upd({ voice: e.target.checked })} />
<span className="muted">Voice</span>
</label>
<span className="tag">clips: alt #{s.clip_offset}</span>
{specs.length > 1 && (
<button className="btn small danger" onClick={() => setSpecs(specs.filter((_, j) => j !== i))}>Remove</button>
)}
</div>
</div>
)
})}
<p className="muted" style={{ marginTop: -4 }}>
Turn off Captions for clips-only, or Voice for a music-only / silent cut. Overlays from the Effects step always render.
</p>
<ErrorBox error={job.error} onRetry={start} />
{job.running && (
<>
<div className="bar"><div className="fill" style={{ width: `${pct}%` }} /></div>
<ProgressBox progress={job.progress.length ? job.progress : ['Starting render…']} />
</>
)}
<div className="row">
{specs.length < 3 && (
<button className="btn secondary"
onClick={() => setSpecs([...specs, { name: `Variant ${'ABC'[specs.length]}`, caption_preset: PRESETS[specs.length % 3], clip_offset: specs.length }])}>
+ Add variant
</button>
)}
<div className="grow" />
<button className="btn" onClick={start} disabled={job.running}>
{job.running ? `Rendering… ${pct}%` : `Render ${specs.length} variants`}
</button>
</div>
{/* Translate → a new reel in another language (same visuals, native narration + captions). */}
<div className="card" style={{ marginTop: 16 }}>
<div className="row">
<strong className="grow">🌍 Make this reel in another language</strong>
<select value={targetLang} onChange={(e) => setTargetLang(e.target.value)} disabled={tjob.running}>
{langs.map((l) => <option key={l.code} value={l.code}>{l.name}</option>)}
</select>
<button className="btn secondary" onClick={translate} disabled={tjob.running || !reelData?.script}>
{tjob.running ? 'Translating…' : 'Translate'}
</button>
</div>
<p className="muted" style={{ margin: '6px 0 0', fontSize: 13 }}>
Creates a new reel with the script translated (visuals stay the same). Pick a matching voice on the Media step, then render — captions come out native.
</p>
{tjob.running && <div style={{ marginTop: 10 }}><ProgressBox progress={tjob.progress.length ? tjob.progress : ['Starting…']} /></div>}
<ErrorBox error={tjob.error} />
</div>
</div>
)
}