import {AbsoluteFill, useCurrentFrame, interpolate, Sequence, Img} from 'remotion' import React from 'react' export type Scene = { type: string heading: string text: string image: string items: string[] pexels_images?: string[] } export type Caption = { start: number end: number text: string } export type StoryVideoProps = { title: string domain: string scenes: Scene[] postUrl: string thumbnail?: string audioFile?: string audioDurationSeconds?: number captions?: Caption[] } const MOTION_PRESETS = [ { sx: 1.00, ex: 1.15, tx0: 0, tx1: 0, ty0: 0, ty1: 0 }, { sx: 1.15, ex: 1.00, tx0: 0, tx1: 0, ty0: 0, ty1: 0 }, { sx: 1.09, ex: 1.09, tx0: 55, tx1: -55, ty0: 0, ty1: 0 }, { sx: 1.09, ex: 1.09, tx0: -55, tx1: 55, ty0: 0, ty1: 0 }, { sx: 1.09, ex: 1.09, tx0: 0, tx1: 0, ty0: 38, ty1: -38 }, { sx: 1.09, ex: 1.09, tx0: 0, tx1: 0, ty0: -38, ty1: 38 }, { sx: 1.00, ex: 1.13, tx0: -38, tx1: 12, ty0: 0, ty1: 0 }, { sx: 1.13, ex: 1.00, tx0: 12, tx1: -38, ty0: 0, ty1: 0 }, { sx: 1.00, ex: 1.13, tx0: 0, tx1: 0, ty0: 28, ty1: -28 }, { sx: 1.13, ex: 1.00, tx0: 0, tx1: 0, ty0: -28, ty1: 28 }, { sx: 1.00, ex: 1.13, tx0: -28, tx1: 28, ty0: -20, ty1: 20 }, { sx: 1.13, ex: 1.00, tx0: 28, tx1: -28, ty0: 20, ty1: -20 }, ] as const const NUM_MOTIONS = MOTION_PRESETS.length const INTRO = 45 const OUTRO = 30 const FPS = 30 const CROSSFADE_FRAMES = 10 const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v)) const normalizeText = (v?: string) => (v ?? '').replace(/\s+/g, ' ').trim() const normalizeScene = (s: Partial): Scene => ({ type: String(s.type ?? 'section'), heading: normalizeText(s.heading), text: normalizeText(s.text), image: String(s.image ?? ''), items: Array.isArray(s.items) ? s.items.map(i => normalizeText(String(i))).filter(Boolean) : [], pexels_images: Array.isArray(s.pexels_images) ? s.pexels_images.filter(Boolean) : [], }) const getSafeScenes = (scenes: Scene[] | undefined): Scene[] => { if (!Array.isArray(scenes) || scenes.length === 0) { return [{type: 'section', heading: '', text: '', image: '', items: [], pexels_images: []}] } return scenes.map(normalizeScene) } function buildGlobalImagePlan(scenes: Scene[]): { scenePexels: string[][] globalStartIndex: number[] } { const scenePexels: string[][] = [] const globalStartIndex: number[] = [] let cursor = 0 for (const scene of scenes) { const imgs = (scene.pexels_images ?? []).filter(Boolean) scenePexels.push(imgs) globalStartIndex.push(cursor) cursor += imgs.length } return {scenePexels, globalStartIndex} } const normalizeForMatch = (v?: string) => (v ?? '') .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[„“"‚‘'`´]/g, '') .replace(/[^a-z0-9äöüß\s]/gi, ' ') .replace(/\s+/g, ' ') .trim() const wordsOf = (v?: string): string[] => normalizeForMatch(v).split(' ').filter(Boolean) const buildSceneBlockText = (scene: Scene): string => { const parts: string[] = [] if (scene.heading) parts.push(scene.heading) if (scene.text) parts.push(scene.text) if (scene.items.length) parts.push(scene.items.join('. ')) return normalizeText(parts.join('. ')) } const buildAnchors = (scene: Scene): string[] => { const anchors: string[] = [] if (scene.heading) { const hw = wordsOf(scene.heading) if (hw.length >= 2) anchors.push(hw.slice(0, Math.min(5, hw.length)).join(' ')) } if (scene.text) { const tw = wordsOf(scene.text) if (tw.length >= 3) anchors.push(tw.slice(0, Math.min(6, tw.length)).join(' ')) } for (const item of scene.items.slice(0, 3)) { const iw = wordsOf(item) if (iw.length >= 2) anchors.push(iw.slice(0, Math.min(5, iw.length)).join(' ')) } return anchors.filter(Boolean) } type SceneTiming = { from: number dur: number headingFrame: number itemsBaseFrame: number itemFrames: number[] } const fallbackSceneDurationInFrames = (scene: Scene) => { const combined = [scene.heading, scene.text, scene.items.join(' ')].filter(Boolean).join(' ') const words = combined.split(/\s+/).filter(Boolean).length const pauses = (combined.match(/[,:;.!?]/g) ?? []).length const bullets = scene.items.length * 10 const headWords = scene.heading ? scene.heading.split(/\s+/).length : 0 const headWeight = scene.heading ? Math.max(8, Math.round(headWords * 1.8)) : 0 const bonus = scene.type === 'bullets' ? 12 : scene.type === 'fazit' ? 8 : 0 const units = Math.max(12, words + pauses * 2 + bullets + headWeight + bonus) return clamp(Math.round((units / 2.7) * FPS), 84, 900) } const getFallbackSceneTimings = (scenes: Scene[], audioDurationSeconds?: number): SceneTiming[] => { const safe = getSafeScenes(scenes) let durs: number[] if (!audioDurationSeconds || audioDurationSeconds <= 0) { durs = safe.map(fallbackSceneDurationInFrames) } else { const raw = safe.map(fallbackSceneDurationInFrames) const availableFrames = Math.max(safe.length, Math.round(audioDurationSeconds * FPS)) const total = raw.reduce((a, b) => a + b, 0) || 1 durs = raw.map(v => Math.max(1, Math.floor((v / total) * availableFrames))) let rem = availableFrames - durs.reduce((a, b) => a + b, 0) for (let i = 0; rem > 0; i++, rem--) durs[i % durs.length] += 1 durs[durs.length - 1] += availableFrames - durs.reduce((a, b) => a + b, 0) } let cursor = INTRO return safe.map((scene, idx) => { const dur = durs[idx] const headingWords = wordsOf(scene.heading).length const textWords = wordsOf(scene.text).length const allWords = wordsOf(buildSceneBlockText(scene)).length || 1 const itemsBaseFrame = Math.max(10, Math.round(((headingWords + textWords) / allWords) * dur)) const timing: SceneTiming = { from: cursor, dur, headingFrame: 0, itemsBaseFrame, itemFrames: scene.items.map((_, i) => itemsBaseFrame + i * 18), } cursor += dur return timing }) } const cueContainsPhrase = (cue: Caption, phrase: string) => { if (!phrase) return false return normalizeForMatch(cue.text).includes(phrase) } const findAnchorCueIndex = (captions: Caption[], anchors: string[], fromIndex: number): number | null => { if (!anchors.length) return null for (let i = fromIndex; i < captions.length; i++) { for (const a of anchors) { if (cueContainsPhrase(captions[i], a)) return i } } return null } const buildSceneTimingsFromCaptions = ( scenes: Scene[], captions: Caption[], audioDurationSeconds?: number ): SceneTiming[] => { const safeScenes = getSafeScenes(scenes) const fallback = getFallbackSceneTimings(safeScenes, audioDurationSeconds) if (!captions || captions.length === 0) return fallback const totalAudioFrames = audioDurationSeconds && audioDurationSeconds > 0 ? Math.round(audioDurationSeconds * FPS) : Math.max( 1, Math.round((captions[captions.length - 1]?.end ?? 0) * FPS) ) const timings: SceneTiming[] = [] let searchIndex = 0 for (let s = 0; s < safeScenes.length; s++) { const scene = safeScenes[s] const nextScene = safeScenes[s + 1] const anchors = buildAnchors(scene) const startIdx = findAnchorCueIndex(captions, anchors, searchIndex) if (startIdx === null) { timings.push(fallback[s]) continue } let endIdx = captions.length - 1 if (nextScene) { const nextAnchors = buildAnchors(nextScene) const nextStartIdx = findAnchorCueIndex(captions, nextAnchors, startIdx + 1) if (nextStartIdx !== null) { endIdx = Math.max(startIdx, nextStartIdx - 1) } } const sceneStartFrame = INTRO + Math.round(captions[startIdx].start * FPS) const sceneEndFrame = INTRO + Math.round(captions[endIdx].end * FPS) const dur = Math.max(24, sceneEndFrame - sceneStartFrame) let itemsBaseFrame = Math.max(10, Math.round(dur * 0.45)) const itemFrames: number[] = [] for (const item of scene.items) { const iw = wordsOf(item) const phrase = iw.slice(0, Math.min(5, iw.length)).join(' ') let local = itemsBaseFrame if (phrase) { const hit = captions.findIndex((c, i) => i >= startIdx && i <= endIdx && cueContainsPhrase(c, phrase)) if (hit !== -1) { local = Math.max(10, Math.round(captions[hit].start * FPS) + INTRO - sceneStartFrame) } } itemFrames.push(local) } if (itemFrames.length > 0) { itemsBaseFrame = itemFrames[0] } timings.push({ from: sceneStartFrame, dur, headingFrame: 0, itemsBaseFrame, itemFrames, }) searchIndex = Math.max(searchIndex, startIdx + 1) } for (let i = 0; i < timings.length; i++) { const cur = timings[i] const next = timings[i + 1] if (next) { cur.dur = Math.max(24, next.from - cur.from) } else { const endOfAudio = INTRO + totalAudioFrames cur.dur = Math.max(24, endOfAudio - cur.from) } } return timings } const CaptionOverlay: React.FC<{captions: Caption[]; fps: number}> = ({captions, fps}) => { const frame = useCurrentFrame() const currentSec = frame / fps const current = captions.find(c => currentSec >= c.start && currentSec < c.end) if (!current) return null const words = current.text.split(' ') const progress = (currentSec - current.start) / Math.max(0.1, current.end - current.start) const activeWord = Math.min(Math.floor(progress * words.length), words.length - 1) return (
{words.map((w, i) => ( {w} ))}
) } const PexelsSlideshow: React.FC<{ images: string[] dur: number globalStartIndex: number }> = ({images, dur}) => { const frame = useCurrentFrame() if (!images || images.length === 0) return null const numImages = images.length const framesPerImage = dur / numImages const imgIdx = Math.min(Math.floor(frame / framesPerImage), numImages - 1) const localFrame = frame - imgIdx * framesPerImage const src = images[imgIdx] const motionIdx = imgIdx % NUM_MOTIONS const m = MOTION_PRESETS[motionIdx] const t = localFrame / Math.max(framesPerImage, 1) const sc = m.sx + (m.ex - m.sx) * t const tx = m.tx0 + (m.tx1 - m.tx0) * t const ty = m.ty0 + (m.ty1 - m.ty0) * t const fade = localFrame < CROSSFADE_FRAMES ? interpolate(localFrame, [0, CROSSFADE_FRAMES], [0, 1], {extrapolateRight: 'clamp'}) : 1 return (
) } const IntroVisual: React.FC<{thumbnail?: string; pexelImages: string[]}> = ({thumbnail, pexelImages}) => { const frame = useCurrentFrame() const opacity = interpolate(frame, [0, 16], [0, 1], {extrapolateRight: 'clamp'}) const src = pexelImages[0] || thumbnail return ( {src ? ( ) : null} ) } const FAQOverlay: React.FC<{items: string[]; heading: string; dur: number}> = ({items, heading}) => { return (
{heading}
{items.map((it, i) => (
{it}
))}
) } const BulletsOverlay: React.FC<{ items: string[] heading: string dur: number headingFrame?: number itemsBaseFrame?: number itemFrames?: number[] }> = ({items, heading, dur, headingFrame = 0, itemsBaseFrame = 10, itemFrames = []}) => { const frame = useCurrentFrame() const safeItems = items.slice(0, 8) const count = safeItems.length const itemFontSize = count <= 5 ? 42 : count <= 7 ? 36 : 32 const gap = count <= 5 ? 14 : count <= 7 ? 10 : 8 const availableFrames = Math.max(count * 12, dur - itemsBaseFrame - 20) const staggerFrames = Math.max(10, Math.floor(availableFrames / (count + 1))) const fadeDuration = Math.max(6, Math.min(14, staggerFrames - 2)) const headOpacity = interpolate( frame, [headingFrame, headingFrame + 18], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} ) const headY = interpolate( frame, [headingFrame, headingFrame + 18], [20, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} ) return (
{heading}
{safeItems.map((item, i) => { const startF = itemFrames[i] ?? (itemsBaseFrame + i * staggerFrames) const op = interpolate(frame, [startF, startF + fadeDuration], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }) const tx = interpolate(frame, [startF, startF + fadeDuration], [-28, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }) return (
{i + 1}
{item}
) })}
) } const SceneVisual: React.FC<{ scene: Scene dur: number sceneIndex: number thumbnail?: string pexelImages: string[] globalStartIndex: number timing: SceneTiming }> = ({scene, dur, sceneIndex, thumbnail, pexelImages, globalStartIndex, timing}) => { const frame = useCurrentFrame() const fadeIn = interpolate(frame, [0, 14], [0, 1], {extrapolateRight: 'clamp'}) const fadeOut = interpolate(frame, [dur - 14, dur], [1, 0], {extrapolateLeft: 'clamp'}) const opacity = Math.min(fadeIn, fadeOut) const progressPct = clamp((frame / Math.max(dur, 1)) * 100, 0, 100) const hasPexels = pexelImages.length > 0 const fallbackSrc = thumbnail || scene.image || null const motionIdx = sceneIndex % NUM_MOTIONS const m = MOTION_PRESETS[motionIdx] const t = frame / Math.max(dur, 1) const fbSc = m.sx + (m.ex - m.sx) * t const fbTx = m.tx0 + (m.tx1 - m.tx0) * t const fbTy = m.ty0 + (m.ty1 - m.ty0) * t return ( {hasPexels ? ( ) : fallbackSrc ? (
) : (
)}
{scene.type === 'faq' && scene.items.length > 0 && ( )} {scene.type === 'bullets' && scene.items.length > 0 && ( )} ) } const OutroVisual: React.FC = () => { const frame = useCurrentFrame() const opacity = interpolate(frame, [0, 22], [0, 1], {extrapolateRight: 'clamp'}) const scale = interpolate(frame, [0, OUTRO], [1.04, 1.0], {extrapolateRight: 'clamp'}) return (
) } export const StoryVideo: React.FC = ({scenes, thumbnail, audioDurationSeconds, captions}) => { const rawScenes = getSafeScenes(scenes) const {scenePexels, globalStartIndex} = React.useMemo( () => buildGlobalImagePlan(rawScenes), [scenes] ) const sceneTimings = React.useMemo( () => buildSceneTimingsFromCaptions(rawScenes, captions ?? [], audioDurationSeconds), [rawScenes, captions, audioDurationSeconds] ) const introPexels = scenePexels[0] return ( {captions && captions.length > 0 && } {rawScenes.map((scene, idx) => { const timing = sceneTimings[idx] if (!timing || timing.dur <= 0) return null return ( ) })} ) } export const STORY_DURATION = ( scenes: Scene[], audioDurationSeconds?: number ): number => { const safe = getSafeScenes(scenes) if (audioDurationSeconds && audioDurationSeconds > 0) { return INTRO + Math.round(audioDurationSeconds * FPS) + OUTRO } return INTRO + safe.reduce((sum, s) => sum + fallbackSceneDurationInFrames(s), 0) + OUTRO } FinanzTippsBlog.de • Täglich Aktuelle Tipps rund um Finanzen

