Skip to content

Commit e3129a3

Browse files
author
cw
committed
feat: add unified Create page, local model system, and fix web UI bugs
- Add unified Create page with 4 modes (img2vid, txt2vid, txt2img, style transfer), scenario templates, provider selection, style presets, and duration/aspect settings - Add local model system with Python inference backend, 6 downloadable models, and Settings UI for environment setup and model management - Fix Bug #1: hero prompt now routes to /create?mode=txt2vid (was /create/video) - Fix Bug #5: Status page Send Test and Reload buttons now have onClick handlers - Add WEB_UI_E2E_TEST_REPORT (42 tests) and FUNCTIONAL_E2E_REPORT (38 tests)
1 parent 0d428a0 commit e3129a3

19 files changed

Lines changed: 4436 additions & 31 deletions

daemon/lib/api/unified_api_server.dart

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:opencli_daemon/ipc/ipc_protocol.dart';
1111
import 'package:opencli_daemon/api/api_translator.dart';
1212
import 'package:opencli_daemon/api/message_handler.dart';
1313
import 'package:opencli_daemon/pipeline/pipeline_api.dart';
14+
import 'package:opencli_daemon/domains/media_creation/local_model_manager.dart';
1415

1516
/// Unified API server on port 9529 for Web UI integration
1617
///
@@ -23,17 +24,20 @@ class UnifiedApiServer {
2324
HttpServer? _server;
2425
PipelineApi? _pipelineApi;
2526
Future<void> Function()? _onConfigSaved;
27+
LocalModelManager? _localModelManager;
2628

2729
UnifiedApiServer({
2830
required RequestRouter requestRouter,
2931
required MessageHandler messageHandler,
3032
this.port = 9529,
3133
PipelineApi? pipelineApi,
3234
Future<void> Function()? onConfigSaved,
35+
LocalModelManager? localModelManager,
3336
}) : _requestRouter = requestRouter,
3437
_messageHandler = messageHandler,
3538
_pipelineApi = pipelineApi,
36-
_onConfigSaved = onConfigSaved;
39+
_onConfigSaved = onConfigSaved,
40+
_localModelManager = localModelManager;
3741

3842
/// Set pipeline API (can be configured after construction).
3943
void setPipelineApi(PipelineApi api) {
@@ -59,6 +63,13 @@ class UnifiedApiServer {
5963
router.get('/api/v1/config', _handleGetConfig);
6064
router.post('/api/v1/config', _handleUpdateConfig);
6165

66+
// Local model API routes
67+
router.get('/api/v1/local-models', _handleListLocalModels);
68+
router.get('/api/v1/local-models/environment', _handleLocalEnv);
69+
router.post('/api/v1/local-models/setup', _handleSetupEnvironment);
70+
router.post('/api/v1/local-models/<modelId>/download', _handleDownloadModel);
71+
router.delete('/api/v1/local-models/<modelId>', _handleDeleteModel);
72+
6273
// Pipeline API routes
6374
_pipelineApi?.registerRoutes(router);
6475

@@ -367,6 +378,173 @@ class UnifiedApiServer {
367378
return s;
368379
}
369380

381+
/// Handle GET /api/v1/local-models
382+
Future<shelf.Response> _handleListLocalModels(shelf.Request request) async {
383+
try {
384+
if (_localModelManager == null) {
385+
return shelf.Response.ok(
386+
jsonEncode({'models': [], 'available': false}),
387+
headers: {'Content-Type': 'application/json'},
388+
);
389+
}
390+
391+
final models = await _localModelManager!.listModels();
392+
return shelf.Response.ok(
393+
jsonEncode({
394+
'models': models.map((m) => m.toJson()).toList(),
395+
'available': _localModelManager!.isAvailable,
396+
}),
397+
headers: {'Content-Type': 'application/json'},
398+
);
399+
} catch (e) {
400+
return shelf.Response.internalServerError(
401+
body: jsonEncode({'error': 'Failed to list models: $e'}),
402+
headers: {'Content-Type': 'application/json'},
403+
);
404+
}
405+
}
406+
407+
/// Handle GET /api/v1/local-models/environment
408+
Future<shelf.Response> _handleLocalEnv(shelf.Request request) async {
409+
try {
410+
if (_localModelManager == null) {
411+
return shelf.Response.ok(
412+
jsonEncode({
413+
'ok': false,
414+
'python_version': 'not configured',
415+
'device': 'unknown',
416+
'missing_packages': ['local-inference not initialized'],
417+
'venv_exists': false,
418+
}),
419+
headers: {'Content-Type': 'application/json'},
420+
);
421+
}
422+
423+
final env = await _localModelManager!.checkEnvironment();
424+
return shelf.Response.ok(
425+
jsonEncode(env.toJson()),
426+
headers: {'Content-Type': 'application/json'},
427+
);
428+
} catch (e) {
429+
return shelf.Response.internalServerError(
430+
body: jsonEncode({'error': 'Failed to check environment: $e'}),
431+
headers: {'Content-Type': 'application/json'},
432+
);
433+
}
434+
}
435+
436+
/// Handle POST /api/v1/local-models/setup
437+
/// Runs local-inference/setup.sh to create venv and install dependencies
438+
Future<shelf.Response> _handleSetupEnvironment(shelf.Request request) async {
439+
try {
440+
// Find setup.sh relative to the daemon's working directory
441+
final scriptPath = path.join(
442+
Directory.current.path, '..', 'local-inference', 'setup.sh',
443+
);
444+
final scriptFile = File(path.normalize(scriptPath));
445+
446+
if (!await scriptFile.exists()) {
447+
// Try from home dir
448+
final home = Platform.environment['HOME'] ?? '.';
449+
final altPath = path.join(home, 'development', 'opencli', 'local-inference', 'setup.sh');
450+
final altFile = File(altPath);
451+
if (!await altFile.exists()) {
452+
return shelf.Response.notFound(
453+
jsonEncode({'error': 'setup.sh not found. Expected at local-inference/setup.sh'}),
454+
headers: {'Content-Type': 'application/json'},
455+
);
456+
}
457+
return _runSetupScript(altPath);
458+
}
459+
return _runSetupScript(path.normalize(scriptPath));
460+
} catch (e) {
461+
return shelf.Response.internalServerError(
462+
body: jsonEncode({'error': 'Setup failed: $e'}),
463+
headers: {'Content-Type': 'application/json'},
464+
);
465+
}
466+
}
467+
468+
Future<shelf.Response> _runSetupScript(String scriptPath) async {
469+
try {
470+
final result = await Process.run(
471+
'bash',
472+
[scriptPath],
473+
environment: Platform.environment,
474+
workingDirectory: path.dirname(scriptPath),
475+
).timeout(const Duration(minutes: 10));
476+
477+
final success = result.exitCode == 0;
478+
return shelf.Response.ok(
479+
jsonEncode({
480+
'success': success,
481+
'exit_code': result.exitCode,
482+
'stdout': result.stdout.toString(),
483+
'stderr': result.stderr.toString(),
484+
'message': success
485+
? 'Environment setup complete!'
486+
: 'Setup failed with exit code ${result.exitCode}',
487+
}),
488+
headers: {'Content-Type': 'application/json'},
489+
);
490+
} on TimeoutException {
491+
return shelf.Response.internalServerError(
492+
body: jsonEncode({'error': 'Setup timed out after 10 minutes'}),
493+
headers: {'Content-Type': 'application/json'},
494+
);
495+
}
496+
}
497+
498+
/// Handle POST /api/v1/local-models/:modelId/download
499+
Future<shelf.Response> _handleDownloadModel(
500+
shelf.Request request, String modelId) async {
501+
try {
502+
if (_localModelManager == null || !_localModelManager!.isAvailable) {
503+
return shelf.Response.internalServerError(
504+
body: jsonEncode({
505+
'error': 'Local inference not available. Run local-inference/setup.sh first.'
506+
}),
507+
headers: {'Content-Type': 'application/json'},
508+
);
509+
}
510+
511+
final result = await _localModelManager!.downloadModel(modelId);
512+
return shelf.Response.ok(
513+
jsonEncode(result),
514+
headers: {'Content-Type': 'application/json'},
515+
);
516+
} catch (e) {
517+
return shelf.Response.internalServerError(
518+
body: jsonEncode({'error': 'Download failed: $e'}),
519+
headers: {'Content-Type': 'application/json'},
520+
);
521+
}
522+
}
523+
524+
/// Handle DELETE /api/v1/local-models/:modelId
525+
Future<shelf.Response> _handleDeleteModel(
526+
shelf.Request request, String modelId) async {
527+
try {
528+
if (_localModelManager == null) {
529+
return shelf.Response.notFound(
530+
jsonEncode({'error': 'Local model manager not available'}),
531+
headers: {'Content-Type': 'application/json'},
532+
);
533+
}
534+
535+
final result = await _localModelManager!.deleteModel(modelId);
536+
return shelf.Response.ok(
537+
jsonEncode(result),
538+
headers: {'Content-Type': 'application/json'},
539+
);
540+
} catch (e) {
541+
return shelf.Response.internalServerError(
542+
body: jsonEncode({'error': 'Delete failed: $e'}),
543+
headers: {'Content-Type': 'application/json'},
544+
);
545+
}
546+
}
547+
370548
/// CORS middleware for Web UI access
371549
shelf.Middleware _corsMiddleware() {
372550
return (shelf.Handler handler) {

daemon/lib/core/daemon.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,20 @@ class Daemon {
234234

235235
// Start unified API server for Web UI integration
236236
TerminalUI.printInitStep('Starting unified API server', last: true);
237+
// Get local model manager from media domain
238+
final mediaDomain = _domainRegistry.getDomain('media_creation');
239+
dynamic localModelMgr;
240+
try {
241+
localModelMgr = (mediaDomain as dynamic).localModelManager;
242+
} catch (_) {}
243+
237244
_unifiedApiServer = UnifiedApiServer(
238245
requestRouter: _router,
239246
messageHandler: MessageHandler(), // Create new instance for unified API
240247
port: 9529,
241248
pipelineApi: pipelineApi,
242249
onConfigSaved: reloadMediaProviders,
250+
localModelManager: localModelMgr,
243251
);
244252
await _unifiedApiServer!.start();
245253
TerminalUI.success('Unified API server listening on port 9529',

0 commit comments

Comments
 (0)