Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ WORKDIR /app

ARG FREESOUND_CLIENT_ID
ARG FREESOUND_API_KEY
# NEXT_PUBLIC_* are inlined at build time — pass VPS URLs as build args.
ARG NEXT_PUBLIC_SITE_URL=http://localhost:3000
ARG NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
ARG NEXT_PUBLIC_AI_BACKEND_URL=http://localhost:8420
ARG NEXT_PUBLIC_OLLAMA_URL=http://localhost:11434
ARG NEXT_PUBLIC_WHISPER_SERVICE_URL=http://localhost:8421
ARG NEXT_PUBLIC_TTS_SERVICE_URL=http://localhost:8422
ARG NEXT_PUBLIC_IMAGE_SERVICE_URL=http://localhost:8423

COPY package.json package.json
COPY bun.lock bun.lock
Expand All @@ -43,9 +51,13 @@ ENV DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut"
ENV BETTER_AUTH_SECRET="build-time-secret"
ENV UPSTASH_REDIS_REST_URL="http://localhost:8079"
ENV UPSTASH_REDIS_REST_TOKEN="example_token"
ENV NEXT_PUBLIC_SITE_URL="http://localhost:3000"
ENV NEXT_PUBLIC_MARBLE_API_URL="https://api.marblecms.com"
ENV NEXT_PUBLIC_AI_BACKEND_URL="http://localhost:8420"
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_MARBLE_API_URL=$NEXT_PUBLIC_MARBLE_API_URL
ENV NEXT_PUBLIC_AI_BACKEND_URL=$NEXT_PUBLIC_AI_BACKEND_URL
ENV NEXT_PUBLIC_OLLAMA_URL=$NEXT_PUBLIC_OLLAMA_URL
ENV NEXT_PUBLIC_WHISPER_SERVICE_URL=$NEXT_PUBLIC_WHISPER_SERVICE_URL
ENV NEXT_PUBLIC_TTS_SERVICE_URL=$NEXT_PUBLIC_TTS_SERVICE_URL
ENV NEXT_PUBLIC_IMAGE_SERVICE_URL=$NEXT_PUBLIC_IMAGE_SERVICE_URL