finanztippsblog.de –
Ehrliche Finanztipps für den Alltag

Auf finanztippsblog.de finden Sie verständliche Ratgeber zu Geld, Recht, Versicherungen, Steuern und Verbraucherfragen – von der Testament-Erstellung bis zur Fluggastrechte-Durchsetzung. Unsere Redaktion ist Teil des Medienunternehmens Minformatik aus Hamburg, einem Netzwerk aus über hundert spezialisierten Journalisten, Juristen, Finanzberatern und Ökonomen. Wir beleuchten jedes Alltagsthema nicht eindimensional, sondern in einem einzigartigen 10‑Perspektiven‑System: So wird aus einem trockenen Paragraphen eine lebendige Geschichte, die Ihnen wirklich hilft – ob bei der Kontoauflösung nach Todesfall, der optimalen Steuererklärung oder beim Gebrauchtwagenkauf.

Unsere Methode – 10 Perspektiven

Deep Thinking für Ihren Geldbeutel: Creative Non-Fiction im Blog

Jeder Artikel auf finanztippsblog.de durchläuft ein strenges redaktionelles Verfahren: die 10‑Perspektiven‑Methode. Wir verbinden fundierte Fakten, Gesetzestexte und Daten mit literarischem Stil und lebendigen Dialogen. Das Ziel: Themen atmen, pulsieren und werden verständlich – damit Sie nicht nur oberflächliche Tipps erhalten, sondern die Zusammenhänge hinter Versicherungsbedingungen, Steuerparagraphen oder Streaming-Preiserhöhungen wirklich durchschauen.

