Add 30-second looping TTREE promo with Mac renders and original music

This commit is contained in:
ZASENJC committed 2026-10-16 19:14:20 +08:00
1 parent db1a28da00
commit 0a1c8f1f97
14 files changed
+3977

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+2
View File
@@ -0,0 +1,2 @@
node_modules/
out/
+32
View File
@@ -0,0 +1,32 @@
# TTREE · FLOW / 30 秒循环宣传片
1920 × 1080,30 fps,900 帧。Mac 界面组件重绘;不使用截图或录屏,不使用 Apple 标识。品牌图标来自项目 `assets/icons/icon.png`。界面结构依据实际 Mac 版以及 `apps/macos/src`;翻译、OCR、AI 能力依据根 README。渲染示意不是功能操作实录。
## 可编辑制作
- `src/index.tsx`:镜头、Mac 窗口组件、排版与逐帧动画。
- `synth.py`:原创 96 BPM 氛围电子配乐,12 小节,48 kHz 双声道;无第三方采样。
- `public/flow-original.wav`:可直接交付和剪辑的无损音乐。
- `out/TTREE-30s.mp4`:最终 H.264 / AAC 成片。
- `../../docs/images/promo-loop-30/cover.png`:封面预览。
安装:`npm ci`。预览:`npm run studio`。渲染:`bash render.sh`;可附加 `--browser-executable=/path/to/chrome`。依赖锁定于 package-lock.json。Node 22、FFmpeg、Python 3;若本机 Python 管道编码异常,直接运行文件。
## 脚本
0–4 秒:品牌图形在干净空间中呼吸,快捷键与窗口逐渐出现。
4–10 秒:轻轻唤起,接着思考;Mac 原生结构窗口从透视空间推近。
10–17 秒:两份译文,一眼对照;连续推进窗口并放大译文。
17–23 秒:截图识字与 AI 对话文案,选择框动效作为能力示意。
23–28 秒:品牌主张及项目地址。
28–30 秒:回到同一品牌空间,与下一轮连续相接。
## 音乐来源与许可
曲名:FLOW / TTREE 30。为此次项目用数学振荡器原创合成,无商业歌曲、外部录音、采样包或订阅。合成源码、生成的音乐及本目录原创动画沿用仓库 MIT 许可,可随成片复制、修改和分发。项目原有品牌资产保留原项目权利。配乐无需署名外部作者。
## 循环设计
900 帧构成一个周期,首尾不人为复制同一帧;品牌呼吸、光场及页脚进度由 900 帧周期函数驱动。中段场景在边界前完全退场,最后一帧至第一帧保持正常一帧运动距离。音乐事件及混响式衰减尾音按 30 秒环形缓冲折回,持续音使用周期内整数振荡次数。最终 QA 数据见 `out/qa.json`,连播检查文件为 `out/loop-check-60s.mp4`。
播放器是否能无间隔重复单个 MP4 取决于播放器的缓存与循环实现;成片内部素材接缝与两轮拼接文件已分别检查。
+3843
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
{"name":"ttree-promo-loop","private":true,"version":"1.0.0","scripts":{"studio":"remotion studio src/index.tsx","render":"remotion render src/index.tsx TTree out/TTREE-30s.mp4 --codec h264 --crf 17 --pixel-format yuv420p"},"dependencies":{"@remotion/cli":"4.0.532","remotion":"4.0.532","react":"19.1.0","react-dom":"19.1.0"}}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="zh"><meta charset="utf-8"><title>TTREE / FLOW</title><style>body{margin:0;background:#eeede9;color:#292b30;font:16px system-ui;padding:4vw}video{width:100%;max-height:80vh}h1{font-weight:500;letter-spacing:3px}p{color:#70747b}</style><h1>TTREE / FLOW</h1><p>30 秒 · 1920 × 1080 · 原创循环配乐 · Mac 界面渲染示意</p><video controls loop playsinline poster="../../docs/images/promo-loop-30/cover.png" src="out/TTREE-30s.mp4"></video><p>可编辑源码:src/index.tsx 配乐:synth.py / public/flow-original.wav</p></html>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+21
View File
@@ -0,0 +1,21 @@
{
"duration_seconds": 30.0,
"width": 1920,
"height": 1080,
"fps": "30/1",
"frames": 900,
"audio_codec": "aac",
"audio_hz": "48000",
"black_frames": [],
"seam_mean_pixel_delta_255": 0.009789737654320988,
"adjacent_start_delta": 0.0035204475308641976,
"adjacent_end_delta": 0.006655092592592593,
"aac_peak_linear": 0.6498312950134277,
"aac_boundary_jump_linear": 0.00845723832026124,
"aac_boundary_jump_dbfs": -41.45542862119808,
"audio_sample_delta_p99": 0.013609051704406738,
"audio_max_sample_delta": 0.03505164850503206,
"seam_below_normal_p99": true,
"double_loop_duration_seconds": 60.0,
"double_loop_frames": 1800
}
+22
View File
@@ -0,0 +1,22 @@
import subprocess,json,array,wave,math
from pathlib import Path
D=Path(__file__).resolve().parent
p=D/'out/TTREE-30s.mp4'
def run(args):return subprocess.check_output(args)
info=json.loads(run(['ffprobe','-v','error','-show_streams','-show_format','-of','json',str(p)]))
v=next(x for x in info['streams'] if x['codec_type']=='video')
a=next(x for x in info['streams'] if x['codec_type']=='audio')
# Decode all frames at analysis resolution; compare seam against adjacent motion.
raw=run(['ffmpeg','-v','error','-i',str(p),'-vf','scale=192:108','-f','rawvideo','-pix_fmt','gray','-'])
size=192*108; frames=[raw[i:i+size] for i in range(0,len(raw),size)]
def mae(a,b):return sum(abs(x-y) for x,y in zip(a,b))/len(a)
# Decode delivery AAC as well as original PCM; assess boundary sample jump.
sound=array.array('f',run(['ffmpeg','-v','error','-i',str(p),'-vn','-f','f32le','-ar','48000','-ac','2','-']))
jump=max(abs(sound[c]-sound[-2+c]) for c in [0,1])
peak=max(abs(x) for x in sound)
black=[i for i,x in enumerate(frames) if sum(x)/len(x)<10]
result={'duration_seconds':float(info['format']['duration']),'width':v['width'],'height':v['height'],'fps':v['r_frame_rate'],'frames':len(frames),'audio_codec':a['codec_name'],'audio_hz':a['sample_rate'],'black_frames':black,'seam_mean_pixel_delta_255':mae(frames[-1],frames[0]),'adjacent_start_delta':mae(frames[0],frames[1]),'adjacent_end_delta':mae(frames[-2],frames[-1]),'aac_peak_linear':peak,'aac_boundary_jump_linear':jump,'aac_boundary_jump_dbfs':20*math.log10(max(jump,1e-12))}
assert len(frames)==900 and not black and abs(result['duration_seconds']-30)<.001
assert jump<.01 and peak<1
(D/'out/qa.json').write_text(json.dumps(result,indent=2))
print(json.dumps(result,indent=2))
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"
python3 synth.py
npx remotion render src/index.tsx TTree out/picture.mp4 --codec h264 --crf 17 --pixel-format yuv420p --concurrency=4 "$@"
ffmpeg -y -i out/picture.mp4 -i public/flow-original.wav -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -b:a 320k -t 30 -movflags +faststart out/TTREE-30s.mp4
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import {AbsoluteFill,Composition,registerRoot,useCurrentFrame,interpolate,Img,Audio,staticFile} from 'remotion';
const clamp={extrapolateLeft:'clamp',extrapolateRight:'clamp'} as const;
const ease=(x:number)=>x*x*(3-2*x);
const ramp=(f:number,a:number,b:number)=>ease(Math.max(0,Math.min(1,(f-a)/(b-a))));
const Label=({top,title,sub,opacity=1}:{top:string,title:string,sub:string,opacity?:number})=><div style={{position:'absolute',left:140,top,opacity}}><div style={{fontSize:18,letterSpacing:5,color:'#777',marginBottom:28}}>TTREE / FLOW</div><div style={{fontSize:68,fontWeight:600,letterSpacing:-3,whiteSpace:'pre-line',lineHeight:1.24}}>{title}</div><div style={{fontSize:25,color:'#72747a',marginTop:30,lineHeight:1.65,whiteSpace:'pre-line'}}>{sub}</div></div>;
const MacWindow=()=> <div style={{width:966,height:1228,borderRadius:34,background:'linear-gradient(135deg,#fafbfdF7,#e8ecf2F5)',border:'2px solid #ffffff',boxShadow:'inset 0 0 0 1px #c7cbd3',overflow:'hidden',color:'#292d35'}}><div style={{height:102,display:'flex',alignItems:'center',padding:'0 36px',gap:20,borderBottom:'1px solid #ced2da'}}><span style={{background:'#dfe3e9',borderRadius:15,padding:'13px 28px',fontSize:34,fontWeight:600}}>翻译</span><span style={{fontSize:34,color:'#9a9ea6',marginLeft:30}}>AI</span><span style={{marginLeft:'auto',fontSize:32,color:'#7d8490'}}>⌖ ⚙</span></div><div style={{height:362,padding:'36px 38px',fontSize:34,lineHeight:1.55}}>Let language step back.<br/>Let ideas move forward.<div style={{marginTop:105,marginLeft:415,color:'#98a8bd',fontSize:34}}>▷</div></div><div style={{height:80,borderTop:'1px solid #cbd1dc',borderBottom:'1px solid #cbd1dc',display:'flex',alignItems:'center',padding:'0 38px',fontSize:29,color:'#6d7581'}}>自动检测 ⌃  →  中 ⇄ 英(自适应) ⌃</div><div style={{height:330,padding:'24px 38px',borderBottom:'1px solid #cbd1dc'}}><div style={{fontSize:23,color:'#9298a2',letterSpacing:1}}>翻译 · GOOGLE / BING <span style={{float:'right'}}>复制</span></div><div style={{fontSize:34,marginTop:38,lineHeight:1.6}}>让语言退后一步。<br/>让想法向前发展。</div></div><div style={{padding:'24px 38px'}}><div style={{fontSize:23,color:'#9298a2'}}>AI · MLX/LOCAL (HY-MT1.5-1.8B-bf16)<span style={{float:'right'}}>复制</span></div><div style={{fontSize:34,marginTop:38,lineHeight:1.6}}>让语言退一步吧,<br/>让思想向前发展。</div></div></div>;
export const Film=()=>{const f=useCurrentFrame(); const t=f/30;const theta=2*Math.PI*f/900;const open=ramp(f,35,135)*(1-ramp(f,730,845));const stage=ramp(f,130,200)*(1-ramp(f,680,750));const split=ramp(f,275,330)*(1-ramp(f,450,505));const zoom=1+split*.35; const screenX=1100+split*100;const screenY=520+split*70;const end=ramp(f,670,745)*(1-ramp(f,785,855));return <AbsoluteFill style={{background:'#f4f3ef',color:'#202124',fontFamily:'"PingFang SC", "Helvetica Neue", sans-serif',overflow:'hidden'}}>
<Audio src={staticFile('flow-original.wav')}/><div style={{position:'absolute',width:1100,height:1100,left:650+70*Math.sin(theta),top:-200+60*Math.cos(theta),borderRadius:'50%',background:'radial-gradient(circle,#dfebed 0%,#e8e7ef55 40%,transparent 70%)'}}/>
<div style={{position:'absolute',left:0,right:0,top:350,height:650,background:'radial-gradient(ellipse at center,#ddd9cf55,transparent 70%)'}}/>
<div style={{position:'absolute',left:140,top:72,fontSize:24,fontWeight:600,letterSpacing:4}}>TTREE</div><div style={{position:'absolute',right:140,top:80,fontSize:15,color:'#8a8b8d',letterSpacing:3}}>让语言退后。让想法向前。</div>
<div style={{position:'absolute',left:960,top:500,opacity:1-open,translate:'-50% -50%',scale:1+.04*Math.sin(theta)}}><Img src={staticFile('icon.png')} style={{width:150,height:150,borderRadius:34,boxShadow:'0 30px 65px #0002'}}/><div style={{fontSize:51,fontWeight:600,textAlign:'center',marginTop:22,letterSpacing:-2}}>TTREE</div></div>
<div style={{position:'absolute',opacity:stage, left:screenX,top:screenY,translate:'-50% -50%',scale:(.58+.05*Math.sin(Math.PI*stage))*zoom,transform:`perspective(1800px) rotateY(${-9+split*9}deg) rotateX(3deg)`,filter:'drop-shadow(0 55px 55px #20263026)'}}><MacWindow/></div>
<div style={{opacity:ramp(f,90,135)*(1-ramp(f,245,285))}}><Label top="325px" title={'轻轻唤起。\n接着思考。'} sub={'一个快捷键,回到心流。'}/><div style={{position:'absolute',left:140,top:665,display:'flex',gap:12}}>{['⌘','⇧','Space'].map(k=><div key={k} style={{padding:'18px 24px',border:'1px solid #ccc',borderBottomWidth:4,borderRadius:12,fontSize:27,background:'#fff8'}}>{k}</div>)}</div></div>
<div style={{opacity:split}}><Label top="305px" title={'两份译文。\n一眼对照。'} sub={'免费翻译 × AI 翻译\n在同一个原生窗口里。'}/><div style={{position:'absolute',left:140,top:770,color:'#85878b',fontSize:18}}>Mac 界面渲染示意 · 本地 HY-MT 翻译</div></div>
<div style={{opacity:ramp(f,475,515)*(1-ramp(f,650,690))}}><Label top="305px" title={'文字,从画面\n走出来。'} sub={'截图识字,接着翻译。\n切换 AI 对话,继续追问。'}/><div style={{position:'absolute',left:140,top:775,fontSize:23,color:'#888'}}>macOS / Windows · 原生体验</div></div>
<div style={{position:'absolute',left:900,top:320,width:440,height:190,border:'2px solid #a5b9bf',borderRadius:12,opacity:ramp(f,515,540)*(1-ramp(f,590,620)),translate:`${-60*ramp(f,515,580)}px 0px`,boxShadow:'0 0 0 1800px #f4f3ef22'}}/>
<div style={{position:'absolute',inset:0,background:'#f4f3ef',opacity:end}}><div style={{position:'absolute',left:140,top:320,fontSize:82,fontWeight:600,lineHeight:1.3,letterSpacing:-4}}>让语言退后。<br/>让想法向前。</div><div style={{position:'absolute',left:145,top:630,fontSize:27,color:'#76787c'}}>TTREE · 翻译 / OCR / AI</div><div style={{position:'absolute',left:145,top:740,fontSize:22,letterSpacing:1}}>github.com/ZASENJC/ttree</div><Img src={staticFile('icon.png')} style={{position:'absolute',width:230,height:230,right:240,top:400,borderRadius:50}}/></div>
<div style={{position:'absolute',bottom:70,left:140,fontSize:15,letterSpacing:3,color:'#96989b'}}>TRANSLATE. CAPTURE. THINK.</div><div style={{position:'absolute',right:140,bottom:70,width:85,height:2,background:'#aaa'}}><div style={{height:2,width:85*(.5+.5*Math.sin(theta)),background:'#24262b'}}/></div>
</AbsoluteFill>};
registerRoot(()=><Composition id="TTree" component={Film} durationInFrames={900} fps={30} width={1920} height={1080}/>);
+28
View File
@@ -0,0 +1,28 @@
"""Original 96 BPM / 12-bar ambient pulse. Circular synthesis, no samples."""
import math, wave, array
SR=48000; N=SR*30; beat=.625
out=[array.array('f',[0])*N for _ in range(2)]
def tone(start,dur,freq,amp,pan=.5,kind='bell'):
for j in range(int(dur*SR)):
u=j/SR
env=(1-math.exp(-u*60))*math.exp(-u*(3.2 if kind=='bell' else 1.9))
v=(math.sin(2*math.pi*freq*u)+.22*math.sin(2*math.pi*freq*2*u))*env*amp
idx=(int(start*SR)+j)%N
out[0][idx]+=v*math.sqrt(1-pan);out[1][idx]+=v*math.sqrt(pan)
# A warm Cmaj9 / Am9 / Fmaj9 / Gsus progression; all tails wrap.
chords=[[130.8128,164.8138,195.9977,246.9417],[110,130.8128,164.8138,195.9977],[87.3071,130.8128,164.8138,195.9977],[97.9989,146.8324,195.9977,220]]
for b in range(48):
chord=chords[(b//12)%4]
tone(b*beat,3,chord[b%4]*2,.12,.25+.5*(b%3)/2)
if b%2==0:tone(b*beat,1,chord[0]/2,.18,.5,'bass')
if b%4==2:tone(b*beat,2,chord[2]*4,.045,.7)
# slow pad uses integer cycles over the whole period, guaranteeing exact seam.
for i in range(N):
t=i/SR
for c in range(2):
out[c][i]+=.018*sum(math.sin(2*math.pi*round(f*30)*t/30+c*.13) for f in [130.8128,195.9977,246.9417])*(.8+.2*math.cos(2*math.pi*t/30))
peak=max(max(abs(v) for v in ch) for ch in out); pcm=array.array('h')
for i in range(N):
for c in range(2):pcm.append(round(out[c][i]/peak*.65*32767))
with wave.open('public/flow-original.wav','wb') as w:w.setnchannels(2);w.setsampwidth(2);w.setframerate(SR);w.writeframes(pcm.tobytes())
print('30 seconds, stereo PCM, peak',peak)