ENV FREESOUND_CLIENT_ID=$FREESOUND_CLIENT_ID
ENV FREESOUND_API_KEY=$FREESOUND_API_KEY
Expand Down
26 changes: 14 additions & 12 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,20 @@ export default function RootLayout({
>
<TooltipProvider>
<Toaster />
<Script
src="https://cdn.databuddy.cc/databuddy.js"
strategy="afterInteractive"
async
data-client-id="UP-Wcoy5arxFeK7oyjMMZ"
data-disabled={webEnv.NODE_ENV === "development"}
data-track-attributes={false}
data-track-errors={true}
data-track-outgoing-links={false}
data-track-web-vitals={false}
data-track-sessions={false}
/>
{process.env.NEXT_PUBLIC_DATABUDDY_CLIENT_ID && (
<Script
src="https://cdn.databuddy.cc/databuddy.js"
strategy="afterInteractive"
async
data-client-id={process.env.NEXT_PUBLIC_DATABUDDY_CLIENT_ID}
data-disabled={webEnv.NODE_ENV === "development"}
data-track-attributes={false}
data-track-errors={true}
data-track-outgoing-links={false}
data-track-web-vitals={false}
data-track-sessions={false}
/>
)}
{children}
</TooltipProvider>
</ThemeProvider>
Expand Down
11 changes: 6 additions & 5 deletions apps/web/src/components/editor/ai/ai-panel-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { cn } from "@/utils/ui";
import { Button } from "@/components/ui/button";
import { HugeiconsIcon } from "@hugeicons/react";
import { SparklesIcon, Cancel01Icon } from "@hugeicons/core-free-icons";
import { generateUUID } from "@/utils/id";

export function AIPanelWrapper() {
const {
Expand Down Expand Up @@ -39,7 +40,7 @@ export function AIPanelWrapper() {
async (message: string) => {
if (!isConnected) {
const errorMessage: AIMessage = {
id: crypto.randomUUID(),
id: generateUUID(),
role: "assistant",
content:
"Cannot process commands — the AI backend is not connected. Open the setup guide from the AI status indicator in the header to get started.",
Expand All @@ -50,7 +51,7 @@ export function AIPanelWrapper() {
}

const userMessage: AIMessage = {
id: crypto.randomUUID(),
id: generateUUID(),
role: "user",
content: message,
timestamp: new Date(),
Expand All @@ -61,12 +62,12 @@ export function AIPanelWrapper() {
const result = await executeCommand(message, null);

const assistantMessage: AIMessage = {
id: crypto.randomUUID(),
id: generateUUID(),
role: "assistant",
content: result.explanation,
timestamp: new Date(),
actions: result.actions.map((action) => ({
id: crypto.randomUUID(),
id: generateUUID(),
type: action.type,
label: action.type.replace(/_/g, " ").toLowerCase(),
description: action.description,
Expand All @@ -84,7 +85,7 @@ export function AIPanelWrapper() {
errorDetail.includes("Failed to fetch");

const errorMessage: AIMessage = {
id: crypto.randomUUID(),
id: generateUUID(),
role: "assistant",
content: isConnectionIssue
? `Cannot reach the AI backend: ${errorDetail}\n\nMake sure the AI backend is running at the configured URL. Open the setup guide for instructions.`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useAIStatus } from "@/hooks/use-ai-status";
import { useAIStore } from "@/stores/ai-store";
import { useTranscriptStore } from "@/stores/transcript-store";
import { toast } from "sonner";
import { generateUUID } from "@/utils/id";
import { TemplatePanel } from "@/components/editor/ai/template-panel";
import { BRollSuggestionsPanel } from "@/components/editor/ai/broll-suggestions-panel";
import { YouTubeReelsPanel } from "@/components/editor/youtube/youtube-reels-panel";
Expand Down Expand Up @@ -364,14 +365,14 @@ export function AIStudioView() {
}

addMessage({
id: crypto.randomUUID(),
id: generateUUID(),
role: "user",
content: trimmed,
});
setInputValue("");
setIsThinking(true);

const assistantId = crypto.randomUUID();
const assistantId = generateUUID();
let messageAdded = false;

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ import { Slider } from "@/components/ui/slider";
import { HugeiconsIcon } from "@hugeicons/react";
import { Add01Icon, Cancel01Icon } from "@hugeicons/core-free-icons";
import { useEditor } from "@/hooks/use-editor";
import { getAllAudioEffectDefinitions, type AudioEffectType } from "@/lib/audio/audio-effects";

interface TrackAudioEffect {
id: string;
type: AudioEffectType;
params: Record<string, number>;
enabled: boolean;
}
import {
getAllAudioEffectDefinitions,
type AudioEffectType,
type TrackAudioEffect,
} from "@/lib/audio/audio-effects";

interface AudioEffectsChainProps {
trackId: string;
Expand All @@ -29,7 +26,10 @@ export function AudioEffectsChainPanel({ trackId, trackType, className }: AudioE
const track = editor.timeline.getTrackById({ trackId });
if (!track || (trackType !== "audio" && trackType !== "video")) return null;

const effects: TrackAudioEffect[] = (track as any).audioEffects ?? [];
const effects: TrackAudioEffect[] =
(track.type === "audio" || track.type === "video"
? track.audioEffects
: undefined) ?? [];

const handleAdd = useCallback(
(type: AudioEffectType) => {
Expand All @@ -45,7 +45,7 @@ export function AudioEffectsChainPanel({ trackId, trackType, className }: AudioE
];
editor.timeline.updateTrack({
trackId,
updates: { audioEffects: updatedEffects } as any,
updates: { audioEffects: updatedEffects },
});
},
[effects, trackId, editor],
Expand All @@ -56,7 +56,7 @@ export function AudioEffectsChainPanel({ trackId, trackType, className }: AudioE
const updatedEffects = effects.filter((e) => e.id !== effectId);
editor.timeline.updateTrack({
trackId,
updates: { audioEffects: updatedEffects } as any,
updates: { audioEffects: updatedEffects },
});
},
[effects, trackId, editor],
Expand All @@ -69,7 +69,7 @@ export function AudioEffectsChainPanel({ trackId, trackType, className }: AudioE
);
editor.timeline.updateTrack({
trackId,
updates: { audioEffects: updatedEffects } as any,
updates: { audioEffects: updatedEffects },
});
},
[effects, trackId, editor],
Expand All @@ -82,7 +82,7 @@ export function AudioEffectsChainPanel({ trackId, trackType, className }: AudioE
);
editor.timeline.updateTrack({
trackId,
updates: { audioEffects: updatedEffects } as any,
updates: { audioEffects: updatedEffects },
});
},
[effects, trackId, editor],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useEditor } from "@/hooks/use-editor";
import { cn } from "@/utils/ui";
import type { TimelineTrack, VideoTrack, AudioTrack } from "@/types/timeline";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { AudioEffectsChainPanel } from "./audio-effects-panel";

type AudioTrackish = VideoTrack | AudioTrack;

Expand Down Expand Up @@ -113,6 +119,7 @@ function TrackMixerStrip({
const pan = "pan" in track ? track.pan ?? 0 : 0;
const isMuted = track.muted;
const isSolo = track.solo ?? false;
const effectCount = track.audioEffects?.length ?? 0;

const handleVolumeChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -229,6 +236,25 @@ function TrackMixerStrip({
</span>
</div>
)}

<Popover>
<PopoverTrigger asChild>
<button
type="button"
className={cn(
"w-full rounded text-[8px] font-medium py-0.5 border",
effectCount > 0
? "border-primary/50 text-primary bg-primary/10"
: "border-border text-muted-foreground",
)}
>
FX{effectCount > 0 ? ` (${effectCount})` : ""}
</button>
</PopoverTrigger>
<PopoverContent side="top" align="center" className="w-56 p-2">
<AudioEffectsChainPanel trackId={track.id} trackType={track.type} />
</PopoverContent>
</Popover>
</div>
);
}
Expand Down
47 changes: 46 additions & 1 deletion apps/web/src/core/managers/audio-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { EditorCore } from "@/core";
import type { AudioClipSource } from "@/lib/media/audio";
import { createAudioContext, collectAudioClips } from "@/lib/media/audio";
import {
getAudioEffectDefinition,
type TrackAudioEffect,
} from "@/lib/audio/audio-effects";
import {
ALL_FORMATS,
AudioBufferSink,
Expand Down Expand Up @@ -154,6 +158,9 @@ export class AudioManager {
(track && ("volume" in track ? track.volume : undefined)) ?? 1;
const trackPan =
(track && ("pan" in track ? track.pan : undefined)) ?? 0;
const audioEffects =
(track && "audioEffects" in track ? track.audioEffects : undefined) ??
[];

const gain = ctx.createGain();
gain.gain.value = trackVolume;
Expand All @@ -165,7 +172,12 @@ export class AudioManager {
analyser.fftSize = 256;
analyser.smoothingTimeConstant = 0.8;

gain.connect(panner);
this.connectAudioEffectsChain({
ctx,
input: gain,
output: panner,
effects: audioEffects,
});
panner.connect(analyser);
analyser.connect(this.masterGain);

Expand All @@ -174,6 +186,39 @@ export class AudioManager {
return nodes;
}

// wires enabled track audio effects (EQ, compressor, reverb, etc.) in
// series between the track gain and panner nodes
private connectAudioEffectsChain({
ctx,
input,
output,
effects,
}: {
ctx: AudioContext;
input: AudioNode;
output: AudioNode;
effects: TrackAudioEffect[];
}): void {
const enabledEffects = effects.filter((effect) => effect.enabled);
if (enabledEffects.length === 0) {
input.connect(output);
return;
}

let previousOutput: AudioNode = input;
for (const effect of enabledEffects) {
const definition = getAudioEffectDefinition(effect.type);
if (!definition) continue;

const nodes = definition.createNodes(ctx, effect.params);
if (nodes.length === 0) continue;

previousOutput.connect(nodes[0]);
previousOutput = nodes[nodes.length - 1];
}
previousOutput.connect(output);
}

private rebuildTrackNodes(): void {
for (const [, nodes] of this.trackNodes) {
try {
Expand Down
Loading