thread-master/backend/internal/library/viral/analyze_viral.go

137 lines
4.3 KiB
Go
Raw Normal View History

2026-06-26 08:37:04 +00:00
package viral
import (
"encoding/json"
"fmt"
"strings"
)
type ViralAnalysis struct {
2026-07-02 14:52:41 +00:00
StoryHook string `json:"storyHook"`
NarrativeBeat string `json:"narrativeBeat"`
EmotionalCore string `json:"emotionalCore"`
SceneSeed string `json:"sceneSeed"`
AuthenticityCues []string `json:"authenticityCues"`
WhatToAvoid string `json:"whatToAvoid"`
// Legacy fields kept for backward-compatible parsing.
2026-06-26 08:37:04 +00:00
HookPattern string `json:"hookPattern"`
StructurePattern string `json:"structurePattern"`
EmotionalTrigger string `json:"emotionalTrigger"`
ReplicationStrategy string `json:"replicationStrategy"`
KeyTakeaways []string `json:"keyTakeaways"`
}
type AnalyzeViralInput struct {
PostText string
AuthorName string
LikeCount int
ReplyCount int
SearchTag string
TopicLabel string
TopicBrief string
Persona string
}
func BuildAnalyzeViralSystemPrompt() string {
2026-07-02 14:52:41 +00:00
return strings.TrimSpace(`你是 Threads 敘事分析師拆解這篇貼文為什麼讓人想看完怎麼寫出同樣的故事感不是抄句型
2026-06-26 08:37:04 +00:00
2026-07-02 14:52:41 +00:00
只回傳 JSON繁體中文
- storyHook讓人停下滑動的開場力量描述手法不要給可照抄的句子
- narrativeBeat故事怎麼推進經歷/轉折/感受不是段落模板
- emotionalCore核心情緒是什麼
- sceneSeed一個值得借鑑的具體場景種子時間/地點/動作供啟發勿照搬
- authenticityCues35 個讓它像真人的小細節口語坦白不完美具體細節等
- whatToAvoid若照搬哪些寫法會立刻變成 AI 複製文`)
2026-06-26 08:37:04 +00:00
}
func BuildAnalyzeViralUserPrompt(in AnalyzeViralInput) string {
var b strings.Builder
b.WriteString("主題:")
b.WriteString(strings.TrimSpace(in.TopicLabel))
b.WriteString("\n")
if brief := strings.TrimSpace(in.TopicBrief); brief != "" {
b.WriteString("Brief")
b.WriteString(brief)
b.WriteString("\n")
}
if tag := strings.TrimSpace(in.SearchTag); tag != "" {
b.WriteString("搜尋標籤:")
b.WriteString(tag)
b.WriteString("\n")
}
author := strings.TrimSpace(in.AuthorName)
if author == "" {
author = "匿名"
}
b.WriteString(fmt.Sprintf("\n作者 @%s · %d 讚 · %d 留言\n", author, in.LikeCount, in.ReplyCount))
b.WriteString("\n貼文\n")
b.WriteString(strings.TrimSpace(in.PostText))
return b.String()
}
func ParseAnalyzeViralOutput(raw string) (ViralAnalysis, error) {
payload, err := extractCopyMapJSON(raw)
if err != nil {
return ViralAnalysis{}, err
}
var out ViralAnalysis
if err := json.Unmarshal(payload, &out); err != nil {
return ViralAnalysis{}, fmt.Errorf("parse viral analysis: %w", err)
}
2026-07-02 14:52:41 +00:00
if strings.TrimSpace(out.StoryHook) == "" &&
strings.TrimSpace(out.NarrativeBeat) == "" &&
strings.TrimSpace(out.HookPattern) == "" &&
strings.TrimSpace(out.StructurePattern) == "" {
2026-06-26 08:37:04 +00:00
return ViralAnalysis{}, fmt.Errorf("viral analysis missing content")
}
return out, nil
}
func FormatAnalysisForReplicate(analysis ViralAnalysis) string {
var b strings.Builder
2026-07-02 14:52:41 +00:00
if hook := firstNonEmpty(analysis.StoryHook, analysis.HookPattern); hook != "" {
b.WriteString("故事開場力量:")
2026-06-26 08:37:04 +00:00
b.WriteString(hook)
b.WriteString("\n")
}
2026-07-02 14:52:41 +00:00
if beat := firstNonEmpty(analysis.NarrativeBeat, analysis.StructurePattern); beat != "" {
b.WriteString("敘事推進:")
b.WriteString(beat)
2026-06-26 08:37:04 +00:00
b.WriteString("\n")
}
2026-07-02 14:52:41 +00:00
if emotion := firstNonEmpty(analysis.EmotionalCore, analysis.EmotionalTrigger); emotion != "" {
b.WriteString("核心情緒:")
2026-06-26 08:37:04 +00:00
b.WriteString(emotion)
b.WriteString("\n")
}
2026-07-02 14:52:41 +00:00
if scene := strings.TrimSpace(analysis.SceneSeed); scene != "" {
b.WriteString("場景種子(啟發用,勿照搬):")
b.WriteString(scene)
b.WriteString("\n")
}
cues := analysis.AuthenticityCues
if len(cues) == 0 {
cues = analysis.KeyTakeaways
}
if len(cues) > 0 {
b.WriteString("真人質感線索:")
b.WriteString(strings.Join(cues, ""))
2026-06-26 08:37:04 +00:00
b.WriteString("\n")
}
2026-07-02 14:52:41 +00:00
if avoid := firstNonEmpty(analysis.WhatToAvoid, analysis.ReplicationStrategy); avoid != "" {
b.WriteString("避免變成複製文:")
b.WriteString(avoid)
2026-06-26 08:37:04 +00:00
}
return strings.TrimSpace(b.String())
}
2026-07-02 14:52:41 +00:00
func firstNonEmpty(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}