Compare commits

..

No commits in common. "135700a4816e4dd899ef956459cd1d33dfdc273d" and "083e878b97725e2c7955d1b1d7f18f46eac070e1" have entirely different histories.

30 changed files with 105 additions and 359 deletions

4
.gitignore vendored
View file

@ -15,7 +15,3 @@ node_modules/
__pycache__/
schnitt/ausgabe/
schnitt/tmp/
# Zugangsschlüssel (ElevenLabs u. a.), nie ins Git
.env
.env.local

View file

@ -33,9 +33,9 @@ mit `node werkzeuge/render.mjs FNN --sprache es`.
| `.claude/agents/` | das Kinderkanal-Team als Subagenten (Kinderpsychologie, Eltern, Finanzen, Cutter, Dolmetscher …) |
## Zuerst lesen
`wissen/stand-und-entscheidungen.md` – der Block „STAND SESSION-ENDE“ mit dem neuesten Datum steht, wo zuletzt
weitergemacht werden sollte (Stand 02.11.2026: ElevenLabs-Stimmen für die
Figuren festlegen, Folgen 1 und 2 sind als Remotion-Entwurf fertig). Die Dateien unter
`wissen/stand-und-entscheidungen.md` – ganz oben steht, wo zuletzt
weitergemacht werden sollte (Stand 28.10.2026: Lulu-Blumen-Szene in Folge 1
neu, drei Startbilder vorgeschlagen, Freigabe ausstehend). Die Dateien unter
`wissen/` stammen aus Laras lokalem Claude-Gedächtnis; neue Entscheidungen
ab jetzt hier im Repo nachtragen.

View file

