feat: Add inpainting (局部重绘) feature for timeline editor

- Add canvas-based mask drawing tools (brush, eraser, rect, lasso)
- Add undo/redo history support for mask editing
- Integrate inpainting UI into preview player
- Add backend API endpoint for inpainting requests
- Add MediaService.inpaint method with ComfyUI workflow support
- Add Flux inpainting workflows for selfhost and RunningHub

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-05 23:44:51 +08:00
parent 56db9bf9d2
commit 79a6c2ef3e
17 changed files with 1444 additions and 5 deletions

View File

@@ -285,3 +285,76 @@ class MediaService(ComfyBaseService):
except Exception as e:
logger.error(f"Media generation error: {e}")
raise
async def inpaint(
self,
image_path: str,
mask_path: str,
prompt: Optional[str] = None,
denoise_strength: float = 0.8,
workflow: Optional[str] = None,
**params
) -> MediaResult:
"""
Inpaint image using mask
Args:
image_path: Path to original image
mask_path: Path to mask image (white=inpaint, black=keep)
prompt: Optional prompt for inpainted region
denoise_strength: How much to change masked area (0.0-1.0)
workflow: Inpainting workflow to use
Returns:
MediaResult with inpainted image URL
"""
logger.info(f"🎨 Inpainting image: {image_path}")
logger.info(f" Mask: {mask_path}")
logger.info(f" Denoise: {denoise_strength}")
try:
# Resolve workflow
if workflow is None:
workflow = "selfhost/image_inpaint_flux.json"
workflow_info = self._resolve_workflow(workflow=workflow)
# Build workflow parameters
workflow_params = {
"image": image_path,
"mask": mask_path,
"denoise": denoise_strength,
}
if prompt:
workflow_params["prompt"] = prompt
workflow_params.update(params)
logger.debug(f"Inpaint workflow parameters: {workflow_params}")
# Execute workflow
kit = await self.core._get_or_create_comfykit()
if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info:
workflow_input = workflow_info["workflow_id"]
else:
workflow_input = workflow_info["path"]
result = await kit.execute(workflow_input, workflow_params)
if not result.images:
logger.error("No image generated from inpainting")
raise Exception("Inpainting failed - no image generated")
image_url = result.images[0]
logger.info(f"✅ Inpainted image: {image_url}")
return MediaResult(
media_type="image",
url=image_url
)
except Exception as e:
logger.error(f"Inpainting error: {e}")
raise