201 lines
6.5 KiB
Go
201 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/ghodss/yaml"
|
|
"github.com/mudler/LocalAI/core/gallery/importers"
|
|
)
|
|
|
|
func formatTextContent(text string) string {
|
|
return formatTextContentWithIndent(text, 4, 6)
|
|
}
|
|
|
|
// formatTextContentWithIndent formats text content with specified base and list item indentation
|
|
func formatTextContentWithIndent(text string, baseIndent int, listItemIndent int) string {
|
|
var formattedLines []string
|
|
lines := strings.Split(text, "\n")
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimRight(line, " \t\r")
|
|
if trimmed == "" {
|
|
// Keep empty lines as empty (no indentation)
|
|
formattedLines = append(formattedLines, "")
|
|
} else {
|
|
// Preserve relative indentation from yaml.Marshal output
|
|
// Count existing leading spaces to preserve relative structure
|
|
leadingSpaces := len(trimmed) - len(strings.TrimLeft(trimmed, " \t"))
|
|
trimmedStripped := strings.TrimLeft(trimmed, " \t")
|
|
|
|
var totalIndent int
|
|
if strings.HasPrefix(trimmedStripped, "-") {
|
|
// List items: use listItemIndent (ignore existing leading spaces)
|
|
totalIndent = listItemIndent
|
|
} else {
|
|
// Regular lines: use baseIndent + preserve relative indentation
|
|
// This handles both top-level keys (leadingSpaces=0) and nested properties (leadingSpaces>0)
|
|
totalIndent = baseIndent + leadingSpaces
|
|
}
|
|
|
|
indentStr := strings.Repeat(" ", totalIndent)
|
|
formattedLines = append(formattedLines, indentStr+trimmedStripped)
|
|
}
|
|
}
|
|
formattedText := strings.Join(formattedLines, "\n")
|
|
// Remove any trailing spaces from the formatted description
|
|
formattedText = strings.TrimRight(formattedText, " \t")
|
|
return formattedText
|
|
}
|
|
|
|
// generateYAMLEntry generates a YAML entry for a model using the specified anchor
|
|
func generateYAMLEntry(model ProcessedModel, quantization string) string {
|
|
modelConfig, err := importers.DiscoverModelConfig("https://huggingface.co/"+model.ModelID, json.RawMessage(`{ "quantization": "`+quantization+`"}`))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Extract model name from ModelID
|
|
parts := strings.Split(model.ModelID, "/")
|
|
modelName := model.ModelID
|
|
if len(parts) > 0 {
|
|
modelName = strings.ToLower(parts[len(parts)-1])
|
|
}
|
|
// Remove common suffixes
|
|
modelName = strings.ReplaceAll(modelName, "-gguf", "")
|
|
modelName = strings.ReplaceAll(modelName, "-q4_k_m", "")
|
|
modelName = strings.ReplaceAll(modelName, "-q4_k_s", "")
|
|
modelName = strings.ReplaceAll(modelName, "-q3_k_m", "")
|
|
modelName = strings.ReplaceAll(modelName, "-q2_k", "")
|
|
|
|
description := model.ReadmeContent
|
|
if description == "" {
|
|
description = fmt.Sprintf("AI model: %s", modelName)
|
|
}
|
|
|
|
// Clean up description to prevent YAML linting issues
|
|
description = cleanTextContent(description)
|
|
formattedDescription := formatTextContent(description)
|
|
|
|
configFile := formatTextContent(modelConfig.ConfigFile)
|
|
|
|
filesYAML, _ := yaml.Marshal(modelConfig.Files)
|
|
|
|
// Files section: list items need 4 spaces (not 6), since files: is at 2 spaces
|
|
files := formatTextContentWithIndent(string(filesYAML), 4, 4)
|
|
|
|
// Build metadata sections
|
|
var metadataSections []string
|
|
|
|
// Add license if present
|
|
if model.License != "" {
|
|
metadataSections = append(metadataSections, fmt.Sprintf(` license: "%s"`, model.License))
|
|
}
|
|
|
|
// Add tags if present
|
|
if len(model.Tags) > 0 {
|
|
tagsYAML, _ := yaml.Marshal(model.Tags)
|
|
tagsFormatted := formatTextContentWithIndent(string(tagsYAML), 4, 4)
|
|
tagsFormatted = strings.TrimRight(tagsFormatted, "\n")
|
|
metadataSections = append(metadataSections, fmt.Sprintf(" tags:\n%s", tagsFormatted))
|
|
}
|
|
|
|
// Add icon if present
|
|
if model.Icon != "" {
|
|
metadataSections = append(metadataSections, fmt.Sprintf(` icon: %s`, model.Icon))
|
|
}
|
|
|
|
// Build the metadata block
|
|
metadataBlock := ""
|
|
if len(metadataSections) > 0 {
|
|
metadataBlock = strings.Join(metadataSections, "\n") + "\n"
|
|
}
|
|
|
|
yamlTemplate := ""
|
|
yamlTemplate = `- name: "%s"
|
|
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
|
urls:
|
|
- https://huggingface.co/%s
|
|
description: |
|
|
%s%s
|
|
overrides:
|
|
%s
|
|
files:
|
|
%s`
|
|
// Trim trailing newlines from formatted sections to prevent extra blank lines
|
|
formattedDescription = strings.TrimRight(formattedDescription, "\n")
|
|
configFile = strings.TrimRight(configFile, "\n")
|
|
files = strings.TrimRight(files, "\n")
|
|
// Add newline before metadata block if present
|
|
if metadataBlock != "" {
|
|
metadataBlock = "\n" + strings.TrimRight(metadataBlock, "\n")
|
|
}
|
|
return fmt.Sprintf(yamlTemplate,
|
|
modelName,
|
|
model.ModelID,
|
|
formattedDescription,
|
|
metadataBlock,
|
|
configFile,
|
|
files,
|
|
)
|
|
}
|
|
|
|
// generateYAMLForModels generates YAML entries for selected models and appends to index.yaml
|
|
func generateYAMLForModels(ctx context.Context, models []ProcessedModel, quantization string) error {
|
|
|
|
// Generate YAML entries for each model
|
|
var yamlEntries []string
|
|
for _, model := range models {
|
|
fmt.Printf("Generating YAML entry for model: %s\n", model.ModelID)
|
|
|
|
// Generate YAML entry
|
|
yamlEntry := generateYAMLEntry(model, quantization)
|
|
yamlEntries = append(yamlEntries, yamlEntry)
|
|
}
|
|
|
|
// Prepend to index.yaml (write at the top)
|
|
if len(yamlEntries) > 0 {
|
|
indexPath := getGalleryIndexPath()
|
|
fmt.Printf("Prepending YAML entries to %s...\n", indexPath)
|
|
|
|
// Read current content
|
|
content, err := os.ReadFile(indexPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read %s: %w", indexPath, err)
|
|
}
|
|
|
|
existingContent := string(content)
|
|
yamlBlock := strings.Join(yamlEntries, "\n")
|
|
|
|
// Check if file starts with "---"
|
|
var newContent string
|
|
if strings.HasPrefix(existingContent, "---\n") {
|
|
// File starts with "---", prepend new entries after it
|
|
restOfContent := strings.TrimPrefix(existingContent, "---\n")
|
|
// Ensure proper spacing: "---\n" + new entries + "\n" + rest of content
|
|
newContent = "---\n" + yamlBlock + "\n" + restOfContent
|
|
} else if strings.HasPrefix(existingContent, "---") {
|
|
// File starts with "---" but no newline after
|
|
restOfContent := strings.TrimPrefix(existingContent, "---")
|
|
newContent = "---\n" + yamlBlock + "\n" + strings.TrimPrefix(restOfContent, "\n")
|
|
} else {
|
|
// No "---" at start, prepend new entries at the very beginning
|
|
// Trim leading whitespace from existing content
|
|
existingContent = strings.TrimLeft(existingContent, " \t\n\r")
|
|
newContent = yamlBlock + "\n" + existingContent
|
|
}
|
|
|
|
// Write back to file
|
|
err = os.WriteFile(indexPath, []byte(newContent), 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write %s: %w", indexPath, err)
|
|
}
|
|
|
|
fmt.Printf("Successfully prepended %d models to %s\n", len(yamlEntries), indexPath)
|
|
}
|
|
|
|
return nil
|
|
}
|