Spaces:
Paused
Paused
| 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> | |
| ) | |
| } | |