Spaces:
Running
Running
wolf-coming: 多角色配音 + 角色与 14 场 + 配乐音效 + 网页
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +5 -0
- wolf-coming/README.md +8 -4
- wolf-coming/scripts/build_bundle.sh +3 -0
- wolf-coming/scripts/tts_multi.py +61 -0
- wolf-coming/video/audio/v_01_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_02_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_03_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_04_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_04_2_boy.mp3 +0 -0
- wolf-coming/video/audio/v_05_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_05_2_farmer.mp3 +0 -0
- wolf-coming/video/audio/v_06_1_boy.mp3 +0 -0
- wolf-coming/video/audio/v_06_2_n.mp3 +0 -0
- wolf-coming/video/audio/v_07_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_07_2_boy.mp3 +0 -0
- wolf-coming/video/audio/v_07_3_n.mp3 +0 -0
- wolf-coming/video/audio/v_08_1_boy.mp3 +0 -0
- wolf-coming/video/audio/v_08_2_farmer.mp3 +0 -0
- wolf-coming/video/audio/v_09_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_10_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_10_2_boy.mp3 +0 -0
- wolf-coming/video/audio/v_11_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_11_2_farmer.mp3 +0 -0
- wolf-coming/video/audio/v_11_3_n.mp3 +0 -0
- wolf-coming/video/audio/v_12_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_13_1_n.mp3 +0 -0
- wolf-coming/video/audio/v_14_1_n.mp3 +0 -0
- wolf-coming/video/film.bundle.js +0 -0
- wolf-coming/video/film/fonts/chars.txt +1 -0
- wolf-coming/video/film/fonts/mashanzheng.woff2 +3 -0
- wolf-coming/video/film/fonts/wenkai.woff2 +3 -0
- wolf-coming/video/film/voice.mp3 +3 -0
- wolf-coming/video/index.html +1 -0
- wolf-coming/video/out/subs.srt +259 -0
- wolf-coming/video/storyboard.json +30 -0
- wolf-coming/video/timeline.json +1134 -0
- wolf-coming/video/wolf/voice.mp3 +3 -0
- wolf-coming/web/public/film/fonts/chars.txt +1 -1
- wolf-coming/web/public/film/fonts/mashanzheng.woff2 +2 -2
- wolf-coming/web/public/film/fonts/wenkai.woff2 +2 -2
- wolf-coming/web/public/wolf/voice.mp3 +3 -0
- wolf-coming/web/src/App.tsx +19 -11
- wolf-coming/web/src/components/Covers.tsx +10 -8
- wolf-coming/web/src/components/ExportPanel.tsx +3 -3
- wolf-coming/web/src/components/LivePhotos.tsx +5 -5
- wolf-coming/web/src/components/Making.tsx +5 -11
- wolf-coming/web/src/components/Player.tsx +2 -2
- wolf-coming/web/src/components/PublishCopy.tsx +4 -3
- wolf-coming/web/src/film/audio.ts +39 -214
- wolf-coming/web/src/film/film.ts +15 -9
.gitattributes
CHANGED
|
@@ -160,3 +160,8 @@ wolf-coming/web/public/film/live/06-fly.jpg filter=lfs diff=lfs merge=lfs -text
|
|
| 160 |
wolf-coming/web/public/film/qa/sheet_landscape.jpg filter=lfs diff=lfs merge=lfs -text
|
| 161 |
wolf-coming/web/public/film/qa/sheet_portrait.jpg filter=lfs diff=lfs merge=lfs -text
|
| 162 |
wolf-coming/web/public/film/voice.mp3 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
wolf-coming/web/public/film/qa/sheet_landscape.jpg filter=lfs diff=lfs merge=lfs -text
|
| 161 |
wolf-coming/web/public/film/qa/sheet_portrait.jpg filter=lfs diff=lfs merge=lfs -text
|
| 162 |
wolf-coming/web/public/film/voice.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 163 |
+
wolf-coming/video/film/fonts/mashanzheng.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 164 |
+
wolf-coming/video/film/fonts/wenkai.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 165 |
+
wolf-coming/video/film/voice.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 166 |
+
wolf-coming/video/wolf/voice.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 167 |
+
wolf-coming/web/public/wolf/voice.mp3 filter=lfs diff=lfs merge=lfs -text
|
wolf-coming/README.md
CHANGED
|
@@ -60,12 +60,16 @@
|
|
| 60 |
- 每帧只由 t 决定;`vibe det` 校验。
|
| 61 |
|
| 62 |
## 4. 步骤
|
| 63 |
-
1. 写本 README → 推送 HF
|
| 64 |
-
2. 分镜 + 多角色配音 → 时间轴 → 推送
|
| 65 |
-
3. 引擎多片化(《丑小鸭》画面逐像素不变)+ 本片角色与 14 场 → 接触表 / 静帧 / det → 推送
|
| 66 |
-
4. 配乐音效混音 + 网页 → 推送
|
| 67 |
5. 封面 + 四平台文案 + 实况图 → 推送
|
| 68 |
6. 全片渲染 + `vibe qa` → 修复 → 部署 `/wolf-coming/` → 最终推送
|
| 69 |
|
| 70 |
## 5. 进度日志
|
| 71 |
- **2026-10-18** 写下本计划。沿用上一部确认过的风格与交付要求。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
- 每帧只由 t 决定;`vibe det` 校验。
|
| 61 |
|
| 62 |
## 4. 步骤
|
| 63 |
+
1. ✅ 写本 README → 推送 HF
|
| 64 |
+
2. ✅ 分镜 + 多角色配音 → 时间轴 → 推送
|
| 65 |
+
3. ✅ 引擎多片化(《丑小鸭》画面逐像素不变)+ 本片角色与 14 场 → 接触表 / 静帧 / det → 推送
|
| 66 |
+
4. ✅ 配乐音效混音 + 网页 → 推送
|
| 67 |
5. 封面 + 四平台文案 + 实况图 → 推送
|
| 68 |
6. 全片渲染 + `vibe qa` → 修复 → 部署 `/wolf-coming/` → 最终推送
|
| 69 |
|
| 70 |
## 5. 进度日志
|
| 71 |
- **2026-10-18** 写下本计划。沿用上一部确认过的风格与交付要求。
|
| 72 |
+
- **2026-10-18** 分镜 14 场(`video/storyboard.json`),23 句台词三种声音:旁白晓晓、放羊娃云夏、农民云健。`scripts/tts_multi.py` 逐句合成 → 按角色统一响度 → 拼接人声轨 + `timeline.json`(与 vibe 同格式,另含 lines/who),全片 137.9 秒。
|
| 73 |
+
- **2026-10-18** 引擎多片化:`web/src/film/` 只放共用引擎(宣纸 / 毛笔 / 合成器 / 混音器 / 导出 / 封面 / 实况图),每部片子在 `web/src/films/<片名>/` 提供时间轴、场景、配乐、文案。改造后《丑小鸭》11 个时间点逐帧 MD5 与改造前完全一致;音轨差异 ≤1 LSB(与同一份旧代码两次渲染之间的差异相同,来自浏览器混响多线程)。
|
| 74 |
+
- **2026-10-18** 本片角色:放羊娃(IK 骨架:站、走、躺、喊、笑、怕、坐抱小羊;冲天辫、红褂、表情系统)、农民 ×4(头巾 / 草帽 / 发髻,锄地、扛锄奔跑、叉腰、指责、摆手)、羊(卷毛、吃草、奔逃)、狼(潜行、奔跑、龇牙、叼羊、树丛里的绿眼睛);景物:梯田、茅草屋炊烟、山石皴法、大树、灌木、乌云;「狼来了」朱红大字从嘴边喊出、第三次被风吹散成墨点。
|
| 75 |
+
- **2026-10-18** 配乐音效:调皮主题(拨弦跳音 + 木鱼)、田园笛、狼来时低音颤奏 + 心跳 + 不协和重音、羊群奔逃的急促颤奏、夕阳下的笛声哀歌;音效含合成羊叫、山谷回声(把放羊娃的喊声再放三遍、越来越远)、锄头落地(与画面锄地动作逐拍对齐)、脚步、喘气、雷声、狼嚎、低吼、羊群奔逃、蟋蟀。响度 −14.0 LUFS。
|
wolf-coming/scripts/build_bundle.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
# 打包无头渲染入口(两个 vibe 项目共用同一份 bundle;狼来了的 index.html 自动带 ?film=wolf)
|
| 3 |
+
cd /workspaces/frontend && ./node_modules/.bin/esbuild src/film/standalone.ts --bundle --format=iife --outfile=/workspaces/ugly-duckling/video/film.bundle.js --loader:.json=json --log-level=error && cp /workspaces/ugly-duckling/video/film.bundle.js /workspaces/wolf-coming/video/film.bundle.js
|
wolf-coming/scripts/tts_multi.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""多角色配音 → timeline.json(与 vibe-voice tts.py 同格式,另含 lines/who)+ audio/voice.wav + out/subs.srt
|
| 3 |
+
每个场景的台词按顺序合成(edge-tts,不同角色不同音色),句间留气口;场景时长 = max(min_dur, 前导 + 台词 + 气口 + 尾巴)。"""
|
| 4 |
+
import asyncio, json, re, subprocess, sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import edge_tts
|
| 7 |
+
proj = Path(sys.argv[1]).resolve(); sb = json.loads((proj / 'storyboard.json').read_text(encoding='utf-8'))
|
| 8 |
+
V = sb['voices']; fps = sb['format']['fps']; LEAD, TAIL, GAP, GAP2 = 0.5, 0.9, 0.22, 0.38
|
| 9 |
+
ad = proj / 'audio'; ad.mkdir(exist_ok=True); (proj / 'out').mkdir(exist_ok=True)
|
| 10 |
+
def dur(p): return float(subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', str(p)], capture_output=True, text=True).stdout.strip())
|
| 11 |
+
def loud(p):
|
| 12 |
+
e = subprocess.run(['ffmpeg', '-nostats', '-i', str(p), '-filter_complex', 'ebur128', '-f', 'null', '-'], capture_output=True, text=True).stderr
|
| 13 |
+
m = re.findall(r'I:\s+(-?[\d.]+) LUFS', e); return float(m[-1]) if m else -20.0
|
| 14 |
+
TARGET = {'n': -17.0, 'boy': -16.0, 'farmer': -16.5} # 各角色台词统一响度(喊叫稍响)
|
| 15 |
+
def split(text, maxlen=16):
|
| 16 |
+
parts = [x.strip() for x in re.split(r'(?<=[,。!?;:])', text) if x.strip()]
|
| 17 |
+
out = []
|
| 18 |
+
for p in parts:
|
| 19 |
+
p = p.rstrip(',。;:')
|
| 20 |
+
while len(p) > maxlen: out.append(p[:maxlen]); p = p[maxlen:]
|
| 21 |
+
if p: out.append(p)
|
| 22 |
+
return out
|
| 23 |
+
async def say(text, v, path):
|
| 24 |
+
for k in range(3):
|
| 25 |
+
try: await edge_tts.Communicate(text, v['voice'], rate=v['rate'], pitch=v['pitch']).save(str(path)); return
|
| 26 |
+
except Exception as e:
|
| 27 |
+
if k == 2: raise
|
| 28 |
+
t = 0.0; scenes = []; clips = []; srt = []
|
| 29 |
+
for i, sc in enumerate(sb['scenes']):
|
| 30 |
+
start = t; cur = start + LEAD; lines = []; prev = None
|
| 31 |
+
for j, ln in enumerate(sc['lines']):
|
| 32 |
+
f = ad / f"v_{i+1:02}_{j+1}_{ln['who']}.mp3"
|
| 33 |
+
if not f.exists(): asyncio.run(say(ln['text'], V[ln['who']], f))
|
| 34 |
+
d = dur(f)
|
| 35 |
+
if prev is not None: cur += GAP2 if prev != ln['who'] else GAP
|
| 36 |
+
subs = split(ln['text']); tot = sum(len(s) for s in subs); x = cur; sub_items = []
|
| 37 |
+
for s in subs:
|
| 38 |
+
dd = d * len(s) / tot; sub_items.append({'t0': round(x, 3), 't1': round(x + dd, 3), 'text': s, 'who': ln['who']}); x += dd
|
| 39 |
+
lines.append({'who': ln['who'], 'text': ln['text'], 't0': round(cur, 3), 'dur': round(d, 3), 'subs': sub_items})
|
| 40 |
+
clips.append((f, cur, loud(f), ln['who'])); cur += d; prev = ln['who']
|
| 41 |
+
dd = max(sc.get('min_dur', 2), cur + TAIL - start); dd = round(dd * fps) / fps
|
| 42 |
+
subs_all = [s for l in lines for s in l['subs']]
|
| 43 |
+
scenes.append({'id': sc['id'], 'start': round(start, 4), 'dur': dd, 'voice_start': lines[0]['t0'], 'voice_dur': round(cur - lines[0]['t0'], 3), 'lines': lines, 'subs': subs_all})
|
| 44 |
+
print(f"{sc['id']:8} {start:7.2f}s +{dd:5.2f}s " + ' / '.join(f"{l['who']}:{l['text'][:10]}" for l in lines))
|
| 45 |
+
t = start + dd
|
| 46 |
+
total = round(t * fps) / fps
|
| 47 |
+
tl = {'title': sb['title'], 'style': sb['style'], 'w': sb['format']['w'], 'h': sb['format']['h'], 'fps': fps, 'total': total, 'frames': int(round(total * fps)), 'voice': {k: v['voice'] for k, v in V.items()}, 'scenes': scenes}
|
| 48 |
+
(proj / 'timeline.json').write_text(json.dumps(tl, ensure_ascii=False, indent=1), encoding='utf-8')
|
| 49 |
+
# 拼接人声轨:每句按时间放置
|
| 50 |
+
args = ['ffmpeg', '-loglevel', 'error', '-y']
|
| 51 |
+
for c in clips: args += ['-i', str(c[0])]
|
| 52 |
+
fl = ''.join(f"[{k}:a]aresample=48000,volume={TARGET[who] - L:.2f}dB,adelay={int(st*1000)}|{int(st*1000)}[a{k}];" for k, (_, st, L, who) in enumerate(clips))
|
| 53 |
+
fl += ''.join(f"[a{k}]" for k in range(len(clips))) + f"amix=inputs={len(clips)}:normalize=0:dropout_transition=0,apad,atrim=0:{total}[out]"
|
| 54 |
+
subprocess.run(args + ['-filter_complex', fl, '-map', '[out]', '-ac', '2', str(ad / 'voice.wav')], check=True)
|
| 55 |
+
def ts(x): ms = int(round(x * 1000)); return f"{ms//3600000:02}:{ms//60000%60:02}:{ms//1000%60:02},{ms%1000:03}"
|
| 56 |
+
n = 0
|
| 57 |
+
for s in scenes:
|
| 58 |
+
for c in s['subs']:
|
| 59 |
+
n += 1; srt.append(f"{n}\n{ts(c['t0'])} --> {ts(c['t1'])}\n{c['text']}\n")
|
| 60 |
+
(proj / 'out/subs.srt').write_text('\n'.join(srt), encoding='utf-8')
|
| 61 |
+
print(f'总时长 {total:.2f}s,{len(clips)} 句')
|
wolf-coming/video/audio/v_01_1_n.mp3
ADDED
|
Binary file (15.4 kB). View file
|
|
|
wolf-coming/video/audio/v_02_1_n.mp3
ADDED
|
Binary file (55 kB). View file
|
|
|
wolf-coming/video/audio/v_03_1_n.mp3
ADDED
|
Binary file (68.5 kB). View file
|
|
|
wolf-coming/video/audio/v_04_1_n.mp3
ADDED
|
Binary file (20.2 kB). View file
|
|
|
wolf-coming/video/audio/v_04_2_boy.mp3
ADDED
|
Binary file (24.9 kB). View file
|
|
|
wolf-coming/video/audio/v_05_1_n.mp3
ADDED
|
Binary file (40.6 kB). View file
|
|
|
wolf-coming/video/audio/v_05_2_farmer.mp3
ADDED
|
Binary file (19.6 kB). View file
|
|
|
wolf-coming/video/audio/v_06_1_boy.mp3
ADDED
|
Binary file (22.9 kB). View file
|
|
|
wolf-coming/video/audio/v_06_2_n.mp3
ADDED
|
Binary file (22.8 kB). View file
|
|
|
wolf-coming/video/audio/v_07_1_n.mp3
ADDED
|
Binary file (19.3 kB). View file
|
|
|
wolf-coming/video/audio/v_07_2_boy.mp3
ADDED
|
Binary file (17.4 kB). View file
|
|
|
wolf-coming/video/audio/v_07_3_n.mp3
ADDED
|
Binary file (15.7 kB). View file
|
|
|
wolf-coming/video/audio/v_08_1_boy.mp3
ADDED
|
Binary file (14.1 kB). View file
|
|
|
wolf-coming/video/audio/v_08_2_farmer.mp3
ADDED
|
Binary file (23.2 kB). View file
|
|
|
wolf-coming/video/audio/v_09_1_n.mp3
ADDED
|
Binary file (63.5 kB). View file
|
|
|
wolf-coming/video/audio/v_10_1_n.mp3
ADDED
|
Binary file (19.2 kB). View file
|
|
|
wolf-coming/video/audio/v_10_2_boy.mp3
ADDED
|
Binary file (26.5 kB). View file
|
|
|
wolf-coming/video/audio/v_11_1_n.mp3
ADDED
|
Binary file (25.8 kB). View file
|
|
|
wolf-coming/video/audio/v_11_2_farmer.mp3
ADDED
|
Binary file (16 kB). View file
|
|
|
wolf-coming/video/audio/v_11_3_n.mp3
ADDED
|
Binary file (12.2 kB). View file
|
|
|
wolf-coming/video/audio/v_12_1_n.mp3
ADDED
|
Binary file (35.9 kB). View file
|
|
|
wolf-coming/video/audio/v_13_1_n.mp3
ADDED
|
Binary file (32 kB). View file
|
|
|
wolf-coming/video/audio/v_14_1_n.mp3
ADDED
|
Binary file (43.3 kB). View file
|
|
|
wolf-coming/video/film.bundle.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
wolf-coming/video/film/fonts/chars.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
!#%+,-./0123456789:;?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz·—‘’“”…、。《》「」【】・一三上下不与丑世丛东丝两个中为主丽举久么乎乐也乡书乱了事二于云五些交享亮亲人仅今介从他代以们仰任伙会传伴位低住体何作你例侧便俏保信倒候值停傍像儿充入全公六共关其具内再写冠冬冰冲决况冷冻准几出击函分则创到制前剩副力加动勾匀包化区半单印即压原又叉友发取变叠口古句另只叫可台右叶号叽各合同名后向吓吗吧呆告命和咔咦咬哀哈响哔哥哩哪唧啄喂善喊喙喜喳嘲嘴器噪嚓四回团围固国图圆圈在地场均坐块垂垛域堆塘填境增墨壁声壳处备复夏外多夜大天太失头夹奇奏她好妈始姐姑娘嫌子字存季孤学孩孵它安完官定实宣宫家宽寂寄密寒寸对导封小尖尚就尺尼尾层屋屏展属山岸峰工左差己已布希帘带帧幅幕干平并序应底度廓延建开式引张弦弯弹归当录形彩影往径很徒得循微心快念忽态怕性怪总息悄悬情惊愈意感慢成截所扇手才扑打扫扬把抖折报拉拍拖拢拨择持按挖振换掉排掠接控推描提插揭撇播擎支收改放政故效敢数整文斑断新旁无日时明春是昼显晓晕晚景晰暂暖暗曲更替最有朋望朝期木未本术朱机权村条来极构果枝枪染柔查柳标树校样核根格桃案梅梦梳检楷模横檐欠次欢欺款歌正此步段每比毛气水池治沼沿法波泥泪泽洁流测浏浓浮消淡深混清渐渡温渲游湖湿溅源溪滴漂漫潮灰点烟烧然照熬片版牙物状猎率环现理瓣生用田由电画畅界痕登白的皮皴益盖盛目直相省看真眼着睡瞬瞻短码砂砰破确碍示神离秋种秒积程空窗窝立竖章童竹笆笑笔笛符等筝签简篇篱簧粒粘系素紧红纤约级纳纷纸纹线组终经绒结绘给络绪续维缓编缘缩网置美群羽翅翻翼而肉背能脊脑脖脚脸腿膀臂自致色艺节芦花芽苇苹范茸草荷落蓝藏蛋蜓蜻蝴蝶融行衣补表被裁裂装西要见观规视览觉角解触计让记讲证试话语说请读课调负败质贴资走起路跳身躲轨转轮软轴轻载输辨边过近还这进远连迟述追送适选逐递通遮那部都配采里重野量金钟钻铃铺错键镜长门间阅阴阵阶阻附限除随难雁雪雾需霞露静面音页顶顿预领颈频颗题颜颤风飘飞马驳验高髻鱼鸟鸡鸣鸭鹅鹜麦黄黑齐️︐!()+,:;?|🐓👀🥚🦢
|
wolf-coming/video/film/fonts/mashanzheng.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fe7ce130e02290a7dff93743e8a7492fb7c41bdbe9d6b338945144e018735476
|
| 3 |
+
size 420940
|
wolf-coming/video/film/fonts/wenkai.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:14374c15ca95593a0fc159be8f69f4435a3e7c4206b1817fd7bfba9495222f02
|
| 3 |
+
size 240976
|
wolf-coming/video/film/voice.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:50046e8a3fb27e30e64cb0ca975c630a567ff84a3e6a217076501607745250fe
|
| 3 |
+
size 1655162
|
wolf-coming/video/index.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><html><head><meta charset="utf-8"><title>狼来了 · render</title></head><body><script>if(!/[?&]film=/.test(location.search)){history.replaceState(null,'',location.pathname+(location.search?location.search+'&':'?')+'film=wolf')}</script><script src="film.bundle.js"></script></body></html>
|
wolf-coming/video/out/subs.srt
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
1
|
| 2 |
+
00:00:00,500 --> 00:00:01,967
|
| 3 |
+
伊索寓言
|
| 4 |
+
|
| 5 |
+
2
|
| 6 |
+
00:00:01,967 --> 00:00:03,068
|
| 7 |
+
狼来了
|
| 8 |
+
|
| 9 |
+
3
|
| 10 |
+
00:00:07,000 --> 00:00:09,096
|
| 11 |
+
山脚下有个小村子
|
| 12 |
+
|
| 13 |
+
4
|
| 14 |
+
00:00:09,096 --> 00:00:11,715
|
| 15 |
+
大人们每天在田里干活
|
| 16 |
+
|
| 17 |
+
5
|
| 18 |
+
00:00:11,715 --> 00:00:13,549
|
| 19 |
+
有个放羊的孩子
|
| 20 |
+
|
| 21 |
+
6
|
| 22 |
+
00:00:13,549 --> 00:00:16,168
|
| 23 |
+
天天赶着羊群上山吃草
|
| 24 |
+
|
| 25 |
+
7
|
| 26 |
+
00:00:17,567 --> 00:00:18,403
|
| 27 |
+
山坡上
|
| 28 |
+
|
| 29 |
+
8
|
| 30 |
+
00:00:18,403 --> 00:00:20,910
|
| 31 |
+
羊儿慢悠悠地吃着草
|
| 32 |
+
|
| 33 |
+
9
|
| 34 |
+
00:00:20,910 --> 00:00:23,139
|
| 35 |
+
放羊娃躺在大树下
|
| 36 |
+
|
| 37 |
+
10
|
| 38 |
+
00:00:23,139 --> 00:00:25,090
|
| 39 |
+
嘴里叼着一根草
|
| 40 |
+
|
| 41 |
+
11
|
| 42 |
+
00:00:25,090 --> 00:00:26,762
|
| 43 |
+
觉得无聊极了
|
| 44 |
+
|
| 45 |
+
12
|
| 46 |
+
00:00:26,762 --> 00:00:27,319
|
| 47 |
+
忽然
|
| 48 |
+
|
| 49 |
+
13
|
| 50 |
+
00:00:27,319 --> 00:00:28,991
|
| 51 |
+
他的眼珠一转
|
| 52 |
+
|
| 53 |
+
14
|
| 54 |
+
00:00:30,400 --> 00:00:32,233
|
| 55 |
+
他跳上大石头
|
| 56 |
+
|
| 57 |
+
15
|
| 58 |
+
00:00:32,233 --> 00:00:33,760
|
| 59 |
+
拢着嘴大喊
|
| 60 |
+
|
| 61 |
+
16
|
| 62 |
+
00:00:34,140 --> 00:00:35,524
|
| 63 |
+
狼来了!
|
| 64 |
+
|
| 65 |
+
17
|
| 66 |
+
00:00:35,524 --> 00:00:36,908
|
| 67 |
+
狼来了!
|
| 68 |
+
|
| 69 |
+
18
|
| 70 |
+
00:00:36,908 --> 00:00:38,292
|
| 71 |
+
快来呀!
|
| 72 |
+
|
| 73 |
+
19
|
| 74 |
+
00:00:39,700 --> 00:00:41,956
|
| 75 |
+
山下的农民听见了
|
| 76 |
+
|
| 77 |
+
20
|
| 78 |
+
00:00:41,956 --> 00:00:43,084
|
| 79 |
+
扛起锄头
|
| 80 |
+
|
| 81 |
+
21
|
| 82 |
+
00:00:43,084 --> 00:00:44,212
|
| 83 |
+
拿起扁担
|
| 84 |
+
|
| 85 |
+
22
|
| 86 |
+
00:00:44,212 --> 00:00:46,468
|
| 87 |
+
急急忙忙往山上跑
|
| 88 |
+
|
| 89 |
+
23
|
| 90 |
+
00:00:46,848 --> 00:00:48,480
|
| 91 |
+
狼在哪儿?
|
| 92 |
+
|
| 93 |
+
24
|
| 94 |
+
00:00:48,480 --> 00:00:50,112
|
| 95 |
+
狼在哪儿?
|
| 96 |
+
|
| 97 |
+
25
|
| 98 |
+
00:00:51,500 --> 00:00:52,215
|
| 99 |
+
哈哈哈
|
| 100 |
+
|
| 101 |
+
26
|
| 102 |
+
00:00:52,215 --> 00:00:53,408
|
| 103 |
+
哪有什么狼
|
| 104 |
+
|
| 105 |
+
27
|
| 106 |
+
00:00:53,408 --> 00:00:55,316
|
| 107 |
+
我是逗你们玩的!
|
| 108 |
+
|
| 109 |
+
28
|
| 110 |
+
00:00:55,696 --> 00:00:57,863
|
| 111 |
+
农民们气得直摇头
|
| 112 |
+
|
| 113 |
+
29
|
| 114 |
+
00:00:57,863 --> 00:00:59,488
|
| 115 |
+
只好下山去了
|
| 116 |
+
|
| 117 |
+
30
|
| 118 |
+
00:01:00,900 --> 00:01:02,069
|
| 119 |
+
过了几天
|
| 120 |
+
|
| 121 |
+
31
|
| 122 |
+
00:01:02,069 --> 00:01:04,116
|
| 123 |
+
放羊娃又喊起来
|
| 124 |
+
|
| 125 |
+
32
|
| 126 |
+
00:01:04,496 --> 00:01:05,948
|
| 127 |
+
狼来了!
|
| 128 |
+
|
| 129 |
+
33
|
| 130 |
+
00:01:05,948 --> 00:01:07,400
|
| 131 |
+
狼来了!
|
| 132 |
+
|
| 133 |
+
34
|
| 134 |
+
00:01:07,780 --> 00:01:10,396
|
| 135 |
+
农民们又一次跑上了山
|
| 136 |
+
|
| 137 |
+
35
|
| 138 |
+
00:01:11,800 --> 00:01:12,472
|
| 139 |
+
哈哈
|
| 140 |
+
|
| 141 |
+
36
|
| 142 |
+
00:01:12,472 --> 00:01:14,152
|
| 143 |
+
又上当啦!
|
| 144 |
+
|
| 145 |
+
37
|
| 146 |
+
00:01:14,532 --> 00:01:15,636
|
| 147 |
+
你再说谎
|
| 148 |
+
|
| 149 |
+
38
|
| 150 |
+
00:01:15,636 --> 00:01:18,396
|
| 151 |
+
下次我们就不信你了!
|
| 152 |
+
|
| 153 |
+
39
|
| 154 |
+
00:01:19,800 --> 00:01:21,564
|
| 155 |
+
又过了些日子
|
| 156 |
+
|
| 157 |
+
40
|
| 158 |
+
00:01:21,564 --> 00:01:23,328
|
| 159 |
+
乌云压着山头
|
| 160 |
+
|
| 161 |
+
41
|
| 162 |
+
00:01:23,328 --> 00:01:24,210
|
| 163 |
+
树丛里
|
| 164 |
+
|
| 165 |
+
42
|
| 166 |
+
00:01:24,210 --> 00:01:26,562
|
| 167 |
+
一双绿莹莹的眼睛
|
| 168 |
+
|
| 169 |
+
43
|
| 170 |
+
00:01:26,562 --> 00:01:28,620
|
| 171 |
+
慢慢靠近了羊群
|
| 172 |
+
|
| 173 |
+
44
|
| 174 |
+
00:01:28,620 --> 00:01:28,914
|
| 175 |
+
狼
|
| 176 |
+
|
| 177 |
+
45
|
| 178 |
+
00:01:28,914 --> 00:01:30,384
|
| 179 |
+
真的来了!
|
| 180 |
+
|
| 181 |
+
46
|
| 182 |
+
00:01:31,800 --> 00:01:33,715
|
| 183 |
+
放羊娃吓坏了
|
| 184 |
+
|
| 185 |
+
47
|
| 186 |
+
00:01:33,715 --> 00:01:34,992
|
| 187 |
+
拼命地喊
|
| 188 |
+
|
| 189 |
+
48
|
| 190 |
+
00:01:35,372 --> 00:01:36,634
|
| 191 |
+
狼来了!
|
| 192 |
+
|
| 193 |
+
49
|
| 194 |
+
00:01:36,634 --> 00:01:38,526
|
| 195 |
+
狼真的来了!
|
| 196 |
+
|
| 197 |
+
50
|
| 198 |
+
00:01:38,526 --> 00:01:39,788
|
| 199 |
+
救命啊!
|
| 200 |
+
|
| 201 |
+
51
|
| 202 |
+
00:01:41,200 --> 00:01:43,348
|
| 203 |
+
山下的农民听见了
|
| 204 |
+
|
| 205 |
+
52
|
| 206 |
+
00:01:43,348 --> 00:01:45,496
|
| 207 |
+
都以为他又在说谎
|
| 208 |
+
|
| 209 |
+
53
|
| 210 |
+
00:01:45,876 --> 00:01:46,409
|
| 211 |
+
哼
|
| 212 |
+
|
| 213 |
+
54
|
| 214 |
+
00:01:46,409 --> 00:01:48,540
|
| 215 |
+
又来骗人
|
| 216 |
+
|
| 217 |
+
55
|
| 218 |
+
00:01:48,920 --> 00:01:50,960
|
| 219 |
+
谁也没有上山
|
| 220 |
+
|
| 221 |
+
56
|
| 222 |
+
00:01:52,367 --> 00:01:53,725
|
| 223 |
+
狼冲进羊群
|
| 224 |
+
|
| 225 |
+
57
|
| 226 |
+
00:01:53,725 --> 00:01:55,898
|
| 227 |
+
羊儿吓得四处乱跑
|
| 228 |
+
|
| 229 |
+
58
|
| 230 |
+
00:01:55,898 --> 00:01:58,343
|
| 231 |
+
好几只羊被狼叼走了
|
| 232 |
+
|
| 233 |
+
59
|
| 234 |
+
00:02:00,367 --> 00:02:01,699
|
| 235 |
+
太阳落山了
|
| 236 |
+
|
| 237 |
+
60
|
| 238 |
+
00:02:01,699 --> 00:02:04,363
|
| 239 |
+
放羊娃抱着剩下的小羊
|
| 240 |
+
|
| 241 |
+
61
|
| 242 |
+
00:02:04,363 --> 00:02:05,695
|
| 243 |
+
伤心地哭了
|
| 244 |
+
|
| 245 |
+
62
|
| 246 |
+
00:02:08,367 --> 00:02:09,437
|
| 247 |
+
说谎的人
|
| 248 |
+
|
| 249 |
+
63
|
| 250 |
+
00:02:09,437 --> 00:02:11,042
|
| 251 |
+
就算说了真话
|
| 252 |
+
|
| 253 |
+
64
|
| 254 |
+
00:02:11,042 --> 00:02:13,183
|
| 255 |
+
也没有人再相信他
|
| 256 |
+
|
| 257 |
+
65
|
| 258 |
+
00:02:13,183 --> 00:02:15,591
|
| 259 |
+
做一个诚实的孩子吧
|
wolf-coming/video/storyboard.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"title": "狼来了",
|
| 3 |
+
"format": { "w": 1920, "h": 1080, "fps": 30 },
|
| 4 |
+
"style": "ink-wash textbook illustration",
|
| 5 |
+
"voices": {
|
| 6 |
+
"n": { "voice": "zh-CN-XiaoxiaoNeural", "rate": "-6%", "pitch": "+0Hz", "name": "旁白" },
|
| 7 |
+
"boy": { "voice": "zh-CN-YunxiaNeural", "rate": "+4%", "pitch": "+6Hz", "name": "放羊娃" },
|
| 8 |
+
"farmer": { "voice": "zh-CN-YunjianNeural", "rate": "-2%", "pitch": "-2Hz", "name": "农民" }
|
| 9 |
+
},
|
| 10 |
+
"sources": [
|
| 11 |
+
"Aesop's Fables, \"The Boy Who Cried Wolf\" (Perry Index 210) — public domain",
|
| 12 |
+
"https://en.wikipedia.org/wiki/The_Boy_Who_Cried_Wolf"
|
| 13 |
+
],
|
| 14 |
+
"scenes": [
|
| 15 |
+
{ "id": "title", "min_dur": 6.5, "lines": [{ "who": "n", "text": "伊索寓言,狼来了。" }] },
|
| 16 |
+
{ "id": "village", "min_dur": 8, "lines": [{ "who": "n", "text": "山脚下有个小村子。大人们每天在田里干活,有个放羊的孩子,天天赶着羊群上山吃草。" }] },
|
| 17 |
+
{ "id": "bored", "min_dur": 8, "lines": [{ "who": "n", "text": "山坡上,羊儿慢悠悠地吃着草。放羊娃躺在大树下,嘴里叼着一根草,觉得无聊极了。忽然,他的眼珠一转。" }] },
|
| 18 |
+
{ "id": "shout1", "min_dur": 6, "lines": [{ "who": "n", "text": "他跳上大石头,拢着嘴大喊:" }, { "who": "boy", "text": "狼来了!狼来了!快来呀!" }] },
|
| 19 |
+
{ "id": "run1", "min_dur": 7, "lines": [{ "who": "n", "text": "山下的农民听见了,扛起锄头,拿起扁担,急急忙忙往山上跑。" }, { "who": "farmer", "text": "狼在哪儿?狼在哪儿?" }] },
|
| 20 |
+
{ "id": "laugh1", "min_dur": 7, "lines": [{ "who": "boy", "text": "哈哈哈,哪有什么狼,我是逗你们玩的!" }, { "who": "n", "text": "农民们气得直摇头,只好下山去了。" }] },
|
| 21 |
+
{ "id": "shout2", "min_dur": 7, "lines": [{ "who": "n", "text": "过了几天,放羊娃又喊起来:" }, { "who": "boy", "text": "狼来了!狼来了!" }, { "who": "n", "text": "农民们又一次跑上了山。" }] },
|
| 22 |
+
{ "id": "warn", "min_dur": 7, "lines": [{ "who": "boy", "text": "哈哈,又上当啦!" }, { "who": "farmer", "text": "你再说谎,下次我们就不信你了!" }] },
|
| 23 |
+
{ "id": "wolf", "min_dur": 9, "lines": [{ "who": "n", "text": "又过了些日子,乌云压着山头。树丛里,一双绿莹莹的眼睛,慢慢靠近了羊群。狼,真的来了!" }] },
|
| 24 |
+
{ "id": "cry", "min_dur": 7, "lines": [{ "who": "n", "text": "放羊娃吓坏了,拼命地喊:" }, { "who": "boy", "text": "狼来了!狼真的来了!救命啊!" }] },
|
| 25 |
+
{ "id": "noone", "min_dur": 7, "lines": [{ "who": "n", "text": "山下的农民听见了,都以为他又在说谎。" }, { "who": "farmer", "text": "哼,又来骗人。" }, { "who": "n", "text": "谁也没有上山。" }] },
|
| 26 |
+
{ "id": "chaos", "min_dur": 8, "lines": [{ "who": "n", "text": "狼冲进羊群,羊儿吓得四处乱跑,好几只羊被狼叼走了。" }] },
|
| 27 |
+
{ "id": "regret", "min_dur": 8, "lines": [{ "who": "n", "text": "太阳落山了,放羊娃抱着剩下的小羊,伤心地哭了。" }] },
|
| 28 |
+
{ "id": "moral", "min_dur": 10, "lines": [{ "who": "n", "text": "说谎的人,就算说了真话,也没有人再相信他。做一个诚实的孩子吧。" }] }
|
| 29 |
+
]
|
| 30 |
+
}
|
wolf-coming/video/timeline.json
ADDED
|
@@ -0,0 +1,1134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"title": "狼来了",
|
| 3 |
+
"style": "ink-wash textbook illustration",
|
| 4 |
+
"w": 1920,
|
| 5 |
+
"h": 1080,
|
| 6 |
+
"fps": 30,
|
| 7 |
+
"total": 137.86666666666667,
|
| 8 |
+
"frames": 4136,
|
| 9 |
+
"voice": {
|
| 10 |
+
"n": "zh-CN-XiaoxiaoNeural",
|
| 11 |
+
"boy": "zh-CN-YunxiaNeural",
|
| 12 |
+
"farmer": "zh-CN-YunjianNeural"
|
| 13 |
+
},
|
| 14 |
+
"scenes": [
|
| 15 |
+
{
|
| 16 |
+
"id": "title",
|
| 17 |
+
"start": 0.0,
|
| 18 |
+
"dur": 6.5,
|
| 19 |
+
"voice_start": 0.5,
|
| 20 |
+
"voice_dur": 2.568,
|
| 21 |
+
"lines": [
|
| 22 |
+
{
|
| 23 |
+
"who": "n",
|
| 24 |
+
"text": "伊索寓言,狼来了。",
|
| 25 |
+
"t0": 0.5,
|
| 26 |
+
"dur": 2.568,
|
| 27 |
+
"subs": [
|
| 28 |
+
{
|
| 29 |
+
"t0": 0.5,
|
| 30 |
+
"t1": 1.967,
|
| 31 |
+
"text": "伊索寓言",
|
| 32 |
+
"who": "n"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"t0": 1.967,
|
| 36 |
+
"t1": 3.068,
|
| 37 |
+
"text": "狼来了",
|
| 38 |
+
"who": "n"
|
| 39 |
+
}
|
| 40 |
+
]
|
| 41 |
+
}
|
| 42 |
+
],
|
| 43 |
+
"subs": [
|
| 44 |
+
{
|
| 45 |
+
"t0": 0.5,
|
| 46 |
+
"t1": 1.967,
|
| 47 |
+
"text": "伊索寓言",
|
| 48 |
+
"who": "n"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"t0": 1.967,
|
| 52 |
+
"t1": 3.068,
|
| 53 |
+
"text": "狼来了",
|
| 54 |
+
"who": "n"
|
| 55 |
+
}
|
| 56 |
+
]
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": "village",
|
| 60 |
+
"start": 6.5,
|
| 61 |
+
"dur": 10.566666666666666,
|
| 62 |
+
"voice_start": 7.0,
|
| 63 |
+
"voice_dur": 9.168,
|
| 64 |
+
"lines": [
|
| 65 |
+
{
|
| 66 |
+
"who": "n",
|
| 67 |
+
"text": "山脚下有个小村子。大人们每天在田里干活,有个放羊的孩子,天天赶着羊群上山吃草。",
|
| 68 |
+
"t0": 7.0,
|
| 69 |
+
"dur": 9.168,
|
| 70 |
+
"subs": [
|
| 71 |
+
{
|
| 72 |
+
"t0": 7.0,
|
| 73 |
+
"t1": 9.096,
|
| 74 |
+
"text": "山脚下有个小村子",
|
| 75 |
+
"who": "n"
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"t0": 9.096,
|
| 79 |
+
"t1": 11.715,
|
| 80 |
+
"text": "大人们每天在田里干活",
|
| 81 |
+
"who": "n"
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"t0": 11.715,
|
| 85 |
+
"t1": 13.549,
|
| 86 |
+
"text": "有个放羊的孩子",
|
| 87 |
+
"who": "n"
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"t0": 13.549,
|
| 91 |
+
"t1": 16.168,
|
| 92 |
+
"text": "天天赶着羊群上山吃草",
|
| 93 |
+
"who": "n"
|
| 94 |
+
}
|
| 95 |
+
]
|
| 96 |
+
}
|
| 97 |
+
],
|
| 98 |
+
"subs": [
|
| 99 |
+
{
|
| 100 |
+
"t0": 7.0,
|
| 101 |
+
"t1": 9.096,
|
| 102 |
+
"text": "山脚下有个小村子",
|
| 103 |
+
"who": "n"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"t0": 9.096,
|
| 107 |
+
"t1": 11.715,
|
| 108 |
+
"text": "大人们每天在田里干活",
|
| 109 |
+
"who": "n"
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"t0": 11.715,
|
| 113 |
+
"t1": 13.549,
|
| 114 |
+
"text": "有个放羊的孩子",
|
| 115 |
+
"who": "n"
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"t0": 13.549,
|
| 119 |
+
"t1": 16.168,
|
| 120 |
+
"text": "天天赶着羊群上山吃草",
|
| 121 |
+
"who": "n"
|
| 122 |
+
}
|
| 123 |
+
]
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"id": "bored",
|
| 127 |
+
"start": 17.0667,
|
| 128 |
+
"dur": 12.833333333333334,
|
| 129 |
+
"voice_start": 17.567,
|
| 130 |
+
"voice_dur": 11.424,
|
| 131 |
+
"lines": [
|
| 132 |
+
{
|
| 133 |
+
"who": "n",
|
| 134 |
+
"text": "山坡上,羊儿慢悠悠地吃着草。放羊娃躺在大树下,嘴里叼着一根草,觉得无聊极了。忽然,他的眼珠一转。",
|
| 135 |
+
"t0": 17.567,
|
| 136 |
+
"dur": 11.424,
|
| 137 |
+
"subs": [
|
| 138 |
+
{
|
| 139 |
+
"t0": 17.567,
|
| 140 |
+
"t1": 18.403,
|
| 141 |
+
"text": "山坡上",
|
| 142 |
+
"who": "n"
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"t0": 18.403,
|
| 146 |
+
"t1": 20.91,
|
| 147 |
+
"text": "羊儿慢悠悠地吃着草",
|
| 148 |
+
"who": "n"
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"t0": 20.91,
|
| 152 |
+
"t1": 23.139,
|
| 153 |
+
"text": "放羊娃躺在大树下",
|
| 154 |
+
"who": "n"
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"t0": 23.139,
|
| 158 |
+
"t1": 25.09,
|
| 159 |
+
"text": "嘴里叼着一根草",
|
| 160 |
+
"who": "n"
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"t0": 25.09,
|
| 164 |
+
"t1": 26.762,
|
| 165 |
+
"text": "觉得无聊极了",
|
| 166 |
+
"who": "n"
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"t0": 26.762,
|
| 170 |
+
"t1": 27.319,
|
| 171 |
+
"text": "忽然",
|
| 172 |
+
"who": "n"
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"t0": 27.319,
|
| 176 |
+
"t1": 28.991,
|
| 177 |
+
"text": "他的眼珠一转",
|
| 178 |
+
"who": "n"
|
| 179 |
+
}
|
| 180 |
+
]
|
| 181 |
+
}
|
| 182 |
+
],
|
| 183 |
+
"subs": [
|
| 184 |
+
{
|
| 185 |
+
"t0": 17.567,
|
| 186 |
+
"t1": 18.403,
|
| 187 |
+
"text": "山坡上",
|
| 188 |
+
"who": "n"
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"t0": 18.403,
|
| 192 |
+
"t1": 20.91,
|
| 193 |
+
"text": "羊儿慢悠悠地吃着草",
|
| 194 |
+
"who": "n"
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"t0": 20.91,
|
| 198 |
+
"t1": 23.139,
|
| 199 |
+
"text": "放羊娃躺在大树下",
|
| 200 |
+
"who": "n"
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"t0": 23.139,
|
| 204 |
+
"t1": 25.09,
|
| 205 |
+
"text": "嘴里叼着一根草",
|
| 206 |
+
"who": "n"
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"t0": 25.09,
|
| 210 |
+
"t1": 26.762,
|
| 211 |
+
"text": "觉得无聊极了",
|
| 212 |
+
"who": "n"
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"t0": 26.762,
|
| 216 |
+
"t1": 27.319,
|
| 217 |
+
"text": "忽然",
|
| 218 |
+
"who": "n"
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"t0": 27.319,
|
| 222 |
+
"t1": 28.991,
|
| 223 |
+
"text": "他的眼珠一转",
|
| 224 |
+
"who": "n"
|
| 225 |
+
}
|
| 226 |
+
]
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"id": "shout1",
|
| 230 |
+
"start": 29.9,
|
| 231 |
+
"dur": 9.3,
|
| 232 |
+
"voice_start": 30.4,
|
| 233 |
+
"voice_dur": 7.892,
|
| 234 |
+
"lines": [
|
| 235 |
+
{
|
| 236 |
+
"who": "n",
|
| 237 |
+
"text": "他跳上大石头,拢着嘴大喊:",
|
| 238 |
+
"t0": 30.4,
|
| 239 |
+
"dur": 3.36,
|
| 240 |
+
"subs": [
|
| 241 |
+
{
|
| 242 |
+
"t0": 30.4,
|
| 243 |
+
"t1": 32.233,
|
| 244 |
+
"text": "他跳上大石头",
|
| 245 |
+
"who": "n"
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"t0": 32.233,
|
| 249 |
+
"t1": 33.76,
|
| 250 |
+
"text": "拢着嘴大喊",
|
| 251 |
+
"who": "n"
|
| 252 |
+
}
|
| 253 |
+
]
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"who": "boy",
|
| 257 |
+
"text": "狼来了!狼来了!快来呀!",
|
| 258 |
+
"t0": 34.14,
|
| 259 |
+
"dur": 4.152,
|
| 260 |
+
"subs": [
|
| 261 |
+
{
|
| 262 |
+
"t0": 34.14,
|
| 263 |
+
"t1": 35.524,
|
| 264 |
+
"text": "狼来了!",
|
| 265 |
+
"who": "boy"
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"t0": 35.524,
|
| 269 |
+
"t1": 36.908,
|
| 270 |
+
"text": "狼来了!",
|
| 271 |
+
"who": "boy"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"t0": 36.908,
|
| 275 |
+
"t1": 38.292,
|
| 276 |
+
"text": "快来呀!",
|
| 277 |
+
"who": "boy"
|
| 278 |
+
}
|
| 279 |
+
]
|
| 280 |
+
}
|
| 281 |
+
],
|
| 282 |
+
"subs": [
|
| 283 |
+
{
|
| 284 |
+
"t0": 30.4,
|
| 285 |
+
"t1": 32.233,
|
| 286 |
+
"text": "他跳上大石头",
|
| 287 |
+
"who": "n"
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"t0": 32.233,
|
| 291 |
+
"t1": 33.76,
|
| 292 |
+
"text": "拢着嘴大喊",
|
| 293 |
+
"who": "n"
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"t0": 34.14,
|
| 297 |
+
"t1": 35.524,
|
| 298 |
+
"text": "狼来了!",
|
| 299 |
+
"who": "boy"
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"t0": 35.524,
|
| 303 |
+
"t1": 36.908,
|
| 304 |
+
"text": "狼来了!",
|
| 305 |
+
"who": "boy"
|
| 306 |
+
},
|
| 307 |
+
{
|
| 308 |
+
"t0": 36.908,
|
| 309 |
+
"t1": 38.292,
|
| 310 |
+
"text": "快来呀!",
|
| 311 |
+
"who": "boy"
|
| 312 |
+
}
|
| 313 |
+
]
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"id": "run1",
|
| 317 |
+
"start": 39.2,
|
| 318 |
+
"dur": 11.8,
|
| 319 |
+
"voice_start": 39.7,
|
| 320 |
+
"voice_dur": 10.412,
|
| 321 |
+
"lines": [
|
| 322 |
+
{
|
| 323 |
+
"who": "n",
|
| 324 |
+
"text": "山下的农民听见了,扛起锄头,拿起扁担,急急忙忙往山上跑。",
|
| 325 |
+
"t0": 39.7,
|
| 326 |
+
"dur": 6.768,
|
| 327 |
+
"subs": [
|
| 328 |
+
{
|
| 329 |
+
"t0": 39.7,
|
| 330 |
+
"t1": 41.956,
|
| 331 |
+
"text": "山下的农民听见了",
|
| 332 |
+
"who": "n"
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"t0": 41.956,
|
| 336 |
+
"t1": 43.084,
|
| 337 |
+
"text": "扛起锄头",
|
| 338 |
+
"who": "n"
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
"t0": 43.084,
|
| 342 |
+
"t1": 44.212,
|
| 343 |
+
"text": "拿起扁担",
|
| 344 |
+
"who": "n"
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"t0": 44.212,
|
| 348 |
+
"t1": 46.468,
|
| 349 |
+
"text": "急急忙忙往山上跑",
|
| 350 |
+
"who": "n"
|
| 351 |
+
}
|
| 352 |
+
]
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
"who": "farmer",
|
| 356 |
+
"text": "狼在哪儿?狼在哪儿?",
|
| 357 |
+
"t0": 46.848,
|
| 358 |
+
"dur": 3.264,
|
| 359 |
+
"subs": [
|
| 360 |
+
{
|
| 361 |
+
"t0": 46.848,
|
| 362 |
+
"t1": 48.48,
|
| 363 |
+
"text": "狼在哪儿?",
|
| 364 |
+
"who": "farmer"
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
"t0": 48.48,
|
| 368 |
+
"t1": 50.112,
|
| 369 |
+
"text": "狼在哪儿?",
|
| 370 |
+
"who": "farmer"
|
| 371 |
+
}
|
| 372 |
+
]
|
| 373 |
+
}
|
| 374 |
+
],
|
| 375 |
+
"subs": [
|
| 376 |
+
{
|
| 377 |
+
"t0": 39.7,
|
| 378 |
+
"t1": 41.956,
|
| 379 |
+
"text": "山下的农民听见了",
|
| 380 |
+
"who": "n"
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"t0": 41.956,
|
| 384 |
+
"t1": 43.084,
|
| 385 |
+
"text": "扛起锄头",
|
| 386 |
+
"who": "n"
|
| 387 |
+
},
|
| 388 |
+
{
|
| 389 |
+
"t0": 43.084,
|
| 390 |
+
"t1": 44.212,
|
| 391 |
+
"text": "拿起扁担",
|
| 392 |
+
"who": "n"
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
"t0": 44.212,
|
| 396 |
+
"t1": 46.468,
|
| 397 |
+
"text": "急急忙忙往山上跑",
|
| 398 |
+
"who": "n"
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
"t0": 46.848,
|
| 402 |
+
"t1": 48.48,
|
| 403 |
+
"text": "狼在哪儿?",
|
| 404 |
+
"who": "farmer"
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
"t0": 48.48,
|
| 408 |
+
"t1": 50.112,
|
| 409 |
+
"text": "狼在哪儿?",
|
| 410 |
+
"who": "farmer"
|
| 411 |
+
}
|
| 412 |
+
]
|
| 413 |
+
},
|
| 414 |
+
{
|
| 415 |
+
"id": "laugh1",
|
| 416 |
+
"start": 51.0,
|
| 417 |
+
"dur": 9.4,
|
| 418 |
+
"voice_start": 51.5,
|
| 419 |
+
"voice_dur": 7.988,
|
| 420 |
+
"lines": [
|
| 421 |
+
{
|
| 422 |
+
"who": "boy",
|
| 423 |
+
"text": "哈哈哈,哪有什么狼,我是逗你们玩的!",
|
| 424 |
+
"t0": 51.5,
|
| 425 |
+
"dur": 3.816,
|
| 426 |
+
"subs": [
|
| 427 |
+
{
|
| 428 |
+
"t0": 51.5,
|
| 429 |
+
"t1": 52.215,
|
| 430 |
+
"text": "哈哈哈",
|
| 431 |
+
"who": "boy"
|
| 432 |
+
},
|
| 433 |
+
{
|
| 434 |
+
"t0": 52.215,
|
| 435 |
+
"t1": 53.408,
|
| 436 |
+
"text": "哪有什么狼",
|
| 437 |
+
"who": "boy"
|
| 438 |
+
},
|
| 439 |
+
{
|
| 440 |
+
"t0": 53.408,
|
| 441 |
+
"t1": 55.316,
|
| 442 |
+
"text": "我是逗你们玩的!",
|
| 443 |
+
"who": "boy"
|
| 444 |
+
}
|
| 445 |
+
]
|
| 446 |
+
},
|
| 447 |
+
{
|
| 448 |
+
"who": "n",
|
| 449 |
+
"text": "农民们气得直摇头,只好下山去了。",
|
| 450 |
+
"t0": 55.696,
|
| 451 |
+
"dur": 3.792,
|
| 452 |
+
"subs": [
|
| 453 |
+
{
|
| 454 |
+
"t0": 55.696,
|
| 455 |
+
"t1": 57.863,
|
| 456 |
+
"text": "农民们气得直摇头",
|
| 457 |
+
"who": "n"
|
| 458 |
+
},
|
| 459 |
+
{
|
| 460 |
+
"t0": 57.863,
|
| 461 |
+
"t1": 59.488,
|
| 462 |
+
"text": "只好下山去了",
|
| 463 |
+
"who": "n"
|
| 464 |
+
}
|
| 465 |
+
]
|
| 466 |
+
}
|
| 467 |
+
],
|
| 468 |
+
"subs": [
|
| 469 |
+
{
|
| 470 |
+
"t0": 51.5,
|
| 471 |
+
"t1": 52.215,
|
| 472 |
+
"text": "哈哈哈",
|
| 473 |
+
"who": "boy"
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"t0": 52.215,
|
| 477 |
+
"t1": 53.408,
|
| 478 |
+
"text": "哪有什么狼",
|
| 479 |
+
"who": "boy"
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
"t0": 53.408,
|
| 483 |
+
"t1": 55.316,
|
| 484 |
+
"text": "我是逗你们玩的!",
|
| 485 |
+
"who": "boy"
|
| 486 |
+
},
|
| 487 |
+
{
|
| 488 |
+
"t0": 55.696,
|
| 489 |
+
"t1": 57.863,
|
| 490 |
+
"text": "农民们气得直摇头",
|
| 491 |
+
"who": "n"
|
| 492 |
+
},
|
| 493 |
+
{
|
| 494 |
+
"t0": 57.863,
|
| 495 |
+
"t1": 59.488,
|
| 496 |
+
"text": "只好下山去了",
|
| 497 |
+
"who": "n"
|
| 498 |
+
}
|
| 499 |
+
]
|
| 500 |
+
},
|
| 501 |
+
{
|
| 502 |
+
"id": "shout2",
|
| 503 |
+
"start": 60.4,
|
| 504 |
+
"dur": 10.9,
|
| 505 |
+
"voice_start": 60.9,
|
| 506 |
+
"voice_dur": 9.496,
|
| 507 |
+
"lines": [
|
| 508 |
+
{
|
| 509 |
+
"who": "n",
|
| 510 |
+
"text": "过了几天,放羊娃又喊起来:",
|
| 511 |
+
"t0": 60.9,
|
| 512 |
+
"dur": 3.216,
|
| 513 |
+
"subs": [
|
| 514 |
+
{
|
| 515 |
+
"t0": 60.9,
|
| 516 |
+
"t1": 62.069,
|
| 517 |
+
"text": "过了几天",
|
| 518 |
+
"who": "n"
|
| 519 |
+
},
|
| 520 |
+
{
|
| 521 |
+
"t0": 62.069,
|
| 522 |
+
"t1": 64.116,
|
| 523 |
+
"text": "放羊娃又喊起来",
|
| 524 |
+
"who": "n"
|
| 525 |
+
}
|
| 526 |
+
]
|
| 527 |
+
},
|
| 528 |
+
{
|
| 529 |
+
"who": "boy",
|
| 530 |
+
"text": "狼来了!狼来了!",
|
| 531 |
+
"t0": 64.496,
|
| 532 |
+
"dur": 2.904,
|
| 533 |
+
"subs": [
|
| 534 |
+
{
|
| 535 |
+
"t0": 64.496,
|
| 536 |
+
"t1": 65.948,
|
| 537 |
+
"text": "狼来了!",
|
| 538 |
+
"who": "boy"
|
| 539 |
+
},
|
| 540 |
+
{
|
| 541 |
+
"t0": 65.948,
|
| 542 |
+
"t1": 67.4,
|
| 543 |
+
"text": "狼来了!",
|
| 544 |
+
"who": "boy"
|
| 545 |
+
}
|
| 546 |
+
]
|
| 547 |
+
},
|
| 548 |
+
{
|
| 549 |
+
"who": "n",
|
| 550 |
+
"text": "农民们又一次跑上了山。",
|
| 551 |
+
"t0": 67.78,
|
| 552 |
+
"dur": 2.616,
|
| 553 |
+
"subs": [
|
| 554 |
+
{
|
| 555 |
+
"t0": 67.78,
|
| 556 |
+
"t1": 70.396,
|
| 557 |
+
"text": "农民们又一次跑上了山",
|
| 558 |
+
"who": "n"
|
| 559 |
+
}
|
| 560 |
+
]
|
| 561 |
+
}
|
| 562 |
+
],
|
| 563 |
+
"subs": [
|
| 564 |
+
{
|
| 565 |
+
"t0": 60.9,
|
| 566 |
+
"t1": 62.069,
|
| 567 |
+
"text": "过了几天",
|
| 568 |
+
"who": "n"
|
| 569 |
+
},
|
| 570 |
+
{
|
| 571 |
+
"t0": 62.069,
|
| 572 |
+
"t1": 64.116,
|
| 573 |
+
"text": "放羊娃又喊起来",
|
| 574 |
+
"who": "n"
|
| 575 |
+
},
|
| 576 |
+
{
|
| 577 |
+
"t0": 64.496,
|
| 578 |
+
"t1": 65.948,
|
| 579 |
+
"text": "狼来了!",
|
| 580 |
+
"who": "boy"
|
| 581 |
+
},
|
| 582 |
+
{
|
| 583 |
+
"t0": 65.948,
|
| 584 |
+
"t1": 67.4,
|
| 585 |
+
"text": "狼来了!",
|
| 586 |
+
"who": "boy"
|
| 587 |
+
},
|
| 588 |
+
{
|
| 589 |
+
"t0": 67.78,
|
| 590 |
+
"t1": 70.396,
|
| 591 |
+
"text": "农民们又一次跑上了山",
|
| 592 |
+
"who": "n"
|
| 593 |
+
}
|
| 594 |
+
]
|
| 595 |
+
},
|
| 596 |
+
{
|
| 597 |
+
"id": "warn",
|
| 598 |
+
"start": 71.3,
|
| 599 |
+
"dur": 8.0,
|
| 600 |
+
"voice_start": 71.8,
|
| 601 |
+
"voice_dur": 6.596,
|
| 602 |
+
"lines": [
|
| 603 |
+
{
|
| 604 |
+
"who": "boy",
|
| 605 |
+
"text": "哈哈,又上当啦!",
|
| 606 |
+
"t0": 71.8,
|
| 607 |
+
"dur": 2.352,
|
| 608 |
+
"subs": [
|
| 609 |
+
{
|
| 610 |
+
"t0": 71.8,
|
| 611 |
+
"t1": 72.472,
|
| 612 |
+
"text": "哈哈",
|
| 613 |
+
"who": "boy"
|
| 614 |
+
},
|
| 615 |
+
{
|
| 616 |
+
"t0": 72.472,
|
| 617 |
+
"t1": 74.152,
|
| 618 |
+
"text": "又上当啦!",
|
| 619 |
+
"who": "boy"
|
| 620 |
+
}
|
| 621 |
+
]
|
| 622 |
+
},
|
| 623 |
+
{
|
| 624 |
+
"who": "farmer",
|
| 625 |
+
"text": "你再说谎,下次我们就不信你了!",
|
| 626 |
+
"t0": 74.532,
|
| 627 |
+
"dur": 3.864,
|
| 628 |
+
"subs": [
|
| 629 |
+
{
|
| 630 |
+
"t0": 74.532,
|
| 631 |
+
"t1": 75.636,
|
| 632 |
+
"text": "你再说谎",
|
| 633 |
+
"who": "farmer"
|
| 634 |
+
},
|
| 635 |
+
{
|
| 636 |
+
"t0": 75.636,
|
| 637 |
+
"t1": 78.396,
|
| 638 |
+
"text": "下次我们就不信你了!",
|
| 639 |
+
"who": "farmer"
|
| 640 |
+
}
|
| 641 |
+
]
|
| 642 |
+
}
|
| 643 |
+
],
|
| 644 |
+
"subs": [
|
| 645 |
+
{
|
| 646 |
+
"t0": 71.8,
|
| 647 |
+
"t1": 72.472,
|
| 648 |
+
"text": "哈哈",
|
| 649 |
+
"who": "boy"
|
| 650 |
+
},
|
| 651 |
+
{
|
| 652 |
+
"t0": 72.472,
|
| 653 |
+
"t1": 74.152,
|
| 654 |
+
"text": "又上当啦!",
|
| 655 |
+
"who": "boy"
|
| 656 |
+
},
|
| 657 |
+
{
|
| 658 |
+
"t0": 74.532,
|
| 659 |
+
"t1": 75.636,
|
| 660 |
+
"text": "你再说谎",
|
| 661 |
+
"who": "farmer"
|
| 662 |
+
},
|
| 663 |
+
{
|
| 664 |
+
"t0": 75.636,
|
| 665 |
+
"t1": 78.396,
|
| 666 |
+
"text": "下次我们就不信你了!",
|
| 667 |
+
"who": "farmer"
|
| 668 |
+
}
|
| 669 |
+
]
|
| 670 |
+
},
|
| 671 |
+
{
|
| 672 |
+
"id": "wolf",
|
| 673 |
+
"start": 79.3,
|
| 674 |
+
"dur": 12.0,
|
| 675 |
+
"voice_start": 79.8,
|
| 676 |
+
"voice_dur": 10.584,
|
| 677 |
+
"lines": [
|
| 678 |
+
{
|
| 679 |
+
"who": "n",
|
| 680 |
+
"text": "又过了些日子,乌云压着山头。树丛里,一双绿莹莹的眼睛,慢慢靠近了羊群。狼,真的来了!",
|
| 681 |
+
"t0": 79.8,
|
| 682 |
+
"dur": 10.584,
|
| 683 |
+
"subs": [
|
| 684 |
+
{
|
| 685 |
+
"t0": 79.8,
|
| 686 |
+
"t1": 81.564,
|
| 687 |
+
"text": "又过了些日子",
|
| 688 |
+
"who": "n"
|
| 689 |
+
},
|
| 690 |
+
{
|
| 691 |
+
"t0": 81.564,
|
| 692 |
+
"t1": 83.328,
|
| 693 |
+
"text": "乌云压着山头",
|
| 694 |
+
"who": "n"
|
| 695 |
+
},
|
| 696 |
+
{
|
| 697 |
+
"t0": 83.328,
|
| 698 |
+
"t1": 84.21,
|
| 699 |
+
"text": "树丛里",
|
| 700 |
+
"who": "n"
|
| 701 |
+
},
|
| 702 |
+
{
|
| 703 |
+
"t0": 84.21,
|
| 704 |
+
"t1": 86.562,
|
| 705 |
+
"text": "一双绿莹莹的眼睛",
|
| 706 |
+
"who": "n"
|
| 707 |
+
},
|
| 708 |
+
{
|
| 709 |
+
"t0": 86.562,
|
| 710 |
+
"t1": 88.62,
|
| 711 |
+
"text": "慢慢靠近了羊群",
|
| 712 |
+
"who": "n"
|
| 713 |
+
},
|
| 714 |
+
{
|
| 715 |
+
"t0": 88.62,
|
| 716 |
+
"t1": 88.914,
|
| 717 |
+
"text": "狼",
|
| 718 |
+
"who": "n"
|
| 719 |
+
},
|
| 720 |
+
{
|
| 721 |
+
"t0": 88.914,
|
| 722 |
+
"t1": 90.384,
|
| 723 |
+
"text": "真的来了!",
|
| 724 |
+
"who": "n"
|
| 725 |
+
}
|
| 726 |
+
]
|
| 727 |
+
}
|
| 728 |
+
],
|
| 729 |
+
"subs": [
|
| 730 |
+
{
|
| 731 |
+
"t0": 79.8,
|
| 732 |
+
"t1": 81.564,
|
| 733 |
+
"text": "又过了些日子",
|
| 734 |
+
"who": "n"
|
| 735 |
+
},
|
| 736 |
+
{
|
| 737 |
+
"t0": 81.564,
|
| 738 |
+
"t1": 83.328,
|
| 739 |
+
"text": "乌云压着山头",
|
| 740 |
+
"who": "n"
|
| 741 |
+
},
|
| 742 |
+
{
|
| 743 |
+
"t0": 83.328,
|
| 744 |
+
"t1": 84.21,
|
| 745 |
+
"text": "树丛里",
|
| 746 |
+
"who": "n"
|
| 747 |
+
},
|
| 748 |
+
{
|
| 749 |
+
"t0": 84.21,
|
| 750 |
+
"t1": 86.562,
|
| 751 |
+
"text": "一双绿莹莹的眼睛",
|
| 752 |
+
"who": "n"
|
| 753 |
+
},
|
| 754 |
+
{
|
| 755 |
+
"t0": 86.562,
|
| 756 |
+
"t1": 88.62,
|
| 757 |
+
"text": "慢慢靠近了羊群",
|
| 758 |
+
"who": "n"
|
| 759 |
+
},
|
| 760 |
+
{
|
| 761 |
+
"t0": 88.62,
|
| 762 |
+
"t1": 88.914,
|
| 763 |
+
"text": "狼",
|
| 764 |
+
"who": "n"
|
| 765 |
+
},
|
| 766 |
+
{
|
| 767 |
+
"t0": 88.914,
|
| 768 |
+
"t1": 90.384,
|
| 769 |
+
"text": "真的来了!",
|
| 770 |
+
"who": "n"
|
| 771 |
+
}
|
| 772 |
+
]
|
| 773 |
+
},
|
| 774 |
+
{
|
| 775 |
+
"id": "cry",
|
| 776 |
+
"start": 91.3,
|
| 777 |
+
"dur": 9.4,
|
| 778 |
+
"voice_start": 91.8,
|
| 779 |
+
"voice_dur": 7.988,
|
| 780 |
+
"lines": [
|
| 781 |
+
{
|
| 782 |
+
"who": "n",
|
| 783 |
+
"text": "放羊娃吓坏了,拼命地喊:",
|
| 784 |
+
"t0": 91.8,
|
| 785 |
+
"dur": 3.192,
|
| 786 |
+
"subs": [
|
| 787 |
+
{
|
| 788 |
+
"t0": 91.8,
|
| 789 |
+
"t1": 93.715,
|
| 790 |
+
"text": "放羊娃吓坏了",
|
| 791 |
+
"who": "n"
|
| 792 |
+
},
|
| 793 |
+
{
|
| 794 |
+
"t0": 93.715,
|
| 795 |
+
"t1": 94.992,
|
| 796 |
+
"text": "拼命地喊",
|
| 797 |
+
"who": "n"
|
| 798 |
+
}
|
| 799 |
+
]
|
| 800 |
+
},
|
| 801 |
+
{
|
| 802 |
+
"who": "boy",
|
| 803 |
+
"text": "狼来了!狼真的来了!救命啊!",
|
| 804 |
+
"t0": 95.372,
|
| 805 |
+
"dur": 4.416,
|
| 806 |
+
"subs": [
|
| 807 |
+
{
|
| 808 |
+
"t0": 95.372,
|
| 809 |
+
"t1": 96.634,
|
| 810 |
+
"text": "狼来了!",
|
| 811 |
+
"who": "boy"
|
| 812 |
+
},
|
| 813 |
+
{
|
| 814 |
+
"t0": 96.634,
|
| 815 |
+
"t1": 98.526,
|
| 816 |
+
"text": "狼真的来了!",
|
| 817 |
+
"who": "boy"
|
| 818 |
+
},
|
| 819 |
+
{
|
| 820 |
+
"t0": 98.526,
|
| 821 |
+
"t1": 99.788,
|
| 822 |
+
"text": "救命啊!",
|
| 823 |
+
"who": "boy"
|
| 824 |
+
}
|
| 825 |
+
]
|
| 826 |
+
}
|
| 827 |
+
],
|
| 828 |
+
"subs": [
|
| 829 |
+
{
|
| 830 |
+
"t0": 91.8,
|
| 831 |
+
"t1": 93.715,
|
| 832 |
+
"text": "放羊娃吓坏了",
|
| 833 |
+
"who": "n"
|
| 834 |
+
},
|
| 835 |
+
{
|
| 836 |
+
"t0": 93.715,
|
| 837 |
+
"t1": 94.992,
|
| 838 |
+
"text": "拼命地喊",
|
| 839 |
+
"who": "n"
|
| 840 |
+
},
|
| 841 |
+
{
|
| 842 |
+
"t0": 95.372,
|
| 843 |
+
"t1": 96.634,
|
| 844 |
+
"text": "狼来了!",
|
| 845 |
+
"who": "boy"
|
| 846 |
+
},
|
| 847 |
+
{
|
| 848 |
+
"t0": 96.634,
|
| 849 |
+
"t1": 98.526,
|
| 850 |
+
"text": "狼真的来了!",
|
| 851 |
+
"who": "boy"
|
| 852 |
+
},
|
| 853 |
+
{
|
| 854 |
+
"t0": 98.526,
|
| 855 |
+
"t1": 99.788,
|
| 856 |
+
"text": "救命啊!",
|
| 857 |
+
"who": "boy"
|
| 858 |
+
}
|
| 859 |
+
]
|
| 860 |
+
},
|
| 861 |
+
{
|
| 862 |
+
"id": "noone",
|
| 863 |
+
"start": 100.7,
|
| 864 |
+
"dur": 11.166666666666666,
|
| 865 |
+
"voice_start": 101.2,
|
| 866 |
+
"voice_dur": 9.76,
|
| 867 |
+
"lines": [
|
| 868 |
+
{
|
| 869 |
+
"who": "n",
|
| 870 |
+
"text": "山下的农民听见了,都以为他又在说谎。",
|
| 871 |
+
"t0": 101.2,
|
| 872 |
+
"dur": 4.296,
|
| 873 |
+
"subs": [
|
| 874 |
+
{
|
| 875 |
+
"t0": 101.2,
|
| 876 |
+
"t1": 103.348,
|
| 877 |
+
"text": "山下的农民听见了",
|
| 878 |
+
"who": "n"
|
| 879 |
+
},
|
| 880 |
+
{
|
| 881 |
+
"t0": 103.348,
|
| 882 |
+
"t1": 105.496,
|
| 883 |
+
"text": "都以为他又在说谎",
|
| 884 |
+
"who": "n"
|
| 885 |
+
}
|
| 886 |
+
]
|
| 887 |
+
},
|
| 888 |
+
{
|
| 889 |
+
"who": "farmer",
|
| 890 |
+
"text": "哼,又来骗人。",
|
| 891 |
+
"t0": 105.876,
|
| 892 |
+
"dur": 2.664,
|
| 893 |
+
"subs": [
|
| 894 |
+
{
|
| 895 |
+
"t0": 105.876,
|
| 896 |
+
"t1": 106.409,
|
| 897 |
+
"text": "哼",
|
| 898 |
+
"who": "farmer"
|
| 899 |
+
},
|
| 900 |
+
{
|
| 901 |
+
"t0": 106.409,
|
| 902 |
+
"t1": 108.54,
|
| 903 |
+
"text": "又来骗人",
|
| 904 |
+
"who": "farmer"
|
| 905 |
+
}
|
| 906 |
+
]
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
"who": "n",
|
| 910 |
+
"text": "谁也没有上山。",
|
| 911 |
+
"t0": 108.92,
|
| 912 |
+
"dur": 2.04,
|
| 913 |
+
"subs": [
|
| 914 |
+
{
|
| 915 |
+
"t0": 108.92,
|
| 916 |
+
"t1": 110.96,
|
| 917 |
+
"text": "谁也没有上山",
|
| 918 |
+
"who": "n"
|
| 919 |
+
}
|
| 920 |
+
]
|
| 921 |
+
}
|
| 922 |
+
],
|
| 923 |
+
"subs": [
|
| 924 |
+
{
|
| 925 |
+
"t0": 101.2,
|
| 926 |
+
"t1": 103.348,
|
| 927 |
+
"text": "山下的农民听见了",
|
| 928 |
+
"who": "n"
|
| 929 |
+
},
|
| 930 |
+
{
|
| 931 |
+
"t0": 103.348,
|
| 932 |
+
"t1": 105.496,
|
| 933 |
+
"text": "都以为他又在说谎",
|
| 934 |
+
"who": "n"
|
| 935 |
+
},
|
| 936 |
+
{
|
| 937 |
+
"t0": 105.876,
|
| 938 |
+
"t1": 106.409,
|
| 939 |
+
"text": "哼",
|
| 940 |
+
"who": "farmer"
|
| 941 |
+
},
|
| 942 |
+
{
|
| 943 |
+
"t0": 106.409,
|
| 944 |
+
"t1": 108.54,
|
| 945 |
+
"text": "又来骗人",
|
| 946 |
+
"who": "farmer"
|
| 947 |
+
},
|
| 948 |
+
{
|
| 949 |
+
"t0": 108.92,
|
| 950 |
+
"t1": 110.96,
|
| 951 |
+
"text": "谁也没有上山",
|
| 952 |
+
"who": "n"
|
| 953 |
+
}
|
| 954 |
+
]
|
| 955 |
+
},
|
| 956 |
+
{
|
| 957 |
+
"id": "chaos",
|
| 958 |
+
"start": 111.8667,
|
| 959 |
+
"dur": 8.0,
|
| 960 |
+
"voice_start": 112.367,
|
| 961 |
+
"voice_dur": 5.976,
|
| 962 |
+
"lines": [
|
| 963 |
+
{
|
| 964 |
+
"who": "n",
|
| 965 |
+
"text": "狼冲进羊群,羊儿吓得四处乱跑,好几只羊被狼叼走了。",
|
| 966 |
+
"t0": 112.367,
|
| 967 |
+
"dur": 5.976,
|
| 968 |
+
"subs": [
|
| 969 |
+
{
|
| 970 |
+
"t0": 112.367,
|
| 971 |
+
"t1": 113.725,
|
| 972 |
+
"text": "狼冲进羊群",
|
| 973 |
+
"who": "n"
|
| 974 |
+
},
|
| 975 |
+
{
|
| 976 |
+
"t0": 113.725,
|
| 977 |
+
"t1": 115.898,
|
| 978 |
+
"text": "羊儿吓得四处乱跑",
|
| 979 |
+
"who": "n"
|
| 980 |
+
},
|
| 981 |
+
{
|
| 982 |
+
"t0": 115.898,
|
| 983 |
+
"t1": 118.343,
|
| 984 |
+
"text": "好几只羊被狼叼走了",
|
| 985 |
+
"who": "n"
|
| 986 |
+
}
|
| 987 |
+
]
|
| 988 |
+
}
|
| 989 |
+
],
|
| 990 |
+
"subs": [
|
| 991 |
+
{
|
| 992 |
+
"t0": 112.367,
|
| 993 |
+
"t1": 113.725,
|
| 994 |
+
"text": "狼冲进羊群",
|
| 995 |
+
"who": "n"
|
| 996 |
+
},
|
| 997 |
+
{
|
| 998 |
+
"t0": 113.725,
|
| 999 |
+
"t1": 115.898,
|
| 1000 |
+
"text": "羊儿吓得四处乱跑",
|
| 1001 |
+
"who": "n"
|
| 1002 |
+
},
|
| 1003 |
+
{
|
| 1004 |
+
"t0": 115.898,
|
| 1005 |
+
"t1": 118.343,
|
| 1006 |
+
"text": "好几只羊被狼叼走了",
|
| 1007 |
+
"who": "n"
|
| 1008 |
+
}
|
| 1009 |
+
]
|
| 1010 |
+
},
|
| 1011 |
+
{
|
| 1012 |
+
"id": "regret",
|
| 1013 |
+
"start": 119.8667,
|
| 1014 |
+
"dur": 8.0,
|
| 1015 |
+
"voice_start": 120.367,
|
| 1016 |
+
"voice_dur": 5.328,
|
| 1017 |
+
"lines": [
|
| 1018 |
+
{
|
| 1019 |
+
"who": "n",
|
| 1020 |
+
"text": "太阳落山了,放羊娃抱着剩下的小羊,伤心地哭了。",
|
| 1021 |
+
"t0": 120.367,
|
| 1022 |
+
"dur": 5.328,
|
| 1023 |
+
"subs": [
|
| 1024 |
+
{
|
| 1025 |
+
"t0": 120.367,
|
| 1026 |
+
"t1": 121.699,
|
| 1027 |
+
"text": "太阳落山了",
|
| 1028 |
+
"who": "n"
|
| 1029 |
+
},
|
| 1030 |
+
{
|
| 1031 |
+
"t0": 121.699,
|
| 1032 |
+
"t1": 124.363,
|
| 1033 |
+
"text": "放羊娃抱着剩下的小羊",
|
| 1034 |
+
"who": "n"
|
| 1035 |
+
},
|
| 1036 |
+
{
|
| 1037 |
+
"t0": 124.363,
|
| 1038 |
+
"t1": 125.695,
|
| 1039 |
+
"text": "伤心地哭了",
|
| 1040 |
+
"who": "n"
|
| 1041 |
+
}
|
| 1042 |
+
]
|
| 1043 |
+
}
|
| 1044 |
+
],
|
| 1045 |
+
"subs": [
|
| 1046 |
+
{
|
| 1047 |
+
"t0": 120.367,
|
| 1048 |
+
"t1": 121.699,
|
| 1049 |
+
"text": "太阳落山了",
|
| 1050 |
+
"who": "n"
|
| 1051 |
+
},
|
| 1052 |
+
{
|
| 1053 |
+
"t0": 121.699,
|
| 1054 |
+
"t1": 124.363,
|
| 1055 |
+
"text": "放羊娃抱着剩下的小羊",
|
| 1056 |
+
"who": "n"
|
| 1057 |
+
},
|
| 1058 |
+
{
|
| 1059 |
+
"t0": 124.363,
|
| 1060 |
+
"t1": 125.695,
|
| 1061 |
+
"text": "伤心地哭了",
|
| 1062 |
+
"who": "n"
|
| 1063 |
+
}
|
| 1064 |
+
]
|
| 1065 |
+
},
|
| 1066 |
+
{
|
| 1067 |
+
"id": "moral",
|
| 1068 |
+
"start": 127.8667,
|
| 1069 |
+
"dur": 10.0,
|
| 1070 |
+
"voice_start": 128.367,
|
| 1071 |
+
"voice_dur": 7.224,
|
| 1072 |
+
"lines": [
|
| 1073 |
+
{
|
| 1074 |
+
"who": "n",
|
| 1075 |
+
"text": "说谎的人,就算说了真话,也没有人再相信他。做一个诚实的孩子吧。",
|
| 1076 |
+
"t0": 128.367,
|
| 1077 |
+
"dur": 7.224,
|
| 1078 |
+
"subs": [
|
| 1079 |
+
{
|
| 1080 |
+
"t0": 128.367,
|
| 1081 |
+
"t1": 129.437,
|
| 1082 |
+
"text": "说谎的人",
|
| 1083 |
+
"who": "n"
|
| 1084 |
+
},
|
| 1085 |
+
{
|
| 1086 |
+
"t0": 129.437,
|
| 1087 |
+
"t1": 131.042,
|
| 1088 |
+
"text": "就算说了真话",
|
| 1089 |
+
"who": "n"
|
| 1090 |
+
},
|
| 1091 |
+
{
|
| 1092 |
+
"t0": 131.042,
|
| 1093 |
+
"t1": 133.183,
|
| 1094 |
+
"text": "也没有人再相信他",
|
| 1095 |
+
"who": "n"
|
| 1096 |
+
},
|
| 1097 |
+
{
|
| 1098 |
+
"t0": 133.183,
|
| 1099 |
+
"t1": 135.591,
|
| 1100 |
+
"text": "做一个诚实的孩子吧",
|
| 1101 |
+
"who": "n"
|
| 1102 |
+
}
|
| 1103 |
+
]
|
| 1104 |
+
}
|
| 1105 |
+
],
|
| 1106 |
+
"subs": [
|
| 1107 |
+
{
|
| 1108 |
+
"t0": 128.367,
|
| 1109 |
+
"t1": 129.437,
|
| 1110 |
+
"text": "说谎的人",
|
| 1111 |
+
"who": "n"
|
| 1112 |
+
},
|
| 1113 |
+
{
|
| 1114 |
+
"t0": 129.437,
|
| 1115 |
+
"t1": 131.042,
|
| 1116 |
+
"text": "就算说了真话",
|
| 1117 |
+
"who": "n"
|
| 1118 |
+
},
|
| 1119 |
+
{
|
| 1120 |
+
"t0": 131.042,
|
| 1121 |
+
"t1": 133.183,
|
| 1122 |
+
"text": "也没有人再相信他",
|
| 1123 |
+
"who": "n"
|
| 1124 |
+
},
|
| 1125 |
+
{
|
| 1126 |
+
"t0": 133.183,
|
| 1127 |
+
"t1": 135.591,
|
| 1128 |
+
"text": "做一个诚实的孩子吧",
|
| 1129 |
+
"who": "n"
|
| 1130 |
+
}
|
| 1131 |
+
]
|
| 1132 |
+
}
|
| 1133 |
+
]
|
| 1134 |
+
}
|
wolf-coming/video/wolf/voice.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0d161c8ce43a4b1d0cc2927f83ec8a118133e873443bc70dc060f425d584e483
|
| 3 |
+
size 1655162
|
wolf-coming/web/public/film/fonts/chars.txt
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
!#%+,-./0123456789:;?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz·—‘’“”…、。《》「」【】・一三上下不与丑世丛东丝两个中为主丽举久么乎乐也乡书乱了事二于云五些交享亮亲人仅今介从他代以们仰任伙会传伴位低住体何作你例侧便俏保信倒候值停傍像儿充入全公六共关其具内再写冠冬冰冲决况冷冻准几出击函分则创到制前剩副力加动勾匀包化区半单印即压原又叉友发取变叠口古句另只叫可台右叶号叽各合同名后向吓吗吧呆告命和咔咦咬哀哈响哔哥哩哪唧啄喂善喊喙喜喳嘲嘴器噪嚓四回团围固国图圆圈在地场均坐块垂垛域堆塘填境增墨壁声壳处备复夏外多夜大天太失头夹奇奏她好妈始姐姑娘嫌子字存季孤学孩孵它安完官定实宣宫家宽寂寄密寒寸对导封小尖尚就尺尼尾层屋屏展属山岸峰工左差己已布希帘带帧幅幕干平并序应底度廓延建开式引张弦弯弹归当录形彩影往径很徒得循微心快念忽态怕性怪总息悄悬情惊愈意感慢成截所扇手才扑打扫扬把抖折报拉拍拖拢拨择持按挖振换掉排掠接控推描提插揭撇播擎支收改放政故效敢数整文斑断新旁无日时明春是昼显晓晕晚景晰暂暖暗曲更替最有朋望朝期木未本术朱机权村条来极构果枝枪染柔查柳标树校样核根格桃案梅梦梳检楷模横檐欠次欢欺款歌正此步段每比毛气水池治沼沿法波泥泪泽洁流测浏浓浮消淡深混清渐渡温渲游湖湿溅源溪滴漂漫潮灰点烟烧然照熬片版牙物状猎率环现理瓣生用田由电画畅界痕登白的皮皴益盖盛目直相省看真眼着睡瞬瞻短码砂砰破确碍示神离秋种秒积程空窗窝立竖章童竹笆笑笔笛符等筝签简篇篱簧粒粘系素紧红纤约级纳纷纸纹线组终经绒结绘给络绪续维缓编缘缩网置美群羽翅翻翼而肉背能脊脑脖脚脸腿膀臂自致色艺节芦花芽苇苹范茸草荷落蓝藏蛋蜓蜻蝴蝶融行衣补表被裁裂装西要见观规视览觉角解触计让记讲证试
|
|
|
|
| 1 |
+
!#%+,-./0123456789:;?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz·—‘’“”…、。《》「」【】・一三上下不与丑专世丛东丝两个中为主丽举久么义之乌乎乐也乡书乱了事二于云互五些交享亮亲人什仅今介从他代以们仰任伊伙会传伤伴伸位低住体何余作你使例供侧便促俏保信倒候值做停健傍像儿充先光入全公六共关其具内再写农冠冬冰冲决况冷冻准几出击函分列则创到制刻前剩剪副力加动勾匀包化区升半单印即却卷压原去又叉及友双发取变叠口古句另只叫可台右叶号叼叽吃各合同名后向吓吗否吧听吹吻呀呆呈告呼命和咔咦咬哀哈响哔哥哩哪哭哼唧啄啊啦喂善喊喘喙喜喳嗓嘲嘴器噪嚎嚓四回团园围固国图圆圈在地场址均坏坐块坡垂型垛域堆塘塞填境墙增墨壁声壳处备复夏夕外多夜大天太失头夹奇奏奔套她好如妈始姐姑姿娃娘嫌子字存季孤学孩孵它安完官定实宣宫家宽寂寄密寒寓寸对导封小尔尖尚就尺尼尾层屋屏展属山岸峰工左差己已巾布希帘带帧帽幅幕干平并序库应底度廓延建开式引张弦弧弯弹强归当录形彩影往径很徒得循微心忙快念忽态怕急性怪总息悄悔悠悬情惊惋惜愈意感慢憋懒成我或截所扁扇手才扑打扛扫扬把抖折报抱担拉拍拖拢拨择拱拼拿持挂按挖振换掉排掠探接控推描提插握揭摇撇播擎支收改放政故效救敢散数整文斑斜断新方旁无日时明易春是昼显晓晕晚景晰暂暖暗曲更替最有朋望朝期木未末本术朱机权村条来极构果枝枪柄染柔查柳标树校样核根格桃案梅梦梯梳检楷模横檐欠次欢欺款歌正此步段每比毛民气水求汗池沙没治沼沿法波泥泪泽洁洋活流测浏浓浮消润淡深混清渐渡温渲游湖湿溅源溪滑滴漂漫潮灯灰点烟烧然照熬爱片版牙物状独狼猎率玩环现珠理瓣生用田由电画畅界略疏痕登白的皮皴益盖盛目直相省眉看真眼着睛睡瞬瞻短石码砂砰破确碍碎示神离秋种秒秧积移稀程稍空窗窝立竖章童竹笆笑笔笛符第等筐筝签简算篇篱簧类粒粗粘系素索紧红纤约级纳纷纸纹线组细终经绑绒结绘给络绪续维绿缓编缘缩网置署羊美群羽翅翻翼而耳聊肉肢肤背胸能脊脑脖脚脸腮腰腿膀膝臂自致色艺节芦花芽苇苗苹范茅茸草荷莹落蓝藏蛋蜓蜻蝴蝶融行衣补表袖被裁裂装裤襟西要见观规视览觉角解触言警计认让记讲设证词试诚��语说请读课谁调谎谷贝负败质贴资走赶起越跑路跳身躯躲躺轨转轮软轴轻载输辨辫边迁过近还这进远连迟述追送适逃选逐递逗通逼遍道遮那部都配采里重野量金钟钻铃铺链锄错键镜长门闪间阅阳阴阵阶阻附限除随难雁雪雷雾需霞露静靠面音页顶顿预领颈频颗题颜额颤风飘飞马驳验骗骨高髻鱼鸟鸡鸣鸦鸭鹅鹜麦黄黑默鼻齐️︐!()+,:;?|~🐑🐓🐺👀📢😂🥚🦢
|
wolf-coming/web/public/film/fonts/mashanzheng.woff2
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fe7ce130e02290a7dff93743e8a7492fb7c41bdbe9d6b338945144e018735476
|
| 3 |
+
size 420940
|
wolf-coming/web/public/film/fonts/wenkai.woff2
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:14374c15ca95593a0fc159be8f69f4435a3e7c4206b1817fd7bfba9495222f02
|
| 3 |
+
size 240976
|
wolf-coming/web/public/wolf/voice.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0d161c8ce43a4b1d0cc2927f83ec8a118133e873443bc70dc060f425d584e483
|
| 3 |
+
size 1655162
|
wolf-coming/web/src/App.tsx
CHANGED
|
@@ -5,7 +5,12 @@ import Covers from '@/components/Covers'
|
|
| 5 |
import PublishCopy from '@/components/PublishCopy'
|
| 6 |
import LivePhotos from '@/components/LivePhotos'
|
| 7 |
import Making from '@/components/Making'
|
| 8 |
-
import { TOTAL } from '@/film/film'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
const isPhone = () => typeof window !== 'undefined' && window.innerWidth < 768 && window.innerHeight > window.innerWidth
|
| 11 |
|
|
@@ -29,8 +34,13 @@ export default function App() {
|
|
| 29 |
<div className="paper-bg min-h-screen text-[#2a2420]">
|
| 30 |
<header className="sticky top-0 z-20 border-b border-[#d9cdb2] bg-[#f3ecdc]/90 backdrop-blur">
|
| 31 |
<div className="mx-auto flex max-w-6xl items-center gap-3 px-4 py-2">
|
| 32 |
-
<span className="flex h-9 w-9 items-center justify-center rounded-sm bg-[#b8322a] font-brush text-lg leading-none text-[#f6efdf]">
|
| 33 |
-
<span className="font-brush text-2xl">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
<nav className="ml-auto flex gap-1 overflow-x-auto text-sm">
|
| 35 |
{nav.map(([id, n]) => <a key={id} href={`#${id}`} className="shrink-0 rounded px-2 py-1 text-[#4a3f30] hover:bg-[#e8dec6]">{n}</a>)}
|
| 36 |
</nav>
|
|
@@ -40,14 +50,12 @@ export default function App() {
|
|
| 40 |
<main className="mx-auto flex max-w-6xl flex-col gap-14 px-4 pb-24 pt-8">
|
| 41 |
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
| 42 |
<div>
|
| 43 |
-
<h1 className="font-brush text-5xl leading-tight md:text-6xl">
|
| 44 |
-
<p className="mt-2 max-w-2xl leading-relaxed text-[#4a3f30]">
|
| 45 |
-
安徒生童话 · 水墨手绘儿童动画。宣纸、毛笔、淡彩、朱红印章,像小学语文课本里的插图那样,一笔一画在你的浏览器里实时画出来;旁白、原创国风配乐与音效同步播放。
|
| 46 |
-
</p>
|
| 47 |
</div>
|
| 48 |
<div className="flex gap-4 text-sm text-[#6b5d48]">
|
| 49 |
<div><div className="font-brush text-2xl text-[#2a2420]">{Math.floor(TOTAL / 60)}′{Math.round(TOTAL % 60)}″</div>片长</div>
|
| 50 |
-
<div><div className="font-brush text-2xl text-[#2a2420]">
|
| 51 |
<div><div className="font-brush text-2xl text-[#2a2420]">2</div>画幅</div>
|
| 52 |
</div>
|
| 53 |
</div>
|
|
@@ -60,7 +68,7 @@ export default function App() {
|
|
| 60 |
<ExportPanel key={orientation} defaultAspect={orientation === 'portrait' ? '9:16' : '16:9'} />
|
| 61 |
</Section>
|
| 62 |
|
| 63 |
-
<Section id="covers" no="三" title="封面" sub=
|
| 64 |
<Covers />
|
| 65 |
</Section>
|
| 66 |
|
|
@@ -68,7 +76,7 @@ export default function App() {
|
|
| 68 |
<PublishCopy />
|
| 69 |
</Section>
|
| 70 |
|
| 71 |
-
<Section id="live" no="五" title="实况图" sub=
|
| 72 |
<LivePhotos />
|
| 73 |
</Section>
|
| 74 |
|
|
@@ -77,7 +85,7 @@ export default function App() {
|
|
| 77 |
</Section>
|
| 78 |
</main>
|
| 79 |
<footer className="border-t border-[#d9cdb2] py-6 text-center text-xs text-[#8a7a5a]">
|
| 80 |
-
|
| 81 |
</footer>
|
| 82 |
</div>
|
| 83 |
)
|
|
|
|
| 5 |
import PublishCopy from '@/components/PublishCopy'
|
| 6 |
import LivePhotos from '@/components/LivePhotos'
|
| 7 |
import Making from '@/components/Making'
|
| 8 |
+
import { TOTAL, FILM, setFilm } from '@/film/film'
|
| 9 |
+
import { FILMS, pickFilm } from '@/films'
|
| 10 |
+
|
| 11 |
+
const PICK = pickFilm()
|
| 12 |
+
setFilm(PICK.film)
|
| 13 |
+
if (typeof window !== 'undefined' && !PICK.locked) window.addEventListener('hashchange', () => location.reload())
|
| 14 |
|
| 15 |
const isPhone = () => typeof window !== 'undefined' && window.innerWidth < 768 && window.innerHeight > window.innerWidth
|
| 16 |
|
|
|
|
| 34 |
<div className="paper-bg min-h-screen text-[#2a2420]">
|
| 35 |
<header className="sticky top-0 z-20 border-b border-[#d9cdb2] bg-[#f3ecdc]/90 backdrop-blur">
|
| 36 |
<div className="mx-auto flex max-w-6xl items-center gap-3 px-4 py-2">
|
| 37 |
+
<span className="flex h-9 w-9 items-center justify-center rounded-sm bg-[#b8322a] font-brush text-lg leading-none text-[#f6efdf]">{[...FILM.title].pop()}</span>
|
| 38 |
+
<span className="font-brush text-2xl">{FILM.title}</span>
|
| 39 |
+
<span className="hidden items-center gap-1 sm:flex">
|
| 40 |
+
{FILMS.filter((f) => f.id !== FILM.id).map((f) => (
|
| 41 |
+
<a key={f.id} href={PICK.locked ? `../${f.slug}/` : `#${f.id}`} className="rounded border border-[#d0c3a5] px-2 py-0.5 text-xs text-[#6b5d48] hover:border-[#8a7a5a]">系列 · 看《{f.title}》</a>
|
| 42 |
+
))}
|
| 43 |
+
</span>
|
| 44 |
<nav className="ml-auto flex gap-1 overflow-x-auto text-sm">
|
| 45 |
{nav.map(([id, n]) => <a key={id} href={`#${id}`} className="shrink-0 rounded px-2 py-1 text-[#4a3f30] hover:bg-[#e8dec6]">{n}</a>)}
|
| 46 |
</nav>
|
|
|
|
| 50 |
<main className="mx-auto flex max-w-6xl flex-col gap-14 px-4 pb-24 pt-8">
|
| 51 |
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
| 52 |
<div>
|
| 53 |
+
<h1 className="font-brush text-5xl leading-tight md:text-6xl">{FILM.title}<span className="ml-3 align-middle text-2xl text-[#8a7a5a] md:text-3xl">{FILM.tagline}</span></h1>
|
| 54 |
+
<p className="mt-2 max-w-2xl leading-relaxed text-[#4a3f30]">{FILM.intro}</p>
|
|
|
|
|
|
|
| 55 |
</div>
|
| 56 |
<div className="flex gap-4 text-sm text-[#6b5d48]">
|
| 57 |
<div><div className="font-brush text-2xl text-[#2a2420]">{Math.floor(TOTAL / 60)}′{Math.round(TOTAL % 60)}″</div>片长</div>
|
| 58 |
+
<div><div className="font-brush text-2xl text-[#2a2420]">{FILM.timeline.scenes.length}</div>场景</div>
|
| 59 |
<div><div className="font-brush text-2xl text-[#2a2420]">2</div>画幅</div>
|
| 60 |
</div>
|
| 61 |
</div>
|
|
|
|
| 68 |
<ExportPanel key={orientation} defaultAspect={orientation === 'portrait' ? '9:16' : '16:9'} />
|
| 69 |
</Section>
|
| 70 |
|
| 71 |
+
<Section id="covers" no="三" title="封面" sub={`4:3 · 3:4 · 16:9 各 ${FILM.covers.length} 款,可多选打包`}>
|
| 72 |
<Covers />
|
| 73 |
</Section>
|
| 74 |
|
|
|
|
| 76 |
<PublishCopy />
|
| 77 |
</Section>
|
| 78 |
|
| 79 |
+
<Section id="live" no="五" title="实况图" sub={`${FILM.live.length} 组 JPG + MOV · 小红书 / 抖音图文`}>
|
| 80 |
<LivePhotos />
|
| 81 |
</Section>
|
| 82 |
|
|
|
|
| 85 |
</Section>
|
| 86 |
</main>
|
| 87 |
<footer className="border-t border-[#d9cdb2] py-6 text-center text-xs text-[#8a7a5a]">
|
| 88 |
+
{FILM.credit} · 画面、配乐、音效均由代码原创生成 · 字体:霞鹜文楷、马善政毛笔楷书(SIL OFL)
|
| 89 |
</footer>
|
| 90 |
</div>
|
| 91 |
)
|
wolf-coming/web/src/components/Covers.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
| 1 |
import { useEffect, useState } from 'react'
|
| 2 |
import { Check, Download, Loader2, PackageOpen } from 'lucide-react'
|
| 3 |
import { zipSync } from 'fflate'
|
| 4 |
-
import {
|
|
|
|
|
|
|
| 5 |
import { loadFonts } from '@/film/core'
|
| 6 |
|
| 7 |
const BASE = import.meta.env.BASE_URL
|
| 8 |
type Ratio = (typeof COVER_RATIOS)[number]
|
| 9 |
-
type Design =
|
| 10 |
type Item = { key: string; ratio: string; design: string; name: string; url: string; w: number; h: number }
|
| 11 |
const THUMB = 420 // 预览图长边;下载时才按原尺寸重新绘制(省内存,手机也流畅)
|
| 12 |
|
|
@@ -20,8 +22,8 @@ async function renderCover(ratio: Ratio, d: Design, scale = 1): Promise<Blob> {
|
|
| 20 |
c.width = c.height = 1
|
| 21 |
return blob
|
| 22 |
}
|
| 23 |
-
const fullName = (r: Ratio, d: Design) => `
|
| 24 |
-
const find = (key: string) => { const [r, d] = key.split('|'); return { r: COVER_RATIOS.find((x) => x.id === r)!, d:
|
| 25 |
|
| 26 |
export default function Covers() {
|
| 27 |
const [items, setItems] = useState<Item[]>([])
|
|
@@ -33,7 +35,7 @@ export default function Covers() {
|
|
| 33 |
let dead = false
|
| 34 |
;(async () => {
|
| 35 |
await loadFonts(BASE)
|
| 36 |
-
for (const r of COVER_RATIOS) for (const d of
|
| 37 |
if (dead) return
|
| 38 |
await new Promise((res) => setTimeout(res, 30)) // 让出主线程
|
| 39 |
const blob = await renderCover(r, d, THUMB / Math.max(r.w, r.h))
|
|
@@ -44,7 +46,7 @@ export default function Covers() {
|
|
| 44 |
return () => { dead = true }
|
| 45 |
}, [])
|
| 46 |
|
| 47 |
-
const total = COVER_RATIOS.length *
|
| 48 |
const shown = items.filter((i) => tab === '全部' || i.ratio === tab)
|
| 49 |
const toggle = (k: string) => setSel((s) => { const n = new Set(s); if (n.has(k)) n.delete(k); else n.add(k); return n })
|
| 50 |
const [busyKey, setBusyKey] = useState<string | null>(null)
|
|
@@ -55,7 +57,7 @@ export default function Covers() {
|
|
| 55 |
const files: Record<string, Uint8Array> = {}
|
| 56 |
for (const it of items.filter((i) => sel.has(i.key))) { const { r, d } = find(it.key); files[it.name] = new Uint8Array(await (await renderCover(r, d)).arrayBuffer()); await new Promise((res) => setTimeout(res, 10)) }
|
| 57 |
const out = zipSync(files, { level: 0 })
|
| 58 |
-
save(new Blob([out as Uint8Array<ArrayBuffer>], { type: 'application/zip' }), `
|
| 59 |
setZipping(false)
|
| 60 |
}
|
| 61 |
|
|
@@ -84,7 +86,7 @@ export default function Covers() {
|
|
| 84 |
</button>
|
| 85 |
<span className={`absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded border-2 ${on ? 'border-[#b8322a] bg-[#b8322a] text-white' : 'border-white/90 bg-black/20'}`}>{on && <Check className="h-4 w-4" />}</span>
|
| 86 |
<div className="flex items-center justify-between px-2 py-1.5 text-xs text-[#4a3f30]">
|
| 87 |
-
<span><b className="mr-1">{it.ratio}</b>{
|
| 88 |
<button onClick={() => dl(it)} className="rounded p-1 hover:bg-[#ece2cc]" aria-label="下载原图" title="下载原尺寸 JPG">{busyKey === it.key ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4" />}</button>
|
| 89 |
</div>
|
| 90 |
</div>
|
|
|
|
| 1 |
import { useEffect, useState } from 'react'
|
| 2 |
import { Check, Download, Loader2, PackageOpen } from 'lucide-react'
|
| 3 |
import { zipSync } from 'fflate'
|
| 4 |
+
import { COVER_RATIOS, drawCover } from '@/film/overlays'
|
| 5 |
+
import { FILM } from '@/film/film'
|
| 6 |
+
import type { CoverDesign } from '@/film/types'
|
| 7 |
import { loadFonts } from '@/film/core'
|
| 8 |
|
| 9 |
const BASE = import.meta.env.BASE_URL
|
| 10 |
type Ratio = (typeof COVER_RATIOS)[number]
|
| 11 |
+
type Design = CoverDesign
|
| 12 |
type Item = { key: string; ratio: string; design: string; name: string; url: string; w: number; h: number }
|
| 13 |
const THUMB = 420 // 预览图长边;下载时才按原尺寸重新绘制(省内存,手机也流畅)
|
| 14 |
|
|
|
|
| 22 |
c.width = c.height = 1
|
| 23 |
return blob
|
| 24 |
}
|
| 25 |
+
const fullName = (r: Ratio, d: Design) => `${FILM.title}封面_${r.id.replace(':', 'x')}_${d.name}_${r.w}x${r.h}.jpg`
|
| 26 |
+
const find = (key: string) => { const [r, d] = key.split('|'); return { r: COVER_RATIOS.find((x) => x.id === r)!, d: FILM.covers.find((x) => x.id === d)! } }
|
| 27 |
|
| 28 |
export default function Covers() {
|
| 29 |
const [items, setItems] = useState<Item[]>([])
|
|
|
|
| 35 |
let dead = false
|
| 36 |
;(async () => {
|
| 37 |
await loadFonts(BASE)
|
| 38 |
+
for (const r of COVER_RATIOS) for (const d of FILM.covers) {
|
| 39 |
if (dead) return
|
| 40 |
await new Promise((res) => setTimeout(res, 30)) // 让出主线程
|
| 41 |
const blob = await renderCover(r, d, THUMB / Math.max(r.w, r.h))
|
|
|
|
| 46 |
return () => { dead = true }
|
| 47 |
}, [])
|
| 48 |
|
| 49 |
+
const total = COVER_RATIOS.length * FILM.covers.length
|
| 50 |
const shown = items.filter((i) => tab === '全部' || i.ratio === tab)
|
| 51 |
const toggle = (k: string) => setSel((s) => { const n = new Set(s); if (n.has(k)) n.delete(k); else n.add(k); return n })
|
| 52 |
const [busyKey, setBusyKey] = useState<string | null>(null)
|
|
|
|
| 57 |
const files: Record<string, Uint8Array> = {}
|
| 58 |
for (const it of items.filter((i) => sel.has(i.key))) { const { r, d } = find(it.key); files[it.name] = new Uint8Array(await (await renderCover(r, d)).arrayBuffer()); await new Promise((res) => setTimeout(res, 10)) }
|
| 59 |
const out = zipSync(files, { level: 0 })
|
| 60 |
+
save(new Blob([out as Uint8Array<ArrayBuffer>], { type: 'application/zip' }), `${FILM.title}封面_${sel.size}张.zip`)
|
| 61 |
setZipping(false)
|
| 62 |
}
|
| 63 |
|
|
|
|
| 86 |
</button>
|
| 87 |
<span className={`absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded border-2 ${on ? 'border-[#b8322a] bg-[#b8322a] text-white' : 'border-white/90 bg-black/20'}`}>{on && <Check className="h-4 w-4" />}</span>
|
| 88 |
<div className="flex items-center justify-between px-2 py-1.5 text-xs text-[#4a3f30]">
|
| 89 |
+
<span><b className="mr-1">{it.ratio}</b>{FILM.covers.find((d) => d.id === it.design)?.name} · {it.w}×{it.h}</span>
|
| 90 |
<button onClick={() => dl(it)} className="rounded p-1 hover:bg-[#ece2cc]" aria-label="下载原图" title="下载原尺寸 JPG">{busyKey === it.key ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4" />}</button>
|
| 91 |
</div>
|
| 92 |
</div>
|
wolf-coming/web/src/components/ExportPanel.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { useRef, useState } from 'react'
|
| 2 |
import { Download, Film, Loader2, X, Smartphone, Monitor } from 'lucide-react'
|
| 3 |
import { Output, Mp4OutputFormat, BufferTarget, CanvasSource, AudioBufferSource, QUALITY_HIGH, getFirstEncodableVideoCodec, getFirstEncodableAudioCodec } from 'mediabunny'
|
| 4 |
-
import { renderFrame, TOTAL } from '@/film/film'
|
| 5 |
import { buildSoundtrack } from '@/film/audio'
|
| 6 |
import { loadFonts } from '@/film/core'
|
| 7 |
|
|
@@ -30,7 +30,7 @@ export default function ExportPanel({ defaultAspect }: { defaultAspect: Aspect }
|
|
| 30 |
try {
|
| 31 |
setMsg('准备字体与音轨…')
|
| 32 |
await loadFonts(BASE)
|
| 33 |
-
const full = await buildSoundtrack(`${BASE}
|
| 34 |
// ?exportSeconds=N 仅用于自动化测试(只导出前 N 秒)
|
| 35 |
const lim = Number(new URLSearchParams(location.search).get('exportSeconds')) || TOTAL
|
| 36 |
const N = Math.round(Math.min(TOTAL, lim) * fps)
|
|
@@ -68,7 +68,7 @@ export default function ExportPanel({ defaultAspect }: { defaultAspect: Aspect }
|
|
| 68 |
await output.finalize()
|
| 69 |
const buf = (output.target as BufferTarget).buffer!
|
| 70 |
const blob = new Blob([buf], { type: 'video/mp4' })
|
| 71 |
-
const name = `
|
| 72 |
setFile({ url: URL.createObjectURL(blob), name, size: blob.size })
|
| 73 |
setProg(1)
|
| 74 |
setMsg(`完成:${vcodec.toUpperCase()} 视频${acodec ? ` + ${acodec.toUpperCase()} 音频` : '(此浏览器不支持音频编码,导出为无声视频)'}`)
|
|
|
|
| 1 |
import { useRef, useState } from 'react'
|
| 2 |
import { Download, Film, Loader2, X, Smartphone, Monitor } from 'lucide-react'
|
| 3 |
import { Output, Mp4OutputFormat, BufferTarget, CanvasSource, AudioBufferSource, QUALITY_HIGH, getFirstEncodableVideoCodec, getFirstEncodableAudioCodec } from 'mediabunny'
|
| 4 |
+
import { renderFrame, TOTAL, FILM } from '@/film/film'
|
| 5 |
import { buildSoundtrack } from '@/film/audio'
|
| 6 |
import { loadFonts } from '@/film/core'
|
| 7 |
|
|
|
|
| 30 |
try {
|
| 31 |
setMsg('准备字体与音轨…')
|
| 32 |
await loadFonts(BASE)
|
| 33 |
+
const full = await buildSoundtrack(`${BASE}${FILM.assets}voice.mp3`)
|
| 34 |
// ?exportSeconds=N 仅用于自动化测试(只导出前 N 秒)
|
| 35 |
const lim = Number(new URLSearchParams(location.search).get('exportSeconds')) || TOTAL
|
| 36 |
const N = Math.round(Math.min(TOTAL, lim) * fps)
|
|
|
|
| 68 |
await output.finalize()
|
| 69 |
const buf = (output.target as BufferTarget).buffer!
|
| 70 |
const blob = new Blob([buf], { type: 'video/mp4' })
|
| 71 |
+
const name = `${FILM.title}_${aspect === '16:9' ? '横屏' : '竖屏'}_${W}x${H}_${fps}fps.mp4`
|
| 72 |
setFile({ url: URL.createObjectURL(blob), name, size: blob.size })
|
| 73 |
setProg(1)
|
| 74 |
setMsg(`完成:${vcodec.toUpperCase()} 视频${acodec ? ` + ${acodec.toUpperCase()} 音频` : '(此浏览器不支持音频编码,导出为无声视频)'}`)
|
wolf-coming/web/src/components/LivePhotos.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
import { Download, PackageOpen, Apple } from 'lucide-react'
|
| 2 |
-
import {
|
| 3 |
import { PhotoCopy } from './PublishCopy'
|
| 4 |
|
| 5 |
const BASE = import.meta.env.BASE_URL
|
| 6 |
-
const L = (f: string) => `${BASE}
|
| 7 |
|
| 8 |
export default function LivePhotos() {
|
| 9 |
return (
|
|
@@ -11,13 +11,13 @@ export default function LivePhotos() {
|
|
| 11 |
<div className="flex flex-wrap items-center gap-3 rounded-md border border-[#d9cdb2] bg-[#fbf7ee] p-4 text-sm text-[#3a3128]">
|
| 12 |
<Apple className="h-5 w-5 shrink-0" />
|
| 13 |
<div className="min-w-[240px] flex-1 leading-relaxed">
|
| 14 |
-
|
| 15 |
在 Mac 上解压后,把同名的 JPG 和 MOV 一起拖进「照片」即合成为实况图,再同步到手机发小红书 / 抖音图文。
|
| 16 |
</div>
|
| 17 |
-
<a href={L('livephotos.zip')} download=
|
| 18 |
</div>
|
| 19 |
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
| 20 |
-
{
|
| 21 |
<figure key={l.id} className="overflow-hidden rounded-md border border-[#d9cdb2] bg-[#fbf7ee]">
|
| 22 |
<video src={L(`${l.id}.mp4`)} poster={L(`${l.id}.jpg`)} autoPlay muted loop playsInline className="block aspect-[3/4] w-full object-cover" />
|
| 23 |
<figcaption className="flex items-center justify-between gap-1 px-2 py-1.5 text-xs text-[#4a3f30]">
|
|
|
|
| 1 |
import { Download, PackageOpen, Apple } from 'lucide-react'
|
| 2 |
+
import { FILM } from '@/film/film'
|
| 3 |
import { PhotoCopy } from './PublishCopy'
|
| 4 |
|
| 5 |
const BASE = import.meta.env.BASE_URL
|
| 6 |
+
const L = (f: string) => `${BASE}${FILM.assets}live/${f}`
|
| 7 |
|
| 8 |
export default function LivePhotos() {
|
| 9 |
return (
|
|
|
|
| 11 |
<div className="flex flex-wrap items-center gap-3 rounded-md border border-[#d9cdb2] bg-[#fbf7ee] p-4 text-sm text-[#3a3128]">
|
| 12 |
<Apple className="h-5 w-5 shrink-0" />
|
| 13 |
<div className="min-w-[240px] flex-1 leading-relaxed">
|
| 14 |
+
{FILM.live.length} 组苹果实况照片(每组 <b>JPG + MOV</b>,3:4 · 1080×1440 · 3 秒 · 30fps),按 livecanvas 流程写入 Live Photo 配对信息(共享 content identifier + still-image-time),另附 .pvt 资源包。
|
| 15 |
在 Mac 上解压后,把同名的 JPG 和 MOV 一起拖进「照片」即合成为实况图,再同步到手机发小红书 / 抖音图文。
|
| 16 |
</div>
|
| 17 |
+
<a href={L('livephotos.zip')} download={`${FILM.title}_水墨实况图_${FILM.live.length}组.zip`} className="inline-flex items-center gap-2 rounded bg-[#b8322a] px-4 py-2 text-[#f6efdf] shadow hover:bg-[#a02a23]"><PackageOpen className="h-4 w-4" />下载实况图压缩包</a>
|
| 18 |
</div>
|
| 19 |
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
| 20 |
+
{FILM.live.map((l) => (
|
| 21 |
<figure key={l.id} className="overflow-hidden rounded-md border border-[#d9cdb2] bg-[#fbf7ee]">
|
| 22 |
<video src={L(`${l.id}.mp4`)} poster={L(`${l.id}.jpg`)} autoPlay muted loop playsInline className="block aspect-[3/4] w-full object-cover" />
|
| 23 |
<figcaption className="flex items-center justify-between gap-1 px-2 py-1.5 text-xs text-[#4a3f30]">
|
wolf-coming/web/src/components/Making.tsx
CHANGED
|
@@ -1,22 +1,16 @@
|
|
| 1 |
-
import { TL, chapters } from '@/film/film'
|
| 2 |
-
import qa from '@/film/qa.json'
|
| 3 |
|
| 4 |
const BASE = import.meta.env.BASE_URL
|
| 5 |
-
type QA = { status: string; generated?: string; video?: string; meta?: { w: number; h: number; fps: number; dur: number }; results: { level: string; check: string; msg: string }[]; verdict?: string }
|
| 6 |
-
const Q = qa as QA
|
| 7 |
const fmt = (t: number) => `${Math.floor(t / 60)}:${String(Math.floor(t % 60)).padStart(2, '0')}`
|
| 8 |
const col: Record<string, string> = { PASS: 'bg-[#e3ecd9] text-[#2f5d3a]', WARN: 'bg-[#f6e7c4] text-[#8a5a10]', FAIL: 'bg-[#f5d9d5] text-[#b8322a]', INFO: 'bg-[#e6e0d2] text-[#5a4f3c]' }
|
| 9 |
|
| 10 |
export default function Making() {
|
| 11 |
const chs = chapters()
|
|
|
|
| 12 |
return (
|
| 13 |
<div className="flex flex-col gap-6">
|
| 14 |
<div className="grid gap-4 md:grid-cols-3">
|
| 15 |
-
{[
|
| 16 |
-
['画面', '全部由代码逐帧绘制:程序生成宣纸纹理,变宽毛笔笔触带起笔、收笔和飞白,淡彩晕染有水彩积边,再盖朱砂印章。同一只「鸟」可以从小黄鸭一路长成天鹅。每帧只由时间决定,可逐帧复现。'],
|
| 17 |
-
['声音', '中文神经网络语音旁白;原创五声音阶配乐(古筝拨弦、竹笛、铺底、铃),随四季换情绪;破壳、鸭叫、枪声、冰裂、振翅、拍手等音效都是程序合成。人声出现时配乐自动压低,整体响度 −14 LUFS。'],
|
| 18 |
-
['流程', '按 vibe-video 流程:分镜 storyboard → 配音生成时间轴 → 接触表 / 静帧检查 → 确定性自检 → 全片渲染 → 混音 → vibe qa 质检 → 封面与发布包;实况图按 livecanvas 流程配对封装。'],
|
| 19 |
-
].map(([h, t]) => (
|
| 20 |
<div key={h} className="rounded-md border border-[#d9cdb2] bg-[#fbf7ee] p-4">
|
| 21 |
<div className="mb-1 font-brush text-xl">{h}</div>
|
| 22 |
<p className="text-sm leading-relaxed text-[#4a3f30]">{t}</p>
|
|
@@ -63,8 +57,8 @@ export default function Making() {
|
|
| 63 |
)}
|
| 64 |
{Q.status !== 'pending' && (
|
| 65 |
<div className="grid gap-3 md:grid-cols-2">
|
| 66 |
-
<figure><img src={`${BASE}
|
| 67 |
-
<figure><img src={`${BASE}
|
| 68 |
</div>
|
| 69 |
)}
|
| 70 |
</div>
|
|
|
|
| 1 |
+
import { TL, chapters, FILM } from '@/film/film'
|
|
|
|
| 2 |
|
| 3 |
const BASE = import.meta.env.BASE_URL
|
|
|
|
|
|
|
| 4 |
const fmt = (t: number) => `${Math.floor(t / 60)}:${String(Math.floor(t % 60)).padStart(2, '0')}`
|
| 5 |
const col: Record<string, string> = { PASS: 'bg-[#e3ecd9] text-[#2f5d3a]', WARN: 'bg-[#f6e7c4] text-[#8a5a10]', FAIL: 'bg-[#f5d9d5] text-[#b8322a]', INFO: 'bg-[#e6e0d2] text-[#5a4f3c]' }
|
| 6 |
|
| 7 |
export default function Making() {
|
| 8 |
const chs = chapters()
|
| 9 |
+
const Q = FILM.qa
|
| 10 |
return (
|
| 11 |
<div className="flex flex-col gap-6">
|
| 12 |
<div className="grid gap-4 md:grid-cols-3">
|
| 13 |
+
{FILM.making.map(([h, t]) => (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
<div key={h} className="rounded-md border border-[#d9cdb2] bg-[#fbf7ee] p-4">
|
| 15 |
<div className="mb-1 font-brush text-xl">{h}</div>
|
| 16 |
<p className="text-sm leading-relaxed text-[#4a3f30]">{t}</p>
|
|
|
|
| 57 |
)}
|
| 58 |
{Q.status !== 'pending' && (
|
| 59 |
<div className="grid gap-3 md:grid-cols-2">
|
| 60 |
+
<figure><img src={`${BASE}${FILM.assets}qa/sheet_landscape.jpg`} alt="横屏接触表" className="w-full rounded border border-[#d9cdb2]" /><figcaption className="mt-1 text-xs text-[#8a7a5a]">横屏接触表(每 2 秒一格)</figcaption></figure>
|
| 61 |
+
<figure><img src={`${BASE}${FILM.assets}qa/sheet_portrait.jpg`} alt="竖屏接触表" className="w-full rounded border border-[#d9cdb2]" /><figcaption className="mt-1 text-xs text-[#8a7a5a]">竖屏接触表</figcaption></figure>
|
| 62 |
</div>
|
| 63 |
)}
|
| 64 |
</div>
|
wolf-coming/web/src/components/Player.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { useCallback, useEffect, useRef, useState } from 'react'
|
| 2 |
import { Play, Pause, Maximize2, Subtitles, Smartphone, Monitor, RotateCcw, Volume2, VolumeX } from 'lucide-react'
|
| 3 |
-
import { renderFrame, TOTAL, chapters } from '@/film/film'
|
| 4 |
import { buildSoundtrack } from '@/film/audio'
|
| 5 |
import { loadFonts } from '@/film/core'
|
| 6 |
|
|
@@ -31,7 +31,7 @@ export default function Player({ orientation, onOrientation }: { orientation: Or
|
|
| 31 |
|
| 32 |
useEffect(() => {
|
| 33 |
loadFonts(BASE).then(() => setFontsOk(true))
|
| 34 |
-
buildSoundtrack(`${BASE}
|
| 35 |
}, [])
|
| 36 |
|
| 37 |
const now = useCallback(() => (playingRef.current && ac.current ? ac.current.currentTime - startAt.current : offset.current), [])
|
|
|
|
| 1 |
import { useCallback, useEffect, useRef, useState } from 'react'
|
| 2 |
import { Play, Pause, Maximize2, Subtitles, Smartphone, Monitor, RotateCcw, Volume2, VolumeX } from 'lucide-react'
|
| 3 |
+
import { renderFrame, TOTAL, chapters, FILM } from '@/film/film'
|
| 4 |
import { buildSoundtrack } from '@/film/audio'
|
| 5 |
import { loadFonts } from '@/film/core'
|
| 6 |
|
|
|
|
| 31 |
|
| 32 |
useEffect(() => {
|
| 33 |
loadFonts(BASE).then(() => setFontsOk(true))
|
| 34 |
+
buildSoundtrack(`${BASE}${FILM.assets}voice.mp3`, setStage).then((b) => { buf.current = b; setReady(true) }).catch((e) => setStage('音轨合成失败:' + (e?.message ?? e)))
|
| 35 |
}, [])
|
| 36 |
|
| 37 |
const now = useCallback(() => (playingRef.current && ac.current ? ac.current.currentTime - startAt.current : offset.current), [])
|
wolf-coming/web/src/components/PublishCopy.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { useState } from 'react'
|
| 2 |
import { Copy, Check } from 'lucide-react'
|
| 3 |
-
import {
|
|
|
|
| 4 |
|
| 5 |
export function useCopy() {
|
| 6 |
const [done, setDone] = useState<string | null>(null)
|
|
@@ -73,10 +74,10 @@ export function PostCard({ p, c, pre }: { p: Post; c: ReturnType<typeof useCopy>
|
|
| 73 |
|
| 74 |
export default function PublishCopy() {
|
| 75 |
const c = useCopy()
|
| 76 |
-
return <div className="grid gap-4 md:grid-cols-2">{
|
| 77 |
}
|
| 78 |
|
| 79 |
export function PhotoCopy() {
|
| 80 |
const c = useCopy()
|
| 81 |
-
return <div className="grid gap-4 md:grid-cols-2">{
|
| 82 |
}
|
|
|
|
| 1 |
import { useState } from 'react'
|
| 2 |
import { Copy, Check } from 'lucide-react'
|
| 3 |
+
import { FILM } from '@/film/film'
|
| 4 |
+
const len = (s: string) => [...s].length
|
| 5 |
|
| 6 |
export function useCopy() {
|
| 7 |
const [done, setDone] = useState<string | null>(null)
|
|
|
|
| 74 |
|
| 75 |
export default function PublishCopy() {
|
| 76 |
const c = useCopy()
|
| 77 |
+
return <div className="grid gap-4 md:grid-cols-2">{FILM.platforms.map((p) => <PostCard key={p.id} p={p} c={c} pre="v-" />)}</div>
|
| 78 |
}
|
| 79 |
|
| 80 |
export function PhotoCopy() {
|
| 81 |
const c = useCopy()
|
| 82 |
+
return <div className="grid gap-4 md:grid-cols-2">{FILM.photoPosts.map((p) => <PostCard key={p.id} p={{ ...p, color: p.id === 'xhs' ? '#ff2442' : '#111111' }} c={c} pre="p-" />)}</div>
|
| 83 |
}
|
wolf-coming/web/src/film/audio.ts
CHANGED
|
@@ -1,24 +1,40 @@
|
|
| 1 |
// 声音:原创五声音阶配乐(古筝拨弦 + 竹笛 + 铺底 + 铃)+ 程序音效 + 旁白,离线混成一条立体声。
|
| 2 |
// 完全确定:同一输入 → 同一音轨。网页播放、MP4 导出、质检用的是同一条音轨。
|
| 3 |
-
import { TL, TOTAL } from './film'
|
| 4 |
import { rng } from './core'
|
| 5 |
-
import { CUE } from './cues'
|
| 6 |
|
| 7 |
-
const SR = 44100
|
| 8 |
type AC = OfflineAudioContext
|
| 9 |
-
const midi = (m: number) => 440 * Math.pow(2, (m - 69) / 12)
|
| 10 |
// 五声音阶(G 宫 / E 羽 共用)
|
| 11 |
-
const N: Record<string, number> = { E2: 40, G2: 43, A2: 45, B2: 47, D3: 50, E3: 52, G3: 55, A3: 57, B3: 59, D4: 62, E4: 64, G4: 67, A4: 69, B4: 71, D5: 74, E5: 76, G5: 79, A5: 81, B5: 83, D6: 86, E6: 88 }
|
| 12 |
-
const sceneStart = (id: string) => TL.scenes.find((s) => s.id === id)?.start ?? 0
|
| 13 |
-
const sceneDur = (id: string) => TL.scenes.find((s) => s.id === id)?.dur ?? 0
|
| 14 |
|
| 15 |
-
class Mixer {
|
| 16 |
ctx: AC
|
| 17 |
music: GainNode; sfx: GainNode; voice: GainNode; rev: ConvolverNode; revIn: GainNode; master: GainNode
|
| 18 |
noise: AudioBuffer
|
| 19 |
ksCache = new Map<number, AudioBuffer>()
|
| 20 |
r = rng(4242)
|
| 21 |
q: { t: number; fn: () => void }[] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
constructor(ctx: AC) {
|
| 23 |
this.ctx = ctx
|
| 24 |
// 延迟建节点:只在需要前 ~1 秒才真正创建(离线渲染快很多)
|
|
@@ -254,206 +270,12 @@ class Mixer {
|
|
| 254 |
lap(t0: number, t1: number, vel = 0.05) { for (let t = t0; t < t1; t += 0.6 + this.r() * 0.9) this.noiseHit(t, 0.5, 'bandpass', 700 + this.r() * 300, 300, vel, this.r() * 1.2 - 0.6, 1.5, 0.3) }
|
| 255 |
}
|
| 256 |
|
| 257 |
-
|
| 258 |
-
function score(m: Mixer) {
|
| 259 |
-
const B = 60 / 72 // 每拍秒数
|
| 260 |
-
const S = sceneStart, D = sceneDur
|
| 261 |
-
const r = rng(808)
|
| 262 |
-
const arp = (t0: number, t1: number, chords: number[][], step: number, vel: number, pattern = [0, 1, 2, 1]) => {
|
| 263 |
-
let i = 0
|
| 264 |
-
for (let t = t0; t < t1 - 0.05; t += step, i++) {
|
| 265 |
-
const ch = chords[Math.floor((t - t0) / (step * 8)) % chords.length]
|
| 266 |
-
m.pluck(t, ch[pattern[i % pattern.length] % ch.length], vel * (i % 4 === 0 ? 1 : 0.7), (i % 2 ? 0.25 : -0.25), { dur: 1.8 })
|
| 267 |
-
}
|
| 268 |
-
}
|
| 269 |
-
const G = [N.G3, N.D4, N.B4], Em = [N.E3, N.B3, N.G4], Dm = [N.D3, N.A3, N.D4], Am = [N.A2, N.E3, N.A3], Gh = [N.G4, N.B4, N.D5]
|
| 270 |
-
// 片头
|
| 271 |
-
let t = S('title')
|
| 272 |
-
m.gliss(t + 0.35, [N.G3, N.A3, N.B3, N.D4, N.E4, N.G4, N.A4, N.B4, N.D5], 0.07, 0.28)
|
| 273 |
-
m.pad(t + 0.5, S('summer') + 1, [N.G2, N.D3], 0.05)
|
| 274 |
-
m.melody(t + 2.2, B, [['D5', 1], ['E5', 0.5], ['G5', 0.5], ['E5', 1], ['D5', 1.5]], 0.17)
|
| 275 |
-
// 夏
|
| 276 |
-
t = S('summer')
|
| 277 |
-
m.pad(t, S('hatch') + 0.5, [N.G2, N.D3, N.B3], 0.045)
|
| 278 |
-
arp(t + 0.3, S('hatch'), [G, Em, G, Dm], B / 2, 0.2)
|
| 279 |
-
m.melody(t + 1.2, B, [['D5', 1], ['E5', 0.5], ['G5', 0.5], ['E5', 1], ['D5', 1], ['B4', 1], ['D5', 0.5], ['B4', 0.5], ['A4', 2], ['G4', 1], ['A4', 0.5], ['B4', 0.5], ['D5', 1], ['E5', 0.5], ['D5', 0.5], ['B4', 1], ['A4', 0.5], ['G4', 0.5], ['G4', 2]], 0.13)
|
| 280 |
-
// 破壳:俏皮的跳音 + 木鱼
|
| 281 |
-
t = S('hatch')
|
| 282 |
-
for (let i = 0; i < Math.floor(D('hatch') / B); i++) { if (i % 2 === 0) m.block(t + i * B, 0.07, 1100, -0.3); m.pluck(t + i * B + B / 2, [N.G4, N.B4, N.D5, N.E5][i % 4], 0.13, 0.3, { dur: 0.5, bright: 0.9 }) }
|
| 283 |
-
m.pad(t, S('bigegg'), [N.G2, N.D3], 0.035)
|
| 284 |
-
// 大蛋:悬念颤音 → 揭晓 → 「咦?」
|
| 285 |
-
t = S('bigegg')
|
| 286 |
-
const BE = CUE.bigegg
|
| 287 |
-
m.pad(t, t + BE.open, [N.E2, N.B2], 0.05, 500)
|
| 288 |
-
for (let x = t + 0.6; x < t + BE.open - 0.05; x += 0.09) m.pluck(x, N.E3, 0.06 + 0.1 * (x - t) / BE.open, 0, { dur: 0.3, send: 0.1 })
|
| 289 |
-
m.pluck(t + BE.surprise, N.D4, 0.3, 0, { bend: -2 }); m.pluck(t + BE.surprise + 0.25, N.A3, 0.3, 0, { bend: -2 })
|
| 290 |
-
m.pluck(t + BE.question + 0.05, N.B4, 0.22, 0.2, { bend: 2 })
|
| 291 |
-
arp(t + BE.walk, S('bullied'), [Em, G], B / 2, 0.12)
|
| 292 |
-
// 被嘲笑:E 羽调式,慢
|
| 293 |
-
t = S('bullied')
|
| 294 |
-
m.pad(t, S('leave') + 0.5, [N.E2, N.B2, N.G3], 0.05, 700)
|
| 295 |
-
for (let i = 0; i < 12; i++) m.pluck(t + 0.3 + i * B, [N.E4, N.D4, N.B3, N.A3, N.G3, N.A3, N.B3, N.E3][i % 8], 0.16, -0.2, { dur: 2, bend: i % 4 === 3 ? -1 : 0 })
|
| 296 |
-
// 离家:竹笛哀歌
|
| 297 |
-
t = S('leave')
|
| 298 |
-
m.pad(t, S('marsh') + 0.3, [N.E2, N.B2], 0.045, 600)
|
| 299 |
-
m.melody(t + 0.4, B * 1.1, [['E5', 1.5], ['D5', 0.5], ['B4', 1], ['A4', 1], ['G4', 1], ['A4', 0.5], ['B4', 0.5], ['E4', 2]], 0.13)
|
| 300 |
-
// 沼泽:低音颤奏 → 枪声后寂静 → 心跳
|
| 301 |
-
t = S('marsh')
|
| 302 |
-
for (let x = t + 0.2; x < t + CUE.marsh.shots[0] - 0.02; x += 0.125) m.pluck(x, (Math.floor((x - t) / 0.5) % 2) ? N.B2 : N.E2, 0.17, 0, { dur: 0.25, send: 0.05 })
|
| 303 |
-
m.pad(t + CUE.marsh.hide, S('autumn') + 0.5, [N.E2], 0.04, 400)
|
| 304 |
-
for (let x = t + CUE.marsh.hide + 0.6; x < t + 13; x += 0.95) m.heart(x)
|
| 305 |
-
// 秋:向往(E 羽 → G 宫的过渡)
|
| 306 |
-
t = S('autumn')
|
| 307 |
-
m.pad(t, S('winter') + 0.5, [N.E2, N.B2, N.G3], 0.04, 800)
|
| 308 |
-
arp(t + 0.3, S('winter'), [Em, Am, G, Em], B * 0.75, 0.13)
|
| 309 |
-
m.bell(t + CUE.autumn.swans + 0.6, N.B5, 0.07, 0.4); m.bell(t + CUE.autumn.swans + 3, N.E6, 0.06, -0.3)
|
| 310 |
-
m.melody(t + CUE.autumn.swans + 1.4, B, [['B4', 1.5], ['D5', 0.5], ['E5', 1], ['G5', 1], ['E5', 2], ['D5', 1], ['B4', 1], ['A4', 2]], 0.11, 0)
|
| 311 |
-
// 冬:几乎只剩风与孤铃
|
| 312 |
-
t = S('winter')
|
| 313 |
-
m.pad(t, S('spring') + 0.5, [N.E2, N.B2], 0.035, 350)
|
| 314 |
-
for (let i = 0; i < 6; i++) m.bell(t + 1 + i * 2.1, [N.E5, N.B4, N.D5, N.A4, N.B4, N.E5][i], 0.05, (i % 2 ? 0.4 : -0.4))
|
| 315 |
-
// 春:回到 G 宫,逐渐加密
|
| 316 |
-
t = S('spring')
|
| 317 |
-
m.bell(t + 0.15, N.G5, 0.1, 0); m.bell(t + 0.4, N.D6, 0.07, 0.3)
|
| 318 |
-
m.pad(t, S('reflection') + 0.5, [N.G2, N.D3, N.B3], 0.045)
|
| 319 |
-
const SP = CUE.spring
|
| 320 |
-
arp(t + 0.5, t + SP.flapHard, [G, Em], B / 2, 0.15)
|
| 321 |
-
arp(t + SP.flapHard, S('reflection'), [G, Dm, Em, G], B / 4, 0.14, [0, 1, 2, 1, 2, 1])
|
| 322 |
-
m.gliss(t + SP.lift - 0.05, [N.G3, N.A3, N.B3, N.D4, N.E4, N.G4, N.A4, N.B4, N.D5, N.E5, N.G5], 0.05, 0.25)
|
| 323 |
-
m.melody(t + SP.lift + 0.4, B, [['D5', 1], ['E5', 0.5], ['G5', 0.5], ['A5', 1], ['G5', 1], ['E5', 1], ['G5', 0.5], ['E5', 0.5], ['D5', 2]], 0.13)
|
| 324 |
-
// 倒影:揭晓
|
| 325 |
-
t = S('reflection')
|
| 326 |
-
const RF = CUE.reflection
|
| 327 |
-
m.pad(t, t + RF.reveal, [N.E2, N.B2, N.E3], 0.04, 600)
|
| 328 |
-
for (let i = 0; i < 10; i++) m.pluck(t + 0.8 + i * B * 1.05, [N.B3, N.D4, N.E4, N.D4, N.B3, N.A3, N.B3, N.D4, N.E4, N.G4][i], 0.12, 0, { dur: 2 })
|
| 329 |
-
m.bell(t + RF.mirror, N.E5, 0.06, 0); m.bell(t + RF.mirror + 0.3, N.B5, 0.04, 0.3)
|
| 330 |
-
m.gliss(t + RF.reveal - 0.05, [N.G3, N.B3, N.D4, N.G4, N.B4, N.D5, N.G5, N.B5, N.D6], 0.06, 0.3)
|
| 331 |
-
;[N.G5, N.B5, N.D6, N.G5].forEach((x, i) => m.bell(t + RF.reveal + i * 0.18, x, 0.08, i % 2 ? 0.3 : -0.3))
|
| 332 |
-
m.pad(t + RF.reveal, S('accepted') + 0.5, [N.G2, N.D3, N.B3, N.D4], 0.05, 1200)
|
| 333 |
-
m.melody(t + RF.reveal + 1.2, B, [['G5', 1.5], ['E5', 0.5], ['D5', 1], ['B4', 1], ['D5', 2]], 0.14)
|
| 334 |
-
// 接纳:温暖
|
| 335 |
-
t = S('accepted')
|
| 336 |
-
arp(t + 0.2, S('fly'), [G, Em, Dm, G], B / 2, 0.15)
|
| 337 |
-
m.pad(t, S('fly') + 0.3, [N.G2, N.D3], 0.04)
|
| 338 |
-
m.melody(t + 0.8, B, [['G4', 1], ['A4', 0.5], ['B4', 0.5], ['D5', 1], ['E5', 0.5], ['D5', 0.5], ['B4', 1], ['A4', 0.5], ['G4', 0.5], ['G4', 2]], 0.12)
|
| 339 |
-
// 高飞:高潮
|
| 340 |
-
t = S('fly')
|
| 341 |
-
m.pad(t, S('ending') + 1, [N.G2, N.D3, N.G3, N.B3], 0.06, 1600)
|
| 342 |
-
arp(t, S('ending'), [Gh, G, Em, Gh], B / 4, 0.16, [0, 1, 2, 1, 2, 1, 0, 2])
|
| 343 |
-
for (let i = 0; i < Math.floor(D('fly') / B); i++) m.block(t + i * B, 0.08, 800, 0)
|
| 344 |
-
m.melody(t + 0.2, B, [['D5', 1], ['E5', 0.5], ['G5', 0.5], ['A5', 1], ['G5', 1], ['E5', 1], ['G5', 0.5], ['A5', 0.5], ['B5', 2]], 0.15, 0)
|
| 345 |
-
// 尾声
|
| 346 |
-
t = S('ending')
|
| 347 |
-
m.pad(t, TOTAL, [N.G2, N.D3, N.B3], 0.045)
|
| 348 |
-
const EN = CUE.ending.end
|
| 349 |
-
arp(t + 0.2, t + EN - 0.2, [G, Em, G, Dm], B * 0.75, 0.12)
|
| 350 |
-
m.gliss(t + EN, [N.G3, N.B3, N.D4, N.G4, N.B4, N.D5, N.G5], 0.08, 0.22)
|
| 351 |
-
m.bell(t + EN + 0.1, N.G5, 0.08); m.bell(t + EN + 0.35, N.D6, 0.05, 0.3)
|
| 352 |
-
m.pluck(t + EN + 0.6, N.G3, 0.2, 0, { dur: 2 }); m.pluck(t + EN + 0.6, N.D4, 0.15, 0, { dur: 2 }); m.pluck(t + EN + 0.6, N.G4, 0.12, 0, { dur: 2 })
|
| 353 |
-
void r
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
function effects(m: Mixer) {
|
| 357 |
-
const S = sceneStart, D = sceneDur
|
| 358 |
-
const r = m.r
|
| 359 |
-
let t = S('title')
|
| 360 |
-
const TT = CUE.title
|
| 361 |
-
m.drop(t + TT.drop); m.swish(t + TT.write); m.swish(t + TT.write + 0.45); m.swish(t + TT.write + 0.9); m.thump(t + TT.seal + 0.01, 0.45)
|
| 362 |
-
for (let i = 0; i < 6; i++) m.peep(t + 2.6 + i * 0.65, 0.04, 0.3)
|
| 363 |
-
// 夏:鸟鸣、微风
|
| 364 |
-
t = S('summer')
|
| 365 |
-
m.wind(t, D('summer'), 0.025)
|
| 366 |
-
for (let x = t + 0.6; x < t + D('summer'); x += 1.1 + r() * 1.6) m.chirp(x, 0.035, r() * 1.4 - 0.7)
|
| 367 |
-
// 破壳
|
| 368 |
-
t = S('hatch')
|
| 369 |
-
for (let k = 0; k < 4; k++) {
|
| 370 |
-
const tc = t + 0.4 + k * 0.75
|
| 371 |
-
m.crack(tc + 0.05, 0.22, -0.3 + k * 0.2); m.crack(tc + 0.25, 0.18, -0.3 + k * 0.2)
|
| 372 |
-
m.noiseHit(tc + 0.55, 0.08, 'bandpass', 1800, 900, 0.18, -0.3 + k * 0.2, 2, 0.1)
|
| 373 |
-
m.peep(tc + 0.75, 0.1, -0.3 + k * 0.2)
|
| 374 |
-
for (let p = 1; p < 5; p++) { const pt = tc + 1 + 2.3 * (p - 1); if (pt - t < D('hatch')) m.peep(pt + 0.05, 0.07, -0.3 + k * 0.2) }
|
| 375 |
-
m.noiseHit(tc + 3.0, 0.05, 'lowpass', 600, 200, 0.12, -0.3 + k * 0.2, 0.7, 0.05)
|
| 376 |
-
}
|
| 377 |
-
// 大蛋
|
| 378 |
-
t = S('bigegg')
|
| 379 |
-
const BE = CUE.bigegg
|
| 380 |
-
for (let x = t + 0.5; x < t + BE.crackFrom; x += Math.max(0.12, 0.5 - (x - t) * 0.12)) m.block(x, 0.05 + (x - t) * 0.02, 420, 0)
|
| 381 |
-
m.crack(t + BE.crackFrom + 0.05, 0.2); m.crack(t + BE.crackFrom + 0.4, 0.22)
|
| 382 |
-
m.crack(t + BE.open, 0.35, 0, true)
|
| 383 |
-
m.noiseHit(t + BE.surprise, 0.25, 'bandpass', 2400, 1200, 0.06, -0.4, 1.2, 0.2)
|
| 384 |
-
m.peep(t + BE.surprise + 0.1, 0.08, -0.4); m.peep(t + BE.surprise + 0.2, 0.08, -0.5)
|
| 385 |
-
for (let x = t + BE.walk + 0.1; x < t + BE.walk + 2.6; x += 0.6) m.noiseHit(x, 0.05, 'lowpass', 600, 200, 0.1, 0, 0.7, 0.05)
|
| 386 |
-
// 被嘲笑
|
| 387 |
-
t = S('bullied')
|
| 388 |
-
const BU = CUE.bullied
|
| 389 |
-
for (let x = t + BU.laugh1[0] + 0.1; x < t + BU.laugh1[1] - 0.2; x += 0.35 + r() * 0.3) m.quack(x, 0.09, -0.5 + r() * 0.3, 1.25 + r() * 0.2)
|
| 390 |
-
for (let x = t + BU.laugh2[0] + 0.1; x < t + BU.laugh2[1] - 0.2; x += 0.4 + r() * 0.35) m.quack(x, 0.07, -0.5 + r() * 0.3, 1.25 + r() * 0.2)
|
| 391 |
-
// 公鸡登场:扑翅 + 啄
|
| 392 |
-
m.flap(t + BU.roosterIn, 6, 9, 0.12, 0.5)
|
| 393 |
-
m.tone(t + BU.roosterIn + 0.2, 600, 900, 0.25, 0.08, 'sawtooth', 0.5, 0.3); m.tone(t + BU.roosterIn + 0.45, 900, 500, 0.5, 0.07, 'sawtooth', 0.5, 0.4)
|
| 394 |
-
for (const p of BU.pecks) { m.block(t + p, 0.25, 1500, 0.2); m.flap(t + p - 0.15, 2, 10, 0.08, 0.3) }
|
| 395 |
-
m.swish(t + BU.shoo, 0.07)
|
| 396 |
-
// 离家
|
| 397 |
-
t = S('leave')
|
| 398 |
-
m.wind(t, D('leave') + 1, 0.07, -0.2)
|
| 399 |
-
for (let x = t + CUE.leave.walk + 0.2; x < t + D('leave'); x += 0.62) m.noiseHit(x, 0.04, 'lowpass', 500, 200, 0.05, 0, 0.7, 0.05)
|
| 400 |
-
// 沼泽
|
| 401 |
-
t = S('marsh')
|
| 402 |
-
m.lap(t, t + D('marsh'), 0.035)
|
| 403 |
-
const MA = CUE.marsh
|
| 404 |
-
for (let x = t + MA.laugh[0] + 0.1; x < t + MA.laugh[1] - 0.2; x += 0.3 + r() * 0.3) m.quack(x, 0.1, 0.4 + r() * 0.3, 0.95 + r() * 0.15)
|
| 405 |
-
m.shot(t + MA.shots[0], 0.6); m.shot(t + MA.shots[1], 0.45)
|
| 406 |
-
m.flap(t + MA.scatter, 14, 12, 0.13, 0.4)
|
| 407 |
-
for (let i = 0; i < 6; i++) m.quack(t + MA.scatter + 0.05 + i * 0.12, 0.08, 0.5, 1.1)
|
| 408 |
-
m.splash(t + MA.hide + 0.2, 0.12)
|
| 409 |
-
// 秋
|
| 410 |
-
t = S('autumn')
|
| 411 |
-
m.wind(t, D('autumn'), 0.03, 0.2)
|
| 412 |
-
const AU = CUE.autumn.swans
|
| 413 |
-
m.swanCall(t + AU + 0.8, 0.06, 0.5); m.swanCall(t + AU + 3.0, 0.07, 0.1); m.swanCall(t + AU + 5.6, 0.05, -0.5)
|
| 414 |
-
m.flap(t + AU + 1.5, 12, 1.4, 0.04, 0.2)
|
| 415 |
-
// 冬
|
| 416 |
-
t = S('winter')
|
| 417 |
-
m.wind(t, D('winter') + 0.8, 0.12, 0)
|
| 418 |
-
m.wind(t + 2, D('winter') - 2, 0.06, 0.6)
|
| 419 |
-
m.crack(t + CUE.winter.ice, 0.2, 0, true); m.noiseHit(t + CUE.winter.ice + 0.05, 0.9, 'bandpass', 300, 120, 0.12, 0, 4, 0.5)
|
| 420 |
-
// 春
|
| 421 |
-
t = S('spring')
|
| 422 |
-
for (let x = t + 0.4; x < t + D('spring'); x += 0.7 + r() * 1.1) m.chirp(x, 0.04, r() * 1.4 - 0.7)
|
| 423 |
-
const SP = CUE.spring
|
| 424 |
-
m.flap(t + SP.spread, 2, 2, 0.1, 0)
|
| 425 |
-
m.flap(t + SP.spread + 0.9, Math.floor((SP.flapHard - SP.spread - 0.9) * 0.83), 0.83, 0.08, 0)
|
| 426 |
-
m.flap(t + SP.flapHard, Math.floor((SP.lift - SP.flapHard) * 1.6), 1.6, 0.13, 0); m.flap(t + SP.lift, 6, 1.46, 0.1, 0.2)
|
| 427 |
-
m.noiseHit(t + SP.lift - 0.1, 1.2, 'bandpass', 400, 2400, 0.12, 0, 1, 0.4)
|
| 428 |
-
m.pluck(t + SP.question, N.B4, 0.2, 0, { bend: 2 })
|
| 429 |
-
// 倒影
|
| 430 |
-
t = S('reflection')
|
| 431 |
-
const RF = CUE.reflection
|
| 432 |
-
m.flap(t, 5, 7, 0.1, -0.2)
|
| 433 |
-
m.splash(t + RF.land, 0.3)
|
| 434 |
-
m.lap(t + 1.2, t + D('reflection'), 0.03)
|
| 435 |
-
m.bell(t + RF.reveal, 91, 0.06, 0, m.sfx)
|
| 436 |
-
// 接纳
|
| 437 |
-
t = S('accepted')
|
| 438 |
-
m.lap(t, t + D('accepted'), 0.03)
|
| 439 |
-
const CH = CUE.accepted.cheer
|
| 440 |
-
for (let x = t + CH + 0.1; x < t + CH + 4.5; x += 0.11 + r() * 0.08) m.clap(x, 0.06 * (1 - (x - t - CH) / 5), -0.55 + r() * 0.2)
|
| 441 |
-
for (let i = 0; i < 4; i++) m.chirp(t + CH + 0.4 + i * 1.3, 0.03, 0.4)
|
| 442 |
-
// 高飞
|
| 443 |
-
t = S('fly')
|
| 444 |
-
m.wind(t, D('fly') + 1, 0.07, 0)
|
| 445 |
-
m.flap(t + 0.3, Math.floor(D('fly') * 3.2 / 2 / Math.PI * 2), 3.2 / Math.PI, 0.09, 0)
|
| 446 |
-
m.noiseHit(t + 0.9, 1.6, 'bandpass', 300, 2000, 0.1, 0, 1, 0.5)
|
| 447 |
-
// 尾声
|
| 448 |
-
t = S('ending')
|
| 449 |
-
m.lap(t, t + D('ending'), 0.025)
|
| 450 |
-
m.thump(t + CUE.ending.end + 0.12, 0.4)
|
| 451 |
-
}
|
| 452 |
-
|
| 453 |
-
let cached: Promise<AudioBuffer> | null = null
|
| 454 |
export function buildSoundtrack(voiceUrl: string, onStage?: (s: string) => void): Promise<AudioBuffer> {
|
| 455 |
-
|
| 456 |
-
|
|
|
|
|
|
|
| 457 |
const len = Math.ceil(SR * (TOTAL + 0.3))
|
| 458 |
const ctx = new OfflineAudioContext(2, len, SR)
|
| 459 |
onStage?.('加载旁白…')
|
|
@@ -464,17 +286,19 @@ export function buildSoundtrack(voiceUrl: string, onStage?: (s: string) => void)
|
|
| 464 |
} catch (e) { console.warn('voice load failed', e) }
|
| 465 |
onStage?.('合成配乐与音效…')
|
| 466 |
const m = new Mixer(ctx)
|
|
|
|
| 467 |
// 人声压低 BGM(sidechain 式):按时间轴里的配音区间自动化
|
| 468 |
const mg = m.music.gain
|
| 469 |
mg.setValueAtTime(0.9, 0)
|
| 470 |
for (const s of TL.scenes) {
|
| 471 |
-
|
| 472 |
-
const a
|
| 473 |
-
|
| 474 |
-
|
|
|
|
| 475 |
}
|
| 476 |
-
score(m)
|
| 477 |
-
effects(m)
|
| 478 |
m.flushPlucks()
|
| 479 |
m.q.sort((a, b) => a.t - b.t)
|
| 480 |
let qi = 0
|
|
@@ -497,7 +321,8 @@ export function buildSoundtrack(voiceUrl: string, onStage?: (s: string) => void)
|
|
| 497 |
onStage?.('完成')
|
| 498 |
return out
|
| 499 |
})()
|
| 500 |
-
|
|
|
|
| 501 |
return cached
|
| 502 |
}
|
| 503 |
|
|
|
|
| 1 |
// 声音:原创五声音阶配乐(古筝拨弦 + 竹笛 + 铺底 + 铃)+ 程序音效 + 旁白,离线混成一条立体声。
|
| 2 |
// 完全确定:同一输入 → 同一音轨。网页播放、MP4 导出、质检用的是同一条音轨。
|
| 3 |
+
import { TL, TOTAL, FILM } from './film'
|
| 4 |
import { rng } from './core'
|
|
|
|
| 5 |
|
| 6 |
+
export const SR = 44100
|
| 7 |
type AC = OfflineAudioContext
|
| 8 |
+
export const midi = (m: number) => 440 * Math.pow(2, (m - 69) / 12)
|
| 9 |
// 五声音阶(G 宫 / E 羽 共用)
|
| 10 |
+
export const N: Record<string, number> = { E2: 40, G2: 43, A2: 45, B2: 47, D3: 50, E3: 52, G3: 55, A3: 57, B3: 59, D4: 62, E4: 64, G4: 67, A4: 69, B4: 71, D5: 74, E5: 76, G5: 79, A5: 81, B5: 83, D6: 86, E6: 88 }
|
| 11 |
+
export const sceneStart = (id: string) => TL.scenes.find((s) => s.id === id)?.start ?? 0
|
| 12 |
+
export const sceneDur = (id: string) => TL.scenes.find((s) => s.id === id)?.dur ?? 0
|
| 13 |
|
| 14 |
+
export class Mixer {
|
| 15 |
ctx: AC
|
| 16 |
music: GainNode; sfx: GainNode; voice: GainNode; rev: ConvolverNode; revIn: GainNode; master: GainNode
|
| 17 |
noise: AudioBuffer
|
| 18 |
ksCache = new Map<number, AudioBuffer>()
|
| 19 |
r = rng(4242)
|
| 20 |
q: { t: number; fn: () => void }[] = []
|
| 21 |
+
/** 人声轨(供「山谷回声」等效果使用;在 score/effects 之前由 buildSoundtrack 设置) */
|
| 22 |
+
voiceBuf: AudioBuffer | null = null
|
| 23 |
+
/** 在 t 时刻真正创建节点(离线渲染按需建节点) */
|
| 24 |
+
at(t: number, fn: () => void) { this.q.push({ t, fn }) }
|
| 25 |
+
/** 把人声 [t0, t0+dur) 这一段当回声再放几遍:越来越远、越来越暗 */
|
| 26 |
+
echo(t0: number, dur: number, delay = 0.42, gain = 0.3, taps = 3) {
|
| 27 |
+
this.at(t0 + delay, () => {
|
| 28 |
+
if (!this.voiceBuf) return
|
| 29 |
+
for (let k = 1; k <= taps; k++) {
|
| 30 |
+
const s = this.ctx.createBufferSource(); s.buffer = this.voiceBuf
|
| 31 |
+
const f = this.ctx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 2800 / k
|
| 32 |
+
const g = this.ctx.createGain(); g.gain.value = gain * Math.pow(0.5, k - 1)
|
| 33 |
+
s.connect(f); f.connect(g); this.out(g, this.sfx, k % 2 ? -0.45 : 0.45, 0.6)
|
| 34 |
+
s.start(t0 + delay * k + 1 / 30, t0, dur)
|
| 35 |
+
}
|
| 36 |
+
})
|
| 37 |
+
}
|
| 38 |
constructor(ctx: AC) {
|
| 39 |
this.ctx = ctx
|
| 40 |
// 延迟建节点:只在需要前 ~1 秒才真正创建(离线渲染快很多)
|
|
|
|
| 270 |
lap(t0: number, t1: number, vel = 0.05) { for (let t = t0; t < t1; t += 0.6 + this.r() * 0.9) this.noiseHit(t, 0.5, 'bandpass', 700 + this.r() * 300, 300, vel, this.r() * 1.2 - 0.6, 1.5, 0.3) }
|
| 271 |
}
|
| 272 |
|
| 273 |
+
const cache = new Map<string, Promise<AudioBuffer>>()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
export function buildSoundtrack(voiceUrl: string, onStage?: (s: string) => void): Promise<AudioBuffer> {
|
| 275 |
+
const film = FILM
|
| 276 |
+
const hit = cache.get(film.id)
|
| 277 |
+
if (hit) return hit
|
| 278 |
+
const cached = (async () => {
|
| 279 |
const len = Math.ceil(SR * (TOTAL + 0.3))
|
| 280 |
const ctx = new OfflineAudioContext(2, len, SR)
|
| 281 |
onStage?.('加载旁白…')
|
|
|
|
| 286 |
} catch (e) { console.warn('voice load failed', e) }
|
| 287 |
onStage?.('合成配乐与音效…')
|
| 288 |
const m = new Mixer(ctx)
|
| 289 |
+
m.voiceBuf = voiceBuf
|
| 290 |
// 人声压低 BGM(sidechain 式):按时间轴里的配音区间自动化
|
| 291 |
const mg = m.music.gain
|
| 292 |
mg.setValueAtTime(0.9, 0)
|
| 293 |
for (const s of TL.scenes) {
|
| 294 |
+
const spans: [number, number][] = s.lines?.length ? s.lines.map((l) => [l.t0, l.t0 + l.dur]) : s.voice_start && s.voice_dur ? [[s.voice_start, s.voice_start + s.voice_dur]] : []
|
| 295 |
+
for (const [a, b] of spans) {
|
| 296 |
+
mg.setTargetAtTime(0.42, Math.max(0, a - 0.25), 0.08)
|
| 297 |
+
mg.setTargetAtTime(0.9, b + 0.1, 0.25)
|
| 298 |
+
}
|
| 299 |
}
|
| 300 |
+
film.score(m)
|
| 301 |
+
film.effects(m)
|
| 302 |
m.flushPlucks()
|
| 303 |
m.q.sort((a, b) => a.t - b.t)
|
| 304 |
let qi = 0
|
|
|
|
| 321 |
onStage?.('完成')
|
| 322 |
return out
|
| 323 |
})()
|
| 324 |
+
cache.set(film.id, cached)
|
| 325 |
+
cached.catch(() => cache.delete(film.id))
|
| 326 |
return cached
|
| 327 |
}
|
| 328 |
|
wolf-coming/web/src/film/film.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
| 1 |
// 合成器:时间轴 → 场景 → 晕染转场 → 字幕。每一帧只由 T 决定。
|
| 2 |
-
import timeline from './timeline.json'
|
| 3 |
import { type Ctx, paper, makeCanvas, ctx2d, blobLoop, spline, tracePath, rgba, INK, PAPER, FONT_KAI, easeInOut, clamp, inv, smooth } from './core'
|
| 4 |
-
import {
|
| 5 |
|
| 6 |
-
|
| 7 |
-
export
|
| 8 |
-
export
|
| 9 |
-
export
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
export type RenderOpts = { subs?: boolean; fadeEnd?: boolean }
|
| 12 |
|
|
@@ -97,7 +102,7 @@ export function renderFrame(g: Ctx, W: number, H: number, T: number, opts: Rende
|
|
| 97 |
g.restore()
|
| 98 |
}
|
| 99 |
|
| 100 |
-
export function currentSub(T: number): { text: string; a: number } | null {
|
| 101 |
const i = sceneIndexAt(T)
|
| 102 |
const subs = TL.scenes[i].subs ?? []
|
| 103 |
for (let k = 0; k < subs.length; k++) {
|
|
@@ -105,7 +110,7 @@ export function currentSub(T: number): { text: string; a: number } | null {
|
|
| 105 |
const end = k + 1 < subs.length ? subs[k + 1].t0 : s.t1 + 0.35
|
| 106 |
if (T >= s.t0 - 0.05 && T < end) {
|
| 107 |
const a = Math.min(smooth((T - s.t0 + 0.05) / 0.18), smooth((end - T) / 0.15))
|
| 108 |
-
return { text: s.text, a }
|
| 109 |
}
|
| 110 |
}
|
| 111 |
return null
|
|
@@ -114,6 +119,7 @@ export function currentSub(T: number): { text: string; a: number } | null {
|
|
| 114 |
export function drawSubs(g: Ctx, W: number, H: number, T: number) {
|
| 115 |
const s = currentSub(T)
|
| 116 |
if (!s || s.a <= 0) return
|
|
|
|
| 117 |
const portrait = H > W * 1.05
|
| 118 |
const u = Math.min(W, H) / 1080
|
| 119 |
const fs = (portrait ? 58 : 50) * u
|
|
@@ -133,7 +139,7 @@ export function drawSubs(g: Ctx, W: number, H: number, T: number) {
|
|
| 133 |
g.lineJoin = 'round'
|
| 134 |
g.strokeStyle = rgba(PAPER, 0.96); g.lineWidth = fs * 0.2
|
| 135 |
g.strokeText(s.text, W / 2, y)
|
| 136 |
-
g.fillStyle = rgba(INK, 0.97)
|
| 137 |
g.fillText(s.text, W / 2, y)
|
| 138 |
g.restore()
|
| 139 |
}
|
|
|
|
| 1 |
// 合成器:时间轴 → 场景 → 晕染转场 → 字幕。每一帧只由 T 决定。
|
|
|
|
| 2 |
import { type Ctx, paper, makeCanvas, ctx2d, blobLoop, spline, tracePath, rgba, INK, PAPER, FONT_KAI, easeInOut, clamp, inv, smooth } from './core'
|
| 3 |
+
import type { FilmDef, SceneCtx, SceneDef, Timeline } from './types'
|
| 4 |
|
| 5 |
+
// 当前片子(ES 模块导出是活绑定:setFilm 之后各处读到的 TL / TOTAL 自动更新)
|
| 6 |
+
export let FILM: FilmDef = null as unknown as FilmDef
|
| 7 |
+
export let TL: Timeline = { total: 1, fps: 30, scenes: [] }
|
| 8 |
+
export let TOTAL = 1
|
| 9 |
+
let SCENES: Record<string, SceneDef> = {}
|
| 10 |
+
export function setFilm(f: FilmDef) {
|
| 11 |
+
if (FILM === f) return
|
| 12 |
+
FILM = f; TL = f.timeline; TOTAL = f.timeline.total; SCENES = f.scenes
|
| 13 |
+
bgCache.clear()
|
| 14 |
+
}
|
| 15 |
|
| 16 |
export type RenderOpts = { subs?: boolean; fadeEnd?: boolean }
|
| 17 |
|
|
|
|
| 102 |
g.restore()
|
| 103 |
}
|
| 104 |
|
| 105 |
+
export function currentSub(T: number): { text: string; a: number; who?: string } | null {
|
| 106 |
const i = sceneIndexAt(T)
|
| 107 |
const subs = TL.scenes[i].subs ?? []
|
| 108 |
for (let k = 0; k < subs.length; k++) {
|
|
|
|
| 110 |
const end = k + 1 < subs.length ? subs[k + 1].t0 : s.t1 + 0.35
|
| 111 |
if (T >= s.t0 - 0.05 && T < end) {
|
| 112 |
const a = Math.min(smooth((T - s.t0 + 0.05) / 0.18), smooth((end - T) / 0.15))
|
| 113 |
+
return { text: s.text, a, who: s.who }
|
| 114 |
}
|
| 115 |
}
|
| 116 |
return null
|
|
|
|
| 119 |
export function drawSubs(g: Ctx, W: number, H: number, T: number) {
|
| 120 |
const s = currentSub(T)
|
| 121 |
if (!s || s.a <= 0) return
|
| 122 |
+
if (SCENES[TL.scenes[sceneIndexAt(T)].id]?.noSubs) return
|
| 123 |
const portrait = H > W * 1.05
|
| 124 |
const u = Math.min(W, H) / 1080
|
| 125 |
const fs = (portrait ? 58 : 50) * u
|
|
|
|
| 139 |
g.lineJoin = 'round'
|
| 140 |
g.strokeStyle = rgba(PAPER, 0.96); g.lineWidth = fs * 0.2
|
| 141 |
g.strokeText(s.text, W / 2, y)
|
| 142 |
+
g.fillStyle = rgba((s.who && FILM.subColors?.[s.who]) || INK, 0.97)
|
| 143 |
g.fillText(s.text, W / 2, y)
|
| 144 |
g.restore()
|
| 145 |
}
|