File size: 10,517 Bytes
1295969
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
//! Music shard module β€” CFT decomposition of audio metadata into DA51 CBOR shards.
//!
//! Scale tower for music (mirrors erdfa-publish text CFT):
//!   Track β†’ Stem β†’ Segment β†’ Frame β†’ Sample β†’ Byte
//!
//! Shards are semantic representations of track structure encoded as DA51-tagged
//! CBOR bytes using the erdfa-publish library.
//!
//! ## NFT gating model (updated)
//!
//! Shard DATA is **fully public** β€” `GET /api/shard/:cid` returns the complete shard
//! to any caller without authentication.  This follows the "public DA" (decentralised
//! availability) model: the bits are always accessible on BTFS.
//!
//! NFT ownership gates only the **ShardManifest** (assembly instructions) served via
//! `GET /api/manifest/:token_id`.  A wallet that holds the NFT can request the
//! ordered CID list + optional AES-256-GCM decryption key for the assembled track.
//!
//! Pre-generated source shards (Emacs Lisp / Fractran VM reflections of each
//! Rust module) live in `shards/` at the repo root and can be served directly
//! via GET /api/shard/:cid once indexed at startup or via POST /api/shard/index.

use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use erdfa_publish::{cft::Scale as TextScale, Component, Shard, ShardSet};
use shared::types::Isrc;
use std::{collections::HashMap, sync::RwLock};
use tracing::info;

// ── Audio CFT scale tower ──────────────────────────────────────────────────

/// Audio-native CFT scales β€” mirrors the text CFT in erdfa-publish.
/// All six variants are part of the public tower API even if only Track/Stem/Segment
/// are emitted by the current decompose_track() implementation.
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
pub enum AudioScale {
    Track,   // whole release
    Stem,    // vocal / drums / bass / keys
    Segment, // verse / chorus / bridge
    Frame,   // ~23 ms audio frame
    Sample,  // individual PCM sample
    Byte,    // raw bytes
}

#[allow(dead_code)]
impl AudioScale {
    pub fn tag(&self) -> &'static str {
        match self {
            Self::Track => "cft.track",
            Self::Stem => "cft.stem",
            Self::Segment => "cft.segment",
            Self::Frame => "cft.frame",
            Self::Sample => "cft.sample",
            Self::Byte => "cft.byte",
        }
    }
    pub fn depth(&self) -> u8 {
        match self {
            Self::Track => 0,
            Self::Stem => 1,
            Self::Segment => 2,
            Self::Frame => 3,
            Self::Sample => 4,
            Self::Byte => 5,
        }
    }
    /// Corresponding text-domain scale for cross-tower morphisms.
    pub fn text_analogue(&self) -> TextScale {
        match self {
            Self::Track => TextScale::Post,
            Self::Stem => TextScale::Paragraph,
            Self::Segment => TextScale::Line,
            Self::Frame => TextScale::Token,
            Self::Sample => TextScale::Emoji,
            Self::Byte => TextScale::Byte,
        }
    }
}

// ── Shard quality tiers ────────────────────────────────────────────────────

/// Quality is now informational only.  All shard data served via the API is
/// `Full` β€” the old `Preview` tier is removed.  NFT gates the *manifest*, not
/// the data.  `Degraded` and `Steganographic` tiers are retained for future
/// p2p stream quality signalling.
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub enum ShardQuality {
    Full,                   // full public shard β€” always returned
    Degraded { kbps: u16 }, // low-bitrate p2p stream (reserved)
    Steganographic,         // hidden in cover content (reserved)
}

// ── In-memory shard store ──────────────────────────────────────────────────

/// Lightweight in-process shard index (cid β†’ JSON metadata).
/// Populated at startup by indexing pre-built shards from disk or via upload.
pub struct ShardStore(pub RwLock<HashMap<String, serde_json::Value>>);

impl ShardStore {
    pub fn new() -> Self {
        Self(RwLock::new(HashMap::new()))
    }

    pub fn insert(&self, cid: &str, data: serde_json::Value) {
        self.0.write().unwrap().insert(cid.to_string(), data);
    }

    pub fn get(&self, cid: &str) -> Option<serde_json::Value> {
        self.0.read().unwrap().get(cid).cloned()
    }
}

impl Default for ShardStore {
    fn default() -> Self {
        Self::new()
    }
}

// ── CFT decomposition ──────────────────────────────────────────────────────

/// Decompose track metadata into erdfa-publish `Shard`s at each audio scale.
///
/// Returns shards for:
///   - one Track-level shard
///   - one Stem shard per stem label
///   - one Segment shard per segment label
pub fn decompose_track(isrc: &Isrc, stems: &[&str], segments: &[&str]) -> Vec<Shard> {
    let prefix = &isrc.0;
    let mut shards = Vec::new();

    // Track level
    shards.push(Shard::new(
        format!("{prefix}_track"),
        Component::KeyValue {
            pairs: vec![
                ("isrc".into(), isrc.0.clone()),
                ("scale".into(), AudioScale::Track.tag().into()),
                ("stems".into(), stems.len().to_string()),
                ("segments".into(), segments.len().to_string()),
            ],
        },
    ));

    // Stem level
    for (i, stem) in stems.iter().enumerate() {
        shards.push(Shard::new(
            format!("{prefix}_{stem}"),
            Component::KeyValue {
                pairs: vec![
                    ("isrc".into(), isrc.0.clone()),
                    ("scale".into(), AudioScale::Stem.tag().into()),
                    ("stem".into(), stem.to_string()),
                    ("index".into(), i.to_string()),
                    ("parent".into(), format!("{prefix}_track")),
                ],
            },
        ));
    }

    // Segment level
    for (i, seg) in segments.iter().enumerate() {
        shards.push(Shard::new(
            format!("{prefix}_seg{i}"),
            Component::KeyValue {
                pairs: vec![
                    ("isrc".into(), isrc.0.clone()),
                    ("scale".into(), AudioScale::Segment.tag().into()),
                    ("label".into(), seg.to_string()),
                    ("index".into(), i.to_string()),
                    ("parent".into(), format!("{prefix}_track")),
                ],
            },
        ));
    }

    shards
}

/// Build a `ShardSet` manifest and serialise all shards to a DA51-tagged CBOR tar archive.
///
/// Each shard is encoded individually with `Shard::to_cbor()` (DA51 tag) and
/// collected using `ShardSet::to_tar()`.  Intended for batch export of track shards.
#[allow(dead_code)]
pub fn shards_to_tar(name: &str, shards: &[Shard]) -> anyhow::Result<Vec<u8>> {
    let mut set = ShardSet::new(name);
    for s in shards {
        set.add(s);
    }
    let mut buf = Vec::new();
    set.to_tar(shards, &mut buf)?;
    Ok(buf)
}

// ── HTTP handlers ──────────────────────────────────────────────────────────

use crate::AppState;

/// `GET /api/shard/:cid`
///
/// Returns the full shard JSON to any caller β€” shards are public DA on BTFS.
/// NFT ownership is NOT checked here; it gates only `/api/manifest/:token_id`
/// (the assembly instructions + optional decryption key).
///
/// The optional `x-wallet-address` header is accepted but only logged for
/// analytics; it does not alter the response.
pub async fn get_shard(
    State(state): State<AppState>,
    Path(cid): Path<String>,
    headers: axum::http::HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
    // LangSec: CID must be non-empty and ≀ 128 chars
    if cid.is_empty() || cid.len() > 128 {
        return Err(StatusCode::UNPROCESSABLE_ENTITY);
    }

    let shard_data = state.shard_store.get(&cid).ok_or(StatusCode::NOT_FOUND)?;

    let wallet = headers
        .get("x-wallet-address")
        .and_then(|v| v.to_str().ok())
        .map(String::from);

    info!(cid = %cid, wallet = ?wallet, "Public shard served");

    Ok(Json(serde_json::json!({
        "cid":     cid,
        "quality": "full",
        "data":    shard_data,
        // Hint: for assembly instructions use GET /api/manifest/<token_id> (NFT-gated)
        "manifest_hint": "/api/manifest/{token_id}",
    })))
}

/// `POST /api/shard/decompose`
///
/// Accepts `{ "isrc": "...", "stems": [...], "segments": [...] }`, runs
/// `decompose_track`, stores shards in the in-process index, and returns
/// the shard CID list.
pub async fn decompose_and_index(
    State(state): State<AppState>,
    Json(body): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, StatusCode> {
    let isrc_str = body
        .get("isrc")
        .and_then(|v| v.as_str())
        .ok_or(StatusCode::BAD_REQUEST)?;

    let isrc =
        shared::parsers::recognize_isrc(isrc_str).map_err(|_| StatusCode::UNPROCESSABLE_ENTITY)?;

    let stems: Vec<&str> = body
        .get("stems")
        .and_then(|v| v.as_array())
        .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
        .unwrap_or_default();

    let segments: Vec<&str> = body
        .get("segments")
        .and_then(|v| v.as_array())
        .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
        .unwrap_or_default();

    let shards = decompose_track(&isrc, &stems, &segments);

    for shard in &shards {
        let json = serde_json::to_value(shard).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
        state.shard_store.insert(&shard.cid, json);
        info!(id = %shard.id, cid = %shard.cid, "Shard indexed");
    }

    Ok(Json(serde_json::json!({
        "isrc":   isrc_str,
        "shards": shards.len(),
        "cids":   shard_cid_list(&shards),
    })))
}

// ── Helpers ────────────────────────────────────────────────────────────────

fn shard_cid_list(shards: &[Shard]) -> Vec<String> {
    shards.iter().map(|s| s.cid.clone()).collect()
}