113 lines
3.4 KiB
Go
113 lines
3.4 KiB
Go
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"))
|
||
}
|