Das 10‑Perspektiven‑Kettenformat im Detail: Jede Frage aus Ihrem Alltag wird aus zehn verschiedenen Blickwinkeln beleuchtet. Jede Rolle stellt am Ende eine Frage, die die nächste Perspektive aufgreift. So entsteht ein dichtes Netz aus persönlicher Betroffenheit, juristischem Tiefgang, historischen Vergleichen und praktischer Handlungsanleitung.

01
Persönlich
Betroffener
02
Experte
Fachanwalt
03
Kultur
Historiker
04
Technik
Digitalexperte
05
Philosophie
Ethiker
06
Soziologie
Gesellschaft
07
Psychologie
Verbraucher
08
Ökonomie
Finanzwelt
09
Politik
Regulierung
10
Kunst
Storytelling

Systemvergleich: So unterscheiden wir uns von üblichen Ratgeber-Portalen

Übliche Online-Ratgeber:

  • Reine Produktbeschreibung ODER
  • Anleitung ohne Hintergrund ODER
  • Gesetzestext schwer verständlich
  • Meist nur eine Perspektive

Unsere Artikel (10 Perspektiven):

  • Rechtliches, Historisches, Psychologisches vereint
  • Dialoge zwischen Betroffenem und Experten (z.B. Notar, Steuerberater)
  • Konkrete Schritt-für-Schritt-Anleitungen & Checklisten
  • Einordnung: Was bedeutet das für mich und die Gesellschaft?

