137 lines
4.3 KiB
Go
137 lines
4.3 KiB
Go
package viral
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
type ViralAnalysis struct {
|
||
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.
|
||
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 {
|
||
return strings.TrimSpace(`你是 Threads 敘事分析師。拆解這篇貼文為什麼讓人想看完、怎麼寫出同樣的故事感(不是抄句型)。
|
||
|
||
只回傳 JSON(繁體中文):
|
||
- storyHook:讓人停下滑動的開場力量(描述手法,不要給可照抄的句子)
|
||
- narrativeBeat:故事怎麼推進(經歷/轉折/感受,不是段落模板)
|
||
- emotionalCore:核心情緒是什麼
|
||
- sceneSeed:一個值得借鑑的「具體場景種子」(時間/地點/動作,供啟發勿照搬)
|
||
- authenticityCues:3~5 個讓它像真人的小細節(口語、坦白、不完美、具體細節等)
|
||
- whatToAvoid:若照搬哪些寫法會立刻變成 AI 複製文`)
|
||
}
|
||
|
||
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)
|
||
}
|
||
if strings.TrimSpace(out.StoryHook) == "" &&
|
||
strings.TrimSpace(out.NarrativeBeat) == "" &&
|
||
strings.TrimSpace(out.HookPattern) == "" &&
|
||
strings.TrimSpace(out.StructurePattern) == "" {
|
||
return ViralAnalysis{}, fmt.Errorf("viral analysis missing content")
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func FormatAnalysisForReplicate(analysis ViralAnalysis) string {
|
||
var b strings.Builder
|
||
if hook := firstNonEmpty(analysis.StoryHook, analysis.HookPattern); hook != "" {
|
||
b.WriteString("故事開場力量:")
|
||
b.WriteString(hook)
|
||
b.WriteString("\n")
|
||
}
|
||
if beat := firstNonEmpty(analysis.NarrativeBeat, analysis.StructurePattern); beat != "" {
|
||
b.WriteString("敘事推進:")
|
||
b.WriteString(beat)
|
||
b.WriteString("\n")
|
||
}
|
||
if emotion := firstNonEmpty(analysis.EmotionalCore, analysis.EmotionalTrigger); emotion != "" {
|
||
b.WriteString("核心情緒:")
|
||
b.WriteString(emotion)
|
||
b.WriteString("\n")
|
||
}
|
||
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, ";"))
|
||
b.WriteString("\n")
|
||
}
|
||
if avoid := firstNonEmpty(analysis.WhatToAvoid, analysis.ReplicationStrategy); avoid != "" {
|
||
b.WriteString("避免變成複製文:")
|
||
b.WriteString(avoid)
|
||
}
|
||
return strings.TrimSpace(b.String())
|
||
}
|
||
|
||
func firstNonEmpty(values ...string) string {
|
||
for _, value := range values {
|
||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||
return trimmed
|
||
}
|
||
}
|
||
return ""
|
||
}
|