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>
  )
}