feat(render): Phase 2 — FlexStory render passthrough + journey template seed
Closes the render boundary so a user's scene list (order, per-scene content,
per-scene duration, theme) actually drives the FlexStory engine — the one gap the
scene-engine mapping found.
- render-svc GetFlexStoryProps (db.go): structured per-scene query that groups
saved_scene_contents BY scene (the flat GetRenderBindings union collides when
scenes share keys like "title"), recovers blockId from the scene key
("<BlockId>__<n>"), and emits the FlexStory props object
{scenes:[{blockId,durationSec,props}], accentColor, …}.
- render-svc Claim (internal.go): when the template is Remotion + comp starts with
"FlexStory", send that object as a single "__flexprops__" binding (no protocol
struct change).
- node-agent remotionProps (remotion.go): if "__flexprops__" is present, pass it
to `remotion render --props` verbatim (it's the complete props object).
- scripts/seed_flexstory.py: seeds the CharacterJourney template (7 scenes, theme
colours, FLEXIBLE) with blockId-encoded scene keys, so the studio's existing
CopyTemplateGraphAsync copies them into saved_scenes with zero studio-svc change.
Both Go services compile; template is live in the catalog (detail 200, per-aspect
previews). End-to-end render verification needs a live Remotion render node.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,15 @@ func npxCmd() string {
|
||||
// (logoText, accentColor, …) and Value is the user's edited string. Anything the
|
||||
// user didn't touch falls back to the composition's defaultProps.
|
||||
func remotionProps(job *Job) (string, error) {
|
||||
// FlexStory (scene engine) passthrough: the orchestrator already built the full
|
||||
// props object (scenes:[{blockId,durationSec,props}] + theme colours) as a single
|
||||
// "__flexprops__" binding. Use it verbatim — it's a complete JSON object, not a
|
||||
// flat key/value map.
|
||||
for _, b := range job.Bindings {
|
||||
if b.Key == "__flexprops__" {
|
||||
return b.Value, nil
|
||||
}
|
||||
}
|
||||
props := make(map[string]string, len(job.Bindings))
|
||||
for _, b := range job.Bindings {
|
||||
if b.Key == "" {
|
||||
|
||||
@@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -739,6 +740,108 @@ func (s *Store) GetRenderBindings(ctx context.Context, savedProjectID uuid.UUID)
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetFlexStoryProps builds the input-props object for a FlexStory (scene-engine)
|
||||
// render. Unlike GetRenderBindings (a flat key/value union, which collides when
|
||||
// several scenes share a key like "title"), this groups content BY SCENE and
|
||||
// returns the structured shape FlexStory expects:
|
||||
//
|
||||
// { "scenes": [ {blockId, durationSec, props:{key:val}}, … ],
|
||||
// "accentColor": "#…", "secondaryColor": "#…", … }
|
||||
//
|
||||
// blockId is encoded in the scene key as "<BlockId>__<n>" (n keeps the per-project
|
||||
// UNIQUE(key) happy across repeated blocks). Returns a JSON string ready for
|
||||
// `npx remotion render … --props=`.
|
||||
func (s *Store) GetFlexStoryProps(ctx context.Context, savedProjectID uuid.UUID) (string, error) {
|
||||
type sceneOut struct {
|
||||
BlockID string `json:"blockId"`
|
||||
DurationSec float64 `json:"durationSec"`
|
||||
Props map[string]string `json:"props"`
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT id, key, scene_length_sec FROM studio.saved_scenes
|
||||
WHERE saved_project_id = $1 ORDER BY sort`, savedProjectID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var scenes []*sceneOut
|
||||
byID := map[int64]*sceneOut{}
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var key string
|
||||
var dur float64
|
||||
if err := rows.Scan(&id, &key, &dur); err != nil {
|
||||
rows.Close()
|
||||
return "", err
|
||||
}
|
||||
blockID := key
|
||||
if i := strings.Index(key, "__"); i >= 0 {
|
||||
blockID = key[:i]
|
||||
}
|
||||
sc := &sceneOut{BlockID: blockID, DurationSec: dur, Props: map[string]string{}}
|
||||
scenes = append(scenes, sc)
|
||||
byID[id] = sc
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// per-scene content values → that scene's props
|
||||
crows, err := s.pool.Query(ctx, `
|
||||
SELECT c.saved_scene_id, c.key, COALESCE(c.value, '')
|
||||
FROM studio.saved_scene_contents c
|
||||
JOIN studio.saved_scenes s ON s.id = c.saved_scene_id
|
||||
WHERE s.saved_project_id = $1 AND c.value IS NOT NULL AND c.value <> ''`,
|
||||
savedProjectID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for crows.Next() {
|
||||
var sid int64
|
||||
var k, v string
|
||||
if err := crows.Scan(&sid, &k, &v); err != nil {
|
||||
crows.Close()
|
||||
return "", err
|
||||
}
|
||||
if sc := byID[sid]; sc != nil {
|
||||
sc.Props[k] = v
|
||||
}
|
||||
}
|
||||
crows.Close()
|
||||
if err := crows.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
out := map[string]any{"scenes": scenes}
|
||||
|
||||
// project-wide theme colours → top-level props (accentColor, …)
|
||||
clr, err := s.pool.Query(ctx,
|
||||
`SELECT element_key, value FROM studio.saved_shared_colors
|
||||
WHERE saved_project_id = $1 AND value IS NOT NULL AND value <> ''`, savedProjectID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for clr.Next() {
|
||||
var k, v string
|
||||
if err := clr.Scan(&k, &v); err != nil {
|
||||
clr.Close()
|
||||
return "", err
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
clr.Close()
|
||||
if err := clr.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateExportForJob(ctx context.Context, jobID uuid.UUID) (*models.Export, error) {
|
||||
// Look up the job to get tenant/user/project context
|
||||
job, err := s.getJobByIDInternal(ctx, jobID)
|
||||
|
||||
@@ -321,7 +321,16 @@ func (h *InternalHandler) Claim(c *gin.Context) {
|
||||
|
||||
// User's edited input values → the node writes them into the AE project before
|
||||
// rendering, or passes them as Remotion --props. Non-fatal: empty → template defaults.
|
||||
bindings, _ := h.store.GetRenderBindings(c.Request.Context(), job.SavedProjectID)
|
||||
// FlexStory (scene engine) needs the structured per-scene shape (grouped by scene
|
||||
// + per-scene duration + theme colours); everything else uses the flat union.
|
||||
var bindings []models.RenderBinding
|
||||
if rcfg.Engine == "Remotion" && strings.HasPrefix(rcfg.CompName, "FlexStory") {
|
||||
if flex, ferr := h.store.GetFlexStoryProps(c.Request.Context(), job.SavedProjectID); ferr == nil && flex != "" {
|
||||
bindings = []models.RenderBinding{{Key: "__flexprops__", Type: "json", Value: flex}}
|
||||
}
|
||||
} else {
|
||||
bindings, _ = h.store.GetRenderBindings(c.Request.Context(), job.SavedProjectID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.ClaimedJob{
|
||||
JobID: job.ID,
|
||||
|
||||
Reference in New Issue
Block a user