Skip to content

Commit 3cd31f4

Browse files
authored
Merge pull request #2 from invoke-ai/v7
Initial v7
2 parents f25b9b7 + a6b35ea commit 3cd31f4

527 files changed

Lines changed: 78320 additions & 103 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/install-frontend-deps/action.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
name: install frontend dependencies
22
description: Installs frontend dependencies with pnpm, with caching
3+
inputs:
4+
working-directory:
5+
description: Directory to install dependencies in
6+
required: false
7+
default: invokeai/frontend/web
38
runs:
49
using: 'composite'
510
steps:
@@ -30,4 +35,4 @@ runs:
3035
- name: install frontend dependencies
3136
run: pnpm install --prefer-frozen-lockfile
3237
shell: bash
33-
working-directory: invokeai/frontend/web
38+
working-directory: ${{ inputs.working-directory }}

.github/workflows/frontend-checks.yml

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,13 @@ on:
3030
type: boolean
3131
default: true
3232

33-
defaults:
34-
run:
35-
working-directory: invokeai/frontend/web
36-
3733
jobs:
3834
frontend-checks:
3935
runs-on: ubuntu-latest
4036
timeout-minutes: 10 # expected run time: <2 min
37+
defaults:
38+
run:
39+
working-directory: invokeai/frontend/web
4140
steps:
4241
- uses: actions/checkout@v6
4342

@@ -94,3 +93,37 @@ jobs:
9493
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
9594
run: 'pnpm lint:knip'
9695
shell: bash
96+
97+
frontend-webv2-checks:
98+
runs-on: ubuntu-latest
99+
timeout-minutes: 10 # expected run time: <2 min
100+
defaults:
101+
run:
102+
working-directory: invokeai/frontend/webv2
103+
steps:
104+
- uses: actions/checkout@v6
105+
106+
- name: check for changed webv2 files
107+
if: ${{ inputs.always_run != true }}
108+
id: changed-files
109+
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
110+
with:
111+
files_yaml: |
112+
webv2:
113+
- 'invokeai/frontend/webv2/**'
114+
115+
- name: install dependencies
116+
if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
117+
uses: ./.github/actions/install-frontend-deps
118+
with:
119+
working-directory: invokeai/frontend/webv2
120+
121+
- name: lint
122+
if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
123+
run: 'pnpm run lint'
124+
shell: bash
125+
126+
- name: build
127+
if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
128+
run: 'pnpm run build'
129+
shell: bash

.github/workflows/frontend-tests.yml

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,13 @@ on:
3030
type: boolean
3131
default: true
3232

33-
defaults:
34-
run:
35-
working-directory: invokeai/frontend/web
36-
3733
jobs:
3834
frontend-tests:
3935
runs-on: ubuntu-latest
4036
timeout-minutes: 10 # expected run time: <2 min
37+
defaults:
38+
run:
39+
working-directory: invokeai/frontend/web
4140
steps:
4241
- uses: actions/checkout@v6
4342

@@ -63,3 +62,32 @@ jobs:
6362
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
6463
run: 'pnpm test:no-watch'
6564
shell: bash
65+
66+
frontend-webv2-tests:
67+
runs-on: ubuntu-latest
68+
timeout-minutes: 10 # expected run time: <2 min
69+
defaults:
70+
run:
71+
working-directory: invokeai/frontend/webv2
72+
steps:
73+
- uses: actions/checkout@v6
74+
75+
- name: check for changed webv2 files
76+
if: ${{ inputs.always_run != true }}
77+
id: changed-files
78+
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
79+
with:
80+
files_yaml: |
81+
webv2:
82+
- 'invokeai/frontend/webv2/**'
83+
84+
- name: install dependencies
85+
if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
86+
uses: ./.github/actions/install-frontend-deps
87+
with:
88+
working-directory: invokeai/frontend/webv2
89+
90+
- name: vitest
91+
if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
92+
run: 'pnpm test'
93+
shell: bash

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@ env/
135135
venv/
136136
ENV/
137137

