/ proof-of-concept / takes
Sign in
proof-of-concept / takes
takes / src / pages / Settings.tsx
129 lines · 5.6 KBCodeBlameHistory
f3253d0Takes: record, organize, and edit explainer videosJeremy Magland 1import { useEffect, useState } from 'react';
2import type { PublicConfig } from '../../shared/types.ts';
3import { api } from '../api.ts';
4import { Icon } from '../components/Icon.tsx';
6export function Settings() {
7 const [cfg, setCfg] = useState<PublicConfig | null>(null);
8 const [key, setKey] = useState('');
9 const [status, setStatus] = useState<string | null>(null);
10 const [error, setError] = useState<string | null>(null);
12 const load = () => api.config().then(setCfg).catch((e) => setError(e.message));
13 useEffect(() => {
14 document.title = 'Settings · Takes';
15 load();
16 }, []);
18 const save = async (patch: Record<string, unknown>) => {
19 try {
20 await api.saveConfig(patch);
21 await load();
22 setStatus('Saved');
23 setTimeout(() => setStatus(null), 1500);
24 } catch (e) {
25 setError((e as Error).message);
26 }
27 };
29 if (!cfg) return <div className="page loading">{error ?? 'Loading…'}</div>;
31 return (
32 <div className="page">
33 <header className="topbar">
34 <a className="btn ghost" href="#/">
35 <Icon name="back" /> Videos
36 </a>
37 <div className="topbar-title">Settings</div>
38 <div className="spacer" />
39 {status && <span className="save-state saved">{status}</span>}
40 </header>
41 {error && <div className="banner error">{error}</div>}
42 <main className="settings">
43 <section className="panel">
44 <h4>Library</h4>
45 <p>
46 Videos are stored in <code>{cfg.libraryDir}</code>. Start the app with <code>--dir</code> to use another directory.
47 </p>
48 </section>
50 <section className="panel">
51 <h4>Transcription (Deepgram)</h4>
52 <p className="muted small">
53 The key is stored in <code>config.json</code> in the library directory, readable only by you. Audio is sent to Deepgram only when a take is transcribed.
54 </p>
55 <div className="row">
56 <input type="password" className="grow" placeholder={cfg.hasDeepgramKey ? 'A key is saved (enter a new one to replace it)' : 'Deepgram API key'} value={key} onChange={(e) => setKey(e.target.value)} />
57 <button
58 className="btn primary"
59 disabled={!key.trim()}
60 onClick={async () => {
61 await save({ deepgramApiKey: key });
62 setKey('');
63 }}
64 >
65 Save key
66 </button>
67 {cfg.hasDeepgramKey && (
68 <button className="btn ghost danger" onClick={() => save({ deepgramApiKey: '' })}>
69 Remove
70 </button>
71 )}
72 </div>
73 <label className="field">
74 <span>Model</span>
75 <input defaultValue={cfg.deepgramModel} onBlur={(e) => e.target.value !== cfg.deepgramModel && save({ deepgramModel: e.target.value })} />
76 </label>
77 <label className="check">
78 <input type="checkbox" checked={cfg.autoTranscribe} onChange={(e) => save({ autoTranscribe: e.target.checked })} />
79 Transcribe new takes automatically
80 </label>
81 </section>
307b4e2Run background removal on a remote GPU over sshJeremy Magland 83 <section className="panel">
84 <h4>Background removal</h4>
85 <p className="muted small">
86 By default the camera background is removed on this computer&rsquo;s CPU. To use a machine with an NVIDIA GPU instead, enter its ssh destination (for example <code>office</code> or <code>user@host</code>). It must accept key-based login without a password prompt and have Python 3.10 or later and ffmpeg; the first job installs onnxruntime-gpu in <code>~/.cache/takes/venv</code> there. Each camera track is copied to that machine for processing and then deleted from it.
87 </p>
88 <label className="field">
89 <span>ssh host</span>
90 <input placeholder="This computer" defaultValue={cfg.matteHost} onBlur={(e) => e.target.value.trim() !== cfg.matteHost && save({ matteHost: e.target.value })} />
91 </label>
92 <label className="field">
93 <span>Setup command on that host</span>
94 <input placeholder="e.g. module load python ffmpeg" defaultValue={cfg.matteSetup} onBlur={(e) => e.target.value.trim() !== cfg.matteSetup && save({ matteSetup: e.target.value })} />
95 </label>
96 </section>
f3253d0Takes: record, organize, and edit explainer videosJeremy Magland 98 <section className="panel">
99 <h4>Defaults</h4>
100 <label className="row">
101 <span>Countdown before recording</span>
102 <select value={cfg.defaultCountdown} onChange={(e) => save({ defaultCountdown: parseInt(e.target.value) })}>
103 {[0, 3, 5, 10].map((n) => (
104 <option key={n} value={n}>
105 {n === 0 ? 'None' : `${n} s`}
106 </option>
107 ))}
108 </select>
109 </label>
110 <label className="row">
111 <span>Camera resolution</span>
112 <select value={cfg.defaultCameraHeight} onChange={(e) => save({ defaultCameraHeight: parseInt(e.target.value) })}>
113 <option value={720}>720p</option>
114 <option value={1080}>1080p</option>
115 </select>
116 </label>
117 <label className="row">
118 <span>Export resolution for new videos</span>
119 <select value={cfg.defaultOutputHeight} onChange={(e) => save({ defaultOutputHeight: parseInt(e.target.value) })}>
120 <option value={720}>720p</option>
121 <option value={1080}>1080p</option>
122 </select>
123 </label>
124 <p className="muted small">The recorder remembers your last choices; these defaults apply until you change them there.</p>
125 </section>
126 </main>
127 </div>
128 );
↑↓ move↵openescclose