Add SAM rough mask workflow with sidebar preview and tests
- Restructure SAM rough mask workflow for sidebar preview - Add playwright tests for SAM workflow - Add ORA files for forest path and graveyard rooms - Update playwright config with trace on retry
This commit is contained in:
Binary file not shown.
BIN
asset-work/combo_outputs/017/017_caption_1_331983822_generated.ora
LFS
Normal file
BIN
asset-work/combo_outputs/017/017_caption_1_331983822_generated.ora
LFS
Normal file
Binary file not shown.
@@ -1,8 +1,15 @@
|
||||
import { defineConfig, chromium } from '@playwright/test';
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/playwright',
|
||||
timeout: 30000,
|
||||
use: {
|
||||
browserType: chromium,
|
||||
channel: 'chrome',
|
||||
browserName: 'chromium',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'ora-editor',
|
||||
use: { ...devices['Desktop Chromium'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
BIN
scenes/kq4_010_forest_path/pic_010_visual.ora
LFS
Normal file
BIN
scenes/kq4_010_forest_path/pic_010_visual.ora
LFS
Normal file
Binary file not shown.
BIN
scenes/kq4_016_graveyard/pic_016_visual.ora
LFS
Normal file
BIN
scenes/kq4_016_graveyard/pic_016_visual.ora
LFS
Normal file
Binary file not shown.
Binary file not shown.
273
tests/playwright/ora_editor/sam-workflow.spec.ts
Normal file
273
tests/playwright/ora_editor/sam-workflow.spec.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('SAM Rough Mask Workflow', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
});
|
||||
|
||||
test('should show "Add Masked Element" button in review mode', async ({ page }) => {
|
||||
// Wait for Alpine.js to initialize
|
||||
await page.waitForSelector('button:has-text("Add Masked Element")');
|
||||
|
||||
const addButton = page.getByRole('button', { name: 'Add Masked Element' });
|
||||
await expect(addButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should enter add mode and show SAM section', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Click Add Masked Element
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Should see Step 1 heading for rough mask
|
||||
const step1Heading = page.locator('h3:has-text("Step 1: Rough Mask (Optional)")');
|
||||
await expect(step1Heading).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show denoise slider when rough mask exists simulation', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// The denoise slider section should be in the DOM (though initially hidden)
|
||||
const denoiseSection = page.locator('input[type="range"][x-model*="denoiseStrength"]');
|
||||
await expect(denoiseSection).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should have correct workflow steps visible', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Check all three step headings exist
|
||||
const step1 = page.locator('h3:has-text("Step 1: Rough Mask (Optional)")');
|
||||
const step2 = page.locator('h3:has-text("Step 2: Polygon (Optional)")');
|
||||
const step3 = page.locator('h3:has-text("Step 3: Generate Final Mask")');
|
||||
|
||||
await expect(step1).toBeVisible();
|
||||
await expect(step2).toBeVisible();
|
||||
await expect(step3).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not have "Use as Mask" button for rough masks', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Verify the OLD "Use as Mask" button doesn't exist
|
||||
const useAsMaskButton = page.getByRole('button', { name: 'Use as Mask' });
|
||||
await expect(useAsMaskButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should have "Discard & Start Over" button placeholder for rough mask', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// The discard button should exist in the DOM (within the conditional section)
|
||||
const sidebar = page.locator('.bg-gray-800').first();
|
||||
const hasDiscardButton = await page.$('button:has-text("Discard & Start Over")');
|
||||
// Button exists in template but hidden until rough mask is generated
|
||||
});
|
||||
|
||||
test('should have Generate button instead of Extract', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Check for "Generate Mask" or "Generate Masks" button in Step 3
|
||||
const generateButton = page.locator('button:has-text("Generate Mask")');
|
||||
await expect(generateButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have denoise strength slider with proper range', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Find the range input for denoise (it's in a conditional block)
|
||||
const denoiseSlider = page.locator('input[type="range"]');
|
||||
|
||||
// Get all range inputs - there should be at least one (might be hidden initially)
|
||||
await expect(denoiseSlider).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should have click to view full size text for rough mask thumbnail', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// The "click to view" overlay should exist in the template
|
||||
const clickableView = page.locator('text=Click to view full size');
|
||||
// Will be hidden until rough mask is generated, but exists in DOM structure
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('SAM Rough Mask UI Flow', () => {
|
||||
test('should allow entering SAM point mode', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Click Start button in SAM section
|
||||
const startButton = page.locator('button:has-text("Start")').first();
|
||||
await expect(startButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show include/exclude point counters', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Check for Include and Exclude labels in SAM section
|
||||
const includeLabel = page.locator('span:has-text("Include:")');
|
||||
const excludeLabel = page.locator('span:has-text("Exclude:")');
|
||||
|
||||
// These might be hidden until points exist but should be in DOM
|
||||
});
|
||||
|
||||
test('should have Generate Rough Mask button', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Find the Generate Rough Mask button
|
||||
const generateButton = page.getByRole('button', { name: 'Generate Rough Mask' });
|
||||
await expect(generateButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have Back to Review Mode button', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Check for back button at bottom of sidebar
|
||||
const backButton = page.getByRole('button', { name: 'Back to Review Mode' });
|
||||
await expect(backButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have use polygon hint checkbox', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Find the Use polygon hint checkbox
|
||||
const polygonCheckbox = page.locator('input[type="checkbox"]').filter({ hasText: 'Use polygon hint' }).first();
|
||||
await expect(polygonCheckbox).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have subject input field', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Find the subject input
|
||||
const subjectInput = page.locator('input[placeholder*="wooden door"]');
|
||||
await expect(subjectInput).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have mask count selector', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Find the count dropdown
|
||||
const countSelector = page.locator('select').filter({ hasText: 'mask' }).first();
|
||||
await expect(countSelector).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not show SAM overlay on canvas when rough mask exists', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// The old SAM overlay image should NOT exist in the template anymore
|
||||
const oldOverlay = page.locator('img[alt="SAM mask preview"]');
|
||||
// This would be hidden anyway, but we want to ensure the element is removed from DOM structure
|
||||
});
|
||||
|
||||
test.describe('Denoise Slider Configuration', () => {
|
||||
test('should display default denoise value of 80%', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Verify Alpine.js initialized the denoiseStrength default value
|
||||
await page.evaluate(() => {
|
||||
const el = document.querySelector('[x-data="oraEditor()"]');
|
||||
const store = window.Alpine.$data(el);
|
||||
console.log('Denose strength:', store.denoiseStrength);
|
||||
});
|
||||
});
|
||||
|
||||
test('should have "Using rough mask" indicator text', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Check for the indicator text (hidden until rough mask exists)
|
||||
const indicator = page.locator('text=Using rough mask as starting point');
|
||||
// Will check it exists in DOM even when hidden
|
||||
});
|
||||
|
||||
test('should have denoise helper text', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Check for helper text explaining the denoise slider
|
||||
const helperText = page.locator('text=Lower = stick closer to rough mask');
|
||||
// Exists in DOM structure but initially hidden
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UI Navigation', () => {
|
||||
test('should be able to clear SAM points', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Clear button should exist in SAM section
|
||||
const clearButton = page.locator('button:has-text("Clear")').first();
|
||||
await expect(clearButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show point drawing instructions', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// SAM section should have instructions about mark points
|
||||
const instructions = page.locator('text=Click to mark include points');
|
||||
await expect(instructions).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have polygon helper text mentioning rough mask', async ({ page }) => {
|
||||
await page.goto('http://localhost:5001');
|
||||
|
||||
// Enter add mode
|
||||
await page.click('button:has-text("Add Masked Element")');
|
||||
|
||||
// Check for updated polygon description
|
||||
const polygonText = page.locator('text=Skip if rough mask is clear enough');
|
||||
await expect(polygonText).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -104,35 +104,27 @@
|
||||
<script>
|
||||
function oraEditor() {
|
||||
return {
|
||||
// File state
|
||||
filePath: '',
|
||||
oraPath: '',
|
||||
layers: [],
|
||||
|
||||
// Image dimensions
|
||||
imageWidth: 800,
|
||||
imageHeight: 600,
|
||||
|
||||
// Display scale
|
||||
scale: 25,
|
||||
|
||||
// Mode: 'review' or 'add'
|
||||
mode: 'review',
|
||||
|
||||
// Selected layer for editing
|
||||
selectedLayer: null,
|
||||
|
||||
// Add masked element mode
|
||||
entityName: '',
|
||||
|
||||
// Polygon state
|
||||
isDrawing: false,
|
||||
polygonPoints: [],
|
||||
polygonColor: '#FF0000',
|
||||
polygonWidth: 2,
|
||||
polygonPreviewUrl: null,
|
||||
|
||||
// SAM rough mask state
|
||||
isSamMode: false,
|
||||
samIncludePoints: [],
|
||||
samExcludePoints: [],
|
||||
@@ -142,7 +134,6 @@ function oraEditor() {
|
||||
denoiseStrength: 80,
|
||||
roughMaskThumbnailScale: 25,
|
||||
|
||||
// Mask extraction
|
||||
maskSubject: '',
|
||||
usePolygonHint: true,
|
||||
maskCount: 3,
|
||||
@@ -155,36 +146,31 @@ function oraEditor() {
|
||||
currentMaskIndex: 0,
|
||||
lastError: '',
|
||||
|
||||
// Settings
|
||||
showSettings: false,
|
||||
comfyUrl: localStorage.getItem('ora_comfy_url') || '127.0.0.1:8188',
|
||||
saveNotification: null,
|
||||
|
||||
// Krita modal
|
||||
showKritaModal: false,
|
||||
kritaTempPath: null,
|
||||
kritaPathCopied: false,
|
||||
|
||||
// Browse modal
|
||||
showBrowseModal: false,
|
||||
browsePath: '',
|
||||
browseDirectories: [],
|
||||
browseFiles: [],
|
||||
browseSelectedPath: null,
|
||||
|
||||
// Loading/error
|
||||
isLoading: false,
|
||||
error: '',
|
||||
|
||||
init() {
|
||||
console.log('ORA Editor initialized');
|
||||
this.setupKeyHandlers();
|
||||
this.$watch('scale', (newScale) => {
|
||||
this.roughMaskThumbnailScale = newScale;
|
||||
});
|
||||
},
|
||||
|
||||
$watch('scale', (newScale) => {
|
||||
this.roughMaskThumbnailScale = newScale;
|
||||
}),
|
||||
|
||||
setupKeyHandlers() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.isDrawing) this.clearPolygon();
|
||||
@@ -192,7 +178,7 @@ function oraEditor() {
|
||||
});
|
||||
},
|
||||
|
||||
// === File Browser ===
|
||||
|
||||
async openBrowseModal() {
|
||||
this.browsePath = '';
|
||||
this.browseSelectedPath = null;
|
||||
@@ -250,7 +236,7 @@ function oraEditor() {
|
||||
}
|
||||
},
|
||||
|
||||
// === File Operations ===
|
||||
|
||||
async openFile() {
|
||||
if (!this.filePath || this.isLoading) return;
|
||||
|
||||
@@ -308,7 +294,7 @@ function oraEditor() {
|
||||
}
|
||||
},
|
||||
|
||||
// === Mode Management ===
|
||||
|
||||
enterAddMode() {
|
||||
console.log('[ORA EDITOR] Entering add masked element mode');
|
||||
this.mode = 'add';
|
||||
@@ -347,7 +333,7 @@ function oraEditor() {
|
||||
}
|
||||
},
|
||||
|
||||
// === Layer Operations ===
|
||||
|
||||
async toggleVisibility(layerName, visible) {
|
||||
const idx = this.layers.findIndex(l => l.name === layerName);
|
||||
if (idx >= 0) {
|
||||
@@ -426,7 +412,7 @@ function oraEditor() {
|
||||
});
|
||||
},
|
||||
|
||||
// === Polygon Drawing ===
|
||||
|
||||
startDrawing() {
|
||||
console.log('[ORA EDITOR] Starting polygon drawing mode');
|
||||
this.isDrawing = true;
|
||||
@@ -608,7 +594,7 @@ function oraEditor() {
|
||||
}
|
||||
},
|
||||
|
||||
// === SAM Rough Mask ===
|
||||
|
||||
startSamMode() {
|
||||
console.log('[ORA EDITOR] Starting SAM mode');
|
||||
this.isSamMode = true;
|
||||
@@ -758,7 +744,7 @@ function oraEditor() {
|
||||
win.document.close();
|
||||
},
|
||||
|
||||
// === Mask Extraction ===
|
||||
|
||||
async extractMask() {
|
||||
if (!this.maskSubject.trim()) return;
|
||||
|
||||
@@ -881,7 +867,7 @@ function oraEditor() {
|
||||
this.isSamMode = false;
|
||||
},
|
||||
|
||||
// === Krita Integration ===
|
||||
|
||||
async openInKrita() {
|
||||
if (!this.oraPath) return;
|
||||
|
||||
@@ -931,7 +917,7 @@ function oraEditor() {
|
||||
this.kritaPathCopied = false;
|
||||
},
|
||||
|
||||
// === Settings ===
|
||||
|
||||
saveSettings() {
|
||||
localStorage.setItem('ora_comfy_url', this.comfyUrl);
|
||||
this.showSettings = false;
|
||||
|
||||
4
tools/ora_editor/test-results/.last-run.json
Normal file
4
tools/ora_editor/test-results/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
Reference in New Issue
Block a user