From a2f7239fb5bcd98bf05313420302b39ea2673621 Mon Sep 17 00:00:00 2001 From: pixelass Date: Wed, 21 Feb 2024 13:51:44 +0100 Subject: [PATCH 1/6] feat: add basic painting area. --- src/client/organisms/layout/index.tsx | 4 +- src/client/pages/[locale]/live-painting.tsx | 203 ++++++++++++++++++++ src/client/public/locales/de/common.json | 4 +- src/client/public/locales/de/labels.json | 1 + src/client/public/locales/en/common.json | 4 +- src/client/public/locales/en/labels.json | 1 + src/client/public/locales/es/common.json | 4 +- src/client/public/locales/es/labels.json | 1 + src/client/public/locales/fr/common.json | 4 +- src/client/public/locales/fr/labels.json | 1 + src/client/public/locales/he/common.json | 4 +- src/client/public/locales/he/labels.json | 1 + src/client/public/locales/it/common.json | 4 +- src/client/public/locales/it/labels.json | 1 + src/client/public/locales/ja/common.json | 4 +- src/client/public/locales/ja/labels.json | 1 + src/client/public/locales/nl/common.json | 4 +- src/client/public/locales/nl/labels.json | 1 + src/client/public/locales/pl/common.json | 4 +- src/client/public/locales/pl/labels.json | 1 + src/client/public/locales/pt/common.json | 4 +- src/client/public/locales/pt/labels.json | 1 + src/client/public/locales/ru/common.json | 4 +- src/client/public/locales/ru/labels.json | 1 + src/client/public/locales/zh/common.json | 4 +- src/client/public/locales/zh/labels.json | 1 + 26 files changed, 241 insertions(+), 26 deletions(-) create mode 100644 src/client/pages/[locale]/live-painting.tsx diff --git a/src/client/organisms/layout/index.tsx b/src/client/organisms/layout/index.tsx index 151a16dcc..07b1da84a 100644 --- a/src/client/organisms/layout/index.tsx +++ b/src/client/organisms/layout/index.tsx @@ -50,8 +50,8 @@ export function Layout({ children }: { children?: ReactNode }) { }> {t("common:training")} - }> - {t("common:livePainting")} + }> + {t("labels:livePainting")} (null); + const context = useRef(null); + const isDrawing = useRef(false); + const [, setImage] = useAtom(imageAtom); + + function startDrawing(event: ReactPointerEvent) { + if (!canvas.current) { + return; + } + + isDrawing.current = true; + const rect = canvas.current.getBoundingClientRect(); + context.current?.beginPath(); + context.current?.moveTo(event.clientX - rect.left, event.clientY - rect.top); + } + + useEffect(() => { + const canvasElement = canvas.current; + let animationFame: number; + if (!canvasElement) { + return; + } + + canvasElement.height = 512; + canvasElement.width = 512; + context.current = canvasElement.getContext("2d"); + + function draw(event: MouseEvent) { + if (!isDrawing.current || !canvas.current) { + return; + } + + const rect = canvas.current.getBoundingClientRect(); + context.current?.lineTo(event.clientX - rect.left, event.clientY - rect.top); + context.current?.stroke(); + const dataUrl = canvas.current.toDataURL(); + setImage(dataUrl); + } + + function handleMouseUp() { + if (isDrawing.current) { + context.current?.closePath(); + isDrawing.current = false; + } + + cancelAnimationFrame(animationFame); + } + + function handleMouseMove(event: MouseEvent) { + animationFame = requestAnimationFrame(() => { + draw(event); + }); + } + + if (context.current) { + context.current.fillStyle = "#ffffff"; + context.current.rect(0, 0, canvasElement.width, canvasElement.height); + context.current.fill(); + context.current.strokeStyle = "#000000"; + context.current.lineWidth = 5; + context.current.lineJoin = "round"; + context.current.lineCap = "round"; + + document.addEventListener("mousemove", handleMouseMove, { passive: true }); + document.addEventListener("mouseup", handleMouseUp); + } + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + cancelAnimationFrame(animationFame); + }; + }, [setImage]); + + return ( + + + + ); +} + +function RenderingArea() { + const [image] = useAtom(imageAtom); + return ( + + + + ); +} + +export function LivePainting() { + const [value, setValue] = useState("side-by-side"); + const isOverlay = value === "overlay"; + return ( + + + { + if (newValue) { + setValue(newValue); + } + }} + > + + + + + + + + + + + + + + ); +} + +export default function Page(_properties: InferGetStaticPropsType) { + const { t } = useTranslation(["common", "labels"]); + return ( + <> + + {`Captain | ${t("labels:livePainting")}`} + + + + + + {t("labels:livePainting")} + + + + + + + + ); +} + +export const getStaticProps = makeStaticProperties(["common", "texts", "labels"]); + +export { getStaticPaths } from "@/ions/i18n/get-static"; diff --git a/src/client/public/locales/de/common.json b/src/client/public/locales/de/common.json index 4aff4bf89..772d69a48 100644 --- a/src/client/public/locales/de/common.json +++ b/src/client/public/locales/de/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Suchen & Ersetzen", "seeAll": "Alle ansehen", "selectAll": "Alle auswählen", - "selected": "Ausgewählt", "selectFolder": "Ordner auswählen", "selectImages": "Bilder auswählen", + "selected": "Ausgewählt", "send": "Senden", - "settings": "Einstellungen", "settingUpApp": "Die App wird eingerichtet. Dies kann je nach Server und Internetgeschwindigkeit eine Weile dauern.", + "settings": "Einstellungen", "show": "anzeigen", "submit": "Einreichen", "suffix": "Suffix", diff --git a/src/client/public/locales/de/labels.json b/src/client/public/locales/de/labels.json index 5f5a6d559..9cd71b33b 100644 --- a/src/client/public/locales/de/labels.json +++ b/src/client/public/locales/de/labels.json @@ -4,6 +4,7 @@ "close": "Schließen", "dashboard": "Dashboard", "downloading": "Herunterladen", + "livePainting": "Live-Malerei", "maximize": "Maximieren", "minimize": "Minimieren", "privacy": "Datenschutz", diff --git a/src/client/public/locales/en/common.json b/src/client/public/locales/en/common.json index 230b731a7..2eb06f95a 100644 --- a/src/client/public/locales/en/common.json +++ b/src/client/public/locales/en/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Search & Replace", "seeAll": "See all", "selectAll": "Select All", - "selected": "Selected", "selectFolder": "select folder", "selectImages": "Select Images", + "selected": "Selected", "send": "Send", - "settings": "Settings", "settingUpApp": "Setting up the app. This might take a while depending on server and internet speed.", + "settings": "Settings", "show": "show", "submit": "Submit", "suffix": "Suffix", diff --git a/src/client/public/locales/en/labels.json b/src/client/public/locales/en/labels.json index 0bf72ac36..256a700a7 100644 --- a/src/client/public/locales/en/labels.json +++ b/src/client/public/locales/en/labels.json @@ -4,6 +4,7 @@ "close": "Close", "dashboard": "Dashboard", "downloading": "Downloading", + "livePainting": "Live Painting", "maximize": "Maximize", "minimize": "Minimize", "privacy": "Privacy", diff --git a/src/client/public/locales/es/common.json b/src/client/public/locales/es/common.json index b2b961445..23d7222f1 100644 --- a/src/client/public/locales/es/common.json +++ b/src/client/public/locales/es/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Buscar y reemplazar", "seeAll": "Ver todos", "selectAll": "Seleccionar todo", - "selected": "Seleccionadas", "selectFolder": "seleccionar carpeta", "selectImages": "Seleccionar imágenes", + "selected": "Seleccionadas", "send": "Enviar", - "settings": "Configuración", "settingUpApp": "Configurando la aplicación. Esto puede tardar un tiempo dependiendo del servidor y la velocidad de internet.", + "settings": "Configuración", "show": "mostrar", "submit": "Enviar", "suffix": "Sufijo", diff --git a/src/client/public/locales/es/labels.json b/src/client/public/locales/es/labels.json index 46ae12aeb..9eb82c2e4 100644 --- a/src/client/public/locales/es/labels.json +++ b/src/client/public/locales/es/labels.json @@ -4,6 +4,7 @@ "close": "Cerrar", "dashboard": "Tablero", "downloading": "Descargando", + "livePainting": "Pintura en Vivo", "maximize": "Maximizar", "minimize": "Minimizar", "privacy": "Privacidad", diff --git a/src/client/public/locales/fr/common.json b/src/client/public/locales/fr/common.json index b6029f8f3..7d924cf78 100644 --- a/src/client/public/locales/fr/common.json +++ b/src/client/public/locales/fr/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Rechercher et remplacer", "seeAll": "Voir tout", "selectAll": "Tout sélectionner", - "selected": "Sélectionnés", "selectFolder": "sélectionner un dossier", "selectImages": "Sélectionner des images", + "selected": "Sélectionnés", "send": "Envoyer", - "settings": "Paramètres", "settingUpApp": "Configuration de l'application. Cela peut prendre un certain temps en fonction du serveur et de la vitesse d'internet.", + "settings": "Paramètres", "show": "afficher", "submit": "Soumettre", "suffix": "Suffixe", diff --git a/src/client/public/locales/fr/labels.json b/src/client/public/locales/fr/labels.json index e6517d42f..4b4d0dc65 100644 --- a/src/client/public/locales/fr/labels.json +++ b/src/client/public/locales/fr/labels.json @@ -4,6 +4,7 @@ "close": "Fermer", "dashboard": "Tableau de bord", "downloading": "Téléchargement", + "livePainting": "Peinture en Direct", "maximize": "Maximiser", "minimize": "Minimiser", "privacy": "Confidentialité", diff --git a/src/client/public/locales/he/common.json b/src/client/public/locales/he/common.json index d330cc5bc..7e0432bb7 100644 --- a/src/client/public/locales/he/common.json +++ b/src/client/public/locales/he/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "חפש והחלף", "seeAll": "ראה הכל", "selectAll": "בחר הכל", - "selected": "נבחרים", "selectFolder": "בחר תיקייה", "selectImages": "בחר תמונות", + "selected": "נבחרים", "send": "שלח", - "settings": "הגדרות", "settingUpApp": "מכינים את האפליקציה. זה עשוי לקחת זמן בהתאם למהירות האינטרנט והשרת.", + "settings": "הגדרות", "show": "הצג", "submit": "שלח", "suffix": "סיומת", diff --git a/src/client/public/locales/he/labels.json b/src/client/public/locales/he/labels.json index 4429fbeba..13f3df05b 100644 --- a/src/client/public/locales/he/labels.json +++ b/src/client/public/locales/he/labels.json @@ -4,6 +4,7 @@ "close": "סגור", "dashboard": "לוח בקרה", "downloading": "מוריד", + "livePainting": "ציור חי", "maximize": "הגדל", "minimize": "מזער", "privacy": "פרטיות", diff --git a/src/client/public/locales/it/common.json b/src/client/public/locales/it/common.json index 1d342af95..b00ef422c 100644 --- a/src/client/public/locales/it/common.json +++ b/src/client/public/locales/it/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Cerca e sostituisci", "seeAll": "Vedi tutto", "selectAll": "Seleziona tutto", - "selected": "Selezionate", "selectFolder": "seleziona cartella", "selectImages": "Seleziona immagini", + "selected": "Selezionate", "send": "Inviare", - "settings": "Impostazioni", "settingUpApp": "Configurazione dell'app in corso. Potrebbe richiedere del tempo a seconda del server e della velocità di internet.", + "settings": "Impostazioni", "show": "mostra", "submit": "Invia", "suffix": "Suffisso", diff --git a/src/client/public/locales/it/labels.json b/src/client/public/locales/it/labels.json index 16d61ea65..1fc03f8bb 100644 --- a/src/client/public/locales/it/labels.json +++ b/src/client/public/locales/it/labels.json @@ -4,6 +4,7 @@ "close": "Chiudi", "dashboard": "Cruscotto", "downloading": "Scaricamento", + "livePainting": "Pittura Dal Vivo", "maximize": "Massimizza", "minimize": "Minimizza", "privacy": "Privacy", diff --git a/src/client/public/locales/ja/common.json b/src/client/public/locales/ja/common.json index 325724e91..7c29d2336 100644 --- a/src/client/public/locales/ja/common.json +++ b/src/client/public/locales/ja/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "検索 & 置換", "seeAll": "すべてを見る", "selectAll": "すべて選択", - "selected": "選択済み", "selectFolder": "フォルダを選択", "selectImages": "画像を選択", + "selected": "選択済み", "send": "送信する", - "settings": "設定", "settingUpApp": "アプリを設定しています。サーバーとインターネットの速度によっては、時間がかかる場合があります。", + "settings": "設定", "show": "表示", "submit": "提出", "suffix": "接尾辞", diff --git a/src/client/public/locales/ja/labels.json b/src/client/public/locales/ja/labels.json index 42b38325d..2e057b922 100644 --- a/src/client/public/locales/ja/labels.json +++ b/src/client/public/locales/ja/labels.json @@ -4,6 +4,7 @@ "close": "閉じる", "dashboard": "ダッシュボード", "downloading": "ダウンロード中", + "livePainting": "ライブペインティング", "maximize": "最大化", "minimize": "最小化", "privacy": "プライバシー", diff --git a/src/client/public/locales/nl/common.json b/src/client/public/locales/nl/common.json index 283beab32..148078f67 100644 --- a/src/client/public/locales/nl/common.json +++ b/src/client/public/locales/nl/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Zoeken & vervangen", "seeAll": "Bekijk alles", "selectAll": "Alles selecteren", - "selected": "Geselecteerd", "selectFolder": "map selecteren", "selectImages": "Selecteer afbeeldingen", + "selected": "Geselecteerd", "send": "Verzenden", - "settings": "Instellingen", "settingUpApp": "De app wordt ingesteld. Dit kan afhankelijk van de server en internetsnelheid enige tijd duren.", + "settings": "Instellingen", "show": "tonen", "submit": "Verzenden", "suffix": "Achtervoegsel", diff --git a/src/client/public/locales/nl/labels.json b/src/client/public/locales/nl/labels.json index 89b2d02b5..1d0f78c82 100644 --- a/src/client/public/locales/nl/labels.json +++ b/src/client/public/locales/nl/labels.json @@ -4,6 +4,7 @@ "close": "Sluiten", "dashboard": "Dashboard", "downloading": "Downloaden", + "livePainting": "Live Schilderen", "maximize": "Maximaliseren", "minimize": "Minimaliseren", "privacy": "Privacy", diff --git a/src/client/public/locales/pl/common.json b/src/client/public/locales/pl/common.json index b0e8232df..4502e9dbc 100644 --- a/src/client/public/locales/pl/common.json +++ b/src/client/public/locales/pl/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Szukaj i zamień", "seeAll": "Zobacz wszystkie", "selectAll": "Zaznacz wszystko", - "selected": "Wybrane", "selectFolder": "wybierz folder", "selectImages": "Wybierz obrazy", + "selected": "Wybrane", "send": "Wyślij", - "settings": "Ustawienia", "settingUpApp": "Konfiguracja aplikacji. Może to zająć trochę czasu, w zależności od serwera i prędkości internetu.", + "settings": "Ustawienia", "show": "pokaż", "submit": "Zatwierdź", "suffix": "Sufiks", diff --git a/src/client/public/locales/pl/labels.json b/src/client/public/locales/pl/labels.json index 788722117..46427bf5b 100644 --- a/src/client/public/locales/pl/labels.json +++ b/src/client/public/locales/pl/labels.json @@ -4,6 +4,7 @@ "close": "Zamknij", "dashboard": "Pulpit", "downloading": "Pobieranie", + "livePainting": "Malowanie na Żywo", "maximize": "Zmaksymalizuj", "minimize": "Zminimalizuj", "privacy": "Prywatność", diff --git a/src/client/public/locales/pt/common.json b/src/client/public/locales/pt/common.json index 909561917..11cbfd8c4 100644 --- a/src/client/public/locales/pt/common.json +++ b/src/client/public/locales/pt/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Pesquisar e substituir", "seeAll": "Ver todos", "selectAll": "Selecionar tudo", - "selected": "Selecionadas", "selectFolder": "selecionar pasta", "selectImages": "Selecionar imagens", + "selected": "Selecionadas", "send": "Enviar", - "settings": "Configurações", "settingUpApp": "Configurando o aplicativo. Isso pode levar um tempo dependendo do servidor e da velocidade da internet.", + "settings": "Configurações", "show": "mostrar", "submit": "Submeter", "suffix": "Sufixo", diff --git a/src/client/public/locales/pt/labels.json b/src/client/public/locales/pt/labels.json index 5ea587a04..2b01b86fd 100644 --- a/src/client/public/locales/pt/labels.json +++ b/src/client/public/locales/pt/labels.json @@ -4,6 +4,7 @@ "close": "Fechar", "dashboard": "Painel de Controle", "downloading": "Baixando", + "livePainting": "Pintura ao Vivo", "maximize": "Maximizar", "minimize": "Minimizar", "privacy": "Privacidade", diff --git a/src/client/public/locales/ru/common.json b/src/client/public/locales/ru/common.json index 2f9ad0cee..6474742d1 100644 --- a/src/client/public/locales/ru/common.json +++ b/src/client/public/locales/ru/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "Поиск и замена", "seeAll": "Смотреть все", "selectAll": "Выбрать все", - "selected": "Выбранные", "selectFolder": "выбрать папку", "selectImages": "Выбрать изображения", + "selected": "Выбранные", "send": "Отправить", - "settings": "Настройки", "settingUpApp": "Настройка приложения. Это может занять некоторое время в зависимости от сервера и скорости интернета.", + "settings": "Настройки", "show": "показать", "submit": "Отправить", "suffix": "Суффикс", diff --git a/src/client/public/locales/ru/labels.json b/src/client/public/locales/ru/labels.json index 158e57d74..3a5354bd7 100644 --- a/src/client/public/locales/ru/labels.json +++ b/src/client/public/locales/ru/labels.json @@ -4,6 +4,7 @@ "close": "Закрыть", "dashboard": "Панель управления", "downloading": "Загрузка", + "livePainting": "Живая Живопись", "maximize": "Развернуть", "minimize": "Свернуть", "privacy": "Конфиденциальность", diff --git a/src/client/public/locales/zh/common.json b/src/client/public/locales/zh/common.json index e38663015..ee0741016 100644 --- a/src/client/public/locales/zh/common.json +++ b/src/client/public/locales/zh/common.json @@ -100,12 +100,12 @@ "searchAndReplace": "搜索与替换", "seeAll": "查看全部", "selectAll": "全选", - "selected": "已选", "selectFolder": "选择文件夹", "selectImages": "选择图片", + "selected": "已选", "send": "发送", - "settings": "设置", "settingUpApp": "正在设置应用程序。这可能会根据服务器和互联网速度而需要一段时间。", + "settings": "设置", "show": "显示", "submit": "提交", "suffix": "后缀", diff --git a/src/client/public/locales/zh/labels.json b/src/client/public/locales/zh/labels.json index 25b813d3b..c8d75a16a 100644 --- a/src/client/public/locales/zh/labels.json +++ b/src/client/public/locales/zh/labels.json @@ -4,6 +4,7 @@ "close": "关闭", "dashboard": "仪表板", "downloading": "下载中", + "livePainting": "实时绘画", "maximize": "最大化", "minimize": "最小化", "privacy": "隐私", From 376c11823328d41e961f84446f0c81e00497f1de Mon Sep 17 00:00:00 2001 From: pixelass Date: Wed, 21 Feb 2024 14:57:56 +0100 Subject: [PATCH 2/6] feat: add basic ipc for live-painting --- src/client/pages/[locale]/live-painting.tsx | 20 +++++++++++++++++ src/electron/future/ipc/listeners.ts | 24 +++++++++++++++++++++ src/shared/enums.ts | 1 + 3 files changed, 45 insertions(+) diff --git a/src/client/pages/[locale]/live-painting.tsx b/src/client/pages/[locale]/live-painting.tsx index 2ef643832..bbf69ed6d 100644 --- a/src/client/pages/[locale]/live-painting.tsx +++ b/src/client/pages/[locale]/live-painting.tsx @@ -11,6 +11,8 @@ import { useTranslation } from "next-i18next"; import type { PointerEvent as ReactPointerEvent } from "react"; import { useEffect, useRef, useState } from "react"; +import { buildKey } from "#/build-key"; +import { ID } from "#/enums"; import { makeStaticProperties } from "@/ions/i18n/get-static"; export type ViewType = "side-by-side" | "overlay"; @@ -55,6 +57,7 @@ function DrawingArea() { context.current?.stroke(); const dataUrl = canvas.current.toDataURL(); setImage(dataUrl); + window.ipc.send(buildKey([ID.LIVE_PAINT], { suffix: ":dataUrl" }), dataUrl); } function handleMouseUp() { @@ -184,6 +187,23 @@ export default function Page(_properties: InferGetStaticPropsType {t("labels:livePainting")} + + + + + { ipcMain.on(buildKey([ID.USER], { suffix: ":language" }), (_event, language) => { userStore.set("language", language); }); + +let process: ExecaChildProcess; + +ipcMain.on(buildKey([ID.LIVE_PAINT], { suffix: ":dataUrl" }), (_event, dataUrl) => { + console.log("image input"); +}); + +ipcMain.on(buildKey([ID.LIVE_PAINT], { suffix: ":start" }), () => { + if (!process) { + process = execa("echo", ["hello", "world", "!"], { stdout: "inherit" }); + console.log("alive"); + } +}); + +ipcMain.on(buildKey([ID.LIVE_PAINT], { suffix: ":stop" }), () => { + if (!process.killed) { + const result = process.kill(); + console.log("killed", result); + } + + console.log(process.pid); +}); diff --git a/src/shared/enums.ts b/src/shared/enums.ts index 248cafbff..223034d49 100644 --- a/src/shared/enums.ts +++ b/src/shared/enums.ts @@ -15,6 +15,7 @@ export enum ID { STORE = "STORE", USER = "USER", WINDOW = "WINDOW", + LIVE_PAINT = "LIVE_PAINT", } /** From 12fa0c404416b96db5c2f1fb36ad51acf9311c7c Mon Sep 17 00:00:00 2001 From: Tim Pietrusky Date: Thu, 22 Feb 2024 11:21:53 +0100 Subject: [PATCH 3/6] test: added basic e2e test --- playwright/live-painting.test.ts | 25 +++++++++++++++++++++++++ src/client/organisms/layout/index.tsx | 6 +++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 playwright/live-painting.test.ts diff --git a/playwright/live-painting.test.ts b/playwright/live-painting.test.ts new file mode 100644 index 000000000..f034feecf --- /dev/null +++ b/playwright/live-painting.test.ts @@ -0,0 +1,25 @@ +import type { ElectronApplication, Page } from "@playwright/test"; +import { test, expect } from "@playwright/test"; +import { _electron as electron } from "playwright"; + +let electronApp: ElectronApplication; +let page: Page; + +test.beforeAll(async () => { + electronApp = await electron.launch({ args: ["."] }); + const isPackaged = await electronApp.evaluate(async ({ app }) => app.isPackaged); + + expect(isPackaged).toBe(false); +}); + +test.afterAll(async () => { + await electronApp.close(); +}); + +test("Open Live Painting", async () => { + page = await electronApp.firstWindow(); + + await expect(page.getByTestId("sidebar-live-painting")).toBeVisible(); + await page.getByTestId("sidebar-live-painting").click(); + await expect(page.getByText("Live Painting")).toBeVisible(); +}); diff --git a/src/client/organisms/layout/index.tsx b/src/client/organisms/layout/index.tsx index 07b1da84a..ac2920ba9 100644 --- a/src/client/organisms/layout/index.tsx +++ b/src/client/organisms/layout/index.tsx @@ -50,7 +50,11 @@ export function Layout({ children }: { children?: ReactNode }) { }> {t("common:training")} - }> + } + data-testid="sidebar-live-painting" + > {t("labels:livePainting")} From 3ac0904a292eb8f6545110385aefbcc0846b2806 Mon Sep 17 00:00:00 2001 From: Tim Pietrusky Date: Thu, 22 Feb 2024 11:22:02 +0100 Subject: [PATCH 4/6] chore: example image --- .../live-painting/test-resources/input.png | Bin 0 -> 41028 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/python/live-painting/test-resources/input.png diff --git a/resources/python/live-painting/test-resources/input.png b/resources/python/live-painting/test-resources/input.png new file mode 100644 index 0000000000000000000000000000000000000000..5f52c3a2e5928091536728bed4ac7e3fe8e4acaa GIT binary patch literal 41028 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 Date: Thu, 22 Feb 2024 11:23:49 +0100 Subject: [PATCH 5/6] feat: advanced python script lifecycle control --- resources/python/live-painting/main.py | 346 ++++++++++++-------- src/client/pages/[locale]/live-painting.tsx | 16 +- src/electron/future/ipc/listeners.ts | 125 ++++++- 3 files changed, 344 insertions(+), 143 deletions(-) diff --git a/resources/python/live-painting/main.py b/resources/python/live-painting/main.py index 990ea3e98..989ca928f 100644 --- a/resources/python/live-painting/main.py +++ b/resources/python/live-painting/main.py @@ -1,14 +1,41 @@ -from diffusers import ( - StableDiffusionImg2ImgPipeline, - AutoencoderTiny, - EulerAncestralDiscreteScheduler, -) +import io +import sys +from contextlib import contextmanager +import json + +print(json.dumps({"status": "starting"})) + + +@contextmanager +def suppress_print(debug=False): + # Hide messages printed to stdout / stderr + + if not debug: + original_stdout = sys.stdout + original_stderr = sys.stderr + sys.stdout = io.StringIO() + sys.stderr = io.StringIO() + + try: + yield + finally: + sys.stdout = original_stdout + sys.stderr = original_stderr + else: + yield + + +# Get rid of message, that Triton is not available +with suppress_print(): + from diffusers import ( + StableDiffusionImg2ImgPipeline, + AutoencoderTiny, + EulerAncestralDiscreteScheduler, + ) import torch import time from PIL import Image import queue -import sys -import json import threading import os import time @@ -16,15 +43,74 @@ import torch from diffusers.utils import load_image from sfast.compilers.diffusion_pipeline_compiler import compile, CompilationConfig +import argparse -def read_stdin_loop(): +# Torch optimizations +torch.set_grad_enabled(False) +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True + + +def parse_args(): + parser = argparse.ArgumentParser(description="Control model and paths via CLI.") + parser.add_argument( + "--model_path", + type=str, + help="Path to the model directory or identifier.", + required=True, + ) + parser.add_argument( + "--vae_path", + type=str, + help="Path to the vae directory or identifier.", + required=True, + ) + parser.add_argument( + "--input_image_path", + type=str, + default="live-canvas-frontend-user-data.png", + help="Path to the input image file.", + required=True, + ) + parser.add_argument( + "--output_image_path", + type=str, + default="live-canvas-generate-image-output.png", + help="Path for the output image file.", + ) + parser.add_argument( + "--disable_stablefast", + action="store_true", + help="Disable the stablefast compilation for faster loading during development.", + ) + parser.add_argument( + "--debug", + action="store_true", + help="Enable debug output.", + ) + + args = parser.parse_args() + return args + + +def read_stdin_loop(params_queue, shutdown_event): for line in sys.stdin: if line.strip(): try: - params_queue.put(json.loads(line)) + data = json.loads(line) + + if data.get("command") == "shutdown": + shutdown_event.set() + break + else: + params_queue.put(data) except json.JSONDecodeError as e: - print(f"Error decoding JSON: {e}") + print( + json.dumps( + {"status": "error", "message": f"Error decoding JSON: {e}"} + ) + ) def safe_rename_with_retries(src, dst, max_retries=5, delay=0.005): @@ -68,134 +154,126 @@ def calculate_min_inference_steps(strength): return math.ceil(1.0 / strength) -# Initial/default values for parameters -prompt = None -seed = None -strength = None -guidance_scale = None -input_path = "live-canvas-frontend-user-data.png" -output_path = "live-canvas-generate-image-output.png" +def prepare_pipeline(model_path, vae_path, disable_stablefast=False): + pipe = StableDiffusionImg2ImgPipeline.from_pretrained( + model_path, + torch_dtype=torch.float16, + variant="fp16", + safety_checker=None, + requires_safety_checker=False, + ) -# Queue to hold parameters received from stdin -params_queue = queue.Queue() + pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.safety_checker = None + pipe.to(torch.device("cuda")) -# Start the stdin reading thread -stdin_thread = threading.Thread(target=read_stdin_loop, daemon=True) -stdin_thread.start() + # Load the stable-fast default config + config = CompilationConfig.Default() + # Whether to preserve parameters when freezing the model. + # If True, parameters will be preserved, but the model will be a bit slower. + # If False, parameters will be marked as constants, and the model will be faster. + config.preserve_parameters = False -# Torch optimizations -torch.set_grad_enabled(False) -torch.backends.cuda.matmul.allow_tf32 = True -torch.backends.cudnn.allow_tf32 = True - + # xformers and Triton are suggested for achieving best performance. + try: + import xformers -pipe = StableDiffusionImg2ImgPipeline.from_pretrained( - "stabilityai/sd-turbo", - torch_dtype=torch.float16, - variant="fp16", - safety_checker=None, - requires_safety_checker=False, -) - -pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config) -pipe.safety_checker = None -pipe.to(torch.device("cuda")) - -# Load the stable-fast default config -config = CompilationConfig.Default() - -# Whether to preserve parameters when freezing the model. -# If True, parameters will be preserved, but the model will be a bit slower. -# If False, parameters will be marked as constants, and the model will be faster. -config.preserve_parameters = False - -# xformers and Triton are suggested for achieving best performance. -try: - import xformers - - config.enable_xformers = True -except ImportError: - print("xformers not installed, skip") -try: - import triton - - config.enable_triton = True -except ImportError: - print("Triton not installed, skip") - -# CUDA Graph is suggested for small batch sizes and small resolutions to reduce CPU overhead. -# But it can increase the amount of GPU memory used. -config.enable_cuda_graph = True - -pipe.width = 512 -pipe.height = 512 - -pipe.vae = AutoencoderTiny.from_pretrained("madebyollin/taesd").to( - device=pipe.device, dtype=pipe.dtype -) - -# Channels-last memory format -# see https://huggingface.co/docs/diffusers/optimization/memory#channelslast-memory-format -pipe.unet.to(memory_format=torch.channels_last) -pipe.vae.to(memory_format=torch.channels_last) - -# Disable inference progress bar -pipe.set_progress_bar_config(leave=False) -pipe.set_progress_bar_config(disable=True) - -pipe = compile(pipe, config) - -# Warmup -print("warmup started") -for _ in range(10): - init_image = load_image_with_retry(input_path) - output_image = pipe( - prompt="the moon, 4k", - image=init_image, - height=512, - width=512, - num_inference_steps=1, - num_images_per_prompt=1, - strength=1.0, - guidance_scale=0.0, - ).images[0] -print("warmup done") - - -def main(): - global prompt, seed, strength, guidance_scale, input_path, output_path - - while True: + config.enable_xformers = True + except ImportError: + print("xformers not installed, skip") + try: + import triton + + config.enable_triton = True + except ImportError: + print("Triton not installed, skip") + + # CUDA Graph is suggested for small batch sizes and small resolutions to reduce CPU overhead. + # But it can increase the amount of GPU memory used. + config.enable_cuda_graph = True + + pipe.width = 512 + pipe.height = 512 + + pipe.vae = AutoencoderTiny.from_pretrained(vae_path).to( + device=pipe.device, dtype=pipe.dtype + ) + + # Channels-last memory format + # see https://huggingface.co/docs/diffusers/optimization/memory#channelslast-memory-format + pipe.unet.to(memory_format=torch.channels_last) + pipe.vae.to(memory_format=torch.channels_last) + + # Disable inference progress bar + pipe.set_progress_bar_config(leave=False) + pipe.set_progress_bar_config(disable=True) + + # Disable stable fast for faster startup of the script, but slower inference + if not disable_stablefast: + pipe = compile(pipe, config) + + return pipe + + +def warmup(pipe, input_image_path): + # Warmup + for _ in range(6): + init_image = load_image_with_retry(input_image_path) + pipe( + prompt="the moon, 4k", + image=init_image, + height=512, + width=512, + num_inference_steps=1, + num_images_per_prompt=1, + strength=1.0, + guidance_scale=0.0, + ).images[0] + + +def main(pipe, input_image_path, output_image_path, shutdown_event): + # Initial/default values for parameters + prompt = "realistic face, 4k" + seed = 1 + strength = 0.99 + guidance_scale = None + + # Queue to hold parameters received from stdin + params_queue = queue.Queue() + + # Read from stdin + stdin_thread = threading.Thread( + target=read_stdin_loop, args=(params_queue, shutdown_event), daemon=True + ) + stdin_thread.start() + + while not shutdown_event.is_set(): try: # Update parameters if new ones are available while not params_queue.empty(): parameters = params_queue.get_nowait() prompt = parameters.get("prompt", prompt) seed = parameters.get("seed", seed) - input_path = parameters.get("input_path", input_path) strength = parameters.get("strength", strength) guidance_scale = parameters.get("guidance_scale", guidance_scale) - output_path = parameters.get("output_path", output_path) print(f"Updated parameters {parameters}") except queue.Empty: pass # No new parameters, proceed with the existing ones # Only generate an image if the prompt is not empty if prompt is not None and prompt.strip(): - start_time = time.time() - torch.manual_seed(seed) - init_image = load_image_with_retry(input_path) + init_image = load_image_with_retry(input_image_path) # Image couldn't be loaded, skip this iteration if init_image is None: continue strength_ = float(strength) - guidance_scale_ = float(guidance_scale) - denoise_steps_ = calculate_min_inference_steps(strength_) + # guidance_scale_ = float(guidance_scale) + # denoise_steps_ = calculate_min_inference_steps(strength_) image = pipe( prompt, @@ -208,35 +286,39 @@ def main(): guidance_scale=1.5, ).images[0] - end_time = time.time() - # Save file - image.save(f"{output_path}.tmp.png") - safe_rename_with_retries(f"{output_path}.tmp.png", output_path) + image.save(f"{output_image_path}.tmp.png") + safe_rename_with_retries(f"{output_image_path}.tmp.png", output_image_path) + + print(json.dumps({"status": "image_generated"})) - duration = (end_time - start_time) * 1000 - # print(f"{duration:.2f} ms") - times.append(duration) else: image = Image.new("RGB", (512, 512), color="white") - image.save(f"{output_path}.tmp.png") - safe_rename_with_retries(f"{output_path}.tmp.png", output_path) + image.save(f"{output_image_path}.tmp.png") + safe_rename_with_retries(f"{output_image_path}.tmp.png", output_image_path) if __name__ == "__main__": - times = [] + # Used for gracefully shutting down the script + shutdown_event = threading.Event() try: - print("started loop") - main() + args = parse_args() + pipe = None + + with suppress_print(args.debug): + pipe = prepare_pipeline( + args.model_path, args.vae_path, args.disable_stablefast + ) + warmup(pipe, args.input_image_path) + + print(json.dumps({"status": "started"})) + + main(pipe, args.input_image_path, args.output_image_path, shutdown_event) + + print(json.dumps({"status": "shutdown"})) + except Exception as error: - print(error) + print(json.dumps({"status": "error", "message": str(error)})) except: - if times: - total_time = sum(times) - average_time = total_time / len(times) - print( - f"Total time: {total_time:.2f} ms, Total executions: {len(times)}, Average time: {average_time:.2f} ms" - ) - else: - print(f"No operations were performed or exception") + print(json.dumps({"status": "stopped"})) diff --git a/src/client/pages/[locale]/live-painting.tsx b/src/client/pages/[locale]/live-painting.tsx index bbf69ed6d..6b937cfeb 100644 --- a/src/client/pages/[locale]/live-painting.tsx +++ b/src/client/pages/[locale]/live-painting.tsx @@ -103,7 +103,20 @@ function DrawingArea() { } function RenderingArea() { - const [image] = useAtom(imageAtom); + const [image, setImage] = useState(""); + + useEffect(() => { + const unsubscribe = window.ipc.on( + buildKey([ID.LIVE_PAINT], { suffix: ":generated" }), + (dataUrl: string) => { + setImage(dataUrl); + } + ); + + return () => { + unsubscribe(); + }; + }, []); return ( @@ -190,6 +203,7 @@ export default function Page(_properties: InferGetStaticPropsType