Der Dreifachnutzen jedes Blogartikels:

  • Informativ: Aktuelle Gesetze, Urteile, Zahlen – von der ETA-Pflicht bis zum Pflichtteil.
  • Bildend: Sie verstehen, warum bestimmte Regeln gelten und wie Sie sie zu Ihrem Vorteil nutzen.
  • Unterhaltend: Erzählungen, die im Gedächtnis bleiben – keine staubigen Paragraphen.

Beispiel: So entsteht ein Beitrag zur „Kontoauflösung nach Todesfall“

01 · Persönlich (Angehörige)
Claudia S., Mutter verstorben

„Meine Mutter ist letzte Woche gestorben. Jetzt soll ich ihr Konto auflösen – aber ich habe keine Ahnung, welche Unterlagen ich brauche, ob ich das allein darf und was mit Daueraufträgen passiert.“

→ Frage an die Expertin: Was ist rechtlich zu beachten?
02 · Expertin (Rechtsanwältin)
Dr. Sarah Kling

„Sie benötigen einen Erbschein oder ein notarielles Testament. Die Bank darf erst auszahlen, wenn die Erbfolge geklärt ist. Viele Banken bieten Checklisten an – aber Vorsicht: Kontovollmachten erlöschen mit dem Tod.“

→ Frage an den Historiker: Warum ist das Erbrecht so kompliziert?
03 · Kultur (Rechtshistoriker)
Prof. Martin Gruber

