@crievo/omni 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/onboarding.md +69 -161
- package/omnivideo.config.example.json +1 -1
- package/package.json +2 -1
- package/reference/learnings.md +12 -0
- package/reference/omnichat.md +5 -4
- package/reference/publish.md +9 -9
- package/reference/studio.md +1 -1
- package/studio/api/app.ts +2 -2
- package/studio/api/routes/sound.ts +22 -3
- package/studio/api/services/onboarding.ts +242 -21
- package/studio/api/services/settings.ts +4 -2
- package/studio/api/services/sound.ts +45 -0
- package/studio/shared/types/api.ts +26 -1
- package/studio/studio.ts +1 -0
- package/studio/web/components/AppShell.tsx +3 -1
- package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +3 -0
- package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +37 -20
- package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +39 -12
- package/studio/web/components/MissingDmsPanel/BatchSend.tsx +13 -4
- package/studio/web/components/Omni/OmniWidget.tsx +14 -5
- package/studio/web/components/Omni/PendingActions.tsx +25 -6
- package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +13 -3
- package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +14 -4
- package/studio/web/components/ResultMessage.tsx +12 -3
- package/studio/web/components/Sidebar.tsx +63 -24
- package/studio/web/components/StageStepper.tsx +105 -34
- package/studio/web/components/Toaster.tsx +25 -0
- package/studio/web/components/VoiceCarousel.tsx +19 -16
- package/studio/web/components/VoiceProfilesSettings.tsx +8 -3
- package/studio/web/components/bencho/ActionNode.tsx +456 -0
- package/studio/web/components/bencho/AssigneePicker.tsx +246 -0
- package/studio/web/components/bencho/BenchoScope.tsx +8 -0
- package/studio/web/components/bencho/Checklist.tsx +421 -0
- package/studio/web/components/bencho/CreateMenu.tsx +258 -0
- package/studio/web/components/bencho/GenerateImage.tsx +290 -0
- package/studio/web/components/bencho/ImageAccordion.tsx +273 -0
- package/studio/web/components/bencho/LiquidToggle.tsx +289 -0
- package/studio/web/components/bencho/MagnifyDock.tsx +200 -0
- package/studio/web/components/bencho/NowPlaying.tsx +403 -0
- package/studio/web/components/bencho/OneTimeCode.tsx +374 -0
- package/studio/web/components/bencho/PullToRefresh.tsx +527 -0
- package/studio/web/components/bencho/SlideToConfirm.tsx +385 -0
- package/studio/web/components/bencho/SloshSlider.tsx +394 -0
- package/studio/web/components/bencho/StackedBars.tsx +342 -0
- package/studio/web/components/bencho/ToastStack.tsx +263 -0
- package/studio/web/components/bencho/UploadDropzone.tsx +394 -0
- package/studio/web/components/bencho/VoiceNote.tsx +705 -0
- package/studio/web/components/icons.tsx +27 -0
- package/studio/web/components/ui/slider.tsx +34 -1
- package/studio/web/components/ui/switch.tsx +18 -5
- package/studio/web/hooks/useOnboarding.ts +24 -6
- package/studio/web/index.html +1 -1
- package/studio/web/lib/alerts.ts +10 -0
- package/studio/web/lib/omni-panel.ts +4 -1
- package/studio/web/lib/toast.ts +196 -0
- package/studio/web/lib/upload.ts +24 -0
- package/studio/web/pages/BenchoPage/index.tsx +74 -0
- package/studio/web/pages/BenchoPage/registry.ts +174 -0
- package/studio/web/pages/GettingStartedPage/components/ComputerCard.tsx +102 -0
- package/studio/web/pages/GettingStartedPage/index.tsx +84 -58
- package/studio/web/pages/NewVideoPage/index.tsx +6 -2
- package/studio/web/pages/ReferencesPage/index.tsx +4 -1
- package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +30 -3
- package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +14 -17
- package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +1 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +24 -17
- package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +2 -1
- package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +12 -14
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +27 -11
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +22 -18
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +3 -4
- package/studio/web/pages/VideoPage/components/SoundTimeline/UploadTrack.tsx +59 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +1 -0
- package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +4 -6
- package/studio/web/pages/VideoPage/index.tsx +4 -0
- package/studio/web/pages/VideosPage/components/VideoRow.tsx +2 -4
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/bencho/action-node.css +227 -0
- package/studio/web/styles/components/bencho/checklist.css +182 -0
- package/studio/web/styles/components/bencho/dock.css +188 -0
- package/studio/web/styles/components/bencho/gallery.css +109 -0
- package/studio/web/styles/components/bencho/generate.css +151 -0
- package/studio/web/styles/components/bencho/image-accordion.css +37 -0
- package/studio/web/styles/components/bencho/liq-create.css +279 -0
- package/studio/web/styles/components/bencho/liq-toggle.css +106 -0
- package/studio/web/styles/components/bencho/one-time-code.css +148 -0
- package/studio/web/styles/components/bencho/picker.css +204 -0
- package/studio/web/styles/components/bencho/pull.css +313 -0
- package/studio/web/styles/components/bencho/scope.css +42 -0
- package/studio/web/styles/components/bencho/slide-confirm.css +139 -0
- package/studio/web/styles/components/bencho/slosh.css +137 -0
- package/studio/web/styles/components/bencho/sound.css +260 -0
- package/studio/web/styles/components/bencho/stacked-bars.css +334 -0
- package/studio/web/styles/components/bencho/toast-stack.css +192 -0
- package/studio/web/styles/components/bencho/tokens.css +243 -0
- package/studio/web/styles/components/bencho/upload-dropzone.css +352 -0
- package/studio/web/styles/components/bencho/voice-note.css +337 -0
- package/studio/web/styles/components/clone-voice.css +12 -2
- package/studio/web/styles/components/omni.css +6 -1
- package/studio/web/styles/components/stepper.css +123 -7
- package/studio/web/styles/components/toaster.css +10 -0
- package/studio/web/styles/components/ui/slider.css +6 -16
- package/studio/web/styles/components/ui/switch.css +2 -41
- package/studio/web/styles/components/voice-carousel.css +46 -4
- package/studio/web/styles/index.css +23 -0
- package/studio/web/styles/pages/getting-started.css +46 -0
- package/studio/web/styles/pages/settings/shell.css +4 -0
- package/studio/web/styles/pages/video/layout.css +1 -4
- package/studio/web/styles/pages/video/sound-timeline.css +8 -0
- package/studio/web/styles/theme/identity/shell.css +10 -26
- package/tools/lib/agents.mjs +24 -13
- package/tools/lib/claude-settings.mjs +32 -3
- package/tools/lib/deps.mjs +24 -0
- package/tools/lib/platforms/instagram.mjs +1 -1
- package/tools/lib/up.mjs +5 -2
- package/tools/omni.mjs +7 -5
- package/tools/omnichat.mjs +5 -7
- package/tools/package.mjs +5 -3
- package/tools/publish.mjs +40 -137
- package/tools/reference.mjs +1 -1
- package/tools/setup.mjs +16 -15
- package/tools/sound.mjs +67 -3
|
@@ -5,6 +5,7 @@ import { useState } from "react";
|
|
|
5
5
|
import { useVoiceRunning } from "../../../hooks/useVoiceRunning";
|
|
6
6
|
import { api, enc, send, type ActionResult } from "../../../lib/api";
|
|
7
7
|
import { ChatCircleText } from "../../../components/icons";
|
|
8
|
+
import { toast, toastResult } from "../../../lib/toast";
|
|
8
9
|
import type { VideoDetail } from "../../../types/api";
|
|
9
10
|
import { scenesOf } from "./StageResult";
|
|
10
11
|
|
|
@@ -18,19 +19,20 @@ export function ChangedVoices({ video }: { video: VideoDetail }) {
|
|
|
18
19
|
if (video.status?.stages.find(e => e.id === "voice")?.pause) return null;
|
|
19
20
|
if (!changed.length || running || !video.status?.stages.find(e => e.id === "timing")?.ok) return null;
|
|
20
21
|
const ask = async () => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}),
|
|
32
|
-
),
|
|
22
|
+
const r = await send(
|
|
23
|
+
api.videos[":slug"].claude.$post({
|
|
24
|
+
param: { slug: enc(video.slug) },
|
|
25
|
+
json: {
|
|
26
|
+
message:
|
|
27
|
+
`Troquei o texto ou a leitura das falas ${changed.join(", ")} pelo studio (texto no scenes.json, omnivideo.py voice --regenerate/--use). ` +
|
|
28
|
+
"O texto e as leituras escolhidas ficam: não reescreva essas falas nem gere voz de novo para elas. Refaça o vídeo a partir do tempo, como na linha " +
|
|
29
|
+
"“texto de uma fala” da tabela Retomar ou refazer da skill omnivideo: tempo, render das cenas afetadas, legenda e montagem.",
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
33
32
|
);
|
|
33
|
+
setResponse(r);
|
|
34
|
+
if (r.ok) toast.success("Pedido enviado", { body: "Acompanhe na sessão acima." });
|
|
35
|
+
else toastResult(r);
|
|
34
36
|
};
|
|
35
37
|
return (
|
|
36
38
|
<div className="voices-notice">
|
|
@@ -42,11 +44,6 @@ export function ChangedVoices({ video }: { video: VideoDetail }) {
|
|
|
42
44
|
<button className="button primary small" onClick={ask} disabled={!!response?.ok}>
|
|
43
45
|
<ChatCircleText className="ic" /> Pedir ao agente para refazer
|
|
44
46
|
</button>
|
|
45
|
-
{response && (
|
|
46
|
-
<span className={`response ${response.ok ? "ok" : "error"}`}>
|
|
47
|
-
{response.ok ? "Pedido enviado: acompanhe na sessão acima" : response.error}
|
|
48
|
-
</span>
|
|
49
|
-
)}
|
|
50
47
|
</div>
|
|
51
48
|
</div>
|
|
52
49
|
);
|
|
@@ -118,7 +118,7 @@ export function LineTextEditor({ video, line, only }: { video: VideoDetail; line
|
|
|
118
118
|
Se o texto tirar a palavra em que um beat entra, o agente grava a fala e muda o beat junto. Escreva como se
|
|
119
119
|
fala: número por extenso quando a voz pode errar.
|
|
120
120
|
</p>
|
|
121
|
-
|
|
121
|
+
<ResultMessage result={response} />
|
|
122
122
|
</div>
|
|
123
123
|
);
|
|
124
124
|
}
|
|
@@ -7,9 +7,12 @@ import { api, enc, send as sendRequest, type ActionResult } from "../../../lib/a
|
|
|
7
7
|
import { formatDayTime, timeAgo } from "../../../lib/format";
|
|
8
8
|
import { PLATFORMS, formatPlatformList, type PlatformId } from "../../../lib/platforms";
|
|
9
9
|
import { deletePublishDraft, readPublishDraft, savePublishDraft } from "../../../lib/publish-draft";
|
|
10
|
+
import { toastResult } from "../../../lib/toast";
|
|
10
11
|
import { PlatformCard } from "../../../components/PlatformCard";
|
|
11
12
|
import { ResultMessage } from "../../../components/ResultMessage";
|
|
12
|
-
import {
|
|
13
|
+
import { BenchoScope } from "../../../components/bencho/BenchoScope";
|
|
14
|
+
import { SlideToConfirm } from "../../../components/bencho/SlideToConfirm";
|
|
15
|
+
import { CalendarBlank, ChatsCircle, UploadSimple, X } from "../../../components/icons";
|
|
13
16
|
import type { FileEntry, VideoDetail } from "../../../types/api";
|
|
14
17
|
import { ScheduledInfo } from "./ScheduledInfo";
|
|
15
18
|
import { CoverPicker } from "./CoverPicker";
|
|
@@ -32,7 +35,6 @@ export function PublishForm({ video, current }: { video: VideoDetail; current: F
|
|
|
32
35
|
},
|
|
33
36
|
);
|
|
34
37
|
const [writing, setWriting] = useState(false);
|
|
35
|
-
const [textNotice, setTextNotice] = useState<ActionResult>(null);
|
|
36
38
|
const [privacy, setPrivacy] = useState<string | null>(draft?.privacy ?? null);
|
|
37
39
|
const [tiktokPrivacy, setTiktokPrivacy] = useState<string | null>(draft?.privacyTiktok ?? null);
|
|
38
40
|
const [checked, setChecked] = useState<Record<string, boolean>>(draft?.checked ?? {});
|
|
@@ -95,12 +97,14 @@ export function PublishForm({ video, current }: { video: VideoDetail; current: F
|
|
|
95
97
|
/* `publish.mjs text`: Claude writes from the script; redo replaces what is in the fields */
|
|
96
98
|
const write = async (redo: boolean) => {
|
|
97
99
|
setWriting(true);
|
|
98
|
-
setTextNotice(null);
|
|
99
100
|
const r = await sendRequest(
|
|
100
101
|
api.videos[":slug"].publish.text.$post({ param: { slug: enc(video.slug) }, json: { redo } }),
|
|
101
102
|
);
|
|
102
103
|
setWriting(false);
|
|
103
|
-
|
|
104
|
+
/* a failure, or Claude answering without writing: a toast (the fields keep what they had) */
|
|
105
|
+
if (!r.ok || /não escreveu/.test(r.output ?? "")) {
|
|
106
|
+
toastResult({ ok: false, error: r.ok ? r.output : r.error }, undefined, { always: true });
|
|
107
|
+
}
|
|
104
108
|
/* the text publish.mjs wrote: a field it didn't write keeps what is there */
|
|
105
109
|
const t = r.ok && r.text && typeof r.text === "object" && !Array.isArray(r.text) ? r.text : null;
|
|
106
110
|
if (t) {
|
|
@@ -255,9 +259,6 @@ export function PublishForm({ video, current }: { video: VideoDetail; current: F
|
|
|
255
259
|
})}
|
|
256
260
|
</div>
|
|
257
261
|
|
|
258
|
-
{textNotice && (!textNotice.ok || /não escreveu/.test(textNotice.output ?? "")) && (
|
|
259
|
-
<p className="response error">{textNotice.ok ? textNotice.output : textNotice.error}</p>
|
|
260
|
-
)}
|
|
261
262
|
<PublishText
|
|
262
263
|
text={text}
|
|
263
264
|
onField={field}
|
|
@@ -350,16 +351,22 @@ export function PublishForm({ video, current }: { video: VideoDetail; current: F
|
|
|
350
351
|
</button>
|
|
351
352
|
) : (
|
|
352
353
|
<>
|
|
353
|
-
<
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
?
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
354
|
+
<p className="hint" style={{ flexBasis: "100%", margin: 0 }}>
|
|
355
|
+
<strong>
|
|
356
|
+
{scheduling ? "Agendar" : "Publicar"} {formatPlatformList(chosen)}
|
|
357
|
+
{scheduling ? ` ${formatDayTime(whenMs)}` : ""}.
|
|
358
|
+
</strong>
|
|
359
|
+
</p>
|
|
360
|
+
<BenchoScope>
|
|
361
|
+
<SlideToConfirm
|
|
362
|
+
className="bn-sld-grow"
|
|
363
|
+
width="fill"
|
|
364
|
+
label={scheduling ? "Deslize para agendar" : "Deslize para publicar"}
|
|
365
|
+
busyLabel={scheduling ? "Agendando…" : "Publicando…"}
|
|
366
|
+
busy={sending}
|
|
367
|
+
onConfirm={send}
|
|
368
|
+
/>
|
|
369
|
+
</BenchoScope>
|
|
363
370
|
{!sending && (
|
|
364
371
|
<button className="button" onClick={() => setConfirm(false)}>
|
|
365
372
|
<X className="ic" /> Cancelar
|
|
@@ -6,6 +6,7 @@ import { api, enc, send, type ActionResult } from "../../../lib/api";
|
|
|
6
6
|
import { formatDayTime } from "../../../lib/format";
|
|
7
7
|
import { formatPlatformList } from "../../../lib/platforms";
|
|
8
8
|
import { Clock } from "../../../components/icons";
|
|
9
|
+
import { ResultMessage } from "../../../components/ResultMessage";
|
|
9
10
|
import type { PlatformAccounts, ScheduledPost, VideoDetail } from "../../../types/api";
|
|
10
11
|
|
|
11
12
|
const TITLE: Record<ScheduledPost["state"], string> = {
|
|
@@ -66,7 +67,7 @@ export function ScheduledInfo({
|
|
|
66
67
|
{cancelling ? "Cancelando…" : "Cancelar agendamento"}
|
|
67
68
|
</button>
|
|
68
69
|
)}
|
|
69
|
-
|
|
70
|
+
<ResultMessage result={response} />
|
|
70
71
|
</div>
|
|
71
72
|
);
|
|
72
73
|
}
|
|
@@ -4,6 +4,8 @@ import { useState } from "react";
|
|
|
4
4
|
import { api, enc, send as sendRequest } from "../../../../lib/api";
|
|
5
5
|
import { Plus } from "../../../../components/icons";
|
|
6
6
|
import { useOmniHandle } from "../../../../lib/omni-ui";
|
|
7
|
+
import { toast } from "../../../../lib/toast";
|
|
8
|
+
import { UploadTrack } from "./UploadTrack";
|
|
7
9
|
|
|
8
10
|
/* adds an Epidemic effect: the URL captured in the player becomes an MP3 in the video's sfx/ */
|
|
9
11
|
export function AddSound({ slug, onAdd }: { slug: string; onAdd: (name: string | null) => void }) {
|
|
@@ -11,7 +13,6 @@ export function AddSound({ slug, onAdd }: { slug: string; onAdd: (name: string |
|
|
|
11
13
|
const [epidemicUrl, setEpidemicUrl] = useState("");
|
|
12
14
|
const [name, setName] = useState("");
|
|
13
15
|
const [downloading, setDownloading] = useState(false);
|
|
14
|
-
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
|
15
16
|
/* a direct MP3 (audiocdn…/lqmp3/<id>.mp3) downloads at once; any other Epidemic page (playlist,
|
|
16
17
|
theme, search) goes to the agent, which opens it in Chrome, plays and captures the MP3 */
|
|
17
18
|
const pageUrl = (u: string) => !/audiocdn\.epidemicsound\.com\/lqmp3\//.test(u);
|
|
@@ -21,7 +22,6 @@ export function AddSound({ slug, onAdd }: { slug: string; onAdd: (name: string |
|
|
|
21
22
|
if (!url.trim() || downloading) return { ok: false, error: downloading ? "já está baixando" : "falta o link" };
|
|
22
23
|
const isPage = pageUrl(url);
|
|
23
24
|
setDownloading(true);
|
|
24
|
-
setMsg(null);
|
|
25
25
|
const param = { slug: enc(slug) };
|
|
26
26
|
const json = { url: url.trim(), name: file.trim() };
|
|
27
27
|
const r = await sendRequest(
|
|
@@ -31,20 +31,17 @@ export function AddSound({ slug, onAdd }: { slug: string; onAdd: (name: string |
|
|
|
31
31
|
);
|
|
32
32
|
setDownloading(false);
|
|
33
33
|
if (!r.ok) {
|
|
34
|
-
|
|
34
|
+
toast.error("Não consegui baixar o som", { body: r.error });
|
|
35
35
|
return { ok: false, error: r.error ?? "não consegui baixar" };
|
|
36
36
|
}
|
|
37
37
|
if (isPage) {
|
|
38
38
|
/* the agent works in the background; the timeline reloads when it stops */
|
|
39
|
-
|
|
40
|
-
ok: true,
|
|
41
|
-
text: "O agente abriu a página no Chrome e vai baixar a faixa. A lista atualiza quando ele terminar.",
|
|
42
|
-
});
|
|
39
|
+
toast.info("O agente abriu a página no Chrome", { body: "Ele baixa a faixa; a lista atualiza quando terminar." });
|
|
43
40
|
setEpidemicUrl("");
|
|
44
41
|
setName("");
|
|
45
42
|
return { ok: true, output: "o agente do editor abriu a página no Chrome e vai baixar a faixa" };
|
|
46
43
|
}
|
|
47
|
-
|
|
44
|
+
toast.success("Som adicionado", { body: r.output?.split(" ←")[0] });
|
|
48
45
|
setEpidemicUrl("");
|
|
49
46
|
setName("");
|
|
50
47
|
onAdd(r.output?.split(" ←")[0] ?? null);
|
|
@@ -55,15 +52,18 @@ export function AddSound({ slug, onAdd }: { slug: string; onAdd: (name: string |
|
|
|
55
52
|
setOpen(true);
|
|
56
53
|
setEpidemicUrl(url);
|
|
57
54
|
setName(file);
|
|
58
|
-
setMsg(null);
|
|
59
55
|
},
|
|
60
56
|
submit: (url: string, file: string) => send(url, file),
|
|
61
57
|
});
|
|
62
58
|
if (!open)
|
|
63
59
|
return (
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
|
|
60
|
+
<div className="sound-add-buttons">
|
|
61
|
+
<button className="button small" onClick={() => setOpen(true)}>
|
|
62
|
+
<Plus className="ic" /> Adicionar som
|
|
63
|
+
</button>
|
|
64
|
+
{/* the user's own file: it joins this video's sounds, armed to place on the timeline */}
|
|
65
|
+
<UploadTrack slug={slug} label="Enviar áudio" onUploaded={onAdd} />
|
|
66
|
+
</div>
|
|
67
67
|
);
|
|
68
68
|
return (
|
|
69
69
|
<div className="sound-add">
|
|
@@ -100,7 +100,6 @@ export function AddSound({ slug, onAdd }: { slug: string; onAdd: (name: string |
|
|
|
100
100
|
className="button small"
|
|
101
101
|
onClick={() => {
|
|
102
102
|
setOpen(false);
|
|
103
|
-
setMsg(null);
|
|
104
103
|
}}
|
|
105
104
|
>
|
|
106
105
|
Fechar
|
|
@@ -111,7 +110,6 @@ export function AddSound({ slug, onAdd }: { slug: string; onAdd: (name: string |
|
|
|
111
110
|
dá play e pega a primeira faixa que o vídeo ainda não tem. Ou cole direto o MP3 do player
|
|
112
111
|
(audiocdn…/lqmp3/….mp3). O arquivo vai para o sfx/ deste vídeo, com a origem registrada.
|
|
113
112
|
</p>
|
|
114
|
-
{msg && <p className={`response ${msg.ok ? "ok" : "error"}`}>{msg.text}</p>}
|
|
115
113
|
</div>
|
|
116
114
|
);
|
|
117
115
|
}
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import { defaultBedName } from "../../../../lib/sound-timeline";
|
|
5
5
|
import type { TimelineGeometry } from "./timelineGeometry";
|
|
6
|
+
import { UploadTrack } from "./UploadTrack";
|
|
6
7
|
import type { SoundTimelineState } from "./useSoundTimeline";
|
|
7
8
|
import type { SoundData } from "./types";
|
|
8
9
|
|
|
9
10
|
export function BedRow({ st, geo, data }: { st: SoundTimelineState; geo: TimelineGeometry; data: SoundData }) {
|
|
10
|
-
const { plan, sel, setSel, dropTarget, changeDraft } = st;
|
|
11
|
+
const { plan, sel, setSel, dropTarget, changeDraft, reloadCatalog } = st;
|
|
11
12
|
const { x } = geo;
|
|
12
13
|
return (
|
|
13
14
|
<div
|
|
@@ -25,20 +26,35 @@ export function BedRow({ st, geo, data }: { st: SoundTimelineState; geo: Timelin
|
|
|
25
26
|
</span>
|
|
26
27
|
</div>
|
|
27
28
|
) : (
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
<div className="sound-no-bed">
|
|
30
|
+
<button
|
|
31
|
+
className="mini-button"
|
|
32
|
+
onClick={() => {
|
|
33
|
+
const name = defaultBedName(data.catalog);
|
|
34
|
+
if (name) {
|
|
35
|
+
changeDraft(r => {
|
|
36
|
+
r.bed = { name };
|
|
37
|
+
});
|
|
38
|
+
setSel("bed");
|
|
39
|
+
}
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
+ música de fundo
|
|
43
|
+
</button>
|
|
44
|
+
{/* the user's own file: it joins this video's sounds and becomes the background music */}
|
|
45
|
+
<UploadTrack
|
|
46
|
+
slug={st.slug}
|
|
47
|
+
className="mini-button"
|
|
48
|
+
label="Enviar música"
|
|
49
|
+
onUploaded={name => {
|
|
50
|
+
reloadCatalog(null);
|
|
33
51
|
changeDraft(r => {
|
|
34
52
|
r.bed = { name };
|
|
35
53
|
});
|
|
36
54
|
setSel("bed");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
>
|
|
40
|
-
+ música de fundo
|
|
41
|
-
</button>
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
42
58
|
)}
|
|
43
59
|
</div>
|
|
44
60
|
);
|
|
@@ -1,32 +1,36 @@
|
|
|
1
1
|
/* Floats at the bottom right of the Legenda box (it sticks as the box scrolls) once the caption has an unsaved change: Salvar legenda saves the
|
|
2
2
|
position and renders the export again; Descartar goes back to what is saved. */
|
|
3
3
|
|
|
4
|
+
import { useEffect } from "react";
|
|
4
5
|
import { ArrowCounterClockwise, FloppyDisk } from "../../../../components/icons";
|
|
5
6
|
import { clone } from "../../../../lib/caption-position";
|
|
7
|
+
import { toastResult } from "../../../../lib/toast";
|
|
6
8
|
import type { CaptionEditor } from "../CaptionPositionEditor/captionEditor";
|
|
7
9
|
|
|
8
10
|
export function CaptionSaveBar({ editor, busy }: { editor: CaptionEditor | null; busy: boolean }) {
|
|
11
|
+
const message = editor?.message;
|
|
12
|
+
/* a failed save is a toast: the bar itself only exists while there is an edit to save or discard */
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (message && !message.ok) toastResult({ ok: false, error: message.text }, undefined, { always: true });
|
|
15
|
+
}, [message]);
|
|
9
16
|
if (!editor) return null;
|
|
10
|
-
const { changed, saving, save, setDraft, data
|
|
11
|
-
if (!changed
|
|
17
|
+
const { changed, saving, save, setDraft, data } = editor;
|
|
18
|
+
if (!changed) return null;
|
|
12
19
|
return (
|
|
13
20
|
<div className="caption-save-bar" role="region" aria-label="Salvar legenda">
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
</button>
|
|
28
|
-
</div>
|
|
29
|
-
)}
|
|
21
|
+
<div className="actions">
|
|
22
|
+
<button className="button" disabled={saving} onClick={() => setDraft(clone(data.position))}>
|
|
23
|
+
<ArrowCounterClockwise className="ic" /> Descartar
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
className="button primary"
|
|
27
|
+
disabled={saving || busy}
|
|
28
|
+
onClick={save}
|
|
29
|
+
title="Salva a posição e o estilo e renderiza o export de novo"
|
|
30
|
+
>
|
|
31
|
+
<FloppyDisk className="ic" /> {saving ? "Salvando…" : "Salvar legenda"}
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
30
34
|
</div>
|
|
31
35
|
);
|
|
32
36
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/* Under the timeline: the save status (autosave 5 s after the last edit) and the last action's response. */
|
|
1
|
+
/* Under the timeline: the save status (autosave 5 s after the last edit) and the last action's response, which is a toast. */
|
|
2
2
|
|
|
3
|
+
import { ResultMessage } from "../../../../components/ResultMessage";
|
|
3
4
|
import type { SoundTimelineState } from "./useSoundTimeline";
|
|
4
5
|
|
|
5
6
|
export function TimelineFooter({ st }: { st: SoundTimelineState }) {
|
|
@@ -18,9 +19,7 @@ export function TimelineFooter({ st }: { st: SoundTimelineState }) {
|
|
|
18
19
|
: "Tudo salvo"}
|
|
19
20
|
{!dirty && !saving && pending && !rendering && " · o export ainda não tem esse som"}
|
|
20
21
|
</span>
|
|
21
|
-
{response
|
|
22
|
-
<p className={`response ${response.ok ? "ok" : "error"}`}>{response.ok ? response.output : response.error}</p>
|
|
23
|
-
)}
|
|
22
|
+
<ResultMessage result={response} />
|
|
24
23
|
</div>
|
|
25
24
|
);
|
|
26
25
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* "Enviar música": the user's own audio file becomes an MP3 in the video's sfx/ (sound.mjs import). The
|
|
2
|
+
button opens the file picker and reports the name the track got in a toast; what to do with it (arm it in the
|
|
3
|
+
library, make it the background music) is up to the caller. */
|
|
4
|
+
|
|
5
|
+
import { useRef, useState } from "react";
|
|
6
|
+
import { Plus } from "../../../../components/icons";
|
|
7
|
+
import { toast } from "../../../../lib/toast";
|
|
8
|
+
import { uploadTrackFile } from "../../../../lib/upload";
|
|
9
|
+
|
|
10
|
+
export function UploadTrack({
|
|
11
|
+
slug,
|
|
12
|
+
onUploaded,
|
|
13
|
+
label = "Enviar música",
|
|
14
|
+
className = "button small",
|
|
15
|
+
}: {
|
|
16
|
+
slug: string;
|
|
17
|
+
onUploaded: (name: string) => void;
|
|
18
|
+
label?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
}) {
|
|
21
|
+
const input = useRef<HTMLInputElement>(null);
|
|
22
|
+
const [pct, setPct] = useState<number | null>(null);
|
|
23
|
+
const pick = async (file: File | undefined) => {
|
|
24
|
+
if (!file) return;
|
|
25
|
+
setPct(0);
|
|
26
|
+
const r = await uploadTrackFile(slug, file, setPct);
|
|
27
|
+
setPct(null);
|
|
28
|
+
const name = r.output?.split(" ←")[0];
|
|
29
|
+
if (!r.ok || !name) {
|
|
30
|
+
toast.error("Não consegui enviar o arquivo", { body: r.error });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
toast.success("Áudio enviado", { body: name });
|
|
34
|
+
onUploaded(name);
|
|
35
|
+
};
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
className={className}
|
|
41
|
+
disabled={pct != null}
|
|
42
|
+
onClick={() => input.current?.click()}
|
|
43
|
+
title="Enviar um arquivo de áudio seu (mp3, wav, m4a…) para este vídeo"
|
|
44
|
+
>
|
|
45
|
+
<Plus className="ic" /> {pct == null ? label : pct < 100 ? `Enviando… ${pct}%` : "Convertendo…"}
|
|
46
|
+
</button>
|
|
47
|
+
<input
|
|
48
|
+
ref={input}
|
|
49
|
+
type="file"
|
|
50
|
+
accept="audio/*,.mp3,.wav,.m4a,.aac,.flac,.ogg,.opus,.aif,.aiff"
|
|
51
|
+
hidden
|
|
52
|
+
onChange={e => {
|
|
53
|
+
void pick(e.target.files?.[0]);
|
|
54
|
+
e.target.value = "";
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -6,6 +6,7 @@ import { useVoiceRunning } from "../../../hooks/useVoiceRunning";
|
|
|
6
6
|
import { api, enc, send, type ActionResult } from "../../../lib/api";
|
|
7
7
|
import { TOOL_LABEL } from "../../../lib/format";
|
|
8
8
|
import { ArrowRight } from "../../../components/icons";
|
|
9
|
+
import { toastResult } from "../../../lib/toast";
|
|
9
10
|
import type { VideoDetail } from "../../../types/api";
|
|
10
11
|
|
|
11
12
|
export function VoicePauseBanner({ video }: { video: VideoDetail }) {
|
|
@@ -32,8 +33,10 @@ export function VoicePauseBanner({ video }: { video: VideoDetail }) {
|
|
|
32
33
|
];
|
|
33
34
|
const proceed = async () => {
|
|
34
35
|
setSending(true);
|
|
35
|
-
|
|
36
|
+
const r = await send(api.videos[":slug"].voice.proceed.$post({ param: { slug: enc(video.slug) }, json: {} }));
|
|
37
|
+
setResponse(r);
|
|
36
38
|
setSending(false);
|
|
39
|
+
toastResult(r, undefined, { always: true });
|
|
37
40
|
};
|
|
38
41
|
return (
|
|
39
42
|
<div className="voices-notice">
|
|
@@ -45,11 +48,6 @@ export function VoicePauseBanner({ video }: { video: VideoDetail }) {
|
|
|
45
48
|
<button className="button primary small" onClick={proceed} disabled={sending || !!running || !!response?.ok}>
|
|
46
49
|
<ArrowRight className="ic" /> {running ? "Esperando a voz terminar…" : "Seguir com estas vozes"}
|
|
47
50
|
</button>
|
|
48
|
-
{response && (
|
|
49
|
-
<span className={`response ${response.ok ? "ok" : "error"}`}>
|
|
50
|
-
{response.ok ? response.output : response.error}
|
|
51
|
-
</span>
|
|
52
|
-
)}
|
|
53
51
|
</div>
|
|
54
52
|
</div>
|
|
55
53
|
);
|
|
@@ -236,6 +236,10 @@ export function VideoPage({ slug, stageParam }: { slug: string; stageParam: stri
|
|
|
236
236
|
runs={video.runs}
|
|
237
237
|
selected={stage.id}
|
|
238
238
|
onSelect={choose}
|
|
239
|
+
onProcess={id => {
|
|
240
|
+
choose(id);
|
|
241
|
+
setPanel(true);
|
|
242
|
+
}}
|
|
239
243
|
stages={pipeline === "video" ? videoStages : PIPELINES[pipeline]}
|
|
240
244
|
/>
|
|
241
245
|
</nav>
|
|
@@ -4,6 +4,7 @@ import { api, enc, send } from "../../../lib/api";
|
|
|
4
4
|
import { formatSeconds, timeAgo } from "../../../lib/format";
|
|
5
5
|
import { aspectRatio, mediaUrl } from "../../../lib/media";
|
|
6
6
|
import { SESSION_STATE } from "../../../lib/sessions";
|
|
7
|
+
import { toast } from "../../../lib/toast";
|
|
7
8
|
import { activeSchedule, livePlatforms, carouselLive, contentReady } from "../../../lib/video-tabs";
|
|
8
9
|
import { PIPELINES, PIPELINE_NAME, videoStagesFor } from "../../../lib/stages";
|
|
9
10
|
import { useSessionFor } from "../../../hooks/useSessions";
|
|
@@ -26,7 +27,6 @@ export function VideoRow({ video, queue }: { video: VideoSummary; queue: Queue |
|
|
|
26
27
|
const hasCarousel = video.deliverables.includes("carousel");
|
|
27
28
|
const warnings = [...(hasVideo ? (st?.warnings ?? []) : []), ...(video.carousel?.warnings ?? [])];
|
|
28
29
|
const [busy, setBusy] = useState(false);
|
|
29
|
-
const [archiveError, setArchiveError] = useState<string | null>(null);
|
|
30
30
|
/* the scheduler publishes with the studio closed, and a running task doesn't vanish midway: the server refuses too */
|
|
31
31
|
const blocked = video.archived
|
|
32
32
|
? null
|
|
@@ -37,11 +37,10 @@ export function VideoRow({ video, queue }: { video: VideoSummary; queue: Queue |
|
|
|
37
37
|
: null;
|
|
38
38
|
const toggleArchive = async () => {
|
|
39
39
|
setBusy(true);
|
|
40
|
-
setArchiveError(null);
|
|
41
40
|
const r = await send(
|
|
42
41
|
api.videos[":slug"].archive.$post({ param: { slug: enc(video.slug) }, json: { archive: !video.archived } }),
|
|
43
42
|
);
|
|
44
|
-
if (!r.ok)
|
|
43
|
+
if (!r.ok) toast.error("Não deu para arquivar", { body: r.error });
|
|
45
44
|
setBusy(false);
|
|
46
45
|
};
|
|
47
46
|
const needsAttention = video.scenes_approval?.status === "pending" || video.script_approval?.status === "pending";
|
|
@@ -88,7 +87,6 @@ export function VideoRow({ video, queue }: { video: VideoSummary; queue: Queue |
|
|
|
88
87
|
{video.archived ? "Desarquivar" : "Arquivar"}
|
|
89
88
|
</button>
|
|
90
89
|
</div>
|
|
91
|
-
{archiveError && <p className="response error">{archiveError}</p>}
|
|
92
90
|
<PublishedOn video={video} />
|
|
93
91
|
{hasVideo && st && !live && (
|
|
94
92
|
<div className="vr-pipeline">
|
package/studio/web/router.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import { SchedulePage } from "./pages/SchedulePage";
|
|
|
21
21
|
import { OmnichatPage, OmnichatRulePage } from "./pages/OmnichatPage";
|
|
22
22
|
import { GettingStartedPage } from "./pages/GettingStartedPage";
|
|
23
23
|
import { NotFoundPage } from "./pages/NotFoundPage";
|
|
24
|
+
import { BenchoPage } from "./pages/BenchoPage";
|
|
24
25
|
|
|
25
26
|
export const rootRoute = createRootRoute({
|
|
26
27
|
component: AppShell,
|
|
@@ -168,6 +169,12 @@ export const gettingStartedRoute = createRoute({
|
|
|
168
169
|
path: "/getting-started",
|
|
169
170
|
component: () => <GettingStartedPage />,
|
|
170
171
|
});
|
|
172
|
+
/* the Bencho ports, side by side; not in the menu */
|
|
173
|
+
export const benchoRoute = createRoute({
|
|
174
|
+
getParentRoute: () => rootRoute,
|
|
175
|
+
path: "/bencho",
|
|
176
|
+
component: () => <BenchoPage />,
|
|
177
|
+
});
|
|
171
178
|
|
|
172
179
|
export const routeTree = rootRoute.addChildren([
|
|
173
180
|
indexRoute,
|
|
@@ -188,6 +195,7 @@ export const routeTree = rootRoute.addChildren([
|
|
|
188
195
|
omnichatRoute,
|
|
189
196
|
omnichatRuleRoute,
|
|
190
197
|
gettingStartedRoute,
|
|
198
|
+
benchoRoute,
|
|
191
199
|
]);
|
|
192
200
|
|
|
193
201
|
export const router = createRouter({ routeTree, defaultPreload: false, scrollRestoration: false });
|