138+
# Generated stuff
139+
invokeai.yaml
140+
invokeai.example.yaml
141+
142+
/models/
143+
/databases/
144+
/configs/
145+
/nodes/
146+
/outputs/
147+
138148
# Spyder project settings
139149
.spyderproject
140150
.spyproject

invokeai/app/api/dependencies.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from invokeai.app.services.names.names_default import SimpleNameService
4242
from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk
4343
from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache
44+
from invokeai.app.services.project_records.project_records_sqlite import ProjectRecordsSqlite
4445
from invokeai.app.services.session_processor.session_processor_default import (
4546
DefaultSessionProcessor,
4647
DefaultSessionRunner,
@@ -187,6 +188,7 @@ def initialize(
187188
style_preset_image_files = StylePresetImageFileStorageDisk(style_presets_folder / "images")
188189
workflow_thumbnails = WorkflowThumbnailFileStorageDisk(workflow_thumbnails_folder)
189190
client_state_persistence = ClientStatePersistenceSqlite(db=db)
191+
project_records = ProjectRecordsSqlite(db=db)
190192
users = UserService(db=db)
191193

192194
services = InvocationServices(
@@ -220,6 +222,7 @@ def initialize(
220222
style_preset_image_files=style_preset_image_files,
221223
workflow_thumbnails=workflow_thumbnails,
222224
client_state_persistence=client_state_persistence,
225+
project_records=project_records,
223226
users=users,
224227
)
225228

invokeai/app/api/routers/_access.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def assert_image_owner(image_name: str, current_user: CurrentUserOrDefault) -> N
2222
"""
2323
if current_user.is_admin:
2424
return
25+
if not ApiDependencies.invoker.services.image_records.exists(image_name):
26+
raise HTTPException(status_code=404, detail="Image not found")
2527
owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
2628
if owner is not None and owner == current_user.user_id:
2729
return
@@ -50,6 +52,8 @@ def assert_image_read_access(image_name: str, current_user: CurrentUserOrDefault
5052
"""
5153
if current_user.is_admin:
5254
return
55+
if not ApiDependencies.invoker.services.image_records.exists(image_name):
56+
raise HTTPException(status_code=404, detail="Image not found")
5357

5458
owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
5559
if owner is not None and owner == current_user.user_id:

invokeai/app/api/routers/app_info.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
load_external_api_keys,
2424
)
2525
from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus
26+
from invokeai.app.services.external_generation.startup import sync_configured_external_starter_models
2627
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
2728
from invokeai.app.services.model_records.model_records_base import UnknownModelException
2829
from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
@@ -178,7 +179,7 @@ async def update_runtime_config(
178179
status_code=200,
179180
response_model=list[ExternalProviderStatusModel],
180181
)
181-
async def get_external_provider_statuses() -> list[ExternalProviderStatusModel]:
182+
async def get_external_provider_statuses(_: AdminUserOrDefault) -> list[ExternalProviderStatusModel]:
182183
statuses = ApiDependencies.invoker.services.external_generation.get_provider_statuses()
183184
return [status_to_model(status) for status in statuses.values()]
184185

@@ -189,7 +190,7 @@ async def get_external_provider_statuses() -> list[ExternalProviderStatusModel]:
189190
status_code=200,
190191
response_model=list[ExternalProviderConfigModel],
191192
)
192-
async def get_external_provider_configs() -> list[ExternalProviderConfigModel]:
193+
async def get_external_provider_configs(_: AdminUserOrDefault) -> list[ExternalProviderConfigModel]:
193194
config = get_config()
194195
return [_build_external_provider_config(provider_id, config) for provider_id in EXTERNAL_PROVIDER_FIELDS]
195196

@@ -219,9 +220,14 @@ async def set_external_provider_config(
219220
raise HTTPException(status_code=400, detail="No external provider config fields provided")
220221

221222
api_key_removed = update.api_key is not None and updates.get(api_key_field) is None
223+
api_key_set = update.api_key is not None and updates.get(api_key_field) is not None
222224
_apply_external_provider_update(updates)
223225
if api_key_removed:
224226
_remove_external_models_for_provider(provider_id)
227+
elif api_key_set:
228+
# Configuring a key should make the provider's models usable without a
229+
# restart; queue its external starter models the same way startup does.
230+
_sync_external_starter_models_for_provider(provider_id)
225231
return _build_external_provider_config(provider_id, get_config())
226232

227233

@@ -316,6 +322,19 @@ def _build_external_provider_config(provider_id: str, config: InvokeAIAppConfig)
316322
)
317323

318324

325+
def _sync_external_starter_models_for_provider(provider_id: str) -> None:
326+
invoker = ApiDependencies.invoker
327+
try:
328+
sync_configured_external_starter_models(
329+
configured_provider_ids={provider_id},
330+
model_manager=invoker.services.model_manager,
331+
logger=invoker.services.logger,
332+
)
333+
except Exception as error:
334+
# Queuing installs must never fail the config save; surface and move on.
335+
invoker.services.logger.warning(f"Failed queueing external starter models for '{provider_id}': {error}")
336+
337+
319338
def _remove_external_models_for_provider(provider_id: str) -> None:
320339
model_manager = ApiDependencies.invoker.services.model_manager
321340
external_models = model_manager.store.search_by_attr(

invokeai/app/api/routers/custom_nodes.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""FastAPI routes for custom node management."""
22

3+
import asyncio
34
import json
45
import shutil
5-
import subprocess
66
import sys
77
import traceback
88
from importlib.util import module_from_spec, spec_from_file_location
@@ -28,6 +28,7 @@
2828
# were imported by that pack. Used on uninstall to delete only pack-imported workflows
2929
# — deleting by tag alone is unsafe because users can edit tags on their own workflows.
3030
PACK_MANIFEST_FILENAME = ".invokeai_pack_manifest.json"
31+
GIT_CLONE_TIMEOUT_SECONDS = 120
3132

3233

3334
class NodePackInfo(BaseModel):
@@ -126,6 +127,26 @@ def _get_installed_packs() -> list[NodePackInfo]:
126127
return packs
127128

128129

130+
async def _clone_node_pack(source: str, target_dir: Path) -> tuple[int, str]:
131+
process = await asyncio.create_subprocess_exec(
132+
"git",
133+
"clone",
134+
source,
135+
str(target_dir),
136+
stdout=asyncio.subprocess.PIPE,
137+
stderr=asyncio.subprocess.PIPE,
138+
)
139+
140+
try:
141+
_stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=GIT_CLONE_TIMEOUT_SECONDS)
142+
except asyncio.TimeoutError:
143+
process.kill()
144+
await process.communicate()
145+
raise
146+
147+
return process.returncode or 0, stderr.decode(errors="replace").strip()
148+
149+
129150
@custom_nodes_router.get(
130151
"/",
131152
operation_id="list_custom_node_packs",
@@ -172,21 +193,16 @@ async def install_custom_node_pack(
172193

173194
try:
174195
# Clone the repository
175-
result = subprocess.run(
176-
["git", "clone", source, str(target_dir)],
177-
capture_output=True,
178-
text=True,
179-
timeout=120,
180-
)
196+
returncode, stderr = await _clone_node_pack(source, target_dir)
181197

182-
if result.returncode != 0:
198+
if returncode != 0:
183199
# Clean up on failure
184200
if target_dir.exists():
185201
shutil.rmtree(target_dir)
186202
return InstallNodePackResponse(
187203
name=pack_name,
188204
success=False,
189-
message=f"Git clone failed: {result.stderr.strip()}",
205+
message=f"Git clone failed: {stderr}",
190206
)
191207

192208
# Detect dependency manifests but do NOT install them automatically.
@@ -232,7 +248,7 @@ async def install_custom_node_pack(
232248
dependency_file=dependency_file,
233249
)
234250

235-
except subprocess.TimeoutExpired:
251+
except asyncio.TimeoutError:
236252
if target_dir.exists():
237253
shutil.rmtree(target_dir)
238254
return InstallNodePackResponse(

0 commit comments

Comments
 (0)