feat: integrate creator hub douyin workflows

This commit is contained in:
2026-09-14 19:22:07 +08:00
parent 025fe62c37
commit 44a28954cf
42 changed files with 2850 additions and 880 deletions
+82 -53
View File
@@ -3,20 +3,61 @@ package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"git.ipao.vip/rogee/creator-hub/internal/creator"
"git.ipao.vip/rogee/creator-hub/internal/hub"
"git.ipao.vip/rogee/creator-hub/internal/phasea"
)
const maxCreatorMaterialBytes int64 = 512 << 20
type creatorMaterialDownloader struct {
store *creator.Store
phaseAStore *phasea.Store
hubStore *hub.Store
}
func processCreatorMaterial(ctx context.Context, store *creator.Store, workID string) (creator.MaterialJob, error) {
func (downloader creatorMaterialDownloader) Download(ctx context.Context, work creator.Work, destination string) error {
if work.Platform != creator.PlatformDouyin || downloader.store == nil || downloader.phaseAStore == nil || downloader.hubStore == nil {
return fmt.Errorf("%w: creator media gateway is unavailable", creator.ErrUnavailable)
}
accountID := work.SourceID
if work.SourceType == creator.SourceCompetitor {
var err error
accountID, err = creatorCollectionAccount(ctx, downloader.store, downloader.phaseAStore, downloader.hubStore, work.Platform)
if err != nil {
return err
}
}
account, err := downloader.phaseAStore.GetAccount(ctx, accountID)
if err != nil {
return err
}
profile, err := downloader.store.GetAccountProfile(ctx, accountID)
if err != nil {
return err
}
if account.Platform != creator.PlatformDouyin || profile.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" || profile.LoginStatus != "logged_in" || account.PlatformAccountKey != profile.PlatformAccountKey {
return fmt.Errorf("%w: media account identity is not verified", creator.ErrConflict)
}
environment, err := downloader.hubStore.GetEnvironmentContextForAccount(ctx, accountID)
if err != nil {
return fmt.Errorf("%w: media browser environment unavailable: %v", creator.ErrUnavailable, err)
}
gateway, err := downloader.hubStore.GetGateway(ctx, environment.Gateway)
if err != nil {
return fmt.Errorf("%w: media gateway unavailable: %v", creator.ErrUnavailable, err)
}
browser := creatorGatewayBrowser{gateway: gateway, environment: environment}
if _, err := browser.Identity(ctx, profile.PlatformAccountKey); err != nil {
return fmt.Errorf("%w: media browser identity verification failed: %v", creator.ErrConflict, err)
}
return browser.Media(ctx, work.OriginalURL, destination)
}
func processCreatorMaterial(ctx context.Context, store *creator.Store, phaseAStore *phasea.Store, hubStore *hub.Store, workID string) (creator.MaterialJob, error) {
if store == nil || workID == "" || filepath.Base(workID) != workID {
return creator.MaterialJob{}, creator.ErrInvalid
}
@@ -46,7 +87,7 @@ func processCreatorMaterial(ctx context.Context, store *creator.Store, workID st
if _, err := store.SetMaterialStep(ctx, workID, "download", "running", "", ""); err != nil {
return creator.MaterialJob{}, err
}
if err := downloadCreatorMaterial(ctx, work.OriginalURL, videoPath); err != nil {
if err := (creatorMaterialDownloader{store: store, phaseAStore: phaseAStore, hubStore: hubStore}).Download(ctx, work, videoPath); err != nil {
return setMaterialFailure(ctx, store, workID, "download", err)
}
job, err = store.SetMaterialStep(ctx, workID, "download", "succeeded", videoReference, "")
@@ -84,8 +125,15 @@ func processCreatorMaterial(ctx context.Context, store *creator.Store, workID st
if job.AudioStatus == "no_audio" {
job, err = store.SetMaterialStep(ctx, workID, "transcription", "no_speech", "", "没有可转写的音轨")
} else {
settings, settingsErr := store.GetSettings(ctx)
if settingsErr != nil {
return creator.MaterialJob{}, settingsErr
}
if !settings.TranscriptionConfigured || strings.TrimSpace(settings.TranscriptionProvider) == "" || strings.TrimSpace(settings.TranscriptionModel) == "" {
return setMaterialFailure(ctx, store, workID, "transcription", fmt.Errorf("transcription provider is not configured"))
}
if _, err = store.SetMaterialStep(ctx, workID, "transcription", "running", "", ""); err == nil {
transcript, transcribeErr := transcribeCreatorAudio(ctx, audioPath)
transcript, transcribeErr := transcribeCreatorAudio(ctx, audioPath, settings.TranscriptionModel)
if transcribeErr != nil {
job, err = setMaterialFailure(ctx, store, workID, "transcription", transcribeErr)
} else if strings.TrimSpace(transcript) == "" {
@@ -107,46 +155,6 @@ func processCreatorMaterial(ctx context.Context, store *creator.Store, workID st
return job, nil
}
func downloadCreatorMaterial(ctx context.Context, rawURL, destination string) error {
parsed, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil || (parsed.URL.Scheme != "http" && parsed.URL.Scheme != "https") || parsed.URL.Host == "" {
return creator.ErrInvalid
}
client := &http.Client{Timeout: 2 * time.Minute}
response, err := client.Do(parsed)
if err != nil {
return fmt.Errorf("download material: %w", err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("download material: HTTP %s", response.Status)
}
if response.ContentLength > maxCreatorMaterialBytes {
return fmt.Errorf("download material exceeds size limit")
}
temporary := destination + ".tmp"
defer os.Remove(temporary)
file, err := os.OpenFile(temporary, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return fmt.Errorf("create material file: %w", err)
}
written, copyErr := io.Copy(file, io.LimitReader(response.Body, maxCreatorMaterialBytes+1))
closeErr := file.Close()
if copyErr != nil {
return fmt.Errorf("write material: %w", copyErr)
}
if closeErr != nil {
return fmt.Errorf("close material file: %w", closeErr)
}
if written > maxCreatorMaterialBytes {
return fmt.Errorf("download material exceeds size limit")
}
if err := os.Rename(temporary, destination); err != nil {
return fmt.Errorf("publish material: %w", err)
}
return nil
}
func creatorMaterialHasAudio(ctx context.Context, videoPath string) (bool, error) {
if _, err := exec.LookPath("ffprobe"); err != nil {
return false, fmt.Errorf("ffprobe is unavailable: %w", err)
@@ -178,15 +186,36 @@ func extractCreatorAudio(ctx context.Context, videoPath, audioPath string) error
return nil
}
func transcribeCreatorAudio(ctx context.Context, audioPath string) (string, error) {
binary := os.Getenv("CREATOR_TRANSCRIPTION_BIN")
if binary == "" {
return "", fmt.Errorf("transcription provider is not configured")
func validateTranscriptionBinary(binary string) error {
path, err := exec.LookPath(binary)
if err != nil {
return fmt.Errorf("lookup transcription provider %q: %w", binary, err)
}
if _, err := exec.LookPath(binary); err != nil {
if path == "" {
return fmt.Errorf("binary path is empty")
}
return nil
}
func transcribeCreatorAudio(ctx context.Context, audioPath, model string) (string, error) {
model = strings.TrimSpace(model)
if model == "" {
return "", fmt.Errorf("transcription model is not configured")
}
configured := strings.TrimSpace(os.Getenv("CREATOR_TRANSCRIPTION_BIN"))
var binary string
switch filepath.Base(configured) {
case "whisper":
binary = "whisper"
case "faster-whisper":
binary = "faster-whisper"
default:
return "", fmt.Errorf("transcription provider is not configured or unsupported")
}
if err := validateTranscriptionBinary(binary); err != nil {
return "", fmt.Errorf("transcription provider is unavailable: %w", err)
}
output, err := exec.CommandContext(ctx, binary, audioPath).Output()
output, err := exec.CommandContext(ctx, binary, audioPath, "--model", model).Output()
if err != nil {
return "", fmt.Errorf("transcribe audio: %w", err)
}