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