Add faster TTREE promo v2 showcasing translation OCR and model settings
This commit is contained in:
1 parent
0a1c8f1f97
commit
4215c9e768
9 files changed
+118
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 592 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,20 @@
|
||||
# TTREE FLOW / v2
|
||||
|
||||
保留 v1。v2 为 30 秒、1920×1080、30 fps,Mac 版组件重绘,非截图或录屏。时间压缩呈现交互,下载速度及文本内容均为演示。
|
||||
|
||||
| 时间 | 展示内容 |
|
||||
| --- | --- |
|
||||
| 0–3.75 秒 | TTREE 标识与品牌主张,鼓点建立 |
|
||||
| 3.75–7.50 秒 | 输入英文、免费翻译返回、AI 中文逐字流式输出、双译文对照 |
|
||||
| 7.50–11.25 秒 | 截图快捷键、框选英文、正在识别、识别完成、结果回填至翻译页 |
|
||||
| 11.25–15.00 秒 | AI 对话、历史对话条、新对话入口、用户追问、助手流式回答 |
|
||||
| 15.00–18.75 秒 | AI 翻译设置、启用开关、HY-MT 接入、4-bit 精度、环境检测、下载进度、启动就绪 |
|
||||
| 18.75–22.50 秒 | AI 对话设置、兼容接口及遮罩密钥、切换 Ollama、本机地址、探测模型、提示词、保存配置 |
|
||||
| 22.50–26.25 秒 | 快捷键,再切至通用:透明度、开机启动、OCR 换行合并、检查更新;Esc 与置顶文案 |
|
||||
| 26.25–30.00 秒 | 返回品牌空间及项目地址,与开头相接 |
|
||||
|
||||
功能核实:apps/macos/src/state.rs 中四个设置分区、HY-MT 控件与服务状态,AiTranslateDelta / ChatDelta 流式文本事件以及顶部历史选取条。OCR 仍为原生框选与 Vision,本片选择框只为重绘动效,不改动产品。
|
||||
|
||||
配乐:synth-v2.py 原创 128 BPM、16 小节电子乐。数学合成鼓、低音、琶音与铺底,无外部采样,沿用仓库 MIT。3.75 秒双小节镜头同步,尾音环形折回。无实际听音能力,不宣称听感验收通过。
|
||||
|
||||
制作源码 src/v2.tsx;执行 bash render-v2.sh。最终文件 out/TTREE-30s-v2.mp4。QA 数据 qa-v2-results.json;六格预览在 docs/images/promo-loop-30/storyboard-v2.jpg。
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"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.009596836419753086,
|
||||
"adjacent_start_delta": 0.0025559413580246914,
|
||||
"adjacent_end_delta": 0.007860725308641976,
|
||||
"aac_peak_linear": 0.5807895064353943,
|
||||
"aac_boundary_jump_linear": 0.004308376461267471,
|
||||
"aac_boundary_jump_dbfs": -47.31372711069423,
|
||||
"audio_sample_delta_p99": 0.03425845503807068,
|
||||
"seam_below_normal_p99": true
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import subprocess,json,array,wave,math
|
||||
from pathlib import Path
|
||||
D=Path(__file__).resolve().parent
|
||||
p=D/'out/TTREE-30s-v2.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
|
||||
d=sorted(abs(sound[i]-sound[i-2]) for i in range(2,len(sound)))
|
||||
result['audio_sample_delta_p99']=d[int(.99*len(d))]
|
||||
result['seam_below_normal_p99']=jump<result['audio_sample_delta_p99']
|
||||
assert jump<.01 and result['seam_below_normal_p99'] and peak<1
|
||||
(D/'out/qa-v2.json').write_text(json.dumps(result,indent=2))
|
||||
print(json.dumps(result,indent=2))
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
python3 synth-v2.py
|
||||
npx remotion render src/v2.tsx TTreeV2 out/picture-v2.mp4 --codec h264 --crf 17 --pixel-format yuv420p --concurrency=4 "$@"
|
||||
ffmpeg -v error -y -i out/picture-v2.mp4 -i public/flow-v2.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-v2.mp4
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';import {AbsoluteFill,Composition,registerRoot,useCurrentFrame,Img,Audio,staticFile} from 'remotion';
|
||||
const beat=112.5,blue='#4569e8',e=(x:number)=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x)},txt=(s:string,f:number,a=5,d=60)=>s.slice(0,Math.floor(s.length*Math.max(0,Math.min(1,(f-a)/d))));
|
||||
const Field=({label,value}:{label:string,value:string})=><div style={{display:'flex',gap:20,alignItems:'center',margin:'18px 0',fontSize:23}}><span style={{width:125,color:'#7a8598'}}>{label}</span><span style={{flex:1,padding:'13px 18px',background:'#ffffffaa',border:'1px solid #d2d9e6',borderRadius:9}}>{value}</span></div>;
|
||||
const Btn=({children}:{children:React.ReactNode})=><span style={{display:'inline-block',background:blue,color:'white',fontSize:22,padding:'12px 22px',borderRadius:8,marginRight:15}}>{children}</span>;
|
||||
const Panel=({children,tab='',settings=false}:{children:React.ReactNode,tab?:string,settings?:boolean})=><div style={{width:1120,height:780,background:'linear-gradient(135deg,#fbfcfef5,#e6ebf4f5)',borderRadius:27,overflow:'hidden',border:'1px solid white',boxShadow:'0 45px 85px #20305022,inset 0 0 0 1px #cfd7e4'}}><div style={{height:70,display:'flex',alignItems:'center',gap:40,padding:'0 30px',borderBottom:'1px solid #d0d8e6',fontSize:25}}><b>翻译</b><b style={{color:tab==='AI'?'#303b4d':'#9ca4b1'}}>AI</b><span style={{marginLeft:'auto',color:'#929bad'}}>⌖ ◷ ⚙</span></div><div style={{display:'flex'}}>{settings&&<div style={{width:200,padding:'30px 18px',background:'#e2e8f2bb',height:710,boxSizing:'border-box'}}>{['AI 翻译','AI 对话','快捷键','通用'].map(x=><div key={x} style={{fontSize:23,padding:'18px 17px',marginBottom:10,borderRadius:10,background:x===tab?'#fff9':'transparent',color:x===tab?'#303b4d':'#8b95a6'}}>{x}</div>)}</div>}<div style={{padding:'30px 36px',flex:1,minWidth:0}}>{children}</div></div></div>;
|
||||
function Translate({f,ocr=false}:{f:number,ocr?:boolean}){const input=ocr?'Ideas deserve room to grow.':'Let language step back. Let ideas move forward.',cn=ocr?'让想法拥有生长的空间。':'让语言退后。让想法向前。';return <Panel><div style={{fontSize:31,lineHeight:1.6,height:155}}>{txt(input,f,0,20)}</div><div style={{fontSize:22,color:'#7a8598',padding:'17px 0',borderTop:'1px solid #ccd6e5',borderBottom:'1px solid #ccd6e5'}}>自动检测 ⌃ → 中 ⇄ 英(自适应) ⌃</div><div style={{fontSize:19,color:'#929bad',marginTop:24}}>翻译 · GOOGLE / BING <span style={{float:'right'}}>复制</span></div><div style={{fontSize:32,height:145,paddingTop:25}}>{f>24?cn:'翻译中…'}</div><div style={{fontSize:19,color:'#929bad',paddingTop:20,borderTop:'1px solid #ccd6e5'}}>AI · HY-MT1.5 <span style={{float:'right'}}>复制</span></div><div style={{fontSize:32,paddingTop:25}}>{txt(cn,f,30,60)}{f<94&&<span style={{color:blue}}>▎</span>}</div></Panel>}
|
||||
function Ocr({f}:{f:number}){return <div style={{position:'relative'}}><div style={{opacity:1-e((f-53)/12)}}><Panel><div style={{fontSize:20,letterSpacing:4,color:'#8c98ac'}}>READ / CAPTURE / CONTINUE</div><div style={{fontSize:58,lineHeight:1.4,marginTop:155}}>Ideas deserve<br/>room to grow.</div><div style={{position:'absolute',left:18,top:222,width:650*e(f/28),height:210*e(f/28),border:'3px solid #4569e8',background:'#4569e80a'}}/><div style={{fontSize:24,color:blue,marginTop:110}}>{f<32?'⌘ ⇧ S 框选屏幕文字':f<53?'正在识别…':'识别完成'}</div></Panel></div><div style={{position:'absolute',inset:0,opacity:e((f-53)/12)}}><Translate f={f-48} ocr/></div></div>}
|
||||
function Chat({f}:{f:number}){return <Panel tab="AI"><div style={{padding:14,background:'#e2e9f5',borderRadius:9,fontSize:21,color:'#818ca0'}}>历史对话 ⌃ 当前新对话 <span style={{float:'right'}}>+ 新对话</span></div><div style={{margin:'40px 0 35px auto',width:600,padding:24,borderRadius:17,background:'#dbe5ff',fontSize:28}}>换一种更自然的说法。</div><div style={{fontSize:20,color:'#929bad'}}>AI</div><div style={{fontSize:32,lineHeight:1.7,minHeight:240,whiteSpace:'pre-line',marginTop:20}}>{txt('让语言不再成为阻碍,\n让每一个想法自在向前。',f,18,64)}</div><div style={{border:'1px solid #cbd5e6',borderRadius:12,padding:22,fontSize:24,color:'#9aa3b3'}}>继续追问… <span style={{float:'right'}}>↵</span></div></Panel>}
|
||||
function Download({f}:{f:number}){const p=e((f-25)/61);return <Panel settings tab="AI 翻译"><h2 style={{fontSize:28,margin:'0 0 15px'}}>AI 翻译</h2><div style={{fontSize:21,color:blue}}>☑ 启用 AI 翻译</div><Field label="接入方式" value="内置 HY-MT1.5 ⌃"/><Field label="模型精度" value="4-bit ⌃"/><div style={{marginTop:25}}><Btn>{f<25?'检测环境':f<90?'取消下载':'下载并运行'}</Btn><span style={{fontSize:23,color:'#8390a5'}}>停止 删除</span></div><div style={{padding:25,background:'#e0e8f6',borderRadius:12,marginTop:35}}><div style={{fontSize:23,color:'#596a87'}}>{f<25?'✓ 环境支持 HY-MT1.5':f<90?'正在下载 HY-MT1.5-1.8B':'✓ HY-MT1.5-1.8B 已启动并就绪'}</div><div style={{height:9,background:'#c5d1e8',borderRadius:10,overflow:'hidden',marginTop:28}}><div style={{width:`${100*p}%`,height:9,background:blue}}/></div><div style={{fontSize:24,color:blue,marginTop:20}}>{Math.round(100*p)}%</div></div><div style={{fontSize:18,color:'#9aa4b6',marginTop:28}}>下载过程加速演示 · 模型按需独立下载</div></Panel>}
|
||||
function Config({f}:{f:number}){return <Panel settings tab="AI 对话"><h2 style={{fontSize:27,margin:'0 0 20px'}}>AI 对话 · 独立模型与系统提示词</h2><Field label="接入方式" value={f<35?'OpenAI 兼容服务 ⌃':'Ollama ⌃'}/><Field label="接口地址" value={f<35?'https://api.example.com/v1':'http://127.0.0.1:11434'}/>{f<35&&<Field label="API 密钥" value="••••••••••••••••"/>}<Field label="模型名称" value={f<35?'自定义模型名称':f<60?'正在探测本机模型…':'已发现本地模型 ⌃'}/><Btn>探测模型</Btn><div style={{fontSize:21,color:'#8a95a9',marginTop:25}}>系统提示词</div><div style={{fontSize:24,background:'#fff9',border:'1px solid #cbd6e7',padding:20,borderRadius:9,margin:'12px 0 25px'}}>请用简洁、自然的中文回答。</div><Btn>保存配置</Btn><span style={{fontSize:21,color:blue}}>{f>85?'✓ 已保存':''}</span></Panel>}
|
||||
function Prefs({f}:{f:number}){const g=f>=53;return <Panel settings tab={g?'通用':'快捷键'}><h2 style={{fontSize:29,margin:'0 0 25px'}}>{g?'通用与外观':'全局快捷键'}</h2>{g?<><Field label="面板透明度" value="━━━━━━━━● 92%"/><div style={{fontSize:24,lineHeight:2.5,marginTop:30}}>☑ 开机自动启动<br/>☑ OCR 识别文字自动去掉换行</div><div style={{marginTop:45}}><Btn>检查更新</Btn></div><div style={{fontSize:23,color:'#8c97a9',marginTop:40}}>Esc 收起 · 点按置顶</div></>:<><Field label="呼出 / 收起" value="⌘ ⇧ Space"/><Field label="截图翻译" value="⌘ ⇧ S"/><Field label="划词翻译" value="按需录入组合键"/><Field label="AI 对话" value="按需录入组合键"/><Btn>保存快捷键</Btn></>}</Panel>}
|
||||
export const FilmV2=()=>{const f=useCurrentFrame(),p=Math.floor(f/beat),l=f-p*beat,theta=2*Math.PI*f/900,active=p>0&&p<7,a=e(l/7)*(1-e((l-105)/7));const titles=['','实时输出,\n思路不停。','框选。识别。\n即刻翻译。','保留上下文,\n继续追问。','模型下载,\n一步就绪。','云端或本地,\n由你选择。','每个习惯,\n都顺手。'];const subs=['','免费译文 + AI 流式翻译','原生截图识字 · 结果自动回填','AI 对话 · 历史对话 · 新对话','内置 HY-MT1.5 · 环境检测','AI 翻译与 AI 对话分别配置','快捷键 · 通用与外观'];return <AbsoluteFill style={{background:'#f3f3ef',color:'#293448',fontFamily:'"PingFang SC","Helvetica Neue",sans-serif',overflow:'hidden'}}><Audio src={staticFile('flow-v2.wav')}/><div style={{position:'absolute',width:1400,height:1300,left:600+60*Math.sin(theta),top:-200,background:'radial-gradient(ellipse,#d9e5f5aa,transparent 65%)'}}/><div style={{position:'absolute',left:100,top:65,fontSize:25,fontWeight:600,letterSpacing:5}}>TTREE</div><div style={{position:'absolute',right:100,top:70,fontSize:17,color:'#8c96a8',letterSpacing:3}}>TRANSLATE. CAPTURE. THINK.</div>{active?<><div style={{position:'absolute',left:100,top:285,width:450,opacity:a,translate:`0px ${20*(1-e(l/12))}px`}}><div style={{fontSize:18,letterSpacing:4,color:blue,marginBottom:28}}>0{p} / FLOW</div><div style={{fontSize:58,fontWeight:600,lineHeight:1.3,letterSpacing:-2,whiteSpace:'pre-line'}}>{titles[p]}</div><div style={{fontSize:24,lineHeight:1.8,color:'#7e8a9e',marginTop:30}}>{subs[p]}</div></div><div style={{position:'absolute',left:650,top:175,opacity:a,scale:.92+.025*Math.sin(Math.PI*l/beat),transform:`perspective(2000px) rotateY(${-4+8*e(l/beat)}deg)`,transformOrigin:'center'}}>{p===1?<Translate f={l}/>:p===2?<Ocr f={l}/>:p===3?<Chat f={l}/>:p===4?<Download f={l}/>:p===5?<Config f={l}/>:<Prefs f={l}/>}</div></>:<div style={{position:'absolute',inset:0,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',opacity:p===0?1-e((l-85)/27):e(l/20)}}><Img src={staticFile('icon.png')} style={{width:170,height:170,borderRadius:37,boxShadow:'0 30px 65px #20304920',scale:1+.035*Math.sin(theta)}}/><div style={{fontSize:61,fontWeight:600,letterSpacing:-2,marginTop:30}}>TTREE</div><div style={{fontSize:29,color:'#7e899c',marginTop:25}}>让语言退后。让想法向前。</div><div style={{fontSize:20,color:'#929bad',marginTop:27}}>github.com/ZASENJC/ttree</div></div>}<div style={{position:'absolute',left:100,bottom:55,fontSize:16,color:'#929bad',letterSpacing:2}}>Mac 界面渲染示意 · 操作过程压缩呈现</div><div style={{position:'absolute',right:100,bottom:58,width:120,height:3,background:'#d5dce7'}}><div style={{height:3,width:120*(.5+.5*Math.sin(theta)),background:blue}}/></div></AbsoluteFill>};registerRoot(()=><Composition id="TTreeV2" component={FilmV2} durationInFrames={900} fps={30} width={1920} height={1080}/>);
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Original 128 BPM / 16-bar electronic pulse. Circular synthesis, no samples."""
|
||||
import math, wave, array
|
||||
SR=48000; N=SR*30; beat=.46875
|
||||
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(64):
|
||||
chord=chords[(b//16)%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)
|
||||
# Original synthesized kick, clap and metallic hats; no samples.
|
||||
for step in range(128):
|
||||
start=step*beat/2
|
||||
for j in range(int(.28*SR)):
|
||||
u=j/SR;idx=(int(start*SR)+j)%N
|
||||
kick=math.sin(2*math.pi*(48*u+8*(1-math.exp(-u*35))))*math.exp(-u*19)*.28 if step%2==0 else 0
|
||||
hat=(math.sin(2*math.pi*7301*u)*math.sin(2*math.pi*5123*u))*math.exp(-u*90)*.055
|
||||
clap=(math.sin(2*math.pi*1777*u)*math.sin(2*math.pi*2341*u))*math.exp(-u*32)*.11 if step%8 in [2,6] else 0
|
||||
for c in range(2):out[c][idx]+=kick+hat+clap
|
||||
# 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*.58*32767))
|
||||
with wave.open('public/flow-v2.wav','wb') as w:w.setnchannels(2);w.setsampwidth(2);w.setframerate(SR);w.writeframes(pcm.tobytes())
|
||||
print('30 seconds, stereo PCM, peak',peak)
|
||||
Reference in new issue
Block a user