File size: 2,535 Bytes
96f2542
 
 
 
 
 
 
 
 
 
 
 
 
 
8122b04
 
96f2542
 
 
 
 
 
 
 
 
8122b04
 
 
96f2542
 
 
 
 
 
 
 
 
 
 
 
8122b04
 
 
 
 
 
 
 
 
96f2542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import Dexie, { type Table } from "dexie";

export const HISTORY_CAP = 50;

export type VoiceRecord = {
  id?: number;
  name: string;
  blob: Blob;
  sampleRate: number;
  durationMs: number;
  createdAt: number;
  isFavorite: boolean;
};

export type SpeakerRef = { letter: "A" | "B" | "C" | "D"; voiceId: number };

export type HistoryRecord = {
  id?: number;
  text: string;
  modelId: string;
  voiceId?: number;
  language?: string;
  params: Record<string, unknown>;
  audioBlob: Blob;
  createdAt: number;
  kind?: "single" | "dialog";
  seedUsed?: number;
  speakers?: SpeakerRef[];
};

class DB extends Dexie {
  voices!: Table<VoiceRecord, number>;
  history!: Table<HistoryRecord, number>;

  constructor() {
    super("chatterbox-voice-studio");
    this.version(1).stores({
      voices: "++id, name, createdAt, isFavorite",
      history: "++id, createdAt",
    });
    this.version(2).stores({
      voices: "++id, name, createdAt, isFavorite",
      history: "++id, createdAt",
    }).upgrade(async (tx) => {
      // Backfill new fields on existing rows so listings stay consistent.
      await tx.table("history").toCollection().modify((r: HistoryRecord) => {
        if (!r.kind) r.kind = "single";
      });
    });
  }
}

export const db = new DB();

export async function addVoice(
  v: Omit<VoiceRecord, "id" | "createdAt" | "isFavorite"> & Partial<Pick<VoiceRecord, "isFavorite">>,
): Promise<number> {
  return db.voices.add({
    ...v,
    isFavorite: v.isFavorite ?? false,
    createdAt: Date.now(),
  });
}

export async function listVoices(): Promise<VoiceRecord[]> {
  return db.voices.orderBy("createdAt").reverse().toArray();
}

export async function deleteVoice(id: number): Promise<void> {
  await db.voices.delete(id);
}

export async function setFavorite(id: number, fav: boolean): Promise<void> {
  await db.voices.update(id, { isFavorite: fav });
}

export async function addHistory(
  h: Omit<HistoryRecord, "id" | "createdAt">,
): Promise<number> {
  const id = await db.history.add({ ...h, createdAt: Date.now() });
  const count = await db.history.count();
  if (count > HISTORY_CAP) {
    const overflow = count - HISTORY_CAP;
    const oldest = await db.history.orderBy("createdAt").limit(overflow).primaryKeys();
    await db.history.bulkDelete(oldest);
  }
  return id;
}

export async function listHistory(): Promise<HistoryRecord[]> {
  return db.history.orderBy("createdAt").reverse().toArray();
}

export async function clearHistory(): Promise<void> {
  await db.history.clear();
}