From 1ce2ab2b242883cc5ee3dd76d982656a14270d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Thu, 13 Aug 2026 13:33:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=BC=E9=9F=B3=E5=86=99=E6=97=B6?= =?UTF-8?q?=E6=A0=87=E6=B3=A8=E6=9C=8D=E5=8A=A1=EF=BC=88=E8=AF=8D=E8=A1=A8?= =?UTF-8?q?=E6=9C=80=E9=95=BF=E5=8C=B9=E9=85=8D=20+=20=E9=80=90=E5=AD=97?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20JSON=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- biz/consts/pinyin.go | 51 ++++++++++++++++++++++++++++++++ common/pinyin.go | 69 +++++++++++++++++++++++++++++++++++++++++++ common/pinyin_test.go | 61 ++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ 5 files changed, 184 insertions(+) create mode 100644 biz/consts/pinyin.go create mode 100644 common/pinyin.go create mode 100644 common/pinyin_test.go diff --git a/biz/consts/pinyin.go b/biz/consts/pinyin.go new file mode 100644 index 0000000..048d7a5 --- /dev/null +++ b/biz/consts/pinyin.go @@ -0,0 +1,51 @@ +package consts + +// PinyinOverrides 拼音标注词表:整词 override(成语整体、专名),标注时最长匹配优先, +// 保证教育内容多音字准确(如「声东击西」不能拆成 zhong dong ji xi)。 +var PinyinOverrides = map[string]string{ + "瞒天过海": "mán tiān guò hǎi", + "围魏救赵": "wéi wèi jiù zhào", + "借刀杀人": "jiè dāo shā rén", + "以逸待劳": "yǐ yì dài láo", + "趁火打劫": "chèn huǒ dǎ jié", + "声东击西": "shēng dōng jī xī", + "无中生有": "wú zhōng shēng yǒu", + "暗渡陈仓": "àn dù chén cāng", + "隔岸观火": "gé àn guān huǒ", + "笑里藏刀": "xiào lǐ cáng dāo", + "李代桃僵": "lǐ dài táo jiāng", + "顺手牵羊": "shùn shǒu qiān yáng", + "打草惊蛇": "dǎ cǎo jīng shé", + "借尸还魂": "jiè shī huán hún", + "调虎离山": "diào hǔ lí shān", + "欲擒故纵": "yù qín gù zòng", + "抛砖引玉": "pāo zhuān yǐn yù", + "擒贼擒王": "qín zéi qín wáng", + "釜底抽薪": "fǔ dǐ chōu xīn", + "混水摸鱼": "hún shuǐ mō yú", + "金蝉脱壳": "jīn chán tuō qiào", + "关门捉贼": "guān mén zhuō zéi", + "远交近攻": "yuǎn jiāo jìn gōng", + "假道伐虢": "jiǎ dào fá guó", + "偷梁换柱": "tōu liáng huàn zhù", + "指桑骂槐": "zhǐ sāng mà huái", + "假痴不癫": "jiǎ chī bù diān", + "上屋抽梯": "shàng wū chōu tī", + "树上开花": "shù shàng kāi huā", + "反客为主": "fǎn kè wéi zhǔ", + "美人计": "měi rén jì", + "空城计": "kōng chéng jì", + "反间计": "fǎn jiàn jì", + "苦肉计": "kǔ ròu jì", + "连环计": "lián huán jì", + "走为上计": "zǒu wéi shàng jì", + // 种子内容常用专名(扫描 seed_36_ji/*.json 的人物/场景名补充,发现多音误标就加) + "小明": "xiǎo míng", + "小虎": "xiǎo hǔ", + "朵朵": "duǒ duǒ", + "幼儿园": "yòu ér yuán", + "足球场": "zú qiú chǎng", + "游乐场": "yóu lè chǎng", + "小兔子": "xiǎo tù zi", + "大哥哥": "dà gē ge", +} diff --git a/common/pinyin.go b/common/pinyin.go new file mode 100644 index 0000000..f97ff72 --- /dev/null +++ b/common/pinyin.go @@ -0,0 +1,69 @@ +package common + +import ( + "encoding/json" + "sort" + "strings" + "unicode" + + "github.com/mozillazg/go-pinyin" + + "36wisdom/biz/consts" +) + +// AnnotatePinyin 写时一次性标注:词表最长匹配优先(整体转拼音),未命中逐字转 +// (pinyin.Tone 带声调);返回逐字对齐的 JSON 数组字符串,非汉字对应空串, +// 下标与原文 rune 一一对应。纯函数,文本未变结果不变(幂等)。 +func AnnotatePinyin(text string) string { + runes := []rune(text) + out := make([]string, len(runes)) + if len(runes) == 0 { + return "[]" + } + + words := make([]string, 0, len(consts.PinyinOverrides)) + for w := range consts.PinyinOverrides { + words = append(words, w) + } + sort.Slice(words, func(i, j int) bool { return len(words[i]) > len(words[j]) }) + + // 词表最长匹配切分:命中片段整体标注(词表拼音音节数须与字数一致) + for i := 0; i < len(runes); { + matched := false + for _, w := range words { + n := len([]rune(w)) + if i+n > len(runes) || string(runes[i:i+n]) != w { + continue + } + pys := strings.Fields(consts.PinyinOverrides[w]) + if len(pys) != n { + continue + } + copy(out[i:i+n], pys) + i += n + matched = true + break + } + if !matched { + i++ + } + } + + // 未命中汉字逐字转拼音;pi 与 pinyin.Pinyin 的汉字输出顺序同步推进 + args := pinyin.NewArgs() + args.Style = pinyin.Tone + all := pinyin.Pinyin(string(runes), args) + pi := 0 + for idx, r := range runes { + if !unicode.Is(unicode.Han, r) { + continue + } + if out[idx] == "" && pi < len(all) && len(all[pi]) > 0 { + out[idx] = all[pi][0] + } + pi++ + } + + b, _ := json.Marshal(out) + return string(b) +} diff --git a/common/pinyin_test.go b/common/pinyin_test.go new file mode 100644 index 0000000..6c0f990 --- /dev/null +++ b/common/pinyin_test.go @@ -0,0 +1,61 @@ +package common + +import ( + "encoding/json" + "testing" +) + +func parse(t *testing.T, s string) []string { + t.Helper() + var out []string + if err := json.Unmarshal([]byte(s), &out); err != nil { + t.Fatalf("拼音 JSON 解析失败: %v", err) + } + return out +} + +func TestAnnotatePinyin_IdiomOverride(t *testing.T) { + // 词表命中整体转拼音(带声调) + pys := parse(t, AnnotatePinyin("声东击西")) + want := []string{"shēng", "dōng", "jī", "xī"} + if len(pys) != len(want) { + t.Fatalf("长度 %d != %d: %v", len(pys), len(want), pys) + } + for i := range want { + if pys[i] != want[i] { + t.Fatalf("第 %d 字 %q != %q", i, pys[i], want[i]) + } + } +} + +func TestAnnotatePinyin_Alignment(t *testing.T) { + // 逐字对齐:标点/非汉字对应空串 + pys := parse(t, AnnotatePinyin("小明,你好!")) + want := []string{"xiǎo", "míng", "", "nǐ", "hǎo", ""} + if len(pys) != len(want) { + t.Fatalf("长度 %d != %d: %v", len(pys), len(want), pys) + } + for i := range want { + if pys[i] != want[i] { + t.Fatalf("第 %d 字 %q != %q", i, pys[i], want[i]) + } + } +} + +func TestAnnotatePinyin_NonHan(t *testing.T) { + pys := parse(t, AnnotatePinyin("123abc")) + if len(pys) != 6 { + t.Fatalf("长度应 6,实际 %d", len(pys)) + } + for i, p := range pys { + if p != "" { + t.Fatalf("第 %d 字符应空串,实际 %q", i, p) + } + } +} + +func TestAnnotatePinyin_Empty(t *testing.T) { + if AnnotatePinyin("") != "[]" { + t.Fatal("空文本应返回 []") + } +} diff --git a/go.mod b/go.mod index bc28703..7c65a7f 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2 github.com/gogf/gf/v2 v2.10.2 github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/mozillazg/go-pinyin v0.21.0 golang.org/x/crypto v0.55.0 ) diff --git a/go.sum b/go.sum index f764e96..8dcfc70 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mozillazg/go-pinyin v0.21.0 h1:Wo8/NT45z7P3er/9YSLHA3/kjZzbLz5hR7i+jGeIGao= +github.com/mozillazg/go-pinyin v0.21.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=