Skip to content

Add Google Ecosystem Sync sample app#2

Open
Azure461003 wants to merge 3 commits intohkust-spark:mainfrom
Azure461003:main
Open

Add Google Ecosystem Sync sample app#2
Azure461003 wants to merge 3 commits intohkust-spark:mainfrom
Azure461003:main

Conversation

@Azure461003
Copy link
Copy Markdown

Summary

  • Add google_sync/ sample: record audio → Whisper transcription → LLM extraction → sync to Google Drive / Calendar / Tasks
  • Includes GoogleSyncEntry.kt (single-file entry), README.md (setup & architecture), and pre-built example.apk
  • Updated root README.md with google_sync section

Test plan

  • Verify xg-glass run GoogleSyncEntry.kt builds and runs successfully
  • Verify Google OAuth token authentication works with Drive, Calendar, Tasks APIs
  • Verify README instructions are accurate

🤖 Generated with Claude Code

- GoogleSyncEntry.kt: record audio → Whisper transcription → LLM extraction → sync to Google Drive/Calendar/Tasks
- README.md with setup instructions and architecture overview
- Pre-built example.apk for quick testing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 31, 2026 12:42
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new google_sync/ sample app demonstrating an end-to-end “conversation → transcription → extraction → Google sync” workflow within the existing single-file-entry sample-app structure of this repository.

Changes:

  • Added a new google_sync sample with a Kotlin entry implementing audio recording, Whisper transcription, LLM JSON extraction, and syncing to Google Drive/Calendar/Tasks.
  • Added google_sync/README.md with setup and architecture notes, and updated the root README.md to link/run the sample.
  • Included a pre-built google_sync/example.apk.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 9 comments.

File Description
README.md Adds a new top-level section describing the google_sync sample and how to run it.
google_sync/README.md Documents setup requirements, settings, and the pipeline architecture.
google_sync/GoogleSyncEntry.kt Implements the sample app’s pipeline and Google API calls.
google_sync/example.apk Bundled pre-built APK for the sample.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +211 to +214
val chunks = withTimeoutOrNull(durationMs) {
session.audio.toList()
} ?: emptyList()
session.stop()
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio recording will almost always produce an empty chunks list here: session.audio.toList() won’t complete until you stop the session, so the withTimeoutOrNull will time out and return null, causing you to drop all partially collected audio. This makes pcmBytes empty and the command will typically report “No speech detected.” Collect into a buffer while the timeout runs (or cancel collection after delay(durationMs)), then stop the session and transcribe the buffered bytes.

Copilot uses AI. Check for mistakes.
Comment on lines +556 to +563
val resp = client.post("https://www.googleapis.com/calendar/v3/calendars/primary/events") {
header("Authorization", "Bearer $token")
contentType(ContentType.Application.Json)
setBody(json)
}
if (resp.status.value in 200..299) created++
}
return created
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-2xx responses from the Calendar API are silently ignored (you only increment created on success). This prevents extractAndSync from surfacing errors and makes failures look like “0 events created.” Check resp.status and throw (or return an error) with a truncated response body similar to the Drive sync path.

Copilot uses AI. Check for mistakes.
contentType(ContentType.Application.Json)
setBody(json)
}
if (resp.status.value in 200..299) created++
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-2xx responses from the Tasks API are silently ignored (you only increment created on success). This prevents extractAndSync from surfacing errors and makes failures look like “0 tasks created.” Check resp.status and throw (or return an error) with a truncated response body similar to the Drive sync path.

Suggested change
if (resp.status.value in 200..299) created++
if (resp.status.value !in 200..299) {
val body = try {
resp.bodyAsText()
} catch (t: Throwable) {
""
}
val truncatedBody =
if (body.length > 1000) body.substring(0, 1000) + "...(truncated)" else body
throw RuntimeException(
"Google Tasks API error ${resp.status.value}: $truncatedBody"
)
}
created++

