blechelse-go/main.go
2025-03-30 17:13:48 +02:00

260 lines
7.6 KiB
Go

package main
import (
"encoding/csv"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"github.com/peterh/liner"
"golang.org/x/text/unicode/norm"
)
// Config represents the structure of the config.json file.
type Config struct {
BaseDir string `json:"Base_Dir"`
Modules []Module `json:"Modules"`
}
// Module represents a module entry in the config.json file.
type Module struct {
Name string `json:"Name"`
Path string `json:"Path"`
CSVFile string `json:"CSVFile"`
DefaultLanguage string `json:"Default_Language"`
DefaultVariants []string `json:"Default_Variants"`
}
func main() {
// Open the config.json file
configFilePath := "config.json"
data, err := ioutil.ReadFile(configFilePath)
if err != nil {
log.Fatalf("Error reading file: %v", err)
}
// Parse the JSON data into a Config struct
var config Config
err = json.Unmarshal(data, &config)
if err != nil {
log.Fatalf("Error parsing JSON: %v", err)
}
fmt.Println("Blechelse - waiting for your orders")
interactiveMode(config)
}
// Interactive mode with tab autocomplete and playing audio
func interactiveMode(config Config) {
line := liner.NewLiner()
defer line.Close()
line.SetCtrlCAborts(true)
displayTexts := collectAllDisplayTexts(config)
// Setting up the autocomplete feature
line.SetCompleter(func(line string) (c []string) {
// Split the input line into parts based on commas
words := strings.Split(line, ",")
lastWord := words[len(words)-1]
// Autocomplete only the last word
for _, text := range displayTexts {
if strings.HasPrefix(strings.ToLower(text), strings.ToLower(lastWord)) {
c = append(c, text)
}
}
return
})
var currentInput []string // Store entered words
for {
// Print the current input prompt above the line
printPrompt(currentInput)
// Read user input
inputText, err := line.Prompt("Enter text (or type 'exit' to quit, 'play' to trigger playback): ")
if err != nil {
fmt.Println("Error reading input:", err)
continue
}
inputText = strings.TrimSpace(inputText)
// Exit condition
if inputText == "exit" {
fmt.Println("Goodbye!")
break
}
// If "play" is typed, trigger audio playback
if inputText == "play" {
if len(currentInput) > 0 {
// Concatenate the current input for display and play each part individually
concatenatedPrompt := strings.Join(currentInput, " ")
fmt.Printf("Playing: %s\n", concatenatedPrompt)
// Play each part individually in the order they were added
autocompleteAndPlay(config, currentInput)
currentInput = nil // Clear the input after playing
} else {
fmt.Println("No input to play.")
}
} else if len(inputText) > 0 {
// If it's not "play", it's a part of the input, so add to the current input
currentInput = append(currentInput, inputText)
}
}
}
// Print the current accumulated prompt above the input line
func printPrompt(currentInput []string) {
fmt.Println("Current prompt:")
for i, word := range currentInput {
fmt.Printf("%d. %s\n", i+1, word)
}
}
// Collect all display texts for autocomplete
func collectAllDisplayTexts(config Config) []string {
var displayTexts []string
for _, module := range config.Modules {
// Unpack both return values (displayTexts and fileNames)
moduleDisplayTexts, _ := readDisplayTextsAndFileNames(config.BaseDir + "/" + module.CSVFile)
displayTexts = append(displayTexts, moduleDisplayTexts...)
}
return displayTexts
}
// Function to collect all display texts and play matching files
func autocompleteAndPlay(config Config, inputText []string) {
var matchingFiles []string // Store matching files in the order of inputText
var moduleNames []string // Store corresponding module names for each matched file
// Normalize each input word (e.g., convert umlauts to standard form)
for _, part := range inputText {
normalizedInputText := norm.NFC.String(part)
// Collect matching filenames from the second column (display text) and first column (filenames)
for _, module := range config.Modules {
displayTexts, fileNames := readDisplayTextsAndFileNames(config.BaseDir + "/" + module.CSVFile)
for i, text := range displayTexts {
// Normalize display text before comparing and remove any trailing spaces
normalizedText := norm.NFC.String(strings.TrimSpace(text))
// Explicitly compare both normalized strings (case-sensitive)
if normalizedText == normalizedInputText {
matchingFiles = append(matchingFiles, fileNames[i])
moduleNames = append(moduleNames, module.Name) // Track the module that matched
}
}
}
}
// If no matching filenames found, prompt again
if len(matchingFiles) == 0 {
fmt.Println("No matching display text found.")
return
}
// Debugging: print matched files and their corresponding modules
fmt.Println("Matched files:")
for i, fileName := range matchingFiles {
fmt.Printf(" - File: %s, Module: %s\n", fileName, moduleNames[i])
}
// Play matching audio files in the exact order they were entered
for i, fileName := range matchingFiles {
// Get the module name associated with this matched file
moduleName := moduleNames[i]
// Construct the correct audio path using the module information
audioPath := constructAudioPath(config, fileName, moduleName)
// Debugging: print the constructed audio path
fmt.Printf("Attempting to play audio from path: %s\n", audioPath)
// Check if the file exists before trying to play it
if _, err := os.Stat(audioPath); os.IsNotExist(err) {
fmt.Printf("File does not exist: %s\n", audioPath)
continue
}
// Play the audio file
err := playAudioFiles(audioPath)
if err != nil {
fmt.Printf("Error playing audio: %v\n", err)
}
}
}
// Function to construct the audio file path with language and variant considerations
func constructAudioPath(config Config, fileName, moduleName string) string {
var audioPath string
var languagePart, variantPart string
// Find the module for which we need to construct the path
var selectedModule Module
for _, module := range config.Modules {
if module.Name == moduleName {
selectedModule = module
break
}
}
// If a language is set in the selected module, use it
if len(selectedModule.DefaultLanguage) > 0 {
languagePart = selectedModule.DefaultLanguage + "/"
}
// If a variant is set in the selected module, use it
if len(selectedModule.DefaultVariants) > 0 {
variantPart = selectedModule.DefaultVariants[0] + "/"
}
// Construct the path
audioPath = fmt.Sprintf("%s/%s%s/%s%s", config.BaseDir, languagePart, selectedModule.Path, variantPart, fileName)
return audioPath
}
// Function to read display texts (second column) and filenames (first column) from a CSV file
func readDisplayTextsAndFileNames(filePath string) ([]string, []string) {
file, err := os.Open(filePath)
if err != nil {
log.Fatalf("Error opening CSV file: %v", err)
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
log.Fatalf("Error reading CSV records: %v", err)
}
var displayTexts, fileNames []string
for _, record := range records {
if len(record) > 1 {
fileNames = append(fileNames, record[0]) // First column is the filename
displayTexts = append(displayTexts, record[1]) // Second column is the display text
}
}
return displayTexts, fileNames
}
// Function to play audio files based on the file name
func playAudioFiles(filePath string) error {
// Debugging: print the file path being played
fmt.Printf("Playing audio from: %s\n", filePath)
cmd := exec.Command("ffplay", "-nodisp", "-autoexit", filePath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("Error playing audio file: %v", err)
}
return nil
}