704de036b6595fdb8229e89df9674e97992b1817

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

config: add module, layout, endpoint/recorder config and command parsing

- go module 'japanese' with cmd/jp entry point
- internal/config: flags + env vars for LLM/TTS/STT endpoints and recorder
  command, with defaults from the service contract
- jp run / jp persona gen subcommand parsing (no game logic yet)
- .gitignore for Go build artifacts

Diff

  1diff --git a/.gitignore b/.gitignore
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..11a6da05b5a2c2e303648d49707eab8ab6135961
  4--- /dev/null
  5+++ b/.gitignore
  6@@ -0,0 +1,7 @@
  7+# Go build artifacts
  8+/jp
  9+/bin/
 10+*.test
 11+
 12+# Local temporary output
 13+/tmp/
 14diff --git a/assets/maps/.gitkeep b/assets/maps/.gitkeep
 15new file mode 100644
 16index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
 17--- /dev/null
 18+++ b/assets/maps/.gitkeep
 19diff --git a/assets/personas/.gitkeep b/assets/personas/.gitkeep
 20new file mode 100644
 21index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
 22--- /dev/null
 23+++ b/assets/personas/.gitkeep
 24diff --git a/cmd/jp/main.go b/cmd/jp/main.go
 25new file mode 100644
 26index 0000000000000000000000000000000000000000..99e4880c4456e374dc1ba0dc9505499e78a12e49
 27--- /dev/null
 28+++ b/cmd/jp/main.go
 29@@ -0,0 +1,71 @@
 30+// Command jp is the entry point for the Japanese learning RPG.
 31+package main
 32+
 33+import (
 34+	"flag"
 35+	"fmt"
 36+	"io"
 37+	"os"
 38+
 39+	"japanese/internal/config"
 40+)
 41+
 42+func main() {
 43+	if len(os.Args) < 2 {
 44+		usage(os.Stderr)
 45+		os.Exit(1)
 46+	}
 47+	switch os.Args[1] {
 48+	case "run":
 49+		runCmd(os.Args[2:])
 50+	case "persona":
 51+		personaCmd(os.Args[2:])
 52+	case "help", "-h", "--help":
 53+		usage(os.Stdout)
 54+	default:
 55+		fmt.Fprintf(os.Stderr, "jp: unknown command %q\n\n", os.Args[1])
 56+		usage(os.Stderr)
 57+		os.Exit(1)
 58+	}
 59+}
 60+
 61+func runCmd(args []string) {
 62+	fs := flag.NewFlagSet("run", flag.ExitOnError)
 63+	var cfg config.Config
 64+	cfg.RegisterFlags(fs)
 65+	fs.Parse(args)
 66+	fmt.Fprintln(os.Stderr, "jp run: not implemented yet")
 67+}
 68+
 69+func personaCmd(args []string) {
 70+	if len(args) == 0 || args[0] != "gen" {
 71+		fmt.Fprintln(os.Stderr, "jp persona: expected subcommand 'gen'")
 72+		os.Exit(1)
 73+	}
 74+	genPersonaCmd(args[1:])
 75+}
 76+
 77+func genPersonaCmd(args []string) {
 78+	fs := flag.NewFlagSet("persona gen", flag.ExitOnError)
 79+	var cfg config.Config
 80+	cfg.RegisterFlags(fs)
 81+	role := fs.String("role", "", "Role to author a persona for (required)")
 82+	fs.Parse(args)
 83+	if *role == "" {
 84+		fmt.Fprintln(os.Stderr, "jp persona gen: --role is required")
 85+		os.Exit(1)
 86+	}
 87+	fmt.Fprintf(os.Stderr, "jp persona gen %q: not implemented yet\n", *role)
 88+}
 89+
 90+func usage(w io.Writer) {
 91+	fmt.Fprint(w, `jp - Japanese learning RPG
 92+
 93+Usage:
 94+  jp run [flags]           Run the game (not implemented yet)
 95+  jp persona gen --role R  Author a persona draft (not implemented yet)
 96+  jp help                  Show this help
 97+
 98+Run "jp run -help" or "jp persona gen -help" for flag details.
 99+`)
100+}
101diff --git a/go.mod b/go.mod
102new file mode 100644
103index 0000000000000000000000000000000000000000..5d960a5762a954d51bb93c6efc14e92b064a8e56
104--- /dev/null
105+++ b/go.mod
106@@ -0,0 +1,3 @@
107+module japanese
108+
109+go 1.27.0
110diff --git a/internal/config/config.go b/internal/config/config.go
111new file mode 100644
112index 0000000000000000000000000000000000000000..cebcaa07692c1975ed95311f7c0e38957a341713
113--- /dev/null
114+++ b/internal/config/config.go
115@@ -0,0 +1,47 @@
116+// Package config holds endpoint and recorder configuration for the game.
117+// Values come from flags and environment variables; flags win over env vars,
118+// which win over built-in defaults.
119+package config
120+
121+import (
122+	"flag"
123+	"os"
124+)
125+
126+const (
127+	DefaultLLMBaseURL    = "http://127.0.0.1:8081/v1"
128+	DefaultTTSBaseURL    = "http://127.0.0.1:8080/v1"
129+	DefaultSTTURL        = "http://127.0.0.1:8178/inference"
130+	DefaultSTTLanguage   = "auto"
131+	DefaultRecordCommand = "arecord"
132+
133+	EnvLLMBaseURL  = "JP_LLM_BASE_URL"
134+	EnvTTSBaseURL  = "JP_TTS_BASE_URL"
135+	EnvSTTURL      = "JP_STT_URL"
136+	EnvSTTLanguage = "JP_STT_LANGUAGE"
137+)
138+
139+type Config struct {
140+	LLMBaseURL    string
141+	TTSBaseURL    string
142+	STTURL        string
143+	STTLanguage   string
144+	RecordCommand string
145+}
146+
147+func envOrDefault(key, fallback string) string {
148+	if value := os.Getenv(key); value != "" {
149+		return value
150+	}
151+	return fallback
152+}
153+
154+// RegisterFlags adds the service endpoint and recorder flags to fs, seeding
155+// each default from the matching environment variable.
156+func (c *Config) RegisterFlags(fs *flag.FlagSet) {
157+	fs.StringVar(&c.LLMBaseURL, "llm-url", envOrDefault(EnvLLMBaseURL, DefaultLLMBaseURL), "OpenAI-compatible LLM base URL")
158+	fs.StringVar(&c.TTSBaseURL, "tts-url", envOrDefault(EnvTTSBaseURL, DefaultTTSBaseURL), "OpenAI-compatible TTS base URL")
159+	fs.StringVar(&c.STTURL, "stt-url", envOrDefault(EnvSTTURL, DefaultSTTURL), "Whisper inference URL")
160+	fs.StringVar(&c.STTLanguage, "stt-language", envOrDefault(EnvSTTLanguage, DefaultSTTLanguage), "Optional Whisper request language")
161+	fs.StringVar(&c.RecordCommand, "record-command", DefaultRecordCommand, "Recorder command for 16kHz mono S16_LE WAV capture")
162+}
163diff --git a/internal/config/config_test.go b/internal/config/config_test.go
164new file mode 100644
165index 0000000000000000000000000000000000000000..084804e157950aa7fd70170e0361e8bd6cd8b22a
166--- /dev/null
167+++ b/internal/config/config_test.go
168@@ -0,0 +1,74 @@
169+package config
170+
171+import (
172+	"flag"
173+	"testing"
174+)
175+
176+func newConfig(t *testing.T, args ...string) Config {
177+	t.Helper()
178+	fs := flag.NewFlagSet("test", flag.ContinueOnError)
179+	var c Config
180+	c.RegisterFlags(fs)
181+	if err := fs.Parse(args); err != nil {
182+		t.Fatalf("parse: %v", err)
183+	}
184+	return c
185+}
186+
187+func clearEnv(t *testing.T) {
188+	t.Helper()
189+	for _, key := range []string{EnvLLMBaseURL, EnvTTSBaseURL, EnvSTTURL, EnvSTTLanguage} {
190+		t.Setenv(key, "")
191+	}
192+}
193+
194+func TestDefaults(t *testing.T) {
195+	clearEnv(t)
196+	c := newConfig(t)
197+	if c.LLMBaseURL != DefaultLLMBaseURL {
198+		t.Errorf("llm-url = %q, want %q", c.LLMBaseURL, DefaultLLMBaseURL)
199+	}
200+	if c.TTSBaseURL != DefaultTTSBaseURL {
201+		t.Errorf("tts-url = %q, want %q", c.TTSBaseURL, DefaultTTSBaseURL)
202+	}
203+	if c.STTURL != DefaultSTTURL {
204+		t.Errorf("stt-url = %q, want %q", c.STTURL, DefaultSTTURL)
205+	}
206+	if c.STTLanguage != DefaultSTTLanguage {
207+		t.Errorf("stt-language = %q, want %q", c.STTLanguage, DefaultSTTLanguage)
208+	}
209+	if c.RecordCommand != DefaultRecordCommand {
210+		t.Errorf("record-command = %q, want %q", c.RecordCommand, DefaultRecordCommand)
211+	}
212+}
213+
214+func TestEnvOverride(t *testing.T) {
215+	clearEnv(t)
216+	t.Setenv(EnvLLMBaseURL, "http://env-llm/v1")
217+	t.Setenv(EnvTTSBaseURL, "http://env-tts/v1")
218+	t.Setenv(EnvSTTURL, "http://env-stt/inference")
219+	t.Setenv(EnvSTTLanguage, "ja")
220+	c := newConfig(t)
221+	if c.LLMBaseURL != "http://env-llm/v1" {
222+		t.Errorf("llm-url = %q, want env value", c.LLMBaseURL)
223+	}
224+	if c.TTSBaseURL != "http://env-tts/v1" {
225+		t.Errorf("tts-url = %q, want env value", c.TTSBaseURL)
226+	}
227+	if c.STTURL != "http://env-stt/inference" {
228+		t.Errorf("stt-url = %q, want env value", c.STTURL)
229+	}
230+	if c.STTLanguage != "ja" {
231+		t.Errorf("stt-language = %q, want env value", c.STTLanguage)
232+	}
233+}
234+
235+func TestFlagBeatsEnv(t *testing.T) {
236+	clearEnv(t)
237+	t.Setenv(EnvLLMBaseURL, "http://env-llm/v1")
238+	c := newConfig(t, "-llm-url", "http://flag-llm/v1")
239+	if c.LLMBaseURL != "http://flag-llm/v1" {
240+		t.Errorf("llm-url = %q, want flag value", c.LLMBaseURL)
241+	}
242+}
243diff --git a/internal/game/.gitkeep b/internal/game/.gitkeep
244new file mode 100644
245index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
246--- /dev/null
247+++ b/internal/game/.gitkeep
248diff --git a/internal/llm/.gitkeep b/internal/llm/.gitkeep
249new file mode 100644
250index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
251--- /dev/null
252+++ b/internal/llm/.gitkeep
253diff --git a/internal/persona/.gitkeep b/internal/persona/.gitkeep
254new file mode 100644
255index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
256--- /dev/null
257+++ b/internal/persona/.gitkeep
258diff --git a/internal/stt/.gitkeep b/internal/stt/.gitkeep
259new file mode 100644
260index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
261--- /dev/null
262+++ b/internal/stt/.gitkeep
263diff --git a/internal/tts/.gitkeep b/internal/tts/.gitkeep
264new file mode 100644
265index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
266--- /dev/null
267+++ b/internal/tts/.gitkeep
268diff --git a/internal/ui/.gitkeep b/internal/ui/.gitkeep
269new file mode 100644
270index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
271--- /dev/null
272+++ b/internal/ui/.gitkeep