Copilot uses AI. Check for mistakes.
Comment on lines +590 to +598
private fun withTimezone(dt: String): String {
if (dt.contains('+') || dt.contains('Z') || dt.matches(Regex(".*-\\d{2}:\\d{2}$"))) return dt
val tz = TimeZone.getDefault()
val off = tz.rawOffset
val h = Math.abs(off) / 3600000
val m = (Math.abs(off) % 3600000) / 60000
val sign = if (off >= 0) "+" else "-"
return "$dt$sign${"%02d".format(h)}:${"%02d".format(m)}"
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withTimezone uses TimeZone.rawOffset, which ignores daylight saving time and can shift event times by an hour during DST. Use an offset computed for the relevant instant (e.g., tz.getOffset(...)) or format the datetime with java.time (ZonedDateTime/OffsetDateTime) in the user’s local zone.

Copilot uses AI. Check for mistakes.
ctx.log("No speech detected.")
return ctx.client.display("No speech detected.", DisplayOptions())
}
ctx.log("Transcript: $transcript")
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logs the full transcript to app logs, which may include sensitive/PII content and can be accessible via device logs. Consider removing this log line or logging only a short redacted/truncated preview behind a debug flag.

Suggested change
ctx.log("Transcript: $transcript")
ctx.log("Transcript captured (length=${transcript.length} chars)")

Copilot uses AI. Check for mistakes.
Comment on lines +640 to +645
tasks.add(TaskItem(
title = o.getString("title"),
notes = o.optString("notes", ""),
dueDate = o.optString("due_date", null).takeIf { !it.isNullOrBlank() && it != "null" },
))
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optString("due_date", null) can return null when the key is missing; calling .takeIf { ... } on that result can throw at runtime. Use a non-null default (e.g. empty string) and then map blank/"null" to null, or check o.isNull("due_date") before reading.

Copilot uses AI. Check for mistakes.
description = o.optString("description", ""),
startDateTime = o.getString("start"),
endDateTime = o.getString("end"),
location = o.optString("location", null).takeIf { !it.isNullOrBlank() && it != "null" },
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optString("location", null) can return null when the key is missing; calling .takeIf { ... } on that result can throw at runtime. Use a non-null default (e.g. empty string) and then map blank/"null" to null, or check o.isNull("location") before reading.

Suggested change
location = o.optString("location", null).takeIf { !it.isNullOrBlank() && it != "null" },
location = o.optString("location", "").let { if (it.isBlank() || it == "null") null else it },

Copilot uses AI. Check for mistakes.
Comment thread google_sync/README.md
Comment on lines +37 to +44
| Setting | Description |
|---------|-------------|
| API Base URL | OpenAI-compatible endpoint (default: `https://api.openai.com/v1/`) |
| API Key | Your API key |
| Model | LLM model for extraction (default: `gpt-4o-mini`) |
| Google OAuth2 Access Token | Bearer token with Drive, Calendar, Tasks scopes |
| Listen Duration | How long to record audio in seconds (default: 30) |

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The settings table uses || at the start of each row, which breaks standard Markdown table rendering. Use a single leading | per row (e.g. | Setting | Description |).

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +100
companion object {
private const val TAG = "GoogleSync"
const val KEY_GOOGLE_TOKEN = "google_oauth_token"
const val KEY_LISTEN_DURATION = "listen_duration_secs"
const val KEY_MANUAL_TEXT = "manual_text_input"
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TAG is declared but not used, and the log calls use a hard-coded string instead. Either use TAG consistently (e.g., Log.e(TAG, ...)) or remove the constant to avoid unused code.

Copilot uses AI. Check for mistakes.
Developer and others added 2 commits March 31, 2026 20:57
- IndoorNavEntry.kt: AR glasses indoor navigation for hospitals/malls
  with 4 commands: Where Am I, Navigate To, Find Nearest, Read Signs
- README.md with setup instructions and architecture overview
- Pre-built example.apk for quick testing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each sample has its own README.md inside its folder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants