Skip to content

Commit b621919

Browse files
authored
feat(verse): new engine verse insert (#9)
* feat(verse): new engine verse insert workflow
1 parent 6461326 commit b621919

20 files changed

Lines changed: 730 additions & 114 deletions

File tree

adapter/verse_management.go

Lines changed: 0 additions & 79 deletions
This file was deleted.

cmd/lab/verse/breakdown.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package verse
2+
3+
import (
4+
"database/sql"
5+
"strings"
6+
"unicode"
7+
8+
"github.com/jmoiron/sqlx"
9+
)
10+
11+
type WordBreakdown struct {
12+
Word string `json:"word"`
13+
Breakdown []string
14+
}
15+
16+
// WordResult holds per-word processing output.
17+
type WordResult struct {
18+
Breakdown string
19+
InDB bool
20+
}
21+
22+
// splitTrailing splits a word into its alpha core and trailing symbols.
23+
// Trailing = punctuation (.,!?;:) and quotes (" ' ")
24+
func splitTrailing(word string) (core, trail string) {
25+
i := len(word)
26+
for i > 0 {
27+
r := rune(word[i-1])
28+
if unicode.IsPunct(r) || r == '"' {
29+
i--
30+
} else {
31+
break
32+
}
33+
}
34+
return word[:i], word[i:]
35+
}
36+
37+
// isElided returns true and strips the underscore prefix.
38+
func isElided(word string) (string, bool) {
39+
if strings.HasPrefix(word, "_") {
40+
return word[1:], true
41+
}
42+
return word, false
43+
}
44+
45+
// applyElision wraps the elision_index-th syllable (1-based) in parentheses.
46+
// e.g. breakdown="ma-lai-kat", elisionIndex=1 → "ma-(lai)-kat"
47+
func applyElision(breakdown string, elisionIndex int) string {
48+
parts := strings.Split(breakdown, "-")
49+
if elisionIndex+1 > len(parts) {
50+
return breakdown
51+
}
52+
parts[elisionIndex] = "_" + parts[elisionIndex]
53+
return strings.Join(parts, "-")
54+
}
55+
56+
// fallbackBreakdown is your own algorithm for words not in DB.
57+
// Replace the body with your real implementation.
58+
func fallbackBreakdown(word string) string {
59+
return strings.Join(SplitSyllable(word), "-")
60+
}
61+
62+
// ProcessSentence looks up each word in the DB, applies elision if marked,
63+
// falls back to your algorithm for unknown words, and preserves casing/trailing symbols.
64+
func ProcessSentence(db *sqlx.DB, sentence string) ([]string, map[string]string, error) {
65+
notInDB := map[string]string{}
66+
result := []string{}
67+
68+
lines := strings.Split(sentence, "\n")
69+
70+
for _, line := range lines {
71+
tokens := strings.Fields(line)
72+
73+
if len(tokens) == 0 {
74+
continue
75+
}
76+
77+
var breakdownParts []string
78+
for _, token := range tokens {
79+
80+
// 1. detect elision marker
81+
raw, elided := isElided(token)
82+
83+
// 2. split trailing punctuation/quotes
84+
core, trail := splitTrailing(raw)
85+
86+
pretrail := ""
87+
if core[0] == '"' || core[0] == '\'' {
88+
pretrail = string(core[0])
89+
core = core[1:]
90+
} else if unicode.IsNumber(rune(core[0])) {
91+
breakdownParts = append(breakdownParts, core+trail)
92+
continue
93+
94+
}
95+
96+
// 3. preserve original casing for output; query in lowercase
97+
lookup := strings.ToLower(core)
98+
99+
// 4. query DB
100+
bd, elisionIdx, found, err := queryWord(db, lookup, elided)
101+
if err != nil {
102+
return nil, nil, err
103+
}
104+
var wordBreakdown string
105+
106+
if found {
107+
if elided && elisionIdx.Valid {
108+
wordBreakdown = applyElision(bd, int(elisionIdx.Int64))
109+
} else {
110+
wordBreakdown = bd
111+
}
112+
} else {
113+
// // not in DB — use fallback algorithm
114+
// notInDB = append(notInDB, core) // original casing, no trail
115+
wordBreakdown = fallbackBreakdown(lookup)
116+
if elided {
117+
wordBreakdown = "_" + wordBreakdown
118+
lookup = "_" + lookup
119+
}
120+
notInDB[lookup] = wordBreakdown
121+
122+
}
123+
124+
res := syncCasing(core, wordBreakdown)
125+
126+
// 5. re-attach trailing symbols
127+
breakdownParts = append(breakdownParts, pretrail+res+trail)
128+
}
129+
result = append(result, strings.Join(breakdownParts, " "))
130+
}
131+
132+
return result, notInDB, nil
133+
134+
}
135+
136+
func syncCasing(original, hyphenated string) string {
137+
var result strings.Builder
138+
origRunes := []rune(original)
139+
origIdx := 0
140+
141+
prefixes := map[int]string{}
142+
143+
for _, char := range hyphenated {
144+
if char == '-' {
145+
result.WriteRune('-')
146+
continue
147+
}
148+
149+
if char == '_' {
150+
prefixes[origIdx] = "_"
151+
continue
152+
}
153+
154+
if p, ok := prefixes[origIdx]; ok {
155+
result.WriteRune(rune(p[0]))
156+
}
157+
158+
// If the original character was uppercase, make this one uppercase
159+
if origIdx < len(origRunes) && unicode.IsUpper(origRunes[origIdx]) {
160+
result.WriteRune(unicode.ToUpper(char))
161+
} else {
162+
result.WriteRune(unicode.ToLower(char))
163+
}
164+
origIdx++
165+
}
166+
167+
return result.String()
168+
}
169+
170+
// queryWord fetches breakdown and elision_index from DB.
171+
// When elided=true it looks for the row WHERE elision_index IS NOT NULL,
172+
// otherwise WHERE elision_index IS NULL.
173+
func queryWord(db *sqlx.DB, word string, elided bool) (breakdown string, elisionIdx sql.NullInt64, found bool, err error) {
174+
var query string
175+
if elided {
176+
query = `
177+
SELECT breakdown, elision_index
178+
FROM syllable_breakdown
179+
WHERE whole = ? AND elision_index IS NOT NULL
180+
LIMIT 1`
181+
} else {
182+
query = `
183+
SELECT breakdown, elision_index
184+
FROM syllable_breakdown
185+
WHERE whole = ? AND elision_index IS NULL
186+
LIMIT 1`
187+
}
188+
row := db.QueryRow(query, strings.ToLower(word))
189+
err = row.Scan(&breakdown, &elisionIdx)
190+
if err == sql.ErrNoRows {
191+
return "", sql.NullInt64{}, false, nil
192+
}
193+
if err != nil {
194+
return "", sql.NullInt64{}, false, err
195+
}
196+
return breakdown, elisionIdx, true, nil
197+
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package lyric
1+
package verse
22

33
import (
44
"strings"
@@ -7,7 +7,6 @@ import (
77
"github.com/jodi-ivan/numbered-notation-xml/internal/utils"
88
)
99

10-
// FIXME: number of syllable is equal to number of vowel. it will be false, if word kacau should be splitted [ka][cau]
1110
func SplitSyllable(word string) []string {
1211
vowels := []string{"a", "i", "u", "e", "o", "A", "I", "U", "E", "O"}
1312

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package lyric
1+
package verse
22

33
import (
44
"testing"
Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import (
66
"net/http"
77
"strings"
88

9-
"github.com/jodi-ivan/numbered-notation-xml/internal/lyric"
9+
"github.com/jmoiron/sqlx"
10+
"github.com/jodi-ivan/numbered-notation-xml/cmd/lab/verse"
1011
"github.com/julienschmidt/httprouter"
1112
)
1213

@@ -37,7 +38,7 @@ func (lp *LyricParser) ServeHTTP(w http.ResponseWriter, r *http.Request, ps http
3738
continue
3839
}
3940
for _, w := range words {
40-
syllable := lyric.SplitSyllable(w)
41+
syllable := verse.SplitSyllable(w)
4142
line = append(line, WordBreakdown{
4243
Word: w,
4344
Breakdown: syllable,
@@ -50,3 +51,34 @@ func (lp *LyricParser) ServeHTTP(w http.ResponseWriter, r *http.Request, ps http
5051
w.WriteHeader(http.StatusOK)
5152
w.Write(raw)
5253
}
54+
55+
type LyricParserV2 struct {
56+
Db *sqlx.DB
57+
}
58+
59+
func (lpv2 LyricParserV2) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
60+
b, err := io.ReadAll(r.Body)
61+
if err != nil {
62+
w.WriteHeader(http.StatusInternalServerError)
63+
w.Write([]byte(err.Error()))
64+
return
65+
}
66+
input := strings.ReplaceAll(strings.TrimSpace(string(b)), "\\t", "")
67+
breakdown, notindb, err := verse.ProcessSentence(lpv2.Db, input)
68+
if err != nil {
69+
w.WriteHeader(http.StatusInternalServerError)
70+
w.Write([]byte(err.Error()))
71+
return
72+
}
73+
74+
data := map[string]interface{}{
75+
"breakdown": breakdown,
76+
"generated": notindb,
77+
}
78+
79+
raw, _ := json.MarshalIndent(data, "", " ")
80+
w.Header().Set("Content-Type", "application/json")
81+
w.WriteHeader(http.StatusOK)
82+
w.Write(raw)
83+
84+
}

0 commit comments

Comments
 (0)