Sprechtexte nach Sprachen getrennt (DE/ES/FR), Folge 2 in allen drei Sprachen renderbar
- Texte stehen nur noch in remotion/src/folge02/texte/<sprache>.json, die Szenen enthalten nur Ablauf und Bewegung - Sprache beim Rendern wählbar (render.mjs --sprache es, Studio über --props); Zeitplan richtet sich nach Textlänge bzw. Aufnahmen der Sprache - Baukasten-Wörter je Sprache in baukasten/sprache.ts: Kanalname im Buch (mit festem Umbruch für ES/FR), „Heute: …“, Sprechernamen, Kalendertitel, „Hatschi!“; Kalenderbild für ES/FR vorberechnet - Stimmen je Sprache: stimmen/<sprache>.json, public/stimmen/folge02/<sprache>/, stimmen.mts --sprache, eigene Stimmen je Sprache möglich - neues werkzeuge/texte.mts: prüft fehlende Übersetzungen, --tabelle zum Gegenlesen - README, CLAUDE.md und Drehbuch-Hinweise angepasst Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
764c7e1fdd
commit
60acaa9921
31 changed files with 516 additions and 112 deletions
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -6,7 +6,12 @@ ES „El pequeño mundo de Pino", FR „Le petit monde de Pino".
|
||||||
|
|
||||||
**Format ab Folge 2:** Folgen entstehen in Remotion nach dem Muster von
|
**Format ab Folge 2:** Folgen entstehen in Remotion nach dem Muster von
|
||||||
`remotion/src/folge02/` (Figuren-Bibliothek + Baukasten, siehe
|
`remotion/src/folge02/` (Figuren-Bibliothek + Baukasten, siehe
|
||||||
`remotion/README.md`), nicht mehr als Higgsfield-Clips.
|
`remotion/README.md`), nicht mehr als Higgsfield-Clips. Sobald eine Folge in
|
||||||
|
Remotion umgesetzt ist, stehen ihre Sprechtexte **nur noch** in
|
||||||
|
`remotion/src/folgeNN/texte/de.json`, `es.json`, `fr.json`; das Drehbuch
|
||||||
|
(`drehbuecher/…`) beschreibt Handlung und Bild. Prüfen mit
|
||||||
|
`npx tsx werkzeuge/texte.mts FNN` (im Ordner `remotion/`), Fassung je Sprache
|
||||||
|
mit `node werkzeuge/render.mjs FNN --sprache es`.
|
||||||
|
|
||||||
## Rahmen
|
## Rahmen
|
||||||
- **Zielgruppe:** Kinder von 3–6 Jahren.
|
- **Zielgruppe:** Kinder von 3–6 Jahren.
|
||||||
|
|
@ -49,6 +54,7 @@ Projektordner (`clips/`, `ton/`). Auf dem VPS liegen die unter
|
||||||
auf dem VPS im gemeinsamen Ordner `/shared/youtube/`.
|
auf dem VPS im gemeinsamen Ordner `/shared/youtube/`.
|
||||||
- **Drehbücher nur hier pflegen.** Das Repo ist die gültige Fassung; das
|
- **Drehbücher nur hier pflegen.** Das Repo ist die gültige Fassung; das
|
||||||
frühere Plan-Dokument (Artifact) dient nur noch als Übersicht.
|
frühere Plan-Dokument (Artifact) dient nur noch als Übersicht.
|
||||||
- Alle drei Sprachfassungen einer Folge gemeinsam ändern, damit sie
|
- Alle drei Sprachfassungen einer Folge gemeinsam ändern (bei Remotion-Folgen
|
||||||
|
in `texte/<sprache>.json`), damit sie
|
||||||
inhaltlich gleich bleiben.
|
inhaltlich gleich bleiben.
|
||||||
- Inhalte kindgerecht: keine Gewalt, keine Angstszenen, kurze Sätze.
|
- Inhalte kindgerecht: keine Gewalt, keine Angstszenen, kurze Sätze.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Drehbuch, deutsche Fassung, Entwurf vom 25.10.2026. Online am 22.11.2026.
|
Drehbuch, deutsche Fassung, Entwurf vom 25.10.2026. Online am 22.11.2026.
|
||||||
|
|
||||||
|
> **Sprechtexte fürs Video:** seit 02.11.2026 gilt `remotion/src/folge02/texte/de.json` (Zeilen-IDs wie `s2/e1`). Dieses Drehbuch beschreibt Handlung und Bild; bei Abweichungen im Text gilt die JSON-Datei.
|
||||||
|
|
||||||
## Eckdaten
|
## Eckdaten
|
||||||
|
|
||||||
Pino gähnt den ganzen Tag, und Lulu und die Erzählstimme erklären den Winterschlaf. Zu dritt bauen sie Pino ein Nest aus Blättern. Aber Pino will nicht schlafen, bevor das Winterfest da ist. Noa hat die Idee mit dem Kalender. So leitet die Folge in den Adventskalender ab 1. Dezember über.
|
Pino gähnt den ganzen Tag, und Lulu und die Erzählstimme erklären den Winterschlaf. Zu dritt bauen sie Pino ein Nest aus Blättern. Aber Pino will nicht schlafen, bevor das Winterfest da ist. Noa hat die Idee mit dem Kalender. So leitet die Folge in den Adventskalender ab 1. Dezember über.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Spanische Sprechfassung für ElevenLabs, Entwurf vom 26.10.2026. Neutrales Spanisch für Spanien und Lateinamerika (kein „vosotros“, kein „coger“). Die Bilder sind dieselben wie in der deutschen Fassung. *Kursiv* = Pausen und Geräusche, nicht einsprechen.
|
Spanische Sprechfassung für ElevenLabs, Entwurf vom 26.10.2026. Neutrales Spanisch für Spanien und Lateinamerika (kein „vosotros“, kein „coger“). Die Bilder sind dieselben wie in der deutschen Fassung. *Kursiv* = Pausen und Geräusche, nicht einsprechen.
|
||||||
|
|
||||||
|
> **Sprechtexte fürs Video:** seit 02.11.2026 gilt `remotion/src/folge02/texte/es.json`. Bei Abweichungen gilt die JSON-Datei.
|
||||||
|
|
||||||
## Escena 1: Intro (0:00–0:15)
|
## Escena 1: Intro (0:00–0:15)
|
||||||
|
|
||||||
*Música de la serie.*
|
*Música de la serie.*
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Französische Sprechfassung für ElevenLabs, Entwurf vom 26.10.2026. Die Figuren sagen „tu“, die Erzählstimme spricht das Kind mit „tu“ an. Die Bilder sind dieselben wie in der deutschen Fassung. *Kursiv* = Pausen und Geräusche, nicht einsprechen.
|
Französische Sprechfassung für ElevenLabs, Entwurf vom 26.10.2026. Die Figuren sagen „tu“, die Erzählstimme spricht das Kind mit „tu“ an. Die Bilder sind dieselben wie in der deutschen Fassung. *Kursiv* = Pausen und Geräusche, nicht einsprechen.
|
||||||
|
|
||||||
|
> **Sprechtexte fürs Video:** seit 02.11.2026 gilt `remotion/src/folge02/texte/fr.json`. Bei Abweichungen gilt die JSON-Datei.
|
||||||
|
|
||||||
## Scène 1 : Intro (0:00–0:15)
|
## Scène 1 : Intro (0:00–0:15)
|
||||||
|
|
||||||
*Générique.*
|
*Générique.*
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@ Mitmach-Pausen zeigen ein „?“.
|
||||||
| `src/baukasten/untertitel.tsx` | Untertitel, Mitmach-Pause, Stimmen abspielen |
|
| `src/baukasten/untertitel.tsx` | Untertitel, Mitmach-Pause, Stimmen abspielen |
|
||||||
| `src/baukasten/folge.tsx` | macht aus der Szenenliste die Kompositionen |
|
| `src/baukasten/folge.tsx` | macht aus der Szenenliste die Kompositionen |
|
||||||
| `src/baukasten/posen.ts` | automatisch erzeugt von `werkzeuge/posen.py` |
|
| `src/baukasten/posen.ts` | automatisch erzeugt von `werkzeuge/posen.py` |
|
||||||
| `src/folge02/` | Folge 2: `Folge02.tsx` (Szenenliste), `szene2.tsx` … `szene7.tsx`, `lied.tsx`, `stimmen.json` |
|
| `src/baukasten/sprache.ts` | gewählte Sprache und die Wörter des Baukastens je Sprache |
|
||||||
|
| `src/folge02/` | Folge 2: `Folge02.tsx` (Szenenliste), `szene2.tsx` … `szene7.tsx`, `lied.tsx`, `plan.ts`, `texte/<sprache>.json` (Sprechtexte), `stimmen/<sprache>.json` (Aufnahmen) |
|
||||||
| `public/bibliothek/figuren/` | freigestellte Standbilder (WebP, im Git) |
|
| `public/bibliothek/figuren/` | freigestellte Standbilder (WebP, im Git) |
|
||||||
| `public/bibliothek/clips/` | freigestellte Clips (WebM mit Alpha, nicht im Git; VPS: Link auf `/shared/youtube/figuren_bibliothek/clips_frei/`) |
|
| `public/bibliothek/clips/` | freigestellte Clips (WebM mit Alpha, nicht im Git; VPS: Link auf `/shared/youtube/figuren_bibliothek/clips_frei/`) |
|
||||||
| `public/stimmen/` | Sprachaufnahmen (nicht im Git; VPS: Link auf `/shared/youtube/stimmen/`) |
|
| `public/stimmen/` | Sprachaufnahmen (nicht im Git; VPS: Link auf `/shared/youtube/stimmen/`) |
|
||||||
|
|
@ -87,8 +88,9 @@ Mitmach-Pausen zeigen ein „?“.
|
||||||
**Kompositionen:** `F02` ist die ganze Folge, `F02-01-Intro`,
|
**Kompositionen:** `F02` ist die ganze Folge, `F02-01-Intro`,
|
||||||
`F02-02-Gaehnen-ueberall` … jede Szene einzeln.
|
`F02-02-Gaehnen-ueberall` … jede Szene einzeln.
|
||||||
|
|
||||||
**Neue Folge:** Ordner `src/folgeNN/` anlegen, `plan.ts` und `stimmen.json`
|
**Neue Folge:** Ordner `src/folgeNN/` anlegen, `plan.ts` aus Folge 2 kopieren,
|
||||||
(`{}`) aus Folge 2 kopieren und den Ordnernamen anpassen, Szenen nach dem
|
`stimmen/de.json`, `es.json`, `fr.json` mit `{}` anlegen, `texte/<sprache>.json`
|
||||||
|
mit den Sprechtexten füllen, Ordnernamen anpassen, Szenen nach dem
|
||||||
Muster von `szene2.tsx` schreiben (oben die Zeilen mit `plane('sX', [...])`,
|
Muster von `szene2.tsx` schreiben (oben die Zeilen mit `plane('sX', [...])`,
|
||||||
darunter Posenfolgen und Kamerafahrt, die sich auf `z.<zeile>.start/ende`
|
darunter Posenfolgen und Kamerafahrt, die sich auf `z.<zeile>.start/ende`
|
||||||
beziehen), in `FolgeNN.tsx` auflisten und in `src/Root.tsx` eintragen.
|
beziehen), in `FolgeNN.tsx` auflisten und in `src/Root.tsx` eintragen.
|
||||||
|
|
@ -99,7 +101,8 @@ node werkzeuge/render.mjs F02 --entwurf # schnelle Fassung zum Begutachten
|
||||||
node werkzeuge/render.mjs F02 # volle Qualität
|
node werkzeuge/render.mjs F02 # volle Qualität
|
||||||
node werkzeuge/render.mjs F02 --szene 3 # nur Szene 3 neu
|
node werkzeuge/render.mjs F02 --szene 3 # nur Szene 3 neu
|
||||||
```
|
```
|
||||||
Das Skript rendert jede Szene einzeln nach `out/F02/` und klebt sie
|
Mit `--sprache es` bzw. `fr` entsteht die jeweilige Fassung (siehe unten).
|
||||||
|
Das Skript rendert jede Szene einzeln nach `out/F02/<sprache>/` und klebt sie
|
||||||
zusammen. Szenen, deren Code, Bilder oder Aufnahmen sich nicht geändert
|
zusammen. Szenen, deren Code, Bilder oder Aufnahmen sich nicht geändert
|
||||||
haben, werden übersprungen. Im Entwurf stehen oben links Gesamtzeit, Szene,
|
haben, werden übersprungen. Im Entwurf stehen oben links Gesamtzeit, Szene,
|
||||||
Szenenzeit und Zeilen-ID – für Rückmeldungen einfach diese Angaben nennen
|
Szenenzeit und Zeilen-ID – für Rückmeldungen einfach diese Angaben nennen
|
||||||
|
|
@ -110,14 +113,36 @@ Auf dem VPS: vorher `export LD_LIBRARY_PATH=$HOME/.local/chrome-libs/root/usr/li
|
||||||
`npx remotion studio` öffnet alle Kompositionen im Browser; Änderungen am
|
`npx remotion studio` öffnet alle Kompositionen im Browser; Änderungen am
|
||||||
Code sind sofort zu sehen. Anleitung für einen Windows-PC: `ANLEITUNG_PC.md`.
|
Code sind sofort zu sehen. Anleitung für einen Windows-PC: `ANLEITUNG_PC.md`.
|
||||||
|
|
||||||
|
### Sprachen (DE, ES, FR)
|
||||||
|
Die Sprechtexte stehen **nur** in `src/folgeNN/texte/de.json`, `es.json`,
|
||||||
|
`fr.json` (Titel der Folge, Liedtitel und jede Zeile unter ihrem Schlüssel
|
||||||
|
`szene/zeile`, z. B. `s2/e1`). Die Szenendateien enthalten nur Ablauf und
|
||||||
|
Bewegung: `{id: 'e1', wer: 'ERZ'}` ohne Text. Wörter, die der Baukasten selbst
|
||||||
|
mitbringt (Kanalname, „Heute: …“, Sprechernamen im Untertitel, Kalendertitel,
|
||||||
|
„Hatschi!“), stehen in `src/baukasten/sprache.ts`.
|
||||||
|
|
||||||
|
Die Sprache wird beim Rendern gewählt; der Zeitplan richtet sich nach der
|
||||||
|
Länge der Texte bzw. Aufnahmen dieser Sprache, die Bewegungen rutschen mit.
|
||||||
|
```bash
|
||||||
|
npx tsx werkzeuge/texte.mts F02 # fehlt in einer Sprache etwas?
|
||||||
|
npx tsx werkzeuge/texte.mts F02 --tabelle # alle Sprachen nebeneinander zum Gegenlesen
|
||||||
|
node werkzeuge/render.mjs F02 --sprache es # spanische Fassung → out/F02/F02-es.mp4
|
||||||
|
npx remotion studio --props='{"sprache":"fr"}'
|
||||||
|
```
|
||||||
|
Orte mit Schrift im Bild (bisher nur `kalender`) gibt es je Sprache als
|
||||||
|
`public/orte/kalender_es.webp` usw. (vorberechnen mit
|
||||||
|
`--props='{"sprache":"es"}'`). Fehlt eine Übersetzung, zeigt der Untertitel
|
||||||
|
`[fehlt: s2/e1]`.
|
||||||
|
|
||||||
### Stimmen (ElevenLabs)
|
### Stimmen (ElevenLabs)
|
||||||
1. Stimmen-IDs je Sprecher in `werkzeuge/stimmen.config.json` eintragen
|
1. Stimmen-IDs je Sprecher in `werkzeuge/stimmen.config.json` eintragen
|
||||||
(`ALLE` und `NOA_LULU` verweisen auf einen anderen Sprecher).
|
(`ALLE` und `NOA_LULU` verweisen auf einen anderen Sprecher). Andere Stimmen
|
||||||
|
für eine Sprache unter `"sprachen": {"es": {"stimmen": {"ERZ": "…"}}}`.
|
||||||
2. `npx tsx werkzeuge/stimmen.mts F02 --probe` zeigt, welche Zeilen erzeugt
|
2. `npx tsx werkzeuge/stimmen.mts F02 --probe` zeigt, welche Zeilen erzeugt
|
||||||
würden, und zählt die Zeichen (kostet nichts).
|
würden, und zählt die Zeichen (kostet nichts).
|
||||||
3. `ELEVENLABS_API_KEY=… npx tsx werkzeuge/stimmen.mts F02` erzeugt die
|
3. `ELEVENLABS_API_KEY=… npx tsx werkzeuge/stimmen.mts F02 --sprache es` erzeugt die
|
||||||
Aufnahmen nach `public/stimmen/folge02/` und trägt ihre Länge in
|
Aufnahmen nach `public/stimmen/folge02/es/` und trägt ihre Länge in
|
||||||
`src/folge02/stimmen.json` ein. Der Zeitplan richtet sich danach; ändert
|
`src/folge02/stimmen/es.json` ein (ohne `--sprache`: Deutsch). Der Zeitplan richtet sich danach; ändert
|
||||||
sich ein Text, wird nur diese Zeile neu erzeugt.
|
sich ein Text, wird nur diese Zeile neu erzeugt.
|
||||||
|
|
||||||
Untertitel ausblenden: `--props='{"untertitel": false}'` (bzw. in
|
Untertitel ausblenden: `--props='{"untertitel": false}'` (bzw. in
|
||||||
|
|
|
||||||
BIN
remotion/public/orte/kalender_es.webp
Normal file
BIN
remotion/public/orte/kalender_es.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
remotion/public/orte/kalender_fr.webp
Normal file
BIN
remotion/public/orte/kalender_fr.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
|
|
@ -4,6 +4,7 @@ import {kurve} from '../bewegung';
|
||||||
import {Figur, Kamera} from './figur';
|
import {Figur, Kamera} from './figur';
|
||||||
import {Blende, Vignette, kamFahrt} from './helfer';
|
import {Blende, Vignette, kamFahrt} from './helfer';
|
||||||
import {Blatt} from './natur';
|
import {Blatt} from './natur';
|
||||||
|
import {W} from './sprache';
|
||||||
import {SCHRIFT, Untertitel} from './untertitel';
|
import {SCHRIFT, Untertitel} from './untertitel';
|
||||||
import {plane, type Plan, type Zeile} from './zeit';
|
import {plane, type Plan, type Zeile} from './zeit';
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ const Seite: React.FC<{links: number; children?: React.ReactNode}> = ({links, ch
|
||||||
);
|
);
|
||||||
|
|
||||||
const Titel: React.FC<{text: string; groesse: number; farbe?: string; y: number}> = ({text, groesse, farbe = '#c8642c', y}) => (
|
const Titel: React.FC<{text: string; groesse: number; farbe?: string; y: number}> = ({text, groesse, farbe = '#c8642c', y}) => (
|
||||||
<div style={{position: 'absolute', left: 0, right: 0, top: y, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: groesse, color: farbe}}>{text}</div>
|
<div style={{position: 'absolute', left: 0, right: 0, top: y, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: groesse, lineHeight: 1.05, whiteSpace: 'pre-line', color: farbe}}>{text}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Deckel: dreht sich um den Buchrücken (0° = zu, -180° = offen)
|
// Deckel: dreht sich um den Buchrücken (0° = zu, -180° = offen)
|
||||||
|
|
@ -49,7 +50,7 @@ const Deckel: React.FC<{winkel: number}> = ({winkel}) => (
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{position: 'absolute', left: 40, right: 40, top: 40, bottom: 40, border: '6px solid #e9c97a', borderRadius: 14}} />
|
<div style={{position: 'absolute', left: 40, right: 40, top: 40, bottom: 40, border: '6px solid #e9c97a', borderRadius: 14}} />
|
||||||
<Titel text="Pinos kleine Welt" groesse={84} farbe="#f6dc95" y={120} />
|
<Titel text={W.kanalZeilen.join('\n')} groesse={W.kanalZeilen.length > 1 ? 72 : 84} farbe="#f6dc95" y={W.kanalZeilen.length > 1 ? 92 : 120} />
|
||||||
<div style={{position: 'absolute', left: 0, right: 0, top: 260, height: 360}}>
|
<div style={{position: 'absolute', left: 0, right: 0, top: 260, height: 360}}>
|
||||||
<Figur pose="pino_A3_lachen" x={250} y={330} hoehe={120} schatten={false} />
|
<Figur pose="pino_A3_lachen" x={250} y={330} hoehe={120} schatten={false} />
|
||||||
<Figur pose="noa_A3_lachen" x={370} y={330} hoehe={250} schatten={false} />
|
<Figur pose="noa_A3_lachen" x={370} y={330} hoehe={250} schatten={false} />
|
||||||
|
|
@ -61,8 +62,8 @@ const Deckel: React.FC<{winkel: number}> = ({winkel}) => (
|
||||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||||
<Blatt key={i} x={120 + i * 95} y={600 + Math.sin(i * 2.1) * 40} r={i * 47} s={0.9} farbe={['#c8642c', '#d99a2b', '#a8452a'][i % 3]} />
|
<Blatt key={i} x={120 + i * 95} y={600 + Math.sin(i * 2.1) * 40} r={i * 47} s={0.9} farbe={['#c8642c', '#d99a2b', '#a8452a'][i % 3]} />
|
||||||
))}
|
))}
|
||||||
<Titel text="Pinos kleine Welt" groesse={70} y={230} />
|
<Titel text={W.kanalZeilen.join('\n')} groesse={70} y={W.kanalZeilen.length > 1 ? 180 : 230} />
|
||||||
<Titel text="Eine Geschichte aus Noas Garten" groesse={34} farbe="#8a6a4a" y={340} />
|
<Titel text={W.buchUntertitel} groesse={34} farbe="#8a6a4a" y={W.kanalZeilen.length > 1 ? 365 : 340} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -94,8 +95,8 @@ const Tisch: React.FC = () => (
|
||||||
// Die Folge plant die Zeilen selbst (mit `introZeilen`), damit Aufnahmen greifen.
|
// Die Folge plant die Zeilen selbst (mit `introZeilen`), damit Aufnahmen greifen.
|
||||||
export const introZeilen = (folgenTitel: string): Zeile[] => [
|
export const introZeilen = (folgenTitel: string): Zeile[] => [
|
||||||
{id: 'buch', dauer: 2.2},
|
{id: 'buch', dauer: 2.2},
|
||||||
{id: 't1', wer: 'ERZ', text: 'Pinos kleine Welt.'},
|
{id: 't1', wer: 'ERZ', text: `${W.kanal}.`},
|
||||||
{id: 't2', wer: 'ERZ', text: `Heute: ${folgenTitel}.`},
|
{id: 't2', wer: 'ERZ', text: W.heute(folgenTitel)},
|
||||||
];
|
];
|
||||||
export const INTRO_OPTIONEN = {vorlauf: 0.3, nachlauf: 1.6};
|
export const INTRO_OPTIONEN = {vorlauf: 0.3, nachlauf: 1.6};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {AbsoluteFill, Img, random, staticFile} from 'remotion';
|
import {AbsoluteFill, Img, random, staticFile} from 'remotion';
|
||||||
|
import {SPRACHE, W} from './sprache';
|
||||||
import './untertitel'; // lädt die Schrift für den Kalender
|
import './untertitel'; // lädt die Schrift für den Kalender
|
||||||
import {NEST_BILD, NestBlaetter} from './natur';
|
import {NEST_BILD, NestBlaetter} from './natur';
|
||||||
|
|
||||||
|
|
@ -330,7 +331,7 @@ export const KalenderSVG: React.FC<{leer?: boolean}> = ({leer = false}) => {
|
||||||
<rect x={250} y={90} width={1420} height={900} fill="#fdf8ec" filter="url(#aq)" />
|
<rect x={250} y={90} width={1420} height={900} fill="#fdf8ec" filter="url(#aq)" />
|
||||||
{!leer && <g filter="url(#stift)">
|
{!leer && <g filter="url(#stift)">
|
||||||
<text x={960} y={190} textAnchor="middle" fontSize={78} fontFamily="Fredoka, sans-serif" fontWeight={600} fill="#c8642c">
|
<text x={960} y={190} textAnchor="middle" fontSize={78} fontFamily="Fredoka, sans-serif" fontWeight={600} fill="#c8642c">
|
||||||
Pinos Winterfest-Kalender
|
{W.kalenderTitel}
|
||||||
</text>
|
</text>
|
||||||
{nummern.map((n, i) => {
|
{nummern.map((n, i) => {
|
||||||
const x = 330 + (i % 6) * 215;
|
const x = 330 + (i % 6) * 215;
|
||||||
|
|
@ -430,7 +431,7 @@ export const KalenderMalen: React.FC<{p: number; wackeln: number}> = ({p, wackel
|
||||||
<rect x={400} y={100} width={tp * 1120} height={120} />
|
<rect x={400} y={100} width={tp * 1120} height={120} />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<text x={960} y={190} textAnchor="middle" fontSize={78} fontFamily="Fredoka, sans-serif" fontWeight={600} fill="#c8642c" clipPath="url(#titelclip)">
|
<text x={960} y={190} textAnchor="middle" fontSize={78} fontFamily="Fredoka, sans-serif" fontWeight={600} fill="#c8642c" clipPath="url(#titelclip)">
|
||||||
Pinos Winterfest-Kalender
|
{W.kalenderTitel}
|
||||||
</text>
|
</text>
|
||||||
{new Array(24).fill(0).map((_, i) => {
|
{new Array(24).fill(0).map((_, i) => {
|
||||||
if (i > aktiv || p < titelAnteil) return null;
|
if (i > aktiv || p < titelAnteil) return null;
|
||||||
|
|
@ -473,7 +474,11 @@ export const KalenderMalen: React.FC<{p: number; wackeln: number}> = ({p, wackel
|
||||||
};
|
};
|
||||||
|
|
||||||
// Vorberechnete Bilder einbinden
|
// Vorberechnete Bilder einbinden
|
||||||
export const Ort: React.FC<{name: string}> = ({name}) => <Img src={staticFile(`orte/${name}.webp`)} style={{position: 'absolute', inset: 0, width: B, height: H}} />;
|
// Orte mit Schrift im Bild gibt es je Sprache (orte/kalender_es.webp …)
|
||||||
|
export const SPRACH_ORTE = ['kalender'];
|
||||||
|
const ortDatei = (name: string) => (SPRACH_ORTE.includes(name) && SPRACHE !== 'de' ? `${name}_${SPRACHE}` : name);
|
||||||
|
|
||||||
|
export const Ort: React.FC<{name: string}> = ({name}) => <Img src={staticFile(`orte/${ortDatei(name)}.webp`)} style={{position: 'absolute', inset: 0, width: B, height: H}} />;
|
||||||
|
|
||||||
// Kompositionen zum Vorberechnen
|
// Kompositionen zum Vorberechnen
|
||||||
export const ORTE: Record<string, React.FC> = {
|
export const ORTE: Record<string, React.FC> = {
|
||||||
|
|
|
||||||
72
remotion/src/baukasten/sprache.ts
Normal file
72
remotion/src/baukasten/sprache.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import {getInputProps} from 'remotion';
|
||||||
|
import type {Sprecher} from './zeit';
|
||||||
|
|
||||||
|
// Sprache des Videos. Wird beim Rendern bzw. in der Vorschau gewählt:
|
||||||
|
// node werkzeuge/render.mjs F02 --sprache es
|
||||||
|
// npx remotion studio --props='{"sprache":"fr"}'
|
||||||
|
// In Node-Skripten (szenenliste, stimmen) über die Umgebungsvariable SPRACHE.
|
||||||
|
// Ohne Angabe: Deutsch.
|
||||||
|
|
||||||
|
export const SPRACHEN = ['de', 'es', 'fr'] as const;
|
||||||
|
export type Sprache = (typeof SPRACHEN)[number];
|
||||||
|
|
||||||
|
const waehle = (): Sprache => {
|
||||||
|
const s = typeof window === 'undefined' ? (typeof process === 'undefined' ? undefined : process.env.SPRACHE) : getInputProps().sprache;
|
||||||
|
return SPRACHEN.includes(s as Sprache) ? (s as Sprache) : 'de';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SPRACHE: Sprache = waehle();
|
||||||
|
|
||||||
|
// Wörter, die der Baukasten selbst ins Bild oder in den Ton bringt
|
||||||
|
type Woerter = {
|
||||||
|
kanal: string;
|
||||||
|
kanalZeilen: string[]; // Umbruch auf dem Buchdeckel
|
||||||
|
buchUntertitel: string;
|
||||||
|
heute: (titel: string) => string;
|
||||||
|
namen: Record<Sprecher, string>;
|
||||||
|
leise: string;
|
||||||
|
kalenderTitel: string;
|
||||||
|
hatschi: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WOERTER: Record<Sprache, Woerter> = {
|
||||||
|
de: {
|
||||||
|
kanal: 'Pinos kleine Welt',
|
||||||
|
kanalZeilen: ['Pinos kleine Welt'],
|
||||||
|
buchUntertitel: 'Eine Geschichte aus Noas Garten',
|
||||||
|
heute: (t) => `Heute: ${t}.`,
|
||||||
|
namen: {ERZ: 'Erzählstimme', PINO: 'Pino', NOA: 'Noa', LULU: 'Lulu', ALLE: 'Alle', NOA_LULU: 'Noa und Lulu'},
|
||||||
|
leise: '(leise)',
|
||||||
|
kalenderTitel: 'Pinos Winterfest-Kalender',
|
||||||
|
hatschi: 'Hatschi!',
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
kanal: 'El pequeño mundo de Pino',
|
||||||
|
kanalZeilen: ['El pequeño mundo', 'de Pino'],
|
||||||
|
buchUntertitel: 'Una historia del jardín de Noa',
|
||||||
|
heute: (t) => `Hoy: ${t}.`,
|
||||||
|
namen: {ERZ: 'Narración', PINO: 'Pino', NOA: 'Noa', LULU: 'Lulu', ALLE: 'Todos', NOA_LULU: 'Noa y Lulu'},
|
||||||
|
leise: '(bajito)',
|
||||||
|
kalenderTitel: 'El calendario de Pino',
|
||||||
|
hatschi: '¡Achís!',
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
kanal: 'Le petit monde de Pino',
|
||||||
|
kanalZeilen: ['Le petit monde', 'de Pino'],
|
||||||
|
buchUntertitel: 'Une histoire du jardin de Noa',
|
||||||
|
heute: (t) => `Aujourd'hui : ${t}.`,
|
||||||
|
namen: {ERZ: 'Narration', PINO: 'Pino', NOA: 'Noa', LULU: 'Lulu', ALLE: 'Tous', NOA_LULU: 'Noa et Lulu'},
|
||||||
|
leise: '(doucement)',
|
||||||
|
kalenderTitel: 'Le calendrier de Pino',
|
||||||
|
hatschi: 'Atchoum !',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const W = WOERTER[SPRACHE];
|
||||||
|
|
||||||
|
// Texte einer Folge: je Sprache eine Datei src/folgeNN/texte/<sprache>.json
|
||||||
|
export type FolgenTexte = {
|
||||||
|
titel: string;
|
||||||
|
lied?: string;
|
||||||
|
zeilen: Record<string, string>; // Schlüssel „szene/zeile“, z. B. „s2/e1“
|
||||||
|
};
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Audio, Sequence, getInputProps, getStaticFiles, interpolate, staticFile, useCurrentFrame} from 'remotion';
|
import {Audio, Sequence, getInputProps, getStaticFiles, interpolate, staticFile, useCurrentFrame} from 'remotion';
|
||||||
import {loadFont} from '@remotion/google-fonts/Fredoka';
|
import {loadFont} from '@remotion/google-fonts/Fredoka';
|
||||||
|
import {W} from './sprache';
|
||||||
import type {Geplant, Sprecher} from './zeit';
|
import type {Geplant, Sprecher} from './zeit';
|
||||||
|
|
||||||
export const {fontFamily: SCHRIFT} = loadFont('normal', {weights: ['500', '600'], subsets: ['latin']});
|
export const {fontFamily: SCHRIFT} = loadFont('normal', {weights: ['500', '600'], subsets: ['latin']});
|
||||||
|
|
||||||
const NAME: Record<Sprecher, string> = {
|
const NAME = W.namen;
|
||||||
ERZ: 'Erzählstimme',
|
|
||||||
PINO: 'Pino',
|
|
||||||
NOA: 'Noa',
|
|
||||||
LULU: 'Lulu',
|
|
||||||
ALLE: 'Alle',
|
|
||||||
NOA_LULU: 'Noa und Lulu',
|
|
||||||
};
|
|
||||||
const FARBE: Record<Sprecher, string> = {
|
const FARBE: Record<Sprecher, string> = {
|
||||||
ERZ: '#7a6f66',
|
ERZ: '#7a6f66',
|
||||||
PINO: '#3f5a8a',
|
PINO: '#3f5a8a',
|
||||||
|
|
@ -74,7 +68,7 @@ export const Untertitel: React.FC<{zeilen: Geplant[]}> = ({zeilen}) => {
|
||||||
>
|
>
|
||||||
<div style={{fontSize: 26, fontWeight: 600, color: FARBE[z.wer], letterSpacing: 1}}>
|
<div style={{fontSize: 26, fontWeight: 600, color: FARBE[z.wer], letterSpacing: 1}}>
|
||||||
{NAME[z.wer]}
|
{NAME[z.wer]}
|
||||||
{z.leise ? ' (leise)' : ''}
|
{z.leise ? ` ${W.leise}` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style={{fontSize: 46, fontWeight: 500, color: '#3b2f28', lineHeight: 1.2, fontStyle: z.leise ? 'italic' : 'normal'}}>{z.text}</div>
|
<div style={{fontSize: 46, fontWeight: 500, color: '#3b2f28', lineHeight: 1.2, fontStyle: z.leise ? 'italic' : 'normal'}}>{z.text}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// Zeitplan einer Szene aus ihren Sprechzeilen.
|
// Zeitplan einer Szene aus ihren Sprechzeilen.
|
||||||
//
|
//
|
||||||
// Ohne Sprachaufnahme wird die Dauer jeder Zeile aus der Textlänge geschätzt.
|
// Ohne Sprachaufnahme wird die Dauer jeder Zeile aus der Textlänge geschätzt.
|
||||||
// Gibt es eine Aufnahme (werkzeuge/stimmen.ts schreibt sie in die
|
// Gibt es eine Aufnahme (werkzeuge/stimmen.mts schreibt sie in die
|
||||||
// stimmen.json der Folge), gilt ihre echte Länge, und alle Bewegungen, die
|
// stimmen/<sprache>.json der Folge), gilt ihre echte Länge, und alle Bewegungen, die
|
||||||
// sich auf die Zeilen beziehen, rutschen automatisch mit.
|
// sich auf die Zeilen beziehen, rutschen automatisch mit.
|
||||||
|
|
||||||
export const FPS = 30;
|
export const FPS = 30;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import {INTRO_OPTIONEN, Intro, OUTRO, Outro, introZeilen} from '../baukasten/buch';
|
import {INTRO_OPTIONEN, Intro, OUTRO, Outro, introZeilen} from '../baukasten/buch';
|
||||||
import {baueFolge, type SzenenEintrag} from '../baukasten/folge';
|
import {baueFolge, type SzenenEintrag} from '../baukasten/folge';
|
||||||
import {LIED, Lied} from './lied';
|
import {LIED, Lied} from './lied';
|
||||||
import {plane} from './plan';
|
import {TITEL, plane} from './plan';
|
||||||
import {S2, Szene2} from './szene2';
|
import {S2, Szene2} from './szene2';
|
||||||
import {S3, Szene3} from './szene3';
|
import {S3, Szene3} from './szene3';
|
||||||
import {S4, Szene4} from './szene4';
|
import {S4, Szene4} from './szene4';
|
||||||
|
|
@ -12,7 +12,6 @@ import {S7, Szene7} from './szene7';
|
||||||
|
|
||||||
// Folge 2 „Pino ist müde“ – komplett per Code aus der Figuren-Bibliothek.
|
// Folge 2 „Pino ist müde“ – komplett per Code aus der Figuren-Bibliothek.
|
||||||
|
|
||||||
const TITEL = 'Pino ist müde';
|
|
||||||
const INTRO = plane('intro', introZeilen(TITEL), INTRO_OPTIONEN);
|
const INTRO = plane('intro', introZeilen(TITEL), INTRO_OPTIONEN);
|
||||||
|
|
||||||
export const SZENEN: SzenenEintrag[] = [
|
export const SZENEN: SzenenEintrag[] = [
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {Figur, Kamera} from '../baukasten/figur';
|
||||||
import {Blende, Vignette} from '../baukasten/helfer';
|
import {Blende, Vignette} from '../baukasten/helfer';
|
||||||
import {BODEN, Ort} from '../baukasten/orte';
|
import {BODEN, Ort} from '../baukasten/orte';
|
||||||
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
|
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
|
||||||
import {plane} from './plan';
|
import {TEXTE, plane} from './plan';
|
||||||
|
|
||||||
// Refrain „Einer klein und einer groß“ (aus Folge 1), einmal. Die drei wippen im Takt.
|
// Refrain „Einer klein und einer groß“ (aus Folge 1), einmal. Die drei wippen im Takt.
|
||||||
|
|
||||||
|
|
@ -12,10 +12,10 @@ const TAKT = 19; // Frames pro Schlag (ca. 95 bpm)
|
||||||
|
|
||||||
export const LIED = plane('lied',
|
export const LIED = plane('lied',
|
||||||
[
|
[
|
||||||
{id: 'r1', wer: 'ALLE', text: 'Einer klein und einer groß,', dauer: 3.2},
|
{id: 'r1', wer: 'ALLE', dauer: 3.2},
|
||||||
{id: 'r2', wer: 'ALLE', text: 'und dazwischen – na, wer bloß?', dauer: 3.2},
|
{id: 'r2', wer: 'ALLE', dauer: 3.2},
|
||||||
{id: 'r3', wer: 'ALLE', text: 'Noa steht in der Mitte drin,', dauer: 3.2},
|
{id: 'r3', wer: 'ALLE', dauer: 3.2},
|
||||||
{id: 'r4', wer: 'ALLE', text: 'zusammen kriegen wir alles hin!', dauer: 3.6},
|
{id: 'r4', wer: 'ALLE', dauer: 3.6},
|
||||||
],
|
],
|
||||||
{vorlauf: 1.0, nachlauf: 1.0},
|
{vorlauf: 1.0, nachlauf: 1.0},
|
||||||
);
|
);
|
||||||
|
|
@ -56,7 +56,7 @@ export const Lied: React.FC = () => {
|
||||||
return <div key={i} style={{opacity: Math.sin(t * Math.PI) * 0.9}}><Note x={x} y={700 - t * 600} s={0.8 + random(`ns${i}`) * 0.6} farbe={['#c8642c', '#3f5a8a', '#5f8a4a', '#a0526b'][i % 4]} /></div>;
|
return <div key={i} style={{opacity: Math.sin(t * Math.PI) * 0.9}}><Note x={x} y={700 - t * 600} s={0.8 + random(`ns${i}`) * 0.6} farbe={['#c8642c', '#3f5a8a', '#5f8a4a', '#a0526b'][i % 4]} /></div>;
|
||||||
})}
|
})}
|
||||||
<div style={{position: 'absolute', top: 60, left: 0, right: 0, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: 40, color: '#8a6a4a', opacity: Math.min(1, f / 20)}}>
|
<div style={{position: 'absolute', top: 60, left: 0, right: 0, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: 40, color: '#8a6a4a', opacity: Math.min(1, f / 20)}}>
|
||||||
♫ Einer klein und einer groß ♫
|
♫ {TEXTE.lied} ♫
|
||||||
</div>
|
</div>
|
||||||
<Vignette />
|
<Vignette />
|
||||||
<Untertitel zeilen={LIED.liste} />
|
<Untertitel zeilen={LIED.liste} />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,29 @@
|
||||||
|
import {SPRACHE, type FolgenTexte} from '../baukasten/sprache';
|
||||||
import {plane as basis, type PlanOptionen, type Zeile} from '../baukasten/zeit';
|
import {plane as basis, type PlanOptionen, type Zeile} from '../baukasten/zeit';
|
||||||
import AUFNAHMEN from './stimmen.json';
|
import DE from './texte/de.json';
|
||||||
|
import ES from './texte/es.json';
|
||||||
|
import FR from './texte/fr.json';
|
||||||
|
import STIMMEN_DE from './stimmen/de.json';
|
||||||
|
import STIMMEN_ES from './stimmen/es.json';
|
||||||
|
import STIMMEN_FR from './stimmen/fr.json';
|
||||||
|
|
||||||
// Zeitplan einer Szene dieser Folge; nimmt vorhandene Sprachaufnahmen
|
// Sprechtexte stehen nur in texte/<sprache>.json (Schlüssel „szene/zeile“),
|
||||||
// (public/stimmen/folge02/, erzeugt von werkzeuge/stimmen.ts) automatisch mit.
|
// die Szenen enthalten nur Ablauf und Bewegung. Aufnahmen je Sprache in
|
||||||
|
// stimmen/<sprache>.json bzw. public/stimmen/folge02/<sprache>/ (erzeugt von
|
||||||
|
// werkzeuge/stimmen.mts).
|
||||||
|
|
||||||
|
export const TEXTE: FolgenTexte = {de: DE, es: ES, fr: FR}[SPRACHE];
|
||||||
|
const AUFNAHMEN = {de: STIMMEN_DE, es: STIMMEN_ES, fr: STIMMEN_FR}[SPRACHE];
|
||||||
|
|
||||||
|
export const TITEL = TEXTE.titel;
|
||||||
|
|
||||||
|
// Zeitplan einer Szene dieser Folge, mit den Texten der gewählten Sprache
|
||||||
export const plane = (szene: string, zeilen: Zeile[], opt: PlanOptionen = {}) =>
|
export const plane = (szene: string, zeilen: Zeile[], opt: PlanOptionen = {}) =>
|
||||||
basis(zeilen, {...opt, szene, aufnahmen: AUFNAHMEN, ordner: 'stimmen/folge02'});
|
basis(
|
||||||
|
zeilen.map((z) => {
|
||||||
|
if (z.text !== undefined || !z.wer) return z;
|
||||||
|
const text = TEXTE.zeilen[`${szene}/${z.id}`];
|
||||||
|
return {...z, text: text ?? `[fehlt: ${szene}/${z.id}]`};
|
||||||
|
}),
|
||||||
|
{...opt, szene, aufnahmen: AUFNAHMEN, ordner: `stimmen/folge02/${SPRACHE}`},
|
||||||
|
);
|
||||||
|
|
|
||||||
1
remotion/src/folge02/stimmen/es.json
Normal file
1
remotion/src/folge02/stimmen/es.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
1
remotion/src/folge02/stimmen/fr.json
Normal file
1
remotion/src/folge02/stimmen/fr.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -15,18 +15,18 @@ export const PINO_X = 1060;
|
||||||
|
|
||||||
export const S2 = plane('s2', [
|
export const S2 = plane('s2', [
|
||||||
{id: 'bild0', dauer: 2.5},
|
{id: 'bild0', dauer: 2.5},
|
||||||
{id: 'e1', wer: 'ERZ', text: 'Es ist kalt geworden in Noas Garten.'},
|
{id: 'e1', wer: 'ERZ'},
|
||||||
{id: 'e2', wer: 'ERZ', text: 'Die Bäume haben fast alle Blätter verloren.'},
|
{id: 'e2', wer: 'ERZ'},
|
||||||
{id: 'bild1', dauer: 2.6},
|
{id: 'bild1', dauer: 2.6},
|
||||||
{id: 'p1', wer: 'PINO', text: 'Hoaaah.', dauer: 2.2},
|
{id: 'p1', wer: 'PINO', dauer: 2.2},
|
||||||
{id: 'n1', wer: 'NOA', text: 'Guten Morgen, Pino! Wollen wir spielen?'},
|
{id: 'n1', wer: 'NOA'},
|
||||||
{id: 'p2', wer: 'PINO', text: 'Ja! Ich bin ganz wach. Gaaanz … wach …', dauer: 4.6},
|
{id: 'p2', wer: 'PINO', dauer: 4.6},
|
||||||
{id: 'bild2', dauer: 3.2},
|
{id: 'bild2', dauer: 3.2},
|
||||||
{id: 'n2', wer: 'NOA', text: 'Pino? Pino!', leise: true, dauer: 2.2},
|
{id: 'n2', wer: 'NOA', leise: true, dauer: 2.2},
|
||||||
{id: 'bild3', dauer: 1.0},
|
{id: 'bild3', dauer: 1.0},
|
||||||
{id: 'p3', wer: 'PINO', text: 'Was? Ich hab nicht geschlafen!'},
|
{id: 'p3', wer: 'PINO'},
|
||||||
{id: 'e3', wer: 'ERZ', text: 'Hat er doch.', dauer: 1.6},
|
{id: 'e3', wer: 'ERZ', dauer: 1.6},
|
||||||
{id: 'e4', wer: 'ERZ', text: 'Was meinst du: Warum ist Pino so müde?', pause: 3},
|
{id: 'e4', wer: 'ERZ', pause: 3},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const Szene2: React.FC = () => {
|
export const Szene2: React.FC = () => {
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,19 @@ export const LULU_X = 1520;
|
||||||
export const S3 = plane('s3', [
|
export const S3 = plane('s3', [
|
||||||
{id: 'bild0', dauer: 3.2},
|
{id: 'bild0', dauer: 3.2},
|
||||||
{id: 'bild1', dauer: 2.4},
|
{id: 'bild1', dauer: 2.4},
|
||||||
{id: 'l1', wer: 'LULU', text: 'Guten Morgen, ihr zwei!'},
|
{id: 'l1', wer: 'LULU'},
|
||||||
{id: 'l2', wer: 'LULU', text: 'Von hier oben sehe ich: Die Vögel fliegen weg, ganz weit in den Süden.'},
|
{id: 'l2', wer: 'LULU'},
|
||||||
{id: 'l3', wer: 'LULU', text: 'Und die Bäume sind kahl. Der Winter kommt.'},
|
{id: 'l3', wer: 'LULU'},
|
||||||
{id: 'e1', wer: 'ERZ', text: 'Wenn es kalt wird, finden Igel kaum noch etwas zu fressen.'},
|
{id: 'e1', wer: 'ERZ'},
|
||||||
{id: 'e2', wer: 'ERZ', text: 'Darum machen sie etwas ganz Besonderes. Sie schlafen.'},
|
{id: 'e2', wer: 'ERZ'},
|
||||||
{id: 'e3', wer: 'ERZ', text: 'Den ganzen Winter lang, bis zum Frühling. Das heißt Winterschlaf.'},
|
{id: 'e3', wer: 'ERZ'},
|
||||||
{id: 'p1', wer: 'PINO', text: 'Den ganzen Winter? Aber dann verpasse ich ja das Winterfest!'},
|
{id: 'p1', wer: 'PINO'},
|
||||||
{id: 'bild2', dauer: 2.4},
|
{id: 'bild2', dauer: 2.4},
|
||||||
{id: 'e4', wer: 'ERZ', text: 'Schau mal, Pinos Stacheln hängen ganz schlaff herunter.'},
|
{id: 'e4', wer: 'ERZ'},
|
||||||
{id: 'e5', wer: 'ERZ', text: 'Wie fühlt sich Pino jetzt?', pause: 3},
|
{id: 'e5', wer: 'ERZ', pause: 3},
|
||||||
{id: 'e6', wer: 'ERZ', text: 'Ja. Pino ist traurig.'},
|
{id: 'e6', wer: 'ERZ'},
|
||||||
{id: 'bild3', dauer: 2.6},
|
{id: 'bild3', dauer: 2.6},
|
||||||
{id: 'n1', wer: 'NOA', text: 'Ach, Pino. Bis zum Winterfest ist es doch noch ganz lange hin.'},
|
{id: 'n1', wer: 'NOA'},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Das große Blatt, auf das Pino sich setzt
|
// Das große Blatt, auf das Pino sich setzt
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {Blaetterregen, Blatt, LAUB, Nest, Ruf} from '../baukasten/natur';
|
||||||
import {BODEN, NEST_X, Ort} from '../baukasten/orte';
|
import {BODEN, NEST_X, Ort} from '../baukasten/orte';
|
||||||
import {NOA_X, PINO_X} from './szene2';
|
import {NOA_X, PINO_X} from './szene2';
|
||||||
import {GROSSES_BLATT_X} from './szene3';
|
import {GROSSES_BLATT_X} from './szene3';
|
||||||
|
import {W} from '../baukasten/sprache';
|
||||||
import {Untertitel} from '../baukasten/untertitel';
|
import {Untertitel} from '../baukasten/untertitel';
|
||||||
import {plane} from './plan';
|
import {plane} from './plan';
|
||||||
|
|
||||||
|
|
@ -18,26 +19,26 @@ const LULU_BAUM_X = 650;
|
||||||
const REIHE = [0, 1, 2, 3, 4].map((i) => ({x: 1290 + i * 78, y: BODEN + 4}));
|
const REIHE = [0, 1, 2, 3, 4].map((i) => ({x: 1290 + i * 78, y: BODEN + 4}));
|
||||||
|
|
||||||
export const S4 = plane('s4', [
|
export const S4 = plane('s4', [
|
||||||
{id: 'n1', wer: 'NOA', text: 'Wenn du so müde bist, brauchst du ein warmes Bett.'},
|
{id: 'n1', wer: 'NOA'},
|
||||||
{id: 'e1', wer: 'ERZ', text: 'Igel bauen sich für den Winter ein Nest.'},
|
{id: 'e1', wer: 'ERZ'},
|
||||||
{id: 'e2', wer: 'ERZ', text: 'Aus ganz vielen Blättern, unter einer Hecke oder einem Holzstapel.'},
|
{id: 'e2', wer: 'ERZ'},
|
||||||
{id: 'e3', wer: 'ERZ', text: 'Da ist es trocken und warm.'},
|
{id: 'e3', wer: 'ERZ'},
|
||||||
{id: 'n2', wer: 'NOA', text: 'Zählst du mit?', pause: 0.6},
|
{id: 'n2', wer: 'NOA', pause: 0.6},
|
||||||
{id: 'c1', wer: 'NOA', text: 'Eins …', dauer: 1.1, pause: 0.5},
|
{id: 'c1', wer: 'NOA', dauer: 1.1, pause: 0.5},
|
||||||
{id: 'c2', wer: 'NOA', text: 'Zwei …', dauer: 1.1, pause: 0.5},
|
{id: 'c2', wer: 'NOA', dauer: 1.1, pause: 0.5},
|
||||||
{id: 'c3', wer: 'NOA', text: 'Drei …', dauer: 1.1, pause: 0.5},
|
{id: 'c3', wer: 'NOA', dauer: 1.1, pause: 0.5},
|
||||||
{id: 'c4', wer: 'NOA', text: 'Vier …', dauer: 1.1, pause: 0.5},
|
{id: 'c4', wer: 'NOA', dauer: 1.1, pause: 0.5},
|
||||||
{id: 'c5', wer: 'NOA', text: 'Fünf!', dauer: 1.4},
|
{id: 'c5', wer: 'NOA', dauer: 1.4},
|
||||||
{id: 'bild1', dauer: 2.6},
|
{id: 'bild1', dauer: 2.6},
|
||||||
{id: 'l1', wer: 'LULU', text: 'Hier oben hängen noch welche! Die erreicht sonst keiner.'},
|
{id: 'l1', wer: 'LULU'},
|
||||||
{id: 'bild2', dauer: 2.4},
|
{id: 'bild2', dauer: 2.4},
|
||||||
{id: 'bild3', dauer: 2.6},
|
{id: 'bild3', dauer: 2.6},
|
||||||
{id: 'l2', wer: 'LULU', text: 'Oh! Entschuldigung!'},
|
{id: 'l2', wer: 'LULU'},
|
||||||
{id: 'n3', wer: 'NOA', text: 'Macht nichts. Jetzt haben wir noch mehr Blätter.'},
|
{id: 'n3', wer: 'NOA'},
|
||||||
{id: 'bild4', dauer: 4.6},
|
{id: 'bild4', dauer: 4.6},
|
||||||
{id: 'bild5', dauer: 2.2},
|
{id: 'bild5', dauer: 2.2},
|
||||||
{id: 'e4', wer: 'ERZ', text: 'Ein Igelnest braucht ganz viele Blätter.'},
|
{id: 'e4', wer: 'ERZ'},
|
||||||
{id: 'e5', wer: 'ERZ', text: 'Wenn ihr im Garten einen Laubhaufen liegen lasst, schenkt ihr einem Igel vielleicht ein Winterbettchen.'},
|
{id: 'e5', wer: 'ERZ'},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const Szene4: React.FC = () => {
|
export const Szene4: React.FC = () => {
|
||||||
|
|
@ -247,7 +248,7 @@ export const Szene4: React.FC = () => {
|
||||||
{zaehlen.map((c, i) => (
|
{zaehlen.map((c, i) => (
|
||||||
<Ruf key={i} text={String(i + 1)} x={960} y={300} start={c} dauer={40} groesse={190} farbe={['#d9534f', '#e0a020', '#5f8a4a', '#3f5a8a', '#a0526b'][i]} />
|
<Ruf key={i} text={String(i + 1)} x={960} y={300} start={c} dauer={40} groesse={190} farbe={['#d9534f', '#e0a020', '#5f8a4a', '#3f5a8a', '#a0526b'][i]} />
|
||||||
))}
|
))}
|
||||||
<Ruf text="Hatschi!" x={1060} y={230} start={niesen} dauer={45} groesse={170} />
|
<Ruf text={W.hatschi} x={1060} y={230} start={niesen} dauer={45} groesse={170} />
|
||||||
<Vignette />
|
<Vignette />
|
||||||
<Untertitel zeilen={S4.liste} />
|
<Untertitel zeilen={S4.liste} />
|
||||||
<Blende dauer={dauer} rein={0} raus={0} />
|
<Blende dauer={dauer} rein={0} raus={0} />
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,13 @@ const LULU_X5 = 1790;
|
||||||
export const S5 = plane('s5', [
|
export const S5 = plane('s5', [
|
||||||
{id: 'bild1', dauer: 3.0},
|
{id: 'bild1', dauer: 3.0},
|
||||||
{id: 'bild2', dauer: 2.2},
|
{id: 'bild2', dauer: 2.2},
|
||||||
{id: 'p1', wer: 'PINO', text: 'Nein! Ich will nicht schlafen, bevor das Winterfest da ist!'},
|
{id: 'p1', wer: 'PINO'},
|
||||||
{id: 'l1', wer: 'LULU', text: 'Aber Pino, du bist doch so müde!'},
|
{id: 'l1', wer: 'LULU'},
|
||||||
{id: 'bild3', dauer: 3.0},
|
{id: 'bild3', dauer: 3.0},
|
||||||
{id: 'e1', wer: 'ERZ', text: 'Hmm. Was können Noa und Lulu tun, damit Pino bis zum Winterfest wach bleibt?'},
|
{id: 'e1', wer: 'ERZ'},
|
||||||
{id: 'e2', wer: 'ERZ', text: 'Was meinst du?', pause: 3},
|
{id: 'e2', wer: 'ERZ', pause: 3},
|
||||||
{id: 'bild4', dauer: 1.2},
|
{id: 'bild4', dauer: 1.2},
|
||||||
{id: 'n1', wer: 'NOA', text: 'Ich hab eine Idee!'},
|
{id: 'n1', wer: 'NOA'},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const Denkblase: React.FC<{x: number; y: number; start: number; ende: number}> = ({x, y, start, ende}) => {
|
const Denkblase: React.FC<{x: number; y: number; start: number; ende: number}> = ({x, y, start, ende}) => {
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,14 @@ const LULU_X6 = 1440;
|
||||||
export const S6 = plane('s6', [
|
export const S6 = plane('s6', [
|
||||||
{id: 'bild0', dauer: 3.0},
|
{id: 'bild0', dauer: 3.0},
|
||||||
{id: 'malen', dauer: 5.5},
|
{id: 'malen', dauer: 5.5},
|
||||||
{id: 'l1', wer: 'LULU', text: 'Was malst du da, Noa?'},
|
{id: 'l1', wer: 'LULU'},
|
||||||
{id: 'n1', wer: 'NOA', text: 'Einen Kalender! Mit 24 Türchen.'},
|
{id: 'n1', wer: 'NOA'},
|
||||||
{id: 'n2', wer: 'NOA', text: 'Wir öffnen jeden Tag eins. Und hinter jedem Türchen steckt etwas, das Pino wach hält.'},
|
{id: 'n2', wer: 'NOA'},
|
||||||
{id: 'p1', wer: 'PINO', text: 'Was denn?'},
|
{id: 'p1', wer: 'PINO'},
|
||||||
{id: 'n3', wer: 'NOA', text: 'Etwas zum Entdecken, zum Basteln, zum Backen und zum Singen.'},
|
{id: 'n3', wer: 'NOA'},
|
||||||
{id: 'n4', wer: 'NOA', text: 'Und wenn das letzte Türchen offen ist, ist Winterfest!'},
|
{id: 'n4', wer: 'NOA'},
|
||||||
{id: 'bild1', dauer: 2.0},
|
{id: 'bild1', dauer: 2.0},
|
||||||
{id: 'p2', wer: 'PINO', text: 'Meine Stacheln kribbeln!'},
|
{id: 'p2', wer: 'PINO'},
|
||||||
{id: 'bild2', dauer: 4.2},
|
{id: 'bild2', dauer: 4.2},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ const NACHT = 'brightness(0.62) saturate(0.8) hue-rotate(-8deg)'; // nur auf Fig
|
||||||
export const S7 = plane('s7',
|
export const S7 = plane('s7',
|
||||||
[
|
[
|
||||||
{id: 'bild0', dauer: 3.4},
|
{id: 'bild0', dauer: 3.4},
|
||||||
{id: 'n1', wer: 'NOA', text: 'Schlaf gut, Pino. Nur bis morgen.', leise: true},
|
{id: 'n1', wer: 'NOA', leise: true},
|
||||||
{id: 'e1', wer: 'ERZ', text: 'Ab dem 1. Dezember öffnen Noa, Lulu und Pino jeden Tag ein Türchen.'},
|
{id: 'e1', wer: 'ERZ'},
|
||||||
{id: 'e2', wer: 'ERZ', text: 'Und du kannst mitmachen! Schaffst du es mit Pino bis zum Winterfest?', pause: 3},
|
{id: 'e2', wer: 'ERZ', pause: 3},
|
||||||
{id: 't1', wer: 'NOA_LULU', text: 'Tschüss! Bis bald!', leise: true, dauer: 2.6},
|
{id: 't1', wer: 'NOA_LULU', leise: true, dauer: 2.6},
|
||||||
{id: 'p1', wer: 'PINO', text: '… bis bald …', leise: true, dauer: 2.6},
|
{id: 'p1', wer: 'PINO', leise: true, dauer: 2.6},
|
||||||
],
|
],
|
||||||
{vorlauf: 0.5, nachlauf: 1.6},
|
{vorlauf: 0.5, nachlauf: 1.6},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
62
remotion/src/folge02/texte/de.json
Normal file
62
remotion/src/folge02/texte/de.json
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"titel": "Pino ist müde",
|
||||||
|
"lied": "Einer klein und einer groß",
|
||||||
|
"zeilen": {
|
||||||
|
"s2/e1": "Es ist kalt geworden in Noas Garten.",
|
||||||
|
"s2/e2": "Die Bäume haben fast alle Blätter verloren.",
|
||||||
|
"s2/p1": "Hoaaah.",
|
||||||
|
"s2/n1": "Guten Morgen, Pino! Wollen wir spielen?",
|
||||||
|
"s2/p2": "Ja! Ich bin ganz wach. Gaaanz … wach …",
|
||||||
|
"s2/n2": "Pino? Pino!",
|
||||||
|
"s2/p3": "Was? Ich hab nicht geschlafen!",
|
||||||
|
"s2/e3": "Hat er doch.",
|
||||||
|
"s2/e4": "Was meinst du: Warum ist Pino so müde?",
|
||||||
|
"s3/l1": "Guten Morgen, ihr zwei!",
|
||||||
|
"s3/l2": "Von hier oben sehe ich: Die Vögel fliegen weg, ganz weit in den Süden.",
|
||||||
|
"s3/l3": "Und die Bäume sind kahl. Der Winter kommt.",
|
||||||
|
"s3/e1": "Wenn es kalt wird, finden Igel kaum noch etwas zu fressen.",
|
||||||
|
"s3/e2": "Darum machen sie etwas ganz Besonderes. Sie schlafen.",
|
||||||
|
"s3/e3": "Den ganzen Winter lang, bis zum Frühling. Das heißt Winterschlaf.",
|
||||||
|
"s3/p1": "Den ganzen Winter? Aber dann verpasse ich ja das Winterfest!",
|
||||||
|
"s3/e4": "Schau mal, Pinos Stacheln hängen ganz schlaff herunter.",
|
||||||
|
"s3/e5": "Wie fühlt sich Pino jetzt?",
|
||||||
|
"s3/e6": "Ja. Pino ist traurig.",
|
||||||
|
"s3/n1": "Ach, Pino. Bis zum Winterfest ist es doch noch ganz lange hin.",
|
||||||
|
"s4/n1": "Wenn du so müde bist, brauchst du ein warmes Bett.",
|
||||||
|
"s4/e1": "Igel bauen sich für den Winter ein Nest.",
|
||||||
|
"s4/e2": "Aus ganz vielen Blättern, unter einer Hecke oder einem Holzstapel.",
|
||||||
|
"s4/e3": "Da ist es trocken und warm.",
|
||||||
|
"s4/n2": "Zählst du mit?",
|
||||||
|
"s4/c1": "Eins …",
|
||||||
|
"s4/c2": "Zwei …",
|
||||||
|
"s4/c3": "Drei …",
|
||||||
|
"s4/c4": "Vier …",
|
||||||
|
"s4/c5": "Fünf!",
|
||||||
|
"s4/l1": "Hier oben hängen noch welche! Die erreicht sonst keiner.",
|
||||||
|
"s4/l2": "Oh! Entschuldigung!",
|
||||||
|
"s4/n3": "Macht nichts. Jetzt haben wir noch mehr Blätter.",
|
||||||
|
"s4/e4": "Ein Igelnest braucht ganz viele Blätter.",
|
||||||
|
"s4/e5": "Wenn ihr im Garten einen Laubhaufen liegen lasst, schenkt ihr einem Igel vielleicht ein Winterbettchen.",
|
||||||
|
"lied/r1": "Einer klein und einer groß,",
|
||||||
|
"lied/r2": "und dazwischen – na, wer bloß?",
|
||||||
|
"lied/r3": "Noa steht in der Mitte drin,",
|
||||||
|
"lied/r4": "zusammen kriegen wir alles hin!",
|
||||||
|
"s5/p1": "Nein! Ich will nicht schlafen, bevor das Winterfest da ist!",
|
||||||
|
"s5/l1": "Aber Pino, du bist doch so müde!",
|
||||||
|
"s5/e1": "Hmm. Was können Noa und Lulu tun, damit Pino bis zum Winterfest wach bleibt?",
|
||||||
|
"s5/e2": "Was meinst du?",
|
||||||
|
"s5/n1": "Ich hab eine Idee!",
|
||||||
|
"s6/l1": "Was malst du da, Noa?",
|
||||||
|
"s6/n1": "Einen Kalender! Mit 24 Türchen.",
|
||||||
|
"s6/n2": "Wir öffnen jeden Tag eins. Und hinter jedem Türchen steckt etwas, das Pino wach hält.",
|
||||||
|
"s6/p1": "Was denn?",
|
||||||
|
"s6/n3": "Etwas zum Entdecken, zum Basteln, zum Backen und zum Singen.",
|
||||||
|
"s6/n4": "Und wenn das letzte Türchen offen ist, ist Winterfest!",
|
||||||
|
"s6/p2": "Meine Stacheln kribbeln!",
|
||||||
|
"s7/n1": "Schlaf gut, Pino. Nur bis morgen.",
|
||||||
|
"s7/e1": "Ab dem 1. Dezember öffnen Noa, Lulu und Pino jeden Tag ein Türchen.",
|
||||||
|
"s7/e2": "Und du kannst mitmachen! Schaffst du es mit Pino bis zum Winterfest?",
|
||||||
|
"s7/t1": "Tschüss! Bis bald!",
|
||||||
|
"s7/p1": "… bis bald …"
|
||||||
|
}
|
||||||
|
}
|
||||||
62
remotion/src/folge02/texte/es.json
Normal file
62
remotion/src/folge02/texte/es.json
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"titel": "Pino tiene sueño",
|
||||||
|
"lied": "Uno pequeño y otra alta",
|
||||||
|
"zeilen": {
|
||||||
|
"s2/e1": "En el jardín de Noa ya hace frío.",
|
||||||
|
"s2/e2": "Los árboles han perdido casi todas sus hojas.",
|
||||||
|
"s2/p1": "Uaaaah.",
|
||||||
|
"s2/n1": "¡Buenos días, Pino! ¿Jugamos?",
|
||||||
|
"s2/p2": "¡Sí! Estoy muy despierto. Muuuy… despierto…",
|
||||||
|
"s2/n2": "¿Pino? ¡Pino!",
|
||||||
|
"s2/p3": "¿Qué? ¡Yo no estaba durmiendo!",
|
||||||
|
"s2/e3": "Sí que estaba durmiendo.",
|
||||||
|
"s2/e4": "¿Tú qué crees? ¿Por qué tiene Pino tanto sueño?",
|
||||||
|
"s3/l1": "¡Buenos días a los dos!",
|
||||||
|
"s3/l2": "Desde aquí arriba lo veo: los pájaros se van volando, muy lejos, hacia el sur.",
|
||||||
|
"s3/l3": "Y los árboles están desnudos. Ya llega el invierno.",
|
||||||
|
"s3/e1": "Cuando hace frío, los erizos casi no encuentran comida.",
|
||||||
|
"s3/e2": "Por eso hacen algo muy especial: duermen.",
|
||||||
|
"s3/e3": "Todo el invierno, hasta la primavera. Eso se llama hibernar.",
|
||||||
|
"s3/p1": "¿Todo el invierno? ¡Pero entonces me pierdo la Fiesta del Invierno!",
|
||||||
|
"s3/e4": "Mira, las púas de Pino cuelgan hacia abajo.",
|
||||||
|
"s3/e5": "¿Cómo se siente Pino ahora?",
|
||||||
|
"s3/e6": "Sí. Pino está triste.",
|
||||||
|
"s3/n1": "Ay, Pino. Todavía falta mucho para la Fiesta del Invierno.",
|
||||||
|
"s4/n1": "Si tienes tanto sueño, necesitas una cama calentita.",
|
||||||
|
"s4/e1": "Para el invierno, los erizos se hacen un nido.",
|
||||||
|
"s4/e2": "Con muchísimas hojas, debajo de un seto o de un montón de leña.",
|
||||||
|
"s4/e3": "Allí está seco y calentito.",
|
||||||
|
"s4/n2": "¿Cuentas conmigo?",
|
||||||
|
"s4/c1": "Uno…",
|
||||||
|
"s4/c2": "Dos…",
|
||||||
|
"s4/c3": "Tres…",
|
||||||
|
"s4/c4": "Cuatro…",
|
||||||
|
"s4/c5": "¡Cinco!",
|
||||||
|
"s4/l1": "¡Aquí arriba quedan algunas! Nadie más llega hasta aquí.",
|
||||||
|
"s4/l2": "¡Uy! ¡Perdón!",
|
||||||
|
"s4/n3": "No pasa nada. Así tenemos todavía más hojas.",
|
||||||
|
"s4/e4": "Un nido de erizo necesita muchas hojas.",
|
||||||
|
"s4/e5": "Mamás y papás: si dejan un montón de hojas en el jardín, a lo mejor un erizo encuentra allí su camita para el invierno.",
|
||||||
|
"lied/r1": "Uno pequeño y otra alta,",
|
||||||
|
"lied/r2": "y en el medio… ¿quién nos falta?",
|
||||||
|
"lied/r3": "Noa, Noa, en el centro está,",
|
||||||
|
"lied/r4": "¡y juntos sí se podrá!",
|
||||||
|
"s5/p1": "¡No! ¡No quiero dormir hasta que llegue la Fiesta del Invierno!",
|
||||||
|
"s5/l1": "¡Pero Pino, si tienes muchísimo sueño!",
|
||||||
|
"s5/e1": "Mmm. ¿Qué pueden hacer Noa y Lulu para que Pino se quede despierto hasta la Fiesta del Invierno?",
|
||||||
|
"s5/e2": "¿Tú qué crees?",
|
||||||
|
"s5/n1": "¡Tengo una idea!",
|
||||||
|
"s6/l1": "¿Qué estás dibujando, Noa?",
|
||||||
|
"s6/n1": "¡Un calendario! Con veinticuatro ventanitas.",
|
||||||
|
"s6/n2": "Cada día abrimos una. Y detrás de cada ventanita hay algo que mantiene despierto a Pino.",
|
||||||
|
"s6/p1": "¿Qué cosa?",
|
||||||
|
"s6/n3": "Algo para descubrir, para hacer manualidades, para hornear y para cantar.",
|
||||||
|
"s6/n4": "¡Y cuando abramos la última ventanita, será la Fiesta del Invierno!",
|
||||||
|
"s6/p2": "¡Me hacen cosquillas las púas!",
|
||||||
|
"s7/n1": "Que duermas bien, Pino. Solo hasta mañana.",
|
||||||
|
"s7/e1": "A partir del uno de diciembre, Noa, Lulu y Pino abren una ventanita cada día.",
|
||||||
|
"s7/e2": "¡Y tú puedes participar! ¿Llegarás con Pino hasta la Fiesta del Invierno?",
|
||||||
|
"s7/t1": "¡Adiós! ¡Hasta pronto!",
|
||||||
|
"s7/p1": "…hasta pronto…"
|
||||||
|
}
|
||||||
|
}
|
||||||
62
remotion/src/folge02/texte/fr.json
Normal file
62
remotion/src/folge02/texte/fr.json
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"titel": "Pino a sommeil",
|
||||||
|
"lied": "Un tout petit, une très grande",
|
||||||
|
"zeilen": {
|
||||||
|
"s2/e1": "Il fait froid dans le jardin de Noa.",
|
||||||
|
"s2/e2": "Les arbres ont perdu presque toutes leurs feuilles.",
|
||||||
|
"s2/p1": "Ouaaaah.",
|
||||||
|
"s2/n1": "Bonjour, Pino ! On joue ?",
|
||||||
|
"s2/p2": "Oui ! Je suis tout réveillé. Tout… ré…veillé…",
|
||||||
|
"s2/n2": "Pino ? Pino !",
|
||||||
|
"s2/p3": "Quoi ? Je ne dormais pas !",
|
||||||
|
"s2/e3": "Mais si, il dormait.",
|
||||||
|
"s2/e4": "Qu'est-ce que tu en penses : pourquoi Pino a-t-il tellement sommeil ?",
|
||||||
|
"s3/l1": "Bonjour, vous deux !",
|
||||||
|
"s3/l2": "D'ici, je vois que les oiseaux s'envolent, très loin, vers le sud.",
|
||||||
|
"s3/l3": "Et les arbres sont tout nus. L'hiver arrive.",
|
||||||
|
"s3/e1": "Quand il fait froid, les hérissons ne trouvent presque plus rien à manger.",
|
||||||
|
"s3/e2": "Alors ils font quelque chose de très spécial : ils dorment.",
|
||||||
|
"s3/e3": "Tout l'hiver, jusqu'au printemps. Ça s'appelle l'hibernation.",
|
||||||
|
"s3/p1": "Tout l'hiver ? Mais alors je vais rater la fête de l'hiver !",
|
||||||
|
"s3/e4": "Regarde, les piquants de Pino pendent tout mous.",
|
||||||
|
"s3/e5": "Comment se sent Pino maintenant ?",
|
||||||
|
"s3/e6": "Oui. Pino est triste.",
|
||||||
|
"s3/n1": "Oh, Pino. La fête de l'hiver, c'est encore dans très longtemps.",
|
||||||
|
"s4/n1": "Si tu as tellement sommeil, il te faut un lit bien chaud.",
|
||||||
|
"s4/e1": "Pour l'hiver, les hérissons se construisent un nid.",
|
||||||
|
"s4/e2": "Avec plein de feuilles, sous une haie ou sous un tas de bois.",
|
||||||
|
"s4/e3": "Là, c'est sec et bien chaud.",
|
||||||
|
"s4/n2": "Tu comptes avec moi ?",
|
||||||
|
"s4/c1": "Un…",
|
||||||
|
"s4/c2": "Deux…",
|
||||||
|
"s4/c3": "Trois…",
|
||||||
|
"s4/c4": "Quatre…",
|
||||||
|
"s4/c5": "Cinq !",
|
||||||
|
"s4/l1": "Il en reste là-haut ! Personne d'autre ne peut les attraper.",
|
||||||
|
"s4/l2": "Oh ! Pardon !",
|
||||||
|
"s4/n3": "Ce n'est pas grave. Comme ça, on a encore plus de feuilles.",
|
||||||
|
"s4/e4": "Un nid de hérisson a besoin de beaucoup de feuilles.",
|
||||||
|
"s4/e5": "Chers parents : si vous laissez un tas de feuilles dans votre jardin, vous offrez peut-être un petit lit d'hiver à un hérisson.",
|
||||||
|
"lied/r1": "Un tout petit, une très grande,",
|
||||||
|
"lied/r2": "Noa au milieu de la bande.",
|
||||||
|
"lied/r3": "En haut, en bas, et entre les deux –",
|
||||||
|
"lied/r4": "ensemble, on fait de notre mieux !",
|
||||||
|
"s5/p1": "Non ! Je ne veux pas dormir avant la fête de l'hiver !",
|
||||||
|
"s5/l1": "Mais Pino, tu es tellement fatigué !",
|
||||||
|
"s5/e1": "Hmm. Que peuvent faire Noa et Lulu pour que Pino reste réveillé jusqu'à la fête de l'hiver ?",
|
||||||
|
"s5/e2": "Qu'est-ce que tu en penses ?",
|
||||||
|
"s5/n1": "J'ai une idée !",
|
||||||
|
"s6/l1": "Qu'est-ce que tu dessines, Noa ?",
|
||||||
|
"s6/n1": "Un calendrier ! Avec vingt-quatre petites portes.",
|
||||||
|
"s6/n2": "Chaque jour, on en ouvre une. Et derrière chaque petite porte, il y a quelque chose qui garde Pino réveillé.",
|
||||||
|
"s6/p1": "Quoi donc ?",
|
||||||
|
"s6/n3": "Quelque chose à découvrir, à bricoler, à cuisiner et à chanter.",
|
||||||
|
"s6/n4": "Et quand la dernière petite porte sera ouverte, ce sera la fête de l'hiver !",
|
||||||
|
"s6/p2": "Mes piquants me chatouillent !",
|
||||||
|
"s7/n1": "Dors bien, Pino. Juste jusqu'à demain.",
|
||||||
|
"s7/e1": "À partir du premier décembre, Noa, Lulu et Pino ouvrent une petite porte chaque jour.",
|
||||||
|
"s7/e2": "Et toi aussi, tu peux participer ! Est-ce que tu arriveras avec Pino jusqu'à la fête de l'hiver ?",
|
||||||
|
"s7/t1": "Au revoir ! À bientôt !",
|
||||||
|
"s7/p1": "…à bientôt…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
//
|
//
|
||||||
// node werkzeuge/render.mjs F02 ganze Folge, volle Qualität
|
// node werkzeuge/render.mjs F02 ganze Folge, volle Qualität
|
||||||
// node werkzeuge/render.mjs F02 --entwurf halbe Auflösung + Zeit/Szene eingeblendet
|
// node werkzeuge/render.mjs F02 --entwurf halbe Auflösung + Zeit/Szene eingeblendet
|
||||||
|
// node werkzeuge/render.mjs F02 --sprache es spanische Fassung (de, es, fr; ohne Angabe de)
|
||||||
// node werkzeuge/render.mjs F02 --szene 3,5 nur diese Szenen (neu) rendern
|
// node werkzeuge/render.mjs F02 --szene 3,5 nur diese Szenen (neu) rendern
|
||||||
// node werkzeuge/render.mjs F02 --neu alles neu, Zwischenstand ignorieren
|
// node werkzeuge/render.mjs F02 --neu alles neu, Zwischenstand ignorieren
|
||||||
// node werkzeuge/render.mjs F02 --kerne 6 Anzahl paralleler Browser-Tabs
|
// node werkzeuge/render.mjs F02 --kerne 6 Anzahl paralleler Browser-Tabs
|
||||||
// node werkzeuge/render.mjs F02 --ziel /shared/youtube/folge02/folge02.mp4
|
// node werkzeuge/render.mjs F02 --ziel /shared/youtube/folge02/folge02.mp4
|
||||||
//
|
//
|
||||||
// Läuft unter Linux und Windows. Ergebnis und Zwischenstand: out/<kürzel>/
|
// Läuft unter Linux und Windows. Ergebnis und Zwischenstand: out/<kürzel>/<sprache>/
|
||||||
|
|
||||||
import {spawnSync} from 'node:child_process';
|
import {spawnSync} from 'node:child_process';
|
||||||
import {createHash} from 'node:crypto';
|
import {createHash} from 'node:crypto';
|
||||||
|
|
@ -35,12 +36,17 @@ const neu = args.includes('--neu');
|
||||||
const nurSzenen = wert('--szene')?.split(',').map(Number);
|
const nurSzenen = wert('--szene')?.split(',').map(Number);
|
||||||
const kerne = Number(wert('--kerne') ?? Math.max(1, os.cpus().length - 1));
|
const kerne = Number(wert('--kerne') ?? Math.max(1, os.cpus().length - 1));
|
||||||
const ziel = wert('--ziel');
|
const ziel = wert('--ziel');
|
||||||
|
const sprache = wert('--sprache') ?? 'de';
|
||||||
|
if (!['de', 'es', 'fr'].includes(sprache)) {
|
||||||
|
console.error(`Unbekannte Sprache: ${sprache} (de, es, fr)`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const nummer = kuerzel.replace(/\D/g, '');
|
const nummer = kuerzel.replace(/\D/g, '');
|
||||||
const ordner = `src/folge${nummer}`;
|
const ordner = `src/folge${nummer}`;
|
||||||
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
||||||
const lauf = (cmd, a, opt = {}) => {
|
const lauf = (cmd, a, opt = {}) => {
|
||||||
const r = spawnSync(cmd, a, {stdio: opt.still ? 'pipe' : 'inherit', encoding: 'utf8', shell: process.platform === 'win32'});
|
const r = spawnSync(cmd, a, {stdio: opt.still ? 'pipe' : 'inherit', encoding: 'utf8', shell: process.platform === 'win32', env: {...process.env, SPRACHE: sprache}});
|
||||||
if (r.status !== 0) {
|
if (r.status !== 0) {
|
||||||
if (opt.still) console.error(r.stdout, r.stderr);
|
if (opt.still) console.error(r.stdout, r.stderr);
|
||||||
throw new Error(`Fehlgeschlagen: ${cmd} ${a.join(' ')}`);
|
throw new Error(`Fehlgeschlagen: ${cmd} ${a.join(' ')}`);
|
||||||
|
|
@ -57,10 +63,11 @@ const liste = JSON.parse(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fingerabdruck: gemeinsamer Code + Bilder/Clips/Stimmen + eigene Szenendatei
|
// Fingerabdruck: gemeinsamer Code + Bilder/Clips/Stimmen + eigene Szenendatei
|
||||||
const hashDatei = (h, p) => {
|
const hashDatei = (h, p, ohne = []) => {
|
||||||
|
if (ohne.includes(p.replaceAll('\\', '/'))) return;
|
||||||
const st = fs.statSync(p);
|
const st = fs.statSync(p);
|
||||||
if (st.isDirectory()) {
|
if (st.isDirectory()) {
|
||||||
for (const n of fs.readdirSync(p).sort()) hashDatei(h, path.join(p, n));
|
for (const n of fs.readdirSync(p).sort()) hashDatei(h, path.join(p, n), ohne);
|
||||||
} else {
|
} else {
|
||||||
h.update(p.replaceAll('\\', '/'));
|
h.update(p.replaceAll('\\', '/'));
|
||||||
h.update(fs.readFileSync(p));
|
h.update(fs.readFileSync(p));
|
||||||
|
|
@ -68,18 +75,22 @@ const hashDatei = (h, p) => {
|
||||||
};
|
};
|
||||||
const szenenDateien = new Set(liste.map((s) => s.datei));
|
const szenenDateien = new Set(liste.map((s) => s.datei));
|
||||||
const gemeinsam = createHash('sha256');
|
const gemeinsam = createHash('sha256');
|
||||||
for (const p of ['src/baukasten', 'src/bewegung.ts', 'public', 'package-lock.json']) if (fs.existsSync(p)) hashDatei(gemeinsam, p);
|
// Texte, Stimmen und Orte mit Schrift: nur die der gewählten Sprache
|
||||||
|
const andere = ['de', 'es', 'fr'].filter((s) => s !== sprache);
|
||||||
|
const ohne = [`public/stimmen/folge${nummer}`, ...andere.flatMap((s) => [`${ordner}/texte/${s}.json`, `${ordner}/stimmen/${s}.json`, `public/orte/kalender_${s}.webp`])];
|
||||||
|
for (const p of ['src/baukasten', 'src/bewegung.ts', 'public', 'package-lock.json']) if (fs.existsSync(p)) hashDatei(gemeinsam, p, ohne);
|
||||||
|
if (fs.existsSync(`public/stimmen/folge${nummer}/${sprache}`)) hashDatei(gemeinsam, `public/stimmen/folge${nummer}/${sprache}`);
|
||||||
for (const n of fs.readdirSync(ordner).sort()) {
|
for (const n of fs.readdirSync(ordner).sort()) {
|
||||||
const p = path.join(ordner, n);
|
const p = path.join(ordner, n);
|
||||||
if (!szenenDateien.has(n) || n === `Folge${nummer}.tsx`) hashDatei(gemeinsam, p);
|
if (!szenenDateien.has(n) || n === `Folge${nummer}.tsx`) hashDatei(gemeinsam, p, ohne);
|
||||||
}
|
}
|
||||||
const basis = gemeinsam.digest('hex');
|
const basis = gemeinsam.digest('hex');
|
||||||
|
|
||||||
const qualitaet = entwurf ? 'entwurf' : 'voll';
|
const qualitaet = entwurf ? 'entwurf' : 'voll';
|
||||||
const aus = path.join('out', kuerzel, qualitaet);
|
const aus = path.join('out', kuerzel, sprache, qualitaet);
|
||||||
fs.mkdirSync(aus, {recursive: true});
|
fs.mkdirSync(aus, {recursive: true});
|
||||||
const propsDatei = path.join(aus, 'props.json');
|
const propsDatei = path.join(aus, 'props.json');
|
||||||
fs.writeFileSync(propsDatei, JSON.stringify({entwurf}));
|
fs.writeFileSync(propsDatei, JSON.stringify({entwurf, sprache}));
|
||||||
const gl = process.platform === 'linux' ? 'swangle' : 'angle';
|
const gl = process.platform === 'linux' ? 'swangle' : 'angle';
|
||||||
|
|
||||||
const teile = [];
|
const teile = [];
|
||||||
|
|
@ -111,6 +122,6 @@ for (const [i, s] of liste.entries()) {
|
||||||
// Zusammenkleben (ohne neu zu kodieren)
|
// Zusammenkleben (ohne neu zu kodieren)
|
||||||
const listeDatei = path.join(aus, 'teile.txt');
|
const listeDatei = path.join(aus, 'teile.txt');
|
||||||
fs.writeFileSync(listeDatei, teile.map((t) => `file '${path.resolve(t).replaceAll('\\', '/')}'`).join('\n'));
|
fs.writeFileSync(listeDatei, teile.map((t) => `file '${path.resolve(t).replaceAll('\\', '/')}'`).join('\n'));
|
||||||
const ergebnis = ziel ?? path.join('out', kuerzel, `${kuerzel}${entwurf ? '-entwurf' : ''}.mp4`);
|
const ergebnis = ziel ?? path.join('out', kuerzel, `${kuerzel}-${sprache}${entwurf ? '-entwurf' : ''}.mp4`);
|
||||||
lauf(npx, ['remotion', 'ffmpeg', '-v', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', listeDatei, '-c', 'copy', '-movflags', '+faststart', ergebnis]);
|
lauf(npx, ['remotion', 'ffmpeg', '-v', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', listeDatei, '-c', 'copy', '-movflags', '+faststart', ergebnis]);
|
||||||
console.log(`\nFertig: ${ergebnis} (${((Date.now() - start) / 60000).toFixed(1)} min)`);
|
console.log(`\nFertig: ${ergebnis} (${((Date.now() - start) / 60000).toFixed(1)} min)`);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
// Erzeugt die Sprachaufnahmen einer Folge mit ElevenLabs und trägt ihre
|
// Erzeugt die Sprachaufnahmen einer Folge mit ElevenLabs und trägt ihre
|
||||||
// Länge in src/folgeNN/stimmen.json ein. Danach passt sich der Zeitplan der
|
// Länge in src/folgeNN/stimmen/<sprache>.json ein. Danach passt sich der Zeitplan der
|
||||||
// Szenen automatisch an die echten Längen an.
|
// Szenen automatisch an die echten Längen an.
|
||||||
//
|
//
|
||||||
// ELEVENLABS_API_KEY=… npx tsx werkzeuge/stimmen.mts F02
|
// ELEVENLABS_API_KEY=… npx tsx werkzeuge/stimmen.mts F02
|
||||||
// … F02 --szene s3 nur eine Szene
|
// … F02 --szene s3 nur eine Szene
|
||||||
// … F02 --neu alle Zeilen neu erzeugen
|
// … F02 --neu alle Zeilen neu erzeugen
|
||||||
// … F02 --probe nur anzeigen, was erzeugt würde (kostet nichts)
|
// … 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 nur aus der Umgebungsvariable, nie aus einer Datei im Repo.
|
// Der Schlüssel kommt nur aus der Umgebungsvariable, nie aus einer Datei im Repo.
|
||||||
// Stimmen-IDs und Modell stehen in werkzeuge/stimmen.config.json.
|
// Stimmen-IDs und Modell stehen in werkzeuge/stimmen.config.json (je Sprache
|
||||||
// Aufnahmen landen in public/stimmen/folgeNN/ (nicht im Git; auf dem VPS ein
|
// lassen sich dort unter „sprachen“ eigene Stimmen eintragen).
|
||||||
|
// Aufnahmen landen in public/stimmen/folgeNN/<sprache>/ (nicht im Git; auf dem VPS ein
|
||||||
// Link nach /shared/youtube/stimmen/). Nur Zeilen, deren Text sich geändert
|
// Link nach /shared/youtube/stimmen/). Nur Zeilen, deren Text sich geändert
|
||||||
// hat oder deren Datei fehlt, werden neu erzeugt.
|
// hat oder deren Datei fehlt, werden neu erzeugt.
|
||||||
|
|
||||||
|
|
@ -27,6 +29,8 @@ const wert = (n: string) => (args.includes(n) ? args[args.indexOf(n) + 1] : unde
|
||||||
const nurSzene = wert('--szene');
|
const nurSzene = wert('--szene');
|
||||||
const neu = args.includes('--neu');
|
const neu = args.includes('--neu');
|
||||||
const probe = args.includes('--probe');
|
const probe = args.includes('--probe');
|
||||||
|
const sprache = wert('--sprache') ?? 'de';
|
||||||
|
if (!['de', 'es', 'fr'].includes(sprache)) throw new Error(`Unbekannte Sprache: ${sprache}`);
|
||||||
if (!kuerzel) throw new Error('Folge angeben, z. B. F02');
|
if (!kuerzel) throw new Error('Folge angeben, z. B. F02');
|
||||||
|
|
||||||
const nummer = kuerzel.replace(/\D/g, '');
|
const nummer = kuerzel.replace(/\D/g, '');
|
||||||
|
|
@ -34,26 +38,27 @@ const cfg = JSON.parse(fs.readFileSync('werkzeuge/stimmen.config.json', 'utf8'))
|
||||||
modell: string;
|
modell: string;
|
||||||
format: string;
|
format: string;
|
||||||
stimmen: Record<string, string>;
|
stimmen: Record<string, string>;
|
||||||
|
sprachen?: Record<string, {stimmen?: Record<string, string>}>;
|
||||||
einstellungen?: Record<string, unknown>;
|
einstellungen?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
const schluessel = process.env.ELEVENLABS_API_KEY;
|
const schluessel = process.env.ELEVENLABS_API_KEY;
|
||||||
if (!schluessel && !probe) throw new Error('ELEVENLABS_API_KEY ist nicht gesetzt.');
|
if (!schluessel && !probe) throw new Error('ELEVENLABS_API_KEY ist nicht gesetzt.');
|
||||||
|
|
||||||
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
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'});
|
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}});
|
||||||
const szenen = JSON.parse(ausgabe.stdout.split('\n').filter((z) => z.startsWith('[')).pop()!) as {
|
const szenen = JSON.parse(ausgabe.stdout.split('\n').filter((z) => z.startsWith('[')).pop()!) as {
|
||||||
szene?: string;
|
szene?: string;
|
||||||
zeilen: {id: string; wer?: string; text: string; leise?: boolean}[];
|
zeilen: {id: string; wer?: string; text: string; leise?: boolean}[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
const jsonDatei = `src/folge${nummer}/stimmen.json`;
|
const jsonDatei = `src/folge${nummer}/stimmen/${sprache}.json`;
|
||||||
const aufnahmen: Record<string, {datei: string; dauer: number; text: string}> = JSON.parse(fs.readFileSync(jsonDatei, 'utf8'));
|
const aufnahmen: Record<string, {datei: string; dauer: number; text: string}> = JSON.parse(fs.readFileSync(jsonDatei, 'utf8'));
|
||||||
const ordner = `public/stimmen/folge${nummer}`;
|
const ordner = `public/stimmen/folge${nummer}/${sprache}`;
|
||||||
fs.mkdirSync(ordner, {recursive: true});
|
fs.mkdirSync(ordner, {recursive: true});
|
||||||
|
|
||||||
// Stimme je Sprecher; ein Eintrag kann auf einen anderen Sprecher verweisen (z. B. ALLE → ERZ)
|
// Stimme je Sprecher; ein Eintrag kann auf einen anderen Sprecher verweisen (z. B. ALLE → ERZ)
|
||||||
const stimmeVon = (wer: string): string | undefined => {
|
const stimmeVon = (wer: string): string | undefined => {
|
||||||
const v = cfg.stimmen[wer];
|
const v = cfg.sprachen?.[sprache]?.stimmen?.[wer] || cfg.stimmen[wer];
|
||||||
if (!v) return undefined;
|
if (!v) return undefined;
|
||||||
return v in cfg.stimmen ? stimmeVon(v) : v;
|
return v in cfg.stimmen ? stimmeVon(v) : v;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Gibt die Szenen einer Folge als JSON aus (für werkzeuge/render.mjs und stimmen.ts).
|
// Gibt die Szenen einer Folge als JSON aus (für werkzeuge/render.mjs, stimmen.mts und texte.mts; Sprache über SPRACHE).
|
||||||
// npx tsx werkzeuge/szenenliste.mts ./src/folge02/Folge02.tsx F02
|
// npx tsx werkzeuge/szenenliste.mts ./src/folge02/Folge02.tsx F02
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import {pathToFileURL} from 'node:url';
|
import {pathToFileURL} from 'node:url';
|
||||||
|
|
|
||||||
69
remotion/werkzeuge/texte.mts
Normal file
69
remotion/werkzeuge/texte.mts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
// Prüft die Sprechtexte einer Folge (src/folgeNN/texte/<sprache>.json):
|
||||||
|
// Fehlt in einer Sprache eine Zeile, die eine Szene braucht, oder steht dort
|
||||||
|
// eine, die keine Szene mehr benutzt?
|
||||||
|
//
|
||||||
|
// npx tsx werkzeuge/texte.mts F02 prüfen
|
||||||
|
// npx tsx werkzeuge/texte.mts F02 --tabelle alle Sprachen nebeneinander (Markdown)
|
||||||
|
//
|
||||||
|
// Kostet nichts und ändert nichts.
|
||||||
|
|
||||||
|
import {spawnSync} from 'node:child_process';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import {fileURLToPath} from 'node:url';
|
||||||
|
|
||||||
|
const WURZEL = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
process.chdir(WURZEL);
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const kuerzel = args.find((a) => !a.startsWith('--'));
|
||||||
|
if (!kuerzel) throw new Error('Folge angeben, z. B. F02');
|
||||||
|
const nummer = kuerzel.replace(/\D/g, '');
|
||||||
|
const SPRACHEN = ['de', 'es', 'fr'];
|
||||||
|
|
||||||
|
type Texte = {titel: string; lied?: string; zeilen: Record<string, string>};
|
||||||
|
const texte: Record<string, Texte> = Object.fromEntries(
|
||||||
|
SPRACHEN.map((s) => [s, JSON.parse(fs.readFileSync(`src/folge${nummer}/texte/${s}.json`, 'utf8')) as Texte]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Welche Zeilen sprechen die Szenen? (Zeilen, die der Baukasten selbst
|
||||||
|
// mitbringt, z. B. das Intro, stehen nicht in den Textdateien.)
|
||||||
|
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: 'de'},
|
||||||
|
});
|
||||||
|
const szenen = JSON.parse(ausgabe.stdout.split('\n').filter((z) => z.startsWith('[')).pop()!) as {szene?: string; zeilen: {id: string; wer?: string; text: string}[]}[];
|
||||||
|
const gebraucht: {schl: string; wer?: string}[] = [];
|
||||||
|
for (const s of szenen) {
|
||||||
|
if (!s.szene) continue;
|
||||||
|
for (const z of s.zeilen) {
|
||||||
|
const schl = `${s.szene}/${z.id}`;
|
||||||
|
if (schl in texte.de.zeilen || z.text.startsWith('[fehlt:')) gebraucht.push({schl, wer: z.wer});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.includes('--tabelle')) {
|
||||||
|
const zelle = (t?: string) => (t ?? '**fehlt**').replaceAll('|', '\\|');
|
||||||
|
console.log(`| Zeile | Wer | ${SPRACHEN.join(' | ')} |`);
|
||||||
|
console.log(`| --- | --- | ${SPRACHEN.map(() => '---').join(' | ')} |`);
|
||||||
|
console.log(`| Titel | | ${SPRACHEN.map((s) => zelle(texte[s].titel)).join(' | ')} |`);
|
||||||
|
for (const g of gebraucht) console.log(`| ${g.schl} | ${g.wer ?? ''} | ${SPRACHEN.map((s) => zelle(texte[s].zeilen[g.schl])).join(' | ')} |`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fehler = 0;
|
||||||
|
const schluessel = new Set(gebraucht.map((g) => g.schl));
|
||||||
|
for (const s of SPRACHEN) {
|
||||||
|
const fehlt = gebraucht.filter((g) => !(g.schl in texte[s].zeilen)).map((g) => g.schl);
|
||||||
|
const uebrig = Object.keys(texte[s].zeilen).filter((k) => !schluessel.has(k));
|
||||||
|
const leer = Object.entries(texte[s].zeilen).filter(([, t]) => !t.trim()).map(([k]) => k);
|
||||||
|
if (!texte[s].titel) fehlt.unshift('titel');
|
||||||
|
for (const k of fehlt) console.log(`✗ ${s}: fehlt ${k}`);
|
||||||
|
for (const k of leer) console.log(`✗ ${s}: leer ${k}`);
|
||||||
|
for (const k of uebrig) console.log(`? ${s}: nicht benutzt ${k}`);
|
||||||
|
fehler += fehlt.length + leer.length;
|
||||||
|
if (!fehlt.length && !leer.length && !uebrig.length) console.log(`✓ ${s}: ${Object.keys(texte[s].zeilen).length} Zeilen`);
|
||||||
|
}
|
||||||
|
process.exit(fehler ? 1 : 0);
|
||||||
Loading…
Reference in a new issue