Skip to content
43 changes: 39 additions & 4 deletions docs/lora.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The pipelines in Scope support using one or multiple LoRAs to customize concepts
- The `memflow` pipeline is compatible with Wan2.1-T2V-1.3B LoRAs.
- The `krea-realtime-video` pipeline is compatible with Wan2.1-T2V-14B LoRAs.

## Downloading LoRAs
## Installing LoRAs

Scope supports using LoRAs that can be downloaded from popular hubs such as [HuggingFace](https://huggingface.co/) or [CivitAI](https://civitai.com/).

Expand All @@ -25,7 +25,42 @@ A few LoRAs that you can start with for `krea-realtime-video`:
- [Film Noir](https://huggingface.co/Remade-AI/Film-Noir)
- [Pixar](https://huggingface.co/Remade-AI/Pixar)

### Local
### Using the Settings Dialog

The easiest way to install LoRAs is through the Settings dialog:

1. Click the **Settings** icon (gear) in the header
2. Select the **LoRAs** tab
3. Paste a LoRA URL from HuggingFace or CivitAI into the input field
4. Click **Install**

The LoRA will be downloaded and saved to your LoRA directory automatically. Once installed, you can select it from the LoRA Adapters section in the Settings panel.

#### CivitAI API Token

CivitAI requires an API token for programmatic downloads. You can configure this in one of two ways:

**Option 1: Settings Dialog**

1. Click the **Settings** icon (gear) in the header
2. Select the **API Keys** tab
3. Enter your CivitAI API token and click **Save**

**Option 2: Environment Variable**

```bash
export CIVITAI_API_TOKEN=your_civitai_token_here
```

> **Note:** The environment variable takes precedence over a token stored through the UI.

Get your API key at [civitai.com/user/account](https://civitai.com/user/account).

### Manual Installation

For manual installation, follow the steps below.

#### Local

If you are running Scope locally you can simply download the LoRA files to your computer and move them to the proper directory.

Expand All @@ -41,9 +76,9 @@ Click the download button and move the file to the `~/.daydream-scope/models/lor

Click the download button and move the file to the `~/.daydream-scope/models/lora` folder.

### Cloud
#### Cloud

If you are running the Scope server on a remote machine in the cloud, then we recommend you progamatically download the LoRA files to the remote machine.
If you are running the Scope server on a remote machine in the cloud, then we recommend you programmatically download the LoRA files to the remote machine.

**HuggingFace**

Expand Down
11 changes: 6 additions & 5 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,12 @@ Options:

### Environment Variables

| Variable | Description |
| ----------------- | ------------------------------------------------------------- |
| `PIPELINE` | Default pipeline to pre-warm on startup |
| `HF_TOKEN` | Hugging Face token for downloading models and Cloudflare TURN |
| `VERBOSE_LOGGING` | Enable verbose logging for debugging |
| Variable | Description |
| -------------------- | ------------------------------------------------------------- |
| `PIPELINE` | Default pipeline to pre-warm on startup |
| `HF_TOKEN` | Hugging Face token for downloading models and Cloudflare TURN |
| `CIVITAI_API_TOKEN` | CivitAI API token for downloading LoRAs from CivitAI |
| `VERBOSE_LOGGING` | Enable verbose logging for debugging |

### Available Pipelines

Expand Down
11 changes: 7 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { StreamPage } from "./pages/StreamPage";
import { Toaster } from "./components/ui/sonner";
import { PipelinesProvider } from "./contexts/PipelinesContext";
import { LoRAsProvider } from "./contexts/LoRAsContext";
import { CloudProvider } from "./lib/cloudContext";
import { CloudStatusProvider } from "./hooks/useCloudStatus";
import { handleOAuthCallback, initElectronAuthListener } from "./lib/auth";
Expand Down Expand Up @@ -95,10 +96,12 @@ function App() {
return (
<CloudStatusProvider>
<PipelinesProvider>
<CloudProvider wsUrl={CLOUD_WS_URL} apiKey={CLOUD_KEY}>
<StreamPage />
</CloudProvider>
<Toaster />
<LoRAsProvider>
<CloudProvider wsUrl={CLOUD_WS_URL} apiKey={CLOUD_KEY}>
<StreamPage />
</CloudProvider>
<Toaster />
</LoRAsProvider>
</PipelinesProvider>
</CloudStatusProvider>
);
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/ComplexFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface SchemaComplexFieldContext {
value: unknown,
isRuntimeParam?: boolean
) => void;
onOpenLoRAsSettings?: () => void;
}

export interface SchemaComplexFieldProps {
Expand Down Expand Up @@ -210,7 +211,7 @@ export function SchemaComplexField({
);
}

if (component === "lora" && !rendered.has("lora") && !ctx.isCloudMode) {
if (component === "lora" && !rendered.has("lora")) {
rendered.add("lora");
return (
<div key="lora" className="space-y-4">
Expand All @@ -220,6 +221,7 @@ export function SchemaComplexField({
disabled={ctx.isLoading ?? false}
isStreaming={ctx.isStreaming ?? false}
loraMergeStrategy={ctx.loraMergeStrategy ?? "permanent_merge"}
onOpenLoRAsSettings={ctx.onOpenLoRAsSettings}
/>
</div>
);
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function Header({
}: HeaderProps) {
const [settingsOpen, setSettingsOpen] = useState(false);
const [initialTab, setInitialTab] = useState<
"general" | "account" | "api-keys" | "plugins"
"general" | "account" | "api-keys" | "plugins" | "loras"
>("general");
const [initialPluginPath, setInitialPluginPath] = useState("");

Expand Down Expand Up @@ -83,7 +83,12 @@ export function Header({
useEffect(() => {
if (openSettingsTab) {
setInitialTab(
openSettingsTab as "general" | "account" | "api-keys" | "plugins"
openSettingsTab as
| "general"
| "account"
| "api-keys"
| "plugins"
| "loras"
);
setSettingsOpen(true);
onSettingsTabOpened?.();
Expand Down
105 changes: 54 additions & 51 deletions frontend/src/components/LoRAManager.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { Button } from "./ui/button";
import { SliderWithInput } from "./ui/slider-with-input";
import {
Expand All @@ -8,11 +8,12 @@ import {
SelectTrigger,
SelectValue,
} from "./ui/select";
import { Plus, X, RefreshCw } from "lucide-react";
import { Plus, X } from "lucide-react";
import { LabelWithTooltip } from "./ui/label-with-tooltip";
import { PARAMETER_METADATA } from "../data/parameterMetadata";
import type { LoRAConfig, LoraMergeStrategy } from "../types";
import { listLoRAFiles, type LoRAFileInfo } from "../lib/api";
import { useLoRAsContext } from "../contexts/LoRAsContext";
import { useCloudStatus } from "../hooks/useCloudStatus";
import { FilePicker } from "./ui/file-picker";

interface LoRAManagerProps {
Expand All @@ -21,6 +22,7 @@ interface LoRAManagerProps {
disabled?: boolean;
isStreaming?: boolean;
loraMergeStrategy?: LoraMergeStrategy;
onOpenLoRAsSettings?: () => void;
}

export function LoRAManager({
Expand All @@ -29,27 +31,12 @@ export function LoRAManager({
disabled,
isStreaming = false,
loraMergeStrategy = "permanent_merge",
onOpenLoRAsSettings,
}: LoRAManagerProps) {
const [availableLoRAs, setAvailableLoRAs] = useState<LoRAFileInfo[]>([]);
const [isLoadingLoRAs, setIsLoadingLoRAs] = useState(false);
const { loraFiles: availableLoRAs } = useLoRAsContext();
const { isConnected: isCloudConnected } = useCloudStatus();
const [localScales, setLocalScales] = useState<Record<string, number>>({});

const loadAvailableLoRAs = async () => {
setIsLoadingLoRAs(true);
try {
const response = await listLoRAFiles();
setAvailableLoRAs(response.lora_files);
} catch (error) {
console.error("loadAvailableLoRAs: Failed to load LoRA files:", error);
} finally {
setIsLoadingLoRAs(false);
}
};

useEffect(() => {
loadAvailableLoRAs();
}, []);

// Sync localScales from loras prop when it changes from outside
useEffect(() => {
const newLocalScales: Record<string, number> = {};
Expand All @@ -59,6 +46,24 @@ export function LoRAManager({
setLocalScales(newLocalScales);
}, [loras]);

// Track cloud connection state and clear configured LoRAs when it changes
// (switching between local/cloud means different LoRA file lists)
const prevCloudConnectedRef = useRef<boolean | null>(null);

useEffect(() => {
if (prevCloudConnectedRef.current === null) {
prevCloudConnectedRef.current = isCloudConnected;
return;
}

// Clear configured LoRAs when cloud connection state changes
if (prevCloudConnectedRef.current !== isCloudConnected) {
onLorasChange([]);
}

prevCloudConnectedRef.current = isCloudConnected;
}, [isCloudConnected, onLorasChange]);

const handleAddLora = () => {
const newLora: LoRAConfig = {
id: crypto.randomUUID(),
Expand Down Expand Up @@ -103,46 +108,44 @@ export function LoRAManager({
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">LoRA Adapters</h3>
<div className="flex gap-1">
<Button
size="sm"
variant="outline"
onClick={loadAvailableLoRAs}
disabled={disabled || isLoadingLoRAs}
className="h-6 px-2"
title="Refresh LoRA list"
>
<RefreshCw
className={`h-3 w-3 ${isLoadingLoRAs ? "animate-spin" : ""}`}
/>
</Button>
<Button
size="sm"
variant="outline"
onClick={handleAddLora}
disabled={disabled || isStreaming}
className="h-6 px-2"
title={
isStreaming ? "Cannot add LoRAs while streaming" : "Add LoRA"
}
>
<Plus className="h-3 w-3" />
</Button>
</div>
<Button
size="sm"
variant="outline"
onClick={handleAddLora}
disabled={disabled || isStreaming}
className="h-6 px-2"
title={isStreaming ? "Cannot add LoRAs while streaming" : "Add LoRA"}
>
<Plus className="h-3 w-3" />
</Button>
</div>

{loras.length === 0 && (
<p className="text-xs text-muted-foreground">
No LoRA adapters configured. Follow the{" "}
No LoRA adapters configured.{" "}
{onOpenLoRAsSettings ? (
<>
<button
type="button"
onClick={onOpenLoRAsSettings}
className="underline hover:text-foreground"
>
Click here
</button>{" "}
to install LoRAs or follow the{" "}
</>
) : (
"Follow the "
)}
<a
href="https://github.com/daydreamlive/scope/blob/main/docs/lora.md"
href="https://docs.daydream.live/scope/guides/loras"
target="_blank"
rel="noopener noreferrer"
className="underline"
className="underline hover:text-foreground"
>
docs
</a>{" "}
to add LoRA files.
for manual installation.
</p>
)}

Expand Down
Loading
Loading