„Unser Erbrecht geht auf das römische Recht zurück und wurde über Jahrhunderte gewachsen. Es soll Willkür verhindern und die Familie schützen. Die Paragrafen sind detailreich, weil jeder Einzelfall anders ist.“

→ Frage an die Psychologie: Wie bewältigen Angehörige diesen Stress?
07 · Psychologie (Trauerbegleiterin)
Eva Lorenz

„In der Trauer fällt logisches Denken schwer. Deshalb ist es gut, wenn die Banken klare Fristen nennen und Unterlagen verständlich erklären. Unser Artikel gibt eine Schritt-für-Schritt-Liste – das reduziert Überforderung.“

→ … und so weiter bis zur zehnten Perspektive mit Checkliste.

Mehr zur 10‑Perspektiven‑Methode

Blog-Themen

Finanztipps für alle Lebenslagen – zehn Perspektiven auf jeden Rat

Ob Sie wissen wollen, wie Sie ein Testament schreiben, Ihre Fluggastrechte durchsetzen, im Homeoffice nicht ausgebrannt werden oder die beste Kfz-Versicherung finden – jeder Blogbeitrag wird nach der 10‑Perspektiven‑Methode recherchiert und geschrieben. Mit konkreten Checklisten, aktuellen Urteilen und verständlichen Erklärungen.

