@@ -11,6 +11,7 @@ import 'package:opencli_daemon/ipc/ipc_protocol.dart';
1111import 'package:opencli_daemon/api/api_translator.dart' ;
1212import 'package:opencli_daemon/api/message_handler.dart' ;
1313import '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) {
0 commit comments