Detail: personality picker beside Re-summarize
Chips: Neutral / Sarcastic / Funny / Dry wit. The chosen voice rides
the reprocess call and the button echoes it ('Re-summarize · Sarcastic');
an existing summary shows '· in a sarcastic voice' next to its heading.
compileKotlin + 39 unit tests green.
This commit is contained in:
parent
b63f49241d
commit
994dbc3690
4 changed files with 61 additions and 10 deletions
|
|
@ -1154,7 +1154,8 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
|
||||||
* endpoint as the upload path. [model] is sent for "transcribe" so a
|
* endpoint as the upload path. [model] is sent for "transcribe" so a
|
||||||
* model override switch persists on the server row.
|
* model override switch persists on the server row.
|
||||||
*/
|
*/
|
||||||
fun reprocessInPlace(d0: DetailUi, job: String, model: String? = null) {
|
fun reprocessInPlace(d0: DetailUi, job: String, model: String? = null,
|
||||||
|
tone: String? = null) {
|
||||||
val remoteId = d0.remoteId ?: return
|
val remoteId = d0.remoteId ?: return
|
||||||
pollJob?.cancel()
|
pollJob?.cancel()
|
||||||
pollJob = scope.launch {
|
pollJob = scope.launch {
|
||||||
|
|
@ -1164,7 +1165,7 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
|
||||||
d0.uploadProgress = null
|
d0.uploadProgress = null
|
||||||
d0.error = null
|
d0.error = null
|
||||||
_detail.value = d0.copy()
|
_detail.value = d0.copy()
|
||||||
runCatching { provider.reprocess(remoteId, job, model) }.onFailure {
|
runCatching { provider.reprocess(remoteId, job, model, tone) }.onFailure {
|
||||||
if (job == "transcribe" && it is ProviderError.NotFound) {
|
if (job == "transcribe" && it is ProviderError.NotFound) {
|
||||||
// Mapping points at a deleted recording: drop it so the
|
// Mapping points at a deleted recording: drop it so the
|
||||||
// next attempt re-uploads instead of failing forever.
|
// next attempt re-uploads instead of failing forever.
|
||||||
|
|
@ -1223,12 +1224,13 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** "Summarize" button: regenerate the summary in place from the
|
/** "Summarize" button: regenerate the summary in place from the
|
||||||
* current transcript. Requires a known server recording. */
|
* current transcript. Requires a known server recording.
|
||||||
fun summarize() {
|
* [tone] (e.g. "sarcastic") restyles the voice; null = neutral. */
|
||||||
|
fun summarize(tone: String? = null) {
|
||||||
val d = _detail.value ?: return
|
val d = _detail.value ?: return
|
||||||
val remoteId = d.remoteId ?: loadMapping(d.file)?.recordingId ?: return
|
val remoteId = d.remoteId ?: loadMapping(d.file)?.recordingId ?: return
|
||||||
d.remoteId = remoteId
|
d.remoteId = remoteId
|
||||||
reprocessInPlace(d, job = "summarize")
|
reprocessInPlace(d, job = "summarize", tone = tone)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Server recording id for a library file, if it was ever uploaded. */
|
/** Server recording id for a library file, if it was ever uploaded. */
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
|
@ -514,6 +515,16 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
|
|
||||||
// ---- detail -----------------------------------------------------------------
|
// ---- detail -----------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Personalities offered beside Re-summarize: (label, tone sent to the
|
||||||
|
* engine). The tone string is interpolated into "in a <tone> tone of
|
||||||
|
* voice", so it must read naturally there. null = neutral house voice. */
|
||||||
|
private val SUMMARIZE_PERSONAS = listOf(
|
||||||
|
"Neutral" to null,
|
||||||
|
"Sarcastic" to "sarcastic",
|
||||||
|
"Funny" to "funny",
|
||||||
|
"Dry wit" to "dry, witty",
|
||||||
|
)
|
||||||
|
|
||||||
@androidx.compose.runtime.Composable
|
@androidx.compose.runtime.Composable
|
||||||
fun DetailScreen(state: DesktopState) {
|
fun DetailScreen(state: DesktopState) {
|
||||||
val detail = state.detail.collectAsState().value ?: run {
|
val detail = state.detail.collectAsState().value ?: run {
|
||||||
|
|
@ -525,6 +536,8 @@ fun DetailScreen(state: DesktopState) {
|
||||||
var confirmDelete by remember { mutableStateOf(false) }
|
var confirmDelete by remember { mutableStateOf(false) }
|
||||||
var deleteError by remember { mutableStateOf<String?>(null) }
|
var deleteError by remember { mutableStateOf<String?>(null) }
|
||||||
var recQuery by remember { mutableStateOf("") }
|
var recQuery by remember { mutableStateOf("") }
|
||||||
|
// Voice for the next summarize: null = Neutral (server default).
|
||||||
|
var sumTone by remember(detail.file.name) { mutableStateOf<String?>(null) }
|
||||||
val playingFile by state.playingFile.collectAsState()
|
val playingFile by state.playingFile.collectAsState()
|
||||||
val playError by state.playError.collectAsState()
|
val playError by state.playError.collectAsState()
|
||||||
val isPlaying = playingFile?.absolutePath == detail.file.absolutePath
|
val isPlaying = playingFile?.absolutePath == detail.file.absolutePath
|
||||||
|
|
@ -718,7 +731,16 @@ fun DetailScreen(state: DesktopState) {
|
||||||
}
|
}
|
||||||
detail.summary?.let { s ->
|
detail.summary?.let { s ->
|
||||||
if (s.short.isNotBlank()) {
|
if (s.short.isNotBlank()) {
|
||||||
Text("Summary", style = MaterialTheme.typography.titleMedium)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text("Summary", style = MaterialTheme.typography.titleMedium)
|
||||||
|
// Honest tag: which voice this summary was written in.
|
||||||
|
s.tone?.let {
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("· in a $it voice",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
Text(s.short)
|
Text(s.short)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -748,13 +770,32 @@ fun DetailScreen(state: DesktopState) {
|
||||||
val hasSummary = detail.summary != null ||
|
val hasSummary = detail.summary != null ||
|
||||||
(report?.contains("## Summary") == true)
|
(report?.contains("## Summary") == true)
|
||||||
val remoteKnown = state.remoteIdFor(detail.file) != null
|
val remoteKnown = state.remoteIdFor(detail.file) != null
|
||||||
|
// Personality for the next summary: pick a voice, then hit
|
||||||
|
// Re-summarize. Neutral = the plain house voice.
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Text("Summarize in the voice of",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
SUMMARIZE_PERSONAS.forEach { (label, tone) ->
|
||||||
|
FilterChip(
|
||||||
|
selected = sumTone == tone,
|
||||||
|
onClick = { sumTone = tone },
|
||||||
|
label = { Text(label) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
TextButton({ state.transcribe() }) {
|
TextButton({ state.transcribe() }) {
|
||||||
Text(if (hasTranscript) "Re-transcribe" else "Transcribe")
|
Text(if (hasTranscript) "Re-transcribe" else "Transcribe")
|
||||||
}
|
}
|
||||||
TextButton({ state.summarize() },
|
TextButton({ state.summarize(sumTone) },
|
||||||
enabled = remoteKnown && hasTranscript) {
|
enabled = remoteKnown && hasTranscript) {
|
||||||
Text(if (hasSummary) "Re-summarize" else "Summarize")
|
Text(buildString {
|
||||||
|
append(if (hasSummary) "Re-summarize" else "Summarize")
|
||||||
|
sumTone?.let { append(" · $it") }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!remoteKnown || !hasTranscript) {
|
if (!remoteKnown || !hasTranscript) {
|
||||||
|
|
|
||||||
|
|
@ -311,11 +311,16 @@ class CustomShonarProvider(
|
||||||
* per-recording override). 409 = stage already running or nothing to
|
* per-recording override). 409 = stage already running or nothing to
|
||||||
* summarize; surfaced as ProviderError.Transient with the server's
|
* summarize; surfaced as ProviderError.Transient with the server's
|
||||||
* detail message. */
|
* detail message. */
|
||||||
suspend fun reprocess(recordingId: String, job: String, model: String? = null): String =
|
suspend fun reprocess(recordingId: String, job: String, model: String? = null,
|
||||||
|
tone: String? = null): String =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ensureConnected()
|
ensureConnected()
|
||||||
val base = currentOrigin()
|
val base = currentOrigin()
|
||||||
val q = if (model != null) "&model=$model" else ""
|
val q = buildString {
|
||||||
|
if (model != null) append("&model=$model")
|
||||||
|
if (tone != null)
|
||||||
|
append("&tone=").append(java.net.URLEncoder.encode(tone, "UTF-8"))
|
||||||
|
}
|
||||||
executeAuthed(base) { token ->
|
executeAuthed(base) { token ->
|
||||||
post(base, "/api/v1/recordings/$recordingId/reprocess?job=$job$q", token, "{}")
|
post(base, "/api/v1/recordings/$recordingId/reprocess?job=$job$q", token, "{}")
|
||||||
}.use { resp ->
|
}.use { resp ->
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ data class SummaryData(
|
||||||
val version: Int,
|
val version: Int,
|
||||||
val provider: String = "",
|
val provider: String = "",
|
||||||
val model: String? = null,
|
val model: String? = null,
|
||||||
|
/** Voice the summary was written in ("sarcastic", …); null = neutral. */
|
||||||
|
val tone: String? = null,
|
||||||
/** Raw content dict; known keys read via [summaryString]/[summaryList]. */
|
/** Raw content dict; known keys read via [summaryString]/[summaryList]. */
|
||||||
val content: Map<String, Any?> = emptyMap(),
|
val content: Map<String, Any?> = emptyMap(),
|
||||||
val editedByUser: Boolean = false,
|
val editedByUser: Boolean = false,
|
||||||
|
|
@ -112,6 +114,7 @@ fun parseSummary(raw: String): SummaryData? = runCatching {
|
||||||
version = json.optInt("version", 0),
|
version = json.optInt("version", 0),
|
||||||
provider = json.optString("provider", ""),
|
provider = json.optString("provider", ""),
|
||||||
model = json.optString("model", null).takeUnless { it.isNullOrBlank() },
|
model = json.optString("model", null).takeUnless { it.isNullOrBlank() },
|
||||||
|
tone = json.optString("tone", null).takeUnless { it.isNullOrBlank() },
|
||||||
content = content,
|
content = content,
|
||||||
editedByUser = json.optBoolean("edited_by_user", false),
|
editedByUser = json.optBoolean("edited_by_user", false),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue