<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Agnes Studio — Création Vidéo Complète</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
:root {
  --bg: #0a0a12; --panel: #14141f; --panel-2: #1c1c2b; --border: #2a2a3e;
  --ink: #e8e8f0; --ink-dim: #8888a0; --ink-faint: #55556a;
  --accent: #e94560; --accent-2: #4a6cf7;
  --success: #2d8a4e; --error: #c0392b; --warn: #d4a017;
  --radius: 12px;
}
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
  background: var(--bg); color: var(--ink); min-height: 100vh;
  padding: 1rem 0.8rem 4rem; font-size: 15px; line-height: 1.5;
  max-width: 640px; margin: 0 auto;
}
h1 { font-size: 1.3rem; font-weight: 700; text-align: center; margin-bottom: 0.2rem;
     background: linear-gradient(135deg, #e94560, #4a6cf7);
     -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.subtitle { font-size: 0.7rem; color: var(--ink-dim); text-align: center; margin-bottom: 1rem; }

/* ================= GRILLE 2x2 POUR ONGLETS ================= */
.tabs-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-bottom: 1rem;
}
.tab-btn {
  padding: 0.9rem 0.6rem;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--ink-dim);
  cursor: pointer;
  text-align: center;
  font-size: 0.72rem;
  font-weight: 700;
  user-select: none;
  transition: all 0.15s;
  line-height: 1.3;
}
.tab-btn .tab-icon {
  display: block;
  font-size: 1.3rem;
  margin-bottom: 0.25rem;
}
.tab-btn.active {
  background: linear-gradient(135deg, #e94560, #4a6cf7);
  color: #fff;
  border-color: transparent;
}
.tab-btn:active { transform: scale(0.97); }

.main-panel { display: none; }
.main-panel.active { display: block; }

.panel { background: var(--panel); border: 1px solid var(--border);
         border-radius: var(--radius); padding: 1rem; margin-bottom: 0.8rem; }
.panel h2 { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.1em;
            color: var(--ink-dim); margin-bottom: 0.6rem; font-weight: 700; }

input[type="password"], input[type="text"], input[type="number"], textarea, select {
  width: 100%; padding: 0.75rem 0.9rem; border-radius: 8px;
  border: 1px solid var(--border); background: var(--bg);
  color: var(--ink); font-family: inherit; font-size: 0.88rem;
  -webkit-appearance: none; appearance: none;
}
select {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%238888a0' d='M0 0l5 6 5-6z'/></svg>");
  background-repeat: no-repeat;
  background-position: right 0.9rem center;
  padding-right: 2.2rem;
}
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent); }
textarea { min-height: 70px; line-height: 1.5; resize: vertical; }

.btn { width: 100%; padding: 0.85rem; border-radius: 8px; border: none;
       font-family: inherit; font-size: 0.88rem; font-weight: 700; cursor: pointer; }