⚖️
Recht & Erbe

Testament, Erbrecht, Patientenverfügung

Von der Enterbung bis zur Kontoauflösung – wir erklären die Rechtslage, zeigen Fallstricke und geben Formulierungshilfen. Mit Interviews mit Fachanwälten und Notaren.

✈️
Reisen & Mobilität

Fluggastrechte, ETA, Maut, CO₂-Kompensation

Was tun bei Flugverspätung? Wie beantrage ich die neue ETA für Großbritannien? Und wie reise ich umweltbewusster? Mit aktuellen Urteilen und Berechnungstools.

💻
Digitales & Verträge

Streaming, Internetverträge, KI, Datenschutz

Preiserhöhung bei Wow? Internetvertrag anfechten? Wir zeigen Ihre Rechte, erklären die Kleingedruckten und geben Musterbriefe – verständlich und sofort anwendbar.

Häufig gefragt

Was Sie über finanztippsblog.de wissen sollten

Antworten zu unserer Arbeitsweise, den Autoren und dem Mehrwert unserer Blogbeiträge.

Was unterscheidet Ihren Blog von anderen Finanzblogs?
Wir schreiben keine oberflächlichen "10 Tipps"-Listen. Jeder Beitrag wird nach der 10‑Perspektiven‑Methode erstellt: Persönliche Betroffenheit, juristische Expertise, historische Einordnung, psychologische Fallstricke und praktische Schritt-für-Schritt-Anleitungen fließen in einen lebendigen Text ein. So wird aus einem Paragraphen eine Geschichte, die Sie verstehen und anwenden können.
Für wen sind die Inhalte gedacht?
Für alle, die im Alltag mit Finanz- oder Rechtsfragen konfrontiert werden – ob Sie ein Haus kaufen, eine Reise buchen, einen Erbfall regeln oder einfach Ihren Streamingvertrag kündigen wollen. Wir erklären so, dass Sie Ihre Rechte kennen und wahrnehmen können, ohne Jura studiert zu haben.
Wie aktuell sind Ihre Beiträge zu Gesetzen und Urteilen?
Unsere Redaktion beobachtet ständig neue Urteile (z.B. BGH, EuGH) und Gesetzesänderungen. Beiträge wie zur ETA-Pflicht oder zu Fluggastrechten werden zeitnah aktualisiert. Jeder Artikel trägt ein Datum, die 10-Perspektiven-Struktur bleibt erhalten.
Bieten Sie auch Vorlagen oder Checklisten zum Herunterladen?
Ja, zu vielen Themen wie Testament, Kontoauflösung oder Widerspruch gegen Preiserhöhungen gibt es kostenlose Checklisten und Musterbriefe als PDF – direkt im Artikel verlinkt. So können Sie das Gelernte sofort umsetzen.
Immer zehn Perspektiven

Finanztipps, die wirklich weiterhelfen.
Was sagt der Anwalt? Was die Psychologin?

Erbrecht · Reisen · Digitales · Steuern · Versicherungen – jeden Monat neue Perspektiven, immer unabhängig, immer mit Tiefgang.