Skip to content

Commit

Permalink
Added option to output the generated images to the current not (if po…
Browse files Browse the repository at this point in the history
…ssible). Closes #7
  • Loading branch information
dsebastien committed Aug 19, 2024
1 parent 9847902 commit 44f1592
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 40 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ Obsidian plugin that integrates [Replicate.com](https://replicate.com/).

Future: text generation.

## Commands

- Generate image(s) using Replicate.com: Generate images using Replicate.com. This first shows a modal dialog where the prompt can be specified

## Configuration

### General

- Replicate.com API Key: the Replicate.com API key to use
- Copy output to clipboard: if you want the generated output to be automatically copied to the clipboard
- Append output to current note: append the generated output to the current note (if possible)

### Image generation model

Expand All @@ -37,6 +42,12 @@ You can find the existing versions here using the method described here: https:/

A JSON object to pass as input to the image generation model. This varies depending on the chosen model and is documented on Replicate's website

## Tips and tricks

In the image generation modal shown after launching the "Generate image(s) using Replicate.com" command, you can use the following keyboard shortcuts:

- `Ctrl/Cmd+Enter` to generate the image(s)

## News & support

To stay up to date about this plugin, Obsidian in general, Personal Knowledge Management and note-taking, subscribe to [my newsletter](https://dsebastien.net). Note that the best way to support my work is to become a paid subscriber ❤️.
14 changes: 12 additions & 2 deletions apps/plugin/src/app/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export class ReplicatePlugin extends Plugin {
}

new PromptModal(this.app, async (prompt) => {
new Notice(`Prompt: ${prompt}`);
await generateImages(prompt, this.settings);
await generateImages(prompt, this.settings, this.app);
}).open();
},
});
Expand Down Expand Up @@ -88,6 +87,17 @@ export class ReplicatePlugin extends Plugin {
needToSaveSettings = true;
}

if (loadedSettings.appendOutputToCurrentNote) {
draft.appendOutputToCurrentNote =
loadedSettings.appendOutputToCurrentNote;
} else {
log(
'The loaded settings miss the [appendOutputToCurrentNote] property',
'debug'
);
needToSaveSettings = true;
}

if (loadedSettings.imageGenerationModel) {
draft.imageGenerationModel = loadedSettings.imageGenerationModel;
} else {
Expand Down
107 changes: 77 additions & 30 deletions apps/plugin/src/app/settingTab/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
App,
debounce,
Notice,
PluginSettingTab,
Setting,
Expand Down Expand Up @@ -32,6 +33,7 @@ export class SettingsTab extends PluginSettingTab {

this.renderApiKey(containerEl);
this.renderCopyOutputToClipboard(containerEl);
this.renderAppendOutputToCurrentNote(containerEl);

const imageGenerationSettingsGroup = new Setting(containerEl);
imageGenerationSettingsGroup.setName('Image Generation');
Expand Down Expand Up @@ -84,6 +86,26 @@ export class SettingsTab extends PluginSettingTab {
});
}

renderAppendOutputToCurrentNote(containerEl: HTMLElement) {
new Setting(containerEl)
.setName('Append the generated output to the current note')
.setDesc(
'If enabled, the generated output will be appended to the current note (if possibvle).'
)
.addToggle((toggle: ToggleComponent) => {
toggle.setValue(this.plugin.settings.appendOutputToCurrentNote);
toggle.onChange(async (newValue: boolean) => {
this.plugin.settings = produce(
this.plugin.settings,
(draft: Draft<PluginSettings>) => {
draft.appendOutputToCurrentNote = newValue;
}
);
await this.plugin.saveSettings();
});
});
}

renderImageGenerationModel(containerEl: HTMLElement) {
new Setting(containerEl)
.setName('Image generation model')
Expand All @@ -109,41 +131,66 @@ export class SettingsTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Image generation model configuration')
.setDesc('The image generation model configuration.')
.setClass('replicate-plugin-setting-image-generation-model-configuration')
.addTextArea((text) => {
text
.setPlaceholder('Valid JSON object')
.setValue(
JSON.stringify(this.plugin.settings.imageGenerationConfiguration)
// Format the JSON nicely
JSON.stringify(
this.plugin.settings.imageGenerationConfiguration,
null,
2
)
)
.onChange(async (newValue) => {
log(
`Image generation model configuration set to: `,
'debug',
newValue
);
let imageGenerationModelConfiguration: object = {};
try {
imageGenerationModelConfiguration = JSON.parse(newValue);
} catch (error) {
log(
'Invalid JSON for image generation model configuration',
'warn',
error
);
new Notice(
'The Replicate.com image generation model configuration is not a valid JSON object. Please correct it.',
NOTICE_TIMEOUT
);
}
this.plugin.settings = produce(
this.plugin.settings,
(draft: Draft<PluginSettings>) => {
draft.imageGenerationConfiguration =
imageGenerationModelConfiguration;
}
);
await this.plugin.saveSettings();
});
// Debounce the change event to avoid saving after each keystroke
.onChange(
debounce(
async (newValue) => {
log(
`Image generation model configuration set to: `,
'debug',
newValue
);

let imageGenerationModelConfiguration: object = {};

if ('' === newValue.trim()) {
text.setValue(
JSON.stringify(imageGenerationModelConfiguration, null, 2)
);
} else {
try {
imageGenerationModelConfiguration = JSON.parse(newValue);
} catch (error) {
log(
'Invalid JSON for image generation model configuration',
'warn',
error
);
new Notice(
'The Replicate.com image generation model configuration is not a valid JSON object. Please correct it.',
NOTICE_TIMEOUT
);
// TODO improve error handling here when the JSON is invalid
imageGenerationModelConfiguration =
newValue as unknown as object;
}
}

this.plugin.settings = produce(
this.plugin.settings,
(draft: Draft<PluginSettings>) => {
draft.imageGenerationConfiguration =
imageGenerationModelConfiguration;
}
);
await this.plugin.saveSettings();
},
500,
true
)
);
});
}

