@@ -786,6 +786,208 @@ def _check_required_files(package: dict, model_path: str) -> None:
786786 if missing :
787787 raise RuntimeError (f"Installed package is missing required files: { missing } " )
788788
789+ @staticmethod
790+ def _strip_package_prefix (remote_path : str , strip_prefix : str ) -> str :
791+ remote = str (remote_path or "" ).replace ("\\ " , "/" ).lstrip ("/" )
792+ prefix = str (strip_prefix or "" ).replace ("\\ " , "/" ).strip ("/" )
793+ if not prefix :
794+ return remote
795+ if remote == prefix :
796+ return ""
797+ head = prefix + "/"
798+ if remote .startswith (head ):
799+ return remote [len (head ) :]
800+ return remote
801+
802+ @staticmethod
803+ def _is_under_root (root : str , path : str ) -> bool :
804+ try :
805+ return os .path .commonpath ([root , path ]) == root
806+ except ValueError :
807+ return False
808+
809+ @classmethod
810+ def _declared_gguf_paths (cls , package : dict , package_root : str ) -> List [str ]:
811+ """GGUF paths declared by package.files (after strip_prefix), if present on disk."""
812+ root = os .path .realpath (package_root )
813+ strip_prefix = str (package .get ("strip_prefix" ) or "" )
814+ found : List [str ] = []
815+ for remote in package .get ("files" ) or []:
816+ remote_s = str (remote or "" )
817+ if not remote_s .lower ().endswith (".gguf" ):
818+ continue
819+ relative = cls ._strip_package_prefix (remote_s , strip_prefix )
820+ if not relative :
821+ continue
822+ candidate = os .path .realpath (os .path .join (root , relative ))
823+ if cls ._is_under_root (root , candidate ) and os .path .isfile (candidate ):
824+ found .append (candidate )
825+ return list (dict .fromkeys (found ))
826+
827+ @classmethod
828+ def _discover_gguf_files (
829+ cls ,
830+ package_root : str ,
831+ * ,
832+ max_depth : int = 4 ,
833+ ) -> List [str ]:
834+ """Find ``*.gguf`` files under a package root (bounded recursive walk)."""
835+ root = os .path .realpath (package_root )
836+ if not os .path .isdir (root ):
837+ return []
838+
839+ found : List [str ] = []
840+ root_depth = root .rstrip (os .sep ).count (os .sep )
841+ for dirpath , dirnames , filenames in os .walk (root ):
842+ # Keep the walk shallow and skip cache / VCS noise.
843+ depth = dirpath .rstrip (os .sep ).count (os .sep ) - root_depth
844+ if depth >= max_depth :
845+ dirnames [:] = []
846+ dirnames [:] = [
847+ name
848+ for name in dirnames
849+ if name not in {".git" , ".cache" , "__pycache__" , ".staging" }
850+ and not name .startswith ("." )
851+ ]
852+ for name in filenames :
853+ if not name .lower ().endswith (".gguf" ):
854+ continue
855+ path = os .path .realpath (os .path .join (dirpath , name ))
856+ if cls ._is_under_root (root , path ) and os .path .isfile (path ):
857+ found .append (path )
858+ found .sort ()
859+ return found
860+
861+ @classmethod
862+ def _select_package_gguf (cls , package : dict , package_root : str ) -> Optional [str ]:
863+ """Choose the package's primary GGUF weight, or None for non-GGUF layouts.
864+
865+ Selection mirrors audio.cpp directory discovery where possible, then falls
866+ back to package declarations and a unique nested GGUF. Ambiguous GGUF
867+ packages raise instead of guessing.
868+ """
869+ root = os .path .realpath (package_root )
870+ if not os .path .isdir (root ):
871+ return None
872+
873+ named = os .path .join (root , "model.gguf" )
874+ if os .path .isfile (named ):
875+ return os .path .realpath (named )
876+
877+ top_level = [
878+ os .path .realpath (os .path .join (root , name ))
879+ for name in sorted (os .listdir (root ))
880+ if name .lower ().endswith (".gguf" )
881+ and os .path .isfile (os .path .join (root , name ))
882+ ]
883+ if len (top_level ) == 1 :
884+ return top_level [0 ]
885+
886+ declared = cls ._declared_gguf_paths (package , root )
887+ if len (declared ) == 1 :
888+ return declared [0 ]
889+
890+ discovered = cls ._discover_gguf_files (root )
891+ # Ignore a root model.gguf symlink target double-count by uniquing.
892+ discovered = list (dict .fromkeys (discovered ))
893+ if len (discovered ) == 1 :
894+ return discovered [0 ]
895+
896+ if len (declared ) > 1 :
897+ raise RuntimeError (
898+ f"Ambiguous GGUF package under { root } : package.files lists "
899+ f"{ len (declared )} GGUF weights. Install a single-weight package "
900+ "or point audiocpp at one file explicitly."
901+ )
902+
903+ expects_gguf = (
904+ str (package .get ("format" ) or "" ).lower () == "gguf"
905+ or any (
906+ str (item or "" ).lower ().endswith (".gguf" )
907+ for item in (package .get ("files" ) or [])
908+ )
909+ )
910+ if expects_gguf and len (discovered ) > 1 :
911+ preview = ", " .join (
912+ os .path .relpath (path , root ) for path in discovered [:6 ]
913+ )
914+ raise RuntimeError (
915+ f"Ambiguous GGUF package under { root } : found { len (discovered )} "
916+ f".gguf files ({ preview } ). audio.cpp only auto-detects a single "
917+ "root GGUF; fix the package layout or declare exactly one "
918+ "package.files GGUF entry."
919+ )
920+ return None
921+
922+ @classmethod
923+ def _ensure_root_model_gguf_link (cls , package_root : str , gguf_path : str ) -> bool :
924+ """Expose a nested GGUF as ``model.gguf`` so audio.cpp directory discovery works.
925+
926+ Returns True when the package root already has, or successfully gains, a
927+ root-level ``model.gguf`` usable by ``find_directory_gguf``.
928+ """
929+ root = os .path .realpath (package_root )
930+ target = os .path .realpath (gguf_path )
931+ if not cls ._is_under_root (root , target ) or not os .path .isfile (target ):
932+ return False
933+
934+ link_path = os .path .join (root , "model.gguf" )
935+ if os .path .isfile (link_path ):
936+ return os .path .realpath (link_path ) == target
937+
938+ if os .path .lexists (link_path ):
939+ # Broken / wrong symlink left behind — replace it.
940+ try :
941+ os .unlink (link_path )
942+ except OSError :
943+ return False
944+
945+ if os .path .dirname (target ) == root :
946+ # Unique top-level GGUF already discoverable; optional convenience link.
947+ rel = os .path .basename (target )
948+ else :
949+ rel = os .path .relpath (target , root )
950+
951+ try :
952+ os .symlink (rel , link_path )
953+ return os .path .isfile (link_path )
954+ except OSError :
955+ # Symlinks unavailable (some Windows/container FS setups): hardlink, then copy.
956+ try :
957+ os .link (target , link_path )
958+ return os .path .isfile (link_path )
959+ except OSError :
960+ try :
961+ shutil .copy2 (target , link_path )
962+ return os .path .isfile (link_path )
963+ except OSError :
964+ return False
965+
966+ @classmethod
967+ def _resolve_installed_model_path (cls , package : dict , package_root : str ) -> str :
968+ """Return the path Studio should inspect / persist for an installed package.
969+
970+ For GGUF packages this normalizes nested layouts (e.g. ``turbo/*.gguf``) to
971+ something audio.cpp can load:
972+ 1. select the primary GGUF (root / declared / unique nested)
973+ 2. publish ``model.gguf`` at the package root when needed
974+ 3. prefer the package directory once discovery works; otherwise the GGUF file
975+ """
976+ root = os .path .realpath (package_root )
977+ if not os .path .isdir (root ):
978+ return package_root
979+
980+ try :
981+ gguf = cls ._select_package_gguf (package , root )
982+ except RuntimeError :
983+ raise
984+ if not gguf :
985+ return root
986+
987+ if cls ._ensure_root_model_gguf_link (root , gguf ):
988+ return root
989+ return gguf
990+
789991 def _model_record (
790992 self ,
791993 package : dict ,
@@ -971,17 +1173,31 @@ async def install_package(
9711173 task_id , package , staging_root , active , options
9721174 )
9731175 self ._check_required_files (package , staged_model_path )
974- inspection = await self ._inspect (
975- task_id ,
976- active ,
977- staged_model_path ,
978- self ._resolve_inspect_family (
979- package = package ,
980- family_hint = str (options .get ("family" ) or "" ) or None ,
981- model_path = staged_model_path ,
982- ),
1176+ runtime_model_path = self ._resolve_installed_model_path (
1177+ package , staged_model_path
9831178 )
984- relative_model_path = os .path .relpath (staged_model_path , staging_root )
1179+ try :
1180+ inspection = await self ._inspect (
1181+ task_id ,
1182+ active ,
1183+ runtime_model_path ,
1184+ self ._resolve_inspect_family (
1185+ package = package ,
1186+ family_hint = str (options .get ("family" ) or "" ) or None ,
1187+ model_path = runtime_model_path ,
1188+ ),
1189+ )
1190+ except RuntimeError as exc :
1191+ detail = str (exc )
1192+ if "missing model package file" in detail and "safetensors" in detail :
1193+ raise RuntimeError (
1194+ f"{ detail } Hint: audio.cpp fell back to the safetensors source, "
1195+ "usually because no discoverable package-root GGUF was present. "
1196+ "Studio tries to publish nested *.gguf weights as model.gguf; "
1197+ "if this persists, the upstream package layout is incomplete."
1198+ ) from exc
1199+ raise
1200+ relative_model_path = os .path .relpath (runtime_model_path , staging_root )
9851201 self .pm .update_task (
9861202 task_id ,
9871203 progress = 95 ,
0 commit comments