@ -44,7 +44,6 @@ unter `figuren/bibliothek/`. Die **Clips** (MP4) sind nicht im Git (siehe
| noa_A8_rueckansicht | Rückansicht | 1 | 1,5 | |
| noa_A_arm-hoch-winken | rechter Arm hoch, winkend | 1 | 1,5 | |
| noa_A_arme-hoch | beide Arme hoch | 1 | 1,5 | |
| noa_A_klatschen | klatscht, Fäustlinge vor der Brust (02.11.2026, Nano Banana 2 aus noa_ref_stehend, für das Lied) | 1 | 1,5 | ohne rembg freigestellt |
| noa_A_laterne | Laterne in rechter Hand | 2 | 3 | 1. Versuch ohne Laterne; 2. gespiegelt; Laterne ragt seitlich weit heraus |
| noa_B1_traurig | traurig | 2 | 1,5 | 1. Versuch abgebrochen, erstattet |
| noa_B2_erschrocken | kurz erschrocken | 1 | 1,5 | |

View file

@ -61,4 +61,3 @@ lulu_B3_schlafen.png 23_e3e6898f-ba75-410d-9488-936dd5af4
lulu_B_kopf-profil.png 23_806c2339-90e8-42af-92b0-36b390e7d
noa_B1_traurig.png 030f14b3-a839-44d0-90bf-5a89e1fc92a9
pino_B3_schlafen.png dc751ba2-c05b-493b-95e1-c92668f1c627
noa_A_klatschen.png cbb2880c-95c7-4276-9cdf-a7eef639c5a7

1 noa_A1_augen-zu.png 46_c019cf04-d4ca-4e0c-8567-85858231b
61 lulu_B_kopf-profil.png 23_806c2339-90e8-42af-92b0-36b390e7d
62 noa_B1_traurig.png 030f14b3-a839-44d0-90bf-5a89e1fc92a9
63 pino_B3_schlafen.png dc751ba2-c05b-493b-95e1-c92668f1c627
noa_A_klatschen.png cbb2880c-95c7-4276-9cdf-a7eef639c5a7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -6,7 +6,6 @@
"": {
"name": "pino-remotion",
"dependencies": {
"@elevenlabs/elevenlabs-js": "^2.75.0",
"@remotion/cli": "^4.0.300",
"@remotion/google-fonts": "^4.0.300",
"react": "^18.3.1",
@ -33,41 +32,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@elevenlabs/elevenlabs-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@elevenlabs/elevenlabs-js/-/elevenlabs-js-2.75.0.tgz",
"integrity": "sha512-sCEddZAQS+JdAvD+jnqXund75mygRkHzgigqbWsQpcJozwe15FOtymS2+28YMOY06Pmja9l8EUE1LlXAtlW0Nw==",
"license": "MIT",
"dependencies": {
"command-exists": "^1.2.9",
"node-fetch": "^2.7.0",
"ws": "^8.18.3"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@elevenlabs/elevenlabs-js/node_modules/ws": {
"version": "8.22.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.1.tgz",
"integrity": "sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
@ -1312,12 +1276,6 @@
"node": ">=6.0"
}
},
"node_modules/command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
"integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==",
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@ -2159,26 +2117,6 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.60",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz",
@ -2807,12 +2745,6 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -3363,12 +3295,6 @@
"node": ">=10.13.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
"version": "5.112.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.2.tgz",
@ -3487,16 +3413,6 @@
}
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -6,7 +6,6 @@
"render": "remotion render"
},
"dependencies": {
"@elevenlabs/elevenlabs-js": "^2.75.0",
"@remotion/cli": "^4.0.300",
"@remotion/google-fonts": "^4.0.300",
"react": "^18.3.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View file

@ -1,42 +0,0 @@
// Korrektur für Clips, in denen die Kamera von Higgsfield leicht zoomt oder schwenkt.
// Gemessen an den Beinen (Mitte und Breite) je Clip-Bild, in Pixeln des Clipbildes.
// Angewendet wird die Rückrechnung: die Beine bleiben am Platz und gleich breit.
type Messung = [number, number, number]; // [Clip-Bild (24 fps), Beinmitte x, Beinbreite]
const MESSUNGEN: Record<string, {bildBreite: number; fussY: number; werte: Messung[]}> = {
// Lulu senkt den Hals: Beine werden um 20 % breiter und wandern 79 px zur Seite
'lulu_C9_hals-runter': {
bildBreite: 662, // Breite des Clipbildes in Pixeln
fussY: 659,
werte: [
[0, 417.5, 211], [16, 417, 210], [20, 417.5, 211], [24, 418.5, 211], [28, 420.5, 213], [32, 423, 214],
[36, 426.5, 215], [40, 430, 216], [44, 434, 218], [48, 438, 218], [52, 442.5, 221], [56, 447, 222],
[60, 452, 224], [64, 457, 226], [68, 461, 228], [72, 466.5, 231], [76, 470.5, 233], [80, 475, 236],
[84, 479.5, 239], [88, 483.5, 241], [92, 487, 244], [96, 490.5, 247], [100, 493.5, 249], [104, 495.5, 251],
[108, 496.5, 253], [120, 496.5, 253],
],
},
};
export type Korrektur = {k: number; mitte: number; ziel: number; fussY: number; bildBreite: number};
// Für Clip-Bild `b`: Maßstab k und Beinmitte (Ist und Soll), alles in Clip-Pixeln.
export const clipKorrektur = (name: string, b: number): Korrektur | null => {
const m = MESSUNGEN[name];
if (!m) return null;
const w = m.werte;
const bb = Math.min(w[w.length - 1][0], Math.max(0, b));
let i = 0;
while (i < w.length - 2 && w[i + 1][0] < bb) i++;
const [b0, m0, w0] = w[i];
const [b1, m1, w1] = w[i + 1];
const t = b1 === b0 ? 0 : (bb - b0) / (b1 - b0);
const mitte = m0 + (m1 - m0) * t;
const breite = w0 + (w1 - w0) * t;
return {k: w[0][2] / breite, mitte, ziel: w[0][1], fussY: m.fussY, bildBreite: m.bildBreite};
};
// Punkt (in Clip-Pixeln) so verschieben, wie die Korrektur das Bild verschiebt
export const korrigiere = (kor: Korrektur | null, x: number, y: number) =>
kor ? {x: kor.ziel + kor.k * (x - kor.mitte), y: kor.fussY + kor.k * (y - kor.fussY)} : {x, y};

View file

@ -1,6 +1,5 @@
import React from 'react';
import {Freeze, Img, OffthreadVideo, getStaticFiles, staticFile, useCurrentFrame, useVideoConfig} from 'remotion';
import {clipKorrektur} from './clipkorrektur';
import {CLIPS, FIGUREN, POSEN} from './posen';
// Figuren aus der Bibliothek (freigestellte Standbilder und Clips).
@ -20,8 +19,7 @@ export const posenStand = (frame: number, folge: Posenfolge, blende = 5) => {
let i = 0;
while (i + 1 < folge.length && frame >= folge[i + 1][0]) i++;
const [start, pose] = folge[i];
const lin = i > 0 ? Math.min(1, (frame - start) / blende) : 1;
const mix = lin * lin * (3 - 2 * lin); // weich an- und auslaufen
const mix = i > 0 ? Math.min(1, (frame - start) / blende) : 1;
return {pose, von: mix < 1 ? folge[i - 1][1] : undefined, mix};
};
@ -83,24 +81,13 @@ type FigurProps = Omit<Lage, 'children' | 'figur' | 'hoehe'> & {
export const NOA_HOEHE = 340;
// Manche Bilder der Bibliothek sind auf dieselbe Höhe gezeichnet wie die stehende Figur,
// obwohl die Figur darin hockt, sitzt oder liegt. Hier die richtige Größe relativ zur
// Standhöhe; beim Überblenden geht die Größe fließend mit (der Kopf wandert nach unten).
export const POSEN_SKALA: Record<string, number> = {
noa_B_hocken: 0.72,
noa_B_sitzt: 0.7,
lulu_B_liegt: 0.65,
};
const skala = (pose: string) => POSEN_SKALA[pose] ?? 1;
export const Figur: React.FC<FigurProps> = ({pose, hoehe, frame, blende, ...lage}) => {
const jetzt = useCurrentFrame();
const folge: Posenfolge = typeof pose === 'string' ? [[0, pose]] : pose;
const st = posenStand(frame ?? jetzt, folge, blende);
const figur = POSEN[st.pose].figur as FigurName;
const k = st.von ? skala(st.von) + (skala(st.pose) - skala(st.von)) * st.mix : skala(st.pose);
return (
<Rahmen {...lage} figur={figur} hoehe={(hoehe ?? NOA_HOEHE * GROESSE[figur]) * k}>
<Rahmen {...lage} figur={figur} hoehe={hoehe ?? NOA_HOEHE * GROESSE[figur]}>
{st.von && <PosenBild name={st.von} />}
<PosenBild name={st.pose} deckkraft={st.mix} />
</Rahmen>
@ -150,15 +137,9 @@ export const ClipFigur: React.FC<ClipProps> = ({name, start, hoehe, tempo = 1, s
let t = Math.max(0, (frame - start) * tempo);
t = schleife ? t % (laenge - 1) : Math.min(t, laenge - 1);
if (rueckwaerts) t = laenge - 1 - t;
// Zoom/Schwenk der Higgsfield-Kamera im Clip zurückrechnen (Beine bleiben am Platz)
const kor = clipKorrektur(name, (t * 24) / fps);
const px = kor ? c.b / kor.bildBreite : 1;
const korStil: React.CSSProperties = kor
? {transformOrigin: `${kor.mitte * px}px ${kor.fussY * px}px`, transform: `translateX(${(kor.ziel - kor.mitte) * px}px) scale(${kor.k})`}
: {};
return (
<Rahmen {...lage} figur={figur} hoehe={hoehe ?? NOA_HOEHE * GROESSE[figur]}>
<div style={{position: 'absolute', left: c.x, top: c.y, width: c.b, height: c.h, ...korStil}}>
<div style={{position: 'absolute', left: c.x, top: c.y, width: c.b, height: c.h}}>
<Freeze frame={Math.floor(t)}>
<OffthreadVideo src={staticFile(c.datei)} transparent muted style={{width: '100%', height: '100%'}} />
</Freeze>

View file

@ -1,4 +1,3 @@
import {clipKorrektur, korrigiere} from './clipkorrektur';
import React from 'react';
import {interpolate, useCurrentFrame} from 'remotion';
import {kurve} from '../bewegung';
@ -89,18 +88,6 @@ export const luluMaulC9 = (clipBild: number) => {
const [b0, x0, y0] = C9_NASE[i];
const [b1, x1, y1] = C9_NASE[i + 1];
const t = b1 === b0 ? 0 : (b - b0) / (b1 - b0);
// Maul knapp über der Unterkante der Nase; dieselbe Zoom-Korrektur wie das Clipbild
const p = korrigiere(clipKorrektur('lulu_C9_hals-runter', b), x0 + (x1 - x0) * t, y0 + (y1 - y0) * t - 6);
return {cx: C9.x + p.x * C9.s, cy: C9.y + p.y * C9.s};
};
// Klatschen im Lied: Noa klatscht auf jeden zweiten Schlag (Schlag 1, 3, 5 … ab Liedbeginn).
// Liefert Posenwechsel für die Zeit von `von` bis `bis` (Hände zusammen kurz um den Schlag).
export const klatschFolge = (von: number, bis: number, takt: number, basis = 'noa_A3_lachen'): [number, string][] => {
const folge: [number, string][] = [];
for (let t = takt; t < bis; t += 2 * takt) {
if (t - 3 < von) continue;
folge.push([t - 3, 'noa_A_klatschen'], [t + 4, basis]);
}
return folge;
// Maul knapp über der Unterkante der Nase
return {cx: C9.x + (x0 + (x1 - x0) * t) * C9.s, cy: C9.y + (y0 + (y1 - y0) * t - 6) * C9.s};
};

View file

@ -69,22 +69,20 @@ export const Blaetterregen: React.FC<{start: number; x0: number; y0: number; bod
);
};
// Ein Vogel wie im Bilderbuch: zwei weiche Flügelbögen („m“), kleiner Körper.
// `schlag` -1 … 1 = Flügel unten … oben.
// Ein Vogel von der Seite (fliegt nach rechts). `schlag` -1 … 1 = Flügel unten … oben.
const Vogel: React.FC<{x: number; y: number; g: number; schlag: number; neigung: number}> = ({x, y, g, schlag, neigung}) => {
const h = 4 + schlag * 13; // Höhe der Flügelbögen (oben = positiv)
const spitze = 3 - schlag * 9; // Flügelspitzen sinken beim Abschlag
const h = -8 - schlag * 24; // Flügelspitze relativ zur Schulter
const fluegel = (dx: number, dy: number) => `M ${3 + dx} ${-3 + dy} Q ${-3 + dx} ${h * 0.55 + dy} ${-15 + dx} ${h + dy} Q ${-6 + dx} ${h * 0.35 + dy} ${-7 + dx} ${1 + dy} Z`;
return (
<g transform={`translate(${x} ${y}) rotate(${neigung}) scale(${g / 30})`}>
<path
d={`M -26 ${spitze} Q -13 ${-h - 4}, -1 0 Q 13 ${-h - 4}, 26 ${spitze}`}
stroke="#4a4550"
strokeWidth={4.5}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<ellipse cx={0} cy={1.5} rx={4.5} ry={3.2} fill="#4a4550" />
<path d={fluegel(4, -1)} fill="#3a3540" opacity={0.85} />
<path d="M -14 0 L -27 -6 L -25 0 L -27 6 Z" fill="#4a4550" />
<ellipse cx={0} cy={0} rx={16} ry={7} fill="#4f4a55" />
<ellipse cx={1} cy={3} rx={11} ry={3.5} fill="#8a7f78" opacity={0.6} />
<circle cx={15} cy={-4} r={6.5} fill="#4f4a55" />
<path d="M 20 -6 L 28 -3.5 L 20 -1.5 Z" fill="#c98a3c" />
<circle cx={17} cy={-5.5} r={1.4} fill="#f4ead8" />
<path d={fluegel(0, 0)} fill="#5c5662" />
</g>
);
};
@ -101,10 +99,10 @@ export const Voegel: React.FC<{start: number; anzahl?: number; skala?: number}>
const x = 160 + t * 5.5 - Math.abs(reihe) * 75 * skala + r('x') * 20;
const y = 600 - t * 1.5 + reihe * 42 * skala + Math.sin(t * 0.05 + r('w') * 6) * 8;
// Flügelschlag ohne Pause, je Vogel etwas anders schnell
const takt = t * (0.22 + r('f') * 0.06) + r('p') * 6;
const takt = t * (0.34 + r('f') * 0.1) + r('p') * 6;
const schlag = Math.sin(takt);
const g = (34 + r('g') * 8) * skala * (1 - Math.abs(reihe) * 0.03);
return <Vogel key={i} x={x} y={y} g={g} schlag={schlag} neigung={-6 + Math.cos(takt) * 2} />;
return <Vogel key={i} x={x} y={y} g={g} schlag={schlag} neigung={-12 + Math.cos(takt) * 3} />;
})}
</svg>
);
@ -176,7 +174,7 @@ export const NEST_BILD = {b: 440, h: 260, x: 220, y: 180}; // Bildgröße und La
export const NestBlaetter: React.FC<{voll?: number; seite: 'hinten' | 'vorne'}> = ({voll = 1, seite}) => (
<>
{seite === 'hinten' && (
<div style={{position: 'absolute', left: -170, top: -30, width: 340, height: 60, borderRadius: '50%', background: 'radial-gradient(closest-side, rgba(70,45,25,0.45), rgba(70,45,25,0))', opacity: Math.min(1, voll * 3)}} />
<div style={{position: 'absolute', left: -170, top: -30, width: 340, height: 60, borderRadius: '50%', background: 'radial-gradient(closest-side, rgba(70,45,25,0.45), rgba(70,45,25,0))'}} />
)}
{NEST[seite].map((b, i) =>
b.reihe < voll ? <Blatt key={i} x={b.x * (0.5 + 0.5 * voll)} y={b.y * (0.4 + 0.6 * voll)} r={b.r} s={b.s} farbe={b.farbe} /> : null,
@ -192,6 +190,9 @@ export const Nest: React.FC<{x: number; y: number; voll?: number; seite: 'hinten
/>
) : (
<div style={{position: 'absolute', left: x, top: y, transform: `scale(${s})`, transformOrigin: '0 0', filter}}>
{seite === 'hinten' && (
<div style={{position: 'absolute', left: -170, top: -30, width: 340, height: 60, borderRadius: '50%', background: 'radial-gradient(closest-side, rgba(70,45,25,0.45), rgba(70,45,25,0))'}} />
)}
<NestBlaetter voll={voll} seite={seite} />
</div>
);
@ -350,31 +351,3 @@ export const Laubhaufen: React.FC<{x: number; y: number; s?: number; wackeln?: n
</svg>
);
};
// Klatsch-Hände fürs Lied (Noas rote Fäustlinge): klatschen auf jeden zweiten Schlag,
// damit Kinder den Rhythmus mitklatschen können. `takt` = Bilder pro Schlag.
export const Klatschen: React.FC<{x: number; y: number; takt: number; s?: number; deckkraft?: number}> = ({x, y, takt, s = 1, deckkraft = 1}) => {
const frame = useCurrentFrame();
// 0 = Hände zusammen (Klatsch auf Schlag 1, 3, 5 …), 1 = weit auseinander
const auf = Math.abs(Math.sin((Math.PI * (frame - takt)) / (2 * takt)));
const weit = 18 + auf * 52;
const klatsch = auf < 0.18;
const Faust: React.FC<{seite: 1 | -1}> = ({seite}) => (
<g transform={`translate(${seite * weit} 0) scale(${-seite} 1) rotate(${-8 + auf * 14})`}>
<path d="M -30 -34 C -46 -30, -50 6, -34 30 C -24 44, 6 46, 20 30 C 30 18, 30 -10, 20 -26 C 10 -40, -14 -42, -30 -34 Z" fill="#c8362c" />
<path d="M 14 -18 C 30 -26, 40 -12, 30 -2 C 24 4, 16 2, 12 -4 Z" fill="#c8362c" />
<rect x={-38} y={26} width={58} height={20} rx={8} fill="#e9e2d2" />
<path d="M -22 -20 C -30 -8, -28 10, -20 20" stroke="#e8786a" strokeWidth={5} strokeLinecap="round" fill="none" opacity={0.6} />
</g>
);
return (
<svg width={360} height={200} viewBox="-180 -100 360 200" style={{position: 'absolute', left: x - 180 * s, top: y - 100 * s, transform: `scale(${s})`, transformOrigin: '0 0', opacity: deckkraft, overflow: 'visible'}}>
{klatsch &&
[-60, -30, 0, 30, 60].map((w) => (
<path key={w} d="M 0 -58 L 0 -84" stroke="#e0a020" strokeWidth={6} strokeLinecap="round" transform={`rotate(${w})`} />
))}
<Faust seite={-1} />
<Faust seite={1} />
</svg>
);
};

View file

@ -501,7 +501,7 @@ export const KalenderAufTisch: React.FC<{p: number}> = ({p}) => {
export const KALENDER_TUER1 = {x: 330 + 4 * 215 + 90, y: 250 + 1 * 180 + 75}; // Türchen „1“ (Index 10)
// Noa malt den Kalender (Blick von oben auf das Blatt, über `kalender_leer`).
// `p` 0…1: Titel, dann die Türchen Zeile für Zeile.
// `p` 0…1: Titel, dann die Türchen Zeile für Zeile. Die Stiftspitze folgt dem Strich.
const drehe = (x: number, y: number) => {
const w = (-2 * Math.PI) / 180;
const dx = x - 960;
@ -569,8 +569,22 @@ export const KalenderMalen: React.FC<{p: number; wackeln: number}> = ({p, wackel
})}
</g>
</g>
{/* Aus Noas Sicht aufs Blatt: die Striche entstehen von selbst, ohne Hand und Stift
(der Arm von unten wirkte wie ein schwebender Stift) */}
{/* Noas Hand mit Buntstift, von unten rechts */}
<g transform={`translate(${s.x} ${s.y}) rotate(-28)`}>
<path d="M -9 -4 L 0 -24 L 9 -4 Z" fill="#f1d2a6" />
<path d="M -3 -17 L 0 -24 L 3 -17 Z" fill={farbe} />
<rect x={-11} y={-6} width={22} height={260} rx={4} fill={farbe} />
<rect x={-11} y={-6} width={7} height={260} fill="rgba(255,255,255,0.18)" />
{/* Ärmel */}
<path d="M -62 150 L 66 150 L 120 900 L -150 900 Z" fill="#d39b3a" />
<path d="M -66 140 Q 2 164 70 140 L 74 176 Q 2 198 -70 176 Z" fill="#c1862c" />
{/* Handrücken und Finger, die den Stift halten */}
<ellipse cx={4} cy={118} rx={52} ry={44} fill="#f2c4a2" stroke="#dba585" strokeWidth={2} />
<ellipse cx={-30} cy={98} rx={15} ry={24} fill="#f2c4a2" stroke="#dba585" strokeWidth={2} transform="rotate(-25 -30 98)" />
<ellipse cx={-6} cy={72} rx={17} ry={13} fill="#f2c4a2" stroke="#dba585" strokeWidth={2} />
<ellipse cx={20} cy={80} rx={16} ry={12} fill="#f2c4a2" stroke="#dba585" strokeWidth={2} />
<ellipse cx={38} cy={94} rx={14} ry={11} fill="#f2c4a2" stroke="#dba585" strokeWidth={2} />
</g>
</svg>
);
};

View file

@ -513,20 +513,6 @@ export const POSEN: Record<string, Pose> = {
701
]
},
"noa_A_klatschen": {
"figur": "noa",
"datei": "bibliothek/figuren/noa_A_klatschen.webp",
"x": 339,
"y": 156,
"b": 715,
"h": 551,
"box": [
543,
165,
807,
700
]
},
"noa_A_arme-hoch": {
"figur": "noa",
"datei": "bibliothek/figuren/noa_A_arme-hoch.webp",

View file

@ -1,7 +1,7 @@
import React from 'react';
import {random, useCurrentFrame} from 'remotion';
import {Figur, Kamera} from '../baukasten/figur';
import {Blende, Vignette, klatschFolge} from '../baukasten/helfer';
import {Blende, Vignette} from '../baukasten/helfer';
import {Blume, Laubhaufen} from '../baukasten/natur';
import {BODEN, Ort} from '../baukasten/orte';
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
@ -57,22 +57,19 @@ export const Lied: React.FC = () => {
[z.s4.start, 'pino_A_hinterbeine'],
[z.w1.start, 'pino_A3_lachen'],
];
// Noa klatscht im Takt (jeder zweite Schlag), bei „zusammen kriegen wir alles hin“ Arme hoch
const noaPose: [number, string][] = [
[0, 'noa_A3_lachen'],
...klatschFolge(z.r1.start, z.r4.start, TAKT),
[z.r4.start, 'noa_A_arme-hoch'],
[z.s1.start, 'noa_A3_lachen'],
...klatschFolge(z.s1.start, z.w4.start, TAKT),
[z.w4.start, 'noa_A_arme-hoch'],
];
return (
<>
<Kamera x={1000} y={690} zoom={1.3}>
<Kamera x={1000} y={600} zoom={1.3}>
<Ort name="garten_herbst" />
<Laubhaufen x={HAUFEN_X + 380} y={BODEN + 6} />
<Figur pose={luluPose} x={1300} y={BODEN} drehung={schwank(0.5, 2.2)} stauchY={1 - 0.012 * schlag} blende={8} />
<Figur pose={noaPose} x={980} y={BODEN + wipp(0.3, 10)} drehung={schwank(0, 3)} blende={3} />
<Figur pose={noaPose} x={980} y={BODEN + wipp(0.3, 10)} drehung={schwank(0, 3)} blende={8} />
<Figur pose={pinoPose} x={760} y={BODEN + wipp(0, 16)} drehung={schwank(1, 4)} stauchY={1 - 0.03 * schlag} blende={8} />
{f >= z.s4.start && f < z.w1.start && <Blume x={764} y={BODEN - 40} s={0.5} />}
</Kamera>

View file

@ -148,7 +148,7 @@ export const Szene2: React.FC = () => {
{imHaufen && <Figur pose={noaPose} x={noaX} y={noaY} stauchY={atmen(f, 2) * hopfStauch(f, landung)} schatten={false} />}
<Laubhaufen x={HAUFEN_X} y={BODEN + 6} wackeln={wackeln} flach={flach} />
{imHaufen ? null : noaGeht ? (
<Geher figur="noa" spiegeln x={noaX} y={BODEN} tempo={1.25} />
<Geher figur="noa" spiegeln x={noaX} y={BODEN} tempo={1.5} />
) : (
<Figur pose={noaPose} x={noaX} y={noaY} stauchY={atmen(f, 2) * hopfStauch(f, landung) * hockeStauch} blende={f >= hocke ? HOCKE_BLENDE : 5} />
)}

View file

@ -99,7 +99,7 @@ export const Szene3: React.FC = () => {
<Laubhaufen x={HAUFEN_X} y={BODEN + 6} />
{noaDa &&
(noaGeht ? (
<Geher figur="noa" spiegeln x={noaX} y={BODEN} tempo={1.25} />
<Geher figur="noa" spiegeln x={noaX} y={BODEN} tempo={1.5} />
) : (
<Figur pose={noaPose} x={noaX} y={noaY} stauchY={atmen(f, 2) * (1 + 0.04 * zehen)} blende={10} />
))}

View file

@ -19,7 +19,7 @@ export const LULU_APFEL_X = APFEL.x + 149;
// Den Hals senkt sie mit dem Clip lulu_C9_hals-runter; der Apfel bleibt dabei im Maul
// (Maul-Bahn gemessen, luluMaulC9). Unten liegt das Maul direkt neben Pino.
const LULU_WARTE_X = 930; // bis zum Pflücken etwas rechts vom Apfel
const C9_TEMPO = 1.25; // 1,25 × 24 fps = 30 fps: jedes Videobild genau ein Clipbild (ruckelfrei)
const C9_TEMPO = 1.3;
const C9_START = 17; // vorher steht sie im Clip still
const C9_UNTEN = 105; // Kopf unten
const c9Frames = (bilder: number) => (bilder * 30) / 24 / C9_TEMPO;
@ -140,7 +140,7 @@ export const Szene4: React.FC = () => {
<Blume x={BLUME_X} y={BLUME_Y} s={0.8} />
{apfelAmBaum && <Apfel x={APFEL.x} y={APFEL.y} s={0.9} r={Math.sin(f * 0.05) * 4} />}
{luluGeht ? (
<Geher figur="lulu" x={luluX} y={luluY} tempo={1.25} />
<Geher figur="lulu" x={luluX} y={luluY} tempo={1} />
) : imClip ? (
f < hoch ? (
<ClipFigur name="lulu_C9_hals-runter" start={runter - c9Frames(C9_START)} tempo={C9_TEMPO} bis={C9_UNTEN / 24} x={luluX} y={luluY} />

View file

@ -45,7 +45,7 @@ export const Szene5: React.FC = () => {
const {z, dauer} = S5;
// Lulu senkt den Hals (Clip) bis zur Hälfte, bleibt wackelnd stecken, hebt ihn wieder (Clip rückwärts)
const C9_TEMPO = 1.25; // ruckelfrei (siehe Szene 4)
const C9_TEMPO = 1.1;
const C9_START = 17;
const C9_HALB = 52; // so weit kommt sie
const c9 = (bilder: number) => (bilder * 30) / 24 / C9_TEMPO;

View file

@ -74,7 +74,7 @@ export const Szene6: React.FC = () => {
const BLENDE = 14;
// Hände: noa_B_hocken auf den Knien (Leinwand ca. 700, 520), noa_A_arme-hoch rechte Hand (800, 190); Fuß (675, 700), Maßstab 340/535
const m = 340 / 535;
const HAND_HOCKE = {x: NOA_BLUME_X + (700 - 675) * m * 0.72, y: BODEN + (520 - 700) * m * 0.72}; // Hocke-Bild wird auf 72 % verkleinert (POSEN_SKALA)
const HAND_HOCKE = {x: NOA_BLUME_X + (700 - 675) * m, y: BODEN + (520 - 700) * m};
const HAND_OBEN = {x: NOA_BLUME_X + (800 - 675) * m, y: BODEN + (190 - 700) * m};
let blume: {x: number; y: number; s: number} = {x: BLUME_X, y: BLUME_Y, s: 0.8};
if (f >= pflueck) blume = {x: pfoten.x, y: pfoten.y + 20, s: 0.55};
@ -148,9 +148,9 @@ export const Szene6: React.FC = () => {
<Figur pose={luluPose} x={LULU_BLUME_X} y={BODEN - 6} stauchY={atmen(f, 3, 0.006)} blende={12} />
<Figur pose={noaPose} x={NOA_BLUME_X} y={BODEN + huepfer(f, aufstehen + BLENDE, 14, 10)} stauchY={atmen(f, 2) * noaStauch * hopfStauch(f, aufstehen + BLENDE + 10)} blende={BLENDE} />
{pinoGehtHin ? (
<Geher figur="pino" spiegeln x={pinoX} y={pinoBoden} tempo={1.25} />
<Geher figur="pino" spiegeln x={pinoX} y={pinoBoden} tempo={1.4} />
) : pinoGehtZurueck ? (
<Geher figur="pino" x={pinoX} y={pinoBoden} tempo={1.25} />
<Geher figur="pino" x={pinoX} y={pinoBoden} tempo={1.4} />
) : (
<Figur pose={pinoPose} x={pinoX} y={pinoBoden + huepfer(f, z.l1.ende, 24, 12)} stauchY={atmen(f, 1, 0.01)} blende={10} />
)}

View file

@ -1,7 +1,7 @@
import React from 'react';
import {random, useCurrentFrame} from 'remotion';
import {Figur, Kamera} from '../baukasten/figur';
import {Blende, Vignette, klatschFolge} from '../baukasten/helfer';
import {Blende, Vignette} from '../baukasten/helfer';
import {BODEN, Ort} from '../baukasten/orte';
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
import {TEXTE, plane} from './plan';
@ -36,20 +36,17 @@ export const Lied: React.FC = () => {
const schwank = (phase: number, g: number) => Math.sin((f * Math.PI) / (2 * TAKT) + phase) * g;
return (
<>
<Kamera x={980} y={690} zoom={1.3}>
<Kamera x={980} y={600} zoom={1.3}>
<Ort name="garten" />
<Figur pose="lulu_A3_lachen" x={1240} y={BODEN} drehung={schwank(0.5, 2.2)} stauchY={1 - 0.012 * schlag} />
<Figur
pose={[
[0, 'noa_A3_lachen'],
// Noa klatscht im Takt (jeder zweite Schlag)
...klatschFolge(z.r1.start, z.r4.start, TAKT),
[z.r4.start, 'noa_A_arme-hoch'],
]}
x={960}
y={BODEN + wipp(0.3, 10)}
drehung={schwank(0, 3)}
blende={3}
/>
<Figur pose="pino_A3_lachen" x={720} y={BODEN + wipp(0, 16)} drehung={schwank(1, 4)} stauchY={1 - 0.03 * schlag} />
</Kamera>

View file

@ -125,8 +125,8 @@ export const Szene2: React.FC = () => {
<>
<Kamera {...kam}>
<Ort name="garten" />
{noaGeht ? <Geher figur="noa" spiegeln x={noaX} y={BODEN} tempo={1.25} /> : <Figur pose={noaPose} x={noaX} y={noaY} stauchY={atmen(f, 2) * kichStauch * (1 + 0.02 * vorbeugen)} drehung={kichDreh + 7 * vorbeugen} />}
{!pinoDa ? null : pinoGeht ? <Geher figur="pino" x={pinoX} y={BODEN} tempo={2.5} /> : <Figur pose={pinoPose} x={pinoX} y={pinoY} stauchY={pinoStauchY} drehung={pinoDreh} blende={4} />}
{noaGeht ? <Geher figur="noa" spiegeln x={noaX} y={BODEN} /> : <Figur pose={noaPose} x={noaX} y={noaY} stauchY={atmen(f, 2) * kichStauch * (1 + 0.02 * vorbeugen)} drehung={kichDreh + 7 * vorbeugen} />}
{!pinoDa ? null : pinoGeht ? <Geher figur="pino" x={pinoX} y={BODEN} tempo={1.8} /> : <Figur pose={pinoPose} x={pinoX} y={pinoY} stauchY={pinoStauchY} drehung={pinoDreh} blende={4} />}
<Zzz x={PINO_X + 40} y={BODEN - 150} start={z.p2.ende} ende={schreck} s={0.7} />
</Kamera>
<Blaetterwind anzahl={6} />

View file

@ -1,7 +1,7 @@
import React from 'react';
import {useCurrentFrame} from 'remotion';
import {kurve} from '../bewegung';
import {ClipFigur, Figur, Geher, Kamera} from '../baukasten/figur';
import {ClipFigur, Figur, Geher, Kamera, NoaHocke} from '../baukasten/figur';
import {Blende, Vignette, atmen, hopfStauch, huepfer, kamFahrt} from '../baukasten/helfer';
import {Blaetterwind, Blatt, Voegel} from '../baukasten/natur';
import {BODEN, Ort} from '../baukasten/orte';
@ -55,11 +55,11 @@ export const Szene3: React.FC = () => {
let lulu: React.ReactNode;
const luluY = BODEN - 10;
if (luluGeht) {
lulu = <Geher figur="lulu" x={luluX} y={luluY} tempo={1.25} />;
lulu = <Geher figur="lulu" x={luluX} y={luluY} />;
} else if (f < halsHoch) {
lulu = <ClipFigur name="lulu_C9_hals-runter" start={halsRunter} tempo={1.25} x={LULU_X} y={luluY} />;
} else if (f < halsHoch + 110) {
lulu = <ClipFigur name="lulu_C9_hals-runter" start={halsHoch} tempo={1.25} rueckwaerts x={LULU_X} y={luluY} />;
lulu = <ClipFigur name="lulu_C9_hals-runter" start={halsHoch} tempo={1.5} rueckwaerts x={LULU_X} y={luluY} />;
} else {
lulu = (
<Figur
@ -82,10 +82,9 @@ export const Szene3: React.FC = () => {
[aufsBlatt, PINO_X],
[aufsBlatt + 24, GROSSES_BLATT_X],
]);
// Das große Blatt liegt von Anfang an neben Pino im Gras (vorher fiel es außerhalb des
// Bildausschnitts und tauchte dann plötzlich unter ihm auf); Pino setzt sich darauf
const blattFaellt = -1;
const u = 1;
// Das große Blatt segelt vom Apfelbaum herunter und landet neben Pino
const blattFaellt = aufsBlatt - 80;
const u = Math.min(1, Math.max(0, (f - blattFaellt) / 70));
const schaukeln = Math.sin(u * 8) * (1 - u);
const blattX = GROSSES_BLATT_X - 180 * (1 - u) + schaukeln * 70;
const blattY = BODEN - 8 - 560 * (1 - u);
@ -126,11 +125,7 @@ export const Szene3: React.FC = () => {
[z.l1.start + 15, 'noa_A3_lachen'],
[z.e1.start, 'noa_A6_blick-rechts'],
[z.bild2.start + 30, 'noa_B1_traurig'],
[knien, 'noa_B_hocken'],
];
// in die Hocke: Standbilder weich überblenden, dabei kurz absinken
const HOCKE_BLENDE = 14;
const hockeStauch = f >= knien && f < knien + HOCKE_BLENDE ? 1 - 0.05 * Math.sin(((f - knien) / HOCKE_BLENDE) * Math.PI) : 1;
const noaGeht = f >= knien - 36 && f < knien;
const kam = kamFahrt(f, [
@ -159,7 +154,7 @@ export const Szene3: React.FC = () => {
) : (
<Kamera {...kam}>
<Ort name="garten" />
{noaGeht ? <Geher figur="noa" spiegeln x={noaX} y={BODEN} tempo={1.25} /> : <Figur pose={noaPose} x={noaX} y={BODEN} stauchY={atmen(f, 2) * hockeStauch} blende={f >= knien ? HOCKE_BLENDE : 5} />}
{noaGeht ? <Geher figur="noa" spiegeln x={noaX} y={BODEN} /> : f >= knien ? <NoaHocke start={knien} x={noaX} y={BODEN} /> : <Figur pose={noaPose} x={noaX} y={BODEN} stauchY={atmen(f, 2)} />}
{f > blattFaellt && <Blatt x={blattX} y={blattY} r={blattR} s={2.6} farbe="#c8642c" />}
<Figur pose={pinoPose} x={pinoX} y={pinoY} stauchY={pinoStauchY} drehung={pinoDreh} blende={6} />
{lulu}

View file

@ -1,7 +1,7 @@
import React from 'react';
import {useCurrentFrame} from 'remotion';
import {kurve} from '../bewegung';
import {Figur, Geher, Kamera} from '../baukasten/figur';
import {Figur, Geher, Kamera, NOA_HOCKE_FRAMES, NoaHocke} from '../baukasten/figur';
import {Blende, LULU_MAUL_A1, LULU_MAUL_LINKS, Vignette, atmen, hopfStauch, huepfer, kamFahrt, punkt, ruck} from '../baukasten/helfer';
import {Blaetterregen, Blatt, LAUB, Nest, Ruf} from '../baukasten/natur';
import {BODEN, NEST_X, Ort} from '../baukasten/orte';
@ -54,13 +54,13 @@ export const Szene4: React.FC = () => {
// Noa
const noaGehtVon = zumStapel + 20;
const noaGehtBis = noaGehtVon + 60;
const noaGehtBis = noaGehtVon + 90;
const noaX = kurve(f, [
[noaGehtVon, NOA_X + 140],
[noaGehtBis, NOA_ZAEHL_X],
]);
const noaGeht = f > noaGehtVon && f < noaGehtBis;
const noaStehPose: [number, string][] = [
const noaPose: [number, string][] = [
[0, 'noa_A3_lachen'],
[noaGehtBis, 'noa_A6_blick-rechts'],
[z.c5.ende, 'noa_A3_lachen'],
@ -68,23 +68,21 @@ export const Szene4: React.FC = () => {
[z.l2.ende, 'noa_A3_lachen'],
[z.e4.start, 'noa_A3_lachen'],
];
// Hocke: Abschnitte, in denen Noa hockt; Wechsel als weiche Überblendung der Standbilder
// (der Hinsetz-Clip ist anders gezeichnet), dabei kurz absinken bzw. sich strecken
const HOCKE_BLENDE = 14;
const hockZeiten: [number, number][] = [
[-1000, z.n1.start + 30],
[z.n2.start - 15, z.c5.ende],
[zurueck, z.e4.start],
];
const inHocke = hockZeiten.some(([a, b]) => f >= a && f < b);
const stehPose = (t: number) => noaStehPose.filter(([s]) => s <= t).pop()![1];
const noaPose: [number, string][] = [...noaStehPose, ...hockZeiten.flatMap(([a, b]): [number, string][] => [[a, 'noa_B_hocken'], [b, stehPose(b)]])].sort((p, q) => p[0] - q[0]);
const bump = (t: number) => (f >= t && f < t + HOCKE_BLENDE ? Math.sin(((f - t) / HOCKE_BLENDE) * Math.PI) : 0);
const hockeStauch = hockZeiten.reduce((s, [a, b]) => s * (1 - 0.05 * bump(a)) * (1 + 0.03 * bump(b)), 1);
// Hocke: Clip vorwärts (runter) oder rückwärts (aufstehen); sonst Standbilder
const hocke = (() => {
const w = (start: number, auf: boolean) => ({start, auf});
if (f < z.n1.start + 30) return w(-1000, false);
if (f < z.n1.start + 30 + NOA_HOCKE_FRAMES) return w(z.n1.start + 30, true);
if (f >= z.n2.start - 15 && f < z.c5.ende) return w(z.n2.start - 15, false);
if (f >= z.c5.ende && f < z.c5.ende + NOA_HOCKE_FRAMES) return w(z.c5.ende, true);
if (f >= zurueck && f < z.e4.start) return w(-1000, false);
if (f >= z.e4.start && f < z.e4.start + NOA_HOCKE_FRAMES) return w(z.e4.start, true);
return null;
})();
// beim Hinlegen jedes Blatts kurz nach vorn beugen
const beugen = zaehlen.reduce((s, c) => s + Math.max(0, Math.sin(Math.min(1, Math.max(0, (f - c + 8) / 16)) * Math.PI)), 0);
// beim Nestbau: früher seitliches Hin- und Herrutschen (wirkte unruhig) – jetzt ruhig, nur die Blätter bewegen sich
// beim Nestbau schieben (vor und zurück)
const schieben = f > zurueck && f < z.bild4.ende ? Math.sin((f - zurueck) * 0.22) : 0;
// Pino
const pinoGehtVon = zumStapel + 40;
@ -106,12 +104,10 @@ export const Szene4: React.FC = () => {
const pinoGeht = (f > pinoGehtVon && f < pinoGehtBis) || (f > zurueck + 10 && f < zurueck + 60);
const pinoSpiegel = f > pinoGehtVon && f < pinoGehtBis; // nach rechts
const imNest = f >= z.bild5.start;
// Pino sitzt beim Zählen rechts von Noa: seine Bilder schauen nach rechts, darum gespiegelt (schaut zu ihr)
const pinoZuNoa = f >= pinoGehtBis && f <= zurueck + 10;
const pinoPose: [number, string][] = [
[0, 'pino_A1_augen-zu'],
[z.n1.start + 20, 'pino_A2_augen-halb'],
[pinoGehtBis, 'pino_A6_blick-rechts'],
[pinoGehtBis, 'pino_A5_blick-links'],
...zaehlen.map((c) => [c, 'pino_A4_staunen'] as [number, string]),
[z.c5.ende, 'pino_A3_lachen'],
[niesen + 4, 'pino_B2_erschrocken'],
@ -202,7 +198,7 @@ export const Szene4: React.FC = () => {
kam.y += ruck(f, niesen, 9, 22);
const lulu = <Figur pose={luluPose} x={LULU_BAUM_X} y={luluY} stauchY={luluStauch} drehung={luluDreh} />;
const luluBeimNest = f >= zurueck && <Figur pose="lulu_A_hals-unten" x={2030} y={BODEN - 4} />;
const luluBeimNest = f >= zurueck && <Figur pose="lulu_A_hals-unten" x={2030} y={BODEN - 4} drehung={schieben * 1.2} />;
return (
<>
@ -221,16 +217,11 @@ export const Szene4: React.FC = () => {
)}
<Nest x={NEST_X} y={BODEN} seite="hinten" voll={f < zurueck ? 0 : nestVoll} />
{noaGeht ? (
<Geher figur="noa" spiegeln x={noaX} y={BODEN} tempo={1.25} />
<Geher figur="noa" spiegeln x={noaX} y={BODEN} />
) : hocke ? (
<NoaHocke start={hocke.start} auf={hocke.auf} x={noaX + schieben * 10} y={BODEN} stauchY={1 - 0.03 * beugen} drehung={beugen * 3 + schieben * 2} />
) : (
<Figur
pose={noaPose}
x={noaX}
y={BODEN}
stauchY={atmen(f, 2) * hockeStauch * (inHocke ? 1 - 0.03 * beugen : 1)}
drehung={inHocke ? beugen * 3 : 0}
blende={HOCKE_BLENDE}
/>
<Figur pose={noaPose} x={noaX} y={BODEN} stauchY={atmen(f, 2)} />
)}
{/* das große Blatt aus Szene 3 bleibt liegen */}
{f < zurueck + 40 && <Blatt x={GROSSES_BLATT_X} y={BODEN - 8} r={88} s={2.6} farbe="#c8642c" deckkraft={f > zurueck ? 1 - (f - zurueck) / 40 : 1} />}
@ -239,11 +230,10 @@ export const Szene4: React.FC = () => {
<Figur pose={pinoPose} x={NEST_X} y={pinoImNestY} schatten={false} />
</div>
) : pinoGeht ? (
<Geher figur="pino" spiegeln={pinoSpiegel} x={pinoX} y={BODEN} tempo={1.25} />
<Geher figur="pino" spiegeln={pinoSpiegel} x={pinoX} y={BODEN} tempo={1.3} />
) : (
<Figur
pose={pinoPose}
spiegeln={pinoZuNoa}
x={pinoX}
y={BODEN - 6 * sitzt + zaehlHuepfer + huepfer(f, niesen + 2, 30, 12) + insNest}
stauchY={atmen(f, 1) * hopfStauch(f, niesen + 2) * hopfStauch(f, z.bild5.start - 20, 20)}

View file

@ -108,7 +108,7 @@ export const Szene5: React.FC = () => {
stauchY={atmen(f, 3, 0.006)}
/>
) : (
<ClipFigur name="lulu_C9_hals-runter" start={z.bild3.start + 10} tempo={1.25} bis={3.6} x={LULU_X5} y={BODEN - 10} />
<ClipFigur name="lulu_C9_hals-runter" start={z.bild3.start + 10} tempo={1.4} bis={3.6} x={LULU_X5} y={BODEN - 10} />
);
const ideeStart = z.bild4.start;

View file

@ -1,7 +1,7 @@
import React from 'react';
import {useCurrentFrame} from 'remotion';
import {kurve} from '../bewegung';
import {ClipFigur, Figur, Kamera, POSEN_SKALA} from '../baukasten/figur';
import {ClipFigur, Figur, Kamera} from '../baukasten/figur';
import {Blende, Vignette, atmen, huepfer, kamFahrt} from '../baukasten/helfer';
import {Funkeln} from '../baukasten/natur';
import {KALENDER_TUER1, KalenderAufTisch, KalenderMalen, Ort} from '../baukasten/orte';
@ -143,7 +143,7 @@ export const Szene6: React.FC = () => {
]}
x={NOA_X6}
y={1000}
hoehe={N / POSEN_SKALA.noa_B_sitzt} // Szene ist auf die volle Bildgröße gebaut (Noa sitzt nah an der Kamera am Tisch)
hoehe={N}
stauchY={atmen(f, 2)}
drehung={malt ? Math.sin(f * 0.3) * 0.8 : 0}
schatten={false}
@ -151,7 +151,7 @@ export const Szene6: React.FC = () => {
<Ort name="zimmer_tisch" />
{f >= z.malen.start && <KalenderAufTisch p={malP} />}
{imSprung ? (
<ClipFigur name="pino_C3_freudensprung" start={sprung} tempo={1.25} x={PINO_X6} y={TISCH_Y} hoehe={N * 0.4} />
<ClipFigur name="pino_C3_freudensprung" start={sprung} tempo={1.4} x={PINO_X6} y={TISCH_Y} hoehe={N * 0.4} />
) : (
<Figur
pose={[

View file

@ -82,7 +82,7 @@ export const Szene7: React.FC = () => {
filter={NACHT}
/>
) : (
<ClipFigur name="noa_C4_winken" start={winken} tempo={1.25} x={NOA_X7} y={BODEN} filter={NACHT} />
<ClipFigur name="noa_C4_winken" start={winken} tempo={1.2} x={NOA_X7} y={BODEN} filter={NACHT} />
)}
<Nest x={NEST_X} y={BODEN} seite="hinten" filter={NACHT} />
<div style={{position: 'absolute', inset: 0, clipPath: `inset(0 0 ${1080 - BODEN - 4}px 0)`}}>

View file

@ -1,12 +1,9 @@
{
"modell": "eleven_v4",
"modell": "eleven_multilingual_v2",
"format": "mp3_44100_128",
"einstellungen": {
"stability": 0.5,
"similarity_boost": 0.75
},
"einstellungen": {"stability": 0.5, "similarity_boost": 0.75, "style": 0.2},
"stimmen": {
"ERZ": "8TMmdpPgqHKvDOGYP2lN",
"ERZ": "",
"PINO": "",
"NOA": "",
"LULU": "",

View file

@ -8,8 +8,7 @@
// … F02 --probe nur anzeigen, was erzeugt würde (kostet nichts)
// … F02 --sprache es andere Sprache (de, es, fr; ohne Angabe de)
//
// Der Schlüssel kommt aus ELEVENLABS_API_KEY bzw. aus youtube/.env.local (angelegt mit
// `npx -y @elevenlabs/cli@latest onboard init`, nicht im Git) – nie aus dem Code.
// Der Schlüssel kommt nur aus der Umgebungsvariable, nie aus einer Datei im Repo.
// Stimmen-IDs und Modell stehen in werkzeuge/stimmen.config.json (je Sprache
// lassen sich dort unter „sprachen“ eigene Stimmen eintragen).
// Aufnahmen landen in public/stimmen/folgeNN/<sprache>/ (nicht im Git; auf dem VPS ein
@ -20,14 +19,9 @@ import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {Readable} from 'node:stream';
import {ElevenLabsClient} from '@elevenlabs/elevenlabs-js';
const WURZEL = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
process.chdir(WURZEL);
// Schlüssel aus der .env.local im Repo-Wurzelordner laden (falls nicht schon gesetzt)
const envDatei = path.join(WURZEL, '..', '.env.local');
if (!process.env.ELEVENLABS_API_KEY && fs.existsSync(envDatei)) process.loadEnvFile(envDatei);
const args = process.argv.slice(2);
const kuerzel = args.find((a) => !a.startsWith('--'));
@ -48,8 +42,7 @@ const cfg = JSON.parse(fs.readFileSync('werkzeuge/stimmen.config.json', 'utf8'))
einstellungen?: Record<string, unknown>;
};
const schluessel = process.env.ELEVENLABS_API_KEY;
if (!schluessel && !probe) throw new Error('ELEVENLABS_API_KEY ist nicht gesetzt (siehe youtube/.env.local).');
const client = probe ? null : new ElevenLabsClient(); // liest ELEVENLABS_API_KEY
if (!schluessel && !probe) throw new Error('ELEVENLABS_API_KEY ist nicht gesetzt.');
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
const ausgabe = spawnSync(npx, ['tsx', 'werkzeuge/szenenliste.mts', `./src/folge${nummer}/Folge${nummer}.tsx`, kuerzel], {encoding: 'utf8', shell: process.platform === 'win32', env: {...process.env, SPRACHE: sprache}});
@ -98,17 +91,13 @@ for (const s of szenen) {
console.log(`~ ${schl} (${z.wer}): ${z.text}`);
continue;
}
// Sprache festlegen (bessere Aussprache); eleven_v4 kennt nur stability und similarity_boost
const audio = await client!.textToSpeech.convert(stimme, {
text: z.text,
modelId: cfg.modell,
languageCode: sprache,
outputFormat: cfg.format as never,
voiceSettings: {stability: cfg.einstellungen?.stability as number, similarityBoost: cfg.einstellungen?.similarity_boost as number},
const antwort = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${stimme}?output_format=${cfg.format}`, {
method: 'POST',
headers: {'xi-api-key': schluessel!, 'Content-Type': 'application/json', Accept: 'audio/mpeg'},
body: JSON.stringify({text: z.text, model_id: cfg.modell, voice_settings: cfg.einstellungen}),
});
const teile: Buffer[] = [];
for await (const t of Readable.fromWeb(audio as never)) teile.push(Buffer.from(t));
fs.writeFileSync(pfad, Buffer.concat(teile));
if (!antwort.ok) throw new Error(`${schl}: ElevenLabs antwortet ${antwort.status} ${await antwort.text()}`);
fs.writeFileSync(pfad, Buffer.from(await antwort.arrayBuffer()));
aufnahmen[schl] = {datei, dauer: laenge(pfad), text: z.text};
fs.writeFileSync(jsonDatei, JSON.stringify(aufnahmen, null, '\t') + '\n');
erzeugt++;

View file

@ -3,7 +3,7 @@ title: YouTube-Kinderkanal mit KI-Videos — Ziel, Strategie, Stand
tags: [projekt, youtube, kinder, ki-video, monetarisierung, mehrsprachig, feiertage, higgsfield, adventskalender, pino]
description: Wenn es um den geplanten YouTube-Kanal für Kindervideos geht — Konzept, Wachstum, Sprachen, Feiertagsplanung, nächste Schritte oder „machen wir weiter mit YouTube".
erstellt: 2026-10-25
aktualisiert: 2026-11-02
aktualisiert: 2026-10-28
---
## ENTSCHIEDEN 02.11.2026
@ -11,34 +11,7 @@ aktualisiert: 2026-11-02
- **Kanalname: „Pinos kleine Welt“** (DE). ES „El pequeño mundo de Pino“ und FR „Le petit monde de Pino“ (ebenfalls entschieden 02.11.). Im ganzen Repo umgestellt (Intro-Titel und gesprochenes Intro, Kanaltext, Titelmuster, Agenten). Ältere Einträge zum Kanalnamen weiter unten sind damit erledigt.
- **Format für alle neuen Folgen: Remotion nach dem Muster von Folge 2** (`remotion/src/folge02/`). Offen vor Folge 3: Sprechtexte je Sprache getrennt (DE/ES/FR) und nur an einer Stelle; Musik/Geräusche; Stimmen aufnehmen.
## >>> STAND SESSION-ENDE 02.11.2026 — 03.11. GENAU HIER WEITERMACHEN <<<
**Nächster Schritt: Stimmen (ElevenLabs).**
1. Nutzerin prüft den ElevenLabs-API-Schlüssel **…55ad** (Developers → API Keys): Recht **Voices = Write** fehlt noch
(Fehler `missing_permissions voices_write`, auch nach ihrer Freigabe am 02.11. abends) → danach erneut versuchen.
2. Mit Schreibrecht: Erzählstimme **Gregory Grumble `8TMmdpPgqHKvDOGYP2lN`** (Bibliothek, DE/ES/FR geprüft; von Nutzerin
gewählt, schon in `remotion/werkzeuge/stimmen.config.json` als ERZ) per `client.voices.share(public_owner_id, id, …)` zu
„Meine Stimmen“ hinzufügen.
3. Nutzerin wählt Entwurf je Figur (Voice Design, `textToVoice.design`, eleven_ttv_v3), dann `client.textToVoice.create(
{voiceName, voiceDescription, generatedVoiceId})` → Voice-IDs in `stimmen.config.json` (PINO/NOA/LULU):
- Pino: noch keine Wahl — Entwürfe 1 `ltJMmAFouy3MnfG3mjaf`, 2 `yRkKzJ2oVloml0Y5VkgX`, 3 `UEaXv1VRYXWLjjmmwc8F`
- Noa: noch keine Wahl — 1 `d0nYhi0uYakUNAHn5VgB`, 2 `rzvlLFwRO4hNYHzYU0y7`, 3 `zPreEwpmnSlF8Z0JQd5U`
- Lulu: **Entwurf 1 `TAxEFZVDGH6NHy0wH1Lh` oder 3 `I9BlCOvyDbKlCBCrrLeU`** gefallen (zwischen beiden entscheiden)
- Hörproben: `C:\Users\lk-lo\Pino-Kanal\Stimmen-Entwuerfe\` (+ entwuerfe.json). generatedVoiceIds können verfallen →
notfalls neu entwerfen (Beschreibungen siehe unten).
- Beschreibungen, die durchgingen: „An adult (female) voice actor/actress performing … in an animated cartoon“ + Stimmart.
ElevenLabs blockiert alles, was als Kind beschrieben wird (auch Probetext „Ich bin erst fünf!“) — keine Umwege.
4. Dann `npx tsx werkzeuge/stimmen.mts F01 --probe`, danach echt erzeugen (DE, dann ES/FR mit `--sprache`), Folgen mit Ton rendern.
**Stand der Folgen (Remotion, beide vollständig, Entwürfe ohne Ton):** `Pino-Kanal\F01-Entwurf.mp4`, `F02-Entwurf.mp4`
(+ `-klein.mp4` fürs Handy). Am 02.11. umgesetzt: Folge 1 komplett neu (Herbstgarten, alter Apfelbaum, Laubhaufen wie
Higgsfield, Apfel-/Blumenübergabe), Folge 2 nach denselben Lehren überarbeitet, ruckelfreie Clips (Tempo 1,25),
POSEN_SKALA (Hocke/Sitzen/Liegen), Lulus Clip-Zoom zurückgerechnet (`clipkorrektur.ts`), neue Pose `noa_A_klatschen`
(1,5 Higgsfield-Credits) – Noa klatscht im Lied, Bilderbuch-Vögel, Kalender ohne Stift.
**Kanäle:** DE „Pinos kleine Welt“ + ES „El pequeño mundo de Pino“ eingerichtet (Beschreibung, Profilbild, Banner, Land,
Schlagwörter); FR wartet auf Ausweisprüfung bei YouTube. „Speziell für Kinder“ setzt die Nutzerin selbst.
## (älter) STAND SESSION-ENDE 28.10.2026 — 29.10. GENAU HIER WEITERMACHEN <<<
## >>> STAND SESSION-ENDE 28.10.2026 — 29.10. GENAU HIER WEITERMACHEN <<<
(Details in den 28.10.-Absätzen direkt darunter.)
**Nutzerin: „wir machen morgen genau da weiter“ = Lulu-Blumen-Szene in Folge 1 neu.** Kritik 28.10.: Lulu-Versuch, die Blume