CoolFace
Apppublic

joejoez/claude-code-projects

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
index.html1131 linesDownload Raw Back to static
1<!DOCTYPE html>2<html lang="en">3<head>4    <meta charset="UTF-8">5    <meta name="viewport" content="width=device-width, initial-scale=1.0">6    <title>RESONANCE — Sound Modifier</title>7    <link rel="preconnect" href="https://fonts.googleapis.com">8    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>9    <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=DM+Sans:ital,wght@0,300;0,400;0,500;0,700;1,400&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">10    <style>11        :root {12            --bg-deep: #0a0a0f;13            --bg-surface: #12121a;14            --bg-elevated: #1a1a26;15            --bg-hover: #222233;16            --border-subtle: rgba(255, 255, 255, 0.06);17            --border-accent: rgba(212, 175, 55, 0.3);18            --text-primary: #e8e6e1;19            --text-secondary: #8a8690;20            --text-muted: #5a5660;21            --accent-gold: #d4af37;22            --accent-gold-dim: rgba(212, 175, 55, 0.15);23            --accent-warm: #c9956b;24            --accent-rose: #b5647a;25            --accent-blue: #6b8cc9;26            --success: #5cb885;27            --error: #c95b5b;28            --radius-sm: 6px;29            --radius-md: 12px;30            --radius-lg: 20px;31            --shadow-glow: 0 0 40px rgba(212, 175, 55, 0.08);32        }33 34        *, *::before, *::after {35            box-sizing: border-box;36            margin: 0;37            padding: 0;38        }39 40        html {41            font-size: 16px;42            -webkit-font-smoothing: antialiased;43            -moz-osx-font-smoothing: grayscale;44        }45 46        body {47            font-family: 'DM Sans', sans-serif;48            background: var(--bg-deep);49            color: var(--text-primary);50            min-height: 100vh;51            overflow-x: hidden;52            position: relative;53        }54 55        /* === BACKGROUND ATMOSPHERE === */56        body::before {57            content: '';58            position: fixed;59            top: -50%;60            left: -50%;61            width: 200%;62            height: 200%;63            background:64                radial-gradient(ellipse at 20% 50%, rgba(212, 175, 55, 0.03) 0%, transparent 50%),65                radial-gradient(ellipse at 80% 20%, rgba(107, 140, 201, 0.03) 0%, transparent 50%),66                radial-gradient(ellipse at 50% 80%, rgba(181, 100, 122, 0.02) 0%, transparent 50%);67            pointer-events: none;68            z-index: 0;69        }70 71        /* Geometric grid overlay */72        body::after {73            content: '';74            position: fixed;75            inset: 0;76            background-image:77                linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px),78                linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px);79            background-size: 60px 60px;80            pointer-events: none;81            z-index: 0;82        }83 84        /* === LAYOUT === */85        .app {86            position: relative;87            z-index: 1;88            max-width: 860px;89            margin: 0 auto;90            padding: 3rem 2rem 4rem;91        }92 93        /* === HEADER === */94        .header {95            text-align: center;96            margin-bottom: 3.5rem;97            animation: fadeInDown 0.8s ease-out;98        }99 100        .header__eyebrow {101            font-family: 'JetBrains Mono', monospace;102            font-size: 0.7rem;103            font-weight: 300;104            letter-spacing: 0.35em;105            text-transform: uppercase;106            color: var(--accent-gold);107            margin-bottom: 1rem;108            display: flex;109            align-items: center;110            justify-content: center;111            gap: 1rem;112        }113 114        .header__eyebrow::before,115        .header__eyebrow::after {116            content: '';117            width: 40px;118            height: 1px;119            background: linear-gradient(90deg, transparent, var(--accent-gold), transparent);120        }121 122        .header__title {123            font-family: 'Playfair Display', serif;124            font-size: 3.5rem;125            font-weight: 900;126            letter-spacing: -0.02em;127            line-height: 1;128            background: linear-gradient(135deg, var(--text-primary) 0%, var(--accent-gold) 50%, var(--accent-warm) 100%);129            -webkit-background-clip: text;130            -webkit-text-fill-color: transparent;131            background-clip: text;132            margin-bottom: 0.75rem;133        }134 135        .header__subtitle {136            font-size: 0.95rem;137            color: var(--text-secondary);138            font-weight: 300;139            letter-spacing: 0.02em;140        }141 142        /* === CARD === */143        .card {144            background: var(--bg-surface);145            border: 1px solid var(--border-subtle);146            border-radius: var(--radius-lg);147            padding: 2.5rem;148            margin-bottom: 1.5rem;149            position: relative;150            overflow: hidden;151            animation: fadeInUp 0.6s ease-out;152            animation-fill-mode: both;153        }154 155        .card:nth-child(2) { animation-delay: 0.1s; }156        .card:nth-child(3) { animation-delay: 0.2s; }157        .card:nth-child(4) { animation-delay: 0.3s; }158 159        .card::before {160            content: '';161            position: absolute;162            top: 0;163            left: 0;164            right: 0;165            height: 1px;166            background: linear-gradient(90deg, transparent, var(--accent-gold-dim), transparent);167        }168 169        .card__title {170            font-family: 'Playfair Display', serif;171            font-size: 1.25rem;172            font-weight: 700;173            margin-bottom: 1.75rem;174            display: flex;175            align-items: center;176            gap: 0.75rem;177        }178 179        .card__title-icon {180            width: 28px;181            height: 28px;182            border-radius: 8px;183            display: flex;184            align-items: center;185            justify-content: center;186            font-size: 0.85rem;187            flex-shrink: 0;188        }189 190        .card__title-icon--gold { background: var(--accent-gold-dim); color: var(--accent-gold); }191        .card__title-icon--blue { background: rgba(107,140,201,0.15); color: var(--accent-blue); }192        .card__title-icon--rose { background: rgba(181,100,122,0.15); color: var(--accent-rose); }193        .card__title-icon--warm { background: rgba(201,149,107,0.15); color: var(--accent-warm); }194 195        /* === UPLOAD ZONE === */196        .upload-zone {197            border: 2px dashed var(--border-subtle);198            border-radius: var(--radius-md);199            padding: 3rem 2rem;200            text-align: center;201            cursor: pointer;202            transition: all 0.3s ease;203            position: relative;204            overflow: hidden;205        }206 207        .upload-zone:hover {208            border-color: var(--accent-gold);209            background: var(--accent-gold-dim);210        }211 212        .upload-zone.drag-over {213            border-color: var(--accent-gold);214            background: var(--accent-gold-dim);215            transform: scale(1.01);216        }217 218        .upload-zone.has-file {219            border-color: var(--success);220            border-style: solid;221            background: rgba(92, 184, 133, 0.06);222        }223 224        .upload-zone__icon {225            font-size: 2.5rem;226            margin-bottom: 0.75rem;227            opacity: 0.6;228        }229 230        .upload-zone__text {231            font-size: 0.95rem;232            color: var(--text-secondary);233            margin-bottom: 0.25rem;234        }235 236        .upload-zone__hint {237            font-size: 0.75rem;238            color: var(--text-muted);239            font-family: 'JetBrains Mono', monospace;240        }241 242        .upload-zone__filename {243            font-family: 'JetBrains Mono', monospace;244            font-size: 0.85rem;245            color: var(--success);246            margin-top: 0.5rem;247            word-break: break-all;248        }249 250        .upload-zone input[type="file"] {251            display: none;252        }253 254        /* === CONTROLS === */255        .control-group {256            margin-bottom: 1.75rem;257        }258 259        .control-group:last-child {260            margin-bottom: 0;261        }262 263        .control-label {264            font-family: 'JetBrains Mono', monospace;265            font-size: 0.7rem;266            font-weight: 500;267            letter-spacing: 0.15em;268            text-transform: uppercase;269            color: var(--text-muted);270            margin-bottom: 0.75rem;271            display: block;272        }273 274        /* === PITCH SELECTOR === */275        .pitch-grid {276            display: grid;277            grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));278            gap: 6px;279        }280 281        .pitch-btn {282            background: var(--bg-elevated);283            border: 1px solid var(--border-subtle);284            border-radius: var(--radius-sm);285            padding: 0.6rem 0.25rem;286            color: var(--text-secondary);287            font-family: 'JetBrains Mono', monospace;288            font-size: 0.75rem;289            cursor: pointer;290            transition: all 0.2s ease;291            text-align: center;292        }293 294        .pitch-btn:hover {295            border-color: var(--accent-gold);296            color: var(--text-primary);297            background: var(--bg-hover);298        }299 300        .pitch-btn.active {301            background: var(--accent-gold-dim);302            border-color: var(--accent-gold);303            color: var(--accent-gold);304            font-weight: 500;305        }306 307        .pitch-btn .note-name {308            display: block;309            font-size: 0.65rem;310            color: var(--text-muted);311            margin-top: 2px;312        }313 314        .pitch-btn.active .note-name {315            color: var(--accent-warm);316        }317 318        /* === SPEED SLIDER === */319        .speed-control {320            display: flex;321            align-items: center;322            gap: 1.25rem;323        }324 325        .speed-slider-wrap {326            flex: 1;327            position: relative;328        }329 330        input[type="range"] {331            -webkit-appearance: none;332            width: 100%;333            height: 4px;334            background: var(--bg-elevated);335            border-radius: 2px;336            outline: none;337            cursor: pointer;338        }339 340        input[type="range"]::-webkit-slider-thumb {341            -webkit-appearance: none;342            width: 18px;343            height: 18px;344            background: var(--accent-gold);345            border-radius: 50%;346            cursor: pointer;347            box-shadow: 0 0 12px rgba(212, 175, 55, 0.3);348            transition: transform 0.15s ease;349        }350 351        input[type="range"]::-webkit-slider-thumb:hover {352            transform: scale(1.2);353        }354 355        input[type="range"]::-moz-range-thumb {356            width: 18px;357            height: 18px;358            background: var(--accent-gold);359            border-radius: 50%;360            cursor: pointer;361            border: none;362            box-shadow: 0 0 12px rgba(212, 175, 55, 0.3);363        }364 365        .speed-value {366            font-family: 'JetBrains Mono', monospace;367            font-size: 1.1rem;368            font-weight: 500;369            color: var(--accent-gold);370            min-width: 52px;371            text-align: right;372        }373 374        .speed-markers {375            display: flex;376            justify-content: space-between;377            margin-top: 6px;378            font-family: 'JetBrains Mono', monospace;379            font-size: 0.6rem;380            color: var(--text-muted);381        }382 383        /* === OUTPUT MODE TOGGLE === */384        .mode-toggle {385            display: grid;386            grid-template-columns: repeat(3, 1fr);387            gap: 8px;388        }389 390        .mode-btn {391            background: var(--bg-elevated);392            border: 1px solid var(--border-subtle);393            border-radius: var(--radius-md);394            padding: 1rem 0.75rem;395            color: var(--text-secondary);396            cursor: pointer;397            transition: all 0.25s ease;398            text-align: center;399        }400 401        .mode-btn:hover {402            border-color: var(--accent-gold);403            background: var(--bg-hover);404        }405 406        .mode-btn.active {407            background: var(--accent-gold-dim);408            border-color: var(--accent-gold);409            color: var(--accent-gold);410        }411 412        .mode-btn__icon {413            font-size: 1.5rem;414            margin-bottom: 0.4rem;415        }416 417        .mode-btn__label {418            font-size: 0.8rem;419            font-weight: 500;420            display: block;421        }422 423        .mode-btn__desc {424            font-size: 0.65rem;425            color: var(--text-muted);426            margin-top: 0.2rem;427            display: block;428            font-family: 'JetBrains Mono', monospace;429        }430 431        .mode-btn.active .mode-btn__desc {432            color: var(--accent-warm);433        }434 435        /* === FORMAT TOGGLE === */436        .format-toggle {437            display: flex;438            gap: 8px;439            margin-top: 1.75rem;440        }441 442        .format-btn {443            flex: 1;444            background: var(--bg-elevated);445            border: 1px solid var(--border-subtle);446            border-radius: var(--radius-sm);447            padding: 0.65rem 1rem;448            color: var(--text-secondary);449            font-family: 'JetBrains Mono', monospace;450            font-size: 0.8rem;451            cursor: pointer;452            transition: all 0.2s ease;453            text-align: center;454        }455 456        .format-btn:hover {457            border-color: var(--accent-gold);458            background: var(--bg-hover);459        }460 461        .format-btn.active {462            background: var(--accent-gold-dim);463            border-color: var(--accent-gold);464            color: var(--accent-gold);465            font-weight: 500;466        }467 468        /* === PROCESS BUTTON === */469        .process-btn {470            width: 100%;471            padding: 1.1rem 2rem;472            background: linear-gradient(135deg, var(--accent-gold), var(--accent-warm));473            border: none;474            border-radius: var(--radius-md);475            color: var(--bg-deep);476            font-family: 'Playfair Display', serif;477            font-size: 1.1rem;478            font-weight: 700;479            letter-spacing: 0.05em;480            cursor: pointer;481            transition: all 0.3s ease;482            position: relative;483            overflow: hidden;484        }485 486        .process-btn::before {487            content: '';488            position: absolute;489            inset: 0;490            background: linear-gradient(135deg, transparent, rgba(255,255,255,0.15), transparent);491            transform: translateX(-100%);492            transition: transform 0.6s ease;493        }494 495        .process-btn:hover::before {496            transform: translateX(100%);497        }498 499        .process-btn:hover {500            transform: translateY(-1px);501            box-shadow: 0 8px 30px rgba(212, 175, 55, 0.25);502        }503 504        .process-btn:disabled {505            opacity: 0.4;506            cursor: not-allowed;507            transform: none;508            box-shadow: none;509        }510 511        .process-btn:disabled::before {512            display: none;513        }514 515        /* === STATUS / PROGRESS === */516        .status-bar {517            display: none;518            margin-top: 1.5rem;519            padding: 1.25rem;520            background: var(--bg-elevated);521            border-radius: var(--radius-md);522            border: 1px solid var(--border-subtle);523        }524 525        .status-bar.visible {526            display: block;527            animation: fadeInUp 0.3s ease-out;528        }529 530        .status-bar__text {531            font-family: 'JetBrains Mono', monospace;532            font-size: 0.8rem;533            color: var(--text-secondary);534            margin-bottom: 0.75rem;535            display: flex;536            align-items: center;537            gap: 0.5rem;538        }539 540        .status-bar__text .spinner {541            display: inline-block;542            width: 14px;543            height: 14px;544            border: 2px solid var(--accent-gold-dim);545            border-top-color: var(--accent-gold);546            border-radius: 50%;547            animation: spin 0.8s linear infinite;548        }549 550        .progress-track {551            height: 3px;552            background: var(--bg-deep);553            border-radius: 2px;554            overflow: hidden;555        }556 557        .progress-fill {558            height: 100%;559            background: linear-gradient(90deg, var(--accent-gold), var(--accent-warm));560            border-radius: 2px;561            width: 0%;562            transition: width 0.3s ease;563            animation: progressPulse 2s ease-in-out infinite;564        }565 566        .status-bar.completed .progress-fill {567            animation: none;568            width: 100%;569            background: var(--success);570        }571 572        .status-bar.error {573            border-color: rgba(201, 91, 91, 0.3);574        }575 576        .status-bar.error .status-bar__text {577            color: var(--error);578        }579 580        /* === DOWNLOAD BUTTON === */581        .download-btn {582            display: none;583            width: 100%;584            margin-top: 1rem;585            padding: 0.9rem 2rem;586            background: transparent;587            border: 1px solid var(--success);588            border-radius: var(--radius-md);589            color: var(--success);590            font-family: 'DM Sans', sans-serif;591            font-size: 0.9rem;592            font-weight: 500;593            cursor: pointer;594            transition: all 0.25s ease;595        }596 597        .download-btn.visible {598            display: block;599            animation: fadeInUp 0.3s ease-out;600        }601 602        .download-btn:hover {603            background: rgba(92, 184, 133, 0.1);604            transform: translateY(-1px);605        }606 607        /* === DOWNLOAD LINK === */608        .download-link {609            display: none;610            margin-top: 0.75rem;611            padding: 0.65rem 1rem;612            background: var(--bg-elevated);613            border: 1px solid var(--border-subtle);614            border-radius: var(--radius-sm);615            font-family: 'JetBrains Mono', monospace;616            font-size: 0.7rem;617            color: var(--text-secondary);618            word-break: break-all;619            position: relative;620        }621 622        .download-link.visible {623            display: block;624            animation: fadeInUp 0.3s ease-out;625        }626 627        .download-link__label {628            font-size: 0.6rem;629            letter-spacing: 0.1em;630            text-transform: uppercase;631            color: var(--text-muted);632            margin-bottom: 0.4rem;633        }634 635        .download-link__url {636            display: flex;637            align-items: center;638            gap: 0.5rem;639        }640 641        .download-link__text {642            flex: 1;643            overflow: hidden;644            text-overflow: ellipsis;645            color: var(--accent-blue);646        }647 648        .download-link__copy {649            flex-shrink: 0;650            background: var(--bg-hover);651            border: 1px solid var(--border-subtle);652            border-radius: var(--radius-sm);653            padding: 0.3rem 0.6rem;654            color: var(--text-secondary);655            font-family: 'JetBrains Mono', monospace;656            font-size: 0.65rem;657            cursor: pointer;658            transition: all 0.2s ease;659        }660 661        .download-link__copy:hover {662            border-color: var(--accent-gold);663            color: var(--accent-gold);664        }665 666        /* === FOOTER === */667        .footer {668            text-align: center;669            margin-top: 3rem;670            padding-top: 2rem;671            border-top: 1px solid var(--border-subtle);672            animation: fadeInUp 0.6s ease-out 0.4s both;673        }674 675        .footer__text {676            font-family: 'JetBrains Mono', monospace;677            font-size: 0.65rem;678            color: var(--text-muted);679            letter-spacing: 0.1em;680        }681 682        .footer__tech {683            margin-top: 0.5rem;684            display: flex;685            justify-content: center;686            gap: 1.5rem;687            font-family: 'JetBrains Mono', monospace;688            font-size: 0.6rem;689            color: var(--text-muted);690        }691 692        .footer__tech span {693            opacity: 0.5;694        }695 696        /* === ANIMATIONS === */697        @keyframes fadeInDown {698            from { opacity: 0; transform: translateY(-20px); }699            to { opacity: 1; transform: translateY(0); }700        }701 702        @keyframes fadeInUp {703            from { opacity: 0; transform: translateY(16px); }704            to { opacity: 1; transform: translateY(0); }705        }706 707        @keyframes spin {708            to { transform: rotate(360deg); }709        }710 711        @keyframes progressPulse {712            0%, 100% { opacity: 1; }713            50% { opacity: 0.7; }714        }715 716        /* === RESPONSIVE === */717        @media (max-width: 640px) {718            .app { padding: 2rem 1.25rem 3rem; }719            .header__title { font-size: 2.5rem; }720            .card { padding: 1.75rem; }721            .pitch-grid { grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); }722            .mode-toggle { grid-template-columns: 1fr; }723        }724    </style>725</head>726<body>727    <div class="app">728        <!-- Header -->729        <header class="header">730            <div class="header__eyebrow">Sound Modifier</div>731            <h1 class="header__title">RESONANCE</h1>732            <p class="header__subtitle">Separate vocals, shift pitch, adjust tempo — all locally on your machine.</p>733        </header>734 735        <!-- Upload Card -->736        <div class="card">737            <h2 class="card__title">738                <span class="card__title-icon card__title-icon--gold">&#9835;</span>739                Source Audio740            </h2>741            <div class="upload-zone" id="uploadZone">742                <div class="upload-zone__icon">&#128190;</div>743                <div class="upload-zone__text">Drop your audio file here or click to browse</div>744                <div class="upload-zone__hint">WAV &middot; MP3 &middot; FLAC &middot; OGG &middot; M4A</div>745                <div class="upload-zone__filename" id="fileName"></div>746                <input type="file" id="fileInput" accept="audio/*">747            </div>748        </div>749 750        <!-- Output Mode Card -->751        <div class="card">752            <h2 class="card__title">753                <span class="card__title-icon card__title-icon--rose">&#9783;</span>754                Output Mode755            </h2>756            <div class="mode-toggle">757                <button class="mode-btn" data-mode="vocals" onclick="setMode('vocals')">758                    <span class="mode-btn__icon">&#127908;</span>759                    <span class="mode-btn__label">Vocals Only</span>760                    <span class="mode-btn__desc">isolated voice</span>761                </button>762                <button class="mode-btn" data-mode="music" onclick="setMode('music')">763                    <span class="mode-btn__icon">&#127929;</span>764                    <span class="mode-btn__label">Music Only</span>765                    <span class="mode-btn__desc">instrumental</span>766                </button>767                <button class="mode-btn active" data-mode="both" onclick="setMode('both')">768                    <span class="mode-btn__icon">&#127926;</span>769                    <span class="mode-btn__label">Full Mix</span>770                    <span class="mode-btn__desc">vocals + music</span>771                </button>772            </div>773 774            <div class="control-group" style="margin-top: 1.75rem;">775                <label class="control-label">Output Format</label>776                <div class="format-toggle">777                    <button class="format-btn active" data-format="mp3" onclick="setFormat('mp3')">MP3</button>778                    <button class="format-btn" data-format="wav" onclick="setFormat('wav')">WAV</button>779                </div>780            </div>781        </div>782 783        <!-- Pitch & Speed Card -->784        <div class="card">785            <h2 class="card__title">786                <span class="card__title-icon card__title-icon--blue">&#9838;</span>787                Pitch &amp; Tempo788            </h2>789 790            <div class="control-group">791                <label class="control-label">Pitch Shift (semitones)</label>792                <div class="pitch-grid" id="pitchGrid"></div>793            </div>794 795            <div class="control-group">796                <label class="control-label">Playback Speed</label>797                <div class="speed-control">798                    <div class="speed-slider-wrap">799                        <input type="range" id="speedSlider" min="0.25" max="2.0" step="0.05" value="1.0">800                        <div class="speed-markers">801                            <span>0.25&times;</span>802                            <span>1.0&times;</span>803                            <span>2.0&times;</span>804                        </div>805                    </div>806                    <div class="speed-value" id="speedValue">1.00&times;</div>807                </div>808            </div>809        </div>810 811        <!-- Process Card -->812        <div class="card">813            <h2 class="card__title">814                <span class="card__title-icon card__title-icon--warm">&#9881;</span>815                Process816            </h2>817            <button class="process-btn" id="processBtn" disabled onclick="processAudio()">818                Transform Audio819            </button>820            <div class="status-bar" id="statusBar">821                <div class="status-bar__text" id="statusText">822                    <span class="spinner"></span>823                    <span>Processing...</span>824                </div>825                <div class="progress-track">826                    <div class="progress-fill" id="progressFill"></div>827                </div>828            </div>829            <button class="download-btn" id="downloadBtn" onclick="downloadResult()">830                &#8595; Download Modified Audio831            </button>832            <div class="download-link" id="downloadLink">833                <div class="download-link__label">Direct link (right-click to copy)</div>834                <div class="download-link__url">835                    <a class="download-link__text" id="downloadLinkUrl" href="#" target="_blank"></a>836                    <button class="download-link__copy" id="copyLinkBtn" onclick="copyDownloadLink()">Copy</button>837                </div>838            </div>839        </div>840 841        <!-- Footer -->842        <footer class="footer">843            <div class="footer__text">RESONANCE &mdash; LOCAL AUDIO PROCESSING</div>844            <div class="footer__tech">845                <span>DEMUCS</span>846                <span>LIBROSA</span>847                <span>FASTAPI</span>848            </div>849        </footer>850    </div>851 852    <script>853        // === STATE ===854        let selectedFile = null;855        let outputMode = 'both';856        let pitchSemitones = 0;857        let speedFactor = 1.0;858        let outputFormat = 'mp3';859        let currentJobId = null;860        let pollInterval = null;861 862        // === PITCH GRID ===863        const pitchNames = {864            '-12': '-12', '-11': '-11', '-10': '-10', '-9': '-9', '-8': '-8',865            '-7': '-7', '-6': '-6', '-5': '-5', '-4': '-4', '-3': '-3',866            '-2': '-2', '-1': '-1', '0': '0', '1': '+1', '2': '+2',867            '3': '+3', '4': '+4', '5': '+5', '6': '+6', '7': '+7',868            '8': '+8', '9': '+9', '10': '+10', '11': '+11', '12': '+12'869        };870 871        const noteLabels = {872            '-12': '-8va', '-7': '-P5', '-5': '-P4', '-3': 'm3↓', '-2': 'M2↓',873            '-1': 'm2↓', '0': '—', '1': 'm2↑', '2': 'M2↑', '3': 'm3↑',874            '5': 'P4↑', '7': 'P5↑', '12': '+8va'875        };876 877        function buildPitchGrid() {878            const grid = document.getElementById('pitchGrid');879            for (let i = -12; i <= 12; i++) {880                const btn = document.createElement('button');881                btn.className = 'pitch-btn' + (i === 0 ? ' active' : '');882                btn.dataset.pitch = i;883                btn.innerHTML = `${i > 0 ? '+' : ''}${i}` +884                    (noteLabels[String(i)] ? `<span class="note-name">${noteLabels[String(i)]}</span>` : '');885                btn.addEventListener('click', () => setPitch(i));886                grid.appendChild(btn);887            }888        }889 890        function setPitch(semitones) {891            pitchSemitones = semitones;892            document.querySelectorAll('.pitch-btn').forEach(btn => {893                btn.classList.toggle('active', parseInt(btn.dataset.pitch) === semitones);894            });895        }896 897        // === MODE TOGGLE ===898        function setMode(mode) {899            outputMode = mode;900            document.querySelectorAll('.mode-btn').forEach(btn => {901                btn.classList.toggle('active', btn.dataset.mode === mode);902            });903        }904 905        // === FORMAT TOGGLE ===906        function setFormat(format) {907            outputFormat = format;908            document.querySelectorAll('.format-btn').forEach(btn => {909                btn.classList.toggle('active', btn.dataset.format === format);910            });911        }912 913        // === SPEED SLIDER ===914        const speedSlider = document.getElementById('speedSlider');915        const speedValue = document.getElementById('speedValue');916        speedSlider.addEventListener('input', () => {917            speedFactor = parseFloat(speedSlider.value);918            speedValue.innerHTML = speedFactor.toFixed(2) + '&times;';919        });920 921        // === FILE UPLOAD ===922        const uploadZone = document.getElementById('uploadZone');923        const fileInput = document.getElementById('fileInput');924        const fileNameEl = document.getElementById('fileName');925        const processBtn = document.getElementById('processBtn');926 927        uploadZone.addEventListener('click', () => fileInput.click());928 929        uploadZone.addEventListener('dragover', (e) => {930            e.preventDefault();931            uploadZone.classList.add('drag-over');932        });933 934        uploadZone.addEventListener('dragleave', () => {935            uploadZone.classList.remove('drag-over');936        });937 938        uploadZone.addEventListener('drop', (e) => {939            e.preventDefault();940            uploadZone.classList.remove('drag-over');941            if (e.dataTransfer.files.length > 0) {942                handleFile(e.dataTransfer.files[0]);943            }944        });945 946        fileInput.addEventListener('change', () => {947            if (fileInput.files.length > 0) {948                handleFile(fileInput.files[0]);949            }950        });951 952        function handleFile(file) {953            selectedFile = file;954            uploadZone.classList.add('has-file');955            fileNameEl.textContent = file.name + ' (' + formatSize(file.size) + ')';956            processBtn.disabled = false;957        }958 959        function formatSize(bytes) {960            if (bytes < 1024) return bytes + ' B';961            if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';962            return (bytes / (1024 * 1024)).toFixed(1) + ' MB';963        }964 965        // === PROCESS AUDIO ===966        async function processAudio() {967            if (!selectedFile) return;968 969            const statusBar = document.getElementById('statusBar');970            const statusText = document.getElementById('statusText');971            const progressFill = document.getElementById('progressFill');972            const downloadBtn = document.getElementById('downloadBtn');973            const downloadLink = document.getElementById('downloadLink');974 975            processBtn.disabled = true;976            downloadBtn.classList.remove('visible');977            downloadLink.classList.remove('visible');978            statusBar.className = 'status-bar visible';979            statusText.innerHTML = '<span class="spinner"></span><span>Uploading file...</span>';980            progressFill.style.width = '5%';981 982            try {983                const formData = new FormData();984                formData.append('file', selectedFile);985                formData.append('output_mode', outputMode);986                formData.append('pitch_semitones', pitchSemitones.toString());987                formData.append('speed_factor', speedFactor.toString());988                formData.append('output_format', outputFormat);989 990                const response = await fetch('/api/process', {991                    method: 'POST',992                    body: formData,993                });994 995                if (!response.ok) {996                    const err = await response.json();997                    throw new Error(err.detail || 'Upload failed');998                }999 1000                const data = await response.json();1001                currentJobId = data.job_id;1002                progressFill.style.width = '15%';1003                statusText.innerHTML = '<span class="spinner"></span><span>Processing queued...</span>';1004 1005                // Poll for completion1006                pollForCompletion();1007 1008            } catch (err) {1009                statusBar.className = 'status-bar visible error';1010                statusText.innerHTML = '&#10007; Error: ' + err.message;1011                processBtn.disabled = false;1012            }1013        }1014 1015        function pollForCompletion() {1016            const statusText = document.getElementById('statusText');1017            const progressFill = document.getElementById('progressFill');1018            const statusBar = document.getElementById('statusBar');1019            const downloadBtn = document.getElementById('downloadBtn');1020            const downloadLink = document.getElementById('downloadLink');1021            const downloadLinkUrl = document.getElementById('downloadLinkUrl');1022            let progress = 15;1023 1024            const messages = [1025                'Separating stems with Demucs AI...',1026                'Applying pitch transformation...',1027                'Adjusting tempo...',1028                'Encoding output...',1029            ];1030            let msgIdx = 0;1031 1032            pollInterval = setInterval(async () => {1033                try {1034                    const res = await fetch(`/api/status/${currentJobId}`);1035                    if (!res.ok) {1036                        // Server might be restarting, keep polling1037                        return;1038                    }1039                    const text = await res.text();1040                    let data;1041                    try { data = JSON.parse(text); } catch { return; } // Non-JSON response, retry1042 1043                    if (data.status === 'completed') {1044                        clearInterval(pollInterval);1045                        progressFill.style.width = '100%';1046                        statusBar.className = 'status-bar visible completed';1047                        statusText.innerHTML = '&#10003; Processing complete!';1048                        downloadBtn.classList.add('visible');1049                        const dlUrl = `${window.location.origin}/api/download/${currentJobId}`;1050                        downloadLinkUrl.href = dlUrl;1051                        downloadLinkUrl.textContent = dlUrl;1052                        downloadLink.classList.add('visible');1053                        processBtn.disabled = false;1054                        return;1055                    }1056 1057                    if (data.status === 'error') {1058                        clearInterval(pollInterval);1059                        statusBar.className = 'status-bar visible error';1060                        statusText.innerHTML = '&#10007; Error: ' + (data.error || 'Unknown error');1061                        processBtn.disabled = false;1062                        return;1063                    }1064 1065                    progress = Math.min(progress + 2, 92);1066                    progressFill.style.width = progress + '%';1067                    msgIdx = Math.min(Math.floor((progress - 15) / 20), messages.length - 1);1068                    statusText.innerHTML = '<span class="spinner"></span><span>' + messages[msgIdx] + '</span>';1069 1070                } catch {1071                    // Transient network error — keep polling1072                }1073            }, 2500);1074        }1075 1076        // === DOWNLOAD ===1077        async function downloadResult() {1078            if (!currentJobId) return;1079            const downloadBtn = document.getElementById('downloadBtn');1080            downloadBtn.textContent = '⏳ Preparing download...';1081            downloadBtn.disabled = true;1082            try {1083                const resp = await fetch(`/api/download/${currentJobId}`);1084                if (!resp.ok) throw new Error('Download failed');1085                const blob = await resp.blob();1086                const disposition = resp.headers.get('content-disposition') || '';1087                const match = disposition.match(/filename="?([^"]+)"?/);1088                const filename = match ? match[1] : `modified_audio.${outputFormat}`;1089                const url = URL.createObjectURL(blob);1090                const a = document.createElement('a');1091                a.href = url;1092                a.download = filename;1093                document.body.appendChild(a);1094                a.click();1095                document.body.removeChild(a);1096                URL.revokeObjectURL(url);1097            } catch (err) {1098                alert('Download failed: ' + err.message);1099            } finally {1100                downloadBtn.textContent = '↓ Download Modified Audio';1101                downloadBtn.disabled = false;1102            }1103        }1104 1105        // === COPY LINK ===1106        async function copyDownloadLink() {1107            const url = document.getElementById('downloadLinkUrl').href;1108            const btn = document.getElementById('copyLinkBtn');1109            try {1110                await navigator.clipboard.writeText(url);1111                btn.textContent = 'Copied!';1112                setTimeout(() => { btn.textContent = 'Copy'; }, 2000);1113            } catch {1114                // Fallback for iframe/older browsers1115                const input = document.createElement('input');1116                input.value = url;1117                document.body.appendChild(input);1118                input.select();1119                document.execCommand('copy');1120                document.body.removeChild(input);1121                btn.textContent = 'Copied!';1122                setTimeout(() => { btn.textContent = 'Copy'; }, 2000);1123            }1124        }1125 1126        // === INIT ===1127        buildPitchGrid();1128    </script>1129</body>1130</html>1131