.btn-primary { background: linear-gradient(135deg, #e94560, #4a6cf7); color: #fff; }
.btn-secondary { background: var(--panel-2); color: var(--ink); border: 1px solid var(--border); }
.btn:disabled { opacity: 0.3; cursor: not-allowed; }
.btn:active:not(:disabled) { transform: scale(0.98); }

.photo-zone {
  border: 2px dashed var(--border); border-radius: var(--radius);
  padding: 1.2rem 1rem; text-align: center; cursor: pointer;
  position: relative; overflow: hidden; min-height: 130px;
  display: flex; align-items: center; justify-content: center;
}
.photo-zone input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.photo-zone img { max-width: 100%; max-height: 160px; border-radius: 8px; display: block; }
.photo-zone .placeholder { color: var(--ink-faint); font-size: 0.8rem; }
.photo-zone .placeholder .icon { font-size: 1.5rem; display: block; margin-bottom: 0.3rem; }

.mode-toggle { display: flex; gap: 0.4rem; margin-bottom: 0.7rem; }
.mode-option {
  flex: 1; padding: 0.6rem; border-radius: 8px; border: 1px solid var(--border);
  background: var(--panel-2); color: var(--ink-dim); cursor: pointer;
  text-align: center; font-size: 0.72rem; font-weight: 600;
}
.mode-option.selected { background: var(--accent); color: #fff; border-color: var(--accent); }
.mode-pane { display: none; }
.mode-pane.active { display: block; }

.record-btn-wrap { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; padding: 0.4rem 0; }
.record-indicator {
  width: 60px; height: 60px; border-radius: 50%; background: var(--error);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.5rem; cursor: pointer; user-select: none;
}
.record-indicator.recording { background: var(--success); box-shadow: 0 0 25px rgba(45,138,78,0.5); }
.record-label { font-size: 0.7rem; color: var(--ink-dim); }
.audio-preview { width: 100%; margin-top: 0.5rem; }
.audio-preview audio { width: 100%; }

.control-row { margin-bottom: 0.7rem; }
.control-label { display: block; font-size: 0.65rem; color: var(--ink-dim);
                 margin-bottom: 0.3rem; text-transform: uppercase; letter-spacing: 0.08em;
                 font-weight: 600; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }

.status { font-size: 0.75rem; color: var(--ink-dim); margin-top: 0.5rem; text-align: center; }
.status.ok { color: var(--success); }
.status.err { color: var(--error); }

.result-video { margin-top: 0.7rem; }
.result-video video { width: 100%; border-radius: var(--radius); background: #000; }
.result-actions { display: flex; gap: 0.4rem; margin-top: 0.5rem; }
.result-actions .btn { flex: 1; padding: 0.55rem; font-size: 0.75rem; }

.hidden { display: none !important; }
.progress-bar { height: 4px; background: var(--panel-2); border-radius: 2px;
                overflow: hidden; margin-top: 0.5rem; }
.progress-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #e94560, #4a6cf7);
                 transition: width 0.4s; border-radius: 2px; }

.log-panel { max-height: 160px; overflow-y: auto; font-family: ui-monospace, monospace;
             font-size: 0.68rem; line-height: 1.5; padding: 0.5rem;
             background: var(--bg); border-radius: 6px; }
.log-line { padding: 0.1rem 0; color: var(--ink-dim); }
.log-line.ok { color: var(--success); }
.log-line.err { color: var(--error); }
.log-line.warn { color: var(--warn); }

.role-list { display: flex; flex-direction: column; gap: 0.5rem; }
.role-card {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 8px; padding: 0.7rem;
}
.role-card h4 { font-size: 0.75rem; color: var(--accent); margin-bottom: 0.4rem; }
.role-photo-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.4rem; }
.role-thumb {
  width: 50px; height: 50px; border-radius: 6px; background: var(--panel-2);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.4rem; overflow: hidden; flex-shrink: 0; cursor: pointer;
  border: 1px dashed var(--border);
}
.role-thumb img { width: 100%; height: 100%; object-fit: cover; }
.role-audio-btn {
  flex: 1; padding: 0.5rem; border-radius: 6px; border: 1px solid var(--border);
  background: var(--panel-2); color: var(--ink-dim); font-size: 0.7rem;
  cursor: pointer; font-family: inherit;
}
.role-audio-btn.recording { background: var(--error); color: #fff; border-color: var(--error); }
.role-audio-btn.ready { background: var(--success); color: #fff; border-color: var(--success); }
.role-input {
  width: 100%; padding: 0.5rem 0.7rem; border-radius: 6px;
  border: 1px solid var(--border); background: var(--panel);
  color: var(--ink); font-size: 0.78rem; font-family: inherit;
}
</style>
</head>
<body>

<h1>Agnes Studio</h1>
<p class="subtitle">Création vidéo IA · 100% gratuit</p>

<!-- ================= GRILLE 2x2 ================= -->
<div class="tabs-grid">
  <div class="tab-btn active" data-panel="photo">
    <span class="tab-icon">🗣️</span>Photo Parlante
  </div>
  <div class="tab-btn" data-panel="text2video">
    <span class="tab-icon">🎬</span>Texte → Vidéo
  </div>
  <div class="tab-btn" data-panel="img2video">
    <span class="tab-icon">🖼️</span>Image → Vidéo
  </div>
  <div class="tab-btn" data-panel="keyframes">
    <span class="tab-icon">🎞️</span>Keyframes
  </div>
</div>

<!-- CLÉ API -->
<div class="panel">
  <h2>🔑 Clé API Agnes</h2>
  <input type="password" id="apiKey" placeholder="sk-..." autocomplete="off">
  <button class="btn btn-secondary" id="saveKey" style="margin-top:0.4rem;">Enregistrer</button>
  <div class="status" id="keyStatus">Aucune clé enregistrée</div>
</div>

<!-- ================= PANEL 1 : PHOTO PARLANTE ================= -->
<div class="main-panel active" id="panel-photo">
  <div class="panel">
    <h2>🖼️ Photo</h2>
    <div class="photo-zone" id="photoZoneP">
      <input type="file" accept="image/*" id="photoInputP">
      <div class="placeholder" id="photoPlaceholderP"><span class="icon">📷</span>Touchez pour choisir</div>
      <img id="photoPreviewP" class="hidden" alt="">
    </div>
  </div>

  <div class="panel">
    <h2>🎤 Ce qu'elle doit dire</h2>
    <div class="mode-toggle">
      <div class="mode-option selected" data-mode="voice" data-target="P">🎙️ Ma voix</div>
      <div class="mode-option" data-mode="text" data-target="P">✍️ Écrire</div>
    </div>
    <div class="mode-pane active" id="paneVoiceP">
      <div class="record-btn-wrap">
        <div class="record-indicator" id="recordBtnP">🎙️</div>
        <div class="record-label" id="recordLabelP">Touchez pour enregistrer</div>
      </div>
      <div class="audio-preview hidden" id="audioPreviewP">
        <audio controls id="audioPlayerP"></audio>
      </div>
    </div>
    <div class="mode-pane" id="paneTextP">
      <textarea id="speechTextP" placeholder="Le texte que la photo doit dire..."></textarea>
      <div class="status" style="font-size:0.65rem;text-align:left;margin-top:0.3rem;">
        ⚠️ Le mode texte ne peut pas être synchronisé avec les lèvres sur le modèle gratuit. Utilisez la voix.
      </div>
    </div>
  </div>

  <div class="panel">
    <h2>⚙️ Paramètres</h2>
    <div class="grid-2">
      <div class="control-row">
        <label class="control-label">Durée</label>
        <select id="durationP">
          <option value="4">4 s</option>
          <option value="5" selected>5 s</option>
          <option value="8">8 s</option>
          <option value="10">10 s</option>
          <option value="12">12 s</option>
        </select>
      </div>
      <div class="control-row">
        <label class="control-label">Format</label>
        <select id="aspectP">
          <option value="16:9">16:9 Paysage</option>
          <option value="9:16" selected>9:16 Portrait</option>
          <option value="1:1">1:1 Carré</option>
        </select>
      </div>
    </div>
    <div class="control-row">
      <label class="control-label">Mouvement</label>
      <select id="motionP">
        <option value="subtle">Subtil</option>
        <option value="moderate" selected>Modéré</option>
        <option value="strong">Fort</option>
      </select>
    </div>
  </div>

  <button class="btn btn-primary" id="genBtnP" disabled>▶ Générer la vidéo parlante</button>
  <div class="status" id="genStatusP"></div>
  <div class="progress-bar"><div class="progress-fill" id="progressP"></div></div>
  <div class="result-video hidden" id="resultP">
    <video controls playsinline id="videoPlayerP"></video>
    <div class="result-actions">
      <a class="btn btn-secondary" id="downloadP" download style="text-decoration:none;text-align:center;">⬇ Télécharger</a>
    </div>
  </div>
</div>

<!-- ================= PANEL 2 : TEXTE → VIDÉO ================= -->
<div class="main-panel" id="panel-text2video">
  <div class="panel">
    <h2>✍️ Description de la scène</h2>
    <textarea id="promptT" placeholder="Ex : Un chat astronaute flottant dans l'espace, style cinématographique..." style="min-height:110px;"></textarea>
  </div>

  <div class="panel">
    <h2>⚙️ Paramètres</h2>
    <div class="grid-2">
      <div class="control-row">
        <label class="control-label">Durée</label>
        <select id="durationT">
          <option value="4">4 s</option>
          <option value="5" selected>5 s</option>
          <option value="8">8 s</option>
          <option value="10">10 s</option>
          <option value="12">12 s</option>
        </select>
      </div>
      <div class="control-row">
        <label class="control-label">Format</label>
        <select id="aspectT">
          <option value="16:9" selected>16:9 Paysage</option>
          <option value="9:16">9:16 Portrait</option>
          <option value="1:1">1:1 Carré</option>
        </select>
      </div>
    </div>
    <div class="control-row">
      <label class="control-label">Mouvement</label>
      <select id="motionT">
        <option value="subtle">Subtil</option>
        <option value="moderate" selected>Modéré</option>
        <option value="strong">Fort</option>
      </select>
    </div>
    <div class="control-row">
      <label class="control-label">Prompt négatif (optionnel)</label>
      <input type="text" id="negativeT" placeholder="Ex : flou, déformation">
    </div>
  </div>

  <button class="btn btn-primary" id="genBtnT" disabled>▶ Générer la vidéo</button>
  <div class="status" id="genStatusT"></div>
  <div class="progress-bar"><div class="progress-fill" id="progressT"></div></div>
  <div class="result-video hidden" id="resultT">
    <video controls playsinline id="videoPlayerT"></video>
    <div class="result-actions">
      <a class="btn btn-secondary" id="downloadT" download style="text-decoration:none;text-align:center;">⬇ Télécharger</a>
    </div>
  </div>
</div>

<!-- ================= PANEL 3 : IMAGE → VIDÉO ================= -->
<div class="main-panel" id="panel-img2video">
  <div class="panel">
    <h2>🖼️ Image de départ</h2>
    <div class="photo-zone" id="photoZoneI">
      <input type="file" accept="image/*" id="photoInputI">
      <div class="placeholder" id="photoPlaceholderI"><span class="icon">📷</span>Touchez pour choisir</div>
      <img id="photoPreviewI" class="hidden" alt="">
    </div>
  </div>

  <div class="panel">
    <h2>✍️ Que doit-il se passer ?</h2>
    <textarea id="promptI" placeholder="Ex : La personne tourne doucement la tête vers la caméra..." style="min-height:90px;"></textarea>
  </div>

  <div class="panel">
    <h2>⚙️ Paramètres</h2>
    <div class="grid-2">
      <div class="control-row">
        <label class="control-label">Durée</label>
        <select id="durationI">
          <option value="4">4 s</option>
          <option value="5" selected>5 s</option>
          <option value="8">8 s</option>
          <option value="10">10 s</option>
          <option value="12">12 s</option>
        </select>
      </div>
      <div class="control-row">
        <label class="control-label">Format</label>
        <select id="aspectI">
          <option value="16:9" selected>16:9 Paysage</option>
          <option value="9:16">9:16 Portrait</option>
          <option value="1:1">1:1 Carré</option>
        </select>
      </div>
    </div>
    <div class="control-row">
      <label class="control-label">Mouvement</label>
      <select id="motionI">
        <option value="subtle">Subtil</option>
        <option value="moderate" selected>Modéré</option>
        <option value="strong">Fort</option>
      </select>
    </div>
  </div>

  <button class="btn btn-primary" id="genBtnI" disabled>▶ Générer la vidéo</button>
  <div class="status" id="genStatusI"></div>
  <div class="progress-bar"><div class="progress-fill" id="progressI"></div></div>
  <div class="result-video hidden" id="resultI">
    <video controls playsinline id="videoPlayerI"></video>
    <div class="result-actions">
      <a class="btn btn-secondary" id="downloadI" download style="text-decoration:none;text-align:center;">⬇ Télécharger</a>
    </div>
  </div>
</div>

<!-- ================= PANEL 4 : KEYFRAMES ================= -->
<div class="main-panel" id="panel-keyframes">
  <div class="panel">
    <h2>🖼️ Image de début</h2>
    <div class="photo-zone" id="photoZoneK1">
      <input type="file" accept="image/*" id="photoInputK1">
      <div class="placeholder" id="photoPlaceholderK1"><span class="icon">📷</span>Touchez pour choisir</div>
      <img id="photoPreviewK1" class="hidden" alt="">
    </div>
  </div>

  <div class="panel">
    <h2>🖼️ Image de fin</h2>
    <div class="photo-zone" id="photoZoneK2">
      <input type="file" accept="image/*" id="photoInputK2">
      <div class="placeholder" id="photoPlaceholderK2"><span class="icon">📷</span>Touchez pour choisir</div>
      <img id="photoPreviewK2" class="hidden" alt="">
    </div>
  </div>

  <div class="panel">
    <h2>✍️ Description du mouvement</h2>
    <textarea id="promptK" placeholder="Ex : Transition fluide entre les deux images, mouvement naturel..." style="min-height:80px;"></textarea>
  </div>

  <div class="panel">
    <h2>⚙️ Paramètres</h2>
    <div class="grid-2">
      <div class="control-row">
        <label class="control-label">Durée</label>
        <select id="durationK">
          <option value="4">4 s</option>
          <option value="5" selected>5 s</option>
          <option value="6">6 s</option>
          <option value="8">8 s</option>
          <option value="10">10 s</option>
          <option value="12">12 s</option>
        </select>
      </div>
      <div class="control-row">
        <label class="control-label">Format</label>
        <select id="aspectK">
          <option value="16:9" selected>16:9 Paysage</option>
          <option value="9:16">9:16 Portrait</option>
          <option value="1:1">1:1 Carré</option>
        </select>
      </div>
    </div>
    <div class="control-row">
      <label class="control-label">Mouvement</label>
      <select id="motionK">
        <option value="subtle">Subtil</option>
        <option value="moderate" selected>Modéré</option>
        <option value="strong">Fort</option>
      </select>
    </div>
  </div>

  <button class="btn btn-primary" id="genBtnK" disabled>▶ Générer la transition</button>
  <div class="status" id="genStatusK"></div>
  <div class="progress-bar"><div class="progress-fill" id="progressK"></div></div>
  <div class="result-video hidden" id="resultK">
    <video controls playsinline id="videoPlayerK"></video>
    <div class="result-actions">
      <a class="btn btn-secondary" id="downloadK" download style="text-decoration:none;text-align:center;">⬇ Télécharger</a>
    </div>
  </div>
</div>

<!-- JOURNAL -->
<div class="panel">
  <h2>📋 Journal</h2>
  <div class="log-panel" id="log">
    <div class="log-line">En attente...</div>
  </div>
</div>

<script>
const API_BASE = 'https://apihub.agnes-ai.com/v1';
const POLL_BASE = 'https://apihub.agnes-ai.com/agnesapi';
const MODEL = 'agnes-video-2.5-flash';
const KEY_STORAGE = 'agnes_studio_key';

const S = {
  photoP: null, voiceBase64P: null, isRecordingP: false, mediaRecorderP: null, chunksP: [],
  photoI: null, photoK1: null, photoK2: null,
  isGenerating: false
};

const $ = id => document.getElementById(id);
const sleep = ms => new Promise(r => setTimeout(r, ms));

function log(msg, type) {
  const el = $('log');
  const line = document.createElement('div');
  line.className = 'log-line' + (type ? ' ' + type : '');
  const t = new Date().toLocaleTimeString('fr-FR');
  line.textContent = '[' + t + '] ' + msg;
  el.appendChild(line);
  el.scrollTop = el.scrollHeight;
  console.log('[Agnes]', msg);
}

function getKey() {
  try { return (localStorage.getItem(KEY_STORAGE) || '').trim(); } catch(e){ return ''; }
}
function saveKey() {
  const v = $('apiKey').value.trim();
  try {
    if (v) localStorage.setItem(KEY_STORAGE, v);
    else localStorage.removeItem(KEY_STORAGE);
  } catch(e){}
  updateKeyUI();
}
function updateKeyUI() {
  const k = getKey();
  const el = $('keyStatus');
  if (k) {
    el.textContent = '✓ Clé active · ' + k.slice(0,7) + '…' + k.slice(-4);
    el.className = 'status ok';
    $('apiKey').value = k;
  } else {
    el.textContent = 'Aucune clé enregistrée';
    el.className = 'status err';
  }
  updateAllButtons();
}

document.querySelectorAll('.tab-btn').forEach(tab => {
  tab.addEventListener('click', () => {
    document.querySelectorAll('.tab-btn').forEach(t => t.classList.remove('active'));
    document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active'));
    tab.classList.add('active');
    $('panel-' + tab.dataset.panel).classList.add('active');
  });
});

function compress(dataUrl, maxSide) {
  return new Promise(resolve => {
    const img = new Image();
    img.onload = () => {
      let { width, height } = img;
      const scale = Math.min(1, maxSide / Math.max(width, height));
      width = Math.round(width * scale);
      height = Math.round(height * scale);
      const c = document.createElement('canvas');
      c.width = width; c.height = height;
      c.getContext('2d').drawImage(img, 0, 0, width, height);
      resolve(c.toDataURL('image/jpeg', 0.85));
    };
    img.onerror = () => resolve(dataUrl);
    img.src = dataUrl;
  });
}

function bindPhoto(zoneId, inputId, previewId, placeholderId, setter, maxSide) {
  $(zoneId).addEventListener('click', () => $(inputId).click());
  $(inputId).addEventListener('change', async (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    const r = new FileReader();
    r.onload = async ev => {
      const compressed = await compress(ev.target.result, maxSide || 1024);
      setter(compressed);
      $(previewId).src = compressed;
      $(previewId).classList.remove('hidden');
      $(placeholderId).classList.add('hidden');
      updateAllButtons();
    };
    r.readAsDataURL(f);
  });
}

bindPhoto('photoZoneP', 'photoInputP', 'photoPreviewP', 'photoPlaceholderP', v => S.photoP = v, 1024);
bindPhoto('photoZoneI', 'photoInputI', 'photoPreviewI', 'photoPlaceholderI', v => S.photoI = v, 1280);
bindPhoto('photoZoneK1', 'photoInputK1', 'photoPreviewK1', 'photoPlaceholderK1', v => S.photoK1 = v, 1280);
bindPhoto('photoZoneK2', 'photoInputK2', 'photoPreviewK2', 'photoPlaceholderK2', v => S.photoK2 = v, 1280);

document.querySelectorAll('.mode-option[data-target="P"]').forEach(el => {
  el.addEventListener('click', () => {
    document.querySelectorAll('.mode-option[data-target="P"]').forEach(o => o.classList.remove('selected'));
    el.classList.add('selected');
    const mode = el.dataset.mode;
    $('paneVoiceP').classList.toggle('active', mode === 'voice');
    $('paneTextP').classList.toggle('active', mode === 'text');
    updateAllButtons();
  });
});

$('recordBtnP').addEventListener('click', toggleRecordingP);
async function toggleRecordingP() {
  if (S.isRecordingP) { stopRecordingP(); return; }
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    S.chunksP = [];
    S.mediaRecorderP = new MediaRecorder(stream);
    S.mediaRecorderP.ondataavailable = e => { if (e.data.size > 0) S.chunksP.push(e.data); };
    S.mediaRecorderP.onstop = async () => {
      stream.getTracks().forEach(t => t.stop());
      const blob = new Blob(S.chunksP, { type: 'audio/webm' });
      S.voiceBase64P = await blobToDataURL(blob);
      $('audioPlayerP').src = URL.createObjectURL(blob);
      $('audioPreviewP').classList.remove('hidden');
      $('recordLabelP').textContent = '✓ Enregistrement prêt';
      $('recordBtnP').classList.remove('recording');
      S.isRecordingP = false;
      updateAllButtons();
    };
    S.mediaRecorderP.start();
    S.isRecordingP = true;
    $('recordBtnP').classList.add('recording');
    $('recordLabelP').textContent = '🔴 Enregistrement…';
  } catch (err) {
    $('genStatusP').textContent = 'Erreur micro : ' + err.message;
    $('genStatusP').className = 'status err';
  }
}
function stopRecordingP() {
  if (S.mediaRecorderP && S.mediaRecorderP.state !== 'inactive') S.mediaRecorderP.stop();
}
function blobToDataURL(blob) {
  return new Promise(res => {
    const r = new FileReader();
    r.onload = e => res(e.target.result);
    r.readAsDataURL(blob);
  });
}

function updateAllButtons() {
  const hasKey = !!getKey();
  let hasSourceP = false;
  if ($('paneTextP').classList.contains('active')) hasSourceP = $('speechTextP').value.trim().length > 0;
  else hasSourceP = !!S.voiceBase64P;
  $('genBtnP').disabled = !(hasKey && S.photoP && hasSourceP) || S.isGenerating;
  $('genBtnT').disabled = !(hasKey && $('promptT').value.trim().length > 0) || S.isGenerating;
  $('genBtnI').disabled = !(hasKey && S.photoI && $('promptI').value.trim().length > 0) || S.isGenerating;
  $('genBtnK').disabled = !(hasKey && S.photoK1 && S.photoK2 && $('promptK').value.trim().length > 0) || S.isGenerating;
}

$('speechTextP').addEventListener('input', updateAllButtons);
$('promptT').addEventListener('input', updateAllButtons);
$('promptI').addEventListener('input', updateAllButtons);
$('promptK').addEventListener('input', updateAllButtons);

async function createTask(body) {
  const key = getKey();
  const res = await fetch(API_BASE + '/videos', {
    method: 'POST',
    headers: { 'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  });
  if (!res.ok) {
    const t = await res.text();
    throw new Error('HTTP ' + res.status + ' — ' + t.slice(0, 200));
  }
  const data = await res.json();
  const id = data.video_id || data.id || data.task_id;
  if (!id) throw new Error('Pas de video_id retourné');
  return id;
}

async function pollTask(videoId, onProgress) {
  const key = getKey();
  for (let i = 0; i < 180; i++) {
    if (i > 0) await sleep(5000);
    const url = POLL_BASE + '?video_id=' + encodeURIComponent(videoId) + '&model_name=' + encodeURIComponent(MODEL);
    const res = await fetch(url, { headers: { 'Authorization': 'Bearer ' + key } });
    if (res.status === 429) {
      log('Rate limit (429), attente 15s…', 'warn');
      await sleep(15000);
      continue;
    }
    if (!res.ok) throw new Error('Polling HTTP ' + res.status);
    const d = await res.json();
    const status = d.status || 'unknown';
    const progress = d.progress || 0;
    if (onProgress) onProgress(progress);
    if (status === 'completed' || status === 'succeeded' || status === 'done') {
      const videoUrl = (d.metadata && d.metadata.url) || d.url || (d.output && d.output.url);
      if (!videoUrl) throw new Error('Terminé mais pas d\'URL');
      return videoUrl;
    }
    if (status === 'failed' || status === 'error' || status === 'cancelled') {
      throw new Error('Échec : ' + (d.error?.message || d.error || status));
    }
  }
  throw new Error('Délai dépassé');
}

async function runGeneration(name, body, statusId, progressId, resultId, playerId, downloadId) {
  S.isGenerating = true;
  updateAllButtons();
  $(statusId).textContent = 'Envoi…';
  $(statusId).className = 'status';
  $(progressId).style.width = '0%';
  $(resultId).classList.add('hidden');

  try {
    log('Création tâche ' + name + '…');
    const videoId = await createTask(body);
    log('Tâche créée : ' + videoId, 'ok');
    $(statusId).textContent = 'Génération…';

    const url = await pollTask(videoId, p => {
      $(progressId).style.width = p + '%';
      $(statusId).textContent = 'Génération… ' + p + '%';
    });

    $(playerId).src = url;
    $(downloadId).href = url;
    $(resultId).classList.remove('hidden');
    $(statusId).textContent = '✓ Vidéo prête';
    $(statusId).className = 'status ok';
    log('Vidéo prête.', 'ok');
  } catch (err) {
    $(statusId).textContent = 'Erreur : ' + err.message;
    $(statusId).className = 'status err';
    log('Erreur : ' + err.message, 'err');
  } finally {
    S.isGenerating = false;
    updateAllButtons();
  }
}

const MOTION_PROMPTS = {
  subtle: 'subtle gentle movement, minimal animation',
  moderate: 'moderate natural movement, clear motion',
  strong: 'dynamic strong movement, energetic action'
};

$('genBtnP').addEventListener('click', () => {
  if (!S.voiceBase64P) {
    $('genStatusP').textContent = 'Enregistrez d\'abord votre voix.';
    $('genStatusP').className = 'status err';
    return;
  }
  const motion = MOTION_PROMPTS[$('motionP').value];
  const body = {
    model: MODEL,
    mode: 'reference',
    prompt: 'The person in <Picture 1> speaks naturally with <Audio 1>, ' + motion + '. Natural expression, cinematic portrait lighting.',
    images: [S.photoP],
    audios: [S.voiceBase64P],
    seconds: $('durationP').value,
    size: '720P',
    aspect_ratio: $('aspectP').value
  };
  runGeneration('photo', body, 'genStatusP', 'progressP', 'resultP', 'videoPlayerP', 'downloadP');
});

$('genBtnT').addEventListener('click', () => {
  const motion = MOTION_PROMPTS[$('motionT').value];
  const neg = $('negativeT').value.trim();
  const body = {
    model: MODEL,
    mode: 'text',
    prompt: $('promptT').value.trim() + ', ' + motion,
    seconds: $('durationT').value,
    size: '720P',
    aspect_ratio: $('aspectT').value
  };
  if (neg) body.negative_prompt = neg;
  runGeneration('text', body, 'genStatusT', 'progressT', 'resultT', 'videoPlayerT', 'downloadT');
});

$('genBtnI').addEventListener('click', () => {
  const motion = MOTION_PROMPTS[$('motionI').value];
  const body = {
    model: MODEL,
    mode: 'text',
    prompt: $('promptI').value.trim() + ', ' + motion,
    first_frame: S.photoI,
    seconds: $('durationI').value,
    size: '720P',
    aspect_ratio: $('aspectI').value
  };
  runGeneration('image', body, 'genStatusI', 'progressI', 'resultI', 'videoPlayerI', 'downloadI');
});

$('genBtnK').addEventListener('click', () => {
  const motion = MOTION_PROMPTS[$('motionK').value];
  const body = {
    model: MODEL,
    mode: 'keyframe',
    prompt: $('promptK').value.trim() + ', ' + motion,
    first_frame: S.photoK1,
    last_frame: S.photoK2,
    seconds: $('durationK').value,
    size: '720P',
    aspect_ratio: $('aspectK').value
  };
  runGeneration('keyframes', body, 'genStatusK', 'progressK', 'resultK', 'videoPlayerK', 'downloadK');
});

$('saveKey').addEventListener('click', saveKey);
$('apiKey').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); saveKey(); } });
updateKeyUI();
log('Application prête.');
</script>
</body>
</html>