Expand Down
4 changes: 3 additions & 1 deletion apps/plugin/src/app/types/plugin-settings.intf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface PluginSettings {
// General
apiKey: string;
copyOutputToClipboard: boolean;
appendOutputToCurrentNote: boolean;

// Image Generation
imageGenerationModel: `${string}/${string}` | `${string}/${string}:${string}`;
Expand All @@ -11,7 +12,8 @@ export interface PluginSettings {
export const DEFAULT_SETTINGS: PluginSettings = {
// General
apiKey: '',
copyOutputToClipboard: true,
copyOutputToClipboard: false,
appendOutputToCurrentNote: true,

// Image Generation model
// Form 1: <model_owner>/<model_name>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Prediction } from 'replicate';

export interface ReplicateRunModelConfiguration {
input: object;
wait?: {
interval?: number;
};
webhook?: string;
signal?: AbortSignal;
progress?: (prediction: Prediction) => void;
}
51 changes: 44 additions & 7 deletions apps/plugin/src/app/utils/generate-images.fn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PluginSettings } from '../types/plugin-settings.intf';
import { log } from './log';
import { Notice } from 'obsidian';
import { App, Notice } from 'obsidian';
import {
MSG_API_KEY_CONFIGURATION_REQUIRED,
MSG_IMAGE_GENERATION_ERROR,
Expand All @@ -12,9 +12,16 @@ import { isImageGenerationModelConfigured } from './is-image-generation-model-co
import { getReplicateClient } from './get-replicate-client.fn';
import { ReplicateRunModelConfiguration } from '../types/replicate-run-model-configuration.intf';

/**
* Generate images using Replicate.com
* @param prompt - The prompt to use
* @param settings - The plugin settings
* @param app - The Obsidian app
*/
export const generateImages = async (
prompt: string | undefined,
settings: PluginSettings
settings: PluginSettings,
app: App
): Promise<void> => {
if (!isApiKeyConfigured(settings)) {
log(
Expand Down Expand Up @@ -51,6 +58,9 @@ export const generateImages = async (
...settings.imageGenerationConfiguration,
prompt,
},
progress: (prediction) => {
log('Image generation progress: ', 'debug', prediction);
},
};

try {
Expand All @@ -76,23 +86,50 @@ export const generateImages = async (
if (Array.isArray(output)) {
result = output.join('\n');
} else {
result = JSON.stringify(output); // FIXME is this ok?
result = JSON.stringify(output);
}

log('Image generation result: ', 'debug', result);
new Notice(
`Successfully generated image(s) using Replicate.com: [${result}]`,
NOTICE_TIMEOUT
);

if (settings.copyOutputToClipboard) {
log('Copying the output to the clipboard', 'debug');
try {
await navigator.clipboard.writeText(result);
} catch (_) {
// Ignore errors (can occur if DevTools are open)
}
}

new Notice(
`Successfully generated image(s) using Replicate.com: [${result}]`,
NOTICE_TIMEOUT
);
if (settings.appendOutputToCurrentNote) {
log('Trying to append the output to the current note', 'debug');

if (!app.workspace.activeEditor || !app.workspace.activeEditor.editor) {
log('No active editor found to append the output to', 'warn');
return;
}

const activeEditor = app.workspace.activeEditor.editor;
const cursor = activeEditor.getCursor();

let textToAppend = `Prompt: ${prompt}\nImages generated using Replicate.com:\n\n`;
if (Array.isArray(output)) {
for (const line of output) {
textToAppend += `![](${line})\n`;
}
} else {
textToAppend += `![](${output})\n`;
}

log(
'Editor found. Appending the output to the current cursor position',
'debug'
);
activeEditor.replaceRange(textToAppend, cursor);
}
} catch (error) {
log('Error while generating image(s) using Replicate.com', 'warn', error);
new Notice(`${MSG_IMAGE_GENERATION_ERROR}: [${error}]`, NOTICE_TIMEOUT);
Expand Down

0 comments on commit 44f1592

Please sign in to comment.