thread-master/backend/internal/library/formula/analyze_pasted.go

164 lines
5.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package formula
import (
"context"
"fmt"
"regexp"
"strings"
libllmjson "haixun-backend/internal/library/llmjson"
libownpost "haixun-backend/internal/library/ownpost"
domai "haixun-backend/internal/model/ai/domain/usecase"
)
var pastedFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```")
type AnalyzePastedInput struct {
PostText string
AuthorName string
PersonaBlock string
LikeCount int
ReplyCount int
}
type PastedAnalysis struct {
Summary string `json:"summary"`
Wins []string `json:"wins"`
Improvements []string `json:"improvements"`
Formula string `json:"formula"`
PostTemplate string `json:"post_template"`
HookPattern string `json:"hook_pattern"`
Structure string `json:"structure"`
ReplicationTips []string `json:"replication_tips"`
Avoid []string `json:"avoid"`
}
func BuildAnalyzePastedPrompt(in AnalyzePastedInput) (system string, user string) {
personaBlock := strings.TrimSpace(in.PersonaBlock)
if personaBlock == "" {
personaBlock = "(未指定人設,請依貼文本身語氣分析)"
}
postText := strings.TrimSpace(in.PostText)
if postText == "" {
postText = "(貼文內容未提供)"
}
author := strings.TrimSpace(in.AuthorName)
if author == "" {
author = "匿名"
}
system = strings.TrimSpace(`
你是 Threads / Instagram 貼文結構分析師。任務是拆解「外部貼文」哪些地方可借鏡,再說明如何用指定人設的語言指紋重新說一次;不要產生會讓文案機械的填空模板。
規則:
- 用台灣繁體中文。
- formula整理可借鏡的敘事/資訊推進方式,但要提醒不可照搬句型。
- post_template不要寫填空骨架改寫成「此人設可採用的鬆結構與注意事項」。
- replication_tips重點放在如何換成人設語言包括段落節奏、標點換行、知識轉譯與 CTA。
- 只輸出一個 JSON 物件,不要 markdown欄位
summary, wins, improvements, formula, post_template, hook_pattern, structure, replication_tips, avoid
- formula / structure / hook_pattern 必須是字串,不要用陣列或巢狀物件
- wins / improvements / replication_tips / avoid 各 25 點字串陣列。`)
var b strings.Builder
b.WriteString("【人設脈絡】\n")
b.WriteString(personaBlock)
b.WriteString("\n\n【貼文】\n作者")
b.WriteString(author)
if in.LikeCount > 0 || in.ReplyCount > 0 {
b.WriteString(fmt.Sprintf(" · %d 讚 · %d 留言", in.LikeCount, in.ReplyCount))
}
b.WriteString("\n")
b.WriteString(postText)
b.WriteString("\n\n請分析爆款結構並輸出 JSON。")
user = b.String()
return system, user
}
func AnalyzePasted(ctx context.Context, ai domai.UseCase, req domai.GenerateRequest, in AnalyzePastedInput) (*PastedAnalysis, error) {
system, user := BuildAnalyzePastedPrompt(in)
temp := 0.65
req.System = system
req.Messages = []domai.Message{{Role: "user", Content: user}}
req.Temperature = &temp
out, err := ai.GenerateText(ctx, req)
if err != nil {
return nil, err
}
return ParsePastedAnalysis(out.Text)
}
func ParsePastedAnalysis(raw string) (*PastedAnalysis, error) {
payload, err := extractPastedJSON(raw)
if err != nil {
return nil, err
}
var flex map[string]any
if err := libllmjson.Unmarshal(payload, &flex); err != nil {
return nil, fmt.Errorf("parse pasted formula json: %w", err)
}
review := pastedAnalysisFromMap(flex)
if review.Summary == "" && review.Formula == "" && review.PostTemplate == "" {
return nil, fmt.Errorf("AI 未回傳有效公式內容")
}
return review, nil
}
func FromOwnPostReview(review *libownpost.PostFormulaReview) *PastedAnalysis {
if review == nil {
return nil
}
return &PastedAnalysis{
Summary: review.Summary,
Wins: review.Wins,
Improvements: review.Improvements,
Formula: review.Formula,
PostTemplate: review.PostTemplate,
HookPattern: review.HookPattern,
Structure: review.Structure,
ReplicationTips: review.ReplicationTips,
Avoid: review.Avoid,
}
}
func FromViralAnalysis(hook, structure, emotion, strategy string, takeaways []string) *PastedAnalysis {
summary := strings.TrimSpace(emotion)
if summary == "" {
summary = "爆款結構分析"
}
formula := strings.TrimSpace(strategy)
if structure != "" {
if formula != "" {
formula = structure + " → " + formula
} else {
formula = structure
}
}
template := ""
if hook != "" && structure != "" {
template = "[" + hook + "] → [" + structure + "] → [你的觀點] → [互動收尾]"
}
return &PastedAnalysis{
Summary: summary,
Wins: takeaways,
Formula: formula,
PostTemplate: template,
HookPattern: hook,
Structure: structure,
ReplicationTips: takeaways,
}
}
func extractPastedJSON(raw string) ([]byte, error) {
raw = strings.TrimSpace(raw)
if m := pastedFenceRE.FindStringSubmatch(raw); len(m) == 2 {
raw = strings.TrimSpace(m[1])
}
start := strings.Index(raw, "{")
end := strings.LastIndex(raw, "}")
if start < 0 || end <= start {
return nil, fmt.Errorf("formula output missing json object")
}
return []byte(raw[start : end+1]), nil
}