README
¶
ð¬ Go Veo Orchestrator
ð æŠèŠ (About) - Music Recipe Driven Veo Orchestrator
Go Veo Orchestrator ã¯ãMusic RecipeïŒé³æ¥œã¬ã·ã / æ¥œæ²æ§ææžïŒ ããåç»ã«ããåãæ§é åããGoogle ã®åç»çæ AI Veo (Vertex AI / Gemini API) ãžæž¡ãããã®ããã¯ãšã³ããªãŒã±ã¹ãã¬ãŒã¿ãŒã§ãã
Gemini Image Kit ã䜿ã£ãŠã«ããããšã®ããŒãã¬ãŒã ãçæããVideoRunner adapter ãéã㊠Veo ã« Prompt / Keyframe / Audio / PreviousVideoID / Seed ãæž¡ããŸããVeo API ã®å
·äœå®è£
㯠ports.VideoRunner ãšããŠå·®ãæ¿ããèšèšã§ãã
video_id ãæ¬¡ã«ããã® PreviousVideoID ãšããŠåŒãç¶ãããšã§ãVideo-to-Video ã®æèãä¿ã£ãé£ç¶ã«ããçæãè¡ããŸããçææžã¿ã«ãã㯠status=generated ãš video_id / video_url ã䜿ã£ãŠã¹ãããã§ãããããéäžå€±æåŸã®åéã«ã察å¿ããããæ§é ã§ãã
âš ã³ã¢ã»ã³ã³ã»ãã (Core Concepts)
-
𧬠Consistency Control:
- ãã£ã©ã¯ã¿ãŒåºæ SeedãããŒãã¬ãŒã ç»åãåãã®ããã³ãããåã«ããã® VideoID ã 1 ã€ã®
VideoGenerationRequestã«ãŸãšããã«ããéã®èŠãç®ãšæèãç¶æããŸãã
- ãã£ã©ã¯ã¿ãŒåºæ SeedãããŒãã¬ãŒã ç»åãåãã®ããã³ãããåã«ããã® VideoID ã 1 ã€ã®
-
â³ Audio-Driven Timeline Logic (鳿¥œäž»å°ã®ã¿ã€ã ã©ã€ã³ç®¡ç):
music_recipe.sectionsãŸãã¯cutsããduration_secãstart_secãend_secãè£å®ããaudio_cueã Veo çšããã³ãããžæ³šå ¥ããŸãã
-
ð Resumable Video Chain:
- å
cutã¯statusãvideo_idãvideo_urlãä¿æããŸããçææžã¿ã«ããã¯åçæãããä¿ææžã¿video_idãæ¬¡ã«ããã®PreviousVideoIDãšããŠäœ¿çšããŸãã - ããŒãã¬ãŒã ãåãèãæ¹ã§ã
keyframe_referenceãæã€ã«ããã¯çŒãçŽããŸããïŒCutKeyframeRunner.Run/RunAndSaveã®äž¡æ¹ïŒãä¿åæžã¿ã¬ã·ããèµ·ç¹ã«åŠçãåéããŠãããã§ã«æã£ãç»åçæã®ã³ã¹ããäºéã«æããŸãããçŒãçŽãããã«ããã¯keyframe_referenceã空ã«ããŠããæž¡ããŸãïŒCut.ResetGeneration(false)ïŒã
- å
-
ð§© Adapter-Oriented Architecture:
- Veo ãžã®å®éä¿¡ã¯
ports.VideoRunnerã«éã蟌ãããªãŒã±ã¹ãã¬ãŒã·ã§ã³ãããŒãã¬ãŒã çæãã¡ã¿ããŒã¿ä¿åãåé¢ããŠããŸãã
- Veo ãžã®å®éä¿¡ã¯
ð¬ 4ã€ã®åç»çæã¯ãŒã¯ãã㌠(Workflows)
| ã¯ãŒã¯ãã㌠| æ åœã€ã³ã¿ãŒãã§ãŒã¹ | å 容 |
|---|---|---|
| 1. Scripting | ScriptRunner |
Music Recipe JSON ãèªã¿èŸŒã¿ãæè©ã»sectionã»æ¥œæ²å±éãããã«ããå²ãã»ã«ã¡ã©ã¯ãŒã¯ã»æšå®ç§æ°ãå«ãVideo Recipeãçæã |
| 2. Cut Keyframe Gen | CutKeyframeRunner |
åã«ããã®ããŒã¹ãšãªãããŒãã¬ãŒã ç»åãããã£ã©ã¯ã¿ãŒ Seed ãšåç
§ç»åã䜿ã£ãŠçæïŒRunAndSaveïŒãkeyframe_reference ãæ¢ã«ããã«ããã¯çæããŸããïŒRun ã¯è©²åœäœçœ®ã« nil ãè¿ããåŒã³åºãåŽã¯æ¢åã®åç
§ããã®ãŸãŸäœ¿ããŸãïŒãæ¢åããŒãã¬ãŒã ã®å±æç·šéã«ã察å¿ïŒEditAndSaveã詳现ã¯åŸè¿°ïŒã |
| 3. Video Gen | VideoTimelineRunner + VideoRunner |
VideoRequestBuilder ã VideoGenerationRequest ãçµã¿ç«ãŠãVeo adapter ã«é 次æå
¥ã |
| 4. Metadata Publish | VideoPublishRunner |
video_id / video_url / status æŽæ°æžã¿ã® video_music_meta.json ãä¿åã |
𩹠åäžã«ããã®ããŒãã¬ãŒã ç·šé (EditAndSave)
CutKeyframeRunner.RunAndSave ã¯ããã³ããããç»åãäœãçŽãããã«çæãã§ãããEditAndSave ã¯æ¢åã®ããŒãã¬ãŒã ç»åãç·šéå
ãšããŠãããã¹ãæç€ºã§å±æçãªä¿®æ£ã ããåæ ããŸããæ§å³ã»ããŒãºã»èæ¯ã¯ä¿ããããããåããã£ã©ã¯ã¿ãŒã®ä»ã«ãããšã®äžè²«æ§ãä¿ã£ããŸãŸãå°ç©ã®æ°ãæžããããè²å³ãæããããšãã£ã軜埮ãªä¿®æ£ã«åããŠããŸãã
// recipe ã¯å¿
ã 1 ã«ããã®ã¿ãå«ã¿ãŸãã察象ã«ããã® KeyframeReference ã¯
// æ¢åã®ïŒç·šéå
ãšãªãïŒããŒãã¬ãŒã ç»åãæããŠããå¿
èŠããããŸãã
recipe := &ports.VideoRecipe{
Cuts: []ports.Cut{
{CutIndex: 2, CharacterID: "zundamon", KeyframeReference: "gs://bucket/jobs/j1/images/keyframe_2.png"},
},
}
updated, err := workflows.CutKeyframe.EditAndSave(ctx, recipe, "è
ã«ã¯çµåµèã1ã2æã®ã¿ã«ããŠãã ãã", "gs://bucket/jobs/j1/regens/cut-2/")
if err != nil {
return err
}
// updated.Cuts[0].KeyframeReference ãç·šéåŸã®ç»åãã¹ã«æŽæ°ãããŠããŸãã
å
éšçã«ã¯ gemini-image-kit ã® ImageGenerator.GenerateSingleImage ã«æ¢åããŒãã¬ãŒã ç»åãå
¥åãšããŠæž¡ããeditPrompt ãããã³ãããšããŠåŒã³åºããŸããRunAndSaveïŒéåžžã®ããŒãã¬ãŒã çæïŒãšåãäŒè©±åãã«ãã¢ãŒãã«ç»åã¢ãã«ïŒConfig.ImageModelãGemini ã®ãNano Bananaãç³»ïŒããã®ãŸãŸåå©çšãããããç·šéå°çšã®ã¢ãã«ãAPIã¯äžèŠã§ãã
Vertex AI Imagen ã®ãã¹ã¯ããŒã¹ç·šé/ã«ã¹ã¿ãã€ãº APIïŒ
imagen-3.0-capability-001ç³»ïŒã¯2026幎6æ30æ¥ã«å»æ¢ãããåŸç¶ã®ãcapabilityãã¢ãã«ãçšæãããŠããŸããããã®ããEditAndSaveã¯ãã¹ã¯æå®ã«ã¯å¯Ÿå¿ãããèªç±èšè¿°ã®ç·šéæç€ºã®ã¿ããµããŒãããŸãã
recipe.Cutsã 1 ä»¶ã§ãªãå Žåã¯ãšã©ãŒ- 察象ã«ããã®
KeyframeReferenceã空ã®å ŽåïŒïŒç·šéå ç»åããªãïŒã¯ãšã©ãŒ - ãã£ã©ã¯ã¿ãŒã® Seed ã¯
RunAndSaveãšåæ§ãchar.Seedããã®ãŸãŸç·šéãªã¯ãšã¹ãã«äœ¿ãããŸã
ð Adapter Boundary
ãã®ãªããžããªã¯ Veo API ã¯ã©ã€ã¢ã³ãã§ã¯ãªããVeo ã«æž¡ãããã® ãã¡ã€ã³ã¢ãã«ãããŒãã¬ãŒã çæããªã¯ãšã¹ãæ§ç¯ãVideo-to-Video é£éãã¡ã¿ããŒã¿ä¿å ãæ åœãã orchestration ã©ã€ãã©ãªã§ãã
Veo API ãžã®å®é信㯠ports.VideoRunner ã®å®è£
ãšããŠãå©çšåŽã¢ããªã±ãŒã·ã§ã³ãŸãã¯å¥ããã±ãŒãžããå·®ã蟌ã¿ãŸãããã®ãªããžããªå
ã«ã¯æ¬çªçš Veo adapter ã¯å«ãããå®è¡ç°å¢ããšã®å·®åã adapter åŽã«éã蟌ããŸãã
VideoRunner å®è£
ãæ
ã責åã¯ä»¥äžã§ãã
- Google Cloud / Vertex AI / Gemini API ãªã©ã®èªèšŒ
ImageReference/AudioReferenceã®è§£æ±ºInputImage/InputAudioã䜿ãå Žåã®ã¢ããããŒããšåç § URI å- Veo API ãžã®åç»çæãªã¯ãšã¹ãéä¿¡
- é·æé operation ã®ããŒãªã³ã°ãã¿ã€ã ã¢ãŠãããªãã©ã€
- çæåç»ã®ä¿åå 管ç
- 次ã«ãããžåŒãç¶ãããã®
VideoResponse.VideoIDè¿åŽ - åç
§å¯èœãª
VideoResponse.CloudURLè¿åŽ
adapter å®è£
ã§ã¯ VideoGenerationRequest.ImageReference ãåªå
ãã空ã®å Žåã ã InputImage ãã¢ããããŒãããŠåç
§ URI ãäœãæ³å®ã§ããAudioReference ãåæ§ã«ãåç
§ URI ãããå Žåã¯ãããåªå
ããå¿
èŠã«å¿ã㊠InputAudio ãã¢ããããŒãããŸãã
ããŒãã¬ãŒã çæã®å®äœã¯ ports.CutImageGeneratorïŒExecute / ä»»æã§ EditCutïŒã§ãã
EditCut ãå®è£
ããŠããªããšã³ãžã³ã§ EditAndSave ãåŒã¶ãš ErrEditingNotSupported ã«ãªããŸãã
VideoRunner ãæå®ããªãå Žåãworkflow.New ãè¿ã Workflows.Video 㯠nil ã§ã¯ãªããåŒã³åºããšåžžã« ports.ErrVideoRunnerNotConfigured ãè¿ããããŒå®è£
ïŒports.NewNoopVideoTimelineRunner()ïŒã«ãªããŸããScript / CutKeyframe / Publish ã ããäœ¿ãæ§æã§ã¯ãã®ãŸãŸå©çšã§ããŸãããåç»çæãŸã§å®è¡ããå Žå㯠ManagerArgs.VideoRunner ã«å®è£
ãæž¡ããŠãã ãããåŒã³åºãåŽã§æªèšå®ãæ€ç¥ãããå Žå㯠errors.Is(err, ports.ErrVideoRunnerNotConfigured) ã§å€å®ã§ããŸãïŒWorkflows.Video == nil ã«ãããã§ãã¯ã¯æ©èœããŸããïŒã
type VeoRunner struct {
// client, bucket, model, location ãªã©ãå®è¡ç°å¢ã«å¿
èŠãªäŸåãä¿æããŸãã
}
func (r *VeoRunner) Run(ctx context.Context, req ports.VideoGenerationRequest) (*ports.VideoResponse, error) {
// 1. req.ImageReference / req.AudioReference ãåªå
ããŠåç
§ã解決
// 2. å¿
èŠãªã req.InputImage / req.InputAudio ãã¢ããããŒã
// 3. req.Prompt, req.PreviousVideoID, req.Seed, req.DurationSec ã Veo API ã«æž¡ã
// 4. operation ã poll ããŠå®äºãåŸ
ã€
// 5. CloudURL ãš VideoID ãè¿ã
return &ports.VideoResponse{
CloudURL: "gs://example-bucket/videos/cut_001.mp4",
VideoID: "veo-video-id",
CutIndex: req.CutIndex,
DurationSec: req.DurationSec,
MimeType: "video/mp4",
}, nil
}
workflows, err := workflow.New(workflow.ManagerArgs{
Config: cfg,
HTTPClient: httpClient,
Reader: reader,
Writer: writer,
AIClient: geminiModel,
VideoRunner: &VeoRunner{},
PromptDeps: promptDeps,
})
if err != nil {
return err
}
videos, err := workflows.Video.Run(ctx, recipe)
if err != nil {
return err
}
// ã¡ã¿ããŒã¿ã®ä¿å㯠Publish ãæ
åœããŸããåç»çæãšä¿åãåããŠããã®ã¯ã
// åŒã³åºãåŽãçæãšä¿åã®éã«åŠçïŒãã§ãŒã³ã®çµåãªã©ïŒãæããããã«ããããã§ãã
if _, err := workflows.Publish.Run(ctx, recipe, "gs://bucket/jobs/<jobID>/"); err != nil {
return err
}
VideoGenerationRequest ã®äž»ãªãã£ãŒã«ãã¯ä»¥äžã®å¥çŽã§äœ¿ãããŸãã
| ãã£ãŒã«ã | adapter åŽã®æ±ã |
|---|---|
Prompt |
Veo ã«æž¡ãæçµããã³ãããã«ããå 容ãã«ã¡ã©ã¯ãŒã¯ã鳿¥œåææç€ºãå«ã¿ãŸãã |
ImageReference |
æ¢ã«åç
§å¯èœãªããŒãã¬ãŒã ç»å URIãååšããå Žå㯠InputImage ããåªå
ããŸãã |
InputImage |
ImageReference ã空ã®å Žåã« adapter åŽã§ã¢ããããŒãããŠäœ¿ãç»åãã€ãåã§ãã |
AudioReference |
æ¢ã«åç §å¯èœãªé³å£°ã»ã°ã¡ã³ã URIã |
InputAudio |
AudioReference ã空ã®å Žåã« adapter åŽã§ã¢ããããŒãããŠäœ¿ãé³å£°ãã€ãåã§ãã |
PreviousVideoID |
åã«ããã®æèãåŒãç¶ãããã® IDã空ã®å Žåã¯ãã§ãŒã³ãªãã§çæããŸãã |
LastFrameReference |
çµäºãã¬ãŒã ãšããŠäœ¿ãç»å URIïŒVeo ã® first/last frame è£éïŒãVeo API ã§ã¯éå§ãã¬ãŒã ç»åãšã®äœµçšãå¿
é ã®ãããimage å
¥åïŒimage_to_videoïŒã®ãšãã ã lastFrame ãšããŠéããŸãã察å¿ã¢ãã«ã¯ Veo 2 / Veo 3.1 ç³»ã®ã¿ã§ãã |
Seed |
ãã£ã©ã¯ã¿ãŒ Seed ãåªå
ããæªæå®æã¯ music_recipe.Seed ã䜿ããŸãã |
CutIndex |
ã¬ã¹ãã³ã¹ããšã©ãŒè¡šç€ºã§äœ¿ãã«ããçªå·ã§ãã |
DurationSec |
ã«ããã®ç®æšç§æ°ã§ãã |
VideoResponse.VideoID ã空ã®å Žåããã®ã«ããã®çæçµæã¯ä¿åã§ããŸãããæ¬¡ã«ãããžã® PreviousVideoID é£éã¯æŽæ°ãããŸãããé£ç¶ã«ããã®äžè²«æ§ãéèŠãã adapter ã§ã¯ãå¯èœãªéã Veo åŽã®åç» ID ãè¿ããŠãã ããã
ðïž Veo çæã¢ãŒããšã«ããå°º (Generation Modes & Durations)
1ã€ã®ãªã¯ãšã¹ãã Veo ã®ã©ã®çææ©èœã§è§£éããããã¯ãports.ClassifyVeoRequest 1ç®æã§æ±ºãŸããŸããadapter ã®ãªã¯ãšã¹ãæ¬ææ§ç¯ãã«ããå°ºã®èšç»ã»æ€èšŒãçæã¢ãŒãããšã®ããã³ããéžæã¯ããã¹ãŠãã®åãå€å®ãå
±æããŠãã ããããããããç¬èªã«åå²ãããšãåç
§ç»åã«åããããšæç€ºããªããåç
§ç»åãéããªãããreference_to_video åæã§8ç§ã«äžžããã®ã«å®é㯠image_to_video ã ã£ãããšãã£ããºã¬ãèµ·ããŸãã
caps := ports.RunnerCapabilities(videoRunner) // Runner ã®ãªãã·ã§ã³ã€ã³ã¿ãŒãã§ãŒã¹ããå°åº
mode := ports.ClassifyVeoRequest(req, usePreviousVideo, caps)
å€å®ã®åªå
é äœãšãåã¢ãŒãã§ Veo ãåãä»ããã«ããå°ºã¯ä»¥äžã§ãïŒã¢ãŒã㯠ports.VeoGenerationModeã
ã¢ãã«ã®å¯Ÿå¿ç¶æ³ã¯ ports.VeoCapabilitiesãå°ºã®äžèŠ§ã¯ ports.ImageToVideoDurationsSec /
ports.ReferenceToVideoDurationsSec ãšããŠå
¬éããŠããŸãïŒã
| åªå | ã¢ãŒã | æ¡ä»¶ | 察å¿å°ºïŒç§ïŒ |
|---|---|---|---|
| 1 | VeoModeVideoExtension |
usePreviousVideo ã〠PreviousVideoID ã gs:// åç
§ãç»ååç
§ã¯ãã¹ãŠç¡èŠãããŸã |
7 åºå® |
| 2 | VeoModeReferenceToVideo |
åç
§ç»åã1ã€ä»¥äžãããã¢ãã«ã referenceImages 察å¿ïŒVeo 3ç³»ã®é FastïŒ |
8 åºå® |
| 3 | VeoModeFramesToVideo |
éå§ãã¬ãŒã ãš LastFrameReference ãäž¡æ¹ãããã¢ãã«ã lastFrame 察å¿ïŒVeo 2 / Veo 3.1ç³»ïŒ |
4 / 6 / 8 |
| 4 | VeoModeImageToVideo |
äžèšä»¥å€ãã¹ãŠ | 4 / 6 / 8 |
ã¢ãã«ã®å¯Ÿå¿ç¶æ³ã¯ãVideoRunner ã«ä»¥äžã®ãªãã·ã§ã³ã€ã³ã¿ãŒãã§ãŒã¹ãå®è£
ãããš ports.RunnerCapabilities ãèªåã§æŸããŸãïŒæªå®è£
ã® Runner ã¯äž¡æ¹ false = image_to_video åŽãžåããŸãïŒã
type ReferenceImagesSupporter interface{ SupportsReferenceImages() bool }
type LastFrameSupporter interface{ SupportsLastFrame() bool }
ã«ããå°ºã®èšç»ã«äœ¿ããã«ããŒã¯ ports ã«ãããŸããVeo ã¯ä»»æé·ã®åç»ãçæã§ããªããããã¬ã·ãåŽã§ãããã®å€ã«åãããŠå°ºãå²ãåœãŠãŠããå®è¡ããŠãã ããã
| API | çšé |
|---|---|
ports.DurationsForMode(mode) |
ãã®ã¢ãŒãã§åãä»ããããå°ºã®äžèЧ |
ports.IsSupportedDuration(sec, mode) |
å°ºãåãä»ãããããã®å€å® |
ports.SnapDuration(sec, candidates) |
æãè¿ã察å¿å°ºãžäžžããïŒåè·é¢ãªãé·ãæ¹ïŒ |
ports.ChainDurations(bases) |
1æ¬ã®ç¶ç¶ãã§ãŒã³ïŒããŒã¹ + 7ç§ Ã nïŒã§å®çŸã§ããåèšå°ºã®åè£ |
ports.VeoContinuationMaxDurationSec |
ãã§ãŒã³ããªã»ãããã环ç©å°ºã®éŸå€ïŒ24ç§ïŒ |
VideoTimelineRunner.Run ã¯åã«ããã Veo ãžæããçŽåã«ãã®å°ºãæ€èšŒãã察å¿å€ãªã ports.ErrUnsupportedCutDuration ãè¿ããŠãã®ã«ããã®çæãè¡ããŸãããé·æéå®è¡ operation ãæã㊠Veo åŽã«æåŠããããŸã§åŸ
ã€ããæåã§ãã©ã®ã«ãããäœç§ã§ã©ã®ã¢ãŒãã ã£ãããŸã§ç€ºããŠèœãšããŸãã
åç
§ç»åïŒreferenceImagesïŒã®çµã¿ç«ãŠèŠå㯠ports.CutReferenceImages(cut, characters) ã«äžæ¬åãããŠããŸãã[ãã£ã©ã¯ã¿ãŒç«ã¡çµµ, ããŒãã¬ãŒã ] ã®é ã«æå€§3æã§ãç«ã¡çµµãç¡ãã«ããã¯ããŒãã¬ãŒã ã ããåç
§ãšããŠäœ¿ããŸãã
âïž èšå®ãšå·®ãæ¿ã (Config / DI)
workflow.New(ManagerArgs) ã«æž¡ãèšå®ãšäŸåã§ããports.Config ã¯ãŒãå€ã§æ§ããŸããïŒApplyDefaults ãè£å®ããŸãïŒã
ports.Config |
åœ¹å² |
|---|---|
GeminiModel |
å°æ¬çæã«äœ¿ãããã¹ãã¢ãã«ïŒæ¢å®ã¯ ports.DefaultGeminiModelïŒ |
ImageModel |
ããŒãã¬ãŒã ç»åçæã«äœ¿ãã¢ãã« |
MaxConcurrency |
ããŒãã¬ãŒã çæã®æå€§äžŠåæ°ïŒåç»çæã¯ Video-to-Video é£éã®ããåžžã«é次ã§ãïŒ |
RateInterval |
AI åŒã³åºãã®çºå°ééã®äžé |
StyleSuffix |
ããŒãã¬ãŒã ç»åã«ä»äžããç»é¢šæå® |
KeyframeAspectRatio |
ããŒãã¬ãŒã ã®ã¢ã¹ãã¯ãæ¯ã空ãªã keyframe.CutAspectRatio |
ports.WithModels(gemini, image) / ports.WithAspectRatio(ratio) ã§ Config ãéšåçã«äžæžãã§ããŸãã
ManagerArgs |
åœ¹å² |
|---|---|
AIClient |
gemini.MultimodalModelïŒå°æ¬çæã»ããŒãã¬ãŒã çæïŒ |
Reader / Writer |
ports.ContentReader / remoteio.WriterïŒã¬ã·ãã®èªã¿èŸŒã¿ã»ææç©ã®ä¿åïŒ |
HTTPClient |
åç §ç»åã®ååŸã«äœ¿ã HTTP ã¯ã©ã€ã¢ã³ã |
VideoRunner |
Veo API ã¢ããã¿ãæªèšå®ãªã ErrVideoRunnerNotConfigured ãè¿ããããŒã«ãªããŸã |
PromptDeps |
ãã£ã©ã¯ã¿ãŒå®çŸ©ãšããã³ããå®è£ ïŒäžèšïŒ |
ããã³ããã¯ãã®ã©ã€ãã©ãªã«å«ãããPromptDeps ããæ³šå
¥ããŸãã
PromptDeps |
ã€ã³ã¿ãŒãã§ãŒã¹ | åœ¹å² |
|---|---|---|
Characters |
*characterkit.Characters |
ãã£ã©ã¯ã¿ãŒå®çŸ©ãã«ããã® CharacterID ãã解決ããæªç¥IDã¯æ¢å®ãã£ã©ã¯ã¿ãŒãžèœãšããŸã |
ScriptPrompt |
ports.ScriptPrompt |
Music Recipe ããåç»å°æ¬ãçæããããã³ããïŒBuild(mode, *TemplateData)ïŒ |
KeyframePrompt |
ports.KeyframePrompt |
ã«ããã®ããŒãã¬ãŒã çæã»ç·šéã®ããã³ããïŒBuildCut / BuildEditïŒ |
ããŒãã¬ãŒã çæã®çްéšã¯ keyframe.Option ã§èª¿æŽããŸãïŒworkflow ã Config ããçµã¿ç«ãŠãŸãïŒã
| ãªãã·ã§ã³ | åœ¹å² |
|---|---|
keyframe.WithMaxConcurrency |
äžŠåæ° |
keyframe.WithRateInterval / WithRateBurst |
çºå°ééãšããŒã¹ã |
keyframe.WithAspectRatio |
ã¢ã¹ãã¯ãæ¯ïŒãã£ã©ã¯ã¿ãŒã«äžèŽããåç §ç»åãããã°ãããåªå ããŸãïŒ |
ðŸ çæãšä¿åã®è²¬ååæ (Generation vs. Persistence)
æžãå
㯠remoteio.Writer ãšããŠæ³šå
¥ãããããããã®ã©ã€ãã©ãªã¯ GCS ã S3 ãç¥ããŸããã
æã£ãŠããã®ã¯ãã©ãã«ä¿åããããã§ã¯ãªããèªåã®ãã©ãŒããããšåœåèŠåã§ãã
ãã®äžã§ãããŒãã¬ãŒã ãšåç»ã§æå³çã«æ±ããåããŠããŸãã
| çæ | ä¿å | |
|---|---|---|
| ããŒãã¬ãŒã | CutKeyframeRunner.Run |
åã Runner ãè¡ãïŒRunAndSave / EditAndSaveïŒ |
| åç» | VideoTimelineRunner.Run |
è¡ããªããåŒã³åºãåŽã VideoPublishRunnerïŒWorkflows.PublishïŒãåŒã³ãŸã |
ããŒãã¬ãŒã ã®ä¿åã Runner ãæã€çç±: ä¿åå keyframe_<ã¬ã·ãå
ã®äœçœ®>.png ãã«ããã®äžŠã³ãšçµã³ã€ããŠããããã§ãã
åŒã³åºãåŽã«åºããšãäœçœ®âãã¡ã€ã«åã®å¯Ÿå¿ãš keyframe_reference ã®èšå®ãåå®è£
ãããããšã«ãªãã
éšåçææã« keyframe_1.png ãå¥ã®ã«ãããæãäºæ
ãæããŸãã
åç»ã®ä¿åãæããªãçç±: çæçŽåŸã«ä¿åãããšãçæãšä¿åã®éã«åŠçãæãåŒã³åºãåŽãå°ãããã§ãã
äŸãã°ç¶ç¶ãã§ãŒã³ãçµåã㊠final_video_url ãåããŠããä¿åãããå Žåã
ã¿ã€ã ã©ã€ã³åŽãæžããã¡ã¿ããŒã¿ã«ã¯å¿
ããã®å€ãæ¬ ããŸãã
ïŒä»¥å㯠VideoTimelineRunner.RunAndSave ããããŸãããããã®çç±ã§ã©ã®åŒã³åºãå
ã䜿ã£ãŠããããåé€ããŸãããïŒ
ð§ ã¬ã·ãã»ã«ããã®æäœ (Recipe helpers)
VideoRecipe / Cut ã¯ãåéã»åçæã®ããã«ç¶æ
ãèªã¿æžããããã«ããŒãæã¡ãŸãã
äžæ¬çæãéäžããåéããåŠçã¯ããããã䜿ã£ãŠãã©ã®ã«ããããŸã ããã倿ããŸãã
| API | çšé |
|---|---|
Cut.IsGenerated() / Cut.StatusïŒports.CutStatusïŒ |
ãã®ã«ãããçææžã¿ãã®å€å®ãšç¶æ |
Cut.ResetGeneration(keepKeyframe) |
çæçµæãæšãŠãŠåçæå¯Ÿè±¡ã«æ»ããkeepKeyframe=false 㯠keyframe_reference ãæ¶ããããæ¬¡ã® RunAndSave ã§ç»åããçŒãçŽãããŸã |
Cut.EffectiveDurationSec() |
ãã®ã«ããã®å®å¹å°º |
VideoRecipe.Normalize() |
ã«ããã®é£çªã»éå§ç§ã»SectionIndex / LocationAnchor ã®äŒæãæŽããŸã |
VideoRecipe.Validate() |
ã¬ã·ãã®æŽåæ§æ€èšŒ |
VideoRecipe.UsesModels() / Cuts.UniqueCharacterIDs() |
䜿çšã¢ãã«ã»ç»å Žãã£ã©ã¯ã¿ãŒã®éå |
ports.NextLastFrameReference(cuts, i) |
frames_to_video ã§æ¬¡ã«ããã® lastFrame ã«äœ¿ãåç
§ã®è§£æ±º |
ports.VideoRecipeSchema() |
å°æ¬çæã®æ§é ååºåã¹ããŒãïŒJSON SchemaïŒ |
workflow.New ãè¿ã *ports.Workflows ã¯ã䜿ãçµãã£ãã Close() ãåŒãã§ãã ãã
ïŒç»åãã£ãã·ã¥ã®ããã¯ã°ã©ãŠã³ã goroutine ã忢ããŸããè€æ°ååŒãã§ãå®å
šã§ãïŒã
å
¬éã»ä¿åã®å
¥åºå㯠ports.PublishOptions / ports.PublishResult ã§ãã
â ïž Sentinel Errors
åŒã³åºãåŽã errors.Is ã§å€å®ããæ±çšãšã©ãŒãšã¯ç°ãªãå¶åŸ¡ïŒãã©ãŒã«ããã¯ããªãã©ã€ïŒãè¡ãããããports ããã±ãŒãžã¯ä»¥äžã® sentinel error ãå
¬éããŠããŸãã
| ãšã©ãŒ | çºçæ¡ä»¶ | æ³å®ãããåŒã³åºãåŽã®å¯Ÿå¿ |
|---|---|---|
ports.ErrRecipeRequired |
VideoRecipe ãå¿
é ã®åŠçã« nil ãæž¡ããå Žå |
åŒã³åºãåŽã®å®è£ äžåãåºæ¬çã«çºçãããªã |
ports.ErrEditingNotSupported |
EditAndSave ã§ãèšå®æžã¿ã®ç»åçæãšã³ãžã³ãããŒãã¬ãŒã ç·šéïŒEditCutïŒãå®è£
ããŠããªãå Žå |
å
šäœåçæïŒRunAndSaveïŒãžã®ãã©ãŒã«ãã㯠|
ports.ErrInvalidAIResponse |
AI ã®å¿çããã¹ãã VideoRecipe ã® JSON ãšããŠè§£æã§ããªãã£ãå Žå |
ãããã¯ãŒã¯/èªèšŒãšã©ãŒãšåºå¥ãããªãã©ã€å€æ |
ports.ErrVideoRunnerNotConfigured |
VideoRunner æªèšå®ã®ãŸãŸ Workflows.Video ãåŒã³åºããå Žå |
åç»çæã¹ãããã®ã¹ããããèšå®äžåã®éç¥ |
ports.ErrInputTooLarge |
ãœãŒã¹ã®å ¥åãµã€ãºã蚱容äžéïŒ5MBïŒãè¶ ããå Žå | å ¥åã®åå²ããœãŒã¹ã®èŠçŽããä¿ã |
ports.ErrUnsupportedCutDuration |
ã«ããã®å°ºãã解決ãã Veo çæã¢ãŒãã§åãä»ããããªãå€ã ã£ãå Žå | ã¬ã·ãåŽã®å°ºã®èšç»ãã¹ããªãã©ã€ãã ports.SnapDuration çã§å°ºãå²ãåœãŠçŽã |
ports.ErrNoKeyframeToEdit |
EditAndSave ã®å¯Ÿè±¡ã«ããã«æ¢åã®ããŒãã¬ãŒã ãç¡ãå Žå |
å
ã« RunAndSave ã§ããŒãã¬ãŒã ãçæããã |
ports.ErrSingleCutRequired |
EditAndSave ã«åäžã«ãã以å€ã®ã¬ã·ããæž¡ããå Žå |
察象ã«ããã ãã®ã¬ã·ãã«çµã£ãŠåŒã³çŽã |
ð§Ÿ Music Recipe JSON
ScriptRunner 㯠sourceURL ã® Music Recipe JSON ã VideoRecipe ãšããŠè§£éããprompt builder ãž parsed object ãæž¡ããŸããprompt builder 㯠music_recipe.lyrics / music_recipe.sections ãããBGM ã®æåã»ææ
ã»çãäžãããå«ãåç»å°æ¬ JSON ãçæããŸãã
æè©æ¬æã¯ music_recipe.lyrics ã«ä¿åãããŸãããVeo prompt ãžçŽæ¥ã¯æ³šå
¥ãããŸãããæè©ã section ã®æå³ã¯ãscript generation stage ã® prompt builder ã cuts[].audio_cue ãš cuts[].visual_anchor ã«å±éããŸãã
å cut 㯠duration_sec ãš audio_cue ãæã€ãããVeo ãžã®ããã³ããã«ã¯ (synchronized with the heavy bass drop at 0:10) ã®ãããªåææç€ºãèªå泚å
¥ã§ããŸããã¢ããªåŽã®è²¬åã¯ãçæããã cuts ã衚瀺ã»ç·šéããããŒãã¬ãŒã çæãŸãã¯åç»çæãã©ãŒã ãžæž¡ãããšã§ãã
{
"project_title": "AIãã«ãã¢ãŒãã«è§£èª¬åç»",
"music_recipe": {
"title": "AIãã«ãã¢ãŒãã«è§£èª¬åç»",
"theme": "AIãã«ãã¢ãŒãã«è§£èª¬",
"mood": "90s retro mech synthwave",
"tempo": 120,
"lyrics": {
"title": "AIãã«ãã¢ãŒãã«è§£èª¬åç»",
"theme": "AIãã«ãã¢ãŒãã«è§£èª¬",
"hook": "æªæ¥ã®æ åå¶äœãã²ãã",
"lyrics": "[Verse] ç»é¢ã®å¥¥ã§å
ãèµ°ã\n[Chorus] æªæ¥ã®ã«ãããåãåºã",
"keywords": [
"AI",
"video",
"orchestration"
],
"mood": "90s retro mech synthwave",
"narrative": "AI ãæ åå¶äœã®å·¥çšãã€ãªãç©èª"
},
"instruments": [
"analog synth",
"electronic drums"
],
"sections": [
{
"name": "Intro",
"duration_seconds": 5,
"prompt": "quiet synth pad and clock tick"
},
{
"name": "Verse",
"duration_seconds": 5,
"prompt": "drum beat starts and tempo lifts"
},
{
"name": "Chorus",
"duration_seconds": 5,
"prompt": "bright synth lead and impact effects"
}
]
},
"cuts": [
{
"cut_index": 1,
"duration_sec": 5,
"audio_cue": "ã€ã³ããïŒéããªã·ã³ã»ã®ãããé³ãç§éã®é³ (mp3_segment_1)",
"visual_anchor": "æéã®äžã«ãã£ã©ã¯ã¿ãŒã®ç³ãå
ããã«ã¡ã©ããã£ãããšåŒããŠãã",
"character_id": "zundamon"
},
{
"cut_index": 2,
"duration_sec": 5,
"audio_cue": "Aã¡ãïŒæè©ã®å°å
¥ã«åãããŠãã©ã ã®ããŒããå»ãŸãå§ããããã³ãã¢ãã (mp3_segment_2)",
"visual_anchor": "æè©ã®ãç»é¢ã®å¥¥ã§å
ãèµ°ããããããã ããã®èåŸã«èµ°ãå
ã®ã©ã€ã³ãšããŠæ ååãã",
"character_id": "zundamon"
},
{
"cut_index": 3,
"duration_sec": 5,
"audio_cue": "ãµãïŒæè©ã® hook ã«åãããŠæ¿ããã·ã³ã»ã®ã¡ããã£ãšãšãã§ã¯ãé³ãå
¥ã (mp3_segment_3)",
"visual_anchor": "æè©ã®ãæªæ¥ã®ã«ãããåãåºããããã«ã¡ã©ãé«éæåããªãããµã€ããŒç©ºéãžåãæ¿ããåãã§è¡šçŸãã",
"character_id": "zundamon_metan"
}
]
}
ãã® JSON 㯠Normalize() ã«ãã start_sec / end_sec / status ãè£å®ãããŸããçæåŸã¯ keyframe_referenceãvideo_idãvideo_url ã远èšããã video_music_meta.json ãšããŠä¿åãããŸãã
Veo ã«æž¡ã prompt 㯠cuts[].visual_anchorãcuts[].audio_cueãmusic_recipe.moodãã¿ã€ã ã©ã€ã³æ
å ±ããæ§ç¯ãããŸããmusic_recipe.lyrics ã¯ã¡ã¿ããŒã¿ãšããŠä¿æãããŸãããåç»åãããæè©ã®å
容㯠cuts ãžå€æããŠããå¿
èŠããããŸãã
cuts ã空ã®å Žåã¯ãmusic_recipe.sections ããã«ããåãèªåçæããŸããmusic_recipe 㯠github.com/shouni/go-gemini-client/lyria.MusicRecipe ããã®ãŸãŸä¿æãããããæ¥œæ²çæåŽã® JSON 㯠music_recipe é
äžãžå
¥ããŸãã
å cut 㯠section_indexïŒ1å§ãŸãïŒã§ãç±æ¥ãšãªã£ã music_recipe.sections ã®äœçœ®ãä¿æããŸãã1ã»ã¯ã·ã§ã³ã scene_split çã§è€æ°ã«ããã«åå²ãããŠããåå²åŸã®å
šã«ãããåã section_index ãåŒãç¶ããããåŒã³åºãåŽã¯ start_sec ãšã»ã¯ã·ã§ã³ã®æéç¯å²ãçªãåãããŠéç®ããã«ãã«ããã®æå±ã»ã¯ã·ã§ã³ãçŽæ¥å€å®ã§ããŸããæç€ºçã«èšå®ãããŠããªãã«ããã¯ãNormalize() ã start_sec ããèªåçã«è£å®ããŸãã
â ïž
ports.Cutã®å éšæ§é ã«ã€ããŠ:Cutã® JSON ã¯ãã©ãããªæ§é ã®ãŸãŸã§ãããGo ã®æ§é äœãšããŠã¯AudioSync/KeyframeResult/VideoResult/ChainControlãžåå²ãããå¿åãã£ãŒã«ããšããŠåã蟌ãŸããŠããŸããcut.VideoIDã®ãããªãã£ãŒã«ãã¢ã¯ã»ã¹ã¯å€ãããŸããããports.Cut{DurationSec: 5, KeyframeReference: "..."}ã®ãããªãã©ãããªã³ã³ããžãããªãã©ã«ã¯ãports.Cut{AudioSync: ports.AudioSync{DurationSec: 5}, KeyframeResult: ports.KeyframeResult{KeyframeReference: "..."}}ã®ããã«ã°ã«ãŒãåäœã§æžãçŽãå¿ èŠããããŸãã
{
"project_title": "AIãã«ãã¢ãŒãã«è§£èª¬åç»",
"music_recipe": {
"title": "AIãã«ãã¢ãŒãã«è§£èª¬åç»",
"theme": "AIãã«ãã¢ãŒãã«è§£èª¬",
"mood": "upbeat electronic documentary score",
"tempo": 120,
"instruments": [
"analog synth",
"electronic drums",
"soft piano"
],
"sections": [
{
"name": "Verse",
"duration_seconds": 40,
"prompt": "quiet opening with restrained melody and gradual rhythmic build"
},
{
"name": "Chorus",
"duration_seconds": 45,
"prompt": "emotional peak with fuller instrumentation and stronger accents"
}
],
"AudioModel": "lyria-3-pro-preview",
"ComposeMode": "game_fantasy",
"Seed": 10
},
"cuts": []
}
ð ãããžã§ã¯ãæ§é (Project Structure)
æ¬ã¢ãŒããã¯ãã£ã¯ ports ã«ããæœè±¡åïŒHexagonal ArchitectureïŒ ãå¢çç·ãšããŠãããVeo API ã®ãšã³ããã€ã³ã倿Žãåç»åæãšã³ãžã³ã®å·®ãæ¿ãã容æã«è¡ããèšèšãæ¡çšããŠããŸãã
go-veo-orchestrator/
âââ workflow/ # ãçµ±å管çãåå·¥çšãçµã¿åãããWorkflows ã€ã³ã¿ãŒãã§ãŒã¹ãå®è£
ã
âââ runner/ # ãå®è¡å®äœãNewVideoScriptRunner / NewCutKeyframeRunner / NewVideoTimelineRunner /
â # NewVideoPublisherRunnerãVeo ãªã¯ãšã¹ãã®çµã¿ç«ãŠã¯ NewVideoRequestBuilder
â # ïŒãã£ã©ã¯ã¿ãŒåç
§ãå«ããå Žå㯠NewVideoRequestBuilderWithCharactersã
â # å·®ãæ¿ã㯠WithRequestBuilderãæ¢å®ã¯ DefaultVideoRequestBuilderïŒã
âââ keyframe/ # ãããŒãã¬ãŒã çææŠç¥ãã«ããåããã®éæ¢ç»çæïŒäžŠå床ã»ã¬ãŒãå¶éã€ãïŒãåç
§ç»åã®è§£æ±ºã¯ gemini-image-kitã
âââ ports/ # ãå¥çŽã»å®çŸ©ãInterfaceïŒVideoRunnerçïŒãå
±éã¢ãã«ãåäœèšå®(Config)ãå
šãŠã®èµ·ç¹ã
ð ã·ãŒã±ã³ã¹ãã㌠(Sequence Flow)
Video Orchestration Flow (NewVideoTimelineRunner)
sequenceDiagram
participant WF as workflow.manager
participant KeyframeGen as keyframe.Generator
participant ImageKit as gemini-image-kit
participant Timeline as runner.VideoTimelineRunner
participant Builder as runner.VideoRequestBuilder
participant VeoAPI as Vertex AI (Veo API)
participant Publisher as runner.VideoPublisherRunner
participant Writer as remoteio.Writer
Note over WF,KeyframeGen: 1) GenerationUnit / Keyframe Runner åæå
WF->>KeyframeGen: keyframe.NewGenerator(characters, imageGenerator, keyframePrompt, model, opts...)
KeyframeGen-->>WF: *keyframe.Generator
WF->>Timeline: runner.NewVideoTimelineRunner(keyframeRunner, videoRunner)
Timeline-->>WF: *runner.VideoTimelineRunner
Note over WF,Timeline: 2) Music Recipeã«åºã¥ãæ°ç ç¹ãåç»çæ
WF->>Timeline: Run(ctx, recipe)
Timeline->>KeyframeGen: Execute(ctx, recipe.Cuts)
KeyframeGen->>ImageKit: GenerateSingleImage(prompt + ImageURI{ReferenceURL})
Note over KeyframeGen,ImageKit: åç
§ã®è§£æ±ºïŒVertex+gs:// ã¯çŽæ¥åç
§ / Gemini API 㯠File API ãž1åã ãã¢ããããŒãïŒã¯ç»åãããåŽ
ImageKit-->>KeyframeGen: ããŒãã¬ãŒã ç»å
Note over Timeline,VeoAPI: Loopå
ã® Video-to-Video ã§åã«ããã®ã³ã³ããã¹ã(lastVideoID)ãé£é
Note over Timeline: generated cut 㯠video_id ã䜿ã£ãŠã¹ãããå¯èœ
loop cuts / sequential Video-to-Video chain
Timeline->>Builder: Build(recipe, cut, keyframe, lastVideoID)
Builder-->>Timeline: VideoGenerationRequest
Timeline->>VeoAPI: GenerateVideo(Prompt + KeyframeReference/InputImage + AudioReference + PreviousVideoID + Seed)
VeoAPI-->>Timeline: VideoResponse (CloudURL + VideoID)
Timeline->>Timeline: cut.video_id / cut.video_url / cut.status æŽæ°
end
Note over WF,Publisher: 3) ã¡ã¿ããŒã¿ã®ä¿å㯠Publish ãæ
åœïŒçæãšã¯å¥ã¹ãããïŒ
WF->>Publisher: Run(ctx, recipe, outputDir)
Publisher->>Writer: Write(ctx, video_music_meta.json, updatedVideoRecipeJSON, remoteio.WithContentType("application/json"), ...)
Publisher-->>WF: *ports.PublishResult
ð€ äŸåé¢ä¿ (Dependencies)
- shouni/gemini-image-kit - 鿢ç»ã»ããŒãã¬ãŒã çæã³ã¢åºç€ïŒåç §ç»åã®è§£æ±ºããããæ ããŸãïŒ
- shouni/go-gemini-client - Gemini API / Vertex AI ã¯ã©ã€ã¢ã³ãïŒå°æ¬çæã®æ§é ååºåã«äœ¿çšïŒ
- shouni/go-character-kit - ãã£ã©ã¯ã¿ãŒè³ç£ïŒcharacters.jsonïŒç®¡ç
- shouni/go-remote-io - GCS / ããŒã«ã« / HTTP 察å¿ã®èªã¿æžãæœè±¡å
- shouni/go-http-kit - HTTP ã¯ã©ã€ã¢ã³ãæœè±¡å
- shouni/go-utils - å ±éãŠãŒãã£ãªãã£
ð ã©ã€ã»ã³ã¹ (License)
ãã®ãããžã§ã¯ã㯠MIT License ã®äžã§å ¬éãããŠããŸãã
Directories
¶
| Path | Synopsis |
|---|---|
|
Package keyframe ã¯ãã«ããæ
å ±ãšãã£ã©ã¯ã¿ãŒå®çŸ©ããåç»ã®ããŒãã¬ãŒã ç»åã çæããããžãã¯ãæäŸããŸãã
|
Package keyframe ã¯ãã«ããæ å ±ãšãã£ã©ã¯ã¿ãŒå®çŸ©ããåç»ã®ããŒãã¬ãŒã ç»åã çæããããžãã¯ãæäŸããŸãã |
|
Package ports ã¯ãgo-veo-orchestrator ã®åã³ã³ããŒãã³ããäŸåãã ã€ã³ã¿ãŒãã§ãŒã¹ïŒããŒãïŒãšãåç»çæã«é¢ããå
±éããŒã¿åã»èšå®ãå®çŸ©ããŸãã
|
Package ports ã¯ãgo-veo-orchestrator ã®åã³ã³ããŒãã³ããäŸåãã ã€ã³ã¿ãŒãã§ãŒã¹ïŒããŒãïŒãšãåç»çæã«é¢ããå ±éããŒã¿åã»èšå®ãå®çŸ©ããŸãã |
|
Package runner ã¯ãåç»ã¬ã·ãã®å®è¡ïŒããŒãã¬ãŒã çæã»ã¹ã¯ãªããå®è¡ã» åç»çæã»å
¬éïŒãçµ±æ¬ããã©ã³ããŒçŸ€ãæäŸããŸãã
|
Package runner ã¯ãåç»ã¬ã·ãã®å®è¡ïŒããŒãã¬ãŒã çæã»ã¹ã¯ãªããå®è¡ã» åç»çæã»å ¬éïŒãçµ±æ¬ããã©ã³ããŒçŸ€ãæäŸããŸãã |
|
Package workflow ã¯ããã£ã©ã¯ã¿ãŒã»ããŒãã¬ãŒã ã»åç»çæããŸãã ã¯ãŒã¯ãããŒå
šäœã®èª¿æŽãšãã£ãã·ã¥ç®¡çãè¡ããŸãã
|
Package workflow ã¯ããã£ã©ã¯ã¿ãŒã»ããŒãã¬ãŒã ã»åç»çæããŸãã ã¯ãŒã¯ãããŒå šäœã®èª¿æŽãšãã£ãã·ã¥ç®¡çãè¡ããŸãã |