package style8d import ( "encoding/json" "strings" ) var dimensionLabels = map[string]string{ "d1Tone": "D1 語氣人格", "d2Structure": "D2 結構模板", "d3Interaction": "D3 互動方式", "d4Topics": "D4 主題分布", "d5Rhythm": "D5 發文節奏", "d6Visual": "D6 視覺語法", "d7Conversion": "D7 轉換方式", "d8Risk": "D8 風險紅線", } var dimensionOrder = []string{ "d1Tone", "d2Structure", "d3Interaction", "d4Topics", "d5Rhythm", "d6Visual", "d7Conversion", "d8Risk", } // ParseStoredProfile decodes the persona.style_profile JSON blob. func ParseStoredProfile(raw string) (*StoredProfile, bool) { raw = strings.TrimSpace(raw) if raw == "" { return nil, false } var profile StoredProfile if err := json.Unmarshal([]byte(raw), &profile); err != nil { return nil, false } if len(profile.Analysis) == 0 && strings.TrimSpace(profile.PersonaDraft) == "" { return nil, false } return &profile, true } // HasReady8D returns true when D1–D8 summaries exist and can drive copy generation. // personaDraft or legacy persona text alone is not sufficient for matrix/copy gates. func HasReady8D(personaText, styleProfileJSON string) bool { _ = personaText return BuildStyle8DPromptBlock(styleProfileJSON) != "" } var copyVoiceDimensionOrder = []string{"d1Tone", "d8Risk"} // BuildStyle8DPromptBlock formats D1–D8 summaries for LLM prompts. func BuildStyle8DPromptBlock(styleProfileJSON string) string { return buildStylePromptBlock(styleProfileJSON, dimensionOrder, "【8D 風格策略】\n產文必須遵守:\n") } // BuildLightPersonaVoiceBlock keeps only tone + risk guardrails for copy generation. func BuildLightPersonaVoiceBlock(styleProfileJSON string) string { return buildStylePromptBlock( styleProfileJSON, copyVoiceDimensionOrder, "【語氣與禁忌參考】\n以下僅供口語風格與紅線參考,勿照搬句型、段落模板或固定 hook:\n", ) } func buildStylePromptBlock(styleProfileJSON string, keys []string, header string) string { profile, ok := ParseStoredProfile(styleProfileJSON) if !ok { return "" } lines := make([]string, 0, len(keys)) for _, key := range keys { summary := strings.TrimSpace(profile.Analysis[key].Summary) if summary == "" { continue } label := dimensionLabels[key] if label == "" { label = key } lines = append(lines, label+":"+summary) } if len(lines) == 0 { return "" } var b strings.Builder b.WriteString(header) b.WriteString(strings.Join(lines, "\n")) return b.String() } // ResolvePersonaBlock combines explicit persona positioning with analyzed writing fingerprints. // The persona field answers "who this is"; personaDraft/style profile answers "how this person says things". func ResolvePersonaBlock(personaText, styleProfileJSON, brief string) string { var parts []string profile, hasProfile := ParseStoredProfile(styleProfileJSON) persona := strings.TrimSpace(personaText) if persona != "" { parts = append(parts, "【人設定位】\n"+persona) } else if fallback := strings.TrimSpace(brief); fallback != "" { parts = append(parts, "【人設定位】\n"+fallback) } if hasProfile { if draft := strings.TrimSpace(profile.PersonaDraft); draft != "" { parts = append(parts, "【語言指紋與寫作規則】\n"+draft) } } if block := BuildLightPersonaVoiceBlock(styleProfileJSON); block != "" { parts = append(parts, block) } return strings.TrimSpace(strings.Join(parts, "\n\n")) }